fuzzi-cli 0.1.0

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/index.js ADDED
@@ -0,0 +1,1880 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/types/brand.ts
12
+ var BRAND, RISK_COLORS, VERSION, DEFAULT_API_URL;
13
+ var init_brand = __esm({
14
+ "src/types/brand.ts"() {
15
+ "use strict";
16
+ BRAND = {
17
+ accent: "#4FC3A1",
18
+ accentDim: "#3A9A7E",
19
+ text: "#FAFAFA",
20
+ textSecondary: "#8C8C8C",
21
+ bg: "#0A0A0A",
22
+ border: "#2A2A2A"
23
+ };
24
+ RISK_COLORS = {
25
+ LOW: "#22C55E",
26
+ MEDIUM: "#F59E0B",
27
+ HIGH: "#EF4444",
28
+ CRITICAL: "#A855F7"
29
+ };
30
+ VERSION = "0.1.0";
31
+ DEFAULT_API_URL = "https://app.fuzzi.dev/api";
32
+ }
33
+ });
34
+
35
+ // src/lib/credentials.ts
36
+ import { mkdir, readFile, writeFile, chmod, unlink } from "fs/promises";
37
+ import { homedir } from "os";
38
+ import { join } from "path";
39
+ function fuzziDir() {
40
+ return FUZZI_DIR;
41
+ }
42
+ async function ensureDir() {
43
+ await mkdir(FUZZI_DIR, { recursive: true, mode: 448 });
44
+ }
45
+ function normalizeCredentials(raw) {
46
+ if (raw.api_key && typeof raw.api_key === "string") {
47
+ return {
48
+ api_key: raw.api_key,
49
+ auth_method: "api_key",
50
+ key_prefix: raw.key_prefix || raw.api_key.slice(0, 12) + "...",
51
+ key_expires_at: raw.key_expires_at,
52
+ email: raw.email,
53
+ full_name: raw.full_name,
54
+ saved_at: raw.saved_at || (/* @__PURE__ */ new Date()).toISOString()
55
+ };
56
+ }
57
+ if (raw.access_token && typeof raw.access_token === "string") {
58
+ return {
59
+ api_key: raw.access_token,
60
+ auth_method: "api_key",
61
+ key_prefix: raw.access_token.slice(0, 12) + "...",
62
+ email: raw.email,
63
+ full_name: raw.full_name,
64
+ saved_at: raw.saved_at || (/* @__PURE__ */ new Date()).toISOString()
65
+ };
66
+ }
67
+ throw new Error("Invalid credentials file");
68
+ }
69
+ async function loadCredentials() {
70
+ for (const path of [CREDENTIALS_PATH, LEGACY_CREDENTIALS_PATH]) {
71
+ try {
72
+ const raw = await readFile(path, "utf8");
73
+ const parsed = JSON.parse(raw);
74
+ if (!parsed || Object.keys(parsed).length === 0) return null;
75
+ return normalizeCredentials(parsed);
76
+ } catch {
77
+ }
78
+ }
79
+ return null;
80
+ }
81
+ async function saveCredentials(creds) {
82
+ await ensureDir();
83
+ await writeFile(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), { mode: 384 });
84
+ try {
85
+ await chmod(CREDENTIALS_PATH, 384);
86
+ } catch {
87
+ }
88
+ }
89
+ async function clearCredentials() {
90
+ for (const path of [CREDENTIALS_PATH, LEGACY_CREDENTIALS_PATH]) {
91
+ try {
92
+ await unlink(path);
93
+ } catch {
94
+ }
95
+ }
96
+ }
97
+ function maskApiKey(key) {
98
+ if (key.length <= 16) return key.slice(0, 8) + "...";
99
+ return key.slice(0, 12) + "...";
100
+ }
101
+ function isValidApiKeyFormat(key) {
102
+ return /^fz_live_[A-Za-z0-9_-]{20,}$/.test(key.trim());
103
+ }
104
+ var FUZZI_DIR, CREDENTIALS_PATH, LEGACY_CREDENTIALS_PATH;
105
+ var init_credentials = __esm({
106
+ "src/lib/credentials.ts"() {
107
+ "use strict";
108
+ FUZZI_DIR = join(homedir(), ".fuzzi");
109
+ CREDENTIALS_PATH = join(FUZZI_DIR, "credentials");
110
+ LEGACY_CREDENTIALS_PATH = join(FUZZI_DIR, "credentials.json");
111
+ }
112
+ });
113
+
114
+ // src/lib/config.ts
115
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod as chmod2 } from "fs/promises";
116
+ import { homedir as homedir2 } from "os";
117
+ import { join as join2 } from "path";
118
+ async function ensureDir2() {
119
+ await mkdir2(fuzziDir(), { recursive: true, mode: 448 });
120
+ }
121
+ async function loadConfig() {
122
+ for (const path of [CONFIG_PATH, LEGACY_CONFIG_PATH]) {
123
+ try {
124
+ const raw = await readFile2(path, "utf8");
125
+ const parsed = JSON.parse(raw);
126
+ return {
127
+ api_url: process.env.FUZZI_API_URL || parsed.api_url || DEFAULT_API_URL,
128
+ default_env: parsed.default_env,
129
+ default_format: parsed.default_format,
130
+ ...parsed
131
+ };
132
+ } catch {
133
+ }
134
+ }
135
+ return { api_url: process.env.FUZZI_API_URL || DEFAULT_API_URL };
136
+ }
137
+ async function saveConfig(config) {
138
+ await ensureDir2();
139
+ await writeFile2(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 384 });
140
+ try {
141
+ await chmod2(CONFIG_PATH, 384);
142
+ } catch {
143
+ }
144
+ }
145
+ async function setConfigValue(key, value) {
146
+ const config = await loadConfig();
147
+ if (key === "api_url") config.api_url = value;
148
+ else if (key === "default_env") config.default_env = value;
149
+ else if (key === "default_format") config.default_format = value;
150
+ else config[key] = value;
151
+ await saveConfig(config);
152
+ }
153
+ async function getConfigValue(key) {
154
+ const config = await loadConfig();
155
+ return config[key];
156
+ }
157
+ async function listConfigEntries() {
158
+ const config = await loadConfig();
159
+ const entries = {};
160
+ for (const [k, v] of Object.entries(config)) {
161
+ if (v !== void 0) entries[k] = String(v);
162
+ }
163
+ return entries;
164
+ }
165
+ var CONFIG_PATH, LEGACY_CONFIG_PATH;
166
+ var init_config = __esm({
167
+ "src/lib/config.ts"() {
168
+ "use strict";
169
+ init_brand();
170
+ init_credentials();
171
+ CONFIG_PATH = join2(homedir2(), ".fuzzi", "config");
172
+ LEGACY_CONFIG_PATH = join2(homedir2(), ".fuzzi", "config.json");
173
+ }
174
+ });
175
+
176
+ // src/lib/logger.ts
177
+ function currentLevel() {
178
+ if (process.env.FUZZI_DEBUG === "1" || process.env.FUZZI_DEBUG === "true") return "debug";
179
+ if (process.env.FUZZI_LOG_LEVEL) return process.env.FUZZI_LOG_LEVEL;
180
+ return "warn";
181
+ }
182
+ function shouldLog(level) {
183
+ return LEVELS[level] >= LEVELS[currentLevel()];
184
+ }
185
+ function stamp() {
186
+ return (/* @__PURE__ */ new Date()).toISOString();
187
+ }
188
+ function isDebugMode() {
189
+ return currentLevel() === "debug";
190
+ }
191
+ var LEVELS, log;
192
+ var init_logger = __esm({
193
+ "src/lib/logger.ts"() {
194
+ "use strict";
195
+ LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
196
+ log = {
197
+ debug(...args) {
198
+ if (shouldLog("debug")) console.error(`[fuzzi ${stamp()}]`, ...args);
199
+ },
200
+ info(...args) {
201
+ if (shouldLog("info")) console.error(`[fuzzi ${stamp()}]`, ...args);
202
+ },
203
+ warn(...args) {
204
+ if (shouldLog("warn")) console.error(`[fuzzi ${stamp()}]`, ...args);
205
+ },
206
+ error(...args) {
207
+ if (shouldLog("error")) console.error(`[fuzzi ${stamp()}]`, ...args);
208
+ }
209
+ };
210
+ }
211
+ });
212
+
213
+ // src/lib/api-client.ts
214
+ function mapErrorMessage(status, body) {
215
+ const code = body.code?.toLowerCase();
216
+ const msg = body.error || body.message || "";
217
+ if (status === 401) {
218
+ if (code === "key_revoked" || msg.toLowerCase().includes("revoked")) {
219
+ return "API key has been revoked. Please log in again.";
220
+ }
221
+ if (code === "key_expired" || msg.toLowerCase().includes("expired")) {
222
+ return "API key has expired. Generate a new one at https://app.fuzzi.dev/settings/api-keys";
223
+ }
224
+ return "Invalid API key. Generate a new one at https://app.fuzzi.dev/settings/api-keys";
225
+ }
226
+ if (status === 403 && (code === "ssrf" || msg.toLowerCase().includes("private ip"))) {
227
+ return "This URL is not allowed (private IP address detected). Please scan a public-facing URL.";
228
+ }
229
+ if (status === 400 && msg.toLowerCase().includes("url")) {
230
+ return "Invalid URL. Please provide a valid URL starting with http:// or https://";
231
+ }
232
+ if (status === 429) {
233
+ return msg || "Rate limit exceeded.";
234
+ }
235
+ if (msg.toLowerCase().startsWith("scan failed")) {
236
+ return msg;
237
+ }
238
+ if (msg) return msg;
239
+ return `Request failed with status ${status}`;
240
+ }
241
+ async function getAuthenticatedClient() {
242
+ const client = await FuzziApiClient.create();
243
+ if (!client["token"]) {
244
+ throw new ApiError("Not authenticated. Run: fuzzi auth login", 401, "not_authenticated", void 0, 2);
245
+ }
246
+ return client;
247
+ }
248
+ var ApiError, FuzziApiClient;
249
+ var init_api_client = __esm({
250
+ "src/lib/api-client.ts"() {
251
+ "use strict";
252
+ init_config();
253
+ init_credentials();
254
+ init_logger();
255
+ ApiError = class extends Error {
256
+ constructor(message, status, code, body, exitCode) {
257
+ super(message);
258
+ this.status = status;
259
+ this.code = code;
260
+ this.body = body;
261
+ this.name = "ApiError";
262
+ this.exitCode = exitCode;
263
+ }
264
+ status;
265
+ code;
266
+ body;
267
+ exitCode;
268
+ };
269
+ FuzziApiClient = class _FuzziApiClient {
270
+ constructor(baseUrl, token) {
271
+ this.baseUrl = baseUrl;
272
+ this.token = token;
273
+ }
274
+ baseUrl;
275
+ token;
276
+ static async create() {
277
+ const config = await loadConfig();
278
+ const creds = await loadCredentials();
279
+ return new _FuzziApiClient(config.api_url.replace(/\/$/, ""), creds?.api_key);
280
+ }
281
+ get base() {
282
+ return this.baseUrl;
283
+ }
284
+ setToken(token) {
285
+ this.token = token;
286
+ }
287
+ headers() {
288
+ const h = { "Content-Type": "application/json", Accept: "application/json" };
289
+ if (this.token) h.Authorization = `Bearer ${this.token}`;
290
+ return h;
291
+ }
292
+ async request(method, path, body) {
293
+ const url = `${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
294
+ log.debug(`${method} ${url}`);
295
+ let res;
296
+ try {
297
+ res = await fetch(url, {
298
+ method,
299
+ headers: this.headers(),
300
+ body: body !== void 0 ? JSON.stringify(body) : void 0
301
+ });
302
+ } catch {
303
+ throw new ApiError(
304
+ "Could not connect to app.fuzzi.dev. Check your internet connection or try again later.",
305
+ 0,
306
+ "network_error",
307
+ void 0,
308
+ 2
309
+ );
310
+ }
311
+ let data;
312
+ const text = await res.text();
313
+ try {
314
+ data = text ? JSON.parse(text) : {};
315
+ } catch {
316
+ data = { error: text };
317
+ }
318
+ if (!res.ok) {
319
+ const errBody = data;
320
+ let message = mapErrorMessage(res.status, errBody);
321
+ if (res.status === 429) {
322
+ const retryAfter = res.headers.get("Retry-After");
323
+ const seconds = retryAfter ? parseInt(retryAfter, 10) : 60;
324
+ message = `Rate limit exceeded. Retry after ${seconds} seconds.`;
325
+ }
326
+ if (res.status >= 500) {
327
+ message = `Scan failed: ${errBody.error || errBody.message || res.statusText}`;
328
+ }
329
+ throw new ApiError(message, res.status, errBody.code, data, 2);
330
+ }
331
+ return data;
332
+ }
333
+ get(path) {
334
+ return this.request("GET", path);
335
+ }
336
+ post(path, body) {
337
+ return this.request("POST", path, body);
338
+ }
339
+ patch(path, body) {
340
+ return this.request("PATCH", path, body);
341
+ }
342
+ delete(path) {
343
+ return this.request("DELETE", path);
344
+ }
345
+ async validateToken() {
346
+ try {
347
+ await this.get("/me");
348
+ return true;
349
+ } catch {
350
+ return false;
351
+ }
352
+ }
353
+ async download(path) {
354
+ const url = `${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
355
+ let res;
356
+ try {
357
+ res = await fetch(url, { headers: this.headers() });
358
+ } catch {
359
+ throw new ApiError(
360
+ "Could not connect to app.fuzzi.dev. Check your internet connection or try again later.",
361
+ 0,
362
+ "network_error",
363
+ void 0,
364
+ 2
365
+ );
366
+ }
367
+ if (!res.ok) {
368
+ const text = await res.text();
369
+ let errBody = {};
370
+ try {
371
+ errBody = JSON.parse(text);
372
+ } catch {
373
+ errBody = { error: text };
374
+ }
375
+ throw new ApiError(mapErrorMessage(res.status, errBody), res.status, errBody.code, errBody, 2);
376
+ }
377
+ const buf = Buffer.from(await res.arrayBuffer());
378
+ return { data: buf, contentType: res.headers.get("content-type") || "application/octet-stream" };
379
+ }
380
+ };
381
+ }
382
+ });
383
+
384
+ // src/terminal/capabilities.ts
385
+ import { stdout } from "process";
386
+ function getCapabilities() {
387
+ if (cached) return cached;
388
+ const cols = stdout.columns ?? 80;
389
+ const term = process.env.TERM ?? "";
390
+ const colorterm = process.env.COLORTERM ?? "";
391
+ const trueColor = colorterm.includes("truecolor") || colorterm.includes("24bit") || term.includes("truecolor") || !!process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0";
392
+ cached = {
393
+ width: Math.max(60, Math.min(cols, 120)),
394
+ trueColor,
395
+ interactive: stdout.isTTY === true
396
+ };
397
+ return cached;
398
+ }
399
+ var cached;
400
+ var init_capabilities = __esm({
401
+ "src/terminal/capabilities.ts"() {
402
+ "use strict";
403
+ cached = null;
404
+ }
405
+ });
406
+
407
+ // src/terminal/theme.ts
408
+ import chalk from "chalk";
409
+ function color(hex, fallback) {
410
+ return getCapabilities().trueColor ? chalk.hex(hex) : fallback;
411
+ }
412
+ function riskColor(level) {
413
+ if (!level) return muted;
414
+ const key = level.toUpperCase();
415
+ const hex = RISK_COLORS[key] || BRAND.textSecondary;
416
+ const fallbacks = {
417
+ LOW: chalk.green,
418
+ MEDIUM: chalk.yellow,
419
+ HIGH: chalk.red,
420
+ CRITICAL: chalk.magenta
421
+ };
422
+ return color(hex, fallbacks[key] ?? chalk.gray).bold;
423
+ }
424
+ function scoreBold(n) {
425
+ if (n == null) return muted("\u2014");
426
+ return chalk.bold(String(n));
427
+ }
428
+ function error(text) {
429
+ return color("#EF4444", chalk.red)(text);
430
+ }
431
+ function success(text) {
432
+ return color("#22C55E", chalk.green)(text);
433
+ }
434
+ function warn(text) {
435
+ return color("#F59E0B", chalk.yellow)(text);
436
+ }
437
+ var accent, accentBold, muted, bold, dim;
438
+ var init_theme = __esm({
439
+ "src/terminal/theme.ts"() {
440
+ "use strict";
441
+ init_brand();
442
+ init_capabilities();
443
+ accent = color(BRAND.accent, chalk.cyan);
444
+ accentBold = accent.bold;
445
+ muted = color(BRAND.textSecondary, chalk.gray);
446
+ bold = chalk.bold;
447
+ dim = chalk.dim;
448
+ }
449
+ });
450
+
451
+ // src/lib/theme.ts
452
+ var init_theme2 = __esm({
453
+ "src/lib/theme.ts"() {
454
+ "use strict";
455
+ init_theme();
456
+ }
457
+ });
458
+
459
+ // src/terminal/table.ts
460
+ import Table from "cli-table3";
461
+ function createTable(headers, rows) {
462
+ const table = new Table({
463
+ head: headers.map((h) => muted(h)),
464
+ style: { head: [], border: [] },
465
+ chars: BOX_CHARS
466
+ });
467
+ for (const row of rows) table.push(row);
468
+ return table.toString();
469
+ }
470
+ var BOX_CHARS;
471
+ var init_table = __esm({
472
+ "src/terminal/table.ts"() {
473
+ "use strict";
474
+ init_theme();
475
+ BOX_CHARS = {
476
+ mid: "\u2500",
477
+ "left-mid": "\u251C",
478
+ "mid-mid": "\u253C",
479
+ "right-mid": "\u2524"
480
+ };
481
+ }
482
+ });
483
+
484
+ // src/terminal/strings.ts
485
+ function truncate(s, max) {
486
+ if (s.length <= max) return s;
487
+ return s.slice(0, max - 1) + "\u2026";
488
+ }
489
+ function padEndVisible(s, width) {
490
+ const plain = s.replace(/\x1b\[[0-9;]*m/g, "");
491
+ const pad = Math.max(0, width - plain.length);
492
+ return s + " ".repeat(pad);
493
+ }
494
+ function formatTimestamp(iso) {
495
+ if (!iso) return "\u2014";
496
+ const d = new Date(iso);
497
+ const p = (n) => String(n).padStart(2, "0");
498
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
499
+ }
500
+ function hostnameFromUrl(url) {
501
+ try {
502
+ return new URL(url).hostname;
503
+ } catch {
504
+ return url;
505
+ }
506
+ }
507
+ var init_strings = __esm({
508
+ "src/terminal/strings.ts"() {
509
+ "use strict";
510
+ }
511
+ });
512
+
513
+ // src/terminal/layout.ts
514
+ import boxen from "boxen";
515
+ function panel(content, opts = {}) {
516
+ return boxen(content, {
517
+ title: opts.title ? accentBold(opts.title) : void 0,
518
+ padding: opts.padding ?? 1,
519
+ margin: { top: 0, bottom: opts.marginBottom ?? 1, left: 0, right: 0 },
520
+ borderStyle: "round",
521
+ borderColor: getCapabilities().trueColor ? BRAND.accent : void 0,
522
+ titleAlignment: "left"
523
+ });
524
+ }
525
+ function columns(left, right, leftWidth) {
526
+ const width = leftWidth ?? Math.floor(getCapabilities().width * 0.45);
527
+ const leftLines = left.split("\n");
528
+ const rightLines = right.split("\n");
529
+ const rows = Math.max(leftLines.length, rightLines.length);
530
+ const out = [];
531
+ for (let i = 0; i < rows; i++) {
532
+ const l = padEndVisible(leftLines[i] ?? "", width);
533
+ const r = rightLines[i] ?? "";
534
+ out.push(`${l} ${r}`);
535
+ }
536
+ return out.join("\n");
537
+ }
538
+ function divider(char = "\u2500", width) {
539
+ const w = width ?? getCapabilities().width - 4;
540
+ return dim(char.repeat(Math.max(20, w)));
541
+ }
542
+ function statusBar(parts) {
543
+ return dim(parts.filter(Boolean).join(" \xB7 "));
544
+ }
545
+ function keyValue(rows, indent = 2) {
546
+ const pad = " ".repeat(indent);
547
+ const maxKey = Math.max(...rows.map(([k]) => k.length), 4);
548
+ return rows.map(([k, v]) => `${pad}${muted(k.padEnd(maxKey))} ${v}`).join("\n");
549
+ }
550
+ var init_layout = __esm({
551
+ "src/terminal/layout.ts"() {
552
+ "use strict";
553
+ init_brand();
554
+ init_theme();
555
+ init_capabilities();
556
+ init_strings();
557
+ }
558
+ });
559
+
560
+ // src/commands/report.ts
561
+ var report_exports = {};
562
+ __export(report_exports, {
563
+ runReportCommand: () => runReportCommand
564
+ });
565
+ import { writeFile as writeFile3 } from "fs/promises";
566
+ async function runReportCommand(client, scanId, format, outputPath) {
567
+ const { data, contentType } = await client.download(`/scan/${scanId}/report?format=${format}`);
568
+ const ext = format === "pdf" ? "pdf" : format === "csv" ? "csv" : "json";
569
+ const path = outputPath || `fuzzi-report-${scanId.slice(0, 8)}.${ext}`;
570
+ await writeFile3(path, data);
571
+ return `Report saved to ${path} (${contentType})`;
572
+ }
573
+ var init_report = __esm({
574
+ "src/commands/report.ts"() {
575
+ "use strict";
576
+ }
577
+ });
578
+
579
+ // src/commands/whatif.ts
580
+ var whatif_exports = {};
581
+ __export(whatif_exports, {
582
+ runWhatIfCommand: () => runWhatIfCommand
583
+ });
584
+ async function runWhatIfCommand(scanId, overrides, format = "table") {
585
+ const client = await getAuthenticatedClient();
586
+ const data = await client.post("/whatif", { scan_id: scanId, overrides });
587
+ if (format === "json") return JSON.stringify(data, null, 2);
588
+ return [
589
+ `Original: ${riskColor(data.original.risk_level)(data.original.risk_level)} (${scoreBold(data.original.overall_score)})`,
590
+ `Simulated: ${riskColor(data.simulated.risk_level)(data.simulated.risk_level)} (${scoreBold(data.simulated.overall_score)})`,
591
+ `Delta: ${data.overall_score_delta > 0 ? "+" : ""}${data.overall_score_delta}`,
592
+ data.summary
593
+ ].join("\n");
594
+ }
595
+ var init_whatif = __esm({
596
+ "src/commands/whatif.ts"() {
597
+ "use strict";
598
+ init_api_client();
599
+ init_theme2();
600
+ }
601
+ });
602
+
603
+ // src/commands/compare.ts
604
+ var compare_exports = {};
605
+ __export(compare_exports, {
606
+ runCompareCommand: () => runCompareCommand
607
+ });
608
+ async function runCompareCommand(scanA, scanB, format = "table") {
609
+ const client = await getAuthenticatedClient();
610
+ const data = await client.post("/compare", {
611
+ scan_a_id: scanA,
612
+ scan_b_id: scanB
613
+ });
614
+ if (format === "json") return JSON.stringify(data, null, 2);
615
+ const summary = [
616
+ `Scan A ${data.scan_a.target_url}`,
617
+ ` ${riskColor(data.scan_a.risk_level || "")(data.scan_a.risk_level || "\u2014")} ${scoreBold(data.scan_a.overall_score)}`,
618
+ `Scan B ${data.scan_b.target_url}`,
619
+ ` ${riskColor(data.scan_b.risk_level || "")(data.scan_b.risk_level || "\u2014")} ${scoreBold(data.scan_b.overall_score)}`,
620
+ `Delta ${data.score_delta > 0 ? "+" : ""}${data.score_delta}`,
621
+ data.summary
622
+ ].join("\n");
623
+ let body = panel(summary, { title: "Compare" });
624
+ if (data.factor_changes?.length) {
625
+ const rows = data.factor_changes.map((f) => [f.name.replace(/_/g, " "), String(f.delta)]);
626
+ body += "\n\n" + createTable(["Factor", "Delta"], rows);
627
+ }
628
+ return body;
629
+ }
630
+ var init_compare = __esm({
631
+ "src/commands/compare.ts"() {
632
+ "use strict";
633
+ init_table();
634
+ init_theme();
635
+ init_layout();
636
+ init_api_client();
637
+ }
638
+ });
639
+
640
+ // src/cli/program.ts
641
+ init_brand();
642
+ init_api_client();
643
+ import { Command } from "commander";
644
+ import { cwd as cwd2 } from "process";
645
+
646
+ // src/commands/auth.ts
647
+ init_credentials();
648
+ init_api_client();
649
+ init_credentials();
650
+ init_config();
651
+ init_theme2();
652
+ import { password, input } from "@inquirer/prompts";
653
+ async function runAuthLogin(opts = {}) {
654
+ const config = await loadConfig();
655
+ const client = new FuzziApiClient(config.api_url);
656
+ let apiKey = opts.apiKey?.trim();
657
+ if (!apiKey) {
658
+ if (opts.interactive === false) {
659
+ throw new ApiError(
660
+ "No API key provided. Generate one at https://app.fuzzi.dev/settings/api-keys",
661
+ 401,
662
+ "missing_key",
663
+ void 0,
664
+ 2
665
+ );
666
+ }
667
+ apiKey = await password({
668
+ message: "Paste your API key (fz_live_...):",
669
+ mask: "\u2022",
670
+ validate: (v) => {
671
+ if (!v.trim()) return "API key is required";
672
+ if (!isValidApiKeyFormat(v)) return "Key must start with fz_live_";
673
+ return true;
674
+ }
675
+ });
676
+ }
677
+ apiKey = apiKey.trim();
678
+ if (!isValidApiKeyFormat(apiKey)) {
679
+ throw new ApiError(
680
+ "Invalid API key format. Generate a new one at https://app.fuzzi.dev/settings/api-keys",
681
+ 401,
682
+ "invalid_key_format",
683
+ void 0,
684
+ 2
685
+ );
686
+ }
687
+ client.setToken(apiKey);
688
+ const valid = await client.validateToken();
689
+ if (!valid) {
690
+ throw new ApiError(
691
+ "Invalid API key. Generate a new one at https://app.fuzzi.dev/settings/api-keys",
692
+ 401,
693
+ "invalid_token",
694
+ void 0,
695
+ 2
696
+ );
697
+ }
698
+ const profile = await client.get("/me");
699
+ await saveCredentials({
700
+ api_key: apiKey,
701
+ auth_method: "api_key",
702
+ key_prefix: profile.key_prefix || maskApiKey(apiKey),
703
+ key_expires_at: profile.key_expires_at || void 0,
704
+ email: profile.email,
705
+ full_name: profile.full_name || void 0,
706
+ saved_at: (/* @__PURE__ */ new Date()).toISOString()
707
+ });
708
+ const name = profile.full_name || profile.email;
709
+ return success(`Authenticated as ${name}`);
710
+ }
711
+ async function runAuthLogout() {
712
+ await clearCredentials();
713
+ return muted("You are now logged out.");
714
+ }
715
+ async function promptNewKeyName() {
716
+ return input({
717
+ message: "Key name:",
718
+ validate: (v) => v.trim().length > 0 ? true : "Name is required"
719
+ });
720
+ }
721
+
722
+ // src/lib/output.ts
723
+ init_theme();
724
+ init_table();
725
+ init_strings();
726
+ init_layout();
727
+ function formatScanDate(scan) {
728
+ return formatTimestamp("created_at" in scan ? scan.created_at : void 0);
729
+ }
730
+ function renderScanResult(scan, format) {
731
+ if (format === "json") return JSON.stringify(buildScanJson(scan), null, 2);
732
+ if (format === "markdown") return renderScanMarkdown(scan);
733
+ return renderScanTable(scan);
734
+ }
735
+ function buildScanJson(scan) {
736
+ const fr = scan.fuzzy_result;
737
+ return {
738
+ scan: {
739
+ id: scan.id,
740
+ target_url: scan.target_url,
741
+ status: scan.status,
742
+ risk_level: fr?.risk_level ?? null,
743
+ risk_score: fr?.risk_score ?? null,
744
+ overall_score: fr?.overall_score ?? null
745
+ },
746
+ factors: scan.factors ?? [],
747
+ recommendations: scan.recommendations ?? []
748
+ };
749
+ }
750
+ function levelIndicator(level) {
751
+ if (level === "LOW") return muted("ok");
752
+ return warn("!");
753
+ }
754
+ function renderScanTable(scan) {
755
+ const fr = scan.fuzzy_result;
756
+ const rc = riskColor(fr?.risk_level);
757
+ const lines = [];
758
+ lines.push(panel([
759
+ bold(scan.target_url),
760
+ fr ? `${rc("Risk")} ${rc(fr.risk_level)} ${muted("Score")} ${scoreBold(fr.overall_score)}/100` : "",
761
+ `${muted("Date")} ${formatScanDate(scan)}`,
762
+ scan.status === "completed_inconclusive" ? warn("Inconclusive \u2014 indicative only") : ""
763
+ ].filter(Boolean).join("\n"), { title: "Scan result" }));
764
+ if (scan.factors?.length) {
765
+ const rows = [...scan.factors].sort((a, b) => a.score_100 - b.score_100).map((f) => {
766
+ const inc = f.details?.inconclusive;
767
+ return [
768
+ f.name.replace(/_/g, " "),
769
+ inc ? muted("\u2014") : `${f.score_100}/100`,
770
+ inc ? muted("n/a") : `${riskColor(f.linguistic_value)(f.linguistic_value)} ${levelIndicator(f.linguistic_value)}`
771
+ ];
772
+ });
773
+ lines.push("", createTable(["Dimension", "Score", "Level"], rows));
774
+ }
775
+ if (scan.recommendations?.length) {
776
+ lines.push("", accent("Findings"));
777
+ scan.recommendations.forEach((r, i) => {
778
+ lines.push(` ${muted(String(i + 1).padStart(2))} ${riskColor(r.severity.toUpperCase())(`[${r.severity.toUpperCase()}]`)} ${r.title}`);
779
+ });
780
+ }
781
+ lines.push("");
782
+ return lines.join("\n");
783
+ }
784
+ function renderScanMarkdown(scan) {
785
+ const fr = scan.fuzzy_result;
786
+ const host = hostnameFromUrl(scan.target_url);
787
+ const lines = [`## Fuzzi Security Scan: ${host}`, ""];
788
+ if (fr) lines.push(`**Risk Level:** ${fr.risk_level} (${fr.overall_score}/100)`, "");
789
+ if (scan.factors?.length) {
790
+ lines.push("| Dimension | Score | Level |", "|-----------|-------|-------|");
791
+ for (const f of scan.factors) {
792
+ lines.push(`| ${f.name.replace(/_/g, " ")} | ${f.score_100}/100 | ${f.linguistic_value} |`);
793
+ }
794
+ lines.push("");
795
+ }
796
+ if (scan.recommendations?.length) {
797
+ lines.push("### Findings");
798
+ scan.recommendations.forEach((r, i) => {
799
+ lines.push(`${i + 1}. [${r.severity.toUpperCase()}] ${r.title}`);
800
+ });
801
+ }
802
+ return lines.join("\n");
803
+ }
804
+ function renderScansList(scans, format) {
805
+ if (format === "json") return JSON.stringify(scans, null, 2);
806
+ if (format === "markdown") {
807
+ if (!scans.length) return "No scans found.";
808
+ const lines = ["| URL | Status | Risk | Score | Date |", "|-----|--------|------|-------|------|"];
809
+ for (const s of scans) {
810
+ lines.push(`| ${s.target_url} | ${s.status} | ${s.risk_level || "\u2014"} | ${s.overall_score ?? "\u2014"} | ${formatScanDate(s)} |`);
811
+ }
812
+ return lines.join("\n");
813
+ }
814
+ if (!scans.length) return muted("No scans found.");
815
+ const rows = scans.map((s) => [
816
+ truncate(s.target_url, 40),
817
+ s.status === "completed_inconclusive" ? warn("inconcl.") : s.status,
818
+ s.risk_level ? riskColor(s.risk_level)(s.risk_level) : muted("\u2014"),
819
+ s.overall_score != null ? scoreBold(s.overall_score) : muted("\u2014"),
820
+ muted(formatScanDate(s))
821
+ ]);
822
+ return createTable(["URL", "Status", "Risk", "Score", "Date"], rows);
823
+ }
824
+
825
+ // src/commands/scan.ts
826
+ init_api_client();
827
+ init_config();
828
+ init_theme();
829
+
830
+ // src/lib/errors.ts
831
+ init_api_client();
832
+ function formatApiError(err) {
833
+ if (err instanceof ApiError) {
834
+ return err.message;
835
+ }
836
+ if (err instanceof Error) {
837
+ if (err.message.includes("fetch failed") || err.message.includes("ECONNREFUSED")) {
838
+ return "Could not connect to app.fuzzi.dev. Check your internet connection or try again later.";
839
+ }
840
+ return `An error occurred: ${err.message}. Please report this at https://github.com/fuzzi-cli/fuzzi-cli/issues`;
841
+ }
842
+ return `An error occurred: ${String(err)}. Please report this at https://github.com/fuzzi-cli/fuzzi-cli/issues`;
843
+ }
844
+ function getExitCode(err) {
845
+ if (err instanceof ApiError && err.exitCode !== void 0) {
846
+ return err.exitCode;
847
+ }
848
+ return 2;
849
+ }
850
+ function validateUrl(url) {
851
+ try {
852
+ const parsed = new URL(url);
853
+ if (!["http:", "https:"].includes(parsed.protocol)) {
854
+ return "Invalid URL. Please provide a valid URL starting with http:// or https://";
855
+ }
856
+ return null;
857
+ } catch {
858
+ return "Invalid URL. Please provide a valid URL starting with http:// or https://";
859
+ }
860
+ }
861
+
862
+ // src/lib/risk.ts
863
+ var RISK_ORDER = {
864
+ LOW: 0,
865
+ MEDIUM: 1,
866
+ HIGH: 2,
867
+ CRITICAL: 3
868
+ };
869
+ function normalizeRiskLevel(level) {
870
+ if (!level) return null;
871
+ const upper = level.toUpperCase();
872
+ return upper in RISK_ORDER ? upper : null;
873
+ }
874
+ function riskMeetsThreshold(actual, threshold) {
875
+ const a = normalizeRiskLevel(actual);
876
+ const t = normalizeRiskLevel(threshold);
877
+ if (!a || !t) return false;
878
+ return RISK_ORDER[a] >= RISK_ORDER[t];
879
+ }
880
+
881
+ // src/lib/project-config.ts
882
+ import { readFile as readFile3 } from "fs/promises";
883
+ import { join as join3 } from "path";
884
+ import { existsSync } from "fs";
885
+ function parseTomlSimple(raw) {
886
+ const config = { scan: {}, output: {} };
887
+ let section = "";
888
+ for (const line of raw.split("\n")) {
889
+ const trimmed = line.trim();
890
+ if (!trimmed || trimmed.startsWith("#")) continue;
891
+ const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/);
892
+ if (sectionMatch) {
893
+ section = sectionMatch[1];
894
+ continue;
895
+ }
896
+ const kv = trimmed.match(/^(\w+)\s*=\s*"?([^"]+)"?$/);
897
+ if (!kv) continue;
898
+ const [, key, value] = kv;
899
+ if (section === "scan" || section === "scan.scan") {
900
+ config.scan ??= {};
901
+ if (key === "url") config.scan.url = value;
902
+ else if (key === "environment") config.scan.environment = value;
903
+ else if (key === "fail_on") config.scan.fail_on = value;
904
+ else if (key === "title") config.scan.title = value;
905
+ } else if (section === "output") {
906
+ config.output ??= {};
907
+ if (key === "format") config.output.format = value;
908
+ }
909
+ }
910
+ return config;
911
+ }
912
+ async function loadProjectConfig(cwd5) {
913
+ const fuzzirc = join3(cwd5, ".fuzzirc");
914
+ const fuzzitoml = join3(cwd5, "fuzzi.toml");
915
+ if (existsSync(fuzzirc)) {
916
+ try {
917
+ const raw = await readFile3(fuzzirc, "utf8");
918
+ return JSON.parse(raw);
919
+ } catch {
920
+ return null;
921
+ }
922
+ }
923
+ if (existsSync(fuzzitoml)) {
924
+ try {
925
+ const raw = await readFile3(fuzzitoml, "utf8");
926
+ return parseTomlSimple(raw);
927
+ } catch {
928
+ return null;
929
+ }
930
+ }
931
+ return null;
932
+ }
933
+
934
+ // src/terminal/progress.ts
935
+ init_theme();
936
+ init_capabilities();
937
+ import ora from "ora";
938
+ function createProgress(label, stream = false) {
939
+ if (!getCapabilities().interactive) {
940
+ return {
941
+ update: () => {
942
+ },
943
+ succeed: () => {
944
+ },
945
+ fail: () => {
946
+ },
947
+ stop: () => {
948
+ }
949
+ };
950
+ }
951
+ if (stream) {
952
+ let last = "";
953
+ return {
954
+ update(message) {
955
+ if (last) process.stdout.write("\r\x1B[K");
956
+ last = message;
957
+ process.stdout.write(muted(message));
958
+ },
959
+ succeed(message) {
960
+ if (last) process.stdout.write("\r\x1B[K");
961
+ if (message) process.stdout.write(muted(message) + "\n");
962
+ last = "";
963
+ },
964
+ fail(message) {
965
+ if (last) process.stdout.write("\r\x1B[K");
966
+ if (message) process.stdout.write(muted(message) + "\n");
967
+ last = "";
968
+ },
969
+ stop() {
970
+ if (last) process.stdout.write("\r\x1B[K");
971
+ last = "";
972
+ }
973
+ };
974
+ }
975
+ let spinner = ora({ text: label, color: "cyan", discardStdin: false }).start();
976
+ return {
977
+ update(message) {
978
+ if (spinner) spinner.text = message;
979
+ },
980
+ succeed(message) {
981
+ if (spinner) {
982
+ spinner.succeed(message);
983
+ spinner = null;
984
+ }
985
+ },
986
+ fail(message) {
987
+ if (spinner) {
988
+ spinner.fail(message);
989
+ spinner = null;
990
+ }
991
+ },
992
+ stop() {
993
+ if (spinner) {
994
+ spinner.stop();
995
+ spinner = null;
996
+ }
997
+ }
998
+ };
999
+ }
1000
+
1001
+ // src/commands/scan.ts
1002
+ init_strings();
1003
+
1004
+ // src/lib/retry.ts
1005
+ init_logger();
1006
+ async function withRetry(fn, opts = {}) {
1007
+ const max = opts.maxAttempts ?? 3;
1008
+ const base = opts.baseDelayMs ?? 500;
1009
+ const retryOn = opts.retryOn ?? ((e) => {
1010
+ const err = e;
1011
+ return err.status === 429 || err.status === 502 || err.status === 503;
1012
+ });
1013
+ let last;
1014
+ for (let attempt = 1; attempt <= max; attempt++) {
1015
+ try {
1016
+ return await fn();
1017
+ } catch (e) {
1018
+ last = e;
1019
+ if (attempt >= max || !retryOn(e)) throw e;
1020
+ const delay = base * Math.pow(2, attempt - 1);
1021
+ log.debug(`retry ${attempt}/${max} in ${delay}ms`);
1022
+ await new Promise((r) => setTimeout(r, delay));
1023
+ }
1024
+ }
1025
+ throw last;
1026
+ }
1027
+
1028
+ // src/commands/scan.ts
1029
+ init_logger();
1030
+ import { cwd } from "process";
1031
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["completed", "completed_inconclusive", "failed"]);
1032
+ var POLL_INTERVAL_MS = 3e3;
1033
+ async function pollScan(client, scanId, onTick, maxWaitMs = 3e5) {
1034
+ const start = Date.now();
1035
+ let last = "pending";
1036
+ while (Date.now() - start < maxWaitMs) {
1037
+ const detail = await withRetry(() => client.get(`/scan/${scanId}`));
1038
+ last = detail.status;
1039
+ onTick?.(Math.floor((Date.now() - start) / 1e3), last);
1040
+ if (TERMINAL_STATUSES.has(detail.status)) {
1041
+ if (detail.status === "failed") {
1042
+ throw new ApiError(
1043
+ `Scan failed: ${detail.inconclusive_message || detail.inconclusive_reason || "unknown error"}`,
1044
+ 500,
1045
+ "scan_failed",
1046
+ detail,
1047
+ 2
1048
+ );
1049
+ }
1050
+ return detail;
1051
+ }
1052
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
1053
+ }
1054
+ throw new ApiError(`Scan timed out after ${maxWaitMs / 1e3}s (last status: ${last})`, 408, "scan_timeout", void 0, 2);
1055
+ }
1056
+ function resolveFormat(format, projectFormat) {
1057
+ return format || projectFormat || "table";
1058
+ }
1059
+ function computeExitCode(scan, opts) {
1060
+ const fr = scan.fuzzy_result;
1061
+ if (!fr) return 0;
1062
+ if (opts.failThreshold != null && fr.risk_score >= opts.failThreshold) return 1;
1063
+ if (opts.failOn && riskMeetsThreshold(fr.risk_level, opts.failOn)) return 1;
1064
+ return 0;
1065
+ }
1066
+ async function runScanCommand(client, opts) {
1067
+ const urlError = validateUrl(opts.url);
1068
+ if (urlError) throw new ApiError(urlError, 400, "invalid_url", void 0, 2);
1069
+ const projectConfig = await loadProjectConfig(cwd());
1070
+ const config = await loadConfig();
1071
+ const format = resolveFormat(opts.format, projectConfig?.output?.format || config.default_format);
1072
+ const env = opts.environment || projectConfig?.scan?.environment || config.default_env || "production";
1073
+ const failOn = opts.failOn || projectConfig?.scan?.fail_on;
1074
+ const shouldWait = opts.noWait ? false : opts.wait !== false;
1075
+ const body = { url: opts.url, environment: env };
1076
+ if (opts.title || projectConfig?.scan?.title) {
1077
+ body.title = opts.title || projectConfig?.scan?.title || "";
1078
+ }
1079
+ log.debug("creating scan", { url: opts.url, env });
1080
+ const created = await withRetry(() => client.post("/scan", body));
1081
+ if (!shouldWait) {
1082
+ const output2 = format === "json" ? JSON.stringify(created, null, 2) : [success("Scan started"), muted(`ID: ${created.scan_id}`), muted(created.message)].join("\n");
1083
+ return { output: output2, exitCode: 0 };
1084
+ }
1085
+ const host = hostnameFromUrl(opts.url);
1086
+ const progress = opts.onProgress ? { update: opts.onProgress, stop: () => {
1087
+ }, succeed: () => {
1088
+ }, fail: () => {
1089
+ } } : createProgress(`Scanning ${host}...`, opts.streamProgress);
1090
+ const detail = await pollScan(client, created.scan_id, (sec, status) => {
1091
+ progress.update(`Scanning ${host}... (${sec}s) ${muted(status)}`);
1092
+ });
1093
+ progress.succeed(`Scan complete \u2014 ${host}`);
1094
+ const exitCode = computeExitCode(detail, { ...opts, failOn });
1095
+ return { output: renderScanResult(detail, format), exitCode, scan: detail };
1096
+ }
1097
+
1098
+ // src/commands/scans.ts
1099
+ async function runScansListCommand(client, format = "table", filters) {
1100
+ const params = new URLSearchParams({ page: "1", page_size: String(filters?.limit || 20) });
1101
+ if (filters?.status) params.set("status", filters.status);
1102
+ if (filters?.riskLevel) params.set("risk_level", filters.riskLevel);
1103
+ const data = await client.get(
1104
+ `/scans?${params.toString()}`
1105
+ );
1106
+ return renderScansList(data.results || [], format);
1107
+ }
1108
+ async function runScanGetCommand(client, scanId, format = "table") {
1109
+ const detail = await client.get(`/scan/${scanId}`);
1110
+ return renderScanResult(detail, format);
1111
+ }
1112
+
1113
+ // src/commands/status.ts
1114
+ init_config();
1115
+ init_credentials();
1116
+ init_layout();
1117
+ init_theme();
1118
+ function daysUntil(dateStr) {
1119
+ const diff = new Date(dateStr).getTime() - Date.now();
1120
+ return Math.max(0, Math.ceil(diff / (1e3 * 60 * 60 * 24)));
1121
+ }
1122
+ async function runRateLimitStatus(client) {
1123
+ try {
1124
+ const data = await client.get("/rate-limit");
1125
+ if (data.limit != null) {
1126
+ return `${data.remaining ?? "?"}/${data.limit} remaining`;
1127
+ }
1128
+ } catch {
1129
+ }
1130
+ return null;
1131
+ }
1132
+ async function runStatusCommand(client) {
1133
+ const creds = await loadCredentials();
1134
+ const profile = await client.get("/me");
1135
+ const keyExpiry = creds?.key_expires_at || profile.key_expires_at;
1136
+ const config = await loadConfig();
1137
+ const rate = await runRateLimitStatus(client);
1138
+ const rows = [
1139
+ ["Name", profile.full_name || profile.email.split("@")[0]],
1140
+ ["Email", profile.email],
1141
+ ["Organization", profile.organization || muted("\u2014")],
1142
+ ["Role", profile.role],
1143
+ ["Auth", `API Key (${creds ? maskApiKey(creds.api_key) : "\u2014"})`]
1144
+ ];
1145
+ if (keyExpiry) {
1146
+ const days = daysUntil(keyExpiry);
1147
+ rows.push(["Key expires", `${keyExpiry.slice(0, 10)} (${days}d remaining)`]);
1148
+ } else {
1149
+ rows.push(["Key expires", muted("No expiry")]);
1150
+ }
1151
+ rows.push(["API", config.api_url]);
1152
+ if (rate) rows.push(["Rate limit", rate]);
1153
+ return [panel(keyValue(rows), { title: "Account" }), "", divider(), muted("Use /keys to manage API keys")].join("\n");
1154
+ }
1155
+
1156
+ // src/commands/config.ts
1157
+ init_config();
1158
+ init_theme2();
1159
+ async function runConfigList() {
1160
+ const entries = await listConfigEntries();
1161
+ const lines = Object.entries(entries).map(([k, v]) => ` ${k} = ${v}`);
1162
+ return lines.length ? [accent("Config (~/.fuzzi/config)"), ...lines].join("\n") : muted("No config set.");
1163
+ }
1164
+ async function runConfigGet(key) {
1165
+ if (!key) return runConfigList();
1166
+ const value = await getConfigValue(key);
1167
+ if (value === void 0) return muted(`Config key not set: ${key}`);
1168
+ return value;
1169
+ }
1170
+ async function runConfigSet(key, value) {
1171
+ const allowed = ["api_url", "default_env", "default_format"];
1172
+ if (!allowed.includes(key)) {
1173
+ return muted(`Unknown config key: ${key}. Supported: ${allowed.join(", ")}`);
1174
+ }
1175
+ await setConfigValue(key, value);
1176
+ return accent("Config updated.");
1177
+ }
1178
+
1179
+ // src/cli/exit.ts
1180
+ function handleCommandError(e) {
1181
+ console.error(formatApiError(e));
1182
+ process.exit(getExitCode(e));
1183
+ }
1184
+ function exitWith(code) {
1185
+ process.exit(code);
1186
+ }
1187
+
1188
+ // src/cli/program.ts
1189
+ function buildProgram() {
1190
+ const program = new Command("fuzzi").name("fuzzi").description("Fuzzi security scanner CLI \u2014 interactive shell and scriptable commands").version(VERSION);
1191
+ const auth = program.command("auth").description("Authentication");
1192
+ auth.command("login").description("Authenticate with an API key from https://app.fuzzi.dev/settings/api-keys").option("--api-key <key>", "API key (fz_live_...)").action(async (opts) => {
1193
+ try {
1194
+ console.log(await runAuthLogin({ apiKey: opts.apiKey, interactive: !opts.apiKey }));
1195
+ } catch (e) {
1196
+ handleCommandError(e);
1197
+ }
1198
+ });
1199
+ auth.command("logout").description("Clear stored credentials").action(async () => {
1200
+ console.log(await runAuthLogout());
1201
+ });
1202
+ auth.command("status").description("Show auth status").action(async () => {
1203
+ try {
1204
+ const client = await getAuthenticatedClient();
1205
+ console.log(await runStatusCommand(client));
1206
+ } catch (e) {
1207
+ handleCommandError(e);
1208
+ }
1209
+ });
1210
+ program.command("scan [url]").description("Scan a URL (waits for completion by default)").option("-t, --title <title>", "Scan title").option("-e, --env <env>", "production|staging|development").option("-w, --wait", "Poll until scan completes (default)").option("--no-wait", "Return immediately with scan ID").option("-f, --format <format>", "table|json|markdown", "table").option("--fail-on <level>", "Exit 1 if risk >= level (low|medium|high|critical)").option("--fail-threshold <n>", "Exit 1 if risk_score >= threshold (0.0-1.0)", parseFloat).action(async (urlArg, opts) => {
1211
+ try {
1212
+ const project = await loadProjectConfig(cwd2());
1213
+ const url = urlArg || project?.scan?.url;
1214
+ if (!url) {
1215
+ console.error("Usage: fuzzi scan <url>");
1216
+ exitWith(2);
1217
+ }
1218
+ const client = await getAuthenticatedClient();
1219
+ const result = await runScanCommand(client, {
1220
+ url,
1221
+ wait: opts.wait,
1222
+ noWait: opts.noWait,
1223
+ format: opts.format,
1224
+ environment: opts.env,
1225
+ title: opts.title,
1226
+ failOn: opts.failOn,
1227
+ failThreshold: opts.failThreshold
1228
+ });
1229
+ console.log(result.output);
1230
+ exitWith(result.exitCode);
1231
+ } catch (e) {
1232
+ handleCommandError(e);
1233
+ }
1234
+ });
1235
+ const scans = program.command("scans").description("Browse scans");
1236
+ scans.command("list").description("List recent scans").option("--status <status>", "pending|running|completed|failed").option("--risk-level <level>", "low|medium|high|critical").option("--limit <n>", "Max results", "20").option("-f, --format <format>", "table|json|markdown", "table").action(async (opts) => {
1237
+ try {
1238
+ const client = await getAuthenticatedClient();
1239
+ console.log(
1240
+ await runScansListCommand(client, opts.format, {
1241
+ status: opts.status,
1242
+ riskLevel: opts.riskLevel,
1243
+ limit: parseInt(opts.limit, 10)
1244
+ })
1245
+ );
1246
+ } catch (e) {
1247
+ handleCommandError(e);
1248
+ }
1249
+ });
1250
+ scans.command("get <scan-id>").description("Get scan details").option("-f, --format <format>", "table|json|markdown", "table").action(async (scanId, opts) => {
1251
+ try {
1252
+ const client = await getAuthenticatedClient();
1253
+ console.log(await runScanGetCommand(client, scanId, opts.format));
1254
+ } catch (e) {
1255
+ handleCommandError(e);
1256
+ }
1257
+ });
1258
+ program.command("report <scan-id>").description("Download a scan report").requiredOption("--format <format>", "pdf|csv|json").option("-o, --output <path>", "Output file path").action(async (scanId, opts) => {
1259
+ try {
1260
+ const { runReportCommand: runReportCommand2 } = await Promise.resolve().then(() => (init_report(), report_exports));
1261
+ const client = await getAuthenticatedClient();
1262
+ console.log(await runReportCommand2(client, scanId, opts.format, opts.output));
1263
+ } catch (e) {
1264
+ handleCommandError(e);
1265
+ }
1266
+ });
1267
+ program.command("whatif <scan-id>").description("Simulate factor overrides").option("--set <pair>", "dimension=value (repeatable)", (v, prev) => [...prev, v], []).option("-f, --format <format>", "table|json", "table").action(async (scanId, opts) => {
1268
+ try {
1269
+ const { runWhatIfCommand: runWhatIfCommand2 } = await Promise.resolve().then(() => (init_whatif(), whatif_exports));
1270
+ const overrides = {};
1271
+ for (const pair of opts.set) {
1272
+ const [k, val] = pair.split("=");
1273
+ if (k && val) overrides[k] = parseFloat(val);
1274
+ }
1275
+ console.log(await runWhatIfCommand2(scanId, overrides, opts.format));
1276
+ } catch (e) {
1277
+ handleCommandError(e);
1278
+ }
1279
+ });
1280
+ program.command("compare <scan-a> <scan-b>").description("Compare two scans").option("-f, --format <format>", "table|json", "table").action(async (scanA, scanB, opts) => {
1281
+ try {
1282
+ const { runCompareCommand: runCompareCommand2 } = await Promise.resolve().then(() => (init_compare(), compare_exports));
1283
+ console.log(await runCompareCommand2(scanA, scanB, opts.format));
1284
+ } catch (e) {
1285
+ handleCommandError(e);
1286
+ }
1287
+ });
1288
+ const config = program.command("config").description("CLI configuration (~/.fuzzi/config)");
1289
+ config.command("list").action(async () => console.log(await runConfigList()));
1290
+ config.command("get [key]").action(async (key) => console.log(await runConfigGet(key)));
1291
+ config.command("set <key> <value>").action(async (key, value) => console.log(await runConfigSet(key, value)));
1292
+ program.command("status").description("Show account status").action(async () => {
1293
+ try {
1294
+ const client = await getAuthenticatedClient();
1295
+ console.log(await runStatusCommand(client));
1296
+ } catch (e) {
1297
+ handleCommandError(e);
1298
+ }
1299
+ });
1300
+ return program;
1301
+ }
1302
+
1303
+ // src/cli/bootstrap.ts
1304
+ init_credentials();
1305
+ init_api_client();
1306
+ import { cwd as cwd4 } from "process";
1307
+
1308
+ // src/shell/prompt-loop.ts
1309
+ import * as readline from "readline/promises";
1310
+ import { stdin as input3, stdout as output } from "process";
1311
+
1312
+ // src/shell/ascii-mark.ts
1313
+ function renderFuzziMark() {
1314
+ return [
1315
+ " \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E",
1316
+ " \u2571 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2572",
1317
+ " \u2502 \u2502 FUZZI \u2502 \u2502",
1318
+ " \u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502",
1319
+ " \u2502 \u2593\u2593\u2593 \u2502",
1320
+ " \u2572 \u2571",
1321
+ " \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"
1322
+ ].join("\n");
1323
+ }
1324
+
1325
+ // src/shell/home-screen.ts
1326
+ init_brand();
1327
+ init_theme();
1328
+ init_layout();
1329
+
1330
+ // src/lib/assets.ts
1331
+ import { readFile as readFile4 } from "fs/promises";
1332
+ import { dirname, join as join4 } from "path";
1333
+ import { fileURLToPath } from "url";
1334
+ import { existsSync as existsSync2 } from "fs";
1335
+ function assetsDir() {
1336
+ const here = dirname(fileURLToPath(import.meta.url));
1337
+ const candidates = [
1338
+ join4(here, "..", "assets"),
1339
+ // dist/lib → dist/../assets = package/assets
1340
+ join4(here, "..", "..", "assets"),
1341
+ // src/lib → package/assets
1342
+ join4(here, "assets")
1343
+ // bundled flat fallback
1344
+ ];
1345
+ for (const p of candidates) {
1346
+ if (existsSync2(join4(p, "changelog.json"))) return p;
1347
+ }
1348
+ return candidates[0];
1349
+ }
1350
+ async function readAsset(name) {
1351
+ return readFile4(join4(assetsDir(), name), "utf8");
1352
+ }
1353
+
1354
+ // src/shell/home-screen.ts
1355
+ async function fetchHomeData(profile, cwd5) {
1356
+ let changelog = [];
1357
+ try {
1358
+ changelog = JSON.parse(await readAsset("changelog.json"));
1359
+ } catch {
1360
+ changelog = [];
1361
+ }
1362
+ return { profile, cwd: cwd5, changelog };
1363
+ }
1364
+ function renderHomeScreen(data) {
1365
+ const name = data.profile?.full_name || data.profile?.email?.split("@")[0] || "there";
1366
+ const org = data.profile?.organization?.trim();
1367
+ const welcome = data.profile ? accentBold(`Welcome back, ${name}!`) : muted("Welcome to Fuzzi");
1368
+ const mark = accent(renderFuzziMark());
1369
+ const connected = data.profile ? statusBar([accent("Connected"), org ?? null].filter(Boolean)) : muted("Not connected");
1370
+ const headerBody = ["", welcome, "", mark, "", connected, muted(data.cwd), ""].join("\n");
1371
+ const latest = data.changelog[0];
1372
+ const whatsNew = latest ? [...latest.highlights.slice(0, 2).map((h) => muted(`\xB7 ${h}`)), muted("/changelog for more")].join("\n") : muted("Stay tuned for updates");
1373
+ const quickActions = [
1374
+ accent("/scan") + muted(" <url>") + muted(" scan a target"),
1375
+ accent("/scans") + muted(" browse history"),
1376
+ accent("/status") + muted(" account info"),
1377
+ accent("/palette") + muted(" search commands"),
1378
+ accent("/help") + muted(" all commands")
1379
+ ].join("\n");
1380
+ const panels = panel(columns(quickActions, whatsNew, 38), { title: "Quick actions" });
1381
+ return panel(headerBody, { title: `Fuzzi CLI v${VERSION}`, marginBottom: 0 }) + "\n" + panels;
1382
+ }
1383
+ function renderChangelog(entries) {
1384
+ if (!entries.length) return muted("No changelog entries.");
1385
+ return entries.map((e) => {
1386
+ const lines = [
1387
+ accentBold(`v${e.version}`) + muted(` \u2014 ${e.date}`),
1388
+ ...e.highlights.map((h) => muted(` \xB7 ${h}`))
1389
+ ];
1390
+ return lines.join("\n");
1391
+ }).join("\n\n");
1392
+ }
1393
+
1394
+ // src/shell/slash-commands.ts
1395
+ init_api_client();
1396
+ import { confirm, input as input2 } from "@inquirer/prompts";
1397
+
1398
+ // src/commands/keys.ts
1399
+ init_table();
1400
+ init_theme();
1401
+
1402
+ // src/terminal/empty-state.ts
1403
+ init_theme();
1404
+ function emptyState(title, hint, action) {
1405
+ const lines = ["", muted(title), muted(hint)];
1406
+ if (action) lines.push("", accent(action));
1407
+ return lines.join("\n");
1408
+ }
1409
+
1410
+ // src/terminal/interactive.ts
1411
+ init_theme();
1412
+ import { select, search } from "@inquirer/prompts";
1413
+ async function pickFromList(message, items) {
1414
+ if (!items.length) return null;
1415
+ try {
1416
+ return await select({ message, choices: items });
1417
+ } catch {
1418
+ return null;
1419
+ }
1420
+ }
1421
+ async function searchPalette(message, choices) {
1422
+ try {
1423
+ return await search({
1424
+ message,
1425
+ source: async (input4) => {
1426
+ if (!input4) return choices;
1427
+ const q = input4.toLowerCase();
1428
+ return choices.filter((c) => c.value.includes(q) || c.description?.includes(q));
1429
+ }
1430
+ });
1431
+ } catch {
1432
+ return null;
1433
+ }
1434
+ }
1435
+ function formatChoice(name, description) {
1436
+ return `${accent(name)}${muted(" \u2014 " + description)}`;
1437
+ }
1438
+
1439
+ // src/commands/keys.ts
1440
+ async function runKeysListCommand(client) {
1441
+ const data = await client.get("/keys");
1442
+ const keys = data.results || [];
1443
+ if (!keys.length) {
1444
+ return emptyState("No API keys", "Create one at app.fuzzi.dev/settings/api-keys", "[n] new key in this view");
1445
+ }
1446
+ const rows = keys.map((k) => [
1447
+ k.name,
1448
+ k.prefix,
1449
+ k.scopes?.join(", ") || muted("\u2014"),
1450
+ k.revoked ? muted("Revoked") : k.active ? success("Active") : muted("Inactive"),
1451
+ k.last_used_at ? new Date(k.last_used_at).toLocaleDateString() : muted("Never")
1452
+ ]);
1453
+ return createTable(["Name", "Prefix", "Scopes", "Status", "Last Used"], rows);
1454
+ }
1455
+ async function runKeyRevoke(client, keyId) {
1456
+ await client.delete(`/keys/${keyId}`);
1457
+ return "API key revoked.";
1458
+ }
1459
+ async function pickKeyForRevoke(client) {
1460
+ const data = await client.get("/keys");
1461
+ const active = (data.results || []).filter((k) => !k.revoked && k.active);
1462
+ return pickFromList(
1463
+ "Select a key to revoke",
1464
+ active.map((k) => ({ name: `${k.name} (${k.prefix})`, value: k.id }))
1465
+ );
1466
+ }
1467
+
1468
+ // src/shell/help-screen.ts
1469
+ init_layout();
1470
+ init_theme();
1471
+
1472
+ // src/shell/registry.ts
1473
+ var SLASH_COMMANDS = [
1474
+ { name: "/scan", description: "Run a security scan on a URL", usage: "/scan <url>", requiresAuth: true },
1475
+ { name: "/scans", description: "Browse recent scans", requiresAuth: true },
1476
+ { name: "/status", description: "Show auth status and rate limits", requiresAuth: true },
1477
+ { name: "/keys", description: "Manage API keys", requiresAuth: true },
1478
+ { name: "/config", description: "Set CLI configuration", usage: "/config key=value" },
1479
+ { name: "/compare", description: "Compare two scans", usage: "/compare <id-a> <id-b>", requiresAuth: true },
1480
+ { name: "/whatif", description: "Simulate factor overrides", usage: "/whatif <id>", requiresAuth: true },
1481
+ { name: "/report", description: "Download scan report", usage: "/report <id>", requiresAuth: true },
1482
+ { name: "/palette", description: "Open command palette", aliases: ["/commands"] },
1483
+ { name: "/changelog", description: "View release notes" },
1484
+ { name: "/help", description: "Show all commands" },
1485
+ { name: "/auth", description: "Log in with API key", aliases: ["/login"] },
1486
+ { name: "/clear", description: "Clear screen and refresh home" },
1487
+ { name: "/history", description: "Show recent commands" },
1488
+ { name: "/exit", description: "Exit the shell", aliases: ["/quit"] }
1489
+ ];
1490
+ function findCommand(input4) {
1491
+ const cmd = input4.trim().split(/\s/)[0].toLowerCase();
1492
+ return SLASH_COMMANDS.find(
1493
+ (c) => c.name === cmd || c.aliases?.some((a) => a === cmd)
1494
+ );
1495
+ }
1496
+
1497
+ // src/shell/help-screen.ts
1498
+ function renderHelpScreen() {
1499
+ const visible = SLASH_COMMANDS.filter((c) => !c.hidden);
1500
+ const left = visible.slice(0, Math.ceil(visible.length / 2)).map((c) => `${accent(c.name.padEnd(12))} ${muted(c.description)}`).join("\n");
1501
+ const right = visible.slice(Math.ceil(visible.length / 2)).map((c) => `${accent(c.name.padEnd(12))} ${muted(c.description)}`).join("\n");
1502
+ const script = [
1503
+ muted("Scriptable commands"),
1504
+ ` fuzzi scan <url> [--format json|markdown] [--fail-on critical]`,
1505
+ ` fuzzi scans list | get <id> \xB7 fuzzi auth login | status`,
1506
+ ` fuzzi config set default_env staging`
1507
+ ].join("\n");
1508
+ return [
1509
+ panel(columns(left, right), { title: "Commands" }),
1510
+ "",
1511
+ script,
1512
+ "",
1513
+ divider(),
1514
+ dim("Tips: Tab to complete \xB7 /palette to search \xB7 Ctrl+C to exit")
1515
+ ].join("\n");
1516
+ }
1517
+ function renderHistoryScreen(entries) {
1518
+ if (!entries.length) return muted("No command history yet.");
1519
+ const body = entries.slice(-15).reverse().map((e, i) => `${dim(String(i + 1).padStart(2))} ${e}`).join("\n");
1520
+ return panel(body, { title: "Recent commands" });
1521
+ }
1522
+
1523
+ // src/shell/command-palette.ts
1524
+ async function openCommandPalette() {
1525
+ const choices = SLASH_COMMANDS.filter((c) => !c.hidden).map((c) => ({
1526
+ name: formatChoice(c.name, c.description),
1527
+ value: c.name,
1528
+ description: c.usage
1529
+ }));
1530
+ return searchPalette("Command palette", choices);
1531
+ }
1532
+
1533
+ // src/shell/session.ts
1534
+ init_credentials();
1535
+ import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile4, appendFile } from "fs/promises";
1536
+ import { join as join5 } from "path";
1537
+ var HISTORY_PATH = join5(fuzziDir(), "history");
1538
+ var MAX_ENTRIES = 200;
1539
+ async function loadHistory() {
1540
+ try {
1541
+ const raw = await readFile5(HISTORY_PATH, "utf8");
1542
+ return raw.split("\n").filter(Boolean).slice(-MAX_ENTRIES);
1543
+ } catch {
1544
+ return [];
1545
+ }
1546
+ }
1547
+ async function appendHistory(line) {
1548
+ const trimmed = line.trim();
1549
+ if (!trimmed || trimmed.startsWith("#")) return;
1550
+ await mkdir3(fuzziDir(), { recursive: true, mode: 448 });
1551
+ await appendFile(HISTORY_PATH, trimmed + "\n", "utf8");
1552
+ }
1553
+
1554
+ // src/shell/slash-commands.ts
1555
+ init_theme();
1556
+
1557
+ // src/terminal/banner.ts
1558
+ init_layout();
1559
+ init_theme();
1560
+ function errorBox(message, hint) {
1561
+ const body = hint ? `${message}
1562
+
1563
+ ${hint}` : message;
1564
+ return panel(body, { title: "Error" });
1565
+ }
1566
+ function successBox(message) {
1567
+ return panel(success(message), { title: "Done" });
1568
+ }
1569
+
1570
+ // src/shell/slash-commands.ts
1571
+ init_strings();
1572
+ async function dispatchSlashCommand(line, ctx) {
1573
+ const trimmed = line.trim();
1574
+ if (!trimmed) return {};
1575
+ if (trimmed === "/exit" || trimmed === "/quit") return { exit: true };
1576
+ const [cmd, ...rest] = trimmed.split(/\s+/);
1577
+ const arg = rest.join(" ").trim();
1578
+ const def = findCommand(cmd);
1579
+ if (!def && cmd.startsWith("/")) {
1580
+ ctx.sink.write(errorBox(`Unknown command: ${cmd}`, "Type /help or /palette"));
1581
+ return {};
1582
+ }
1583
+ try {
1584
+ switch (cmd.toLowerCase()) {
1585
+ case "/help":
1586
+ ctx.sink.write(renderHelpScreen());
1587
+ break;
1588
+ case "/palette":
1589
+ case "/commands": {
1590
+ const picked = await openCommandPalette();
1591
+ if (picked) return dispatchSlashCommand(picked, ctx);
1592
+ break;
1593
+ }
1594
+ case "/clear":
1595
+ return { redraw: true };
1596
+ case "/history": {
1597
+ const hist = await loadHistory();
1598
+ ctx.sink.write(renderHistoryScreen(hist));
1599
+ break;
1600
+ }
1601
+ case "/scan": {
1602
+ if (!arg) {
1603
+ ctx.sink.write(error("Usage: /scan <url>"));
1604
+ break;
1605
+ }
1606
+ const client = await getAuthenticatedClient();
1607
+ const progress = createStreamProgress(ctx.sink);
1608
+ const result = await runScanCommand(client, {
1609
+ url: arg,
1610
+ wait: true,
1611
+ onProgress: progress.update
1612
+ });
1613
+ progress.stop();
1614
+ ctx.sink.write(result.output);
1615
+ break;
1616
+ }
1617
+ case "/status": {
1618
+ const client = await getAuthenticatedClient();
1619
+ const status = await runStatusCommand(client);
1620
+ const rate = await runRateLimitStatus(client);
1621
+ ctx.sink.write(rate ? `${status}
1622
+
1623
+ ${muted("Rate limit")} ${rate}` : status);
1624
+ break;
1625
+ }
1626
+ case "/scans":
1627
+ await runScansInteractive(ctx);
1628
+ break;
1629
+ case "/keys":
1630
+ await runKeysInteractive(ctx);
1631
+ break;
1632
+ case "/config": {
1633
+ if (!arg.includes("=")) {
1634
+ ctx.sink.write(error("Usage: /config key=value"));
1635
+ break;
1636
+ }
1637
+ const [k, ...vParts] = arg.split("=");
1638
+ ctx.sink.write(await runConfigSet(k.trim(), vParts.join("=").trim()));
1639
+ break;
1640
+ }
1641
+ case "/changelog": {
1642
+ const raw = await readAsset("changelog.json");
1643
+ ctx.sink.write(renderChangelog(JSON.parse(raw)));
1644
+ break;
1645
+ }
1646
+ case "/compare": {
1647
+ const [a, b] = arg.split(/\s+/);
1648
+ if (!a || !b) {
1649
+ ctx.sink.write(error("Usage: /compare <scan-a> <scan-b>"));
1650
+ break;
1651
+ }
1652
+ const { runCompareCommand: runCompareCommand2 } = await Promise.resolve().then(() => (init_compare(), compare_exports));
1653
+ ctx.sink.write(await runCompareCommand2(a, b, "table"));
1654
+ break;
1655
+ }
1656
+ case "/whatif": {
1657
+ if (!arg) {
1658
+ ctx.sink.write(error("Usage: /whatif <scan-id>"));
1659
+ break;
1660
+ }
1661
+ const { runWhatIfCommand: runWhatIfCommand2 } = await Promise.resolve().then(() => (init_whatif(), whatif_exports));
1662
+ ctx.sink.write(await runWhatIfCommand2(arg, {}, "table"));
1663
+ break;
1664
+ }
1665
+ case "/report": {
1666
+ if (!arg) {
1667
+ ctx.sink.write(error("Usage: /report <scan-id>"));
1668
+ break;
1669
+ }
1670
+ const { runReportCommand: runReportCommand2 } = await Promise.resolve().then(() => (init_report(), report_exports));
1671
+ const client = await getAuthenticatedClient();
1672
+ ctx.sink.write(await runReportCommand2(client, arg, "json"));
1673
+ break;
1674
+ }
1675
+ case "/login":
1676
+ case "/auth": {
1677
+ ctx.sink.write(await runAuthLogin({ interactive: true }));
1678
+ const client = await getAuthenticatedClient();
1679
+ return { profile: await client.get("/me"), redraw: true };
1680
+ }
1681
+ default:
1682
+ ctx.sink.write(errorBox(`Unknown command: ${cmd}`, "Type /help"));
1683
+ }
1684
+ } catch (e) {
1685
+ ctx.sink.error(formatApiError(e));
1686
+ }
1687
+ return {};
1688
+ }
1689
+ function createStreamProgress(sink) {
1690
+ let last = "";
1691
+ return {
1692
+ update(msg) {
1693
+ if (last) sink.clearLine?.();
1694
+ last = msg;
1695
+ process.stdout.write(muted(msg));
1696
+ },
1697
+ stop() {
1698
+ if (last) sink.clearLine?.();
1699
+ last = "";
1700
+ }
1701
+ };
1702
+ }
1703
+ async function runScansInteractive(ctx) {
1704
+ const client = await getAuthenticatedClient();
1705
+ while (true) {
1706
+ const list = await runScansListCommand(client, "table", { limit: 20 });
1707
+ const data = await client.get("/scans?page=1&page_size=20");
1708
+ const scans = data.results || [];
1709
+ if (!scans.length) {
1710
+ ctx.sink.write(emptyState("No scans yet", "Run /scan <url> to create your first scan", "/scan https://example.com"));
1711
+ return;
1712
+ }
1713
+ ctx.sink.write(list);
1714
+ ctx.sink.write("");
1715
+ const scanId = await pickFromList(
1716
+ "Select a scan (Esc to go back)",
1717
+ scans.map((s) => ({
1718
+ name: `${truncate(s.target_url, 32)} ${s.risk_level ?? s.status} ${s.overall_score ?? "\u2014"} ${formatTimestamp(s.created_at)}`,
1719
+ value: s.id
1720
+ }))
1721
+ );
1722
+ if (!scanId) return;
1723
+ const detail = await client.get(`/scan/${scanId}`);
1724
+ ctx.sink.write(renderScanResult(detail, "table"));
1725
+ const cont = await input2({
1726
+ message: muted("Enter = back to list \xB7 q = exit"),
1727
+ default: ""
1728
+ }).catch(() => "q");
1729
+ if (cont.toLowerCase() === "q") return;
1730
+ }
1731
+ }
1732
+ async function runKeysInteractive(ctx) {
1733
+ const client = await getAuthenticatedClient();
1734
+ ctx.sink.write(await runKeysListCommand(client));
1735
+ ctx.sink.write(muted("\nActions: [r] revoke [n] new key [Enter] back"));
1736
+ const action = await input2({ message: "Action", default: "" }).catch(() => "");
1737
+ if (action.toLowerCase() === "r") {
1738
+ const keyId = await pickKeyForRevoke(client);
1739
+ if (!keyId) return;
1740
+ const ok = await confirm({ message: "Revoke this API key?", default: false }).catch(() => false);
1741
+ if (ok) ctx.sink.write(successBox(await runKeyRevoke(client, keyId)));
1742
+ } else if (action.toLowerCase() === "n") {
1743
+ const name = await promptNewKeyName();
1744
+ const created = await client.post("/keys", { name });
1745
+ ctx.sink.write(success(`Created: ${created.name} (${created.prefix})`));
1746
+ ctx.sink.write(accent("Save this key \u2014 it won't be shown again:"));
1747
+ ctx.sink.write(created.key);
1748
+ }
1749
+ }
1750
+
1751
+ // src/shell/prompt-loop.ts
1752
+ init_theme();
1753
+ import { cwd as cwd3 } from "process";
1754
+
1755
+ // src/shell/completer.ts
1756
+ function buildCompleter(commands, history) {
1757
+ const names = commands.map((c) => c.name);
1758
+ return (line) => {
1759
+ const trimmed = line.trimStart();
1760
+ if (!trimmed.startsWith("/")) {
1761
+ if (trimmed === "") return [[...names, ...history.slice(-20).reverse()], line];
1762
+ return [[], line];
1763
+ }
1764
+ const hits = names.filter((n) => n.startsWith(trimmed.split(/\s/)[0]));
1765
+ return [hits.length ? hits : names, line];
1766
+ };
1767
+ }
1768
+
1769
+ // src/shell/prompt-loop.ts
1770
+ init_capabilities();
1771
+ init_layout();
1772
+ init_logger();
1773
+ async function runPromptLoop(initialProfile) {
1774
+ let profile = initialProfile;
1775
+ const workDir = cwd3();
1776
+ const history = await loadHistory();
1777
+ const refresh = async () => {
1778
+ if (getCapabilities().interactive) console.clear();
1779
+ const data = await fetchHomeData(profile, workDir);
1780
+ console.log(renderHomeScreen(data));
1781
+ const bar = statusBar([
1782
+ profile ? muted(profile.email) : muted("guest"),
1783
+ dim(workDir),
1784
+ isDebugMode() ? muted("debug") : null
1785
+ ].filter(Boolean));
1786
+ console.log(bar);
1787
+ console.log("");
1788
+ };
1789
+ await refresh();
1790
+ const rl = readline.createInterface({
1791
+ input: input3,
1792
+ output,
1793
+ terminal: true,
1794
+ completer: buildCompleter(SLASH_COMMANDS, history),
1795
+ historySize: 300
1796
+ });
1797
+ rl.on("SIGINT", () => {
1798
+ console.log(muted("\nGoodbye!"));
1799
+ rl.close();
1800
+ process.exit(0);
1801
+ });
1802
+ const prompt = () => process.stdout.write(accent("\u203A "));
1803
+ prompt();
1804
+ for await (const line of rl) {
1805
+ await appendHistory(line);
1806
+ history.push(line.trim());
1807
+ const sink = {
1808
+ write: (text) => console.log(text),
1809
+ error: (text) => console.error(text),
1810
+ clearLine: () => process.stdout.write("\r\x1B[K")
1811
+ };
1812
+ const result = await dispatchSlashCommand(line, {
1813
+ cwd: workDir,
1814
+ profile,
1815
+ sink,
1816
+ refresh
1817
+ });
1818
+ if (result.profile) profile = result.profile;
1819
+ if (result.exit) {
1820
+ console.log(muted("Goodbye!"));
1821
+ rl.close();
1822
+ break;
1823
+ }
1824
+ if (result.redraw) await refresh();
1825
+ prompt();
1826
+ }
1827
+ }
1828
+
1829
+ // src/shell/onboarding.ts
1830
+ init_theme();
1831
+ function renderOnboarding() {
1832
+ return [
1833
+ muted("Authenticate to run scans and browse results."),
1834
+ "",
1835
+ ` 1. Generate an API key at ${accent("app.fuzzi.dev/settings/api-keys")}`,
1836
+ ` 2. Run ${accent("/auth")} or ${accent("fuzzi auth login")}`,
1837
+ ` 3. Scan a URL with ${accent("/scan https://example.com")}`,
1838
+ "",
1839
+ muted("Type /help for all commands \xB7 /palette to search")
1840
+ ].join("\n");
1841
+ }
1842
+
1843
+ // src/cli/bootstrap.ts
1844
+ init_layout();
1845
+ init_logger();
1846
+ async function tryGetProfile() {
1847
+ try {
1848
+ const creds = await loadCredentials();
1849
+ if (!creds?.api_key) return null;
1850
+ const client = await getAuthenticatedClient();
1851
+ return await client.get("/me");
1852
+ } catch (e) {
1853
+ log.debug("profile bootstrap failed", e);
1854
+ return null;
1855
+ }
1856
+ }
1857
+ async function runInteractiveMode() {
1858
+ const profile = await tryGetProfile();
1859
+ const workDir = cwd4();
1860
+ const data = await fetchHomeData(profile, workDir);
1861
+ console.log(renderHomeScreen(data));
1862
+ if (!profile) {
1863
+ console.log(panel(renderOnboarding(), { title: "Get started" }));
1864
+ }
1865
+ await runPromptLoop(profile);
1866
+ }
1867
+
1868
+ // src/index.ts
1869
+ async function main(argv) {
1870
+ if (argv.length <= 2) {
1871
+ await runInteractiveMode();
1872
+ return;
1873
+ }
1874
+ await buildProgram().parseAsync(argv);
1875
+ }
1876
+ main(process.argv).catch((e) => {
1877
+ console.error(formatApiError(e));
1878
+ process.exit(getExitCode(e));
1879
+ });
1880
+ //# sourceMappingURL=index.js.map