@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/manifest.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The persisted install manifest (read/write/build) and the manifest-driven uninstall-exactness
|
|
3
|
+
* policy (spec 05). This module locates the hidden parent-sibling manifest, reads/validates and
|
|
4
|
+
* atomically writes it, builds an {@link InstallManifest} from an apply result, and owns the
|
|
5
|
+
* uninstall removal POLICY (`planUninstall`). The safe EXECUTION of that plan is `apply()` in
|
|
6
|
+
* spec 04 — there is no `applyUninstall` here.
|
|
7
|
+
*
|
|
8
|
+
* Zero runtime dependencies; only `node:` built-ins. Named exports only. Core functions return
|
|
9
|
+
* `Result<T, E>` and never throw for expected errors; `JSON.parse` is wrapped in `try/catch`.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { AGENT_TARGETS, MANIFEST_PREFIX, SCHEMA_VERSION, ok, err, } from "./types.js";
|
|
14
|
+
import { destinationFor } from "./agent-targets.js";
|
|
15
|
+
/**
|
|
16
|
+
* Assemble an {@link InstallManifest} from an apply result (REQ-SAFE-01/03). Pure — no I/O.
|
|
17
|
+
*
|
|
18
|
+
* Timestamp policy: `updatedAt` is always "now"; `installedAt` is `previous.installedAt` when
|
|
19
|
+
* reconciling an existing install, else "now". `featureForgeVersion` is always `null` today
|
|
20
|
+
* (OQ-A/IR-1; C-3 forbids synthesizing one).
|
|
21
|
+
*/
|
|
22
|
+
export function buildManifest(args) {
|
|
23
|
+
const now = (args.now ?? (() => new Date()))().toISOString();
|
|
24
|
+
const installedAt = args.previous?.installedAt ?? now;
|
|
25
|
+
const files = [...args.files]
|
|
26
|
+
.map((f) => ({ path: f.path, ...(f.sha256 !== undefined ? { sha256: f.sha256 } : {}) }))
|
|
27
|
+
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
28
|
+
const skills = [...args.skills].sort();
|
|
29
|
+
return {
|
|
30
|
+
schemaVersion: SCHEMA_VERSION,
|
|
31
|
+
agent: args.agent,
|
|
32
|
+
scope: args.scope,
|
|
33
|
+
mode: args.mode,
|
|
34
|
+
destination: args.destination,
|
|
35
|
+
featureForgeVersion: null, // null today (OQ-A/IR-1); C-3 forbids synthesizing one.
|
|
36
|
+
sourceHash: args.sourceHash,
|
|
37
|
+
raufPin: args.raufPin,
|
|
38
|
+
installedAt,
|
|
39
|
+
updatedAt: now,
|
|
40
|
+
skills,
|
|
41
|
+
files,
|
|
42
|
+
...(args.link !== undefined ? { link: args.link } : {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// manifestPath
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
/**
|
|
49
|
+
* Absolute path of the hidden parent-sibling manifest for an agent + scope (D6/D8):
|
|
50
|
+
* `<scopeRoot>/<configDirName>/<installSubdir>/.feature-forge.<scope>.json`
|
|
51
|
+
* e.g. `~/.claude/skills/.feature-forge.global.json`. Identical for copy and symlink mode.
|
|
52
|
+
*/
|
|
53
|
+
export function manifestPath(agent, scope, opts) {
|
|
54
|
+
const destination = destinationFor(AGENT_TARGETS[agent], scope, opts);
|
|
55
|
+
const installSubdirAbs = path.dirname(destination); // the skills/rules/extensions dir
|
|
56
|
+
return path.join(installSubdirAbs, `${MANIFEST_PREFIX}${scope}.json`);
|
|
57
|
+
}
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// readManifest / writeManifest
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
/**
|
|
62
|
+
* Read and validate the manifest at `p`. Absent (`ENOENT`) → `ok(null)`; present + valid →
|
|
63
|
+
* `ok(manifest)`; unreadable / invalid JSON / failed shape validation → `err(MANIFEST_CORRUPT)`.
|
|
64
|
+
*/
|
|
65
|
+
export function readManifest(p) {
|
|
66
|
+
let raw;
|
|
67
|
+
try {
|
|
68
|
+
raw = fs.readFileSync(p, "utf8");
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
if (e.code === "ENOENT")
|
|
72
|
+
return ok(null);
|
|
73
|
+
return err({
|
|
74
|
+
code: "MANIFEST_CORRUPT",
|
|
75
|
+
message: `cannot read install manifest at ${p}: ${e.message}`,
|
|
76
|
+
path: p,
|
|
77
|
+
remedy: "check read permissions, or remove the file to force a fresh install",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
let parsed;
|
|
81
|
+
try {
|
|
82
|
+
parsed = JSON.parse(raw);
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
return err({
|
|
86
|
+
code: "MANIFEST_CORRUPT",
|
|
87
|
+
message: `install manifest at ${p} is not valid JSON: ${e.message}`,
|
|
88
|
+
path: p,
|
|
89
|
+
remedy: "the manifest is corrupt; remove it and re-run install to regenerate it",
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const v = validateManifest(parsed);
|
|
93
|
+
if (!v.ok) {
|
|
94
|
+
return err({
|
|
95
|
+
code: "MANIFEST_CORRUPT",
|
|
96
|
+
message: `install manifest at ${p} failed validation: ${v.reason}`,
|
|
97
|
+
path: p,
|
|
98
|
+
remedy: "the manifest is corrupt; remove it and re-run install to regenerate it",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return ok(v.value);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Atomically write the manifest to `p` (write `<p>.tmp` → `rename`). Creates the parent dir if
|
|
105
|
+
* missing. Returns `err(WRITE_DENIED)` on a permission failure, cleaning up the temp file.
|
|
106
|
+
*/
|
|
107
|
+
export function writeManifest(p, m) {
|
|
108
|
+
const tmp = `${p}.tmp`;
|
|
109
|
+
try {
|
|
110
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
111
|
+
fs.writeFileSync(tmp, `${JSON.stringify(m, null, 2)}\n`, "utf8");
|
|
112
|
+
fs.renameSync(tmp, p); // atomic on a single filesystem
|
|
113
|
+
return ok(undefined);
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
// Best-effort cleanup of the temp file; ignore secondary failures.
|
|
117
|
+
try {
|
|
118
|
+
fs.rmSync(tmp, { force: true });
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
/* ignore */
|
|
122
|
+
}
|
|
123
|
+
const code = e.code;
|
|
124
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
125
|
+
return err({
|
|
126
|
+
code: "WRITE_DENIED",
|
|
127
|
+
message: `no write permission for install manifest at ${p}`,
|
|
128
|
+
path: p,
|
|
129
|
+
remedy: `ensure you can write to ${path.dirname(p)} (do not use elevated privileges)`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return err({
|
|
133
|
+
code: "UNEXPECTED",
|
|
134
|
+
message: `failed to write install manifest at ${p}: ${e.message}`,
|
|
135
|
+
path: p,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const AGENT_IDS_SET = new Set(["claude", "codex", "copilot", "cursor", "gemini"]);
|
|
140
|
+
/** Structural validation of a parsed manifest (internal to manifest.ts). */
|
|
141
|
+
function validateManifest(x) {
|
|
142
|
+
if (typeof x !== "object" || x === null)
|
|
143
|
+
return { ok: false, reason: "not an object" };
|
|
144
|
+
const o = x;
|
|
145
|
+
if (o.schemaVersion !== SCHEMA_VERSION) {
|
|
146
|
+
return { ok: false, reason: `unsupported schemaVersion ${String(o.schemaVersion)}` };
|
|
147
|
+
}
|
|
148
|
+
if (typeof o.agent !== "string" || !AGENT_IDS_SET.has(o.agent)) {
|
|
149
|
+
return { ok: false, reason: `invalid agent ${String(o.agent)}` };
|
|
150
|
+
}
|
|
151
|
+
if (o.scope !== "global" && o.scope !== "project") {
|
|
152
|
+
return { ok: false, reason: `invalid scope ${String(o.scope)}` };
|
|
153
|
+
}
|
|
154
|
+
if (o.mode !== "copy" && o.mode !== "symlink") {
|
|
155
|
+
return { ok: false, reason: `invalid mode ${String(o.mode)}` };
|
|
156
|
+
}
|
|
157
|
+
if (typeof o.destination !== "string" || o.destination.length === 0) {
|
|
158
|
+
return { ok: false, reason: "missing destination" };
|
|
159
|
+
}
|
|
160
|
+
if (!(o.featureForgeVersion === null || typeof o.featureForgeVersion === "string")) {
|
|
161
|
+
return { ok: false, reason: "invalid featureForgeVersion" };
|
|
162
|
+
}
|
|
163
|
+
if (typeof o.sourceHash !== "string")
|
|
164
|
+
return { ok: false, reason: "missing sourceHash" };
|
|
165
|
+
if (!(o.raufPin === null || typeof o.raufPin === "string")) {
|
|
166
|
+
return { ok: false, reason: "invalid raufPin" };
|
|
167
|
+
}
|
|
168
|
+
if (typeof o.installedAt !== "string" || typeof o.updatedAt !== "string") {
|
|
169
|
+
return { ok: false, reason: "missing timestamps" };
|
|
170
|
+
}
|
|
171
|
+
if (!Array.isArray(o.skills) || !o.skills.every((s) => typeof s === "string")) {
|
|
172
|
+
return { ok: false, reason: "invalid skills[]" };
|
|
173
|
+
}
|
|
174
|
+
if (!Array.isArray(o.files))
|
|
175
|
+
return { ok: false, reason: "invalid files[]" };
|
|
176
|
+
for (const f of o.files) {
|
|
177
|
+
if (typeof f !== "object" || f === null)
|
|
178
|
+
return { ok: false, reason: "invalid files[] entry" };
|
|
179
|
+
const ff = f;
|
|
180
|
+
if (typeof ff.path !== "string")
|
|
181
|
+
return { ok: false, reason: "files[].path not a string" };
|
|
182
|
+
if (ff.sha256 !== undefined && typeof ff.sha256 !== "string") {
|
|
183
|
+
return { ok: false, reason: "files[].sha256 not a string" };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (o.link !== undefined) {
|
|
187
|
+
const l = o.link;
|
|
188
|
+
if (typeof l !== "object" || l === null || typeof l.target !== "string") {
|
|
189
|
+
return { ok: false, reason: "invalid link" };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Cross-field: symlink ⇒ link present; copy ⇒ link absent (D8 invariant).
|
|
193
|
+
if (o.mode === "symlink" && o.link === undefined) {
|
|
194
|
+
return { ok: false, reason: "symlink mode manifest missing link.target" };
|
|
195
|
+
}
|
|
196
|
+
if (o.mode === "copy" && o.link !== undefined) {
|
|
197
|
+
return { ok: false, reason: "copy mode manifest must not carry link" };
|
|
198
|
+
}
|
|
199
|
+
return { ok: true, value: x };
|
|
200
|
+
}
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// planUninstall — the uninstall removal POLICY
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
/**
|
|
205
|
+
* Compute the uninstall plan from a manifest (REQ-OPS-03, REQ-SAFE-01/02). PURE — no I/O,
|
|
206
|
+
* manifest only. Returns an all-`"remove"` {@link PlannedAction}: copy mode one
|
|
207
|
+
* `{ relpath, action: "remove" }` per `manifest.files[].path` in recorded order; symlink mode the
|
|
208
|
+
* single `{ relpath: ".", action: "remove" }`. The safe EXECUTION is `apply()` in spec 04.
|
|
209
|
+
*/
|
|
210
|
+
export function planUninstall(manifest) {
|
|
211
|
+
const isSymlink = manifest.mode === "symlink" || manifest.link !== undefined;
|
|
212
|
+
const files = isSymlink
|
|
213
|
+
? [{ relpath: ".", action: "remove" }]
|
|
214
|
+
: manifest.files.map((f) => ({ relpath: f.path, action: "remove" }));
|
|
215
|
+
return ok({
|
|
216
|
+
agent: manifest.agent,
|
|
217
|
+
scope: manifest.scope,
|
|
218
|
+
mode: manifest.mode,
|
|
219
|
+
destination: manifest.destination,
|
|
220
|
+
files,
|
|
221
|
+
});
|
|
222
|
+
}
|
package/dist/plan.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pure planner (spec 04 §3-§6) — the dry-run = real-run engine.
|
|
3
|
+
*
|
|
4
|
+
* `plan.ts` performs ZERO filesystem writes and ZERO network calls. It MAY read destination bytes
|
|
5
|
+
* (via `sha256File`) and the prior manifest (already read by 05 and passed in via `PlanContext`).
|
|
6
|
+
* It diffs three facts per bundle-relative path — source hash (S), destination hash (D), and the
|
|
7
|
+
* manifest-recorded hash (M) — and emits a `PlannedAction`. The CLI hands the SAME object to
|
|
8
|
+
* `apply()` on a real run, so dry-run and real run can never drift. Zero runtime dependencies.
|
|
9
|
+
*/
|
|
10
|
+
import type { AgentId, FileActionKind, Mode, PlannedAction, Result, Scope, InstallManifest } from "./types.js";
|
|
11
|
+
import { type LocatedSource } from "./source.js";
|
|
12
|
+
/**
|
|
13
|
+
* Everything the pure planner needs to diff source ⇆ destination ⇆ manifest for ONE agent
|
|
14
|
+
* (spec 04 §4). Built by cli.ts (07); the planner reads these and writes nothing.
|
|
15
|
+
*/
|
|
16
|
+
export interface PlanContext {
|
|
17
|
+
/** The agent being planned. */
|
|
18
|
+
readonly agent: AgentId;
|
|
19
|
+
/** Active scope; copied onto the plan. */
|
|
20
|
+
readonly scope: Scope;
|
|
21
|
+
/** Resolved materialization mode. MUST already account for Windows (see `resolveMode`). */
|
|
22
|
+
readonly mode: Mode;
|
|
23
|
+
/** Absolute path of the `feature-forge/` namespace dir to be governed. */
|
|
24
|
+
readonly destination: string;
|
|
25
|
+
/** The located, integrity-checked source bundle (03), or `null` (absent/invalid bundle). */
|
|
26
|
+
readonly source: LocatedSource | null;
|
|
27
|
+
/** The prior manifest for this destination, or `null` if none exists (fresh install). */
|
|
28
|
+
readonly priorManifest: InstallManifest | null;
|
|
29
|
+
/** `--force`: overwrite `skip-modified` destinations instead of skipping. */
|
|
30
|
+
readonly force: boolean;
|
|
31
|
+
/** The pinned rauf coordinate to surface on the plan (06); the planner only echoes it. */
|
|
32
|
+
readonly raufPin?: string | null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Classify one bundle-relative path (spec 04 §6 table). PURE: hashes are read, nothing is written.
|
|
36
|
+
*
|
|
37
|
+
* @param relpath bundle-relative POSIX path (informational; not used in the decision)
|
|
38
|
+
* @param srcHash sha256 of the source file (S)
|
|
39
|
+
* @param destHash sha256 of the destination file, or undefined if absent (D)
|
|
40
|
+
* @param manifestHash sha256 recorded for this path in the prior manifest, or undefined (M)
|
|
41
|
+
* @param force whether --force promotes skip-modified → overwrite
|
|
42
|
+
*/
|
|
43
|
+
export declare function classifyFile(relpath: string, srcHash: string, destHash: string | undefined, manifestHash: string | undefined, force: boolean): FileActionKind;
|
|
44
|
+
/**
|
|
45
|
+
* PURE. Compute the install plan for one agent (spec 04 §4.1). Writes nothing. Returns
|
|
46
|
+
* err(SOURCE_MISSING/SOURCE_INVALID) when `ctx.source` is null.
|
|
47
|
+
*/
|
|
48
|
+
export declare function planInstall(ctx: PlanContext): Result<PlannedAction>;
|
|
49
|
+
/**
|
|
50
|
+
* PURE. Compute the update/reconcile plan for one agent (spec 04 §4.2): identical to planInstall
|
|
51
|
+
* for create/overwrite/unchanged/skip-modified, PLUS manifest-scoped orphan removal — any path in
|
|
52
|
+
* `priorManifest.files` the current source no longer contains becomes `remove`. With no prior
|
|
53
|
+
* manifest, behaves exactly like planInstall (first install).
|
|
54
|
+
*/
|
|
55
|
+
export declare function planUpdate(ctx: PlanContext): Result<PlannedAction>;
|
|
56
|
+
/**
|
|
57
|
+
* Convenience dispatcher used by cli.ts (07). Routes install/update to the typed planner;
|
|
58
|
+
* `uninstall` delegates to `planUninstall` (manifest-driven, from 05) — an absent prior manifest
|
|
59
|
+
* yields an empty-files plan (no-op).
|
|
60
|
+
*/
|
|
61
|
+
export declare function plan(subcommand: "install" | "update" | "uninstall", ctx: PlanContext): Result<PlannedAction>;
|
|
62
|
+
/**
|
|
63
|
+
* Resolve the effective materialization mode (spec 04, REQ-FLAG-03, D8). `--symlink` requests
|
|
64
|
+
* symlink, but Windows ALWAYS copies. Pure; `windows` is injectable for tests.
|
|
65
|
+
*/
|
|
66
|
+
export declare function resolveMode(wantSymlink: boolean, windows?: boolean): Mode;
|
package/dist/plan.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pure planner (spec 04 §3-§6) — the dry-run = real-run engine.
|
|
3
|
+
*
|
|
4
|
+
* `plan.ts` performs ZERO filesystem writes and ZERO network calls. It MAY read destination bytes
|
|
5
|
+
* (via `sha256File`) and the prior manifest (already read by 05 and passed in via `PlanContext`).
|
|
6
|
+
* It diffs three facts per bundle-relative path — source hash (S), destination hash (D), and the
|
|
7
|
+
* manifest-recorded hash (M) — and emits a `PlannedAction`. The CLI hands the SAME object to
|
|
8
|
+
* `apply()` on a real run, so dry-run and real run can never drift. Zero runtime dependencies.
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import { ok, err } from "./types.js";
|
|
13
|
+
import { sha256File } from "./hash.js";
|
|
14
|
+
import { planUninstall } from "./manifest.js";
|
|
15
|
+
import { isWindows } from "./fsutil.js";
|
|
16
|
+
/**
|
|
17
|
+
* Classify one bundle-relative path (spec 04 §6 table). PURE: hashes are read, nothing is written.
|
|
18
|
+
*
|
|
19
|
+
* @param relpath bundle-relative POSIX path (informational; not used in the decision)
|
|
20
|
+
* @param srcHash sha256 of the source file (S)
|
|
21
|
+
* @param destHash sha256 of the destination file, or undefined if absent (D)
|
|
22
|
+
* @param manifestHash sha256 recorded for this path in the prior manifest, or undefined (M)
|
|
23
|
+
* @param force whether --force promotes skip-modified → overwrite
|
|
24
|
+
*/
|
|
25
|
+
export function classifyFile(relpath, srcHash, destHash, manifestHash, force) {
|
|
26
|
+
void relpath;
|
|
27
|
+
if (destHash === undefined)
|
|
28
|
+
return "create"; // row 1
|
|
29
|
+
if (destHash === srcHash)
|
|
30
|
+
return "unchanged"; // row 3 (wins over rows 2/4)
|
|
31
|
+
// dest exists and differs from source:
|
|
32
|
+
const clean = manifestHash !== undefined && destHash === manifestHash;
|
|
33
|
+
if (clean)
|
|
34
|
+
return "overwrite"; // row 2 (REQ-IDEM-03: clean prior, source changed, no --force)
|
|
35
|
+
return force ? "overwrite" : "skip-modified"; // rows 4/5 (REQ-IDEM-02/REQ-FLAG-04)
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* PURE. Compute the install plan for one agent (spec 04 §4.1). Writes nothing. Returns
|
|
39
|
+
* err(SOURCE_MISSING/SOURCE_INVALID) when `ctx.source` is null.
|
|
40
|
+
*/
|
|
41
|
+
export function planInstall(ctx) {
|
|
42
|
+
return buildPlan(ctx, /* withOrphans */ false);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* PURE. Compute the update/reconcile plan for one agent (spec 04 §4.2): identical to planInstall
|
|
46
|
+
* for create/overwrite/unchanged/skip-modified, PLUS manifest-scoped orphan removal — any path in
|
|
47
|
+
* `priorManifest.files` the current source no longer contains becomes `remove`. With no prior
|
|
48
|
+
* manifest, behaves exactly like planInstall (first install).
|
|
49
|
+
*/
|
|
50
|
+
export function planUpdate(ctx) {
|
|
51
|
+
return buildPlan(ctx, /* withOrphans */ true);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Convenience dispatcher used by cli.ts (07). Routes install/update to the typed planner;
|
|
55
|
+
* `uninstall` delegates to `planUninstall` (manifest-driven, from 05) — an absent prior manifest
|
|
56
|
+
* yields an empty-files plan (no-op).
|
|
57
|
+
*/
|
|
58
|
+
export function plan(subcommand, ctx) {
|
|
59
|
+
switch (subcommand) {
|
|
60
|
+
case "install":
|
|
61
|
+
return planInstall(ctx);
|
|
62
|
+
case "update":
|
|
63
|
+
return planUpdate(ctx);
|
|
64
|
+
case "uninstall":
|
|
65
|
+
return ctx.priorManifest === null
|
|
66
|
+
? ok({
|
|
67
|
+
agent: ctx.agent,
|
|
68
|
+
scope: ctx.scope,
|
|
69
|
+
mode: ctx.mode,
|
|
70
|
+
destination: ctx.destination,
|
|
71
|
+
files: [],
|
|
72
|
+
})
|
|
73
|
+
: planUninstall(ctx.priorManifest);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Resolve the effective materialization mode (spec 04, REQ-FLAG-03, D8). `--symlink` requests
|
|
78
|
+
* symlink, but Windows ALWAYS copies. Pure; `windows` is injectable for tests.
|
|
79
|
+
*/
|
|
80
|
+
export function resolveMode(wantSymlink, windows = isWindows()) {
|
|
81
|
+
return wantSymlink && !windows ? "symlink" : "copy";
|
|
82
|
+
}
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Internal — plan assembly
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
/** Shared core of planInstall/planUpdate (the orphan pass differs). */
|
|
87
|
+
function buildPlan(ctx, withOrphans) {
|
|
88
|
+
if (ctx.source === null) {
|
|
89
|
+
// A null source means 03 already failed to locate/validate the bundle (SOURCE_MISSING or
|
|
90
|
+
// SOURCE_INVALID). The CLI (07) surfaces 03's exact error; the planner only refuses to plan.
|
|
91
|
+
return err({
|
|
92
|
+
code: "SOURCE_MISSING",
|
|
93
|
+
agent: ctx.agent,
|
|
94
|
+
message: `no usable source bundle for agent "${ctx.agent}"`,
|
|
95
|
+
remedy: "run the adapters build to generate adapters/<agent>/, or pass --source <dir>",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const files = ctx.mode === "symlink"
|
|
99
|
+
? planSymlink(ctx)
|
|
100
|
+
: planCopy(ctx, withOrphans);
|
|
101
|
+
const action = {
|
|
102
|
+
agent: ctx.agent,
|
|
103
|
+
scope: ctx.scope,
|
|
104
|
+
mode: ctx.mode,
|
|
105
|
+
files,
|
|
106
|
+
...(ctx.raufPin !== undefined ? { raufPin: ctx.raufPin } : {}),
|
|
107
|
+
};
|
|
108
|
+
return ok(action);
|
|
109
|
+
}
|
|
110
|
+
/** Copy-mode per-file diff (spec 04 §6). `ctx.source` is non-null here. */
|
|
111
|
+
function planCopy(ctx, withOrphans) {
|
|
112
|
+
const source = ctx.source;
|
|
113
|
+
const manifestByPath = new Map();
|
|
114
|
+
for (const f of ctx.priorManifest?.files ?? [])
|
|
115
|
+
manifestByPath.set(f.path, f);
|
|
116
|
+
const actions = [];
|
|
117
|
+
for (const sf of source.files) {
|
|
118
|
+
const destAbs = path.join(ctx.destination, sf.relpath);
|
|
119
|
+
const destHash = hashIfExists(destAbs);
|
|
120
|
+
const manifestHash = manifestByPath.get(sf.relpath)?.sha256;
|
|
121
|
+
const kind = classifyFile(sf.relpath, sf.sha256, destHash, manifestHash, ctx.force);
|
|
122
|
+
actions.push({ relpath: sf.relpath, action: kind });
|
|
123
|
+
}
|
|
124
|
+
if (withOrphans && ctx.priorManifest !== null) {
|
|
125
|
+
actions.push(...orphanRemovals(ctx.priorManifest.files, source.files));
|
|
126
|
+
}
|
|
127
|
+
return actions;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Symlink-mode plan shape (spec 04 §6): a single synthetic FileAction for the namespace dir.
|
|
131
|
+
* The prior-state decision is read from the manifest (no `readlink`), keeping the planner pure.
|
|
132
|
+
*/
|
|
133
|
+
function planSymlink(ctx) {
|
|
134
|
+
const source = ctx.source;
|
|
135
|
+
const prior = ctx.priorManifest;
|
|
136
|
+
const priorExists = prior !== null;
|
|
137
|
+
const priorIsLiveSymlinkToSameTarget = priorExists && prior.link?.target === source.root;
|
|
138
|
+
const action = priorIsLiveSymlinkToSameTarget
|
|
139
|
+
? "unchanged"
|
|
140
|
+
: priorExists
|
|
141
|
+
? ctx.force
|
|
142
|
+
? "overwrite"
|
|
143
|
+
: "skip-modified"
|
|
144
|
+
: "create";
|
|
145
|
+
return [{ relpath: ".", action }];
|
|
146
|
+
}
|
|
147
|
+
/** Paths in the prior manifest that the current source no longer contains → `remove` (row 6). */
|
|
148
|
+
function orphanRemovals(priorFiles, sourceFiles) {
|
|
149
|
+
const inSource = new Set(sourceFiles.map((f) => f.relpath));
|
|
150
|
+
return priorFiles
|
|
151
|
+
.filter((f) => !inSource.has(f.path))
|
|
152
|
+
.map((f) => ({ relpath: f.path, action: "remove" }));
|
|
153
|
+
}
|
|
154
|
+
/** sha256 of a destination file if it exists as a regular file, else undefined. PURE read. */
|
|
155
|
+
function hashIfExists(absPath) {
|
|
156
|
+
let st;
|
|
157
|
+
try {
|
|
158
|
+
st = fs.lstatSync(absPath);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
if (!st.isFile())
|
|
164
|
+
return undefined;
|
|
165
|
+
return sha256File(absPath);
|
|
166
|
+
}
|
package/dist/rauf.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rauf provisioning (spec 06): the single pinned rauf coordinate (`RAUF_PIN`), the
|
|
3
|
+
* install-time read-only resolvability preflight, the `--skip-rauf` short-circuit, and the
|
|
4
|
+
* fixed unavailable-pin failure mode.
|
|
5
|
+
*
|
|
6
|
+
* Scope: this module makes rauf the *provisioned default* loop runner by recording a pin and
|
|
7
|
+
* preflighting its resolvability — it never vendors a binary, never mutates global npm state,
|
|
8
|
+
* never invokes rauf, and performs NO filesystem write. The only side effect is the read-only
|
|
9
|
+
* `npm view` registry query in the default registry query (skipped when `opts.skip` or an
|
|
10
|
+
* injected query is used). Named exports only; zero runtime dependencies (only `node:`
|
|
11
|
+
* built-ins). No throw for expected errors — returns `Result<T, E>` from `00-core-definitions`.
|
|
12
|
+
*/
|
|
13
|
+
import { type Result } from "./types.js";
|
|
14
|
+
/**
|
|
15
|
+
* The single pinned rauf coordinate the install provisions as the default loop runner
|
|
16
|
+
* (REQ-RAUF-03). One source of truth: re-exported by `src/index.ts` so importers and the
|
|
17
|
+
* downstream `forge-rauf-loop-default` read the same value, and recorded into each manifest
|
|
18
|
+
* as `InstallManifest.raufPin` (05-manifest-and-uninstall.md).
|
|
19
|
+
*
|
|
20
|
+
* Shape: `<name>@<version>` — UNSCOPED `rauf`. Advanced on each feature-forge release to a new
|
|
21
|
+
* known-compatible rauf (REQ-RAUF-03). The current rauf version is 0.6.0.
|
|
22
|
+
*
|
|
23
|
+
* Correctable config (OQ-C): the FINAL published coordinate is confirmed by `packaging-docs-ci`
|
|
24
|
+
* when rauf's publish path is stood up. Until then this resolves to a package that does not yet
|
|
25
|
+
* exist on npm (IR-2), so the preflight WILL fail — the known, designed failure mode, not a bug.
|
|
26
|
+
*/
|
|
27
|
+
export declare const RAUF_PIN = "rauf@0.6.0";
|
|
28
|
+
/**
|
|
29
|
+
* An injectable, READ-ONLY registry query (D1). Given a coordinate `name@version`, returns the
|
|
30
|
+
* resolved version string on success, or an `InstallerError` if it is not resolvable.
|
|
31
|
+
*
|
|
32
|
+
* Injectable so tests mock the registry with NO real network: the default implementation
|
|
33
|
+
* (`defaultRegistryQuery`) shells `npm view <coordinate> version`; a test passes a stub
|
|
34
|
+
* returning `ok("0.6.0")` or `err({ code: "RAUF_UNRESOLVABLE", ... })`.
|
|
35
|
+
*
|
|
36
|
+
* Contract: the query MUST be read-only — it MUST NOT install, MUST NOT mutate global npm
|
|
37
|
+
* state, and MUST NOT execute rauf. `npm view` satisfies this (it only reads registry metadata).
|
|
38
|
+
*
|
|
39
|
+
* @param coordinate - the `name@version` to resolve, e.g. "rauf@0.6.0"
|
|
40
|
+
* @returns Result<string> — the resolved version on success; RAUF_UNRESOLVABLE on failure.
|
|
41
|
+
*/
|
|
42
|
+
export type RegistryQuery = (coordinate: string) => Result<string>;
|
|
43
|
+
/** Options for the rauf preflight. */
|
|
44
|
+
export interface PreflightRaufOpts {
|
|
45
|
+
/**
|
|
46
|
+
* When true (the `--skip-rauf` flag), skip the preflight entirely: perform NO network call
|
|
47
|
+
* and return `{ raufPin: null }`. For environments that knowingly defer rauf (e.g. CI
|
|
48
|
+
* dry-runs while rauf is unpublished — IR-2).
|
|
49
|
+
*/
|
|
50
|
+
readonly skip?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* The registry query to use. Default: `defaultRegistryQuery` (`npm view rauf@<pin> version`
|
|
53
|
+
* via node:child_process). Tests inject a stub so no real network call is made.
|
|
54
|
+
*/
|
|
55
|
+
readonly query?: RegistryQuery;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Resolvability preflight for the pinned default loop runner (D1; REQ-RAUF-01/02/03, OQ-1).
|
|
59
|
+
*
|
|
60
|
+
* Behavior:
|
|
61
|
+
* - `opts.skip` (the `--skip-rauf` flag) ⇒ return `ok({ raufPin: null })` immediately, with NO
|
|
62
|
+
* network call.
|
|
63
|
+
* - otherwise ⇒ run a READ-ONLY registry resolvability check on `RAUF_PIN` (default query:
|
|
64
|
+
* `npm view rauf@<pin> version`). No install, no global-npm mutation, no execution of rauf.
|
|
65
|
+
* · resolvable ⇒ return `ok({ raufPin: RAUF_PIN })` — the value the manifest records.
|
|
66
|
+
* · unresolvable ⇒ return `err(<RAUF_UNRESOLVABLE>)` carrying the FIXED message (§6).
|
|
67
|
+
*
|
|
68
|
+
* NEVER throws for the expected unresolvable case — that is an `err(...)`. An unexpected spawn
|
|
69
|
+
* failure inside the default query is normalized to the same `RAUF_UNRESOLVABLE` error, so
|
|
70
|
+
* callers handle one code. Performs no filesystem write.
|
|
71
|
+
*
|
|
72
|
+
* @param opts - skip flag and/or an injected registry query (tests)
|
|
73
|
+
* @returns Result<{ raufPin: string | null }>:
|
|
74
|
+
* ok + `raufPin: RAUF_PIN` when resolvable,
|
|
75
|
+
* ok + `raufPin: null` when skipped,
|
|
76
|
+
* err(RAUF_UNRESOLVABLE) when the pin is not resolvable.
|
|
77
|
+
*/
|
|
78
|
+
export declare function preflightRauf(opts?: {
|
|
79
|
+
skip?: boolean;
|
|
80
|
+
query?: RegistryQuery;
|
|
81
|
+
}): Result<{
|
|
82
|
+
raufPin: string | null;
|
|
83
|
+
}>;
|
package/dist/rauf.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rauf provisioning (spec 06): the single pinned rauf coordinate (`RAUF_PIN`), the
|
|
3
|
+
* install-time read-only resolvability preflight, the `--skip-rauf` short-circuit, and the
|
|
4
|
+
* fixed unavailable-pin failure mode.
|
|
5
|
+
*
|
|
6
|
+
* Scope: this module makes rauf the *provisioned default* loop runner by recording a pin and
|
|
7
|
+
* preflighting its resolvability — it never vendors a binary, never mutates global npm state,
|
|
8
|
+
* never invokes rauf, and performs NO filesystem write. The only side effect is the read-only
|
|
9
|
+
* `npm view` registry query in the default registry query (skipped when `opts.skip` or an
|
|
10
|
+
* injected query is used). Named exports only; zero runtime dependencies (only `node:`
|
|
11
|
+
* built-ins). No throw for expected errors — returns `Result<T, E>` from `00-core-definitions`.
|
|
12
|
+
*/
|
|
13
|
+
import { spawnSync } from "node:child_process";
|
|
14
|
+
import { err, ok } from "./types.js";
|
|
15
|
+
/**
|
|
16
|
+
* The single pinned rauf coordinate the install provisions as the default loop runner
|
|
17
|
+
* (REQ-RAUF-03). One source of truth: re-exported by `src/index.ts` so importers and the
|
|
18
|
+
* downstream `forge-rauf-loop-default` read the same value, and recorded into each manifest
|
|
19
|
+
* as `InstallManifest.raufPin` (05-manifest-and-uninstall.md).
|
|
20
|
+
*
|
|
21
|
+
* Shape: `<name>@<version>` — UNSCOPED `rauf`. Advanced on each feature-forge release to a new
|
|
22
|
+
* known-compatible rauf (REQ-RAUF-03). The current rauf version is 0.6.0.
|
|
23
|
+
*
|
|
24
|
+
* Correctable config (OQ-C): the FINAL published coordinate is confirmed by `packaging-docs-ci`
|
|
25
|
+
* when rauf's publish path is stood up. Until then this resolves to a package that does not yet
|
|
26
|
+
* exist on npm (IR-2), so the preflight WILL fail — the known, designed failure mode, not a bug.
|
|
27
|
+
*/
|
|
28
|
+
export const RAUF_PIN = "rauf@0.6.0";
|
|
29
|
+
/**
|
|
30
|
+
* Resolvability preflight for the pinned default loop runner (D1; REQ-RAUF-01/02/03, OQ-1).
|
|
31
|
+
*
|
|
32
|
+
* Behavior:
|
|
33
|
+
* - `opts.skip` (the `--skip-rauf` flag) ⇒ return `ok({ raufPin: null })` immediately, with NO
|
|
34
|
+
* network call.
|
|
35
|
+
* - otherwise ⇒ run a READ-ONLY registry resolvability check on `RAUF_PIN` (default query:
|
|
36
|
+
* `npm view rauf@<pin> version`). No install, no global-npm mutation, no execution of rauf.
|
|
37
|
+
* · resolvable ⇒ return `ok({ raufPin: RAUF_PIN })` — the value the manifest records.
|
|
38
|
+
* · unresolvable ⇒ return `err(<RAUF_UNRESOLVABLE>)` carrying the FIXED message (§6).
|
|
39
|
+
*
|
|
40
|
+
* NEVER throws for the expected unresolvable case — that is an `err(...)`. An unexpected spawn
|
|
41
|
+
* failure inside the default query is normalized to the same `RAUF_UNRESOLVABLE` error, so
|
|
42
|
+
* callers handle one code. Performs no filesystem write.
|
|
43
|
+
*
|
|
44
|
+
* @param opts - skip flag and/or an injected registry query (tests)
|
|
45
|
+
* @returns Result<{ raufPin: string | null }>:
|
|
46
|
+
* ok + `raufPin: RAUF_PIN` when resolvable,
|
|
47
|
+
* ok + `raufPin: null` when skipped,
|
|
48
|
+
* err(RAUF_UNRESOLVABLE) when the pin is not resolvable.
|
|
49
|
+
*/
|
|
50
|
+
export function preflightRauf(opts) {
|
|
51
|
+
// --skip-rauf: no network, record null.
|
|
52
|
+
if (opts?.skip) {
|
|
53
|
+
return ok({ raufPin: null });
|
|
54
|
+
}
|
|
55
|
+
const query = opts?.query ?? defaultRegistryQuery;
|
|
56
|
+
const resolved = query(RAUF_PIN);
|
|
57
|
+
if (resolved.ok) {
|
|
58
|
+
// Resolvable: record the pin. (We deliberately ignore the resolved version string — the
|
|
59
|
+
// recorded coordinate is RAUF_PIN itself, the single source of truth, REQ-RAUF-03.)
|
|
60
|
+
return ok({ raufPin: RAUF_PIN });
|
|
61
|
+
}
|
|
62
|
+
// Unresolvable: the designed failure mode (§6). Surface the FIXED, actionable error,
|
|
63
|
+
// regardless of any message the injected query returned.
|
|
64
|
+
return err(raufUnresolvableError());
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Internal: the default read-only registry query. Runs `npm view <coordinate> version` via
|
|
68
|
+
* `node:child_process.spawnSync` — registry metadata read ONLY (no install, no global mutation,
|
|
69
|
+
* no rauf execution). Network is permitted at install (C-7).
|
|
70
|
+
*
|
|
71
|
+
* Resolution rule:
|
|
72
|
+
* - exit code 0 AND non-empty stdout ⇒ ok(trimmed stdout) (the resolved version).
|
|
73
|
+
* - anything else (non-zero exit, E404, a spawn error, npm absent) ⇒ err(RAUF_UNRESOLVABLE).
|
|
74
|
+
*
|
|
75
|
+
* NOT exported as public API — `preflightRauf`'s `query` option is the seam tests use.
|
|
76
|
+
*/
|
|
77
|
+
function defaultRegistryQuery(coordinate) {
|
|
78
|
+
let res;
|
|
79
|
+
try {
|
|
80
|
+
res = spawnSync("npm", ["view", coordinate, "version"], {
|
|
81
|
+
encoding: "utf8",
|
|
82
|
+
// No shell; argv form avoids injection. Timeout bounds a hung registry.
|
|
83
|
+
timeout: 30_000,
|
|
84
|
+
windowsHide: true,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// spawn itself threw (e.g. npm not found on some platforms) — treat as unresolvable.
|
|
89
|
+
return err(raufUnresolvableError());
|
|
90
|
+
}
|
|
91
|
+
if (res.error || res.status !== 0) {
|
|
92
|
+
return err(raufUnresolvableError());
|
|
93
|
+
}
|
|
94
|
+
const version = String(res.stdout ?? "").trim();
|
|
95
|
+
if (version.length === 0) {
|
|
96
|
+
return err(raufUnresolvableError());
|
|
97
|
+
}
|
|
98
|
+
return ok(version);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Internal: builds the structured RAUF_UNRESOLVABLE error with the FIXED message (§6,
|
|
102
|
+
* REQ-OBS-02). `<pin>` in the message is substituted with `RAUF_PIN`. Single constructor so the
|
|
103
|
+
* wording is identical everywhere the failure can arise (preflight + default query).
|
|
104
|
+
*/
|
|
105
|
+
function raufUnresolvableError() {
|
|
106
|
+
return {
|
|
107
|
+
code: "RAUF_UNRESOLVABLE",
|
|
108
|
+
message: "pinned default loop runner `" +
|
|
109
|
+
RAUF_PIN +
|
|
110
|
+
"` is not resolvable from the npm registry. Network is required at " +
|
|
111
|
+
"install; if rauf is not yet published this is the known cross-repo " +
|
|
112
|
+
"prerequisite (see packaging-docs-ci). Skills were still installed; " +
|
|
113
|
+
"the default loop will be unavailable until rauf publishes.",
|
|
114
|
+
remedy: "Ensure network access and that `" +
|
|
115
|
+
RAUF_PIN +
|
|
116
|
+
"` is published, or re-run with `--skip-rauf` to defer the default loop.",
|
|
117
|
+
};
|
|
118
|
+
}
|