@pugi/cli 0.1.0-beta.3 → 0.1.0-beta.31

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.
Files changed (219) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/assets/pugi-mascot.ansi +15 -40
  3. package/bin/run.js +33 -1
  4. package/dist/commands/jobs-watch.js +201 -0
  5. package/dist/commands/jobs.js +15 -0
  6. package/dist/core/agent-progress/cleanup.js +134 -0
  7. package/dist/core/agent-progress/schema.js +144 -0
  8. package/dist/core/agent-progress/writer.js +101 -0
  9. package/dist/core/artifact-chain/dispatcher.js +148 -0
  10. package/dist/core/artifact-chain/exporter.js +164 -0
  11. package/dist/core/artifact-chain/state.js +243 -0
  12. package/dist/core/artifact-chain/steps.js +169 -0
  13. package/dist/core/auth/env-provider.js +238 -0
  14. package/dist/core/auto-update/channels.js +122 -0
  15. package/dist/core/auto-update/checker.js +241 -0
  16. package/dist/core/auto-update/state.js +235 -0
  17. package/dist/core/bare-mode/index.js +107 -0
  18. package/dist/core/checkpoint/resumer.js +149 -0
  19. package/dist/core/checkpoint/rewinder.js +291 -0
  20. package/dist/core/compact/auto-trigger.js +96 -0
  21. package/dist/core/compact/buffer-rewriter.js +115 -0
  22. package/dist/core/compact/summarizer.js +208 -0
  23. package/dist/core/compact/token-counter.js +108 -0
  24. package/dist/core/consensus/diff-capture.js +73 -0
  25. package/dist/core/context/index.js +7 -0
  26. package/dist/core/context/markdown-traverse.js +255 -0
  27. package/dist/core/cost/rate-card.js +129 -0
  28. package/dist/core/cost/tracker.js +221 -0
  29. package/dist/core/denial-tracking/index.js +8 -0
  30. package/dist/core/denial-tracking/state.js +264 -0
  31. package/dist/core/diagnostics/probe-runner.js +93 -0
  32. package/dist/core/diagnostics/probes/api.js +46 -0
  33. package/dist/core/diagnostics/probes/auth.js +86 -0
  34. package/dist/core/diagnostics/probes/bare-mode.js +42 -0
  35. package/dist/core/diagnostics/probes/cli-version.js +127 -0
  36. package/dist/core/diagnostics/probes/config.js +72 -0
  37. package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
  38. package/dist/core/diagnostics/probes/disk.js +81 -0
  39. package/dist/core/diagnostics/probes/git.js +65 -0
  40. package/dist/core/diagnostics/probes/mcp.js +75 -0
  41. package/dist/core/diagnostics/probes/node.js +59 -0
  42. package/dist/core/diagnostics/probes/pnpm.js +36 -0
  43. package/dist/core/diagnostics/probes/pugi-md.js +89 -0
  44. package/dist/core/diagnostics/probes/session.js +74 -0
  45. package/dist/core/diagnostics/probes/status-snapshot.js +442 -0
  46. package/dist/core/diagnostics/probes/workspace.js +63 -0
  47. package/dist/core/diagnostics/types.js +70 -0
  48. package/dist/core/dispatch/cache-cleanup.js +197 -0
  49. package/dist/core/dispatch/cache-handoff.js +295 -0
  50. package/dist/core/edits/dispatch.js +218 -2
  51. package/dist/core/edits/journal.js +199 -0
  52. package/dist/core/edits/layer-d-ast.js +557 -14
  53. package/dist/core/edits/verify-hook.js +273 -0
  54. package/dist/core/edits/worktree.js +111 -18
  55. package/dist/core/engine/anvil-client.js +115 -5
  56. package/dist/core/engine/budgets.js +89 -0
  57. package/dist/core/engine/context-prefix.js +155 -0
  58. package/dist/core/engine/intent.js +260 -0
  59. package/dist/core/engine/native-pugi.js +852 -210
  60. package/dist/core/engine/prompts.js +89 -6
  61. package/dist/core/engine/strip-internal-fields.js +124 -0
  62. package/dist/core/engine/tool-bridge.js +972 -33
  63. package/dist/core/feedback/queue.js +177 -0
  64. package/dist/core/feedback/submitter.js +145 -0
  65. package/dist/core/file-cache.js +113 -1
  66. package/dist/core/hooks/events.js +44 -0
  67. package/dist/core/hooks/index.js +15 -0
  68. package/dist/core/hooks/registry.js +213 -0
  69. package/dist/core/hooks/runner.js +236 -0
  70. package/dist/core/init/scaffold.js +195 -0
  71. package/dist/core/lsp/cache.js +105 -0
  72. package/dist/core/lsp/client.js +174 -29
  73. package/dist/core/lsp/language-detect.js +66 -0
  74. package/dist/core/lsp/post-edit-diagnostics.js +171 -0
  75. package/dist/core/mcp/client.js +75 -6
  76. package/dist/core/mcp/http-server.js +553 -0
  77. package/dist/core/mcp/permission.js +190 -0
  78. package/dist/core/mcp/registry.js +24 -2
  79. package/dist/core/mcp/server-tools.js +219 -0
  80. package/dist/core/mcp/server.js +397 -0
  81. package/dist/core/memory/dual-write.js +416 -0
  82. package/dist/core/memory/dual-write.spec.js +297 -0
  83. package/dist/core/memory/phase1-kinds.js +20 -0
  84. package/dist/core/memory-sync/queue.js +158 -0
  85. package/dist/core/memory-sync/queue.spec.js +105 -0
  86. package/dist/core/onboarding/marker.js +111 -0
  87. package/dist/core/onboarding/telemetry-state.js +108 -0
  88. package/dist/core/output-style/presets.js +176 -0
  89. package/dist/core/output-style/state.js +185 -0
  90. package/dist/core/permissions/gate.js +187 -0
  91. package/dist/core/permissions/index.js +18 -0
  92. package/dist/core/permissions/mode.js +102 -0
  93. package/dist/core/permissions/state.js +215 -0
  94. package/dist/core/permissions/tool-class.js +93 -0
  95. package/dist/core/prd-check/parser.js +215 -0
  96. package/dist/core/prd-check/reporter.js +127 -0
  97. package/dist/core/prd-check/session-review.js +557 -0
  98. package/dist/core/prd-check/verifiers.js +223 -0
  99. package/dist/core/pugi-md/context-injector.js +76 -0
  100. package/dist/core/pugi-md/walk-up.js +207 -0
  101. package/dist/core/release-notes/parser.js +241 -0
  102. package/dist/core/release-notes/state.js +116 -0
  103. package/dist/core/repl/codebase-survey.js +308 -0
  104. package/dist/core/repl/history.js +11 -1
  105. package/dist/core/repl/init-interview.js +457 -0
  106. package/dist/core/repl/model-pricing.js +135 -0
  107. package/dist/core/repl/onboarding-state.js +297 -0
  108. package/dist/core/repl/session.js +1529 -30
  109. package/dist/core/repl/slash-commands.js +361 -13
  110. package/dist/core/repl/store/session-store.js +31 -2
  111. package/dist/core/repl/workspace-context.js +22 -0
  112. package/dist/core/repo-map/build.js +125 -0
  113. package/dist/core/repo-map/cache.js +185 -0
  114. package/dist/core/repo-map/extractor.js +254 -0
  115. package/dist/core/repo-map/formatter.js +145 -0
  116. package/dist/core/repo-map/scanner.js +211 -0
  117. package/dist/core/retry-budget/budget.js +284 -0
  118. package/dist/core/retry-budget/index.js +5 -0
  119. package/dist/core/session.js +44 -0
  120. package/dist/core/settings.js +80 -0
  121. package/dist/core/share/formatter.js +271 -0
  122. package/dist/core/share/redactor.js +221 -0
  123. package/dist/core/share/uploader.js +267 -0
  124. package/dist/core/skills/defaults.js +457 -0
  125. package/dist/core/subagents/dispatcher-real.js +600 -0
  126. package/dist/core/subagents/dispatcher.js +113 -24
  127. package/dist/core/subagents/index.js +18 -5
  128. package/dist/core/subagents/isolation-matrix.js +213 -0
  129. package/dist/core/subagents/spawn.js +19 -4
  130. package/dist/core/telemetry/emitter.js +229 -0
  131. package/dist/core/telemetry/queue.js +251 -0
  132. package/dist/core/theme/context.js +91 -0
  133. package/dist/core/theme/presets.js +228 -0
  134. package/dist/core/theme/state.js +181 -0
  135. package/dist/core/todos/invariant.js +10 -0
  136. package/dist/core/todos/state.js +177 -0
  137. package/dist/core/transport/version-interceptor.js +166 -0
  138. package/dist/core/vim/keymap.js +288 -0
  139. package/dist/core/vim/state.js +92 -0
  140. package/dist/index.js +28 -0
  141. package/dist/runtime/bootstrap.js +190 -0
  142. package/dist/runtime/cli.js +2603 -278
  143. package/dist/runtime/commands/chain.js +489 -0
  144. package/dist/runtime/commands/compact.js +297 -0
  145. package/dist/runtime/commands/cost.js +199 -0
  146. package/dist/runtime/commands/delegate.js +312 -0
  147. package/dist/runtime/commands/dispatch.js +126 -0
  148. package/dist/runtime/commands/doctor.js +390 -0
  149. package/dist/runtime/commands/feedback.js +184 -0
  150. package/dist/runtime/commands/hooks.js +184 -0
  151. package/dist/runtime/commands/lsp.js +212 -28
  152. package/dist/runtime/commands/mcp.js +824 -0
  153. package/dist/runtime/commands/memory.js +508 -0
  154. package/dist/runtime/commands/memory.spec.js +174 -0
  155. package/dist/runtime/commands/model.js +237 -0
  156. package/dist/runtime/commands/onboarding.js +275 -0
  157. package/dist/runtime/commands/patch.js +17 -0
  158. package/dist/runtime/commands/permissions.js +87 -0
  159. package/dist/runtime/commands/plan.js +143 -0
  160. package/dist/runtime/commands/prd-check.js +285 -0
  161. package/dist/runtime/commands/release-notes.js +229 -0
  162. package/dist/runtime/commands/repo-map.js +95 -0
  163. package/dist/runtime/commands/report.js +299 -0
  164. package/dist/runtime/commands/resume.js +118 -0
  165. package/dist/runtime/commands/review-consensus.js +17 -2
  166. package/dist/runtime/commands/rewind.js +333 -0
  167. package/dist/runtime/commands/roster.js +117 -0
  168. package/dist/runtime/commands/sessions.js +163 -0
  169. package/dist/runtime/commands/share.js +316 -0
  170. package/dist/runtime/commands/status.js +178 -0
  171. package/dist/runtime/commands/stickers.js +82 -0
  172. package/dist/runtime/commands/style.js +194 -0
  173. package/dist/runtime/commands/theme.js +196 -0
  174. package/dist/runtime/commands/update.js +289 -0
  175. package/dist/runtime/commands/vim.js +140 -0
  176. package/dist/runtime/commands/worktree.js +50 -6
  177. package/dist/runtime/headless.js +543 -0
  178. package/dist/runtime/load-hooks-or-exit.js +71 -0
  179. package/dist/runtime/plan-decompose.js +531 -0
  180. package/dist/runtime/version.js +65 -0
  181. package/dist/tools/agent-tool.js +229 -0
  182. package/dist/tools/apply-patch.js +281 -39
  183. package/dist/tools/ask-user-question.js +213 -0
  184. package/dist/tools/ask-user.js +115 -0
  185. package/dist/tools/file-tools.js +85 -14
  186. package/dist/tools/mcp-tool.js +260 -0
  187. package/dist/tools/multi-edit.js +361 -0
  188. package/dist/tools/registry.js +30 -2
  189. package/dist/tools/skill-tool.js +96 -0
  190. package/dist/tools/tasks.js +208 -0
  191. package/dist/tools/todo-write.js +184 -0
  192. package/dist/tools/web-fetch.js +147 -2
  193. package/dist/tools/web-search.js +458 -0
  194. package/dist/tui/agent-progress-card.js +111 -0
  195. package/dist/tui/agent-tree.js +10 -0
  196. package/dist/tui/ask-modal.js +2 -2
  197. package/dist/tui/ask-user-question-prompt.js +192 -0
  198. package/dist/tui/compact-banner.js +81 -0
  199. package/dist/tui/conversation-pane.js +82 -8
  200. package/dist/tui/cost-table.js +111 -0
  201. package/dist/tui/doctor-table.js +46 -0
  202. package/dist/tui/feedback-prompt.js +156 -0
  203. package/dist/tui/input-box.js +46 -2
  204. package/dist/tui/markdown-render.js +4 -4
  205. package/dist/tui/onboarding-wizard.js +240 -0
  206. package/dist/tui/repl-render.js +293 -35
  207. package/dist/tui/repl-splash.js +2 -2
  208. package/dist/tui/repl.js +45 -13
  209. package/dist/tui/splash.js +1 -1
  210. package/dist/tui/status-bar.js +94 -16
  211. package/dist/tui/status-table.js +7 -0
  212. package/dist/tui/stickers-art.js +136 -0
  213. package/dist/tui/style-table.js +28 -0
  214. package/dist/tui/theme-table.js +29 -0
  215. package/dist/tui/tool-stream-pane.js +7 -0
  216. package/dist/tui/update-banner.js +20 -2
  217. package/dist/tui/vim-input.js +267 -0
  218. package/docs/examples/codegraph.mcp.json +10 -0
  219. package/package.json +9 -6
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Leak L18 (2026-05-27) — `pugi style` top-level command + REPL slash
3
+ * companion.
4
+ *
5
+ * Operator surface:
6
+ *
7
+ * pugi style Show active preset + table.
8
+ * pugi style <name> Switch workspace preset (current cwd).
9
+ * pugi style <name> --persist Switch + also write user default.
10
+ * pugi style --reset Clear workspace override → back to default.
11
+ * pugi style --reset --user Also clear the user default.
12
+ * pugi style --list Print the catalogue (no flip).
13
+ * pugi style --json Structured envelope variant.
14
+ *
15
+ * The same runner powers `/style` from inside the REPL. The REPL
16
+ * dispatcher (see `core/repl/session.ts`) routes through here so the
17
+ * two surfaces stay single-sourced — operators trained on one read
18
+ * the same payload + table on the other.
19
+ *
20
+ * Exit codes:
21
+ * 0 — show / switch / reset all succeed
22
+ * 1 — unknown preset slug (returned BEFORE any write)
23
+ * 2 — conflicting flags (e.g. `--reset` with a positional slug)
24
+ *
25
+ * The exit codes are surfaced through `process.exitCode` by the
26
+ * dispatcher in `cli.ts` — this module returns a structured payload
27
+ * + writes via the injected `writeOutput`. Throwing is reserved for
28
+ * truly unexpected errors (fs permissions etc.); the spec hooks the
29
+ * happy + sad paths through `writeOutput` shape, not via try/catch
30
+ * on the throw.
31
+ */
32
+ import { DEFAULT_OUTPUT_STYLE, isOutputStyleSlug, OUTPUT_STYLE_SLUGS, renderStyleTable, } from '../../core/output-style/presets.js';
33
+ import { clearUserOutputStyle, clearWorkspaceOutputStyle, resolveOutputStyle, setUserOutputStyle, setWorkspaceOutputStyle, } from '../../core/output-style/state.js';
34
+ /**
35
+ * Entry point. Parses `args`, applies the operation, emits the
36
+ * payload + text via `ctx.writeOutput`, and returns the exit code the
37
+ * dispatcher should hand back to the shell.
38
+ */
39
+ export async function runStyleCommand(args, ctx) {
40
+ const flags = parseFlags(args);
41
+ // Reset path
42
+ if (flags.reset) {
43
+ if (flags.slug !== null) {
44
+ const payload = buildPayload({
45
+ status: 'invalid_flags',
46
+ ctx,
47
+ message: '/style --reset cannot be combined with a preset name. Use one or the other.',
48
+ });
49
+ ctx.writeOutput(payload, payload.message);
50
+ return 2;
51
+ }
52
+ if (flags.persist) {
53
+ const payload = buildPayload({
54
+ status: 'invalid_flags',
55
+ ctx,
56
+ message: '/style --reset cannot be combined with --persist. Use --reset --user to also clear the user default.',
57
+ });
58
+ ctx.writeOutput(payload, payload.message);
59
+ return 2;
60
+ }
61
+ clearWorkspaceOutputStyle({ workspaceRoot: ctx.workspaceRoot, env: ctx.env });
62
+ if (flags.user) {
63
+ clearUserOutputStyle({ workspaceRoot: ctx.workspaceRoot, env: ctx.env });
64
+ }
65
+ const payload = buildPayload({
66
+ status: 'reset',
67
+ ctx,
68
+ message: flags.user
69
+ ? `Cleared workspace + user output style. Active: ${DEFAULT_OUTPUT_STYLE} (default).`
70
+ : `Cleared workspace output style. Active: ${describeActive(ctx)}.`,
71
+ });
72
+ ctx.writeOutput(payload, payload.message);
73
+ return 0;
74
+ }
75
+ // List path
76
+ if (flags.list && flags.slug === null) {
77
+ const resolved = resolveOutputStyle({ workspaceRoot: ctx.workspaceRoot, env: ctx.env });
78
+ const payload = buildPayload({
79
+ status: 'listed',
80
+ ctx,
81
+ message: renderStyleTable(resolved.slug),
82
+ });
83
+ ctx.writeOutput(payload, payload.message);
84
+ return 0;
85
+ }
86
+ // Switch path
87
+ if (flags.slug !== null) {
88
+ if (!isOutputStyleSlug(flags.slug)) {
89
+ const payload = buildPayload({
90
+ status: 'invalid_slug',
91
+ ctx,
92
+ attemptedSlug: flags.slug,
93
+ message: `Unknown style "${flags.slug}". Try one of: ${OUTPUT_STYLE_SLUGS.join(', ')}.`,
94
+ });
95
+ ctx.writeOutput(payload, payload.message);
96
+ return 1;
97
+ }
98
+ const before = resolveOutputStyle({ workspaceRoot: ctx.workspaceRoot, env: ctx.env });
99
+ setWorkspaceOutputStyle(flags.slug, { workspaceRoot: ctx.workspaceRoot, env: ctx.env });
100
+ if (flags.persist) {
101
+ setUserOutputStyle(flags.slug, { workspaceRoot: ctx.workspaceRoot, env: ctx.env });
102
+ }
103
+ const tail = flags.persist ? ' (workspace + user default)' : ' (workspace)';
104
+ const payload = buildPayload({
105
+ status: 'switched',
106
+ ctx,
107
+ previous: before.slug,
108
+ persistedToUser: flags.persist,
109
+ message: `Output style → ${flags.slug}${tail}. Was: ${before.slug} (${before.source}).`,
110
+ });
111
+ ctx.writeOutput(payload, payload.message);
112
+ return 0;
113
+ }
114
+ // Show path (no args)
115
+ const resolved = resolveOutputStyle({ workspaceRoot: ctx.workspaceRoot, env: ctx.env });
116
+ const banner = `Active output style: ${resolved.slug} (${resolved.source})`;
117
+ const table = renderStyleTable(resolved.slug);
118
+ const payload = buildPayload({
119
+ status: 'show',
120
+ ctx,
121
+ message: `${banner}\n\n${table}`,
122
+ });
123
+ ctx.writeOutput(payload, payload.message);
124
+ return 0;
125
+ }
126
+ function describeActive(ctx) {
127
+ const resolved = resolveOutputStyle({ workspaceRoot: ctx.workspaceRoot, env: ctx.env });
128
+ return `${resolved.slug} (${resolved.source})`;
129
+ }
130
+ function buildPayload(args) {
131
+ const resolved = resolveOutputStyle({ workspaceRoot: args.ctx.workspaceRoot, env: args.ctx.env });
132
+ const payload = {
133
+ command: 'style',
134
+ status: args.status,
135
+ active: resolved.slug,
136
+ source: resolved.source,
137
+ presets: OUTPUT_STYLE_SLUGS,
138
+ message: args.message,
139
+ };
140
+ if (args.previous !== undefined)
141
+ payload.previous = args.previous;
142
+ if (args.persistedToUser !== undefined)
143
+ payload.persistedToUser = args.persistedToUser;
144
+ if (args.attemptedSlug !== undefined)
145
+ payload.attemptedSlug = args.attemptedSlug;
146
+ return payload;
147
+ }
148
+ function parseFlags(args) {
149
+ const flags = {
150
+ slug: null,
151
+ persist: false,
152
+ reset: false,
153
+ user: false,
154
+ list: false,
155
+ };
156
+ for (const arg of args) {
157
+ if (arg === '--persist')
158
+ flags.persist = true;
159
+ else if (arg === '--reset')
160
+ flags.reset = true;
161
+ else if (arg === '--user')
162
+ flags.user = true;
163
+ else if (arg === '--list')
164
+ flags.list = true;
165
+ else if (arg.startsWith('-')) {
166
+ // Unknown flag — keep simple parser. Treat as positional so the
167
+ // downstream isOutputStyleSlug check rejects it with a clear
168
+ // "unknown style" message rather than swallowing silently.
169
+ if (flags.slug === null)
170
+ flags.slug = arg;
171
+ }
172
+ else if (flags.slug === null) {
173
+ flags.slug = arg;
174
+ }
175
+ }
176
+ // Normalise the slug to lowercase so `pugi style TERSE` works the
177
+ // same as `pugi style terse`. The catalogue is lowercase-only by
178
+ // contract; this keeps operators from tripping on shift-key habits.
179
+ if (flags.slug !== null)
180
+ flags.slug = flags.slug.toLowerCase();
181
+ return flags;
182
+ }
183
+ /**
184
+ * Re-export for the slash-command dispatcher in `core/repl/session.ts`
185
+ * so it can render a compiled prompt block when the operator runs
186
+ * `/style --preview` (follow-up surface; current slash returns the
187
+ * runner's standard payload).
188
+ *
189
+ * Kept here so the runtime module is the single import point for
190
+ * style-related surfaces; consumers should NOT reach into
191
+ * `core/output-style/*` directly.
192
+ */
193
+ export { OUTPUT_STYLES as OUTPUT_STYLE_CATALOGUE, } from '../../core/output-style/presets.js';
194
+ //# sourceMappingURL=style.js.map
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Leak L30 (2026-05-27) — `pugi theme` top-level command + REPL slash
3
+ * companion.
4
+ *
5
+ * Operator surface:
6
+ *
7
+ * pugi theme Show active theme + table.
8
+ * pugi theme <name> Switch workspace theme (current cwd).
9
+ * pugi theme <name> --persist Switch + also write user default.
10
+ * pugi theme --reset Clear workspace override → back to default.
11
+ * pugi theme --reset --user Also clear the user default.
12
+ * pugi theme --list Print the catalogue (no flip).
13
+ * pugi theme --json Structured envelope variant.
14
+ *
15
+ * The same runner powers `/theme` from inside the REPL. The REPL
16
+ * dispatcher (see `core/repl/session.ts`) routes through here so the
17
+ * two surfaces stay single-sourced — operators trained on one read
18
+ * the same payload + table on the other. Matches the leak L18
19
+ * `/style` runner exactly so the two settings surfaces are
20
+ * paste-comparable for the operator + grep-comparable for future
21
+ * maintenance.
22
+ *
23
+ * Exit codes:
24
+ * 0 — show / switch / reset all succeed
25
+ * 1 — unknown preset slug (returned BEFORE any write)
26
+ * 2 — conflicting flags (e.g. `--reset` with a positional slug)
27
+ *
28
+ * The exit codes are surfaced through `process.exitCode` by the
29
+ * dispatcher in `cli.ts` — this module returns a structured payload
30
+ * + writes via the injected `writeOutput`. Throwing is reserved for
31
+ * truly unexpected errors (fs permissions etc.); the spec hooks the
32
+ * happy + sad paths through `writeOutput` shape, not via try/catch
33
+ * on the throw.
34
+ */
35
+ import { DEFAULT_THEME, isThemeSlug, renderThemeTable, THEME_SLUGS, } from '../../core/theme/presets.js';
36
+ import { clearUserTheme, clearWorkspaceTheme, resolveTheme, setUserTheme, setWorkspaceTheme, } from '../../core/theme/state.js';
37
+ /**
38
+ * Entry point. Parses `args`, applies the operation, emits the
39
+ * payload + text via `ctx.writeOutput`, and returns the exit code the
40
+ * dispatcher should hand back to the shell.
41
+ */
42
+ export async function runThemeCommand(args, ctx) {
43
+ const flags = parseFlags(args);
44
+ // Reset path
45
+ if (flags.reset) {
46
+ if (flags.slug !== null) {
47
+ const payload = buildPayload({
48
+ status: 'invalid_flags',
49
+ ctx,
50
+ message: '/theme --reset cannot be combined with a preset name. Use one or the other.',
51
+ });
52
+ ctx.writeOutput(payload, payload.message);
53
+ return 2;
54
+ }
55
+ if (flags.persist) {
56
+ const payload = buildPayload({
57
+ status: 'invalid_flags',
58
+ ctx,
59
+ message: '/theme --reset cannot be combined with --persist. Use --reset --user to also clear the user default.',
60
+ });
61
+ ctx.writeOutput(payload, payload.message);
62
+ return 2;
63
+ }
64
+ clearWorkspaceTheme({ workspaceRoot: ctx.workspaceRoot, env: ctx.env });
65
+ if (flags.user) {
66
+ clearUserTheme({ workspaceRoot: ctx.workspaceRoot, env: ctx.env });
67
+ }
68
+ const payload = buildPayload({
69
+ status: 'reset',
70
+ ctx,
71
+ message: flags.user
72
+ ? `Cleared workspace + user theme. Active: ${DEFAULT_THEME} (default).`
73
+ : `Cleared workspace theme. Active: ${describeActive(ctx)}.`,
74
+ });
75
+ ctx.writeOutput(payload, payload.message);
76
+ return 0;
77
+ }
78
+ // List path
79
+ if (flags.list && flags.slug === null) {
80
+ const resolved = resolveTheme({ workspaceRoot: ctx.workspaceRoot, env: ctx.env });
81
+ const payload = buildPayload({
82
+ status: 'listed',
83
+ ctx,
84
+ message: renderThemeTable(resolved.slug),
85
+ });
86
+ ctx.writeOutput(payload, payload.message);
87
+ return 0;
88
+ }
89
+ // Switch path
90
+ if (flags.slug !== null) {
91
+ if (!isThemeSlug(flags.slug)) {
92
+ const payload = buildPayload({
93
+ status: 'invalid_slug',
94
+ ctx,
95
+ attemptedSlug: flags.slug,
96
+ message: `Unknown theme "${flags.slug}". Try one of: ${THEME_SLUGS.join(', ')}.`,
97
+ });
98
+ ctx.writeOutput(payload, payload.message);
99
+ return 1;
100
+ }
101
+ const before = resolveTheme({ workspaceRoot: ctx.workspaceRoot, env: ctx.env });
102
+ setWorkspaceTheme(flags.slug, { workspaceRoot: ctx.workspaceRoot, env: ctx.env });
103
+ if (flags.persist) {
104
+ setUserTheme(flags.slug, { workspaceRoot: ctx.workspaceRoot, env: ctx.env });
105
+ }
106
+ const tail = flags.persist ? ' (workspace + user default)' : ' (workspace)';
107
+ const payload = buildPayload({
108
+ status: 'switched',
109
+ ctx,
110
+ previous: before.slug,
111
+ persistedToUser: flags.persist,
112
+ message: `Theme → ${flags.slug}${tail}. Was: ${before.slug} (${before.source}).`,
113
+ });
114
+ ctx.writeOutput(payload, payload.message);
115
+ return 0;
116
+ }
117
+ // Show path (no args)
118
+ const resolved = resolveTheme({ workspaceRoot: ctx.workspaceRoot, env: ctx.env });
119
+ const banner = `Active theme: ${resolved.slug} (${resolved.source})`;
120
+ const table = renderThemeTable(resolved.slug);
121
+ const payload = buildPayload({
122
+ status: 'show',
123
+ ctx,
124
+ message: `${banner}\n\n${table}`,
125
+ });
126
+ ctx.writeOutput(payload, payload.message);
127
+ return 0;
128
+ }
129
+ function describeActive(ctx) {
130
+ const resolved = resolveTheme({ workspaceRoot: ctx.workspaceRoot, env: ctx.env });
131
+ return `${resolved.slug} (${resolved.source})`;
132
+ }
133
+ function buildPayload(args) {
134
+ const resolved = resolveTheme({ workspaceRoot: args.ctx.workspaceRoot, env: args.ctx.env });
135
+ const payload = {
136
+ command: 'theme',
137
+ status: args.status,
138
+ active: resolved.slug,
139
+ source: resolved.source,
140
+ presets: THEME_SLUGS,
141
+ message: args.message,
142
+ };
143
+ if (args.previous !== undefined)
144
+ payload.previous = args.previous;
145
+ if (args.persistedToUser !== undefined)
146
+ payload.persistedToUser = args.persistedToUser;
147
+ if (args.attemptedSlug !== undefined)
148
+ payload.attemptedSlug = args.attemptedSlug;
149
+ return payload;
150
+ }
151
+ function parseFlags(args) {
152
+ const flags = {
153
+ slug: null,
154
+ persist: false,
155
+ reset: false,
156
+ user: false,
157
+ list: false,
158
+ };
159
+ for (const arg of args) {
160
+ if (arg === '--persist')
161
+ flags.persist = true;
162
+ else if (arg === '--reset')
163
+ flags.reset = true;
164
+ else if (arg === '--user')
165
+ flags.user = true;
166
+ else if (arg === '--list')
167
+ flags.list = true;
168
+ else if (arg.startsWith('-')) {
169
+ // Unknown flag — keep simple parser. Treat as positional so the
170
+ // downstream isThemeSlug check rejects it with a clear "unknown
171
+ // theme" message rather than swallowing silently. Mirrors the
172
+ // L18 style runner's behaviour for grep-parity.
173
+ if (flags.slug === null)
174
+ flags.slug = arg;
175
+ }
176
+ else if (flags.slug === null) {
177
+ flags.slug = arg;
178
+ }
179
+ }
180
+ // Normalise the slug to lowercase so `pugi theme DARK` works the
181
+ // same as `pugi theme dark`. The catalogue is lowercase-only by
182
+ // contract; this keeps operators from tripping on shift-key habits.
183
+ if (flags.slug !== null)
184
+ flags.slug = flags.slug.toLowerCase();
185
+ return flags;
186
+ }
187
+ /**
188
+ * Re-export for the slash-command dispatcher in
189
+ * `core/repl/session.ts` so it can render a preview block if a
190
+ * future surface (`/theme --preview`) lands. Kept here so the
191
+ * runtime module is the single import point for theme-related
192
+ * surfaces; consumers should NOT reach into `core/theme/*` directly
193
+ * unless they are Ink components that need the React context.
194
+ */
195
+ export { THEMES as THEME_CATALOGUE, } from '../../core/theme/presets.js';
196
+ //# sourceMappingURL=theme.js.map
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Leak L27 (2026-05-27) — `pugi update` dispatcher.
3
+ *
4
+ * Top-level command + in-REPL `/update` slash share this handler.
5
+ * Both surfaces delegate IO + persistence here so the channel
6
+ * resolution, the registry probe, and the install shell-out stay
7
+ * single-sourced.
8
+ *
9
+ * Command grammar:
10
+ *
11
+ * pugi update -> probe + interactive prompt
12
+ * pugi update --check -> probe + JSON envelope (scripted)
13
+ * pugi update --channel <name> -> switch channel + probe
14
+ * pugi update --apply -> probe + shell `npm i -g …`
15
+ * (confirms unless --yes)
16
+ * pugi update --apply --yes -> probe + shell, no confirmation
17
+ *
18
+ * Exit codes:
19
+ *
20
+ * 0 — happy path (no update OR update completed) OR `--check` JSON
21
+ * 1 — install / probe failure with structured error envelope
22
+ * 2 — argument error (bad channel, conflicting flags)
23
+ *
24
+ * R2 atomic swap deferred (sprint plan L27 mention): the leak research
25
+ * also describes Claude Code's R2-backed binary swap. Pugi ships
26
+ * exclusively via npm today; building a parallel R2 distribution
27
+ * channel + checksum verification + rollback is materially more work
28
+ * than the L27 acceptance criteria allow. Document the deferral in
29
+ * the PR body and revisit when npm's once-per-day rate-limit or
30
+ * outage cadence justifies the parallel pipeline.
31
+ */
32
+ import { spawn } from 'node:child_process';
33
+ import { homedir } from 'node:os';
34
+ import { DEFAULT_UPDATE_CHANNEL, UPDATE_CHANNELS, describeChannel, npmTagForChannel, parseUpdateChannel, } from '../../core/auto-update/channels.js';
35
+ import { checkForChannelUpdate, } from '../../core/auto-update/checker.js';
36
+ import { resolveEffectiveChannel, setUpdateChannel, writeLastCheckedAt, } from '../../core/auto-update/state.js';
37
+ import { PUGI_CLI_VERSION } from '../version.js';
38
+ /**
39
+ * Default subprocess runner. Spawns `npm install -g @pugi/cli@<tag>`
40
+ * inheriting stdio so the operator sees the live npm output.
41
+ */
42
+ export function defaultSpawnInstaller(channel) {
43
+ const tag = npmTagForChannel(channel);
44
+ const args = ['install', '-g', `@pugi/cli@${tag}`];
45
+ return new Promise((resolvePromise) => {
46
+ const child = spawn('npm', args, { stdio: 'inherit' });
47
+ child.on('exit', (code) => {
48
+ resolvePromise(typeof code === 'number' ? code : 1);
49
+ });
50
+ child.on('error', () => {
51
+ // ENOENT / EACCES — npm not on PATH or permission denied. We
52
+ // surface a non-zero code so the dispatcher's JSON envelope
53
+ // tells the operator the apply failed; the inherited stdio
54
+ // already printed the underlying error to the terminal.
55
+ resolvePromise(127);
56
+ });
57
+ });
58
+ }
59
+ /**
60
+ * Parse a CLI / slash argv into our `UpdateCommandFlags`. Returns
61
+ * `null` AND writes the usage error via `writeError` for conflicting
62
+ * combinations. Both `pugi update` and the in-REPL `/update <args>`
63
+ * surface call through this so the validation lives in one place.
64
+ */
65
+ export function parseUpdateArgs(argv, options = {}) {
66
+ let check = false;
67
+ let apply = false;
68
+ let yes = false;
69
+ let json = options.jsonDefault ?? false;
70
+ let channel = null;
71
+ let channelInvalid;
72
+ for (let i = 0; i < argv.length; i += 1) {
73
+ const token = argv[i] ?? '';
74
+ if (token === '--check') {
75
+ check = true;
76
+ }
77
+ else if (token === '--apply') {
78
+ apply = true;
79
+ }
80
+ else if (token === '--yes' || token === '-y') {
81
+ yes = true;
82
+ }
83
+ else if (token === '--json') {
84
+ json = true;
85
+ }
86
+ else if (token === '--channel') {
87
+ const value = argv[i + 1];
88
+ i += 1;
89
+ const parsed = parseUpdateChannel(value);
90
+ if (!parsed) {
91
+ channelInvalid = value ?? '';
92
+ continue;
93
+ }
94
+ channel = parsed;
95
+ }
96
+ else if (token.startsWith('--channel=')) {
97
+ const value = token.slice('--channel='.length);
98
+ const parsed = parseUpdateChannel(value);
99
+ if (!parsed) {
100
+ channelInvalid = value;
101
+ continue;
102
+ }
103
+ channel = parsed;
104
+ }
105
+ else {
106
+ return {
107
+ error: `pugi update: unknown argument '${token}'. See \`pugi update --help\`.`,
108
+ };
109
+ }
110
+ }
111
+ if (channelInvalid !== undefined) {
112
+ return {
113
+ error: `pugi update: unknown channel '${channelInvalid}'. Allowed: ${UPDATE_CHANNELS.join(' / ')}.`,
114
+ };
115
+ }
116
+ return {
117
+ check,
118
+ apply,
119
+ yes,
120
+ json,
121
+ channel,
122
+ };
123
+ }
124
+ /**
125
+ * Run the full command. Returns the structured envelope so the in-
126
+ * REPL slash can decide whether to render the human-readable text OR
127
+ * pretty-print the JSON.
128
+ */
129
+ export async function runUpdateCommand(ctx) {
130
+ const flags = ctx.flags;
131
+ const home = ctx.home;
132
+ // 1. Resolve channel. `--channel <name>` wins; otherwise read the
133
+ // persisted preference; otherwise hard default `beta`.
134
+ const cliFlagChannel = flags.channel;
135
+ const effectiveChannel = resolveEffectiveChannel({
136
+ cliFlag: cliFlagChannel,
137
+ homeDir: home,
138
+ });
139
+ // 2. Persist the channel switch BEFORE the probe so a probe failure
140
+ // still leaves the operator on the channel they asked for.
141
+ let switched = false;
142
+ if (cliFlagChannel) {
143
+ setUpdateChannel(cliFlagChannel, home);
144
+ switched = true;
145
+ }
146
+ // 3. Probe the registry.
147
+ const outcome = await checkForChannelUpdate({
148
+ channel: effectiveChannel,
149
+ currentVersion: PUGI_CLI_VERSION,
150
+ ...(ctx.fetchImpl ? { fetchImpl: ctx.fetchImpl } : {}),
151
+ ...(ctx.registryUrl ? { registryUrl: ctx.registryUrl } : {}),
152
+ });
153
+ // 4. Record the timestamp on a successful probe (regardless of
154
+ // whether an update is available). Failed probes do NOT update
155
+ // the timestamp so the cold-start banner retries on the next
156
+ // invocation.
157
+ if (!outcome.error) {
158
+ const now = ctx.now ? new Date(ctx.now()) : new Date();
159
+ try {
160
+ writeLastCheckedAt(now, home);
161
+ }
162
+ catch {
163
+ // Best-effort — a read-only home should not crash the dispatcher.
164
+ }
165
+ }
166
+ // 5. Build the envelope shape ALL action paths share.
167
+ const baseEnvelope = {
168
+ command: 'update',
169
+ ok: outcome.error === null,
170
+ available: outcome.available,
171
+ channel: outcome.channel,
172
+ npmTag: outcome.npmTag,
173
+ current: outcome.current,
174
+ latest: outcome.latest,
175
+ gap: outcome.gap,
176
+ installCommand: outcome.installCommand,
177
+ action: outcome.error ? 'error' : 'probe',
178
+ error: outcome.error,
179
+ meta: { cliVersion: PUGI_CLI_VERSION },
180
+ };
181
+ // 6. --check: emit the envelope, never prompt, never apply.
182
+ if (flags.check) {
183
+ const text = renderHumanText(baseEnvelope, { switched });
184
+ ctx.writeOutput(baseEnvelope, text);
185
+ return baseEnvelope;
186
+ }
187
+ // 7. No update available — bail with a friendly message.
188
+ if (!outcome.available || !outcome.latest) {
189
+ const envelope = {
190
+ ...baseEnvelope,
191
+ action: outcome.error ? 'error' : (switched ? 'switch' : 'no_update'),
192
+ };
193
+ const text = renderHumanText(envelope, { switched });
194
+ ctx.writeOutput(envelope, text);
195
+ return envelope;
196
+ }
197
+ // 8. Update IS available. Branch on --apply.
198
+ if (!flags.apply) {
199
+ // Default: print the offer, suggest the install command, leave
200
+ // the install to the operator. Mirrors the leak research note:
201
+ // operators install side-effects must remain explicit on the CLI
202
+ // happy path — `pugi update` showing the gap is informational,
203
+ // `pugi update --apply` is the destructive verb.
204
+ const envelope = {
205
+ ...baseEnvelope,
206
+ action: switched ? 'switch' : 'probe',
207
+ };
208
+ const text = renderHumanText(envelope, { switched });
209
+ ctx.writeOutput(envelope, text);
210
+ return envelope;
211
+ }
212
+ // 9. --apply path. Confirm unless --yes, then spawn npm.
213
+ const installer = ctx.spawnInstaller ?? defaultSpawnInstaller;
214
+ let confirmed = flags.yes;
215
+ if (!confirmed) {
216
+ confirmed = await ctx.promptConfirm(`Run \`${outcome.installCommand}\` to update ${outcome.current} -> ${outcome.latest}? [y/N]`);
217
+ }
218
+ if (!confirmed) {
219
+ const envelope = {
220
+ ...baseEnvelope,
221
+ action: 'probe',
222
+ ok: false,
223
+ error: 'apply_cancelled_by_operator',
224
+ };
225
+ const text = `Cancelled. Run \`${outcome.installCommand}\` manually when you are ready.`;
226
+ ctx.writeOutput(envelope, text);
227
+ return envelope;
228
+ }
229
+ const exitCode = await installer(outcome.channel);
230
+ const applyEnvelope = {
231
+ ...baseEnvelope,
232
+ action: 'apply',
233
+ installExitCode: exitCode,
234
+ ok: exitCode === 0,
235
+ error: exitCode === 0 ? null : `npm_install_exit_${exitCode}`,
236
+ };
237
+ const applyText = exitCode === 0
238
+ ? `Updated to @pugi/cli@${outcome.latest}. Restart your shell so the new binary takes effect.`
239
+ : `Update failed (npm exit ${exitCode}). Try \`${outcome.installCommand}\` manually.`;
240
+ ctx.writeOutput(applyEnvelope, applyText);
241
+ return applyEnvelope;
242
+ }
243
+ /**
244
+ * Build the operator-readable hint that lives next to the JSON
245
+ * envelope. The text + the JSON are both passed to `writeOutput`; the
246
+ * caller (`writeOutput` in cli.ts) picks based on `--json`. Exported
247
+ * so the spec can assert the literal strings the operator sees.
248
+ */
249
+ export function renderHumanText(envelope, options = {}) {
250
+ const { current, latest, channel, installCommand, available, error } = envelope;
251
+ const lines = [];
252
+ lines.push(`Channel: ${channel} — ${describeChannel(channel)}`);
253
+ if (options.switched) {
254
+ lines.push(`Persisted channel selection -> ${channel}.`);
255
+ }
256
+ if (error) {
257
+ lines.push(`Update check failed: ${error}`);
258
+ lines.push(`Manual: \`${installCommand}\` (no probe necessary).`);
259
+ return lines.join('\n');
260
+ }
261
+ if (available && latest) {
262
+ lines.push(`Update available: ${current} -> ${latest}.`);
263
+ lines.push(`Run \`${installCommand}\` to upgrade.`);
264
+ lines.push(`Or \`pugi update --apply\` to upgrade with confirmation.`);
265
+ return lines.join('\n');
266
+ }
267
+ lines.push(`Up to date (${current} is the latest on ${channel}).`);
268
+ return lines.join('\n');
269
+ }
270
+ /**
271
+ * Render a one-line cold-start hint that callers (REPL boot, doctor
272
+ * banner) splice above their own UI. Returns `null` when there is
273
+ * nothing to nudge the operator about. Pure — no IO.
274
+ */
275
+ export function renderUpdateHint(outcome) {
276
+ if (!outcome.available || !outcome.latest)
277
+ return null;
278
+ return `Update available: ${outcome.current} -> ${outcome.latest}. Run \`pugi update\`.`;
279
+ }
280
+ /**
281
+ * Convenience entry point — resolve the effective channel from
282
+ * `~/.pugi/config.json` + DEFAULT_UPDATE_CHANNEL without forcing the
283
+ * caller to import multiple modules. Used by the cold-start banner +
284
+ * the doctor probe.
285
+ */
286
+ export function effectiveChannel(home = homedir()) {
287
+ return resolveEffectiveChannel({ homeDir: home }) ?? DEFAULT_UPDATE_CHANNEL;
288
+ }
289
+ //# sourceMappingURL=update.js.map