fuzzi-cli 0.1.0 → 0.1.1
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 +10 -0
- package/dist/index.js +352 -81
- 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.1";
|
|
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,6 +438,9 @@ function success(text) {
|
|
|
434
438
|
function warn(text) {
|
|
435
439
|
return color("#F59E0B", chalk.yellow)(text);
|
|
436
440
|
}
|
|
441
|
+
function info(text) {
|
|
442
|
+
return color(BRAND.accent, chalk.cyan)(text);
|
|
443
|
+
}
|
|
437
444
|
var accent, accentBold, muted, bold, dim;
|
|
438
445
|
var init_theme = __esm({
|
|
439
446
|
"src/terminal/theme.ts"() {
|
|
@@ -448,14 +455,6 @@ var init_theme = __esm({
|
|
|
448
455
|
}
|
|
449
456
|
});
|
|
450
457
|
|
|
451
|
-
// src/lib/theme.ts
|
|
452
|
-
var init_theme2 = __esm({
|
|
453
|
-
"src/lib/theme.ts"() {
|
|
454
|
-
"use strict";
|
|
455
|
-
init_theme();
|
|
456
|
-
}
|
|
457
|
-
});
|
|
458
|
-
|
|
459
458
|
// src/terminal/table.ts
|
|
460
459
|
import Table from "cli-table3";
|
|
461
460
|
function createTable(headers, rows) {
|
|
@@ -510,33 +509,52 @@ var init_strings = __esm({
|
|
|
510
509
|
}
|
|
511
510
|
});
|
|
512
511
|
|
|
512
|
+
// src/terminal/width.ts
|
|
513
|
+
import { stdout as stdout2 } from "process";
|
|
514
|
+
function terminalWidth() {
|
|
515
|
+
resetCapabilities();
|
|
516
|
+
return Math.max(64, (stdout2.columns ?? 80) - 2);
|
|
517
|
+
}
|
|
518
|
+
function contentWidth() {
|
|
519
|
+
return terminalWidth() - 4;
|
|
520
|
+
}
|
|
521
|
+
var init_width = __esm({
|
|
522
|
+
"src/terminal/width.ts"() {
|
|
523
|
+
"use strict";
|
|
524
|
+
init_capabilities();
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
|
|
513
528
|
// src/terminal/layout.ts
|
|
514
529
|
import boxen from "boxen";
|
|
515
530
|
function panel(content, opts = {}) {
|
|
531
|
+
const width = opts.fullWidth !== false ? terminalWidth() : void 0;
|
|
516
532
|
return boxen(content, {
|
|
517
533
|
title: opts.title ? accentBold(opts.title) : void 0,
|
|
518
534
|
padding: opts.padding ?? 1,
|
|
519
535
|
margin: { top: 0, bottom: opts.marginBottom ?? 1, left: 0, right: 0 },
|
|
520
536
|
borderStyle: "round",
|
|
521
537
|
borderColor: getCapabilities().trueColor ? BRAND.accent : void 0,
|
|
522
|
-
titleAlignment: "left"
|
|
538
|
+
titleAlignment: "left",
|
|
539
|
+
width
|
|
523
540
|
});
|
|
524
541
|
}
|
|
525
542
|
function columns(left, right, leftWidth) {
|
|
526
|
-
const
|
|
543
|
+
const total = contentWidth();
|
|
544
|
+
const split = leftWidth ?? Math.floor(total * 0.48);
|
|
527
545
|
const leftLines = left.split("\n");
|
|
528
546
|
const rightLines = right.split("\n");
|
|
529
547
|
const rows = Math.max(leftLines.length, rightLines.length);
|
|
530
548
|
const out = [];
|
|
531
549
|
for (let i = 0; i < rows; i++) {
|
|
532
|
-
const l = padEndVisible(leftLines[i] ?? "",
|
|
550
|
+
const l = padEndVisible(leftLines[i] ?? "", split);
|
|
533
551
|
const r = rightLines[i] ?? "";
|
|
534
552
|
out.push(`${l} ${r}`);
|
|
535
553
|
}
|
|
536
554
|
return out.join("\n");
|
|
537
555
|
}
|
|
538
556
|
function divider(char = "\u2500", width) {
|
|
539
|
-
const w = width ??
|
|
557
|
+
const w = width ?? contentWidth();
|
|
540
558
|
return dim(char.repeat(Math.max(20, w)));
|
|
541
559
|
}
|
|
542
560
|
function statusBar(parts) {
|
|
@@ -547,6 +565,13 @@ function keyValue(rows, indent = 2) {
|
|
|
547
565
|
const maxKey = Math.max(...rows.map(([k]) => k.length), 4);
|
|
548
566
|
return rows.map(([k, v]) => `${pad}${muted(k.padEnd(maxKey))} ${v}`).join("\n");
|
|
549
567
|
}
|
|
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
|
+
}
|
|
550
575
|
var init_layout = __esm({
|
|
551
576
|
"src/terminal/layout.ts"() {
|
|
552
577
|
"use strict";
|
|
@@ -554,6 +579,15 @@ var init_layout = __esm({
|
|
|
554
579
|
init_theme();
|
|
555
580
|
init_capabilities();
|
|
556
581
|
init_strings();
|
|
582
|
+
init_width();
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
// src/lib/theme.ts
|
|
587
|
+
var init_theme2 = __esm({
|
|
588
|
+
"src/lib/theme.ts"() {
|
|
589
|
+
"use strict";
|
|
590
|
+
init_theme();
|
|
557
591
|
}
|
|
558
592
|
});
|
|
559
593
|
|
|
@@ -648,16 +682,125 @@ init_credentials();
|
|
|
648
682
|
init_api_client();
|
|
649
683
|
init_credentials();
|
|
650
684
|
init_config();
|
|
651
|
-
|
|
652
|
-
import { password, input } from "@inquirer/prompts";
|
|
685
|
+
init_theme();
|
|
686
|
+
import { password, input, confirm } from "@inquirer/prompts";
|
|
687
|
+
|
|
688
|
+
// src/lib/browser-auth.ts
|
|
689
|
+
init_config();
|
|
690
|
+
init_credentials();
|
|
691
|
+
init_api_client();
|
|
692
|
+
init_brand();
|
|
693
|
+
init_logger();
|
|
694
|
+
import { randomBytes } from "crypto";
|
|
695
|
+
import { createServer } from "http";
|
|
696
|
+
import { exec } from "child_process";
|
|
697
|
+
var TIMEOUT_MS = 5 * 60 * 1e3;
|
|
698
|
+
function generateState() {
|
|
699
|
+
return randomBytes(24).toString("base64url");
|
|
700
|
+
}
|
|
701
|
+
function openBrowser(url) {
|
|
702
|
+
const platform = process.platform;
|
|
703
|
+
const cmd = platform === "darwin" ? `open ${JSON.stringify(url)}` : platform === "win32" ? `start "" ${JSON.stringify(url)}` : `xdg-open ${JSON.stringify(url)}`;
|
|
704
|
+
exec(cmd, (err) => {
|
|
705
|
+
if (err) log.warn("could not open browser automatically", err.message);
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
function apiOrigin(apiUrl) {
|
|
709
|
+
return apiUrl.replace(/\/api\/?$/, "") || APP_ORIGIN;
|
|
710
|
+
}
|
|
711
|
+
async function runBrowserLogin() {
|
|
712
|
+
const config = await loadConfig();
|
|
713
|
+
const state = generateState();
|
|
714
|
+
const handoffToken = await new Promise((resolve, reject) => {
|
|
715
|
+
const server = createServer((req, res) => {
|
|
716
|
+
try {
|
|
717
|
+
const addr = server.address();
|
|
718
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
719
|
+
const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
|
|
720
|
+
if (url.pathname !== "/callback") {
|
|
721
|
+
res.writeHead(404);
|
|
722
|
+
res.end();
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const token = url.searchParams.get("token");
|
|
726
|
+
const returnedState = url.searchParams.get("state");
|
|
727
|
+
if (!token || returnedState !== state) {
|
|
728
|
+
res.writeHead(400);
|
|
729
|
+
res.end("Invalid callback");
|
|
730
|
+
reject(new ApiError("Invalid sign-in callback.", 400, "invalid_callback", void 0, 2));
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
734
|
+
res.end(`<!DOCTYPE html><html><body style="font-family:system-ui;text-align:center;padding:48px">
|
|
735
|
+
<h1>Signed in to Fuzzi CLI</h1><p>Return to your terminal.</p>
|
|
736
|
+
<script>setTimeout(()=>window.close(),1200)</script></body></html>`);
|
|
737
|
+
server.close();
|
|
738
|
+
resolve(token);
|
|
739
|
+
} catch (e) {
|
|
740
|
+
server.close();
|
|
741
|
+
reject(e);
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
const timer = setTimeout(() => {
|
|
745
|
+
server.close();
|
|
746
|
+
reject(new ApiError("Sign-in timed out after 5 minutes.", 408, "auth_timeout", void 0, 2));
|
|
747
|
+
}, TIMEOUT_MS);
|
|
748
|
+
server.listen(0, "127.0.0.1", () => {
|
|
749
|
+
const addr = server.address();
|
|
750
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
751
|
+
const loginUrl = `${apiOrigin(config.api_url)}/cli-auth?state=${encodeURIComponent(state)}&callback_port=${port}`;
|
|
752
|
+
openBrowser(loginUrl);
|
|
753
|
+
log.debug("browser auth", loginUrl);
|
|
754
|
+
});
|
|
755
|
+
server.on("error", (e) => {
|
|
756
|
+
clearTimeout(timer);
|
|
757
|
+
reject(e);
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
const client = new FuzziApiClient(config.api_url);
|
|
761
|
+
const handoff = await client.post("/cli/handoff", {
|
|
762
|
+
handoff_token: handoffToken,
|
|
763
|
+
state
|
|
764
|
+
});
|
|
765
|
+
if (!handoff.api_key) {
|
|
766
|
+
throw new ApiError("Sign-in failed: no API key returned.", 500, "handoff_failed", void 0, 2);
|
|
767
|
+
}
|
|
768
|
+
client.setToken(handoff.api_key);
|
|
769
|
+
const profile = await client.get("/me");
|
|
770
|
+
await saveCredentials({
|
|
771
|
+
api_key: handoff.api_key,
|
|
772
|
+
auth_method: "api_key",
|
|
773
|
+
key_prefix: handoff.prefix || profile.key_prefix || maskApiKey(handoff.api_key),
|
|
774
|
+
key_expires_at: handoff.expires_at || profile.key_expires_at || void 0,
|
|
775
|
+
email: profile.email,
|
|
776
|
+
full_name: profile.full_name || void 0,
|
|
777
|
+
saved_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
778
|
+
});
|
|
779
|
+
const name = profile.full_name || profile.email;
|
|
780
|
+
return { message: `Signed in as ${name}`, profile };
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// src/commands/auth.ts
|
|
653
784
|
async function runAuthLogin(opts = {}) {
|
|
785
|
+
if (opts.browser || opts.interactive !== false && !opts.apiKey && !opts.apiKeyOnly) {
|
|
786
|
+
try {
|
|
787
|
+
const result = await runBrowserLogin();
|
|
788
|
+
return success(result.message);
|
|
789
|
+
} catch (e) {
|
|
790
|
+
if (opts.browser) throw e;
|
|
791
|
+
if (opts.apiKeyOnly) throw e;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
return runApiKeyLogin(opts);
|
|
795
|
+
}
|
|
796
|
+
async function runApiKeyLogin(opts = {}) {
|
|
654
797
|
const config = await loadConfig();
|
|
655
798
|
const client = new FuzziApiClient(config.api_url);
|
|
656
799
|
let apiKey = opts.apiKey?.trim();
|
|
657
800
|
if (!apiKey) {
|
|
658
801
|
if (opts.interactive === false) {
|
|
659
802
|
throw new ApiError(
|
|
660
|
-
"No API key provided.
|
|
803
|
+
"No API key provided. Run fuzzi auth login or sign in via browser.",
|
|
661
804
|
401,
|
|
662
805
|
"missing_key",
|
|
663
806
|
void 0,
|
|
@@ -909,9 +1052,9 @@ function parseTomlSimple(raw) {
|
|
|
909
1052
|
}
|
|
910
1053
|
return config;
|
|
911
1054
|
}
|
|
912
|
-
async function loadProjectConfig(
|
|
913
|
-
const fuzzirc = join3(
|
|
914
|
-
const fuzzitoml = join3(
|
|
1055
|
+
async function loadProjectConfig(cwd4) {
|
|
1056
|
+
const fuzzirc = join3(cwd4, ".fuzzirc");
|
|
1057
|
+
const fuzzitoml = join3(cwd4, "fuzzi.toml");
|
|
915
1058
|
if (existsSync(fuzzirc)) {
|
|
916
1059
|
try {
|
|
917
1060
|
const raw = await readFile3(fuzzirc, "utf8");
|
|
@@ -1079,8 +1222,8 @@ async function runScanCommand(client, opts) {
|
|
|
1079
1222
|
log.debug("creating scan", { url: opts.url, env });
|
|
1080
1223
|
const created = await withRetry(() => client.post("/scan", body));
|
|
1081
1224
|
if (!shouldWait) {
|
|
1082
|
-
const
|
|
1083
|
-
return { output:
|
|
1225
|
+
const output3 = format === "json" ? JSON.stringify(created, null, 2) : [success("Scan started"), muted(`ID: ${created.scan_id}`), muted(created.message)].join("\n");
|
|
1226
|
+
return { output: output3, exitCode: 0 };
|
|
1084
1227
|
}
|
|
1085
1228
|
const host = hostnameFromUrl(opts.url);
|
|
1086
1229
|
const progress = opts.onProgress ? { update: opts.onProgress, stop: () => {
|
|
@@ -1189,9 +1332,17 @@ function exitWith(code) {
|
|
|
1189
1332
|
function buildProgram() {
|
|
1190
1333
|
const program = new Command("fuzzi").name("fuzzi").description("Fuzzi security scanner CLI \u2014 interactive shell and scriptable commands").version(VERSION);
|
|
1191
1334
|
const auth = program.command("auth").description("Authentication");
|
|
1192
|
-
auth.command("login").description("
|
|
1335
|
+
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
1336
|
try {
|
|
1194
|
-
|
|
1337
|
+
const useApiKey = !!opts.apiKey;
|
|
1338
|
+
console.log(
|
|
1339
|
+
await runAuthLogin({
|
|
1340
|
+
apiKey: opts.apiKey,
|
|
1341
|
+
interactive: !opts.apiKey,
|
|
1342
|
+
browser: !useApiKey,
|
|
1343
|
+
apiKeyOnly: useApiKey
|
|
1344
|
+
})
|
|
1345
|
+
);
|
|
1195
1346
|
} catch (e) {
|
|
1196
1347
|
handleCommandError(e);
|
|
1197
1348
|
}
|
|
@@ -1300,11 +1451,6 @@ function buildProgram() {
|
|
|
1300
1451
|
return program;
|
|
1301
1452
|
}
|
|
1302
1453
|
|
|
1303
|
-
// src/cli/bootstrap.ts
|
|
1304
|
-
init_credentials();
|
|
1305
|
-
init_api_client();
|
|
1306
|
-
import { cwd as cwd4 } from "process";
|
|
1307
|
-
|
|
1308
1454
|
// src/shell/prompt-loop.ts
|
|
1309
1455
|
import * as readline from "readline/promises";
|
|
1310
1456
|
import { stdin as input3, stdout as output } from "process";
|
|
@@ -1326,6 +1472,7 @@ function renderFuzziMark() {
|
|
|
1326
1472
|
init_brand();
|
|
1327
1473
|
init_theme();
|
|
1328
1474
|
init_layout();
|
|
1475
|
+
init_width();
|
|
1329
1476
|
|
|
1330
1477
|
// src/lib/assets.ts
|
|
1331
1478
|
import { readFile as readFile4 } from "fs/promises";
|
|
@@ -1352,33 +1499,61 @@ async function readAsset(name) {
|
|
|
1352
1499
|
}
|
|
1353
1500
|
|
|
1354
1501
|
// src/shell/home-screen.ts
|
|
1355
|
-
async function fetchHomeData(profile,
|
|
1502
|
+
async function fetchHomeData(profile, cwd4) {
|
|
1356
1503
|
let changelog = [];
|
|
1357
1504
|
try {
|
|
1358
1505
|
changelog = JSON.parse(await readAsset("changelog.json"));
|
|
1359
1506
|
} catch {
|
|
1360
1507
|
changelog = [];
|
|
1361
1508
|
}
|
|
1362
|
-
return { profile, cwd:
|
|
1509
|
+
return { profile, cwd: cwd4, changelog };
|
|
1363
1510
|
}
|
|
1364
1511
|
function renderHomeScreen(data) {
|
|
1512
|
+
const w = contentWidth();
|
|
1365
1513
|
const name = data.profile?.full_name || data.profile?.email?.split("@")[0] || "there";
|
|
1366
1514
|
const org = data.profile?.organization?.trim();
|
|
1367
|
-
const welcome = data.profile ? accentBold(`Welcome back, ${name}!`) :
|
|
1368
|
-
const mark = accent(renderFuzziMark());
|
|
1369
|
-
const
|
|
1370
|
-
const
|
|
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);
|
|
1371
1519
|
const latest = data.changelog[0];
|
|
1372
|
-
const
|
|
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");
|
|
1373
1527
|
const quickActions = [
|
|
1374
|
-
|
|
1375
|
-
accent("/
|
|
1376
|
-
accent("/
|
|
1377
|
-
accent("/
|
|
1378
|
-
accent("/
|
|
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")
|
|
1379
1536
|
].join("\n");
|
|
1380
|
-
const
|
|
1381
|
-
|
|
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
|
+
"",
|
|
1553
|
+
centerBlock(footer, w),
|
|
1554
|
+
""
|
|
1555
|
+
].join("\n");
|
|
1556
|
+
return panel(body, { title: `Fuzzi CLI v${VERSION}`, marginBottom: 0 });
|
|
1382
1557
|
}
|
|
1383
1558
|
function renderChangelog(entries) {
|
|
1384
1559
|
if (!entries.length) return muted("No changelog entries.");
|
|
@@ -1393,7 +1568,7 @@ function renderChangelog(entries) {
|
|
|
1393
1568
|
|
|
1394
1569
|
// src/shell/slash-commands.ts
|
|
1395
1570
|
init_api_client();
|
|
1396
|
-
import { confirm, input as input2 } from "@inquirer/prompts";
|
|
1571
|
+
import { confirm as confirm2, input as input2 } from "@inquirer/prompts";
|
|
1397
1572
|
|
|
1398
1573
|
// src/commands/keys.ts
|
|
1399
1574
|
init_table();
|
|
@@ -1422,9 +1597,9 @@ async function searchPalette(message, choices) {
|
|
|
1422
1597
|
try {
|
|
1423
1598
|
return await search({
|
|
1424
1599
|
message,
|
|
1425
|
-
source: async (
|
|
1426
|
-
if (!
|
|
1427
|
-
const q =
|
|
1600
|
+
source: async (input5) => {
|
|
1601
|
+
if (!input5) return choices;
|
|
1602
|
+
const q = input5.toLowerCase();
|
|
1428
1603
|
return choices.filter((c) => c.value.includes(q) || c.description?.includes(q));
|
|
1429
1604
|
}
|
|
1430
1605
|
});
|
|
@@ -1482,13 +1657,14 @@ var SLASH_COMMANDS = [
|
|
|
1482
1657
|
{ name: "/palette", description: "Open command palette", aliases: ["/commands"] },
|
|
1483
1658
|
{ name: "/changelog", description: "View release notes" },
|
|
1484
1659
|
{ name: "/help", description: "Show all commands" },
|
|
1485
|
-
{ name: "/auth", description: "
|
|
1660
|
+
{ name: "/auth", description: "Sign in via browser", aliases: ["/login"] },
|
|
1661
|
+
{ name: "/auth-key", description: "Paste an API key instead", usage: "/auth-key" },
|
|
1486
1662
|
{ name: "/clear", description: "Clear screen and refresh home" },
|
|
1487
1663
|
{ name: "/history", description: "Show recent commands" },
|
|
1488
1664
|
{ name: "/exit", description: "Exit the shell", aliases: ["/quit"] }
|
|
1489
1665
|
];
|
|
1490
|
-
function findCommand(
|
|
1491
|
-
const cmd =
|
|
1666
|
+
function findCommand(input5) {
|
|
1667
|
+
const cmd = input5.trim().split(/\s/)[0].toLowerCase();
|
|
1492
1668
|
return SLASH_COMMANDS.find(
|
|
1493
1669
|
(c) => c.name === cmd || c.aliases?.some((a) => a === cmd)
|
|
1494
1670
|
);
|
|
@@ -1569,14 +1745,59 @@ function successBox(message) {
|
|
|
1569
1745
|
|
|
1570
1746
|
// src/shell/slash-commands.ts
|
|
1571
1747
|
init_strings();
|
|
1748
|
+
function normalizeInput(line) {
|
|
1749
|
+
let t = line.trim();
|
|
1750
|
+
if (!t || t.startsWith("/")) return t;
|
|
1751
|
+
if (t.toLowerCase().startsWith("fuzzi ")) t = t.slice(6).trim();
|
|
1752
|
+
const lower = t.toLowerCase();
|
|
1753
|
+
const aliases = {
|
|
1754
|
+
"auth login": "/auth",
|
|
1755
|
+
auth: "/auth",
|
|
1756
|
+
login: "/auth",
|
|
1757
|
+
"auth-key": "/auth-key",
|
|
1758
|
+
logout: "/exit",
|
|
1759
|
+
help: "/help",
|
|
1760
|
+
exit: "/exit",
|
|
1761
|
+
quit: "/exit",
|
|
1762
|
+
clear: "/clear",
|
|
1763
|
+
changelog: "/changelog",
|
|
1764
|
+
status: "/status",
|
|
1765
|
+
scans: "/scans",
|
|
1766
|
+
keys: "/keys",
|
|
1767
|
+
palette: "/palette"
|
|
1768
|
+
};
|
|
1769
|
+
if (aliases[lower]) return aliases[lower];
|
|
1770
|
+
if (lower.startsWith("scan ")) return `/scan ${t.slice(5).trim()}`;
|
|
1771
|
+
if (lower.startsWith("config set ")) {
|
|
1772
|
+
const parts = t.slice(11).trim().split(/\s+/);
|
|
1773
|
+
if (parts.length >= 2) return `/config ${parts[0]}=${parts.slice(1).join(" ")}`;
|
|
1774
|
+
}
|
|
1775
|
+
if (lower.startsWith("config ")) return `/config ${t.slice(7).trim().replace(/\s+/, "=")}`;
|
|
1776
|
+
if (!t.includes(" ") && t.includes(".")) return `/scan ${t}`;
|
|
1777
|
+
return t;
|
|
1778
|
+
}
|
|
1779
|
+
function normalizeScanUrl(url) {
|
|
1780
|
+
const u = url.trim();
|
|
1781
|
+
if (!/^https?:\/\//i.test(u)) return `https://${u}`;
|
|
1782
|
+
return u;
|
|
1783
|
+
}
|
|
1572
1784
|
async function dispatchSlashCommand(line, ctx) {
|
|
1573
1785
|
const trimmed = line.trim();
|
|
1574
1786
|
if (!trimmed) return {};
|
|
1575
1787
|
if (trimmed === "/exit" || trimmed === "/quit") return { exit: true };
|
|
1576
1788
|
const [cmd, ...rest] = trimmed.split(/\s+/);
|
|
1577
1789
|
const arg = rest.join(" ").trim();
|
|
1578
|
-
|
|
1579
|
-
|
|
1790
|
+
if (!cmd.startsWith("/")) {
|
|
1791
|
+
ctx.sink.write(
|
|
1792
|
+
errorBox(
|
|
1793
|
+
`Not a shell command: ${trimmed}`,
|
|
1794
|
+
`Use slash commands here \u2014 e.g. ${accent("/auth")} not "fuzzi auth login"
|
|
1795
|
+
${accent("/help")} lists everything`
|
|
1796
|
+
)
|
|
1797
|
+
);
|
|
1798
|
+
return {};
|
|
1799
|
+
}
|
|
1800
|
+
if (!findCommand(cmd) && cmd.startsWith("/")) {
|
|
1580
1801
|
ctx.sink.write(errorBox(`Unknown command: ${cmd}`, "Type /help or /palette"));
|
|
1581
1802
|
return {};
|
|
1582
1803
|
}
|
|
@@ -1606,9 +1827,10 @@ async function dispatchSlashCommand(line, ctx) {
|
|
|
1606
1827
|
const client = await getAuthenticatedClient();
|
|
1607
1828
|
const progress = createStreamProgress(ctx.sink);
|
|
1608
1829
|
const result = await runScanCommand(client, {
|
|
1609
|
-
url: arg,
|
|
1830
|
+
url: normalizeScanUrl(arg),
|
|
1610
1831
|
wait: true,
|
|
1611
|
-
onProgress: progress.update
|
|
1832
|
+
onProgress: progress.update,
|
|
1833
|
+
streamProgress: true
|
|
1612
1834
|
});
|
|
1613
1835
|
progress.stop();
|
|
1614
1836
|
ctx.sink.write(result.output);
|
|
@@ -1674,7 +1896,12 @@ ${muted("Rate limit")} ${rate}` : status);
|
|
|
1674
1896
|
}
|
|
1675
1897
|
case "/login":
|
|
1676
1898
|
case "/auth": {
|
|
1677
|
-
ctx.sink.write(await runAuthLogin({ interactive: true }));
|
|
1899
|
+
ctx.sink.write(await runAuthLogin({ interactive: true, browser: true }));
|
|
1900
|
+
const client = await getAuthenticatedClient();
|
|
1901
|
+
return { profile: await client.get("/me"), redraw: true };
|
|
1902
|
+
}
|
|
1903
|
+
case "/auth-key": {
|
|
1904
|
+
ctx.sink.write(await runApiKeyLogin({ interactive: true }));
|
|
1678
1905
|
const client = await getAuthenticatedClient();
|
|
1679
1906
|
return { profile: await client.get("/me"), redraw: true };
|
|
1680
1907
|
}
|
|
@@ -1737,7 +1964,7 @@ async function runKeysInteractive(ctx) {
|
|
|
1737
1964
|
if (action.toLowerCase() === "r") {
|
|
1738
1965
|
const keyId = await pickKeyForRevoke(client);
|
|
1739
1966
|
if (!keyId) return;
|
|
1740
|
-
const ok = await
|
|
1967
|
+
const ok = await confirm2({ message: "Revoke this API key?", default: false }).catch(() => false);
|
|
1741
1968
|
if (ok) ctx.sink.write(successBox(await runKeyRevoke(client, keyId)));
|
|
1742
1969
|
} else if (action.toLowerCase() === "n") {
|
|
1743
1970
|
const name = await promptNewKeyName();
|
|
@@ -1775,15 +2002,16 @@ async function runPromptLoop(initialProfile) {
|
|
|
1775
2002
|
const workDir = cwd3();
|
|
1776
2003
|
const history = await loadHistory();
|
|
1777
2004
|
const refresh = async () => {
|
|
1778
|
-
if (getCapabilities().interactive)
|
|
2005
|
+
if (getCapabilities().interactive) {
|
|
2006
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
2007
|
+
}
|
|
1779
2008
|
const data = await fetchHomeData(profile, workDir);
|
|
1780
2009
|
console.log(renderHomeScreen(data));
|
|
1781
|
-
|
|
2010
|
+
console.log(statusBar([
|
|
1782
2011
|
profile ? muted(profile.email) : muted("guest"),
|
|
1783
2012
|
dim(workDir),
|
|
1784
2013
|
isDebugMode() ? muted("debug") : null
|
|
1785
|
-
].filter(Boolean));
|
|
1786
|
-
console.log(bar);
|
|
2014
|
+
].filter(Boolean)));
|
|
1787
2015
|
console.log("");
|
|
1788
2016
|
};
|
|
1789
2017
|
await refresh();
|
|
@@ -1809,7 +2037,8 @@ async function runPromptLoop(initialProfile) {
|
|
|
1809
2037
|
error: (text) => console.error(text),
|
|
1810
2038
|
clearLine: () => process.stdout.write("\r\x1B[K")
|
|
1811
2039
|
};
|
|
1812
|
-
const
|
|
2040
|
+
const normalized = normalizeInput(line);
|
|
2041
|
+
const result = await dispatchSlashCommand(normalized, {
|
|
1813
2042
|
cwd: workDir,
|
|
1814
2043
|
profile,
|
|
1815
2044
|
sink,
|
|
@@ -1826,22 +2055,16 @@ async function runPromptLoop(initialProfile) {
|
|
|
1826
2055
|
}
|
|
1827
2056
|
}
|
|
1828
2057
|
|
|
1829
|
-
// src/shell/
|
|
2058
|
+
// src/shell/auth-gate.ts
|
|
2059
|
+
init_layout();
|
|
1830
2060
|
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
|
-
}
|
|
2061
|
+
init_width();
|
|
2062
|
+
import * as readline2 from "readline";
|
|
2063
|
+
import { stdin as input4, stdout as output2 } from "process";
|
|
1842
2064
|
|
|
1843
|
-
// src/cli/
|
|
1844
|
-
|
|
2065
|
+
// src/cli/profile.ts
|
|
2066
|
+
init_credentials();
|
|
2067
|
+
init_api_client();
|
|
1845
2068
|
init_logger();
|
|
1846
2069
|
async function tryGetProfile() {
|
|
1847
2070
|
try {
|
|
@@ -1854,13 +2077,61 @@ async function tryGetProfile() {
|
|
|
1854
2077
|
return null;
|
|
1855
2078
|
}
|
|
1856
2079
|
}
|
|
2080
|
+
|
|
2081
|
+
// src/shell/auth-gate.ts
|
|
2082
|
+
function renderAuthGate() {
|
|
2083
|
+
const w = contentWidth();
|
|
2084
|
+
const body = [
|
|
2085
|
+
"",
|
|
2086
|
+
centerBlock(accent(renderFuzziMark()), w),
|
|
2087
|
+
"",
|
|
2088
|
+
centerBlock(accentBold("Sign in to continue"), w),
|
|
2089
|
+
"",
|
|
2090
|
+
centerBlock(muted("The CLI needs your Fuzzi account to run scans."), w),
|
|
2091
|
+
"",
|
|
2092
|
+
divider(),
|
|
2093
|
+
"",
|
|
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
|
+
""
|
|
2097
|
+
].join("\n");
|
|
2098
|
+
return panel(body, { title: "Fuzzi CLI", marginBottom: 1 });
|
|
2099
|
+
}
|
|
2100
|
+
function waitForEnter() {
|
|
2101
|
+
return new Promise((resolve) => {
|
|
2102
|
+
const rl = readline2.createInterface({ input: input4, output: output2, terminal: true });
|
|
2103
|
+
output2.write(accent("\n \u203A Press Enter to open browser... "));
|
|
2104
|
+
rl.once("line", () => {
|
|
2105
|
+
rl.close();
|
|
2106
|
+
resolve();
|
|
2107
|
+
});
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2110
|
+
async function runAuthGate() {
|
|
2111
|
+
const existing = await tryGetProfile();
|
|
2112
|
+
if (existing) return existing;
|
|
2113
|
+
if (!output2.isTTY) return null;
|
|
2114
|
+
console.log(renderAuthGate());
|
|
2115
|
+
await waitForEnter();
|
|
2116
|
+
const progress = createProgress("Opening browser...");
|
|
2117
|
+
try {
|
|
2118
|
+
const result = await runBrowserLogin();
|
|
2119
|
+
progress.succeed("Signed in");
|
|
2120
|
+
console.log(accent(result.message));
|
|
2121
|
+
return result.profile;
|
|
2122
|
+
} catch (e) {
|
|
2123
|
+
progress.fail("Sign-in failed");
|
|
2124
|
+
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."));
|
|
2126
|
+
return null;
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
// src/cli/bootstrap.ts
|
|
1857
2131
|
async function runInteractiveMode() {
|
|
1858
|
-
|
|
1859
|
-
const workDir = cwd4();
|
|
1860
|
-
const data = await fetchHomeData(profile, workDir);
|
|
1861
|
-
console.log(renderHomeScreen(data));
|
|
2132
|
+
let profile = await tryGetProfile();
|
|
1862
2133
|
if (!profile) {
|
|
1863
|
-
|
|
2134
|
+
profile = await runAuthGate();
|
|
1864
2135
|
}
|
|
1865
2136
|
await runPromptLoop(profile);
|
|
1866
2137
|
}
|