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