fuzzi-cli 0.1.0 → 0.1.2
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 +143 -72
- package/assets/changelog.json +20 -0
- package/dist/index.js +472 -93
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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, DEFAULT_API_URL;
|
|
12
|
+
var BRAND, RISK_COLORS, VERSION, APP_ORIGIN, DEFAULT_API_URL;
|
|
13
13
|
var init_brand = __esm({
|
|
14
14
|
"src/types/brand.ts"() {
|
|
15
15
|
"use strict";
|
|
@@ -27,8 +27,9 @@ var init_brand = __esm({
|
|
|
27
27
|
HIGH: "#EF4444",
|
|
28
28
|
CRITICAL: "#A855F7"
|
|
29
29
|
};
|
|
30
|
-
VERSION = "0.1.
|
|
31
|
-
|
|
30
|
+
VERSION = "0.1.2";
|
|
31
|
+
APP_ORIGIN = "https://app.fuzzi.dev";
|
|
32
|
+
DEFAULT_API_URL = `${APP_ORIGIN}/api`;
|
|
32
33
|
}
|
|
33
34
|
});
|
|
34
35
|
|
|
@@ -396,6 +397,9 @@ function getCapabilities() {
|
|
|
396
397
|
};
|
|
397
398
|
return cached;
|
|
398
399
|
}
|
|
400
|
+
function resetCapabilities() {
|
|
401
|
+
cached = null;
|
|
402
|
+
}
|
|
399
403
|
var cached;
|
|
400
404
|
var init_capabilities = __esm({
|
|
401
405
|
"src/terminal/capabilities.ts"() {
|
|
@@ -434,7 +438,10 @@ function success(text) {
|
|
|
434
438
|
function warn(text) {
|
|
435
439
|
return color("#F59E0B", chalk.yellow)(text);
|
|
436
440
|
}
|
|
437
|
-
|
|
441
|
+
function info(text) {
|
|
442
|
+
return color(BRAND.accent, chalk.cyan)(text);
|
|
443
|
+
}
|
|
444
|
+
var accent, accentBold, muted, bold, dim, italic;
|
|
438
445
|
var init_theme = __esm({
|
|
439
446
|
"src/terminal/theme.ts"() {
|
|
440
447
|
"use strict";
|
|
@@ -445,14 +452,7 @@ var init_theme = __esm({
|
|
|
445
452
|
muted = color(BRAND.textSecondary, chalk.gray);
|
|
446
453
|
bold = chalk.bold;
|
|
447
454
|
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();
|
|
455
|
+
italic = chalk.italic;
|
|
456
456
|
}
|
|
457
457
|
});
|
|
458
458
|
|
|
@@ -510,33 +510,79 @@ var init_strings = __esm({
|
|
|
510
510
|
}
|
|
511
511
|
});
|
|
512
512
|
|
|
513
|
+
// src/terminal/width.ts
|
|
514
|
+
import { stdout as stdout2 } from "process";
|
|
515
|
+
function terminalWidth() {
|
|
516
|
+
resetCapabilities();
|
|
517
|
+
return Math.max(64, (stdout2.columns ?? 80) - 2);
|
|
518
|
+
}
|
|
519
|
+
function contentWidth() {
|
|
520
|
+
return terminalWidth() - 4;
|
|
521
|
+
}
|
|
522
|
+
var init_width = __esm({
|
|
523
|
+
"src/terminal/width.ts"() {
|
|
524
|
+
"use strict";
|
|
525
|
+
init_capabilities();
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
|
|
513
529
|
// src/terminal/layout.ts
|
|
514
530
|
import boxen from "boxen";
|
|
515
531
|
function panel(content, opts = {}) {
|
|
532
|
+
const width = opts.fullWidth !== false ? terminalWidth() : void 0;
|
|
516
533
|
return boxen(content, {
|
|
517
534
|
title: opts.title ? accentBold(opts.title) : void 0,
|
|
518
535
|
padding: opts.padding ?? 1,
|
|
519
536
|
margin: { top: 0, bottom: opts.marginBottom ?? 1, left: 0, right: 0 },
|
|
520
|
-
borderStyle: "
|
|
537
|
+
borderStyle: opts.borderStyle ?? "classic",
|
|
521
538
|
borderColor: getCapabilities().trueColor ? BRAND.accent : void 0,
|
|
522
|
-
titleAlignment: "left"
|
|
539
|
+
titleAlignment: "left",
|
|
540
|
+
width
|
|
523
541
|
});
|
|
524
542
|
}
|
|
543
|
+
function centerInColumn(text, colWidth) {
|
|
544
|
+
return text.split("\n").map((line) => {
|
|
545
|
+
const plain = line.replace(/\x1b\[[0-9;]*m/g, "");
|
|
546
|
+
const pad = Math.max(0, Math.floor((colWidth - plain.length) / 2));
|
|
547
|
+
return " ".repeat(pad) + line;
|
|
548
|
+
}).join("\n");
|
|
549
|
+
}
|
|
550
|
+
function splitHomePanel(opts) {
|
|
551
|
+
const total = contentWidth();
|
|
552
|
+
const leftW = Math.max(28, Math.floor(total * (opts.leftRatio ?? 0.34)));
|
|
553
|
+
const rightW = total - leftW - 3;
|
|
554
|
+
const leftLines = opts.left.split("\n");
|
|
555
|
+
const rightTop = opts.rightTop.split("\n");
|
|
556
|
+
const rightDiv = dim("\u2500".repeat(Math.max(10, rightW)));
|
|
557
|
+
const rightBottom = opts.rightBottom.split("\n");
|
|
558
|
+
const rightLines = [...rightTop, "", rightDiv, "", ...rightBottom];
|
|
559
|
+
const rows = Math.max(leftLines.length, rightLines.length);
|
|
560
|
+
const sep = dim("\u2502");
|
|
561
|
+
const body = [""];
|
|
562
|
+
for (let i = 0; i < rows; i++) {
|
|
563
|
+
const l = padEndVisible(leftLines[i] ?? "", leftW);
|
|
564
|
+
const r = rightLines[i] ?? "";
|
|
565
|
+
body.push(`${l} ${sep} ${r}`);
|
|
566
|
+
}
|
|
567
|
+
body.push("");
|
|
568
|
+
return panel(body.join("\n"), { title: opts.title, marginBottom: 0, borderStyle: "classic" });
|
|
569
|
+
}
|
|
525
570
|
function columns(left, right, leftWidth) {
|
|
526
|
-
const
|
|
571
|
+
const total = contentWidth();
|
|
572
|
+
const split = leftWidth ?? Math.floor(total * 0.48);
|
|
527
573
|
const leftLines = left.split("\n");
|
|
528
574
|
const rightLines = right.split("\n");
|
|
529
575
|
const rows = Math.max(leftLines.length, rightLines.length);
|
|
530
576
|
const out = [];
|
|
531
577
|
for (let i = 0; i < rows; i++) {
|
|
532
|
-
const l = padEndVisible(leftLines[i] ?? "",
|
|
578
|
+
const l = padEndVisible(leftLines[i] ?? "", split);
|
|
533
579
|
const r = rightLines[i] ?? "";
|
|
534
580
|
out.push(`${l} ${r}`);
|
|
535
581
|
}
|
|
536
582
|
return out.join("\n");
|
|
537
583
|
}
|
|
538
584
|
function divider(char = "\u2500", width) {
|
|
539
|
-
const w = width ??
|
|
585
|
+
const w = width ?? contentWidth();
|
|
540
586
|
return dim(char.repeat(Math.max(20, w)));
|
|
541
587
|
}
|
|
542
588
|
function statusBar(parts) {
|
|
@@ -554,6 +600,15 @@ var init_layout = __esm({
|
|
|
554
600
|
init_theme();
|
|
555
601
|
init_capabilities();
|
|
556
602
|
init_strings();
|
|
603
|
+
init_width();
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
// src/lib/theme.ts
|
|
608
|
+
var init_theme2 = __esm({
|
|
609
|
+
"src/lib/theme.ts"() {
|
|
610
|
+
"use strict";
|
|
611
|
+
init_theme();
|
|
557
612
|
}
|
|
558
613
|
});
|
|
559
614
|
|
|
@@ -648,16 +703,125 @@ init_credentials();
|
|
|
648
703
|
init_api_client();
|
|
649
704
|
init_credentials();
|
|
650
705
|
init_config();
|
|
651
|
-
|
|
652
|
-
import { password, input } from "@inquirer/prompts";
|
|
706
|
+
init_theme();
|
|
707
|
+
import { password, input, confirm } from "@inquirer/prompts";
|
|
708
|
+
|
|
709
|
+
// src/lib/browser-auth.ts
|
|
710
|
+
init_config();
|
|
711
|
+
init_credentials();
|
|
712
|
+
init_api_client();
|
|
713
|
+
init_brand();
|
|
714
|
+
init_logger();
|
|
715
|
+
import { randomBytes } from "crypto";
|
|
716
|
+
import { createServer } from "http";
|
|
717
|
+
import { exec } from "child_process";
|
|
718
|
+
var TIMEOUT_MS = 5 * 60 * 1e3;
|
|
719
|
+
function generateState() {
|
|
720
|
+
return randomBytes(24).toString("base64url");
|
|
721
|
+
}
|
|
722
|
+
function openBrowser(url) {
|
|
723
|
+
const platform = process.platform;
|
|
724
|
+
const cmd = platform === "darwin" ? `open ${JSON.stringify(url)}` : platform === "win32" ? `start "" ${JSON.stringify(url)}` : `xdg-open ${JSON.stringify(url)}`;
|
|
725
|
+
exec(cmd, (err) => {
|
|
726
|
+
if (err) log.warn("could not open browser automatically", err.message);
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
function apiOrigin(apiUrl) {
|
|
730
|
+
return apiUrl.replace(/\/api\/?$/, "") || APP_ORIGIN;
|
|
731
|
+
}
|
|
732
|
+
async function runBrowserLogin() {
|
|
733
|
+
const config = await loadConfig();
|
|
734
|
+
const state = generateState();
|
|
735
|
+
const handoffToken = await new Promise((resolve, reject) => {
|
|
736
|
+
const server = createServer((req, res) => {
|
|
737
|
+
try {
|
|
738
|
+
const addr = server.address();
|
|
739
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
740
|
+
const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
|
|
741
|
+
if (url.pathname !== "/callback") {
|
|
742
|
+
res.writeHead(404);
|
|
743
|
+
res.end();
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
const token = url.searchParams.get("token");
|
|
747
|
+
const returnedState = url.searchParams.get("state");
|
|
748
|
+
if (!token || returnedState !== state) {
|
|
749
|
+
res.writeHead(400);
|
|
750
|
+
res.end("Invalid callback");
|
|
751
|
+
reject(new ApiError("Invalid sign-in callback.", 400, "invalid_callback", void 0, 2));
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
755
|
+
res.end(`<!DOCTYPE html><html><body style="font-family:system-ui;text-align:center;padding:48px">
|
|
756
|
+
<h1>Signed in to Fuzzi CLI</h1><p>Return to your terminal.</p>
|
|
757
|
+
<script>setTimeout(()=>window.close(),1200)</script></body></html>`);
|
|
758
|
+
server.close();
|
|
759
|
+
resolve(token);
|
|
760
|
+
} catch (e) {
|
|
761
|
+
server.close();
|
|
762
|
+
reject(e);
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
const timer = setTimeout(() => {
|
|
766
|
+
server.close();
|
|
767
|
+
reject(new ApiError("Sign-in timed out after 5 minutes.", 408, "auth_timeout", void 0, 2));
|
|
768
|
+
}, TIMEOUT_MS);
|
|
769
|
+
server.listen(0, "127.0.0.1", () => {
|
|
770
|
+
const addr = server.address();
|
|
771
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
772
|
+
const loginUrl = `${apiOrigin(config.api_url)}/cli-auth?state=${encodeURIComponent(state)}&callback_port=${port}`;
|
|
773
|
+
openBrowser(loginUrl);
|
|
774
|
+
log.debug("browser auth", loginUrl);
|
|
775
|
+
});
|
|
776
|
+
server.on("error", (e) => {
|
|
777
|
+
clearTimeout(timer);
|
|
778
|
+
reject(e);
|
|
779
|
+
});
|
|
780
|
+
});
|
|
781
|
+
const client = new FuzziApiClient(config.api_url);
|
|
782
|
+
const handoff = await client.post("/cli/handoff", {
|
|
783
|
+
handoff_token: handoffToken,
|
|
784
|
+
state
|
|
785
|
+
});
|
|
786
|
+
if (!handoff.api_key) {
|
|
787
|
+
throw new ApiError("Sign-in failed: no API key returned.", 500, "handoff_failed", void 0, 2);
|
|
788
|
+
}
|
|
789
|
+
client.setToken(handoff.api_key);
|
|
790
|
+
const profile = await client.get("/me");
|
|
791
|
+
await saveCredentials({
|
|
792
|
+
api_key: handoff.api_key,
|
|
793
|
+
auth_method: "api_key",
|
|
794
|
+
key_prefix: handoff.prefix || profile.key_prefix || maskApiKey(handoff.api_key),
|
|
795
|
+
key_expires_at: handoff.expires_at || profile.key_expires_at || void 0,
|
|
796
|
+
email: profile.email,
|
|
797
|
+
full_name: profile.full_name || void 0,
|
|
798
|
+
saved_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
799
|
+
});
|
|
800
|
+
const name = profile.full_name || profile.email;
|
|
801
|
+
return { message: `Signed in as ${name}`, profile };
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// src/commands/auth.ts
|
|
653
805
|
async function runAuthLogin(opts = {}) {
|
|
806
|
+
if (opts.browser || opts.interactive !== false && !opts.apiKey && !opts.apiKeyOnly) {
|
|
807
|
+
try {
|
|
808
|
+
const result = await runBrowserLogin();
|
|
809
|
+
return success(result.message);
|
|
810
|
+
} catch (e) {
|
|
811
|
+
if (opts.browser) throw e;
|
|
812
|
+
if (opts.apiKeyOnly) throw e;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return runApiKeyLogin(opts);
|
|
816
|
+
}
|
|
817
|
+
async function runApiKeyLogin(opts = {}) {
|
|
654
818
|
const config = await loadConfig();
|
|
655
819
|
const client = new FuzziApiClient(config.api_url);
|
|
656
820
|
let apiKey = opts.apiKey?.trim();
|
|
657
821
|
if (!apiKey) {
|
|
658
822
|
if (opts.interactive === false) {
|
|
659
823
|
throw new ApiError(
|
|
660
|
-
"No API key provided.
|
|
824
|
+
"No API key provided. Run fuzzi auth login or sign in via browser.",
|
|
661
825
|
401,
|
|
662
826
|
"missing_key",
|
|
663
827
|
void 0,
|
|
@@ -909,9 +1073,9 @@ function parseTomlSimple(raw) {
|
|
|
909
1073
|
}
|
|
910
1074
|
return config;
|
|
911
1075
|
}
|
|
912
|
-
async function loadProjectConfig(
|
|
913
|
-
const fuzzirc = join3(
|
|
914
|
-
const fuzzitoml = join3(
|
|
1076
|
+
async function loadProjectConfig(cwd4) {
|
|
1077
|
+
const fuzzirc = join3(cwd4, ".fuzzirc");
|
|
1078
|
+
const fuzzitoml = join3(cwd4, "fuzzi.toml");
|
|
915
1079
|
if (existsSync(fuzzirc)) {
|
|
916
1080
|
try {
|
|
917
1081
|
const raw = await readFile3(fuzzirc, "utf8");
|
|
@@ -1079,8 +1243,8 @@ async function runScanCommand(client, opts) {
|
|
|
1079
1243
|
log.debug("creating scan", { url: opts.url, env });
|
|
1080
1244
|
const created = await withRetry(() => client.post("/scan", body));
|
|
1081
1245
|
if (!shouldWait) {
|
|
1082
|
-
const
|
|
1083
|
-
return { output:
|
|
1246
|
+
const output3 = format === "json" ? JSON.stringify(created, null, 2) : [success("Scan started"), muted(`ID: ${created.scan_id}`), muted(created.message)].join("\n");
|
|
1247
|
+
return { output: output3, exitCode: 0 };
|
|
1084
1248
|
}
|
|
1085
1249
|
const host = hostnameFromUrl(opts.url);
|
|
1086
1250
|
const progress = opts.onProgress ? { update: opts.onProgress, stop: () => {
|
|
@@ -1189,9 +1353,17 @@ function exitWith(code) {
|
|
|
1189
1353
|
function buildProgram() {
|
|
1190
1354
|
const program = new Command("fuzzi").name("fuzzi").description("Fuzzi security scanner CLI \u2014 interactive shell and scriptable commands").version(VERSION);
|
|
1191
1355
|
const auth = program.command("auth").description("Authentication");
|
|
1192
|
-
auth.command("login").description("
|
|
1356
|
+
auth.command("login").description("Sign in via browser (default) or API key").option("--browser", "Sign in via browser (default when interactive)").option("--api-key <key>", "Paste an API key (fz_live_...)").action(async (opts) => {
|
|
1193
1357
|
try {
|
|
1194
|
-
|
|
1358
|
+
const useApiKey = !!opts.apiKey;
|
|
1359
|
+
console.log(
|
|
1360
|
+
await runAuthLogin({
|
|
1361
|
+
apiKey: opts.apiKey,
|
|
1362
|
+
interactive: !opts.apiKey,
|
|
1363
|
+
browser: !useApiKey,
|
|
1364
|
+
apiKeyOnly: useApiKey
|
|
1365
|
+
})
|
|
1366
|
+
);
|
|
1195
1367
|
} catch (e) {
|
|
1196
1368
|
handleCommandError(e);
|
|
1197
1369
|
}
|
|
@@ -1300,25 +1472,27 @@ function buildProgram() {
|
|
|
1300
1472
|
return program;
|
|
1301
1473
|
}
|
|
1302
1474
|
|
|
1303
|
-
// src/cli/bootstrap.ts
|
|
1304
|
-
init_credentials();
|
|
1305
|
-
init_api_client();
|
|
1306
|
-
import { cwd as cwd4 } from "process";
|
|
1307
|
-
|
|
1308
1475
|
// src/shell/prompt-loop.ts
|
|
1309
1476
|
import * as readline from "readline/promises";
|
|
1310
1477
|
import { stdin as input3, stdout as output } from "process";
|
|
1311
1478
|
|
|
1479
|
+
// src/shell/home-screen.ts
|
|
1480
|
+
import { homedir as homedir3 } from "os";
|
|
1481
|
+
|
|
1312
1482
|
// src/shell/ascii-mark.ts
|
|
1313
1483
|
function renderFuzziMark() {
|
|
1314
1484
|
return [
|
|
1315
|
-
"
|
|
1316
|
-
"
|
|
1317
|
-
"
|
|
1318
|
-
"
|
|
1319
|
-
"
|
|
1320
|
-
"
|
|
1321
|
-
"
|
|
1485
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
|
|
1486
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
|
|
1487
|
+
" \u2588\u2588 \u2588\u2588",
|
|
1488
|
+
" \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588",
|
|
1489
|
+
" \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588",
|
|
1490
|
+
" \u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588",
|
|
1491
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
|
|
1492
|
+
" \u2588\u2588 \u2588\u2588",
|
|
1493
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
|
|
1494
|
+
" \u2588\u2588 \u2588\u2588",
|
|
1495
|
+
" \u2588\u2588 \u2588\u2588"
|
|
1322
1496
|
].join("\n");
|
|
1323
1497
|
}
|
|
1324
1498
|
|
|
@@ -1326,6 +1500,7 @@ function renderFuzziMark() {
|
|
|
1326
1500
|
init_brand();
|
|
1327
1501
|
init_theme();
|
|
1328
1502
|
init_layout();
|
|
1503
|
+
init_width();
|
|
1329
1504
|
|
|
1330
1505
|
// src/lib/assets.ts
|
|
1331
1506
|
import { readFile as readFile4 } from "fs/promises";
|
|
@@ -1352,33 +1527,119 @@ async function readAsset(name) {
|
|
|
1352
1527
|
}
|
|
1353
1528
|
|
|
1354
1529
|
// src/shell/home-screen.ts
|
|
1355
|
-
async function fetchHomeData(profile,
|
|
1530
|
+
async function fetchHomeData(profile, cwd4) {
|
|
1356
1531
|
let changelog = [];
|
|
1357
1532
|
try {
|
|
1358
1533
|
changelog = JSON.parse(await readAsset("changelog.json"));
|
|
1359
1534
|
} catch {
|
|
1360
1535
|
changelog = [];
|
|
1361
1536
|
}
|
|
1362
|
-
return { profile, cwd:
|
|
1537
|
+
return { profile, cwd: cwd4, changelog };
|
|
1363
1538
|
}
|
|
1364
|
-
function
|
|
1539
|
+
function isHomeDir(dir) {
|
|
1540
|
+
return dir === homedir3() || dir === homedir3().replace(/\/$/, "");
|
|
1541
|
+
}
|
|
1542
|
+
function renderLeftColumn(data) {
|
|
1543
|
+
const colW = Math.max(28, Math.floor(contentWidth() * 0.34));
|
|
1365
1544
|
const name = data.profile?.full_name || data.profile?.email?.split("@")[0] || "there";
|
|
1366
1545
|
const org = data.profile?.organization?.trim();
|
|
1367
|
-
const
|
|
1368
|
-
const
|
|
1369
|
-
|
|
1370
|
-
|
|
1546
|
+
const mark = centerInColumn(accent(renderFuzziMark()), colW);
|
|
1547
|
+
const lines = [];
|
|
1548
|
+
if (data.profile) {
|
|
1549
|
+
lines.push(
|
|
1550
|
+
accentBold(`Welcome back ${name}!`),
|
|
1551
|
+
"",
|
|
1552
|
+
mark,
|
|
1553
|
+
"",
|
|
1554
|
+
[accent("\u25CF Connected"), muted("\xB7"), info("API Key auth"), org ? muted("\xB7 " + org) : ""].filter(Boolean).join(" "),
|
|
1555
|
+
muted(data.profile.email),
|
|
1556
|
+
data.profile.role ? muted(`Role: ${data.profile.role}`) : "",
|
|
1557
|
+
"",
|
|
1558
|
+
muted(data.cwd),
|
|
1559
|
+
"",
|
|
1560
|
+
accent("/scan") + muted(" <url> scan a target"),
|
|
1561
|
+
accent("/scans") + muted(" browse history"),
|
|
1562
|
+
accent("/status") + muted(" account info"),
|
|
1563
|
+
accent("/keys") + muted(" manage keys"),
|
|
1564
|
+
accent("/palette") + muted(" find commands")
|
|
1565
|
+
);
|
|
1566
|
+
} else {
|
|
1567
|
+
lines.push(
|
|
1568
|
+
accentBold("Welcome to Fuzzi!"),
|
|
1569
|
+
"",
|
|
1570
|
+
mark,
|
|
1571
|
+
"",
|
|
1572
|
+
muted("Not connected"),
|
|
1573
|
+
info("Press Enter to sign in"),
|
|
1574
|
+
"",
|
|
1575
|
+
muted(data.cwd),
|
|
1576
|
+
"",
|
|
1577
|
+
accent("/auth") + muted(" browser sign-in"),
|
|
1578
|
+
accent("/auth-key") + muted(" paste API key"),
|
|
1579
|
+
accent("/scan") + muted(" <url> after login"),
|
|
1580
|
+
accent("/help") + muted(" all commands")
|
|
1581
|
+
);
|
|
1582
|
+
}
|
|
1583
|
+
return lines.filter((l) => l !== "").join("\n");
|
|
1584
|
+
}
|
|
1585
|
+
function renderTipsColumn(data) {
|
|
1586
|
+
const lines = [
|
|
1587
|
+
accentBold("Tips for getting started"),
|
|
1588
|
+
"",
|
|
1589
|
+
`Run ${accent("/scan")}${muted(" <url>")} to scan a site for security risks`,
|
|
1590
|
+
`Run ${accent("/palette")} to search every available command`,
|
|
1591
|
+
`Run ${accent("/help")} for the full command reference`,
|
|
1592
|
+
""
|
|
1593
|
+
];
|
|
1594
|
+
if (!data.profile) {
|
|
1595
|
+
lines.push(
|
|
1596
|
+
muted("Note: You launched without credentials."),
|
|
1597
|
+
muted("Press Enter at the prompt to open your browser,"),
|
|
1598
|
+
muted("or use /auth-key to paste an API key."),
|
|
1599
|
+
""
|
|
1600
|
+
);
|
|
1601
|
+
} else if (isHomeDir(data.cwd)) {
|
|
1602
|
+
lines.push(
|
|
1603
|
+
muted("Note: You launched fuzzi in your home directory."),
|
|
1604
|
+
muted("cd into a project folder first for better context,"),
|
|
1605
|
+
muted("or pass URLs directly: /scan https://example.com"),
|
|
1606
|
+
""
|
|
1607
|
+
);
|
|
1608
|
+
} else {
|
|
1609
|
+
lines.push(
|
|
1610
|
+
muted("Note: Add a .fuzzirc in this directory to set default"),
|
|
1611
|
+
muted("scan URL, environment, and output format for the team."),
|
|
1612
|
+
""
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
lines.push(
|
|
1616
|
+
muted("CI usage: "),
|
|
1617
|
+
muted("fuzzi scan <url> --fail-on critical --format json")
|
|
1618
|
+
);
|
|
1619
|
+
return lines.join("\n");
|
|
1620
|
+
}
|
|
1621
|
+
function renderWhatsNewColumn(data) {
|
|
1371
1622
|
const latest = data.changelog[0];
|
|
1372
|
-
const
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1623
|
+
const lines = [accentBold("What's new"), ""];
|
|
1624
|
+
if (latest) {
|
|
1625
|
+
for (const h of latest.highlights.slice(0, 4)) {
|
|
1626
|
+
lines.push(muted(h));
|
|
1627
|
+
}
|
|
1628
|
+
lines.push("");
|
|
1629
|
+
lines.push(italic(muted("/changelog for more")));
|
|
1630
|
+
} else {
|
|
1631
|
+
lines.push(muted("Stay tuned for updates."));
|
|
1632
|
+
}
|
|
1633
|
+
return lines.join("\n");
|
|
1634
|
+
}
|
|
1635
|
+
function renderHomeScreen(data) {
|
|
1636
|
+
return splitHomePanel({
|
|
1637
|
+
title: `Fuzzi CLI v${VERSION}`,
|
|
1638
|
+
left: renderLeftColumn(data),
|
|
1639
|
+
rightTop: renderTipsColumn(data),
|
|
1640
|
+
rightBottom: renderWhatsNewColumn(data),
|
|
1641
|
+
leftRatio: 0.36
|
|
1642
|
+
});
|
|
1382
1643
|
}
|
|
1383
1644
|
function renderChangelog(entries) {
|
|
1384
1645
|
if (!entries.length) return muted("No changelog entries.");
|
|
@@ -1393,7 +1654,7 @@ function renderChangelog(entries) {
|
|
|
1393
1654
|
|
|
1394
1655
|
// src/shell/slash-commands.ts
|
|
1395
1656
|
init_api_client();
|
|
1396
|
-
import { confirm, input as input2 } from "@inquirer/prompts";
|
|
1657
|
+
import { confirm as confirm2, input as input2 } from "@inquirer/prompts";
|
|
1397
1658
|
|
|
1398
1659
|
// src/commands/keys.ts
|
|
1399
1660
|
init_table();
|
|
@@ -1422,9 +1683,9 @@ async function searchPalette(message, choices) {
|
|
|
1422
1683
|
try {
|
|
1423
1684
|
return await search({
|
|
1424
1685
|
message,
|
|
1425
|
-
source: async (
|
|
1426
|
-
if (!
|
|
1427
|
-
const q =
|
|
1686
|
+
source: async (input5) => {
|
|
1687
|
+
if (!input5) return choices;
|
|
1688
|
+
const q = input5.toLowerCase();
|
|
1428
1689
|
return choices.filter((c) => c.value.includes(q) || c.description?.includes(q));
|
|
1429
1690
|
}
|
|
1430
1691
|
});
|
|
@@ -1482,13 +1743,14 @@ var SLASH_COMMANDS = [
|
|
|
1482
1743
|
{ name: "/palette", description: "Open command palette", aliases: ["/commands"] },
|
|
1483
1744
|
{ name: "/changelog", description: "View release notes" },
|
|
1484
1745
|
{ name: "/help", description: "Show all commands" },
|
|
1485
|
-
{ name: "/auth", description: "
|
|
1746
|
+
{ name: "/auth", description: "Sign in via browser", aliases: ["/login"] },
|
|
1747
|
+
{ name: "/auth-key", description: "Paste an API key instead", usage: "/auth-key" },
|
|
1486
1748
|
{ name: "/clear", description: "Clear screen and refresh home" },
|
|
1487
1749
|
{ name: "/history", description: "Show recent commands" },
|
|
1488
1750
|
{ name: "/exit", description: "Exit the shell", aliases: ["/quit"] }
|
|
1489
1751
|
];
|
|
1490
|
-
function findCommand(
|
|
1491
|
-
const cmd =
|
|
1752
|
+
function findCommand(input5) {
|
|
1753
|
+
const cmd = input5.trim().split(/\s/)[0].toLowerCase();
|
|
1492
1754
|
return SLASH_COMMANDS.find(
|
|
1493
1755
|
(c) => c.name === cmd || c.aliases?.some((a) => a === cmd)
|
|
1494
1756
|
);
|
|
@@ -1569,14 +1831,59 @@ function successBox(message) {
|
|
|
1569
1831
|
|
|
1570
1832
|
// src/shell/slash-commands.ts
|
|
1571
1833
|
init_strings();
|
|
1834
|
+
function normalizeInput(line) {
|
|
1835
|
+
let t = line.trim();
|
|
1836
|
+
if (!t || t.startsWith("/")) return t;
|
|
1837
|
+
if (t.toLowerCase().startsWith("fuzzi ")) t = t.slice(6).trim();
|
|
1838
|
+
const lower = t.toLowerCase();
|
|
1839
|
+
const aliases = {
|
|
1840
|
+
"auth login": "/auth",
|
|
1841
|
+
auth: "/auth",
|
|
1842
|
+
login: "/auth",
|
|
1843
|
+
"auth-key": "/auth-key",
|
|
1844
|
+
logout: "/exit",
|
|
1845
|
+
help: "/help",
|
|
1846
|
+
exit: "/exit",
|
|
1847
|
+
quit: "/exit",
|
|
1848
|
+
clear: "/clear",
|
|
1849
|
+
changelog: "/changelog",
|
|
1850
|
+
status: "/status",
|
|
1851
|
+
scans: "/scans",
|
|
1852
|
+
keys: "/keys",
|
|
1853
|
+
palette: "/palette"
|
|
1854
|
+
};
|
|
1855
|
+
if (aliases[lower]) return aliases[lower];
|
|
1856
|
+
if (lower.startsWith("scan ")) return `/scan ${t.slice(5).trim()}`;
|
|
1857
|
+
if (lower.startsWith("config set ")) {
|
|
1858
|
+
const parts = t.slice(11).trim().split(/\s+/);
|
|
1859
|
+
if (parts.length >= 2) return `/config ${parts[0]}=${parts.slice(1).join(" ")}`;
|
|
1860
|
+
}
|
|
1861
|
+
if (lower.startsWith("config ")) return `/config ${t.slice(7).trim().replace(/\s+/, "=")}`;
|
|
1862
|
+
if (!t.includes(" ") && t.includes(".")) return `/scan ${t}`;
|
|
1863
|
+
return t;
|
|
1864
|
+
}
|
|
1865
|
+
function normalizeScanUrl(url) {
|
|
1866
|
+
const u = url.trim();
|
|
1867
|
+
if (!/^https?:\/\//i.test(u)) return `https://${u}`;
|
|
1868
|
+
return u;
|
|
1869
|
+
}
|
|
1572
1870
|
async function dispatchSlashCommand(line, ctx) {
|
|
1573
1871
|
const trimmed = line.trim();
|
|
1574
1872
|
if (!trimmed) return {};
|
|
1575
1873
|
if (trimmed === "/exit" || trimmed === "/quit") return { exit: true };
|
|
1576
1874
|
const [cmd, ...rest] = trimmed.split(/\s+/);
|
|
1577
1875
|
const arg = rest.join(" ").trim();
|
|
1578
|
-
|
|
1579
|
-
|
|
1876
|
+
if (!cmd.startsWith("/")) {
|
|
1877
|
+
ctx.sink.write(
|
|
1878
|
+
errorBox(
|
|
1879
|
+
`Not a shell command: ${trimmed}`,
|
|
1880
|
+
`Use slash commands here \u2014 e.g. ${accent("/auth")} not "fuzzi auth login"
|
|
1881
|
+
${accent("/help")} lists everything`
|
|
1882
|
+
)
|
|
1883
|
+
);
|
|
1884
|
+
return {};
|
|
1885
|
+
}
|
|
1886
|
+
if (!findCommand(cmd) && cmd.startsWith("/")) {
|
|
1580
1887
|
ctx.sink.write(errorBox(`Unknown command: ${cmd}`, "Type /help or /palette"));
|
|
1581
1888
|
return {};
|
|
1582
1889
|
}
|
|
@@ -1606,9 +1913,10 @@ async function dispatchSlashCommand(line, ctx) {
|
|
|
1606
1913
|
const client = await getAuthenticatedClient();
|
|
1607
1914
|
const progress = createStreamProgress(ctx.sink);
|
|
1608
1915
|
const result = await runScanCommand(client, {
|
|
1609
|
-
url: arg,
|
|
1916
|
+
url: normalizeScanUrl(arg),
|
|
1610
1917
|
wait: true,
|
|
1611
|
-
onProgress: progress.update
|
|
1918
|
+
onProgress: progress.update,
|
|
1919
|
+
streamProgress: true
|
|
1612
1920
|
});
|
|
1613
1921
|
progress.stop();
|
|
1614
1922
|
ctx.sink.write(result.output);
|
|
@@ -1674,7 +1982,12 @@ ${muted("Rate limit")} ${rate}` : status);
|
|
|
1674
1982
|
}
|
|
1675
1983
|
case "/login":
|
|
1676
1984
|
case "/auth": {
|
|
1677
|
-
ctx.sink.write(await runAuthLogin({ interactive: true }));
|
|
1985
|
+
ctx.sink.write(await runAuthLogin({ interactive: true, browser: true }));
|
|
1986
|
+
const client = await getAuthenticatedClient();
|
|
1987
|
+
return { profile: await client.get("/me"), redraw: true };
|
|
1988
|
+
}
|
|
1989
|
+
case "/auth-key": {
|
|
1990
|
+
ctx.sink.write(await runApiKeyLogin({ interactive: true }));
|
|
1678
1991
|
const client = await getAuthenticatedClient();
|
|
1679
1992
|
return { profile: await client.get("/me"), redraw: true };
|
|
1680
1993
|
}
|
|
@@ -1737,7 +2050,7 @@ async function runKeysInteractive(ctx) {
|
|
|
1737
2050
|
if (action.toLowerCase() === "r") {
|
|
1738
2051
|
const keyId = await pickKeyForRevoke(client);
|
|
1739
2052
|
if (!keyId) return;
|
|
1740
|
-
const ok = await
|
|
2053
|
+
const ok = await confirm2({ message: "Revoke this API key?", default: false }).catch(() => false);
|
|
1741
2054
|
if (ok) ctx.sink.write(successBox(await runKeyRevoke(client, keyId)));
|
|
1742
2055
|
} else if (action.toLowerCase() === "n") {
|
|
1743
2056
|
const name = await promptNewKeyName();
|
|
@@ -1775,15 +2088,16 @@ async function runPromptLoop(initialProfile) {
|
|
|
1775
2088
|
const workDir = cwd3();
|
|
1776
2089
|
const history = await loadHistory();
|
|
1777
2090
|
const refresh = async () => {
|
|
1778
|
-
if (getCapabilities().interactive)
|
|
2091
|
+
if (getCapabilities().interactive) {
|
|
2092
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
2093
|
+
}
|
|
1779
2094
|
const data = await fetchHomeData(profile, workDir);
|
|
1780
2095
|
console.log(renderHomeScreen(data));
|
|
1781
|
-
|
|
2096
|
+
console.log(statusBar([
|
|
1782
2097
|
profile ? muted(profile.email) : muted("guest"),
|
|
1783
2098
|
dim(workDir),
|
|
1784
2099
|
isDebugMode() ? muted("debug") : null
|
|
1785
|
-
].filter(Boolean));
|
|
1786
|
-
console.log(bar);
|
|
2100
|
+
].filter(Boolean)));
|
|
1787
2101
|
console.log("");
|
|
1788
2102
|
};
|
|
1789
2103
|
await refresh();
|
|
@@ -1809,7 +2123,8 @@ async function runPromptLoop(initialProfile) {
|
|
|
1809
2123
|
error: (text) => console.error(text),
|
|
1810
2124
|
clearLine: () => process.stdout.write("\r\x1B[K")
|
|
1811
2125
|
};
|
|
1812
|
-
const
|
|
2126
|
+
const normalized = normalizeInput(line);
|
|
2127
|
+
const result = await dispatchSlashCommand(normalized, {
|
|
1813
2128
|
cwd: workDir,
|
|
1814
2129
|
profile,
|
|
1815
2130
|
sink,
|
|
@@ -1826,22 +2141,16 @@ async function runPromptLoop(initialProfile) {
|
|
|
1826
2141
|
}
|
|
1827
2142
|
}
|
|
1828
2143
|
|
|
1829
|
-
// src/shell/
|
|
2144
|
+
// src/shell/auth-gate.ts
|
|
2145
|
+
init_layout();
|
|
1830
2146
|
init_theme();
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
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
|
-
}
|
|
2147
|
+
init_width();
|
|
2148
|
+
import * as readline2 from "readline";
|
|
2149
|
+
import { stdin as input4, stdout as output2 } from "process";
|
|
1842
2150
|
|
|
1843
|
-
// src/cli/
|
|
1844
|
-
|
|
2151
|
+
// src/cli/profile.ts
|
|
2152
|
+
init_credentials();
|
|
2153
|
+
init_api_client();
|
|
1845
2154
|
init_logger();
|
|
1846
2155
|
async function tryGetProfile() {
|
|
1847
2156
|
try {
|
|
@@ -1854,13 +2163,83 @@ async function tryGetProfile() {
|
|
|
1854
2163
|
return null;
|
|
1855
2164
|
}
|
|
1856
2165
|
}
|
|
2166
|
+
|
|
2167
|
+
// src/shell/auth-gate.ts
|
|
2168
|
+
init_brand();
|
|
2169
|
+
function renderAuthGate() {
|
|
2170
|
+
const colW = Math.max(28, Math.floor(contentWidth() * 0.36));
|
|
2171
|
+
const mark = centerInColumn(accent(renderFuzziMark()), colW);
|
|
2172
|
+
const left = [
|
|
2173
|
+
accentBold("Welcome to Fuzzi!"),
|
|
2174
|
+
"",
|
|
2175
|
+
mark,
|
|
2176
|
+
"",
|
|
2177
|
+
muted("Not connected"),
|
|
2178
|
+
info("Sign in to run scans"),
|
|
2179
|
+
"",
|
|
2180
|
+
accent("/auth-key") + muted(" paste API key"),
|
|
2181
|
+
accent("/help") + muted(" commands")
|
|
2182
|
+
].join("\n");
|
|
2183
|
+
const rightTop = [
|
|
2184
|
+
accentBold("Sign in to continue"),
|
|
2185
|
+
"",
|
|
2186
|
+
info("Press Enter to open your browser"),
|
|
2187
|
+
muted("and authorize the CLI."),
|
|
2188
|
+
"",
|
|
2189
|
+
muted("A local server receives the callback"),
|
|
2190
|
+
muted("from app.fuzzi.dev automatically.")
|
|
2191
|
+
].join("\n");
|
|
2192
|
+
const rightBottom = [
|
|
2193
|
+
accentBold("Other options"),
|
|
2194
|
+
"",
|
|
2195
|
+
muted("Paste an API key with /auth-key"),
|
|
2196
|
+
muted("from Settings \u2192 API Keys on the web."),
|
|
2197
|
+
"",
|
|
2198
|
+
italic(muted("docs: app.fuzzi.dev/settings/api-keys"))
|
|
2199
|
+
].join("\n");
|
|
2200
|
+
return splitHomePanel({
|
|
2201
|
+
title: `Fuzzi CLI v${VERSION}`,
|
|
2202
|
+
left,
|
|
2203
|
+
rightTop,
|
|
2204
|
+
rightBottom,
|
|
2205
|
+
leftRatio: 0.36
|
|
2206
|
+
});
|
|
2207
|
+
}
|
|
2208
|
+
function waitForEnter() {
|
|
2209
|
+
return new Promise((resolve) => {
|
|
2210
|
+
const rl = readline2.createInterface({ input: input4, output: output2, terminal: true });
|
|
2211
|
+
output2.write(accent("\n \u203A Press Enter to open browser... "));
|
|
2212
|
+
rl.once("line", () => {
|
|
2213
|
+
rl.close();
|
|
2214
|
+
resolve();
|
|
2215
|
+
});
|
|
2216
|
+
});
|
|
2217
|
+
}
|
|
2218
|
+
async function runAuthGate() {
|
|
2219
|
+
const existing = await tryGetProfile();
|
|
2220
|
+
if (existing) return existing;
|
|
2221
|
+
if (!output2.isTTY) return null;
|
|
2222
|
+
console.log(renderAuthGate());
|
|
2223
|
+
await waitForEnter();
|
|
2224
|
+
const progress = createProgress("Opening browser...");
|
|
2225
|
+
try {
|
|
2226
|
+
const result = await runBrowserLogin();
|
|
2227
|
+
progress.succeed("Signed in");
|
|
2228
|
+
console.log(accent(result.message));
|
|
2229
|
+
return result.profile;
|
|
2230
|
+
} catch (e) {
|
|
2231
|
+
progress.fail("Sign-in failed");
|
|
2232
|
+
console.log(muted(formatApiError(e)));
|
|
2233
|
+
console.log(muted("Use /auth-key to paste an API key, or /auth to retry."));
|
|
2234
|
+
return null;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
// src/cli/bootstrap.ts
|
|
1857
2239
|
async function runInteractiveMode() {
|
|
1858
|
-
|
|
1859
|
-
const workDir = cwd4();
|
|
1860
|
-
const data = await fetchHomeData(profile, workDir);
|
|
1861
|
-
console.log(renderHomeScreen(data));
|
|
2240
|
+
let profile = await tryGetProfile();
|
|
1862
2241
|
if (!profile) {
|
|
1863
|
-
|
|
2242
|
+
profile = await runAuthGate();
|
|
1864
2243
|
}
|
|
1865
2244
|
await runPromptLoop(profile);
|
|
1866
2245
|
}
|