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 +21 -61
- package/assets/changelog.json +20 -0
- package/dist/index.js +197 -80
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
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://
|
|
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
|
-
#
|
|
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
|
|
97
|
-
fuzzi
|
|
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
|
|
115
|
-
|
|
|
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
|
-
|
|
113
|
+
**Default API:** `https://fuzzi-ten.vercel.app/api`
|
|
132
114
|
|
|
133
115
|
```bash
|
|
134
116
|
fuzzi config set default_env staging
|
|
135
|
-
fuzzi
|
|
136
|
-
export
|
|
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
|
|
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
|
|
130
|
+
fuzzi scan https://staging.example.com --fail-on critical
|
|
150
131
|
```
|
|
151
132
|
|
|
152
133
|
---
|
|
153
134
|
|
|
154
|
-
## For web
|
|
135
|
+
## For web developers
|
|
155
136
|
|
|
156
|
-
|
|
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)**
|
|
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
|
-
|
|
171
|
-
|
|
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
|
package/assets/changelog.json
CHANGED
|
@@ -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.
|
|
31
|
-
APP_ORIGIN = "https://
|
|
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
|
|
226
|
+
return `API key has expired. Generate a new one at ${SETTINGS_API_KEYS_URL}`;
|
|
224
227
|
}
|
|
225
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
"
|
|
1462
|
-
"
|
|
1463
|
-
"
|
|
1464
|
-
"
|
|
1465
|
-
"
|
|
1466
|
-
"
|
|
1467
|
-
"
|
|
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
|
|
1512
|
-
|
|
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
|
|
1516
|
-
const
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
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
|
-
|
|
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
|
-
]
|
|
1556
|
-
|
|
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",
|
|
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
|
|
2084
|
-
const
|
|
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
|
-
|
|
2184
|
+
mark,
|
|
2087
2185
|
"",
|
|
2088
|
-
|
|
2186
|
+
muted("Not connected"),
|
|
2187
|
+
info("Sign in to run scans"),
|
|
2089
2188
|
"",
|
|
2090
|
-
|
|
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
|
-
|
|
2195
|
+
info("Press Enter to open your browser"),
|
|
2196
|
+
muted("and authorize the CLI."),
|
|
2093
2197
|
"",
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
""
|
|
2198
|
+
muted("A local server receives the callback"),
|
|
2199
|
+
muted(`from ${APP_HOST} automatically.`)
|
|
2097
2200
|
].join("\n");
|
|
2098
|
-
|
|
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("
|
|
2242
|
+
console.log(muted("Use /auth-key to paste an API key, or /auth to retry."));
|
|
2126
2243
|
return null;
|
|
2127
2244
|
}
|
|
2128
2245
|
}
|