108 lines
3 KiB
JavaScript
108 lines
3 KiB
JavaScript
import { spawn } from 'node:child_process';
|
|
import path from 'node:path';
|
|
|
|
const CLI_REL_PATH = 'bin/byan-v2-cli.js';
|
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
|
|
export function parseCliOutput(stdout) {
|
|
const trimmed = stdout.trim();
|
|
if (!trimmed) return { raw: '' };
|
|
|
|
try {
|
|
return JSON.parse(trimmed);
|
|
} catch {
|
|
// fall through
|
|
}
|
|
|
|
const firstBracket = trimmed.search(/[\[{]/);
|
|
if (firstBracket >= 0) {
|
|
const candidate = trimmed.slice(firstBracket);
|
|
try {
|
|
return JSON.parse(candidate);
|
|
} catch {
|
|
// still not valid JSON
|
|
}
|
|
}
|
|
|
|
return { raw: trimmed };
|
|
}
|
|
|
|
function resolveProjectRoot(envRoot) {
|
|
return envRoot || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
}
|
|
|
|
export function runByanCli(args, options = {}) {
|
|
const root = resolveProjectRoot(options.projectRoot);
|
|
const script = path.join(root, CLI_REL_PATH);
|
|
const timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn('node', [script, ...args], {
|
|
cwd: root,
|
|
env: { ...process.env },
|
|
});
|
|
|
|
let stdout = '';
|
|
let stderr = '';
|
|
const timer = setTimeout(() => {
|
|
child.kill('SIGKILL');
|
|
reject(new Error(`byan-v2-cli timed out after ${timeoutMs}ms: ${args.join(' ')}`));
|
|
}, timeoutMs);
|
|
|
|
child.stdout.on('data', (d) => (stdout += d.toString()));
|
|
child.stderr.on('data', (d) => (stderr += d.toString()));
|
|
|
|
child.on('error', (err) => {
|
|
clearTimeout(timer);
|
|
reject(err);
|
|
});
|
|
|
|
child.on('close', (code) => {
|
|
clearTimeout(timer);
|
|
if (code !== 0) {
|
|
const err = new Error(
|
|
`byan-v2-cli exited ${code}: ${stderr.trim() || stdout.trim()}`
|
|
);
|
|
err.code = code;
|
|
err.stderr = stderr;
|
|
err.stdout = stdout;
|
|
reject(err);
|
|
return;
|
|
}
|
|
resolve(parseCliOutput(stdout));
|
|
});
|
|
});
|
|
}
|
|
|
|
export async function eloSummary(opts) {
|
|
return runByanCli(['elo', 'summary'], opts);
|
|
}
|
|
|
|
export async function eloContext({ domain, ...opts }) {
|
|
if (!domain) throw new Error('domain is required');
|
|
return runByanCli(['elo', 'context', domain], opts);
|
|
}
|
|
|
|
export async function eloDashboard({ domain, ...opts }) {
|
|
return runByanCli(domain ? ['elo', 'dashboard', domain] : ['elo', 'dashboard'], opts);
|
|
}
|
|
|
|
export async function eloRecord({ domain, result, reason, ...opts }) {
|
|
if (!domain) throw new Error('domain is required');
|
|
if (!['VALIDATED', 'BLOCKED', 'PARTIAL'].includes(result)) {
|
|
throw new Error(`result must be VALIDATED|BLOCKED|PARTIAL, got: ${result}`);
|
|
}
|
|
const args = ['elo', 'record', domain, result];
|
|
if (reason) args.push(reason);
|
|
return runByanCli(args, opts);
|
|
}
|
|
|
|
export async function fcCheck({ text, ...opts }) {
|
|
if (!text || typeof text !== 'string') throw new Error('text is required');
|
|
return runByanCli(['fc', 'check', text], opts);
|
|
}
|
|
|
|
export async function fcParse({ text, ...opts }) {
|
|
if (!text || typeof text !== 'string') throw new Error('text is required');
|
|
return runByanCli(['fc', 'parse', text], opts);
|
|
}
|