@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/fsutil.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Result } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve `segs` against `root` and assert the result lies WITHIN `root` (REQ-SEC-02). Returns the
|
|
4
|
+
* resolved absolute path on success, or a PATH_ESCAPE InstallerError if a `..` segment or a
|
|
5
|
+
* malformed agent id would escape the agent config root. MUST be called before ANY write/delete.
|
|
6
|
+
*
|
|
7
|
+
* Uses `path.relative` + the `..` prefix test (the robust boundary check — a bare string startsWith
|
|
8
|
+
* would false-pass `/root-evil`).
|
|
9
|
+
*
|
|
10
|
+
* @param root - the containment boundary (the agent config root, e.g. <home>/.claude)
|
|
11
|
+
* @param segs - path segments to join under root (destination, then a bundle-relative path)
|
|
12
|
+
* @returns ok(absolutePath) if inside; err(PATH_ESCAPE) otherwise.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveWithin(root: string, ...segs: string[]): Result<string>;
|
|
15
|
+
/**
|
|
16
|
+
* Recursively copy `src` → `dest` (copy mode). The CALLER is responsible for having
|
|
17
|
+
* containment-checked `dest` via resolveWithin first (REQ-SEC-02).
|
|
18
|
+
*
|
|
19
|
+
* @returns ok(undefined) on success; err(WRITE_DENIED) on EACCES/EPERM, naming `dest`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function copyDir(src: string, dest: string): Promise<Result<void>>;
|
|
22
|
+
/**
|
|
23
|
+
* Link the whole namespace dir `linkPath` → `target` (REQ-FLAG-03). On Windows, OR if the symlink
|
|
24
|
+
* syscall fails for any reason, FALL BACK to copyDir and report it via the `mode` so apply records
|
|
25
|
+
* the truthful mode.
|
|
26
|
+
*
|
|
27
|
+
* @returns ok({ mode }) where mode is "symlink" (link created) or "copy" (fallback fired);
|
|
28
|
+
* err(WRITE_DENIED) if even the copy fallback fails.
|
|
29
|
+
*/
|
|
30
|
+
export declare function symlinkDir(target: string, linkPath: string): Promise<Result<{
|
|
31
|
+
mode: "symlink" | "copy";
|
|
32
|
+
}>>;
|
|
33
|
+
/**
|
|
34
|
+
* Remove `p` safely (REQ-SEC-03/REQ-SAFE-02). NEVER follows a symlink to delete its target — uses
|
|
35
|
+
* `lstat` (which does not dereference) to distinguish a symbolic link (unlink the link only), a real
|
|
36
|
+
* directory (recursive rm), and a real file (unlink). ENOENT → ok (idempotent removal).
|
|
37
|
+
*
|
|
38
|
+
* The CALLER must have containment-checked `p` via resolveWithin first (REQ-SEC-02).
|
|
39
|
+
*
|
|
40
|
+
* @returns ok(undefined) on success or already-absent; err(WRITE_DENIED) on EACCES/EPERM.
|
|
41
|
+
*/
|
|
42
|
+
export declare function removePath(p: string): Promise<Result<void>>;
|
|
43
|
+
/**
|
|
44
|
+
* Prune now-empty directories from `startDir` UPWARD, stopping before `stopRoot` (exclusive). Used by
|
|
45
|
+
* `apply`'s copy-mode uninstall (§5.3) after the recorded files are removed. NEVER removes a
|
|
46
|
+
* non-empty dir nor `stopRoot` itself (REQ-SAFE-01). A non-existent `cur` is treated as
|
|
47
|
+
* already-removed and the ascent continues.
|
|
48
|
+
*
|
|
49
|
+
* @param startDir - the deepest dir to consider pruning (e.g. the namespace dir).
|
|
50
|
+
* @param stopRoot - the boundary; pruning never removes `stopRoot` itself nor anything outside it.
|
|
51
|
+
* @returns ok(undefined) when the upward prune completes (or halts on a non-empty dir);
|
|
52
|
+
* err(PATH_ESCAPE) if `startDir` is not within `stopRoot`; err(WRITE_DENIED) on EACCES/EPERM.
|
|
53
|
+
*/
|
|
54
|
+
export declare function removeEmptyDirsWithin(startDir: string, stopRoot: string): Promise<Result<void>>;
|
|
55
|
+
/** Platform check (REQ-FLAG-03, C-6). Centralized so resolveMode/symlinkDir share one decision. */
|
|
56
|
+
export declare function isWindows(): boolean;
|
package/dist/fsutil.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandboxed filesystem primitives (spec 04 §7). Every filesystem mutation in `apply` routes through
|
|
3
|
+
* these — they are the single place REQ-SEC-01/02/03 are enforced. Cross-platform: node:fs/promises,
|
|
4
|
+
* node:path, node:os only (no shelling out). No throw for expected errors — all return `Result`.
|
|
5
|
+
*/
|
|
6
|
+
import * as fsp from "node:fs/promises";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import * as os from "node:os";
|
|
9
|
+
import { ok, err } from "./types.js";
|
|
10
|
+
/**
|
|
11
|
+
* Resolve `segs` against `root` and assert the result lies WITHIN `root` (REQ-SEC-02). Returns the
|
|
12
|
+
* resolved absolute path on success, or a PATH_ESCAPE InstallerError if a `..` segment or a
|
|
13
|
+
* malformed agent id would escape the agent config root. MUST be called before ANY write/delete.
|
|
14
|
+
*
|
|
15
|
+
* Uses `path.relative` + the `..` prefix test (the robust boundary check — a bare string startsWith
|
|
16
|
+
* would false-pass `/root-evil`).
|
|
17
|
+
*
|
|
18
|
+
* @param root - the containment boundary (the agent config root, e.g. <home>/.claude)
|
|
19
|
+
* @param segs - path segments to join under root (destination, then a bundle-relative path)
|
|
20
|
+
* @returns ok(absolutePath) if inside; err(PATH_ESCAPE) otherwise.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveWithin(root, ...segs) {
|
|
23
|
+
const base = path.resolve(root);
|
|
24
|
+
const target = path.resolve(base, ...segs);
|
|
25
|
+
const rel = path.relative(base, target);
|
|
26
|
+
const inside = rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
27
|
+
if (!inside) {
|
|
28
|
+
return err({
|
|
29
|
+
code: "PATH_ESCAPE",
|
|
30
|
+
message: `refusing to write outside the agent config root: resolved "${target}" escapes "${base}"`,
|
|
31
|
+
path: target,
|
|
32
|
+
remedy: "this indicates a malformed agent id or path segment; report it as a bug",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return ok(target);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Recursively copy `src` → `dest` (copy mode). The CALLER is responsible for having
|
|
39
|
+
* containment-checked `dest` via resolveWithin first (REQ-SEC-02).
|
|
40
|
+
*
|
|
41
|
+
* @returns ok(undefined) on success; err(WRITE_DENIED) on EACCES/EPERM, naming `dest`.
|
|
42
|
+
*/
|
|
43
|
+
export async function copyDir(src, dest) {
|
|
44
|
+
try {
|
|
45
|
+
await fsp.mkdir(path.dirname(dest), { recursive: true });
|
|
46
|
+
await fsp.cp(src, dest, { recursive: true, force: true, dereference: false });
|
|
47
|
+
return ok(undefined);
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
return err(toWriteError(e, dest));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Link the whole namespace dir `linkPath` → `target` (REQ-FLAG-03). On Windows, OR if the symlink
|
|
55
|
+
* syscall fails for any reason, FALL BACK to copyDir and report it via the `mode` so apply records
|
|
56
|
+
* the truthful mode.
|
|
57
|
+
*
|
|
58
|
+
* @returns ok({ mode }) where mode is "symlink" (link created) or "copy" (fallback fired);
|
|
59
|
+
* err(WRITE_DENIED) if even the copy fallback fails.
|
|
60
|
+
*/
|
|
61
|
+
export async function symlinkDir(target, linkPath) {
|
|
62
|
+
if (isWindows()) {
|
|
63
|
+
const copied = await copyDir(target, linkPath);
|
|
64
|
+
return copied.ok ? ok({ mode: "copy" }) : err(copied.error);
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
await fsp.mkdir(path.dirname(linkPath), { recursive: true });
|
|
68
|
+
await fsp.symlink(target, linkPath, "dir");
|
|
69
|
+
return ok({ mode: "symlink" });
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
const copied = await copyDir(target, linkPath);
|
|
73
|
+
return copied.ok ? ok({ mode: "copy" }) : err(copied.error);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Remove `p` safely (REQ-SEC-03/REQ-SAFE-02). NEVER follows a symlink to delete its target — uses
|
|
78
|
+
* `lstat` (which does not dereference) to distinguish a symbolic link (unlink the link only), a real
|
|
79
|
+
* directory (recursive rm), and a real file (unlink). ENOENT → ok (idempotent removal).
|
|
80
|
+
*
|
|
81
|
+
* The CALLER must have containment-checked `p` via resolveWithin first (REQ-SEC-02).
|
|
82
|
+
*
|
|
83
|
+
* @returns ok(undefined) on success or already-absent; err(WRITE_DENIED) on EACCES/EPERM.
|
|
84
|
+
*/
|
|
85
|
+
export async function removePath(p) {
|
|
86
|
+
let st;
|
|
87
|
+
try {
|
|
88
|
+
st = await fsp.lstat(p); // lstat: does NOT dereference the link
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
if (e.code === "ENOENT")
|
|
92
|
+
return ok(undefined);
|
|
93
|
+
return err(toWriteError(e, p));
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
if (st.isSymbolicLink()) {
|
|
97
|
+
await fsp.unlink(p); // remove the LINK only — never the target (REQ-SAFE-02)
|
|
98
|
+
}
|
|
99
|
+
else if (st.isDirectory()) {
|
|
100
|
+
await fsp.rm(p, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
await fsp.unlink(p);
|
|
104
|
+
}
|
|
105
|
+
return ok(undefined);
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
return err(toWriteError(e, p));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Prune now-empty directories from `startDir` UPWARD, stopping before `stopRoot` (exclusive). Used by
|
|
113
|
+
* `apply`'s copy-mode uninstall (§5.3) after the recorded files are removed. NEVER removes a
|
|
114
|
+
* non-empty dir nor `stopRoot` itself (REQ-SAFE-01). A non-existent `cur` is treated as
|
|
115
|
+
* already-removed and the ascent continues.
|
|
116
|
+
*
|
|
117
|
+
* @param startDir - the deepest dir to consider pruning (e.g. the namespace dir).
|
|
118
|
+
* @param stopRoot - the boundary; pruning never removes `stopRoot` itself nor anything outside it.
|
|
119
|
+
* @returns ok(undefined) when the upward prune completes (or halts on a non-empty dir);
|
|
120
|
+
* err(PATH_ESCAPE) if `startDir` is not within `stopRoot`; err(WRITE_DENIED) on EACCES/EPERM.
|
|
121
|
+
*/
|
|
122
|
+
export async function removeEmptyDirsWithin(startDir, stopRoot) {
|
|
123
|
+
const contained = resolveWithin(stopRoot, startDir);
|
|
124
|
+
if (!contained.ok)
|
|
125
|
+
return contained;
|
|
126
|
+
const stop = path.resolve(stopRoot);
|
|
127
|
+
let cur = contained.value;
|
|
128
|
+
while (cur !== stop && cur.startsWith(stop)) {
|
|
129
|
+
let entries;
|
|
130
|
+
try {
|
|
131
|
+
entries = await fsp.readdir(cur);
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
if (e.code === "ENOENT") {
|
|
135
|
+
cur = path.dirname(cur);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
return err(toWriteError(e, cur));
|
|
139
|
+
}
|
|
140
|
+
if (entries.length > 0)
|
|
141
|
+
break; // non-empty ⇒ MUST NOT remove (REQ-SAFE-01) — halt
|
|
142
|
+
try {
|
|
143
|
+
await fsp.rmdir(cur); // remove this now-empty dir only
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
return err(toWriteError(e, cur));
|
|
147
|
+
}
|
|
148
|
+
cur = path.dirname(cur); // ascend
|
|
149
|
+
}
|
|
150
|
+
return ok(undefined);
|
|
151
|
+
}
|
|
152
|
+
/** Platform check (REQ-FLAG-03, C-6). Centralized so resolveMode/symlinkDir share one decision. */
|
|
153
|
+
export function isWindows() {
|
|
154
|
+
return os.platform() === "win32";
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Map a caught fs exception to an actionable InstallerError (REQ-OBS-02). EACCES/EPERM → WRITE_DENIED;
|
|
158
|
+
* anything else → UNEXPECTED carrying the message. Internal helper (not exported as public surface).
|
|
159
|
+
*/
|
|
160
|
+
function toWriteError(e, p) {
|
|
161
|
+
const code = e?.code;
|
|
162
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
163
|
+
return {
|
|
164
|
+
code: "WRITE_DENIED",
|
|
165
|
+
message: `no write permission to ${p}`,
|
|
166
|
+
path: p,
|
|
167
|
+
remedy: "check directory permissions, or choose a different scope (--global vs project)",
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
code: "UNEXPECTED",
|
|
172
|
+
message: `filesystem error at ${p}: ${e?.message ?? String(e)}`,
|
|
173
|
+
path: p,
|
|
174
|
+
};
|
|
175
|
+
}
|
package/dist/hash.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content hashing for the cross-agent installer (spec 03 §3.3-§3.6, OQ-4).
|
|
3
|
+
*
|
|
4
|
+
* Drift detection is decided by SHA-256 *content* hashing, NEVER mtime. The tree digest is a
|
|
5
|
+
* function of the set of `{ relativePosixPath, fileContentHash }` pairs only, so two
|
|
6
|
+
* materializations of the same bundle at different paths/times hash identically. Zero runtime
|
|
7
|
+
* dependencies — only `node:` built-ins.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* SHA-256 of a single file's bytes, hex-encoded (OQ-4 — content hash, never mtime).
|
|
11
|
+
*
|
|
12
|
+
* @param filePath - Absolute path to a regular file.
|
|
13
|
+
* @returns 64-char lowercase hex digest of the file's bytes.
|
|
14
|
+
* @throws Propagates the underlying node:fs error (ENOENT/EACCES) — an *unexpected* IO failure
|
|
15
|
+
* for an already-located, integrity-checked bundle, caught at the operation boundary.
|
|
16
|
+
*/
|
|
17
|
+
export declare function sha256File(filePath: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Deterministic SHA-256 over a directory tree's file set (OQ-4). The digest is a function of the
|
|
20
|
+
* set of `{ relativePosixPath, fileContentHash }` pairs ONLY — never of mtime, inode, or
|
|
21
|
+
* traversal order.
|
|
22
|
+
*
|
|
23
|
+
* Canonical form: walk regular files, compute POSIX-relative paths, sort byte-wise, then fold a
|
|
24
|
+
* single hash over `update(rel); update("\0"); update(contentHash); update("\n")` per file.
|
|
25
|
+
*
|
|
26
|
+
* @param dir - Absolute path to the directory whose tree to hash.
|
|
27
|
+
* @returns 64-char lowercase hex digest, invariant under relocation and traversal order.
|
|
28
|
+
*/
|
|
29
|
+
export declare function sha256Tree(dir: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Compute the `sourceHash` stored in InstallManifest (spec 00 §3, spec 03 §3.4). It is exactly
|
|
32
|
+
* `sha256Tree(bundlePath)` — the sorted-path canonical digest over the bundle's file set, so two
|
|
33
|
+
* materializations of the same bundle produce the SAME hash (REQ-IDEM-01 basis).
|
|
34
|
+
*
|
|
35
|
+
* @param bundlePath - Absolute path to a *located, integrity-checked* bundle dir.
|
|
36
|
+
* @returns 64-char lowercase hex digest — store verbatim in `InstallManifest.sourceHash`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function computeSourceHash(bundlePath: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* The bundle's per-file inventory: every regular file under `bundlePath`, each as its
|
|
41
|
+
* bundle-relative POSIX path plus the content `sha256`. Sorted by relative POSIX path — the SAME
|
|
42
|
+
* sorted walk `sha256Tree` folds over, so the inventory and `sourceHash` always agree.
|
|
43
|
+
*
|
|
44
|
+
* @param bundlePath - Absolute path to a located, integrity-checked bundle dir.
|
|
45
|
+
* @returns Array of `{ relpath, sha256 }`, sorted by `relpath` (POSIX `/` separators).
|
|
46
|
+
*/
|
|
47
|
+
export declare function listBundleFiles(bundlePath: string): Array<{
|
|
48
|
+
relpath: string;
|
|
49
|
+
sha256: string;
|
|
50
|
+
}>;
|
package/dist/hash.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content hashing for the cross-agent installer (spec 03 §3.3-§3.6, OQ-4).
|
|
3
|
+
*
|
|
4
|
+
* Drift detection is decided by SHA-256 *content* hashing, NEVER mtime. The tree digest is a
|
|
5
|
+
* function of the set of `{ relativePosixPath, fileContentHash }` pairs only, so two
|
|
6
|
+
* materializations of the same bundle at different paths/times hash identically. Zero runtime
|
|
7
|
+
* dependencies — only `node:` built-ins.
|
|
8
|
+
*/
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
/**
|
|
13
|
+
* SHA-256 of a single file's bytes, hex-encoded (OQ-4 — content hash, never mtime).
|
|
14
|
+
*
|
|
15
|
+
* @param filePath - Absolute path to a regular file.
|
|
16
|
+
* @returns 64-char lowercase hex digest of the file's bytes.
|
|
17
|
+
* @throws Propagates the underlying node:fs error (ENOENT/EACCES) — an *unexpected* IO failure
|
|
18
|
+
* for an already-located, integrity-checked bundle, caught at the operation boundary.
|
|
19
|
+
*/
|
|
20
|
+
export function sha256File(filePath) {
|
|
21
|
+
const buf = fs.readFileSync(filePath);
|
|
22
|
+
return createHash("sha256").update(buf).digest("hex");
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Deterministic SHA-256 over a directory tree's file set (OQ-4). The digest is a function of the
|
|
26
|
+
* set of `{ relativePosixPath, fileContentHash }` pairs ONLY — never of mtime, inode, or
|
|
27
|
+
* traversal order.
|
|
28
|
+
*
|
|
29
|
+
* Canonical form: walk regular files, compute POSIX-relative paths, sort byte-wise, then fold a
|
|
30
|
+
* single hash over `update(rel); update("\0"); update(contentHash); update("\n")` per file.
|
|
31
|
+
*
|
|
32
|
+
* @param dir - Absolute path to the directory whose tree to hash.
|
|
33
|
+
* @returns 64-char lowercase hex digest, invariant under relocation and traversal order.
|
|
34
|
+
*/
|
|
35
|
+
export function sha256Tree(dir) {
|
|
36
|
+
const files = walkFiles(dir);
|
|
37
|
+
const entries = files
|
|
38
|
+
.map((abs) => ({
|
|
39
|
+
rel: toPosix(path.relative(dir, abs)),
|
|
40
|
+
contentHash: sha256File(abs),
|
|
41
|
+
}))
|
|
42
|
+
.sort((a, b) => (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0));
|
|
43
|
+
const h = createHash("sha256");
|
|
44
|
+
for (const e of entries) {
|
|
45
|
+
h.update(e.rel);
|
|
46
|
+
h.update("\0");
|
|
47
|
+
h.update(e.contentHash);
|
|
48
|
+
h.update("\n");
|
|
49
|
+
}
|
|
50
|
+
return h.digest("hex");
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Compute the `sourceHash` stored in InstallManifest (spec 00 §3, spec 03 §3.4). It is exactly
|
|
54
|
+
* `sha256Tree(bundlePath)` — the sorted-path canonical digest over the bundle's file set, so two
|
|
55
|
+
* materializations of the same bundle produce the SAME hash (REQ-IDEM-01 basis).
|
|
56
|
+
*
|
|
57
|
+
* @param bundlePath - Absolute path to a *located, integrity-checked* bundle dir.
|
|
58
|
+
* @returns 64-char lowercase hex digest — store verbatim in `InstallManifest.sourceHash`.
|
|
59
|
+
*/
|
|
60
|
+
export function computeSourceHash(bundlePath) {
|
|
61
|
+
return sha256Tree(bundlePath);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The bundle's per-file inventory: every regular file under `bundlePath`, each as its
|
|
65
|
+
* bundle-relative POSIX path plus the content `sha256`. Sorted by relative POSIX path — the SAME
|
|
66
|
+
* sorted walk `sha256Tree` folds over, so the inventory and `sourceHash` always agree.
|
|
67
|
+
*
|
|
68
|
+
* @param bundlePath - Absolute path to a located, integrity-checked bundle dir.
|
|
69
|
+
* @returns Array of `{ relpath, sha256 }`, sorted by `relpath` (POSIX `/` separators).
|
|
70
|
+
*/
|
|
71
|
+
export function listBundleFiles(bundlePath) {
|
|
72
|
+
const files = walkFiles(bundlePath);
|
|
73
|
+
return files
|
|
74
|
+
.map((abs) => ({
|
|
75
|
+
relpath: toPosix(path.relative(bundlePath, abs)),
|
|
76
|
+
sha256: sha256File(abs),
|
|
77
|
+
}))
|
|
78
|
+
.sort((a, b) => (a.relpath < b.relpath ? -1 : a.relpath > b.relpath ? 1 : 0));
|
|
79
|
+
}
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Internal helpers (module-private — spec 03 §4.2)
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
/**
|
|
84
|
+
* Recursively collect every REGULAR FILE under `dir` (absolute paths). Directories contribute only
|
|
85
|
+
* via their files; symlink entries are NOT followed and NOT included. Order is unspecified —
|
|
86
|
+
* callers sort by relative path so traversal order never affects the digest.
|
|
87
|
+
*/
|
|
88
|
+
function walkFiles(dir) {
|
|
89
|
+
const out = [];
|
|
90
|
+
const stack = [dir];
|
|
91
|
+
while (stack.length > 0) {
|
|
92
|
+
const cur = stack.pop();
|
|
93
|
+
for (const ent of fs.readdirSync(cur, { withFileTypes: true })) {
|
|
94
|
+
const abs = path.join(cur, ent.name);
|
|
95
|
+
if (ent.isDirectory())
|
|
96
|
+
stack.push(abs);
|
|
97
|
+
else if (ent.isFile())
|
|
98
|
+
out.push(abs);
|
|
99
|
+
// symlinks / sockets / fifos: ignored (not expected in a copied bundle)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
/** Normalize an OS-relative path to POSIX separators so hashes match across Windows and POSIX. */
|
|
105
|
+
function toPosix(rel) {
|
|
106
|
+
return rel.split(path.sep).join("/");
|
|
107
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library barrel (spec 01 §4): the public surface of the cross-agent installer as a Node
|
|
3
|
+
* library. Re-exports the agent-detection-map surface, the rauf pin, and the shared spec-00
|
|
4
|
+
* types. Named exports only; no runtime logic of its own.
|
|
5
|
+
*/
|
|
6
|
+
export { AGENT_TARGETS, resolveRoots, destinationFor, detectAgent, detectAgents, formatZeroDetection, } from "./agent-targets.js";
|
|
7
|
+
export { RAUF_PIN } from "./rauf.js";
|
|
8
|
+
export type { AgentId, AgentTarget, DetectionResult, ResolveOpts, Scope, Mode, InstallManifest, PlannedAction, RunReport, } from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library barrel (spec 01 §4): the public surface of the cross-agent installer as a Node
|
|
3
|
+
* library. Re-exports the agent-detection-map surface, the rauf pin, and the shared spec-00
|
|
4
|
+
* types. Named exports only; no runtime logic of its own.
|
|
5
|
+
*/
|
|
6
|
+
// The agent-detection-map surface (spec 02).
|
|
7
|
+
export { AGENT_TARGETS, resolveRoots, destinationFor, detectAgent, detectAgents, formatZeroDetection, } from "./agent-targets.js";
|
|
8
|
+
// The pinned default loop-runner coordinate (spec 06).
|
|
9
|
+
export { RAUF_PIN } from "./rauf.js";
|
|
@@ -0,0 +1,72 @@
|
|
|
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 { type AgentId, type InstallManifest, type ManifestFile, type Mode, type PlannedAction, type ResolveOpts, type Result, type Scope } from "./types.js";
|
|
12
|
+
/**
|
|
13
|
+
* Inputs to {@link buildManifest}. The caller (apply.ts, spec 04) assembles this from the resolved
|
|
14
|
+
* detection target, the chosen scope/mode, and the apply result's per-file inventory.
|
|
15
|
+
*/
|
|
16
|
+
export interface BuildManifestArgs {
|
|
17
|
+
readonly agent: AgentId;
|
|
18
|
+
readonly scope: Scope;
|
|
19
|
+
readonly mode: Mode;
|
|
20
|
+
/** Absolute path of the `feature-forge/` namespace dir this manifest governs. */
|
|
21
|
+
readonly destination: string;
|
|
22
|
+
/**
|
|
23
|
+
* Per-file inventory of what was written, paths relative to `destination`. In `"symlink"` mode
|
|
24
|
+
* this is `[]` (no per-file copy exists). In `"copy"` mode each entry carries its `sha256`.
|
|
25
|
+
*/
|
|
26
|
+
readonly files: readonly ManifestFile[];
|
|
27
|
+
/** Installed skill ids (the bundle's `skills/*` dir names). */
|
|
28
|
+
readonly skills: readonly string[];
|
|
29
|
+
/** SHA-256 over the source bundle's canonical (sorted-path) file set — drift anchor (spec 03). */
|
|
30
|
+
readonly sourceHash: string;
|
|
31
|
+
/** Recorded pinned rauf coordinate (e.g. "rauf@0.6.0"); `null` when `--skip-rauf` (spec 06). */
|
|
32
|
+
readonly raufPin: string | null;
|
|
33
|
+
/** Symlink mode only: the source bundle the namespace dir links to (REQ-SAFE-02). */
|
|
34
|
+
readonly link?: {
|
|
35
|
+
readonly target: string;
|
|
36
|
+
};
|
|
37
|
+
/** Prior manifest, if any. When present, its `installedAt` is preserved (this is an update). */
|
|
38
|
+
readonly previous?: InstallManifest | null;
|
|
39
|
+
/** Injectable clock for deterministic tests. Default: `() => new Date()`. */
|
|
40
|
+
readonly now?: () => Date;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Assemble an {@link InstallManifest} from an apply result (REQ-SAFE-01/03). Pure — no I/O.
|
|
44
|
+
*
|
|
45
|
+
* Timestamp policy: `updatedAt` is always "now"; `installedAt` is `previous.installedAt` when
|
|
46
|
+
* reconciling an existing install, else "now". `featureForgeVersion` is always `null` today
|
|
47
|
+
* (OQ-A/IR-1; C-3 forbids synthesizing one).
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildManifest(args: BuildManifestArgs): InstallManifest;
|
|
50
|
+
/**
|
|
51
|
+
* Absolute path of the hidden parent-sibling manifest for an agent + scope (D6/D8):
|
|
52
|
+
* `<scopeRoot>/<configDirName>/<installSubdir>/.feature-forge.<scope>.json`
|
|
53
|
+
* e.g. `~/.claude/skills/.feature-forge.global.json`. Identical for copy and symlink mode.
|
|
54
|
+
*/
|
|
55
|
+
export declare function manifestPath(agent: AgentId, scope: Scope, opts?: Omit<ResolveOpts, "scope">): string;
|
|
56
|
+
/**
|
|
57
|
+
* Read and validate the manifest at `p`. Absent (`ENOENT`) → `ok(null)`; present + valid →
|
|
58
|
+
* `ok(manifest)`; unreadable / invalid JSON / failed shape validation → `err(MANIFEST_CORRUPT)`.
|
|
59
|
+
*/
|
|
60
|
+
export declare function readManifest(p: string): Result<InstallManifest | null>;
|
|
61
|
+
/**
|
|
62
|
+
* Atomically write the manifest to `p` (write `<p>.tmp` → `rename`). Creates the parent dir if
|
|
63
|
+
* missing. Returns `err(WRITE_DENIED)` on a permission failure, cleaning up the temp file.
|
|
64
|
+
*/
|
|
65
|
+
export declare function writeManifest(p: string, m: InstallManifest): Result<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Compute the uninstall plan from a manifest (REQ-OPS-03, REQ-SAFE-01/02). PURE — no I/O,
|
|
68
|
+
* manifest only. Returns an all-`"remove"` {@link PlannedAction}: copy mode one
|
|
69
|
+
* `{ relpath, action: "remove" }` per `manifest.files[].path` in recorded order; symlink mode the
|
|
70
|
+
* single `{ relpath: ".", action: "remove" }`. The safe EXECUTION is `apply()` in spec 04.
|
|
71
|
+
*/
|
|
72
|
+
export declare function planUninstall(manifest: InstallManifest): Result<PlannedAction>;
|