@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.
@@ -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
@@ -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 --env PUGI_API_KEY Read from an env var.',
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 Promote PUGI_API_KEY from the environment into the store.',
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=sk-xx pugi login --provider env',
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
- const envKey = process.env.PUGI_API_KEY;
4555
- if (!envKey) {
4556
- throw new Error('pugi login --provider env requires PUGI_API_KEY to be exported in the current shell.');
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: envKey,
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
- `Pugi logged in for ${record.apiUrl}`,
4591
- `Method: ${input.source}${record.label ? ` (${record.label})` : ''}`,
4592
- `Token: ${maskApiKey(record.apiKey)}`,
4593
- 'Stored at ~/.pugi/credentials.json (mode 0600). Run `pugi whoami` to verify.',
4594
- ].join('\n'));
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
  }