fuzzi-cli 0.1.2 → 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/README.md +21 -61
- package/assets/changelog.json +24 -4
- package/dist/index.js +311 -330
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -9,27 +9,38 @@ var __export = (target, all) => {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
// src/types/brand.ts
|
|
12
|
-
var BRAND, RISK_COLORS, VERSION, APP_ORIGIN, DEFAULT_API_URL;
|
|
12
|
+
var BRAND, RISK_COLORS, VERSION, APP_ORIGIN, DEFAULT_API_URL, SETTINGS_API_KEYS_URL, CLI_AUTH_URL, APP_HOST;
|
|
13
13
|
var init_brand = __esm({
|
|
14
14
|
"src/types/brand.ts"() {
|
|
15
15
|
"use strict";
|
|
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.
|
|
31
|
-
APP_ORIGIN = "https://
|
|
38
|
+
VERSION = "0.1.4";
|
|
39
|
+
APP_ORIGIN = "https://fuzzi-ten.vercel.app";
|
|
32
40
|
DEFAULT_API_URL = `${APP_ORIGIN}/api`;
|
|
41
|
+
SETTINGS_API_KEYS_URL = `${APP_ORIGIN}/settings/api-keys`;
|
|
42
|
+
CLI_AUTH_URL = `${APP_ORIGIN}/cli-auth`;
|
|
43
|
+
APP_HOST = "fuzzi-ten.vercel.app";
|
|
33
44
|
}
|
|
34
45
|
});
|
|
35
46
|
|
|
@@ -186,9 +197,6 @@ function shouldLog(level) {
|
|
|
186
197
|
function stamp() {
|
|
187
198
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
188
199
|
}
|
|
189
|
-
function isDebugMode() {
|
|
190
|
-
return currentLevel() === "debug";
|
|
191
|
-
}
|
|
192
200
|
var LEVELS, log;
|
|
193
201
|
var init_logger = __esm({
|
|
194
202
|
"src/lib/logger.ts"() {
|
|
@@ -220,9 +228,9 @@ function mapErrorMessage(status, body) {
|
|
|
220
228
|
return "API key has been revoked. Please log in again.";
|
|
221
229
|
}
|
|
222
230
|
if (code === "key_expired" || msg.toLowerCase().includes("expired")) {
|
|
223
|
-
return
|
|
231
|
+
return `API key has expired. Generate a new one at ${SETTINGS_API_KEYS_URL}`;
|
|
224
232
|
}
|
|
225
|
-
return
|
|
233
|
+
return `Invalid API key. Generate a new one at ${SETTINGS_API_KEYS_URL}`;
|
|
226
234
|
}
|
|
227
235
|
if (status === 403 && (code === "ssrf" || msg.toLowerCase().includes("private ip"))) {
|
|
228
236
|
return "This URL is not allowed (private IP address detected). Please scan a public-facing URL.";
|
|
@@ -253,6 +261,7 @@ var init_api_client = __esm({
|
|
|
253
261
|
init_config();
|
|
254
262
|
init_credentials();
|
|
255
263
|
init_logger();
|
|
264
|
+
init_brand();
|
|
256
265
|
ApiError = class extends Error {
|
|
257
266
|
constructor(message, status, code, body, exitCode) {
|
|
258
267
|
super(message);
|
|
@@ -302,7 +311,7 @@ var init_api_client = __esm({
|
|
|
302
311
|
});
|
|
303
312
|
} catch {
|
|
304
313
|
throw new ApiError(
|
|
305
|
-
|
|
314
|
+
`Could not connect to ${APP_HOST}. Check your internet connection or try again later.`,
|
|
306
315
|
0,
|
|
307
316
|
"network_error",
|
|
308
317
|
void 0,
|
|
@@ -358,7 +367,7 @@ var init_api_client = __esm({
|
|
|
358
367
|
res = await fetch(url, { headers: this.headers() });
|
|
359
368
|
} catch {
|
|
360
369
|
throw new ApiError(
|
|
361
|
-
|
|
370
|
+
`Could not connect to ${APP_HOST}. Check your internet connection or try again later.`,
|
|
362
371
|
0,
|
|
363
372
|
"network_error",
|
|
364
373
|
void 0,
|
|
@@ -391,7 +400,7 @@ function getCapabilities() {
|
|
|
391
400
|
const colorterm = process.env.COLORTERM ?? "";
|
|
392
401
|
const trueColor = colorterm.includes("truecolor") || colorterm.includes("24bit") || term.includes("truecolor") || !!process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0";
|
|
393
402
|
cached = {
|
|
394
|
-
width: Math.max(60,
|
|
403
|
+
width: Math.max(60, cols),
|
|
395
404
|
trueColor,
|
|
396
405
|
interactive: stdout.isTTY === true
|
|
397
406
|
};
|
|
@@ -430,18 +439,18 @@ function scoreBold(n) {
|
|
|
430
439
|
return chalk.bold(String(n));
|
|
431
440
|
}
|
|
432
441
|
function error(text) {
|
|
433
|
-
return color(
|
|
442
|
+
return color(BRAND.danger, chalk.red)(text);
|
|
434
443
|
}
|
|
435
444
|
function success(text) {
|
|
436
|
-
return color(
|
|
445
|
+
return color(BRAND.success, chalk.green)(text);
|
|
437
446
|
}
|
|
438
447
|
function warn(text) {
|
|
439
|
-
return color(
|
|
448
|
+
return color(BRAND.warning, chalk.yellow)(text);
|
|
440
449
|
}
|
|
441
|
-
function
|
|
442
|
-
return
|
|
450
|
+
function cmd(text) {
|
|
451
|
+
return accent(text);
|
|
443
452
|
}
|
|
444
|
-
var accent, accentBold, muted, bold, dim, italic;
|
|
453
|
+
var accent, accentBold, primary, primaryBold, muted, dimText, border, bold, dim, italic;
|
|
445
454
|
var init_theme = __esm({
|
|
446
455
|
"src/terminal/theme.ts"() {
|
|
447
456
|
"use strict";
|
|
@@ -449,7 +458,11 @@ var init_theme = __esm({
|
|
|
449
458
|
init_capabilities();
|
|
450
459
|
accent = color(BRAND.accent, chalk.cyan);
|
|
451
460
|
accentBold = accent.bold;
|
|
461
|
+
primary = color(BRAND.textPrimary, chalk.white);
|
|
462
|
+
primaryBold = primary.bold;
|
|
452
463
|
muted = color(BRAND.textSecondary, chalk.gray);
|
|
464
|
+
dimText = color(BRAND.textTertiary, chalk.dim);
|
|
465
|
+
border = color(BRAND.borderSubtle, chalk.gray);
|
|
453
466
|
bold = chalk.bold;
|
|
454
467
|
dim = chalk.dim;
|
|
455
468
|
italic = chalk.italic;
|
|
@@ -540,33 +553,6 @@ function panel(content, opts = {}) {
|
|
|
540
553
|
width
|
|
541
554
|
});
|
|
542
555
|
}
|
|
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
|
-
}
|
|
570
556
|
function columns(left, right, leftWidth) {
|
|
571
557
|
const total = contentWidth();
|
|
572
558
|
const split = leftWidth ?? Math.floor(total * 0.48);
|
|
@@ -585,9 +571,6 @@ function divider(char = "\u2500", width) {
|
|
|
585
571
|
const w = width ?? contentWidth();
|
|
586
572
|
return dim(char.repeat(Math.max(20, w)));
|
|
587
573
|
}
|
|
588
|
-
function statusBar(parts) {
|
|
589
|
-
return dim(parts.filter(Boolean).join(" \xB7 "));
|
|
590
|
-
}
|
|
591
574
|
function keyValue(rows, indent = 2) {
|
|
592
575
|
const pad = " ".repeat(indent);
|
|
593
576
|
const maxKey = Math.max(...rows.map(([k]) => k.length), 4);
|
|
@@ -715,102 +698,44 @@ init_logger();
|
|
|
715
698
|
import { randomBytes } from "crypto";
|
|
716
699
|
import { createServer } from "http";
|
|
717
700
|
import { exec } from "child_process";
|
|
718
|
-
var TIMEOUT_MS = 5 * 60 * 1e3;
|
|
719
|
-
function generateState() {
|
|
720
|
-
return randomBytes(24).toString("base64url");
|
|
721
|
-
}
|
|
722
701
|
function openBrowser(url) {
|
|
723
702
|
const platform = process.platform;
|
|
724
|
-
const
|
|
725
|
-
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) => {
|
|
726
705
|
if (err) log.warn("could not open browser automatically", err.message);
|
|
727
706
|
});
|
|
728
707
|
}
|
|
729
708
|
function apiOrigin(apiUrl) {
|
|
730
709
|
return apiUrl.replace(/\/api\/?$/, "") || APP_ORIGIN;
|
|
731
710
|
}
|
|
732
|
-
async function
|
|
711
|
+
async function openCliAuthPage() {
|
|
733
712
|
const config = await loadConfig();
|
|
734
|
-
const
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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 };
|
|
713
|
+
const url = `${apiOrigin(config.api_url)}/cli-auth`;
|
|
714
|
+
openBrowser(url);
|
|
715
|
+
log.debug("opened cli auth page", url);
|
|
716
|
+
return url;
|
|
802
717
|
}
|
|
803
718
|
|
|
804
719
|
// src/commands/auth.ts
|
|
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
|
+
}
|
|
805
732
|
async function runAuthLogin(opts = {}) {
|
|
806
|
-
if (opts.
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
if (opts.apiKeyOnly) throw e;
|
|
813
|
-
}
|
|
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;
|
|
814
739
|
}
|
|
815
740
|
return runApiKeyLogin(opts);
|
|
816
741
|
}
|
|
@@ -841,7 +766,7 @@ async function runApiKeyLogin(opts = {}) {
|
|
|
841
766
|
apiKey = apiKey.trim();
|
|
842
767
|
if (!isValidApiKeyFormat(apiKey)) {
|
|
843
768
|
throw new ApiError(
|
|
844
|
-
|
|
769
|
+
`Invalid API key format. Generate a new one at ${SETTINGS_API_KEYS_URL}`,
|
|
845
770
|
401,
|
|
846
771
|
"invalid_key_format",
|
|
847
772
|
void 0,
|
|
@@ -852,7 +777,7 @@ async function runApiKeyLogin(opts = {}) {
|
|
|
852
777
|
const valid = await client.validateToken();
|
|
853
778
|
if (!valid) {
|
|
854
779
|
throw new ApiError(
|
|
855
|
-
|
|
780
|
+
`Invalid API key. Generate a new one at ${SETTINGS_API_KEYS_URL}`,
|
|
856
781
|
401,
|
|
857
782
|
"invalid_token",
|
|
858
783
|
void 0,
|
|
@@ -993,13 +918,14 @@ init_theme();
|
|
|
993
918
|
|
|
994
919
|
// src/lib/errors.ts
|
|
995
920
|
init_api_client();
|
|
921
|
+
init_brand();
|
|
996
922
|
function formatApiError(err) {
|
|
997
923
|
if (err instanceof ApiError) {
|
|
998
924
|
return err.message;
|
|
999
925
|
}
|
|
1000
926
|
if (err instanceof Error) {
|
|
1001
927
|
if (err.message.includes("fetch failed") || err.message.includes("ECONNREFUSED")) {
|
|
1002
|
-
return
|
|
928
|
+
return `Could not connect to ${APP_HOST}. Check your internet connection or try again later.`;
|
|
1003
929
|
}
|
|
1004
930
|
return `An error occurred: ${err.message}. Please report this at https://github.com/fuzzi-cli/fuzzi-cli/issues`;
|
|
1005
931
|
}
|
|
@@ -1474,33 +1400,7 @@ function buildProgram() {
|
|
|
1474
1400
|
|
|
1475
1401
|
// src/shell/prompt-loop.ts
|
|
1476
1402
|
import * as readline from "readline/promises";
|
|
1477
|
-
import { stdin as input3, stdout as output } from "process";
|
|
1478
|
-
|
|
1479
|
-
// src/shell/home-screen.ts
|
|
1480
|
-
import { homedir as homedir3 } from "os";
|
|
1481
|
-
|
|
1482
|
-
// src/shell/ascii-mark.ts
|
|
1483
|
-
function renderFuzziMark() {
|
|
1484
|
-
return [
|
|
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"
|
|
1496
|
-
].join("\n");
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
// src/shell/home-screen.ts
|
|
1500
|
-
init_brand();
|
|
1501
|
-
init_theme();
|
|
1502
|
-
init_layout();
|
|
1503
|
-
init_width();
|
|
1403
|
+
import { stdin as input3, stdout as output, cwd as cwd3 } from "process";
|
|
1504
1404
|
|
|
1505
1405
|
// src/lib/assets.ts
|
|
1506
1406
|
import { readFile as readFile4 } from "fs/promises";
|
|
@@ -1527,130 +1427,261 @@ async function readAsset(name) {
|
|
|
1527
1427
|
}
|
|
1528
1428
|
|
|
1529
1429
|
// src/shell/home-screen.ts
|
|
1530
|
-
|
|
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) {
|
|
1531
1541
|
let changelog = [];
|
|
1532
1542
|
try {
|
|
1533
1543
|
changelog = JSON.parse(await readAsset("changelog.json"));
|
|
1534
1544
|
} catch {
|
|
1535
1545
|
changelog = [];
|
|
1536
1546
|
}
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
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 };
|
|
1541
1573
|
}
|
|
1542
|
-
function
|
|
1543
|
-
const
|
|
1544
|
-
const name = data.profile?.full_name || data.profile?.email?.split("@")[0] || "there";
|
|
1545
|
-
const org = data.profile?.organization?.trim();
|
|
1546
|
-
const mark = centerInColumn(accent(renderFuzziMark()), colW);
|
|
1547
|
-
const lines = [];
|
|
1574
|
+
function renderAccountColumn(data) {
|
|
1575
|
+
const mark = accent(renderFuzziMark()).split("\n");
|
|
1548
1576
|
if (data.profile) {
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
data.profile.role ? muted(`Role: ${data.profile.role}`) : "",
|
|
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),
|
|
1557
1584
|
"",
|
|
1558
|
-
muted(
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
);
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
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
|
-
);
|
|
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;
|
|
1582
1600
|
}
|
|
1583
|
-
|
|
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), ""];
|
|
1584
1609
|
}
|
|
1585
|
-
function
|
|
1610
|
+
function renderQuickStartColumn(data) {
|
|
1586
1611
|
const lines = [
|
|
1587
|
-
accentBold("Tips for getting started"),
|
|
1588
1612
|
"",
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
""
|
|
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)
|
|
1593
1621
|
];
|
|
1594
|
-
if (
|
|
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)) {
|
|
1622
|
+
if (data.profile) {
|
|
1602
1623
|
lines.push(
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
muted("or pass URLs directly: /scan https://example.com"),
|
|
1606
|
-
""
|
|
1624
|
+
popularCommand("/keys", "Manage API keys", 12),
|
|
1625
|
+
popularCommand("/config", "CLI settings", 12)
|
|
1607
1626
|
);
|
|
1608
1627
|
} else {
|
|
1609
1628
|
lines.push(
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
""
|
|
1629
|
+
popularCommand("/auth", "Browser sign-in", 12),
|
|
1630
|
+
popularCommand("/auth-key", "Paste API key", 12)
|
|
1613
1631
|
);
|
|
1614
1632
|
}
|
|
1615
|
-
lines
|
|
1616
|
-
muted("CI usage: "),
|
|
1617
|
-
muted("fuzzi scan <url> --fail-on critical --format json")
|
|
1618
|
-
);
|
|
1619
|
-
return lines.join("\n");
|
|
1633
|
+
return lines;
|
|
1620
1634
|
}
|
|
1621
1635
|
function renderWhatsNewColumn(data) {
|
|
1622
|
-
const
|
|
1623
|
-
const
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
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));
|
|
1632
1645
|
}
|
|
1633
|
-
|
|
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.");
|
|
1634
1655
|
}
|
|
1635
1656
|
function renderHomeScreen(data) {
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
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
|
+
);
|
|
1643
1667
|
}
|
|
1644
1668
|
function renderChangelog(entries) {
|
|
1645
1669
|
if (!entries.length) return muted("No changelog entries.");
|
|
1646
1670
|
return entries.map((e) => {
|
|
1647
1671
|
const lines = [
|
|
1648
1672
|
accentBold(`v${e.version}`) + muted(` \u2014 ${e.date}`),
|
|
1649
|
-
...e.highlights.map((h) =>
|
|
1673
|
+
...e.highlights.map((h) => success("\u2713 ") + primary(h.replace(/^✓\s*/, "")))
|
|
1650
1674
|
];
|
|
1651
1675
|
return lines.join("\n");
|
|
1652
1676
|
}).join("\n\n");
|
|
1653
1677
|
}
|
|
1678
|
+
function renderAuthGateScreen() {
|
|
1679
|
+
return renderHomeScreen({
|
|
1680
|
+
profile: null,
|
|
1681
|
+
changelog: [],
|
|
1682
|
+
stats: null
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1654
1685
|
|
|
1655
1686
|
// src/shell/slash-commands.ts
|
|
1656
1687
|
init_api_client();
|
|
@@ -1668,6 +1699,9 @@ function emptyState(title, hint, action) {
|
|
|
1668
1699
|
return lines.join("\n");
|
|
1669
1700
|
}
|
|
1670
1701
|
|
|
1702
|
+
// src/commands/keys.ts
|
|
1703
|
+
init_brand();
|
|
1704
|
+
|
|
1671
1705
|
// src/terminal/interactive.ts
|
|
1672
1706
|
init_theme();
|
|
1673
1707
|
import { select, search } from "@inquirer/prompts";
|
|
@@ -1702,7 +1736,7 @@ async function runKeysListCommand(client) {
|
|
|
1702
1736
|
const data = await client.get("/keys");
|
|
1703
1737
|
const keys = data.results || [];
|
|
1704
1738
|
if (!keys.length) {
|
|
1705
|
-
return emptyState("No API keys",
|
|
1739
|
+
return emptyState("No API keys", `Create one at ${SETTINGS_API_KEYS_URL}`, "[n] new key in this view");
|
|
1706
1740
|
}
|
|
1707
1741
|
const rows = keys.map((k) => [
|
|
1708
1742
|
k.name,
|
|
@@ -1750,9 +1784,9 @@ var SLASH_COMMANDS = [
|
|
|
1750
1784
|
{ name: "/exit", description: "Exit the shell", aliases: ["/quit"] }
|
|
1751
1785
|
];
|
|
1752
1786
|
function findCommand(input5) {
|
|
1753
|
-
const
|
|
1787
|
+
const cmd2 = input5.trim().split(/\s/)[0].toLowerCase();
|
|
1754
1788
|
return SLASH_COMMANDS.find(
|
|
1755
|
-
(c) => c.name ===
|
|
1789
|
+
(c) => c.name === cmd2 || c.aliases?.some((a) => a === cmd2)
|
|
1756
1790
|
);
|
|
1757
1791
|
}
|
|
1758
1792
|
|
|
@@ -1871,9 +1905,9 @@ async function dispatchSlashCommand(line, ctx) {
|
|
|
1871
1905
|
const trimmed = line.trim();
|
|
1872
1906
|
if (!trimmed) return {};
|
|
1873
1907
|
if (trimmed === "/exit" || trimmed === "/quit") return { exit: true };
|
|
1874
|
-
const [
|
|
1908
|
+
const [cmd2, ...rest] = trimmed.split(/\s+/);
|
|
1875
1909
|
const arg = rest.join(" ").trim();
|
|
1876
|
-
if (!
|
|
1910
|
+
if (!cmd2.startsWith("/")) {
|
|
1877
1911
|
ctx.sink.write(
|
|
1878
1912
|
errorBox(
|
|
1879
1913
|
`Not a shell command: ${trimmed}`,
|
|
@@ -1883,12 +1917,12 @@ ${accent("/help")} lists everything`
|
|
|
1883
1917
|
);
|
|
1884
1918
|
return {};
|
|
1885
1919
|
}
|
|
1886
|
-
if (!findCommand(
|
|
1887
|
-
ctx.sink.write(errorBox(`Unknown command: ${
|
|
1920
|
+
if (!findCommand(cmd2) && cmd2.startsWith("/")) {
|
|
1921
|
+
ctx.sink.write(errorBox(`Unknown command: ${cmd2}`, "Type /help or /palette"));
|
|
1888
1922
|
return {};
|
|
1889
1923
|
}
|
|
1890
1924
|
try {
|
|
1891
|
-
switch (
|
|
1925
|
+
switch (cmd2.toLowerCase()) {
|
|
1892
1926
|
case "/help":
|
|
1893
1927
|
ctx.sink.write(renderHelpScreen());
|
|
1894
1928
|
break;
|
|
@@ -1982,9 +2016,9 @@ ${muted("Rate limit")} ${rate}` : status);
|
|
|
1982
2016
|
}
|
|
1983
2017
|
case "/login":
|
|
1984
2018
|
case "/auth": {
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
return { profile:
|
|
2019
|
+
const result = await runAssistedBrowserLogin();
|
|
2020
|
+
ctx.sink.write(result.message);
|
|
2021
|
+
return { profile: result.profile, redraw: true };
|
|
1988
2022
|
}
|
|
1989
2023
|
case "/auth-key": {
|
|
1990
2024
|
ctx.sink.write(await runApiKeyLogin({ interactive: true }));
|
|
@@ -1992,7 +2026,7 @@ ${muted("Rate limit")} ${rate}` : status);
|
|
|
1992
2026
|
return { profile: await client.get("/me"), redraw: true };
|
|
1993
2027
|
}
|
|
1994
2028
|
default:
|
|
1995
|
-
ctx.sink.write(errorBox(`Unknown command: ${
|
|
2029
|
+
ctx.sink.write(errorBox(`Unknown command: ${cmd2}`, "Type /help"));
|
|
1996
2030
|
}
|
|
1997
2031
|
} catch (e) {
|
|
1998
2032
|
ctx.sink.error(formatApiError(e));
|
|
@@ -2063,7 +2097,6 @@ async function runKeysInteractive(ctx) {
|
|
|
2063
2097
|
|
|
2064
2098
|
// src/shell/prompt-loop.ts
|
|
2065
2099
|
init_theme();
|
|
2066
|
-
import { cwd as cwd3 } from "process";
|
|
2067
2100
|
|
|
2068
2101
|
// src/shell/completer.ts
|
|
2069
2102
|
function buildCompleter(commands, history) {
|
|
@@ -2081,8 +2114,6 @@ function buildCompleter(commands, history) {
|
|
|
2081
2114
|
|
|
2082
2115
|
// src/shell/prompt-loop.ts
|
|
2083
2116
|
init_capabilities();
|
|
2084
|
-
init_layout();
|
|
2085
|
-
init_logger();
|
|
2086
2117
|
async function runPromptLoop(initialProfile) {
|
|
2087
2118
|
let profile = initialProfile;
|
|
2088
2119
|
const workDir = cwd3();
|
|
@@ -2091,13 +2122,8 @@ async function runPromptLoop(initialProfile) {
|
|
|
2091
2122
|
if (getCapabilities().interactive) {
|
|
2092
2123
|
process.stdout.write("\x1B[2J\x1B[H");
|
|
2093
2124
|
}
|
|
2094
|
-
const data = await fetchHomeData(profile
|
|
2125
|
+
const data = await fetchHomeData(profile);
|
|
2095
2126
|
console.log(renderHomeScreen(data));
|
|
2096
|
-
console.log(statusBar([
|
|
2097
|
-
profile ? muted(profile.email) : muted("guest"),
|
|
2098
|
-
dim(workDir),
|
|
2099
|
-
isDebugMode() ? muted("debug") : null
|
|
2100
|
-
].filter(Boolean)));
|
|
2101
2127
|
console.log("");
|
|
2102
2128
|
};
|
|
2103
2129
|
await refresh();
|
|
@@ -2113,7 +2139,7 @@ async function runPromptLoop(initialProfile) {
|
|
|
2113
2139
|
rl.close();
|
|
2114
2140
|
process.exit(0);
|
|
2115
2141
|
});
|
|
2116
|
-
const prompt = () => process.stdout.write(accent("
|
|
2142
|
+
const prompt = () => process.stdout.write(accent("> "));
|
|
2117
2143
|
prompt();
|
|
2118
2144
|
for await (const line of rl) {
|
|
2119
2145
|
await appendHistory(line);
|
|
@@ -2142,11 +2168,9 @@ async function runPromptLoop(initialProfile) {
|
|
|
2142
2168
|
}
|
|
2143
2169
|
|
|
2144
2170
|
// src/shell/auth-gate.ts
|
|
2145
|
-
init_layout();
|
|
2146
|
-
init_theme();
|
|
2147
|
-
init_width();
|
|
2148
2171
|
import * as readline2 from "readline";
|
|
2149
2172
|
import { stdin as input4, stdout as output2 } from "process";
|
|
2173
|
+
init_theme();
|
|
2150
2174
|
|
|
2151
2175
|
// src/cli/profile.ts
|
|
2152
2176
|
init_credentials();
|
|
@@ -2165,50 +2189,10 @@ async function tryGetProfile() {
|
|
|
2165
2189
|
}
|
|
2166
2190
|
|
|
2167
2191
|
// 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
2192
|
function waitForEnter() {
|
|
2209
2193
|
return new Promise((resolve) => {
|
|
2210
2194
|
const rl = readline2.createInterface({ input: input4, output: output2, terminal: true });
|
|
2211
|
-
output2.write(accent("\n
|
|
2195
|
+
output2.write(accent("\n> Press Enter to open browser... "));
|
|
2212
2196
|
rl.once("line", () => {
|
|
2213
2197
|
rl.close();
|
|
2214
2198
|
resolve();
|
|
@@ -2219,18 +2203,15 @@ async function runAuthGate() {
|
|
|
2219
2203
|
const existing = await tryGetProfile();
|
|
2220
2204
|
if (existing) return existing;
|
|
2221
2205
|
if (!output2.isTTY) return null;
|
|
2222
|
-
console.log(
|
|
2206
|
+
console.log(renderAuthGateScreen());
|
|
2223
2207
|
await waitForEnter();
|
|
2224
|
-
const progress = createProgress("Opening browser...");
|
|
2225
2208
|
try {
|
|
2226
|
-
const result = await
|
|
2227
|
-
|
|
2228
|
-
console.log(accent(result.message));
|
|
2209
|
+
const result = await runAssistedBrowserLogin();
|
|
2210
|
+
console.log(result.message);
|
|
2229
2211
|
return result.profile;
|
|
2230
2212
|
} catch (e) {
|
|
2231
|
-
progress.fail("Sign-in failed");
|
|
2232
2213
|
console.log(muted(formatApiError(e)));
|
|
2233
|
-
console.log(muted("
|
|
2214
|
+
console.log(muted("Run /auth-key to paste your API key manually."));
|
|
2234
2215
|
return null;
|
|
2235
2216
|
}
|
|
2236
2217
|
}
|