@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.
Files changed (231) hide show
  1. package/LICENSE +21 -0
  2. package/adapters/GENERATION-REPORT.md +128 -0
  3. package/adapters/claude/agents/forge-researcher.md +137 -0
  4. package/adapters/claude/agents/forge-spec-writer.md +115 -0
  5. package/adapters/claude/agents/forge-verifier.md +121 -0
  6. package/adapters/claude/references/epic-manifest-schema.json +120 -0
  7. package/adapters/claude/references/forge-config-schema.json +166 -0
  8. package/adapters/claude/references/pipeline-state-schema.json +110 -0
  9. package/adapters/claude/references/portable-root.md +56 -0
  10. package/adapters/claude/references/process-overview.md +123 -0
  11. package/adapters/claude/references/ralph-loop-contract.md +221 -0
  12. package/adapters/claude/references/shared-conventions.md +144 -0
  13. package/adapters/claude/references/skill-frontmatter.schema.json +17 -0
  14. package/adapters/claude/references/stack-resolution.md +51 -0
  15. package/adapters/claude/references/stacks/_generic.md +90 -0
  16. package/adapters/claude/references/stacks/go.md +138 -0
  17. package/adapters/claude/references/stacks/python.md +163 -0
  18. package/adapters/claude/references/stacks/rust.md +151 -0
  19. package/adapters/claude/references/stacks/typescript.md +111 -0
  20. package/adapters/claude/references/vendor-construct-inventory.md +49 -0
  21. package/adapters/claude/scripts/forge-root.sh +50 -0
  22. package/adapters/claude/skills/forge/SKILL.md +165 -0
  23. package/adapters/claude/skills/forge-0-epic/SKILL.md +303 -0
  24. package/adapters/claude/skills/forge-0-epic/references/edit-mode.md +222 -0
  25. package/adapters/claude/skills/forge-0-epic/references/epic-manifest-subcommands.md +64 -0
  26. package/adapters/claude/skills/forge-1-prd/SKILL.md +121 -0
  27. package/adapters/claude/skills/forge-1-prd/references/prd-template.md +106 -0
  28. package/adapters/claude/skills/forge-2-tech/SKILL.md +198 -0
  29. package/adapters/claude/skills/forge-2-tech/references/stack-discovery-checklist.md +95 -0
  30. package/adapters/claude/skills/forge-3-specs/SKILL.md +154 -0
  31. package/adapters/claude/skills/forge-3-specs/references/spec-archetypes.md +106 -0
  32. package/adapters/claude/skills/forge-3-specs/references/spec-examples.md +71 -0
  33. package/adapters/claude/skills/forge-4-backlog/SKILL.md +146 -0
  34. package/adapters/claude/skills/forge-5-loop/SKILL.md +303 -0
  35. package/adapters/claude/skills/forge-5-loop/references/result-reporting.md +63 -0
  36. package/adapters/claude/skills/forge-5-loop/references/runner-contract.md +214 -0
  37. package/adapters/claude/skills/forge-6-docs/SKILL.md +179 -0
  38. package/adapters/claude/skills/forge-6-docs/references/doc-conventions.md +126 -0
  39. package/adapters/claude/skills/forge-fix/SKILL.md +65 -0
  40. package/adapters/claude/skills/forge-init/SKILL.md +29 -0
  41. package/adapters/claude/skills/forge-verify/SKILL.md +219 -0
  42. package/adapters/claude/skills/forge-verify/references/verification-checklists.md +379 -0
  43. package/adapters/codex/agents/forge-researcher.md +133 -0
  44. package/adapters/codex/agents/forge-spec-writer.md +112 -0
  45. package/adapters/codex/agents/forge-verifier.md +115 -0
  46. package/adapters/codex/agents/openai.yaml +10 -0
  47. package/adapters/codex/references/epic-manifest-schema.json +120 -0
  48. package/adapters/codex/references/forge-config-schema.json +166 -0
  49. package/adapters/codex/references/pipeline-state-schema.json +110 -0
  50. package/adapters/codex/references/portable-root.md +56 -0
  51. package/adapters/codex/references/process-overview.md +123 -0
  52. package/adapters/codex/references/ralph-loop-contract.md +221 -0
  53. package/adapters/codex/references/shared-conventions.md +144 -0
  54. package/adapters/codex/references/skill-frontmatter.schema.json +17 -0
  55. package/adapters/codex/references/stack-resolution.md +51 -0
  56. package/adapters/codex/references/stacks/_generic.md +90 -0
  57. package/adapters/codex/references/stacks/go.md +138 -0
  58. package/adapters/codex/references/stacks/python.md +163 -0
  59. package/adapters/codex/references/stacks/rust.md +151 -0
  60. package/adapters/codex/references/stacks/typescript.md +111 -0
  61. package/adapters/codex/references/vendor-construct-inventory.md +49 -0
  62. package/adapters/codex/scripts/forge-root.sh +50 -0
  63. package/adapters/codex/skills/forge/forge.md +164 -0
  64. package/adapters/codex/skills/forge-0-epic/forge-0-epic.md +302 -0
  65. package/adapters/codex/skills/forge-0-epic/references/edit-mode.md +222 -0
  66. package/adapters/codex/skills/forge-0-epic/references/epic-manifest-subcommands.md +64 -0
  67. package/adapters/codex/skills/forge-1-prd/forge-1-prd.md +120 -0
  68. package/adapters/codex/skills/forge-1-prd/references/prd-template.md +106 -0
  69. package/adapters/codex/skills/forge-2-tech/forge-2-tech.md +197 -0
  70. package/adapters/codex/skills/forge-2-tech/references/stack-discovery-checklist.md +95 -0
  71. package/adapters/codex/skills/forge-3-specs/forge-3-specs.md +153 -0
  72. package/adapters/codex/skills/forge-3-specs/references/spec-archetypes.md +106 -0
  73. package/adapters/codex/skills/forge-3-specs/references/spec-examples.md +71 -0
  74. package/adapters/codex/skills/forge-4-backlog/forge-4-backlog.md +145 -0
  75. package/adapters/codex/skills/forge-5-loop/forge-5-loop.md +302 -0
  76. package/adapters/codex/skills/forge-5-loop/references/result-reporting.md +63 -0
  77. package/adapters/codex/skills/forge-5-loop/references/runner-contract.md +214 -0
  78. package/adapters/codex/skills/forge-6-docs/forge-6-docs.md +178 -0
  79. package/adapters/codex/skills/forge-6-docs/references/doc-conventions.md +126 -0
  80. package/adapters/codex/skills/forge-fix/forge-fix.md +64 -0
  81. package/adapters/codex/skills/forge-init/forge-init.md +29 -0
  82. package/adapters/codex/skills/forge-verify/forge-verify.md +218 -0
  83. package/adapters/codex/skills/forge-verify/references/verification-checklists.md +379 -0
  84. package/adapters/copilot/agents/forge-researcher.md +133 -0
  85. package/adapters/copilot/agents/forge-spec-writer.md +112 -0
  86. package/adapters/copilot/agents/forge-verifier.md +115 -0
  87. package/adapters/copilot/references/epic-manifest-schema.json +120 -0
  88. package/adapters/copilot/references/forge-config-schema.json +166 -0
  89. package/adapters/copilot/references/pipeline-state-schema.json +110 -0
  90. package/adapters/copilot/references/portable-root.md +56 -0
  91. package/adapters/copilot/references/process-overview.md +123 -0
  92. package/adapters/copilot/references/ralph-loop-contract.md +221 -0
  93. package/adapters/copilot/references/shared-conventions.md +144 -0
  94. package/adapters/copilot/references/skill-frontmatter.schema.json +17 -0
  95. package/adapters/copilot/references/stack-resolution.md +51 -0
  96. package/adapters/copilot/references/stacks/_generic.md +90 -0
  97. package/adapters/copilot/references/stacks/go.md +138 -0
  98. package/adapters/copilot/references/stacks/python.md +163 -0
  99. package/adapters/copilot/references/stacks/rust.md +151 -0
  100. package/adapters/copilot/references/stacks/typescript.md +111 -0
  101. package/adapters/copilot/references/vendor-construct-inventory.md +49 -0
  102. package/adapters/copilot/scripts/forge-root.sh +50 -0
  103. package/adapters/copilot/skills/forge/forge.md +164 -0
  104. package/adapters/copilot/skills/forge-0-epic/forge-0-epic.md +302 -0
  105. package/adapters/copilot/skills/forge-0-epic/references/edit-mode.md +222 -0
  106. package/adapters/copilot/skills/forge-0-epic/references/epic-manifest-subcommands.md +64 -0
  107. package/adapters/copilot/skills/forge-1-prd/forge-1-prd.md +120 -0
  108. package/adapters/copilot/skills/forge-1-prd/references/prd-template.md +106 -0
  109. package/adapters/copilot/skills/forge-2-tech/forge-2-tech.md +197 -0
  110. package/adapters/copilot/skills/forge-2-tech/references/stack-discovery-checklist.md +95 -0
  111. package/adapters/copilot/skills/forge-3-specs/forge-3-specs.md +153 -0
  112. package/adapters/copilot/skills/forge-3-specs/references/spec-archetypes.md +106 -0
  113. package/adapters/copilot/skills/forge-3-specs/references/spec-examples.md +71 -0
  114. package/adapters/copilot/skills/forge-4-backlog/forge-4-backlog.md +145 -0
  115. package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +302 -0
  116. package/adapters/copilot/skills/forge-5-loop/references/result-reporting.md +63 -0
  117. package/adapters/copilot/skills/forge-5-loop/references/runner-contract.md +214 -0
  118. package/adapters/copilot/skills/forge-6-docs/forge-6-docs.md +178 -0
  119. package/adapters/copilot/skills/forge-6-docs/references/doc-conventions.md +126 -0
  120. package/adapters/copilot/skills/forge-fix/forge-fix.md +64 -0
  121. package/adapters/copilot/skills/forge-init/forge-init.md +29 -0
  122. package/adapters/copilot/skills/forge-verify/forge-verify.md +218 -0
  123. package/adapters/copilot/skills/forge-verify/references/verification-checklists.md +379 -0
  124. package/adapters/cursor/agents/forge-researcher.mdc +134 -0
  125. package/adapters/cursor/agents/forge-spec-writer.mdc +113 -0
  126. package/adapters/cursor/agents/forge-verifier.mdc +116 -0
  127. package/adapters/cursor/references/epic-manifest-schema.json +120 -0
  128. package/adapters/cursor/references/forge-config-schema.json +166 -0
  129. package/adapters/cursor/references/pipeline-state-schema.json +110 -0
  130. package/adapters/cursor/references/portable-root.md +56 -0
  131. package/adapters/cursor/references/process-overview.md +123 -0
  132. package/adapters/cursor/references/ralph-loop-contract.md +221 -0
  133. package/adapters/cursor/references/shared-conventions.md +144 -0
  134. package/adapters/cursor/references/skill-frontmatter.schema.json +17 -0
  135. package/adapters/cursor/references/stack-resolution.md +51 -0
  136. package/adapters/cursor/references/stacks/_generic.md +90 -0
  137. package/adapters/cursor/references/stacks/go.md +138 -0
  138. package/adapters/cursor/references/stacks/python.md +163 -0
  139. package/adapters/cursor/references/stacks/rust.md +151 -0
  140. package/adapters/cursor/references/stacks/typescript.md +111 -0
  141. package/adapters/cursor/references/vendor-construct-inventory.md +49 -0
  142. package/adapters/cursor/scripts/forge-root.sh +50 -0
  143. package/adapters/cursor/skills/forge/forge.mdc +165 -0
  144. package/adapters/cursor/skills/forge-0-epic/forge-0-epic.mdc +303 -0
  145. package/adapters/cursor/skills/forge-0-epic/references/edit-mode.md +222 -0
  146. package/adapters/cursor/skills/forge-0-epic/references/epic-manifest-subcommands.md +64 -0
  147. package/adapters/cursor/skills/forge-1-prd/forge-1-prd.mdc +121 -0
  148. package/adapters/cursor/skills/forge-1-prd/references/prd-template.md +106 -0
  149. package/adapters/cursor/skills/forge-2-tech/forge-2-tech.mdc +198 -0
  150. package/adapters/cursor/skills/forge-2-tech/references/stack-discovery-checklist.md +95 -0
  151. package/adapters/cursor/skills/forge-3-specs/forge-3-specs.mdc +154 -0
  152. package/adapters/cursor/skills/forge-3-specs/references/spec-archetypes.md +106 -0
  153. package/adapters/cursor/skills/forge-3-specs/references/spec-examples.md +71 -0
  154. package/adapters/cursor/skills/forge-4-backlog/forge-4-backlog.mdc +146 -0
  155. package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +303 -0
  156. package/adapters/cursor/skills/forge-5-loop/references/result-reporting.md +63 -0
  157. package/adapters/cursor/skills/forge-5-loop/references/runner-contract.md +214 -0
  158. package/adapters/cursor/skills/forge-6-docs/forge-6-docs.mdc +179 -0
  159. package/adapters/cursor/skills/forge-6-docs/references/doc-conventions.md +126 -0
  160. package/adapters/cursor/skills/forge-fix/forge-fix.mdc +65 -0
  161. package/adapters/cursor/skills/forge-init/forge-init.mdc +30 -0
  162. package/adapters/cursor/skills/forge-verify/forge-verify.mdc +219 -0
  163. package/adapters/cursor/skills/forge-verify/references/verification-checklists.md +379 -0
  164. package/adapters/gemini/agents/forge-researcher.md +133 -0
  165. package/adapters/gemini/agents/forge-spec-writer.md +112 -0
  166. package/adapters/gemini/agents/forge-verifier.md +115 -0
  167. package/adapters/gemini/gemini-extension.json +54 -0
  168. package/adapters/gemini/references/epic-manifest-schema.json +120 -0
  169. package/adapters/gemini/references/forge-config-schema.json +166 -0
  170. package/adapters/gemini/references/pipeline-state-schema.json +110 -0
  171. package/adapters/gemini/references/portable-root.md +56 -0
  172. package/adapters/gemini/references/process-overview.md +123 -0
  173. package/adapters/gemini/references/ralph-loop-contract.md +221 -0
  174. package/adapters/gemini/references/shared-conventions.md +144 -0
  175. package/adapters/gemini/references/skill-frontmatter.schema.json +17 -0
  176. package/adapters/gemini/references/stack-resolution.md +51 -0
  177. package/adapters/gemini/references/stacks/_generic.md +90 -0
  178. package/adapters/gemini/references/stacks/go.md +138 -0
  179. package/adapters/gemini/references/stacks/python.md +163 -0
  180. package/adapters/gemini/references/stacks/rust.md +151 -0
  181. package/adapters/gemini/references/stacks/typescript.md +111 -0
  182. package/adapters/gemini/references/vendor-construct-inventory.md +49 -0
  183. package/adapters/gemini/scripts/forge-root.sh +50 -0
  184. package/adapters/gemini/skills/forge/forge.md +164 -0
  185. package/adapters/gemini/skills/forge-0-epic/forge-0-epic.md +302 -0
  186. package/adapters/gemini/skills/forge-0-epic/references/edit-mode.md +222 -0
  187. package/adapters/gemini/skills/forge-0-epic/references/epic-manifest-subcommands.md +64 -0
  188. package/adapters/gemini/skills/forge-1-prd/forge-1-prd.md +120 -0
  189. package/adapters/gemini/skills/forge-1-prd/references/prd-template.md +106 -0
  190. package/adapters/gemini/skills/forge-2-tech/forge-2-tech.md +197 -0
  191. package/adapters/gemini/skills/forge-2-tech/references/stack-discovery-checklist.md +95 -0
  192. package/adapters/gemini/skills/forge-3-specs/forge-3-specs.md +153 -0
  193. package/adapters/gemini/skills/forge-3-specs/references/spec-archetypes.md +106 -0
  194. package/adapters/gemini/skills/forge-3-specs/references/spec-examples.md +71 -0
  195. package/adapters/gemini/skills/forge-4-backlog/forge-4-backlog.md +145 -0
  196. package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +302 -0
  197. package/adapters/gemini/skills/forge-5-loop/references/result-reporting.md +63 -0
  198. package/adapters/gemini/skills/forge-5-loop/references/runner-contract.md +214 -0
  199. package/adapters/gemini/skills/forge-6-docs/forge-6-docs.md +178 -0
  200. package/adapters/gemini/skills/forge-6-docs/references/doc-conventions.md +126 -0
  201. package/adapters/gemini/skills/forge-fix/forge-fix.md +64 -0
  202. package/adapters/gemini/skills/forge-init/forge-init.md +29 -0
  203. package/adapters/gemini/skills/forge-verify/forge-verify.md +218 -0
  204. package/adapters/gemini/skills/forge-verify/references/verification-checklists.md +379 -0
  205. package/dist/agent-targets.d.ts +70 -0
  206. package/dist/agent-targets.js +111 -0
  207. package/dist/apply.d.ts +49 -0
  208. package/dist/apply.js +246 -0
  209. package/dist/cli.d.ts +94 -0
  210. package/dist/cli.js +508 -0
  211. package/dist/detect.d.ts +45 -0
  212. package/dist/detect.js +72 -0
  213. package/dist/fsutil.d.ts +56 -0
  214. package/dist/fsutil.js +175 -0
  215. package/dist/hash.d.ts +50 -0
  216. package/dist/hash.js +107 -0
  217. package/dist/index.d.ts +8 -0
  218. package/dist/index.js +9 -0
  219. package/dist/manifest.d.ts +72 -0
  220. package/dist/manifest.js +222 -0
  221. package/dist/plan.d.ts +66 -0
  222. package/dist/plan.js +166 -0
  223. package/dist/rauf.d.ts +83 -0
  224. package/dist/rauf.js +118 -0
  225. package/dist/report.d.ts +35 -0
  226. package/dist/report.js +110 -0
  227. package/dist/source.d.ts +69 -0
  228. package/dist/source.js +164 -0
  229. package/dist/types.d.ts +264 -0
  230. package/dist/types.js +57 -0
  231. package/package.json +42 -0
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The `agent-detection-map` exposed surface (spec 02): the static per-agent table plus the
3
+ * pure path derivations and the stat-based detection over it.
4
+ *
5
+ * One contract, two halves (REQ-DET-05):
6
+ * - static — `AGENT_TARGETS` (re-exported), `resolveRoots`, `destinationFor`
7
+ * - behavioral — `detectAgent`, `detectAgents`, `formatZeroDetection`
8
+ *
9
+ * Every function is data-driven over `AGENT_TARGETS` / `AGENT_IDS`, so a new agent is exactly
10
+ * one table row (REQ-SCALE-01) with no edit here. Read-only and total: nothing writes, spawns
11
+ * an agent, or creates a directory. Zero runtime dependencies; only `node:` built-ins.
12
+ */
13
+ import { type AgentId, type AgentTarget, type DetectionResult, type ResolveOpts, type Scope } from "./types.js";
14
+ export { AGENT_TARGETS } from "./types.js";
15
+ /**
16
+ * Resolve the two filesystem roots all destinations derive from, applying defaults. This is
17
+ * the single injection point for the home and working directories, so tests sandbox every
18
+ * path computation without touching the real `~` (spec 02 §4.2).
19
+ *
20
+ * - `home` (global scope root) defaults to `os.homedir()`.
21
+ * - `cwd` (project scope root) defaults to `process.cwd()`.
22
+ *
23
+ * Both returned paths are absolute and `path.resolve`d. Pure: reads no files, spawns nothing.
24
+ */
25
+ export declare function resolveRoots(opts?: ResolveOpts): {
26
+ home: string;
27
+ cwd: string;
28
+ };
29
+ /**
30
+ * Derive the absolute install destination for one agent under a given scope (REQ-DET-01,
31
+ * REQ-FLAG-02):
32
+ *
33
+ * <scopeRoot>/<configDirName>/<installSubdir>/<FEATURE_FORGE_NS>/
34
+ *
35
+ * where `scopeRoot` is the home dir for `"global"` and the cwd for `"project"`. The path is
36
+ * derived, never stored, so a new agent is one `AGENT_TARGETS` row (REQ-SCALE-01). Pure.
37
+ *
38
+ * @example destinationFor(AGENT_TARGETS.claude, "global", { home: "/h" })
39
+ * // → "/h/.claude/skills/feature-forge"
40
+ */
41
+ export declare function destinationFor(target: AgentTarget, scope: Scope, opts?: ResolveOpts): string;
42
+ /**
43
+ * Detect a single agent on the host (REQ-DET-02). Detection is decided solely by the presence
44
+ * of the agent's config dir under the active scope root (a `stat`, never an agent subprocess).
45
+ *
46
+ * Populates `configDirsProbed` (named in the zero-detection report, REQ-DET-04), `destination`
47
+ * (the resolved install dest for the active scope), and the advisory-only `cliOnPath` (never the
48
+ * detection signal). Total: any valid `AgentId` yields a `DetectionResult`; absence is
49
+ * `detected: false`, never an error.
50
+ */
51
+ export declare function detectAgent(id: AgentId, opts?: ResolveOpts): DetectionResult;
52
+ /** Options for {@link detectAgents}: {@link ResolveOpts} plus a single-agent scope (REQ-FLAG-01). */
53
+ export interface DetectAgentsOpts extends ResolveOpts {
54
+ /** Restrict detection to this one agent (`--agent/-a`). Absent ⇒ all five (REQ-DET-03). */
55
+ readonly only?: AgentId;
56
+ }
57
+ /**
58
+ * Detect every supported agent in canonical `AGENT_IDS` order (REQ-DET-03). Pass `opts.only`
59
+ * to scope to a single agent (REQ-FLAG-01); the result is then a one-element array. The default
60
+ * project/global scope comes from `opts.scope`. Total — never throws; each agent is probed
61
+ * independently.
62
+ */
63
+ export declare function detectAgents(opts?: DetectAgentsOpts): DetectionResult[];
64
+ /**
65
+ * Build the clear, actionable message for the zero-agents-detected case (REQ-DET-04). Names
66
+ * every config dir probed (drawn from the supplied results' `configDirsProbed`) so the user sees
67
+ * exactly where the installer looked. Creates no directory and produces no opaque error.
68
+ * Pure: derives text from already-computed {@link DetectionResult}s.
69
+ */
70
+ export declare function formatZeroDetection(results: DetectionResult[], scope: Scope): string;
@@ -0,0 +1,111 @@
1
+ /**
2
+ * The `agent-detection-map` exposed surface (spec 02): the static per-agent table plus the
3
+ * pure path derivations and the stat-based detection over it.
4
+ *
5
+ * One contract, two halves (REQ-DET-05):
6
+ * - static — `AGENT_TARGETS` (re-exported), `resolveRoots`, `destinationFor`
7
+ * - behavioral — `detectAgent`, `detectAgents`, `formatZeroDetection`
8
+ *
9
+ * Every function is data-driven over `AGENT_TARGETS` / `AGENT_IDS`, so a new agent is exactly
10
+ * one table row (REQ-SCALE-01) with no edit here. Read-only and total: nothing writes, spawns
11
+ * an agent, or creates a directory. Zero runtime dependencies; only `node:` built-ins.
12
+ */
13
+ import * as os from "node:os";
14
+ import * as path from "node:path";
15
+ import { AGENT_IDS, AGENT_TARGETS, FEATURE_FORGE_NS, } from "./types.js";
16
+ import { probeConfigDir, cliOnPath } from "./detect.js";
17
+ // Re-export the static table so importers reach the data and the behavior from one surface
18
+ // (REQ-DET-01, REQ-DET-05).
19
+ export { AGENT_TARGETS } from "./types.js";
20
+ /**
21
+ * Resolve the two filesystem roots all destinations derive from, applying defaults. This is
22
+ * the single injection point for the home and working directories, so tests sandbox every
23
+ * path computation without touching the real `~` (spec 02 §4.2).
24
+ *
25
+ * - `home` (global scope root) defaults to `os.homedir()`.
26
+ * - `cwd` (project scope root) defaults to `process.cwd()`.
27
+ *
28
+ * Both returned paths are absolute and `path.resolve`d. Pure: reads no files, spawns nothing.
29
+ */
30
+ export function resolveRoots(opts) {
31
+ return {
32
+ home: path.resolve(opts?.home ?? os.homedir()),
33
+ cwd: path.resolve(opts?.cwd ?? process.cwd()),
34
+ };
35
+ }
36
+ /**
37
+ * Internal: select the root directory for a scope (REQ-FLAG-02).
38
+ * `"global"` → the resolved home dir; `"project"` → the resolved cwd.
39
+ */
40
+ function scopeRootFor(scope, roots) {
41
+ return scope === "global" ? roots.home : roots.cwd;
42
+ }
43
+ /**
44
+ * Derive the absolute install destination for one agent under a given scope (REQ-DET-01,
45
+ * REQ-FLAG-02):
46
+ *
47
+ * <scopeRoot>/<configDirName>/<installSubdir>/<FEATURE_FORGE_NS>/
48
+ *
49
+ * where `scopeRoot` is the home dir for `"global"` and the cwd for `"project"`. The path is
50
+ * derived, never stored, so a new agent is one `AGENT_TARGETS` row (REQ-SCALE-01). Pure.
51
+ *
52
+ * @example destinationFor(AGENT_TARGETS.claude, "global", { home: "/h" })
53
+ * // → "/h/.claude/skills/feature-forge"
54
+ */
55
+ export function destinationFor(target, scope, opts) {
56
+ const roots = resolveRoots(opts);
57
+ const root = scopeRootFor(scope, roots);
58
+ return path.resolve(root, target.configDirName, target.installSubdir, FEATURE_FORGE_NS);
59
+ }
60
+ /**
61
+ * Detect a single agent on the host (REQ-DET-02). Detection is decided solely by the presence
62
+ * of the agent's config dir under the active scope root (a `stat`, never an agent subprocess).
63
+ *
64
+ * Populates `configDirsProbed` (named in the zero-detection report, REQ-DET-04), `destination`
65
+ * (the resolved install dest for the active scope), and the advisory-only `cliOnPath` (never the
66
+ * detection signal). Total: any valid `AgentId` yields a `DetectionResult`; absence is
67
+ * `detected: false`, never an error.
68
+ */
69
+ export function detectAgent(id, opts) {
70
+ const target = AGENT_TARGETS[id];
71
+ const scope = opts?.scope ?? "project";
72
+ const roots = resolveRoots(opts);
73
+ const root = scopeRootFor(scope, roots);
74
+ // Primary signal (REQ-DET-02): presence of the config dir under the active scope root.
75
+ const configDir = path.resolve(root, target.configDirName);
76
+ const detected = probeConfigDir(configDir);
77
+ return {
78
+ agent: id,
79
+ detected,
80
+ configDirsProbed: [configDir],
81
+ destination: destinationFor(target, scope, opts),
82
+ cliOnPath: cliOnPath(id), // advisory only; never gates `detected`
83
+ };
84
+ }
85
+ /**
86
+ * Detect every supported agent in canonical `AGENT_IDS` order (REQ-DET-03). Pass `opts.only`
87
+ * to scope to a single agent (REQ-FLAG-01); the result is then a one-element array. The default
88
+ * project/global scope comes from `opts.scope`. Total — never throws; each agent is probed
89
+ * independently.
90
+ */
91
+ export function detectAgents(opts) {
92
+ const ids = opts?.only ? [opts.only] : AGENT_IDS;
93
+ return ids.map((id) => detectAgent(id, opts));
94
+ }
95
+ /**
96
+ * Build the clear, actionable message for the zero-agents-detected case (REQ-DET-04). Names
97
+ * every config dir probed (drawn from the supplied results' `configDirsProbed`) so the user sees
98
+ * exactly where the installer looked. Creates no directory and produces no opaque error.
99
+ * Pure: derives text from already-computed {@link DetectionResult}s.
100
+ */
101
+ export function formatZeroDetection(results, scope) {
102
+ const probed = results.flatMap((r) => r.configDirsProbed);
103
+ const lines = [
104
+ `No supported coding agents detected (scope: ${scope}).`,
105
+ `Probed config directories (none present):`,
106
+ ...probed.map((p) => ` - ${p}`),
107
+ `No directories were created. Install an agent (or pass --global/project scope, or`,
108
+ `--source for tests) and re-run.`,
109
+ ];
110
+ return lines.join("\n");
111
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The apply engine (spec 04 §5) — the executor that turns a pure `PlannedAction` into filesystem
3
+ * mutations, then records the install manifest. `apply` NEVER throws for expected errors: a failure
4
+ * (write denied, path escape, manifest unwritable) returns `AgentReport{ ok:false, error }` so the
5
+ * CLI loop (07) can record one agent's failure and proceed with the others (REQ-OBS-03).
6
+ *
7
+ * Every mutation routes through the sandboxed `fsutil` primitives (each preceded by a
8
+ * `resolveWithin` containment check, REQ-SEC-01/02/03). There is NO gemini-specific branch — the
9
+ * bundle's `gemini-extension.json` is just another file in `source.files` (D9). There is no
10
+ * `applyUninstall`: an all-`"remove"` plan from `planUninstall` (05) is executed here (§5.3).
11
+ *
12
+ * Zero runtime dependencies; only `node:` built-ins.
13
+ */
14
+ import type { AgentReport, InstallManifest, Mode, PlannedAction, Result, Scope, AgentId } from "./types.js";
15
+ import type { LocatedSource } from "./source.js";
16
+ /**
17
+ * Apply-time context for ONE agent (spec 04 §5). `agentRoot` is the containment boundary every
18
+ * write is checked against (REQ-SEC-02): the resolved `<scopeRoot>/<configDirName>` (from 02).
19
+ * `destination` is the namespace dir; `manifestPath` the parent-sibling hidden file (05).
20
+ */
21
+ export interface ApplyContext {
22
+ readonly agent: AgentId;
23
+ readonly scope: Scope;
24
+ readonly mode: Mode;
25
+ /** Containment boundary for REQ-SEC-02: the agent's config root (e.g. `<home>/.claude`). */
26
+ readonly agentRoot: string;
27
+ /** The `feature-forge/` namespace dir (manifest.destination). */
28
+ readonly destination: string;
29
+ /** Hidden parent-sibling manifest path, from 05 `manifestPath`. */
30
+ readonly manifestPath: string;
31
+ /** Located source bundle (copy bytes / symlink target). `null` only for uninstall. */
32
+ readonly source: LocatedSource | null;
33
+ /** Pinned rauf coordinate to record in the manifest (06); `null` under `--skip-rauf`. */
34
+ readonly raufPin: string | null;
35
+ /** ISO-8601 "now" (injectable for deterministic tests). */
36
+ readonly now: string;
37
+ /** Prior manifest (for preserving `installedAt` across updates, and carrying inventory). */
38
+ readonly priorManifest: InstallManifest | null;
39
+ /**
40
+ * Injectable per-file write seam (tests force a deterministic WRITE_DENIED). Default:
41
+ * mkdir parent + `fs.copyFile`. Returns `Result` and never throws for expected errors.
42
+ */
43
+ readonly writeFileSeam?: (srcAbs: string, destAbs: string) => Promise<Result<void>>;
44
+ }
45
+ /**
46
+ * Execute one agent's `PlannedAction` against the filesystem, then write/delete the manifest.
47
+ * Returns an `AgentReport` instead of throwing (REQ-OBS-03). See spec 04 §5.
48
+ */
49
+ export declare function apply(planned: PlannedAction, ctx: ApplyContext): Promise<AgentReport>;
package/dist/apply.js ADDED
@@ -0,0 +1,246 @@
1
+ /**
2
+ * The apply engine (spec 04 §5) — the executor that turns a pure `PlannedAction` into filesystem
3
+ * mutations, then records the install manifest. `apply` NEVER throws for expected errors: a failure
4
+ * (write denied, path escape, manifest unwritable) returns `AgentReport{ ok:false, error }` so the
5
+ * CLI loop (07) can record one agent's failure and proceed with the others (REQ-OBS-03).
6
+ *
7
+ * Every mutation routes through the sandboxed `fsutil` primitives (each preceded by a
8
+ * `resolveWithin` containment check, REQ-SEC-01/02/03). There is NO gemini-specific branch — the
9
+ * bundle's `gemini-extension.json` is just another file in `source.files` (D9). There is no
10
+ * `applyUninstall`: an all-`"remove"` plan from `planUninstall` (05) is executed here (§5.3).
11
+ *
12
+ * Zero runtime dependencies; only `node:` built-ins.
13
+ */
14
+ import * as fsp from "node:fs/promises";
15
+ import * as path from "node:path";
16
+ import { ok, err } from "./types.js";
17
+ import { sha256File } from "./hash.js";
18
+ import { resolveWithin, symlinkDir, removePath, removeEmptyDirsWithin, } from "./fsutil.js";
19
+ import { buildManifest, writeManifest } from "./manifest.js";
20
+ /**
21
+ * Execute one agent's `PlannedAction` against the filesystem, then write/delete the manifest.
22
+ * Returns an `AgentReport` instead of throwing (REQ-OBS-03). See spec 04 §5.
23
+ */
24
+ export async function apply(planned, ctx) {
25
+ // Empty-files plan (e.g. uninstall with no prior manifest) is a no-op — touch nothing.
26
+ if (planned.files.length === 0)
27
+ return success(ctx, planned);
28
+ const isUninstall = planned.files.every((f) => f.action === "remove");
29
+ if (ctx.mode === "symlink") {
30
+ return isUninstall
31
+ ? applySymlinkUninstall(planned, ctx)
32
+ : applySymlinkInstall(planned, ctx);
33
+ }
34
+ return isUninstall
35
+ ? applyCopyUninstall(planned, ctx)
36
+ : applyCopyInstall(planned, ctx);
37
+ }
38
+ // ---------------------------------------------------------------------------
39
+ // Copy mode
40
+ // ---------------------------------------------------------------------------
41
+ /** §5.1 copy flow: per-file copy/remove, then write the manifest (unless every action unchanged). */
42
+ async function applyCopyInstall(planned, ctx) {
43
+ const source = ctx.source;
44
+ if (source === null) {
45
+ return fail(ctx, planned, {
46
+ code: "UNEXPECTED",
47
+ agent: ctx.agent,
48
+ message: `apply(copy) for "${ctx.agent}" requires a located source bundle but got null`,
49
+ });
50
+ }
51
+ const priorByPath = new Map();
52
+ for (const f of ctx.priorManifest?.files ?? [])
53
+ priorByPath.set(f.path, f);
54
+ const writeFile = ctx.writeFileSeam ?? defaultCopyFile;
55
+ const inventory = [];
56
+ for (const fa of planned.files) {
57
+ const resolved = resolveWithin(ctx.agentRoot, ctx.destination, fa.relpath);
58
+ if (!resolved.ok)
59
+ return fail(ctx, planned, resolved.error);
60
+ const destAbs = resolved.value;
61
+ switch (fa.action) {
62
+ case "create":
63
+ case "overwrite": {
64
+ const srcAbs = path.join(source.root, fa.relpath);
65
+ const wrote = await writeFile(srcAbs, destAbs);
66
+ if (!wrote.ok)
67
+ return fail(ctx, planned, wrote.error);
68
+ inventory.push({ path: fa.relpath, sha256: sha256File(destAbs) });
69
+ break;
70
+ }
71
+ case "remove": {
72
+ const removed = await removePath(destAbs);
73
+ if (!removed.ok)
74
+ return fail(ctx, planned, removed.error);
75
+ break;
76
+ }
77
+ case "unchanged":
78
+ case "skip-modified": {
79
+ // No write. Carry the prior inventory entry forward so the rewritten manifest is faithful.
80
+ const prior = priorByPath.get(fa.relpath);
81
+ if (prior !== undefined)
82
+ inventory.push(prior);
83
+ break;
84
+ }
85
+ }
86
+ }
87
+ // No-op short-circuit (REQ-IDEM-01): every action unchanged ⇒ zero writes, manifest untouched.
88
+ if (planned.files.every((f) => f.action === "unchanged")) {
89
+ return success(ctx, planned);
90
+ }
91
+ const manifest = buildManifest({
92
+ agent: ctx.agent,
93
+ scope: ctx.scope,
94
+ mode: "copy",
95
+ destination: ctx.destination,
96
+ files: inventory,
97
+ skills: source.skills,
98
+ sourceHash: source.sourceHash,
99
+ raufPin: ctx.raufPin,
100
+ previous: ctx.priorManifest,
101
+ now: () => new Date(ctx.now),
102
+ });
103
+ const wrote = writeManifest(ctx.manifestPath, manifest);
104
+ if (!wrote.ok)
105
+ return fail(ctx, planned, wrote.error);
106
+ return success(ctx, planned);
107
+ }
108
+ /** §5.3 copy-mode uninstall: remove recorded files, prune empty dirs, delete the manifest LAST. */
109
+ async function applyCopyUninstall(planned, ctx) {
110
+ for (const fa of planned.files) {
111
+ const resolved = resolveWithin(ctx.agentRoot, ctx.destination, fa.relpath);
112
+ if (!resolved.ok)
113
+ return fail(ctx, planned, resolved.error);
114
+ const removed = await removePath(resolved.value);
115
+ if (!removed.ok)
116
+ return fail(ctx, planned, removed.error);
117
+ }
118
+ const pruned = await removeEmptyDirsWithin(ctx.destination, ctx.agentRoot);
119
+ if (!pruned.ok)
120
+ return fail(ctx, planned, pruned.error);
121
+ const deleted = await deleteManifest(ctx);
122
+ if (!deleted.ok)
123
+ return fail(ctx, planned, deleted.error);
124
+ return success(ctx, planned);
125
+ }
126
+ // ---------------------------------------------------------------------------
127
+ // Symlink mode
128
+ // ---------------------------------------------------------------------------
129
+ /** §5.2 symlink flow: remove any prior, link the whole namespace dir → source bundle root. */
130
+ async function applySymlinkInstall(planned, ctx) {
131
+ const source = ctx.source;
132
+ if (source === null) {
133
+ return fail(ctx, planned, {
134
+ code: "UNEXPECTED",
135
+ agent: ctx.agent,
136
+ message: `apply(symlink) for "${ctx.agent}" requires a located source bundle but got null`,
137
+ });
138
+ }
139
+ const resolved = resolveWithin(ctx.agentRoot, ctx.destination);
140
+ if (!resolved.ok)
141
+ return fail(ctx, planned, resolved.error);
142
+ const linkPath = resolved.value;
143
+ // Unchanged (live link already points at the same target) ⇒ zero writes, manifest untouched.
144
+ if (planned.files.every((f) => f.action === "unchanged")) {
145
+ return success(ctx, planned);
146
+ }
147
+ // skip-modified (prior exists, no --force) ⇒ leave it; report, write nothing.
148
+ if (planned.files.every((f) => f.action === "skip-modified")) {
149
+ return success(ctx, planned);
150
+ }
151
+ const removed = await removePath(linkPath);
152
+ if (!removed.ok)
153
+ return fail(ctx, planned, removed.error);
154
+ const linked = await symlinkDir(source.root, linkPath);
155
+ if (!linked.ok)
156
+ return fail(ctx, planned, linked.error);
157
+ const effectiveMode = linked.value.mode;
158
+ // files[] lists the bundle-relative paths with sha256 OMITTED (no per-file copy exists, 00 §3).
159
+ const files = source.files.map((f) => ({ path: f.relpath }));
160
+ const manifest = buildManifest({
161
+ agent: ctx.agent,
162
+ scope: ctx.scope,
163
+ mode: effectiveMode,
164
+ destination: ctx.destination,
165
+ files,
166
+ skills: source.skills,
167
+ sourceHash: source.sourceHash,
168
+ raufPin: ctx.raufPin,
169
+ // Truthful record: a copy fallback must NOT carry link (copy-mode manifest invariant, 05).
170
+ ...(effectiveMode === "symlink" ? { link: { target: source.root } } : {}),
171
+ previous: ctx.priorManifest,
172
+ now: () => new Date(ctx.now),
173
+ });
174
+ const wrote = writeManifest(ctx.manifestPath, manifest);
175
+ if (!wrote.ok)
176
+ return fail(ctx, planned, wrote.error);
177
+ return success(ctx, planned);
178
+ }
179
+ /** §5.3 symlink-mode uninstall: `lstat`+`unlink` the link only (never recurse), delete manifest. */
180
+ async function applySymlinkUninstall(planned, ctx) {
181
+ const resolved = resolveWithin(ctx.agentRoot, ctx.destination);
182
+ if (!resolved.ok)
183
+ return fail(ctx, planned, resolved.error);
184
+ const removed = await removePath(resolved.value);
185
+ if (!removed.ok)
186
+ return fail(ctx, planned, removed.error);
187
+ const deleted = await deleteManifest(ctx);
188
+ if (!deleted.ok)
189
+ return fail(ctx, planned, deleted.error);
190
+ return success(ctx, planned);
191
+ }
192
+ // ---------------------------------------------------------------------------
193
+ // Internal helpers
194
+ // ---------------------------------------------------------------------------
195
+ /** Default per-file write seam: ensure the parent dir, then copy the source bytes. */
196
+ async function defaultCopyFile(srcAbs, destAbs) {
197
+ try {
198
+ await fsp.mkdir(path.dirname(destAbs), { recursive: true });
199
+ await fsp.copyFile(srcAbs, destAbs);
200
+ return ok(undefined);
201
+ }
202
+ catch (e) {
203
+ const code = e?.code;
204
+ if (code === "EACCES" || code === "EPERM") {
205
+ return err({
206
+ code: "WRITE_DENIED",
207
+ message: `no write permission to ${destAbs}`,
208
+ path: destAbs,
209
+ remedy: "check directory permissions, or choose a different scope (--global vs project)",
210
+ });
211
+ }
212
+ return err({
213
+ code: "UNEXPECTED",
214
+ message: `filesystem error at ${destAbs}: ${e?.message ?? String(e)}`,
215
+ path: destAbs,
216
+ });
217
+ }
218
+ }
219
+ /** Delete the manifest file (containment-checked), idempotent (ENOENT → ok). */
220
+ async function deleteManifest(ctx) {
221
+ const resolved = resolveWithin(ctx.agentRoot, ctx.manifestPath);
222
+ if (!resolved.ok)
223
+ return resolved;
224
+ return removePath(resolved.value);
225
+ }
226
+ /** Build a successful AgentReport for `ctx`/`planned`. */
227
+ function success(ctx, planned) {
228
+ return {
229
+ agent: ctx.agent,
230
+ detected: true,
231
+ ok: true,
232
+ actions: planned.files,
233
+ raufPin: ctx.raufPin,
234
+ };
235
+ }
236
+ /** Build a failed AgentReport carrying `error` (never throws — REQ-OBS-03). */
237
+ function fail(ctx, planned, error) {
238
+ return {
239
+ agent: ctx.agent,
240
+ detected: true,
241
+ ok: false,
242
+ actions: planned.files,
243
+ error,
244
+ raufPin: ctx.raufPin,
245
+ };
246
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,94 @@
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 { type CliFlags, type ExitCode, type InstallerError, type Result, type RunReport, type Subcommand } from "./types.js";
13
+ import { type RegistryQuery } from "./rauf.js";
14
+ /** A flag's declarative spec — drives both parseArgs config and helpText (REQ-DIST-03). */
15
+ interface FlagSpec {
16
+ /** Long name without leading dashes, e.g. "agent". */
17
+ readonly name: string;
18
+ /** Single-char alias without dash, e.g. "a"; omitted if none. */
19
+ readonly short?: string;
20
+ /** parseArgs type. */
21
+ readonly type: "boolean" | "string";
22
+ /** One-line help description. */
23
+ readonly help: string;
24
+ /** Hidden from --help (e.g. --source for tests). */
25
+ readonly hidden?: boolean;
26
+ /** Placeholder shown in help for string flags, e.g. "<id>". */
27
+ readonly arg?: string;
28
+ }
29
+ /** A subcommand's declarative spec. */
30
+ interface SubcommandSpec {
31
+ readonly canonical: Subcommand;
32
+ /** Accepted aliases that resolve to `canonical` (e.g. ["add"]). */
33
+ readonly aliases: readonly string[];
34
+ readonly help: string;
35
+ }
36
+ /** Canonical subcommand table (REQ-DIST-03, §1.2). */
37
+ export declare const SUBCOMMANDS: readonly SubcommandSpec[];
38
+ /** Canonical flag table (REQ-FLAG-01..05, §1.3). */
39
+ export declare const FLAGS: readonly FlagSpec[];
40
+ /** Parsed CLI invocation: a resolved subcommand plus normalized flags. */
41
+ export interface ParsedCli {
42
+ readonly subcommand: Subcommand;
43
+ readonly flags: CliFlags;
44
+ }
45
+ /**
46
+ * Parse and validate `argv` (already sliced past `node` + script — `process.argv.slice(2)`)
47
+ * via `node:util.parseArgs` (zero-dep). Resolves aliases, rejects unknown
48
+ * subcommand/flag/agent (and a parseArgs throw) as a `USAGE` error. Pure: no I/O, no exit.
49
+ */
50
+ export declare function parseCliArgs(argv: string[]): Result<ParsedCli>;
51
+ /**
52
+ * Map a structured `InstallerError` to a process exit code (tech-spec §7).
53
+ * "USAGE" → EXIT.USAGE (2); everything else → EXIT.FAILURE (1).
54
+ */
55
+ export declare function mapErrorToExit(error: InstallerError): ExitCode;
56
+ /**
57
+ * Injected environment for a programmatic CLI run (the hermetic-test seam, 08 §3.4). Every
58
+ * field is optional; an omitted field falls back to the real default `main` uses.
59
+ */
60
+ export interface CliEnv {
61
+ /** Stand-in for `~` — threaded into detection/destination/manifest resolution as ResolveOpts.home. */
62
+ readonly home?: string;
63
+ /** Stand-in for `process.cwd()` — threaded into resolution as ResolveOpts.cwd. */
64
+ readonly cwd?: string;
65
+ /** Mock rauf registry query (06) for the preflight; default = the real `npm view` query. */
66
+ readonly registry?: RegistryQuery;
67
+ /** Forced platform for the copy/symlink mode decision (REQ-FLAG-03); default = process.platform. */
68
+ readonly platform?: NodeJS.Platform;
69
+ }
70
+ /**
71
+ * Run the full CLI pipeline programmatically and return the assembled `RunReport` WITHOUT
72
+ * touching `process` (no argv read, no stdout/stderr write, no exit). This is the testable
73
+ * core (08 §3.4): it threads env.home/cwd into detection/manifest calls, env.registry into the
74
+ * rauf preflight, and env.platform into the copy/symlink mode decision.
75
+ */
76
+ export declare function runCli(argv: string[], env?: CliEnv): Promise<RunReport>;
77
+ /**
78
+ * Parse → help/version precedence → run pipeline (catching per-agent errors via runCli) →
79
+ * render → exit code. The only place that writes to stdout/stderr and decides the exit code.
80
+ * Never reads stdin (REQ-DIST-02).
81
+ *
82
+ * @param argv - the post-`node` argument list (`process.argv.slice(2)` in production).
83
+ * @param env - the injectable CLI env (the hermetic-test seam, §3.1a); default `{}` = real
84
+ * defaults. Tests inject a throwing seam (e.g. a registry that throws) here to
85
+ * exercise the UNEXPECTED boundary catch deterministically without a network call.
86
+ */
87
+ export declare function main(argv: string[], env?: CliEnv): Promise<ExitCode>;
88
+ /**
89
+ * Build the full `--help` text from the single CLI_SPEC (SUBCOMMANDS + FLAGS, §1.5) so the
90
+ * listed surface can never drift from what parseArgs accepts (REQ-DIST-03). Hidden flags
91
+ * (--source) are omitted. Pure: returns a string, no I/O.
92
+ */
93
+ export declare function helpText(): string;
94
+ export {};