fuzzi-cli 0.1.3 → 0.1.5
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 +28 -8
- package/dist/index.js +583 -481
- 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.5";
|
|
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"() {
|
|
@@ -214,10 +219,40 @@ var init_logger = __esm({
|
|
|
214
219
|
}
|
|
215
220
|
});
|
|
216
221
|
|
|
222
|
+
// src/lib/api-utils.ts
|
|
223
|
+
function errorText(value) {
|
|
224
|
+
if (value == null) return "";
|
|
225
|
+
if (typeof value === "string") return value;
|
|
226
|
+
if (Array.isArray(value)) return value.map(errorText).filter(Boolean).join("; ");
|
|
227
|
+
if (typeof value === "object") {
|
|
228
|
+
const o = value;
|
|
229
|
+
if (typeof o.detail === "string") return o.detail;
|
|
230
|
+
if (Array.isArray(o.detail)) return o.detail.map(errorText).join("; ");
|
|
231
|
+
if (typeof o.message === "string") return o.message;
|
|
232
|
+
if (typeof o.error === "string") return o.error;
|
|
233
|
+
try {
|
|
234
|
+
return JSON.stringify(value);
|
|
235
|
+
} catch {
|
|
236
|
+
return String(value);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return String(value);
|
|
240
|
+
}
|
|
241
|
+
function asList(data) {
|
|
242
|
+
if (!data) return [];
|
|
243
|
+
if (Array.isArray(data)) return data;
|
|
244
|
+
return data.results ?? data.data ?? [];
|
|
245
|
+
}
|
|
246
|
+
var init_api_utils = __esm({
|
|
247
|
+
"src/lib/api-utils.ts"() {
|
|
248
|
+
"use strict";
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
217
252
|
// src/lib/api-client.ts
|
|
218
253
|
function mapErrorMessage(status, body) {
|
|
219
|
-
const code = body.code
|
|
220
|
-
const msg = body.error
|
|
254
|
+
const code = typeof body.code === "string" ? body.code.toLowerCase() : "";
|
|
255
|
+
const msg = errorText(body.error ?? body.message ?? body.detail);
|
|
221
256
|
if (status === 401) {
|
|
222
257
|
if (code === "key_revoked" || msg.toLowerCase().includes("revoked")) {
|
|
223
258
|
return "API key has been revoked. Please log in again.";
|
|
@@ -225,7 +260,7 @@ function mapErrorMessage(status, body) {
|
|
|
225
260
|
if (code === "key_expired" || msg.toLowerCase().includes("expired")) {
|
|
226
261
|
return `API key has expired. Generate a new one at ${SETTINGS_API_KEYS_URL}`;
|
|
227
262
|
}
|
|
228
|
-
return `Invalid API key. Generate a new one at ${SETTINGS_API_KEYS_URL}`;
|
|
263
|
+
return msg || `Invalid API key. Generate a new one at ${SETTINGS_API_KEYS_URL}`;
|
|
229
264
|
}
|
|
230
265
|
if (status === 403 && (code === "ssrf" || msg.toLowerCase().includes("private ip"))) {
|
|
231
266
|
return "This URL is not allowed (private IP address detected). Please scan a public-facing URL.";
|
|
@@ -257,6 +292,7 @@ var init_api_client = __esm({
|
|
|
257
292
|
init_credentials();
|
|
258
293
|
init_logger();
|
|
259
294
|
init_brand();
|
|
295
|
+
init_api_utils();
|
|
260
296
|
ApiError = class extends Error {
|
|
261
297
|
constructor(message, status, code, body, exitCode) {
|
|
262
298
|
super(message);
|
|
@@ -329,7 +365,7 @@ var init_api_client = __esm({
|
|
|
329
365
|
message = `Rate limit exceeded. Retry after ${seconds} seconds.`;
|
|
330
366
|
}
|
|
331
367
|
if (res.status >= 500) {
|
|
332
|
-
message = `
|
|
368
|
+
message = `Request failed: ${errorText(errBody.error ?? errBody.message ?? errBody.detail) || res.statusText}`;
|
|
333
369
|
}
|
|
334
370
|
throw new ApiError(message, res.status, errBody.code, data, 2);
|
|
335
371
|
}
|
|
@@ -395,7 +431,7 @@ function getCapabilities() {
|
|
|
395
431
|
const colorterm = process.env.COLORTERM ?? "";
|
|
396
432
|
const trueColor = colorterm.includes("truecolor") || colorterm.includes("24bit") || term.includes("truecolor") || !!process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0";
|
|
397
433
|
cached = {
|
|
398
|
-
width: Math.max(60,
|
|
434
|
+
width: Math.max(60, cols),
|
|
399
435
|
trueColor,
|
|
400
436
|
interactive: stdout.isTTY === true
|
|
401
437
|
};
|
|
@@ -434,18 +470,18 @@ function scoreBold(n) {
|
|
|
434
470
|
return chalk.bold(String(n));
|
|
435
471
|
}
|
|
436
472
|
function error(text) {
|
|
437
|
-
return color(
|
|
473
|
+
return color(BRAND.danger, chalk.red)(text);
|
|
438
474
|
}
|
|
439
475
|
function success(text) {
|
|
440
|
-
return color(
|
|
476
|
+
return color(BRAND.success, chalk.green)(text);
|
|
441
477
|
}
|
|
442
478
|
function warn(text) {
|
|
443
|
-
return color(
|
|
479
|
+
return color(BRAND.warning, chalk.yellow)(text);
|
|
444
480
|
}
|
|
445
|
-
function
|
|
446
|
-
return
|
|
481
|
+
function cmd(text) {
|
|
482
|
+
return accent(text);
|
|
447
483
|
}
|
|
448
|
-
var accent, accentBold, muted, bold, dim, italic;
|
|
484
|
+
var accent, accentBold, primary, primaryBold, muted, dimText, border, bold, dim, italic;
|
|
449
485
|
var init_theme = __esm({
|
|
450
486
|
"src/terminal/theme.ts"() {
|
|
451
487
|
"use strict";
|
|
@@ -453,7 +489,11 @@ var init_theme = __esm({
|
|
|
453
489
|
init_capabilities();
|
|
454
490
|
accent = color(BRAND.accent, chalk.cyan);
|
|
455
491
|
accentBold = accent.bold;
|
|
492
|
+
primary = color(BRAND.textPrimary, chalk.white);
|
|
493
|
+
primaryBold = primary.bold;
|
|
456
494
|
muted = color(BRAND.textSecondary, chalk.gray);
|
|
495
|
+
dimText = color(BRAND.textTertiary, chalk.dim);
|
|
496
|
+
border = color(BRAND.borderSubtle, chalk.gray);
|
|
457
497
|
bold = chalk.bold;
|
|
458
498
|
dim = chalk.dim;
|
|
459
499
|
italic = chalk.italic;
|
|
@@ -544,33 +584,6 @@ function panel(content, opts = {}) {
|
|
|
544
584
|
width
|
|
545
585
|
});
|
|
546
586
|
}
|
|
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
587
|
function columns(left, right, leftWidth) {
|
|
575
588
|
const total = contentWidth();
|
|
576
589
|
const split = leftWidth ?? Math.floor(total * 0.48);
|
|
@@ -589,9 +602,6 @@ function divider(char = "\u2500", width) {
|
|
|
589
602
|
const w = width ?? contentWidth();
|
|
590
603
|
return dim(char.repeat(Math.max(20, w)));
|
|
591
604
|
}
|
|
592
|
-
function statusBar(parts) {
|
|
593
|
-
return dim(parts.filter(Boolean).join(" \xB7 "));
|
|
594
|
-
}
|
|
595
605
|
function keyValue(rows, indent = 2) {
|
|
596
606
|
const pad = " ".repeat(indent);
|
|
597
607
|
const maxKey = Math.max(...rows.map(([k]) => k.length), 4);
|
|
@@ -719,103 +729,96 @@ init_logger();
|
|
|
719
729
|
import { randomBytes } from "crypto";
|
|
720
730
|
import { createServer } from "http";
|
|
721
731
|
import { exec } from "child_process";
|
|
722
|
-
var TIMEOUT_MS = 5 * 60 * 1e3;
|
|
723
|
-
function generateState() {
|
|
724
|
-
return randomBytes(24).toString("base64url");
|
|
725
|
-
}
|
|
726
732
|
function openBrowser(url) {
|
|
727
733
|
const platform = process.platform;
|
|
728
|
-
const
|
|
729
|
-
exec(
|
|
734
|
+
const cmd2 = platform === "darwin" ? `open ${JSON.stringify(url)}` : platform === "win32" ? `start "" ${JSON.stringify(url)}` : `xdg-open ${JSON.stringify(url)}`;
|
|
735
|
+
exec(cmd2, (err) => {
|
|
730
736
|
if (err) log.warn("could not open browser automatically", err.message);
|
|
731
737
|
});
|
|
732
738
|
}
|
|
733
739
|
function apiOrigin(apiUrl) {
|
|
734
740
|
return apiUrl.replace(/\/api\/?$/, "") || APP_ORIGIN;
|
|
735
741
|
}
|
|
736
|
-
async function
|
|
742
|
+
async function openCliAuthPage() {
|
|
737
743
|
const config = await loadConfig();
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
744
|
+
const url = `${apiOrigin(config.api_url)}/cli-auth`;
|
|
745
|
+
openBrowser(url);
|
|
746
|
+
log.debug("opened cli auth page", url);
|
|
747
|
+
return url;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// src/commands/auth.ts
|
|
751
|
+
init_brand();
|
|
752
|
+
|
|
753
|
+
// src/terminal/interactive.ts
|
|
754
|
+
init_theme();
|
|
755
|
+
import { select, search } from "@inquirer/prompts";
|
|
756
|
+
var pauseHook = null;
|
|
757
|
+
var resumeHook = null;
|
|
758
|
+
function setReadlineHooks(pause, resume) {
|
|
759
|
+
pauseHook = pause;
|
|
760
|
+
resumeHook = resume;
|
|
761
|
+
}
|
|
762
|
+
async function runInteractive(fn) {
|
|
763
|
+
return withReadlinePaused(fn);
|
|
764
|
+
}
|
|
765
|
+
async function withReadlinePaused(fn) {
|
|
766
|
+
pauseHook?.();
|
|
767
|
+
try {
|
|
768
|
+
return await fn();
|
|
769
|
+
} finally {
|
|
770
|
+
resumeHook?.();
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
async function pickFromList(message, items) {
|
|
774
|
+
if (!items.length) return null;
|
|
775
|
+
try {
|
|
776
|
+
return await withReadlinePaused(() => select({ message, choices: items }));
|
|
777
|
+
} catch {
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
async function searchPalette(message, choices) {
|
|
782
|
+
try {
|
|
783
|
+
return await withReadlinePaused(
|
|
784
|
+
() => search({
|
|
785
|
+
message,
|
|
786
|
+
source: async (input5) => {
|
|
787
|
+
if (!input5) return choices;
|
|
788
|
+
const q = input5.toLowerCase();
|
|
789
|
+
return choices.filter(
|
|
790
|
+
(c) => c.value.toLowerCase().includes(q) || String(c.description ?? "").toLowerCase().includes(q)
|
|
791
|
+
);
|
|
757
792
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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);
|
|
793
|
+
})
|
|
794
|
+
);
|
|
795
|
+
} catch {
|
|
796
|
+
return null;
|
|
792
797
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
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 };
|
|
798
|
+
}
|
|
799
|
+
function formatChoice(name, description) {
|
|
800
|
+
return `${accent(name)}${muted(" \u2014 " + description)}`;
|
|
806
801
|
}
|
|
807
802
|
|
|
808
803
|
// src/commands/auth.ts
|
|
809
|
-
|
|
804
|
+
async function runAssistedBrowserLogin() {
|
|
805
|
+
await openCliAuthPage();
|
|
806
|
+
console.log("");
|
|
807
|
+
console.log(accent(" Browser opened \u2014 authorize Fuzzi CLI on the web page."));
|
|
808
|
+
console.log(muted(" Copy the API key shown, then paste it below."));
|
|
809
|
+
console.log("");
|
|
810
|
+
const msg = await runApiKeyLogin({ interactive: true });
|
|
811
|
+
const client = await getAuthenticatedClient();
|
|
812
|
+
const profile = await client.get("/me");
|
|
813
|
+
return { message: msg, profile };
|
|
814
|
+
}
|
|
810
815
|
async function runAuthLogin(opts = {}) {
|
|
811
|
-
if (opts.
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
if (opts.apiKeyOnly) throw e;
|
|
818
|
-
}
|
|
816
|
+
if (opts.apiKeyOnly || opts.apiKey) {
|
|
817
|
+
return runApiKeyLogin(opts);
|
|
818
|
+
}
|
|
819
|
+
if (opts.browser || opts.interactive !== false) {
|
|
820
|
+
const result = await runAssistedBrowserLogin();
|
|
821
|
+
return result.message;
|
|
819
822
|
}
|
|
820
823
|
return runApiKeyLogin(opts);
|
|
821
824
|
}
|
|
@@ -833,15 +836,17 @@ async function runApiKeyLogin(opts = {}) {
|
|
|
833
836
|
2
|
|
834
837
|
);
|
|
835
838
|
}
|
|
836
|
-
apiKey = await
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
839
|
+
apiKey = await runInteractive(
|
|
840
|
+
() => password({
|
|
841
|
+
message: "Paste your API key (fz_live_...):",
|
|
842
|
+
mask: "\u2022",
|
|
843
|
+
validate: (v) => {
|
|
844
|
+
if (!v.trim()) return "API key is required";
|
|
845
|
+
if (!isValidApiKeyFormat(v)) return "Key must start with fz_live_";
|
|
846
|
+
return true;
|
|
847
|
+
}
|
|
848
|
+
})
|
|
849
|
+
);
|
|
845
850
|
}
|
|
846
851
|
apiKey = apiKey.trim();
|
|
847
852
|
if (!isValidApiKeyFormat(apiKey)) {
|
|
@@ -1167,6 +1172,17 @@ function createProgress(label, stream = false) {
|
|
|
1167
1172
|
}
|
|
1168
1173
|
};
|
|
1169
1174
|
}
|
|
1175
|
+
async function withSpinner(label, fn) {
|
|
1176
|
+
const p = createProgress(label);
|
|
1177
|
+
try {
|
|
1178
|
+
const result = await fn();
|
|
1179
|
+
p.stop();
|
|
1180
|
+
return result;
|
|
1181
|
+
} catch (e) {
|
|
1182
|
+
p.fail();
|
|
1183
|
+
throw e;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1170
1186
|
|
|
1171
1187
|
// src/commands/scan.ts
|
|
1172
1188
|
init_strings();
|
|
@@ -1266,6 +1282,7 @@ async function runScanCommand(client, opts) {
|
|
|
1266
1282
|
}
|
|
1267
1283
|
|
|
1268
1284
|
// src/commands/scans.ts
|
|
1285
|
+
init_api_utils();
|
|
1269
1286
|
async function runScansListCommand(client, format = "table", filters) {
|
|
1270
1287
|
const params = new URLSearchParams({ page: "1", page_size: String(filters?.limit || 20) });
|
|
1271
1288
|
if (filters?.status) params.set("status", filters.status);
|
|
@@ -1273,7 +1290,7 @@ async function runScansListCommand(client, format = "table", filters) {
|
|
|
1273
1290
|
const data = await client.get(
|
|
1274
1291
|
`/scans?${params.toString()}`
|
|
1275
1292
|
);
|
|
1276
|
-
return renderScansList(data
|
|
1293
|
+
return renderScansList(asList(data), format);
|
|
1277
1294
|
}
|
|
1278
1295
|
async function runScanGetCommand(client, scanId, format = "table") {
|
|
1279
1296
|
const detail = await client.get(`/scan/${scanId}`);
|
|
@@ -1480,33 +1497,7 @@ function buildProgram() {
|
|
|
1480
1497
|
|
|
1481
1498
|
// src/shell/prompt-loop.ts
|
|
1482
1499
|
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();
|
|
1500
|
+
import { stdin as input3, stdout as output, cwd as cwd3 } from "process";
|
|
1510
1501
|
|
|
1511
1502
|
// src/lib/assets.ts
|
|
1512
1503
|
import { readFile as readFile4 } from "fs/promises";
|
|
@@ -1533,130 +1524,265 @@ async function readAsset(name) {
|
|
|
1533
1524
|
}
|
|
1534
1525
|
|
|
1535
1526
|
// src/shell/home-screen.ts
|
|
1536
|
-
|
|
1527
|
+
init_api_client();
|
|
1528
|
+
|
|
1529
|
+
// src/shell/ascii-mark.ts
|
|
1530
|
+
function renderFuzziMark() {
|
|
1531
|
+
return [
|
|
1532
|
+
" \u2554\u2550\u2550\u2550\u2550\u2557",
|
|
1533
|
+
" \u2551 \u25C6 \u2551",
|
|
1534
|
+
" \u255A\u2550\u2566\u2550\u2550\u255D",
|
|
1535
|
+
" \u2554\u2550\u2569\u2550\u2557",
|
|
1536
|
+
" \u2551 \u25C6 \u2551",
|
|
1537
|
+
" \u255A\u2550\u2550\u2550\u255D"
|
|
1538
|
+
].join("\n");
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// src/shell/home-screen.ts
|
|
1542
|
+
init_brand();
|
|
1543
|
+
init_theme();
|
|
1544
|
+
init_width();
|
|
1545
|
+
|
|
1546
|
+
// src/components/Panel.ts
|
|
1547
|
+
init_theme();
|
|
1548
|
+
init_strings();
|
|
1549
|
+
init_width();
|
|
1550
|
+
function visibleLen(s) {
|
|
1551
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
1552
|
+
}
|
|
1553
|
+
function padCell(content, width) {
|
|
1554
|
+
const inner = width - 2;
|
|
1555
|
+
return " " + padEndVisible(content, inner) + " ";
|
|
1556
|
+
}
|
|
1557
|
+
function topBanner(title, width = contentWidth()) {
|
|
1558
|
+
const inner = ` ${title} `;
|
|
1559
|
+
const dashes = Math.max(0, width - 2 - visibleLen(inner));
|
|
1560
|
+
const left = Math.floor(dashes / 2);
|
|
1561
|
+
const right = dashes - left;
|
|
1562
|
+
return [
|
|
1563
|
+
border(`\u250F${"\u2501".repeat(left)}${inner}${"\u2501".repeat(right)}\u2513`),
|
|
1564
|
+
border(`\u2517${"\u2501".repeat(width - 2)}\u251B`)
|
|
1565
|
+
].join("\n");
|
|
1566
|
+
}
|
|
1567
|
+
function titleSegment(title, width) {
|
|
1568
|
+
const prefix = `\u2500 ${title} `;
|
|
1569
|
+
const dashes = Math.max(0, width - visibleLen(prefix));
|
|
1570
|
+
return prefix + "\u2500".repeat(dashes);
|
|
1571
|
+
}
|
|
1572
|
+
function tripleColumnPanel(cols, totalWidth = contentWidth()) {
|
|
1573
|
+
const sep = 1;
|
|
1574
|
+
const inner = totalWidth - 2;
|
|
1575
|
+
const ratios = [0.44, 0.28, 0.28];
|
|
1576
|
+
const raw = ratios.map((r) => Math.floor(inner * r));
|
|
1577
|
+
const used = raw.reduce((a, b) => a + b, 0) + 2 * sep;
|
|
1578
|
+
raw[0] += inner - used;
|
|
1579
|
+
const widths = raw;
|
|
1580
|
+
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");
|
|
1581
|
+
const maxRows = Math.max(...cols.map((c) => c.lines.length), 1);
|
|
1582
|
+
const body = [top];
|
|
1583
|
+
for (let i = 0; i < maxRows; i++) {
|
|
1584
|
+
const cells = cols.map((c, idx) => padCell(c.lines[i] ?? "", widths[idx]));
|
|
1585
|
+
body.push(
|
|
1586
|
+
border("\u2502") + cells[0] + border("\u2502") + cells[1] + border("\u2502") + cells[2] + border("\u2502")
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
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");
|
|
1590
|
+
body.push(bottom);
|
|
1591
|
+
return body.join("\n");
|
|
1592
|
+
}
|
|
1593
|
+
function stackedPanels(cols, width = contentWidth()) {
|
|
1594
|
+
return cols.map((col) => singlePanel(col.title, col.lines, width)).join("\n\n");
|
|
1595
|
+
}
|
|
1596
|
+
function singlePanel(title, lines, width = contentWidth()) {
|
|
1597
|
+
const inner = width - 2;
|
|
1598
|
+
const prefix = `\u2500 ${title} `;
|
|
1599
|
+
const dashes = Math.max(0, inner - visibleLen(prefix));
|
|
1600
|
+
const top = border(`\u250C${prefix}${"\u2500".repeat(dashes)}\u2510`);
|
|
1601
|
+
const bottom = border(`\u2514${"\u2500".repeat(inner)}\u2518`);
|
|
1602
|
+
const body = lines.map((l) => border("\u2502") + padCell(l, inner + 2) + border("\u2502"));
|
|
1603
|
+
return [top, ...body, bottom].join("\n");
|
|
1604
|
+
}
|
|
1605
|
+
function tipPanel(text, width = contentWidth()) {
|
|
1606
|
+
const inner = width - 2;
|
|
1607
|
+
const prefix = "\u2500 Tip ";
|
|
1608
|
+
const dashes = Math.max(0, inner - visibleLen(prefix));
|
|
1609
|
+
const top = border(`\u250C${prefix}${"\u2500".repeat(dashes)}\u2510`);
|
|
1610
|
+
const bottom = border(`\u2514${"\u2500".repeat(inner)}\u2518`);
|
|
1611
|
+
return [top, border("\u2502") + padCell(text, inner + 2) + border("\u2502"), bottom].join("\n");
|
|
1612
|
+
}
|
|
1613
|
+
function besideMark(mark, text, markCol = 18, gap = 3) {
|
|
1614
|
+
const rows = Math.max(mark.length, text.length);
|
|
1615
|
+
const out = [];
|
|
1616
|
+
for (let i = 0; i < rows; i++) {
|
|
1617
|
+
const m = padEndVisible(mark[i] ?? "", markCol);
|
|
1618
|
+
const t = text[i] ?? "";
|
|
1619
|
+
out.push(m + " ".repeat(gap) + t);
|
|
1620
|
+
}
|
|
1621
|
+
return out;
|
|
1622
|
+
}
|
|
1623
|
+
function popularCommand(command, desc, cmdWidth = 14) {
|
|
1624
|
+
return padEndVisible(cmd(command), cmdWidth) + muted(desc);
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// src/shell/home-screen.ts
|
|
1628
|
+
function daysUntil2(dateStr) {
|
|
1629
|
+
const diff = new Date(dateStr).getTime() - Date.now();
|
|
1630
|
+
return Math.max(0, Math.ceil(diff / (1e3 * 60 * 60 * 24)));
|
|
1631
|
+
}
|
|
1632
|
+
function formatKeyExpiry(dateStr) {
|
|
1633
|
+
if (!dateStr) return null;
|
|
1634
|
+
const days = daysUntil2(dateStr);
|
|
1635
|
+
return `Key expires: ${days} days remaining`;
|
|
1636
|
+
}
|
|
1637
|
+
function formatKeyExpiryDate(dateStr) {
|
|
1638
|
+
if (!dateStr) return null;
|
|
1639
|
+
return `(${dateStr.slice(0, 10)})`;
|
|
1640
|
+
}
|
|
1641
|
+
async function fetchHomeData(profile) {
|
|
1537
1642
|
let changelog = [];
|
|
1538
1643
|
try {
|
|
1539
1644
|
changelog = JSON.parse(await readAsset("changelog.json"));
|
|
1540
1645
|
} catch {
|
|
1541
1646
|
changelog = [];
|
|
1542
1647
|
}
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1648
|
+
let stats = null;
|
|
1649
|
+
if (profile) {
|
|
1650
|
+
try {
|
|
1651
|
+
const client = await getAuthenticatedClient();
|
|
1652
|
+
const rateRaw = await runRateLimitStatus(client);
|
|
1653
|
+
let rateHour = null;
|
|
1654
|
+
if (rateRaw) {
|
|
1655
|
+
const m = rateRaw.match(/(\d+)\/(\d+)/);
|
|
1656
|
+
if (m) {
|
|
1657
|
+
const remaining = Number(m[1]);
|
|
1658
|
+
const limit2 = Number(m[2]);
|
|
1659
|
+
const used2 = limit2 - remaining;
|
|
1660
|
+
rateHour = `${used2}/${limit2} scans this hour`;
|
|
1661
|
+
} else {
|
|
1662
|
+
rateHour = rateRaw;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
const used = profile.scans_used_this_month ?? profile.total_scans;
|
|
1666
|
+
const limit = profile.monthly_scan_limit;
|
|
1667
|
+
const usageMonth = used != null && limit != null ? `${used}/${limit} scans this month` : used != null ? `${used} scans total` : null;
|
|
1668
|
+
stats = { rateHour, usageMonth };
|
|
1669
|
+
} catch {
|
|
1670
|
+
stats = null;
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
return { profile, changelog, stats };
|
|
1547
1674
|
}
|
|
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 = [];
|
|
1675
|
+
function renderAccountColumn(data) {
|
|
1676
|
+
const mark = accent(renderFuzziMark()).split("\n");
|
|
1554
1677
|
if (data.profile) {
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
data.profile.role ? muted(`Role: ${data.profile.role}`) : "",
|
|
1563
|
-
"",
|
|
1564
|
-
muted(data.cwd),
|
|
1565
|
-
"",
|
|
1566
|
-
accent("/scan") + muted(" <url> scan a target"),
|
|
1567
|
-
accent("/scans") + muted(" browse history"),
|
|
1568
|
-
accent("/status") + muted(" account info"),
|
|
1569
|
-
accent("/keys") + muted(" manage keys"),
|
|
1570
|
-
accent("/palette") + muted(" find commands")
|
|
1571
|
-
);
|
|
1572
|
-
} else {
|
|
1573
|
-
lines.push(
|
|
1574
|
-
accentBold("Welcome to Fuzzi!"),
|
|
1678
|
+
const p = data.profile;
|
|
1679
|
+
const name = p.full_name || p.email.split("@")[0];
|
|
1680
|
+
const keyExpiry = formatKeyExpiry(p.key_expires_at ?? void 0);
|
|
1681
|
+
const keyDate = formatKeyExpiryDate(p.key_expires_at ?? void 0);
|
|
1682
|
+
const textBlock2 = [
|
|
1683
|
+
primaryBold(`Welcome back, ${name}!`),
|
|
1684
|
+
primary(p.email),
|
|
1575
1685
|
"",
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1686
|
+
muted(`Organization: ${p.organization || "\u2014"}`),
|
|
1687
|
+
muted(`Role: ${p.role}`)
|
|
1688
|
+
];
|
|
1689
|
+
const beside = besideMark(mark, textBlock2, 14, 4);
|
|
1690
|
+
const lines = ["", ...beside, ""];
|
|
1691
|
+
lines.push(success("\u25CF Connected"));
|
|
1692
|
+
lines.push(muted("Status: Ready"));
|
|
1693
|
+
if (data.stats?.rateHour) lines.push(muted(`Rate limit: ${data.stats.rateHour}`));
|
|
1694
|
+
if (data.stats?.usageMonth) lines.push(muted(`Usage: ${data.stats.usageMonth}`));
|
|
1695
|
+
if (keyExpiry) {
|
|
1696
|
+
lines.push("");
|
|
1697
|
+
lines.push(muted(keyExpiry));
|
|
1698
|
+
if (keyDate) lines.push(muted(keyDate));
|
|
1699
|
+
}
|
|
1700
|
+
return lines;
|
|
1588
1701
|
}
|
|
1589
|
-
|
|
1702
|
+
const textBlock = [
|
|
1703
|
+
primaryBold("Welcome to Fuzzi!"),
|
|
1704
|
+
muted("Not connected"),
|
|
1705
|
+
"",
|
|
1706
|
+
muted("Press Enter to sign in"),
|
|
1707
|
+
muted("or run /auth-key")
|
|
1708
|
+
];
|
|
1709
|
+
return ["", ...besideMark(mark, textBlock, 14, 4), ""];
|
|
1590
1710
|
}
|
|
1591
|
-
function
|
|
1711
|
+
function renderQuickStartColumn(data) {
|
|
1592
1712
|
const lines = [
|
|
1593
|
-
accentBold("Tips for getting started"),
|
|
1594
1713
|
"",
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
""
|
|
1714
|
+
muted("Type ") + cmd("/help") + muted(" for all"),
|
|
1715
|
+
muted("commands."),
|
|
1716
|
+
"",
|
|
1717
|
+
primaryBold("Popular commands:"),
|
|
1718
|
+
"",
|
|
1719
|
+
popularCommand("/scan", "Start a security scan", 12),
|
|
1720
|
+
popularCommand("/scans", "Browse recent scans", 12),
|
|
1721
|
+
popularCommand("/status", "Show account info", 12)
|
|
1599
1722
|
];
|
|
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)) {
|
|
1723
|
+
if (data.profile) {
|
|
1608
1724
|
lines.push(
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
muted("or pass URLs directly: /scan https://example.com"),
|
|
1612
|
-
""
|
|
1725
|
+
popularCommand("/keys", "Manage API keys", 12),
|
|
1726
|
+
popularCommand("/config", "CLI settings", 12)
|
|
1613
1727
|
);
|
|
1614
1728
|
} else {
|
|
1615
1729
|
lines.push(
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
""
|
|
1730
|
+
popularCommand("/auth", "Browser sign-in", 12),
|
|
1731
|
+
popularCommand("/auth-key", "Paste API key", 12)
|
|
1619
1732
|
);
|
|
1620
1733
|
}
|
|
1621
|
-
lines
|
|
1622
|
-
muted("CI usage: "),
|
|
1623
|
-
muted("fuzzi scan <url> --fail-on critical --format json")
|
|
1624
|
-
);
|
|
1625
|
-
return lines.join("\n");
|
|
1734
|
+
return lines;
|
|
1626
1735
|
}
|
|
1627
1736
|
function renderWhatsNewColumn(data) {
|
|
1628
|
-
const
|
|
1629
|
-
const
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
lines.push(muted("Stay tuned for updates."));
|
|
1737
|
+
const lines = [""];
|
|
1738
|
+
const highlights = data.changelog[0]?.highlights ?? [
|
|
1739
|
+
"Confidence gating added",
|
|
1740
|
+
"Netflix-style false positives fixed",
|
|
1741
|
+
"CLI shell interface"
|
|
1742
|
+
];
|
|
1743
|
+
for (const h of highlights.slice(0, 4)) {
|
|
1744
|
+
const text = h.replace(/^✓\s*/, "");
|
|
1745
|
+
lines.push(success("\u2713 ") + primary(text));
|
|
1638
1746
|
}
|
|
1639
|
-
|
|
1747
|
+
lines.push("");
|
|
1748
|
+
lines.push(muted("Run ") + cmd("/changelog") + muted(" for more details."));
|
|
1749
|
+
return lines;
|
|
1750
|
+
}
|
|
1751
|
+
function renderTip(data) {
|
|
1752
|
+
if (data.profile) {
|
|
1753
|
+
return muted("Tip: Run ") + cmd("/scan <url>") + muted(" to scan a target, or ") + cmd("/palette") + muted(" to find commands.");
|
|
1754
|
+
}
|
|
1755
|
+
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
1756
|
}
|
|
1641
1757
|
function renderHomeScreen(data) {
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1758
|
+
const width = contentWidth();
|
|
1759
|
+
const cols = [
|
|
1760
|
+
{ title: "Account", lines: renderAccountColumn(data) },
|
|
1761
|
+
{ title: "Quick Start", lines: renderQuickStartColumn(data) },
|
|
1762
|
+
{ title: "What's New", lines: renderWhatsNewColumn(data) }
|
|
1763
|
+
];
|
|
1764
|
+
const main2 = terminalWidth() >= 100 ? tripleColumnPanel(cols, width) : stackedPanels(cols, width);
|
|
1765
|
+
return [topBanner(`Fuzzi CLI v${VERSION}`, width), "", main2, "", tipPanel(renderTip(data), width)].join(
|
|
1766
|
+
"\n"
|
|
1767
|
+
);
|
|
1649
1768
|
}
|
|
1650
1769
|
function renderChangelog(entries) {
|
|
1651
1770
|
if (!entries.length) return muted("No changelog entries.");
|
|
1652
1771
|
return entries.map((e) => {
|
|
1653
1772
|
const lines = [
|
|
1654
1773
|
accentBold(`v${e.version}`) + muted(` \u2014 ${e.date}`),
|
|
1655
|
-
...e.highlights.map((h) =>
|
|
1774
|
+
...e.highlights.map((h) => success("\u2713 ") + primary(h.replace(/^✓\s*/, "")))
|
|
1656
1775
|
];
|
|
1657
1776
|
return lines.join("\n");
|
|
1658
1777
|
}).join("\n\n");
|
|
1659
1778
|
}
|
|
1779
|
+
function renderAuthGateScreen() {
|
|
1780
|
+
return renderHomeScreen({
|
|
1781
|
+
profile: null,
|
|
1782
|
+
changelog: [],
|
|
1783
|
+
stats: null
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1660
1786
|
|
|
1661
1787
|
// src/shell/slash-commands.ts
|
|
1662
1788
|
init_api_client();
|
|
@@ -1676,47 +1802,17 @@ function emptyState(title, hint, action) {
|
|
|
1676
1802
|
|
|
1677
1803
|
// src/commands/keys.ts
|
|
1678
1804
|
init_brand();
|
|
1679
|
-
|
|
1680
|
-
// src/terminal/interactive.ts
|
|
1681
|
-
init_theme();
|
|
1682
|
-
import { select, search } from "@inquirer/prompts";
|
|
1683
|
-
async function pickFromList(message, items) {
|
|
1684
|
-
if (!items.length) return null;
|
|
1685
|
-
try {
|
|
1686
|
-
return await select({ message, choices: items });
|
|
1687
|
-
} catch {
|
|
1688
|
-
return null;
|
|
1689
|
-
}
|
|
1690
|
-
}
|
|
1691
|
-
async function searchPalette(message, choices) {
|
|
1692
|
-
try {
|
|
1693
|
-
return await search({
|
|
1694
|
-
message,
|
|
1695
|
-
source: async (input5) => {
|
|
1696
|
-
if (!input5) return choices;
|
|
1697
|
-
const q = input5.toLowerCase();
|
|
1698
|
-
return choices.filter((c) => c.value.includes(q) || c.description?.includes(q));
|
|
1699
|
-
}
|
|
1700
|
-
});
|
|
1701
|
-
} catch {
|
|
1702
|
-
return null;
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
function formatChoice(name, description) {
|
|
1706
|
-
return `${accent(name)}${muted(" \u2014 " + description)}`;
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
// src/commands/keys.ts
|
|
1805
|
+
init_api_utils();
|
|
1710
1806
|
async function runKeysListCommand(client) {
|
|
1711
1807
|
const data = await client.get("/keys");
|
|
1712
|
-
const keys = data
|
|
1808
|
+
const keys = asList(data);
|
|
1713
1809
|
if (!keys.length) {
|
|
1714
1810
|
return emptyState("No API keys", `Create one at ${SETTINGS_API_KEYS_URL}`, "[n] new key in this view");
|
|
1715
1811
|
}
|
|
1716
1812
|
const rows = keys.map((k) => [
|
|
1717
1813
|
k.name,
|
|
1718
1814
|
k.prefix,
|
|
1719
|
-
k.scopes
|
|
1815
|
+
k.scopes && Array.isArray(k.scopes) ? k.scopes.join(", ") : muted("\u2014"),
|
|
1720
1816
|
k.revoked ? muted("Revoked") : k.active ? success("Active") : muted("Inactive"),
|
|
1721
1817
|
k.last_used_at ? new Date(k.last_used_at).toLocaleDateString() : muted("Never")
|
|
1722
1818
|
]);
|
|
@@ -1728,13 +1824,16 @@ async function runKeyRevoke(client, keyId) {
|
|
|
1728
1824
|
}
|
|
1729
1825
|
async function pickKeyForRevoke(client) {
|
|
1730
1826
|
const data = await client.get("/keys");
|
|
1731
|
-
const active = (data
|
|
1827
|
+
const active = asList(data).filter((k) => !k.revoked && k.active);
|
|
1732
1828
|
return pickFromList(
|
|
1733
1829
|
"Select a key to revoke",
|
|
1734
1830
|
active.map((k) => ({ name: `${k.name} (${k.prefix})`, value: k.id }))
|
|
1735
1831
|
);
|
|
1736
1832
|
}
|
|
1737
1833
|
|
|
1834
|
+
// src/shell/slash-commands.ts
|
|
1835
|
+
init_api_utils();
|
|
1836
|
+
|
|
1738
1837
|
// src/shell/help-screen.ts
|
|
1739
1838
|
init_layout();
|
|
1740
1839
|
init_theme();
|
|
@@ -1759,9 +1858,9 @@ var SLASH_COMMANDS = [
|
|
|
1759
1858
|
{ name: "/exit", description: "Exit the shell", aliases: ["/quit"] }
|
|
1760
1859
|
];
|
|
1761
1860
|
function findCommand(input5) {
|
|
1762
|
-
const
|
|
1861
|
+
const cmd2 = input5.trim().split(/\s/)[0].toLowerCase();
|
|
1763
1862
|
return SLASH_COMMANDS.find(
|
|
1764
|
-
(c) => c.name ===
|
|
1863
|
+
(c) => c.name === cmd2 || c.aliases?.some((a) => a === cmd2)
|
|
1765
1864
|
);
|
|
1766
1865
|
}
|
|
1767
1866
|
|
|
@@ -1876,13 +1975,142 @@ function normalizeScanUrl(url) {
|
|
|
1876
1975
|
if (!/^https?:\/\//i.test(u)) return `https://${u}`;
|
|
1877
1976
|
return u;
|
|
1878
1977
|
}
|
|
1978
|
+
var SPINNER_LABELS = {
|
|
1979
|
+
"/scan": "Scanning target...",
|
|
1980
|
+
"/status": "Loading account status...",
|
|
1981
|
+
"/scans": "Loading scans...",
|
|
1982
|
+
"/keys": "Loading API keys...",
|
|
1983
|
+
"/config": "Updating configuration...",
|
|
1984
|
+
"/compare": "Comparing scans...",
|
|
1985
|
+
"/whatif": "Running simulation...",
|
|
1986
|
+
"/report": "Fetching report...",
|
|
1987
|
+
"/auth": "Signing in...",
|
|
1988
|
+
"/auth-key": "Validating API key...",
|
|
1989
|
+
"/login": "Signing in...",
|
|
1990
|
+
"/changelog": "Loading changelog...",
|
|
1991
|
+
"/history": "Loading history...",
|
|
1992
|
+
"/palette": "Opening command palette...",
|
|
1993
|
+
"/commands": "Opening command palette..."
|
|
1994
|
+
};
|
|
1995
|
+
function spinnerLabel(cmd2) {
|
|
1996
|
+
return SPINNER_LABELS[cmd2.toLowerCase()] ?? null;
|
|
1997
|
+
}
|
|
1998
|
+
async function executeSlashCommand(cmd2, arg, ctx) {
|
|
1999
|
+
switch (cmd2.toLowerCase()) {
|
|
2000
|
+
case "/help":
|
|
2001
|
+
ctx.sink.write(renderHelpScreen());
|
|
2002
|
+
break;
|
|
2003
|
+
case "/palette":
|
|
2004
|
+
case "/commands": {
|
|
2005
|
+
const picked = await openCommandPalette();
|
|
2006
|
+
if (picked) return dispatchSlashCommand(picked, ctx);
|
|
2007
|
+
break;
|
|
2008
|
+
}
|
|
2009
|
+
case "/clear":
|
|
2010
|
+
return { redraw: true };
|
|
2011
|
+
case "/history": {
|
|
2012
|
+
const hist = await loadHistory();
|
|
2013
|
+
ctx.sink.write(renderHistoryScreen(hist));
|
|
2014
|
+
break;
|
|
2015
|
+
}
|
|
2016
|
+
case "/scan": {
|
|
2017
|
+
if (!arg) {
|
|
2018
|
+
ctx.sink.write(error("Usage: /scan <url>"));
|
|
2019
|
+
break;
|
|
2020
|
+
}
|
|
2021
|
+
const client = await getAuthenticatedClient();
|
|
2022
|
+
const progress = createStreamProgress(ctx.sink);
|
|
2023
|
+
const result = await runScanCommand(client, {
|
|
2024
|
+
url: normalizeScanUrl(arg),
|
|
2025
|
+
wait: true,
|
|
2026
|
+
onProgress: progress.update,
|
|
2027
|
+
streamProgress: true
|
|
2028
|
+
});
|
|
2029
|
+
progress.stop();
|
|
2030
|
+
ctx.sink.write(result.output);
|
|
2031
|
+
break;
|
|
2032
|
+
}
|
|
2033
|
+
case "/status": {
|
|
2034
|
+
const client = await getAuthenticatedClient();
|
|
2035
|
+
const status = await runStatusCommand(client);
|
|
2036
|
+
const rate = await runRateLimitStatus(client);
|
|
2037
|
+
ctx.sink.write(rate ? `${status}
|
|
2038
|
+
|
|
2039
|
+
${muted("Rate limit")} ${rate}` : status);
|
|
2040
|
+
break;
|
|
2041
|
+
}
|
|
2042
|
+
case "/scans":
|
|
2043
|
+
await runScansInteractive(ctx);
|
|
2044
|
+
break;
|
|
2045
|
+
case "/keys":
|
|
2046
|
+
await runKeysInteractive(ctx);
|
|
2047
|
+
break;
|
|
2048
|
+
case "/config": {
|
|
2049
|
+
if (!arg.includes("=")) {
|
|
2050
|
+
ctx.sink.write(error("Usage: /config key=value"));
|
|
2051
|
+
break;
|
|
2052
|
+
}
|
|
2053
|
+
const [k, ...vParts] = arg.split("=");
|
|
2054
|
+
ctx.sink.write(await runConfigSet(k.trim(), vParts.join("=").trim()));
|
|
2055
|
+
break;
|
|
2056
|
+
}
|
|
2057
|
+
case "/changelog": {
|
|
2058
|
+
const raw = await readAsset("changelog.json");
|
|
2059
|
+
ctx.sink.write(renderChangelog(JSON.parse(raw)));
|
|
2060
|
+
break;
|
|
2061
|
+
}
|
|
2062
|
+
case "/compare": {
|
|
2063
|
+
const [a, b] = arg.split(/\s+/);
|
|
2064
|
+
if (!a || !b) {
|
|
2065
|
+
ctx.sink.write(error("Usage: /compare <scan-a> <scan-b>"));
|
|
2066
|
+
break;
|
|
2067
|
+
}
|
|
2068
|
+
const { runCompareCommand: runCompareCommand2 } = await Promise.resolve().then(() => (init_compare(), compare_exports));
|
|
2069
|
+
ctx.sink.write(await runCompareCommand2(a, b, "table"));
|
|
2070
|
+
break;
|
|
2071
|
+
}
|
|
2072
|
+
case "/whatif": {
|
|
2073
|
+
if (!arg) {
|
|
2074
|
+
ctx.sink.write(error("Usage: /whatif <scan-id>"));
|
|
2075
|
+
break;
|
|
2076
|
+
}
|
|
2077
|
+
const { runWhatIfCommand: runWhatIfCommand2 } = await Promise.resolve().then(() => (init_whatif(), whatif_exports));
|
|
2078
|
+
ctx.sink.write(await runWhatIfCommand2(arg, {}, "table"));
|
|
2079
|
+
break;
|
|
2080
|
+
}
|
|
2081
|
+
case "/report": {
|
|
2082
|
+
if (!arg) {
|
|
2083
|
+
ctx.sink.write(error("Usage: /report <scan-id>"));
|
|
2084
|
+
break;
|
|
2085
|
+
}
|
|
2086
|
+
const { runReportCommand: runReportCommand2 } = await Promise.resolve().then(() => (init_report(), report_exports));
|
|
2087
|
+
const client = await getAuthenticatedClient();
|
|
2088
|
+
ctx.sink.write(await runReportCommand2(client, arg, "json"));
|
|
2089
|
+
break;
|
|
2090
|
+
}
|
|
2091
|
+
case "/login":
|
|
2092
|
+
case "/auth": {
|
|
2093
|
+
const result = await runAssistedBrowserLogin();
|
|
2094
|
+
ctx.sink.write(result.message);
|
|
2095
|
+
return { profile: result.profile, redraw: true };
|
|
2096
|
+
}
|
|
2097
|
+
case "/auth-key": {
|
|
2098
|
+
ctx.sink.write(await runApiKeyLogin({ interactive: true }));
|
|
2099
|
+
const client = await getAuthenticatedClient();
|
|
2100
|
+
return { profile: await client.get("/me"), redraw: true };
|
|
2101
|
+
}
|
|
2102
|
+
default:
|
|
2103
|
+
ctx.sink.write(errorBox(`Unknown command: ${cmd2}`, "Type /help"));
|
|
2104
|
+
}
|
|
2105
|
+
return {};
|
|
2106
|
+
}
|
|
1879
2107
|
async function dispatchSlashCommand(line, ctx) {
|
|
1880
2108
|
const trimmed = line.trim();
|
|
1881
2109
|
if (!trimmed) return {};
|
|
1882
2110
|
if (trimmed === "/exit" || trimmed === "/quit") return { exit: true };
|
|
1883
|
-
const [
|
|
2111
|
+
const [cmd2, ...rest] = trimmed.split(/\s+/);
|
|
1884
2112
|
const arg = rest.join(" ").trim();
|
|
1885
|
-
if (!
|
|
2113
|
+
if (!cmd2.startsWith("/")) {
|
|
1886
2114
|
ctx.sink.write(
|
|
1887
2115
|
errorBox(
|
|
1888
2116
|
`Not a shell command: ${trimmed}`,
|
|
@@ -1892,117 +2120,16 @@ ${accent("/help")} lists everything`
|
|
|
1892
2120
|
);
|
|
1893
2121
|
return {};
|
|
1894
2122
|
}
|
|
1895
|
-
if (!findCommand(
|
|
1896
|
-
ctx.sink.write(errorBox(`Unknown command: ${
|
|
2123
|
+
if (!findCommand(cmd2) && cmd2.startsWith("/")) {
|
|
2124
|
+
ctx.sink.write(errorBox(`Unknown command: ${cmd2}`, "Type /help or /palette"));
|
|
1897
2125
|
return {};
|
|
1898
2126
|
}
|
|
1899
2127
|
try {
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
break;
|
|
1904
|
-
case "/palette":
|
|
1905
|
-
case "/commands": {
|
|
1906
|
-
const picked = await openCommandPalette();
|
|
1907
|
-
if (picked) return dispatchSlashCommand(picked, ctx);
|
|
1908
|
-
break;
|
|
1909
|
-
}
|
|
1910
|
-
case "/clear":
|
|
1911
|
-
return { redraw: true };
|
|
1912
|
-
case "/history": {
|
|
1913
|
-
const hist = await loadHistory();
|
|
1914
|
-
ctx.sink.write(renderHistoryScreen(hist));
|
|
1915
|
-
break;
|
|
1916
|
-
}
|
|
1917
|
-
case "/scan": {
|
|
1918
|
-
if (!arg) {
|
|
1919
|
-
ctx.sink.write(error("Usage: /scan <url>"));
|
|
1920
|
-
break;
|
|
1921
|
-
}
|
|
1922
|
-
const client = await getAuthenticatedClient();
|
|
1923
|
-
const progress = createStreamProgress(ctx.sink);
|
|
1924
|
-
const result = await runScanCommand(client, {
|
|
1925
|
-
url: normalizeScanUrl(arg),
|
|
1926
|
-
wait: true,
|
|
1927
|
-
onProgress: progress.update,
|
|
1928
|
-
streamProgress: true
|
|
1929
|
-
});
|
|
1930
|
-
progress.stop();
|
|
1931
|
-
ctx.sink.write(result.output);
|
|
1932
|
-
break;
|
|
1933
|
-
}
|
|
1934
|
-
case "/status": {
|
|
1935
|
-
const client = await getAuthenticatedClient();
|
|
1936
|
-
const status = await runStatusCommand(client);
|
|
1937
|
-
const rate = await runRateLimitStatus(client);
|
|
1938
|
-
ctx.sink.write(rate ? `${status}
|
|
1939
|
-
|
|
1940
|
-
${muted("Rate limit")} ${rate}` : status);
|
|
1941
|
-
break;
|
|
1942
|
-
}
|
|
1943
|
-
case "/scans":
|
|
1944
|
-
await runScansInteractive(ctx);
|
|
1945
|
-
break;
|
|
1946
|
-
case "/keys":
|
|
1947
|
-
await runKeysInteractive(ctx);
|
|
1948
|
-
break;
|
|
1949
|
-
case "/config": {
|
|
1950
|
-
if (!arg.includes("=")) {
|
|
1951
|
-
ctx.sink.write(error("Usage: /config key=value"));
|
|
1952
|
-
break;
|
|
1953
|
-
}
|
|
1954
|
-
const [k, ...vParts] = arg.split("=");
|
|
1955
|
-
ctx.sink.write(await runConfigSet(k.trim(), vParts.join("=").trim()));
|
|
1956
|
-
break;
|
|
1957
|
-
}
|
|
1958
|
-
case "/changelog": {
|
|
1959
|
-
const raw = await readAsset("changelog.json");
|
|
1960
|
-
ctx.sink.write(renderChangelog(JSON.parse(raw)));
|
|
1961
|
-
break;
|
|
1962
|
-
}
|
|
1963
|
-
case "/compare": {
|
|
1964
|
-
const [a, b] = arg.split(/\s+/);
|
|
1965
|
-
if (!a || !b) {
|
|
1966
|
-
ctx.sink.write(error("Usage: /compare <scan-a> <scan-b>"));
|
|
1967
|
-
break;
|
|
1968
|
-
}
|
|
1969
|
-
const { runCompareCommand: runCompareCommand2 } = await Promise.resolve().then(() => (init_compare(), compare_exports));
|
|
1970
|
-
ctx.sink.write(await runCompareCommand2(a, b, "table"));
|
|
1971
|
-
break;
|
|
1972
|
-
}
|
|
1973
|
-
case "/whatif": {
|
|
1974
|
-
if (!arg) {
|
|
1975
|
-
ctx.sink.write(error("Usage: /whatif <scan-id>"));
|
|
1976
|
-
break;
|
|
1977
|
-
}
|
|
1978
|
-
const { runWhatIfCommand: runWhatIfCommand2 } = await Promise.resolve().then(() => (init_whatif(), whatif_exports));
|
|
1979
|
-
ctx.sink.write(await runWhatIfCommand2(arg, {}, "table"));
|
|
1980
|
-
break;
|
|
1981
|
-
}
|
|
1982
|
-
case "/report": {
|
|
1983
|
-
if (!arg) {
|
|
1984
|
-
ctx.sink.write(error("Usage: /report <scan-id>"));
|
|
1985
|
-
break;
|
|
1986
|
-
}
|
|
1987
|
-
const { runReportCommand: runReportCommand2 } = await Promise.resolve().then(() => (init_report(), report_exports));
|
|
1988
|
-
const client = await getAuthenticatedClient();
|
|
1989
|
-
ctx.sink.write(await runReportCommand2(client, arg, "json"));
|
|
1990
|
-
break;
|
|
1991
|
-
}
|
|
1992
|
-
case "/login":
|
|
1993
|
-
case "/auth": {
|
|
1994
|
-
ctx.sink.write(await runAuthLogin({ interactive: true, browser: true }));
|
|
1995
|
-
const client = await getAuthenticatedClient();
|
|
1996
|
-
return { profile: await client.get("/me"), redraw: true };
|
|
1997
|
-
}
|
|
1998
|
-
case "/auth-key": {
|
|
1999
|
-
ctx.sink.write(await runApiKeyLogin({ interactive: true }));
|
|
2000
|
-
const client = await getAuthenticatedClient();
|
|
2001
|
-
return { profile: await client.get("/me"), redraw: true };
|
|
2002
|
-
}
|
|
2003
|
-
default:
|
|
2004
|
-
ctx.sink.write(errorBox(`Unknown command: ${cmd}`, "Type /help"));
|
|
2128
|
+
const label = spinnerLabel(cmd2);
|
|
2129
|
+
if (label) {
|
|
2130
|
+
return await withSpinner(label, () => executeSlashCommand(cmd2, arg, ctx));
|
|
2005
2131
|
}
|
|
2132
|
+
return await executeSlashCommand(cmd2, arg, ctx);
|
|
2006
2133
|
} catch (e) {
|
|
2007
2134
|
ctx.sink.error(formatApiError(e));
|
|
2008
2135
|
}
|
|
@@ -2027,7 +2154,7 @@ async function runScansInteractive(ctx) {
|
|
|
2027
2154
|
while (true) {
|
|
2028
2155
|
const list = await runScansListCommand(client, "table", { limit: 20 });
|
|
2029
2156
|
const data = await client.get("/scans?page=1&page_size=20");
|
|
2030
|
-
const scans = data
|
|
2157
|
+
const scans = asList(data);
|
|
2031
2158
|
if (!scans.length) {
|
|
2032
2159
|
ctx.sink.write(emptyState("No scans yet", "Run /scan <url> to create your first scan", "/scan https://example.com"));
|
|
2033
2160
|
return;
|
|
@@ -2044,10 +2171,12 @@ async function runScansInteractive(ctx) {
|
|
|
2044
2171
|
if (!scanId) return;
|
|
2045
2172
|
const detail = await client.get(`/scan/${scanId}`);
|
|
2046
2173
|
ctx.sink.write(renderScanResult(detail, "table"));
|
|
2047
|
-
const cont = await
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2174
|
+
const cont = await runInteractive(
|
|
2175
|
+
() => input2({
|
|
2176
|
+
message: muted("Enter = back to list \xB7 q = exit"),
|
|
2177
|
+
default: ""
|
|
2178
|
+
})
|
|
2179
|
+
).catch(() => "q");
|
|
2051
2180
|
if (cont.toLowerCase() === "q") return;
|
|
2052
2181
|
}
|
|
2053
2182
|
}
|
|
@@ -2055,14 +2184,16 @@ async function runKeysInteractive(ctx) {
|
|
|
2055
2184
|
const client = await getAuthenticatedClient();
|
|
2056
2185
|
ctx.sink.write(await runKeysListCommand(client));
|
|
2057
2186
|
ctx.sink.write(muted("\nActions: [r] revoke [n] new key [Enter] back"));
|
|
2058
|
-
const action = await input2({ message: "Action", default: "" }).catch(() => "");
|
|
2187
|
+
const action = await runInteractive(() => input2({ message: "Action", default: "" })).catch(() => "");
|
|
2059
2188
|
if (action.toLowerCase() === "r") {
|
|
2060
2189
|
const keyId = await pickKeyForRevoke(client);
|
|
2061
2190
|
if (!keyId) return;
|
|
2062
|
-
const ok = await
|
|
2191
|
+
const ok = await runInteractive(
|
|
2192
|
+
() => confirm2({ message: "Revoke this API key?", default: false })
|
|
2193
|
+
).catch(() => false);
|
|
2063
2194
|
if (ok) ctx.sink.write(successBox(await runKeyRevoke(client, keyId)));
|
|
2064
2195
|
} else if (action.toLowerCase() === "n") {
|
|
2065
|
-
const name = await promptNewKeyName();
|
|
2196
|
+
const name = await runInteractive(() => promptNewKeyName());
|
|
2066
2197
|
const created = await client.post("/keys", { name });
|
|
2067
2198
|
ctx.sink.write(success(`Created: ${created.name} (${created.prefix})`));
|
|
2068
2199
|
ctx.sink.write(accent("Save this key \u2014 it won't be shown again:"));
|
|
@@ -2072,7 +2203,6 @@ async function runKeysInteractive(ctx) {
|
|
|
2072
2203
|
|
|
2073
2204
|
// src/shell/prompt-loop.ts
|
|
2074
2205
|
init_theme();
|
|
2075
|
-
import { cwd as cwd3 } from "process";
|
|
2076
2206
|
|
|
2077
2207
|
// src/shell/completer.ts
|
|
2078
2208
|
function buildCompleter(commands, history) {
|
|
@@ -2090,8 +2220,6 @@ function buildCompleter(commands, history) {
|
|
|
2090
2220
|
|
|
2091
2221
|
// src/shell/prompt-loop.ts
|
|
2092
2222
|
init_capabilities();
|
|
2093
|
-
init_layout();
|
|
2094
|
-
init_logger();
|
|
2095
2223
|
async function runPromptLoop(initialProfile) {
|
|
2096
2224
|
let profile = initialProfile;
|
|
2097
2225
|
const workDir = cwd3();
|
|
@@ -2100,13 +2228,8 @@ async function runPromptLoop(initialProfile) {
|
|
|
2100
2228
|
if (getCapabilities().interactive) {
|
|
2101
2229
|
process.stdout.write("\x1B[2J\x1B[H");
|
|
2102
2230
|
}
|
|
2103
|
-
const data = await fetchHomeData(profile
|
|
2231
|
+
const data = await fetchHomeData(profile);
|
|
2104
2232
|
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
2233
|
console.log("");
|
|
2111
2234
|
};
|
|
2112
2235
|
await refresh();
|
|
@@ -2122,7 +2245,14 @@ async function runPromptLoop(initialProfile) {
|
|
|
2122
2245
|
rl.close();
|
|
2123
2246
|
process.exit(0);
|
|
2124
2247
|
});
|
|
2125
|
-
|
|
2248
|
+
setReadlineHooks(
|
|
2249
|
+
() => rl.pause(),
|
|
2250
|
+
() => {
|
|
2251
|
+
process.stdout.write("\n");
|
|
2252
|
+
rl.resume();
|
|
2253
|
+
}
|
|
2254
|
+
);
|
|
2255
|
+
const prompt = () => process.stdout.write(accent("> "));
|
|
2126
2256
|
prompt();
|
|
2127
2257
|
for await (const line of rl) {
|
|
2128
2258
|
await appendHistory(line);
|
|
@@ -2151,73 +2281,48 @@ async function runPromptLoop(initialProfile) {
|
|
|
2151
2281
|
}
|
|
2152
2282
|
|
|
2153
2283
|
// src/shell/auth-gate.ts
|
|
2154
|
-
init_layout();
|
|
2155
|
-
init_theme();
|
|
2156
|
-
init_width();
|
|
2157
2284
|
import * as readline2 from "readline";
|
|
2158
2285
|
import { stdin as input4, stdout as output2 } from "process";
|
|
2286
|
+
init_theme();
|
|
2159
2287
|
|
|
2160
2288
|
// src/cli/profile.ts
|
|
2161
2289
|
init_credentials();
|
|
2162
2290
|
init_api_client();
|
|
2163
2291
|
init_logger();
|
|
2292
|
+
function profileFromCredentials(creds) {
|
|
2293
|
+
return {
|
|
2294
|
+
id: "local",
|
|
2295
|
+
email: creds.email || "user@local",
|
|
2296
|
+
full_name: creds.full_name || null,
|
|
2297
|
+
role: "analyst",
|
|
2298
|
+
organization: "\u2014",
|
|
2299
|
+
key_expires_at: creds.key_expires_at ?? null,
|
|
2300
|
+
key_prefix: creds.key_prefix ?? null
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
2164
2303
|
async function tryGetProfile() {
|
|
2304
|
+
const creds = await loadCredentials();
|
|
2305
|
+
if (!creds?.api_key) return null;
|
|
2165
2306
|
try {
|
|
2166
|
-
const
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
return await client.get("/me");
|
|
2307
|
+
const client = await FuzziApiClient.create();
|
|
2308
|
+
const profile = await client.get("/me");
|
|
2309
|
+
return profile;
|
|
2170
2310
|
} catch (e) {
|
|
2171
|
-
|
|
2172
|
-
|
|
2311
|
+
if (e instanceof ApiError && e.status === 401) {
|
|
2312
|
+
log.debug("stored credentials invalid, clearing");
|
|
2313
|
+
await clearCredentials();
|
|
2314
|
+
return null;
|
|
2315
|
+
}
|
|
2316
|
+
log.debug("profile bootstrap failed, using cached credentials", e);
|
|
2317
|
+
return profileFromCredentials(creds);
|
|
2173
2318
|
}
|
|
2174
2319
|
}
|
|
2175
2320
|
|
|
2176
2321
|
// 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
2322
|
function waitForEnter() {
|
|
2218
2323
|
return new Promise((resolve) => {
|
|
2219
2324
|
const rl = readline2.createInterface({ input: input4, output: output2, terminal: true });
|
|
2220
|
-
output2.write(accent("\n
|
|
2325
|
+
output2.write(accent("\n> Press Enter to open browser... "));
|
|
2221
2326
|
rl.once("line", () => {
|
|
2222
2327
|
rl.close();
|
|
2223
2328
|
resolve();
|
|
@@ -2228,18 +2333,15 @@ async function runAuthGate() {
|
|
|
2228
2333
|
const existing = await tryGetProfile();
|
|
2229
2334
|
if (existing) return existing;
|
|
2230
2335
|
if (!output2.isTTY) return null;
|
|
2231
|
-
console.log(
|
|
2336
|
+
console.log(renderAuthGateScreen());
|
|
2232
2337
|
await waitForEnter();
|
|
2233
|
-
const progress = createProgress("Opening browser...");
|
|
2234
2338
|
try {
|
|
2235
|
-
const result = await
|
|
2236
|
-
|
|
2237
|
-
console.log(accent(result.message));
|
|
2339
|
+
const result = await runAssistedBrowserLogin();
|
|
2340
|
+
console.log(result.message);
|
|
2238
2341
|
return result.profile;
|
|
2239
2342
|
} catch (e) {
|
|
2240
|
-
progress.fail("Sign-in failed");
|
|
2241
2343
|
console.log(muted(formatApiError(e)));
|
|
2242
|
-
console.log(muted("
|
|
2344
|
+
console.log(muted("Run /auth-key to paste your API key manually."));
|
|
2243
2345
|
return null;
|
|
2244
2346
|
}
|
|
2245
2347
|
}
|