From f02ecd30e169e071b3b8964c684cee1c0dd27a0a Mon Sep 17 00:00:00 2001 From: Lior Kanfi Date: Fri, 9 Oct 2026 19:32:45 +0300 Subject: [PATCH] feat(cli): factory setup passthrough mirroring team setup (ADR-428) --- src/commands/factory.mjs | 50 ++++++++++++++++++++++++++++++++++++++ src/dispatch.mjs | 19 +++++++++++++++ src/help.mjs | 18 ++++++++++++++ tests/factory.test.mjs | 52 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+) create mode 100644 src/commands/factory.mjs create mode 100644 tests/factory.test.mjs diff --git a/src/commands/factory.mjs b/src/commands/factory.mjs new file mode 100644 index 0000000..7d9ef09 --- /dev/null +++ b/src/commands/factory.mjs @@ -0,0 +1,50 @@ +// Factory commands: setup for factory tracker provider configuration. + +import { readAgent } from "../utils/init-options.mjs"; +import { findInstalledSkills } from "../source.mjs"; +import { getAgent } from "../registry.mjs"; + +const PROVIDERS = ["github", "gitlab", "linear", "jira"]; + +// ── setup ────────────────────────────────────────────────────────────── +export async function cmdFactorySetup(args, flags) { + const provider = flags.provider; + if (provider && !PROVIDERS.includes(provider)) { + console.error(`Error: --provider must be one of: ${PROVIDERS.join(", ")}`); + return 1; + } + + const agentKey = flags.agents[0] || readAgent(); + if (!agentKey) { + console.error("Error: Agent required (-a or set in init-options.json)"); + return 1; + } + + // No auto-install: the skills source is unknown at this layer (unlike + // `team setup`, which receives it as an argument). Fail loudly instead — + // guessing a source would install untrusted skill content. + const agent = getAgent(agentKey); + if (agent) { + const projectRoot = process.cwd(); + const skillsDir = flags.global ? agent.global_skills_dir : agent.skills_dir; + if (skillsDir) { + const skills = await findInstalledSkills(skillsDir, projectRoot); + if (!skills.some((s) => s.name === "factory-setup")) { + console.error( + "Error: factory-setup skill not installed. Run 'adlc-cli skills add ' (or 'adlc-cli team setup ') first." + ); + return 1; + } + } + } + + const { cmdAgentRun } = await import("./agent.mjs"); + const prompt = provider + ? `Run /factory-setup to configure the factory tracker provider for this project (provider preselected: ${provider})` + : "Run /factory-setup to configure the factory tracker provider for this project"; + const runArgs = ["-a", agentKey, "--timeout", "300"]; + if (flags.format) runArgs.push("--format", flags.format); + if (flags.cwd) runArgs.push("--cwd", flags.cwd); + runArgs.push(prompt); + return cmdAgentRun(runArgs); +} diff --git a/src/dispatch.mjs b/src/dispatch.mjs index 0ec7c8e..cd6f385 100644 --- a/src/dispatch.mjs +++ b/src/dispatch.mjs @@ -4,6 +4,7 @@ import { createRequire } from "node:module"; import { AGENTS } from "./registry.mjs"; import { cmdAdd, cmdUpdate, cmdRemove, cmdStatus } from "./commands/skills.mjs"; import { cmdTeamSetup, cmdTeamUpdate, cmdTeamRepair } from "./commands/team.mjs"; +import { cmdFactorySetup } from "./commands/factory.mjs"; import { cmdAgentRun, cmdAgentList } from "./commands/agent.mjs"; import { cmdWorkspaceSetup, cmdWorkspaceInit, cmdWorkspaceStatus } from "./commands/workspace.mjs"; import { cmdWorkflow } from "./commands/workflow.mjs"; @@ -11,6 +12,7 @@ import { printCliHelp, printSkillsHelp, printTeamHelp, + printFactoryHelp, printAgentHelp, printWorkspaceHelp, printWorkflowHelp, @@ -91,6 +93,21 @@ function runNewTree({ command, args, flags }, argv) { return 1; } } + case "factory": { + const sub = args[0] ?? "help"; + const rest = args.slice(1); + switch (sub) { + case "setup": + return cmdFactorySetup(rest, flags); + case "help": + printFactoryHelp(); + return 0; + default: + console.error(`Unknown factory command: "${sub}"`); + printFactoryHelp(); + return 1; + } + } case "agent": { const sub = args[0] ?? "help"; const rest = argv.slice(argv.indexOf(sub) + 1); @@ -194,6 +211,8 @@ function parseArgs(argv) { flags.yes = true; } else if (arg === "--skip-skills") { flags.skipSkills = true; + } else if (arg === "--provider") { + flags.provider = rest[++i]; } else if (arg === "--update-confidence") { flags.updateConfidence = true; } else if (arg === "--build-to-delete") { diff --git a/src/help.mjs b/src/help.mjs index 1ef8b2b..a17abb0 100644 --- a/src/help.mjs +++ b/src/help.mjs @@ -15,6 +15,7 @@ COMMANDS: team setup -a Install skills + configure team-ai-directives team update Git pull + skills update + confidence update team repair Validate and repair team-ai-directives state + factory setup [--provider

] -a Configure factory tracker provider agent run "" [flags] Run a coding agent headlessly with a task agent list List supported agents + run profiles workspace setup [file] Apply workspace file (git modules, skills, commands, goal) @@ -91,6 +92,23 @@ Run 'adlc-cli help' for the full list. `); } +export function printFactoryHelp() { + console.log(` +USAGE: + adlc-cli factory [flags] + +COMMANDS: + setup [--provider

] -a Configure factory tracker provider (.adlc/issues-provider.yml) + help Show this help + +SETUP FLAGS: + --provider

Preselect provider: github | gitlab | linear | jira (skips the question, headless-safe) + -a Agent key (default: from init-options.json) + +Run 'adlc-cli help' for the full list. +`); +} + export function printAgentHelp() { console.log(` USAGE: diff --git a/tests/factory.test.mjs b/tests/factory.test.mjs new file mode 100644 index 0000000..dc7998a --- /dev/null +++ b/tests/factory.test.mjs @@ -0,0 +1,52 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { mkdtempSync, rmSync } from "node:fs"; +import { join, resolve, dirname } from "node:path"; +import { tmpdir } from "node:os"; +import { fileURLToPath } from "node:url"; + +const exec = promisify(execFile); +const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const BIN = join(REPO_ROOT, "bin", "adlc-cli.mjs"); + +test("factory setup without agent errors", async () => { + const tmp = mkdtempSync(join(tmpdir(), "adlc-test-")); + try { + const r = await exec(process.execPath, [BIN, "factory", "setup"], { cwd: tmp }).catch((e) => e); + assert.equal(r.code ?? r.exitCode, 1); + assert.match(String(r.stderr ?? r.stdout), /agent required/i); + } finally { rmSync(tmp, { recursive: true, force: true }); } +}); + +test("factory setup with invalid provider errors", async () => { + const r = await exec(process.execPath, [BIN, "factory", "setup", "--provider", "bogus"]).catch((e) => e); + assert.equal(r.code ?? r.exitCode, 1); + assert.match(String(r.stderr ?? r.stdout), /must be one of/i); +}); + +test("factory setup without installed skill errors", async () => { + const tmp = mkdtempSync(join(tmpdir(), "adlc-test-")); + try { + const r = await exec(process.execPath, [BIN, "factory", "setup", "-a", "opencode"], { cwd: tmp }).catch((e) => e); + assert.equal(r.code ?? r.exitCode, 1); + assert.match(String(r.stderr ?? r.stdout), /not installed/i); + } finally { rmSync(tmp, { recursive: true, force: true }); } +}); + +test("factory help shows setup", async () => { + const r = await exec(process.execPath, [BIN, "factory", "help"]).catch((e) => e); + assert.match(String(r.stdout ?? ""), /setup/); +}); + +test("factory unknown subcommand exits 1 with usage", async () => { + const r = await exec(process.execPath, [BIN, "factory", "bogus"]).catch((e) => e); + assert.equal(r.code ?? r.exitCode, 1); + assert.match(String(r.message ?? r.stderr ?? r.stdout), /unknown factory command|usage/i); +}); + +test("default help includes factory setup", async () => { + const r = await exec(process.execPath, [BIN]).catch((e) => e); + assert.match(String(r.stdout ?? ""), /factory setup/); +});