@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
|
@@ -38,20 +38,26 @@ import { listRoles } from '../agents/registry.js';
|
|
|
38
38
|
* silently appear here with empty placeholders.
|
|
39
39
|
*/
|
|
40
40
|
export const SLASH_STUB_MESSAGES = Object.freeze({
|
|
41
|
-
|
|
41
|
+
// Leak L8 (2026-05-27): /compact graduated from stub. The session
|
|
42
|
+
// module now owns the summariser round-trip + boundary marker append
|
|
43
|
+
// via `dispatchCompact`. Keep the type record exhaustive so a future
|
|
44
|
+
// stub addition cannot silently overlap the wired set.
|
|
42
45
|
memory: 'Session memory editor lands in α6.5b.',
|
|
43
46
|
config: 'Run `pugi config list` from a fresh shell for the full surface; in-REPL editor lands in α6.5.',
|
|
44
47
|
// alpha 6.13: /privacy graduated from stub; nothing reads this at
|
|
45
48
|
// runtime but the type record stays exhaustive.
|
|
46
49
|
privacy: '',
|
|
47
50
|
budget: 'Run `pugi budget` from a fresh shell; in-REPL summary lands in α6.5.',
|
|
48
|
-
|
|
51
|
+
// β4 Sl7 (2026-05-26): /mcp graduated from stub to a real handler
|
|
52
|
+
// that forwards to `runMcpCommand`. Stub message removed from the
|
|
53
|
+
// exhaustive record so the type narrows correctly.
|
|
49
54
|
undo: 'Run `pugi undo` from a fresh shell; in-REPL undo lands in α6.5.',
|
|
50
55
|
});
|
|
51
56
|
export const SLASH_COMMAND_HELP = Object.freeze([
|
|
52
57
|
// Workforce dispatch
|
|
53
58
|
{ name: 'brief', args: '<text>', gloss: 'Dispatch a brief to the workforce', group: 'Workforce dispatch' },
|
|
54
59
|
{ name: 'agents', args: '', gloss: 'List the on-watch agent roster', group: 'Workforce dispatch' },
|
|
60
|
+
{ name: 'delegate', args: '<slug> <brief>', gloss: 'Dispatch a brief to one Tier 1 specialist (α7.5)', group: 'Workforce dispatch' },
|
|
55
61
|
{ name: 'stop', args: '<persona>', gloss: 'Stop one agent by persona slug', group: 'Workforce dispatch' },
|
|
56
62
|
{ name: 'jobs', args: '', gloss: 'List background jobs', group: 'Workforce dispatch' },
|
|
57
63
|
{ name: 'ask', args: '<question>', gloss: 'Surface a yes/no modal locally (α6.3 forcing question)', group: 'Workforce dispatch' },
|
|
@@ -59,23 +65,42 @@ export const SLASH_COMMAND_HELP = Object.freeze([
|
|
|
59
65
|
{ name: 'clear', args: '', gloss: 'Clear conversation pane', group: 'Session' },
|
|
60
66
|
{ name: 'resume', args: '', gloss: 'Pick a stored session to restore', group: 'Session' },
|
|
61
67
|
{ name: 'context', args: '', gloss: 'Show three-tier context summary (Tier 0 skeleton + Tier 1 working set)', group: 'Session' },
|
|
62
|
-
{ name: 'compact', args: '', gloss: '
|
|
68
|
+
{ name: 'compact', args: '[--force]', gloss: 'Summarise older turns into a boundary marker (leak L8). --force bypasses the noop-empty guard', group: 'Session' },
|
|
69
|
+
{ name: 'rewind', args: '[N | --to <id>]', gloss: 'Roll the conversation back to a checkpoint (leak L9)', group: 'Session' },
|
|
63
70
|
{ name: 'memory', args: '', gloss: 'Session memory editor (α6.5b)', group: 'Session', stub: true },
|
|
71
|
+
{ name: 'init', args: '', gloss: 'Scaffold .pugi/ in the current workspace (β1 Sl11)', group: 'Session' },
|
|
64
72
|
// Pugi tools
|
|
65
73
|
{ name: 'web', args: '<url>', gloss: 'Fetch a URL into context', group: 'Pugi tools' },
|
|
66
74
|
{ name: 'diff', args: '', gloss: 'Show pending diff', group: 'Pugi tools' },
|
|
67
|
-
{ name: 'cost', args: '', gloss: '
|
|
68
|
-
{ name: '
|
|
75
|
+
{ name: 'cost', args: '', gloss: 'Session token + USD totals + last 5 turn breakdown', group: 'Pugi tools' },
|
|
76
|
+
{ name: 'quota', args: '', gloss: 'Plan tier + monthly usage caps (sync / review / engine)', group: 'Pugi tools' },
|
|
77
|
+
{ name: 'status', args: '', gloss: 'Session snapshot — id · cwd · mode · tokens · dispatches · auth', group: 'Pugi tools' },
|
|
69
78
|
{ name: 'consensus', args: '[ref]', gloss: '3-model consensus review (codex · claude · deepseek)', group: 'Pugi tools' },
|
|
79
|
+
{ name: 'repo-map', args: '[refresh]', gloss: 'AST-light symbol summary of the workspace (leak L28)', group: 'Pugi tools' },
|
|
70
80
|
// Settings
|
|
71
81
|
{ name: 'config', args: '', gloss: 'Show config', group: 'Settings', stub: true },
|
|
72
82
|
{ name: 'privacy', args: '', gloss: 'Show privacy mode + contract', group: 'Settings' },
|
|
83
|
+
{ name: 'permissions', args: '[mode] [--persist]', gloss: 'Show or flip permission mode (plan / ask / allow / bypass) (also: /plan)', group: 'Settings' },
|
|
84
|
+
{ name: 'plan', args: '[--back | --persist] [<prompt>]', gloss: 'Switch to plan mode (read-only). Same as /permissions plan, slicker UX.', group: 'Settings' },
|
|
85
|
+
{ name: 'model', args: '[<slug>]', gloss: 'Show or select the active model. Bare /model lists tier-gated options', group: 'Settings' },
|
|
73
86
|
{ name: 'budget', args: '', gloss: 'Show usage budget', group: 'Settings', stub: true },
|
|
74
|
-
{ name: 'mcp', args: '', gloss: '
|
|
87
|
+
{ name: 'mcp', args: '[sub]', gloss: 'MCP servers — list / trust / deny / install / serve / perms', group: 'Settings' },
|
|
88
|
+
{ name: 'style', args: '[name] [--persist|--reset|--list]', gloss: 'Output-style preset (default / terse / explanatory / russian-formal / casual)', group: 'Settings' },
|
|
89
|
+
{ name: 'theme', args: '[name] [--persist|--reset|--list]', gloss: 'TUI color palette (default / dark / light / colorblind)', group: 'Settings' },
|
|
90
|
+
{ name: 'onboarding', args: '[--reset|--non-interactive]', gloss: 'First-run wizard — auth / mode / style / MCP / telemetry (leak L25)', group: 'Settings' },
|
|
91
|
+
{ name: 'vim', args: '[on|off|status]', gloss: 'Toggle vim-style modal editing in the input buffer (leak L26)', group: 'Settings' },
|
|
75
92
|
{ name: 'undo', args: '', gloss: 'Undo last write', group: 'Settings', stub: true },
|
|
76
93
|
// Meta
|
|
77
94
|
{ name: 'help', args: '', gloss: 'Show this help overlay', group: 'Meta' },
|
|
78
95
|
{ name: 'version', args: '', gloss: 'Show CLI version', group: 'Meta' },
|
|
96
|
+
{ name: 'doctor', args: '', gloss: 'Environment health report (auth · API · Node · disk · MCP · …)', group: 'Meta' },
|
|
97
|
+
{ name: 'prd-check', args: '<prd-path | --all> [--json]', gloss: 'Verify PRD acceptance criteria against committed code/tests/docs (Wave 6)', group: 'Meta' },
|
|
98
|
+
{ name: 'chain', args: '<new|status|next|show|export|list> [...args]', gloss: 'Artifact chain — PRD → ADR → mindmap → ER → sequence → tests → code (Wave 6)', group: 'Meta' },
|
|
99
|
+
{ name: 'stickers', args: '', gloss: 'show Pugi brand stickers (gimmick)', group: 'Meta' },
|
|
100
|
+
{ name: 'feedback', args: '', gloss: 'file a bug / feature / general comment without leaving the REPL', group: 'Meta' },
|
|
101
|
+
{ name: 'share', args: '[--gist|--pugi] [--redact] [--preview]', gloss: 'Export session transcript to gist / pugi.io (leak L20)', group: 'Meta' },
|
|
102
|
+
{ name: 'release-notes', args: '[--reset]', gloss: 'Show changelog diff since last upgrade (leak L24)', group: 'Meta' },
|
|
103
|
+
{ name: 'update', args: '[--check|--apply [--yes]] [--channel <name>]', gloss: 'Check for / apply CLI update on stable / beta / canary (leak L27)', group: 'Meta' },
|
|
79
104
|
{ name: 'quit', args: '', gloss: 'Exit the REPL', group: 'Meta' },
|
|
80
105
|
]);
|
|
81
106
|
/**
|
|
@@ -135,6 +160,38 @@ export function parseSlashCommand(input) {
|
|
|
135
160
|
case 'roster': {
|
|
136
161
|
return { kind: 'roster' };
|
|
137
162
|
}
|
|
163
|
+
case 'delegate': {
|
|
164
|
+
// tail must start with the persona slug followed by the brief.
|
|
165
|
+
// Slug accepts only the closed-set lowercase ASCII pattern the
|
|
166
|
+
// server-side persona registry enforces; anything else surfaces
|
|
167
|
+
// as a usage error so the operator sees the typo before the
|
|
168
|
+
// round-trip.
|
|
169
|
+
const innerSpace = tail.indexOf(' ');
|
|
170
|
+
if (innerSpace === -1 || innerSpace === 0) {
|
|
171
|
+
return {
|
|
172
|
+
kind: 'error',
|
|
173
|
+
message: 'Usage: /delegate <slug> <one-sentence brief>',
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const persona = tail.slice(0, innerSpace).toLowerCase();
|
|
177
|
+
const brief = tail.slice(innerSpace + 1).trim();
|
|
178
|
+
// Pattern intentionally mirrors server-side PUGI_DELEGATE_REGEX in
|
|
179
|
+
// sessions.controller.ts (^[a-z]+$). Keeping them lockstep means
|
|
180
|
+
// the REPL surfaces typos locally instead of round-tripping a 4xx.
|
|
181
|
+
if (!/^[a-z]+$/.test(persona)) {
|
|
182
|
+
return {
|
|
183
|
+
kind: 'error',
|
|
184
|
+
message: `/delegate slug must be lowercase ASCII (a-z only); got '${persona}'`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if (brief.length === 0) {
|
|
188
|
+
return {
|
|
189
|
+
kind: 'error',
|
|
190
|
+
message: 'Usage: /delegate <slug> <one-sentence brief>',
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return { kind: 'delegate', persona, brief };
|
|
194
|
+
}
|
|
138
195
|
case 'stop':
|
|
139
196
|
case 'kill': {
|
|
140
197
|
if (tail.length === 0) {
|
|
@@ -181,9 +238,19 @@ export function parseSlashCommand(input) {
|
|
|
181
238
|
case 'diff': {
|
|
182
239
|
return { kind: 'diff' };
|
|
183
240
|
}
|
|
184
|
-
case 'cost':
|
|
241
|
+
case 'cost':
|
|
242
|
+
case 'usage': {
|
|
243
|
+
// L19 (2026-05-27): `/usage` is an alias of `/cost` per the cost-
|
|
244
|
+
// command spec. The previous mapping routed `/usage` to the
|
|
245
|
+
// network-backed `/quota` surface, but operators trained on Claude
|
|
246
|
+
// Code expect `/usage` to surface the per-model token breakdown
|
|
247
|
+
// (same shape as `/cost`). `/quota` remains the canonical name
|
|
248
|
+
// for the tier + monthly-cap fetch.
|
|
185
249
|
return { kind: 'cost' };
|
|
186
250
|
}
|
|
251
|
+
case 'quota': {
|
|
252
|
+
return { kind: 'quota' };
|
|
253
|
+
}
|
|
187
254
|
case 'status': {
|
|
188
255
|
return { kind: 'status' };
|
|
189
256
|
}
|
|
@@ -215,11 +282,280 @@ export function parseSlashCommand(input) {
|
|
|
215
282
|
// device flow + audit identity are wired correctly).
|
|
216
283
|
return { kind: 'privacy' };
|
|
217
284
|
}
|
|
218
|
-
case '
|
|
285
|
+
case 'permissions':
|
|
286
|
+
case 'perms': {
|
|
287
|
+
// Leak L6: `/permissions [mode] [--persist] [--confirm]`.
|
|
288
|
+
//
|
|
289
|
+
// Argument grammar (single line, no quoting):
|
|
290
|
+
// /permissions -> show current mode + table
|
|
291
|
+
// /permissions plan|ask|allow -> flip mode
|
|
292
|
+
// /permissions bypass --confirm -> flip to bypass (refused
|
|
293
|
+
// without --confirm — safety)
|
|
294
|
+
// /permissions <mode> --persist -> also write to ~/.pugi/config.json
|
|
295
|
+
//
|
|
296
|
+
// Anything else returns an `error` result so the runtime can
|
|
297
|
+
// render the usage hint inline.
|
|
298
|
+
const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
|
|
299
|
+
if (tokens.length === 0) {
|
|
300
|
+
return { kind: 'permissions', persist: false, confirmBypass: false };
|
|
301
|
+
}
|
|
302
|
+
const head0 = tokens[0]?.toLowerCase();
|
|
303
|
+
if (head0 !== 'plan' && head0 !== 'ask' && head0 !== 'allow' && head0 !== 'bypass') {
|
|
304
|
+
return {
|
|
305
|
+
kind: 'error',
|
|
306
|
+
message: `Usage: /permissions [plan|ask|allow|bypass] [--persist] [--confirm]; unknown mode '${tokens[0] ?? ''}'`,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
const flags = tokens.slice(1);
|
|
310
|
+
let persist = false;
|
|
311
|
+
let confirmBypass = false;
|
|
312
|
+
for (const flag of flags) {
|
|
313
|
+
if (flag === '--persist') {
|
|
314
|
+
persist = true;
|
|
315
|
+
}
|
|
316
|
+
else if (flag === '--confirm') {
|
|
317
|
+
confirmBypass = true;
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
return {
|
|
321
|
+
kind: 'error',
|
|
322
|
+
message: `/permissions: unknown flag '${flag}' (allowed: --persist, --confirm)`,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return { kind: 'permissions', mode: head0, persist, confirmBypass };
|
|
327
|
+
}
|
|
328
|
+
case 'init': {
|
|
329
|
+
// β1 Sl11: surface the init flow inside the REPL. Tail args
|
|
330
|
+
// are ignored — the init handler is parameterless today; `pugi
|
|
331
|
+
// init --no-defaults` is the CLI surface for skipping bundled
|
|
332
|
+
// skills.
|
|
333
|
+
return { kind: 'init' };
|
|
334
|
+
}
|
|
335
|
+
case 'plan': {
|
|
336
|
+
// Leak L7: `/plan [--back | --persist] [<prompt>]`.
|
|
337
|
+
//
|
|
338
|
+
// Argument grammar (single line, no quoting):
|
|
339
|
+
// /plan -> enter plan mode + banner
|
|
340
|
+
// /plan --back -> restore previous mode
|
|
341
|
+
// /plan --persist -> enter + write global config
|
|
342
|
+
// /plan <prompt...> -> enter + run one-shot engine
|
|
343
|
+
// /plan --auto-back <prompt...> -> enter + run + restore mode
|
|
344
|
+
//
|
|
345
|
+
// The parser pulls the flags off the head of the tail; whatever
|
|
346
|
+
// remains is the prompt. `--back` + a non-empty prompt and
|
|
347
|
+
// `--back` + `--auto-back` are both refused as `error` because
|
|
348
|
+
// they conflict at the verb level.
|
|
349
|
+
const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
|
|
350
|
+
let back = false;
|
|
351
|
+
let persist = false;
|
|
352
|
+
let autoBack = false;
|
|
353
|
+
const promptTokens = [];
|
|
354
|
+
for (const token of tokens) {
|
|
355
|
+
if (token === '--back') {
|
|
356
|
+
back = true;
|
|
357
|
+
}
|
|
358
|
+
else if (token === '--persist') {
|
|
359
|
+
persist = true;
|
|
360
|
+
}
|
|
361
|
+
else if (token === '--auto-back') {
|
|
362
|
+
autoBack = true;
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
promptTokens.push(token);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const prompt = promptTokens.join(' ');
|
|
369
|
+
if (back && prompt.length > 0) {
|
|
370
|
+
return {
|
|
371
|
+
kind: 'error',
|
|
372
|
+
message: '/plan --back does not accept a prompt; revert first, then dispatch.',
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
if (back && autoBack) {
|
|
376
|
+
return {
|
|
377
|
+
kind: 'error',
|
|
378
|
+
message: '/plan --back and --auto-back cannot be combined.',
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
return { kind: 'plan', back, persist, autoBack, prompt };
|
|
382
|
+
}
|
|
383
|
+
case 'model': {
|
|
384
|
+
// Wave 6 BT 8 (Claude Code parity): `/model [<slug>]`. Bare form
|
|
385
|
+
// prints the tier-gated model menu + current selection; with a
|
|
386
|
+
// slug it flips workspace selection. Slug grammar (loose): alnum
|
|
387
|
+
// + dash + dot + slash. Anything outside that range becomes an
|
|
388
|
+
// error verdict so the operator sees a clear message instead of a
|
|
389
|
+
// silent no-op. Whitespace inside the tail = multiple tokens = we
|
|
390
|
+
// take the first; the help gloss documents single-slug usage.
|
|
391
|
+
const trimmedTail = tail.trim();
|
|
392
|
+
if (trimmedTail.length === 0) {
|
|
393
|
+
return { kind: 'model', slug: undefined };
|
|
394
|
+
}
|
|
395
|
+
const firstToken = trimmedTail.split(/\s+/)[0] ?? '';
|
|
396
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._\-\/]{0,63}$/.test(firstToken)) {
|
|
397
|
+
return {
|
|
398
|
+
kind: 'error',
|
|
399
|
+
message: `/model: invalid slug '${firstToken}'. Use letters / digits / '-' / '.' / '/' only.`,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
return { kind: 'model', slug: firstToken };
|
|
403
|
+
}
|
|
404
|
+
case 'mcp': {
|
|
405
|
+
// β4 Sl7: tokenize the tail. Empty tail -> `list` (matches CLI).
|
|
406
|
+
// Quoting / shell-escapes are NOT supported — the slash surface is
|
|
407
|
+
// intentionally simple; complex installs (env vars, multi-word
|
|
408
|
+
// args) go through `pugi mcp install` from a fresh shell.
|
|
409
|
+
const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
|
|
410
|
+
return { kind: 'mcp', args: tokens };
|
|
411
|
+
}
|
|
412
|
+
case 'style':
|
|
413
|
+
case 'output-style': {
|
|
414
|
+
// Leak L18 (2026-05-27): forward the tokenized tail unchanged so
|
|
415
|
+
// the slash + top-level CLI surfaces share one parser inside
|
|
416
|
+
// `runStyleCommand`. Quoting / multi-word args are not used (the
|
|
417
|
+
// preset slugs are single tokens by contract).
|
|
418
|
+
const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
|
|
419
|
+
return { kind: 'style', args: tokens };
|
|
420
|
+
}
|
|
421
|
+
case 'theme':
|
|
422
|
+
case 'palette':
|
|
423
|
+
case 'colors': {
|
|
424
|
+
// Leak L30 (2026-05-27): forward the tokenized tail unchanged so
|
|
425
|
+
// the slash + top-level `pugi theme` surfaces share one parser
|
|
426
|
+
// inside `runThemeCommand`. Aliases `/palette` and `/colors`
|
|
427
|
+
// exist because the leak-landscape audit found operators reach
|
|
428
|
+
// for either word interchangeably — same surface, same handler.
|
|
429
|
+
const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
|
|
430
|
+
return { kind: 'theme', args: tokens };
|
|
431
|
+
}
|
|
432
|
+
case 'onboarding':
|
|
433
|
+
case 'onboard':
|
|
434
|
+
case 'setup': {
|
|
435
|
+
// Leak L25 (2026-05-27): forward the tokenized tail unchanged.
|
|
436
|
+
// The slash always routes through the non-interactive snapshot
|
|
437
|
+
// path (the REPL already owns the Ink tree); the runner picks
|
|
438
|
+
// it up from `ctx.interactive = false` in the session
|
|
439
|
+
// dispatcher.
|
|
440
|
+
const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
|
|
441
|
+
return { kind: 'onboarding', args: tokens };
|
|
442
|
+
}
|
|
443
|
+
case 'vim': {
|
|
444
|
+
// Leak L26 (2026-05-27): forward the tokenized tail unchanged so
|
|
445
|
+
// the slash + top-level CLI surfaces share one parser inside
|
|
446
|
+
// `runVimCommand`. Subcommands are single tokens (on / off /
|
|
447
|
+
// status); a bare `/vim` toggles.
|
|
448
|
+
const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
|
|
449
|
+
return { kind: 'vim', args: tokens };
|
|
450
|
+
}
|
|
451
|
+
case 'doctor':
|
|
452
|
+
case 'health': {
|
|
453
|
+
// L17 (2026-05-27): run the probe sweep inline. Tail is ignored —
|
|
454
|
+
// the doctor command has no operator-facing arguments (every
|
|
455
|
+
// probe runs unconditionally; per-probe disable lives on the CLI
|
|
456
|
+
// shell surface, not the slash one).
|
|
457
|
+
return { kind: 'doctor' };
|
|
458
|
+
}
|
|
459
|
+
case 'prd-check':
|
|
460
|
+
case 'prdcheck': {
|
|
461
|
+
// Wave 6 (2026-05-27): tokenise the tail and forward verbatim
|
|
462
|
+
// so the slash + shell surfaces share one `parsePrdCheckArgs`.
|
|
463
|
+
// Supports `<prd-path>`, `--all`, and `--json`.
|
|
464
|
+
const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
|
|
465
|
+
return { kind: 'prd-check', args: tokens };
|
|
466
|
+
}
|
|
467
|
+
case 'chain': {
|
|
468
|
+
// Wave 6 (2026-05-27): forward the tokenized argv to
|
|
469
|
+
// `runChainCommand` via the session module. Subcommands are
|
|
470
|
+
// single tokens (new / status / next / show / export / list);
|
|
471
|
+
// the `new` subcommand accepts the intent as the joined tail so
|
|
472
|
+
// operators can write `/chain new add auth flow` без quoting.
|
|
473
|
+
const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
|
|
474
|
+
return { kind: 'chain', args: tokens };
|
|
475
|
+
}
|
|
476
|
+
case 'compact': {
|
|
477
|
+
// Leak L8 (2026-05-27): graduated from stub. The session module
|
|
478
|
+
// owns the summariser round-trip. Wave 6 BT 8: `--force` overrides
|
|
479
|
+
// the noop-empty guard. Unknown flags fall through silently per
|
|
480
|
+
// the existing tail-tolerance behaviour (operators wanting a
|
|
481
|
+
// per-session compact run `pugi compact --session <id>` from a
|
|
482
|
+
// fresh shell).
|
|
483
|
+
const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
|
|
484
|
+
const force = tokens.some((t) => t === '--force' || t === '-f');
|
|
485
|
+
return { kind: 'compact', force };
|
|
486
|
+
}
|
|
487
|
+
case 'rewind': {
|
|
488
|
+
// Leak L9 (2026-05-27): `/rewind [N | --to <id>]`. Tokenize the
|
|
489
|
+
// tail unchanged so `runRewindCommand` (in `runtime/commands/
|
|
490
|
+
// rewind.ts`) handles every mode (picker / turns / to-event)
|
|
491
|
+
// through one parser. The slash + top-level CLI surfaces stay
|
|
492
|
+
// single-sourced — same separation as `/compact`.
|
|
493
|
+
const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
|
|
494
|
+
return { kind: 'rewind', args: tokens };
|
|
495
|
+
}
|
|
496
|
+
case 'stickers': {
|
|
497
|
+
// Leak L33 (2026-05-27): brand-personality gimmick. Tail args
|
|
498
|
+
// are ignored — the surface is intentionally parameterless. The
|
|
499
|
+
// session module delegates to the shared `runStickersCommand`
|
|
500
|
+
// so the slash + top-level paths stay single-sourced.
|
|
501
|
+
return { kind: 'stickers' };
|
|
502
|
+
}
|
|
503
|
+
case 'feedback': {
|
|
504
|
+
// Leak L21 (2026-05-27): in-CLI feedback collector. The wizard
|
|
505
|
+
// collects category/rating/comment/context/confirm interactively
|
|
506
|
+
// so the slash surface is parameterless. Tail args are reserved
|
|
507
|
+
// for a future `--message=...` quick-path; today they are
|
|
508
|
+
// accepted but ignored so the operator-level UX matches
|
|
509
|
+
// Claude Code's `/feedback`.
|
|
510
|
+
return { kind: 'feedback' };
|
|
511
|
+
}
|
|
512
|
+
case 'share': {
|
|
513
|
+
// Leak L20 (2026-05-27): forward the tokenized arg list verbatim
|
|
514
|
+
// so the session module (which owns the network + readline
|
|
515
|
+
// affordances) can hand them to runShareCommand. Defaults: no
|
|
516
|
+
// tokens means "auto-pick target + prompt for confirmation".
|
|
517
|
+
const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
|
|
518
|
+
return { kind: 'share', args: tokens };
|
|
519
|
+
}
|
|
520
|
+
case 'repo-map':
|
|
521
|
+
case 'repomap': {
|
|
522
|
+
// Leak L28 (2026-05-27): build + show the AST-light symbol
|
|
523
|
+
// summary. Accepts `refresh` as a positional или `--refresh`
|
|
524
|
+
// flag so muscle memory from both shells lands the same way.
|
|
525
|
+
const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
|
|
526
|
+
const refresh = tokens.includes('--refresh') || tokens.includes('refresh') || tokens.includes('-r');
|
|
527
|
+
return { kind: 'repo-map', refresh };
|
|
528
|
+
}
|
|
529
|
+
case 'release-notes':
|
|
530
|
+
case 'releasenotes':
|
|
531
|
+
case 'changelog': {
|
|
532
|
+
// Leak L24 (2026-05-27): changelog diff between last-seen +
|
|
533
|
+
// installed CLI version. Tail args are tokenized so `--reset`
|
|
534
|
+
// can flip the marker-clear bit; no other flags are honoured —
|
|
535
|
+
// the surface mirrors the CLI top-level's intentional minimalism.
|
|
536
|
+
// `changelog` alias matches operator muscle memory from npm /
|
|
537
|
+
// cargo / brew, all of which ship `changelog` subcommands.
|
|
538
|
+
const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
|
|
539
|
+
const reset = tokens.includes('--reset') || tokens.includes('-r');
|
|
540
|
+
return { kind: 'release-notes', reset };
|
|
541
|
+
}
|
|
542
|
+
case 'update': {
|
|
543
|
+
// Leak L27 (2026-05-27): forward the tokenized argv to the
|
|
544
|
+
// session module which delegates to `runUpdateCommand`. The
|
|
545
|
+
// dispatcher owns argv validation (unknown channel / flag) so
|
|
546
|
+
// the slash parser stays as thin as the rest of the surface.
|
|
547
|
+
// The slash form does NOT support `--apply` because spawning
|
|
548
|
+
// `npm install -g` from inside a running REPL session would
|
|
549
|
+
// corrupt the operator's running binary — the dispatcher treats
|
|
550
|
+
// `--apply` from a slash as a non-interactive offer (probe +
|
|
551
|
+
// install command, no shell-out). Top-level `pugi update --apply`
|
|
552
|
+
// remains the recommended path for the actual install.
|
|
553
|
+
const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
|
|
554
|
+
return { kind: 'update', args: tokens };
|
|
555
|
+
}
|
|
219
556
|
case 'memory':
|
|
220
557
|
case 'config':
|
|
221
558
|
case 'budget':
|
|
222
|
-
case 'mcp':
|
|
223
559
|
case 'undo': {
|
|
224
560
|
const stubName = name;
|
|
225
561
|
return {
|
|
@@ -361,7 +361,7 @@ export class SqliteSessionStore {
|
|
|
361
361
|
// which maps to SQLITE_OPEN_READONLY. The option form is the
|
|
362
362
|
// documented API; the file-URI form (file:...?mode=ro) also works.
|
|
363
363
|
const db = new DatabaseSync(dbPath, { readOnly: true });
|
|
364
|
-
return new SqliteSessionStoreReadOnlyView(db);
|
|
364
|
+
return new SqliteSessionStoreReadOnlyView(db, projectStoreDir);
|
|
365
365
|
}
|
|
366
366
|
/* ------------------------------------------------------------ */
|
|
367
367
|
/* Internals */
|
|
@@ -584,8 +584,37 @@ export class SqliteSessionStore {
|
|
|
584
584
|
*/
|
|
585
585
|
export class SqliteSessionStoreReadOnlyView {
|
|
586
586
|
db;
|
|
587
|
-
|
|
587
|
+
projectStoreDir;
|
|
588
|
+
constructor(db,
|
|
589
|
+
/**
|
|
590
|
+
* Project store directory — required for the JSONL event read path.
|
|
591
|
+
* L9 (2026-05-27): `/rewind` + `/resume` need to walk events from
|
|
592
|
+
* inside the read-only view so the rewind picker + resume preview
|
|
593
|
+
* never take the writer lockfile.
|
|
594
|
+
*/
|
|
595
|
+
projectStoreDir) {
|
|
588
596
|
this.db = db;
|
|
597
|
+
this.projectStoreDir = projectStoreDir;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Read every event for a session via the durable JSONL log. The
|
|
601
|
+
* SQLite cache is NOT used here — JSONL is the source of truth and
|
|
602
|
+
* the cache only holds counters. The walk stitches across rotation
|
|
603
|
+
* files (`events.<n>.jsonl`) in the same order `JsonlEventLog.read`
|
|
604
|
+
* uses inside the writer path so consumers see one consistent stream
|
|
605
|
+
* whether they came in via the writer store OR the read-only view.
|
|
606
|
+
*/
|
|
607
|
+
async events(sessionId, opts) {
|
|
608
|
+
const sessionDir = resolve(this.projectStoreDir, 'sessions', sessionId);
|
|
609
|
+
if (!existsSync(sessionDir))
|
|
610
|
+
return [];
|
|
611
|
+
const log = new JsonlEventLog({ sessionDir });
|
|
612
|
+
try {
|
|
613
|
+
return log.read(opts);
|
|
614
|
+
}
|
|
615
|
+
finally {
|
|
616
|
+
log.close();
|
|
617
|
+
}
|
|
589
618
|
}
|
|
590
619
|
async list(opts) {
|
|
591
620
|
const limit = clampLimit(opts?.limit ?? DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT);
|
|
@@ -27,6 +27,16 @@
|
|
|
27
27
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
28
28
|
import { basename, resolve as resolvePath } from 'node:path';
|
|
29
29
|
import { slugForCwd } from './history.js';
|
|
30
|
+
import { isBareMode } from '../bare-mode/index.js';
|
|
31
|
+
/**
|
|
32
|
+
* Workspace summary shown when the operator launched with `--bare` (or
|
|
33
|
+
* `PUGI_BARE=1`). Leak L22: bare mode disables project auto-discovery
|
|
34
|
+
* across the CLI, so we never read `.pugi/PUGI.md` and never advertise
|
|
35
|
+
* a real workspace label to admin-api. Explicit string so the splash +
|
|
36
|
+
* status bar agree, and so operators triaging "why is Mira ignoring
|
|
37
|
+
* my repo" see a clear cause.
|
|
38
|
+
*/
|
|
39
|
+
export const BARE_MODE_WORKSPACE_LABEL = '(bare mode - auto-discovery disabled)';
|
|
30
40
|
/** Cap on the PUGI.md head we forward. Mirrors the admin-api clamp. */
|
|
31
41
|
const PUGI_MD_HEAD_LIMIT = 200;
|
|
32
42
|
/**
|
|
@@ -48,6 +58,18 @@ export const UNBOUND_WORKSPACE_LABEL = '(not bound - run /init OR cd into projec
|
|
|
48
58
|
export function resolveWorkspaceContext(cwd) {
|
|
49
59
|
const normalised = resolvePath(cwd);
|
|
50
60
|
const slug = slugForCwd(normalised);
|
|
61
|
+
// Leak L22 (2026-05-27): `--bare` short-circuits BEFORE any PUGI.md
|
|
62
|
+
// / project-marker reads so the resolver never advertises a real
|
|
63
|
+
// workspace summary to admin-api. The cwd + slug still travel for
|
|
64
|
+
// telemetry, but the model + Mira treat the session as if launched
|
|
65
|
+
// from a fresh, unbound directory.
|
|
66
|
+
if (isBareMode()) {
|
|
67
|
+
return {
|
|
68
|
+
workspaceCwd: normalised,
|
|
69
|
+
workspaceSlug: slug,
|
|
70
|
+
workspaceSummary: BARE_MODE_WORKSPACE_LABEL,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
51
73
|
// α6.14.2 wave 5: when the cwd has no project markers, prefer the
|
|
52
74
|
// explicit "not bound" summary so admin-api's prompt builder knows
|
|
53
75
|
// not to fabricate a workspace context for Mira/Pugi. The cwd +
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-map build orchestrator — Leak L28 (2026-05-27).
|
|
3
|
+
*
|
|
4
|
+
* Single entry point that the CLI command + the engine boot path both
|
|
5
|
+
* call. Wires the scanner → extractor → cache → formatter pipeline
|
|
6
|
+
* together and surfaces a structured result the caller can render or
|
|
7
|
+
* inject without knowing the inner module shapes.
|
|
8
|
+
*
|
|
9
|
+
* The orchestrator is split out от cache.ts and the command handler
|
|
10
|
+
* so:
|
|
11
|
+
*
|
|
12
|
+
* 1. The CLI command + the engine system-prompt injector share one
|
|
13
|
+
* code path. Drift between the two would silently change what
|
|
14
|
+
* the model sees vs. what the operator sees.
|
|
15
|
+
*
|
|
16
|
+
* 2. The spec can exercise the full pipeline against a temp dir
|
|
17
|
+
* without mounting Ink or the engine.
|
|
18
|
+
*
|
|
19
|
+
* Pure-ish: reads from disk (the source files + the cache), but never
|
|
20
|
+
* mutates anything outside `.pugi/repo-map.json` and never logs. The
|
|
21
|
+
* caller decides whether к persist the cache (`writeCache: true`) or
|
|
22
|
+
* к compute the map в-memory (`writeCache: false` — useful for
|
|
23
|
+
* non-interactive `--json` invocations on read-only fs).
|
|
24
|
+
*/
|
|
25
|
+
import { readFileSync } from 'node:fs';
|
|
26
|
+
import { loadPugiIgnore } from '../context/pugiignore.js';
|
|
27
|
+
import { defaultCachePath, diffCacheAgainstScan, mergeCache, readRepoMapCache, writeRepoMapCache, } from './cache.js';
|
|
28
|
+
import { extractFromFile } from './extractor.js';
|
|
29
|
+
import { scanRepoForMap } from './scanner.js';
|
|
30
|
+
import { formatRepoMap } from './formatter.js';
|
|
31
|
+
/**
|
|
32
|
+
* Run the full pipeline. Returns a structured verdict; never throws.
|
|
33
|
+
* The 'too-large' branch fires when the workspace exceeds the file
|
|
34
|
+
* cap — callers surface a hint к the operator ("repo too large for
|
|
35
|
+
* inline map — try .pugiignore") and skip injection.
|
|
36
|
+
*/
|
|
37
|
+
export function buildRepoMap(options) {
|
|
38
|
+
const root = options.root;
|
|
39
|
+
const refresh = options.refresh === true;
|
|
40
|
+
const writeCache = options.writeCache !== false;
|
|
41
|
+
const cachePath = options.cachePath ?? defaultCachePath(root);
|
|
42
|
+
const readFile = options.readFile ?? ((path) => readFileSync(path, 'utf8'));
|
|
43
|
+
const ignore = loadPugiIgnore(root);
|
|
44
|
+
const scan = scanRepoForMap({ root, ignore });
|
|
45
|
+
if (!scan.ok) {
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
root,
|
|
49
|
+
reason: scan.skipped.reason,
|
|
50
|
+
walked: scan.skipped.walked,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const prior = refresh ? null : readCacheOrNull(cachePath);
|
|
54
|
+
const diff = diffCacheAgainstScan(prior, scan.files);
|
|
55
|
+
const freshExtracts = new Map();
|
|
56
|
+
for (const file of diff.toRebuild) {
|
|
57
|
+
let source;
|
|
58
|
+
try {
|
|
59
|
+
source = readFile(file.absPath);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// File vanished или became unreadable mid-build — skip. The
|
|
63
|
+
// cache layer will just not have an entry for it; next refresh
|
|
64
|
+
// picks it up if it reappears.
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
freshExtracts.set(file.relPath, extractFromFile(file, source));
|
|
68
|
+
}
|
|
69
|
+
const cache = mergeCache({
|
|
70
|
+
root,
|
|
71
|
+
prior,
|
|
72
|
+
scanned: scan.files,
|
|
73
|
+
freshExtracts,
|
|
74
|
+
});
|
|
75
|
+
let cacheWritten = false;
|
|
76
|
+
if (writeCache) {
|
|
77
|
+
const writeResult = writeRepoMapCache(cachePath, cache);
|
|
78
|
+
cacheWritten = writeResult.ok;
|
|
79
|
+
}
|
|
80
|
+
// Surface the extracts в the same order the scanner produced (sorted
|
|
81
|
+
// by POSIX path) so callers iterating the result render deterministic
|
|
82
|
+
// output. The formatter does its own priority sort, so a different
|
|
83
|
+
// order here would only affect callers that iterate manually.
|
|
84
|
+
const extracts = [];
|
|
85
|
+
for (const file of scan.files) {
|
|
86
|
+
const entry = cache.entries[file.relPath];
|
|
87
|
+
if (entry)
|
|
88
|
+
extracts.push(entry.extract);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
ok: true,
|
|
92
|
+
root,
|
|
93
|
+
cache,
|
|
94
|
+
extracts,
|
|
95
|
+
scanStats: scan.stats,
|
|
96
|
+
diffStats: {
|
|
97
|
+
rebuilt: diff.toRebuild.length,
|
|
98
|
+
reused: diff.reuse.length,
|
|
99
|
+
dropped: diff.toDrop.length,
|
|
100
|
+
},
|
|
101
|
+
cachePath,
|
|
102
|
+
cacheWritten,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Convenience wrapper: build + format в one call. The engine boot
|
|
107
|
+
* path uses this so it does not have к know the formatter shape.
|
|
108
|
+
*/
|
|
109
|
+
export function buildAndFormatRepoMap(options) {
|
|
110
|
+
const build = buildRepoMap(options);
|
|
111
|
+
if (!build.ok)
|
|
112
|
+
return { build };
|
|
113
|
+
const format = formatRepoMap(build.extracts, {
|
|
114
|
+
maxBytes: options.formatBytesCap,
|
|
115
|
+
omitHeader: options.omitHeader,
|
|
116
|
+
});
|
|
117
|
+
return { build, format };
|
|
118
|
+
}
|
|
119
|
+
function readCacheOrNull(path) {
|
|
120
|
+
const verdict = readRepoMapCache(path);
|
|
121
|
+
if (verdict.ok)
|
|
122
|
+
return verdict.cache;
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=build.js.map
|