@garygentry/feature-forge 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/adapters/GENERATION-REPORT.md +128 -0
- package/adapters/claude/agents/forge-researcher.md +137 -0
- package/adapters/claude/agents/forge-spec-writer.md +115 -0
- package/adapters/claude/agents/forge-verifier.md +121 -0
- package/adapters/claude/references/epic-manifest-schema.json +120 -0
- package/adapters/claude/references/forge-config-schema.json +166 -0
- package/adapters/claude/references/pipeline-state-schema.json +110 -0
- package/adapters/claude/references/portable-root.md +56 -0
- package/adapters/claude/references/process-overview.md +123 -0
- package/adapters/claude/references/ralph-loop-contract.md +221 -0
- package/adapters/claude/references/shared-conventions.md +144 -0
- package/adapters/claude/references/skill-frontmatter.schema.json +17 -0
- package/adapters/claude/references/stack-resolution.md +51 -0
- package/adapters/claude/references/stacks/_generic.md +90 -0
- package/adapters/claude/references/stacks/go.md +138 -0
- package/adapters/claude/references/stacks/python.md +163 -0
- package/adapters/claude/references/stacks/rust.md +151 -0
- package/adapters/claude/references/stacks/typescript.md +111 -0
- package/adapters/claude/references/vendor-construct-inventory.md +49 -0
- package/adapters/claude/scripts/forge-root.sh +50 -0
- package/adapters/claude/skills/forge/SKILL.md +165 -0
- package/adapters/claude/skills/forge-0-epic/SKILL.md +303 -0
- package/adapters/claude/skills/forge-0-epic/references/edit-mode.md +222 -0
- package/adapters/claude/skills/forge-0-epic/references/epic-manifest-subcommands.md +64 -0
- package/adapters/claude/skills/forge-1-prd/SKILL.md +121 -0
- package/adapters/claude/skills/forge-1-prd/references/prd-template.md +106 -0
- package/adapters/claude/skills/forge-2-tech/SKILL.md +198 -0
- package/adapters/claude/skills/forge-2-tech/references/stack-discovery-checklist.md +95 -0
- package/adapters/claude/skills/forge-3-specs/SKILL.md +154 -0
- package/adapters/claude/skills/forge-3-specs/references/spec-archetypes.md +106 -0
- package/adapters/claude/skills/forge-3-specs/references/spec-examples.md +71 -0
- package/adapters/claude/skills/forge-4-backlog/SKILL.md +146 -0
- package/adapters/claude/skills/forge-5-loop/SKILL.md +303 -0
- package/adapters/claude/skills/forge-5-loop/references/result-reporting.md +63 -0
- package/adapters/claude/skills/forge-5-loop/references/runner-contract.md +214 -0
- package/adapters/claude/skills/forge-6-docs/SKILL.md +179 -0
- package/adapters/claude/skills/forge-6-docs/references/doc-conventions.md +126 -0
- package/adapters/claude/skills/forge-fix/SKILL.md +65 -0
- package/adapters/claude/skills/forge-init/SKILL.md +29 -0
- package/adapters/claude/skills/forge-verify/SKILL.md +219 -0
- package/adapters/claude/skills/forge-verify/references/verification-checklists.md +379 -0
- package/adapters/codex/agents/forge-researcher.md +133 -0
- package/adapters/codex/agents/forge-spec-writer.md +112 -0
- package/adapters/codex/agents/forge-verifier.md +115 -0
- package/adapters/codex/agents/openai.yaml +10 -0
- package/adapters/codex/references/epic-manifest-schema.json +120 -0
- package/adapters/codex/references/forge-config-schema.json +166 -0
- package/adapters/codex/references/pipeline-state-schema.json +110 -0
- package/adapters/codex/references/portable-root.md +56 -0
- package/adapters/codex/references/process-overview.md +123 -0
- package/adapters/codex/references/ralph-loop-contract.md +221 -0
- package/adapters/codex/references/shared-conventions.md +144 -0
- package/adapters/codex/references/skill-frontmatter.schema.json +17 -0
- package/adapters/codex/references/stack-resolution.md +51 -0
- package/adapters/codex/references/stacks/_generic.md +90 -0
- package/adapters/codex/references/stacks/go.md +138 -0
- package/adapters/codex/references/stacks/python.md +163 -0
- package/adapters/codex/references/stacks/rust.md +151 -0
- package/adapters/codex/references/stacks/typescript.md +111 -0
- package/adapters/codex/references/vendor-construct-inventory.md +49 -0
- package/adapters/codex/scripts/forge-root.sh +50 -0
- package/adapters/codex/skills/forge/forge.md +164 -0
- package/adapters/codex/skills/forge-0-epic/forge-0-epic.md +302 -0
- package/adapters/codex/skills/forge-0-epic/references/edit-mode.md +222 -0
- package/adapters/codex/skills/forge-0-epic/references/epic-manifest-subcommands.md +64 -0
- package/adapters/codex/skills/forge-1-prd/forge-1-prd.md +120 -0
- package/adapters/codex/skills/forge-1-prd/references/prd-template.md +106 -0
- package/adapters/codex/skills/forge-2-tech/forge-2-tech.md +197 -0
- package/adapters/codex/skills/forge-2-tech/references/stack-discovery-checklist.md +95 -0
- package/adapters/codex/skills/forge-3-specs/forge-3-specs.md +153 -0
- package/adapters/codex/skills/forge-3-specs/references/spec-archetypes.md +106 -0
- package/adapters/codex/skills/forge-3-specs/references/spec-examples.md +71 -0
- package/adapters/codex/skills/forge-4-backlog/forge-4-backlog.md +145 -0
- package/adapters/codex/skills/forge-5-loop/forge-5-loop.md +302 -0
- package/adapters/codex/skills/forge-5-loop/references/result-reporting.md +63 -0
- package/adapters/codex/skills/forge-5-loop/references/runner-contract.md +214 -0
- package/adapters/codex/skills/forge-6-docs/forge-6-docs.md +178 -0
- package/adapters/codex/skills/forge-6-docs/references/doc-conventions.md +126 -0
- package/adapters/codex/skills/forge-fix/forge-fix.md +64 -0
- package/adapters/codex/skills/forge-init/forge-init.md +29 -0
- package/adapters/codex/skills/forge-verify/forge-verify.md +218 -0
- package/adapters/codex/skills/forge-verify/references/verification-checklists.md +379 -0
- package/adapters/copilot/agents/forge-researcher.md +133 -0
- package/adapters/copilot/agents/forge-spec-writer.md +112 -0
- package/adapters/copilot/agents/forge-verifier.md +115 -0
- package/adapters/copilot/references/epic-manifest-schema.json +120 -0
- package/adapters/copilot/references/forge-config-schema.json +166 -0
- package/adapters/copilot/references/pipeline-state-schema.json +110 -0
- package/adapters/copilot/references/portable-root.md +56 -0
- package/adapters/copilot/references/process-overview.md +123 -0
- package/adapters/copilot/references/ralph-loop-contract.md +221 -0
- package/adapters/copilot/references/shared-conventions.md +144 -0
- package/adapters/copilot/references/skill-frontmatter.schema.json +17 -0
- package/adapters/copilot/references/stack-resolution.md +51 -0
- package/adapters/copilot/references/stacks/_generic.md +90 -0
- package/adapters/copilot/references/stacks/go.md +138 -0
- package/adapters/copilot/references/stacks/python.md +163 -0
- package/adapters/copilot/references/stacks/rust.md +151 -0
- package/adapters/copilot/references/stacks/typescript.md +111 -0
- package/adapters/copilot/references/vendor-construct-inventory.md +49 -0
- package/adapters/copilot/scripts/forge-root.sh +50 -0
- package/adapters/copilot/skills/forge/forge.md +164 -0
- package/adapters/copilot/skills/forge-0-epic/forge-0-epic.md +302 -0
- package/adapters/copilot/skills/forge-0-epic/references/edit-mode.md +222 -0
- package/adapters/copilot/skills/forge-0-epic/references/epic-manifest-subcommands.md +64 -0
- package/adapters/copilot/skills/forge-1-prd/forge-1-prd.md +120 -0
- package/adapters/copilot/skills/forge-1-prd/references/prd-template.md +106 -0
- package/adapters/copilot/skills/forge-2-tech/forge-2-tech.md +197 -0
- package/adapters/copilot/skills/forge-2-tech/references/stack-discovery-checklist.md +95 -0
- package/adapters/copilot/skills/forge-3-specs/forge-3-specs.md +153 -0
- package/adapters/copilot/skills/forge-3-specs/references/spec-archetypes.md +106 -0
- package/adapters/copilot/skills/forge-3-specs/references/spec-examples.md +71 -0
- package/adapters/copilot/skills/forge-4-backlog/forge-4-backlog.md +145 -0
- package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +302 -0
- package/adapters/copilot/skills/forge-5-loop/references/result-reporting.md +63 -0
- package/adapters/copilot/skills/forge-5-loop/references/runner-contract.md +214 -0
- package/adapters/copilot/skills/forge-6-docs/forge-6-docs.md +178 -0
- package/adapters/copilot/skills/forge-6-docs/references/doc-conventions.md +126 -0
- package/adapters/copilot/skills/forge-fix/forge-fix.md +64 -0
- package/adapters/copilot/skills/forge-init/forge-init.md +29 -0
- package/adapters/copilot/skills/forge-verify/forge-verify.md +218 -0
- package/adapters/copilot/skills/forge-verify/references/verification-checklists.md +379 -0
- package/adapters/cursor/agents/forge-researcher.mdc +134 -0
- package/adapters/cursor/agents/forge-spec-writer.mdc +113 -0
- package/adapters/cursor/agents/forge-verifier.mdc +116 -0
- package/adapters/cursor/references/epic-manifest-schema.json +120 -0
- package/adapters/cursor/references/forge-config-schema.json +166 -0
- package/adapters/cursor/references/pipeline-state-schema.json +110 -0
- package/adapters/cursor/references/portable-root.md +56 -0
- package/adapters/cursor/references/process-overview.md +123 -0
- package/adapters/cursor/references/ralph-loop-contract.md +221 -0
- package/adapters/cursor/references/shared-conventions.md +144 -0
- package/adapters/cursor/references/skill-frontmatter.schema.json +17 -0
- package/adapters/cursor/references/stack-resolution.md +51 -0
- package/adapters/cursor/references/stacks/_generic.md +90 -0
- package/adapters/cursor/references/stacks/go.md +138 -0
- package/adapters/cursor/references/stacks/python.md +163 -0
- package/adapters/cursor/references/stacks/rust.md +151 -0
- package/adapters/cursor/references/stacks/typescript.md +111 -0
- package/adapters/cursor/references/vendor-construct-inventory.md +49 -0
- package/adapters/cursor/scripts/forge-root.sh +50 -0
- package/adapters/cursor/skills/forge/forge.mdc +165 -0
- package/adapters/cursor/skills/forge-0-epic/forge-0-epic.mdc +303 -0
- package/adapters/cursor/skills/forge-0-epic/references/edit-mode.md +222 -0
- package/adapters/cursor/skills/forge-0-epic/references/epic-manifest-subcommands.md +64 -0
- package/adapters/cursor/skills/forge-1-prd/forge-1-prd.mdc +121 -0
- package/adapters/cursor/skills/forge-1-prd/references/prd-template.md +106 -0
- package/adapters/cursor/skills/forge-2-tech/forge-2-tech.mdc +198 -0
- package/adapters/cursor/skills/forge-2-tech/references/stack-discovery-checklist.md +95 -0
- package/adapters/cursor/skills/forge-3-specs/forge-3-specs.mdc +154 -0
- package/adapters/cursor/skills/forge-3-specs/references/spec-archetypes.md +106 -0
- package/adapters/cursor/skills/forge-3-specs/references/spec-examples.md +71 -0
- package/adapters/cursor/skills/forge-4-backlog/forge-4-backlog.mdc +146 -0
- package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +303 -0
- package/adapters/cursor/skills/forge-5-loop/references/result-reporting.md +63 -0
- package/adapters/cursor/skills/forge-5-loop/references/runner-contract.md +214 -0
- package/adapters/cursor/skills/forge-6-docs/forge-6-docs.mdc +179 -0
- package/adapters/cursor/skills/forge-6-docs/references/doc-conventions.md +126 -0
- package/adapters/cursor/skills/forge-fix/forge-fix.mdc +65 -0
- package/adapters/cursor/skills/forge-init/forge-init.mdc +30 -0
- package/adapters/cursor/skills/forge-verify/forge-verify.mdc +219 -0
- package/adapters/cursor/skills/forge-verify/references/verification-checklists.md +379 -0
- package/adapters/gemini/agents/forge-researcher.md +133 -0
- package/adapters/gemini/agents/forge-spec-writer.md +112 -0
- package/adapters/gemini/agents/forge-verifier.md +115 -0
- package/adapters/gemini/gemini-extension.json +54 -0
- package/adapters/gemini/references/epic-manifest-schema.json +120 -0
- package/adapters/gemini/references/forge-config-schema.json +166 -0
- package/adapters/gemini/references/pipeline-state-schema.json +110 -0
- package/adapters/gemini/references/portable-root.md +56 -0
- package/adapters/gemini/references/process-overview.md +123 -0
- package/adapters/gemini/references/ralph-loop-contract.md +221 -0
- package/adapters/gemini/references/shared-conventions.md +144 -0
- package/adapters/gemini/references/skill-frontmatter.schema.json +17 -0
- package/adapters/gemini/references/stack-resolution.md +51 -0
- package/adapters/gemini/references/stacks/_generic.md +90 -0
- package/adapters/gemini/references/stacks/go.md +138 -0
- package/adapters/gemini/references/stacks/python.md +163 -0
- package/adapters/gemini/references/stacks/rust.md +151 -0
- package/adapters/gemini/references/stacks/typescript.md +111 -0
- package/adapters/gemini/references/vendor-construct-inventory.md +49 -0
- package/adapters/gemini/scripts/forge-root.sh +50 -0
- package/adapters/gemini/skills/forge/forge.md +164 -0
- package/adapters/gemini/skills/forge-0-epic/forge-0-epic.md +302 -0
- package/adapters/gemini/skills/forge-0-epic/references/edit-mode.md +222 -0
- package/adapters/gemini/skills/forge-0-epic/references/epic-manifest-subcommands.md +64 -0
- package/adapters/gemini/skills/forge-1-prd/forge-1-prd.md +120 -0
- package/adapters/gemini/skills/forge-1-prd/references/prd-template.md +106 -0
- package/adapters/gemini/skills/forge-2-tech/forge-2-tech.md +197 -0
- package/adapters/gemini/skills/forge-2-tech/references/stack-discovery-checklist.md +95 -0
- package/adapters/gemini/skills/forge-3-specs/forge-3-specs.md +153 -0
- package/adapters/gemini/skills/forge-3-specs/references/spec-archetypes.md +106 -0
- package/adapters/gemini/skills/forge-3-specs/references/spec-examples.md +71 -0
- package/adapters/gemini/skills/forge-4-backlog/forge-4-backlog.md +145 -0
- package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +302 -0
- package/adapters/gemini/skills/forge-5-loop/references/result-reporting.md +63 -0
- package/adapters/gemini/skills/forge-5-loop/references/runner-contract.md +214 -0
- package/adapters/gemini/skills/forge-6-docs/forge-6-docs.md +178 -0
- package/adapters/gemini/skills/forge-6-docs/references/doc-conventions.md +126 -0
- package/adapters/gemini/skills/forge-fix/forge-fix.md +64 -0
- package/adapters/gemini/skills/forge-init/forge-init.md +29 -0
- package/adapters/gemini/skills/forge-verify/forge-verify.md +218 -0
- package/adapters/gemini/skills/forge-verify/references/verification-checklists.md +379 -0
- package/dist/agent-targets.d.ts +70 -0
- package/dist/agent-targets.js +111 -0
- package/dist/apply.d.ts +49 -0
- package/dist/apply.js +246 -0
- package/dist/cli.d.ts +94 -0
- package/dist/cli.js +508 -0
- package/dist/detect.d.ts +45 -0
- package/dist/detect.js +72 -0
- package/dist/fsutil.d.ts +56 -0
- package/dist/fsutil.js +175 -0
- package/dist/hash.d.ts +50 -0
- package/dist/hash.js +107 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +9 -0
- package/dist/manifest.d.ts +72 -0
- package/dist/manifest.js +222 -0
- package/dist/plan.d.ts +66 -0
- package/dist/plan.js +166 -0
- package/dist/rauf.d.ts +83 -0
- package/dist/rauf.js +118 -0
- package/dist/report.d.ts +35 -0
- package/dist/report.js +110 -0
- package/dist/source.d.ts +69 -0
- package/dist/source.js +164 -0
- package/dist/types.d.ts +264 -0
- package/dist/types.js +57 -0
- package/package.json +42 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI entry & dispatch (spec 07). The installer's process entry point and orchestration
|
|
3
|
+
* layer: it parses `process.argv` via `node:util.parseArgs`, resolves the target agent set,
|
|
4
|
+
* runs the per-agent plan/apply (or list) pipeline catching per-agent failures, renders the
|
|
5
|
+
* `RunReport`, and returns the process exit code.
|
|
6
|
+
*
|
|
7
|
+
* Zero runtime deps (`node:` built-ins only). Core functions return `Result`/`RunReport` and
|
|
8
|
+
* never throw for expected errors; `main` is the single boundary that maps to exit codes and
|
|
9
|
+
* touches `process`. `runCli` is the env-injectable testable core (the hermetic seam item 011
|
|
10
|
+
* relies on). Named exports only.
|
|
11
|
+
*/
|
|
12
|
+
import { parseArgs } from "node:util";
|
|
13
|
+
import process from "node:process";
|
|
14
|
+
import { readFileSync } from "node:fs";
|
|
15
|
+
import { pathToFileURL } from "node:url";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import { AGENT_IDS, AGENT_TARGETS, EXIT, err, ok, } from "./types.js";
|
|
18
|
+
import { detectAgent, detectAgents, resolveRoots } from "./agent-targets.js"; // 02
|
|
19
|
+
import { locateSource } from "./source.js"; // 03
|
|
20
|
+
import { plan, resolveMode } from "./plan.js"; // 04
|
|
21
|
+
import { apply } from "./apply.js"; // 04
|
|
22
|
+
import { manifestPath, readManifest, planUninstall } from "./manifest.js"; // 05
|
|
23
|
+
import { preflightRauf, RAUF_PIN } from "./rauf.js"; // 06
|
|
24
|
+
import { sha256File } from "./hash.js"; // 03 (list destination-drift hashing)
|
|
25
|
+
import { renderReport } from "./report.js"; // 09
|
|
26
|
+
/** Canonical subcommand table (REQ-DIST-03, §1.2). */
|
|
27
|
+
export const SUBCOMMANDS = [
|
|
28
|
+
{ canonical: "install", aliases: ["add"], help: "Install feature-forge into the target agent(s)." },
|
|
29
|
+
{ canonical: "update", aliases: [], help: "Reconcile an existing install to the current adapters." },
|
|
30
|
+
{ canonical: "uninstall", aliases: ["remove"], help: "Remove a prior install (manifest-tracked files only)." },
|
|
31
|
+
{ canonical: "list", aliases: ["ls"], help: "Report per-agent detected / installed / up-to-date status." },
|
|
32
|
+
];
|
|
33
|
+
/** Canonical flag table (REQ-FLAG-01..05, §1.3). */
|
|
34
|
+
export const FLAGS = [
|
|
35
|
+
{ name: "agent", short: "a", type: "string", arg: "<id>", help: `Scope to one agent (${AGENT_IDS.join("|")}). Default: all detected.` },
|
|
36
|
+
{ name: "global", short: "g", type: "boolean", help: "Install into the user-level config dir (default: project-local)." },
|
|
37
|
+
{ name: "symlink", type: "boolean", help: "Symlink the bundle instead of copying (default: copy; Windows always copies)." },
|
|
38
|
+
{ name: "force", type: "boolean", help: "Overwrite a locally-modified destination that would otherwise be skipped." },
|
|
39
|
+
{ name: "dry-run", type: "boolean", help: "Print the planned actions without changing anything." },
|
|
40
|
+
{ name: "yes", short: "y", type: "boolean", help: "Non-interactive: assume confirmed; never block on input." },
|
|
41
|
+
{ name: "json", type: "boolean", help: "Emit the run report as JSON." },
|
|
42
|
+
{ name: "skip-rauf", type: "boolean", help: "Skip the rauf resolvability preflight (records raufPin: null)." },
|
|
43
|
+
{ name: "source", type: "string", arg: "<dir>", hidden: true, help: "(test only) Override the adapters source directory." },
|
|
44
|
+
{ name: "help", short: "h", type: "boolean", help: "Show this help and exit." },
|
|
45
|
+
{ name: "version", type: "boolean", help: "Print the installer version and exit." },
|
|
46
|
+
];
|
|
47
|
+
/**
|
|
48
|
+
* Parse and validate `argv` (already sliced past `node` + script — `process.argv.slice(2)`)
|
|
49
|
+
* via `node:util.parseArgs` (zero-dep). Resolves aliases, rejects unknown
|
|
50
|
+
* subcommand/flag/agent (and a parseArgs throw) as a `USAGE` error. Pure: no I/O, no exit.
|
|
51
|
+
*/
|
|
52
|
+
export function parseCliArgs(argv) {
|
|
53
|
+
let parsed;
|
|
54
|
+
try {
|
|
55
|
+
parsed = parseArgs({ args: argv, options: buildParseOptions(), allowPositionals: true });
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
return usage(`invalid arguments: ${e.message}`);
|
|
59
|
+
}
|
|
60
|
+
const positionals = parsed.positionals;
|
|
61
|
+
const values = parsed.values;
|
|
62
|
+
const wantsHelp = values.help === true;
|
|
63
|
+
const wantsVersion = values.version === true;
|
|
64
|
+
// Resolve the subcommand (first positional) unless help/version short-circuits in main().
|
|
65
|
+
const raw = positionals[0];
|
|
66
|
+
let subcommand;
|
|
67
|
+
if (raw !== undefined) {
|
|
68
|
+
subcommand = resolveSubcommand(raw);
|
|
69
|
+
if (subcommand === undefined) {
|
|
70
|
+
return usage(`unknown subcommand '${raw}'. Run 'feature-forge --help' for usage.`);
|
|
71
|
+
}
|
|
72
|
+
if (positionals.length > 1) {
|
|
73
|
+
return usage(`unexpected extra argument '${positionals[1]}'.`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else if (!wantsHelp && !wantsVersion) {
|
|
77
|
+
return usage("no subcommand given. Run 'feature-forge --help' for usage.");
|
|
78
|
+
}
|
|
79
|
+
// Validate --agent against the closed set (REQ-FLAG-01).
|
|
80
|
+
let agent;
|
|
81
|
+
const agentRaw = values.agent;
|
|
82
|
+
if (typeof agentRaw === "string") {
|
|
83
|
+
if (!isAgentId(agentRaw)) {
|
|
84
|
+
return usage(`unknown agent '${agentRaw}'. Valid: ${AGENT_IDS.join(", ")}.`);
|
|
85
|
+
}
|
|
86
|
+
agent = agentRaw;
|
|
87
|
+
}
|
|
88
|
+
const flags = {
|
|
89
|
+
agent,
|
|
90
|
+
global: values.global === true,
|
|
91
|
+
symlink: values.symlink === true,
|
|
92
|
+
force: values.force === true,
|
|
93
|
+
dryRun: values["dry-run"] === true,
|
|
94
|
+
yes: values.yes === true,
|
|
95
|
+
json: values.json === true,
|
|
96
|
+
skipRauf: values["skip-rauf"] === true,
|
|
97
|
+
source: typeof values.source === "string" ? values.source : undefined,
|
|
98
|
+
};
|
|
99
|
+
// help/version with no subcommand is still "ok": main() acts on flags before requiring a
|
|
100
|
+
// subcommand. "list" is a harmless placeholder main() never reaches in that case.
|
|
101
|
+
return ok({ subcommand: subcommand ?? "list", flags });
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Build the `node:util.parseArgs` `options` object from the single FLAGS spec (§1.5) so parsing
|
|
105
|
+
* always equals the documented surface (REQ-DIST-03). The SOLE source of the parse config —
|
|
106
|
+
* shared by `parseCliArgs` and `rawParse` so the two can never drift.
|
|
107
|
+
*/
|
|
108
|
+
function buildParseOptions() {
|
|
109
|
+
const options = {};
|
|
110
|
+
for (const f of FLAGS) {
|
|
111
|
+
options[f.name] = f.short ? { type: f.type, short: f.short } : { type: f.type };
|
|
112
|
+
}
|
|
113
|
+
return options;
|
|
114
|
+
}
|
|
115
|
+
/** Resolve an alias/canonical token to a `Subcommand`, or undefined if unknown. */
|
|
116
|
+
function resolveSubcommand(token) {
|
|
117
|
+
for (const s of SUBCOMMANDS) {
|
|
118
|
+
if (s.canonical === token || s.aliases.includes(token))
|
|
119
|
+
return s.canonical;
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
/** Narrowing guard for the closed `AgentId` set. */
|
|
124
|
+
function isAgentId(v) {
|
|
125
|
+
return AGENT_IDS.includes(v);
|
|
126
|
+
}
|
|
127
|
+
/** Small helper: a USAGE Result (mapped to EXIT.USAGE at the boundary). */
|
|
128
|
+
function usage(message) {
|
|
129
|
+
return err({ code: "USAGE", message, remedy: "Run 'feature-forge --help' for usage." });
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Map a structured `InstallerError` to a process exit code (tech-spec §7).
|
|
133
|
+
* "USAGE" → EXIT.USAGE (2); everything else → EXIT.FAILURE (1).
|
|
134
|
+
*/
|
|
135
|
+
export function mapErrorToExit(error) {
|
|
136
|
+
return error.code === "USAGE" ? EXIT.USAGE : EXIT.FAILURE;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Run the full CLI pipeline programmatically and return the assembled `RunReport` WITHOUT
|
|
140
|
+
* touching `process` (no argv read, no stdout/stderr write, no exit). This is the testable
|
|
141
|
+
* core (08 §3.4): it threads env.home/cwd into detection/manifest calls, env.registry into the
|
|
142
|
+
* rauf preflight, and env.platform into the copy/symlink mode decision.
|
|
143
|
+
*/
|
|
144
|
+
export async function runCli(argv, env = {}) {
|
|
145
|
+
const parsed = parseCliArgs(argv);
|
|
146
|
+
if (!parsed.ok) {
|
|
147
|
+
// runCli's contract is the dispatch core; parse validation is owned by main (§3.1). Direct
|
|
148
|
+
// callers with malformed argv surface here as an unexpected throw (main never reaches this).
|
|
149
|
+
throw new Error(parsed.error.message);
|
|
150
|
+
}
|
|
151
|
+
const { subcommand, flags } = parsed.value;
|
|
152
|
+
return subcommand === "list"
|
|
153
|
+
? runList(flags, env)
|
|
154
|
+
: runMutation(subcommand, flags, env);
|
|
155
|
+
}
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// 4. main — the dispatch boundary (§3.1)
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
/**
|
|
160
|
+
* Parse → help/version precedence → run pipeline (catching per-agent errors via runCli) →
|
|
161
|
+
* render → exit code. The only place that writes to stdout/stderr and decides the exit code.
|
|
162
|
+
* Never reads stdin (REQ-DIST-02).
|
|
163
|
+
*
|
|
164
|
+
* @param argv - the post-`node` argument list (`process.argv.slice(2)` in production).
|
|
165
|
+
* @param env - the injectable CLI env (the hermetic-test seam, §3.1a); default `{}` = real
|
|
166
|
+
* defaults. Tests inject a throwing seam (e.g. a registry that throws) here to
|
|
167
|
+
* exercise the UNEXPECTED boundary catch deterministically without a network call.
|
|
168
|
+
*/
|
|
169
|
+
export async function main(argv, env = {}) {
|
|
170
|
+
let report;
|
|
171
|
+
let json = false;
|
|
172
|
+
try {
|
|
173
|
+
const parsed = parseCliArgs(argv);
|
|
174
|
+
if (!parsed.ok) {
|
|
175
|
+
process.stderr.write(`error: ${parsed.error.message}\n\n`);
|
|
176
|
+
process.stderr.write(helpText() + "\n");
|
|
177
|
+
return mapErrorToExit(parsed.error); // EXIT.USAGE
|
|
178
|
+
}
|
|
179
|
+
const { flags } = parsed.value;
|
|
180
|
+
json = flags.json;
|
|
181
|
+
const meta = parseMetaFlags(argv); // --help/--version (not part of CliFlags)
|
|
182
|
+
if (meta.help) {
|
|
183
|
+
process.stdout.write(helpText() + "\n");
|
|
184
|
+
return EXIT.SUCCESS;
|
|
185
|
+
}
|
|
186
|
+
if (meta.version) {
|
|
187
|
+
process.stdout.write(readInstallerVersion() + "\n");
|
|
188
|
+
return EXIT.SUCCESS;
|
|
189
|
+
}
|
|
190
|
+
if (!hadSubcommand(argv)) {
|
|
191
|
+
process.stderr.write("error: no subcommand given.\n\n");
|
|
192
|
+
process.stderr.write(helpText() + "\n");
|
|
193
|
+
return EXIT.USAGE;
|
|
194
|
+
}
|
|
195
|
+
report = await runCli(argv, env);
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
// Boundary catch: an UNEXPECTED exception must never surface as a bare stack alone
|
|
199
|
+
// (tech-spec §7). Print a one-line actionable message and exit 1.
|
|
200
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
201
|
+
process.stderr.write(`error: unexpected failure: ${msg}\n`);
|
|
202
|
+
return EXIT.FAILURE;
|
|
203
|
+
}
|
|
204
|
+
process.stdout.write(renderReport(report, { json }) + "\n");
|
|
205
|
+
return report.exitCode;
|
|
206
|
+
}
|
|
207
|
+
/** Raw parseArgs over the single FLAGS spec (shared `buildParseOptions`); null if argv is malformed. */
|
|
208
|
+
function rawParse(argv) {
|
|
209
|
+
try {
|
|
210
|
+
return parseArgs({ args: argv, options: buildParseOptions(), allowPositionals: true });
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/** Read the `--help`/`--version` booleans (recognized before subcommand validation, §1.4). */
|
|
217
|
+
function parseMetaFlags(argv) {
|
|
218
|
+
const parsed = rawParse(argv);
|
|
219
|
+
const values = (parsed?.values ?? {});
|
|
220
|
+
return { help: values.help === true, version: values.version === true };
|
|
221
|
+
}
|
|
222
|
+
/** True iff `argv` carries a leading positional (a subcommand token), per parseArgs. */
|
|
223
|
+
function hadSubcommand(argv) {
|
|
224
|
+
const parsed = rawParse(argv);
|
|
225
|
+
return parsed !== null && parsed.positionals.length > 0;
|
|
226
|
+
}
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
// 5. runMutation — install / update / uninstall (§3.2)
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
/** Orchestrate a mutating run (install | update | uninstall). Never throws for expected errors. */
|
|
231
|
+
async function runMutation(subcommand, flags, env) {
|
|
232
|
+
const scope = flags.global ? "global" : "project";
|
|
233
|
+
const mode = resolveMode(flags.symlink, (env.platform ?? process.platform) === "win32");
|
|
234
|
+
const ropts = { home: env.home, cwd: env.cwd, scope };
|
|
235
|
+
const targets = flags.agent
|
|
236
|
+
? [flags.agent]
|
|
237
|
+
: detectAgents(ropts).filter((d) => d.detected).map((d) => d.agent);
|
|
238
|
+
let raufPin = flags.skipRauf ? null : RAUF_PIN;
|
|
239
|
+
let raufError;
|
|
240
|
+
// Rauf preflight: install/update only, once, network only when not dry-run/skip AND there is at
|
|
241
|
+
// least one target (zero detected ⇒ nothing to do, so no network query — DET-04, REQ-PERF-01).
|
|
242
|
+
if (targets.length > 0 &&
|
|
243
|
+
(subcommand === "install" || subcommand === "update") &&
|
|
244
|
+
!flags.skipRauf &&
|
|
245
|
+
!flags.dryRun) {
|
|
246
|
+
const pf = preflightRauf({ skip: flags.skipRauf, query: env.registry });
|
|
247
|
+
if (!pf.ok) {
|
|
248
|
+
raufError = pf.error; // RAUF_UNRESOLVABLE — recorded, does NOT abort skill installs
|
|
249
|
+
raufPin = null; // not resolvable ⇒ no usable pin recorded this run
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const agentReports = [];
|
|
253
|
+
for (const agent of targets) {
|
|
254
|
+
const detection = detectAgent(agent, ropts);
|
|
255
|
+
const r = await runOneAgent(subcommand, agent, detection, flags, scope, mode, raufPin, env);
|
|
256
|
+
agentReports.push(r);
|
|
257
|
+
}
|
|
258
|
+
const anyAgentFailed = agentReports.some((r) => !r.ok);
|
|
259
|
+
const exitCode = anyAgentFailed || raufError !== undefined ? EXIT.FAILURE : EXIT.SUCCESS;
|
|
260
|
+
// NOTE (spec 07 §3.2): the `attachRaufError(reports, raufError)` hook is intentionally elided in
|
|
261
|
+
// favor of the sanctioned run-level `RunReport.raufError` field (a §3.2 MAY). renderReport surfaces
|
|
262
|
+
// it and it rides the `--json` machine surface (REQ-DET-05); there is no separate attach step.
|
|
263
|
+
return {
|
|
264
|
+
subcommand,
|
|
265
|
+
scope,
|
|
266
|
+
mode,
|
|
267
|
+
dryRun: flags.dryRun,
|
|
268
|
+
agents: agentReports,
|
|
269
|
+
exitCode,
|
|
270
|
+
...(raufError ? { raufError } : {}),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/** Run the pipeline for a single agent, returning its AgentReport (catches every expected error). */
|
|
274
|
+
async function runOneAgent(subcommand, agent, detection, flags, scope, mode, raufPin, env) {
|
|
275
|
+
const mpath = manifestPath(agent, scope, { home: env.home, cwd: env.cwd });
|
|
276
|
+
const roots = resolveRoots({ home: env.home, cwd: env.cwd, scope });
|
|
277
|
+
const scopeRoot = scope === "global" ? roots.home : roots.cwd;
|
|
278
|
+
const agentRoot = path.join(scopeRoot, AGENT_TARGETS[agent].configDirName);
|
|
279
|
+
// uninstall path: manifest → planUninstall → apply.
|
|
280
|
+
if (subcommand === "uninstall") {
|
|
281
|
+
const m = readManifest(mpath);
|
|
282
|
+
if (!m.ok)
|
|
283
|
+
return failed(agent, detection.detected, m.error);
|
|
284
|
+
if (m.value === null) {
|
|
285
|
+
// Nothing installed for this agent: not an error — an "ok, no-op" report.
|
|
286
|
+
return { agent, detected: detection.detected, ok: true, actions: [], raufPin: null };
|
|
287
|
+
}
|
|
288
|
+
const rp = planUninstall(m.value);
|
|
289
|
+
if (!rp.ok)
|
|
290
|
+
return failed(agent, detection.detected, rp.error);
|
|
291
|
+
const ctx = {
|
|
292
|
+
agent,
|
|
293
|
+
scope,
|
|
294
|
+
mode: m.value.mode,
|
|
295
|
+
agentRoot,
|
|
296
|
+
destination: m.value.destination,
|
|
297
|
+
manifestPath: mpath,
|
|
298
|
+
source: null,
|
|
299
|
+
raufPin: null,
|
|
300
|
+
now: new Date().toISOString(),
|
|
301
|
+
priorManifest: m.value,
|
|
302
|
+
};
|
|
303
|
+
return finishAgent(agent, detection.detected, rp.value, flags, raufPin, ctx);
|
|
304
|
+
}
|
|
305
|
+
// install/update path: locate+integrity+fingerprint → readManifest → plan → apply.
|
|
306
|
+
const located = locateSource(agent, { source: flags.source });
|
|
307
|
+
if (!located.ok)
|
|
308
|
+
return failed(agent, detection.detected, located.error);
|
|
309
|
+
const prior = readManifest(mpath);
|
|
310
|
+
if (!prior.ok)
|
|
311
|
+
return failed(agent, detection.detected, prior.error);
|
|
312
|
+
const planCtx = {
|
|
313
|
+
agent,
|
|
314
|
+
scope,
|
|
315
|
+
mode,
|
|
316
|
+
destination: detection.destination,
|
|
317
|
+
source: located.value,
|
|
318
|
+
priorManifest: prior.value,
|
|
319
|
+
force: flags.force,
|
|
320
|
+
raufPin,
|
|
321
|
+
};
|
|
322
|
+
const planned = plan(subcommand, planCtx);
|
|
323
|
+
if (!planned.ok)
|
|
324
|
+
return failed(agent, detection.detected, planned.error);
|
|
325
|
+
const ctx = {
|
|
326
|
+
agent,
|
|
327
|
+
scope,
|
|
328
|
+
mode,
|
|
329
|
+
agentRoot,
|
|
330
|
+
destination: detection.destination,
|
|
331
|
+
manifestPath: mpath,
|
|
332
|
+
source: located.value,
|
|
333
|
+
raufPin,
|
|
334
|
+
now: new Date().toISOString(),
|
|
335
|
+
priorManifest: prior.value,
|
|
336
|
+
};
|
|
337
|
+
return finishAgent(agent, detection.detected, planned.value, flags, raufPin, ctx);
|
|
338
|
+
}
|
|
339
|
+
/** Apply a plan unless --dry-run; build the agent's report either way. */
|
|
340
|
+
async function finishAgent(agent, detected, planned, flags, raufPin, ctx) {
|
|
341
|
+
if (flags.dryRun) {
|
|
342
|
+
// Plan only: the actions shown are exactly what a real run performs (REQ-OPS-05). No writes.
|
|
343
|
+
return { agent, detected, ok: true, actions: planned.files, raufPin };
|
|
344
|
+
}
|
|
345
|
+
const report = await apply(planned, ctx);
|
|
346
|
+
if (!report.ok)
|
|
347
|
+
return failed(agent, detected, report.error ?? unexpected(agent));
|
|
348
|
+
return { agent, detected, ok: true, actions: report.actions, raufPin };
|
|
349
|
+
}
|
|
350
|
+
/** A failed single-agent report (REQ-OBS-03): ok:false + the structured error. */
|
|
351
|
+
function failed(agent, detected, error) {
|
|
352
|
+
return { agent, detected, ok: false, actions: [], error };
|
|
353
|
+
}
|
|
354
|
+
/** Fallback error if apply reports ok:false with no attached error (defensive). */
|
|
355
|
+
function unexpected(agent) {
|
|
356
|
+
return { code: "UNEXPECTED", agent, message: `apply for "${agent}" failed without an error` };
|
|
357
|
+
}
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
// 6. runList — read-only list/ls (§3.3, REQ-OPS-04, REQ-PERF-01)
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
/** Orchestrate the read-only `list` operation: no network, no apply, no writes. */
|
|
362
|
+
async function runList(flags, env) {
|
|
363
|
+
const scope = flags.global ? "global" : "project";
|
|
364
|
+
const ropts = { home: env.home, cwd: env.cwd, scope };
|
|
365
|
+
const targets = flags.agent ? [flags.agent] : [...AGENT_IDS];
|
|
366
|
+
const agentReports = [];
|
|
367
|
+
for (const agent of targets) {
|
|
368
|
+
const detection = detectAgent(agent, ropts);
|
|
369
|
+
agentReports.push(listOneAgent(agent, detection, flags, scope, env));
|
|
370
|
+
}
|
|
371
|
+
const anyFailed = agentReports.some((r) => !r.ok);
|
|
372
|
+
return {
|
|
373
|
+
subcommand: "list",
|
|
374
|
+
scope,
|
|
375
|
+
mode: "copy", // mode is irrelevant for list; report the default for shape stability
|
|
376
|
+
dryRun: false,
|
|
377
|
+
agents: agentReports,
|
|
378
|
+
exitCode: anyFailed ? EXIT.FAILURE : EXIT.SUCCESS,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
/** Compute one agent's list status without any write or network call (REQ-PERF-01). */
|
|
382
|
+
function listOneAgent(agent, detection, flags, scope, env) {
|
|
383
|
+
const mpath = manifestPath(agent, scope, { home: env.home, cwd: env.cwd });
|
|
384
|
+
const m = readManifest(mpath);
|
|
385
|
+
if (!m.ok)
|
|
386
|
+
return failed(agent, detection.detected, m.error);
|
|
387
|
+
const installed = m.value !== null;
|
|
388
|
+
// Status is carried as synthetic FileAction rows the renderer decodes (status, not file writes):
|
|
389
|
+
const statusActions = [
|
|
390
|
+
{ relpath: `detected:${detection.detected}`, action: "unchanged" },
|
|
391
|
+
{ relpath: `installed:${installed}`, action: "unchanged" },
|
|
392
|
+
];
|
|
393
|
+
if (installed && m.value !== null) {
|
|
394
|
+
const located = locateSource(agent, { source: flags.source });
|
|
395
|
+
if (located.ok) {
|
|
396
|
+
const upToDate = located.value.sourceHash === m.value.sourceHash;
|
|
397
|
+
statusActions.push({ relpath: `up-to-date:${upToDate}`, action: "unchanged" });
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
statusActions.push({ relpath: "up-to-date:unknown(source-missing)", action: "unchanged" });
|
|
401
|
+
}
|
|
402
|
+
// Destination drift (REQ-SAFE-03, §5.13 list half): a copy-mode install is "drifted" when any
|
|
403
|
+
// manifest-recorded file's bytes on disk no longer match its recorded sha256 — a local user
|
|
404
|
+
// edit, independent of whether the SOURCE changed. Reads ONLY the manifest (per-file sha256)
|
|
405
|
+
// against a fresh local hash (no network, no source needed). Symlink mode has no per-file
|
|
406
|
+
// sha256 to compare, so drift is reported as not-applicable.
|
|
407
|
+
statusActions.push({ relpath: `drift:${detectDestinationDrift(m.value)}`, action: "unchanged" });
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
agent,
|
|
411
|
+
detected: detection.detected,
|
|
412
|
+
ok: true,
|
|
413
|
+
actions: statusActions,
|
|
414
|
+
raufPin: m.value?.raufPin ?? null,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Return `"true"` if any manifest-recorded file's on-disk bytes differ from its recorded sha256
|
|
419
|
+
* (a locally-modified destination, REQ-SAFE-03), `"false"` if every recorded file matches, or
|
|
420
|
+
* `"n/a(symlink)"` for a symlink-mode install (no per-file sha256 to compare). A missing recorded
|
|
421
|
+
* file also counts as drift. Pure read of the manifest's per-file sha256 against a fresh local
|
|
422
|
+
* hash — no network, no source bundle needed (REQ-PERF-01). Hash errors are swallowed as drift
|
|
423
|
+
* (an unreadable recorded file is itself a deviation from the clean install).
|
|
424
|
+
*/
|
|
425
|
+
function detectDestinationDrift(manifest) {
|
|
426
|
+
if (manifest.mode === "symlink")
|
|
427
|
+
return "n/a(symlink)";
|
|
428
|
+
for (const f of manifest.files) {
|
|
429
|
+
if (f.sha256 === undefined)
|
|
430
|
+
continue; // no recorded hash to compare against
|
|
431
|
+
try {
|
|
432
|
+
if (sha256File(path.join(manifest.destination, f.path)) !== f.sha256)
|
|
433
|
+
return "true";
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
return "true"; // unreadable/absent recorded file ⇒ drift
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return "false";
|
|
440
|
+
}
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
// 7. helpText and --version (§3.4)
|
|
443
|
+
// ---------------------------------------------------------------------------
|
|
444
|
+
/**
|
|
445
|
+
* Build the full `--help` text from the single CLI_SPEC (SUBCOMMANDS + FLAGS, §1.5) so the
|
|
446
|
+
* listed surface can never drift from what parseArgs accepts (REQ-DIST-03). Hidden flags
|
|
447
|
+
* (--source) are omitted. Pure: returns a string, no I/O.
|
|
448
|
+
*/
|
|
449
|
+
export function helpText() {
|
|
450
|
+
const lines = [];
|
|
451
|
+
lines.push("feature-forge — cross-agent installer for the feature-forge skill suite");
|
|
452
|
+
lines.push("");
|
|
453
|
+
lines.push("USAGE:");
|
|
454
|
+
lines.push(" feature-forge <command> [flags]");
|
|
455
|
+
lines.push("");
|
|
456
|
+
lines.push("COMMANDS:");
|
|
457
|
+
for (const s of SUBCOMMANDS) {
|
|
458
|
+
const alias = s.aliases.length ? ` (alias: ${s.aliases.join(", ")})` : "";
|
|
459
|
+
lines.push(` ${s.canonical.padEnd(10)} ${s.help}${alias}`);
|
|
460
|
+
}
|
|
461
|
+
lines.push("");
|
|
462
|
+
lines.push("FLAGS:");
|
|
463
|
+
for (const f of FLAGS) {
|
|
464
|
+
if (f.hidden)
|
|
465
|
+
continue;
|
|
466
|
+
const long = `--${f.name}${f.arg ? " " + f.arg : ""}`;
|
|
467
|
+
const short = f.short ? `-${f.short}, ` : " ";
|
|
468
|
+
lines.push(` ${short}${long.padEnd(18)} ${f.help}`);
|
|
469
|
+
}
|
|
470
|
+
lines.push("");
|
|
471
|
+
lines.push("EXAMPLES:");
|
|
472
|
+
lines.push(" npx feature-forge install # install into all detected agents (project scope)");
|
|
473
|
+
lines.push(" npx feature-forge install -a claude -g # install into ~/.claude only");
|
|
474
|
+
lines.push(" npx feature-forge update --dry-run # preview an update, change nothing");
|
|
475
|
+
lines.push(" npx feature-forge list --json # machine-readable per-agent status");
|
|
476
|
+
lines.push(" npx feature-forge uninstall -a cursor # remove the cursor install (manifest-tracked only)");
|
|
477
|
+
return lines.join("\n");
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Read the installer package's own version from the bundled package.json (REQ-DIST-03). Resolved
|
|
481
|
+
* relative to the compiled module via `import.meta.url` so it works when run via `npx`. On any
|
|
482
|
+
* read error, fall back to "unknown" (never throw from --version).
|
|
483
|
+
*/
|
|
484
|
+
function readInstallerVersion() {
|
|
485
|
+
try {
|
|
486
|
+
const url = new URL("../package.json", import.meta.url);
|
|
487
|
+
const pkg = JSON.parse(readFileSync(url, "utf8"));
|
|
488
|
+
return typeof pkg.version === "string" ? pkg.version : "unknown";
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
return "unknown";
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
// ---------------------------------------------------------------------------
|
|
495
|
+
// 8. Process entry shim
|
|
496
|
+
// ---------------------------------------------------------------------------
|
|
497
|
+
// Only run when invoked as the bin (not when imported by tests / index.ts).
|
|
498
|
+
const entry = process.argv[1];
|
|
499
|
+
if (entry !== undefined && import.meta.url === pathToFileURL(entry).href) {
|
|
500
|
+
main(process.argv.slice(2))
|
|
501
|
+
.then((code) => {
|
|
502
|
+
process.exitCode = code;
|
|
503
|
+
})
|
|
504
|
+
.catch((e) => {
|
|
505
|
+
process.stderr.write(`error: fatal: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
506
|
+
process.exitCode = EXIT.FAILURE;
|
|
507
|
+
});
|
|
508
|
+
}
|
package/dist/detect.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal detection probes for the agent-detection-map surface (spec 02 §5.1, §5.3).
|
|
3
|
+
*
|
|
4
|
+
* Two probes, both total and non-throwing:
|
|
5
|
+
* - `probeConfigDir` — the PRIMARY detection signal: a single `fs.statSync`. Never mkdir,
|
|
6
|
+
* never throws (REQ-DET-02/04, REQ-PERF-01).
|
|
7
|
+
* - `cliOnPath` — SECONDARY, advisory only: is the agent's CLI on PATH? Never gates
|
|
8
|
+
* `detected` (REQ-DET-02). `cursor` is intentionally omitted (IDE/GUI agent, no CLI).
|
|
9
|
+
*
|
|
10
|
+
* Zero runtime dependencies; only `node:` built-ins.
|
|
11
|
+
*/
|
|
12
|
+
import type { AgentId } from "./types.js";
|
|
13
|
+
/**
|
|
14
|
+
* The primary detection signal (REQ-DET-02): is `configDir` an existing directory?
|
|
15
|
+
* Uses a single synchronous `fs.statSync` — never an agent subprocess (detection stays
|
|
16
|
+
* instant, REQ-PERF-01) and never creates the dir (REQ-DET-04). Any stat failure
|
|
17
|
+
* (`ENOENT` not present, `EACCES` unreadable, or a non-directory at the path) ⇒ `false`.
|
|
18
|
+
*
|
|
19
|
+
* Synchronous by design: exactly one stat per agent (five total), so async adds no
|
|
20
|
+
* throughput and would complicate the pure-derivation surface.
|
|
21
|
+
*/
|
|
22
|
+
export declare function probeConfigDir(configDir: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* The PATH-resolver seam: given a binary basename, return true iff it resolves on PATH.
|
|
25
|
+
* Default uses the platform's real resolver executable (`where` on Windows, `which` on POSIX) —
|
|
26
|
+
* NOT the shell builtin `command`, which is not an executable on PATH and so always throws
|
|
27
|
+
* ENOENT under `execFileSync` (no shell). Injectable so tests can drive present/absent without
|
|
28
|
+
* depending on the host PATH.
|
|
29
|
+
*/
|
|
30
|
+
export type PathResolver = (bin: string) => boolean;
|
|
31
|
+
/** The real resolver: `which <bin>` (POSIX) / `where <bin>` (Windows). Any failure ⇒ false. */
|
|
32
|
+
export declare const realPathResolver: PathResolver;
|
|
33
|
+
/**
|
|
34
|
+
* Secondary, **advisory** info (REQ-DET-02): is the agent's CLI resolvable on PATH? This is
|
|
35
|
+
* reported as `DetectionResult.cliOnPath` but **never** gates `detected`. Uses the platform's
|
|
36
|
+
* resolver executable (`where` on Windows, `which` elsewhere — a real binary on PATH, not the
|
|
37
|
+
* shell builtin `command`) once per agent. Any failure — no resolver, not found, spawn error —
|
|
38
|
+
* yields `false`; it never throws and never blocks detection.
|
|
39
|
+
*
|
|
40
|
+
* Agents without a canonical CLI (cursor) always report `false` here without spawning.
|
|
41
|
+
*
|
|
42
|
+
* @param id - the agent whose CLI basename to probe.
|
|
43
|
+
* @param resolve - the PATH resolver seam (default: {@link realPathResolver}); injectable for tests.
|
|
44
|
+
*/
|
|
45
|
+
export declare function cliOnPath(id: AgentId, resolve?: PathResolver): boolean;
|
package/dist/detect.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal detection probes for the agent-detection-map surface (spec 02 §5.1, §5.3).
|
|
3
|
+
*
|
|
4
|
+
* Two probes, both total and non-throwing:
|
|
5
|
+
* - `probeConfigDir` — the PRIMARY detection signal: a single `fs.statSync`. Never mkdir,
|
|
6
|
+
* never throws (REQ-DET-02/04, REQ-PERF-01).
|
|
7
|
+
* - `cliOnPath` — SECONDARY, advisory only: is the agent's CLI on PATH? Never gates
|
|
8
|
+
* `detected` (REQ-DET-02). `cursor` is intentionally omitted (IDE/GUI agent, no CLI).
|
|
9
|
+
*
|
|
10
|
+
* Zero runtime dependencies; only `node:` built-ins.
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import { execFileSync } from "node:child_process";
|
|
14
|
+
/**
|
|
15
|
+
* The primary detection signal (REQ-DET-02): is `configDir` an existing directory?
|
|
16
|
+
* Uses a single synchronous `fs.statSync` — never an agent subprocess (detection stays
|
|
17
|
+
* instant, REQ-PERF-01) and never creates the dir (REQ-DET-04). Any stat failure
|
|
18
|
+
* (`ENOENT` not present, `EACCES` unreadable, or a non-directory at the path) ⇒ `false`.
|
|
19
|
+
*
|
|
20
|
+
* Synchronous by design: exactly one stat per agent (five total), so async adds no
|
|
21
|
+
* throughput and would complicate the pure-derivation surface.
|
|
22
|
+
*/
|
|
23
|
+
export function probeConfigDir(configDir) {
|
|
24
|
+
try {
|
|
25
|
+
return fs.statSync(configDir).isDirectory();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false; // ENOENT / EACCES / not-a-dir → not detected (never throws, REQ-DET-04)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Per-agent CLI executable basename probed on PATH (advisory only). */
|
|
32
|
+
const CLI_NAMES = {
|
|
33
|
+
claude: "claude",
|
|
34
|
+
codex: "codex",
|
|
35
|
+
copilot: "copilot",
|
|
36
|
+
gemini: "gemini",
|
|
37
|
+
// cursor: intentionally omitted — IDE/GUI agent, no canonical CLI on PATH (REQ-DET-02).
|
|
38
|
+
};
|
|
39
|
+
/** The real resolver: `which <bin>` (POSIX) / `where <bin>` (Windows). Any failure ⇒ false. */
|
|
40
|
+
export const realPathResolver = (bin) => {
|
|
41
|
+
const isWin = process.platform === "win32";
|
|
42
|
+
try {
|
|
43
|
+
execFileSync(isWin ? "where" : "which", [bin], { stdio: "ignore" });
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false; // not found / no resolver / spawn error — advisory, never an error
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Secondary, **advisory** info (REQ-DET-02): is the agent's CLI resolvable on PATH? This is
|
|
52
|
+
* reported as `DetectionResult.cliOnPath` but **never** gates `detected`. Uses the platform's
|
|
53
|
+
* resolver executable (`where` on Windows, `which` elsewhere — a real binary on PATH, not the
|
|
54
|
+
* shell builtin `command`) once per agent. Any failure — no resolver, not found, spawn error —
|
|
55
|
+
* yields `false`; it never throws and never blocks detection.
|
|
56
|
+
*
|
|
57
|
+
* Agents without a canonical CLI (cursor) always report `false` here without spawning.
|
|
58
|
+
*
|
|
59
|
+
* @param id - the agent whose CLI basename to probe.
|
|
60
|
+
* @param resolve - the PATH resolver seam (default: {@link realPathResolver}); injectable for tests.
|
|
61
|
+
*/
|
|
62
|
+
export function cliOnPath(id, resolve = realPathResolver) {
|
|
63
|
+
const bin = CLI_NAMES[id];
|
|
64
|
+
if (!bin)
|
|
65
|
+
return false;
|
|
66
|
+
try {
|
|
67
|
+
return resolve(bin);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false; // advisory; absence is normal, never an error
|
|
71
|
+
}
|
|
72
|
+
}
|