Files
TORCH/src/cli.mjs
T

655 lines
30 KiB
JavaScript

import { analyzeRepository } from './kernel/analyze.mjs';
import { BacklogService } from './backlog/service.mjs';
import { createClaudeAdapter } from './adapters/claude.mjs';
import { createCodexAdapter } from './adapters/codex.mjs';
import { CheckService } from './checks/service.mjs';
import { classifyRecoverability, createLocalCanonical, planLocalCanonical } from './canonical/local.mjs';
import { openControlPlane } from './control-plane/service.mjs';
import { spawnSync } from 'node:child_process';
import { diagnoseProject } from './kernel/doctor.mjs';
import { asErrorRecord, TorchError } from './kernel/errors.mjs';
import { inspectRepository } from './kernel/git.mjs';
import { inspectSpecifications } from './kernel/specifications.mjs';
import { installProject, planInstall, uninstallProject } from './kernel/install.mjs';
import { IntegrationService } from './integration/service.mjs';
import { analyzeCombatrigFleet } from './importers/combatrig.mjs';
import { proposeDomains } from './kernel/domains.mjs';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { writeNewFile } from './kernel/files.mjs';
import { createWorktrees, planWorktrees } from './kernel/worktrees.mjs';
import { planFleetDown, planFleetUp, startFleet, stopFleet } from './runtime/lifecycle.mjs';
import { ResourceService } from './resources/service.mjs';
import { CANDIDATE_ACCEPTANCE_SCENARIOS, createVersionService } from './self-host/service.mjs';
const HELP = `TORCH — portable agent fleet
Usage:
torch init [--repo <path>] [--spec <path>] [--json]
torch analyze [--repo <path>] [--spec <path>] [--json]
torch design [--repo <path>] [--spec <path>] [--output <path>] [--json]
torch domains [--repo <path>] [--spec <path>] [--output <path>] [--json]
torch install --proposal <path> [--dry-run] [--yes] [--json]
torch worktrees [--parent <path>] [--dry-run] [--yes] [--json]
torch up [--fresh] [--dry-run] [--yes] [--json]
torch down [--dry-run] [--yes] [--json]
torch identity --area <id> [--json]
torch agents [--json]
torch agent --area <id> [--json]
torch roster [--json]
torch who-owns (--path <path> | --capability <text>) [--json]
torch neighbours --area <id> [--json]
torch message --from <id> --to <id|all> --body <text> [--task <id>] [--path <path>] [--commit <sha>] [--json]
torch inbox --area <id> [--unacknowledged] [--limit <n>] [--json]
torch ack --area <id> --message <id> [--json]
torch status --area <id> --state <state> [--summary <text>] [--runtime <name>] [--session <id>] [--task <id>] [--json]
torch complete --area <id> --summary <text> [--task <id>] [--evidence <text>] [--commit <sha>] [--json]
torch blocked --area <id> --summary <text> [--task <id>] [--evidence <text>] [--path <path>] [--commit <sha>] [--json]
torch coordinate --from <id> --body <text> [--with <id,id>] [--task <id>] [--path <path>] [--json]
torch handoff --from <id> [--to <id>] (--path <path> | --task <id>) --reason <text> [--json]
torch backlog list [--state <state>] [--owner <id>] [--json]
torch backlog get --task <id> [--json]
torch backlog create --area <id> --title <text> --description <text> --accept <text,...> [--priority <priority>] [--domains <id,...>] [--depends <task,...>] [--json]
torch backlog transition --task <id> --area <id> --state <state> --revision <n> [--owner <id>] [--evidence <text,...>] [--commit <sha>] [--integration <id>] [--reason <text>] [--note <text>] [--json]
torch checks list [--json]
torch checks receipts [--commit <sha>] [--id <check>] [--area <id>] [--json]
torch checks plan --id <check> --area <id> [--json]
torch checks run --id <check> --area <id> --yes [--json]
torch resources list [--json]
torch resources status --id <resource> [--json]
torch resources acquire --id <resource> --area <id> [--json]
torch resources release --id <resource> --area <id> [--json]
torch resources cancel --id <resource> --area <id> [--json]
torch integrate list [--state <state>] [--json]
torch integrate request --area <id> [--commit <sha>] [--json]
torch integrate evaluate --request <id> [--json]
torch integrate authorize --request <id> --area <id> [--json]
torch integrate plan --request <id> --area <id> [--json]
torch integrate land --request <id> --area <id> --yes [--json]
torch canonical plan [--json]
torch canonical create --yes [--json]
torch recoverability [--commit <sha>] [--json]
torch candidate plan --source <path> [--version <version>] [--json]
torch candidate build --source <path> [--version <version>] --yes [--json]
torch candidate status [--json]
torch candidate test --version <version> [--json]
torch upgrade --version <version> [--dry-run] --yes [--json]
torch rollback [--dry-run] --yes [--json]
torch import combatrig --source <path> [--output <path>] [--json]
torch doctor [--json]
torch uninstall [--dry-run] [--purge] [--json]
`;
function print(value, { json = false } = {}) {
if (json || typeof value !== 'string') process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
else process.stdout.write(`${value}\n`);
}
function optionValue(argv, name) {
const direct = argv.find((arg) => arg.startsWith(`${name}=`));
if (direct) return direct.slice(name.length + 1);
const index = argv.indexOf(name);
return index >= 0 ? argv[index + 1] : undefined;
}
function optionValues(argv, name) {
const values = [];
for (let index = 0; index < argv.length; index += 1) {
if (argv[index].startsWith(`${name}=`)) values.push(argv[index].slice(name.length + 1));
else if (argv[index] === name && argv[index + 1] !== undefined) values.push(argv[index + 1]);
}
return values;
}
function commaList(value) {
return (value ?? '').split(',').map((item) => item.trim()).filter(Boolean);
}
function loadProposal(cwd, proposalPath) {
if (!proposalPath) {
throw new TorchError('Installation requires an approved domain proposal from torch domains --output.', {
code: 'PROPOSAL_REQUIRED',
});
}
try { return JSON.parse(readFileSync(resolve(cwd, proposalPath), 'utf8')); } catch (error) {
throw new TorchError(`Cannot read domain proposal: ${proposalPath}`, {
code: 'PROPOSAL_INVALID', details: error.message,
});
}
}
function withControlPlane(repository, env, callback) {
const controlPlane = openControlPlane({ repositoryRoot: repository.root, env });
try {
return callback(controlPlane);
} finally {
controlPlane.close();
}
}
function runtimeAdapters() {
return new Map([
['claude', createClaudeAdapter()],
['codex', createCodexAdapter()],
]);
}
function runCandidateAcceptance(candidateRoot, spawn) {
const checks = [
{ id: 'test', args: ['test'] },
{ id: 'lint', args: ['run', 'lint'] },
{ id: 'syntax', args: ['run', 'check'] },
].map((check) => {
const result = spawn('npm', check.args, {
cwd: candidateRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 300_000,
});
return {
id: check.id, status: result.status ?? (result.error ? 1 : 0),
signal: result.signal ?? null, error: result.error?.message ?? null,
stdout: result.stdout ?? '',
};
});
const testOutput = checks.find((check) => check.id === 'test')?.stdout ?? '';
const evidenceByScenario = new Map([
['init-analyze', ['SCN-init-read-only']],
['spec-aware-design', ['SCN-spec-fleet-design', 'SCN-cli-spec-design']],
['review-install-roster', ['SCN-cli-domain-review']],
['branches-worktrees', ['SCN-worktree-bootstrap']],
['runtime-identities', ['SCN-mixed-runtime']],
['durable-messaging', ['SCN-durable-message']],
['ownership-query', ['SCN-ownership-handoff']],
['unsafe-worktree-detection', ['SCN-worktree-purge-safety']],
['backlog-integration', ['SCN-backlog-lifecycle', 'SCN-native-integration']],
['resource-lifecycle', ['SCN-resource-fifo']],
['capture-stop-resume', ['SCN-fleet-fresh-resume']],
['detach-uninstall', ['SCN-install-doctor-purge']],
['combatrig-compatibility', ['SCN-combatrig-import', 'SCN-cli-combatrig-import']],
]);
const scenarios = CANDIDATE_ACCEPTANCE_SCENARIOS.filter((scenario) =>
(evidenceByScenario.get(scenario) ?? []).every((marker) => testOutput.includes(marker)));
const passed = checks.every((check) => check.status === 0 && !check.signal && !check.error)
&& scenarios.length === CANDIDATE_ACCEPTANCE_SCENARIOS.length;
return {
passed,
scenarios,
evidence: checks.map(({ stdout, ...check }) => ({
...check, outputBytes: Buffer.byteLength(stdout),
})),
};
}
export async function runCli(argv = process.argv.slice(2), {
cwd = process.cwd(), env = process.env, spawn = spawnSync,
} = {}) {
const command = argv.find((arg) => !arg.startsWith('-')) ?? 'help';
const json = argv.includes('--json');
try {
if (command === 'help' || argv.includes('--help') || argv.includes('-h')) {
process.stdout.write(HELP);
return 0;
}
if (command === 'candidate') {
const operation = argv[1] ?? 'status';
const versions = createVersionService({ env });
if (operation === 'status') {
print(versions.status(), { json });
return 0;
}
if (operation === 'test') {
const version = optionValue(argv, '--version');
const status = versions.status();
const validationPath = resolve(status.root, 'versions', version ?? '', '.torch-validation.json');
if (!version || !status.installed.includes(version)) {
throw new TorchError('Candidate test receipt requires an installed version', {
code: 'VERSION_NOT_INSTALLED', details: { version: version ?? null },
});
}
print(JSON.parse(readFileSync(validationPath, 'utf8')), { json });
return 0;
}
const source = optionValue(argv, '--source');
const version = optionValue(argv, '--version');
if (operation === 'plan') {
print(versions.planCandidate({ source: resolve(cwd, source ?? ''), version }), { json });
return 0;
}
if (operation === 'build') {
if (!argv.includes('--yes')) {
throw new TorchError('Candidate build copies code and runs its acceptance suite. Review candidate plan, then re-run with --yes.', {
code: 'APPROVAL_REQUIRED',
});
}
print(versions.installCandidate({
source: resolve(cwd, source ?? ''), version,
validate: ({ candidateRoot }) => runCandidateAcceptance(candidateRoot, spawn),
}), { json });
return 0;
}
throw new TorchError(`Unknown candidate operation: ${operation}`, { code: 'UNKNOWN_COMMAND' });
}
if (command === 'upgrade') {
const versions = createVersionService({ env });
const version = optionValue(argv, '--version');
const status = versions.status();
const plan = {
action: 'upgrade', from: status.activeVersion, to: version ?? null,
canProceed: Boolean(version && status.installed.includes(version)), mutationPerformed: false,
};
if (argv.includes('--dry-run')) {
print(plan, { json });
return plan.canProceed ? 0 : 1;
}
if (!argv.includes('--yes')) {
throw new TorchError('Upgrade atomically changes the active TORCH version. Review --dry-run, then re-run with --yes.', {
code: 'APPROVAL_REQUIRED',
});
}
print(versions.activate(version), { json });
return 0;
}
if (command === 'rollback') {
const versions = createVersionService({ env });
const status = versions.status();
const plan = {
action: 'rollback', from: status.activeVersion, to: status.previousVersion,
canProceed: Boolean(status.previousVersion), mutationPerformed: false,
};
if (argv.includes('--dry-run')) {
print(plan, { json });
return plan.canProceed ? 0 : 1;
}
if (!argv.includes('--yes')) {
throw new TorchError('Rollback atomically restores the previous TORCH version. Review --dry-run, then re-run with --yes.', {
code: 'APPROVAL_REQUIRED',
});
}
print(versions.rollback(), { json });
return 0;
}
if (command === 'import') {
const format = argv[1];
if (format !== 'combatrig') {
throw new TorchError(`Unsupported fleet import format: ${format ?? '<missing>'}`, {
code: 'IMPORT_FORMAT_UNSUPPORTED',
});
}
const source = optionValue(argv, '--source');
if (!source) throw new TorchError('COMBATRIG import requires --source', { code: 'IMPORT_SOURCE_REQUIRED' });
const sourceRepository = inspectRepository(resolve(cwd, source));
const proposal = analyzeCombatrigFleet({ repository: sourceRepository });
const output = optionValue(argv, '--output');
if (output) {
const path = resolve(cwd, output);
writeNewFile(path, `${JSON.stringify(proposal, null, 2)}\n`);
print({ proposal, output: path, mutationPerformed: true }, { json });
} else print({ proposal, mutationPerformed: false }, { json });
return proposal.compatibility.readyForApproval ? 0 : 1;
}
const designCommand = ['init', 'analyze', 'domains', 'design'].includes(command);
const repositoryRoot = designCommand ? resolve(cwd, optionValue(argv, '--repo') ?? '.') : cwd;
const repository = inspectRepository(repositoryRoot);
if (command === 'init' || command === 'analyze') {
const specifications = inspectSpecifications(optionValues(argv, '--spec'), {
cwd, repositoryRoot: repository.root,
});
const analysis = analyzeRepository(repository, { specifications });
print({
command, repository, analysis,
next: 'Run torch design with the same repository/specification inputs, review its proposal, then install.',
}, { json });
return 0;
}
if (command === 'domains' || command === 'design') {
const specifications = inspectSpecifications(optionValues(argv, '--spec'), {
cwd, repositoryRoot: repository.root,
});
const analysis = analyzeRepository(repository, { specifications });
const proposal = proposeDomains({ repository, analysis });
const output = optionValue(argv, '--output');
if (output) {
const path = resolve(cwd, output);
writeNewFile(path, `${JSON.stringify(proposal, null, 2)}\n`);
print({ proposal, output: path, mutationPerformed: true }, { json });
} else {
print({ proposal, mutationPerformed: false }, { json });
}
return 0;
}
if (command === 'install') {
const proposalPath = optionValue(argv, '--proposal');
if (argv.includes('--dry-run')) {
const proposal = loadProposal(cwd, proposalPath);
print({ ...planInstall({ repository, proposal, env }), proposal: resolve(cwd, proposalPath) }, { json });
return 0;
}
if (!argv.includes('--yes')) {
throw new TorchError('Installation requires reviewed approval. Re-run with --dry-run, then --yes.', {
code: 'APPROVAL_REQUIRED',
});
}
const proposal = loadProposal(cwd, proposalPath);
print(installProject({ repository, proposal, env }), { json });
return 0;
}
if (command === 'doctor') {
const diagnosis = diagnoseProject({ repository, env });
print(diagnosis, { json });
return diagnosis.healthy ? 0 : 1;
}
if (command === 'worktrees') {
const parentOverride = optionValue(argv, '--parent');
if (argv.includes('--dry-run')) {
const plan = planWorktrees({ repository, parentOverride });
print(plan, { json });
return plan.canProceed ? 0 : 1;
}
if (!argv.includes('--yes')) {
throw new TorchError('Worktree creation requires a reviewed dry run. Re-run with --dry-run, then --yes.', {
code: 'APPROVAL_REQUIRED',
});
}
print(createWorktrees({ repository, parentOverride }), { json });
return 0;
}
if (command === 'up') {
const control = openControlPlane({ repositoryRoot: repository.root, env });
try {
const adapters = runtimeAdapters();
const plan = planFleetUp({
repositoryRoot: repository.root, controlPlane: control, adapters, fresh: argv.includes('--fresh'),
});
if (argv.includes('--dry-run')) {
print(plan, { json });
return plan.canProceed ? 0 : 1;
}
if (!argv.includes('--yes')) {
throw new TorchError('Fleet startup launches configured AI runtimes. Review --dry-run, then re-run with --yes.', {
code: 'APPROVAL_REQUIRED',
});
}
print(startFleet({
plan, controlPlane: control, adapters,
executor: (launch) => spawn(launch.command, launch.args, {
cwd: launch.cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
}),
}), { json });
return 0;
} finally {
control.close();
}
}
if (command === 'down') {
const control = openControlPlane({ repositoryRoot: repository.root, env });
try {
const adapters = runtimeAdapters();
const plan = planFleetDown({ repositoryRoot: repository.root, controlPlane: control, adapters });
if (argv.includes('--dry-run')) {
print(plan, { json });
return plan.canProceed ? 0 : 1;
}
if (!argv.includes('--yes')) {
throw new TorchError('Fleet wind-down stops configured runtime sessions. Review --dry-run, then re-run with --yes.', {
code: 'APPROVAL_REQUIRED',
});
}
const runtimeStoppers = new Map([
['claude', createClaudeAdapter({
runner: (executable, args) => spawn(executable, args, {
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
}),
})],
['codex', createCodexAdapter()],
]);
print(stopFleet({
plan, controlPlane: control,
stopRuntime: (action) => runtimeStoppers.get(action.runtime)
.stopSession({ runtimeSessionId: action.runtimeSessionId }),
}), { json });
return 0;
} finally {
control.close();
}
}
if (command === 'identity') {
print(withControlPlane(repository, env, (control) => control.identity(optionValue(argv, '--area'))), { json });
return 0;
}
if (command === 'agents') {
print(withControlPlane(repository, env, (control) => ({ agents: control.listAgents() })), { json });
return 0;
}
if (command === 'agent') {
print(withControlPlane(repository, env, (control) => control.getAgent(optionValue(argv, '--area'))), { json });
return 0;
}
if (command === 'roster') {
print(withControlPlane(repository, env, (control) => control.getRoster()), { json });
return 0;
}
if (command === 'who-owns') {
print(withControlPlane(repository, env, (control) => control.whoOwns({
path: optionValue(argv, '--path'), capability: optionValue(argv, '--capability'),
})), { json });
return 0;
}
if (command === 'neighbours') {
const areaId = optionValue(argv, '--area');
print(withControlPlane(repository, env, (control) => ({ areaId, neighbours: control.neighbours(areaId) })), { json });
return 0;
}
if (command === 'message') {
print(withControlPlane(repository, env, (control) => control.sendMessage({
sender: optionValue(argv, '--from'), recipient: optionValue(argv, '--to'),
body: optionValue(argv, '--body'), references: {
task: optionValue(argv, '--task'), path: optionValue(argv, '--path'),
commit: optionValue(argv, '--commit'), handoff: optionValue(argv, '--handoff'),
},
})), { json });
return 0;
}
if (command === 'inbox') {
print(withControlPlane(repository, env, (control) => ({
recipient: optionValue(argv, '--area'),
messages: control.readMessages({
recipient: optionValue(argv, '--area'), unacknowledgedOnly: argv.includes('--unacknowledged'),
limit: Number(optionValue(argv, '--limit') ?? 100),
}),
})), { json });
return 0;
}
if (command === 'ack') {
print(withControlPlane(repository, env, (control) => control.ackMessage({
recipient: optionValue(argv, '--area'), messageId: optionValue(argv, '--message'),
})), { json });
return 0;
}
if (command === 'status') {
print(withControlPlane(repository, env, (control) => control.reportStatus({
areaId: optionValue(argv, '--area'), state: optionValue(argv, '--state'),
summary: optionValue(argv, '--summary'), runtime: optionValue(argv, '--runtime'),
runtimeSessionId: optionValue(argv, '--session'), task: optionValue(argv, '--task'),
})), { json });
return 0;
}
if (command === 'complete' || command === 'blocked') {
const input = {
areaId: optionValue(argv, '--area'), summary: optionValue(argv, '--summary'),
task: optionValue(argv, '--task'), evidence: optionValue(argv, '--evidence'),
path: optionValue(argv, '--path'), commit: optionValue(argv, '--commit'),
};
print(withControlPlane(repository, env, (control) =>
(command === 'complete' ? control.reportComplete(input) : control.reportBlocked(input))), { json });
return 0;
}
if (command === 'coordinate') {
const participants = (optionValue(argv, '--with') ?? '').split(',').filter(Boolean);
print(withControlPlane(repository, env, (control) => control.requestCoordination({
sender: optionValue(argv, '--from'), participants, body: optionValue(argv, '--body'),
task: optionValue(argv, '--task'), path: optionValue(argv, '--path'),
})), { json });
return 0;
}
if (command === 'handoff') {
print(withControlPlane(repository, env, (control) => control.requestHandoff({
sender: optionValue(argv, '--from'), recipient: optionValue(argv, '--to') ?? 'session-manager',
path: optionValue(argv, '--path'), task: optionValue(argv, '--task'), reason: optionValue(argv, '--reason'),
})), { json });
return 0;
}
if (command === 'backlog') {
const operation = argv[1] ?? 'list';
const control = openControlPlane({ repositoryRoot: repository.root, env });
try {
const resources = new ResourceService({ repositoryRoot: repository.root, controlPlane: control });
const checks = new CheckService({
repositoryRoot: repository.root, controlPlane: control, resourceService: resources,
});
const integration = new IntegrationService({
repositoryRoot: repository.root, controlPlane: control, checkService: checks,
});
const backlog = new BacklogService({
repositoryRoot: repository.root, controlPlane: control,
integrationLookup: (requestId) => integration.get(requestId),
});
if (operation === 'list') print({ tasks: backlog.list({
state: optionValue(argv, '--state'), owner: optionValue(argv, '--owner'),
}) }, { json });
else if (operation === 'get') print(backlog.get(optionValue(argv, '--task')), { json });
else if (operation === 'create') print(backlog.create({
actorId: optionValue(argv, '--area'), title: optionValue(argv, '--title'),
description: optionValue(argv, '--description'), priority: optionValue(argv, '--priority') ?? 'normal',
acceptanceCriteria: commaList(optionValue(argv, '--accept')),
affectedDomains: commaList(optionValue(argv, '--domains')),
dependencies: commaList(optionValue(argv, '--depends')),
}), { json });
else if (operation === 'transition') print(backlog.transition({
taskId: optionValue(argv, '--task'), actorId: optionValue(argv, '--area'),
to: optionValue(argv, '--state'), expectedRevision: Number(optionValue(argv, '--revision')),
owner: optionValue(argv, '--owner'), evidence: commaList(optionValue(argv, '--evidence')),
commit: optionValue(argv, '--commit'), integrationRequest: optionValue(argv, '--integration'),
blockedReason: optionValue(argv, '--reason'), note: optionValue(argv, '--note'),
}), { json });
else throw new TorchError(`Unknown backlog operation: ${operation}`, { code: 'UNKNOWN_COMMAND' });
return 0;
} finally {
control.close();
}
}
if (command === 'checks') {
const operation = argv[1] ?? 'list';
const control = openControlPlane({ repositoryRoot: repository.root, env });
try {
const resources = new ResourceService({ repositoryRoot: repository.root, controlPlane: control });
const checks = new CheckService({
repositoryRoot: repository.root, controlPlane: control, resourceService: resources,
});
if (operation === 'list') print({ checks: checks.listChecks() }, { json });
else if (operation === 'receipts') print({ receipts: checks.receipts({
commit: optionValue(argv, '--commit'), checkId: optionValue(argv, '--id'),
areaId: optionValue(argv, '--area'),
}) }, { json });
else if (operation === 'plan') print(checks.plan({
checkId: optionValue(argv, '--id'), areaId: optionValue(argv, '--area'),
}), { json });
else if (operation === 'run') {
if (!argv.includes('--yes')) {
throw new TorchError('Configured check execution requires an explicit --yes.', { code: 'APPROVAL_REQUIRED' });
}
print(checks.run({ checkId: optionValue(argv, '--id'), areaId: optionValue(argv, '--area') }), { json });
} else throw new TorchError(`Unknown checks operation: ${operation}`, { code: 'UNKNOWN_COMMAND' });
return 0;
} finally {
control.close();
}
}
if (command === 'resources') {
const operation = argv[1] ?? 'list';
const control = openControlPlane({ repositoryRoot: repository.root, env });
try {
const resources = new ResourceService({ repositoryRoot: repository.root, controlPlane: control });
const resourceId = optionValue(argv, '--id');
const areaId = optionValue(argv, '--area');
if (operation === 'list') print({ resources: resources.list() }, { json });
else if (operation === 'status') print(resources.status(resourceId), { json });
else if (operation === 'acquire') print(resources.acquire({ resourceId, areaId }), { json });
else if (operation === 'release') print(resources.release({ resourceId, areaId }), { json });
else if (operation === 'cancel') print(resources.cancel({ resourceId, areaId }), { json });
else throw new TorchError(`Unknown resources operation: ${operation}`, { code: 'UNKNOWN_COMMAND' });
return 0;
} finally {
control.close();
}
}
if (command === 'integrate') {
const operation = argv[1] ?? 'list';
const control = openControlPlane({ repositoryRoot: repository.root, env });
try {
const resources = new ResourceService({ repositoryRoot: repository.root, controlPlane: control });
const checks = new CheckService({
repositoryRoot: repository.root, controlPlane: control, resourceService: resources,
});
const integration = new IntegrationService({
repositoryRoot: repository.root, controlPlane: control, checkService: checks,
});
const requestId = optionValue(argv, '--request');
const actorId = optionValue(argv, '--area');
if (operation === 'list') print({ requests: integration.list({ state: optionValue(argv, '--state') }) }, { json });
else if (operation === 'request') print(integration.request({
areaId: actorId, commit: optionValue(argv, '--commit'),
}), { json });
else if (operation === 'evaluate') print(integration.evaluate(requestId), { json });
else if (operation === 'authorize') print(integration.authorize({ requestId, actorId }), { json });
else if (operation === 'plan') print(integration.planLanding({ requestId, actorId }), { json });
else if (operation === 'land') {
if (!argv.includes('--yes')) {
throw new TorchError('Landing changes canonical main. Review integrate plan, then re-run with --yes.', {
code: 'APPROVAL_REQUIRED',
});
}
print(integration.land({ requestId, actorId }), { json });
} else throw new TorchError(`Unknown integrate operation: ${operation}`, { code: 'UNKNOWN_COMMAND' });
return 0;
} finally {
control.close();
}
}
if (command === 'canonical') {
const operation = argv[1] ?? 'plan';
if (operation === 'plan') {
const plan = planLocalCanonical({ repositoryRoot: repository.root });
print(plan, { json });
return plan.canProceed ? 0 : 1;
}
if (operation === 'create') {
if (!argv.includes('--yes')) {
throw new TorchError('Local canonical creation changes Git configuration. Review canonical plan, then use --yes.', {
code: 'APPROVAL_REQUIRED',
});
}
print(createLocalCanonical({ repositoryRoot: repository.root }), { json });
return 0;
}
throw new TorchError(`Unknown canonical operation: ${operation}`, { code: 'UNKNOWN_COMMAND' });
}
if (command === 'recoverability') {
print(classifyRecoverability({
repositoryRoot: repository.root, commit: optionValue(argv, '--commit') ?? 'HEAD',
}), { json });
return 0;
}
if (command === 'uninstall') {
const result = uninstallProject({
repository,
purge: argv.includes('--purge'),
dryRun: argv.includes('--dry-run'),
});
print(result, { json });
return result.canProceed === false ? 1 : 0;
}
throw new TorchError(`Unknown command: ${command}`, { code: 'UNKNOWN_COMMAND' });
} catch (error) {
print(asErrorRecord(error), { json: true });
return error instanceof TorchError ? 2 : 1;
}
}