fuzzi-cli 0.1.3 → 0.1.4

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 CHANGED
@@ -16,18 +16,26 @@ var init_brand = __esm({
16
16
  BRAND = {
17
17
  accent: "#4FC3A1",
18
18
  accentDim: "#3A9A7E",
19
- text: "#FAFAFA",
20
- textSecondary: "#8C8C8C",
21
- bg: "#0A0A0A",
22
- border: "#2A2A2A"
19
+ bg: "#0A0C10",
20
+ surface: "#12151B",
21
+ surfaceHover: "#181C24",
22
+ borderSubtle: "#232832",
23
+ borderStrong: "#313846",
24
+ textPrimary: "#E8EAED",
25
+ textSecondary: "#9AA3B2",
26
+ textTertiary: "#5C6470",
27
+ success: "#22C55E",
28
+ warning: "#F59E0B",
29
+ danger: "#EF4444",
30
+ critical: "#A855F7"
23
31
  };
24
32
  RISK_COLORS = {
25
- LOW: "#22C55E",
26
- MEDIUM: "#F59E0B",
27
- HIGH: "#EF4444",
28
- CRITICAL: "#A855F7"
33
+ LOW: BRAND.success,
34
+ MEDIUM: BRAND.warning,
35
+ HIGH: BRAND.danger,
36
+ CRITICAL: BRAND.critical
29
37
  };
30
- VERSION = "0.1.3";
38
+ VERSION = "0.1.4";
31
39
  APP_ORIGIN = "https://fuzzi-ten.vercel.app";
32
40
  DEFAULT_API_URL = `${APP_ORIGIN}/api`;
33
41
  SETTINGS_API_KEYS_URL = `${APP_ORIGIN}/settings/api-keys`;
@@ -189,9 +197,6 @@ function shouldLog(level) {
189
197
  function stamp() {
190
198
  return (/* @__PURE__ */ new Date()).toISOString();
191
199
  }
192
- function isDebugMode() {
193
- return currentLevel() === "debug";
194
- }
195
200
  var LEVELS, log;
196
201
  var init_logger = __esm({
197
202
  "src/lib/logger.ts"() {
@@ -395,7 +400,7 @@ function getCapabilities() {
395
400
  const colorterm = process.env.COLORTERM ?? "";
396
401
  const trueColor = colorterm.includes("truecolor") || colorterm.includes("24bit") || term.includes("truecolor") || !!process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0";
397
402
  cached = {
398
- width: Math.max(60, Math.min(cols, 120)),
403
+ width: Math.max(60, cols),
399
404
  trueColor,
400
405
  interactive: stdout.isTTY === true
401
406
  };
@@ -434,18 +439,18 @@ function scoreBold(n) {
434
439
  return chalk.bold(String(n));
435
440
  }
436
441
  function error(text) {
437
- return color("#EF4444", chalk.red)(text);
442
+ return color(BRAND.danger, chalk.red)(text);
438
443
  }
439
444
  function success(text) {
440
- return color("#22C55E", chalk.green)(text);
445
+ return color(BRAND.success, chalk.green)(text);
441
446
  }
442
447
  function warn(text) {
443
- return color("#F59E0B", chalk.yellow)(text);
448
+ return color(BRAND.warning, chalk.yellow)(text);
444
449
  }
445
- function info(text) {
446
- return color(BRAND.accent, chalk.cyan)(text);
450
+ function cmd(text) {
451
+ return accent(text);
447
452
  }
448
- var accent, accentBold, muted, bold, dim, italic;
453
+ var accent, accentBold, primary, primaryBold, muted, dimText, border, bold, dim, italic;
449
454
  var init_theme = __esm({
450
455
  "src/terminal/theme.ts"() {
451
456
  "use strict";
@@ -453,7 +458,11 @@ var init_theme = __esm({
453
458
  init_capabilities();
454
459
  accent = color(BRAND.accent, chalk.cyan);
455
460
  accentBold = accent.bold;
461
+ primary = color(BRAND.textPrimary, chalk.white);
462
+ primaryBold = primary.bold;
456
463
  muted = color(BRAND.textSecondary, chalk.gray);
464
+ dimText = color(BRAND.textTertiary, chalk.dim);
465
+ border = color(BRAND.borderSubtle, chalk.gray);
457
466
  bold = chalk.bold;
458
467
  dim = chalk.dim;
459
468
  italic = chalk.italic;
@@ -544,33 +553,6 @@ function panel(content, opts = {}) {
544
553
  width
545
554
  });
546
555
  }
547
- function centerInColumn(text, colWidth) {
548
- return text.split("\n").map((line) => {
549
- const plain = line.replace(/\x1b\[[0-9;]*m/g, "");
550
- const pad = Math.max(0, Math.floor((colWidth - plain.length) / 2));
551
- return " ".repeat(pad) + line;
552
- }).join("\n");
553
- }
554
- function splitHomePanel(opts) {
555
- const total = contentWidth();
556
- const leftW = Math.max(28, Math.floor(total * (opts.leftRatio ?? 0.34)));
557
- const rightW = total - leftW - 3;
558
- const leftLines = opts.left.split("\n");
559
- const rightTop = opts.rightTop.split("\n");
560
- const rightDiv = dim("\u2500".repeat(Math.max(10, rightW)));
561
- const rightBottom = opts.rightBottom.split("\n");
562
- const rightLines = [...rightTop, "", rightDiv, "", ...rightBottom];
563
- const rows = Math.max(leftLines.length, rightLines.length);
564
- const sep = dim("\u2502");
565
- const body = [""];
566
- for (let i = 0; i < rows; i++) {
567
- const l = padEndVisible(leftLines[i] ?? "", leftW);
568
- const r = rightLines[i] ?? "";
569
- body.push(`${l} ${sep} ${r}`);
570
- }
571
- body.push("");
572
- return panel(body.join("\n"), { title: opts.title, marginBottom: 0, borderStyle: "classic" });
573
- }
574
556
  function columns(left, right, leftWidth) {
575
557
  const total = contentWidth();
576
558
  const split = leftWidth ?? Math.floor(total * 0.48);
@@ -589,9 +571,6 @@ function divider(char = "\u2500", width) {
589
571
  const w = width ?? contentWidth();
590
572
  return dim(char.repeat(Math.max(20, w)));
591
573
  }
592
- function statusBar(parts) {
593
- return dim(parts.filter(Boolean).join(" \xB7 "));
594
- }
595
574
  function keyValue(rows, indent = 2) {
596
575
  const pad = " ".repeat(indent);
597
576
  const maxKey = Math.max(...rows.map(([k]) => k.length), 4);
@@ -719,103 +698,44 @@ init_logger();
719
698
  import { randomBytes } from "crypto";
720
699
  import { createServer } from "http";
721
700
  import { exec } from "child_process";
722
- var TIMEOUT_MS = 5 * 60 * 1e3;
723
- function generateState() {
724
- return randomBytes(24).toString("base64url");
725
- }
726
701
  function openBrowser(url) {
727
702
  const platform = process.platform;
728
- const cmd = platform === "darwin" ? `open ${JSON.stringify(url)}` : platform === "win32" ? `start "" ${JSON.stringify(url)}` : `xdg-open ${JSON.stringify(url)}`;
729
- exec(cmd, (err) => {
703
+ const cmd2 = platform === "darwin" ? `open ${JSON.stringify(url)}` : platform === "win32" ? `start "" ${JSON.stringify(url)}` : `xdg-open ${JSON.stringify(url)}`;
704
+ exec(cmd2, (err) => {
730
705
  if (err) log.warn("could not open browser automatically", err.message);
731
706
  });
732
707
  }
733
708
  function apiOrigin(apiUrl) {
734
709
  return apiUrl.replace(/\/api\/?$/, "") || APP_ORIGIN;
735
710
  }
736
- async function runBrowserLogin() {
711
+ async function openCliAuthPage() {
737
712
  const config = await loadConfig();
738
- const state = generateState();
739
- const handoffToken = await new Promise((resolve, reject) => {
740
- const server = createServer((req, res) => {
741
- try {
742
- const addr = server.address();
743
- const port = typeof addr === "object" && addr ? addr.port : 0;
744
- const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
745
- if (url.pathname !== "/callback") {
746
- res.writeHead(404);
747
- res.end();
748
- return;
749
- }
750
- const token = url.searchParams.get("token");
751
- const returnedState = url.searchParams.get("state");
752
- if (!token || returnedState !== state) {
753
- res.writeHead(400);
754
- res.end("Invalid callback");
755
- reject(new ApiError("Invalid sign-in callback.", 400, "invalid_callback", void 0, 2));
756
- return;
757
- }
758
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
759
- res.end(`<!DOCTYPE html><html><body style="font-family:system-ui;text-align:center;padding:48px">
760
- <h1>Signed in to Fuzzi CLI</h1><p>Return to your terminal.</p>
761
- <script>setTimeout(()=>window.close(),1200)</script></body></html>`);
762
- server.close();
763
- resolve(token);
764
- } catch (e) {
765
- server.close();
766
- reject(e);
767
- }
768
- });
769
- const timer = setTimeout(() => {
770
- server.close();
771
- reject(new ApiError("Sign-in timed out after 5 minutes.", 408, "auth_timeout", void 0, 2));
772
- }, TIMEOUT_MS);
773
- server.listen(0, "127.0.0.1", () => {
774
- const addr = server.address();
775
- const port = typeof addr === "object" && addr ? addr.port : 0;
776
- const loginUrl = `${apiOrigin(config.api_url)}/cli-auth?state=${encodeURIComponent(state)}&callback_port=${port}`;
777
- openBrowser(loginUrl);
778
- log.debug("browser auth", loginUrl);
779
- });
780
- server.on("error", (e) => {
781
- clearTimeout(timer);
782
- reject(e);
783
- });
784
- });
785
- const client = new FuzziApiClient(config.api_url);
786
- const handoff = await client.post("/cli/handoff", {
787
- handoff_token: handoffToken,
788
- state
789
- });
790
- if (!handoff.api_key) {
791
- throw new ApiError("Sign-in failed: no API key returned.", 500, "handoff_failed", void 0, 2);
792
- }
793
- client.setToken(handoff.api_key);
794
- const profile = await client.get("/me");
795
- await saveCredentials({
796
- api_key: handoff.api_key,
797
- auth_method: "api_key",
798
- key_prefix: handoff.prefix || profile.key_prefix || maskApiKey(handoff.api_key),
799
- key_expires_at: handoff.expires_at || profile.key_expires_at || void 0,
800
- email: profile.email,
801
- full_name: profile.full_name || void 0,
802
- saved_at: (/* @__PURE__ */ new Date()).toISOString()
803
- });
804
- const name = profile.full_name || profile.email;
805
- return { message: `Signed in as ${name}`, profile };
713
+ const url = `${apiOrigin(config.api_url)}/cli-auth`;
714
+ openBrowser(url);
715
+ log.debug("opened cli auth page", url);
716
+ return url;
806
717
  }
807
718
 
808
719
  // src/commands/auth.ts
809
720
  init_brand();
721
+ async function runAssistedBrowserLogin() {
722
+ await openCliAuthPage();
723
+ console.log("");
724
+ console.log(accent(" Browser opened \u2014 authorize Fuzzi CLI on the web page."));
725
+ console.log(muted(" Copy the API key shown, then paste it below."));
726
+ console.log("");
727
+ const msg = await runApiKeyLogin({ interactive: true });
728
+ const client = await getAuthenticatedClient();
729
+ const profile = await client.get("/me");
730
+ return { message: msg, profile };
731
+ }
810
732
  async function runAuthLogin(opts = {}) {
811
- if (opts.browser || opts.interactive !== false && !opts.apiKey && !opts.apiKeyOnly) {
812
- try {
813
- const result = await runBrowserLogin();
814
- return success(result.message);
815
- } catch (e) {
816
- if (opts.browser) throw e;
817
- if (opts.apiKeyOnly) throw e;
818
- }
733
+ if (opts.apiKeyOnly || opts.apiKey) {
734
+ return runApiKeyLogin(opts);
735
+ }
736
+ if (opts.browser || opts.interactive !== false) {
737
+ const result = await runAssistedBrowserLogin();
738
+ return result.message;
819
739
  }
820
740
  return runApiKeyLogin(opts);
821
741
  }
@@ -1480,33 +1400,7 @@ function buildProgram() {
1480
1400
 
1481
1401
  // src/shell/prompt-loop.ts
1482
1402
  import * as readline from "readline/promises";
1483
- import { stdin as input3, stdout as output } from "process";
1484
-
1485
- // src/shell/home-screen.ts
1486
- import { homedir as homedir3 } from "os";
1487
-
1488
- // src/shell/ascii-mark.ts
1489
- function renderFuzziMark() {
1490
- return [
1491
- " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
1492
- " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
1493
- " \u2588\u2588 \u2588\u2588",
1494
- " \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588",
1495
- " \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588",
1496
- " \u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588",
1497
- " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
1498
- " \u2588\u2588 \u2588\u2588",
1499
- " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
1500
- " \u2588\u2588 \u2588\u2588",
1501
- " \u2588\u2588 \u2588\u2588"
1502
- ].join("\n");
1503
- }
1504
-
1505
- // src/shell/home-screen.ts
1506
- init_brand();
1507
- init_theme();
1508
- init_layout();
1509
- init_width();
1403
+ import { stdin as input3, stdout as output, cwd as cwd3 } from "process";
1510
1404
 
1511
1405
  // src/lib/assets.ts
1512
1406
  import { readFile as readFile4 } from "fs/promises";
@@ -1533,130 +1427,261 @@ async function readAsset(name) {
1533
1427
  }
1534
1428
 
1535
1429
  // src/shell/home-screen.ts
1536
- async function fetchHomeData(profile, cwd4) {
1430
+ init_api_client();
1431
+
1432
+ // src/shell/ascii-mark.ts
1433
+ function renderFuzziMark() {
1434
+ return [
1435
+ " \u2554\u2550\u2550\u2550\u2550\u2557",
1436
+ " \u2551 \u25C6 \u2551",
1437
+ " \u255A\u2550\u2566\u2550\u2550\u255D",
1438
+ " \u2554\u2550\u2569\u2550\u2557",
1439
+ " \u2551 \u25C6 \u2551",
1440
+ " \u255A\u2550\u2550\u2550\u255D"
1441
+ ].join("\n");
1442
+ }
1443
+
1444
+ // src/shell/home-screen.ts
1445
+ init_brand();
1446
+ init_theme();
1447
+ init_width();
1448
+
1449
+ // src/components/Panel.ts
1450
+ init_theme();
1451
+ init_strings();
1452
+ init_width();
1453
+ function visibleLen(s) {
1454
+ return s.replace(/\x1b\[[0-9;]*m/g, "").length;
1455
+ }
1456
+ function topBanner(title, width = contentWidth()) {
1457
+ const inner = ` ${title} `;
1458
+ const dashes = Math.max(0, width - 2 - visibleLen(inner));
1459
+ const left = Math.floor(dashes / 2);
1460
+ const right = dashes - left;
1461
+ return [
1462
+ border(`\u250F${"\u2501".repeat(left)}${inner}${"\u2501".repeat(right)}\u2513`),
1463
+ border(`\u2517${"\u2501".repeat(width - 2)}\u251B`)
1464
+ ].join("\n");
1465
+ }
1466
+ function titleSegment(title, width) {
1467
+ const prefix = `\u2500 ${title} `;
1468
+ const dashes = Math.max(0, width - visibleLen(prefix));
1469
+ return prefix + "\u2500".repeat(dashes);
1470
+ }
1471
+ function tripleColumnPanel(cols, totalWidth = contentWidth()) {
1472
+ const sep = 1;
1473
+ const inner = totalWidth - 2;
1474
+ const ratios = [0.44, 0.28, 0.28];
1475
+ const raw = ratios.map((r) => Math.floor(inner * r));
1476
+ const used = raw.reduce((a, b) => a + b, 0) + 2 * sep;
1477
+ raw[0] += inner - used;
1478
+ const widths = raw;
1479
+ const top = border("\u250C") + titleSegment(cols[0].title, widths[0]) + border("\u252C") + titleSegment(cols[1].title, widths[1]) + border("\u252C") + titleSegment(cols[2].title, widths[2]) + border("\u2510");
1480
+ const maxRows = Math.max(...cols.map((c) => c.lines.length), 1);
1481
+ const body = [top];
1482
+ for (let i = 0; i < maxRows; i++) {
1483
+ const cells = cols.map((c, idx) => padEndVisible(c.lines[i] ?? "", widths[idx]));
1484
+ body.push(
1485
+ border("\u2502") + cells[0] + border("\u2502") + cells[1] + border("\u2502") + cells[2] + border("\u2502")
1486
+ );
1487
+ }
1488
+ const bottom = border("\u2514") + border("\u2500".repeat(widths[0])) + border("\u2534") + border("\u2500".repeat(widths[1])) + border("\u2534") + border("\u2500".repeat(widths[2])) + border("\u2518");
1489
+ body.push(bottom);
1490
+ return body.join("\n");
1491
+ }
1492
+ function stackedPanels(cols, width = contentWidth()) {
1493
+ return cols.map((col) => singlePanel(col.title, col.lines, width)).join("\n\n");
1494
+ }
1495
+ function singlePanel(title, lines, width = contentWidth()) {
1496
+ const inner = width - 2;
1497
+ const prefix = `\u2500 ${title} `;
1498
+ const dashes = Math.max(0, inner - visibleLen(prefix));
1499
+ const top = border(`\u250C${prefix}${"\u2500".repeat(dashes)}\u2510`);
1500
+ const bottom = border(`\u2514${"\u2500".repeat(inner)}\u2518`);
1501
+ const body = lines.map((l) => border("\u2502") + padEndVisible(l, inner) + border("\u2502"));
1502
+ return [top, ...body, bottom].join("\n");
1503
+ }
1504
+ function tipPanel(text, width = contentWidth()) {
1505
+ const inner = width - 2;
1506
+ const prefix = "\u2500 Tip ";
1507
+ const dashes = Math.max(0, inner - visibleLen(prefix));
1508
+ const top = border(`\u250C${prefix}${"\u2500".repeat(dashes)}\u2510`);
1509
+ const bottom = border(`\u2514${"\u2500".repeat(inner)}\u2518`);
1510
+ return [top, border("\u2502") + padEndVisible(text, inner) + border("\u2502"), bottom].join("\n");
1511
+ }
1512
+ function besideMark(mark, text, markCol = 14, gap = 2) {
1513
+ const rows = Math.max(mark.length, text.length);
1514
+ const out = [];
1515
+ for (let i = 0; i < rows; i++) {
1516
+ const m = padEndVisible(mark[i] ?? "", markCol);
1517
+ const t = text[i] ?? "";
1518
+ out.push(m + " ".repeat(gap) + t);
1519
+ }
1520
+ return out;
1521
+ }
1522
+ function popularCommand(command, desc, cmdWidth = 14) {
1523
+ return padEndVisible(cmd(command), cmdWidth) + muted(desc);
1524
+ }
1525
+
1526
+ // src/shell/home-screen.ts
1527
+ function daysUntil2(dateStr) {
1528
+ const diff = new Date(dateStr).getTime() - Date.now();
1529
+ return Math.max(0, Math.ceil(diff / (1e3 * 60 * 60 * 24)));
1530
+ }
1531
+ function formatKeyExpiry(dateStr) {
1532
+ if (!dateStr) return null;
1533
+ const days = daysUntil2(dateStr);
1534
+ return `Key expires: ${days} days remaining`;
1535
+ }
1536
+ function formatKeyExpiryDate(dateStr) {
1537
+ if (!dateStr) return null;
1538
+ return `(${dateStr.slice(0, 10)})`;
1539
+ }
1540
+ async function fetchHomeData(profile) {
1537
1541
  let changelog = [];
1538
1542
  try {
1539
1543
  changelog = JSON.parse(await readAsset("changelog.json"));
1540
1544
  } catch {
1541
1545
  changelog = [];
1542
1546
  }
1543
- return { profile, cwd: cwd4, changelog };
1544
- }
1545
- function isHomeDir(dir) {
1546
- return dir === homedir3() || dir === homedir3().replace(/\/$/, "");
1547
+ let stats = null;
1548
+ if (profile) {
1549
+ try {
1550
+ const client = await getAuthenticatedClient();
1551
+ const rateRaw = await runRateLimitStatus(client);
1552
+ let rateHour = null;
1553
+ if (rateRaw) {
1554
+ const m = rateRaw.match(/(\d+)\/(\d+)/);
1555
+ if (m) {
1556
+ const remaining = Number(m[1]);
1557
+ const limit2 = Number(m[2]);
1558
+ const used2 = limit2 - remaining;
1559
+ rateHour = `${used2}/${limit2} scans this hour`;
1560
+ } else {
1561
+ rateHour = rateRaw;
1562
+ }
1563
+ }
1564
+ const used = profile.scans_used_this_month ?? profile.total_scans;
1565
+ const limit = profile.monthly_scan_limit;
1566
+ const usageMonth = used != null && limit != null ? `${used}/${limit} scans this month` : used != null ? `${used} scans total` : null;
1567
+ stats = { rateHour, usageMonth };
1568
+ } catch {
1569
+ stats = null;
1570
+ }
1571
+ }
1572
+ return { profile, changelog, stats };
1547
1573
  }
1548
- function renderLeftColumn(data) {
1549
- const colW = Math.max(28, Math.floor(contentWidth() * 0.34));
1550
- const name = data.profile?.full_name || data.profile?.email?.split("@")[0] || "there";
1551
- const org = data.profile?.organization?.trim();
1552
- const mark = centerInColumn(accent(renderFuzziMark()), colW);
1553
- const lines = [];
1574
+ function renderAccountColumn(data) {
1575
+ const mark = accent(renderFuzziMark()).split("\n");
1554
1576
  if (data.profile) {
1555
- lines.push(
1556
- accentBold(`Welcome back ${name}!`),
1557
- "",
1558
- mark,
1559
- "",
1560
- [accent("\u25CF Connected"), muted("\xB7"), info("API Key auth"), org ? muted("\xB7 " + org) : ""].filter(Boolean).join(" "),
1561
- muted(data.profile.email),
1562
- data.profile.role ? muted(`Role: ${data.profile.role}`) : "",
1563
- "",
1564
- muted(data.cwd),
1577
+ const p = data.profile;
1578
+ const name = p.full_name || p.email.split("@")[0];
1579
+ const keyExpiry = formatKeyExpiry(p.key_expires_at ?? void 0);
1580
+ const keyDate = formatKeyExpiryDate(p.key_expires_at ?? void 0);
1581
+ const textBlock2 = [
1582
+ primaryBold(`Welcome back, ${name}!`),
1583
+ primary(p.email),
1565
1584
  "",
1566
- accent("/scan") + muted(" <url> scan a target"),
1567
- accent("/scans") + muted(" browse history"),
1568
- accent("/status") + muted(" account info"),
1569
- accent("/keys") + muted(" manage keys"),
1570
- accent("/palette") + muted(" find commands")
1571
- );
1572
- } else {
1573
- lines.push(
1574
- accentBold("Welcome to Fuzzi!"),
1575
- "",
1576
- mark,
1577
- "",
1578
- muted("Not connected"),
1579
- info("Press Enter to sign in"),
1580
- "",
1581
- muted(data.cwd),
1582
- "",
1583
- accent("/auth") + muted(" browser sign-in"),
1584
- accent("/auth-key") + muted(" paste API key"),
1585
- accent("/scan") + muted(" <url> after login"),
1586
- accent("/help") + muted(" all commands")
1587
- );
1585
+ muted(`Organization: ${p.organization || "\u2014"}`),
1586
+ muted(`Role: ${p.role}`)
1587
+ ];
1588
+ const beside = besideMark(mark, textBlock2, 14, 4);
1589
+ const lines = ["", ...beside, ""];
1590
+ lines.push(success("\u25CF Connected"));
1591
+ lines.push(muted("Status: Ready"));
1592
+ if (data.stats?.rateHour) lines.push(muted(`Rate limit: ${data.stats.rateHour}`));
1593
+ if (data.stats?.usageMonth) lines.push(muted(`Usage: ${data.stats.usageMonth}`));
1594
+ if (keyExpiry) {
1595
+ lines.push("");
1596
+ lines.push(muted(keyExpiry));
1597
+ if (keyDate) lines.push(muted(keyDate));
1598
+ }
1599
+ return lines;
1588
1600
  }
1589
- return lines.filter((l) => l !== "").join("\n");
1601
+ const textBlock = [
1602
+ primaryBold("Welcome to Fuzzi!"),
1603
+ muted("Not connected"),
1604
+ "",
1605
+ muted("Press Enter to sign in"),
1606
+ muted("or run /auth-key")
1607
+ ];
1608
+ return ["", ...besideMark(mark, textBlock, 14, 4), ""];
1590
1609
  }
1591
- function renderTipsColumn(data) {
1610
+ function renderQuickStartColumn(data) {
1592
1611
  const lines = [
1593
- accentBold("Tips for getting started"),
1594
1612
  "",
1595
- `Run ${accent("/scan")}${muted(" <url>")} to scan a site for security risks`,
1596
- `Run ${accent("/palette")} to search every available command`,
1597
- `Run ${accent("/help")} for the full command reference`,
1598
- ""
1613
+ muted("Type ") + cmd("/help") + muted(" for all"),
1614
+ muted("commands."),
1615
+ "",
1616
+ primaryBold("Popular commands:"),
1617
+ "",
1618
+ popularCommand("/scan", "Start a security scan", 12),
1619
+ popularCommand("/scans", "Browse recent scans", 12),
1620
+ popularCommand("/status", "Show account info", 12)
1599
1621
  ];
1600
- if (!data.profile) {
1601
- lines.push(
1602
- muted("Note: You launched without credentials."),
1603
- muted("Press Enter at the prompt to open your browser,"),
1604
- muted("or use /auth-key to paste an API key."),
1605
- ""
1606
- );
1607
- } else if (isHomeDir(data.cwd)) {
1622
+ if (data.profile) {
1608
1623
  lines.push(
1609
- muted("Note: You launched fuzzi in your home directory."),
1610
- muted("cd into a project folder first for better context,"),
1611
- muted("or pass URLs directly: /scan https://example.com"),
1612
- ""
1624
+ popularCommand("/keys", "Manage API keys", 12),
1625
+ popularCommand("/config", "CLI settings", 12)
1613
1626
  );
1614
1627
  } else {
1615
1628
  lines.push(
1616
- muted("Note: Add a .fuzzirc in this directory to set default"),
1617
- muted("scan URL, environment, and output format for the team."),
1618
- ""
1629
+ popularCommand("/auth", "Browser sign-in", 12),
1630
+ popularCommand("/auth-key", "Paste API key", 12)
1619
1631
  );
1620
1632
  }
1621
- lines.push(
1622
- muted("CI usage: "),
1623
- muted("fuzzi scan <url> --fail-on critical --format json")
1624
- );
1625
- return lines.join("\n");
1633
+ return lines;
1626
1634
  }
1627
1635
  function renderWhatsNewColumn(data) {
1628
- const latest = data.changelog[0];
1629
- const lines = [accentBold("What's new"), ""];
1630
- if (latest) {
1631
- for (const h of latest.highlights.slice(0, 4)) {
1632
- lines.push(muted(h));
1633
- }
1634
- lines.push("");
1635
- lines.push(italic(muted("/changelog for more")));
1636
- } else {
1637
- lines.push(muted("Stay tuned for updates."));
1636
+ const lines = [""];
1637
+ const highlights = data.changelog[0]?.highlights ?? [
1638
+ "Confidence gating added",
1639
+ "Netflix-style false positives fixed",
1640
+ "CLI shell interface"
1641
+ ];
1642
+ for (const h of highlights.slice(0, 4)) {
1643
+ const text = h.replace(/^✓\s*/, "");
1644
+ lines.push(success("\u2713 ") + primary(text));
1638
1645
  }
1639
- return lines.join("\n");
1646
+ lines.push("");
1647
+ lines.push(muted("Run ") + cmd("/changelog") + muted(" for more details."));
1648
+ return lines;
1649
+ }
1650
+ function renderTip(data) {
1651
+ if (data.profile) {
1652
+ return muted("Tip: Run ") + cmd("/scan <url>") + muted(" to scan a target, or ") + cmd("/palette") + muted(" to find commands.");
1653
+ }
1654
+ return muted("Not logged in? Run ") + cmd("/auth-key") + muted(" to paste an API key from settings, or press Enter to sign in via browser.");
1640
1655
  }
1641
1656
  function renderHomeScreen(data) {
1642
- return splitHomePanel({
1643
- title: `Fuzzi CLI v${VERSION}`,
1644
- left: renderLeftColumn(data),
1645
- rightTop: renderTipsColumn(data),
1646
- rightBottom: renderWhatsNewColumn(data),
1647
- leftRatio: 0.36
1648
- });
1657
+ const width = contentWidth();
1658
+ const cols = [
1659
+ { title: "Account", lines: renderAccountColumn(data) },
1660
+ { title: "Quick Start", lines: renderQuickStartColumn(data) },
1661
+ { title: "What's New", lines: renderWhatsNewColumn(data) }
1662
+ ];
1663
+ const main2 = terminalWidth() >= 100 ? tripleColumnPanel(cols, width) : stackedPanels(cols, width);
1664
+ return [topBanner(`Fuzzi CLI v${VERSION}`, width), "", main2, "", tipPanel(renderTip(data), width)].join(
1665
+ "\n"
1666
+ );
1649
1667
  }
1650
1668
  function renderChangelog(entries) {
1651
1669
  if (!entries.length) return muted("No changelog entries.");
1652
1670
  return entries.map((e) => {
1653
1671
  const lines = [
1654
1672
  accentBold(`v${e.version}`) + muted(` \u2014 ${e.date}`),
1655
- ...e.highlights.map((h) => muted(` \xB7 ${h}`))
1673
+ ...e.highlights.map((h) => success("\u2713 ") + primary(h.replace(/^✓\s*/, "")))
1656
1674
  ];
1657
1675
  return lines.join("\n");
1658
1676
  }).join("\n\n");
1659
1677
  }
1678
+ function renderAuthGateScreen() {
1679
+ return renderHomeScreen({
1680
+ profile: null,
1681
+ changelog: [],
1682
+ stats: null
1683
+ });
1684
+ }
1660
1685
 
1661
1686
  // src/shell/slash-commands.ts
1662
1687
  init_api_client();
@@ -1759,9 +1784,9 @@ var SLASH_COMMANDS = [
1759
1784
  { name: "/exit", description: "Exit the shell", aliases: ["/quit"] }
1760
1785
  ];
1761
1786
  function findCommand(input5) {
1762
- const cmd = input5.trim().split(/\s/)[0].toLowerCase();
1787
+ const cmd2 = input5.trim().split(/\s/)[0].toLowerCase();
1763
1788
  return SLASH_COMMANDS.find(
1764
- (c) => c.name === cmd || c.aliases?.some((a) => a === cmd)
1789
+ (c) => c.name === cmd2 || c.aliases?.some((a) => a === cmd2)
1765
1790
  );
1766
1791
  }
1767
1792
 
@@ -1880,9 +1905,9 @@ async function dispatchSlashCommand(line, ctx) {
1880
1905
  const trimmed = line.trim();
1881
1906
  if (!trimmed) return {};
1882
1907
  if (trimmed === "/exit" || trimmed === "/quit") return { exit: true };
1883
- const [cmd, ...rest] = trimmed.split(/\s+/);
1908
+ const [cmd2, ...rest] = trimmed.split(/\s+/);
1884
1909
  const arg = rest.join(" ").trim();
1885
- if (!cmd.startsWith("/")) {
1910
+ if (!cmd2.startsWith("/")) {
1886
1911
  ctx.sink.write(
1887
1912
  errorBox(
1888
1913
  `Not a shell command: ${trimmed}`,
@@ -1892,12 +1917,12 @@ ${accent("/help")} lists everything`
1892
1917
  );
1893
1918
  return {};
1894
1919
  }
1895
- if (!findCommand(cmd) && cmd.startsWith("/")) {
1896
- ctx.sink.write(errorBox(`Unknown command: ${cmd}`, "Type /help or /palette"));
1920
+ if (!findCommand(cmd2) && cmd2.startsWith("/")) {
1921
+ ctx.sink.write(errorBox(`Unknown command: ${cmd2}`, "Type /help or /palette"));
1897
1922
  return {};
1898
1923
  }
1899
1924
  try {
1900
- switch (cmd.toLowerCase()) {
1925
+ switch (cmd2.toLowerCase()) {
1901
1926
  case "/help":
1902
1927
  ctx.sink.write(renderHelpScreen());
1903
1928
  break;
@@ -1991,9 +2016,9 @@ ${muted("Rate limit")} ${rate}` : status);
1991
2016
  }
1992
2017
  case "/login":
1993
2018
  case "/auth": {
1994
- ctx.sink.write(await runAuthLogin({ interactive: true, browser: true }));
1995
- const client = await getAuthenticatedClient();
1996
- return { profile: await client.get("/me"), redraw: true };
2019
+ const result = await runAssistedBrowserLogin();
2020
+ ctx.sink.write(result.message);
2021
+ return { profile: result.profile, redraw: true };
1997
2022
  }
1998
2023
  case "/auth-key": {
1999
2024
  ctx.sink.write(await runApiKeyLogin({ interactive: true }));
@@ -2001,7 +2026,7 @@ ${muted("Rate limit")} ${rate}` : status);
2001
2026
  return { profile: await client.get("/me"), redraw: true };
2002
2027
  }
2003
2028
  default:
2004
- ctx.sink.write(errorBox(`Unknown command: ${cmd}`, "Type /help"));
2029
+ ctx.sink.write(errorBox(`Unknown command: ${cmd2}`, "Type /help"));
2005
2030
  }
2006
2031
  } catch (e) {
2007
2032
  ctx.sink.error(formatApiError(e));
@@ -2072,7 +2097,6 @@ async function runKeysInteractive(ctx) {
2072
2097
 
2073
2098
  // src/shell/prompt-loop.ts
2074
2099
  init_theme();
2075
- import { cwd as cwd3 } from "process";
2076
2100
 
2077
2101
  // src/shell/completer.ts
2078
2102
  function buildCompleter(commands, history) {
@@ -2090,8 +2114,6 @@ function buildCompleter(commands, history) {
2090
2114
 
2091
2115
  // src/shell/prompt-loop.ts
2092
2116
  init_capabilities();
2093
- init_layout();
2094
- init_logger();
2095
2117
  async function runPromptLoop(initialProfile) {
2096
2118
  let profile = initialProfile;
2097
2119
  const workDir = cwd3();
@@ -2100,13 +2122,8 @@ async function runPromptLoop(initialProfile) {
2100
2122
  if (getCapabilities().interactive) {
2101
2123
  process.stdout.write("\x1B[2J\x1B[H");
2102
2124
  }
2103
- const data = await fetchHomeData(profile, workDir);
2125
+ const data = await fetchHomeData(profile);
2104
2126
  console.log(renderHomeScreen(data));
2105
- console.log(statusBar([
2106
- profile ? muted(profile.email) : muted("guest"),
2107
- dim(workDir),
2108
- isDebugMode() ? muted("debug") : null
2109
- ].filter(Boolean)));
2110
2127
  console.log("");
2111
2128
  };
2112
2129
  await refresh();
@@ -2122,7 +2139,7 @@ async function runPromptLoop(initialProfile) {
2122
2139
  rl.close();
2123
2140
  process.exit(0);
2124
2141
  });
2125
- const prompt = () => process.stdout.write(accent("\u203A "));
2142
+ const prompt = () => process.stdout.write(accent("> "));
2126
2143
  prompt();
2127
2144
  for await (const line of rl) {
2128
2145
  await appendHistory(line);
@@ -2151,11 +2168,9 @@ async function runPromptLoop(initialProfile) {
2151
2168
  }
2152
2169
 
2153
2170
  // src/shell/auth-gate.ts
2154
- init_layout();
2155
- init_theme();
2156
- init_width();
2157
2171
  import * as readline2 from "readline";
2158
2172
  import { stdin as input4, stdout as output2 } from "process";
2173
+ init_theme();
2159
2174
 
2160
2175
  // src/cli/profile.ts
2161
2176
  init_credentials();
@@ -2174,50 +2189,10 @@ async function tryGetProfile() {
2174
2189
  }
2175
2190
 
2176
2191
  // src/shell/auth-gate.ts
2177
- init_brand();
2178
- function renderAuthGate() {
2179
- const colW = Math.max(28, Math.floor(contentWidth() * 0.36));
2180
- const mark = centerInColumn(accent(renderFuzziMark()), colW);
2181
- const left = [
2182
- accentBold("Welcome to Fuzzi!"),
2183
- "",
2184
- mark,
2185
- "",
2186
- muted("Not connected"),
2187
- info("Sign in to run scans"),
2188
- "",
2189
- accent("/auth-key") + muted(" paste API key"),
2190
- accent("/help") + muted(" commands")
2191
- ].join("\n");
2192
- const rightTop = [
2193
- accentBold("Sign in to continue"),
2194
- "",
2195
- info("Press Enter to open your browser"),
2196
- muted("and authorize the CLI."),
2197
- "",
2198
- muted("A local server receives the callback"),
2199
- muted(`from ${APP_HOST} automatically.`)
2200
- ].join("\n");
2201
- const rightBottom = [
2202
- accentBold("Other options"),
2203
- "",
2204
- muted("Paste an API key with /auth-key"),
2205
- muted("from Settings \u2192 API Keys on the web."),
2206
- "",
2207
- italic(muted(SETTINGS_API_KEYS_URL))
2208
- ].join("\n");
2209
- return splitHomePanel({
2210
- title: `Fuzzi CLI v${VERSION}`,
2211
- left,
2212
- rightTop,
2213
- rightBottom,
2214
- leftRatio: 0.36
2215
- });
2216
- }
2217
2192
  function waitForEnter() {
2218
2193
  return new Promise((resolve) => {
2219
2194
  const rl = readline2.createInterface({ input: input4, output: output2, terminal: true });
2220
- output2.write(accent("\n \u203A Press Enter to open browser... "));
2195
+ output2.write(accent("\n> Press Enter to open browser... "));
2221
2196
  rl.once("line", () => {
2222
2197
  rl.close();
2223
2198
  resolve();
@@ -2228,18 +2203,15 @@ async function runAuthGate() {
2228
2203
  const existing = await tryGetProfile();
2229
2204
  if (existing) return existing;
2230
2205
  if (!output2.isTTY) return null;
2231
- console.log(renderAuthGate());
2206
+ console.log(renderAuthGateScreen());
2232
2207
  await waitForEnter();
2233
- const progress = createProgress("Opening browser...");
2234
2208
  try {
2235
- const result = await runBrowserLogin();
2236
- progress.succeed("Signed in");
2237
- console.log(accent(result.message));
2209
+ const result = await runAssistedBrowserLogin();
2210
+ console.log(result.message);
2238
2211
  return result.profile;
2239
2212
  } catch (e) {
2240
- progress.fail("Sign-in failed");
2241
2213
  console.log(muted(formatApiError(e)));
2242
- console.log(muted("Use /auth-key to paste an API key, or /auth to retry."));
2214
+ console.log(muted("Run /auth-key to paste your API key manually."));
2243
2215
  return null;
2244
2216
  }
2245
2217
  }