@pugi/cli 0.1.0-alpha.8 → 0.1.0-beta.1

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 (62) hide show
  1. package/README.md +33 -0
  2. package/assets/pugi-mascot.ansi +41 -0
  3. package/dist/commands/deploy.js +439 -0
  4. package/dist/core/agents/loader.js +104 -0
  5. package/dist/core/agents/registry.js +1 -1
  6. package/dist/core/consensus/anvil-fanout.js +276 -0
  7. package/dist/core/consensus/diff-capture.js +382 -0
  8. package/dist/core/consensus/rubric.js +233 -0
  9. package/dist/core/context/index.js +21 -0
  10. package/dist/core/context/pugiignore.js +316 -0
  11. package/dist/core/context/repo-skeleton.js +533 -0
  12. package/dist/core/context/watcher.js +342 -0
  13. package/dist/core/context/working-set.js +165 -0
  14. package/dist/core/edits/dispatch.js +185 -0
  15. package/dist/core/edits/index.js +15 -0
  16. package/dist/core/edits/layer-a-apply.js +217 -0
  17. package/dist/core/edits/layer-b-apply.js +211 -0
  18. package/dist/core/edits/layer-c-apply.js +160 -0
  19. package/dist/core/edits/layer-d-ast.js +29 -0
  20. package/dist/core/edits/marker-parser.js +401 -0
  21. package/dist/core/edits/security-gate.js +223 -0
  22. package/dist/core/engine/native-pugi.js +6 -1
  23. package/dist/core/engine/tool-bridge.js +33 -1
  24. package/dist/core/repl/ask.js +512 -0
  25. package/dist/core/repl/cancellation.js +98 -0
  26. package/dist/core/repl/dispatch-fsm.js +220 -0
  27. package/dist/core/repl/privacy-banner.js +71 -0
  28. package/dist/core/repl/session.js +1909 -13
  29. package/dist/core/repl/slash-commands.js +59 -32
  30. package/dist/core/repl/store/index.js +12 -0
  31. package/dist/core/repl/store/jsonl-log.js +321 -0
  32. package/dist/core/repl/store/lockfile.js +155 -0
  33. package/dist/core/repl/store/session-store.js +792 -0
  34. package/dist/core/repl/store/types.js +44 -0
  35. package/dist/core/repl/store/uuid-v7.js +68 -0
  36. package/dist/core/repl/workspace-context.js +184 -0
  37. package/dist/core/skills/loader.js +454 -0
  38. package/dist/core/skills/sources.js +480 -0
  39. package/dist/core/skills/trust.js +172 -0
  40. package/dist/runtime/cli.js +728 -10
  41. package/dist/runtime/commands/agents.js +385 -0
  42. package/dist/runtime/commands/config.js +338 -8
  43. package/dist/runtime/commands/review-consensus.js +399 -0
  44. package/dist/runtime/commands/skills.js +401 -0
  45. package/dist/tools/file-tools.js +90 -0
  46. package/dist/tools/web-fetch.js +1 -1
  47. package/dist/tui/agent-tree-pane.js +9 -0
  48. package/dist/tui/ask-cli.js +52 -0
  49. package/dist/tui/ask-modal.js +211 -0
  50. package/dist/tui/conversation-pane.js +48 -3
  51. package/dist/tui/input-box.js +48 -5
  52. package/dist/tui/markdown-render.js +266 -0
  53. package/dist/tui/repl-render.js +183 -4
  54. package/dist/tui/repl-splash-art.js +64 -0
  55. package/dist/tui/repl-splash-mascot.js +130 -0
  56. package/dist/tui/repl-splash.js +117 -0
  57. package/dist/tui/repl.js +108 -11
  58. package/dist/tui/slash-palette.js +47 -10
  59. package/dist/tui/status-bar.js +77 -4
  60. package/dist/tui/tool-stream-pane.js +91 -0
  61. package/dist/tui/workspace-context.js +105 -0
  62. package/package.json +11 -5
@@ -19,7 +19,12 @@
19
19
  import React from 'react';
20
20
  import { render } from 'ink';
21
21
  import { Repl } from './repl.js';
22
- import { ReplSession } from '../core/repl/session.js';
22
+ import { printPugMascotPreInk } from './repl-splash-mascot.js';
23
+ import { ReplSession, } from '../core/repl/session.js';
24
+ import { resolveWorkspaceContext } from '../core/repl/workspace-context.js';
25
+ import { SqliteSessionStore } from '../core/repl/store/index.js';
26
+ import { slugForCwd } from '../core/repl/history.js';
27
+ import { WorkingSet, buildRepoSkeleton, loadPugiIgnore, PugiWatcher, } from '../core/context/index.js';
23
28
  /**
24
29
  * Mount the REPL and resolve when the user exits via Ctrl+C × 2 or
25
30
  * `/quit`. The session is closed (server-side stays alive; resume via
@@ -27,34 +32,208 @@ import { ReplSession } from '../core/repl/session.js';
27
32
  */
28
33
  export async function renderRepl(options) {
29
34
  const transport = createProductionTransport();
35
+ // Auto-bind the workspace context from process.cwd() so Mira knows
36
+ // which repo the operator launched the CLI in. The resolver is
37
+ // best-effort — any FS error falls back to a basename-only summary,
38
+ // never blocks REPL launch. Wave 4 fix 2026-05-25.
39
+ const workspace = options.workspace ?? resolveWorkspaceContext(process.cwd());
40
+ // α6.4: open the local SessionStore for `/resume` persistence. The
41
+ // store lives under `~/.pugi/projects/<slug>/`; failure is fail-safe
42
+ // — we log a one-line warning to stderr and continue with the REPL
43
+ // in memory-only mode. Lock-busy errors get the friendliest message
44
+ // so an operator running two REPLs in the same project understands
45
+ // the constraint.
46
+ const projectSlug = slugForCwd(process.cwd());
47
+ const { store, openedSessionId } = await openLocalStore({
48
+ projectSlug,
49
+ workspaceRoot: process.cwd(),
50
+ resumeLocalSessionId: options.resumeLocalSessionId,
51
+ });
52
+ // α6.5 three-tier context bootstrap. The skeleton + working set
53
+ // + watcher are local-first and best-effort: every step is wrapped
54
+ // in try/catch so an unreadable workspace never blocks REPL launch.
55
+ // Opt-out via PUGI_DISABLE_CONTEXT=1 for hermetic test runs.
56
+ const { skeleton, workingSet, watcher } = await bootstrapContext({
57
+ cwd: process.cwd(),
58
+ env: process.env,
59
+ });
30
60
  const session = new ReplSession({
31
61
  apiUrl: options.apiUrl,
32
62
  apiKey: options.apiKey,
33
63
  workspaceLabel: options.workspaceLabel,
34
64
  cliVersion: options.cliVersion,
35
65
  transport,
66
+ workspace,
67
+ store,
68
+ localSessionId: openedSessionId,
69
+ repoSkeleton: skeleton,
70
+ workingSet,
71
+ watcher,
36
72
  });
73
+ // Restore the transcript from the JSONL log if we resumed an
74
+ // existing session. The restore is idempotent and bypasses persist
75
+ // (no double-write of replayed rows).
76
+ if (store && openedSessionId && options.resumeLocalSessionId) {
77
+ try {
78
+ const events = await store.loadEvents(openedSessionId, { limit: 500 });
79
+ session.restoreTranscript(events);
80
+ }
81
+ catch (error) {
82
+ const msg = error instanceof Error ? error.message : String(error);
83
+ process.stderr.write(`[pugi] Could not restore session ${openedSessionId.slice(0, 13)}: ${msg}\n`);
84
+ }
85
+ }
37
86
  // Kick off the connect; the Repl renders the connecting state until
38
87
  // the session pushes `connection: 'on_watch'` from the SSE onOpen.
39
88
  void session.start();
40
- const instance = render(React.createElement(Repl, { session, updateBanner: options.updateBanner ?? null }));
89
+ // α6.14.2 wave 5: paint the chafa-baked brand-pug ANSI render to
90
+ // stdout BEFORE Ink mounts. Ink's layout engine would mis-measure
91
+ // the truecolor escape sequences, so the pug must land verbatim.
92
+ // The flag is passed into <Repl /> so the splash component knows to
93
+ // skip its own hand-crafted PUG_MASCOT column — otherwise the
94
+ // operator sees both the chafa pug AND the ASCII fallback stacked.
95
+ // When skipSplash is true (operator opted out via --no-splash), we
96
+ // suppress the pre-print too so the boot stays silent.
97
+ const mascotPrePrinted = options.skipSplash === true ? false : printPugMascotPreInk(process.stdout);
98
+ const instance = render(React.createElement(Repl, {
99
+ session,
100
+ updateBanner: options.updateBanner ?? null,
101
+ skipSplash: options.skipSplash === true,
102
+ hideToolStream: options.hideToolStream === true,
103
+ mascotPrePrinted,
104
+ }));
41
105
  try {
42
106
  await instance.waitUntilExit();
43
107
  }
44
108
  finally {
45
109
  session.close();
110
+ if (store) {
111
+ try {
112
+ await store.close();
113
+ }
114
+ catch {
115
+ /* idempotent — already closed */
116
+ }
117
+ }
118
+ if (watcher) {
119
+ try {
120
+ await watcher.close();
121
+ }
122
+ catch {
123
+ /* idempotent — chokidar may already be torn down */
124
+ }
125
+ }
126
+ }
127
+ }
128
+ /**
129
+ * Open the local SessionStore for the REPL bootstrap. Returns
130
+ * `{ store: null, openedSessionId: undefined }` on any error so the
131
+ * caller falls through to memory-only mode rather than failing the
132
+ * launch. The one error we surface verbatim is the lock-busy case —
133
+ * that one is operator-actionable.
134
+ */
135
+ async function openLocalStore(input) {
136
+ // Honour an explicit opt-out for offline-strict environments / CI.
137
+ // PUGI_DISABLE_SESSION_STORE=1 wipes the integration to zero. Useful
138
+ // for hermetic test runs and for operators who do not want any
139
+ // persistence under $HOME.
140
+ if (process.env.PUGI_DISABLE_SESSION_STORE === '1') {
141
+ return { store: null, openedSessionId: undefined };
142
+ }
143
+ try {
144
+ const store = new SqliteSessionStore({ projectSlug: input.projectSlug });
145
+ const row = await store.open({
146
+ id: input.resumeLocalSessionId,
147
+ workspaceRoot: input.workspaceRoot,
148
+ projectSlug: input.projectSlug,
149
+ });
150
+ return { store, openedSessionId: row.id };
151
+ }
152
+ catch (error) {
153
+ const code = error?.code;
154
+ const msg = error instanceof Error ? error.message : String(error);
155
+ if (code === 'EBUSY_SESSION_LOCK') {
156
+ process.stderr.write(`[pugi] ${msg} Continuing without local session persistence.\n`);
157
+ }
158
+ else {
159
+ process.stderr.write(`[pugi] Local session store unavailable (${msg}). Continuing in memory-only mode.\n`);
160
+ }
161
+ return { store: null, openedSessionId: undefined };
162
+ }
163
+ }
164
+ /**
165
+ * Bootstrap the α6.5 three-tier context primitives:
166
+ *
167
+ * - Tier 0: `RepoSkeleton` (~5KB ASCII tree + meta) for prompt injection.
168
+ * - Tier 1: `WorkingSet` LRU bounded at 50 entries.
169
+ * - Filewatch: chokidar started against cwd, ignore-filtered.
170
+ *
171
+ * The bootstrap is fail-safe: every primitive is wrapped so the REPL
172
+ * still launches when (e.g.) chokidar refuses to start on a
173
+ * permission-blocked dir. The PUGI_DISABLE_CONTEXT=1 env var skips
174
+ * the bootstrap entirely for hermetic test runs and for operators
175
+ * who want a zero-touch REPL.
176
+ */
177
+ async function bootstrapContext(input) {
178
+ if (input.env.PUGI_DISABLE_CONTEXT === '1') {
179
+ return { skeleton: null, workingSet: null, watcher: null };
180
+ }
181
+ let ignore;
182
+ try {
183
+ ignore = loadPugiIgnore(input.cwd);
184
+ }
185
+ catch (error) {
186
+ const msg = error instanceof Error ? error.message : String(error);
187
+ process.stderr.write(`[pugi] Three-tier context bootstrap skipped (ignore matcher failed: ${msg}).\n`);
188
+ return { skeleton: null, workingSet: null, watcher: null };
189
+ }
190
+ let skeleton = null;
191
+ try {
192
+ skeleton = buildRepoSkeleton(input.cwd, { ignore });
193
+ }
194
+ catch (error) {
195
+ const msg = error instanceof Error ? error.message : String(error);
196
+ process.stderr.write(`[pugi] Repo skeleton bootstrap failed (${msg}). Continuing without Tier 0.\n`);
197
+ }
198
+ const workingSet = new WorkingSet();
199
+ let watcher = null;
200
+ // chokidar opt-out: PUGI_DISABLE_FILEWATCH=1 keeps Tier 0/1 wired
201
+ // but skips the live-update channel. Useful on CI runners and on
202
+ // network mounts where fsevents misbehaves.
203
+ if (input.env.PUGI_DISABLE_FILEWATCH !== '1') {
204
+ try {
205
+ const w = new PugiWatcher({ cwd: input.cwd, ignore });
206
+ await w.start();
207
+ watcher = w;
208
+ }
209
+ catch (error) {
210
+ const msg = error instanceof Error ? error.message : String(error);
211
+ process.stderr.write(`[pugi] Filewatch bootstrap failed (${msg}). Continuing without live updates.\n`);
212
+ }
46
213
  }
214
+ return { skeleton, workingSet, watcher };
47
215
  }
48
216
  /* ------------------------------------------------------------------ */
49
217
  /* Production transport */
50
218
  /* ------------------------------------------------------------------ */
51
219
  function createProductionTransport() {
52
220
  return {
53
- async createSession({ apiUrl, apiKey }) {
221
+ async createSession({ apiUrl, apiKey, workspace }) {
222
+ // Forward the workspace bundle in the POST body so admin-api can
223
+ // surface `<workspace-context>` in Mira's prompt. Older admin-api
224
+ // builds ignore unknown fields, so this stays forward-compatible.
225
+ // Wave 4 fix 2026-05-25.
226
+ const body = {};
227
+ if (workspace?.workspaceCwd)
228
+ body.workspaceCwd = workspace.workspaceCwd;
229
+ if (workspace?.workspaceSlug)
230
+ body.workspaceSlug = workspace.workspaceSlug;
231
+ if (workspace?.workspaceSummary)
232
+ body.workspaceSummary = workspace.workspaceSummary;
54
233
  const response = await fetch(joinUrl(apiUrl, '/api/pugi/sessions'), {
55
234
  method: 'POST',
56
235
  headers: jsonHeaders(apiKey),
57
- body: JSON.stringify({}),
236
+ body: JSON.stringify(body),
58
237
  });
59
238
  const json = await readJson(response);
60
239
  const sessionId = json.sessionId;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * ASCII pug mascot for the REPL boot splash (α6.14 wave 3).
3
+ *
4
+ * Hand-crafted at 9 rows × 20 columns to read as a pug at a single
5
+ * glance — references the cyber-zoo hero glyph in
6
+ * `apps/clawhost-web/public/brand/hero-pug.png`: blocky pug face with
7
+ * angular ear flaps on either side of the head, forehead crease,
8
+ * angular cyan eyes (`◉`), smushed snout, undershot jaw, and a small
9
+ * cyan circuit chip (`▐■▌`) on the lower-right cheek.
10
+ *
11
+ * Separation of art + cyan mask lets the unit test assert structure
12
+ * (row count, line widths, mask shape, at-least-one cyan pixel per
13
+ * eye row) without coupling to the Ink renderer. The renderer in
14
+ * `repl-splash.tsx` splits each row into runs and colors the masked
15
+ * columns cyan (#3DA9FC, brandbook §05).
16
+ *
17
+ * Convention:
18
+ * - PUG_MASCOT[i] = one row of the silhouette
19
+ * - PUG_MASCOT_CYAN_MASK[i] = parallel boolean array, true => that
20
+ * column renders cyan instead of gray
21
+ *
22
+ * Both arrays MUST stay the same length and each mask row MUST be the
23
+ * same length as the corresponding art row. A unit test enforces this.
24
+ */
25
+ /* eslint-disable no-irregular-whitespace */
26
+ export const PUG_MASCOT = [
27
+ ' ▄▀▀▀▄▄▄▀▀▀▄ ',
28
+ ' █▄▄ ▄▄█ ',
29
+ ' █ ▀▄▄▄▄▄▀ █ ',
30
+ ' █ ◉ ◉ █ ',
31
+ ' ▀▄ ▀█▀ ▄▀ ',
32
+ ' █▀▀▀▀▀█ ',
33
+ ' █▒▒▒▒▒█ ▐■▌ ',
34
+ ' ▀▄▄▄▀ ',
35
+ ' ▀ ',
36
+ ];
37
+ /**
38
+ * Cyan accents are derived from the source characters so the art file
39
+ * stays the single source of truth. Two glyph classes get colored:
40
+ * - `◉` -> the two cyan eyes on row 3
41
+ * - `▐■▌` -> the cyan chip cluster on row 6 (right cheek)
42
+ *
43
+ * Everything else renders gray. The derivation runs at module load,
44
+ * which keeps the mask trivially auditable from the source array.
45
+ */
46
+ export const PUG_MASCOT_CYAN_MASK = PUG_MASCOT.map((row) => {
47
+ const mask = new Array(row.length).fill(false);
48
+ for (let column = 0; column < row.length; column += 1) {
49
+ const ch = row.charAt(column);
50
+ if (ch === '◉' || ch === '▐' || ch === '■' || ch === '▌') {
51
+ mask[column] = true;
52
+ }
53
+ }
54
+ return mask;
55
+ });
56
+ /**
57
+ * Pre-computed silhouette dimensions for layout math in the splash
58
+ * component. The unit test asserts these stay inside the documented
59
+ * envelope (≤22 chars wide, 9 ≤ rows ≤ 14) so a future edit can not
60
+ * silently bloat the terminal real estate.
61
+ */
62
+ export const PUG_MASCOT_MAX_WIDTH = PUG_MASCOT.reduce((max, row) => Math.max(max, row.length), 0);
63
+ export const PUG_MASCOT_HEIGHT = PUG_MASCOT.length;
64
+ //# sourceMappingURL=repl-splash-art.js.map
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Chafa-validated brand-pug ANSI loader (α6.14.4 wave 6, mascot regen).
3
+ *
4
+ * CEO dogfood 2026-05-25 (first pass, α6.14.2 wave 5): the hand-crafted
5
+ * 9-row ASCII pug in `repl-splash-art.ts` reads as "точно не похожа" —
6
+ * too abstract to carry the brand at boot. This module loads a pre-baked
7
+ * truecolor ANSI render of the canonical hero-pug PNG (cyber-zoo pug
8
+ * face with cyan eyes + circuit + chip) so the splash matches the brand
9
+ * glyph the operator already sees on pugi.io.
10
+ *
11
+ * CEO dogfood 2026-05-25 (α6.14.4): the first chafa bake at 32x16 still
12
+ * read as "monitor on stand", not pug — too few rows to resolve the
13
+ * snout / eyes / wrinkles. The vertical resolution was the bottleneck:
14
+ * 16 char rows ≈ 16 pixel rows with the block symbol set. The fresh
15
+ * bake uses `vhalf` (vertical half blocks ▀ / ▄ with independent fg+bg
16
+ * colours per cell) which doubles the vertical resolution per character
17
+ * cell, at an 80x40 frame which is 2.5× the prior dimensions. End
18
+ * result: ~80×80 effective pixel resolution — enough to read the
19
+ * snout, eye sockets, ear lines, and the circuit board accent the
20
+ * brand glyph carries. File grew from 8.8KB to ~40KB; ship budget
21
+ * gates at 100KB so we stay well under cap.
22
+ *
23
+ * Generation (operator-side, one-shot):
24
+ * chafa --size 80x40 --symbols=vhalf --colors=full \
25
+ * apps/clawhost-web/public/brand/hero-pug.png \
26
+ * > apps/pugi-cli/assets/pugi-mascot.ansi
27
+ *
28
+ * The output is committed verbatim to the repo and shipped inside the
29
+ * `@pugi/cli` npm tarball under `assets/pugi-mascot.ansi` (the
30
+ * `package.json` `files` allowlist explicitly opts in). Runtime does
31
+ * NOT need `chafa` installed — we just read the file bytes and write
32
+ * them to stdout. If the file is missing (degraded install, tarball
33
+ * corruption, dev cwd drift), the splash falls back to the hand-crafted
34
+ * `PUG_MASCOT` art so the boot never crashes.
35
+ *
36
+ * The pre-Ink write convention mirrors the Claude Code Chrome plugin
37
+ * splash pattern: raw bytes go to `process.stdout` BEFORE the Ink
38
+ * render mount, so the terminal interprets the truecolor escapes
39
+ * directly instead of Ink trying to layout-engine over them.
40
+ */
41
+ import { existsSync, readFileSync } from 'node:fs';
42
+ import { dirname, resolve as resolvePath } from 'node:path';
43
+ import { fileURLToPath } from 'node:url';
44
+ /**
45
+ * Resolve the on-disk path to `pugi-mascot.ansi` relative to the
46
+ * compiled module. The CLI ships to `node_modules/@pugi/cli/dist/tui/`
47
+ * so the asset lives at `node_modules/@pugi/cli/assets/pugi-mascot.ansi`
48
+ * — two directory hops up from this file. In a local `pnpm dev`
49
+ * checkout the structure is the same (`src/tui/` ⇒ `../../assets/`)
50
+ * because tsx re-resolves the same relative tree.
51
+ */
52
+ export function pugMascotAssetPath() {
53
+ const here = dirname(fileURLToPath(import.meta.url));
54
+ return resolvePath(here, '..', '..', 'assets', 'pugi-mascot.ansi');
55
+ }
56
+ /**
57
+ * Read the chafa-baked ANSI render of the brand pug. Returns the raw
58
+ * bytes verbatim (UTF-8 string) — the terminal interprets the truecolor
59
+ * escapes directly. Returns null when the file is missing, unreadable,
60
+ * or trivially empty so the caller can fall back to `PUG_MASCOT`.
61
+ *
62
+ * `chafa --colors=full` wraps the render with cursor-hide (`\e[?25l`)
63
+ * on the head and cursor-show (`\e[?25h`) on the tail. We strip those
64
+ * so the splash does not accidentally hide the cursor across the rest
65
+ * of the REPL boot (Ink itself manages the cursor once it mounts).
66
+ *
67
+ * The asset is supply-chain controlled (committed in-repo, shipped in
68
+ * the npm tarball) so an arbitrary attacker cannot inject escapes
69
+ * today. The defence-in-depth strip below still drops categories of
70
+ * escapes that the splash has no legitimate need to emit — OSC window
71
+ * title sets, mouse-tracking enables, screen clears, cursor-position
72
+ * reports — so a future swap of the asset (or a corrupt tarball) cannot
73
+ * disrupt the terminal beyond the splash region. Truecolor (`CSI 38;2;
74
+ * R;G;B m`), reset (`CSI 0 m`), and explicit forms of cursor / line
75
+ * motion the render needs are left in.
76
+ */
77
+ export function loadPugMascotAnsi() {
78
+ const path = pugMascotAssetPath();
79
+ try {
80
+ if (!existsSync(path))
81
+ return null;
82
+ const raw = readFileSync(path, 'utf8');
83
+ if (!raw || raw.length === 0)
84
+ return null;
85
+ // 1. Drop OSC sequences. Two terminator forms:
86
+ // ESC ] ... BEL (0x1b 0x5d ... 0x07)
87
+ // ESC ] ... ESC \ (0x1b 0x5d ... 0x1b 0x5c, the ST form)
88
+ // A truecolor splash never needs OSC (those are for window title,
89
+ // icon, clipboard, hyperlinks, color-palette change). Drop them
90
+ // so a corrupted asset cannot rename the operator's terminal tab
91
+ // 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.
96
+ // 3. Drop CSI 6 n (cursor-position report). Would inject a fake
97
+ // CPR into the operator's stdin stream.
98
+ // 4. Drop CSI [23]J / CSI [23]K (full screen / line clear). A
99
+ // chafa render uses cursor-positioning per row, not bulk
100
+ // erases; bulk clears would wipe whatever the operator already
101
+ // 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
+ const stripped = raw
105
+ .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, '')
107
+ .replace(/\x1b\[6n/g, '')
108
+ .replace(/\x1b\[[23]?[JK]/g, '');
109
+ if (stripped.trim().length === 0)
110
+ return null;
111
+ return stripped;
112
+ }
113
+ catch {
114
+ // Best-effort: any FS / decode error returns null so the splash
115
+ // falls back to the hand-crafted ASCII art. Never throws.
116
+ return null;
117
+ }
118
+ }
119
+ export function printPugMascotPreInk(sink) {
120
+ const ansi = loadPugMascotAnsi();
121
+ if (ansi === null)
122
+ return false;
123
+ // Trailing newline so the Ink header lands on a fresh row rather
124
+ // than smashing into the last pug row.
125
+ sink.write(ansi);
126
+ if (!ansi.endsWith('\n'))
127
+ sink.write('\n');
128
+ return true;
129
+ }
130
+ //# sourceMappingURL=repl-splash-mascot.js.map
@@ -0,0 +1,117 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * REPL boot splash (α6.14 wave 3).
4
+ *
5
+ * Rendered on REPL first paint — before the conversation pane, before
6
+ * any operator input lands. Mirrors the Claude Code / Codex / Gemini
7
+ * CLI boot-screen aesthetic while staying Pugi-brand-pure:
8
+ *
9
+ * [PUG ASCII] Pugi.io v0.1.0-alphaN
10
+ * Plan: <plan>
11
+ * Model: <model>
12
+ * Tenant: <customerId>
13
+ * Workspace: <basename>
14
+ *
15
+ * ─────────────────────────────────────
16
+ * Tips for getting started:
17
+ * 1. Type a brief, the workforce dispatches
18
+ * 2. /help for slash commands, /web <url> to pull a page
19
+ * 3. /skills install <name> for Anthropic / OpenClaw skills
20
+ *
21
+ * The splash auto-dismisses on:
22
+ * - first operator keystroke (the REPL `<Repl />` host owns this and
23
+ * calls the `onInteract` callback we expose),
24
+ * - 10s idle timeout (built-in, configurable via `skipSplash`),
25
+ * - `--no-splash` CLI flag or PUGI_SKIP_SPLASH=1 env (host gates the
26
+ * mount entirely; we still respect the `skipSplash` prop as a belt
27
+ * so a stray render in a test environment produces nothing).
28
+ *
29
+ * Brand voice gate: every visible string here is reviewed against the
30
+ * forbidden list (`journey / explore / delight / magical / friendly /
31
+ * AI-powered / pug-tastic`). Power words used: `brief / dispatch /
32
+ * ship / workforce / sentinel / skills`. No em-dashes; box-drawing
33
+ * `─` is OK (matches existing REPL header conventions).
34
+ */
35
+ import { useEffect } from 'react';
36
+ import { Box, Text } from 'ink';
37
+ import { PUG_MASCOT, PUG_MASCOT_CYAN_MASK, PUG_MASCOT_MAX_WIDTH, } from './repl-splash-art.js';
38
+ const DEFAULT_AUTO_DISMISS_MS = 10_000;
39
+ const PLACEHOLDER = '—';
40
+ export function ReplSplash(props) {
41
+ // Hooks MUST run unconditionally so the React reconciler can keep
42
+ // its hook order. We branch on `skipSplash` AFTER the effect
43
+ // declaration; the effect itself bails early when the splash is
44
+ // suppressed so no stray timer fires in the skip path.
45
+ useEffect(() => {
46
+ if (props.skipSplash)
47
+ return undefined;
48
+ const ms = props.autoDismissMs ?? DEFAULT_AUTO_DISMISS_MS;
49
+ const handle = setTimeout(() => {
50
+ props.onDismiss?.();
51
+ }, ms);
52
+ return () => clearTimeout(handle);
53
+ // Dependency on the onDismiss callback would re-arm the timer on
54
+ // every parent rerender; the host wraps it in useCallback so
55
+ // identity is stable for the splash's lifetime.
56
+ }, [props.autoDismissMs, props.onDismiss, props.skipSplash]);
57
+ // Belt for stray test renders: when the host already knows the
58
+ // operator opted out, we still want a render call to produce nothing
59
+ // visible. The host is the source of truth for mount-or-not; this is
60
+ // the no-op fallback.
61
+ if (props.skipSplash) {
62
+ return null;
63
+ }
64
+ // α6.14.2 wave 5: when the host pre-printed the chafa-baked brand-pug
65
+ // ANSI render to stdout before Ink mounted, suppress the hand-crafted
66
+ // PUG_MASCOT column here so the operator does not see two stacked
67
+ // pugs. The header card still renders inline so wordmark + status
68
+ // rows stay attached to the splash flow.
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" })] })] }));
71
+ }
72
+ /**
73
+ * Renders the multi-line ASCII pug. Each row is split into colored
74
+ * runs based on `PUG_MASCOT_CYAN_MASK` so the eyes + chip come out
75
+ * cyan and the body stays gray. Pure render of the static art array;
76
+ * no IO, no state.
77
+ */
78
+ function MascotColumn() {
79
+ return (_jsx(Box, { flexDirection: "column", minWidth: PUG_MASCOT_MAX_WIDTH, children: PUG_MASCOT.map((row, rowIndex) => (_jsx(MascotRow, { row: row, mask: PUG_MASCOT_CYAN_MASK[rowIndex] ?? [] }, rowIndex))) }));
80
+ }
81
+ function MascotRow({ row, mask, }) {
82
+ // Split the row into contiguous runs of same-color cells so we emit
83
+ // one <Text> per run instead of one per character. Keeps the Ink
84
+ // render tree shallow and the snapshot diff readable.
85
+ const runs = [];
86
+ let buffer = '';
87
+ let bufferCyan = false;
88
+ for (let column = 0; column < row.length; column += 1) {
89
+ const ch = row.charAt(column);
90
+ const cyan = mask[column] === true;
91
+ if (buffer.length === 0) {
92
+ buffer = ch;
93
+ bufferCyan = cyan;
94
+ continue;
95
+ }
96
+ if (cyan === bufferCyan) {
97
+ buffer += ch;
98
+ }
99
+ else {
100
+ runs.push({ text: buffer, cyan: bufferCyan });
101
+ buffer = ch;
102
+ bufferCyan = cyan;
103
+ }
104
+ }
105
+ if (buffer.length > 0) {
106
+ runs.push({ text: buffer, cyan: bufferCyan });
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))) }));
109
+ }
110
+ function HeaderRow({ label, value }) {
111
+ const padded = `${label}:`.padEnd(11, ' ');
112
+ return (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: padded }), _jsx(Text, { children: value })] }));
113
+ }
114
+ function TipRow({ index, text }) {
115
+ return (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: ` ${index}. ` }), _jsx(Text, { children: text })] }));
116
+ }
117
+ //# sourceMappingURL=repl-splash.js.map