@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,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leak L26 (2026-05-27) — Vim mode preference persistence.
|
|
3
|
+
*
|
|
4
|
+
* Single-tier storage (user-only, `~/.pugi/config.json`):
|
|
5
|
+
*
|
|
6
|
+
* `{ ..., "vimMode": true }`
|
|
7
|
+
*
|
|
8
|
+
* The leak research shows Claude Code persists `/vim` as a user-level
|
|
9
|
+
* preference (not per-workspace) — vim-style editing is a body-memory
|
|
10
|
+
* trait of the operator, not a per-repo concern. We match that.
|
|
11
|
+
*
|
|
12
|
+
* Reader tolerances mirror `core/output-style/state.ts`:
|
|
13
|
+
* - missing file → returns `false` (vim mode off, the default)
|
|
14
|
+
* - empty file → returns `false`
|
|
15
|
+
* - malformed JSON → returns `false` (REPL must not crash on a
|
|
16
|
+
* hand-edited config)
|
|
17
|
+
* - non-boolean → returns `false`
|
|
18
|
+
*
|
|
19
|
+
* Writer is a read-modify-write — neighbouring keys (`outputStyle`,
|
|
20
|
+
* `permissionMode`, …) survive a vim-mode flip.
|
|
21
|
+
*
|
|
22
|
+
* File mode 0o600 mirrors `core/output-style/state.ts` so the same
|
|
23
|
+
* config does not silently downgrade its permission bits depending on
|
|
24
|
+
* which helper last touched it.
|
|
25
|
+
*/
|
|
26
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
|
|
27
|
+
import { homedir } from 'node:os';
|
|
28
|
+
import { dirname, resolve } from 'node:path';
|
|
29
|
+
/**
|
|
30
|
+
* Env override for `~/.pugi`. Re-uses the same `PUGI_HOME` knob as
|
|
31
|
+
* the output-style helpers so test sandboxes are uniform.
|
|
32
|
+
*/
|
|
33
|
+
export const PUGI_HOME_ENV = 'PUGI_HOME';
|
|
34
|
+
/** Default when the file is missing / malformed / does not set the key. */
|
|
35
|
+
export const VIM_MODE_DEFAULT = false;
|
|
36
|
+
/**
|
|
37
|
+
* Read the persisted vim-mode preference. Pure read, never throws —
|
|
38
|
+
* every IO failure path degrades to `VIM_MODE_DEFAULT`.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveVimMode(io = {}) {
|
|
41
|
+
const config = readConfigFile(userConfigPath(io.env ?? process.env));
|
|
42
|
+
return typeof config.vimMode === 'boolean' ? config.vimMode : VIM_MODE_DEFAULT;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Write the vim-mode preference. Read-modify-write so neighbouring
|
|
46
|
+
* config keys survive (`outputStyle`, `permissionMode`, …).
|
|
47
|
+
*/
|
|
48
|
+
export function setVimMode(value, io = {}) {
|
|
49
|
+
const path = userConfigPath(io.env ?? process.env);
|
|
50
|
+
const config = readConfigFile(path);
|
|
51
|
+
config.vimMode = value;
|
|
52
|
+
writeConfigFile(path, config);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Path resolver exported for the spec; production callers should use
|
|
56
|
+
* `resolveVimMode` / `setVimMode`.
|
|
57
|
+
*/
|
|
58
|
+
export function userConfigPath(env = process.env) {
|
|
59
|
+
const home = env[PUGI_HOME_ENV] ?? resolve(homedir(), '.pugi');
|
|
60
|
+
return resolve(home, 'config.json');
|
|
61
|
+
}
|
|
62
|
+
function readConfigFile(path) {
|
|
63
|
+
if (!existsSync(path))
|
|
64
|
+
return {};
|
|
65
|
+
let raw;
|
|
66
|
+
try {
|
|
67
|
+
raw = readFileSync(path, 'utf8');
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
if (raw.trim().length === 0)
|
|
73
|
+
return {};
|
|
74
|
+
let parsed;
|
|
75
|
+
try {
|
|
76
|
+
parsed = JSON.parse(raw);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return {};
|
|
80
|
+
}
|
|
81
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
82
|
+
return {};
|
|
83
|
+
return parsed;
|
|
84
|
+
}
|
|
85
|
+
function writeConfigFile(path, config) {
|
|
86
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
87
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, {
|
|
88
|
+
encoding: 'utf8',
|
|
89
|
+
mode: 0o600,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=state.js.map
|
package/dist/runtime/cli.js
CHANGED
|
@@ -22,17 +22,21 @@ import { PUGI_TAGLINE } from '@pugi/personas';
|
|
|
22
22
|
import { resolveRoster, renderRosterTable } from './commands/roster.js';
|
|
23
23
|
import { runDelegateCommand } from './commands/delegate.js';
|
|
24
24
|
import { clearApiKey, DEFAULT_API_URL, listStoredCredentials, maskApiKey, normalizeApiUrl, purgeAllCredentials, readCredentialsFile, resolveActiveCredential, storeApiKey, switchActiveAccount, } from '../core/credentials.js';
|
|
25
|
+
import { resolveAndValidateEnvLogin, } from '../core/auth/env-provider.js';
|
|
25
26
|
import { runDeployCommand } from '../commands/deploy.js';
|
|
26
27
|
import { runJobsCommand } from '../commands/jobs.js';
|
|
27
28
|
import { runConfigCommand } from './commands/config.js';
|
|
28
29
|
import { runStyleCommand } from './commands/style.js';
|
|
30
|
+
import { runThemeCommand } from './commands/theme.js';
|
|
29
31
|
import { runOnboardingCommand } from './commands/onboarding.js';
|
|
32
|
+
import { runVimCommand } from './commands/vim.js';
|
|
30
33
|
import { isOnboarded } from '../core/onboarding/marker.js';
|
|
31
34
|
import { runPrivacyCommand } from './commands/privacy.js';
|
|
32
35
|
import { runReport } from './commands/report.js';
|
|
33
36
|
import { runDoctorCommand, defaultHome as defaultDoctorHome } from './commands/doctor.js';
|
|
34
37
|
import { runStatusCommand, defaultStatusHome, } from './commands/status.js';
|
|
35
38
|
import { runStickersCommand } from './commands/stickers.js';
|
|
39
|
+
import { runReleaseNotesCommand, defaultReleaseNotesHome, } from './commands/release-notes.js';
|
|
36
40
|
import { runUndoCommand } from './commands/undo.js';
|
|
37
41
|
import { runCompactCommand } from './commands/compact.js';
|
|
38
42
|
import { runBudgetCommand } from './commands/budget.js';
|
|
@@ -104,6 +108,12 @@ const handlers = {
|
|
|
104
108
|
plan: dispatchPlan,
|
|
105
109
|
'plan-review': dispatchPlanReview,
|
|
106
110
|
privacy: dispatchPrivacy,
|
|
111
|
+
// L24 (2026-05-27): `pugi release-notes` shows the bundled CHANGELOG
|
|
112
|
+
// diff between the operator's last-seen version + installed version.
|
|
113
|
+
// The slash counterpart `/release-notes` shares this handler via the
|
|
114
|
+
// shared `runReleaseNotesCommand` runner.
|
|
115
|
+
'release-notes': releaseNotes,
|
|
116
|
+
releaseNotes,
|
|
107
117
|
// PAVF-7 (2026-05-27): `pugi report --from-error` captures the
|
|
108
118
|
// most-recent failed session as a redacted bundle so operators can
|
|
109
119
|
// file clean bug reports without manual log-grepping.
|
|
@@ -122,11 +132,19 @@ const handlers = {
|
|
|
122
132
|
feedback: dispatchFeedback,
|
|
123
133
|
sync,
|
|
124
134
|
style: dispatchStyle,
|
|
135
|
+
// Leak L30 (2026-05-27): `pugi theme` flips the local TUI color
|
|
136
|
+
// palette (orthogonal to `pugi style` — that one steers engine
|
|
137
|
+
// prose register). 4 presets: default / dark / light / colorblind.
|
|
138
|
+
theme: dispatchTheme,
|
|
125
139
|
// Leak L25 (2026-05-27): `pugi onboarding` walks the new operator
|
|
126
140
|
// through auth / mode / style / MCP / telemetry. Idempotent;
|
|
127
141
|
// `--reset` clears the marker file so the bare-invocation hint
|
|
128
142
|
// re-arms without nuking persisted defaults.
|
|
129
143
|
onboarding: dispatchOnboarding,
|
|
144
|
+
// Leak L26 (2026-05-27): `pugi vim` toggles vim-style modal editing
|
|
145
|
+
// in the REPL input buffer. Bare invocation toggles, `on`/`off`
|
|
146
|
+
// sets explicitly; preference persists in ~/.pugi/config.json.
|
|
147
|
+
vim: dispatchVim,
|
|
130
148
|
undo: dispatchUndo,
|
|
131
149
|
compact: dispatchCompact,
|
|
132
150
|
// L19 (2026-05-27): `pugi usage` is an alias of `pugi cost` — same
|
|
@@ -330,6 +348,29 @@ async function dispatchStyle(args, flags, _session) {
|
|
|
330
348
|
if (rc !== 0)
|
|
331
349
|
process.exitCode = rc;
|
|
332
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* Leak L30 (2026-05-27) — `pugi theme` top-level dispatcher.
|
|
353
|
+
*
|
|
354
|
+
* Forwards to the shared `runThemeCommand` runner. The REPL `/theme`
|
|
355
|
+
* slash uses the same runner via a dynamic import inside
|
|
356
|
+
* `core/repl/session.ts` so the two surfaces stay single-sourced.
|
|
357
|
+
*
|
|
358
|
+
* Exit-code policy mirrors `dispatchStyle`:
|
|
359
|
+
* - 0 — show / switch / reset / list happy paths
|
|
360
|
+
* - 1 — unknown preset slug
|
|
361
|
+
* - 2 — conflicting flags (`--reset` + positional / `--reset --persist`)
|
|
362
|
+
*
|
|
363
|
+
* The runner returns the code; we attach it to `process.exitCode` so
|
|
364
|
+
* subsequent dispatch wrappers do not clobber it on success.
|
|
365
|
+
*/
|
|
366
|
+
async function dispatchTheme(args, flags, _session) {
|
|
367
|
+
const rc = await runThemeCommand(args, {
|
|
368
|
+
workspaceRoot: process.cwd(),
|
|
369
|
+
writeOutput: (payload, text) => writeOutput(flags, payload, text),
|
|
370
|
+
});
|
|
371
|
+
if (rc !== 0)
|
|
372
|
+
process.exitCode = rc;
|
|
373
|
+
}
|
|
333
374
|
/**
|
|
334
375
|
* Leak L25 (2026-05-27) — `pugi onboarding` top-level dispatcher.
|
|
335
376
|
*
|
|
@@ -360,6 +401,25 @@ async function dispatchOnboarding(args, flags, _session) {
|
|
|
360
401
|
if (rc !== 0)
|
|
361
402
|
process.exitCode = rc;
|
|
362
403
|
}
|
|
404
|
+
/**
|
|
405
|
+
* Leak L26 (2026-05-27) — `pugi vim` top-level dispatcher.
|
|
406
|
+
*
|
|
407
|
+
* Forwards to the shared `runVimCommand` runner. The REPL `/vim` slash
|
|
408
|
+
* uses the same runner via a dynamic import inside
|
|
409
|
+
* `core/repl/session.ts` so the two surfaces stay single-sourced.
|
|
410
|
+
*
|
|
411
|
+
* Exit-code policy:
|
|
412
|
+
* - 0 — show / enable / disable / toggle happy paths
|
|
413
|
+
* - 2 — unknown subcommand (e.g. `pugi vim chaos`) or too many args
|
|
414
|
+
*/
|
|
415
|
+
async function dispatchVim(args, flags, _session) {
|
|
416
|
+
const rc = await runVimCommand(args, {
|
|
417
|
+
env: process.env,
|
|
418
|
+
writeOutput: (payload, text) => writeOutput(flags, payload, text),
|
|
419
|
+
});
|
|
420
|
+
if (rc !== 0)
|
|
421
|
+
process.exitCode = rc;
|
|
422
|
+
}
|
|
363
423
|
/**
|
|
364
424
|
* PAVF-7 (2026-05-27): `pugi report --from-error` — bundle the most-
|
|
365
425
|
* recent failed session into a redacted local report so operators can
|
|
@@ -924,6 +984,10 @@ function parseArgs(argv) {
|
|
|
924
984
|
// interactive surface keeps its boxed renderer; opt-in via flag
|
|
925
985
|
// for pipe / script use.
|
|
926
986
|
asciiOnly: false,
|
|
987
|
+
// Leak L24 — `--reset` for `pugi release-notes`. Default off so a
|
|
988
|
+
// bare invocation only surfaces new sections. Opt-in to force the
|
|
989
|
+
// full bundled changelog к re-render (clears the on-disk marker).
|
|
990
|
+
reset: false,
|
|
927
991
|
};
|
|
928
992
|
const args = [];
|
|
929
993
|
// Leak L22: scan for `--bare` BEFORE the early-return short-circuits
|
|
@@ -1010,6 +1074,14 @@ function parseArgs(argv) {
|
|
|
1010
1074
|
// through to runStickersCommand without per-command argv slicing.
|
|
1011
1075
|
flags.asciiOnly = true;
|
|
1012
1076
|
}
|
|
1077
|
+
else if (arg === '--reset') {
|
|
1078
|
+
// Leak L24 — `pugi release-notes --reset` clears the on-disk
|
|
1079
|
+
// `~/.pugi/.last-seen-version` marker so the full bundled
|
|
1080
|
+
// changelog re-renders. Parsed globally for symmetry with the
|
|
1081
|
+
// rest of the flag grammar; `runReleaseNotesCommand` is the
|
|
1082
|
+
// single consumer today.
|
|
1083
|
+
flags.reset = true;
|
|
1084
|
+
}
|
|
1013
1085
|
else if (arg === '--decompose') {
|
|
1014
1086
|
// α6.8 EXTEND PR1: plan-only flag. Other engine commands ignore
|
|
1015
1087
|
// it. Parsed globally for symmetry with the rest of the flag
|
|
@@ -1368,7 +1440,8 @@ const COMMAND_HELP_BODIES = {
|
|
|
1368
1440
|
'Interactive picker by default (browser OAuth / PAT / env). Non-interactive:',
|
|
1369
1441
|
' --provider device Device-flow OAuth.',
|
|
1370
1442
|
' --provider token --token <jwt> Pass a JWT directly.',
|
|
1371
|
-
' --provider env --
|
|
1443
|
+
' --provider env Read PUGI_API_KEY (or --key) + verify via /api/pugi/health.',
|
|
1444
|
+
' --provider env --key <value> --skip-validate Explicit key, no probe (CI bootstrap).',
|
|
1372
1445
|
],
|
|
1373
1446
|
accounts: [
|
|
1374
1447
|
'pugi accounts — manage stored credentials across endpoints.',
|
|
@@ -1456,6 +1529,19 @@ const COMMAND_HELP_BODIES = {
|
|
|
1456
1529
|
'',
|
|
1457
1530
|
'Also available as /feedback from inside the REPL.',
|
|
1458
1531
|
],
|
|
1532
|
+
'release-notes': [
|
|
1533
|
+
'pugi release-notes — show what changed since you last upgraded.',
|
|
1534
|
+
'',
|
|
1535
|
+
'Reads the bundled CHANGELOG.md, slices to sections strictly newer than',
|
|
1536
|
+
'~/.pugi/.last-seen-version, renders Markdown to stdout, then bumps the',
|
|
1537
|
+
'last-seen marker to the installed CLI version. Re-running is a no-op',
|
|
1538
|
+
'until you upgrade again.',
|
|
1539
|
+
'',
|
|
1540
|
+
' --json Emit a structured envelope (sections + meta).',
|
|
1541
|
+
' --reset Clear last-seen marker; re-render every section.',
|
|
1542
|
+
'',
|
|
1543
|
+
'Also available as /release-notes from inside the REPL.',
|
|
1544
|
+
],
|
|
1459
1545
|
deploy: [
|
|
1460
1546
|
'pugi deploy — trigger a vendor deployment from the bound Git source.',
|
|
1461
1547
|
'',
|
|
@@ -1686,6 +1772,31 @@ async function dispatchFeedback(_args, flags, _session) {
|
|
|
1686
1772
|
});
|
|
1687
1773
|
writeOutput(flags, { ok: true, result }, renderFeedbackToast(result));
|
|
1688
1774
|
}
|
|
1775
|
+
/**
|
|
1776
|
+
* `pugi release-notes` — Leak L24 (2026-05-27). Diff between the
|
|
1777
|
+
* last-seen + installed CLI versions, rendered from the bundled
|
|
1778
|
+
* `apps/pugi-cli/CHANGELOG.md`. Bumps `~/.pugi/.last-seen-version`
|
|
1779
|
+
* to the installed version on every successful render so the next
|
|
1780
|
+
* invocation is a no-op until the operator upgrades again.
|
|
1781
|
+
*
|
|
1782
|
+
* The handler stays thin: parser, slicer, and state I/O all live in
|
|
1783
|
+
* `core/release-notes/`. This wrapper just hands ambient state to
|
|
1784
|
+
* `runReleaseNotesCommand` so `--json` keeps producing the same
|
|
1785
|
+
* envelope from both the top-level shell + the in-REPL `/release-notes`
|
|
1786
|
+
* slash dispatcher.
|
|
1787
|
+
*
|
|
1788
|
+
* Always exits 0 — the command is informational, never a gate. Read
|
|
1789
|
+
* failures, missing CHANGELOG, and write failures all degrade to a
|
|
1790
|
+
* structured envelope with a human-readable footer.
|
|
1791
|
+
*/
|
|
1792
|
+
async function releaseNotes(_args, flags, _session) {
|
|
1793
|
+
runReleaseNotesCommand({
|
|
1794
|
+
home: defaultReleaseNotesHome(),
|
|
1795
|
+
json: flags.json,
|
|
1796
|
+
reset: flags.reset,
|
|
1797
|
+
writeOutput: (payload, text) => writeOutput(flags, payload, text),
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1689
1800
|
/**
|
|
1690
1801
|
* Programmatic init scaffolder. Idempotent — every helper call is a
|
|
1691
1802
|
* `*_IfMissing` write, so re-running over an existing .pugi/ workspace
|
|
@@ -4229,7 +4340,7 @@ async function login(args, flags, _session) {
|
|
|
4229
4340
|
if (args.includes('--help') || args.includes('-h')) {
|
|
4230
4341
|
writeOutput(flags, {
|
|
4231
4342
|
command: 'login',
|
|
4232
|
-
usage: 'pugi login [--provider device|token|env] [--token <PAT>] [--token-stdin] [--label <name>] [--api-url <url>]',
|
|
4343
|
+
usage: 'pugi login [--provider device|token|env] [--token <PAT>] [--token-stdin] [--key <value>] [--skip-validate] [--label <name>] [--api-url <url>]',
|
|
4233
4344
|
}, [
|
|
4234
4345
|
'Usage: pugi login [options]',
|
|
4235
4346
|
'',
|
|
@@ -4241,19 +4352,27 @@ async function login(args, flags, _session) {
|
|
|
4241
4352
|
'Non-interactive options:',
|
|
4242
4353
|
' --provider device Run the device-flow login (recommended).',
|
|
4243
4354
|
' --provider token Store an API key passed via --token / --token-stdin / PUGI_LOGIN_TOKEN.',
|
|
4244
|
-
' --provider env
|
|
4355
|
+
' --provider env Read PUGI_API_KEY (or --key) and verify it via /api/pugi/health.',
|
|
4245
4356
|
' --token <PAT> Inline API key (visible in `ps`).',
|
|
4246
4357
|
' --token-stdin Read API key from stdin (gh-CLI style).',
|
|
4358
|
+
' --key <value> Explicit key for --provider env; beats PUGI_API_KEY.',
|
|
4359
|
+
' --skip-validate Skip the /api/pugi/health probe for --provider env (CI bootstrap).',
|
|
4247
4360
|
' --label <name> Short label surfaced in `pugi accounts list`.',
|
|
4248
4361
|
' --api-url <url> Override the Anvil endpoint (self-hosted).',
|
|
4249
4362
|
' --no-device-flow Refuse the device flow; fail fast in CI without a token.',
|
|
4250
4363
|
'',
|
|
4364
|
+
'Environment variables:',
|
|
4365
|
+
' PUGI_API_KEY Read by --provider env. Pass --key to override.',
|
|
4366
|
+
' PUGI_LOGIN_TOKEN Read by --provider token in non-interactive shells.',
|
|
4367
|
+
' PUGI_API_URL Override the Anvil endpoint (same as --api-url).',
|
|
4368
|
+
'',
|
|
4251
4369
|
'Examples:',
|
|
4252
4370
|
' pugi login # interactive picker on a TTY',
|
|
4253
4371
|
' pugi login --provider device # explicit browser OAuth',
|
|
4254
4372
|
' pugi login --provider token --token sk-xx # paste in a key',
|
|
4255
4373
|
' echo $TOKEN | pugi login --provider token --token-stdin',
|
|
4256
|
-
' PUGI_API_KEY=
|
|
4374
|
+
' PUGI_API_KEY=pugi_xxx pugi login --provider env',
|
|
4375
|
+
' pugi login --provider env --key pugi_xxx # explicit key beats env',
|
|
4257
4376
|
].join('\n'));
|
|
4258
4377
|
return;
|
|
4259
4378
|
}
|
|
@@ -4276,6 +4395,11 @@ async function login(args, flags, _session) {
|
|
|
4276
4395
|
const apiUrlOverride = extractApiUrlFlag(args);
|
|
4277
4396
|
const labelFlag = extractLabelFlag(args);
|
|
4278
4397
|
const provider = parseProviderFlag(args);
|
|
4398
|
+
// Leak L35 (2026-05-27): `--key` is the explicit-arg path for
|
|
4399
|
+
// `--provider env`; `--skip-validate` bypasses the /api/pugi/health
|
|
4400
|
+
// probe (CI bootstrap before the network is up).
|
|
4401
|
+
const envExplicitKey = extractKeyFlag(args);
|
|
4402
|
+
const envSkipValidate = args.includes('--skip-validate');
|
|
4279
4403
|
const apiUrl = normalizeApiUrl(apiUrlOverride ?? process.env.PUGI_API_URL ?? DEFAULT_API_URL);
|
|
4280
4404
|
// Path 1: explicit --provider trumps everything else.
|
|
4281
4405
|
if (provider) {
|
|
@@ -4286,6 +4410,8 @@ async function login(args, flags, _session) {
|
|
|
4286
4410
|
explicitToken: tokenFromArgs,
|
|
4287
4411
|
tokenStdinFlag,
|
|
4288
4412
|
noDeviceFlow,
|
|
4413
|
+
envExplicitKey,
|
|
4414
|
+
envSkipValidate,
|
|
4289
4415
|
});
|
|
4290
4416
|
return;
|
|
4291
4417
|
}
|
|
@@ -4333,6 +4459,8 @@ async function login(args, flags, _session) {
|
|
|
4333
4459
|
flags,
|
|
4334
4460
|
label: labelFlag,
|
|
4335
4461
|
noDeviceFlow,
|
|
4462
|
+
envExplicitKey,
|
|
4463
|
+
envSkipValidate,
|
|
4336
4464
|
});
|
|
4337
4465
|
return;
|
|
4338
4466
|
}
|
|
@@ -4551,16 +4679,28 @@ async function dispatchLoginProvider(provider, ctx) {
|
|
|
4551
4679
|
return;
|
|
4552
4680
|
}
|
|
4553
4681
|
case 'env': {
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4682
|
+
// Leak L35 (2026-05-27): resolve the env / --key candidate,
|
|
4683
|
+
// run the local format check, then probe `/api/pugi/health`
|
|
4684
|
+
// BEFORE persisting. A bad token never lands on disk so the
|
|
4685
|
+
// next `pugi <anything>` does not silently 401 against the
|
|
4686
|
+
// cabinet. `--skip-validate` opts out for CI bootstrap.
|
|
4687
|
+
const resolved = await resolveAndValidateEnvLogin({
|
|
4688
|
+
apiUrl: ctx.apiUrl,
|
|
4689
|
+
explicitKey: ctx.envExplicitKey,
|
|
4690
|
+
env: process.env,
|
|
4691
|
+
skipValidate: ctx.envSkipValidate ?? false,
|
|
4692
|
+
});
|
|
4693
|
+
if (resolved.kind !== 'ok') {
|
|
4694
|
+
reportEnvLoginFailure(resolved, ctx.flags);
|
|
4695
|
+
return;
|
|
4557
4696
|
}
|
|
4558
4697
|
storeAndAnnounceToken({
|
|
4559
4698
|
apiUrl: ctx.apiUrl,
|
|
4560
|
-
apiKey:
|
|
4699
|
+
apiKey: resolved.token,
|
|
4561
4700
|
label: ctx.label,
|
|
4562
4701
|
source: 'env',
|
|
4563
4702
|
flags: ctx.flags,
|
|
4703
|
+
validatedLatencyMs: resolved.latencyMs > 0 ? resolved.latencyMs : undefined,
|
|
4564
4704
|
});
|
|
4565
4705
|
return;
|
|
4566
4706
|
}
|
|
@@ -4579,6 +4719,15 @@ function storeAndAnnounceToken(input) {
|
|
|
4579
4719
|
label: input.label,
|
|
4580
4720
|
source: input.source,
|
|
4581
4721
|
});
|
|
4722
|
+
const textLines = [
|
|
4723
|
+
`Pugi logged in for ${record.apiUrl}`,
|
|
4724
|
+
`Method: ${input.source}${record.label ? ` (${record.label})` : ''}`,
|
|
4725
|
+
`Token: ${maskApiKey(record.apiKey)}`,
|
|
4726
|
+
];
|
|
4727
|
+
if (typeof input.validatedLatencyMs === 'number') {
|
|
4728
|
+
textLines.push(`Verified via /api/pugi/health in ${input.validatedLatencyMs}ms`);
|
|
4729
|
+
}
|
|
4730
|
+
textLines.push('Stored at ~/.pugi/credentials.json (mode 0600). Run `pugi whoami` to verify.');
|
|
4582
4731
|
writeOutput(input.flags, {
|
|
4583
4732
|
status: 'logged_in',
|
|
4584
4733
|
apiUrl: record.apiUrl,
|
|
@@ -4586,12 +4735,55 @@ function storeAndAnnounceToken(input) {
|
|
|
4586
4735
|
label: record.label ?? null,
|
|
4587
4736
|
createdAt: record.createdAt,
|
|
4588
4737
|
source: input.source,
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4738
|
+
...(typeof input.validatedLatencyMs === 'number'
|
|
4739
|
+
? { validatedLatencyMs: input.validatedLatencyMs }
|
|
4740
|
+
: {}),
|
|
4741
|
+
}, textLines.join('\n'));
|
|
4742
|
+
}
|
|
4743
|
+
/**
|
|
4744
|
+
* Render a typed `EnvLoginFailure` from `resolveAndValidateEnvLogin`
|
|
4745
|
+
* onto the surrounding CLI surface. Maps the failure kind to:
|
|
4746
|
+
* - an exit code (1 by default; 2 for invalid format so a CI step
|
|
4747
|
+
* can disambiguate "missing key" vs "key shape wrong" without
|
|
4748
|
+
* parsing stderr; 4 for network / server errors so retry logic
|
|
4749
|
+
* can distinguish transient failures from credential failures)
|
|
4750
|
+
* - a structured JSON payload for `--json` consumers
|
|
4751
|
+
* - a human-readable stderr line for the interactive path
|
|
4752
|
+
*
|
|
4753
|
+
* The token itself is never echoed — only the validator's own message
|
|
4754
|
+
* (which the env-provider module composed without the secret in it).
|
|
4755
|
+
*/
|
|
4756
|
+
function reportEnvLoginFailure(failure, flags) {
|
|
4757
|
+
const exitCode = (() => {
|
|
4758
|
+
switch (failure.kind) {
|
|
4759
|
+
case 'missing':
|
|
4760
|
+
return 1;
|
|
4761
|
+
case 'invalid-format':
|
|
4762
|
+
return 2;
|
|
4763
|
+
case 'unauthorized':
|
|
4764
|
+
return 3;
|
|
4765
|
+
case 'network-error':
|
|
4766
|
+
case 'server-error':
|
|
4767
|
+
return 4;
|
|
4768
|
+
case 'unexpected-status':
|
|
4769
|
+
return 5;
|
|
4770
|
+
default: {
|
|
4771
|
+
const exhaustive = failure;
|
|
4772
|
+
return Number(exhaustive) || 1;
|
|
4773
|
+
}
|
|
4774
|
+
}
|
|
4775
|
+
})();
|
|
4776
|
+
const payload = {
|
|
4777
|
+
status: 'login_failed',
|
|
4778
|
+
kind: failure.kind,
|
|
4779
|
+
message: failure.message,
|
|
4780
|
+
};
|
|
4781
|
+
if ('status' in failure)
|
|
4782
|
+
payload.httpStatus = failure.status;
|
|
4783
|
+
if ('cause' in failure && failure.cause)
|
|
4784
|
+
payload.cause = failure.cause;
|
|
4785
|
+
writeOutput(flags, payload, failure.message);
|
|
4786
|
+
process.exitCode = exitCode;
|
|
4595
4787
|
}
|
|
4596
4788
|
/**
|
|
4597
4789
|
* OAuth 2.0 Device Authorization Grant client (RFC 8628). Renders
|
|
@@ -5347,6 +5539,17 @@ function extractApiUrlFlag(args) {
|
|
|
5347
5539
|
function extractLabelFlag(args) {
|
|
5348
5540
|
return extractNamedFlagValue(args, 'label');
|
|
5349
5541
|
}
|
|
5542
|
+
/**
|
|
5543
|
+
* `pugi login --provider env --key <value>` — explicit key arg that
|
|
5544
|
+
* beats `PUGI_API_KEY` env. Same precedence rule as `gh auth login
|
|
5545
|
+
* --with-token`, `aws configure set`, and `pugi config`: the most
|
|
5546
|
+
* specific operator intent (a typed flag) overrides the ambient
|
|
5547
|
+
* environment so an operator can override a stale `PUGI_API_KEY`
|
|
5548
|
+
* from their shell rc without unsetting it first.
|
|
5549
|
+
*/
|
|
5550
|
+
function extractKeyFlag(args) {
|
|
5551
|
+
return extractNamedFlagValue(args, 'key');
|
|
5552
|
+
}
|
|
5350
5553
|
/**
|
|
5351
5554
|
* `pugi jobs` — surface the persistent JobRegistry on the CLI.
|
|
5352
5555
|
* 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
|
}
|