@pugi/cli 0.1.0-beta.14 → 0.1.0-beta.16

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/bin/run.js CHANGED
@@ -1,2 +1,34 @@
1
1
  #!/usr/bin/env node
2
- import '../dist/index.js';
2
+ // 2026-05-27 — silence the noisy `node:sqlite` ExperimentalWarning.
3
+ // Node 22.x prints a multi-line warning the first time any consumer
4
+ // imports `node:sqlite` (see core/repl/store/session-store.ts). CEO has
5
+ // pinged this surface twice now ("(node:XXXXX) ExperimentalWarning:
6
+ // SQLite is an experimental feature and might change at any time" +
7
+ // trace-warnings hint). Hiding it cleanly requires a tap on
8
+ // process.emitWarning BEFORE any module loads the sqlite binding —
9
+ // hence wiring it here in the binary entry, not deep inside the CLI.
10
+ //
11
+ // We allowlist only the exact sqlite warning + leave every other
12
+ // runtime warning (deprecation, security, custom code paths) intact.
13
+ // Operator can still re-enable the noise by exporting
14
+ // PUGI_SHOW_EXPERIMENTAL_WARNINGS=1 — useful when debugging a node
15
+ // version bump that might surface a new experimental flag we should
16
+ // notice.
17
+ if (!process.env.PUGI_SHOW_EXPERIMENTAL_WARNINGS) {
18
+ const originalEmit = process.emit;
19
+ process.emit = function patchedEmit(name, ...args) {
20
+ if (name === 'warning') {
21
+ const w = args[0];
22
+ const isSqliteExperimental =
23
+ w &&
24
+ typeof w === 'object' &&
25
+ w.name === 'ExperimentalWarning' &&
26
+ typeof w.message === 'string' &&
27
+ /SQLite is an experimental feature/i.test(w.message);
28
+ if (isSqliteExperimental) return false;
29
+ }
30
+ return originalEmit.call(this, name, ...args);
31
+ };
32
+ }
33
+
34
+ import('../dist/index.js');
@@ -832,7 +832,176 @@ async function version(_args, flags, _session) {
832
832
  };
833
833
  writeOutput(flags, payload, `pugi ${payload.version}`);
834
834
  }
835
- async function help(_args, flags, _session) {
835
+ /**
836
+ * Per-command help bodies (task #100). When the operator types
837
+ * `pugi <cmd> --help` the dispatcher routes here with `args = [cmd]`.
838
+ * If we have a focused body for that command, print it instead of the
839
+ * global summary. Falls back to the global summary so unknown / new
840
+ * commands still get a useful response.
841
+ *
842
+ * Source of truth for each entry: the comment block at the top of the
843
+ * command's implementation module + any flags the command declares.
844
+ * Keep entries short — operators want the one-liner of intent + the
845
+ * 2-5 most useful flags, not a tutorial. The global help still has the
846
+ * full per-section reference; the per-command body is the "tell me
847
+ * how to use this NOW" surface.
848
+ */
849
+ const COMMAND_HELP_BODIES = {
850
+ init: [
851
+ 'pugi init — bootstrap a new Pugi workspace in the current directory.',
852
+ '',
853
+ 'Creates .pugi/{PUGI.md, mcp.json, index.json, artifacts/, sessions/} and',
854
+ 'seeds the 6 default skills. Idempotent — running again only fills gaps.',
855
+ '',
856
+ 'Flags:',
857
+ ' --no-defaults Skip the bundled default-skills install.',
858
+ '',
859
+ 'Env:',
860
+ ' PUGI_INIT_NO_DEFAULTS=1 Same as --no-defaults.',
861
+ ],
862
+ explain: [
863
+ 'pugi explain "<question>" — read-only Q&A about the workspace.',
864
+ '',
865
+ 'Calls the engine loop in explain mode (budget: 5 calls / 20k tokens).',
866
+ 'No file writes; safe to run against unfamiliar code.',
867
+ '',
868
+ 'Examples:',
869
+ ' pugi explain "what does this package.json define?"',
870
+ ' pugi explain "trace the auth flow in src/auth/"',
871
+ ],
872
+ code: [
873
+ 'pugi code "<brief>" — engineering-mode write loop (30k token budget).',
874
+ '',
875
+ 'Writes files in the current workspace. Use --no-tty in CI / pipes.',
876
+ ],
877
+ fix: [
878
+ 'pugi fix "<brief>" — minimal-diff bugfix loop (30k token budget).',
879
+ '',
880
+ 'Same as `pugi code` but the prompt biases toward the smallest patch',
881
+ 'that closes the brief — refuses scope creep / refactor invitations.',
882
+ ],
883
+ build: [
884
+ 'pugi build "<brief>" — feature-build loop (200k token budget).',
885
+ '',
886
+ 'Multi-turn engineering with plan-review checkpoints. Pairs with',
887
+ 'pugi plan --decompose <idea> when the brief is bigger than one PR.',
888
+ ],
889
+ plan: [
890
+ 'pugi plan --decompose <idea> — split an idea into 3-7 components.',
891
+ '',
892
+ 'Writes .pugi/plan/<session-id>/splits/NN-<name>/spec.md plus',
893
+ 'manifest.md with the dependency DAG. Pass each split to `pugi build`.',
894
+ ],
895
+ review: [
896
+ 'pugi review — code review surfaces.',
897
+ '',
898
+ ' --triple 3-model consensus via Anvil paid fleet.',
899
+ ' --triple --commit <SHA> Review a specific commit (vs origin/main).',
900
+ ' --consensus Customer-facing consensus review (codex + claude + deepseek).',
901
+ ' Optional: --commit <sha> | --pr <num> | --branch <name>.',
902
+ '',
903
+ 'Exit codes: 0 PASS · 1 WARN · 2 BLOCK · 5 auth_missing · 7 rate_limited.',
904
+ ],
905
+ privacy: [
906
+ 'pugi privacy — privacy-mode operations.',
907
+ '',
908
+ ' show Display effective mode + source.',
909
+ ' set <mode> Local-only legacy values (local-only|metadata|full).',
910
+ '',
911
+ 'For tenant-scoped server-side modes (strict|balanced|permissive), use:',
912
+ ' pugi config get privacy',
913
+ ' pugi config set privacy=<mode>',
914
+ ],
915
+ config: [
916
+ 'pugi config — read / write CLI + tenant configuration.',
917
+ '',
918
+ ' get <key> Local config value.',
919
+ ' get privacy Tenant privacy snapshot (admin-api).',
920
+ ' get routing Effective routing table.',
921
+ ' set <key>=<value> Local config write.',
922
+ ' set privacy=<mode> Flip tenant privacy (strict|balanced|permissive).',
923
+ ' set routing.<tag>.<budget>=<model> Override one routing lane.',
924
+ ' unset routing.<tag>.<budget> Revert a routing override.',
925
+ ' mcp trust|deny|list <name> MCP server trust + visibility.',
926
+ ],
927
+ sync: [
928
+ 'pugi sync — explicit-continuation handoff bundle upload.',
929
+ '',
930
+ ' --dry-run Print the bundle plan without uploading.',
931
+ ' --privacy <mode> Override per-bundle privacy posture.',
932
+ ],
933
+ whoami: [
934
+ 'pugi whoami — show the active credential + JWT principal + plan tier.',
935
+ '',
936
+ 'Reads from ~/.pugi/credentials.json. No network call unless --remote.',
937
+ ],
938
+ login: [
939
+ 'pugi login — authenticate against an api.pugi.io endpoint.',
940
+ '',
941
+ 'Interactive picker by default (browser OAuth / PAT / env). Non-interactive:',
942
+ ' --provider device Device-flow OAuth.',
943
+ ' --provider token --token <jwt> Pass a JWT directly.',
944
+ ' --provider env --env PUGI_API_KEY Read from an env var.',
945
+ ],
946
+ accounts: [
947
+ 'pugi accounts — manage stored credentials across endpoints.',
948
+ '',
949
+ ' list Every account + its endpoint + active flag.',
950
+ ' switch <label> Re-point the active account.',
951
+ ' remove <label> Delete a stored credential.',
952
+ ],
953
+ jobs: [
954
+ 'pugi jobs — list, tail, or kill background dispatch jobs.',
955
+ '',
956
+ ' list All jobs in the registry.',
957
+ ' tail <id> Stream output from one job.',
958
+ ' kill <id> Cancel a running job.',
959
+ ],
960
+ delegate: [
961
+ 'pugi delegate <slug> "<brief>" — dispatch a brief to one specialist persona.',
962
+ '',
963
+ 'Slugs (Tier 1 alpha 7.5): dev qa pm devops researcher analyst designer',
964
+ 'frontend architect. `pugi roster` lists the live set.',
965
+ ],
966
+ roster: [
967
+ 'pugi roster — list the live Tier 1 personas + roles.',
968
+ ],
969
+ doctor: [
970
+ 'pugi doctor — diagnose CLI + workspace + adapter capabilities.',
971
+ '',
972
+ 'Prints CLI version, Node version, workspace state (.pugi presence,',
973
+ 'event log, settings), permission mode, and the capability matrix per',
974
+ 'engine adapter. Safe to run anywhere; no network calls.',
975
+ ],
976
+ ask: [
977
+ 'pugi ask "<question>" — surface a yes/no question modal locally.',
978
+ '',
979
+ 'Useful in shell scripts that need a human-confirm before a destructive',
980
+ 'step. Exits 0 on yes, 1 on no, 2 on cancel.',
981
+ ],
982
+ deploy: [
983
+ 'pugi deploy — trigger a vendor deployment from the bound Git source.',
984
+ '',
985
+ ' --target vercel <vercelProject> --project <id> Vercel deploy.',
986
+ ' --target render <renderService> --project <id> Render deploy (Sprint 2 stub).',
987
+ ' --status <id> Vendor-agnostic status snapshot.',
988
+ ' --logs <id> [--tail] Build-log tail.',
989
+ '',
990
+ 'Optional: --target-env production|preview, --ref <ref>, --integration <id>.',
991
+ ],
992
+ };
993
+ async function help(args, flags, _session) {
994
+ // 2026-05-27 task #100: per-command help bodies. When dispatcher
995
+ // routed `pugi <cmd> --help` here it passes `args = [cmd]`; if we
996
+ // have a focused body, print that. Falls through to the global
997
+ // summary on unknown / new commands so the dispatcher's redirect
998
+ // never produces a worse-than-baseline response.
999
+ const requested = args[0];
1000
+ if (requested && COMMAND_HELP_BODIES[requested]) {
1001
+ const body = COMMAND_HELP_BODIES[requested];
1002
+ writeOutput(flags, { command: requested, lines: body }, body.join('\n'));
1003
+ return;
1004
+ }
836
1005
  const commands = Object.keys(handlers).sort();
837
1006
  writeOutput(flags, { commands }, [
838
1007
  'Pugi CLI',
@@ -0,0 +1,299 @@
1
+ /**
2
+ * PAVF-7 — `pugi report --from-error` field-bug capture.
3
+ *
4
+ * Operator hit a CLI failure ("pugi explain: failed [auth_missing]...")
5
+ * and wants to file a clean report без manual log-grepping. This command:
6
+ *
7
+ * 1. Locates the most-recently-modified session under .pugi/sessions/
8
+ * (the engine adapter mirrors EVERY dispatch's events to a fresh
9
+ * session dir; the latest one is always the failure that just
10
+ * surprised the operator).
11
+ * 2. Reads events.jsonl + extracts the terminal-state event +
12
+ * the last 50 frames before it (enough context to triage; small
13
+ * enough for a GH issue body or email paste).
14
+ * 3. Captures workspace metadata (CLI version, Node version, OS,
15
+ * tenant id from credentials, current dir, .pugi/PUGI.md presence).
16
+ * 4. Strips secrets — auth tokens, env values, JWT signatures —
17
+ * before the report ever touches disk OR network.
18
+ * 5. Writes the bundle к .pugi/reports/<ISO-timestamp>-<session-id>/
19
+ * with both a machine-readable report.json and a human-readable
20
+ * report.md the operator can paste into a GH issue / email.
21
+ * 6. Prints the path + the canonical share command the operator can
22
+ * run when ready to upload (the upload endpoint is deferred to a
23
+ * follow-up; v1 keeps everything LOCAL so an operator working
24
+ * offline / behind a corporate firewall can still file a clean
25
+ * report).
26
+ *
27
+ * Why not auto-upload in v1:
28
+ * The CEO HARD rule `feedback_no_fake_dispatch_promises` says we do
29
+ * not invent dispatch we cannot deliver. Without a live
30
+ * /api/pugi/report endpoint, an auto-upload would either silently
31
+ * no-op or claim shipped и lie. v1 emits the artifacts + a clear
32
+ * "upload pending" status; v2 (separate PR) wires the endpoint и
33
+ * flips the default к upload-on-success.
34
+ *
35
+ * Exit codes (match the existing PAVF-1 stage_code table):
36
+ * 0 = report written successfully
37
+ * 8 = no sessions found (operator ran in a workspace без .pugi/)
38
+ * 9 = session events.jsonl unreadable / corrupted
39
+ * 20 = output path not writable (disk full / perms)
40
+ *
41
+ * Secret-redaction posture: PII / tokens / env values are stripped at
42
+ * the report-generation layer, NOT at upload time. Even if the operator
43
+ * never uploads, the report dir on disk MUST NOT carry plaintext
44
+ * secrets — a colleague who later runs `cat .pugi/reports/.../report.md`
45
+ * over the shoulder sees the bug context but not the bearer token.
46
+ */
47
+ import { existsSync, readdirSync, readFileSync, mkdirSync, statSync, writeFileSync } from 'node:fs';
48
+ import { join, resolve as resolvePath } from 'node:path';
49
+ import { homedir, platform, release } from 'node:os';
50
+ import { PUGI_CLI_VERSION } from '../version.js';
51
+ const MAX_TAIL_FRAMES = 50;
52
+ const MAX_DETAIL_CHARS = 400;
53
+ const TERMINAL_TYPES = new Set([
54
+ 'agent.completed',
55
+ 'agent.failed',
56
+ 'agent.blocked',
57
+ 'subagent.outcome',
58
+ 'result',
59
+ ]);
60
+ /**
61
+ * Bearer / JWT / env-secret patterns. We do NOT try to be exhaustive
62
+ * (cat-and-mouse with custom secret formats is unwinnable); we cover
63
+ * the shapes that actually appear in Pugi sessions:
64
+ *
65
+ * - `Authorization: Bearer eyJ...` (JWT header.payload.signature)
66
+ * - `apiKey: eyJ...` inside captured JSON envelopes
67
+ * - any long base64-ish token (>= 20 chars, [A-Za-z0-9_-]) following
68
+ * `token`, `password`, `secret`, or `key` field names
69
+ *
70
+ * Replacement is a length-preserving `[REDACTED:<n>]` marker so the
71
+ * operator can still verify the report at-a-glance ("yes, a 32-char
72
+ * token was here") без leaking the value.
73
+ */
74
+ function redact(text) {
75
+ if (!text)
76
+ return text;
77
+ // Bearer + JWT shape.
78
+ text = text.replace(/(Bearer\s+)([A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)/gi, (_m, prefix, tok) => `${prefix}[REDACTED:${tok.length}]`);
79
+ // Bare JWTs (no Bearer prefix) inside JSON / log lines.
80
+ text = text.replace(/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g, (tok) => `[REDACTED:${tok.length}]`);
81
+ // `"token": "..."` / `"apiKey": "..."` / `"password": "..."` shapes.
82
+ text = text.replace(/("(?:apiKey|api_key|token|access_token|refresh_token|password|secret|bearer)"\s*:\s*")([^"]{10,})(")/gi, (_m, before, val, after) => `${before}[REDACTED:${val.length}]${after}`);
83
+ // Bare env-style KEY=VALUE на длинных значениях.
84
+ text = text.replace(/\b((?:PUGI_API_KEY|GITHUB_TOKEN|NPM_TOKEN|ANVIL_API_KEY|OPENAI_API_KEY|ANTHROPIC_API_KEY|GEMINI_API_KEY)=)([^\s"']{10,})/g, (_m, prefix, val) => `${prefix}[REDACTED:${val.length}]`);
85
+ return text;
86
+ }
87
+ function clampDetail(value) {
88
+ if (typeof value !== 'string')
89
+ return undefined;
90
+ const redacted = redact(value);
91
+ return redacted.length > MAX_DETAIL_CHARS
92
+ ? `${redacted.slice(0, MAX_DETAIL_CHARS)}…`
93
+ : redacted;
94
+ }
95
+ function findLatestSession(cwd) {
96
+ const dir = resolvePath(cwd, '.pugi/sessions');
97
+ if (!existsSync(dir))
98
+ return null;
99
+ const entries = readdirSync(dir, { withFileTypes: true })
100
+ .filter((e) => e.isDirectory())
101
+ .map((e) => {
102
+ const path = join(dir, e.name);
103
+ let mtime = 0;
104
+ try {
105
+ mtime = statSync(join(path, 'events.jsonl')).mtimeMs;
106
+ }
107
+ catch {
108
+ // Session dir without events.jsonl yet — never opened. Skip.
109
+ return null;
110
+ }
111
+ return { name: e.name, path, mtime };
112
+ })
113
+ .filter((x) => x !== null)
114
+ .sort((a, b) => b.mtime - a.mtime);
115
+ return entries[0]?.path ?? null;
116
+ }
117
+ function readTenantIdSafely() {
118
+ const credPath = resolvePath(homedir(), '.pugi/credentials.json');
119
+ if (!existsSync(credPath))
120
+ return undefined;
121
+ try {
122
+ const raw = JSON.parse(readFileSync(credPath, 'utf8'));
123
+ const first = raw.tokens?.[0]?.apiKey;
124
+ if (!first || typeof first !== 'string')
125
+ return undefined;
126
+ // JWT payload is the middle segment; base64-decode + parse for the
127
+ // `customerId` claim. Failure here returns undefined (the report
128
+ // still emits useful context without it).
129
+ const parts = first.split('.');
130
+ if (parts.length !== 3)
131
+ return undefined;
132
+ const payload = JSON.parse(Buffer.from(parts[1] ?? '', 'base64').toString('utf8'));
133
+ return typeof payload.customerId === 'string' ? payload.customerId : undefined;
134
+ }
135
+ catch {
136
+ return undefined;
137
+ }
138
+ }
139
+ function captureFrames(eventsPath) {
140
+ const lines = readFileSync(eventsPath, 'utf8')
141
+ .split('\n')
142
+ .filter((l) => l.trim().length > 0);
143
+ const parsed = lines
144
+ .map((line) => {
145
+ try {
146
+ return JSON.parse(line);
147
+ }
148
+ catch {
149
+ return null;
150
+ }
151
+ })
152
+ .filter((f) => f !== null);
153
+ // Keep the LAST MAX_TAIL_FRAMES frames — failures cluster at the
154
+ // end, and the tail is where the operator's context actually lives.
155
+ const tail = parsed.slice(-MAX_TAIL_FRAMES);
156
+ return tail.map((f) => {
157
+ const out = {
158
+ type: typeof f.type === 'string' ? f.type : 'unknown',
159
+ };
160
+ if (typeof f.taskId === 'string')
161
+ out.taskId = f.taskId;
162
+ if (typeof f.timestamp === 'string')
163
+ out.timestamp = f.timestamp;
164
+ if (typeof f.outcome === 'string')
165
+ out.outcome = f.outcome;
166
+ // Keep detail / error ONLY on terminal frames (full reply text on
167
+ // every agent.message would blow the report past the GH issue cap).
168
+ if (TERMINAL_TYPES.has(out.type)) {
169
+ const detail = clampDetail(f.detail) ?? clampDetail(f.error);
170
+ if (detail)
171
+ out.detail = detail;
172
+ if (typeof f.error === 'string')
173
+ out.error = clampDetail(f.error);
174
+ }
175
+ return out;
176
+ });
177
+ }
178
+ export function runReport(args, ctx) {
179
+ const fromError = args.includes('--from-error');
180
+ if (!fromError) {
181
+ ctx.writeOutput({
182
+ command: 'report',
183
+ status: 'no_sessions',
184
+ message: 'pugi report — capture a bug report from the most-recent session.\n\n' +
185
+ 'Usage:\n' +
186
+ ' pugi report --from-error Bundle the most-recent failed session as a report.\n\n' +
187
+ 'Output: writes .pugi/reports/<timestamp>-<session-id>/{report.json, report.md}.\n' +
188
+ 'Secrets (bearer tokens, JWTs, named env values) are stripped before disk write.',
189
+ }, 'pugi report — see `pugi report --help`');
190
+ return 0;
191
+ }
192
+ const sessionPath = findLatestSession(ctx.cwd);
193
+ if (!sessionPath) {
194
+ ctx.writeOutput({
195
+ command: 'report',
196
+ status: 'no_sessions',
197
+ message: 'No sessions found under .pugi/sessions/. Run a `pugi` command first.',
198
+ }, 'pugi report: no sessions found under .pugi/sessions/ — run a `pugi` command first.');
199
+ return 8;
200
+ }
201
+ const eventsPath = join(sessionPath, 'events.jsonl');
202
+ let frames;
203
+ try {
204
+ frames = captureFrames(eventsPath);
205
+ }
206
+ catch (err) {
207
+ const message = err instanceof Error ? err.message : String(err);
208
+ ctx.writeOutput({
209
+ command: 'report',
210
+ status: 'unreadable',
211
+ message: `Failed to read ${eventsPath}: ${message}`,
212
+ }, `pugi report: cannot read session events (${message})`);
213
+ return 9;
214
+ }
215
+ const sessionId = sessionPath.split('/').pop() ?? 'unknown';
216
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
217
+ const reportDir = resolvePath(ctx.cwd, '.pugi/reports', `${timestamp}-${sessionId}`);
218
+ let reportJson;
219
+ let reportMd;
220
+ try {
221
+ mkdirSync(reportDir, { recursive: true });
222
+ reportJson = join(reportDir, 'report.json');
223
+ reportMd = join(reportDir, 'report.md');
224
+ const meta = {
225
+ schema: 1,
226
+ generatedAt: new Date().toISOString(),
227
+ cliVersion: PUGI_CLI_VERSION,
228
+ nodeVersion: process.version,
229
+ os: `${platform()} ${release()}`,
230
+ cwd: ctx.cwd,
231
+ sessionId,
232
+ tenantId: readTenantIdSafely() ?? '(not resolvable)',
233
+ pugiMd: existsSync(resolvePath(ctx.cwd, '.pugi/PUGI.md')),
234
+ frames,
235
+ };
236
+ writeFileSync(reportJson, JSON.stringify(meta, null, 2), 'utf8');
237
+ const mdLines = [
238
+ `# Pugi bug report — ${sessionId}`,
239
+ '',
240
+ `Generated: \`${meta.generatedAt}\``,
241
+ `CLI version: \`${meta.cliVersion}\``,
242
+ `Node: \`${meta.nodeVersion}\` · OS: \`${meta.os}\``,
243
+ `Workspace: \`${meta.cwd}\` (PUGI.md present: ${meta.pugiMd ? 'yes' : 'no'})`,
244
+ `Tenant: \`${meta.tenantId}\``,
245
+ '',
246
+ `## Last ${frames.length} frames`,
247
+ '',
248
+ '```jsonl',
249
+ ...frames.map((f) => JSON.stringify(f)),
250
+ '```',
251
+ '',
252
+ '## How to share',
253
+ '',
254
+ '1. Review `report.md` for accidental PII or sensitive paths.',
255
+ '2. Paste the contents into a GH issue at https://github.com/pugi-io/pugi/issues',
256
+ ' OR attach the `report.json` as a file.',
257
+ '',
258
+ 'Auto-upload to api.pugi.io is planned (`pugi report --upload`) but',
259
+ 'NOT shipped in this build — v1 keeps everything local so an operator',
260
+ 'behind a firewall can still file a clean report.',
261
+ ];
262
+ writeFileSync(reportMd, mdLines.join('\n'), 'utf8');
263
+ }
264
+ catch (err) {
265
+ const message = err instanceof Error ? err.message : String(err);
266
+ ctx.writeOutput({
267
+ command: 'report',
268
+ status: 'output_not_writable',
269
+ message: `Failed to write report bundle to ${reportDir}: ${message}`,
270
+ }, `pugi report: cannot write report dir (${message})`);
271
+ return 20;
272
+ }
273
+ ctx.writeOutput({
274
+ command: 'report',
275
+ status: 'written',
276
+ reportDir,
277
+ reportJson,
278
+ reportMd,
279
+ sessionId,
280
+ message: `Report written: ${reportDir}`,
281
+ }, [
282
+ `pugi report: bundle written`,
283
+ ` Session: ${sessionId}`,
284
+ ` Frames captured: ${frames.length}`,
285
+ ` Files:`,
286
+ ` ${reportJson}`,
287
+ ` ${reportMd}`,
288
+ ``,
289
+ `Review report.md for accidental PII, then paste into a GH issue OR`,
290
+ `attach report.json. Auto-upload is planned for a follow-up build.`,
291
+ ].join('\n'));
292
+ return 0;
293
+ }
294
+ // Test seam — the redactor is the most-tested piece (false negatives
295
+ // leak secrets; false positives garble bug context). Exported so
296
+ // apps/pugi-cli/test/report.spec.ts can assert the regex behaviour
297
+ // без spinning up a full session.
298
+ export const __INTERNAL_FOR_TESTS = { redact, clampDetail };
299
+ //# sourceMappingURL=report.js.map
@@ -44,7 +44,7 @@ export function sanitizeSemver(raw) {
44
44
  * during import). When bumping the CLI version BOTH literals must be
45
45
  * updated; the release smoke-test (`pack:smoke`) verifies they agree.
46
46
  */
47
- export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.14');
47
+ export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.16');
48
48
  /**
49
49
  * Outbound: the CLI's installed semver. Read at request time by
50
50
  * `version-interceptor.ts` and injected on every `fetch` call.
@@ -16,6 +16,8 @@
16
16
  * a tiny `event:`/`data:`/`id:` parser. This keeps the dependency
17
17
  * graph at zero new packages.
18
18
  */
19
+ import { existsSync } from 'node:fs';
20
+ import { resolve } from 'node:path';
19
21
  import React from 'react';
20
22
  import { render } from 'ink';
21
23
  import { Repl } from './repl.js';
@@ -314,11 +316,9 @@ export function drainBufferedStdin(stdin = process.stdin) {
314
316
  * future unit spec can lock the contract.
315
317
  */
316
318
  export function isProjectRoot(cwd) {
317
- // Local import keeps the bootstrap free of top-of-file `fs` calls
318
- // that would run at module-load time Ink + the SSE transport are
319
- // happier when this file's side-effect surface stays small.
320
- const { existsSync } = require('node:fs');
321
- const { resolve } = require('node:path');
319
+ // ESM static imports `require()` is not defined in a `"type": "module"`
320
+ // bundle and would throw `ReferenceError: require is not defined` the
321
+ // moment the REPL bootstrap calls this gate. Beta.16 P0 fix 2026-05-27.
322
322
  return (existsSync(resolve(cwd, 'package.json')) ||
323
323
  existsSync(resolve(cwd, '.git')) ||
324
324
  existsSync(resolve(cwd, '.pugi')) ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pugi/cli",
3
- "version": "0.1.0-beta.14",
3
+ "version": "0.1.0-beta.16",
4
4
  "description": "Pugi CLI - terminal-native software execution system",
5
5
  "homepage": "https://pugi.io",
6
6
  "repository": {
@@ -48,13 +48,13 @@
48
48
  "ink": "^5.0.1",
49
49
  "linkedom": "^0.18.12",
50
50
  "react": "^18.3.1",
51
- "tar": "^6.2.1",
51
+ "tar": "^7.5.11",
52
52
  "tinyglobby": "^0.2.16",
53
53
  "turndown": "^7.2.4",
54
54
  "undici": "^8.3.0",
55
55
  "zod": "^3.23.0",
56
56
  "@pugi/personas": "0.1.2",
57
- "@pugi/sdk": "0.1.0-beta.14"
57
+ "@pugi/sdk": "0.1.0-beta.16"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/node": "^22.0.0",