fuzzi-cli 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Run Fuzzi security scans from your terminal. Interactive shell for daily use, scriptable commands for CI.
4
4
 
5
+ **Web app:** [fuzzi-ten.vercel.app](https://fuzzi-ten.vercel.app)
6
+
5
7
  ```bash
6
8
  npm install -g fuzzi-cli
7
9
  fuzzi
@@ -14,7 +16,7 @@ fuzzi
14
16
  1. **Install** the CLI (above)
15
17
  2. **Run** `fuzzi`
16
18
  3. You'll see **Sign in to continue** — press **Enter**
17
- 4. Your **browser opens** to app.fuzzi.dev — log in or sign up
19
+ 4. Your **browser opens** to [fuzzi-ten.vercel.app](https://fuzzi-ten.vercel.app) — log in or sign up
18
20
  5. After authorizing, return to the terminal — you're in
19
21
 
20
22
  ```
@@ -24,7 +26,7 @@ fuzzi
24
26
  › /palette # search commands
25
27
  ```
26
28
 
27
- No browser? Use **`/auth-key`** to paste an API key from [Settings → API Keys](https://app.fuzzi.dev/settings/api-keys).
29
+ No browser? Use **`/auth-key`** to paste an API key from [Settings → API Keys](https://fuzzi-ten.vercel.app/settings/api-keys).
28
30
 
29
31
  ---
30
32
 
@@ -74,10 +76,7 @@ fuzzi scan https://staging.example.com --fail-on high
74
76
  # JSON for pipelines
75
77
  fuzzi scan https://example.com --format json
76
78
 
77
- # Machine-readable exit codes
78
- # 0 = success, risk below threshold
79
- # 1 = scan done, risk at/above --fail-on
80
- # 2 = error (network, auth, bad URL)
79
+ # Exit codes: 0 = pass, 1 = risk threshold met, 2 = error
81
80
  ```
82
81
 
83
82
  ### All commands
@@ -91,15 +90,12 @@ fuzzi auth logout
91
90
  fuzzi scan <url> [--wait] [--no-wait] [--format table|json|markdown]
92
91
  [--env production|staging|development]
93
92
  [--fail-on low|medium|high|critical]
94
- [--fail-threshold 0.0-1.0]
95
93
 
96
- fuzzi scans list [--status] [--risk-level] [--limit 20]
97
- fuzzi scans get <scan-id> [--format table|json|markdown]
98
- fuzzi report <scan-id> --format pdf|csv|json [-o file]
94
+ fuzzi scans list | get <scan-id>
95
+ fuzzi report <scan-id> --format pdf|csv|json
99
96
  fuzzi whatif <scan-id> --set dimension=0.5
100
97
  fuzzi compare <scan-a> <scan-b>
101
-
102
- fuzzi config list | get [key] | set <key> <value>
98
+ fuzzi config list | get | set
103
99
  fuzzi status
104
100
  fuzzi --help
105
101
  ```
@@ -111,84 +107,48 @@ fuzzi --help
111
107
  | File | Purpose |
112
108
  |------|---------|
113
109
  | `~/.fuzzi/credentials` | API key (mode 600) |
114
- | `~/.fuzzi/config` | CLI defaults (`default_env`, `default_format`) |
115
- | `~/.fuzzi/history` | Shell command history |
116
- | `.fuzzirc` or `fuzzi.toml` | Project defaults in repo root |
117
-
118
- **Example `.fuzzirc`:**
119
-
120
- ```json
121
- {
122
- "scan": {
123
- "url": "https://staging.example.com",
124
- "environment": "staging",
125
- "fail_on": "high"
126
- },
127
- "output": { "format": "markdown" }
128
- }
129
- ```
110
+ | `~/.fuzzi/config` | CLI defaults |
111
+ | `.fuzzirc` / `fuzzi.toml` | Project defaults |
130
112
 
131
- Flags on the command line override file values.
113
+ **Default API:** `https://fuzzi-ten.vercel.app/api`
132
114
 
133
115
  ```bash
134
116
  fuzzi config set default_env staging
135
- fuzzi config set default_format markdown
136
- export FUZZI_API_URL=https://app.fuzzi.dev/api # override API
137
- export FUZZI_DEBUG=1 # debug logging
117
+ export FUZZI_API_URL=https://fuzzi-ten.vercel.app/api # override if needed
118
+ export FUZZI_DEBUG=1
138
119
  ```
139
120
 
140
121
  ---
141
122
 
142
- ## CI example (GitHub Actions)
123
+ ## CI example
143
124
 
144
125
  ```yaml
145
126
  - name: Fuzzi security gate
146
127
  run: |
147
128
  npm install -g fuzzi-cli
148
129
  fuzzi auth login --api-key "${{ secrets.FUZZI_API_KEY }}"
149
- fuzzi scan https://staging.example.com --fail-on critical --format markdown
130
+ fuzzi scan https://staging.example.com --fail-on critical
150
131
  ```
151
132
 
152
133
  ---
153
134
 
154
- ## For web / frontend developers
135
+ ## For web developers
155
136
 
156
- The CLI browser login flow requires pages and API routes on **app.fuzzi.dev**.
137
+ Browser login and API contracts for [fuzzi-ten.vercel.app](https://fuzzi-ten.vercel.app):
157
138
 
158
- See **[docs/frontend-integration.md](./docs/frontend-integration.md)** for:
159
-
160
- - `/cli-auth` page spec
161
- - `POST /api/cli/handoff` contract
162
- - API keys settings UI
163
- - Full feature parity checklist
139
+ See **[docs/frontend-integration.md](./docs/frontend-integration.md)**
164
140
 
165
141
  ---
166
142
 
167
143
  ## Development
168
144
 
169
145
  ```bash
170
- git clone <repo>
171
- cd fuzzi-cli
172
- npm install
173
- npm test
174
- npm run build
175
- npm link # optional: global `fuzzi` command
146
+ npm install && npm test && npm run build
147
+ npm link # optional global `fuzzi` command
176
148
  ```
177
149
 
178
- ---
179
-
180
- ## Publish to npm
150
+ ## Publish
181
151
 
182
152
  ```bash
183
- npm login
184
153
  npm publish --access public
185
154
  ```
186
-
187
- Or tag `v0.1.0` and let GitHub Actions publish (requires `NPM_TOKEN` secret).
188
-
189
- ---
190
-
191
- ## Brand
192
-
193
- - Accent: `#4FC3A1` (teal)
194
- - Risk: LOW green · MEDIUM amber · HIGH red · CRITICAL purple
@@ -1,4 +1,24 @@
1
1
  [
2
+ {
3
+ "version": "0.1.3",
4
+ "date": "2026-06-19",
5
+ "highlights": [
6
+ "Production app URL: fuzzi-ten.vercel.app",
7
+ "API default: fuzzi-ten.vercel.app/api",
8
+ "Claude Code-style two-column home screen",
9
+ "Browser sign-in on startup"
10
+ ]
11
+ },
12
+ {
13
+ "version": "0.1.2",
14
+ "date": "2026-06-19",
15
+ "highlights": [
16
+ "Claude Code-style two-column home screen",
17
+ "Thicker pixel shield mascot",
18
+ "Contextual tips and what's-new panels",
19
+ "Full terminal width layout"
20
+ ]
21
+ },
2
22
  {
3
23
  "version": "0.1.1",
4
24
  "date": "2026-06-19",
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ var __export = (target, all) => {
9
9
  };
10
10
 
11
11
  // src/types/brand.ts
12
- var BRAND, RISK_COLORS, VERSION, APP_ORIGIN, DEFAULT_API_URL;
12
+ var BRAND, RISK_COLORS, VERSION, APP_ORIGIN, DEFAULT_API_URL, SETTINGS_API_KEYS_URL, CLI_AUTH_URL, APP_HOST;
13
13
  var init_brand = __esm({
14
14
  "src/types/brand.ts"() {
15
15
  "use strict";
@@ -27,9 +27,12 @@ var init_brand = __esm({
27
27
  HIGH: "#EF4444",
28
28
  CRITICAL: "#A855F7"
29
29
  };
30
- VERSION = "0.1.1";
31
- APP_ORIGIN = "https://app.fuzzi.dev";
30
+ VERSION = "0.1.3";
31
+ APP_ORIGIN = "https://fuzzi-ten.vercel.app";
32
32
  DEFAULT_API_URL = `${APP_ORIGIN}/api`;
33
+ SETTINGS_API_KEYS_URL = `${APP_ORIGIN}/settings/api-keys`;
34
+ CLI_AUTH_URL = `${APP_ORIGIN}/cli-auth`;
35
+ APP_HOST = "fuzzi-ten.vercel.app";
33
36
  }
34
37
  });
35
38
 
@@ -220,9 +223,9 @@ function mapErrorMessage(status, body) {
220
223
  return "API key has been revoked. Please log in again.";
221
224
  }
222
225
  if (code === "key_expired" || msg.toLowerCase().includes("expired")) {
223
- return "API key has expired. Generate a new one at https://app.fuzzi.dev/settings/api-keys";
226
+ return `API key has expired. Generate a new one at ${SETTINGS_API_KEYS_URL}`;
224
227
  }
225
- return "Invalid API key. Generate a new one at https://app.fuzzi.dev/settings/api-keys";
228
+ return `Invalid API key. Generate a new one at ${SETTINGS_API_KEYS_URL}`;
226
229
  }
227
230
  if (status === 403 && (code === "ssrf" || msg.toLowerCase().includes("private ip"))) {
228
231
  return "This URL is not allowed (private IP address detected). Please scan a public-facing URL.";
@@ -253,6 +256,7 @@ var init_api_client = __esm({
253
256
  init_config();
254
257
  init_credentials();
255
258
  init_logger();
259
+ init_brand();
256
260
  ApiError = class extends Error {
257
261
  constructor(message, status, code, body, exitCode) {
258
262
  super(message);
@@ -302,7 +306,7 @@ var init_api_client = __esm({
302
306
  });
303
307
  } catch {
304
308
  throw new ApiError(
305
- "Could not connect to app.fuzzi.dev. Check your internet connection or try again later.",
309
+ `Could not connect to ${APP_HOST}. Check your internet connection or try again later.`,
306
310
  0,
307
311
  "network_error",
308
312
  void 0,
@@ -358,7 +362,7 @@ var init_api_client = __esm({
358
362
  res = await fetch(url, { headers: this.headers() });
359
363
  } catch {
360
364
  throw new ApiError(
361
- "Could not connect to app.fuzzi.dev. Check your internet connection or try again later.",
365
+ `Could not connect to ${APP_HOST}. Check your internet connection or try again later.`,
362
366
  0,
363
367
  "network_error",
364
368
  void 0,
@@ -441,7 +445,7 @@ function warn(text) {
441
445
  function info(text) {
442
446
  return color(BRAND.accent, chalk.cyan)(text);
443
447
  }
444
- var accent, accentBold, muted, bold, dim;
448
+ var accent, accentBold, muted, bold, dim, italic;
445
449
  var init_theme = __esm({
446
450
  "src/terminal/theme.ts"() {
447
451
  "use strict";
@@ -452,6 +456,7 @@ var init_theme = __esm({
452
456
  muted = color(BRAND.textSecondary, chalk.gray);
453
457
  bold = chalk.bold;
454
458
  dim = chalk.dim;
459
+ italic = chalk.italic;
455
460
  }
456
461
  });
457
462
 
@@ -533,12 +538,39 @@ function panel(content, opts = {}) {
533
538
  title: opts.title ? accentBold(opts.title) : void 0,
534
539
  padding: opts.padding ?? 1,
535
540
  margin: { top: 0, bottom: opts.marginBottom ?? 1, left: 0, right: 0 },
536
- borderStyle: "round",
541
+ borderStyle: opts.borderStyle ?? "classic",
537
542
  borderColor: getCapabilities().trueColor ? BRAND.accent : void 0,
538
543
  titleAlignment: "left",
539
544
  width
540
545
  });
541
546
  }
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
+ }
542
574
  function columns(left, right, leftWidth) {
543
575
  const total = contentWidth();
544
576
  const split = leftWidth ?? Math.floor(total * 0.48);
@@ -565,13 +597,6 @@ function keyValue(rows, indent = 2) {
565
597
  const maxKey = Math.max(...rows.map(([k]) => k.length), 4);
566
598
  return rows.map(([k, v]) => `${pad}${muted(k.padEnd(maxKey))} ${v}`).join("\n");
567
599
  }
568
- function centerBlock(text, width = contentWidth()) {
569
- return text.split("\n").map((line) => {
570
- const plain = line.replace(/\x1b\[[0-9;]*m/g, "");
571
- const pad = Math.max(0, Math.floor((width - plain.length) / 2));
572
- return " ".repeat(pad) + line;
573
- }).join("\n");
574
- }
575
600
  var init_layout = __esm({
576
601
  "src/terminal/layout.ts"() {
577
602
  "use strict";
@@ -781,6 +806,7 @@ async function runBrowserLogin() {
781
806
  }
782
807
 
783
808
  // src/commands/auth.ts
809
+ init_brand();
784
810
  async function runAuthLogin(opts = {}) {
785
811
  if (opts.browser || opts.interactive !== false && !opts.apiKey && !opts.apiKeyOnly) {
786
812
  try {
@@ -820,7 +846,7 @@ async function runApiKeyLogin(opts = {}) {
820
846
  apiKey = apiKey.trim();
821
847
  if (!isValidApiKeyFormat(apiKey)) {
822
848
  throw new ApiError(
823
- "Invalid API key format. Generate a new one at https://app.fuzzi.dev/settings/api-keys",
849
+ `Invalid API key format. Generate a new one at ${SETTINGS_API_KEYS_URL}`,
824
850
  401,
825
851
  "invalid_key_format",
826
852
  void 0,
@@ -831,7 +857,7 @@ async function runApiKeyLogin(opts = {}) {
831
857
  const valid = await client.validateToken();
832
858
  if (!valid) {
833
859
  throw new ApiError(
834
- "Invalid API key. Generate a new one at https://app.fuzzi.dev/settings/api-keys",
860
+ `Invalid API key. Generate a new one at ${SETTINGS_API_KEYS_URL}`,
835
861
  401,
836
862
  "invalid_token",
837
863
  void 0,
@@ -972,13 +998,14 @@ init_theme();
972
998
 
973
999
  // src/lib/errors.ts
974
1000
  init_api_client();
1001
+ init_brand();
975
1002
  function formatApiError(err) {
976
1003
  if (err instanceof ApiError) {
977
1004
  return err.message;
978
1005
  }
979
1006
  if (err instanceof Error) {
980
1007
  if (err.message.includes("fetch failed") || err.message.includes("ECONNREFUSED")) {
981
- return "Could not connect to app.fuzzi.dev. Check your internet connection or try again later.";
1008
+ return `Could not connect to ${APP_HOST}. Check your internet connection or try again later.`;
982
1009
  }
983
1010
  return `An error occurred: ${err.message}. Please report this at https://github.com/fuzzi-cli/fuzzi-cli/issues`;
984
1011
  }
@@ -1455,16 +1482,23 @@ function buildProgram() {
1455
1482
  import * as readline from "readline/promises";
1456
1483
  import { stdin as input3, stdout as output } from "process";
1457
1484
 
1485
+ // src/shell/home-screen.ts
1486
+ import { homedir as homedir3 } from "os";
1487
+
1458
1488
  // src/shell/ascii-mark.ts
1459
1489
  function renderFuzziMark() {
1460
1490
  return [
1461
- " \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E",
1462
- " \u2571 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2572",
1463
- " \u2502 \u2502 FUZZI \u2502 \u2502",
1464
- " \u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502",
1465
- " \u2502 \u2593\u2593\u2593 \u2502",
1466
- " \u2572 \u2571",
1467
- " \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"
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"
1468
1502
  ].join("\n");
1469
1503
  }
1470
1504
 
@@ -1508,52 +1542,110 @@ async function fetchHomeData(profile, cwd4) {
1508
1542
  }
1509
1543
  return { profile, cwd: cwd4, changelog };
1510
1544
  }
1511
- function renderHomeScreen(data) {
1512
- const w = contentWidth();
1545
+ function isHomeDir(dir) {
1546
+ return dir === homedir3() || dir === homedir3().replace(/\/$/, "");
1547
+ }
1548
+ function renderLeftColumn(data) {
1549
+ const colW = Math.max(28, Math.floor(contentWidth() * 0.34));
1513
1550
  const name = data.profile?.full_name || data.profile?.email?.split("@")[0] || "there";
1514
1551
  const org = data.profile?.organization?.trim();
1515
- const welcome = data.profile ? accentBold(`Welcome back, ${name}!`) : accentBold("Welcome to Fuzzi");
1516
- const mark = centerBlock(accent(renderFuzziMark()), w);
1517
- const statusLine = data.profile ? [accent("\u25CF Connected"), org, muted(data.profile.email)].filter(Boolean).join(muted(" \xB7 ")) : muted("Not connected") + muted(" \xB7 ") + info("Press Enter to sign in via browser");
1518
- const cwdLine = centerBlock(muted(data.cwd), w);
1519
- const latest = data.changelog[0];
1520
- const whatsNewTitle = accentBold("What's new");
1521
- const whatsNew = latest ? [
1522
- whatsNewTitle,
1523
- ...latest.highlights.slice(0, 3).map((h) => muted(` \xB7 ${h}`)),
1524
- muted(" /changelog for more")
1525
- ].join("\n") : [whatsNewTitle, muted(" Stay tuned for updates")].join("\n");
1526
- const quickTitle = accentBold("Quick actions");
1527
- const quickActions = [
1528
- quickTitle,
1529
- accent("/scan") + muted(" <url> ") + muted("security scan"),
1530
- accent("/scans") + muted(" ") + muted("browse history"),
1531
- accent("/status") + muted(" ") + muted("account info"),
1532
- accent("/auth") + muted(" ") + muted("sign in (browser)"),
1533
- accent("/auth-key") + muted(" ") + muted("paste API key"),
1534
- accent("/palette") + muted(" ") + muted("search commands"),
1535
- accent("/help") + muted(" ") + muted("all commands")
1536
- ].join("\n");
1537
- const footer = data.profile ? muted("Type a command below \xB7 /palette to search \xB7 Ctrl+C to exit") : muted("Press Enter at startup to sign in via browser \xB7 or /auth-key");
1538
- const body = [
1539
- "",
1540
- centerBlock(welcome, w),
1541
- "",
1542
- mark,
1543
- "",
1544
- centerBlock(statusLine, w),
1545
- cwdLine,
1546
- "",
1547
- divider(),
1548
- "",
1549
- columns(quickActions, whatsNew),
1550
- "",
1551
- divider(),
1552
+ const mark = centerInColumn(accent(renderFuzziMark()), colW);
1553
+ const lines = [];
1554
+ 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),
1565
+ "",
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
+ );
1588
+ }
1589
+ return lines.filter((l) => l !== "").join("\n");
1590
+ }
1591
+ function renderTipsColumn(data) {
1592
+ const lines = [
1593
+ accentBold("Tips for getting started"),
1552
1594
  "",
1553
- centerBlock(footer, w),
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`,
1554
1598
  ""
1555
- ].join("\n");
1556
- return panel(body, { title: `Fuzzi CLI v${VERSION}`, marginBottom: 0 });
1599
+ ];
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)) {
1608
+ 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
+ ""
1613
+ );
1614
+ } else {
1615
+ 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
+ ""
1619
+ );
1620
+ }
1621
+ lines.push(
1622
+ muted("CI usage: "),
1623
+ muted("fuzzi scan <url> --fail-on critical --format json")
1624
+ );
1625
+ return lines.join("\n");
1626
+ }
1627
+ 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."));
1638
+ }
1639
+ return lines.join("\n");
1640
+ }
1641
+ 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
+ });
1557
1649
  }
1558
1650
  function renderChangelog(entries) {
1559
1651
  if (!entries.length) return muted("No changelog entries.");
@@ -1582,6 +1674,9 @@ function emptyState(title, hint, action) {
1582
1674
  return lines.join("\n");
1583
1675
  }
1584
1676
 
1677
+ // src/commands/keys.ts
1678
+ init_brand();
1679
+
1585
1680
  // src/terminal/interactive.ts
1586
1681
  init_theme();
1587
1682
  import { select, search } from "@inquirer/prompts";
@@ -1616,7 +1711,7 @@ async function runKeysListCommand(client) {
1616
1711
  const data = await client.get("/keys");
1617
1712
  const keys = data.results || [];
1618
1713
  if (!keys.length) {
1619
- return emptyState("No API keys", "Create one at app.fuzzi.dev/settings/api-keys", "[n] new key in this view");
1714
+ return emptyState("No API keys", `Create one at ${SETTINGS_API_KEYS_URL}`, "[n] new key in this view");
1620
1715
  }
1621
1716
  const rows = keys.map((k) => [
1622
1717
  k.name,
@@ -2079,23 +2174,45 @@ async function tryGetProfile() {
2079
2174
  }
2080
2175
 
2081
2176
  // src/shell/auth-gate.ts
2177
+ init_brand();
2082
2178
  function renderAuthGate() {
2083
- const w = contentWidth();
2084
- const body = [
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!"),
2085
2183
  "",
2086
- centerBlock(accent(renderFuzziMark()), w),
2184
+ mark,
2087
2185
  "",
2088
- centerBlock(accentBold("Sign in to continue"), w),
2186
+ muted("Not connected"),
2187
+ info("Sign in to run scans"),
2089
2188
  "",
2090
- centerBlock(muted("The CLI needs your Fuzzi account to run scans."), w),
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"),
2091
2194
  "",
2092
- divider(),
2195
+ info("Press Enter to open your browser"),
2196
+ muted("and authorize the CLI."),
2093
2197
  "",
2094
- centerBlock(info("Press Enter to open your browser and sign in"), w),
2095
- centerBlock(muted("Or type /auth-key later to paste an API key instead"), w),
2096
- ""
2198
+ muted("A local server receives the callback"),
2199
+ muted(`from ${APP_HOST} automatically.`)
2097
2200
  ].join("\n");
2098
- return panel(body, { title: "Fuzzi CLI", marginBottom: 1 });
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
+ });
2099
2216
  }
2100
2217
  function waitForEnter() {
2101
2218
  return new Promise((resolve) => {
@@ -2122,7 +2239,7 @@ async function runAuthGate() {
2122
2239
  } catch (e) {
2123
2240
  progress.fail("Sign-in failed");
2124
2241
  console.log(muted(formatApiError(e)));
2125
- console.log(muted("You can still use /auth-key to paste an API key, or /auth to retry browser login."));
2242
+ console.log(muted("Use /auth-key to paste an API key, or /auth to retry."));
2126
2243
  return null;
2127
2244
  }
2128
2245
  }