@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.
- package/dist/core/auth/env-provider.js +238 -0
- package/dist/core/bare-mode/index.js +107 -0
- package/dist/core/diagnostics/probes/bare-mode.js +42 -0
- package/dist/core/diagnostics/probes/pugi-md.js +89 -0
- package/dist/core/engine/native-pugi.js +55 -11
- package/dist/core/engine/prompts.js +30 -2
- package/dist/core/engine/tool-bridge.js +32 -0
- package/dist/core/feedback/queue.js +177 -0
- package/dist/core/feedback/submitter.js +145 -0
- package/dist/core/onboarding/marker.js +111 -0
- package/dist/core/onboarding/telemetry-state.js +108 -0
- package/dist/core/output-style/presets.js +176 -0
- package/dist/core/output-style/state.js +185 -0
- package/dist/core/permissions/index.js +1 -1
- package/dist/core/permissions/state.js +55 -0
- package/dist/core/pugi-md/context-injector.js +76 -0
- package/dist/core/pugi-md/walk-up.js +207 -0
- package/dist/core/release-notes/parser.js +241 -0
- package/dist/core/release-notes/state.js +116 -0
- package/dist/core/repl/session.js +482 -12
- package/dist/core/repl/slash-commands.js +134 -1
- package/dist/core/repl/workspace-context.js +22 -0
- package/dist/core/share/formatter.js +271 -0
- package/dist/core/share/redactor.js +221 -0
- package/dist/core/share/uploader.js +267 -0
- package/dist/core/theme/context.js +91 -0
- package/dist/core/theme/presets.js +228 -0
- package/dist/core/theme/state.js +181 -0
- package/dist/core/todos/invariant.js +10 -0
- package/dist/core/todos/state.js +177 -0
- package/dist/core/vim/keymap.js +288 -0
- package/dist/core/vim/state.js +92 -0
- package/dist/runtime/cli.js +603 -15
- package/dist/runtime/commands/doctor.js +21 -0
- package/dist/runtime/commands/feedback.js +184 -0
- package/dist/runtime/commands/onboarding.js +275 -0
- package/dist/runtime/commands/plan.js +143 -0
- package/dist/runtime/commands/release-notes.js +229 -0
- package/dist/runtime/commands/share.js +316 -0
- package/dist/runtime/commands/stickers.js +82 -0
- package/dist/runtime/commands/style.js +194 -0
- package/dist/runtime/commands/theme.js +196 -0
- package/dist/runtime/commands/vim.js +140 -0
- package/dist/runtime/version.js +1 -1
- package/dist/tools/registry.js +8 -0
- package/dist/tools/todo-write.js +184 -0
- package/dist/tui/compact-banner.js +28 -1
- package/dist/tui/conversation-pane.js +13 -0
- package/dist/tui/doctor-table.js +32 -17
- package/dist/tui/feedback-prompt.js +156 -0
- package/dist/tui/onboarding-wizard.js +240 -0
- package/dist/tui/repl-render.js +26 -3
- package/dist/tui/repl.js +9 -1
- package/dist/tui/stickers-art.js +136 -0
- package/dist/tui/style-table.js +28 -0
- package/dist/tui/theme-table.js +29 -0
- package/dist/tui/vim-input.js +267 -0
- package/package.json +2 -2
- package/dist/core/engine/compaction-hook.js +0 -154
- package/dist/core/init/scaffold.js +0 -195
- package/dist/core/repl/codebase-survey.js +0 -308
- package/dist/core/repl/init-interview.js +0 -457
- 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
|
package/dist/tui/repl-render.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|