@pugi/cli 0.1.0-beta.5 → 0.1.0-beta.51

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 (264) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/assets/pugi-mascot.ansi +15 -25
  3. package/assets/pugi-prozr2-mascot.ansi +9 -0
  4. package/bin/run.js +33 -1
  5. package/dist/commands/jobs-watch.js +201 -0
  6. package/dist/commands/jobs.js +15 -0
  7. package/dist/commands/smoke.js +133 -0
  8. package/dist/core/agent-progress/cleanup.js +134 -0
  9. package/dist/core/agent-progress/schema.js +144 -0
  10. package/dist/core/agent-progress/writer.js +101 -0
  11. package/dist/core/artifact-chain/dispatcher.js +148 -0
  12. package/dist/core/artifact-chain/exporter.js +164 -0
  13. package/dist/core/artifact-chain/state.js +243 -0
  14. package/dist/core/artifact-chain/steps.js +169 -0
  15. package/dist/core/auth/ensure-authenticated.js +129 -0
  16. package/dist/core/auth/env-provider.js +238 -0
  17. package/dist/core/auto-update/channels.js +122 -0
  18. package/dist/core/auto-update/checker.js +241 -0
  19. package/dist/core/auto-update/state.js +235 -0
  20. package/dist/core/bare-mode/index.js +107 -0
  21. package/dist/core/bash-classifier.js +400 -4
  22. package/dist/core/checkpoint/resumer.js +149 -0
  23. package/dist/core/checkpoint/rewinder.js +291 -0
  24. package/dist/core/codegraph/decision-store.js +248 -0
  25. package/dist/core/codegraph/detect-repo.js +459 -0
  26. package/dist/core/codegraph/install.js +134 -0
  27. package/dist/core/codegraph/offer-hook.js +220 -0
  28. package/dist/core/compact/auto-trigger.js +96 -0
  29. package/dist/core/compact/buffer-rewriter.js +115 -0
  30. package/dist/core/compact/summarizer.js +208 -0
  31. package/dist/core/compact/token-counter.js +108 -0
  32. package/dist/core/consensus/diff-capture.js +112 -3
  33. package/dist/core/context/index.js +7 -0
  34. package/dist/core/context/markdown-traverse.js +255 -0
  35. package/dist/core/cost/rate-card.js +129 -0
  36. package/dist/core/cost/tracker.js +221 -0
  37. package/dist/core/denial-tracking/index.js +8 -0
  38. package/dist/core/denial-tracking/state.js +264 -0
  39. package/dist/core/diagnostics/probe-runner.js +93 -0
  40. package/dist/core/diagnostics/probes/api.js +46 -0
  41. package/dist/core/diagnostics/probes/auth.js +86 -0
  42. package/dist/core/diagnostics/probes/bare-mode.js +42 -0
  43. package/dist/core/diagnostics/probes/cli-version.js +127 -0
  44. package/dist/core/diagnostics/probes/config.js +72 -0
  45. package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
  46. package/dist/core/diagnostics/probes/disk.js +81 -0
  47. package/dist/core/diagnostics/probes/git.js +65 -0
  48. package/dist/core/diagnostics/probes/hooks.js +118 -0
  49. package/dist/core/diagnostics/probes/mcp.js +75 -0
  50. package/dist/core/diagnostics/probes/node.js +59 -0
  51. package/dist/core/diagnostics/probes/pnpm.js +36 -0
  52. package/dist/core/diagnostics/probes/pugi-md.js +89 -0
  53. package/dist/core/diagnostics/probes/sandbox.js +40 -0
  54. package/dist/core/diagnostics/probes/session.js +74 -0
  55. package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
  56. package/dist/core/diagnostics/probes/workspace.js +63 -0
  57. package/dist/core/diagnostics/types.js +70 -0
  58. package/dist/core/dispatch/cache-cleanup.js +197 -0
  59. package/dist/core/dispatch/cache-handoff.js +295 -0
  60. package/dist/core/edits/dispatch.js +218 -2
  61. package/dist/core/edits/journal.js +199 -0
  62. package/dist/core/edits/layer-d-ast.js +557 -14
  63. package/dist/core/edits/verify-hook.js +273 -0
  64. package/dist/core/edits/worktree.js +322 -0
  65. package/dist/core/engine/anvil-client.js +115 -5
  66. package/dist/core/engine/auto-compact.js +179 -0
  67. package/dist/core/engine/budgets.js +155 -0
  68. package/dist/core/engine/context-prefix.js +155 -0
  69. package/dist/core/engine/intent.js +260 -0
  70. package/dist/core/engine/native-pugi.js +897 -211
  71. package/dist/core/engine/prompts.js +88 -2
  72. package/dist/core/engine/strip-internal-fields.js +124 -0
  73. package/dist/core/engine/tool-bridge.js +1045 -36
  74. package/dist/core/feedback/queue.js +177 -0
  75. package/dist/core/feedback/submitter.js +145 -0
  76. package/dist/core/file-cache.js +113 -1
  77. package/dist/core/hooks/events.js +44 -0
  78. package/dist/core/hooks/index.js +15 -0
  79. package/dist/core/hooks/registry.js +213 -0
  80. package/dist/core/hooks/runner.js +236 -0
  81. package/dist/core/hooks/v2/event-emitter.js +115 -0
  82. package/dist/core/hooks/v2/executor.js +282 -0
  83. package/dist/core/hooks/v2/index.js +25 -0
  84. package/dist/core/hooks/v2/lifecycle.js +104 -0
  85. package/dist/core/hooks/v2/loader.js +216 -0
  86. package/dist/core/hooks/v2/matcher.js +125 -0
  87. package/dist/core/hooks/v2/trust.js +143 -0
  88. package/dist/core/hooks/v2/types.js +86 -0
  89. package/dist/core/lsp/cache.js +105 -0
  90. package/dist/core/lsp/client.js +776 -0
  91. package/dist/core/lsp/language-detect.js +66 -0
  92. package/dist/core/lsp/post-edit-diagnostics.js +171 -0
  93. package/dist/core/mcp/client.js +75 -6
  94. package/dist/core/mcp/http-server.js +553 -0
  95. package/dist/core/mcp/orchestrator-tools.js +662 -0
  96. package/dist/core/mcp/permission.js +190 -0
  97. package/dist/core/mcp/registry.js +24 -2
  98. package/dist/core/mcp/server-tools.js +219 -0
  99. package/dist/core/mcp/server.js +397 -0
  100. package/dist/core/memory/dual-write.js +416 -0
  101. package/dist/core/memory/phase1-kinds.js +20 -0
  102. package/dist/core/memory-sync/queue.js +158 -0
  103. package/dist/core/onboarding/ensure-initialized.js +133 -0
  104. package/dist/core/onboarding/marker.js +111 -0
  105. package/dist/core/onboarding/telemetry-state.js +108 -0
  106. package/dist/core/output-style/presets.js +176 -0
  107. package/dist/core/output-style/state.js +185 -0
  108. package/dist/core/path-security.js +284 -2
  109. package/dist/core/permissions/auto-classifier.js +124 -0
  110. package/dist/core/permissions/circuit-breaker.js +83 -0
  111. package/dist/core/permissions/gate.js +278 -0
  112. package/dist/core/permissions/index.js +20 -0
  113. package/dist/core/permissions/mode.js +174 -0
  114. package/dist/core/permissions/state.js +241 -0
  115. package/dist/core/permissions/tool-class.js +93 -0
  116. package/dist/core/prd-check/parser.js +215 -0
  117. package/dist/core/prd-check/reporter.js +127 -0
  118. package/dist/core/prd-check/session-review.js +557 -0
  119. package/dist/core/prd-check/verifiers.js +223 -0
  120. package/dist/core/pugi-md/context-injector.js +76 -0
  121. package/dist/core/pugi-md/walk-up.js +207 -0
  122. package/dist/core/release-notes/parser.js +241 -0
  123. package/dist/core/release-notes/state.js +116 -0
  124. package/dist/core/repl/history.js +11 -1
  125. package/dist/core/repl/model-pricing.js +135 -0
  126. package/dist/core/repl/session.js +1897 -37
  127. package/dist/core/repl/slash-commands.js +430 -15
  128. package/dist/core/repl/store/session-store.js +31 -2
  129. package/dist/core/repl/workspace-context.js +22 -0
  130. package/dist/core/repo-map/build.js +125 -0
  131. package/dist/core/repo-map/cache.js +185 -0
  132. package/dist/core/repo-map/extractor.js +254 -0
  133. package/dist/core/repo-map/formatter.js +145 -0
  134. package/dist/core/repo-map/scanner.js +211 -0
  135. package/dist/core/retry-budget/budget.js +284 -0
  136. package/dist/core/retry-budget/index.js +5 -0
  137. package/dist/core/session.js +92 -0
  138. package/dist/core/settings.js +80 -0
  139. package/dist/core/share/formatter.js +271 -0
  140. package/dist/core/share/redactor.js +221 -0
  141. package/dist/core/share/uploader.js +267 -0
  142. package/dist/core/skills/defaults.js +457 -0
  143. package/dist/core/smoke/headless-driver.js +174 -0
  144. package/dist/core/smoke/orchestrator.js +194 -0
  145. package/dist/core/smoke/runner.js +238 -0
  146. package/dist/core/smoke/scenario-parser.js +316 -0
  147. package/dist/core/subagents/dispatcher-real.js +600 -0
  148. package/dist/core/subagents/dispatcher.js +113 -24
  149. package/dist/core/subagents/index.js +18 -5
  150. package/dist/core/subagents/isolation-matrix.js +213 -0
  151. package/dist/core/subagents/spawn.js +19 -4
  152. package/dist/core/telemetry/emitter.js +229 -0
  153. package/dist/core/telemetry/queue.js +251 -0
  154. package/dist/core/theme/context.js +91 -0
  155. package/dist/core/theme/presets.js +228 -0
  156. package/dist/core/theme/state.js +181 -0
  157. package/dist/core/todos/invariant.js +10 -0
  158. package/dist/core/todos/state.js +177 -0
  159. package/dist/core/transport/version-interceptor.js +166 -0
  160. package/dist/core/vim/keymap.js +288 -0
  161. package/dist/core/vim/state.js +92 -0
  162. package/dist/core/worktree-manager/cleanup.js +123 -0
  163. package/dist/core/worktree-manager/manager.js +303 -0
  164. package/dist/index.js +28 -0
  165. package/dist/runtime/bootstrap.js +190 -0
  166. package/dist/runtime/cli.js +3241 -343
  167. package/dist/runtime/commands/cancel.js +231 -0
  168. package/dist/runtime/commands/chain.js +489 -0
  169. package/dist/runtime/commands/codegraph-status.js +227 -0
  170. package/dist/runtime/commands/compact.js +297 -0
  171. package/dist/runtime/commands/cost.js +199 -0
  172. package/dist/runtime/commands/delegate.js +242 -11
  173. package/dist/runtime/commands/dispatch.js +126 -0
  174. package/dist/runtime/commands/doctor.js +412 -0
  175. package/dist/runtime/commands/feedback.js +184 -0
  176. package/dist/runtime/commands/hooks.js +184 -0
  177. package/dist/runtime/commands/lsp.js +368 -0
  178. package/dist/runtime/commands/mcp.js +879 -0
  179. package/dist/runtime/commands/memory.js +508 -0
  180. package/dist/runtime/commands/model.js +237 -0
  181. package/dist/runtime/commands/onboarding.js +275 -0
  182. package/dist/runtime/commands/patch.js +128 -0
  183. package/dist/runtime/commands/permissions.js +112 -0
  184. package/dist/runtime/commands/plan.js +143 -0
  185. package/dist/runtime/commands/prd-check.js +285 -0
  186. package/dist/runtime/commands/redo-blob-store.js +92 -0
  187. package/dist/runtime/commands/redo.js +361 -0
  188. package/dist/runtime/commands/release-notes.js +229 -0
  189. package/dist/runtime/commands/repo-map.js +95 -0
  190. package/dist/runtime/commands/report.js +299 -0
  191. package/dist/runtime/commands/resume.js +118 -0
  192. package/dist/runtime/commands/review-consensus.js +17 -2
  193. package/dist/runtime/commands/rewind.js +333 -0
  194. package/dist/runtime/commands/sessions.js +163 -0
  195. package/dist/runtime/commands/share.js +316 -0
  196. package/dist/runtime/commands/status.js +186 -0
  197. package/dist/runtime/commands/stickers.js +82 -0
  198. package/dist/runtime/commands/style.js +194 -0
  199. package/dist/runtime/commands/theme.js +196 -0
  200. package/dist/runtime/commands/undo.js +32 -0
  201. package/dist/runtime/commands/update.js +289 -0
  202. package/dist/runtime/commands/vim.js +140 -0
  203. package/dist/runtime/commands/worktree.js +177 -0
  204. package/dist/runtime/commands/worktrees.js +155 -0
  205. package/dist/runtime/headless-repl.js +195 -0
  206. package/dist/runtime/headless.js +543 -0
  207. package/dist/runtime/load-hooks-or-exit.js +71 -0
  208. package/dist/runtime/plan-decompose.js +531 -0
  209. package/dist/runtime/version.js +65 -0
  210. package/dist/tools/agent-tool.js +229 -0
  211. package/dist/tools/apply-patch.js +556 -0
  212. package/dist/tools/ask-user-question.js +213 -0
  213. package/dist/tools/ask-user.js +115 -0
  214. package/dist/tools/bash.js +203 -4
  215. package/dist/tools/file-tools.js +85 -14
  216. package/dist/tools/lsp-tools.js +189 -0
  217. package/dist/tools/mcp-tool.js +260 -0
  218. package/dist/tools/multi-edit.js +361 -0
  219. package/dist/tools/powershell.js +268 -0
  220. package/dist/tools/registry.js +51 -0
  221. package/dist/tools/skill-tool.js +96 -0
  222. package/dist/tools/tasks.js +208 -0
  223. package/dist/tools/todo-write.js +184 -0
  224. package/dist/tools/web-fetch.js +147 -2
  225. package/dist/tools/web-search.js +458 -0
  226. package/dist/tui/agent-progress-card.js +111 -0
  227. package/dist/tui/agent-tree.js +10 -0
  228. package/dist/tui/ask-modal.js +2 -2
  229. package/dist/tui/ask-user-question-prompt.js +192 -0
  230. package/dist/tui/compact-banner.js +81 -0
  231. package/dist/tui/conversation-pane.js +82 -8
  232. package/dist/tui/cost-table.js +111 -0
  233. package/dist/tui/doctor-table.js +46 -0
  234. package/dist/tui/feedback-prompt.js +156 -0
  235. package/dist/tui/input-box.js +218 -3
  236. package/dist/tui/markdown-render.js +4 -4
  237. package/dist/tui/onboarding-wizard.js +240 -0
  238. package/dist/tui/permissions-picker.js +86 -0
  239. package/dist/tui/render.js +35 -0
  240. package/dist/tui/repl-render.js +313 -35
  241. package/dist/tui/repl-splash-art.js +1 -1
  242. package/dist/tui/repl-splash-mascot.js +32 -8
  243. package/dist/tui/repl-splash.js +2 -2
  244. package/dist/tui/repl.js +85 -5
  245. package/dist/tui/splash.js +1 -1
  246. package/dist/tui/status-bar.js +94 -16
  247. package/dist/tui/status-table.js +7 -0
  248. package/dist/tui/stickers-art.js +136 -0
  249. package/dist/tui/style-table.js +28 -0
  250. package/dist/tui/theme-table.js +29 -0
  251. package/dist/tui/thinking-spinner.js +123 -0
  252. package/dist/tui/tool-stream-pane.js +52 -3
  253. package/dist/tui/update-banner.js +27 -2
  254. package/dist/tui/vim-input.js +267 -0
  255. package/dist/tui/welcome-banner.js +107 -0
  256. package/dist/tui/welcome-data.js +293 -0
  257. package/docs/examples/codegraph.mcp.json +10 -0
  258. package/package.json +13 -7
  259. package/test/scenarios/codegen-create-file.scenario.txt +13 -0
  260. package/test/scenarios/compact-force.scenario.txt +11 -0
  261. package/test/scenarios/identity.scenario.txt +11 -0
  262. package/test/scenarios/persona-handoff.scenario.txt +11 -0
  263. package/test/scenarios/walkback.scenario.txt +12 -0
  264. package/dist/core/engine/compaction-hook.js +0 -154
@@ -16,14 +16,20 @@
16
16
  * a tiny `event:`/`data:`/`id:` parser. This keeps the dependency
17
17
  * graph at zero new packages.
18
18
  */
19
+ import { existsSync } from 'node:fs';
20
+ import { resolve } from 'node:path';
19
21
  import React from 'react';
20
22
  import { render } from 'ink';
21
23
  import { Repl } from './repl.js';
22
24
  import { printPugMascotPreInk } from './repl-splash-mascot.js';
25
+ import { collectWelcomeData } from './welcome-data.js';
26
+ import { ThemeProvider } from '../core/theme/context.js';
27
+ import { resolveTheme } from '../core/theme/state.js';
23
28
  import { ReplSession, } from '../core/repl/session.js';
24
29
  import { resolveWorkspaceContext } from '../core/repl/workspace-context.js';
25
30
  import { SqliteSessionStore } from '../core/repl/store/index.js';
26
31
  import { slugForCwd } from '../core/repl/history.js';
32
+ import { loadSettings } from '../core/settings.js';
27
33
  import { WorkingSet, buildRepoSkeleton, loadPugiIgnore, PugiWatcher, } from '../core/context/index.js';
28
34
  /**
29
35
  * Mount the REPL and resolve when the user exits via Ctrl+C × 2 or
@@ -31,12 +37,85 @@ import { WorkingSet, buildRepoSkeleton, loadPugiIgnore, PugiWatcher, } from '../
31
37
  * `pugi resume <sessionId>` once that command exists).
32
38
  */
33
39
  export async function renderRepl(options) {
40
+ // beta.9 CEO dogfood 2026-05-26: claim stdin raw mode + alt-screen
41
+ // BEFORE any async bootstrap step so keystrokes typed during the
42
+ // [launch -> Ink mount] window cannot echo into the terminal in
43
+ // cooked mode. Previously openLocalStore (SQLite open) +
44
+ // bootstrapContext (chokidar start) could take hundreds of ms to
45
+ // multiple seconds on a fresh install / large repo; during that
46
+ // window stdin stayed in cooked mode and the terminal echoed
47
+ // every typed character literally onto the screen below the
48
+ // pre-printed mascot/header. The visible result was the operator's
49
+ // "ssssss" landing on the rendered status-bar bottom row (CEO
50
+ // screenshot 2026-05-26: beta.8 REPL bug 2).
51
+ //
52
+ // The claim is idempotent with Ink's own raw-mode enable: Ink
53
+ // ref-counts setRawMode calls, and Node's stdin.setRawMode is
54
+ // safe to call twice with the same value. The pre-Ink claim acts
55
+ // as a "raw-mode floor" - whatever Ink does after mount layers on
56
+ // top, and our finally{} restore drops the floor only after Ink
57
+ // has cleanly torn down (or never mounted on a bootstrap crash).
58
+ const bootstrap = claimTerminalForRepl();
59
+ // beta.13 auto-init wire (CEO dogfood 2026-05-26): scaffold the
60
+ // `.pugi/` workspace silently on REPL boot so launching `pugi` in a
61
+ // fresh cwd no longer demands an explicit `pugi init` round-trip.
62
+ // Idempotent — every helper inside scaffoldPugiWorkspace is a
63
+ // `*_IfMissing` write, so re-running over an existing workspace is
64
+ // a no-op. Fail-safe: any FS / perms error never blocks REPL launch.
65
+ // Operator escape hatch: PUGI_NO_AUTO_INIT=1.
66
+ //
67
+ // Beta.13 P2 fix 2026-05-26: gate the scaffold on project-root markers
68
+ // so launching `pugi` from `$HOME` / `/tmp` / arbitrary dirs does NOT
69
+ // sprinkle `.pugi/` directories all over the filesystem. The gate
70
+ // mirrors `isBoundWorkspace` from workspace-context.ts but also
71
+ // accepts non-JS roots (Cargo / pyproject / go.mod) because the CLI
72
+ // is language-agnostic and an operator working in a Rust repo deserves
73
+ // the same auto-init UX as a Node operator. Already-bound `.pugi/`
74
+ // dirs also opt back in so the scaffold can fill any missing
75
+ // sub-artifacts the operator deleted.
76
+ // Leak L22 (2026-05-27): `--bare` (PUGI_BARE=1) ALSO suppresses the
77
+ // auto-init scaffold. Bare mode is the deterministic "fresh install
78
+ // anywhere" path — no `.pugi/` writes, no PUGI.md scaffold, no
79
+ // settings.json seed. The pre-existing PUGI_NO_AUTO_INIT escape
80
+ // hatch stays — bare mode just unions with it.
81
+ const { isBareMode } = await import('../core/bare-mode/index.js');
82
+ if (process.env.PUGI_NO_AUTO_INIT !== '1' &&
83
+ !isBareMode() &&
84
+ isProjectRoot(process.cwd())) {
85
+ try {
86
+ const { scaffoldPugiWorkspace } = await import('../runtime/cli.js');
87
+ await scaffoldPugiWorkspace({
88
+ cwd: process.cwd(),
89
+ noDefaults: true,
90
+ log: () => {
91
+ /* silent — never leak scaffold progress into the REPL alt-screen */
92
+ },
93
+ });
94
+ }
95
+ catch (err) {
96
+ // Fail-safe: read-only FS or perms error never blocks REPL launch.
97
+ // Beta.13 P2 fix 2026-05-26: bare-catch swallowed the diagnostic;
98
+ // surface it on stderr under PUGI_DEBUG=1 so operator-triage on
99
+ // "why isn't .pugi/ being created?" has a starting point without
100
+ // having to re-instrument the bootstrap.
101
+ if (process.env.PUGI_DEBUG === '1') {
102
+ const msg = err instanceof Error ? err.message : String(err);
103
+ process.stderr.write(`[pugi-debug] auto-init failed: ${msg}\n`);
104
+ }
105
+ }
106
+ }
34
107
  const transport = createProductionTransport();
35
108
  // Auto-bind the workspace context from process.cwd() so Mira knows
36
109
  // which repo the operator launched the CLI in. The resolver is
37
110
  // best-effort — any FS error falls back to a basename-only summary,
38
111
  // never blocks REPL launch. Wave 4 fix 2026-05-25.
39
112
  const workspace = options.workspace ?? resolveWorkspaceContext(process.cwd());
113
+ // Beta.13 P1 fix 2026-05-26: read `ui.cyberZoo` from
114
+ // `.pugi/settings.json` so the operator's splash posture flows to
115
+ // admin-api on session open. Without this, the renderer's `cyberZoo`
116
+ // parameter (added beta.13) was always defaulted to 'on' regardless
117
+ // of the operator's actual setting.
118
+ const cyberZoo = readCyberZooSetting(process.cwd());
40
119
  // α6.4: open the local SessionStore for `/resume` persistence. The
41
120
  // store lives under `~/.pugi/projects/<slug>/`; failure is fail-safe
42
121
  // — we log a one-line warning to stderr and continue with the REPL
@@ -64,6 +143,7 @@ export async function renderRepl(options) {
64
143
  cliVersion: options.cliVersion,
65
144
  transport,
66
145
  workspace,
146
+ cyberZoo,
67
147
  store,
68
148
  localSessionId: openedSessionId,
69
149
  repoSkeleton: skeleton,
@@ -86,21 +166,14 @@ export async function renderRepl(options) {
86
166
  // Kick off the connect; the Repl renders the connecting state until
87
167
  // the session pushes `connection: 'on_watch'` from the SSE onOpen.
88
168
  void session.start();
89
- // α6.14.4 CEO dogfood 2026-05-25 (parity with Claude Code): enter
90
- // the terminal's alternate screen buffer so the REPL renders on a
91
- // fresh "screen" the operator cannot scroll above. On exit, leave
92
- // restores the previous terminal contents - the conversation does
93
- // not pollute the operator's shell history. Skipped under --no-tty
94
- // and when stdout is not a TTY (pipe/CI), where the escapes would
95
- // appear as literal characters.
96
- // ORDER MATTERS (beta.2 follow-up): alt-screen enter MUST happen
97
- // BEFORE the chafa mascot pre-print. Reversed, the alt-screen clear
98
- // wiped the freshly-painted pug and the operator saw nothing.
99
- const supportsAltScreen = process.stdout.isTTY === true;
100
- if (supportsAltScreen) {
101
- process.stdout.write('\x1b[?1049h');
102
- process.stdout.write('\x1b[H');
103
- }
169
+ // beta.9: drain any keystrokes that landed in stdin between the
170
+ // pre-Ink raw-mode claim and now. Without this, the queued bytes
171
+ // would feed Ink's first useInput tick as a flood of "stale"
172
+ // characters once the InputBox mounts - the operator would see
173
+ // their pre-typed input materialise in the prompt as if they had
174
+ // typed it after the REPL became interactive. Idempotent: no-op
175
+ // when stdin is not a TTY or no bytes were buffered.
176
+ drainBufferedStdin(process.stdin);
104
177
  // α6.14.2 wave 5: paint the chafa-baked brand-pug ANSI render to
105
178
  // stdout BEFORE Ink mounts (but AFTER alt-screen enter). Ink's
106
179
  // layout engine would mis-measure the truecolor escape sequences,
@@ -111,33 +184,55 @@ export async function renderRepl(options) {
111
184
  // (operator opted out via --no-splash), we suppress the pre-print
112
185
  // too so the boot stays silent.
113
186
  const mascotPrePrinted = options.skipSplash === true ? false : printPugMascotPreInk(process.stdout);
114
- const instance = render(React.createElement(Repl, {
187
+ // Leak L30 (2026-05-27): resolve the active theme ONCE at mount
188
+ // and wrap `<Repl />` in `<ThemeProvider>` so every Ink consumer
189
+ // (`<Header>`, `<DoctorTable>`, `<StyleTable>`, `<ThemeTable>`,
190
+ // …) picks up the same color tokens. The provider is stable for
191
+ // the lifetime of the REPL — operator `/theme <name>` writes to
192
+ // disk + appends a system line, and the next `pugi` launch re-
193
+ // mounts with the new slug. Re-mounting mid-session would race
194
+ // against Ink's raw-mode handler so we deliberately keep the
195
+ // session-lifetime contract instead of polling the config file.
196
+ const resolvedTheme = resolveTheme({
197
+ workspaceRoot: process.cwd(),
198
+ env: process.env,
199
+ });
200
+ // CEO P0 #2 (2026-05-29): collect welcome banner data BEFORE Ink
201
+ // mounts so the banner paints on the first frame instead of swapping
202
+ // in mid-render. The collector swallows every IO error so а missing
203
+ // CHANGELOG / unreadable credential / malformed settings never
204
+ // blocks the boot.
205
+ let welcomeData;
206
+ if (options.skipSplash !== true) {
207
+ try {
208
+ welcomeData = collectWelcomeData({
209
+ cliVersion: options.cliVersion,
210
+ cwd: process.cwd(),
211
+ });
212
+ }
213
+ catch {
214
+ welcomeData = undefined;
215
+ }
216
+ }
217
+ const instance = render(React.createElement(ThemeProvider, { slug: resolvedTheme.slug }, React.createElement(Repl, {
115
218
  session,
116
219
  updateBanner: options.updateBanner ?? null,
117
220
  skipSplash: options.skipSplash === true,
118
221
  hideToolStream: options.hideToolStream === true,
119
222
  mascotPrePrinted,
120
- }));
121
- const restoreAltScreen = () => {
122
- if (supportsAltScreen) {
123
- try {
124
- process.stdout.write('\x1b[?1049l');
125
- }
126
- catch {
127
- /* shutdown race — terminal already detached */
128
- }
129
- }
130
- };
223
+ welcomeData,
224
+ autoInitStatus: options.autoInitStatus ?? null,
225
+ })));
131
226
  // Make sure we leave the alt screen on abrupt exits too. Without
132
227
  // this the operator's shell stays "frozen" on the Pugi splash.
133
- process.once('exit', restoreAltScreen);
134
- process.once('SIGINT', restoreAltScreen);
135
- process.once('SIGTERM', restoreAltScreen);
228
+ process.once('exit', bootstrap.restore);
229
+ process.once('SIGINT', bootstrap.restore);
230
+ process.once('SIGTERM', bootstrap.restore);
136
231
  try {
137
232
  await instance.waitUntilExit();
138
233
  }
139
234
  finally {
140
- restoreAltScreen();
235
+ bootstrap.restore();
141
236
  session.close();
142
237
  if (store) {
143
238
  try {
@@ -157,6 +252,138 @@ export async function renderRepl(options) {
157
252
  }
158
253
  }
159
254
  }
255
+ export function claimTerminalForRepl(stdin = process.stdin, stdout = process.stdout) {
256
+ const isStdoutTty = stdout.isTTY === true;
257
+ const isStdinTty = stdin.isTTY === true && typeof stdin.setRawMode === 'function';
258
+ let altScreenEntered = false;
259
+ if (isStdoutTty) {
260
+ try {
261
+ stdout.write('\x1b[?1049h');
262
+ stdout.write('\x1b[H');
263
+ altScreenEntered = true;
264
+ }
265
+ catch {
266
+ /* terminal already detached */
267
+ }
268
+ }
269
+ let rawModeClaimed = false;
270
+ if (isStdinTty) {
271
+ try {
272
+ stdin.setEncoding('utf8');
273
+ stdin.setRawMode(true);
274
+ // Resume so the kernel actually delivers bytes to Node's event
275
+ // loop. Without resume, raw mode is set but data does not flow
276
+ // until something else (e.g. Ink) attaches a 'data' listener.
277
+ stdin.resume();
278
+ rawModeClaimed = true;
279
+ }
280
+ catch {
281
+ /* raw mode unsupported - the operator's shell still works */
282
+ }
283
+ }
284
+ let restored = false;
285
+ const restore = () => {
286
+ if (restored)
287
+ return;
288
+ restored = true;
289
+ if (rawModeClaimed && isStdinTty) {
290
+ try {
291
+ stdin.setRawMode(false);
292
+ }
293
+ catch {
294
+ /* terminal already detached */
295
+ }
296
+ }
297
+ if (altScreenEntered) {
298
+ try {
299
+ stdout.write('\x1b[?1049l');
300
+ }
301
+ catch {
302
+ /* shutdown race - terminal already detached */
303
+ }
304
+ }
305
+ };
306
+ return { altScreenEntered, rawModeClaimed, restore };
307
+ }
308
+ /**
309
+ * Read and discard any bytes buffered in stdin between
310
+ * `claimTerminalForRepl()` and the Ink mount. Returns the number of
311
+ * bytes drained so tests can assert the behaviour without intercepting
312
+ * the side effect.
313
+ *
314
+ * `stdin.read()` is a no-op when no data is buffered, so this is safe
315
+ * to call whether or not the operator actually typed during bootstrap.
316
+ * Wrapped in try/catch because a closed / piped stdin will throw on
317
+ * read in some Node versions.
318
+ */
319
+ export function drainBufferedStdin(stdin = process.stdin) {
320
+ if (stdin.isTTY !== true)
321
+ return 0;
322
+ try {
323
+ let bytesDrained = 0;
324
+ // Loop until read() returns null - readable streams may chunk
325
+ // buffered bytes across multiple read() calls when the operator
326
+ // typed faster than the kernel could deliver to Node's loop.
327
+ for (;;) {
328
+ const chunk = stdin.read();
329
+ if (chunk === null)
330
+ return bytesDrained;
331
+ bytesDrained += typeof chunk === 'string' ? chunk.length : chunk.byteLength;
332
+ }
333
+ }
334
+ catch {
335
+ return 0;
336
+ }
337
+ }
338
+ /**
339
+ * Project-root probe — beta.13 P2 fix 2026-05-26.
340
+ *
341
+ * Beta.13 auto-init was unconditional and silently created `.pugi/` in
342
+ * every cwd the REPL was launched from, including `$HOME` and `/tmp`.
343
+ * Operators who ran `pugi` to ask a quick question outside of any
344
+ * project ended up with stray `.pugi/` directories polluting their
345
+ * filesystem. The gate looks for any of six project-root markers
346
+ * before scaffolding:
347
+ *
348
+ * - `package.json` — JS / TS workspaces
349
+ * - `.git` — any cloned repo regardless of language
350
+ * - `.pugi` — already-bound Pugi workspace (re-scaffold
351
+ * fills any missing artifacts the operator
352
+ * deleted, idempotent over existing files)
353
+ * - `Cargo.toml` — Rust crates
354
+ * - `pyproject.toml` — Python projects (PEP 518)
355
+ * - `go.mod` — Go modules
356
+ *
357
+ * The probe is six cheap `existsSync` calls; the cost is negligible
358
+ * compared with the alt-screen + Ink mount that follows. Exported so a
359
+ * future unit spec can lock the contract.
360
+ */
361
+ export function isProjectRoot(cwd) {
362
+ // ESM static imports — `require()` is not defined in a `"type": "module"`
363
+ // bundle and would throw `ReferenceError: require is not defined` the
364
+ // moment the REPL bootstrap calls this gate. Beta.16 P0 fix 2026-05-27.
365
+ return (existsSync(resolve(cwd, 'package.json')) ||
366
+ existsSync(resolve(cwd, '.git')) ||
367
+ existsSync(resolve(cwd, '.pugi')) ||
368
+ existsSync(resolve(cwd, 'Cargo.toml')) ||
369
+ existsSync(resolve(cwd, 'pyproject.toml')) ||
370
+ existsSync(resolve(cwd, 'go.mod')));
371
+ }
372
+ /**
373
+ * Read the operator's cyber-zoo posture from `.pugi/settings.json`.
374
+ * Best-effort: when the file is missing / malformed, fall through to
375
+ * the historical 'on' default so the REPL never refuses to launch on
376
+ * a settings error. Beta.13 P1 fix 2026-05-26.
377
+ */
378
+ function readCyberZooSetting(cwd) {
379
+ try {
380
+ const settings = loadSettings(cwd);
381
+ return settings.ui?.cyberZoo ?? 'on';
382
+ }
383
+ catch {
384
+ return 'on';
385
+ }
386
+ }
160
387
  /**
161
388
  * Open the local SessionStore for the REPL bootstrap. Returns
162
389
  * `{ store: null, openedSessionId: undefined }` on any error so the
@@ -248,13 +475,20 @@ async function bootstrapContext(input) {
248
475
  /* ------------------------------------------------------------------ */
249
476
  /* Production transport */
250
477
  /* ------------------------------------------------------------------ */
251
- function createProductionTransport() {
478
+ export function createProductionTransport() {
252
479
  return {
253
- async createSession({ apiUrl, apiKey, workspace }) {
480
+ async createSession({ apiUrl, apiKey, workspace, cyberZoo }) {
254
481
  // Forward the workspace bundle in the POST body so admin-api can
255
482
  // surface `<workspace-context>` in Mira's prompt. Older admin-api
256
483
  // builds ignore unknown fields, so this stays forward-compatible.
257
484
  // Wave 4 fix 2026-05-25.
485
+ //
486
+ // Beta.13 P1 fix 2026-05-26: also forward `cyberZoo` so admin-api
487
+ // can render Mira's `<cyber-zoo>` marker matching the operator's
488
+ // `.pugi/settings.json::ui.cyberZoo` toggle instead of the
489
+ // historical 'on' default. Only included on the wire when set
490
+ // explicitly so a missing setting still survives older admin-api
491
+ // builds that do not declare the DTO field.
258
492
  const body = {};
259
493
  if (workspace?.workspaceCwd)
260
494
  body.workspaceCwd = workspace.workspaceCwd;
@@ -262,6 +496,8 @@ function createProductionTransport() {
262
496
  body.workspaceSlug = workspace.workspaceSlug;
263
497
  if (workspace?.workspaceSummary)
264
498
  body.workspaceSummary = workspace.workspaceSummary;
499
+ if (cyberZoo === 'on' || cyberZoo === 'off')
500
+ body.cyberZoo = cyberZoo;
265
501
  const response = await fetch(joinUrl(apiUrl, '/api/pugi/sessions'), {
266
502
  method: 'POST',
267
503
  headers: jsonHeaders(apiKey),
@@ -307,6 +543,31 @@ function createProductionTransport() {
307
543
  if (lastEventId) {
308
544
  headers['Last-Event-ID'] = lastEventId;
309
545
  }
546
+ // beta.9 CEO dogfood 2026-05-26: hard timeout on the SSE
547
+ // handshake so a CDN/proxy that buffers the response (or an
548
+ // admin-api that accepted the route but never flushed headers)
549
+ // cannot freeze the REPL in `connecting` forever. The 5s budget
550
+ // is generous - admin-api routinely responds in <500ms when
551
+ // healthy - but tight enough that an operator who launched
552
+ // `pugi` and is staring at the screen will see the status flip
553
+ // to `reconnecting` instead of an indefinite hang. The
554
+ // AbortController bound to the fetch aborts the in-flight
555
+ // request when the timer fires, which surfaces as an
556
+ // `AbortError` and routes through the existing onError handler
557
+ // (which calls scheduleReconnect via the session). The timer
558
+ // is cleared the moment onOpen fires so a slow-but-eventually-
559
+ // successful handshake still works.
560
+ const handshakeDeadlineMs = 5_000;
561
+ const handshakeTimer = setTimeout(() => {
562
+ controller.abort();
563
+ // onError is called from the catch block below (the abort
564
+ // synthesises an AbortError that consumeSseStream / fetch
565
+ // will throw). No explicit onError call here - we let the
566
+ // catch path normalise the error message so the operator
567
+ // sees the consistent "SSE handshake timed out (5s)" prose
568
+ // through the same plumbing that surfaces every other
569
+ // transport failure.
570
+ }, handshakeDeadlineMs);
310
571
  void (async () => {
311
572
  try {
312
573
  const response = await fetch(url, {
@@ -320,6 +581,9 @@ function createProductionTransport() {
320
581
  if (!response.body) {
321
582
  throw new Error('SSE response has no body');
322
583
  }
584
+ // Handshake survived; cancel the deadline so a slow
585
+ // first-event stream does not get aborted later.
586
+ clearTimeout(handshakeTimer);
323
587
  onOpen();
324
588
  await consumeSseStream(response.body, onEvent);
325
589
  // Server closed the stream cleanly. Treat as an error so
@@ -329,13 +593,27 @@ function createProductionTransport() {
329
593
  onError(new Error('SSE stream ended'));
330
594
  }
331
595
  catch (error) {
332
- if (controller.signal.aborted)
596
+ clearTimeout(handshakeTimer);
597
+ if (controller.signal.aborted) {
598
+ // Distinguish operator-driven close (session.close())
599
+ // from the handshake-deadline abort. The session sets a
600
+ // `closed` flag before calling controller.abort(); the
601
+ // handshake-deadline abort fires while the session is
602
+ // still expecting onOpen. We cannot read session state
603
+ // from here, so we surface a single error class with a
604
+ // clear message - the session-side onError handler
605
+ // already short-circuits when `closed=true`.
606
+ onError(new Error(`SSE handshake timed out after ${handshakeDeadlineMs}ms`));
333
607
  return;
608
+ }
334
609
  onError(error instanceof Error ? error : new Error(String(error)));
335
610
  }
336
611
  })();
337
612
  return {
338
- close: () => controller.abort(),
613
+ close: () => {
614
+ clearTimeout(handshakeTimer);
615
+ controller.abort();
616
+ },
339
617
  };
340
618
  },
341
619
  };
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Hand-crafted at 9 rows × 20 columns to read as a pug at a single
5
5
  * glance — references the cyber-zoo hero glyph in
6
- * `apps/clawhost-web/public/brand/hero-pug.png`: blocky pug face with
6
+ * `apps/console-web/public/brand/hero-pug.png`: blocky pug face with
7
7
  * angular ear flaps on either side of the head, forehead crease,
8
8
  * angular cyan eyes (`◉`), smushed snout, undershot jaw, and a small
9
9
  * cyan circuit chip (`▐■▌`) on the lower-right cheek.
@@ -22,7 +22,7 @@
22
22
  *
23
23
  * Generation (operator-side, one-shot):
24
24
  * chafa --size 80x40 --symbols=vhalf --colors=full \
25
- * apps/clawhost-web/public/brand/hero-pug.png \
25
+ * apps/console-web/public/brand/hero-pug.png \
26
26
  * > apps/pugi-cli/assets/pugi-mascot.ansi
27
27
  *
28
28
  * The output is committed verbatim to the repo and shipped inside the
@@ -48,9 +48,21 @@ import { fileURLToPath } from 'node:url';
48
48
  * — two directory hops up from this file. In a local `pnpm dev`
49
49
  * checkout the structure is the same (`src/tui/` ⇒ `../../assets/`)
50
50
  * because tsx re-resolves the same relative tree.
51
+ *
52
+ * CEO P0 #2 (2026-05-29) — banner mascot bake. The prozr2 portrait is
53
+ * the canonical brand-pug glyph from `apps/console-web/public/brand/
54
+ * Pugi-prozr2.png`, baked к а 16x8 vhalf truecolor render. We prefer
55
+ * the prozr2 bake when it ships alongside the CLI (small — ~900 bytes,
56
+ * shaped for the compact welcome-banner left column) and fall back к
57
+ * the legacy 40KB `pugi-mascot.ansi` (hero-pug 80x40) when prozr2 is
58
+ * missing — preserves the splash-mascot install surface for any
59
+ * tarball that predates the bake.
51
60
  */
52
61
  export function pugMascotAssetPath() {
53
62
  const here = dirname(fileURLToPath(import.meta.url));
63
+ const prozr2 = resolvePath(here, '..', '..', 'assets', 'pugi-prozr2-mascot.ansi');
64
+ if (existsSync(prozr2))
65
+ return prozr2;
54
66
  return resolvePath(here, '..', '..', 'assets', 'pugi-mascot.ansi');
55
67
  }
56
68
  /**
@@ -89,21 +101,33 @@ export function loadPugMascotAnsi() {
89
101
  // icon, clipboard, hyperlinks, color-palette change). Drop them
90
102
  // so a corrupted asset cannot rename the operator's terminal tab
91
103
  // or smuggle a hyperlink into the splash region.
92
- // 2. Drop CSI ? <mode> [hl] for mouse-tracking and screen-buffer
93
- // switch modes (1000, 1001, 1002, 1003, 1004, 1005, 1006, 1015,
94
- // 1049, 47, 1047, 1048). These would either start swallowing
95
- // mouse input or flip the terminal into the alternate screen.
104
+ // 2. Drop ALL CSI ? <numbers and semicolons> [lh] (DEC private-mode
105
+ // set / reset). The legitimate chafa output for a splash is
106
+ // truecolor SGR (`CSI 38;2;R;G;B m`) plus cursor-positioning
107
+ // no private-mode toggle ever appears там legitimately. A
108
+ // permissive deny-all pattern covers every disruptive private
109
+ // mode in one regex:
110
+ // - cursor visibility (25)
111
+ // - alt-screen buffer (47, 1047, 1048, 1049)
112
+ // - mouse tracking (1000, 1001, 1002, 1003, 1004, 1005, 1006, 1015)
113
+ // - bracketed paste (2004)
114
+ // - focus reporting (1004)
115
+ // - multi-mode forms (e.g. `CSI ? 47 ; 1049 h` — legal per
116
+ // xterm ctlseqs but missed by the previous single-mode regex)
117
+ // - any future private mode a corrupt asset might emit
118
+ // Allowlisting the modes the splash needs is impossible because
119
+ // the splash needs ZERO of them — chafa renders glyph-by-glyph,
120
+ // not via private-mode toggles. A pure deny-all is strictly
121
+ // safer than enumerating known-bad modes one-by-one.
96
122
  // 3. Drop CSI 6 n (cursor-position report). Would inject a fake
97
123
  // CPR into the operator's stdin stream.
98
124
  // 4. Drop CSI [23]J / CSI [23]K (full screen / line clear). A
99
125
  // chafa render uses cursor-positioning per row, not bulk
100
126
  // erases; bulk clears would wipe whatever the operator already
101
127
  // had on screen above the splash.
102
- // The cursor-hide/show wrappers (CSI ? 25 [lh]) are handled by
103
- // the same CSI-?-mode pattern as the mouse / alt-screen modes.
104
128
  const stripped = raw
105
129
  .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
106
- .replace(/\x1b\[\?(?:25|47|1000|1001|1002|1003|1004|1005|1006|1015|1047|1048|1049)[lh]/g, '')
130
+ .replace(/\x1b\[\?[0-9;]+[lh]/g, '')
107
131
  .replace(/\x1b\[6n/g, '')
108
132
  .replace(/\x1b\[[23]?[JK]/g, '');
109
133
  if (stripped.trim().length === 0)
@@ -67,7 +67,7 @@ export function ReplSplash(props) {
67
67
  // pugs. The header card still renders inline so wordmark + status
68
68
  // rows stay attached to the splash flow.
69
69
  const showHandCraftedMascot = props.mascotPrePrinted !== true;
70
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsxs(Box, { flexDirection: "row", children: [showHandCraftedMascot ? _jsx(MascotColumn, {}) : null, _jsxs(Box, { flexDirection: "column", marginLeft: showHandCraftedMascot ? 2 : 0, marginTop: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Pugi" }), _jsx(Text, { bold: true, color: "cyan", children: ".io" }), _jsx(Text, { dimColor: true, children: ` v${props.cliVersion}` })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(HeaderRow, { label: "Plan", value: props.plan ?? PLACEHOLDER }), _jsx(HeaderRow, { label: "Model", value: props.model ?? PLACEHOLDER }), _jsx(HeaderRow, { label: "Tenant", value: props.tenant ?? PLACEHOLDER }), _jsx(HeaderRow, { label: "Workspace", value: props.workspaceLabel })] })] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: '─'.repeat(40) }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Tips for getting started:" }), _jsx(TipRow, { index: 1, text: "Type a brief, the workforce dispatches" }), _jsx(TipRow, { index: 2, text: "/help for slash commands, /web <url> to pull a page" }), _jsx(TipRow, { index: 3, text: "/skills install <name> for Anthropic / OpenClaw skills" })] })] }));
70
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsxs(Box, { flexDirection: "row", children: [showHandCraftedMascot ? _jsx(MascotColumn, {}) : null, _jsxs(Box, { flexDirection: "column", marginLeft: showHandCraftedMascot ? 2 : 0, marginTop: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Pugi" }), _jsx(Text, { bold: true, color: "#3da9fc", children: ".io" }), _jsx(Text, { dimColor: true, children: ` v${props.cliVersion}` })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(HeaderRow, { label: "Plan", value: props.plan ?? PLACEHOLDER }), _jsx(HeaderRow, { label: "Model", value: props.model ?? PLACEHOLDER }), _jsx(HeaderRow, { label: "Tenant", value: props.tenant ?? PLACEHOLDER }), _jsx(HeaderRow, { label: "Workspace", value: props.workspaceLabel })] })] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: '─'.repeat(40) }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Tips for getting started:" }), _jsx(TipRow, { index: 1, text: "Type a brief, the workforce dispatches" }), _jsx(TipRow, { index: 2, text: "/help for slash commands, /web <url> to pull a page" }), _jsx(TipRow, { index: 3, text: "/skills install <name> for Anthropic / OpenClaw skills" })] })] }));
71
71
  }
72
72
  /**
73
73
  * Renders the multi-line ASCII pug. Each row is split into colored
@@ -105,7 +105,7 @@ function MascotRow({ row, mask, }) {
105
105
  if (buffer.length > 0) {
106
106
  runs.push({ text: buffer, cyan: bufferCyan });
107
107
  }
108
- return (_jsx(Text, { children: runs.map((run, runIndex) => run.cyan ? (_jsx(Text, { color: "cyan", children: run.text }, runIndex)) : (_jsx(Text, { color: "gray", children: run.text }, runIndex))) }));
108
+ return (_jsx(Text, { children: runs.map((run, runIndex) => run.cyan ? (_jsx(Text, { color: "#3da9fc", children: run.text }, runIndex)) : (_jsx(Text, { color: "gray", children: run.text }, runIndex))) }));
109
109
  }
110
110
  function HeaderRow({ label, value }) {
111
111
  const padded = `${label}:`.padEnd(11, ' ');