@vibgrate/cli 1.0.30 → 1.0.32
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.
|
@@ -117,6 +117,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
117
117
|
"node_modules",
|
|
118
118
|
".git",
|
|
119
119
|
".vibgrate",
|
|
120
|
+
".wrangler",
|
|
120
121
|
".next",
|
|
121
122
|
"dist",
|
|
122
123
|
"build",
|
|
@@ -1015,6 +1016,11 @@ function formatExtended(ext) {
|
|
|
1015
1016
|
if (ss.heuristicFindings.length > 0) {
|
|
1016
1017
|
lines.push(` ${chalk.red("Potential secret signals")}: ${ss.heuristicFindings.length}`);
|
|
1017
1018
|
}
|
|
1019
|
+
const missingNames = tools.filter((t) => !t.available).map((t) => t.name);
|
|
1020
|
+
if (missingNames.length > 0) {
|
|
1021
|
+
lines.push(` ${chalk.dim("Install with:")} ${chalk.cyan(`brew install ${missingNames.join(" ")}`)}`);
|
|
1022
|
+
lines.push(` ${chalk.dim("Or run:")} ${chalk.cyan("vibgrate scan --install-tools")}`);
|
|
1023
|
+
}
|
|
1018
1024
|
lines.push("");
|
|
1019
1025
|
}
|
|
1020
1026
|
if (ext.platformMatrix) {
|
|
@@ -1643,7 +1649,7 @@ var pushCommand = new Command2("push").description("Push scan results to Vibgrat
|
|
|
1643
1649
|
// src/commands/scan.ts
|
|
1644
1650
|
import * as path20 from "path";
|
|
1645
1651
|
import { Command as Command3 } from "commander";
|
|
1646
|
-
import
|
|
1652
|
+
import chalk6 from "chalk";
|
|
1647
1653
|
|
|
1648
1654
|
// src/scanners/node-scanner.ts
|
|
1649
1655
|
import * as path5 from "path";
|
|
@@ -4317,6 +4323,7 @@ import * as path14 from "path";
|
|
|
4317
4323
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
4318
4324
|
"node_modules",
|
|
4319
4325
|
".git",
|
|
4326
|
+
".wrangler",
|
|
4320
4327
|
".next",
|
|
4321
4328
|
"dist",
|
|
4322
4329
|
"build",
|
|
@@ -5955,6 +5962,98 @@ async function scanOwaspCategoryMapping(rootDir, cache, options = {}, runner = r
|
|
|
5955
5962
|
};
|
|
5956
5963
|
}
|
|
5957
5964
|
|
|
5965
|
+
// src/utils/tool-installer.ts
|
|
5966
|
+
import { spawn as spawn4 } from "child_process";
|
|
5967
|
+
import chalk5 from "chalk";
|
|
5968
|
+
var SECURITY_TOOLS = [
|
|
5969
|
+
{ name: "semgrep", command: "semgrep", brew: "semgrep", winget: null, scoop: null, pip: "semgrep" },
|
|
5970
|
+
{ name: "gitleaks", command: "gitleaks", brew: "gitleaks", winget: "gitleaks.gitleaks", scoop: "gitleaks", pip: null },
|
|
5971
|
+
{ name: "trufflehog", command: "trufflehog", brew: "trufflehog", winget: null, scoop: "trufflehog", pip: null }
|
|
5972
|
+
];
|
|
5973
|
+
var IS_WIN = process.platform === "win32";
|
|
5974
|
+
function runCommand(cmd, args) {
|
|
5975
|
+
return new Promise((resolve9) => {
|
|
5976
|
+
const child = spawn4(cmd, args, {
|
|
5977
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
5978
|
+
shell: IS_WIN
|
|
5979
|
+
// required for .cmd/.ps1 wrappers on Windows
|
|
5980
|
+
});
|
|
5981
|
+
let stdout = "";
|
|
5982
|
+
let stderr = "";
|
|
5983
|
+
child.stdout.on("data", (d) => {
|
|
5984
|
+
stdout += d.toString();
|
|
5985
|
+
});
|
|
5986
|
+
child.stderr.on("data", (d) => {
|
|
5987
|
+
stderr += d.toString();
|
|
5988
|
+
});
|
|
5989
|
+
child.on("error", () => resolve9({ exitCode: 127, stdout, stderr }));
|
|
5990
|
+
child.on("close", (code) => resolve9({ exitCode: code ?? 1, stdout, stderr }));
|
|
5991
|
+
});
|
|
5992
|
+
}
|
|
5993
|
+
async function commandExists(command) {
|
|
5994
|
+
const checker = IS_WIN ? "where" : "which";
|
|
5995
|
+
const { exitCode } = await runCommand(checker, [command]);
|
|
5996
|
+
return exitCode === 0;
|
|
5997
|
+
}
|
|
5998
|
+
async function tryInstall(tool, strategies, log) {
|
|
5999
|
+
for (const { pm, args } of strategies) {
|
|
6000
|
+
log(chalk5.dim(` ${pm} ${args.join(" ")}\u2026`));
|
|
6001
|
+
const { exitCode, stderr } = await runCommand(pm, args);
|
|
6002
|
+
if (exitCode === 0) {
|
|
6003
|
+
log(` ${chalk5.green("\u2714")} ${tool.name} installed via ${pm}`);
|
|
6004
|
+
return { ok: true, pm };
|
|
6005
|
+
}
|
|
6006
|
+
const hint = stderr.split("\n").find((l) => l.trim().length > 0) ?? "";
|
|
6007
|
+
log(chalk5.dim(` ${pm} failed${hint ? `: ${hint.trim()}` : ""}`));
|
|
6008
|
+
}
|
|
6009
|
+
return { ok: false, pm: "" };
|
|
6010
|
+
}
|
|
6011
|
+
async function installMissingTools(log = (m) => process.stderr.write(m + "\n")) {
|
|
6012
|
+
const result = { installed: [], failed: [], skipped: [], packageManager: null };
|
|
6013
|
+
const missing = [];
|
|
6014
|
+
for (const tool of SECURITY_TOOLS) {
|
|
6015
|
+
if (!await commandExists(tool.command)) {
|
|
6016
|
+
missing.push(tool);
|
|
6017
|
+
}
|
|
6018
|
+
}
|
|
6019
|
+
if (missing.length === 0) return result;
|
|
6020
|
+
const [hasBrew, hasWinget, hasScoop, hasPipx, hasPip] = await Promise.all([
|
|
6021
|
+
commandExists("brew"),
|
|
6022
|
+
IS_WIN ? commandExists("winget") : Promise.resolve(false),
|
|
6023
|
+
IS_WIN ? commandExists("scoop") : Promise.resolve(false),
|
|
6024
|
+
commandExists("pipx"),
|
|
6025
|
+
commandExists("pip")
|
|
6026
|
+
]);
|
|
6027
|
+
log(chalk5.dim(` Installing ${missing.map((t) => t.name).join(", ")}\u2026`));
|
|
6028
|
+
for (const tool of missing) {
|
|
6029
|
+
const strategies = [];
|
|
6030
|
+
if (!IS_WIN) {
|
|
6031
|
+
if (hasBrew) strategies.push({ pm: "brew", args: ["install", tool.brew] });
|
|
6032
|
+
if (tool.pip && hasPipx) strategies.push({ pm: "pipx", args: ["install", tool.pip] });
|
|
6033
|
+
if (tool.pip && hasPip) strategies.push({ pm: "pip", args: ["install", tool.pip] });
|
|
6034
|
+
} else {
|
|
6035
|
+
if (tool.winget && hasWinget) strategies.push({ pm: "winget", args: ["install", "--id", tool.winget, "-e", "--accept-source-agreements", "--accept-package-agreements"] });
|
|
6036
|
+
if (tool.scoop && hasScoop) strategies.push({ pm: "scoop", args: ["install", tool.scoop] });
|
|
6037
|
+
if (tool.pip && hasPipx) strategies.push({ pm: "pipx", args: ["install", tool.pip] });
|
|
6038
|
+
if (tool.pip && hasPip) strategies.push({ pm: "pip", args: ["install", tool.pip] });
|
|
6039
|
+
}
|
|
6040
|
+
if (strategies.length === 0) {
|
|
6041
|
+
result.skipped.push(tool.name);
|
|
6042
|
+
log(` ${chalk5.yellow("\u26A0")} ${tool.name}: no supported package manager found`);
|
|
6043
|
+
continue;
|
|
6044
|
+
}
|
|
6045
|
+
const { ok, pm } = await tryInstall(tool, strategies, log);
|
|
6046
|
+
if (ok) {
|
|
6047
|
+
result.installed.push(tool.name);
|
|
6048
|
+
if (!result.packageManager) result.packageManager = pm;
|
|
6049
|
+
} else {
|
|
6050
|
+
result.failed.push(tool.name);
|
|
6051
|
+
log(` ${chalk5.red("\u2716")} ${tool.name}: all install methods failed`);
|
|
6052
|
+
}
|
|
6053
|
+
}
|
|
6054
|
+
return result;
|
|
6055
|
+
}
|
|
6056
|
+
|
|
5958
6057
|
// src/commands/scan.ts
|
|
5959
6058
|
async function runScan(rootDir, opts) {
|
|
5960
6059
|
const scanStart = Date.now();
|
|
@@ -6001,14 +6100,14 @@ async function runScan(rootDir, opts) {
|
|
|
6001
6100
|
progress.finish();
|
|
6002
6101
|
const msg = [
|
|
6003
6102
|
"",
|
|
6004
|
-
|
|
6103
|
+
chalk6.red.bold(" \u2716 Vibgrate cannot connect to the npm registry to check package versions."),
|
|
6005
6104
|
"",
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6105
|
+
chalk6.dim(" Possible causes:"),
|
|
6106
|
+
chalk6.dim(" \u2022 No internet connection"),
|
|
6107
|
+
chalk6.dim(" \u2022 Corporate proxy/firewall blocking registry.npmjs.org"),
|
|
6108
|
+
chalk6.dim(" \u2022 npm is not installed or not in PATH"),
|
|
6010
6109
|
"",
|
|
6011
|
-
|
|
6110
|
+
chalk6.dim(" Try running: ") + chalk6.cyan("npm view npm dist-tags.latest"),
|
|
6012
6111
|
""
|
|
6013
6112
|
].join("\n");
|
|
6014
6113
|
console.error(msg);
|
|
@@ -6123,6 +6222,13 @@ async function runScan(rootDir, opts) {
|
|
|
6123
6222
|
);
|
|
6124
6223
|
}
|
|
6125
6224
|
if (scanners?.securityScanners?.enabled !== false) {
|
|
6225
|
+
if (opts.installTools) {
|
|
6226
|
+
const installResult = await installMissingTools();
|
|
6227
|
+
if (installResult.installed.length > 0) {
|
|
6228
|
+
process.stderr.write(chalk6.dim(` Installed: ${installResult.installed.join(", ")}
|
|
6229
|
+
`));
|
|
6230
|
+
}
|
|
6231
|
+
}
|
|
6126
6232
|
progress.startStep("secscan");
|
|
6127
6233
|
scannerTasks.push(
|
|
6128
6234
|
scanSecurityScanners(rootDir, fileCache).then((result) => {
|
|
@@ -6265,35 +6371,35 @@ async function runScan(rootDir, opts) {
|
|
|
6265
6371
|
const skippedLarge = fileCache.skippedLargeFiles;
|
|
6266
6372
|
if (stuckPaths.length > 0) {
|
|
6267
6373
|
console.log(
|
|
6268
|
-
|
|
6374
|
+
chalk6.yellow(`
|
|
6269
6375
|
\u26A0 ${stuckPaths.length} path${stuckPaths.length === 1 ? "" : "s"} timed out (>60s) and ${stuckPaths.length === 1 ? "was" : "were"} skipped:`)
|
|
6270
6376
|
);
|
|
6271
6377
|
for (const d of stuckPaths) {
|
|
6272
|
-
console.log(
|
|
6378
|
+
console.log(chalk6.dim(` \u2192 ${d}`));
|
|
6273
6379
|
}
|
|
6274
6380
|
const newExcludes = stuckPaths.map((d) => `${d}/**`);
|
|
6275
6381
|
const updated = await appendExcludePatterns(rootDir, newExcludes);
|
|
6276
6382
|
if (updated) {
|
|
6277
|
-
console.log(
|
|
6383
|
+
console.log(chalk6.green("\u2714") + ` Added ${newExcludes.length} pattern${newExcludes.length !== 1 ? "s" : ""} to exclude list in config`);
|
|
6278
6384
|
}
|
|
6279
6385
|
}
|
|
6280
6386
|
if (skippedLarge.length > 0) {
|
|
6281
6387
|
const sizeLimit = config.maxFileSizeToScan ?? 5242880;
|
|
6282
6388
|
const sizeMB = (sizeLimit / 1048576).toFixed(0);
|
|
6283
6389
|
console.log(
|
|
6284
|
-
|
|
6390
|
+
chalk6.yellow(`
|
|
6285
6391
|
\u26A0 ${skippedLarge.length} file${skippedLarge.length === 1 ? "" : "s"} skipped (>${sizeMB} MB):`)
|
|
6286
6392
|
);
|
|
6287
6393
|
for (const f of skippedLarge.slice(0, 10)) {
|
|
6288
|
-
console.log(
|
|
6394
|
+
console.log(chalk6.dim(` \u2192 ${f}`));
|
|
6289
6395
|
}
|
|
6290
6396
|
if (skippedLarge.length > 10) {
|
|
6291
|
-
console.log(
|
|
6397
|
+
console.log(chalk6.dim(` \u2026 and ${skippedLarge.length - 10} more`));
|
|
6292
6398
|
}
|
|
6293
6399
|
}
|
|
6294
6400
|
fileCache.clear();
|
|
6295
6401
|
if (allProjects.length === 0) {
|
|
6296
|
-
console.log(
|
|
6402
|
+
console.log(chalk6.yellow("No projects found."));
|
|
6297
6403
|
}
|
|
6298
6404
|
if (extended.fileHotspots) filesScanned += extended.fileHotspots.totalFiles;
|
|
6299
6405
|
if (extended.securityPosture) filesScanned += 1;
|
|
@@ -6329,7 +6435,7 @@ async function runScan(rootDir, opts) {
|
|
|
6329
6435
|
artifact.baseline = baselinePath;
|
|
6330
6436
|
artifact.delta = artifact.drift.score - baseline.drift.score;
|
|
6331
6437
|
} catch {
|
|
6332
|
-
console.error(
|
|
6438
|
+
console.error(chalk6.yellow(`Warning: Could not read baseline file: ${baselinePath}`));
|
|
6333
6439
|
}
|
|
6334
6440
|
}
|
|
6335
6441
|
}
|
|
@@ -6366,7 +6472,7 @@ async function runScan(rootDir, opts) {
|
|
|
6366
6472
|
const jsonStr = JSON.stringify(artifact, null, 2);
|
|
6367
6473
|
if (opts.out) {
|
|
6368
6474
|
await writeTextFile(path20.resolve(opts.out), jsonStr);
|
|
6369
|
-
console.log(
|
|
6475
|
+
console.log(chalk6.green("\u2714") + ` JSON written to ${opts.out}`);
|
|
6370
6476
|
} else {
|
|
6371
6477
|
console.log(jsonStr);
|
|
6372
6478
|
}
|
|
@@ -6375,7 +6481,7 @@ async function runScan(rootDir, opts) {
|
|
|
6375
6481
|
const sarifStr = JSON.stringify(sarif, null, 2);
|
|
6376
6482
|
if (opts.out) {
|
|
6377
6483
|
await writeTextFile(path20.resolve(opts.out), sarifStr);
|
|
6378
|
-
console.log(
|
|
6484
|
+
console.log(chalk6.green("\u2714") + ` SARIF written to ${opts.out}`);
|
|
6379
6485
|
} else {
|
|
6380
6486
|
console.log(sarifStr);
|
|
6381
6487
|
}
|
|
@@ -6391,14 +6497,14 @@ async function runScan(rootDir, opts) {
|
|
|
6391
6497
|
async function autoPush(artifact, rootDir, opts) {
|
|
6392
6498
|
const dsn = opts.dsn || process.env.VIBGRATE_DSN;
|
|
6393
6499
|
if (!dsn) {
|
|
6394
|
-
console.error(
|
|
6395
|
-
console.error(
|
|
6500
|
+
console.error(chalk6.red("No DSN provided for push."));
|
|
6501
|
+
console.error(chalk6.dim("Set VIBGRATE_DSN environment variable or use --dsn flag."));
|
|
6396
6502
|
if (opts.strict) process.exit(1);
|
|
6397
6503
|
return;
|
|
6398
6504
|
}
|
|
6399
6505
|
const parsed = parseDsn(dsn);
|
|
6400
6506
|
if (!parsed) {
|
|
6401
|
-
console.error(
|
|
6507
|
+
console.error(chalk6.red("Invalid DSN format."));
|
|
6402
6508
|
if (opts.strict) process.exit(1);
|
|
6403
6509
|
return;
|
|
6404
6510
|
}
|
|
@@ -6409,13 +6515,13 @@ async function autoPush(artifact, rootDir, opts) {
|
|
|
6409
6515
|
try {
|
|
6410
6516
|
host = resolveIngestHost(opts.region);
|
|
6411
6517
|
} catch (e) {
|
|
6412
|
-
console.error(
|
|
6518
|
+
console.error(chalk6.red(e instanceof Error ? e.message : String(e)));
|
|
6413
6519
|
if (opts.strict) process.exit(1);
|
|
6414
6520
|
return;
|
|
6415
6521
|
}
|
|
6416
6522
|
}
|
|
6417
6523
|
const url = `${parsed.scheme}://${host}/v1/ingest/scan`;
|
|
6418
|
-
console.log(
|
|
6524
|
+
console.log(chalk6.dim(`Uploading to ${host}...`));
|
|
6419
6525
|
try {
|
|
6420
6526
|
const response = await fetch(url, {
|
|
6421
6527
|
method: "POST",
|
|
@@ -6431,20 +6537,20 @@ async function autoPush(artifact, rootDir, opts) {
|
|
|
6431
6537
|
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
6432
6538
|
}
|
|
6433
6539
|
const result = await response.json();
|
|
6434
|
-
console.log(
|
|
6540
|
+
console.log(chalk6.green("\u2714") + ` Uploaded successfully (${result.ingestId ?? "ok"})`);
|
|
6435
6541
|
if (result.ingestId) {
|
|
6436
|
-
console.log(
|
|
6542
|
+
console.log(chalk6.dim(` See Dashboard for full report: `) + chalk6.cyan(`https://dash.vibgrate.com/${parsed.workspaceId}/scan/${result.ingestId}`));
|
|
6437
6543
|
}
|
|
6438
6544
|
} catch (e) {
|
|
6439
6545
|
const msg = e instanceof Error ? e.message : String(e);
|
|
6440
|
-
console.error(
|
|
6546
|
+
console.error(chalk6.red(`Upload failed: ${msg}`));
|
|
6441
6547
|
if (opts.strict) process.exit(1);
|
|
6442
6548
|
}
|
|
6443
6549
|
}
|
|
6444
|
-
var scanCommand = new Command3("scan").description("Scan a project for upgrade drift").argument("[path]", "Path to scan", ".").option("--out <file>", "Output file path").option("--format <format>", "Output format (text|json|sarif)", "text").option("--fail-on <level>", "Fail on warn or error").option("--baseline <file>", "Compare against baseline").option("--changed-only", "Only scan changed files").option("--concurrency <n>", "Max concurrent npm calls", "8").option("--push", "Auto-push results to Vibgrate API after scan").option("--dsn <dsn>", "DSN token for push (or use VIBGRATE_DSN env)").option("--region <region>", "Override data residency region for push (us, eu)").option("--strict", "Fail on push errors").action(async (targetPath, opts) => {
|
|
6550
|
+
var scanCommand = new Command3("scan").description("Scan a project for upgrade drift").argument("[path]", "Path to scan", ".").option("--out <file>", "Output file path").option("--format <format>", "Output format (text|json|sarif)", "text").option("--fail-on <level>", "Fail on warn or error").option("--baseline <file>", "Compare against baseline").option("--changed-only", "Only scan changed files").option("--concurrency <n>", "Max concurrent npm calls", "8").option("--push", "Auto-push results to Vibgrate API after scan").option("--dsn <dsn>", "DSN token for push (or use VIBGRATE_DSN env)").option("--region <region>", "Override data residency region for push (us, eu)").option("--strict", "Fail on push errors").option("--install-tools", "Auto-install missing security scanners via Homebrew").action(async (targetPath, opts) => {
|
|
6445
6551
|
const rootDir = path20.resolve(targetPath);
|
|
6446
6552
|
if (!await pathExists(rootDir)) {
|
|
6447
|
-
console.error(
|
|
6553
|
+
console.error(chalk6.red(`Path does not exist: ${rootDir}`));
|
|
6448
6554
|
process.exit(1);
|
|
6449
6555
|
}
|
|
6450
6556
|
const scanOpts = {
|
|
@@ -6457,19 +6563,20 @@ var scanCommand = new Command3("scan").description("Scan a project for upgrade d
|
|
|
6457
6563
|
push: opts.push,
|
|
6458
6564
|
dsn: opts.dsn,
|
|
6459
6565
|
region: opts.region,
|
|
6460
|
-
strict: opts.strict
|
|
6566
|
+
strict: opts.strict,
|
|
6567
|
+
installTools: opts.installTools
|
|
6461
6568
|
};
|
|
6462
6569
|
const artifact = await runScan(rootDir, scanOpts);
|
|
6463
6570
|
if (opts.failOn) {
|
|
6464
6571
|
const hasErrors = artifact.findings.some((f) => f.level === "error");
|
|
6465
6572
|
const hasWarnings = artifact.findings.some((f) => f.level === "warning");
|
|
6466
6573
|
if (opts.failOn === "error" && hasErrors) {
|
|
6467
|
-
console.error(
|
|
6574
|
+
console.error(chalk6.red(`
|
|
6468
6575
|
Failing: ${artifact.findings.filter((f) => f.level === "error").length} error finding(s) detected.`));
|
|
6469
6576
|
process.exit(2);
|
|
6470
6577
|
}
|
|
6471
6578
|
if (opts.failOn === "warn" && (hasErrors || hasWarnings)) {
|
|
6472
|
-
console.error(
|
|
6579
|
+
console.error(chalk6.red(`
|
|
6473
6580
|
Failing: findings detected at warn level or above.`));
|
|
6474
6581
|
process.exit(2);
|
|
6475
6582
|
}
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "./chunk-GN3IWKSY.js";
|
|
5
5
|
import {
|
|
6
6
|
baselineCommand
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-GVVGEQ3G.js";
|
|
8
8
|
import {
|
|
9
9
|
VERSION,
|
|
10
10
|
dsnCommand,
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
readJsonFile,
|
|
16
16
|
scanCommand,
|
|
17
17
|
writeDefaultConfig
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-NBHQS66G.js";
|
|
19
19
|
|
|
20
20
|
// src/cli.ts
|
|
21
21
|
import { Command as Command4 } from "commander";
|
|
@@ -38,7 +38,7 @@ var initCommand = new Command("init").description("Initialize vibgrate in a proj
|
|
|
38
38
|
console.log(chalk.green("\u2714") + ` Created ${chalk.bold("vibgrate.config.ts")}`);
|
|
39
39
|
}
|
|
40
40
|
if (opts.baseline) {
|
|
41
|
-
const { runBaseline } = await import("./baseline-
|
|
41
|
+
const { runBaseline } = await import("./baseline-LGEXW6IN.js");
|
|
42
42
|
await runBaseline(rootDir);
|
|
43
43
|
}
|
|
44
44
|
console.log("");
|
package/dist/index.d.ts
CHANGED
|
@@ -103,6 +103,8 @@ interface ScanOptions {
|
|
|
103
103
|
region?: string;
|
|
104
104
|
/** Fail on push errors (like --strict on push command) */
|
|
105
105
|
strict?: boolean;
|
|
106
|
+
/** Auto-install missing security tools via Homebrew */
|
|
107
|
+
installTools?: boolean;
|
|
106
108
|
}
|
|
107
109
|
interface ScannerToggle {
|
|
108
110
|
enabled: boolean;
|
package/dist/index.js
CHANGED