@pugi/cli 0.1.0-beta.22 → 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/diagnostics/probes/pugi-md.js +89 -0
- package/dist/core/engine/native-pugi.js +34 -1
- 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 +107 -0
- package/dist/core/repl/slash-commands.js +35 -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/vim/keymap.js +288 -0
- package/dist/core/vim/state.js +92 -0
- package/dist/runtime/cli.js +217 -14
- package/dist/runtime/commands/doctor.js +13 -0
- package/dist/runtime/commands/release-notes.js +229 -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/tui/doctor-table.js +32 -17
- package/dist/tui/repl-render.js +17 -2
- package/dist/tui/repl.js +9 -1
- package/dist/tui/style-table.js +9 -3
- 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,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pugi release-notes` — changelog diff between last-seen + current
|
|
3
|
+
* (Leak L24, 2026-05-27).
|
|
4
|
+
*
|
|
5
|
+
* Parity command with Claude Code's `/release-notes`, which shows
|
|
6
|
+
* what changed between the previously-installed CLI version and the
|
|
7
|
+
* currently-installed one. The Pugi variant reads the bundled
|
|
8
|
+
* `CHANGELOG.md`, slices it к the range `(last-seen, current]`, and
|
|
9
|
+
* renders the Markdown sections to the operator. After а successful
|
|
10
|
+
* render the marker is bumped к `current` so the next invocation is а
|
|
11
|
+
* no-op until the operator upgrades again.
|
|
12
|
+
*
|
|
13
|
+
* # Module contract
|
|
14
|
+
*
|
|
15
|
+
* - This file owns the WIRING from CLI flags + ambient state к the
|
|
16
|
+
* parser + state I/O helpers. The parser + state modules в
|
|
17
|
+
* `core/release-notes/` have zero coupling к the CLI dispatch
|
|
18
|
+
* surface.
|
|
19
|
+
*
|
|
20
|
+
* - `runReleaseNotesCommand` is the single entry point. Both the
|
|
21
|
+
* top-level `pugi release-notes` handler в `runtime/cli.ts` AND
|
|
22
|
+
* the in-REPL `/release-notes` slash command call it. The
|
|
23
|
+
* function returns а structured `ReleaseNotesResult` so the
|
|
24
|
+
* slash dispatcher can route the lines к the system pane
|
|
25
|
+
* without re-reading the changelog.
|
|
26
|
+
*
|
|
27
|
+
* - Exit code is ALWAYS 0. The command is informational, never а
|
|
28
|
+
* gate. Read failures, missing CHANGELOG, and write failures all
|
|
29
|
+
* degrade к а structured envelope with а human-readable footer.
|
|
30
|
+
*
|
|
31
|
+
* - The changelog source is captured behind а function so the spec
|
|
32
|
+
* can stub it without touching disk. The default reads the file
|
|
33
|
+
* bundled with the CLI install (resolved relative к the package
|
|
34
|
+
* root); fixtures pass an in-memory string.
|
|
35
|
+
*
|
|
36
|
+
* - `--reset` flag clears the last-seen marker AND re-renders the
|
|
37
|
+
* full bundled changelog as if the operator had never run the
|
|
38
|
+
* command. Distinct from а plain `--all` toggle because the
|
|
39
|
+
* reset PERSISTS (the next invocation again shows everything
|
|
40
|
+
* newer than the cleared marker — `none`).
|
|
41
|
+
*/
|
|
42
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
43
|
+
import { homedir } from 'node:os';
|
|
44
|
+
import { dirname, resolve } from 'node:path';
|
|
45
|
+
import { fileURLToPath } from 'node:url';
|
|
46
|
+
import { parseChangelog, sliceVersionsBetween, } from '../../core/release-notes/parser.js';
|
|
47
|
+
import { clearLastSeenVersion, readLastSeenVersion, writeLastSeenVersion, } from '../../core/release-notes/state.js';
|
|
48
|
+
import { PUGI_CLI_VERSION } from '../version.js';
|
|
49
|
+
/**
|
|
50
|
+
* Default loader для the bundled `apps/pugi-cli/CHANGELOG.md`. The
|
|
51
|
+
* compiled bundle ships under `dist/runtime/commands/release-notes.js`;
|
|
52
|
+
* the CHANGELOG sits next к `package.json` at the package root, two
|
|
53
|
+
* directories up from `dist/runtime/commands/`. We also probe а
|
|
54
|
+
* couple of fallback locations so the dev path (running the source
|
|
55
|
+
* directly из `src/`) works без а compile step.
|
|
56
|
+
*/
|
|
57
|
+
export function defaultReadChangelog() {
|
|
58
|
+
const candidates = resolveChangelogCandidates();
|
|
59
|
+
for (const candidate of candidates) {
|
|
60
|
+
try {
|
|
61
|
+
if (existsSync(candidate)) {
|
|
62
|
+
return readFileSync(candidate, 'utf8');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Permission errors, transient FS hiccups — keep probing the
|
|
67
|
+
// remaining candidates. Returning null at the end is fine; the
|
|
68
|
+
// renderer surfaces а "changelog-missing" envelope.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
function resolveChangelogCandidates() {
|
|
74
|
+
// import.meta.url points к the compiled JS in production
|
|
75
|
+
// (`dist/runtime/commands/release-notes.js`) and к the source TS в
|
|
76
|
+
// tests / dev runs. We probe both relative ancestries so either
|
|
77
|
+
// path lands on `<package>/CHANGELOG.md`.
|
|
78
|
+
try {
|
|
79
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
80
|
+
return [
|
|
81
|
+
resolve(here, '../../..', 'CHANGELOG.md'),
|
|
82
|
+
resolve(here, '../../../..', 'CHANGELOG.md'),
|
|
83
|
+
resolve(process.cwd(), 'apps/pugi-cli/CHANGELOG.md'),
|
|
84
|
+
resolve(process.cwd(), 'CHANGELOG.md'),
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Some non-ESM contexts (very old node, eval'd code) reject
|
|
89
|
+
// `import.meta.url`. Fall back к cwd-relative probes — works for
|
|
90
|
+
// tests that run from the package root.
|
|
91
|
+
return [
|
|
92
|
+
resolve(process.cwd(), 'apps/pugi-cli/CHANGELOG.md'),
|
|
93
|
+
resolve(process.cwd(), 'CHANGELOG.md'),
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Default home dir resolver. Centralised so the CLI handler can call
|
|
99
|
+
* `runReleaseNotesCommand` without re-importing `os.homedir`.
|
|
100
|
+
*/
|
|
101
|
+
export function defaultReleaseNotesHome() {
|
|
102
|
+
return homedir();
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Pick + render the release notes, hand the result к the output
|
|
106
|
+
* sink, persist the marker. Always exits 0.
|
|
107
|
+
*/
|
|
108
|
+
export function runReleaseNotesCommand(ctx) {
|
|
109
|
+
const readChangelog = ctx.readChangelog ?? defaultReadChangelog;
|
|
110
|
+
const currentVersion = ctx.currentVersion ?? PUGI_CLI_VERSION;
|
|
111
|
+
// `--reset` clears the marker before slicing. We capture the
|
|
112
|
+
// pre-clear value so the JSON envelope still shows the operator
|
|
113
|
+
// what their previous marker was, which makes scripting + bug
|
|
114
|
+
// reports easier.
|
|
115
|
+
const lastSeenBefore = readLastSeenVersion(ctx.home);
|
|
116
|
+
if (ctx.reset) {
|
|
117
|
+
clearLastSeenVersion(ctx.home);
|
|
118
|
+
}
|
|
119
|
+
const lastSeen = ctx.reset ? null : lastSeenBefore;
|
|
120
|
+
const raw = readChangelog();
|
|
121
|
+
if (raw === null) {
|
|
122
|
+
const result = {
|
|
123
|
+
command: 'release-notes',
|
|
124
|
+
currentVersion,
|
|
125
|
+
lastSeenVersion: lastSeenBefore ?? 'none',
|
|
126
|
+
sections: [],
|
|
127
|
+
status: 'changelog-missing',
|
|
128
|
+
markerPersisted: false,
|
|
129
|
+
persistFailure: null,
|
|
130
|
+
text: renderMissingChangelog(currentVersion),
|
|
131
|
+
};
|
|
132
|
+
ctx.writeOutput(result, result.text);
|
|
133
|
+
process.exitCode = 0;
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
const sections = parseChangelog(raw);
|
|
137
|
+
if (sections.length === 0) {
|
|
138
|
+
const result = {
|
|
139
|
+
command: 'release-notes',
|
|
140
|
+
currentVersion,
|
|
141
|
+
lastSeenVersion: lastSeenBefore ?? 'none',
|
|
142
|
+
sections: [],
|
|
143
|
+
status: 'changelog-empty',
|
|
144
|
+
markerPersisted: false,
|
|
145
|
+
persistFailure: null,
|
|
146
|
+
text: renderEmptyChangelog(currentVersion),
|
|
147
|
+
};
|
|
148
|
+
ctx.writeOutput(result, result.text);
|
|
149
|
+
process.exitCode = 0;
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
const slice = sliceVersionsBetween(sections, lastSeen, currentVersion);
|
|
153
|
+
if (slice.length === 0) {
|
|
154
|
+
// Nothing new — render the no-op message and DO NOT touch the
|
|
155
|
+
// marker (marker already equals current OR is newer; either way
|
|
156
|
+
// re-writing it is а no-op write we can avoid).
|
|
157
|
+
const result = {
|
|
158
|
+
command: 'release-notes',
|
|
159
|
+
currentVersion,
|
|
160
|
+
lastSeenVersion: lastSeenBefore ?? 'none',
|
|
161
|
+
sections: [],
|
|
162
|
+
status: 'up-to-date',
|
|
163
|
+
markerPersisted: false,
|
|
164
|
+
persistFailure: null,
|
|
165
|
+
text: renderUpToDate(currentVersion, lastSeenBefore),
|
|
166
|
+
};
|
|
167
|
+
ctx.writeOutput(result, result.text);
|
|
168
|
+
process.exitCode = 0;
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
const persist = writeLastSeenVersion(ctx.home, currentVersion);
|
|
172
|
+
const text = renderSections(slice, currentVersion, lastSeen, persist);
|
|
173
|
+
const result = {
|
|
174
|
+
command: 'release-notes',
|
|
175
|
+
currentVersion,
|
|
176
|
+
lastSeenVersion: lastSeenBefore ?? 'none',
|
|
177
|
+
sections: slice,
|
|
178
|
+
status: ctx.reset ? 'reset' : 'rendered',
|
|
179
|
+
markerPersisted: persist.status === 'ok',
|
|
180
|
+
persistFailure: persist.status === 'failed' ? persist.reason : null,
|
|
181
|
+
text,
|
|
182
|
+
};
|
|
183
|
+
ctx.writeOutput(result, text);
|
|
184
|
+
process.exitCode = 0;
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
function renderSections(sections, current, lastSeen, persist) {
|
|
188
|
+
const header = lastSeen
|
|
189
|
+
? `Pugi release notes — ${lastSeen} → ${current}`
|
|
190
|
+
: `Pugi release notes — up to ${current}`;
|
|
191
|
+
const blocks = [header, '═'.repeat(Math.max(header.length, 30))];
|
|
192
|
+
for (const section of sections) {
|
|
193
|
+
const subhead = section.date
|
|
194
|
+
? `## [${section.version}] - ${section.date}`
|
|
195
|
+
: `## [${section.version}]`;
|
|
196
|
+
blocks.push('');
|
|
197
|
+
blocks.push(subhead);
|
|
198
|
+
if (section.body.length > 0) {
|
|
199
|
+
blocks.push('');
|
|
200
|
+
blocks.push(section.body);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (persist.status === 'failed') {
|
|
204
|
+
blocks.push('');
|
|
205
|
+
blocks.push(`Warning: could not persist last-seen marker (${persist.reason}). Next run will surface the same notes.`);
|
|
206
|
+
}
|
|
207
|
+
return blocks.join('\n');
|
|
208
|
+
}
|
|
209
|
+
function renderUpToDate(current, lastSeen) {
|
|
210
|
+
const lines = ['No new release notes.'];
|
|
211
|
+
lines.push(`Installed: ${current}`);
|
|
212
|
+
lines.push(`Last seen: ${lastSeen ?? 'none'}`);
|
|
213
|
+
return lines.join('\n');
|
|
214
|
+
}
|
|
215
|
+
function renderMissingChangelog(current) {
|
|
216
|
+
return [
|
|
217
|
+
'Release notes are not bundled with this install.',
|
|
218
|
+
`Installed: ${current}`,
|
|
219
|
+
'See https://pugi.io/changelog for the rendered changelog.',
|
|
220
|
+
].join('\n');
|
|
221
|
+
}
|
|
222
|
+
function renderEmptyChangelog(current) {
|
|
223
|
+
return [
|
|
224
|
+
'Bundled changelog is empty — no parsed sections.',
|
|
225
|
+
`Installed: ${current}`,
|
|
226
|
+
'See https://pugi.io/changelog for the rendered changelog.',
|
|
227
|
+
].join('\n');
|
|
228
|
+
}
|
|
229
|
+
//# sourceMappingURL=release-notes.js.map
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leak L30 (2026-05-27) — `pugi theme` top-level command + REPL slash
|
|
3
|
+
* companion.
|
|
4
|
+
*
|
|
5
|
+
* Operator surface:
|
|
6
|
+
*
|
|
7
|
+
* pugi theme Show active theme + table.
|
|
8
|
+
* pugi theme <name> Switch workspace theme (current cwd).
|
|
9
|
+
* pugi theme <name> --persist Switch + also write user default.
|
|
10
|
+
* pugi theme --reset Clear workspace override → back to default.
|
|
11
|
+
* pugi theme --reset --user Also clear the user default.
|
|
12
|
+
* pugi theme --list Print the catalogue (no flip).
|
|
13
|
+
* pugi theme --json Structured envelope variant.
|
|
14
|
+
*
|
|
15
|
+
* The same runner powers `/theme` from inside the REPL. The REPL
|
|
16
|
+
* dispatcher (see `core/repl/session.ts`) routes through here so the
|
|
17
|
+
* two surfaces stay single-sourced — operators trained on one read
|
|
18
|
+
* the same payload + table on the other. Matches the leak L18
|
|
19
|
+
* `/style` runner exactly so the two settings surfaces are
|
|
20
|
+
* paste-comparable for the operator + grep-comparable for future
|
|
21
|
+
* maintenance.
|
|
22
|
+
*
|
|
23
|
+
* Exit codes:
|
|
24
|
+
* 0 — show / switch / reset all succeed
|
|
25
|
+
* 1 — unknown preset slug (returned BEFORE any write)
|
|
26
|
+
* 2 — conflicting flags (e.g. `--reset` with a positional slug)
|
|
27
|
+
*
|
|
28
|
+
* The exit codes are surfaced through `process.exitCode` by the
|
|
29
|
+
* dispatcher in `cli.ts` — this module returns a structured payload
|
|
30
|
+
* + writes via the injected `writeOutput`. Throwing is reserved for
|
|
31
|
+
* truly unexpected errors (fs permissions etc.); the spec hooks the
|
|
32
|
+
* happy + sad paths through `writeOutput` shape, not via try/catch
|
|
33
|
+
* on the throw.
|
|
34
|
+
*/
|
|
35
|
+
import { DEFAULT_THEME, isThemeSlug, renderThemeTable, THEME_SLUGS, } from '../../core/theme/presets.js';
|
|
36
|
+
import { clearUserTheme, clearWorkspaceTheme, resolveTheme, setUserTheme, setWorkspaceTheme, } from '../../core/theme/state.js';
|
|
37
|
+
/**
|
|
38
|
+
* Entry point. Parses `args`, applies the operation, emits the
|
|
39
|
+
* payload + text via `ctx.writeOutput`, and returns the exit code the
|
|
40
|
+
* dispatcher should hand back to the shell.
|
|
41
|
+
*/
|
|
42
|
+
export async function runThemeCommand(args, ctx) {
|
|
43
|
+
const flags = parseFlags(args);
|
|
44
|
+
// Reset path
|
|
45
|
+
if (flags.reset) {
|
|
46
|
+
if (flags.slug !== null) {
|
|
47
|
+
const payload = buildPayload({
|
|
48
|
+
status: 'invalid_flags',
|
|
49
|
+
ctx,
|
|
50
|
+
message: '/theme --reset cannot be combined with a preset name. Use one or the other.',
|
|
51
|
+
});
|
|
52
|
+
ctx.writeOutput(payload, payload.message);
|
|
53
|
+
return 2;
|
|
54
|
+
}
|
|
55
|
+
if (flags.persist) {
|
|
56
|
+
const payload = buildPayload({
|
|
57
|
+
status: 'invalid_flags',
|
|
58
|
+
ctx,
|
|
59
|
+
message: '/theme --reset cannot be combined with --persist. Use --reset --user to also clear the user default.',
|
|
60
|
+
});
|
|
61
|
+
ctx.writeOutput(payload, payload.message);
|
|
62
|
+
return 2;
|
|
63
|
+
}
|
|
64
|
+
clearWorkspaceTheme({ workspaceRoot: ctx.workspaceRoot, env: ctx.env });
|
|
65
|
+
if (flags.user) {
|
|
66
|
+
clearUserTheme({ workspaceRoot: ctx.workspaceRoot, env: ctx.env });
|
|
67
|
+
}
|
|
68
|
+
const payload = buildPayload({
|
|
69
|
+
status: 'reset',
|
|
70
|
+
ctx,
|
|
71
|
+
message: flags.user
|
|
72
|
+
? `Cleared workspace + user theme. Active: ${DEFAULT_THEME} (default).`
|
|
73
|
+
: `Cleared workspace theme. Active: ${describeActive(ctx)}.`,
|
|
74
|
+
});
|
|
75
|
+
ctx.writeOutput(payload, payload.message);
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
// List path
|
|
79
|
+
if (flags.list && flags.slug === null) {
|
|
80
|
+
const resolved = resolveTheme({ workspaceRoot: ctx.workspaceRoot, env: ctx.env });
|
|
81
|
+
const payload = buildPayload({
|
|
82
|
+
status: 'listed',
|
|
83
|
+
ctx,
|
|
84
|
+
message: renderThemeTable(resolved.slug),
|
|
85
|
+
});
|
|
86
|
+
ctx.writeOutput(payload, payload.message);
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
// Switch path
|
|
90
|
+
if (flags.slug !== null) {
|
|
91
|
+
if (!isThemeSlug(flags.slug)) {
|
|
92
|
+
const payload = buildPayload({
|
|
93
|
+
status: 'invalid_slug',
|
|
94
|
+
ctx,
|
|
95
|
+
attemptedSlug: flags.slug,
|
|
96
|
+
message: `Unknown theme "${flags.slug}". Try one of: ${THEME_SLUGS.join(', ')}.`,
|
|
97
|
+
});
|
|
98
|
+
ctx.writeOutput(payload, payload.message);
|
|
99
|
+
return 1;
|
|
100
|
+
}
|
|
101
|
+
const before = resolveTheme({ workspaceRoot: ctx.workspaceRoot, env: ctx.env });
|
|
102
|
+
setWorkspaceTheme(flags.slug, { workspaceRoot: ctx.workspaceRoot, env: ctx.env });
|
|
103
|
+
if (flags.persist) {
|
|
104
|
+
setUserTheme(flags.slug, { workspaceRoot: ctx.workspaceRoot, env: ctx.env });
|
|
105
|
+
}
|
|
106
|
+
const tail = flags.persist ? ' (workspace + user default)' : ' (workspace)';
|
|
107
|
+
const payload = buildPayload({
|
|
108
|
+
status: 'switched',
|
|
109
|
+
ctx,
|
|
110
|
+
previous: before.slug,
|
|
111
|
+
persistedToUser: flags.persist,
|
|
112
|
+
message: `Theme → ${flags.slug}${tail}. Was: ${before.slug} (${before.source}).`,
|
|
113
|
+
});
|
|
114
|
+
ctx.writeOutput(payload, payload.message);
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
// Show path (no args)
|
|
118
|
+
const resolved = resolveTheme({ workspaceRoot: ctx.workspaceRoot, env: ctx.env });
|
|
119
|
+
const banner = `Active theme: ${resolved.slug} (${resolved.source})`;
|
|
120
|
+
const table = renderThemeTable(resolved.slug);
|
|
121
|
+
const payload = buildPayload({
|
|
122
|
+
status: 'show',
|
|
123
|
+
ctx,
|
|
124
|
+
message: `${banner}\n\n${table}`,
|
|
125
|
+
});
|
|
126
|
+
ctx.writeOutput(payload, payload.message);
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
function describeActive(ctx) {
|
|
130
|
+
const resolved = resolveTheme({ workspaceRoot: ctx.workspaceRoot, env: ctx.env });
|
|
131
|
+
return `${resolved.slug} (${resolved.source})`;
|
|
132
|
+
}
|
|
133
|
+
function buildPayload(args) {
|
|
134
|
+
const resolved = resolveTheme({ workspaceRoot: args.ctx.workspaceRoot, env: args.ctx.env });
|
|
135
|
+
const payload = {
|
|
136
|
+
command: 'theme',
|
|
137
|
+
status: args.status,
|
|
138
|
+
active: resolved.slug,
|
|
139
|
+
source: resolved.source,
|
|
140
|
+
presets: THEME_SLUGS,
|
|
141
|
+
message: args.message,
|
|
142
|
+
};
|
|
143
|
+
if (args.previous !== undefined)
|
|
144
|
+
payload.previous = args.previous;
|
|
145
|
+
if (args.persistedToUser !== undefined)
|
|
146
|
+
payload.persistedToUser = args.persistedToUser;
|
|
147
|
+
if (args.attemptedSlug !== undefined)
|
|
148
|
+
payload.attemptedSlug = args.attemptedSlug;
|
|
149
|
+
return payload;
|
|
150
|
+
}
|
|
151
|
+
function parseFlags(args) {
|
|
152
|
+
const flags = {
|
|
153
|
+
slug: null,
|
|
154
|
+
persist: false,
|
|
155
|
+
reset: false,
|
|
156
|
+
user: false,
|
|
157
|
+
list: false,
|
|
158
|
+
};
|
|
159
|
+
for (const arg of args) {
|
|
160
|
+
if (arg === '--persist')
|
|
161
|
+
flags.persist = true;
|
|
162
|
+
else if (arg === '--reset')
|
|
163
|
+
flags.reset = true;
|
|
164
|
+
else if (arg === '--user')
|
|
165
|
+
flags.user = true;
|
|
166
|
+
else if (arg === '--list')
|
|
167
|
+
flags.list = true;
|
|
168
|
+
else if (arg.startsWith('-')) {
|
|
169
|
+
// Unknown flag — keep simple parser. Treat as positional so the
|
|
170
|
+
// downstream isThemeSlug check rejects it with a clear "unknown
|
|
171
|
+
// theme" message rather than swallowing silently. Mirrors the
|
|
172
|
+
// L18 style runner's behaviour for grep-parity.
|
|
173
|
+
if (flags.slug === null)
|
|
174
|
+
flags.slug = arg;
|
|
175
|
+
}
|
|
176
|
+
else if (flags.slug === null) {
|
|
177
|
+
flags.slug = arg;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Normalise the slug to lowercase so `pugi theme DARK` works the
|
|
181
|
+
// same as `pugi theme dark`. The catalogue is lowercase-only by
|
|
182
|
+
// contract; this keeps operators from tripping on shift-key habits.
|
|
183
|
+
if (flags.slug !== null)
|
|
184
|
+
flags.slug = flags.slug.toLowerCase();
|
|
185
|
+
return flags;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Re-export for the slash-command dispatcher in
|
|
189
|
+
* `core/repl/session.ts` so it can render a preview block if a
|
|
190
|
+
* future surface (`/theme --preview`) lands. Kept here so the
|
|
191
|
+
* runtime module is the single import point for theme-related
|
|
192
|
+
* surfaces; consumers should NOT reach into `core/theme/*` directly
|
|
193
|
+
* unless they are Ink components that need the React context.
|
|
194
|
+
*/
|
|
195
|
+
export { THEMES as THEME_CATALOGUE, } from '../../core/theme/presets.js';
|
|
196
|
+
//# sourceMappingURL=theme.js.map
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leak L26 (2026-05-27) — `pugi vim` top-level command + REPL slash
|
|
3
|
+
* companion.
|
|
4
|
+
*
|
|
5
|
+
* Operator surface:
|
|
6
|
+
*
|
|
7
|
+
* pugi vim Show current vim-mode state.
|
|
8
|
+
* pugi vim on Enable + persist (~/.pugi/config.json).
|
|
9
|
+
* pugi vim off Disable + persist (default).
|
|
10
|
+
* /vim Toggle from inside the REPL.
|
|
11
|
+
* /vim on Enable from inside the REPL.
|
|
12
|
+
* /vim off Disable from inside the REPL.
|
|
13
|
+
*
|
|
14
|
+
* The same runner backs both surfaces so the slash + the shell verb
|
|
15
|
+
* stay single-sourced — operators trained on one read the same
|
|
16
|
+
* payload + banner on the other.
|
|
17
|
+
*
|
|
18
|
+
* Exit codes:
|
|
19
|
+
* 0 — show / enable / disable / toggle happy path
|
|
20
|
+
* 2 — unknown subcommand (e.g. `pugi vim chaos`)
|
|
21
|
+
*
|
|
22
|
+
* NOTE: there are NO `--persist` flag and no workspace tier. Vim mode
|
|
23
|
+
* is a single user-level preference, matching the leak research note
|
|
24
|
+
* that operators expect modal editing to be a body-memory trait, not
|
|
25
|
+
* a per-repo concern (see `core/vim/state.ts` header).
|
|
26
|
+
*/
|
|
27
|
+
import { resolveVimMode, setVimMode } from '../../core/vim/state.js';
|
|
28
|
+
/**
|
|
29
|
+
* Banner shown on enable so the operator can see the binding cheat
|
|
30
|
+
* sheet without reaching for `/help`. Echoed by both surfaces.
|
|
31
|
+
*/
|
|
32
|
+
export const VIM_ENABLED_BANNER = 'Vim mode on. Esc=normal, i=insert, :w=submit, :q=cancel.';
|
|
33
|
+
/**
|
|
34
|
+
* Entry point. Parses `args`, flips persistence as needed, emits the
|
|
35
|
+
* payload + text via `ctx.writeOutput`, and returns the exit code.
|
|
36
|
+
*/
|
|
37
|
+
export async function runVimCommand(args, ctx) {
|
|
38
|
+
// Validate args before reading state so a bad call never has a
|
|
39
|
+
// side-effect.
|
|
40
|
+
if (args.length > 1) {
|
|
41
|
+
const payload = {
|
|
42
|
+
command: 'vim',
|
|
43
|
+
status: 'invalid_args',
|
|
44
|
+
active: resolveVimMode({ env: ctx.env }),
|
|
45
|
+
message: `pugi vim takes at most one argument (on / off / toggle). Got: ${args.join(' ')}`,
|
|
46
|
+
attemptedArg: args.join(' '),
|
|
47
|
+
};
|
|
48
|
+
ctx.writeOutput(payload, payload.message);
|
|
49
|
+
return 2;
|
|
50
|
+
}
|
|
51
|
+
const current = resolveVimMode({ env: ctx.env });
|
|
52
|
+
const verb = args[0]?.toLowerCase() ?? '';
|
|
53
|
+
// No args → toggle (slash convention from the leak research). The
|
|
54
|
+
// CLI shell surface ALSO toggles on bare invocation so the two
|
|
55
|
+
// surfaces converge; if the operator wants the read-only show they
|
|
56
|
+
// can pipe through `pugi vim --json` or query the config directly.
|
|
57
|
+
// We surface the change as a `show` status when nothing flipped so
|
|
58
|
+
// scripts can distinguish read from write.
|
|
59
|
+
if (verb === '') {
|
|
60
|
+
const next = !current;
|
|
61
|
+
setVimMode(next, { env: ctx.env });
|
|
62
|
+
const status = next ? 'enabled' : 'disabled';
|
|
63
|
+
const message = next ? VIM_ENABLED_BANNER : 'Vim mode off.';
|
|
64
|
+
const payload = {
|
|
65
|
+
command: 'vim',
|
|
66
|
+
status,
|
|
67
|
+
active: next,
|
|
68
|
+
previous: current,
|
|
69
|
+
message,
|
|
70
|
+
};
|
|
71
|
+
ctx.writeOutput(payload, payload.message);
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
if (verb === 'on' || verb === 'enable' || verb === 'true') {
|
|
75
|
+
if (current) {
|
|
76
|
+
const payload = {
|
|
77
|
+
command: 'vim',
|
|
78
|
+
status: 'unchanged',
|
|
79
|
+
active: true,
|
|
80
|
+
previous: true,
|
|
81
|
+
message: 'Vim mode already on.',
|
|
82
|
+
};
|
|
83
|
+
ctx.writeOutput(payload, payload.message);
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
setVimMode(true, { env: ctx.env });
|
|
87
|
+
const payload = {
|
|
88
|
+
command: 'vim',
|
|
89
|
+
status: 'enabled',
|
|
90
|
+
active: true,
|
|
91
|
+
previous: current,
|
|
92
|
+
message: VIM_ENABLED_BANNER,
|
|
93
|
+
};
|
|
94
|
+
ctx.writeOutput(payload, payload.message);
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
if (verb === 'off' || verb === 'disable' || verb === 'false') {
|
|
98
|
+
if (!current) {
|
|
99
|
+
const payload = {
|
|
100
|
+
command: 'vim',
|
|
101
|
+
status: 'unchanged',
|
|
102
|
+
active: false,
|
|
103
|
+
previous: false,
|
|
104
|
+
message: 'Vim mode already off.',
|
|
105
|
+
};
|
|
106
|
+
ctx.writeOutput(payload, payload.message);
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
setVimMode(false, { env: ctx.env });
|
|
110
|
+
const payload = {
|
|
111
|
+
command: 'vim',
|
|
112
|
+
status: 'disabled',
|
|
113
|
+
active: false,
|
|
114
|
+
previous: current,
|
|
115
|
+
message: 'Vim mode off.',
|
|
116
|
+
};
|
|
117
|
+
ctx.writeOutput(payload, payload.message);
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
if (verb === 'status' || verb === 'show') {
|
|
121
|
+
const payload = {
|
|
122
|
+
command: 'vim',
|
|
123
|
+
status: 'show',
|
|
124
|
+
active: current,
|
|
125
|
+
message: current ? 'Vim mode is on.' : 'Vim mode is off.',
|
|
126
|
+
};
|
|
127
|
+
ctx.writeOutput(payload, payload.message);
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
const payload = {
|
|
131
|
+
command: 'vim',
|
|
132
|
+
status: 'invalid_args',
|
|
133
|
+
active: current,
|
|
134
|
+
message: `Unknown vim subcommand "${verb}". Try one of: on, off, status.`,
|
|
135
|
+
attemptedArg: verb,
|
|
136
|
+
};
|
|
137
|
+
ctx.writeOutput(payload, payload.message);
|
|
138
|
+
return 2;
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=vim.js.map
|
package/dist/runtime/version.js
CHANGED
|
@@ -44,7 +44,7 @@ export function sanitizeSemver(raw) {
|
|
|
44
44
|
* during import). When bumping the CLI version BOTH literals must be
|
|
45
45
|
* updated; the release smoke-test (`pack:smoke`) verifies they agree.
|
|
46
46
|
*/
|
|
47
|
-
export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.
|
|
47
|
+
export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.23');
|
|
48
48
|
/**
|
|
49
49
|
* Outbound: the CLI's installed semver. Read at request time by
|
|
50
50
|
* `version-interceptor.ts` and injected on every `fetch` call.
|
package/dist/tui/doctor-table.js
CHANGED
|
@@ -1,31 +1,46 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
3
|
+
import { useTheme } from '../core/theme/context.js';
|
|
4
|
+
/**
|
|
5
|
+
* Leak L30 (2026-05-27): status colors flow through the active theme
|
|
6
|
+
* instead of being hard-coded Ink color names. The `colorblind`
|
|
7
|
+
* preset re-maps OK/WARN/ERROR to a blue-yellow-magenta axis so
|
|
8
|
+
* deuteranopia operators can still distinguish the three states; the
|
|
9
|
+
* `default` / `dark` / `light` themes route to green/yellow/red
|
|
10
|
+
* variants tuned for each background. `skipped` keeps a muted color
|
|
11
|
+
* (theme-controlled muted token) so dimmed rows still pick up the
|
|
12
|
+
* theme tint.
|
|
13
|
+
*/
|
|
14
|
+
function buildStatusColor(theme) {
|
|
15
|
+
return {
|
|
16
|
+
ok: theme.success,
|
|
17
|
+
warn: theme.warning,
|
|
18
|
+
error: theme.error,
|
|
19
|
+
skipped: theme.muted,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function buildOverallColor(theme) {
|
|
23
|
+
return {
|
|
24
|
+
healthy: theme.success,
|
|
25
|
+
warning: theme.warning,
|
|
26
|
+
error: theme.error,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
17
29
|
export function DoctorTable({ envelope }) {
|
|
30
|
+
const theme = useTheme();
|
|
31
|
+
const statusColor = buildStatusColor(theme);
|
|
32
|
+
const overallColor = buildOverallColor(theme);
|
|
18
33
|
const nameWidth = Math.max('NAME'.length, ...envelope.probes.map((row) => row.name.length));
|
|
19
34
|
const statusWidth = Math.max('STATUS'.length, ...envelope.probes.map((row) => row.status.length));
|
|
20
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Pugi Doctor" }) }), envelope.probes.map((row) => (_jsx(DoctorRow, { row: row, nameWidth: nameWidth, statusWidth: statusWidth }, row.name))), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [envelope.counts.error, " error(s), ", envelope.counts.warn, " warning(s), ", envelope.counts.ok, " ok,", ' ', envelope.counts.skipped, " skipped. Overall:", ' ', _jsx(Text, { color:
|
|
35
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Pugi Doctor" }) }), envelope.probes.map((row) => (_jsx(DoctorRow, { row: row, nameWidth: nameWidth, statusWidth: statusWidth, statusColor: statusColor }, row.name))), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [envelope.counts.error, " error(s), ", envelope.counts.warn, " warning(s), ", envelope.counts.ok, " ok,", ' ', envelope.counts.skipped, " skipped. Overall:", ' ', _jsx(Text, { color: overallColor[envelope.overall], bold: true, children: envelope.overall.toUpperCase() })] }) }), _jsx(Box, { children: _jsxs(Text, { dimColor: true, children: ["CLI ", envelope.meta.cliVersion, " Node ", envelope.meta.nodeVersion, " cwd ", envelope.meta.cwd] }) })] }));
|
|
21
36
|
}
|
|
22
|
-
function DoctorRow({ row, nameWidth, statusWidth }) {
|
|
37
|
+
function DoctorRow({ row, nameWidth, statusWidth, statusColor }) {
|
|
23
38
|
const namePart = row.name.padEnd(nameWidth, ' ');
|
|
24
39
|
const statusPart = row.status.toUpperCase().padEnd(statusWidth, ' ');
|
|
25
40
|
const latencyPart = typeof row.latencyMs === 'number' ? ` (${row.latencyMs}ms)` : '';
|
|
26
41
|
const showRemediation = typeof row.remediation === 'string' &&
|
|
27
42
|
row.remediation.length > 0 &&
|
|
28
43
|
(row.status === 'warn' || row.status === 'error');
|
|
29
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { children: [namePart, " "] }), _jsx(Text, { color:
|
|
44
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { children: [namePart, " "] }), _jsx(Text, { color: statusColor[row.status], bold: true, children: statusPart }), _jsxs(Text, { children: [' ', row.detail, latencyPart] })] }), showRemediation && (_jsx(Box, { marginLeft: nameWidth + statusWidth + 4, children: _jsxs(Text, { dimColor: true, children: ["\u2192 ", row.remediation] }) }))] }));
|
|
30
45
|
}
|
|
31
46
|
//# sourceMappingURL=doctor-table.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';
|
|
@@ -181,13 +183,26 @@ export async function renderRepl(options) {
|
|
|
181
183
|
// (operator opted out via --no-splash), we suppress the pre-print
|
|
182
184
|
// too so the boot stays silent.
|
|
183
185
|
const mascotPrePrinted = options.skipSplash === true ? false : printPugMascotPreInk(process.stdout);
|
|
184
|
-
|
|
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, {
|
|
185
200
|
session,
|
|
186
201
|
updateBanner: options.updateBanner ?? null,
|
|
187
202
|
skipSplash: options.skipSplash === true,
|
|
188
203
|
hideToolStream: options.hideToolStream === true,
|
|
189
204
|
mascotPrePrinted,
|
|
190
|
-
}));
|
|
205
|
+
})));
|
|
191
206
|
// Make sure we leave the alt screen on abrupt exits too. Without
|
|
192
207
|
// this the operator's shell stays "frozen" on the Pugi splash.
|
|
193
208
|
process.once('exit', bootstrap.restore);
|