@pugi/cli 0.1.0-beta.4 → 0.1.0-beta.41
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/THIRD_PARTY_NOTICES.md +40 -0
- package/assets/pugi-mascot.ansi +15 -25
- package/bin/run.js +33 -1
- package/dist/commands/jobs-watch.js +201 -0
- package/dist/commands/jobs.js +15 -0
- package/dist/commands/smoke.js +133 -0
- package/dist/core/agent-progress/cleanup.js +134 -0
- package/dist/core/agent-progress/schema.js +144 -0
- package/dist/core/agent-progress/writer.js +101 -0
- package/dist/core/artifact-chain/dispatcher.js +148 -0
- package/dist/core/artifact-chain/exporter.js +164 -0
- package/dist/core/artifact-chain/state.js +243 -0
- package/dist/core/artifact-chain/steps.js +169 -0
- package/dist/core/auth/ensure-authenticated.js +129 -0
- package/dist/core/auth/env-provider.js +238 -0
- package/dist/core/auto-update/channels.js +122 -0
- package/dist/core/auto-update/checker.js +241 -0
- package/dist/core/auto-update/state.js +235 -0
- package/dist/core/bare-mode/index.js +107 -0
- package/dist/core/bash-classifier.js +108 -1
- package/dist/core/checkpoint/resumer.js +149 -0
- package/dist/core/checkpoint/rewinder.js +291 -0
- package/dist/core/codegraph/decision-store.js +248 -0
- package/dist/core/codegraph/detect-repo.js +459 -0
- package/dist/core/codegraph/install.js +134 -0
- package/dist/core/codegraph/offer-hook.js +220 -0
- package/dist/core/compact/auto-trigger.js +96 -0
- package/dist/core/compact/buffer-rewriter.js +115 -0
- package/dist/core/compact/summarizer.js +208 -0
- package/dist/core/compact/token-counter.js +108 -0
- package/dist/core/consensus/diff-capture.js +73 -0
- package/dist/core/context/index.js +7 -0
- package/dist/core/context/markdown-traverse.js +255 -0
- package/dist/core/cost/rate-card.js +129 -0
- package/dist/core/cost/tracker.js +221 -0
- package/dist/core/denial-tracking/index.js +8 -0
- package/dist/core/denial-tracking/state.js +264 -0
- package/dist/core/diagnostics/probe-runner.js +93 -0
- package/dist/core/diagnostics/probes/api.js +46 -0
- package/dist/core/diagnostics/probes/auth.js +86 -0
- package/dist/core/diagnostics/probes/bare-mode.js +42 -0
- package/dist/core/diagnostics/probes/cli-version.js +127 -0
- package/dist/core/diagnostics/probes/config.js +72 -0
- package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
- package/dist/core/diagnostics/probes/disk.js +81 -0
- package/dist/core/diagnostics/probes/git.js +65 -0
- package/dist/core/diagnostics/probes/mcp.js +75 -0
- package/dist/core/diagnostics/probes/node.js +59 -0
- package/dist/core/diagnostics/probes/pnpm.js +36 -0
- package/dist/core/diagnostics/probes/pugi-md.js +89 -0
- package/dist/core/diagnostics/probes/session.js +74 -0
- package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
- package/dist/core/diagnostics/probes/workspace.js +63 -0
- package/dist/core/diagnostics/types.js +70 -0
- package/dist/core/dispatch/cache-cleanup.js +197 -0
- package/dist/core/dispatch/cache-handoff.js +295 -0
- package/dist/core/edits/dispatch.js +218 -2
- package/dist/core/edits/journal.js +199 -0
- package/dist/core/edits/layer-d-ast.js +557 -14
- package/dist/core/edits/verify-hook.js +273 -0
- package/dist/core/edits/worktree.js +322 -0
- package/dist/core/engine/anvil-client.js +115 -5
- package/dist/core/engine/budgets.js +98 -0
- package/dist/core/engine/context-prefix.js +155 -0
- package/dist/core/engine/intent.js +260 -0
- package/dist/core/engine/native-pugi.js +860 -211
- package/dist/core/engine/prompts.js +88 -2
- package/dist/core/engine/strip-internal-fields.js +124 -0
- package/dist/core/engine/tool-bridge.js +1045 -36
- package/dist/core/feedback/queue.js +177 -0
- package/dist/core/feedback/submitter.js +145 -0
- package/dist/core/file-cache.js +113 -1
- package/dist/core/hooks/events.js +44 -0
- package/dist/core/hooks/index.js +15 -0
- package/dist/core/hooks/registry.js +213 -0
- package/dist/core/hooks/runner.js +236 -0
- package/dist/core/hooks/v2/event-emitter.js +115 -0
- package/dist/core/hooks/v2/executor.js +282 -0
- package/dist/core/hooks/v2/index.js +25 -0
- package/dist/core/hooks/v2/lifecycle.js +104 -0
- package/dist/core/hooks/v2/loader.js +216 -0
- package/dist/core/hooks/v2/matcher.js +125 -0
- package/dist/core/hooks/v2/trust.js +143 -0
- package/dist/core/hooks/v2/types.js +86 -0
- package/dist/core/lsp/cache.js +105 -0
- package/dist/core/lsp/client.js +776 -0
- package/dist/core/lsp/language-detect.js +66 -0
- package/dist/core/lsp/post-edit-diagnostics.js +171 -0
- package/dist/core/mcp/client.js +75 -6
- package/dist/core/mcp/http-server.js +553 -0
- package/dist/core/mcp/orchestrator-tools.js +662 -0
- package/dist/core/mcp/permission.js +190 -0
- package/dist/core/mcp/registry.js +24 -2
- package/dist/core/mcp/server-tools.js +219 -0
- package/dist/core/mcp/server.js +397 -0
- package/dist/core/memory/dual-write.js +416 -0
- package/dist/core/memory/phase1-kinds.js +20 -0
- package/dist/core/memory-sync/queue.js +158 -0
- package/dist/core/onboarding/ensure-initialized.js +133 -0
- package/dist/core/onboarding/marker.js +111 -0
- package/dist/core/onboarding/telemetry-state.js +108 -0
- package/dist/core/output-style/presets.js +176 -0
- package/dist/core/output-style/state.js +185 -0
- package/dist/core/permissions/auto-classifier.js +124 -0
- package/dist/core/permissions/circuit-breaker.js +83 -0
- package/dist/core/permissions/gate.js +278 -0
- package/dist/core/permissions/index.js +20 -0
- package/dist/core/permissions/mode.js +174 -0
- package/dist/core/permissions/state.js +241 -0
- package/dist/core/permissions/tool-class.js +93 -0
- package/dist/core/prd-check/parser.js +215 -0
- package/dist/core/prd-check/reporter.js +127 -0
- package/dist/core/prd-check/session-review.js +557 -0
- package/dist/core/prd-check/verifiers.js +223 -0
- package/dist/core/pugi-md/context-injector.js +76 -0
- package/dist/core/pugi-md/walk-up.js +207 -0
- package/dist/core/release-notes/parser.js +241 -0
- package/dist/core/release-notes/state.js +116 -0
- package/dist/core/repl/history.js +11 -1
- package/dist/core/repl/model-pricing.js +135 -0
- package/dist/core/repl/session.js +1899 -38
- package/dist/core/repl/slash-commands.js +406 -21
- package/dist/core/repl/store/session-store.js +31 -2
- package/dist/core/repl/workspace-context.js +22 -0
- package/dist/core/repo-map/build.js +125 -0
- package/dist/core/repo-map/cache.js +185 -0
- package/dist/core/repo-map/extractor.js +254 -0
- package/dist/core/repo-map/formatter.js +145 -0
- package/dist/core/repo-map/scanner.js +211 -0
- package/dist/core/retry-budget/budget.js +284 -0
- package/dist/core/retry-budget/index.js +5 -0
- package/dist/core/session.js +92 -0
- package/dist/core/settings.js +80 -0
- package/dist/core/share/formatter.js +271 -0
- package/dist/core/share/redactor.js +221 -0
- package/dist/core/share/uploader.js +267 -0
- package/dist/core/skills/defaults.js +457 -0
- package/dist/core/smoke/headless-driver.js +174 -0
- package/dist/core/smoke/orchestrator.js +194 -0
- package/dist/core/smoke/runner.js +238 -0
- package/dist/core/smoke/scenario-parser.js +316 -0
- package/dist/core/subagents/dispatcher-real.js +600 -0
- package/dist/core/subagents/dispatcher.js +113 -24
- package/dist/core/subagents/index.js +18 -5
- package/dist/core/subagents/isolation-matrix.js +213 -0
- package/dist/core/subagents/spawn.js +19 -4
- package/dist/core/telemetry/emitter.js +229 -0
- package/dist/core/telemetry/queue.js +251 -0
- package/dist/core/theme/context.js +91 -0
- package/dist/core/theme/presets.js +228 -0
- package/dist/core/theme/state.js +181 -0
- package/dist/core/todos/invariant.js +10 -0
- package/dist/core/todos/state.js +177 -0
- package/dist/core/transport/version-interceptor.js +166 -0
- package/dist/core/vim/keymap.js +288 -0
- package/dist/core/vim/state.js +92 -0
- package/dist/index.js +28 -0
- package/dist/runtime/bootstrap.js +190 -0
- package/dist/runtime/cli.js +3073 -321
- package/dist/runtime/commands/cancel.js +231 -0
- package/dist/runtime/commands/chain.js +489 -0
- package/dist/runtime/commands/codegraph-status.js +227 -0
- package/dist/runtime/commands/compact.js +297 -0
- package/dist/runtime/commands/cost.js +199 -0
- package/dist/runtime/commands/delegate.js +242 -11
- package/dist/runtime/commands/dispatch.js +126 -0
- package/dist/runtime/commands/doctor.js +390 -0
- package/dist/runtime/commands/feedback.js +184 -0
- package/dist/runtime/commands/hooks.js +184 -0
- package/dist/runtime/commands/lsp.js +368 -0
- package/dist/runtime/commands/mcp.js +879 -0
- package/dist/runtime/commands/memory.js +508 -0
- package/dist/runtime/commands/model.js +237 -0
- package/dist/runtime/commands/onboarding.js +275 -0
- package/dist/runtime/commands/patch.js +128 -0
- package/dist/runtime/commands/permissions.js +112 -0
- package/dist/runtime/commands/plan.js +143 -0
- package/dist/runtime/commands/prd-check.js +285 -0
- package/dist/runtime/commands/redo-blob-store.js +92 -0
- package/dist/runtime/commands/redo.js +361 -0
- package/dist/runtime/commands/release-notes.js +229 -0
- package/dist/runtime/commands/repo-map.js +95 -0
- package/dist/runtime/commands/report.js +299 -0
- package/dist/runtime/commands/resume.js +118 -0
- package/dist/runtime/commands/review-consensus.js +17 -2
- package/dist/runtime/commands/rewind.js +333 -0
- package/dist/runtime/commands/sessions.js +163 -0
- package/dist/runtime/commands/share.js +316 -0
- package/dist/runtime/commands/status.js +186 -0
- package/dist/runtime/commands/stickers.js +82 -0
- package/dist/runtime/commands/style.js +194 -0
- package/dist/runtime/commands/theme.js +196 -0
- package/dist/runtime/commands/undo.js +32 -0
- package/dist/runtime/commands/update.js +289 -0
- package/dist/runtime/commands/vim.js +140 -0
- package/dist/runtime/commands/worktree.js +177 -0
- package/dist/runtime/headless-repl.js +195 -0
- package/dist/runtime/headless.js +543 -0
- package/dist/runtime/load-hooks-or-exit.js +71 -0
- package/dist/runtime/plan-decompose.js +531 -0
- package/dist/runtime/version.js +65 -0
- package/dist/tools/agent-tool.js +229 -0
- package/dist/tools/apply-patch.js +556 -0
- package/dist/tools/ask-user-question.js +213 -0
- package/dist/tools/ask-user.js +115 -0
- package/dist/tools/file-tools.js +85 -14
- package/dist/tools/lsp-tools.js +189 -0
- package/dist/tools/mcp-tool.js +260 -0
- package/dist/tools/multi-edit.js +361 -0
- package/dist/tools/powershell.js +156 -0
- package/dist/tools/registry.js +51 -0
- package/dist/tools/skill-tool.js +96 -0
- package/dist/tools/tasks.js +208 -0
- package/dist/tools/todo-write.js +184 -0
- package/dist/tools/web-fetch.js +147 -2
- package/dist/tools/web-search.js +458 -0
- package/dist/tui/agent-progress-card.js +111 -0
- package/dist/tui/agent-tree.js +10 -0
- package/dist/tui/ask-modal.js +2 -2
- package/dist/tui/ask-user-question-prompt.js +192 -0
- package/dist/tui/compact-banner.js +81 -0
- package/dist/tui/conversation-pane.js +82 -8
- package/dist/tui/cost-table.js +111 -0
- package/dist/tui/doctor-table.js +46 -0
- package/dist/tui/feedback-prompt.js +156 -0
- package/dist/tui/input-box.js +69 -2
- package/dist/tui/markdown-render.js +4 -4
- package/dist/tui/onboarding-wizard.js +240 -0
- package/dist/tui/permissions-picker.js +86 -0
- package/dist/tui/render.js +35 -0
- package/dist/tui/repl-render.js +303 -13
- package/dist/tui/repl-splash.js +2 -2
- package/dist/tui/repl.js +72 -14
- package/dist/tui/splash.js +1 -1
- package/dist/tui/status-bar.js +94 -16
- package/dist/tui/status-table.js +7 -0
- package/dist/tui/stickers-art.js +136 -0
- package/dist/tui/style-table.js +28 -0
- package/dist/tui/theme-table.js +29 -0
- package/dist/tui/tool-stream-pane.js +52 -3
- package/dist/tui/update-banner.js +20 -2
- package/dist/tui/vim-input.js +267 -0
- package/docs/examples/codegraph.mcp.json +10 -0
- package/package.json +12 -6
- package/test/scenarios/codegen-create-file.scenario.txt +13 -0
- package/test/scenarios/compact-force.scenario.txt +11 -0
- package/test/scenarios/identity.scenario.txt +11 -0
- package/test/scenarios/persona-handoff.scenario.txt +11 -0
- package/test/scenarios/walkback.scenario.txt +12 -0
- package/dist/core/engine/compaction-hook.js +0 -154
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pugi permissions` / `/permissions` — Leak L6 4-mode gate control.
|
|
3
|
+
*
|
|
4
|
+
* Two entry points share one runtime helper:
|
|
5
|
+
* 1. `/permissions` in the REPL — forwarded by `core/repl/session.ts`.
|
|
6
|
+
* 2. `pugi permissions ...` top-level CLI command (handler in
|
|
7
|
+
* `runtime/cli.ts`).
|
|
8
|
+
*
|
|
9
|
+
* Both pass a `PermissionsCommand` payload describing the operator
|
|
10
|
+
* intent (show / flip / persist) and a `writeOutput` callback that
|
|
11
|
+
* lets the caller route the rendered lines into the right surface
|
|
12
|
+
* (REPL transcript vs. stdout). The helper is intentionally I/O-free
|
|
13
|
+
* itself — it produces lines and lets the caller stream them.
|
|
14
|
+
*/
|
|
15
|
+
import { DEFAULT_PERMISSION_MODE, PERMISSION_MODES, PERMISSION_MODE_GLOSS, getCurrentMode, getGlobalDefaultMode, setCurrentMode, setGlobalDefaultMode, } from '../../core/permissions/index.js';
|
|
16
|
+
/**
|
|
17
|
+
* Run the `/permissions` or `pugi permissions` flow. Side effects:
|
|
18
|
+
* - When `command.mode` is undefined: prints the current mode + the
|
|
19
|
+
* 4-mode table (no writes).
|
|
20
|
+
* - When `command.mode === 'bypass'` without `confirmBypass`: prints
|
|
21
|
+
* a refusal + the safety copy, no writes.
|
|
22
|
+
* - When `command.mode` is set + valid: writes workspace session
|
|
23
|
+
* state; optionally writes global default when `persist` is true.
|
|
24
|
+
* - Always prints the new effective mode + a one-line confirmation.
|
|
25
|
+
*/
|
|
26
|
+
export async function runPermissionsCommand(command, ctx) {
|
|
27
|
+
if (!command.mode) {
|
|
28
|
+
renderCurrentMode(ctx);
|
|
29
|
+
renderModeTable(ctx);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (command.mode === 'bypassPermissions' && !command.confirmBypass) {
|
|
33
|
+
ctx.writeOutput('bypassPermissions disables policy hooks (skill steering, denial tracking) AND skips the deny-list.');
|
|
34
|
+
ctx.writeOutput('Catastrophic patterns (rm -rf /, fork bomb, dd if=/) still trip the circuit-breaker, но that is the only guardrail left.');
|
|
35
|
+
ctx.writeOutput('Run `/permissions bypassPermissions --confirm` to acknowledge before flipping.');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
setCurrentMode(ctx.workspaceRoot, command.mode);
|
|
39
|
+
if (command.persist) {
|
|
40
|
+
setGlobalDefaultMode(command.mode, ctx.homeDir);
|
|
41
|
+
}
|
|
42
|
+
const persistedHint = command.persist
|
|
43
|
+
? ' Persisted to ~/.pugi/config.json for future sessions.'
|
|
44
|
+
: '';
|
|
45
|
+
ctx.writeOutput(`Permission mode set to '${command.mode}'.${persistedHint} ${PERMISSION_MODE_GLOSS[command.mode]}`);
|
|
46
|
+
if (command.mode === 'bypassPermissions') {
|
|
47
|
+
ctx.writeOutput('bypassPermissions — all tools execute without prompts AND policy hooks disabled (circuit-breaker still trips on rm -rf /). Switch back with /permissions dontAsk.');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Print the resolved current mode + the layered source. The merge
|
|
52
|
+
* order mirrors `resolveMode()`: workspace > global > default.
|
|
53
|
+
*/
|
|
54
|
+
function renderCurrentMode(ctx) {
|
|
55
|
+
const workspace = getCurrentMode(ctx.workspaceRoot);
|
|
56
|
+
const global = getGlobalDefaultMode(ctx.homeDir);
|
|
57
|
+
const effective = workspace ?? global ?? DEFAULT_PERMISSION_MODE;
|
|
58
|
+
const source = workspace
|
|
59
|
+
? 'workspace session.json'
|
|
60
|
+
: global
|
|
61
|
+
? 'global ~/.pugi/config.json'
|
|
62
|
+
: 'default (no override)';
|
|
63
|
+
ctx.writeOutput(`Current permission mode: ${effective} (source: ${source})`);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Print the 4-mode reference table. Keeps the gloss + the side-effect
|
|
67
|
+
* matrix in one place so the operator can see the contract while they
|
|
68
|
+
* decide which mode to switch to.
|
|
69
|
+
*/
|
|
70
|
+
function renderModeTable(ctx) {
|
|
71
|
+
ctx.writeOutput('');
|
|
72
|
+
ctx.writeOutput('Permission modes (Shift+Tab cycles in REPL):');
|
|
73
|
+
for (const mode of PERMISSION_MODES) {
|
|
74
|
+
// Wave 7: longest canonical name is `bypassPermissions` (17 chars).
|
|
75
|
+
ctx.writeOutput(` ${mode.padEnd(18)} ${PERMISSION_MODE_GLOSS[mode]}`);
|
|
76
|
+
}
|
|
77
|
+
ctx.writeOutput('');
|
|
78
|
+
ctx.writeOutput('Switch with `/permissions <mode> [--persist]`. bypassPermissions requires `--confirm`.');
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Render the one-shot banner shown on session boot when the effective
|
|
82
|
+
* mode is `bypass`. The caller (engine adapter / REPL bootstrap) calls
|
|
83
|
+
* this once per session — repeated invocations are idempotent in copy
|
|
84
|
+
* but the caller is responsible for the once-only semantics.
|
|
85
|
+
*/
|
|
86
|
+
export function renderBypassBanner(writeOutput) {
|
|
87
|
+
writeOutput('bypassPermissions — all tools execute without prompts AND policy hooks disabled (circuit-breaker still trips on rm -rf /). Switch back with /permissions dontAsk.');
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Resolve the effective mode + the layered source label used by the
|
|
91
|
+
* Ink picker. Shares the merge order with `renderCurrentMode` above so
|
|
92
|
+
* the picker title and the bare `/permissions` print stay in lock-step.
|
|
93
|
+
*
|
|
94
|
+
* Exposed publicly because the host (session.ts) needs the same merge
|
|
95
|
+
* shape to seed the Ink picker BEFORE it mounts.
|
|
96
|
+
*/
|
|
97
|
+
export function resolveLayeredMode(workspaceRoot, homeDir) {
|
|
98
|
+
const workspace = getCurrentMode(workspaceRoot);
|
|
99
|
+
const global = getGlobalDefaultMode(homeDir);
|
|
100
|
+
const effective = workspace ?? global ?? DEFAULT_PERMISSION_MODE;
|
|
101
|
+
const source = workspace
|
|
102
|
+
? 'workspace session.json'
|
|
103
|
+
: global
|
|
104
|
+
? 'global ~/.pugi/config.json'
|
|
105
|
+
: 'default (no override)';
|
|
106
|
+
return {
|
|
107
|
+
effective,
|
|
108
|
+
source,
|
|
109
|
+
firstRun: !workspace && !global,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=permissions.js.map
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pugi plan` / `/plan` — Leak L7 quick mode-switch shortcut.
|
|
3
|
+
*
|
|
4
|
+
* `/plan` is the slick UX shortcut for `/permissions plan`: one keystroke
|
|
5
|
+
* (well, five) puts the gate into plan mode + surfaces a banner so the
|
|
6
|
+
* operator knows write/dispatch tools are refused. The model goes off and
|
|
7
|
+
* thinks / researches without side effects until the operator types
|
|
8
|
+
* `/plan --back` (restore previous mode) or explicitly flips with
|
|
9
|
+
* `/permissions <mode>`.
|
|
10
|
+
*
|
|
11
|
+
* The slash and CLI surfaces both go through `runPlanCommand` — same
|
|
12
|
+
* separation as `runPermissionsCommand`. The runtime is I/O free w.r.t.
|
|
13
|
+
* the engine; the optional one-shot dispatch (`/plan <prompt>`) is
|
|
14
|
+
* handled by the CLI dispatcher AFTER this helper sets the workspace
|
|
15
|
+
* mode so the existing `runEngineTask('plan')` path sees plan mode as
|
|
16
|
+
* the workspace state without needing a parallel code path.
|
|
17
|
+
*
|
|
18
|
+
* Verdicts (the helper returns one so the caller can decide what to do
|
|
19
|
+
* after the mode write — print the banner, dispatch the engine, no-op):
|
|
20
|
+
* - `entered` — first `/plan` from a non-plan mode. Print the
|
|
21
|
+
* banner. Caller may then run a one-shot prompt.
|
|
22
|
+
* - `already-in-plan` — `/plan` while already in plan. No-op + show
|
|
23
|
+
* current. No banner reprint.
|
|
24
|
+
* - `reverted` — `/plan --back` popped the snapshot. Print a
|
|
25
|
+
* one-line confirmation; no banner.
|
|
26
|
+
* - `no-previous` — `/plan --back` without a snapshot. Print a
|
|
27
|
+
* clear "nothing to revert" line.
|
|
28
|
+
* - `persisted` — `/plan --persist` wrote the global default
|
|
29
|
+
* AND set workspace state to plan. Banner +
|
|
30
|
+
* persistence-confirmation line.
|
|
31
|
+
*
|
|
32
|
+
* `previousMode` semantics: stashed BEFORE the workspace write on
|
|
33
|
+
* `entered` / `persisted`. Cleared after a successful `reverted` so a
|
|
34
|
+
* second `--back` reports `no-previous` instead of looping back to plan.
|
|
35
|
+
*/
|
|
36
|
+
import { PERMISSION_MODES, PERMISSION_MODE_GLOSS, getCurrentMode, getGlobalDefaultMode, getPreviousMode, setCurrentMode, setGlobalDefaultMode, setPreviousMode, } from '../../core/permissions/index.js';
|
|
37
|
+
/**
|
|
38
|
+
* Run the `/plan` flow. Side effects:
|
|
39
|
+
*
|
|
40
|
+
* command.back = true:
|
|
41
|
+
* - If a previousMode snapshot exists → restore it, clear snapshot,
|
|
42
|
+
* return `reverted`.
|
|
43
|
+
* - Otherwise → no writes, return `no-previous`.
|
|
44
|
+
*
|
|
45
|
+
* command.back = false, current mode is plan:
|
|
46
|
+
* - If `--persist`, write global config (no workspace re-write — it
|
|
47
|
+
* is already plan).
|
|
48
|
+
* - Print "already in plan" + the banner-summary line. Return
|
|
49
|
+
* `already-in-plan`.
|
|
50
|
+
*
|
|
51
|
+
* command.back = false, current mode is NOT plan:
|
|
52
|
+
* - Snapshot current mode → previousPermissionMode.
|
|
53
|
+
* - Write workspace mode = plan.
|
|
54
|
+
* - If `--persist`, also write global config.
|
|
55
|
+
* - Print the banner + (if persisted) the persistence line.
|
|
56
|
+
* - Return `entered` or `persisted`.
|
|
57
|
+
*
|
|
58
|
+
* --back + --persist is a no-op for persistence (revert never writes
|
|
59
|
+
* global config) but the revert itself fires.
|
|
60
|
+
*/
|
|
61
|
+
export async function runPlanCommand(command, ctx) {
|
|
62
|
+
const current = effectiveMode(ctx);
|
|
63
|
+
if (command.back) {
|
|
64
|
+
const prev = getPreviousMode(ctx.workspaceRoot);
|
|
65
|
+
if (!prev) {
|
|
66
|
+
ctx.writeOutput(`No previous mode to restore. Current: ${current}. Use \`/permissions <mode>\` to switch explicitly.`);
|
|
67
|
+
return { verdict: 'no-previous', mode: current };
|
|
68
|
+
}
|
|
69
|
+
setCurrentMode(ctx.workspaceRoot, prev);
|
|
70
|
+
setPreviousMode(ctx.workspaceRoot, null);
|
|
71
|
+
ctx.writeOutput(`Switched back to '${prev}' mode. ${PERMISSION_MODE_GLOSS[prev]}`);
|
|
72
|
+
return { verdict: 'reverted', mode: prev };
|
|
73
|
+
}
|
|
74
|
+
if (current === 'plan') {
|
|
75
|
+
// Repeat /plan in plan mode is a no-op for the mode write, but
|
|
76
|
+
// --persist still honours the operator's intent to lock plan as
|
|
77
|
+
// the global default for future sessions.
|
|
78
|
+
if (command.persist) {
|
|
79
|
+
setGlobalDefaultMode('plan', ctx.homeDir);
|
|
80
|
+
ctx.writeOutput('Already in plan mode. Persisted plan as the default for future sessions (~/.pugi/config.json).');
|
|
81
|
+
return { verdict: 'persisted', mode: 'plan' };
|
|
82
|
+
}
|
|
83
|
+
ctx.writeOutput(`Already in plan mode. ${PERMISSION_MODE_GLOSS.plan} Switch back with \`/plan --back\` or \`/permissions <mode>\`.`);
|
|
84
|
+
return { verdict: 'already-in-plan', mode: 'plan' };
|
|
85
|
+
}
|
|
86
|
+
// Entering plan mode from a non-plan baseline. Stash the current mode
|
|
87
|
+
// BEFORE the write so /plan --back can pop it. We intentionally use the
|
|
88
|
+
// observed effective mode (workspace || global || default) rather than
|
|
89
|
+
// strictly the workspace value — if the operator's previous mode was
|
|
90
|
+
// sourced from the global config, `--back` should restore that observed
|
|
91
|
+
// state, not silently degrade to default.
|
|
92
|
+
setPreviousMode(ctx.workspaceRoot, current);
|
|
93
|
+
setCurrentMode(ctx.workspaceRoot, 'plan');
|
|
94
|
+
if (command.persist) {
|
|
95
|
+
setGlobalDefaultMode('plan', ctx.homeDir);
|
|
96
|
+
}
|
|
97
|
+
for (const line of renderPlanBanner()) {
|
|
98
|
+
ctx.writeOutput(line);
|
|
99
|
+
}
|
|
100
|
+
if (command.persist) {
|
|
101
|
+
ctx.writeOutput('Persisted plan as the default for future sessions (~/.pugi/config.json).');
|
|
102
|
+
return { verdict: 'persisted', mode: 'plan' };
|
|
103
|
+
}
|
|
104
|
+
return { verdict: 'entered', mode: 'plan' };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Render the plan-mode banner as a sequence of lines. The slash + CLI
|
|
108
|
+
* surfaces both print these line-by-line through their respective
|
|
109
|
+
* `writeOutput` sinks so the Ink REPL conversation pane and the plain
|
|
110
|
+
* stdout pipeline render identically.
|
|
111
|
+
*
|
|
112
|
+
* The box-drawing uses light-line glyphs (U+2500 family) which render in
|
|
113
|
+
* every modern terminal we target (Linux/macOS/Windows Terminal/iTerm/
|
|
114
|
+
* Ghostty/Alacritty). No emoji per brand-voice gate.
|
|
115
|
+
*/
|
|
116
|
+
export function renderPlanBanner() {
|
|
117
|
+
return [
|
|
118
|
+
'┌─ Plan mode active ────────────────────────────────────────┐',
|
|
119
|
+
'│ Read-only tools allowed. Write/dispatch tools blocked. │',
|
|
120
|
+
'│ Pugi will think + research without making changes. │',
|
|
121
|
+
'│ Switch back: /plan --back or /permissions <mode> │',
|
|
122
|
+
'└───────────────────────────────────────────────────────────┘',
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Resolve the effective mode at the moment the helper was invoked,
|
|
127
|
+
* mirroring `resolveMode` but without taking a CLI flag (the `/plan`
|
|
128
|
+
* helper is called AFTER the top-level `--mode` flag has been applied
|
|
129
|
+
* to the workspace, so the file state is the source of truth here).
|
|
130
|
+
*/
|
|
131
|
+
function effectiveMode(ctx) {
|
|
132
|
+
const workspace = getCurrentMode(ctx.workspaceRoot);
|
|
133
|
+
if (workspace)
|
|
134
|
+
return workspace;
|
|
135
|
+
const global = getGlobalDefaultMode(ctx.homeDir);
|
|
136
|
+
if (global)
|
|
137
|
+
return global;
|
|
138
|
+
// Wave 7 canonical default — `PERMISSION_MODES[0]` is `default` (the
|
|
139
|
+
// CC-parity ask-every-call ground state). Fall back literally if the
|
|
140
|
+
// array is empty (defensive — should never happen).
|
|
141
|
+
return PERMISSION_MODES[0] ?? 'default';
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=plan.js.map
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pugi prd-check` — Pugi α7 Wave 6 verified-deliverable gate (2026-05-27).
|
|
3
|
+
*
|
|
4
|
+
* Runs a PRD ↔ delivered-artifact verification sweep BEFORE the
|
|
5
|
+
* operator (or an autonomous agent) claims a feature is done.
|
|
6
|
+
* Reads `docs/prd/<feature>.md` (or any explicit path), parses the
|
|
7
|
+
* acceptance-criteria section, and runs file / test / doc / command
|
|
8
|
+
* / route verifiers per criterion. Output mirrors `pugi doctor`:
|
|
9
|
+
* either a plain-text table or a JSON envelope (`--json`).
|
|
10
|
+
*
|
|
11
|
+
* Module contract (mirrors L17 doctor split):
|
|
12
|
+
*
|
|
13
|
+
* - parser + verifiers + reporter are pure with respect to deps;
|
|
14
|
+
* this file is the THIN wiring that resolves cwd, glob-expands
|
|
15
|
+
* `--all`, loads each PRD, and forwards the structured result
|
|
16
|
+
* к the supplied writeOutput sink.
|
|
17
|
+
*
|
|
18
|
+
* - `runPrdCheckCommand` is the single entry point. Both the
|
|
19
|
+
* top-level `pugi prd-check` shell command AND the in-REPL
|
|
20
|
+
* `/prd-check` slash command call it. The function returns the
|
|
21
|
+
* `PrdCheckEnvelope[]` so the REPL can render via Ink without
|
|
22
|
+
* re-running the verification.
|
|
23
|
+
*
|
|
24
|
+
* - Exit codes follow `exitCodeFor` from the reporter:
|
|
25
|
+
* 0 — healthy (every criterion PASS or SKIPPED)
|
|
26
|
+
* 1 — failing (≥1 FAIL across the scanned PRDs)
|
|
27
|
+
* 2 — unparsed (≥1 PRD missing the acceptance section)
|
|
28
|
+
* When `--all` scans multiple PRDs the worst verdict wins (1 > 2 > 0).
|
|
29
|
+
*
|
|
30
|
+
* - The `knownCommands` set is sourced from the CLI registry — we
|
|
31
|
+
* accept it as an injected parameter so the spec can drive
|
|
32
|
+
* command-verification without importing the entire cli.ts
|
|
33
|
+
* module (which would pull the engine graph into the test).
|
|
34
|
+
*/
|
|
35
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
36
|
+
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
37
|
+
import { parsePrd } from '../../core/prd-check/parser.js';
|
|
38
|
+
import { buildEnvelope, exitCodeFor, renderTable, } from '../../core/prd-check/reporter.js';
|
|
39
|
+
import { defaultSessionReviewDeps, renderSessionReview, runSessionReview, } from '../../core/prd-check/session-review.js';
|
|
40
|
+
import { createDefaultDeps, verifyAll, } from '../../core/prd-check/verifiers.js';
|
|
41
|
+
const DEFAULT_PRD_DIR = 'docs/prd';
|
|
42
|
+
/**
|
|
43
|
+
* Run the gate. Resolves which PRDs to inspect, runs the parser +
|
|
44
|
+
* verifiers + reporter chain per PRD, emits the combined output,
|
|
45
|
+
* and sets `process.exitCode` to the worst verdict across the set.
|
|
46
|
+
*/
|
|
47
|
+
export async function runPrdCheckCommand(ctx) {
|
|
48
|
+
// Wave 6 final (2026-05-27): session-review mode. Orthogonal to
|
|
49
|
+
// the verifier-graph mode — no PRD path resolution, no command
|
|
50
|
+
// registry. The runner walks up for PRD.md, reads the NDJSON
|
|
51
|
+
// events log, and dispatches a cross-review subagent.
|
|
52
|
+
if (ctx.flags.session) {
|
|
53
|
+
const status = (line) => {
|
|
54
|
+
// Emit status lines through the output sink so the slash
|
|
55
|
+
// surface can render them inline. The status payload is
|
|
56
|
+
// intentionally lightweight — only the human-facing text.
|
|
57
|
+
ctx.writeOutput({
|
|
58
|
+
command: 'prd-check',
|
|
59
|
+
overall: 'healthy',
|
|
60
|
+
envelopes: [],
|
|
61
|
+
}, line);
|
|
62
|
+
};
|
|
63
|
+
const sessionDeps = ctx.sessionDeps ??
|
|
64
|
+
defaultSessionReviewDeps({ onStatus: status });
|
|
65
|
+
const review = await runSessionReview(ctx.cwd, sessionDeps);
|
|
66
|
+
const overall = review.status === 'ok'
|
|
67
|
+
? review.outstanding.length === 0
|
|
68
|
+
? 'healthy'
|
|
69
|
+
: 'failing'
|
|
70
|
+
: 'unparsed';
|
|
71
|
+
const result = {
|
|
72
|
+
command: 'prd-check',
|
|
73
|
+
overall,
|
|
74
|
+
envelopes: [],
|
|
75
|
+
sessionReview: review,
|
|
76
|
+
};
|
|
77
|
+
ctx.writeOutput(result, renderSessionReview(review));
|
|
78
|
+
process.exitCode = exitCodeFor(overall);
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
const paths = resolveTargets(ctx);
|
|
82
|
+
if (paths.length === 0) {
|
|
83
|
+
const result = {
|
|
84
|
+
command: 'prd-check',
|
|
85
|
+
overall: 'unparsed',
|
|
86
|
+
envelopes: [],
|
|
87
|
+
};
|
|
88
|
+
ctx.writeOutput(result, 'No PRD files found.');
|
|
89
|
+
process.exitCode = exitCodeFor('unparsed');
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
const deps = ctx.deps ??
|
|
93
|
+
createDefaultDeps({
|
|
94
|
+
workspaceRoot: ctx.cwd,
|
|
95
|
+
knownCommands: ctx.knownCommands,
|
|
96
|
+
});
|
|
97
|
+
const envelopes = [];
|
|
98
|
+
for (const path of paths) {
|
|
99
|
+
envelopes.push(checkSinglePrd(path, ctx.cwd, deps));
|
|
100
|
+
}
|
|
101
|
+
const overall = combineOverall(envelopes.map((e) => e.overall));
|
|
102
|
+
const result = {
|
|
103
|
+
command: 'prd-check',
|
|
104
|
+
overall,
|
|
105
|
+
envelopes,
|
|
106
|
+
};
|
|
107
|
+
const text = renderRun(result, ctx.cwd);
|
|
108
|
+
ctx.writeOutput(result, text);
|
|
109
|
+
process.exitCode = exitCodeFor(overall);
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Render the combined plain-text view. Multi-PRD runs print one
|
|
114
|
+
* table per envelope separated by a divider; single-PRD runs print
|
|
115
|
+
* just the table to keep the output narrow.
|
|
116
|
+
*/
|
|
117
|
+
function renderRun(result, cwd) {
|
|
118
|
+
if (result.envelopes.length === 0) {
|
|
119
|
+
return 'No PRD files found.';
|
|
120
|
+
}
|
|
121
|
+
if (result.envelopes.length === 1) {
|
|
122
|
+
return renderTable(result.envelopes[0]);
|
|
123
|
+
}
|
|
124
|
+
const parts = [];
|
|
125
|
+
for (const envelope of result.envelopes) {
|
|
126
|
+
const relPath = relative(cwd, envelope.prdPath) || envelope.prdPath;
|
|
127
|
+
parts.push(renderTable({ ...envelope, prdPath: relPath }));
|
|
128
|
+
parts.push('');
|
|
129
|
+
}
|
|
130
|
+
parts.push('-'.repeat(50));
|
|
131
|
+
const summary = `${result.envelopes.length} PRD(s) scanned. Overall: ${result.overall.toUpperCase()}`;
|
|
132
|
+
parts.push(summary);
|
|
133
|
+
return parts.join('\n');
|
|
134
|
+
}
|
|
135
|
+
function checkSinglePrd(prdPath, workspaceRoot, deps) {
|
|
136
|
+
let source;
|
|
137
|
+
try {
|
|
138
|
+
source = readFileSync(prdPath, 'utf8');
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
142
|
+
return {
|
|
143
|
+
command: 'prd-check',
|
|
144
|
+
prdPath: relative(workspaceRoot, prdPath) || prdPath,
|
|
145
|
+
title: null,
|
|
146
|
+
overall: 'unparsed',
|
|
147
|
+
counts: { pass: 0, fail: 0, skipped: 0 },
|
|
148
|
+
criteria: [
|
|
149
|
+
{
|
|
150
|
+
index: 0,
|
|
151
|
+
text: `PRD file unreadable: ${message}`,
|
|
152
|
+
status: 'fail',
|
|
153
|
+
results: [],
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const parsed = parsePrd(source);
|
|
159
|
+
const verified = verifyAll(parsed.criteria, deps);
|
|
160
|
+
return buildEnvelope({
|
|
161
|
+
prdPath: relative(workspaceRoot, prdPath) || prdPath,
|
|
162
|
+
title: parsed.title,
|
|
163
|
+
hasAcceptanceSection: parsed.hasAcceptanceSection,
|
|
164
|
+
verified,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
function resolveTargets(ctx) {
|
|
168
|
+
if (ctx.flags.all) {
|
|
169
|
+
const prdDir = resolve(ctx.cwd, DEFAULT_PRD_DIR);
|
|
170
|
+
return listMarkdownFiles(prdDir);
|
|
171
|
+
}
|
|
172
|
+
if (ctx.prdPath) {
|
|
173
|
+
const absolute = isAbsolute(ctx.prdPath)
|
|
174
|
+
? ctx.prdPath
|
|
175
|
+
: resolve(ctx.cwd, ctx.prdPath);
|
|
176
|
+
return [absolute];
|
|
177
|
+
}
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
function listMarkdownFiles(dir) {
|
|
181
|
+
let entries;
|
|
182
|
+
try {
|
|
183
|
+
entries = readdirSync(dir);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
const out = [];
|
|
189
|
+
for (const entry of entries) {
|
|
190
|
+
const full = join(dir, entry);
|
|
191
|
+
let isDirectory = false;
|
|
192
|
+
try {
|
|
193
|
+
isDirectory = statSync(full).isDirectory();
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (isDirectory) {
|
|
199
|
+
out.push(...listMarkdownFiles(full));
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (entry.endsWith('.md')) {
|
|
203
|
+
out.push(full);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return out.sort();
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Combine per-PRD verdicts into the run-wide one. failing > unparsed > healthy.
|
|
210
|
+
* Exported for the spec.
|
|
211
|
+
*/
|
|
212
|
+
export function combineOverall(verdicts) {
|
|
213
|
+
if (verdicts.length === 0)
|
|
214
|
+
return 'unparsed';
|
|
215
|
+
if (verdicts.some((v) => v === 'failing'))
|
|
216
|
+
return 'failing';
|
|
217
|
+
if (verdicts.some((v) => v === 'unparsed'))
|
|
218
|
+
return 'unparsed';
|
|
219
|
+
return 'healthy';
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Parse the CLI argv tail. Accepts:
|
|
223
|
+
*
|
|
224
|
+
* pugi prd-check -> error (no target)
|
|
225
|
+
* pugi prd-check <path> -> single PRD
|
|
226
|
+
* pugi prd-check --all -> scan docs/prd/**.md
|
|
227
|
+
* pugi prd-check <path> --json -> single PRD, JSON envelope
|
|
228
|
+
*
|
|
229
|
+
* `--json` is also forwarded from the global flag set in cli.ts;
|
|
230
|
+
* the local parse re-honours it so the slash command can use the
|
|
231
|
+
* same parser without the global flag plumbing.
|
|
232
|
+
*/
|
|
233
|
+
export function parsePrdCheckArgs(args, options) {
|
|
234
|
+
let json = options.jsonDefault;
|
|
235
|
+
let all = false;
|
|
236
|
+
let session = false;
|
|
237
|
+
let prdPath;
|
|
238
|
+
for (const arg of args) {
|
|
239
|
+
if (arg === '--json') {
|
|
240
|
+
json = true;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (arg === '--all') {
|
|
244
|
+
all = true;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (arg === '--session') {
|
|
248
|
+
session = true;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (arg.startsWith('--')) {
|
|
252
|
+
return { ok: false, error: `unknown flag: ${arg}` };
|
|
253
|
+
}
|
|
254
|
+
if (prdPath !== undefined) {
|
|
255
|
+
return { ok: false, error: `unexpected extra argument: ${arg}` };
|
|
256
|
+
}
|
|
257
|
+
prdPath = arg;
|
|
258
|
+
}
|
|
259
|
+
// Wave 6 final (2026-05-27): `--session` is mutually exclusive with
|
|
260
|
+
// the verifier modes — it walks up for a PRD, reads NDJSON turns,
|
|
261
|
+
// and dispatches a subagent. No <path>, no --all, no command-
|
|
262
|
+
// registry inputs. Validating up-front gives operators a clear
|
|
263
|
+
// error instead of a silent fall-through.
|
|
264
|
+
if (session && (all || prdPath !== undefined)) {
|
|
265
|
+
return {
|
|
266
|
+
ok: false,
|
|
267
|
+
error: 'cannot combine --session with --all or <path>',
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
if (!session && !all && prdPath === undefined) {
|
|
271
|
+
return {
|
|
272
|
+
ok: false,
|
|
273
|
+
error: 'pugi prd-check <prd-path> | --all | --session (pass a PRD path, --all to scan docs/prd/**.md, or --session to review the live session against PRD.md)',
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
if (all && prdPath !== undefined) {
|
|
277
|
+
return { ok: false, error: 'cannot combine <path> with --all' };
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
ok: true,
|
|
281
|
+
flags: { json, all, session },
|
|
282
|
+
...(prdPath !== undefined ? { prdPath } : {}),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
//# sourceMappingURL=prd-check.js.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redo blob store — Wave 6 (2026-05-27).
|
|
3
|
+
*
|
|
4
|
+
* `/undo` walks the most recent successful mutating tool result and
|
|
5
|
+
* reverts each file on disk. `/redo` reapplies that change. To reapply,
|
|
6
|
+
* we need the post-mutation content — but the event log records only
|
|
7
|
+
* sha256 hashes, not content (see core/file-cache.ts comment).
|
|
8
|
+
*
|
|
9
|
+
* Solution: a content-addressable sidecar store at
|
|
10
|
+
* `<workspaceRoot>/.pugi/undo-blobs/<sha256>`. The undo runner captures
|
|
11
|
+
* each file's CURRENT content (which equals the original AFTER state)
|
|
12
|
+
* into the store BEFORE reverting on disk. The redo runner reads the
|
|
13
|
+
* blob keyed by `beforeHash` of the inverse mutation (which records
|
|
14
|
+
* the pre-revert hash = the original AFTER hash) and writes it back.
|
|
15
|
+
*
|
|
16
|
+
* The store is deliberately untracked (`.pugi/` already lives in
|
|
17
|
+
* .gitignore) and self-cleaning — after a successful redo we delete
|
|
18
|
+
* the blob so a second redo without a fresh undo is a noop. Stale
|
|
19
|
+
* blobs left behind by an interrupted undo are reaped after 7 days
|
|
20
|
+
* by the existing `.pugi/cleanup` cadence (best-effort; not a
|
|
21
|
+
* correctness requirement).
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
24
|
+
import { resolve } from 'node:path';
|
|
25
|
+
import { hashContent } from '../../core/file-cache.js';
|
|
26
|
+
/** Sha256-keyed blob path under `<root>/.pugi/undo-blobs/`. */
|
|
27
|
+
export function blobPathFor(root, sha) {
|
|
28
|
+
return resolve(root, '.pugi/undo-blobs', sha);
|
|
29
|
+
}
|
|
30
|
+
/** Ensure the blob directory exists. Idempotent. */
|
|
31
|
+
function ensureBlobDir(root) {
|
|
32
|
+
const dir = resolve(root, '.pugi/undo-blobs');
|
|
33
|
+
if (!existsSync(dir)) {
|
|
34
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
35
|
+
}
|
|
36
|
+
return dir;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Write `content` into the blob store. Returns the resolved blob path.
|
|
40
|
+
* Atomic tmp+rename so partial writes never present a half-blob к
|
|
41
|
+
* future redo invocations.
|
|
42
|
+
*
|
|
43
|
+
* Idempotent: if a blob with the same content already exists, the
|
|
44
|
+
* second write is a noop (the rename target already matches).
|
|
45
|
+
*/
|
|
46
|
+
export function writeBlob(root, content) {
|
|
47
|
+
ensureBlobDir(root);
|
|
48
|
+
const sha = hashContent(content);
|
|
49
|
+
const dst = blobPathFor(root, sha);
|
|
50
|
+
if (existsSync(dst)) {
|
|
51
|
+
// Same content already cached; nothing to do.
|
|
52
|
+
return { sha, path: dst };
|
|
53
|
+
}
|
|
54
|
+
const tmp = `${dst}.tmp-${process.pid}-${Date.now()}`;
|
|
55
|
+
writeFileSync(tmp, content, { encoding: 'utf8', mode: 0o600 });
|
|
56
|
+
renameSync(tmp, dst);
|
|
57
|
+
return { sha, path: dst };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Read a blob by sha. Returns `undefined` when the blob is missing
|
|
61
|
+
* (cleanup ran, repo cloned fresh, blob never captured). Callers
|
|
62
|
+
* MUST treat undefined as "redo not available" rather than crashing.
|
|
63
|
+
*/
|
|
64
|
+
export function readBlob(root, sha) {
|
|
65
|
+
const path = blobPathFor(root, sha);
|
|
66
|
+
if (!existsSync(path))
|
|
67
|
+
return undefined;
|
|
68
|
+
try {
|
|
69
|
+
return readFileSync(path, 'utf8');
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Best-effort blob deletion. Used by the redo runner after a successful
|
|
77
|
+
* reapply so the blob does not get reused on a second redo (which would
|
|
78
|
+
* be incorrect — once redone, the next undo must capture fresh state).
|
|
79
|
+
* Missing-file errors are swallowed — the store self-heals.
|
|
80
|
+
*/
|
|
81
|
+
export function deleteBlob(root, sha) {
|
|
82
|
+
const path = blobPathFor(root, sha);
|
|
83
|
+
if (!existsSync(path))
|
|
84
|
+
return;
|
|
85
|
+
try {
|
|
86
|
+
unlinkSync(path);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Best-effort.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=redo-blob-store.js.map
|