@pugi/cli 0.1.0-beta.21 → 0.1.0-beta.23

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 (63) hide show
  1. package/dist/core/auth/env-provider.js +238 -0
  2. package/dist/core/bare-mode/index.js +107 -0
  3. package/dist/core/diagnostics/probes/bare-mode.js +42 -0
  4. package/dist/core/diagnostics/probes/pugi-md.js +89 -0
  5. package/dist/core/engine/native-pugi.js +55 -11
  6. package/dist/core/engine/prompts.js +30 -2
  7. package/dist/core/engine/tool-bridge.js +32 -0
  8. package/dist/core/feedback/queue.js +177 -0
  9. package/dist/core/feedback/submitter.js +145 -0
  10. package/dist/core/onboarding/marker.js +111 -0
  11. package/dist/core/onboarding/telemetry-state.js +108 -0
  12. package/dist/core/output-style/presets.js +176 -0
  13. package/dist/core/output-style/state.js +185 -0
  14. package/dist/core/permissions/index.js +1 -1
  15. package/dist/core/permissions/state.js +55 -0
  16. package/dist/core/pugi-md/context-injector.js +76 -0
  17. package/dist/core/pugi-md/walk-up.js +207 -0
  18. package/dist/core/release-notes/parser.js +241 -0
  19. package/dist/core/release-notes/state.js +116 -0
  20. package/dist/core/repl/session.js +482 -12
  21. package/dist/core/repl/slash-commands.js +134 -1
  22. package/dist/core/repl/workspace-context.js +22 -0
  23. package/dist/core/share/formatter.js +271 -0
  24. package/dist/core/share/redactor.js +221 -0
  25. package/dist/core/share/uploader.js +267 -0
  26. package/dist/core/theme/context.js +91 -0
  27. package/dist/core/theme/presets.js +228 -0
  28. package/dist/core/theme/state.js +181 -0
  29. package/dist/core/todos/invariant.js +10 -0
  30. package/dist/core/todos/state.js +177 -0
  31. package/dist/core/vim/keymap.js +288 -0
  32. package/dist/core/vim/state.js +92 -0
  33. package/dist/runtime/cli.js +603 -15
  34. package/dist/runtime/commands/doctor.js +21 -0
  35. package/dist/runtime/commands/feedback.js +184 -0
  36. package/dist/runtime/commands/onboarding.js +275 -0
  37. package/dist/runtime/commands/plan.js +143 -0
  38. package/dist/runtime/commands/release-notes.js +229 -0
  39. package/dist/runtime/commands/share.js +316 -0
  40. package/dist/runtime/commands/stickers.js +82 -0
  41. package/dist/runtime/commands/style.js +194 -0
  42. package/dist/runtime/commands/theme.js +196 -0
  43. package/dist/runtime/commands/vim.js +140 -0
  44. package/dist/runtime/version.js +1 -1
  45. package/dist/tools/registry.js +8 -0
  46. package/dist/tools/todo-write.js +184 -0
  47. package/dist/tui/compact-banner.js +28 -1
  48. package/dist/tui/conversation-pane.js +13 -0
  49. package/dist/tui/doctor-table.js +32 -17
  50. package/dist/tui/feedback-prompt.js +156 -0
  51. package/dist/tui/onboarding-wizard.js +240 -0
  52. package/dist/tui/repl-render.js +26 -3
  53. package/dist/tui/repl.js +9 -1
  54. package/dist/tui/stickers-art.js +136 -0
  55. package/dist/tui/style-table.js +28 -0
  56. package/dist/tui/theme-table.js +29 -0
  57. package/dist/tui/vim-input.js +267 -0
  58. package/package.json +2 -2
  59. package/dist/core/engine/compaction-hook.js +0 -154
  60. package/dist/core/init/scaffold.js +0 -195
  61. package/dist/core/repl/codebase-survey.js +0 -308
  62. package/dist/core/repl/init-interview.js +0 -457
  63. package/dist/core/repl/onboarding-state.js +0 -297
@@ -0,0 +1,240 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Leak L25 (2026-05-27) — Onboarding Ink wizard.
4
+ *
5
+ * Six-screen interactive walk:
6
+ *
7
+ * 1. Welcome + auth status (single Enter to continue)
8
+ * 2. Default permission mode (4-row picker)
9
+ * 3. Output style preset (5-row picker)
10
+ * 4. MCP server pointer (informational, Enter to continue)
11
+ * 5. Telemetry consent (3-row picker)
12
+ * 6. Recap card (Enter to commit + exit)
13
+ *
14
+ * Driven entirely by Ink's `useInput`. The component does NOT perform
15
+ * any fs writes — it resolves the verdict back to the caller
16
+ * (`runOnboardingCommand`), which translates verdicts into L6 / L18 /
17
+ * telemetry-state mutations. Single source of truth: the runner.
18
+ *
19
+ * Each picker step pre-selects the CURRENT persisted value (from the
20
+ * snapshot passed in props) so pressing Enter on Step 2/3/5 keeps the
21
+ * current value — that is the idempotency contract.
22
+ *
23
+ * Cancellation:
24
+ * - Esc / Ctrl-C at any step → verdict.cancelled = true, the runner
25
+ * skips ALL writes (including the marker touch) so the next bare
26
+ * `pugi` invocation still surfaces the first-run hint.
27
+ *
28
+ * Keystrokes:
29
+ * - ↑/↓ or j/k — move selection in pickers.
30
+ * - Enter — confirm step (keep current = pass through; new pick
31
+ * = update verdict for that tier).
32
+ * - 's' — skip current step explicitly (verdict slot stays null).
33
+ * - Esc / 'q' — cancel the wizard.
34
+ */
35
+ import { useState } from 'react';
36
+ import { Box, Text, render, useApp, useInput } from 'ink';
37
+ import { PERMISSION_MODES, PERMISSION_MODE_GLOSS, } from '../core/permissions/index.js';
38
+ import { OUTPUT_STYLES, OUTPUT_STYLE_SLUGS, } from '../core/output-style/presets.js';
39
+ import { TELEMETRY_CHOICES, } from '../core/onboarding/telemetry-state.js';
40
+ const EMPTY_DRAFT = {
41
+ permissionMode: null,
42
+ outputStyle: null,
43
+ telemetry: null,
44
+ };
45
+ /**
46
+ * Lookup the snapshot value's index within the picker rows so the
47
+ * initial cursor sits on the current value. Returns 0 (safe default)
48
+ * when the snapshot value is outside the closed list — should never
49
+ * happen given the type guards in the state modules, but the index
50
+ * fallback keeps Ink from crashing on a malformed config.
51
+ */
52
+ function indexOf(rows, value) {
53
+ const idx = rows.indexOf(value);
54
+ return idx === -1 ? 0 : idx;
55
+ }
56
+ /**
57
+ * The wizard component. Pure: no fs, no env, no network. Verdicts
58
+ * flow up via `onComplete`; the caller owns the writes.
59
+ */
60
+ export function OnboardingWizard(props) {
61
+ const { snapshot, onComplete } = props;
62
+ const [step, setStep] = useState(1);
63
+ const [permissionIdx, setPermissionIdx] = useState(indexOf(PERMISSION_MODES, snapshot.permissionMode));
64
+ const [styleIdx, setStyleIdx] = useState(indexOf(OUTPUT_STYLE_SLUGS, snapshot.outputStyle));
65
+ const [telemetryIdx, setTelemetryIdx] = useState(indexOf(TELEMETRY_CHOICES, snapshot.telemetry));
66
+ const [draft, setDraft] = useState(EMPTY_DRAFT);
67
+ const finish = (final, cancelled) => {
68
+ onComplete({
69
+ permissionMode: final.permissionMode,
70
+ outputStyle: final.outputStyle,
71
+ telemetry: final.telemetry,
72
+ cancelled,
73
+ });
74
+ };
75
+ useInput((input, key) => {
76
+ // Universal cancel.
77
+ if (key.escape || (key.ctrl && input === 'c')) {
78
+ finish(EMPTY_DRAFT, true);
79
+ return;
80
+ }
81
+ // Universal skip — Enter on a picker means "keep current"; explicit
82
+ // 's' makes the skip intent obvious in the recap.
83
+ const isAdvance = key.return;
84
+ const moveUp = key.upArrow || input === 'k';
85
+ const moveDown = key.downArrow || input === 'j';
86
+ const explicitSkip = input === 's';
87
+ switch (step) {
88
+ case 1: {
89
+ if (isAdvance)
90
+ setStep(2);
91
+ return;
92
+ }
93
+ case 2: {
94
+ if (moveUp) {
95
+ setPermissionIdx((i) => (i === 0 ? PERMISSION_MODES.length - 1 : i - 1));
96
+ return;
97
+ }
98
+ if (moveDown) {
99
+ setPermissionIdx((i) => (i === PERMISSION_MODES.length - 1 ? 0 : i + 1));
100
+ return;
101
+ }
102
+ if (isAdvance || explicitSkip) {
103
+ const picked = PERMISSION_MODES[permissionIdx];
104
+ const verdict = explicitSkip || picked === snapshot.permissionMode ? null : picked ?? null;
105
+ setDraft((d) => ({ ...d, permissionMode: verdict }));
106
+ setStep(3);
107
+ return;
108
+ }
109
+ return;
110
+ }
111
+ case 3: {
112
+ if (moveUp) {
113
+ setStyleIdx((i) => (i === 0 ? OUTPUT_STYLE_SLUGS.length - 1 : i - 1));
114
+ return;
115
+ }
116
+ if (moveDown) {
117
+ setStyleIdx((i) => (i === OUTPUT_STYLE_SLUGS.length - 1 ? 0 : i + 1));
118
+ return;
119
+ }
120
+ if (isAdvance || explicitSkip) {
121
+ const picked = OUTPUT_STYLE_SLUGS[styleIdx];
122
+ const verdict = explicitSkip || picked === snapshot.outputStyle ? null : picked ?? null;
123
+ setDraft((d) => ({ ...d, outputStyle: verdict }));
124
+ setStep(4);
125
+ return;
126
+ }
127
+ return;
128
+ }
129
+ case 4: {
130
+ if (isAdvance)
131
+ setStep(5);
132
+ return;
133
+ }
134
+ case 5: {
135
+ if (moveUp) {
136
+ setTelemetryIdx((i) => (i === 0 ? TELEMETRY_CHOICES.length - 1 : i - 1));
137
+ return;
138
+ }
139
+ if (moveDown) {
140
+ setTelemetryIdx((i) => (i === TELEMETRY_CHOICES.length - 1 ? 0 : i + 1));
141
+ return;
142
+ }
143
+ if (isAdvance || explicitSkip) {
144
+ const picked = TELEMETRY_CHOICES[telemetryIdx];
145
+ const verdict = explicitSkip || picked === snapshot.telemetry ? null : picked ?? null;
146
+ setDraft((d) => ({ ...d, telemetry: verdict }));
147
+ setStep(6);
148
+ return;
149
+ }
150
+ return;
151
+ }
152
+ case 6: {
153
+ if (isAdvance)
154
+ finish(draft, false);
155
+ return;
156
+ }
157
+ }
158
+ });
159
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsx(StepHeader, { step: step }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [step === 1 && _jsx(WelcomeStep, { snapshot: snapshot }), step === 2 && (_jsx(ModeStep, { current: snapshot.permissionMode, selectedIdx: permissionIdx })), step === 3 && (_jsx(StyleStep, { current: snapshot.outputStyle, currentSource: snapshot.outputStyleSource, selectedIdx: styleIdx })), step === 4 && _jsx(McpStep, {}), step === 5 && (_jsx(TelemetryStep, { current: snapshot.telemetry, selectedIdx: telemetryIdx })), step === 6 && _jsx(RecapStep, { snapshot: snapshot, draft: draft })] }), _jsx(FooterHints, { step: step })] }));
160
+ }
161
+ function StepHeader({ step }) {
162
+ const titles = {
163
+ 1: 'Welcome to Pugi',
164
+ 2: 'Step 2 / 5 — Default permission mode',
165
+ 3: 'Step 3 / 5 — Output style',
166
+ 4: 'Step 4 / 5 — MCP servers',
167
+ 5: 'Step 5 / 5 — Telemetry consent',
168
+ 6: 'Setup complete',
169
+ };
170
+ return (_jsx(Text, { bold: true, color: "cyan", children: titles[step] }));
171
+ }
172
+ function WelcomeStep({ snapshot }) {
173
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Brief it. It ships." }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: snapshot.authPresent
174
+ ? 'You are signed in. The wizard configures local defaults; values persist to ~/.pugi/config.json.'
175
+ : 'You are NOT signed in. The wizard still configures local defaults, but you should run `pugi login` after.' }) })] }));
176
+ }
177
+ function ModeStep({ current, selectedIdx, }) {
178
+ return (_jsx(Box, { flexDirection: "column", children: PERMISSION_MODES.map((mode, idx) => (_jsx(PickerRow, { isSelected: idx === selectedIdx, isCurrent: mode === current, title: mode, gloss: PERMISSION_MODE_GLOSS[mode] }, mode))) }));
179
+ }
180
+ function StyleStep({ current, currentSource, selectedIdx, }) {
181
+ return (_jsxs(Box, { flexDirection: "column", children: [OUTPUT_STYLE_SLUGS.map((slug, idx) => (_jsx(PickerRow, { isSelected: idx === selectedIdx, isCurrent: slug === current, title: slug, gloss: OUTPUT_STYLES[slug].gloss }, slug))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: currentSource === 'workspace'
182
+ ? 'Active style is currently a workspace override. The wizard writes the user-tier default.'
183
+ : `Active source: ${currentSource}.` }) })] }));
184
+ }
185
+ function McpStep() {
186
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "MCP servers extend Pugi with extra tools (filesystem, browser, custom APIs)." }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Add one with:" }) }), _jsx(Text, { children: ' pugi mcp add <name> <command>' }), _jsx(Text, { children: ' pugi mcp list' }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "You can skip for now and add servers later." }) })] }));
187
+ }
188
+ function TelemetryStep({ current, selectedIdx, }) {
189
+ const gloss = {
190
+ off: 'No telemetry of any kind.',
191
+ anonymous: 'Counts + error categories only; no payloads.',
192
+ community: 'Anonymous + opt-in usage panels.',
193
+ };
194
+ return (_jsx(Box, { flexDirection: "column", children: TELEMETRY_CHOICES.map((choice, idx) => (_jsx(PickerRow, { isSelected: idx === selectedIdx, isCurrent: choice === current, title: choice, gloss: gloss[choice] }, choice))) }));
195
+ }
196
+ function RecapStep({ snapshot, draft, }) {
197
+ const finalMode = draft.permissionMode ?? snapshot.permissionMode;
198
+ const finalStyle = draft.outputStyle ?? snapshot.outputStyle;
199
+ const finalTelemetry = draft.telemetry ?? snapshot.telemetry;
200
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: ` Permission mode: ${finalMode}${draft.permissionMode === null ? ' (unchanged)' : ''}` }), _jsx(Text, { children: ` Output style: ${finalStyle}${draft.outputStyle === null ? ' (unchanged)' : ''}` }), _jsx(Text, { children: ` Telemetry: ${finalTelemetry}${draft.telemetry === null ? ' (unchanged)' : ''}` }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter to write defaults + exit. Esc to cancel without saving." }) })] }));
201
+ }
202
+ function PickerRow({ isSelected, isCurrent, title, gloss, }) {
203
+ const indicator = isSelected ? '▸ ' : ' ';
204
+ const currentTag = isCurrent ? ' [current]' : '';
205
+ return (_jsxs(Text, { children: [_jsxs(Text, { color: isSelected ? 'cyan' : undefined, bold: isSelected, children: [indicator, title.padEnd(18, ' ')] }), _jsx(Text, { dimColor: true, children: `${gloss}${currentTag}` })] }));
206
+ }
207
+ function FooterHints({ step }) {
208
+ const hint = step === 1 || step === 4
209
+ ? 'Enter continue Esc cancel'
210
+ : step === 6
211
+ ? 'Enter commit + exit Esc cancel'
212
+ : '↑/↓ select Enter confirm s skip Esc cancel';
213
+ return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: hint }) }));
214
+ }
215
+ /**
216
+ * Mount the wizard, await the operator's verdict, unmount Ink, return
217
+ * the verdict to the runner. Wrapped in a `useApp` consumer so we can
218
+ * call `exit()` and let `waitUntilExit()` resolve cleanly.
219
+ */
220
+ export async function renderOnboardingWizard(opts) {
221
+ return new Promise((resolvePromise) => {
222
+ let resolved = false;
223
+ const handleComplete = (verdict) => {
224
+ if (resolved)
225
+ return;
226
+ resolved = true;
227
+ app.unmount();
228
+ resolvePromise(verdict);
229
+ };
230
+ const Wrapper = () => {
231
+ const { exit } = useApp();
232
+ return (_jsx(OnboardingWizard, { snapshot: opts.snapshot, onComplete: (verdict) => {
233
+ handleComplete(verdict);
234
+ exit();
235
+ } }));
236
+ };
237
+ const app = render(_jsx(Wrapper, {}));
238
+ });
239
+ }
240
+ //# sourceMappingURL=onboarding-wizard.js.map
@@ -22,6 +22,8 @@ import React from 'react';
22
22
  import { render } from 'ink';
23
23
  import { Repl } from './repl.js';
24
24
  import { printPugMascotPreInk } from './repl-splash-mascot.js';
25
+ import { ThemeProvider } from '../core/theme/context.js';
26
+ import { resolveTheme } from '../core/theme/state.js';
25
27
  import { ReplSession, } from '../core/repl/session.js';
26
28
  import { resolveWorkspaceContext } from '../core/repl/workspace-context.js';
27
29
  import { SqliteSessionStore } from '../core/repl/store/index.js';
@@ -70,7 +72,15 @@ export async function renderRepl(options) {
70
72
  // the same auto-init UX as a Node operator. Already-bound `.pugi/`
71
73
  // dirs also opt back in so the scaffold can fill any missing
72
74
  // sub-artifacts the operator deleted.
73
- if (process.env.PUGI_NO_AUTO_INIT !== '1' && isProjectRoot(process.cwd())) {
75
+ // Leak L22 (2026-05-27): `--bare` (PUGI_BARE=1) ALSO suppresses the
76
+ // auto-init scaffold. Bare mode is the deterministic "fresh install
77
+ // anywhere" path — no `.pugi/` writes, no PUGI.md scaffold, no
78
+ // settings.json seed. The pre-existing PUGI_NO_AUTO_INIT escape
79
+ // hatch stays — bare mode just unions with it.
80
+ const { isBareMode } = await import('../core/bare-mode/index.js');
81
+ if (process.env.PUGI_NO_AUTO_INIT !== '1' &&
82
+ !isBareMode() &&
83
+ isProjectRoot(process.cwd())) {
74
84
  try {
75
85
  const { scaffoldPugiWorkspace } = await import('../runtime/cli.js');
76
86
  await scaffoldPugiWorkspace({
@@ -173,13 +183,26 @@ export async function renderRepl(options) {
173
183
  // (operator opted out via --no-splash), we suppress the pre-print
174
184
  // too so the boot stays silent.
175
185
  const mascotPrePrinted = options.skipSplash === true ? false : printPugMascotPreInk(process.stdout);
176
- const instance = render(React.createElement(Repl, {
186
+ // Leak L30 (2026-05-27): resolve the active theme ONCE at mount
187
+ // and wrap `<Repl />` in `<ThemeProvider>` so every Ink consumer
188
+ // (`<Header>`, `<DoctorTable>`, `<StyleTable>`, `<ThemeTable>`,
189
+ // …) picks up the same color tokens. The provider is stable for
190
+ // the lifetime of the REPL — operator `/theme <name>` writes to
191
+ // disk + appends a system line, and the next `pugi` launch re-
192
+ // mounts with the new slug. Re-mounting mid-session would race
193
+ // against Ink's raw-mode handler so we deliberately keep the
194
+ // session-lifetime contract instead of polling the config file.
195
+ const resolvedTheme = resolveTheme({
196
+ workspaceRoot: process.cwd(),
197
+ env: process.env,
198
+ });
199
+ const instance = render(React.createElement(ThemeProvider, { slug: resolvedTheme.slug }, React.createElement(Repl, {
177
200
  session,
178
201
  updateBanner: options.updateBanner ?? null,
179
202
  skipSplash: options.skipSplash === true,
180
203
  hideToolStream: options.hideToolStream === true,
181
204
  mascotPrePrinted,
182
- }));
205
+ })));
183
206
  // Make sure we leave the alt screen on abrupt exits too. Without
184
207
  // this the operator's shell stays "frozen" on the Pugi splash.
185
208
  process.once('exit', bootstrap.restore);
package/dist/tui/repl.js CHANGED
@@ -29,6 +29,7 @@ import { StatusBar } from './status-bar.js';
29
29
  import { ToolStreamPane } from './tool-stream-pane.js';
30
30
  import { UpdateBanner } from './update-banner.js';
31
31
  import { collectWorkspaceContext } from './workspace-context.js';
32
+ import { useTheme } from '../core/theme/context.js';
32
33
  import { slugForCwd } from '../core/repl/history.js';
33
34
  import { SLASH_COMMAND_HELP, SLASH_COMMAND_GROUPS } from '../core/repl/slash-commands.js';
34
35
  const TICK_INTERVAL_MS = 200;
@@ -202,7 +203,14 @@ export function Repl(props) {
202
203
  sessionTokensIn: state.sessionTokensIn, sessionTokensOut: state.sessionTokensOut, sessionCostUsd: state.sessionCostUsd, sessionStartedAtEpochMs: state.sessionStartedAtEpochMs, lastTurnDelta: state.lastTurnDelta })] })] }));
203
204
  }
204
205
  function Header({ state }) {
205
- return (_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Pugi" }), _jsx(Text, { bold: true, color: "#3da9fc", children: ".io" }), _jsx(Text, { dimColor: true, children: ` · workspace: ${state.workspaceLabel} · v${state.cliVersion} · ` }), _jsx(Text, { color: "#3da9fc", children: state.connection === 'on_watch' ? 'on watch' : state.connection.replace('_', ' ') })] }));
206
+ // Leak L30 (2026-05-27): the header `.io` brand accent + connection
207
+ // pill route through `useTheme()` so the operator's `/theme` flip
208
+ // (default / dark / light / colorblind) re-tints the chrome on
209
+ // re-mount. The `useTheme` hook returns the `default` preset's
210
+ // colors when no provider is mounted, preserving the previous
211
+ // `#3da9fc` constants for tests that import `<Repl />` standalone.
212
+ const theme = useTheme();
213
+ return (_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Pugi" }), _jsx(Text, { bold: true, color: theme.accent, children: ".io" }), _jsx(Text, { dimColor: true, children: ` · workspace: ${state.workspaceLabel} · v${state.cliVersion} · ` }), _jsx(Text, { color: theme.accent, children: state.connection === 'on_watch' ? 'on watch' : state.connection.replace('_', ' ') })] }));
206
214
  }
207
215
  function MainArea({ state, personaNames, nowEpochMs, hideToolStream, toolStreamCollapsed, }) {
208
216
  // α6.12: three vertical panes stacked above the input box.
@@ -0,0 +1,136 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ /**
4
+ * Curated ASCII pug corpus. Five variants — wide enough that repeat
5
+ * invocations look fresh, narrow enough that every entry stays
6
+ * hand-vetted (no procedural slop). Each art block intentionally fits
7
+ * inside an 80-column terminal so the surrounding box border does not
8
+ * wrap on narrow shells.
9
+ *
10
+ * The trailing newline at the end of each `art` string is intentional —
11
+ * keeps the renderer's join logic uniform between the boxed and the
12
+ * `--ascii-only` paths.
13
+ */
14
+ export const PUG_STICKERS = Object.freeze([
15
+ {
16
+ id: 'classic-face',
17
+ caption: 'classic pug face',
18
+ art: [
19
+ ' _._ _,-\'""`-._',
20
+ ' (,-.`._,\'( |\\`-/|',
21
+ ' `-.-\' \\ )-`( , o o)',
22
+ ' `- \\`_`"\'-',
23
+ ].join('\n'),
24
+ },
25
+ {
26
+ id: 'sit-pose',
27
+ caption: 'sit, stay, ship',
28
+ art: [
29
+ ' /\\___/\\',
30
+ ' ( o o )',
31
+ ' ( =^= )',
32
+ ' (______)',
33
+ ].join('\n'),
34
+ },
35
+ {
36
+ id: 'peek',
37
+ caption: 'peek-a-pug',
38
+ art: [
39
+ ' __',
40
+ ' ___/ \\___',
41
+ ' / o o \\',
42
+ ' | > ^ < |',
43
+ ' \\__________/',
44
+ ].join('\n'),
45
+ },
46
+ {
47
+ id: 'sleepy',
48
+ caption: 'sleepy pug, no Zzz today',
49
+ art: [
50
+ ' .--.',
51
+ ' / - -\\',
52
+ ' ( ^ ^ )',
53
+ ' \\ ^^ /',
54
+ ' `----\'',
55
+ ].join('\n'),
56
+ },
57
+ {
58
+ id: 'shipping',
59
+ caption: 'shipping pug',
60
+ art: [
61
+ ' .---. .---.',
62
+ ' |o_o| |o_o|',
63
+ ' \\_^_/ \\_^_/',
64
+ ' /| |\\ /| |\\',
65
+ ' shipped • shipped',
66
+ ].join('\n'),
67
+ },
68
+ ]);
69
+ /**
70
+ * Curated rotating-quote pool. Brand voice gate (brandbook §08):
71
+ * `brief / dispatch / stop / agents / quit / shipped` are the power
72
+ * words; quotes lean on those and на the operator-mode register.
73
+ * Adding lines: keep each ≤ 64 chars so the boxed renderer never wraps,
74
+ * stay в the operator's voice, no AI attribution, no hype.
75
+ */
76
+ export const PUG_QUOTES = Object.freeze([
77
+ 'Pugi: your engineering co-pilot.',
78
+ 'Brief it. It ships.',
79
+ 'Built for operators, not for benchmarks.',
80
+ 'Pugi: твой инженерный напарник.',
81
+ 'Dispatch agents, not promises.',
82
+ 'Small CLI. Loud workforce.',
83
+ 'Engineering at the speed of brief.',
84
+ 'Pugi: shipping is the default mode.',
85
+ ]);
86
+ /**
87
+ * Clamp a raw rng draw to a safe array index. Handles every hostile
88
+ * shape the spec exercises:
89
+ * - rng returns NaN → fall back to 0
90
+ * - rng returns 1.0 → clamp to length-1 (Math.floor would land at n)
91
+ * - rng returns -ε → clamp to 0
92
+ * The caller hands в the corpus length; the helper never touches the
93
+ * corpus itself so it stays trivially testable.
94
+ */
95
+ function safeIndex(raw, length) {
96
+ if (!Number.isFinite(raw))
97
+ return 0;
98
+ const floored = Math.floor(raw);
99
+ if (floored < 0)
100
+ return 0;
101
+ if (floored >= length)
102
+ return length - 1;
103
+ return floored;
104
+ }
105
+ /**
106
+ * Pick one art variant. Defaults to `Math.random` but the caller can
107
+ * inject a deterministic source — the spec uses a sequence-driven
108
+ * stub to assert the picker hits each entry в the corpus.
109
+ */
110
+ export function pickArtVariant(rng = Math.random) {
111
+ const raw = rng() * PUG_STICKERS.length;
112
+ return PUG_STICKERS[safeIndex(raw, PUG_STICKERS.length)];
113
+ }
114
+ /**
115
+ * Pick one rotating brand quote. Same contract as `pickArtVariant` —
116
+ * test-injectable rng so the spec can pin the chosen index.
117
+ */
118
+ export function pickQuote(rng = Math.random) {
119
+ const raw = rng() * PUG_QUOTES.length;
120
+ return PUG_QUOTES[safeIndex(raw, PUG_QUOTES.length)];
121
+ }
122
+ /**
123
+ * Plain-text renderer for the `--ascii-only` flag and the non-TTY shell
124
+ * path. Emits the art verbatim, then a blank line, then the quote.
125
+ * No box border — scripting use-case (`pugi stickers --ascii-only`
126
+ * piped to `figlet`, `lolcat`, or a regression-fixture file) gets a
127
+ * stable contract free of decorative ANSI noise.
128
+ */
129
+ export function renderPugStickersText(art, quote) {
130
+ return `${art.art}\n\n${quote}`;
131
+ }
132
+ export function PugStickersArt({ art, quote }) {
133
+ const lines = art.art.split('\n');
134
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Pugi stickers" }), _jsxs(Text, { dimColor: true, children: [" \u2014 ", art.caption] })] }), _jsx(Box, { flexDirection: "column", children: lines.map((line, i) => (_jsx(Text, { children: line }, `art-${i}`))) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["\"", quote, "\""] }) })] }));
135
+ }
136
+ //# sourceMappingURL=stickers-art.js.map
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { OUTPUT_STYLES, OUTPUT_STYLE_SLUGS, } from '../core/output-style/presets.js';
4
+ import { useTheme } from '../core/theme/context.js';
5
+ /**
6
+ * Banner above the table. Plain text (not bold) so the prefix `*`
7
+ * remains the dominant active-row cue.
8
+ */
9
+ function buildBanner(active, source) {
10
+ return `Active style: ${active} (${source})`;
11
+ }
12
+ export function StyleTable({ active, source }) {
13
+ // Leak L30 (2026-05-27): the active-row marker color flows through
14
+ // the theme so `colorblind` operators see cyan instead of green
15
+ // (which their palette re-maps to `success`). Falls back to the
16
+ // default theme's `success` token when no provider is mounted.
17
+ const theme = useTheme();
18
+ const slugWidth = Math.max('NAME'.length, ...OUTPUT_STYLE_SLUGS.map((slug) => slug.length));
19
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Pugi output styles" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: ` ${'NAME'.padEnd(slugWidth)} GLOSS` }) }), OUTPUT_STYLE_SLUGS.map((slug) => (_jsx(StyleRow, { slug: slug, active: active, slugWidth: slugWidth, activeColor: theme.success }, slug))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: buildBanner(active, source) }) })] }));
20
+ }
21
+ function StyleRow({ slug, active, slugWidth, activeColor }) {
22
+ const isActive = slug === active;
23
+ const marker = isActive ? '*' : ' ';
24
+ const slugPart = slug.padEnd(slugWidth, ' ');
25
+ const gloss = OUTPUT_STYLES[slug].gloss;
26
+ return (_jsxs(Box, { children: [_jsx(Text, { color: isActive ? activeColor : undefined, bold: isActive, children: `${marker} ${slugPart}` }), _jsx(Text, { children: ` ${gloss}` })] }));
27
+ }
28
+ //# sourceMappingURL=style-table.js.map
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { compileSampleRow, THEMES, THEME_SLUGS, } from '../core/theme/presets.js';
4
+ /**
5
+ * Banner above the table. Plain text (not bold) so the prefix `*`
6
+ * remains the dominant active-row cue. Mirrors `<StyleTable>` so the
7
+ * Settings-group surfaces read identically.
8
+ */
9
+ function buildBanner(active, source) {
10
+ return `Active theme: ${active} (${source})`;
11
+ }
12
+ export function ThemeTable({ active, source }) {
13
+ const slugWidth = Math.max('NAME'.length, ...THEME_SLUGS.map((slug) => slug.length));
14
+ // The gloss column gets sized to the widest gloss + 2 padding so
15
+ // the sample column lines up. Computed once per render so the
16
+ // layout stays stable when the catalogue grows.
17
+ const glossWidth = Math.max('GLOSS'.length, ...THEME_SLUGS.map((slug) => THEMES[slug].gloss.length));
18
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Pugi themes" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: ` ${'NAME'.padEnd(slugWidth)} ${'GLOSS'.padEnd(glossWidth)} SAMPLE` }) }), THEME_SLUGS.map((slug) => (_jsx(ThemeRow, { slug: slug, active: active, slugWidth: slugWidth, glossWidth: glossWidth }, slug))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: buildBanner(active, source) }) })] }));
19
+ }
20
+ function ThemeRow({ slug, active, slugWidth, glossWidth }) {
21
+ const isActive = slug === active;
22
+ const marker = isActive ? '*' : ' ';
23
+ const slugPart = slug.padEnd(slugWidth, ' ');
24
+ const preset = THEMES[slug];
25
+ const gloss = preset.gloss.padEnd(glossWidth, ' ');
26
+ const sample = compileSampleRow(slug);
27
+ return (_jsxs(Box, { children: [_jsx(Text, { color: isActive ? preset.colors.accent : undefined, bold: isActive, children: `${marker} ${slugPart}` }), _jsx(Text, { children: ` ${gloss} ` }), _jsx(Text, { color: preset.colors.foreground, children: sample.foreground }), _jsx(Text, { children: ' ' }), _jsx(Text, { color: preset.colors.accent, children: sample.accent }), _jsx(Text, { children: ' ' }), _jsx(Text, { color: preset.colors.success, children: sample.success }), _jsx(Text, { children: ' ' }), _jsx(Text, { color: preset.colors.warning, children: sample.warning }), _jsx(Text, { children: ' ' }), _jsx(Text, { color: preset.colors.error, children: sample.error })] }));
28
+ }
29
+ //# sourceMappingURL=theme-table.js.map