@pugi/cli 0.1.0-beta.22 → 0.1.0-beta.24
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/auto-update/channels.js +122 -0
- package/dist/core/auto-update/checker.js +241 -0
- package/dist/core/auto-update/state.js +235 -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 +156 -0
- package/dist/core/repl/slash-commands.js +50 -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 +297 -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/update.js +289 -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/runtime/cli.js
CHANGED
|
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
3
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { statSync } from 'node:fs';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
5
6
|
import { dirname, relative, resolve } from 'node:path';
|
|
6
7
|
import { fileURLToPath } from 'node:url';
|
|
7
8
|
import { AnvilEngineLoopClient } from '../core/engine/anvil-client.js';
|
|
@@ -22,17 +23,21 @@ import { PUGI_TAGLINE } from '@pugi/personas';
|
|
|
22
23
|
import { resolveRoster, renderRosterTable } from './commands/roster.js';
|
|
23
24
|
import { runDelegateCommand } from './commands/delegate.js';
|
|
24
25
|
import { clearApiKey, DEFAULT_API_URL, listStoredCredentials, maskApiKey, normalizeApiUrl, purgeAllCredentials, readCredentialsFile, resolveActiveCredential, storeApiKey, switchActiveAccount, } from '../core/credentials.js';
|
|
26
|
+
import { resolveAndValidateEnvLogin, } from '../core/auth/env-provider.js';
|
|
25
27
|
import { runDeployCommand } from '../commands/deploy.js';
|
|
26
28
|
import { runJobsCommand } from '../commands/jobs.js';
|
|
27
29
|
import { runConfigCommand } from './commands/config.js';
|
|
28
30
|
import { runStyleCommand } from './commands/style.js';
|
|
31
|
+
import { runThemeCommand } from './commands/theme.js';
|
|
29
32
|
import { runOnboardingCommand } from './commands/onboarding.js';
|
|
33
|
+
import { runVimCommand } from './commands/vim.js';
|
|
30
34
|
import { isOnboarded } from '../core/onboarding/marker.js';
|
|
31
35
|
import { runPrivacyCommand } from './commands/privacy.js';
|
|
32
36
|
import { runReport } from './commands/report.js';
|
|
33
37
|
import { runDoctorCommand, defaultHome as defaultDoctorHome } from './commands/doctor.js';
|
|
34
38
|
import { runStatusCommand, defaultStatusHome, } from './commands/status.js';
|
|
35
39
|
import { runStickersCommand } from './commands/stickers.js';
|
|
40
|
+
import { runReleaseNotesCommand, defaultReleaseNotesHome, } from './commands/release-notes.js';
|
|
36
41
|
import { runUndoCommand } from './commands/undo.js';
|
|
37
42
|
import { runCompactCommand } from './commands/compact.js';
|
|
38
43
|
import { runBudgetCommand } from './commands/budget.js';
|
|
@@ -104,6 +109,12 @@ const handlers = {
|
|
|
104
109
|
plan: dispatchPlan,
|
|
105
110
|
'plan-review': dispatchPlanReview,
|
|
106
111
|
privacy: dispatchPrivacy,
|
|
112
|
+
// L24 (2026-05-27): `pugi release-notes` shows the bundled CHANGELOG
|
|
113
|
+
// diff between the operator's last-seen version + installed version.
|
|
114
|
+
// The slash counterpart `/release-notes` shares this handler via the
|
|
115
|
+
// shared `runReleaseNotesCommand` runner.
|
|
116
|
+
'release-notes': releaseNotes,
|
|
117
|
+
releaseNotes,
|
|
107
118
|
// PAVF-7 (2026-05-27): `pugi report --from-error` captures the
|
|
108
119
|
// most-recent failed session as a redacted bundle so operators can
|
|
109
120
|
// file clean bug reports without manual log-grepping.
|
|
@@ -122,17 +133,31 @@ const handlers = {
|
|
|
122
133
|
feedback: dispatchFeedback,
|
|
123
134
|
sync,
|
|
124
135
|
style: dispatchStyle,
|
|
136
|
+
// Leak L30 (2026-05-27): `pugi theme` flips the local TUI color
|
|
137
|
+
// palette (orthogonal to `pugi style` — that one steers engine
|
|
138
|
+
// prose register). 4 presets: default / dark / light / colorblind.
|
|
139
|
+
theme: dispatchTheme,
|
|
125
140
|
// Leak L25 (2026-05-27): `pugi onboarding` walks the new operator
|
|
126
141
|
// through auth / mode / style / MCP / telemetry. Idempotent;
|
|
127
142
|
// `--reset` clears the marker file so the bare-invocation hint
|
|
128
143
|
// re-arms without nuking persisted defaults.
|
|
129
144
|
onboarding: dispatchOnboarding,
|
|
145
|
+
// Leak L26 (2026-05-27): `pugi vim` toggles vim-style modal editing
|
|
146
|
+
// in the REPL input buffer. Bare invocation toggles, `on`/`off`
|
|
147
|
+
// sets explicitly; preference persists in ~/.pugi/config.json.
|
|
148
|
+
vim: dispatchVim,
|
|
130
149
|
undo: dispatchUndo,
|
|
131
150
|
compact: dispatchCompact,
|
|
132
151
|
// L19 (2026-05-27): `pugi usage` is an alias of `pugi cost` — same
|
|
133
152
|
// handler, same flags. Operators trained on Claude Code expect either
|
|
134
153
|
// verb to surface the per-model token + USD table.
|
|
135
154
|
usage: dispatchCost,
|
|
155
|
+
// Leak L27 (2026-05-27): `pugi update` — channel-aware npm registry
|
|
156
|
+
// probe + optional npm install shell-out. Same handler powers the
|
|
157
|
+
// in-REPL `/update` slash via the session module. R2 atomic swap
|
|
158
|
+
// deferred to Phase 2 per the sprint plan; npm is the single
|
|
159
|
+
// distribution channel today.
|
|
160
|
+
update: dispatchUpdate,
|
|
136
161
|
version,
|
|
137
162
|
web: dispatchWeb,
|
|
138
163
|
whoami,
|
|
@@ -330,6 +355,29 @@ async function dispatchStyle(args, flags, _session) {
|
|
|
330
355
|
if (rc !== 0)
|
|
331
356
|
process.exitCode = rc;
|
|
332
357
|
}
|
|
358
|
+
/**
|
|
359
|
+
* Leak L30 (2026-05-27) — `pugi theme` top-level dispatcher.
|
|
360
|
+
*
|
|
361
|
+
* Forwards to the shared `runThemeCommand` runner. The REPL `/theme`
|
|
362
|
+
* slash uses the same runner via a dynamic import inside
|
|
363
|
+
* `core/repl/session.ts` so the two surfaces stay single-sourced.
|
|
364
|
+
*
|
|
365
|
+
* Exit-code policy mirrors `dispatchStyle`:
|
|
366
|
+
* - 0 — show / switch / reset / list happy paths
|
|
367
|
+
* - 1 — unknown preset slug
|
|
368
|
+
* - 2 — conflicting flags (`--reset` + positional / `--reset --persist`)
|
|
369
|
+
*
|
|
370
|
+
* The runner returns the code; we attach it to `process.exitCode` so
|
|
371
|
+
* subsequent dispatch wrappers do not clobber it on success.
|
|
372
|
+
*/
|
|
373
|
+
async function dispatchTheme(args, flags, _session) {
|
|
374
|
+
const rc = await runThemeCommand(args, {
|
|
375
|
+
workspaceRoot: process.cwd(),
|
|
376
|
+
writeOutput: (payload, text) => writeOutput(flags, payload, text),
|
|
377
|
+
});
|
|
378
|
+
if (rc !== 0)
|
|
379
|
+
process.exitCode = rc;
|
|
380
|
+
}
|
|
333
381
|
/**
|
|
334
382
|
* Leak L25 (2026-05-27) — `pugi onboarding` top-level dispatcher.
|
|
335
383
|
*
|
|
@@ -360,6 +408,25 @@ async function dispatchOnboarding(args, flags, _session) {
|
|
|
360
408
|
if (rc !== 0)
|
|
361
409
|
process.exitCode = rc;
|
|
362
410
|
}
|
|
411
|
+
/**
|
|
412
|
+
* Leak L26 (2026-05-27) — `pugi vim` top-level dispatcher.
|
|
413
|
+
*
|
|
414
|
+
* Forwards to the shared `runVimCommand` runner. The REPL `/vim` slash
|
|
415
|
+
* uses the same runner via a dynamic import inside
|
|
416
|
+
* `core/repl/session.ts` so the two surfaces stay single-sourced.
|
|
417
|
+
*
|
|
418
|
+
* Exit-code policy:
|
|
419
|
+
* - 0 — show / enable / disable / toggle happy paths
|
|
420
|
+
* - 2 — unknown subcommand (e.g. `pugi vim chaos`) or too many args
|
|
421
|
+
*/
|
|
422
|
+
async function dispatchVim(args, flags, _session) {
|
|
423
|
+
const rc = await runVimCommand(args, {
|
|
424
|
+
env: process.env,
|
|
425
|
+
writeOutput: (payload, text) => writeOutput(flags, payload, text),
|
|
426
|
+
});
|
|
427
|
+
if (rc !== 0)
|
|
428
|
+
process.exitCode = rc;
|
|
429
|
+
}
|
|
363
430
|
/**
|
|
364
431
|
* PAVF-7 (2026-05-27): `pugi report --from-error` — bundle the most-
|
|
365
432
|
* recent failed session into a redacted local report so operators can
|
|
@@ -924,6 +991,10 @@ function parseArgs(argv) {
|
|
|
924
991
|
// interactive surface keeps its boxed renderer; opt-in via flag
|
|
925
992
|
// for pipe / script use.
|
|
926
993
|
asciiOnly: false,
|
|
994
|
+
// Leak L24 — `--reset` for `pugi release-notes`. Default off so a
|
|
995
|
+
// bare invocation only surfaces new sections. Opt-in to force the
|
|
996
|
+
// full bundled changelog к re-render (clears the on-disk marker).
|
|
997
|
+
reset: false,
|
|
927
998
|
};
|
|
928
999
|
const args = [];
|
|
929
1000
|
// Leak L22: scan for `--bare` BEFORE the early-return short-circuits
|
|
@@ -1010,6 +1081,14 @@ function parseArgs(argv) {
|
|
|
1010
1081
|
// through to runStickersCommand without per-command argv slicing.
|
|
1011
1082
|
flags.asciiOnly = true;
|
|
1012
1083
|
}
|
|
1084
|
+
else if (arg === '--reset') {
|
|
1085
|
+
// Leak L24 — `pugi release-notes --reset` clears the on-disk
|
|
1086
|
+
// `~/.pugi/.last-seen-version` marker so the full bundled
|
|
1087
|
+
// changelog re-renders. Parsed globally for symmetry with the
|
|
1088
|
+
// rest of the flag grammar; `runReleaseNotesCommand` is the
|
|
1089
|
+
// single consumer today.
|
|
1090
|
+
flags.reset = true;
|
|
1091
|
+
}
|
|
1013
1092
|
else if (arg === '--decompose') {
|
|
1014
1093
|
// α6.8 EXTEND PR1: plan-only flag. Other engine commands ignore
|
|
1015
1094
|
// it. Parsed globally for symmetry with the rest of the flag
|
|
@@ -1368,7 +1447,8 @@ const COMMAND_HELP_BODIES = {
|
|
|
1368
1447
|
'Interactive picker by default (browser OAuth / PAT / env). Non-interactive:',
|
|
1369
1448
|
' --provider device Device-flow OAuth.',
|
|
1370
1449
|
' --provider token --token <jwt> Pass a JWT directly.',
|
|
1371
|
-
' --provider env --
|
|
1450
|
+
' --provider env Read PUGI_API_KEY (or --key) + verify via /api/pugi/health.',
|
|
1451
|
+
' --provider env --key <value> --skip-validate Explicit key, no probe (CI bootstrap).',
|
|
1372
1452
|
],
|
|
1373
1453
|
accounts: [
|
|
1374
1454
|
'pugi accounts — manage stored credentials across endpoints.',
|
|
@@ -1430,6 +1510,32 @@ const COMMAND_HELP_BODIES = {
|
|
|
1430
1510
|
'Useful in shell scripts that need a human-confirm before a destructive',
|
|
1431
1511
|
'step. Exits 0 on yes, 1 on no, 2 on cancel.',
|
|
1432
1512
|
],
|
|
1513
|
+
update: [
|
|
1514
|
+
'pugi update — channel-aware @pugi/cli update check + install.',
|
|
1515
|
+
'',
|
|
1516
|
+
'Polls npm registry dist-tags for a newer @pugi/cli on the configured',
|
|
1517
|
+
'channel (stable / beta / canary). Without flags, prints the install',
|
|
1518
|
+
'command and exits. With --apply, shells out to `npm install -g …`.',
|
|
1519
|
+
'',
|
|
1520
|
+
' --check Non-interactive probe + JSON envelope.',
|
|
1521
|
+
' --channel <name> Switch channel (stable | beta | canary) and probe.',
|
|
1522
|
+
' Persisted to ~/.pugi/config.json::updateChannel.',
|
|
1523
|
+
' --apply Shell out to `npm install -g @pugi/cli@<tag>`',
|
|
1524
|
+
' after a y/n confirmation.',
|
|
1525
|
+
' --yes, -y Skip the confirmation prompt on --apply.',
|
|
1526
|
+
' --json Force JSON envelope (auto-on with --check).',
|
|
1527
|
+
'',
|
|
1528
|
+
'Channel mapping: stable -> npm `latest`, beta -> npm `beta`,',
|
|
1529
|
+
'canary -> npm `next`. Default channel is `beta` (Pugi currently',
|
|
1530
|
+
'ships beta releases only).',
|
|
1531
|
+
'',
|
|
1532
|
+
'Also available as /update from inside the REPL — slash form NEVER',
|
|
1533
|
+
'spawns npm (would corrupt the running binary); it only prints the',
|
|
1534
|
+
'install command for the operator к run after exit.',
|
|
1535
|
+
'',
|
|
1536
|
+
'R2 atomic swap (sprint plan L27) deferred к Phase 2 — npm is the',
|
|
1537
|
+
'only distribution channel today.',
|
|
1538
|
+
],
|
|
1433
1539
|
stickers: [
|
|
1434
1540
|
'pugi stickers — show a Pugi brand sticker (gimmick).',
|
|
1435
1541
|
'',
|
|
@@ -1456,6 +1562,19 @@ const COMMAND_HELP_BODIES = {
|
|
|
1456
1562
|
'',
|
|
1457
1563
|
'Also available as /feedback from inside the REPL.',
|
|
1458
1564
|
],
|
|
1565
|
+
'release-notes': [
|
|
1566
|
+
'pugi release-notes — show what changed since you last upgraded.',
|
|
1567
|
+
'',
|
|
1568
|
+
'Reads the bundled CHANGELOG.md, slices to sections strictly newer than',
|
|
1569
|
+
'~/.pugi/.last-seen-version, renders Markdown to stdout, then bumps the',
|
|
1570
|
+
'last-seen marker to the installed CLI version. Re-running is a no-op',
|
|
1571
|
+
'until you upgrade again.',
|
|
1572
|
+
'',
|
|
1573
|
+
' --json Emit a structured envelope (sections + meta).',
|
|
1574
|
+
' --reset Clear last-seen marker; re-render every section.',
|
|
1575
|
+
'',
|
|
1576
|
+
'Also available as /release-notes from inside the REPL.',
|
|
1577
|
+
],
|
|
1459
1578
|
deploy: [
|
|
1460
1579
|
'pugi deploy — trigger a vendor deployment from the bound Git source.',
|
|
1461
1580
|
'',
|
|
@@ -1582,6 +1701,53 @@ async function doctor(_args, flags, _session) {
|
|
|
1582
1701
|
writeOutput: (payload, text) => writeOutput(flags, payload, text),
|
|
1583
1702
|
});
|
|
1584
1703
|
}
|
|
1704
|
+
/**
|
|
1705
|
+
* `pugi update` — Leak L27 (2026-05-27). Channel-aware npm registry
|
|
1706
|
+
* probe + optional shell-out to `npm install -g @pugi/cli@<tag>`.
|
|
1707
|
+
*
|
|
1708
|
+
* Argument grammar:
|
|
1709
|
+
* pugi update -> probe + offer install command
|
|
1710
|
+
* pugi update --check -> probe + JSON envelope (scripted)
|
|
1711
|
+
* pugi update --channel <name> -> persist channel + probe
|
|
1712
|
+
* pugi update --apply [--yes] -> probe + shell out to npm
|
|
1713
|
+
* pugi update --json -> JSON envelope (any subcommand)
|
|
1714
|
+
*
|
|
1715
|
+
* The handler delegates to `runUpdateCommand` in
|
|
1716
|
+
* `runtime/commands/update.ts` so the in-REPL `/update` slash + the
|
|
1717
|
+
* top-level shell command share one channel-resolution + persistence
|
|
1718
|
+
* + probe surface. Exit codes:
|
|
1719
|
+
*
|
|
1720
|
+
* 0 — happy path (no update OR update completed OR probe-only)
|
|
1721
|
+
* 1 — install / probe failure with structured error
|
|
1722
|
+
* 2 — argument error (unknown flag, unknown channel)
|
|
1723
|
+
*/
|
|
1724
|
+
async function dispatchUpdate(args, flags, _session) {
|
|
1725
|
+
const { parseUpdateArgs, runUpdateCommand, defaultSpawnInstaller } = await import('./commands/update.js');
|
|
1726
|
+
const parsed = parseUpdateArgs(args, { jsonDefault: flags.json });
|
|
1727
|
+
if ('error' in parsed) {
|
|
1728
|
+
writeOutput(flags, { ok: false, error: parsed.error }, parsed.error);
|
|
1729
|
+
process.exitCode = 2;
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
const envelope = await runUpdateCommand({
|
|
1733
|
+
cwd: process.cwd(),
|
|
1734
|
+
home: homedir(),
|
|
1735
|
+
env: process.env,
|
|
1736
|
+
flags: parsed,
|
|
1737
|
+
promptConfirm: async (question) => {
|
|
1738
|
+
const answer = await readSingleChoice(`${question} `);
|
|
1739
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
1740
|
+
},
|
|
1741
|
+
writeOutput: (payload, text) => writeOutput(flags, payload, text),
|
|
1742
|
+
spawnInstaller: defaultSpawnInstaller,
|
|
1743
|
+
});
|
|
1744
|
+
if (!envelope.ok) {
|
|
1745
|
+
// `apply_cancelled_by_operator` is a benign decline; we still
|
|
1746
|
+
// surface a non-zero exit so scripted callers can detect that the
|
|
1747
|
+
// operator did not green-light the install.
|
|
1748
|
+
process.exitCode = 1;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1585
1751
|
/**
|
|
1586
1752
|
* `pugi status` — Leak L34 (2026-05-27). Concise session-state probe
|
|
1587
1753
|
* mirroring Claude Code's `/status`. Distinct from `pugi doctor`
|
|
@@ -1686,6 +1852,31 @@ async function dispatchFeedback(_args, flags, _session) {
|
|
|
1686
1852
|
});
|
|
1687
1853
|
writeOutput(flags, { ok: true, result }, renderFeedbackToast(result));
|
|
1688
1854
|
}
|
|
1855
|
+
/**
|
|
1856
|
+
* `pugi release-notes` — Leak L24 (2026-05-27). Diff between the
|
|
1857
|
+
* last-seen + installed CLI versions, rendered from the bundled
|
|
1858
|
+
* `apps/pugi-cli/CHANGELOG.md`. Bumps `~/.pugi/.last-seen-version`
|
|
1859
|
+
* to the installed version on every successful render so the next
|
|
1860
|
+
* invocation is a no-op until the operator upgrades again.
|
|
1861
|
+
*
|
|
1862
|
+
* The handler stays thin: parser, slicer, and state I/O all live in
|
|
1863
|
+
* `core/release-notes/`. This wrapper just hands ambient state to
|
|
1864
|
+
* `runReleaseNotesCommand` so `--json` keeps producing the same
|
|
1865
|
+
* envelope from both the top-level shell + the in-REPL `/release-notes`
|
|
1866
|
+
* slash dispatcher.
|
|
1867
|
+
*
|
|
1868
|
+
* Always exits 0 — the command is informational, never a gate. Read
|
|
1869
|
+
* failures, missing CHANGELOG, and write failures all degrade to a
|
|
1870
|
+
* structured envelope with a human-readable footer.
|
|
1871
|
+
*/
|
|
1872
|
+
async function releaseNotes(_args, flags, _session) {
|
|
1873
|
+
runReleaseNotesCommand({
|
|
1874
|
+
home: defaultReleaseNotesHome(),
|
|
1875
|
+
json: flags.json,
|
|
1876
|
+
reset: flags.reset,
|
|
1877
|
+
writeOutput: (payload, text) => writeOutput(flags, payload, text),
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1689
1880
|
/**
|
|
1690
1881
|
* Programmatic init scaffolder. Idempotent — every helper call is a
|
|
1691
1882
|
* `*_IfMissing` write, so re-running over an existing .pugi/ workspace
|
|
@@ -4229,7 +4420,7 @@ async function login(args, flags, _session) {
|
|
|
4229
4420
|
if (args.includes('--help') || args.includes('-h')) {
|
|
4230
4421
|
writeOutput(flags, {
|
|
4231
4422
|
command: 'login',
|
|
4232
|
-
usage: 'pugi login [--provider device|token|env] [--token <PAT>] [--token-stdin] [--label <name>] [--api-url <url>]',
|
|
4423
|
+
usage: 'pugi login [--provider device|token|env] [--token <PAT>] [--token-stdin] [--key <value>] [--skip-validate] [--label <name>] [--api-url <url>]',
|
|
4233
4424
|
}, [
|
|
4234
4425
|
'Usage: pugi login [options]',
|
|
4235
4426
|
'',
|
|
@@ -4241,19 +4432,27 @@ async function login(args, flags, _session) {
|
|
|
4241
4432
|
'Non-interactive options:',
|
|
4242
4433
|
' --provider device Run the device-flow login (recommended).',
|
|
4243
4434
|
' --provider token Store an API key passed via --token / --token-stdin / PUGI_LOGIN_TOKEN.',
|
|
4244
|
-
' --provider env
|
|
4435
|
+
' --provider env Read PUGI_API_KEY (or --key) and verify it via /api/pugi/health.',
|
|
4245
4436
|
' --token <PAT> Inline API key (visible in `ps`).',
|
|
4246
4437
|
' --token-stdin Read API key from stdin (gh-CLI style).',
|
|
4438
|
+
' --key <value> Explicit key for --provider env; beats PUGI_API_KEY.',
|
|
4439
|
+
' --skip-validate Skip the /api/pugi/health probe for --provider env (CI bootstrap).',
|
|
4247
4440
|
' --label <name> Short label surfaced in `pugi accounts list`.',
|
|
4248
4441
|
' --api-url <url> Override the Anvil endpoint (self-hosted).',
|
|
4249
4442
|
' --no-device-flow Refuse the device flow; fail fast in CI without a token.',
|
|
4250
4443
|
'',
|
|
4444
|
+
'Environment variables:',
|
|
4445
|
+
' PUGI_API_KEY Read by --provider env. Pass --key to override.',
|
|
4446
|
+
' PUGI_LOGIN_TOKEN Read by --provider token in non-interactive shells.',
|
|
4447
|
+
' PUGI_API_URL Override the Anvil endpoint (same as --api-url).',
|
|
4448
|
+
'',
|
|
4251
4449
|
'Examples:',
|
|
4252
4450
|
' pugi login # interactive picker on a TTY',
|
|
4253
4451
|
' pugi login --provider device # explicit browser OAuth',
|
|
4254
4452
|
' pugi login --provider token --token sk-xx # paste in a key',
|
|
4255
4453
|
' echo $TOKEN | pugi login --provider token --token-stdin',
|
|
4256
|
-
' PUGI_API_KEY=
|
|
4454
|
+
' PUGI_API_KEY=pugi_xxx pugi login --provider env',
|
|
4455
|
+
' pugi login --provider env --key pugi_xxx # explicit key beats env',
|
|
4257
4456
|
].join('\n'));
|
|
4258
4457
|
return;
|
|
4259
4458
|
}
|
|
@@ -4276,6 +4475,11 @@ async function login(args, flags, _session) {
|
|
|
4276
4475
|
const apiUrlOverride = extractApiUrlFlag(args);
|
|
4277
4476
|
const labelFlag = extractLabelFlag(args);
|
|
4278
4477
|
const provider = parseProviderFlag(args);
|
|
4478
|
+
// Leak L35 (2026-05-27): `--key` is the explicit-arg path for
|
|
4479
|
+
// `--provider env`; `--skip-validate` bypasses the /api/pugi/health
|
|
4480
|
+
// probe (CI bootstrap before the network is up).
|
|
4481
|
+
const envExplicitKey = extractKeyFlag(args);
|
|
4482
|
+
const envSkipValidate = args.includes('--skip-validate');
|
|
4279
4483
|
const apiUrl = normalizeApiUrl(apiUrlOverride ?? process.env.PUGI_API_URL ?? DEFAULT_API_URL);
|
|
4280
4484
|
// Path 1: explicit --provider trumps everything else.
|
|
4281
4485
|
if (provider) {
|
|
@@ -4286,6 +4490,8 @@ async function login(args, flags, _session) {
|
|
|
4286
4490
|
explicitToken: tokenFromArgs,
|
|
4287
4491
|
tokenStdinFlag,
|
|
4288
4492
|
noDeviceFlow,
|
|
4493
|
+
envExplicitKey,
|
|
4494
|
+
envSkipValidate,
|
|
4289
4495
|
});
|
|
4290
4496
|
return;
|
|
4291
4497
|
}
|
|
@@ -4333,6 +4539,8 @@ async function login(args, flags, _session) {
|
|
|
4333
4539
|
flags,
|
|
4334
4540
|
label: labelFlag,
|
|
4335
4541
|
noDeviceFlow,
|
|
4542
|
+
envExplicitKey,
|
|
4543
|
+
envSkipValidate,
|
|
4336
4544
|
});
|
|
4337
4545
|
return;
|
|
4338
4546
|
}
|
|
@@ -4551,16 +4759,28 @@ async function dispatchLoginProvider(provider, ctx) {
|
|
|
4551
4759
|
return;
|
|
4552
4760
|
}
|
|
4553
4761
|
case 'env': {
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4762
|
+
// Leak L35 (2026-05-27): resolve the env / --key candidate,
|
|
4763
|
+
// run the local format check, then probe `/api/pugi/health`
|
|
4764
|
+
// BEFORE persisting. A bad token never lands on disk so the
|
|
4765
|
+
// next `pugi <anything>` does not silently 401 against the
|
|
4766
|
+
// cabinet. `--skip-validate` opts out for CI bootstrap.
|
|
4767
|
+
const resolved = await resolveAndValidateEnvLogin({
|
|
4768
|
+
apiUrl: ctx.apiUrl,
|
|
4769
|
+
explicitKey: ctx.envExplicitKey,
|
|
4770
|
+
env: process.env,
|
|
4771
|
+
skipValidate: ctx.envSkipValidate ?? false,
|
|
4772
|
+
});
|
|
4773
|
+
if (resolved.kind !== 'ok') {
|
|
4774
|
+
reportEnvLoginFailure(resolved, ctx.flags);
|
|
4775
|
+
return;
|
|
4557
4776
|
}
|
|
4558
4777
|
storeAndAnnounceToken({
|
|
4559
4778
|
apiUrl: ctx.apiUrl,
|
|
4560
|
-
apiKey:
|
|
4779
|
+
apiKey: resolved.token,
|
|
4561
4780
|
label: ctx.label,
|
|
4562
4781
|
source: 'env',
|
|
4563
4782
|
flags: ctx.flags,
|
|
4783
|
+
validatedLatencyMs: resolved.latencyMs > 0 ? resolved.latencyMs : undefined,
|
|
4564
4784
|
});
|
|
4565
4785
|
return;
|
|
4566
4786
|
}
|
|
@@ -4579,6 +4799,15 @@ function storeAndAnnounceToken(input) {
|
|
|
4579
4799
|
label: input.label,
|
|
4580
4800
|
source: input.source,
|
|
4581
4801
|
});
|
|
4802
|
+
const textLines = [
|
|
4803
|
+
`Pugi logged in for ${record.apiUrl}`,
|
|
4804
|
+
`Method: ${input.source}${record.label ? ` (${record.label})` : ''}`,
|
|
4805
|
+
`Token: ${maskApiKey(record.apiKey)}`,
|
|
4806
|
+
];
|
|
4807
|
+
if (typeof input.validatedLatencyMs === 'number') {
|
|
4808
|
+
textLines.push(`Verified via /api/pugi/health in ${input.validatedLatencyMs}ms`);
|
|
4809
|
+
}
|
|
4810
|
+
textLines.push('Stored at ~/.pugi/credentials.json (mode 0600). Run `pugi whoami` to verify.');
|
|
4582
4811
|
writeOutput(input.flags, {
|
|
4583
4812
|
status: 'logged_in',
|
|
4584
4813
|
apiUrl: record.apiUrl,
|
|
@@ -4586,12 +4815,55 @@ function storeAndAnnounceToken(input) {
|
|
|
4586
4815
|
label: record.label ?? null,
|
|
4587
4816
|
createdAt: record.createdAt,
|
|
4588
4817
|
source: input.source,
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4818
|
+
...(typeof input.validatedLatencyMs === 'number'
|
|
4819
|
+
? { validatedLatencyMs: input.validatedLatencyMs }
|
|
4820
|
+
: {}),
|
|
4821
|
+
}, textLines.join('\n'));
|
|
4822
|
+
}
|
|
4823
|
+
/**
|
|
4824
|
+
* Render a typed `EnvLoginFailure` from `resolveAndValidateEnvLogin`
|
|
4825
|
+
* onto the surrounding CLI surface. Maps the failure kind to:
|
|
4826
|
+
* - an exit code (1 by default; 2 for invalid format so a CI step
|
|
4827
|
+
* can disambiguate "missing key" vs "key shape wrong" without
|
|
4828
|
+
* parsing stderr; 4 for network / server errors so retry logic
|
|
4829
|
+
* can distinguish transient failures from credential failures)
|
|
4830
|
+
* - a structured JSON payload for `--json` consumers
|
|
4831
|
+
* - a human-readable stderr line for the interactive path
|
|
4832
|
+
*
|
|
4833
|
+
* The token itself is never echoed — only the validator's own message
|
|
4834
|
+
* (which the env-provider module composed without the secret in it).
|
|
4835
|
+
*/
|
|
4836
|
+
function reportEnvLoginFailure(failure, flags) {
|
|
4837
|
+
const exitCode = (() => {
|
|
4838
|
+
switch (failure.kind) {
|
|
4839
|
+
case 'missing':
|
|
4840
|
+
return 1;
|
|
4841
|
+
case 'invalid-format':
|
|
4842
|
+
return 2;
|
|
4843
|
+
case 'unauthorized':
|
|
4844
|
+
return 3;
|
|
4845
|
+
case 'network-error':
|
|
4846
|
+
case 'server-error':
|
|
4847
|
+
return 4;
|
|
4848
|
+
case 'unexpected-status':
|
|
4849
|
+
return 5;
|
|
4850
|
+
default: {
|
|
4851
|
+
const exhaustive = failure;
|
|
4852
|
+
return Number(exhaustive) || 1;
|
|
4853
|
+
}
|
|
4854
|
+
}
|
|
4855
|
+
})();
|
|
4856
|
+
const payload = {
|
|
4857
|
+
status: 'login_failed',
|
|
4858
|
+
kind: failure.kind,
|
|
4859
|
+
message: failure.message,
|
|
4860
|
+
};
|
|
4861
|
+
if ('status' in failure)
|
|
4862
|
+
payload.httpStatus = failure.status;
|
|
4863
|
+
if ('cause' in failure && failure.cause)
|
|
4864
|
+
payload.cause = failure.cause;
|
|
4865
|
+
writeOutput(flags, payload, failure.message);
|
|
4866
|
+
process.exitCode = exitCode;
|
|
4595
4867
|
}
|
|
4596
4868
|
/**
|
|
4597
4869
|
* OAuth 2.0 Device Authorization Grant client (RFC 8628). Renders
|
|
@@ -5347,6 +5619,17 @@ function extractApiUrlFlag(args) {
|
|
|
5347
5619
|
function extractLabelFlag(args) {
|
|
5348
5620
|
return extractNamedFlagValue(args, 'label');
|
|
5349
5621
|
}
|
|
5622
|
+
/**
|
|
5623
|
+
* `pugi login --provider env --key <value>` — explicit key arg that
|
|
5624
|
+
* beats `PUGI_API_KEY` env. Same precedence rule as `gh auth login
|
|
5625
|
+
* --with-token`, `aws configure set`, and `pugi config`: the most
|
|
5626
|
+
* specific operator intent (a typed flag) overrides the ambient
|
|
5627
|
+
* environment so an operator can override a stale `PUGI_API_KEY`
|
|
5628
|
+
* from their shell rc without unsetting it first.
|
|
5629
|
+
*/
|
|
5630
|
+
function extractKeyFlag(args) {
|
|
5631
|
+
return extractNamedFlagValue(args, 'key');
|
|
5632
|
+
}
|
|
5350
5633
|
/**
|
|
5351
5634
|
* `pugi jobs` — surface the persistent JobRegistry on the CLI.
|
|
5352
5635
|
* Sprint α5.9 (ADR-0056 PR-PUGI-CLI-M1-GAP-J). Subcommand parsing
|
|
@@ -50,6 +50,7 @@ import { probeConfig } from '../../core/diagnostics/probes/config.js';
|
|
|
50
50
|
import { probeSession } from '../../core/diagnostics/probes/session.js';
|
|
51
51
|
import { probeDenialTracking } from '../../core/diagnostics/probes/denial-tracking.js';
|
|
52
52
|
import { probeBareMode } from '../../core/diagnostics/probes/bare-mode.js';
|
|
53
|
+
import { probePugiMdHierarchy } from '../../core/diagnostics/probes/pugi-md.js';
|
|
53
54
|
/**
|
|
54
55
|
* Default API URL when no PUGI_API_URL env override is set. Mirrors
|
|
55
56
|
* the constant in `core/credentials.ts` (kept local to avoid an
|
|
@@ -214,6 +215,18 @@ export function buildDefaultProbes(ctx, options = {}) {
|
|
|
214
215
|
name: 'BARE MODE',
|
|
215
216
|
run: async () => probeBareMode({ env: ctx.env }),
|
|
216
217
|
},
|
|
218
|
+
// Leak L32 (2026-05-27): PUGI.md HIERARCHY row. Reports how many
|
|
219
|
+
// ambient `PUGI.md` / `CLAUDE.md` files the cwd → homedir walk
|
|
220
|
+
// discovered, and the closest path. `skipped` when bare mode is
|
|
221
|
+
// active (walk disabled) or zero files found.
|
|
222
|
+
{
|
|
223
|
+
name: 'PUGI.md HIERARCHY',
|
|
224
|
+
run: async () => probePugiMdHierarchy({
|
|
225
|
+
cwd: ctx.cwd,
|
|
226
|
+
homedir: ctx.home,
|
|
227
|
+
env: ctx.env,
|
|
228
|
+
}),
|
|
229
|
+
},
|
|
217
230
|
];
|
|
218
231
|
return probes;
|
|
219
232
|
}
|