@pugi/cli 0.1.0-beta.3 → 0.1.0-beta.30
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 -40
- package/bin/run.js +33 -1
- package/dist/commands/jobs-watch.js +201 -0
- package/dist/commands/jobs.js +15 -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/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/checkpoint/resumer.js +149 -0
- package/dist/core/checkpoint/rewinder.js +291 -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 +442 -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 +111 -18
- package/dist/core/engine/anvil-client.js +115 -5
- package/dist/core/engine/budgets.js +89 -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 +852 -210
- package/dist/core/engine/prompts.js +89 -6
- package/dist/core/engine/strip-internal-fields.js +124 -0
- package/dist/core/engine/tool-bridge.js +972 -33
- 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/init/scaffold.js +195 -0
- package/dist/core/lsp/cache.js +105 -0
- package/dist/core/lsp/client.js +174 -29
- 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/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/dual-write.spec.js +297 -0
- package/dist/core/memory/phase1-kinds.js +20 -0
- package/dist/core/memory-sync/queue.js +158 -0
- package/dist/core/memory-sync/queue.spec.js +105 -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/gate.js +187 -0
- package/dist/core/permissions/index.js +18 -0
- package/dist/core/permissions/mode.js +102 -0
- package/dist/core/permissions/state.js +215 -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/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/codebase-survey.js +308 -0
- package/dist/core/repl/history.js +11 -1
- package/dist/core/repl/init-interview.js +457 -0
- package/dist/core/repl/model-pricing.js +135 -0
- package/dist/core/repl/onboarding-state.js +297 -0
- package/dist/core/repl/session.js +1486 -30
- package/dist/core/repl/slash-commands.js +345 -9
- 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 +44 -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/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 +2595 -278
- package/dist/runtime/commands/chain.js +489 -0
- package/dist/runtime/commands/compact.js +297 -0
- package/dist/runtime/commands/cost.js +199 -0
- package/dist/runtime/commands/delegate.js +312 -0
- 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 +212 -28
- package/dist/runtime/commands/mcp.js +824 -0
- package/dist/runtime/commands/memory.js +508 -0
- package/dist/runtime/commands/memory.spec.js +174 -0
- package/dist/runtime/commands/model.js +237 -0
- package/dist/runtime/commands/onboarding.js +275 -0
- package/dist/runtime/commands/patch.js +17 -0
- package/dist/runtime/commands/permissions.js +87 -0
- package/dist/runtime/commands/plan.js +143 -0
- package/dist/runtime/commands/prd-check.js +235 -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/roster.js +117 -0
- package/dist/runtime/commands/sessions.js +163 -0
- package/dist/runtime/commands/share.js +316 -0
- package/dist/runtime/commands/status.js +178 -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/update.js +289 -0
- package/dist/runtime/commands/vim.js +140 -0
- package/dist/runtime/commands/worktree.js +50 -6
- 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 +281 -39
- 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/mcp-tool.js +260 -0
- package/dist/tools/multi-edit.js +361 -0
- package/dist/tools/registry.js +30 -2
- 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 +46 -2
- package/dist/tui/markdown-render.js +4 -4
- package/dist/tui/onboarding-wizard.js +240 -0
- package/dist/tui/repl-render.js +293 -35
- package/dist/tui/repl-splash.js +2 -2
- package/dist/tui/repl.js +45 -13
- 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 +7 -0
- 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 +9 -6
|
@@ -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
|
+
// Defensive: PERMISSION_MODES[1] is 'ask' (the canonical default). We
|
|
139
|
+
// index off the canonical list rather than re-import DEFAULT_PERMISSION_MODE
|
|
140
|
+
// here to keep the symbol surface narrow.
|
|
141
|
+
return PERMISSION_MODES[1] ?? 'ask';
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=plan.js.map
|
|
@@ -0,0 +1,235 @@
|
|
|
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 { createDefaultDeps, verifyAll, } from '../../core/prd-check/verifiers.js';
|
|
40
|
+
const DEFAULT_PRD_DIR = 'docs/prd';
|
|
41
|
+
/**
|
|
42
|
+
* Run the gate. Resolves which PRDs to inspect, runs the parser +
|
|
43
|
+
* verifiers + reporter chain per PRD, emits the combined output,
|
|
44
|
+
* and sets `process.exitCode` to the worst verdict across the set.
|
|
45
|
+
*/
|
|
46
|
+
export async function runPrdCheckCommand(ctx) {
|
|
47
|
+
const paths = resolveTargets(ctx);
|
|
48
|
+
if (paths.length === 0) {
|
|
49
|
+
const result = {
|
|
50
|
+
command: 'prd-check',
|
|
51
|
+
overall: 'unparsed',
|
|
52
|
+
envelopes: [],
|
|
53
|
+
};
|
|
54
|
+
ctx.writeOutput(result, 'No PRD files found.');
|
|
55
|
+
process.exitCode = exitCodeFor('unparsed');
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
const deps = ctx.deps ??
|
|
59
|
+
createDefaultDeps({
|
|
60
|
+
workspaceRoot: ctx.cwd,
|
|
61
|
+
knownCommands: ctx.knownCommands,
|
|
62
|
+
});
|
|
63
|
+
const envelopes = [];
|
|
64
|
+
for (const path of paths) {
|
|
65
|
+
envelopes.push(checkSinglePrd(path, ctx.cwd, deps));
|
|
66
|
+
}
|
|
67
|
+
const overall = combineOverall(envelopes.map((e) => e.overall));
|
|
68
|
+
const result = {
|
|
69
|
+
command: 'prd-check',
|
|
70
|
+
overall,
|
|
71
|
+
envelopes,
|
|
72
|
+
};
|
|
73
|
+
const text = renderRun(result, ctx.cwd);
|
|
74
|
+
ctx.writeOutput(result, text);
|
|
75
|
+
process.exitCode = exitCodeFor(overall);
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Render the combined plain-text view. Multi-PRD runs print one
|
|
80
|
+
* table per envelope separated by a divider; single-PRD runs print
|
|
81
|
+
* just the table to keep the output narrow.
|
|
82
|
+
*/
|
|
83
|
+
function renderRun(result, cwd) {
|
|
84
|
+
if (result.envelopes.length === 0) {
|
|
85
|
+
return 'No PRD files found.';
|
|
86
|
+
}
|
|
87
|
+
if (result.envelopes.length === 1) {
|
|
88
|
+
return renderTable(result.envelopes[0]);
|
|
89
|
+
}
|
|
90
|
+
const parts = [];
|
|
91
|
+
for (const envelope of result.envelopes) {
|
|
92
|
+
const relPath = relative(cwd, envelope.prdPath) || envelope.prdPath;
|
|
93
|
+
parts.push(renderTable({ ...envelope, prdPath: relPath }));
|
|
94
|
+
parts.push('');
|
|
95
|
+
}
|
|
96
|
+
parts.push('-'.repeat(50));
|
|
97
|
+
const summary = `${result.envelopes.length} PRD(s) scanned. Overall: ${result.overall.toUpperCase()}`;
|
|
98
|
+
parts.push(summary);
|
|
99
|
+
return parts.join('\n');
|
|
100
|
+
}
|
|
101
|
+
function checkSinglePrd(prdPath, workspaceRoot, deps) {
|
|
102
|
+
let source;
|
|
103
|
+
try {
|
|
104
|
+
source = readFileSync(prdPath, 'utf8');
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
108
|
+
return {
|
|
109
|
+
command: 'prd-check',
|
|
110
|
+
prdPath: relative(workspaceRoot, prdPath) || prdPath,
|
|
111
|
+
title: null,
|
|
112
|
+
overall: 'unparsed',
|
|
113
|
+
counts: { pass: 0, fail: 0, skipped: 0 },
|
|
114
|
+
criteria: [
|
|
115
|
+
{
|
|
116
|
+
index: 0,
|
|
117
|
+
text: `PRD file unreadable: ${message}`,
|
|
118
|
+
status: 'fail',
|
|
119
|
+
results: [],
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const parsed = parsePrd(source);
|
|
125
|
+
const verified = verifyAll(parsed.criteria, deps);
|
|
126
|
+
return buildEnvelope({
|
|
127
|
+
prdPath: relative(workspaceRoot, prdPath) || prdPath,
|
|
128
|
+
title: parsed.title,
|
|
129
|
+
hasAcceptanceSection: parsed.hasAcceptanceSection,
|
|
130
|
+
verified,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function resolveTargets(ctx) {
|
|
134
|
+
if (ctx.flags.all) {
|
|
135
|
+
const prdDir = resolve(ctx.cwd, DEFAULT_PRD_DIR);
|
|
136
|
+
return listMarkdownFiles(prdDir);
|
|
137
|
+
}
|
|
138
|
+
if (ctx.prdPath) {
|
|
139
|
+
const absolute = isAbsolute(ctx.prdPath)
|
|
140
|
+
? ctx.prdPath
|
|
141
|
+
: resolve(ctx.cwd, ctx.prdPath);
|
|
142
|
+
return [absolute];
|
|
143
|
+
}
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
function listMarkdownFiles(dir) {
|
|
147
|
+
let entries;
|
|
148
|
+
try {
|
|
149
|
+
entries = readdirSync(dir);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
const out = [];
|
|
155
|
+
for (const entry of entries) {
|
|
156
|
+
const full = join(dir, entry);
|
|
157
|
+
let isDirectory = false;
|
|
158
|
+
try {
|
|
159
|
+
isDirectory = statSync(full).isDirectory();
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (isDirectory) {
|
|
165
|
+
out.push(...listMarkdownFiles(full));
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (entry.endsWith('.md')) {
|
|
169
|
+
out.push(full);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return out.sort();
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Combine per-PRD verdicts into the run-wide one. failing > unparsed > healthy.
|
|
176
|
+
* Exported for the spec.
|
|
177
|
+
*/
|
|
178
|
+
export function combineOverall(verdicts) {
|
|
179
|
+
if (verdicts.length === 0)
|
|
180
|
+
return 'unparsed';
|
|
181
|
+
if (verdicts.some((v) => v === 'failing'))
|
|
182
|
+
return 'failing';
|
|
183
|
+
if (verdicts.some((v) => v === 'unparsed'))
|
|
184
|
+
return 'unparsed';
|
|
185
|
+
return 'healthy';
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Parse the CLI argv tail. Accepts:
|
|
189
|
+
*
|
|
190
|
+
* pugi prd-check -> error (no target)
|
|
191
|
+
* pugi prd-check <path> -> single PRD
|
|
192
|
+
* pugi prd-check --all -> scan docs/prd/**.md
|
|
193
|
+
* pugi prd-check <path> --json -> single PRD, JSON envelope
|
|
194
|
+
*
|
|
195
|
+
* `--json` is also forwarded from the global flag set in cli.ts;
|
|
196
|
+
* the local parse re-honours it so the slash command can use the
|
|
197
|
+
* same parser without the global flag plumbing.
|
|
198
|
+
*/
|
|
199
|
+
export function parsePrdCheckArgs(args, options) {
|
|
200
|
+
let json = options.jsonDefault;
|
|
201
|
+
let all = false;
|
|
202
|
+
let prdPath;
|
|
203
|
+
for (const arg of args) {
|
|
204
|
+
if (arg === '--json') {
|
|
205
|
+
json = true;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (arg === '--all') {
|
|
209
|
+
all = true;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (arg.startsWith('--')) {
|
|
213
|
+
return { ok: false, error: `unknown flag: ${arg}` };
|
|
214
|
+
}
|
|
215
|
+
if (prdPath !== undefined) {
|
|
216
|
+
return { ok: false, error: `unexpected extra argument: ${arg}` };
|
|
217
|
+
}
|
|
218
|
+
prdPath = arg;
|
|
219
|
+
}
|
|
220
|
+
if (!all && prdPath === undefined) {
|
|
221
|
+
return {
|
|
222
|
+
ok: false,
|
|
223
|
+
error: 'pugi prd-check <prd-path> | --all (pass a PRD path or --all to scan docs/prd/**.md)',
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (all && prdPath !== undefined) {
|
|
227
|
+
return { ok: false, error: 'cannot combine <path> with --all' };
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
ok: true,
|
|
231
|
+
flags: { json, all },
|
|
232
|
+
...(prdPath !== undefined ? { prdPath } : {}),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
//# sourceMappingURL=prd-check.js.map
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pugi release-notes` — changelog diff between last-seen + current
|
|
3
|
+
* (Leak L24, 2026-05-27).
|
|
4
|
+
*
|
|
5
|
+
* Parity command with Claude Code's `/release-notes`, which shows
|
|
6
|
+
* what changed between the previously-installed CLI version and the
|
|
7
|
+
* currently-installed one. The Pugi variant reads the bundled
|
|
8
|
+
* `CHANGELOG.md`, slices it к the range `(last-seen, current]`, and
|
|
9
|
+
* renders the Markdown sections to the operator. After а successful
|
|
10
|
+
* render the marker is bumped к `current` so the next invocation is а
|
|
11
|
+
* no-op until the operator upgrades again.
|
|
12
|
+
*
|
|
13
|
+
* # Module contract
|
|
14
|
+
*
|
|
15
|
+
* - This file owns the WIRING from CLI flags + ambient state к the
|
|
16
|
+
* parser + state I/O helpers. The parser + state modules в
|
|
17
|
+
* `core/release-notes/` have zero coupling к the CLI dispatch
|
|
18
|
+
* surface.
|
|
19
|
+
*
|
|
20
|
+
* - `runReleaseNotesCommand` is the single entry point. Both the
|
|
21
|
+
* top-level `pugi release-notes` handler в `runtime/cli.ts` AND
|
|
22
|
+
* the in-REPL `/release-notes` slash command call it. The
|
|
23
|
+
* function returns а structured `ReleaseNotesResult` so the
|
|
24
|
+
* slash dispatcher can route the lines к the system pane
|
|
25
|
+
* without re-reading the changelog.
|
|
26
|
+
*
|
|
27
|
+
* - Exit code is ALWAYS 0. The command is informational, never а
|
|
28
|
+
* gate. Read failures, missing CHANGELOG, and write failures all
|
|
29
|
+
* degrade к а structured envelope with а human-readable footer.
|
|
30
|
+
*
|
|
31
|
+
* - The changelog source is captured behind а function so the spec
|
|
32
|
+
* can stub it without touching disk. The default reads the file
|
|
33
|
+
* bundled with the CLI install (resolved relative к the package
|
|
34
|
+
* root); fixtures pass an in-memory string.
|
|
35
|
+
*
|
|
36
|
+
* - `--reset` flag clears the last-seen marker AND re-renders the
|
|
37
|
+
* full bundled changelog as if the operator had never run the
|
|
38
|
+
* command. Distinct from а plain `--all` toggle because the
|
|
39
|
+
* reset PERSISTS (the next invocation again shows everything
|
|
40
|
+
* newer than the cleared marker — `none`).
|
|
41
|
+
*/
|
|
42
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
43
|
+
import { homedir } from 'node:os';
|
|
44
|
+
import { dirname, resolve } from 'node:path';
|
|
45
|
+
import { fileURLToPath } from 'node:url';
|
|
46
|
+
import { parseChangelog, sliceVersionsBetween, } from '../../core/release-notes/parser.js';
|
|
47
|
+
import { clearLastSeenVersion, readLastSeenVersion, writeLastSeenVersion, } from '../../core/release-notes/state.js';
|
|
48
|
+
import { PUGI_CLI_VERSION } from '../version.js';
|
|
49
|
+
/**
|
|
50
|
+
* Default loader для the bundled `apps/pugi-cli/CHANGELOG.md`. The
|
|
51
|
+
* compiled bundle ships under `dist/runtime/commands/release-notes.js`;
|
|
52
|
+
* the CHANGELOG sits next к `package.json` at the package root, two
|
|
53
|
+
* directories up from `dist/runtime/commands/`. We also probe а
|
|
54
|
+
* couple of fallback locations so the dev path (running the source
|
|
55
|
+
* directly из `src/`) works без а compile step.
|
|
56
|
+
*/
|
|
57
|
+
export function defaultReadChangelog() {
|
|
58
|
+
const candidates = resolveChangelogCandidates();
|
|
59
|
+
for (const candidate of candidates) {
|
|
60
|
+
try {
|
|
61
|
+
if (existsSync(candidate)) {
|
|
62
|
+
return readFileSync(candidate, 'utf8');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Permission errors, transient FS hiccups — keep probing the
|
|
67
|
+
// remaining candidates. Returning null at the end is fine; the
|
|
68
|
+
// renderer surfaces а "changelog-missing" envelope.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
function resolveChangelogCandidates() {
|
|
74
|
+
// import.meta.url points к the compiled JS in production
|
|
75
|
+
// (`dist/runtime/commands/release-notes.js`) and к the source TS в
|
|
76
|
+
// tests / dev runs. We probe both relative ancestries so either
|
|
77
|
+
// path lands on `<package>/CHANGELOG.md`.
|
|
78
|
+
try {
|
|
79
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
80
|
+
return [
|
|
81
|
+
resolve(here, '../../..', 'CHANGELOG.md'),
|
|
82
|
+
resolve(here, '../../../..', 'CHANGELOG.md'),
|
|
83
|
+
resolve(process.cwd(), 'apps/pugi-cli/CHANGELOG.md'),
|
|
84
|
+
resolve(process.cwd(), 'CHANGELOG.md'),
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Some non-ESM contexts (very old node, eval'd code) reject
|
|
89
|
+
// `import.meta.url`. Fall back к cwd-relative probes — works for
|
|
90
|
+
// tests that run from the package root.
|
|
91
|
+
return [
|
|
92
|
+
resolve(process.cwd(), 'apps/pugi-cli/CHANGELOG.md'),
|
|
93
|
+
resolve(process.cwd(), 'CHANGELOG.md'),
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Default home dir resolver. Centralised so the CLI handler can call
|
|
99
|
+
* `runReleaseNotesCommand` without re-importing `os.homedir`.
|
|
100
|
+
*/
|
|
101
|
+
export function defaultReleaseNotesHome() {
|
|
102
|
+
return homedir();
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Pick + render the release notes, hand the result к the output
|
|
106
|
+
* sink, persist the marker. Always exits 0.
|
|
107
|
+
*/
|
|
108
|
+
export function runReleaseNotesCommand(ctx) {
|
|
109
|
+
const readChangelog = ctx.readChangelog ?? defaultReadChangelog;
|
|
110
|
+
const currentVersion = ctx.currentVersion ?? PUGI_CLI_VERSION;
|
|
111
|
+
// `--reset` clears the marker before slicing. We capture the
|
|
112
|
+
// pre-clear value so the JSON envelope still shows the operator
|
|
113
|
+
// what their previous marker was, which makes scripting + bug
|
|
114
|
+
// reports easier.
|
|
115
|
+
const lastSeenBefore = readLastSeenVersion(ctx.home);
|
|
116
|
+
if (ctx.reset) {
|
|
117
|
+
clearLastSeenVersion(ctx.home);
|
|
118
|
+
}
|
|
119
|
+
const lastSeen = ctx.reset ? null : lastSeenBefore;
|
|
120
|
+
const raw = readChangelog();
|
|
121
|
+
if (raw === null) {
|
|
122
|
+
const result = {
|
|
123
|
+
command: 'release-notes',
|
|
124
|
+
currentVersion,
|
|
125
|
+
lastSeenVersion: lastSeenBefore ?? 'none',
|
|
126
|
+
sections: [],
|
|
127
|
+
status: 'changelog-missing',
|
|
128
|
+
markerPersisted: false,
|
|
129
|
+
persistFailure: null,
|
|
130
|
+
text: renderMissingChangelog(currentVersion),
|
|
131
|
+
};
|
|
132
|
+
ctx.writeOutput(result, result.text);
|
|
133
|
+
process.exitCode = 0;
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
const sections = parseChangelog(raw);
|
|
137
|
+
if (sections.length === 0) {
|
|
138
|
+
const result = {
|
|
139
|
+
command: 'release-notes',
|
|
140
|
+
currentVersion,
|
|
141
|
+
lastSeenVersion: lastSeenBefore ?? 'none',
|
|
142
|
+
sections: [],
|
|
143
|
+
status: 'changelog-empty',
|
|
144
|
+
markerPersisted: false,
|
|
145
|
+
persistFailure: null,
|
|
146
|
+
text: renderEmptyChangelog(currentVersion),
|
|
147
|
+
};
|
|
148
|
+
ctx.writeOutput(result, result.text);
|
|
149
|
+
process.exitCode = 0;
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
const slice = sliceVersionsBetween(sections, lastSeen, currentVersion);
|
|
153
|
+
if (slice.length === 0) {
|
|
154
|
+
// Nothing new — render the no-op message and DO NOT touch the
|
|
155
|
+
// marker (marker already equals current OR is newer; either way
|
|
156
|
+
// re-writing it is а no-op write we can avoid).
|
|
157
|
+
const result = {
|
|
158
|
+
command: 'release-notes',
|
|
159
|
+
currentVersion,
|
|
160
|
+
lastSeenVersion: lastSeenBefore ?? 'none',
|
|
161
|
+
sections: [],
|
|
162
|
+
status: 'up-to-date',
|
|
163
|
+
markerPersisted: false,
|
|
164
|
+
persistFailure: null,
|
|
165
|
+
text: renderUpToDate(currentVersion, lastSeenBefore),
|
|
166
|
+
};
|
|
167
|
+
ctx.writeOutput(result, result.text);
|
|
168
|
+
process.exitCode = 0;
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
const persist = writeLastSeenVersion(ctx.home, currentVersion);
|
|
172
|
+
const text = renderSections(slice, currentVersion, lastSeen, persist);
|
|
173
|
+
const result = {
|
|
174
|
+
command: 'release-notes',
|
|
175
|
+
currentVersion,
|
|
176
|
+
lastSeenVersion: lastSeenBefore ?? 'none',
|
|
177
|
+
sections: slice,
|
|
178
|
+
status: ctx.reset ? 'reset' : 'rendered',
|
|
179
|
+
markerPersisted: persist.status === 'ok',
|
|
180
|
+
persistFailure: persist.status === 'failed' ? persist.reason : null,
|
|
181
|
+
text,
|
|
182
|
+
};
|
|
183
|
+
ctx.writeOutput(result, text);
|
|
184
|
+
process.exitCode = 0;
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
function renderSections(sections, current, lastSeen, persist) {
|
|
188
|
+
const header = lastSeen
|
|
189
|
+
? `Pugi release notes — ${lastSeen} → ${current}`
|
|
190
|
+
: `Pugi release notes — up to ${current}`;
|
|
191
|
+
const blocks = [header, '═'.repeat(Math.max(header.length, 30))];
|
|
192
|
+
for (const section of sections) {
|
|
193
|
+
const subhead = section.date
|
|
194
|
+
? `## [${section.version}] - ${section.date}`
|
|
195
|
+
: `## [${section.version}]`;
|
|
196
|
+
blocks.push('');
|
|
197
|
+
blocks.push(subhead);
|
|
198
|
+
if (section.body.length > 0) {
|
|
199
|
+
blocks.push('');
|
|
200
|
+
blocks.push(section.body);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (persist.status === 'failed') {
|
|
204
|
+
blocks.push('');
|
|
205
|
+
blocks.push(`Warning: could not persist last-seen marker (${persist.reason}). Next run will surface the same notes.`);
|
|
206
|
+
}
|
|
207
|
+
return blocks.join('\n');
|
|
208
|
+
}
|
|
209
|
+
function renderUpToDate(current, lastSeen) {
|
|
210
|
+
const lines = ['No new release notes.'];
|
|
211
|
+
lines.push(`Installed: ${current}`);
|
|
212
|
+
lines.push(`Last seen: ${lastSeen ?? 'none'}`);
|
|
213
|
+
return lines.join('\n');
|
|
214
|
+
}
|
|
215
|
+
function renderMissingChangelog(current) {
|
|
216
|
+
return [
|
|
217
|
+
'Release notes are not bundled with this install.',
|
|
218
|
+
`Installed: ${current}`,
|
|
219
|
+
'See https://pugi.io/changelog for the rendered changelog.',
|
|
220
|
+
].join('\n');
|
|
221
|
+
}
|
|
222
|
+
function renderEmptyChangelog(current) {
|
|
223
|
+
return [
|
|
224
|
+
'Bundled changelog is empty — no parsed sections.',
|
|
225
|
+
`Installed: ${current}`,
|
|
226
|
+
'See https://pugi.io/changelog for the rendered changelog.',
|
|
227
|
+
].join('\n');
|
|
228
|
+
}
|
|
229
|
+
//# sourceMappingURL=release-notes.js.map
|