@pugi/cli 0.1.0-beta.21 → 0.1.0-beta.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/auth/env-provider.js +238 -0
- package/dist/core/bare-mode/index.js +107 -0
- package/dist/core/diagnostics/probes/bare-mode.js +42 -0
- package/dist/core/diagnostics/probes/pugi-md.js +89 -0
- package/dist/core/engine/native-pugi.js +55 -11
- package/dist/core/engine/prompts.js +30 -2
- package/dist/core/engine/tool-bridge.js +32 -0
- package/dist/core/feedback/queue.js +177 -0
- package/dist/core/feedback/submitter.js +145 -0
- package/dist/core/onboarding/marker.js +111 -0
- package/dist/core/onboarding/telemetry-state.js +108 -0
- package/dist/core/output-style/presets.js +176 -0
- package/dist/core/output-style/state.js +185 -0
- package/dist/core/permissions/index.js +1 -1
- package/dist/core/permissions/state.js +55 -0
- package/dist/core/pugi-md/context-injector.js +76 -0
- package/dist/core/pugi-md/walk-up.js +207 -0
- package/dist/core/release-notes/parser.js +241 -0
- package/dist/core/release-notes/state.js +116 -0
- package/dist/core/repl/session.js +482 -12
- package/dist/core/repl/slash-commands.js +134 -1
- package/dist/core/repl/workspace-context.js +22 -0
- package/dist/core/share/formatter.js +271 -0
- package/dist/core/share/redactor.js +221 -0
- package/dist/core/share/uploader.js +267 -0
- package/dist/core/theme/context.js +91 -0
- package/dist/core/theme/presets.js +228 -0
- package/dist/core/theme/state.js +181 -0
- package/dist/core/todos/invariant.js +10 -0
- package/dist/core/todos/state.js +177 -0
- package/dist/core/vim/keymap.js +288 -0
- package/dist/core/vim/state.js +92 -0
- package/dist/runtime/cli.js +603 -15
- package/dist/runtime/commands/doctor.js +21 -0
- package/dist/runtime/commands/feedback.js +184 -0
- package/dist/runtime/commands/onboarding.js +275 -0
- package/dist/runtime/commands/plan.js +143 -0
- package/dist/runtime/commands/release-notes.js +229 -0
- package/dist/runtime/commands/share.js +316 -0
- package/dist/runtime/commands/stickers.js +82 -0
- package/dist/runtime/commands/style.js +194 -0
- package/dist/runtime/commands/theme.js +196 -0
- package/dist/runtime/commands/vim.js +140 -0
- package/dist/runtime/version.js +1 -1
- package/dist/tools/registry.js +8 -0
- package/dist/tools/todo-write.js +184 -0
- package/dist/tui/compact-banner.js +28 -1
- package/dist/tui/conversation-pane.js +13 -0
- package/dist/tui/doctor-table.js +32 -17
- package/dist/tui/feedback-prompt.js +156 -0
- package/dist/tui/onboarding-wizard.js +240 -0
- package/dist/tui/repl-render.js +26 -3
- package/dist/tui/repl.js +9 -1
- package/dist/tui/stickers-art.js +136 -0
- package/dist/tui/style-table.js +28 -0
- package/dist/tui/theme-table.js +29 -0
- package/dist/tui/vim-input.js +267 -0
- package/package.json +2 -2
- package/dist/core/engine/compaction-hook.js +0 -154
- package/dist/core/init/scaffold.js +0 -195
- package/dist/core/repl/codebase-survey.js +0 -308
- package/dist/core/repl/init-interview.js +0 -457
- package/dist/core/repl/onboarding-state.js +0 -297
package/dist/runtime/cli.js
CHANGED
|
@@ -22,17 +22,27 @@ 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';
|
|
29
|
+
import { runStyleCommand } from './commands/style.js';
|
|
30
|
+
import { runThemeCommand } from './commands/theme.js';
|
|
31
|
+
import { runOnboardingCommand } from './commands/onboarding.js';
|
|
32
|
+
import { runVimCommand } from './commands/vim.js';
|
|
33
|
+
import { isOnboarded } from '../core/onboarding/marker.js';
|
|
28
34
|
import { runPrivacyCommand } from './commands/privacy.js';
|
|
29
35
|
import { runReport } from './commands/report.js';
|
|
30
36
|
import { runDoctorCommand, defaultHome as defaultDoctorHome } from './commands/doctor.js';
|
|
31
37
|
import { runStatusCommand, defaultStatusHome, } from './commands/status.js';
|
|
38
|
+
import { runStickersCommand } from './commands/stickers.js';
|
|
39
|
+
import { runReleaseNotesCommand, defaultReleaseNotesHome, } from './commands/release-notes.js';
|
|
32
40
|
import { runUndoCommand } from './commands/undo.js';
|
|
33
41
|
import { runCompactCommand } from './commands/compact.js';
|
|
34
42
|
import { runBudgetCommand } from './commands/budget.js';
|
|
43
|
+
import { BARE_MODE_BANNER, isBareMode, setBareMode, } from '../core/bare-mode/index.js';
|
|
35
44
|
import { runCostCommand } from './commands/cost.js';
|
|
45
|
+
import { runShareCommand } from './commands/share.js';
|
|
36
46
|
import { runSkillsCommand } from './commands/skills.js';
|
|
37
47
|
import { installDefaultSkills } from '../core/skills/defaults.js';
|
|
38
48
|
import { runAgentsCommand } from './commands/agents.js';
|
|
@@ -43,6 +53,7 @@ import { resolveWorkspaceLabel } from '../core/repl/workspace-context.js';
|
|
|
43
53
|
import { runReviewConsensus } from './commands/review-consensus.js';
|
|
44
54
|
import { runMcpCommand } from './commands/mcp.js';
|
|
45
55
|
import { runPermissionsCommand } from './commands/permissions.js';
|
|
56
|
+
import { runPlanCommand } from './commands/plan.js';
|
|
46
57
|
import { parsePermissionMode } from '../core/permissions/index.js';
|
|
47
58
|
import { DECOMPOSE_PROMPT_SUFFIX, parseDecompositionFromText, writeDecomposition, } from './plan-decompose.js';
|
|
48
59
|
import { FtsSyntaxError, SqliteSessionStore, resolveProjectStoreDir } from '../core/repl/store/index.js';
|
|
@@ -94,9 +105,15 @@ const handlers = {
|
|
|
94
105
|
patch: dispatchPatch,
|
|
95
106
|
permissions: dispatchPermissions,
|
|
96
107
|
perms: dispatchPermissions,
|
|
97
|
-
plan:
|
|
108
|
+
plan: dispatchPlan,
|
|
98
109
|
'plan-review': dispatchPlanReview,
|
|
99
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,
|
|
100
117
|
// PAVF-7 (2026-05-27): `pugi report --from-error` captures the
|
|
101
118
|
// most-recent failed session as a redacted bundle so operators can
|
|
102
119
|
// file clean bug reports without manual log-grepping.
|
|
@@ -105,9 +122,29 @@ const handlers = {
|
|
|
105
122
|
resume,
|
|
106
123
|
roster: dispatchRoster,
|
|
107
124
|
sessions,
|
|
125
|
+
share: dispatchShare,
|
|
108
126
|
skills: dispatchSkills,
|
|
109
127
|
status,
|
|
128
|
+
stickers,
|
|
129
|
+
// Leak L21 (2026-05-27): in-CLI feedback collector. Shares the
|
|
130
|
+
// same handler as the in-REPL `/feedback` slash; the wrapper just
|
|
131
|
+
// routes TTY vs non-TTY before mounting Ink.
|
|
132
|
+
feedback: dispatchFeedback,
|
|
110
133
|
sync,
|
|
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,
|
|
139
|
+
// Leak L25 (2026-05-27): `pugi onboarding` walks the new operator
|
|
140
|
+
// through auth / mode / style / MCP / telemetry. Idempotent;
|
|
141
|
+
// `--reset` clears the marker file so the bare-invocation hint
|
|
142
|
+
// re-arms without nuking persisted defaults.
|
|
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,
|
|
111
148
|
undo: dispatchUndo,
|
|
112
149
|
compact: dispatchCompact,
|
|
113
150
|
// L19 (2026-05-27): `pugi usage` is an alias of `pugi cost` — same
|
|
@@ -288,6 +325,101 @@ async function dispatchPrivacy(args, flags, _session) {
|
|
|
288
325
|
writeOutput: (payload, text) => writeOutput(flags, payload, text),
|
|
289
326
|
});
|
|
290
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* Leak L18 (2026-05-27) — `pugi style` top-level dispatcher.
|
|
330
|
+
*
|
|
331
|
+
* Forwards to the shared `runStyleCommand` runner. The REPL `/style`
|
|
332
|
+
* slash uses the same runner via a dynamic import inside
|
|
333
|
+
* `core/repl/session.ts` so the two surfaces stay single-sourced.
|
|
334
|
+
*
|
|
335
|
+
* Exit-code policy:
|
|
336
|
+
* - 0 — show / switch / reset / list happy paths
|
|
337
|
+
* - 1 — unknown preset slug
|
|
338
|
+
* - 2 — conflicting flags (`--reset` + positional / `--reset --persist`)
|
|
339
|
+
*
|
|
340
|
+
* The runner returns the code; we attach it to `process.exitCode` so
|
|
341
|
+
* subsequent dispatch wrappers do not clobber it on success.
|
|
342
|
+
*/
|
|
343
|
+
async function dispatchStyle(args, flags, _session) {
|
|
344
|
+
const rc = await runStyleCommand(args, {
|
|
345
|
+
workspaceRoot: process.cwd(),
|
|
346
|
+
writeOutput: (payload, text) => writeOutput(flags, payload, text),
|
|
347
|
+
});
|
|
348
|
+
if (rc !== 0)
|
|
349
|
+
process.exitCode = rc;
|
|
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
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Leak L25 (2026-05-27) — `pugi onboarding` top-level dispatcher.
|
|
376
|
+
*
|
|
377
|
+
* Walks the new operator through auth / permission mode / output
|
|
378
|
+
* style / MCP / telemetry consent. The Ink wizard mounts only when
|
|
379
|
+
* stdin is a TTY and `--json` is not set; otherwise we dump the
|
|
380
|
+
* current snapshot + hints in the non-interactive envelope so
|
|
381
|
+
* scripted callers see the same structured payload.
|
|
382
|
+
*
|
|
383
|
+
* Auth status: we resolve credentials once up front and pass the
|
|
384
|
+
* boolean to the runner; the wizard surfaces a `pugi login` hint
|
|
385
|
+
* when auth is missing but DOES NOT block — local defaults are still
|
|
386
|
+
* configurable without an active credential.
|
|
387
|
+
*
|
|
388
|
+
* Exit-code policy:
|
|
389
|
+
* 0 — completed / cancelled / non-interactive / reset
|
|
390
|
+
* 2 — conflicting / unknown flags
|
|
391
|
+
*/
|
|
392
|
+
async function dispatchOnboarding(args, flags, _session) {
|
|
393
|
+
const credential = resolveActiveCredential();
|
|
394
|
+
const rc = await runOnboardingCommand(args, {
|
|
395
|
+
workspaceRoot: process.cwd(),
|
|
396
|
+
env: process.env,
|
|
397
|
+
authPresent: credential !== null,
|
|
398
|
+
interactive: isInteractive(flags) && !flags.json,
|
|
399
|
+
writeOutput: (payload, text) => writeOutput(flags, payload, text),
|
|
400
|
+
});
|
|
401
|
+
if (rc !== 0)
|
|
402
|
+
process.exitCode = rc;
|
|
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
|
+
}
|
|
291
423
|
/**
|
|
292
424
|
* PAVF-7 (2026-05-27): `pugi report --from-error` — bundle the most-
|
|
293
425
|
* recent failed session into a redacted local report so operators can
|
|
@@ -438,6 +570,124 @@ async function dispatchCost(args, flags, _session) {
|
|
|
438
570
|
writeOutput: (payload, text) => writeOutput(flags, payload, text),
|
|
439
571
|
});
|
|
440
572
|
}
|
|
573
|
+
/**
|
|
574
|
+
* Leak L20 (2026-05-27): `pugi share` top-level surface. Exports the
|
|
575
|
+
* current session transcript as Markdown to gist (default when `gh` is
|
|
576
|
+
* available) or pugi.io (--pugi). The handler delegates to
|
|
577
|
+
* `runShareCommand` so the slash surface (`/share`) and the shell
|
|
578
|
+
* surface share one code path. JSON output mode is honoured via the
|
|
579
|
+
* shared `writeOutput` wrapper.
|
|
580
|
+
*/
|
|
581
|
+
async function dispatchShare(args, flags, _session) {
|
|
582
|
+
await runShareCommand(args, {
|
|
583
|
+
workspaceRoot: process.cwd(),
|
|
584
|
+
cliVersion: PUGI_CLI_VERSION,
|
|
585
|
+
writeOutput: (payload, text) => writeOutput(flags, payload, text),
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Leak L7 — `pugi plan [--back | --persist | <prompt...>]`.
|
|
590
|
+
*
|
|
591
|
+
* Quick mode-switch shortcut + optional one-shot engine dispatch. Slash
|
|
592
|
+
* surface `/plan` shares the same `runPlanCommand` helper so the
|
|
593
|
+
* workspace-state writes go through one code path. Argument grammar:
|
|
594
|
+
*
|
|
595
|
+
* pugi plan -> set workspace mode = plan + banner
|
|
596
|
+
* pugi plan --back -> restore the mode that was active
|
|
597
|
+
* before the most recent /plan entry
|
|
598
|
+
* pugi plan --persist -> set + also write ~/.pugi/config.json
|
|
599
|
+
* pugi plan <prompt...> -> set + run `runEngineTask('plan')`
|
|
600
|
+
* with the prompt (existing offline /
|
|
601
|
+
* engine path; the permission gate now
|
|
602
|
+
* sees plan as workspace state)
|
|
603
|
+
* pugi plan <prompt> --auto-back -> ALSO restore previous mode once
|
|
604
|
+
* the engine returns (defaults to
|
|
605
|
+
* leaving the operator in plan
|
|
606
|
+
* mode so they can iterate)
|
|
607
|
+
*
|
|
608
|
+
* The handler intentionally intercepts the mode-switch flags BEFORE
|
|
609
|
+
* delegating to `runEngineTask('plan')` for the prompt path. Without
|
|
610
|
+
* this wrapper, `pugi plan` (no args) would error out of the engine
|
|
611
|
+
* task ("requires a prompt") which is the legacy behaviour; the L7
|
|
612
|
+
* spec wants bare `pugi plan` to be the mode switch.
|
|
613
|
+
*/
|
|
614
|
+
async function dispatchPlan(args, flags, session) {
|
|
615
|
+
// Strip `--back` / `--auto-back` from the positional args — the global
|
|
616
|
+
// parseArgs does not consume them (they are command-local). Anything
|
|
617
|
+
// else stays in `prompt` so the engine sees the operator's text
|
|
618
|
+
// verbatim. The flag parser keeps both `--back` and the spelling
|
|
619
|
+
// variants the operator might type from muscle memory after using
|
|
620
|
+
// `git checkout --` style flows.
|
|
621
|
+
let back = false;
|
|
622
|
+
let autoBack = false;
|
|
623
|
+
const remaining = [];
|
|
624
|
+
for (const arg of args) {
|
|
625
|
+
if (arg === '--back') {
|
|
626
|
+
back = true;
|
|
627
|
+
}
|
|
628
|
+
else if (arg === '--auto-back') {
|
|
629
|
+
autoBack = true;
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
remaining.push(arg);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
const hasPrompt = remaining.length > 0;
|
|
636
|
+
const persist = Boolean(flags.persist);
|
|
637
|
+
// --back and a prompt are mutually exclusive — back is a revert action,
|
|
638
|
+
// not a dispatch one. Refuse the combination with a clear hint instead
|
|
639
|
+
// of silently dropping one or the other.
|
|
640
|
+
if (back && hasPrompt) {
|
|
641
|
+
writeOutput(flags, { ok: false, error: 'pugi plan --back does not accept a prompt; revert first, then dispatch.' }, 'pugi plan --back does not accept a prompt; revert first, then dispatch.');
|
|
642
|
+
process.exitCode = 2;
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
// --back + --auto-back is incoherent (auto-back applies to the
|
|
646
|
+
// dispatch path) — refuse rather than degrade silently.
|
|
647
|
+
if (back && autoBack) {
|
|
648
|
+
writeOutput(flags, { ok: false, error: 'pugi plan --back and --auto-back cannot be combined.' }, 'pugi plan --back and --auto-back cannot be combined.');
|
|
649
|
+
process.exitCode = 2;
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
// When a prompt is going to be dispatched in --json mode, suppress
|
|
653
|
+
// the human-readable banner writes so the engine task remains the
|
|
654
|
+
// single JSON emitter on stdout. The mode write still happens. In
|
|
655
|
+
// human (non --json) mode the banner prints normally so the operator
|
|
656
|
+
// sees the gate-state change before the engine starts thinking.
|
|
657
|
+
const sinkSilent = hasPrompt && flags.json;
|
|
658
|
+
const writeLine = (line) => {
|
|
659
|
+
if (sinkSilent)
|
|
660
|
+
return;
|
|
661
|
+
writeOutput(flags, { text: line }, line);
|
|
662
|
+
};
|
|
663
|
+
const result = await runPlanCommand({ back, persist }, {
|
|
664
|
+
workspaceRoot: process.cwd(),
|
|
665
|
+
writeOutput: writeLine,
|
|
666
|
+
});
|
|
667
|
+
// No prompt → mode-switch only. Done.
|
|
668
|
+
if (!hasPrompt)
|
|
669
|
+
return;
|
|
670
|
+
// Prompt present → fall through to the existing engine task with the
|
|
671
|
+
// remaining args. The workspace mode is now `plan` (or stayed `plan`
|
|
672
|
+
// if already there); the engine sees the same plan-task semantics it
|
|
673
|
+
// always has — read-only schema + executor refusal sentinel — but the
|
|
674
|
+
// permission GATE now also enforces plan independently.
|
|
675
|
+
try {
|
|
676
|
+
await runEngineTask('plan')(remaining, flags, session);
|
|
677
|
+
}
|
|
678
|
+
finally {
|
|
679
|
+
// --auto-back restores the previous mode AFTER the engine returns
|
|
680
|
+
// (success OR failure) so the operator's gate state mirrors a normal
|
|
681
|
+
// `--back` invocation. Without --auto-back the operator stays in
|
|
682
|
+
// plan and can iterate / inspect before acting.
|
|
683
|
+
if (autoBack && (result.verdict === 'entered' || result.verdict === 'persisted')) {
|
|
684
|
+
await runPlanCommand({ back: true, persist: false }, {
|
|
685
|
+
workspaceRoot: process.cwd(),
|
|
686
|
+
writeOutput: writeLine,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
441
691
|
async function dispatchSkills(args, flags, _session) {
|
|
442
692
|
await runSkillsCommand(args, {
|
|
443
693
|
workspaceRoot: process.cwd(),
|
|
@@ -567,6 +817,21 @@ async function dispatchWorktree(args, flags, _session) {
|
|
|
567
817
|
}
|
|
568
818
|
export async function runCli(argv) {
|
|
569
819
|
const { command, args, flags, isBareInvocation } = parseArgs(argv);
|
|
820
|
+
// Leak L22 — print the one-line bare banner once per invocation when
|
|
821
|
+
// the flag is active and stdout is NOT bound for JSON consumption. The
|
|
822
|
+
// banner goes to stderr so it never lands in a `--json` envelope or a
|
|
823
|
+
// pipe-captured stdout stream; operators see it on the terminal,
|
|
824
|
+
// scripted callers stay clean. Suppressed for `pugi version` / `pugi
|
|
825
|
+
// help` (short, scripted-friendly surfaces) and when the operator
|
|
826
|
+
// sets PUGI_BARE without the flag (avoids double-printing across
|
|
827
|
+
// scripted nested invocations).
|
|
828
|
+
if (flags.bare &&
|
|
829
|
+
!flags.json &&
|
|
830
|
+
command !== 'version' &&
|
|
831
|
+
command !== 'help' &&
|
|
832
|
+
argv.includes('--bare')) {
|
|
833
|
+
process.stderr.write(`${BARE_MODE_BANNER}\n`);
|
|
834
|
+
}
|
|
570
835
|
// β-headless dispatch (CEO directive 2026-05-27 "нужно тестирование по
|
|
571
836
|
// кругу"): when `--print <brief>` is set we route to the headless
|
|
572
837
|
// runner BEFORE the REPL / splash / command branches. The runner
|
|
@@ -598,6 +863,19 @@ export async function runCli(argv) {
|
|
|
598
863
|
process.exitCode = exitCode;
|
|
599
864
|
return;
|
|
600
865
|
}
|
|
866
|
+
// Leak L25 (2026-05-27): first-run hint. When the operator types a
|
|
867
|
+
// bare `pugi` on a real TTY AND the onboarding marker is absent, drop
|
|
868
|
+
// a one-line hint on stderr BEFORE the REPL splash mounts. Stderr so
|
|
869
|
+
// the line never lands in a `--json` envelope or a scripted stdout
|
|
870
|
+
// pipe; suppressed when --json is set or the operator already walked
|
|
871
|
+
// the wizard. The marker check is best-effort — a fs glitch returns
|
|
872
|
+
// false and we print the hint, which is harmless.
|
|
873
|
+
if (isBareInvocation
|
|
874
|
+
&& isInteractive(flags)
|
|
875
|
+
&& !flags.json
|
|
876
|
+
&& !isOnboarded(process.env)) {
|
|
877
|
+
process.stderr.write('Tip: run `pugi onboarding` to configure defaults.\n');
|
|
878
|
+
}
|
|
601
879
|
// Bare `pugi` on a TTY enters the REPL-by-default agentic session
|
|
602
880
|
// (Sprint α5.7, ADR-0056). The REPL is the customer-facing surface
|
|
603
881
|
// that brings Pugi to parity with Claude Code / Codex CLI. When the
|
|
@@ -697,8 +975,31 @@ function parseArgs(argv) {
|
|
|
697
975
|
// surface.
|
|
698
976
|
persist: false,
|
|
699
977
|
confirm: false,
|
|
978
|
+
// Leak L22 — `--bare` flag (skip project auto-discovery). Default
|
|
979
|
+
// honors the env var so a wrapper script that exports PUGI_BARE=1
|
|
980
|
+
// keeps the bit even when the operator forgets the flag, and the
|
|
981
|
+
// explicit flag overrides on the way through the loop below.
|
|
982
|
+
bare: isBareMode(),
|
|
983
|
+
// Leak L33 — `--ascii-only` for `pugi stickers`. Default off so the
|
|
984
|
+
// interactive surface keeps its boxed renderer; opt-in via flag
|
|
985
|
+
// for pipe / script use.
|
|
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,
|
|
700
991
|
};
|
|
701
992
|
const args = [];
|
|
993
|
+
// Leak L22: scan for `--bare` BEFORE the early-return short-circuits
|
|
994
|
+
// below. Operators may pass `pugi --bare --version` or `pugi --bare
|
|
995
|
+
// --help` and the short-circuit return must still flip the bare bit
|
|
996
|
+
// so subprocesses + env-consulting modules see the activated state.
|
|
997
|
+
// The bit is idempotent — re-applied inside the main loop below for
|
|
998
|
+
// non-short-circuit paths.
|
|
999
|
+
if (argv.includes('--bare')) {
|
|
1000
|
+
flags.bare = true;
|
|
1001
|
+
setBareMode();
|
|
1002
|
+
}
|
|
702
1003
|
// Sprint 2E: `pugi --version` / `-v` are universal install-test conventions
|
|
703
1004
|
// (npm uses --version on every published bin, Homebrew formula uses it in
|
|
704
1005
|
// the test block). Normalize them to the `version` command so users can
|
|
@@ -767,6 +1068,20 @@ function parseArgs(argv) {
|
|
|
767
1068
|
// at the global level for consistency with --no-splash / --no-tool-stream.
|
|
768
1069
|
flags.noDefaults = true;
|
|
769
1070
|
}
|
|
1071
|
+
else if (arg === '--ascii-only') {
|
|
1072
|
+
// Leak L33 — `pugi stickers --ascii-only` skips the Ink boxed
|
|
1073
|
+
// renderer. Parsed globally so the dispatcher can pass the flag
|
|
1074
|
+
// through to runStickersCommand without per-command argv slicing.
|
|
1075
|
+
flags.asciiOnly = true;
|
|
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
|
+
}
|
|
770
1085
|
else if (arg === '--decompose') {
|
|
771
1086
|
// α6.8 EXTEND PR1: plan-only flag. Other engine commands ignore
|
|
772
1087
|
// it. Parsed globally for symmetry with the rest of the flag
|
|
@@ -917,6 +1232,16 @@ function parseArgs(argv) {
|
|
|
917
1232
|
// acknowledgement).
|
|
918
1233
|
flags.confirm = true;
|
|
919
1234
|
}
|
|
1235
|
+
else if (arg === '--bare') {
|
|
1236
|
+
// Leak L22: disable project auto-discovery for this invocation.
|
|
1237
|
+
// Set BOTH the parsed flag and the process env so downstream
|
|
1238
|
+
// modules consulting `isBareMode()` (markdown-traverse callsite,
|
|
1239
|
+
// REPL auto-init gate, doctor probe, subprocess spawns) see a
|
|
1240
|
+
// coherent activated state without re-threading the bit through
|
|
1241
|
+
// every call signature.
|
|
1242
|
+
flags.bare = true;
|
|
1243
|
+
setBareMode();
|
|
1244
|
+
}
|
|
920
1245
|
else {
|
|
921
1246
|
args.push(arg);
|
|
922
1247
|
}
|
|
@@ -1044,6 +1369,28 @@ const COMMAND_HELP_BODIES = {
|
|
|
1044
1369
|
' pugi config get privacy',
|
|
1045
1370
|
' pugi config set privacy=<mode>',
|
|
1046
1371
|
],
|
|
1372
|
+
share: [
|
|
1373
|
+
'pugi share — export the current session transcript (leak L20).',
|
|
1374
|
+
'',
|
|
1375
|
+
'Reads .pugi/events.jsonl, formats it as Markdown, and uploads to',
|
|
1376
|
+
'either a GitHub Gist (`gh`-backed, default when `gh` is available)',
|
|
1377
|
+
'or pugi.io (--pugi). Always prompts before upload unless --yes is',
|
|
1378
|
+
'set. Refuses upload entirely if the transcript carries an active',
|
|
1379
|
+
'`Bearer ` credential — re-run with --redact to scrub it first.',
|
|
1380
|
+
'',
|
|
1381
|
+
'Flags:',
|
|
1382
|
+
' --gist Force gist target; refuses if gh CLI is absent.',
|
|
1383
|
+
' --pugi Force pugi.io target (requires `pugi login`).',
|
|
1384
|
+
' --redact Run PII scrubber before upload.',
|
|
1385
|
+
' --preview Print the transcript to stdout WITHOUT upload.',
|
|
1386
|
+
' --yes, -y Skip the y/n confirmation prompt.',
|
|
1387
|
+
' --json Emit a structured JSON envelope only.',
|
|
1388
|
+
'',
|
|
1389
|
+
'Examples:',
|
|
1390
|
+
' pugi share Auto-pick + confirm.',
|
|
1391
|
+
' pugi share --preview --redact See what would be shared.',
|
|
1392
|
+
' pugi share --gist --redact --yes Scripted secret-gist upload.',
|
|
1393
|
+
],
|
|
1047
1394
|
cost: [
|
|
1048
1395
|
'pugi cost — token + USD breakdown for the current Pugi session.',
|
|
1049
1396
|
'',
|
|
@@ -1093,7 +1440,8 @@ const COMMAND_HELP_BODIES = {
|
|
|
1093
1440
|
'Interactive picker by default (browser OAuth / PAT / env). Non-interactive:',
|
|
1094
1441
|
' --provider device Device-flow OAuth.',
|
|
1095
1442
|
' --provider token --token <jwt> Pass a JWT directly.',
|
|
1096
|
-
' --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).',
|
|
1097
1445
|
],
|
|
1098
1446
|
accounts: [
|
|
1099
1447
|
'pugi accounts — manage stored credentials across endpoints.',
|
|
@@ -1155,6 +1503,45 @@ const COMMAND_HELP_BODIES = {
|
|
|
1155
1503
|
'Useful in shell scripts that need a human-confirm before a destructive',
|
|
1156
1504
|
'step. Exits 0 on yes, 1 on no, 2 on cancel.',
|
|
1157
1505
|
],
|
|
1506
|
+
stickers: [
|
|
1507
|
+
'pugi stickers — show a Pugi brand sticker (gimmick).',
|
|
1508
|
+
'',
|
|
1509
|
+
'Picks one of the curated pug-face ASCII variants at random and footers',
|
|
1510
|
+
'it with a rotating brand quote. Brand-personality surface — never a gate.',
|
|
1511
|
+
'',
|
|
1512
|
+
' --json Emit a structured envelope (id · caption · quote).',
|
|
1513
|
+
' --ascii-only Plain stdout (no box, no dim accents) for scripting.',
|
|
1514
|
+
'',
|
|
1515
|
+
'Also available as /stickers from inside the REPL.',
|
|
1516
|
+
],
|
|
1517
|
+
feedback: [
|
|
1518
|
+
'pugi feedback — file a bug / feature / general comment from the CLI.',
|
|
1519
|
+
'',
|
|
1520
|
+
'Interactive five-step wizard:',
|
|
1521
|
+
' 1. category (bug / feature / general / praise)',
|
|
1522
|
+
' 2. rating (1-5 stars)',
|
|
1523
|
+
' 3. comment (multi-line, Ctrl-D submits)',
|
|
1524
|
+
' 4. include redacted last 5 turns? (y/n, default n)',
|
|
1525
|
+
' 5. confirm submit (y/n, default y)',
|
|
1526
|
+
'',
|
|
1527
|
+
'On network failure the envelope is appended to',
|
|
1528
|
+
'.pugi/feedback-queue.jsonl and drained on the next online session.',
|
|
1529
|
+
'',
|
|
1530
|
+
'Also available as /feedback from inside the REPL.',
|
|
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
|
+
],
|
|
1158
1545
|
deploy: [
|
|
1159
1546
|
'pugi deploy — trigger a vendor deployment from the bound Git source.',
|
|
1160
1547
|
'',
|
|
@@ -1248,6 +1635,10 @@ async function help(args, flags, _session) {
|
|
|
1248
1635
|
' Pairs with PUGI_HIDE_TOOL_STREAM=1.',
|
|
1249
1636
|
' --no-defaults Skip bundled default-skills install on',
|
|
1250
1637
|
' `pugi init`. Pairs with PUGI_INIT_NO_DEFAULTS=1.',
|
|
1638
|
+
' --bare Disable project auto-discovery — no PUGI.md /',
|
|
1639
|
+
' AGENTS.md / CLAUDE.md / GEMINI.md walk-up, no',
|
|
1640
|
+
' auto-init of .pugi/, no persona auto-load.',
|
|
1641
|
+
' Pairs with PUGI_BARE=1.',
|
|
1251
1642
|
'',
|
|
1252
1643
|
PUGI_TAGLINE,
|
|
1253
1644
|
'Execution defaults to local. Use --remote or --web to create a handoff bundle.',
|
|
@@ -1301,6 +1692,111 @@ async function status(_args, flags, _session) {
|
|
|
1301
1692
|
writeOutput: (payload, text) => writeOutput(flags, payload, text),
|
|
1302
1693
|
});
|
|
1303
1694
|
}
|
|
1695
|
+
/**
|
|
1696
|
+
* `pugi stickers` — Leak L33 (2026-05-27). Brand-personality gimmick
|
|
1697
|
+
* mirroring Claude Code's `/stickers` easter egg. Picks one curated
|
|
1698
|
+
* pug-face ASCII variant at random + footers it with a rotating quote
|
|
1699
|
+
* from the Pugi brand corpus. Always exits 0 — never a gate.
|
|
1700
|
+
*
|
|
1701
|
+
* The handler stays thin: corpus + picker + pure renderers live in
|
|
1702
|
+
* `tui/stickers-art.tsx`; this wrapper just hands the resolved result
|
|
1703
|
+
* к the shared `writeOutput` helper so `--json` keeps producing a
|
|
1704
|
+
* structured envelope (id + caption + quote + meta) for scripted
|
|
1705
|
+
* callers. The `--ascii-only` flag drops the box decoration in the
|
|
1706
|
+
* non-JSON path so pipes (`pugi stickers --ascii-only | lolcat`) get
|
|
1707
|
+
* clean plain-text frames.
|
|
1708
|
+
*
|
|
1709
|
+
* The same handler powers the in-REPL `/stickers` slash, which routes
|
|
1710
|
+
* the text through the conversation system pane line-buffer.
|
|
1711
|
+
*/
|
|
1712
|
+
async function stickers(_args, flags, _session) {
|
|
1713
|
+
runStickersCommand({
|
|
1714
|
+
json: flags.json,
|
|
1715
|
+
asciiOnly: flags.asciiOnly,
|
|
1716
|
+
writeOutput: (payload, text) => writeOutput(flags, payload, text),
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
/**
|
|
1720
|
+
* `pugi feedback` — Leak L21 (2026-05-27). In-CLI feedback collector.
|
|
1721
|
+
*
|
|
1722
|
+
* Five-step wizard:
|
|
1723
|
+
* 1. category (bug / feature / general / praise)
|
|
1724
|
+
* 2. rating (1-5)
|
|
1725
|
+
* 3. comment (multi-line, Ctrl-D submits)
|
|
1726
|
+
* 4. include redacted session context? (y/n, default n)
|
|
1727
|
+
* 5. confirm submit (y/n, default y)
|
|
1728
|
+
*
|
|
1729
|
+
* POSTs to `<apiUrl>/api/pugi/feedback`. On transient failure (404,
|
|
1730
|
+
* 5xx, network error) the envelope is appended to
|
|
1731
|
+
* `<cwd>/.pugi/feedback-queue.jsonl`. On next online session the
|
|
1732
|
+
* background flusher drains the queue silently.
|
|
1733
|
+
*
|
|
1734
|
+
* Non-TTY callers (CI, pipes) get a one-line "non-interactive — re-run
|
|
1735
|
+
* in a real terminal" stub. The feedback wizard is intentionally
|
|
1736
|
+
* TTY-only — scripting a star-rating + multi-line comment from a
|
|
1737
|
+
* shell pipe would just produce low-signal noise.
|
|
1738
|
+
*/
|
|
1739
|
+
async function dispatchFeedback(_args, flags, _session) {
|
|
1740
|
+
if (!isInteractive(flags)) {
|
|
1741
|
+
writeOutput(flags, {
|
|
1742
|
+
ok: false,
|
|
1743
|
+
error: 'pugi feedback requires an interactive terminal. Re-run from a real TTY.',
|
|
1744
|
+
}, 'pugi feedback: non-interactive shell — re-run from a real terminal.');
|
|
1745
|
+
process.exitCode = 2;
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
const { renderFeedbackPrompt } = await import('../tui/feedback-prompt.js');
|
|
1749
|
+
const { runFeedbackCommand, renderFeedbackToast } = await import('./commands/feedback.js');
|
|
1750
|
+
const { submitFeedback } = await import('../core/feedback/submitter.js');
|
|
1751
|
+
const verdict = await renderFeedbackPrompt();
|
|
1752
|
+
if (verdict.cancelled || !verdict.draft) {
|
|
1753
|
+
writeOutput(flags, { ok: true, kind: 'cancelled' }, 'Feedback cancelled. Nothing was sent.');
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
// Best-effort credential resolution. Anonymous submission is allowed
|
|
1757
|
+
// (the server may still accept it for ungated `/api/pugi/feedback`
|
|
1758
|
+
// routes); on no-credential we route the POST through an empty
|
|
1759
|
+
// bearer + the operator gets the 4xx → "rejected" toast if the
|
|
1760
|
+
// server requires auth.
|
|
1761
|
+
const credential = resolveActiveCredential(process.env);
|
|
1762
|
+
const apiUrl = credential?.apiUrl ?? (process.env.PUGI_API_URL || 'https://api.pugi.io');
|
|
1763
|
+
const apiKey = credential?.apiKey ?? '';
|
|
1764
|
+
const result = await runFeedbackCommand({
|
|
1765
|
+
cwd: process.cwd(),
|
|
1766
|
+
cliVersion: PUGI_CLI_VERSION,
|
|
1767
|
+
submit: async (env) => submitFeedback(env, { apiUrl, apiKey }),
|
|
1768
|
+
draft: verdict.draft,
|
|
1769
|
+
// `pugi feedback` from a fresh shell has no live transcript — the
|
|
1770
|
+
// session-context provider is omitted. The REPL slash variant
|
|
1771
|
+
// wires this in via `runFeedbackSlash` (session.ts).
|
|
1772
|
+
});
|
|
1773
|
+
writeOutput(flags, { ok: true, result }, renderFeedbackToast(result));
|
|
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
|
+
}
|
|
1304
1800
|
/**
|
|
1305
1801
|
* Programmatic init scaffolder. Idempotent — every helper call is a
|
|
1306
1802
|
* `*_IfMissing` write, so re-running over an existing .pugi/ workspace
|
|
@@ -3844,7 +4340,7 @@ async function login(args, flags, _session) {
|
|
|
3844
4340
|
if (args.includes('--help') || args.includes('-h')) {
|
|
3845
4341
|
writeOutput(flags, {
|
|
3846
4342
|
command: 'login',
|
|
3847
|
-
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>]',
|
|
3848
4344
|
}, [
|
|
3849
4345
|
'Usage: pugi login [options]',
|
|
3850
4346
|
'',
|
|
@@ -3856,19 +4352,27 @@ async function login(args, flags, _session) {
|
|
|
3856
4352
|
'Non-interactive options:',
|
|
3857
4353
|
' --provider device Run the device-flow login (recommended).',
|
|
3858
4354
|
' --provider token Store an API key passed via --token / --token-stdin / PUGI_LOGIN_TOKEN.',
|
|
3859
|
-
' --provider env
|
|
4355
|
+
' --provider env Read PUGI_API_KEY (or --key) and verify it via /api/pugi/health.',
|
|
3860
4356
|
' --token <PAT> Inline API key (visible in `ps`).',
|
|
3861
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).',
|
|
3862
4360
|
' --label <name> Short label surfaced in `pugi accounts list`.',
|
|
3863
4361
|
' --api-url <url> Override the Anvil endpoint (self-hosted).',
|
|
3864
4362
|
' --no-device-flow Refuse the device flow; fail fast in CI without a token.',
|
|
3865
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
|
+
'',
|
|
3866
4369
|
'Examples:',
|
|
3867
4370
|
' pugi login # interactive picker on a TTY',
|
|
3868
4371
|
' pugi login --provider device # explicit browser OAuth',
|
|
3869
4372
|
' pugi login --provider token --token sk-xx # paste in a key',
|
|
3870
4373
|
' echo $TOKEN | pugi login --provider token --token-stdin',
|
|
3871
|
-
' 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',
|
|
3872
4376
|
].join('\n'));
|
|
3873
4377
|
return;
|
|
3874
4378
|
}
|
|
@@ -3891,6 +4395,11 @@ async function login(args, flags, _session) {
|
|
|
3891
4395
|
const apiUrlOverride = extractApiUrlFlag(args);
|
|
3892
4396
|
const labelFlag = extractLabelFlag(args);
|
|
3893
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');
|
|
3894
4403
|
const apiUrl = normalizeApiUrl(apiUrlOverride ?? process.env.PUGI_API_URL ?? DEFAULT_API_URL);
|
|
3895
4404
|
// Path 1: explicit --provider trumps everything else.
|
|
3896
4405
|
if (provider) {
|
|
@@ -3901,6 +4410,8 @@ async function login(args, flags, _session) {
|
|
|
3901
4410
|
explicitToken: tokenFromArgs,
|
|
3902
4411
|
tokenStdinFlag,
|
|
3903
4412
|
noDeviceFlow,
|
|
4413
|
+
envExplicitKey,
|
|
4414
|
+
envSkipValidate,
|
|
3904
4415
|
});
|
|
3905
4416
|
return;
|
|
3906
4417
|
}
|
|
@@ -3948,6 +4459,8 @@ async function login(args, flags, _session) {
|
|
|
3948
4459
|
flags,
|
|
3949
4460
|
label: labelFlag,
|
|
3950
4461
|
noDeviceFlow,
|
|
4462
|
+
envExplicitKey,
|
|
4463
|
+
envSkipValidate,
|
|
3951
4464
|
});
|
|
3952
4465
|
return;
|
|
3953
4466
|
}
|
|
@@ -4166,16 +4679,28 @@ async function dispatchLoginProvider(provider, ctx) {
|
|
|
4166
4679
|
return;
|
|
4167
4680
|
}
|
|
4168
4681
|
case 'env': {
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
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;
|
|
4172
4696
|
}
|
|
4173
4697
|
storeAndAnnounceToken({
|
|
4174
4698
|
apiUrl: ctx.apiUrl,
|
|
4175
|
-
apiKey:
|
|
4699
|
+
apiKey: resolved.token,
|
|
4176
4700
|
label: ctx.label,
|
|
4177
4701
|
source: 'env',
|
|
4178
4702
|
flags: ctx.flags,
|
|
4703
|
+
validatedLatencyMs: resolved.latencyMs > 0 ? resolved.latencyMs : undefined,
|
|
4179
4704
|
});
|
|
4180
4705
|
return;
|
|
4181
4706
|
}
|
|
@@ -4194,6 +4719,15 @@ function storeAndAnnounceToken(input) {
|
|
|
4194
4719
|
label: input.label,
|
|
4195
4720
|
source: input.source,
|
|
4196
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.');
|
|
4197
4731
|
writeOutput(input.flags, {
|
|
4198
4732
|
status: 'logged_in',
|
|
4199
4733
|
apiUrl: record.apiUrl,
|
|
@@ -4201,12 +4735,55 @@ function storeAndAnnounceToken(input) {
|
|
|
4201
4735
|
label: record.label ?? null,
|
|
4202
4736
|
createdAt: record.createdAt,
|
|
4203
4737
|
source: input.source,
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
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;
|
|
4210
4787
|
}
|
|
4211
4788
|
/**
|
|
4212
4789
|
* OAuth 2.0 Device Authorization Grant client (RFC 8628). Renders
|
|
@@ -4962,6 +5539,17 @@ function extractApiUrlFlag(args) {
|
|
|
4962
5539
|
function extractLabelFlag(args) {
|
|
4963
5540
|
return extractNamedFlagValue(args, 'label');
|
|
4964
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
|
+
}
|
|
4965
5553
|
/**
|
|
4966
5554
|
* `pugi jobs` — surface the persistent JobRegistry on the CLI.
|
|
4967
5555
|
* Sprint α5.9 (ADR-0056 PR-PUGI-CLI-M1-GAP-J). Subcommand parsing
|