@vibedrift/cli 0.1.4 → 0.3.0
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 +53 -17
- package/dist/index.js +1290 -67
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -230,12 +230,12 @@ function normalizeBody(body, language) {
|
|
|
230
230
|
}
|
|
231
231
|
const sortedVars = [...varNames.entries()].sort((a, b) => b[0].length - a[0].length);
|
|
232
232
|
for (const [name, placeholder] of sortedVars) {
|
|
233
|
-
normalized = normalized.replace(new RegExp(`\\b${
|
|
233
|
+
normalized = normalized.replace(new RegExp(`\\b${escapeRegex3(name)}\\b`, "g"), placeholder);
|
|
234
234
|
}
|
|
235
235
|
normalized = normalized.replace(/\s+/g, " ").trim();
|
|
236
236
|
return normalized;
|
|
237
237
|
}
|
|
238
|
-
function
|
|
238
|
+
function escapeRegex3(s) {
|
|
239
239
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
240
240
|
}
|
|
241
241
|
function fnv1aHash(str) {
|
|
@@ -1012,13 +1012,13 @@ var init_sampler = __esm({
|
|
|
1012
1012
|
});
|
|
1013
1013
|
|
|
1014
1014
|
// src/ml-client/client.ts
|
|
1015
|
-
async function callMlApi(request,
|
|
1015
|
+
async function callMlApi(request, token, apiUrl) {
|
|
1016
1016
|
const url = `${apiUrl ?? DEFAULT_API_URL}/v1/analyze`;
|
|
1017
1017
|
const headers = {
|
|
1018
1018
|
"Content-Type": "application/json"
|
|
1019
1019
|
};
|
|
1020
|
-
if (
|
|
1021
|
-
headers["
|
|
1020
|
+
if (token) {
|
|
1021
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
1022
1022
|
}
|
|
1023
1023
|
const controller = new AbortController();
|
|
1024
1024
|
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
@@ -1031,7 +1031,7 @@ async function callMlApi(request, apiKey, apiUrl) {
|
|
|
1031
1031
|
});
|
|
1032
1032
|
if (!response.ok) {
|
|
1033
1033
|
const errorBody = await response.text().catch(() => "");
|
|
1034
|
-
throw new Error(`
|
|
1034
|
+
throw new Error(`Deep-analysis API error ${response.status}: ${errorBody.slice(0, 200)}`);
|
|
1035
1035
|
}
|
|
1036
1036
|
return await response.json();
|
|
1037
1037
|
} finally {
|
|
@@ -1199,6 +1199,95 @@ var init_confidence = __esm({
|
|
|
1199
1199
|
}
|
|
1200
1200
|
});
|
|
1201
1201
|
|
|
1202
|
+
// src/ml-client/project-name.ts
|
|
1203
|
+
var project_name_exports = {};
|
|
1204
|
+
__export(project_name_exports, {
|
|
1205
|
+
detectProjectIdentity: () => detectProjectIdentity
|
|
1206
|
+
});
|
|
1207
|
+
import { basename, join as join6 } from "path";
|
|
1208
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1209
|
+
import { createHash as createHash2 } from "crypto";
|
|
1210
|
+
async function detectProjectIdentity(rootDir, override) {
|
|
1211
|
+
const hash = createHash2("sha256").update(rootDir).digest("hex");
|
|
1212
|
+
if (override && override.trim()) {
|
|
1213
|
+
return { name: override.trim(), hash };
|
|
1214
|
+
}
|
|
1215
|
+
const fromPackageJson = await readJsonField(
|
|
1216
|
+
join6(rootDir, "package.json"),
|
|
1217
|
+
"name"
|
|
1218
|
+
);
|
|
1219
|
+
if (fromPackageJson) return { name: fromPackageJson, hash };
|
|
1220
|
+
const fromCargo = await readTomlFieldInSection(
|
|
1221
|
+
join6(rootDir, "Cargo.toml"),
|
|
1222
|
+
"package",
|
|
1223
|
+
"name"
|
|
1224
|
+
);
|
|
1225
|
+
if (fromCargo) return { name: fromCargo, hash };
|
|
1226
|
+
const fromGoMod = await readGoModule(join6(rootDir, "go.mod"));
|
|
1227
|
+
if (fromGoMod) return { name: fromGoMod, hash };
|
|
1228
|
+
const fromPyProject = await readTomlFieldInSection(
|
|
1229
|
+
join6(rootDir, "pyproject.toml"),
|
|
1230
|
+
"project",
|
|
1231
|
+
"name"
|
|
1232
|
+
) ?? await readTomlFieldInSection(
|
|
1233
|
+
join6(rootDir, "pyproject.toml"),
|
|
1234
|
+
"tool.poetry",
|
|
1235
|
+
"name"
|
|
1236
|
+
);
|
|
1237
|
+
if (fromPyProject) return { name: fromPyProject, hash };
|
|
1238
|
+
return { name: basename(rootDir) || "untitled", hash };
|
|
1239
|
+
}
|
|
1240
|
+
async function readJsonField(path2, field) {
|
|
1241
|
+
try {
|
|
1242
|
+
const raw = await readFile5(path2, "utf-8");
|
|
1243
|
+
const parsed = JSON.parse(raw);
|
|
1244
|
+
const value = parsed[field];
|
|
1245
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
1246
|
+
} catch {
|
|
1247
|
+
}
|
|
1248
|
+
return null;
|
|
1249
|
+
}
|
|
1250
|
+
async function readTomlFieldInSection(path2, section, key) {
|
|
1251
|
+
try {
|
|
1252
|
+
const raw = await readFile5(path2, "utf-8");
|
|
1253
|
+
const lines = raw.split("\n");
|
|
1254
|
+
let inSection = false;
|
|
1255
|
+
for (const line of lines) {
|
|
1256
|
+
const trimmed = line.trim();
|
|
1257
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
1258
|
+
inSection = trimmed === `[${section}]`;
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
if (!inSection) continue;
|
|
1262
|
+
const match = trimmed.match(/^([\w-]+)\s*=\s*"([^"]+)"/);
|
|
1263
|
+
if (match && match[1] === key && match[2]) {
|
|
1264
|
+
return match[2];
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
} catch {
|
|
1268
|
+
}
|
|
1269
|
+
return null;
|
|
1270
|
+
}
|
|
1271
|
+
async function readGoModule(path2) {
|
|
1272
|
+
try {
|
|
1273
|
+
const raw = await readFile5(path2, "utf-8");
|
|
1274
|
+
const match = raw.match(/^\s*module\s+(\S+)/m);
|
|
1275
|
+
if (match && match[1]) {
|
|
1276
|
+
const segs = match[1].split("/");
|
|
1277
|
+
const last = segs[segs.length - 1];
|
|
1278
|
+
if (last) return last;
|
|
1279
|
+
}
|
|
1280
|
+
} catch {
|
|
1281
|
+
}
|
|
1282
|
+
return null;
|
|
1283
|
+
}
|
|
1284
|
+
var init_project_name = __esm({
|
|
1285
|
+
"src/ml-client/project-name.ts"() {
|
|
1286
|
+
"use strict";
|
|
1287
|
+
init_esm_shims();
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1202
1291
|
// src/ml-client/index.ts
|
|
1203
1292
|
var ml_client_exports = {};
|
|
1204
1293
|
__export(ml_client_exports, {
|
|
@@ -1266,23 +1355,39 @@ async function runMlAnalysis(ctx, codeDnaResult, findings, options) {
|
|
|
1266
1355
|
const deviations = buildDeviationPayloads(codeDnaResult, options.driftFindings ?? []);
|
|
1267
1356
|
const payloadSize = JSON.stringify(sampled).length + JSON.stringify(deviations).length;
|
|
1268
1357
|
if (options.verbose) {
|
|
1269
|
-
console.error(`[
|
|
1270
|
-
console.error(`[
|
|
1358
|
+
console.error(`[deep] Sending ${sampled.length} functions + ${deviations.length} deviations (${Math.round(payloadSize / 1024)}KB) to VibeDrift API...`);
|
|
1359
|
+
console.error(`[deep] No full files transmitted \u2014 only function snippets and structural metadata.`);
|
|
1271
1360
|
}
|
|
1361
|
+
const { detectProjectIdentity: detectProjectIdentity2 } = await Promise.resolve().then(() => (init_project_name(), project_name_exports));
|
|
1362
|
+
const projectIdentity = await detectProjectIdentity2(
|
|
1363
|
+
ctx.rootDir,
|
|
1364
|
+
options.projectName
|
|
1365
|
+
);
|
|
1272
1366
|
const request = {
|
|
1273
1367
|
language: ctx.dominantLanguage ?? "unknown",
|
|
1274
1368
|
file_count: ctx.files.length,
|
|
1369
|
+
project_hash: projectIdentity.hash,
|
|
1370
|
+
project_name: projectIdentity.name,
|
|
1371
|
+
score_hint: options.scoreHint,
|
|
1372
|
+
grade_hint: options.gradeHint,
|
|
1373
|
+
// Tell the server NOT to persist a row from the analyze call —
|
|
1374
|
+
// the CLI logs the full scan summary via /v1/scans/log AFTER the
|
|
1375
|
+
// pipeline finishes, which has accurate metadata.
|
|
1376
|
+
defer_persist: true,
|
|
1275
1377
|
functions: sampled,
|
|
1276
1378
|
deviations,
|
|
1277
1379
|
llm_validations: []
|
|
1278
1380
|
};
|
|
1279
|
-
const response = await callMlApi(request, options.
|
|
1381
|
+
const response = await callMlApi(request, options.token, options.apiUrl);
|
|
1280
1382
|
if (options.verbose) {
|
|
1281
1383
|
console.error(
|
|
1282
|
-
`[
|
|
1384
|
+
`[deep] API returned: ${response.duplicates.length} duplicates, ${response.intent_mismatches.length} intent mismatches, ${response.anomalies.length} anomalies, ${response.deviations?.length ?? 0} deviation verdicts \u2014 ${response.processing_time_ms}ms`
|
|
1283
1385
|
);
|
|
1284
1386
|
}
|
|
1285
1387
|
const filtered = filterByConfidence(response);
|
|
1388
|
+
if (response.scan_id) {
|
|
1389
|
+
filtered.scanId = response.scan_id;
|
|
1390
|
+
}
|
|
1286
1391
|
if (response.deviations?.length > 0 && codeDnaResult?.deviationJustifications) {
|
|
1287
1392
|
for (const mlDev of response.deviations) {
|
|
1288
1393
|
const local = codeDnaResult.deviationJustifications.find(
|
|
@@ -1372,7 +1477,7 @@ var summarize_exports = {};
|
|
|
1372
1477
|
__export(summarize_exports, {
|
|
1373
1478
|
fetchAiSummary: () => fetchAiSummary
|
|
1374
1479
|
});
|
|
1375
|
-
async function fetchAiSummary(result, apiUrl,
|
|
1480
|
+
async function fetchAiSummary(result, apiUrl, token) {
|
|
1376
1481
|
const dna = result.codeDnaResult;
|
|
1377
1482
|
const mlFindings = result.findings.filter((f) => f.tags?.includes("ml"));
|
|
1378
1483
|
const body = {
|
|
@@ -1409,7 +1514,7 @@ async function fetchAiSummary(result, apiUrl, mlKey) {
|
|
|
1409
1514
|
}).slice(0, 5).map((d) => d.recommendation)
|
|
1410
1515
|
};
|
|
1411
1516
|
const headers = { "Content-Type": "application/json" };
|
|
1412
|
-
if (
|
|
1517
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
1413
1518
|
const controller = new AbortController();
|
|
1414
1519
|
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS2);
|
|
1415
1520
|
try {
|
|
@@ -1445,6 +1550,160 @@ var init_summarize = __esm({
|
|
|
1445
1550
|
}
|
|
1446
1551
|
});
|
|
1447
1552
|
|
|
1553
|
+
// src/ml-client/log-scan.ts
|
|
1554
|
+
var log_scan_exports = {};
|
|
1555
|
+
__export(log_scan_exports, {
|
|
1556
|
+
logScan: () => logScan
|
|
1557
|
+
});
|
|
1558
|
+
async function logScan(opts) {
|
|
1559
|
+
const { payload, token, apiUrl, verbose } = opts;
|
|
1560
|
+
const base = apiUrl ?? DEFAULT_API_URL2;
|
|
1561
|
+
const trimmed = { ...payload };
|
|
1562
|
+
if (trimmed.report_html) {
|
|
1563
|
+
const size = Buffer.byteLength(trimmed.report_html, "utf-8");
|
|
1564
|
+
if (size > MAX_HTML_BYTES) {
|
|
1565
|
+
if (verbose) {
|
|
1566
|
+
console.error(
|
|
1567
|
+
`[scan-log] HTML too large (${Math.round(size / 1024)}KB), dropping the blob`
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
delete trimmed.report_html;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
const controller = new AbortController();
|
|
1574
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS3);
|
|
1575
|
+
try {
|
|
1576
|
+
const res = await fetch(`${base}/v1/scans/log`, {
|
|
1577
|
+
method: "POST",
|
|
1578
|
+
headers: {
|
|
1579
|
+
Authorization: `Bearer ${token}`,
|
|
1580
|
+
"Content-Type": "application/json"
|
|
1581
|
+
},
|
|
1582
|
+
body: JSON.stringify(trimmed),
|
|
1583
|
+
signal: controller.signal
|
|
1584
|
+
});
|
|
1585
|
+
if (!res.ok) {
|
|
1586
|
+
const text = await res.text().catch(() => "");
|
|
1587
|
+
if (verbose) {
|
|
1588
|
+
console.error(`[scan-log] HTTP ${res.status}: ${text.slice(0, 200)}`);
|
|
1589
|
+
}
|
|
1590
|
+
return { ok: false, error: `HTTP ${res.status}` };
|
|
1591
|
+
}
|
|
1592
|
+
const data = await res.json().catch(() => ({}));
|
|
1593
|
+
if (verbose) {
|
|
1594
|
+
console.error(
|
|
1595
|
+
`[scan-log] Logged scan ${data.scan_id?.slice(0, 8) ?? "?"} (project ${data.project_id?.slice(0, 8) ?? "?"}, ${data.bytes_stored ?? 0} HTML bytes)`
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
return {
|
|
1599
|
+
ok: true,
|
|
1600
|
+
scanId: data.scan_id,
|
|
1601
|
+
projectId: data.project_id,
|
|
1602
|
+
bytesStored: data.bytes_stored
|
|
1603
|
+
};
|
|
1604
|
+
} catch (err) {
|
|
1605
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1606
|
+
if (verbose) console.error(`[scan-log] Failed: ${msg}`);
|
|
1607
|
+
return { ok: false, error: msg };
|
|
1608
|
+
} finally {
|
|
1609
|
+
clearTimeout(timer);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
var DEFAULT_API_URL2, TIMEOUT_MS3, MAX_HTML_BYTES;
|
|
1613
|
+
var init_log_scan = __esm({
|
|
1614
|
+
"src/ml-client/log-scan.ts"() {
|
|
1615
|
+
"use strict";
|
|
1616
|
+
init_esm_shims();
|
|
1617
|
+
DEFAULT_API_URL2 = "https://vibedrift-api.fly.dev";
|
|
1618
|
+
TIMEOUT_MS3 = 2e4;
|
|
1619
|
+
MAX_HTML_BYTES = 15e5;
|
|
1620
|
+
}
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
// src/ml-client/sanitize-result.ts
|
|
1624
|
+
var sanitize_result_exports = {};
|
|
1625
|
+
__export(sanitize_result_exports, {
|
|
1626
|
+
sanitizeResultForUpload: () => sanitizeResultForUpload
|
|
1627
|
+
});
|
|
1628
|
+
function sanitizeResultForUpload(result) {
|
|
1629
|
+
const ctx = result.context;
|
|
1630
|
+
const rootDir = ctx?.rootDir ?? "";
|
|
1631
|
+
const stripPath = (p) => {
|
|
1632
|
+
if (!p) return null;
|
|
1633
|
+
if (rootDir && p.startsWith(rootDir)) {
|
|
1634
|
+
const rel = p.slice(rootDir.length).replace(/^\/+/, "");
|
|
1635
|
+
return rel || ".";
|
|
1636
|
+
}
|
|
1637
|
+
return p;
|
|
1638
|
+
};
|
|
1639
|
+
const sanitizeNode = (node) => {
|
|
1640
|
+
if (typeof node === "string") return stripPath(node) ?? node;
|
|
1641
|
+
if (Array.isArray(node)) return node.map(sanitizeNode);
|
|
1642
|
+
if (node && typeof node === "object") {
|
|
1643
|
+
const out = {};
|
|
1644
|
+
for (const [k, v] of Object.entries(node)) {
|
|
1645
|
+
if (k === "rootDir") continue;
|
|
1646
|
+
if (k === "files" && Array.isArray(v)) {
|
|
1647
|
+
out[k] = v.map((f) => ({
|
|
1648
|
+
relativePath: stripPath(f.relativePath) ?? f.relativePath,
|
|
1649
|
+
lineCount: f.lineCount,
|
|
1650
|
+
language: f.language
|
|
1651
|
+
}));
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1654
|
+
if (k === "ast" || k === "treeSitterNode") continue;
|
|
1655
|
+
out[k] = sanitizeNode(v);
|
|
1656
|
+
}
|
|
1657
|
+
return out;
|
|
1658
|
+
}
|
|
1659
|
+
if (node instanceof Map) {
|
|
1660
|
+
const obj = {};
|
|
1661
|
+
for (const [k, v] of node.entries()) {
|
|
1662
|
+
const safeKey = typeof k === "string" ? stripPath(k) ?? k : String(k);
|
|
1663
|
+
obj[safeKey] = sanitizeNode(v);
|
|
1664
|
+
}
|
|
1665
|
+
return obj;
|
|
1666
|
+
}
|
|
1667
|
+
if (node instanceof Set) {
|
|
1668
|
+
return [...node].map(sanitizeNode);
|
|
1669
|
+
}
|
|
1670
|
+
return node;
|
|
1671
|
+
};
|
|
1672
|
+
const envelope = {
|
|
1673
|
+
schema: "vibedrift-scan-result/v1",
|
|
1674
|
+
project: {
|
|
1675
|
+
// populated by the caller, since the CLI knows project_name + hash
|
|
1676
|
+
},
|
|
1677
|
+
language: {
|
|
1678
|
+
dominant: ctx?.dominantLanguage ?? null,
|
|
1679
|
+
breakdown: sanitizeNode(ctx?.languageBreakdown),
|
|
1680
|
+
totalLines: ctx?.totalLines ?? 0
|
|
1681
|
+
},
|
|
1682
|
+
fileCount: (ctx?.files ?? []).length,
|
|
1683
|
+
files: sanitizeNode(ctx?.files),
|
|
1684
|
+
score: {
|
|
1685
|
+
composite: result.compositeScore,
|
|
1686
|
+
max: result.maxCompositeScore,
|
|
1687
|
+
categories: sanitizeNode(result.scores)
|
|
1688
|
+
},
|
|
1689
|
+
findings: sanitizeNode(result.findings),
|
|
1690
|
+
driftFindings: sanitizeNode(result.driftFindings),
|
|
1691
|
+
driftScores: sanitizeNode(result.driftScores),
|
|
1692
|
+
codeDnaResult: sanitizeNode(result.codeDnaResult),
|
|
1693
|
+
perFileScores: sanitizeNode(result.perFileScores),
|
|
1694
|
+
teaseMessages: result.teaseMessages,
|
|
1695
|
+
aiSummary: result.aiSummary ?? null,
|
|
1696
|
+
scanTimeMs: result.scanTimeMs
|
|
1697
|
+
};
|
|
1698
|
+
return envelope;
|
|
1699
|
+
}
|
|
1700
|
+
var init_sanitize_result = __esm({
|
|
1701
|
+
"src/ml-client/sanitize-result.ts"() {
|
|
1702
|
+
"use strict";
|
|
1703
|
+
init_esm_shims();
|
|
1704
|
+
}
|
|
1705
|
+
});
|
|
1706
|
+
|
|
1448
1707
|
// src/output/csv.ts
|
|
1449
1708
|
var csv_exports = {};
|
|
1450
1709
|
__export(csv_exports, {
|
|
@@ -1969,8 +2228,8 @@ import { Command, Option } from "commander";
|
|
|
1969
2228
|
// src/cli/commands/scan.ts
|
|
1970
2229
|
init_esm_shims();
|
|
1971
2230
|
import { resolve } from "path";
|
|
1972
|
-
import { writeFile as
|
|
1973
|
-
import { stat as
|
|
2231
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
2232
|
+
import { stat as stat3 } from "fs/promises";
|
|
1974
2233
|
import chalk2 from "chalk";
|
|
1975
2234
|
import ora from "ora";
|
|
1976
2235
|
|
|
@@ -2086,8 +2345,8 @@ async function discoverFiles(rootDir) {
|
|
|
2086
2345
|
const language = detectLanguage(entry.name);
|
|
2087
2346
|
if (!language) continue;
|
|
2088
2347
|
try {
|
|
2089
|
-
const
|
|
2090
|
-
if (
|
|
2348
|
+
const info2 = await stat(fullPath);
|
|
2349
|
+
if (info2.size > MAX_FILE_SIZE) continue;
|
|
2091
2350
|
const content = await readFile2(fullPath, "utf-8");
|
|
2092
2351
|
const lineCount = content.split("\n").length;
|
|
2093
2352
|
files.push({ path: fullPath, relativePath: relPath, language, content, lineCount });
|
|
@@ -4917,10 +5176,10 @@ function extractSymbols(file) {
|
|
|
4917
5176
|
function analyzeFileNaming(files) {
|
|
4918
5177
|
const fileNameConventions = /* @__PURE__ */ new Map();
|
|
4919
5178
|
for (const file of files) {
|
|
4920
|
-
const
|
|
4921
|
-
if (
|
|
4922
|
-
if (/(?:test|spec|config|setup|__)/i.test(
|
|
4923
|
-
const conv = classifyName(
|
|
5179
|
+
const basename2 = file.path.split("/").pop()?.replace(/\.[^.]+$/, "") ?? "";
|
|
5180
|
+
if (basename2.length <= 1) continue;
|
|
5181
|
+
if (/(?:test|spec|config|setup|__)/i.test(basename2)) continue;
|
|
5182
|
+
const conv = classifyName(basename2);
|
|
4924
5183
|
if (!conv) continue;
|
|
4925
5184
|
if (!fileNameConventions.has(conv)) fileNameConventions.set(conv, []);
|
|
4926
5185
|
fileNameConventions.get(conv).push(file.path);
|
|
@@ -5962,33 +6221,36 @@ function computePerFileScores(findings, ctx) {
|
|
|
5962
6221
|
|
|
5963
6222
|
// src/output/tease.ts
|
|
5964
6223
|
init_esm_shims();
|
|
5965
|
-
function generateTeaseMessages(ctx, findings,
|
|
5966
|
-
if (
|
|
6224
|
+
function generateTeaseMessages(ctx, findings, deepUsed) {
|
|
6225
|
+
if (deepUsed) return [];
|
|
5967
6226
|
const messages = [];
|
|
5968
6227
|
const securityFindings = findings.filter((f) => f.analyzerId === "security");
|
|
5969
6228
|
if (securityFindings.length > 0) {
|
|
5970
6229
|
const errorCount = securityFindings.filter((f) => f.severity === "error").length;
|
|
5971
6230
|
messages.push(
|
|
5972
|
-
`${securityFindings.length} security issues found${errorCount > 0 ? ` (${errorCount} critical)` : ""} \u2014 run --
|
|
6231
|
+
`${securityFindings.length} security issues found${errorCount > 0 ? ` (${errorCount} critical)` : ""} \u2014 run \`vibedrift --deep\` for AI-powered context analysis`
|
|
5973
6232
|
);
|
|
5974
6233
|
}
|
|
5975
6234
|
const complexityFindings = findings.filter((f) => f.analyzerId === "complexity");
|
|
5976
6235
|
if (complexityFindings.length > 0) {
|
|
5977
6236
|
messages.push(
|
|
5978
|
-
`${complexityFindings.length} complexity concerns detected \u2014 run --
|
|
6237
|
+
`${complexityFindings.length} complexity concerns detected \u2014 run \`vibedrift --deep\` for AI-powered architectural recommendations`
|
|
5979
6238
|
);
|
|
5980
6239
|
}
|
|
5981
6240
|
const deadCodeFindings = findings.filter((f) => f.analyzerId === "dead-code");
|
|
5982
6241
|
if (deadCodeFindings.length > 0) {
|
|
5983
6242
|
messages.push(
|
|
5984
|
-
`Potential dead code detected \u2014 run --
|
|
6243
|
+
`Potential dead code detected \u2014 run \`vibedrift --deep\` for AI-powered semantic analysis`
|
|
5985
6244
|
);
|
|
5986
6245
|
}
|
|
5987
6246
|
if (ctx.files.length > 10 && messages.length === 0) {
|
|
5988
6247
|
messages.push(
|
|
5989
|
-
`Run --
|
|
6248
|
+
`Run \`vibedrift --deep\` for AI-powered architectural pattern detection and intent analysis`
|
|
5990
6249
|
);
|
|
5991
6250
|
}
|
|
6251
|
+
if (messages.length > 0) {
|
|
6252
|
+
messages.push(`Sign in first with \`vibedrift login\` \u2014 it's free.`);
|
|
6253
|
+
}
|
|
5992
6254
|
return messages;
|
|
5993
6255
|
}
|
|
5994
6256
|
|
|
@@ -6870,9 +7132,9 @@ function buildMlInsights(result) {
|
|
|
6870
7132
|
</details>`;
|
|
6871
7133
|
}).join("");
|
|
6872
7134
|
return `<section class="section">
|
|
6873
|
-
<div class="label">
|
|
7135
|
+
<div class="label">AI ANALYSIS <span style="font-size:11px;font-weight:400;letter-spacing:0;text-transform:none;color:var(--info-blue);margin-left:8px">Deep Layer · Code Embeddings · ${mlFindings.length} findings</span></div>
|
|
6874
7136
|
<p style="font-size:13px;color:var(--text-secondary);margin-bottom:16px;line-height:1.6">
|
|
6875
|
-
These findings were detected by
|
|
7137
|
+
These findings were detected by VibeDrift’s AI inference API. Only function snippets (not full files) were sent for analysis — snippets are processed in memory and not stored.
|
|
6876
7138
|
Functions were embedded as 768-dimensional vectors and compared for semantic similarity, name-body alignment, and clustering anomalies.
|
|
6877
7139
|
</p>
|
|
6878
7140
|
${sections}
|
|
@@ -6982,11 +7244,11 @@ function buildCodeDnaSummary(result) {
|
|
|
6982
7244
|
</section>`;
|
|
6983
7245
|
}
|
|
6984
7246
|
function buildFooter(result) {
|
|
6985
|
-
const
|
|
6986
|
-
const premiumUpsell = !
|
|
6987
|
-
Want
|
|
6988
|
-
<code class="mono" style="color:var(--text-primary);background:var(--bg-code);padding:2px 6px;border-radius:0;margin-top:4px;display:inline-block">vibedrift . --
|
|
6989
|
-
<span data-copy="vibedrift . --
|
|
7247
|
+
const hasDeep = result.findings.some((f) => f.tags?.includes("ml")) || !!result.aiSummary;
|
|
7248
|
+
const premiumUpsell = !hasDeep ? `<div style="margin-top:16px;padding:14px 18px;background:var(--bg-surface);border-radius:0;border-left:3px solid var(--border);text-align:left;font-size:13px;color:var(--text-secondary);max-width:520px;margin-left:auto;margin-right:auto">
|
|
7249
|
+
Want AI-powered deep analysis? Sign in once with <code class="mono" style="color:var(--text-primary);background:var(--bg-code);padding:2px 6px;border-radius:0">vibedrift login</code> then run:<br>
|
|
7250
|
+
<code class="mono" style="color:var(--text-primary);background:var(--bg-code);padding:2px 6px;border-radius:0;margin-top:4px;display:inline-block">vibedrift . --deep</code>
|
|
7251
|
+
<span data-copy="vibedrift . --deep" style="cursor:pointer;margin-left:6px;font-size:11px;color:var(--text-tertiary)">[copy]</span>
|
|
6990
7252
|
</div>` : "";
|
|
6991
7253
|
return `<footer style="border-top:1px solid var(--border);padding-top:28px;margin-top:48px;text-align:center;color:var(--text-tertiary);font-size:13px">
|
|
6992
7254
|
<div style="display:flex;gap:8px;justify-content:center;margin-bottom:20px;flex-wrap:wrap">
|
|
@@ -6996,7 +7258,7 @@ function buildFooter(result) {
|
|
|
6996
7258
|
</div>
|
|
6997
7259
|
<p>Generated by <span style="color:var(--text-primary);font-weight:600">VibeDrift v${getVersion()}</span></p>
|
|
6998
7260
|
<p style="margin:4px 0">${result.context.files.length} files · ${result.context.totalLines.toLocaleString()} lines · ${(result.scanTimeMs / 1e3).toFixed(1)}s</p>
|
|
6999
|
-
<p style="margin:4px 0;font-size:12px">${
|
|
7261
|
+
<p style="margin:4px 0;font-size:12px">${hasDeep ? "Function snippets were sent to VibeDrift’s AI API for analysis. No full files transmitted. Snippets processed in memory and not stored." : "No data sent externally."}</p>
|
|
7000
7262
|
<p style="margin-top:10px">Fix the top issues and re-scan: <code class="mono" style="background:var(--bg-code);padding:2px 6px;border-radius:0;color:var(--text-primary)">vibedrift .</code> <span data-copy="vibedrift ." style="cursor:pointer;font-size:11px;color:var(--text-tertiary)">[copy]</span></p>
|
|
7001
7263
|
${premiumUpsell}
|
|
7002
7264
|
<div style="margin-top:16px;padding:12px 18px;background:var(--bg-surface);border-radius:0;display:inline-block;text-align:left">
|
|
@@ -7404,16 +7666,25 @@ function esc2(s) { return String(s || '').replace(/&/g,'&').replace(/</g,'&l
|
|
|
7404
7666
|
// src/core/history.ts
|
|
7405
7667
|
init_esm_shims();
|
|
7406
7668
|
import { readdir as readdir2, readFile as readFile3, writeFile, mkdir } from "fs/promises";
|
|
7669
|
+
import { homedir } from "os";
|
|
7670
|
+
import { createHash } from "crypto";
|
|
7407
7671
|
import { join as join4 } from "path";
|
|
7408
|
-
var
|
|
7672
|
+
var ROOT_DIR = join4(homedir(), ".vibedrift", "scans");
|
|
7673
|
+
function projectHash(rootDir) {
|
|
7674
|
+
return createHash("sha256").update(rootDir).digest("hex").slice(0, 16);
|
|
7675
|
+
}
|
|
7676
|
+
function projectDir(rootDir) {
|
|
7677
|
+
return join4(ROOT_DIR, projectHash(rootDir));
|
|
7678
|
+
}
|
|
7409
7679
|
async function saveScanResult(rootDir, scores, compositeScore) {
|
|
7410
|
-
const dir =
|
|
7680
|
+
const dir = projectDir(rootDir);
|
|
7411
7681
|
try {
|
|
7412
|
-
await mkdir(dir, { recursive: true });
|
|
7682
|
+
await mkdir(dir, { recursive: true, mode: 448 });
|
|
7413
7683
|
} catch {
|
|
7414
7684
|
}
|
|
7415
7685
|
const data = {
|
|
7416
7686
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7687
|
+
rootDir,
|
|
7417
7688
|
scores,
|
|
7418
7689
|
compositeScore
|
|
7419
7690
|
};
|
|
@@ -7421,7 +7692,7 @@ async function saveScanResult(rootDir, scores, compositeScore) {
|
|
|
7421
7692
|
await writeFile(join4(dir, filename), JSON.stringify(data, null, 2));
|
|
7422
7693
|
}
|
|
7423
7694
|
async function loadPreviousScores(rootDir) {
|
|
7424
|
-
const dir =
|
|
7695
|
+
const dir = projectDir(rootDir);
|
|
7425
7696
|
try {
|
|
7426
7697
|
const files = await readdir2(dir);
|
|
7427
7698
|
const scanFiles = files.filter((f) => f.startsWith("scan-") && f.endsWith(".json")).sort().reverse();
|
|
@@ -7434,12 +7705,196 @@ async function loadPreviousScores(rootDir) {
|
|
|
7434
7705
|
}
|
|
7435
7706
|
}
|
|
7436
7707
|
|
|
7708
|
+
// src/core/file-filter.ts
|
|
7709
|
+
init_esm_shims();
|
|
7710
|
+
function applyIncludeExclude(files, includes, excludes) {
|
|
7711
|
+
const includeRegexes = includes.map(globToRegex);
|
|
7712
|
+
const excludeRegexes = excludes.map(globToRegex);
|
|
7713
|
+
const useInclude = includeRegexes.length > 0;
|
|
7714
|
+
return files.filter((file) => {
|
|
7715
|
+
const path2 = file.relativePath;
|
|
7716
|
+
if (useInclude && !includeRegexes.some((re) => re.test(path2))) {
|
|
7717
|
+
return false;
|
|
7718
|
+
}
|
|
7719
|
+
if (excludeRegexes.some((re) => re.test(path2))) {
|
|
7720
|
+
return false;
|
|
7721
|
+
}
|
|
7722
|
+
return true;
|
|
7723
|
+
});
|
|
7724
|
+
}
|
|
7725
|
+
function globToRegex(glob) {
|
|
7726
|
+
let pattern = glob.replace(/^\.\//, "");
|
|
7727
|
+
if (pattern.endsWith("/")) {
|
|
7728
|
+
pattern = pattern + "**";
|
|
7729
|
+
}
|
|
7730
|
+
let result = "";
|
|
7731
|
+
let i = 0;
|
|
7732
|
+
let inClass = false;
|
|
7733
|
+
while (i < pattern.length) {
|
|
7734
|
+
const ch = pattern[i];
|
|
7735
|
+
if (inClass) {
|
|
7736
|
+
if (ch === "]") {
|
|
7737
|
+
inClass = false;
|
|
7738
|
+
result += "]";
|
|
7739
|
+
} else {
|
|
7740
|
+
result += escapeInClass(ch);
|
|
7741
|
+
}
|
|
7742
|
+
i++;
|
|
7743
|
+
continue;
|
|
7744
|
+
}
|
|
7745
|
+
switch (ch) {
|
|
7746
|
+
case "*": {
|
|
7747
|
+
if (pattern[i + 1] === "*") {
|
|
7748
|
+
if (pattern[i + 2] === "/") {
|
|
7749
|
+
result += "(?:.*/)?";
|
|
7750
|
+
i += 3;
|
|
7751
|
+
} else {
|
|
7752
|
+
result += ".*";
|
|
7753
|
+
i += 2;
|
|
7754
|
+
}
|
|
7755
|
+
} else {
|
|
7756
|
+
result += "[^/]*";
|
|
7757
|
+
i++;
|
|
7758
|
+
}
|
|
7759
|
+
break;
|
|
7760
|
+
}
|
|
7761
|
+
case "?": {
|
|
7762
|
+
result += "[^/]";
|
|
7763
|
+
i++;
|
|
7764
|
+
break;
|
|
7765
|
+
}
|
|
7766
|
+
case "[": {
|
|
7767
|
+
inClass = true;
|
|
7768
|
+
result += "[";
|
|
7769
|
+
i++;
|
|
7770
|
+
break;
|
|
7771
|
+
}
|
|
7772
|
+
case "{": {
|
|
7773
|
+
const close = pattern.indexOf("}", i);
|
|
7774
|
+
if (close === -1) {
|
|
7775
|
+
result += "\\{";
|
|
7776
|
+
i++;
|
|
7777
|
+
break;
|
|
7778
|
+
}
|
|
7779
|
+
const inner = pattern.slice(i + 1, close);
|
|
7780
|
+
const parts = inner.split(",").map((p) => p.trim()).filter(Boolean);
|
|
7781
|
+
if (parts.length > 0) {
|
|
7782
|
+
result += "(?:" + parts.map(escapeRegex2).join("|") + ")";
|
|
7783
|
+
} else {
|
|
7784
|
+
result += "\\{\\}";
|
|
7785
|
+
}
|
|
7786
|
+
i = close + 1;
|
|
7787
|
+
break;
|
|
7788
|
+
}
|
|
7789
|
+
default: {
|
|
7790
|
+
result += escapeRegex2(ch);
|
|
7791
|
+
i++;
|
|
7792
|
+
break;
|
|
7793
|
+
}
|
|
7794
|
+
}
|
|
7795
|
+
}
|
|
7796
|
+
return new RegExp("^" + result + "$");
|
|
7797
|
+
}
|
|
7798
|
+
function escapeRegex2(ch) {
|
|
7799
|
+
if (/[.+^${}()|\\]/.test(ch)) return "\\" + ch;
|
|
7800
|
+
return ch;
|
|
7801
|
+
}
|
|
7802
|
+
function escapeInClass(ch) {
|
|
7803
|
+
if (ch === "\\") return "\\\\";
|
|
7804
|
+
return ch;
|
|
7805
|
+
}
|
|
7806
|
+
|
|
7807
|
+
// src/auth/resolver.ts
|
|
7808
|
+
init_esm_shims();
|
|
7809
|
+
|
|
7810
|
+
// src/auth/config.ts
|
|
7811
|
+
init_esm_shims();
|
|
7812
|
+
import { homedir as homedir2 } from "os";
|
|
7813
|
+
import { join as join5 } from "path";
|
|
7814
|
+
import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir2, chmod, unlink, stat as stat2 } from "fs/promises";
|
|
7815
|
+
var DEFAULT_DIR = join5(homedir2(), ".vibedrift");
|
|
7816
|
+
var DEFAULT_FILE = join5(DEFAULT_DIR, "config.json");
|
|
7817
|
+
function getConfigDir() {
|
|
7818
|
+
return DEFAULT_DIR;
|
|
7819
|
+
}
|
|
7820
|
+
function getConfigPath() {
|
|
7821
|
+
return DEFAULT_FILE;
|
|
7822
|
+
}
|
|
7823
|
+
async function ensureConfigDir() {
|
|
7824
|
+
await mkdir2(DEFAULT_DIR, { recursive: true, mode: 448 });
|
|
7825
|
+
}
|
|
7826
|
+
async function readConfig() {
|
|
7827
|
+
try {
|
|
7828
|
+
const raw = await readFile4(DEFAULT_FILE, "utf-8");
|
|
7829
|
+
const parsed = JSON.parse(raw);
|
|
7830
|
+
if (typeof parsed !== "object" || parsed === null) return {};
|
|
7831
|
+
return parsed;
|
|
7832
|
+
} catch (err) {
|
|
7833
|
+
if (err?.code === "ENOENT") return {};
|
|
7834
|
+
process.stderr.write(`vibedrift: warning \u2014 config at ${DEFAULT_FILE} is unreadable (${err?.message ?? err}). Treating as empty.
|
|
7835
|
+
`);
|
|
7836
|
+
return {};
|
|
7837
|
+
}
|
|
7838
|
+
}
|
|
7839
|
+
async function writeConfig(config) {
|
|
7840
|
+
await ensureConfigDir();
|
|
7841
|
+
const json = JSON.stringify(config, null, 2);
|
|
7842
|
+
await writeFile2(DEFAULT_FILE, json, { mode: 384 });
|
|
7843
|
+
try {
|
|
7844
|
+
await chmod(DEFAULT_FILE, 384);
|
|
7845
|
+
} catch {
|
|
7846
|
+
}
|
|
7847
|
+
}
|
|
7848
|
+
async function clearConfig() {
|
|
7849
|
+
try {
|
|
7850
|
+
await unlink(DEFAULT_FILE);
|
|
7851
|
+
} catch (err) {
|
|
7852
|
+
if (err?.code !== "ENOENT") throw err;
|
|
7853
|
+
}
|
|
7854
|
+
}
|
|
7855
|
+
async function patchConfig(patch) {
|
|
7856
|
+
const current = await readConfig();
|
|
7857
|
+
const next = { ...current, ...patch };
|
|
7858
|
+
await writeConfig(next);
|
|
7859
|
+
return next;
|
|
7860
|
+
}
|
|
7861
|
+
|
|
7862
|
+
// src/auth/resolver.ts
|
|
7863
|
+
async function resolveToken(input = {}) {
|
|
7864
|
+
if (input.explicitToken && input.explicitToken.trim().length > 0) {
|
|
7865
|
+
return { token: input.explicitToken.trim(), source: "flag" };
|
|
7866
|
+
}
|
|
7867
|
+
const fromEnv = process.env.VIBEDRIFT_TOKEN;
|
|
7868
|
+
if (fromEnv && fromEnv.trim().length > 0) {
|
|
7869
|
+
return { token: fromEnv.trim(), source: "env" };
|
|
7870
|
+
}
|
|
7871
|
+
const config = await readConfig();
|
|
7872
|
+
if (config.token && config.token.trim().length > 0) {
|
|
7873
|
+
return { token: config.token.trim(), source: "config" };
|
|
7874
|
+
}
|
|
7875
|
+
return null;
|
|
7876
|
+
}
|
|
7877
|
+
async function resolveApiUrl(explicitUrl) {
|
|
7878
|
+
if (explicitUrl && explicitUrl.trim().length > 0) return explicitUrl.trim();
|
|
7879
|
+
if (process.env.VIBEDRIFT_API_URL && process.env.VIBEDRIFT_API_URL.trim().length > 0) {
|
|
7880
|
+
return process.env.VIBEDRIFT_API_URL.trim();
|
|
7881
|
+
}
|
|
7882
|
+
const config = await readConfig();
|
|
7883
|
+
if (config.apiUrl && config.apiUrl.trim().length > 0) return config.apiUrl.trim();
|
|
7884
|
+
return "https://vibedrift-api.fly.dev";
|
|
7885
|
+
}
|
|
7886
|
+
function previewToken(token) {
|
|
7887
|
+
if (!token) return "(none)";
|
|
7888
|
+
if (token.length <= 12) return token.slice(0, 4) + "\u2026";
|
|
7889
|
+
return token.slice(0, 12) + "\u2026";
|
|
7890
|
+
}
|
|
7891
|
+
|
|
7437
7892
|
// src/cli/commands/scan.ts
|
|
7438
7893
|
async function runScan(targetPath, options) {
|
|
7439
7894
|
const rootDir = resolve(targetPath);
|
|
7440
7895
|
try {
|
|
7441
|
-
const
|
|
7442
|
-
if (!
|
|
7896
|
+
const info2 = await stat3(rootDir);
|
|
7897
|
+
if (!info2.isDirectory()) {
|
|
7443
7898
|
console.error(`Error: ${rootDir} is not a directory`);
|
|
7444
7899
|
process.exit(1);
|
|
7445
7900
|
}
|
|
@@ -7447,12 +7902,48 @@ async function runScan(targetPath, options) {
|
|
|
7447
7902
|
console.error(`Error: ${rootDir} does not exist`);
|
|
7448
7903
|
process.exit(1);
|
|
7449
7904
|
}
|
|
7905
|
+
let bearerToken = null;
|
|
7906
|
+
let apiUrl = options.apiUrl;
|
|
7907
|
+
if (options.deep) {
|
|
7908
|
+
const resolved = await resolveToken();
|
|
7909
|
+
if (!resolved) {
|
|
7910
|
+
console.error("");
|
|
7911
|
+
console.error(chalk2.red(" \u2717 Deep scans require a VibeDrift account."));
|
|
7912
|
+
console.error("");
|
|
7913
|
+
console.error(" Run " + chalk2.bold("vibedrift login") + " to sign in.");
|
|
7914
|
+
console.error(" Or set " + chalk2.bold("VIBEDRIFT_TOKEN") + " in your environment for CI.");
|
|
7915
|
+
console.error("");
|
|
7916
|
+
process.exit(1);
|
|
7917
|
+
}
|
|
7918
|
+
bearerToken = resolved.token;
|
|
7919
|
+
apiUrl = await resolveApiUrl(options.apiUrl);
|
|
7920
|
+
} else {
|
|
7921
|
+
try {
|
|
7922
|
+
const resolved = await resolveToken();
|
|
7923
|
+
if (resolved) {
|
|
7924
|
+
bearerToken = resolved.token;
|
|
7925
|
+
apiUrl = await resolveApiUrl(options.apiUrl);
|
|
7926
|
+
}
|
|
7927
|
+
} catch {
|
|
7928
|
+
}
|
|
7929
|
+
}
|
|
7450
7930
|
const startTime = Date.now();
|
|
7451
7931
|
const timings = {};
|
|
7452
7932
|
const isTerminal = options.format === "terminal" && !options.json;
|
|
7453
7933
|
const spinner = isTerminal ? ora("Discovering files...").start() : null;
|
|
7454
7934
|
const t0 = Date.now();
|
|
7455
7935
|
const { ctx, warnings } = await buildAnalysisContext(rootDir);
|
|
7936
|
+
const includes = options.include ?? [];
|
|
7937
|
+
const excludes = options.exclude ?? [];
|
|
7938
|
+
if (includes.length > 0 || excludes.length > 0) {
|
|
7939
|
+
const before = ctx.files.length;
|
|
7940
|
+
const filtered = applyIncludeExclude(ctx.files, includes, excludes);
|
|
7941
|
+
ctx.files = filtered;
|
|
7942
|
+
ctx.totalLines = filtered.reduce((sum, f) => sum + f.lineCount, 0);
|
|
7943
|
+
if (options.verbose) {
|
|
7944
|
+
console.error(chalk2.dim(`[filter] ${before} \u2192 ${filtered.length} files after include/exclude`));
|
|
7945
|
+
}
|
|
7946
|
+
}
|
|
7456
7947
|
timings.discovery = Date.now() - t0;
|
|
7457
7948
|
if (isTerminal) {
|
|
7458
7949
|
if (warnings.truncated) {
|
|
@@ -7503,26 +7994,28 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
|
|
|
7503
7994
|
}
|
|
7504
7995
|
}
|
|
7505
7996
|
let mlMediumConfidence = [];
|
|
7506
|
-
if (options.
|
|
7997
|
+
if (options.deep && bearerToken) {
|
|
7507
7998
|
const t5 = Date.now();
|
|
7508
|
-
if (spinner) spinner.text = "
|
|
7999
|
+
if (spinner) spinner.text = "Running AI deep analysis (may take ~30s on cold start)...";
|
|
7509
8000
|
try {
|
|
7510
8001
|
const { runMlAnalysis: runMlAnalysis2 } = await Promise.resolve().then(() => (init_ml_client(), ml_client_exports));
|
|
7511
8002
|
const mlResult = await runMlAnalysis2(ctx, codeDnaResult, allFindings, {
|
|
7512
|
-
|
|
7513
|
-
|
|
8003
|
+
token: bearerToken,
|
|
8004
|
+
apiUrl,
|
|
7514
8005
|
verbose: options.verbose,
|
|
7515
|
-
driftFindings: driftResult.driftFindings
|
|
8006
|
+
driftFindings: driftResult.driftFindings,
|
|
8007
|
+
projectName: options.projectName
|
|
7516
8008
|
});
|
|
7517
8009
|
allFindings.push(...mlResult.highConfidence);
|
|
7518
8010
|
mlMediumConfidence = mlResult.mediumConfidence;
|
|
7519
8011
|
if (options.verbose) {
|
|
7520
|
-
console.error(`[
|
|
8012
|
+
console.error(`[deep] ${mlResult.highConfidence.length} high-confidence findings shipped, ${mlResult.mediumConfidence.length} sent to LLM, ${mlResult.droppedCount} dropped`);
|
|
7521
8013
|
}
|
|
7522
8014
|
} catch (err) {
|
|
7523
|
-
console.error(`[
|
|
8015
|
+
console.error(chalk2.red(`[deep] AI analysis failed: ${err.message}`));
|
|
8016
|
+
console.error(chalk2.dim(" The local scan will continue. Run `vibedrift doctor` if this persists."));
|
|
7524
8017
|
}
|
|
7525
|
-
timings.
|
|
8018
|
+
timings.deep = Date.now() - t5;
|
|
7526
8019
|
}
|
|
7527
8020
|
const { deduplicateFindingsAcrossLayers: deduplicateFindingsAcrossLayers2 } = await Promise.resolve().then(() => (init_dedup(), dedup_exports));
|
|
7528
8021
|
const dedupedCount = allFindings.length;
|
|
@@ -7541,13 +8034,13 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
|
|
|
7541
8034
|
previousScores ?? void 0
|
|
7542
8035
|
);
|
|
7543
8036
|
const deepInsights = [];
|
|
7544
|
-
const teaseMessages = generateTeaseMessages(ctx, allFindings,
|
|
8037
|
+
const teaseMessages = generateTeaseMessages(ctx, allFindings, options.deep === true);
|
|
7545
8038
|
const scanTimeMs = Date.now() - startTime;
|
|
7546
8039
|
if (spinner) spinner.stop();
|
|
7547
8040
|
const layer1Ms = (timings.discovery ?? 0) + (timings.parsing ?? 0) + (timings.analyzers ?? 0) + (timings.drift ?? 0);
|
|
7548
8041
|
const parts = [`Layer 1: ${(layer1Ms / 1e3).toFixed(1)}s`];
|
|
7549
8042
|
if (timings.codedna) parts.push(`Code DNA: ${(timings.codedna / 1e3).toFixed(1)}s`);
|
|
7550
|
-
if (timings.
|
|
8043
|
+
if (timings.deep) parts.push(`AI: ${(timings.deep / 1e3).toFixed(1)}s`);
|
|
7551
8044
|
parts.push(`Total: ${(scanTimeMs / 1e3).toFixed(1)}s`);
|
|
7552
8045
|
console.error(chalk2.dim(` ${parts.join(" \xB7 ")}`));
|
|
7553
8046
|
const result = {
|
|
@@ -7564,13 +8057,13 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
|
|
|
7564
8057
|
perFileScores,
|
|
7565
8058
|
codeDnaResult
|
|
7566
8059
|
};
|
|
7567
|
-
if (options.
|
|
8060
|
+
if (options.deep && bearerToken) {
|
|
7568
8061
|
if (spinner) spinner.text = "Generating AI summary...";
|
|
7569
8062
|
try {
|
|
7570
8063
|
const { fetchAiSummary: fetchAiSummary2 } = await Promise.resolve().then(() => (init_summarize(), summarize_exports));
|
|
7571
|
-
const
|
|
7572
|
-
if (options.verbose) console.error(`[summary] Calling ${
|
|
7573
|
-
const summary = await fetchAiSummary2(result,
|
|
8064
|
+
const targetUrl = apiUrl ?? "https://vibedrift-api.fly.dev";
|
|
8065
|
+
if (options.verbose) console.error(`[summary] Calling ${targetUrl}/v1/summarize...`);
|
|
8066
|
+
const summary = await fetchAiSummary2(result, targetUrl, bearerToken);
|
|
7574
8067
|
if (summary) {
|
|
7575
8068
|
result.aiSummary = summary;
|
|
7576
8069
|
if (options.verbose) console.error(`[summary] AI summary generated (${summary.highlights.length} highlights)`);
|
|
@@ -7582,11 +8075,67 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
|
|
|
7582
8075
|
}
|
|
7583
8076
|
}
|
|
7584
8077
|
await saveScanResult(rootDir, scores, compositeScore);
|
|
8078
|
+
if (bearerToken) {
|
|
8079
|
+
try {
|
|
8080
|
+
const { logScan: logScan2 } = await Promise.resolve().then(() => (init_log_scan(), log_scan_exports));
|
|
8081
|
+
const { detectProjectIdentity: detectProjectIdentity2 } = await Promise.resolve().then(() => (init_project_name(), project_name_exports));
|
|
8082
|
+
const { sanitizeResultForUpload: sanitizeResultForUpload2 } = await Promise.resolve().then(() => (init_sanitize_result(), sanitize_result_exports));
|
|
8083
|
+
const projectIdentity = await detectProjectIdentity2(
|
|
8084
|
+
rootDir,
|
|
8085
|
+
options.projectName
|
|
8086
|
+
);
|
|
8087
|
+
const mlDuplicates = allFindings.filter((f) => f.analyzerId === "ml-duplicate").length;
|
|
8088
|
+
const mlIntent = allFindings.filter((f) => f.analyzerId === "ml-intent").length;
|
|
8089
|
+
const mlAnomaly = allFindings.filter((f) => f.analyzerId === "ml-anomaly").length;
|
|
8090
|
+
let html;
|
|
8091
|
+
try {
|
|
8092
|
+
html = renderHtmlReport(result);
|
|
8093
|
+
} catch {
|
|
8094
|
+
html = void 0;
|
|
8095
|
+
}
|
|
8096
|
+
const pct = maxCompositeScore > 0 ? compositeScore / maxCompositeScore * 100 : 0;
|
|
8097
|
+
const grade = pct >= 90 ? "A" : pct >= 75 ? "B" : pct >= 50 ? "C" : pct >= 25 ? "D" : "F";
|
|
8098
|
+
const sanitizedResult = sanitizeResultForUpload2(result);
|
|
8099
|
+
sanitizedResult.project = {
|
|
8100
|
+
name: projectIdentity.name,
|
|
8101
|
+
hash: projectIdentity.hash
|
|
8102
|
+
};
|
|
8103
|
+
sanitizedResult.grade = grade;
|
|
8104
|
+
sanitizedResult.scanType = options.deep ? "deep" : "free";
|
|
8105
|
+
sanitizedResult.scannedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8106
|
+
await logScan2({
|
|
8107
|
+
token: bearerToken,
|
|
8108
|
+
apiUrl,
|
|
8109
|
+
verbose: options.verbose,
|
|
8110
|
+
payload: {
|
|
8111
|
+
project_hash: projectIdentity.hash,
|
|
8112
|
+
project_name: projectIdentity.name,
|
|
8113
|
+
language: ctx.dominantLanguage ?? "unknown",
|
|
8114
|
+
file_count: ctx.files.length,
|
|
8115
|
+
function_count: codeDnaResult?.functions?.length ?? 0,
|
|
8116
|
+
finding_count: allFindings.length,
|
|
8117
|
+
score: compositeScore,
|
|
8118
|
+
grade,
|
|
8119
|
+
duplicates_found: mlDuplicates,
|
|
8120
|
+
intent_mismatches: mlIntent,
|
|
8121
|
+
anomalies_found: mlAnomaly,
|
|
8122
|
+
is_deep: !!options.deep,
|
|
8123
|
+
processing_time_ms: scanTimeMs,
|
|
8124
|
+
report_html: html,
|
|
8125
|
+
result_json: sanitizedResult
|
|
8126
|
+
}
|
|
8127
|
+
});
|
|
8128
|
+
} catch (err) {
|
|
8129
|
+
if (options.verbose) {
|
|
8130
|
+
console.error(chalk2.dim(`[scan-log] Failed: ${err.message}`));
|
|
8131
|
+
}
|
|
8132
|
+
}
|
|
8133
|
+
}
|
|
7585
8134
|
const format = options.format ?? (options.json ? "json" : "html");
|
|
7586
8135
|
if (format === "html") {
|
|
7587
8136
|
const html = renderHtmlReport(result);
|
|
7588
8137
|
const outputPath = options.output ?? "vibedrift-report.html";
|
|
7589
|
-
await
|
|
8138
|
+
await writeFile3(outputPath, html);
|
|
7590
8139
|
const { createServer } = await import("http");
|
|
7591
8140
|
const server = createServer((req, res) => {
|
|
7592
8141
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
@@ -7612,18 +8161,18 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
|
|
|
7612
8161
|
const { renderCsvReport: renderCsvReport2 } = await Promise.resolve().then(() => (init_csv(), csv_exports));
|
|
7613
8162
|
const csv = renderCsvReport2(result);
|
|
7614
8163
|
const outputPath = options.output ?? "vibedrift-report.csv";
|
|
7615
|
-
await
|
|
8164
|
+
await writeFile3(outputPath, csv);
|
|
7616
8165
|
console.log(`CSV report written to ${outputPath}`);
|
|
7617
8166
|
} else if (format === "docx") {
|
|
7618
8167
|
const { renderDocxReport: renderDocxReport2 } = await Promise.resolve().then(() => (init_docx(), docx_exports));
|
|
7619
8168
|
const docx = renderDocxReport2(result);
|
|
7620
8169
|
const outputPath = options.output ?? "vibedrift-report.docx";
|
|
7621
|
-
await
|
|
8170
|
+
await writeFile3(outputPath, docx);
|
|
7622
8171
|
console.log(`DOCX report written to ${outputPath}`);
|
|
7623
8172
|
} else if (format === "json") {
|
|
7624
8173
|
const json = renderJsonOutput(result);
|
|
7625
8174
|
if (options.output) {
|
|
7626
|
-
await
|
|
8175
|
+
await writeFile3(options.output, json);
|
|
7627
8176
|
console.log(`JSON report written to ${options.output}`);
|
|
7628
8177
|
} else {
|
|
7629
8178
|
console.log(json);
|
|
@@ -7728,6 +8277,614 @@ Update failed: ${message}`));
|
|
|
7728
8277
|
});
|
|
7729
8278
|
}
|
|
7730
8279
|
|
|
8280
|
+
// src/cli/commands/login.ts
|
|
8281
|
+
init_esm_shims();
|
|
8282
|
+
import chalk4 from "chalk";
|
|
8283
|
+
|
|
8284
|
+
// src/auth/api.ts
|
|
8285
|
+
init_esm_shims();
|
|
8286
|
+
var REQUEST_TIMEOUT_MS = 3e4;
|
|
8287
|
+
var VibeDriftApiError = class extends Error {
|
|
8288
|
+
constructor(status, message) {
|
|
8289
|
+
super(message);
|
|
8290
|
+
this.status = status;
|
|
8291
|
+
this.name = "VibeDriftApiError";
|
|
8292
|
+
}
|
|
8293
|
+
status;
|
|
8294
|
+
};
|
|
8295
|
+
async function jsonFetch(url, init = {}) {
|
|
8296
|
+
const controller = new AbortController();
|
|
8297
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
8298
|
+
try {
|
|
8299
|
+
const res = await fetch(url, {
|
|
8300
|
+
...init,
|
|
8301
|
+
signal: controller.signal,
|
|
8302
|
+
headers: {
|
|
8303
|
+
Accept: "application/json",
|
|
8304
|
+
...init.body ? { "Content-Type": "application/json" } : {},
|
|
8305
|
+
...init.headers ?? {}
|
|
8306
|
+
}
|
|
8307
|
+
});
|
|
8308
|
+
if (!res.ok) {
|
|
8309
|
+
let detail = "";
|
|
8310
|
+
try {
|
|
8311
|
+
const body = await res.json();
|
|
8312
|
+
detail = body.detail ?? body.message ?? "";
|
|
8313
|
+
} catch {
|
|
8314
|
+
try {
|
|
8315
|
+
detail = await res.text();
|
|
8316
|
+
} catch {
|
|
8317
|
+
}
|
|
8318
|
+
}
|
|
8319
|
+
throw new VibeDriftApiError(res.status, detail || `HTTP ${res.status}`);
|
|
8320
|
+
}
|
|
8321
|
+
return await res.json();
|
|
8322
|
+
} catch (err) {
|
|
8323
|
+
if (err?.name === "AbortError") {
|
|
8324
|
+
throw new VibeDriftApiError(0, `Request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s`);
|
|
8325
|
+
}
|
|
8326
|
+
if (err instanceof VibeDriftApiError) throw err;
|
|
8327
|
+
throw new VibeDriftApiError(0, err?.message ?? String(err));
|
|
8328
|
+
} finally {
|
|
8329
|
+
clearTimeout(timer);
|
|
8330
|
+
}
|
|
8331
|
+
}
|
|
8332
|
+
async function startDeviceAuth(opts) {
|
|
8333
|
+
const base = await resolveApiUrl(opts?.apiUrl);
|
|
8334
|
+
return jsonFetch(`${base}/auth/device`, {
|
|
8335
|
+
method: "POST",
|
|
8336
|
+
body: JSON.stringify({ client_id: "vibedrift-cli" })
|
|
8337
|
+
});
|
|
8338
|
+
}
|
|
8339
|
+
async function pollDeviceAuth(deviceCode, opts) {
|
|
8340
|
+
const base = await resolveApiUrl(opts?.apiUrl);
|
|
8341
|
+
return jsonFetch(`${base}/auth/poll`, {
|
|
8342
|
+
method: "POST",
|
|
8343
|
+
body: JSON.stringify({ device_code: deviceCode })
|
|
8344
|
+
});
|
|
8345
|
+
}
|
|
8346
|
+
async function validateToken(token, opts) {
|
|
8347
|
+
const base = await resolveApiUrl(opts?.apiUrl);
|
|
8348
|
+
return jsonFetch(`${base}/auth/validate`, {
|
|
8349
|
+
method: "GET",
|
|
8350
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
8351
|
+
});
|
|
8352
|
+
}
|
|
8353
|
+
async function revokeToken(token, opts) {
|
|
8354
|
+
const base = await resolveApiUrl(opts?.apiUrl);
|
|
8355
|
+
await jsonFetch(`${base}/auth/revoke`, {
|
|
8356
|
+
method: "POST",
|
|
8357
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
8358
|
+
});
|
|
8359
|
+
}
|
|
8360
|
+
async function fetchUsage(token, opts) {
|
|
8361
|
+
const base = await resolveApiUrl(opts?.apiUrl);
|
|
8362
|
+
return jsonFetch(`${base}/account/usage`, {
|
|
8363
|
+
method: "GET",
|
|
8364
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
8365
|
+
});
|
|
8366
|
+
}
|
|
8367
|
+
async function createPortalSession(token, opts) {
|
|
8368
|
+
const base = await resolveApiUrl(opts?.apiUrl);
|
|
8369
|
+
return jsonFetch(`${base}/account/portal`, {
|
|
8370
|
+
method: "POST",
|
|
8371
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
8372
|
+
});
|
|
8373
|
+
}
|
|
8374
|
+
|
|
8375
|
+
// src/auth/browser.ts
|
|
8376
|
+
init_esm_shims();
|
|
8377
|
+
import { spawn as spawn2 } from "child_process";
|
|
8378
|
+
function openInBrowser(url) {
|
|
8379
|
+
if (!isInteractive()) return false;
|
|
8380
|
+
const env = process.env.BROWSER;
|
|
8381
|
+
if (env && env.length > 0 && env !== "none") {
|
|
8382
|
+
return spawnDetached(env, [url]);
|
|
8383
|
+
}
|
|
8384
|
+
switch (process.platform) {
|
|
8385
|
+
case "darwin":
|
|
8386
|
+
return spawnDetached("open", [url]);
|
|
8387
|
+
case "win32":
|
|
8388
|
+
return spawnDetached("cmd", ["/c", "start", "", url]);
|
|
8389
|
+
default:
|
|
8390
|
+
return spawnDetached("xdg-open", [url]);
|
|
8391
|
+
}
|
|
8392
|
+
}
|
|
8393
|
+
function isInteractive() {
|
|
8394
|
+
if (process.env.CI === "true" || process.env.CI === "1") return false;
|
|
8395
|
+
if (process.env.VIBEDRIFT_NO_BROWSER === "1") return false;
|
|
8396
|
+
return process.stdout.isTTY ?? false;
|
|
8397
|
+
}
|
|
8398
|
+
function spawnDetached(cmd, args) {
|
|
8399
|
+
try {
|
|
8400
|
+
const child = spawn2(cmd, args, {
|
|
8401
|
+
detached: true,
|
|
8402
|
+
stdio: "ignore",
|
|
8403
|
+
shell: false
|
|
8404
|
+
});
|
|
8405
|
+
child.on("error", () => {
|
|
8406
|
+
});
|
|
8407
|
+
child.unref();
|
|
8408
|
+
return true;
|
|
8409
|
+
} catch {
|
|
8410
|
+
return false;
|
|
8411
|
+
}
|
|
8412
|
+
}
|
|
8413
|
+
|
|
8414
|
+
// src/cli/commands/login.ts
|
|
8415
|
+
async function runLogin(options = {}) {
|
|
8416
|
+
const existing = await readConfig();
|
|
8417
|
+
if (existing.token) {
|
|
8418
|
+
console.log(
|
|
8419
|
+
chalk4.yellow(
|
|
8420
|
+
`
|
|
8421
|
+
You're already logged in as ${chalk4.bold(existing.email ?? "unknown")} (${existing.plan ?? "free"}).`
|
|
8422
|
+
)
|
|
8423
|
+
);
|
|
8424
|
+
console.log(chalk4.dim(` Token: ${previewToken(existing.token)}`));
|
|
8425
|
+
console.log(chalk4.dim(" Continuing will replace this token.\n"));
|
|
8426
|
+
}
|
|
8427
|
+
let device;
|
|
8428
|
+
try {
|
|
8429
|
+
device = await startDeviceAuth({ apiUrl: options.apiUrl });
|
|
8430
|
+
} catch (err) {
|
|
8431
|
+
fail("Could not start the login flow", err);
|
|
8432
|
+
return;
|
|
8433
|
+
}
|
|
8434
|
+
console.log("");
|
|
8435
|
+
console.log(chalk4.bold(" First, copy your one-time code:"));
|
|
8436
|
+
console.log("");
|
|
8437
|
+
console.log(` ${chalk4.bgYellow.black.bold(` ${device.user_code} `)}`);
|
|
8438
|
+
console.log("");
|
|
8439
|
+
console.log(chalk4.dim(` This code expires in ${formatDuration(device.expires_in)}.`));
|
|
8440
|
+
console.log("");
|
|
8441
|
+
const opened = !options.noBrowser && openInBrowser(device.verification_uri_complete);
|
|
8442
|
+
if (opened) {
|
|
8443
|
+
console.log(chalk4.bold(" Opened your browser to:"));
|
|
8444
|
+
} else {
|
|
8445
|
+
console.log(chalk4.bold(" Open this URL in your browser:"));
|
|
8446
|
+
}
|
|
8447
|
+
console.log(` ${chalk4.cyan(device.verification_uri_complete)}`);
|
|
8448
|
+
console.log("");
|
|
8449
|
+
console.log(chalk4.dim(" Waiting for you to authorize the CLI..."));
|
|
8450
|
+
console.log("");
|
|
8451
|
+
let interval = Math.max(1, device.interval);
|
|
8452
|
+
const deadline = Date.now() + device.expires_in * 1e3;
|
|
8453
|
+
while (Date.now() < deadline) {
|
|
8454
|
+
await sleep(interval * 1e3);
|
|
8455
|
+
let result;
|
|
8456
|
+
try {
|
|
8457
|
+
result = await pollDeviceAuth(device.device_code, { apiUrl: options.apiUrl });
|
|
8458
|
+
} catch (err) {
|
|
8459
|
+
if (err instanceof VibeDriftApiError && err.status === 429) {
|
|
8460
|
+
interval = Math.min(interval * 2, 30);
|
|
8461
|
+
continue;
|
|
8462
|
+
}
|
|
8463
|
+
fail("Polling for authorization failed", err);
|
|
8464
|
+
return;
|
|
8465
|
+
}
|
|
8466
|
+
if (result.status === "pending") continue;
|
|
8467
|
+
if (result.status === "authorized") {
|
|
8468
|
+
await patchConfig({
|
|
8469
|
+
token: result.access_token,
|
|
8470
|
+
email: result.email,
|
|
8471
|
+
plan: result.plan,
|
|
8472
|
+
expiresAt: result.expires_at,
|
|
8473
|
+
loggedInAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8474
|
+
apiUrl: options.apiUrl
|
|
8475
|
+
});
|
|
8476
|
+
console.log(chalk4.green(" \u2713 Logged in successfully."));
|
|
8477
|
+
console.log("");
|
|
8478
|
+
console.log(` Account: ${chalk4.bold(result.email)}`);
|
|
8479
|
+
console.log(` Plan: ${chalk4.bold(result.plan)}`);
|
|
8480
|
+
console.log("");
|
|
8481
|
+
if (result.plan === "free") {
|
|
8482
|
+
console.log(chalk4.dim(" Run `vibedrift upgrade` to enable deep AI scans."));
|
|
8483
|
+
console.log("");
|
|
8484
|
+
} else {
|
|
8485
|
+
console.log(chalk4.dim(" Run `vibedrift . --deep` to use AI-powered analysis."));
|
|
8486
|
+
console.log("");
|
|
8487
|
+
}
|
|
8488
|
+
return;
|
|
8489
|
+
}
|
|
8490
|
+
if (result.status === "denied") {
|
|
8491
|
+
console.error(chalk4.red("\n \u2717 Authorization was denied in the browser."));
|
|
8492
|
+
console.error(chalk4.dim(" Run `vibedrift login` again to retry.\n"));
|
|
8493
|
+
process.exit(1);
|
|
8494
|
+
}
|
|
8495
|
+
if (result.status === "expired") {
|
|
8496
|
+
console.error(chalk4.red("\n \u2717 The login code expired before you authorized it."));
|
|
8497
|
+
console.error(chalk4.dim(" Run `vibedrift login` again to retry.\n"));
|
|
8498
|
+
process.exit(1);
|
|
8499
|
+
}
|
|
8500
|
+
}
|
|
8501
|
+
console.error(chalk4.red("\n \u2717 Login timed out before authorization completed."));
|
|
8502
|
+
console.error(chalk4.dim(" Run `vibedrift login` again to retry.\n"));
|
|
8503
|
+
process.exit(1);
|
|
8504
|
+
}
|
|
8505
|
+
function fail(intro, err) {
|
|
8506
|
+
const msg = err instanceof VibeDriftApiError ? `${err.status ? `HTTP ${err.status}: ` : ""}${err.message}` : err instanceof Error ? err.message : String(err);
|
|
8507
|
+
console.error(chalk4.red(`
|
|
8508
|
+
\u2717 ${intro}: ${msg}
|
|
8509
|
+
`));
|
|
8510
|
+
process.exit(1);
|
|
8511
|
+
}
|
|
8512
|
+
function sleep(ms) {
|
|
8513
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
8514
|
+
}
|
|
8515
|
+
function formatDuration(seconds) {
|
|
8516
|
+
if (seconds < 60) return `${seconds}s`;
|
|
8517
|
+
const m = Math.round(seconds / 60);
|
|
8518
|
+
return `${m} minute${m === 1 ? "" : "s"}`;
|
|
8519
|
+
}
|
|
8520
|
+
|
|
8521
|
+
// src/cli/commands/logout.ts
|
|
8522
|
+
init_esm_shims();
|
|
8523
|
+
import chalk5 from "chalk";
|
|
8524
|
+
async function runLogout() {
|
|
8525
|
+
const config = await readConfig();
|
|
8526
|
+
if (!config.token) {
|
|
8527
|
+
console.log(chalk5.dim(" Not logged in. Nothing to do."));
|
|
8528
|
+
return;
|
|
8529
|
+
}
|
|
8530
|
+
try {
|
|
8531
|
+
await revokeToken(config.token, { apiUrl: config.apiUrl });
|
|
8532
|
+
} catch (err) {
|
|
8533
|
+
if (err instanceof VibeDriftApiError && (err.status === 401 || err.status === 404)) {
|
|
8534
|
+
} else {
|
|
8535
|
+
console.warn(
|
|
8536
|
+
chalk5.yellow(
|
|
8537
|
+
` \u26A0 Could not revoke token on the server: ${err instanceof Error ? err.message : String(err)}`
|
|
8538
|
+
)
|
|
8539
|
+
);
|
|
8540
|
+
console.warn(chalk5.dim(" Local token will still be removed."));
|
|
8541
|
+
}
|
|
8542
|
+
}
|
|
8543
|
+
await clearConfig();
|
|
8544
|
+
console.log(chalk5.green(" \u2713 Logged out."));
|
|
8545
|
+
}
|
|
8546
|
+
|
|
8547
|
+
// src/cli/commands/status.ts
|
|
8548
|
+
init_esm_shims();
|
|
8549
|
+
import chalk6 from "chalk";
|
|
8550
|
+
init_version();
|
|
8551
|
+
async function runStatus() {
|
|
8552
|
+
const version = getVersion();
|
|
8553
|
+
const config = await readConfig();
|
|
8554
|
+
const resolved = await resolveToken();
|
|
8555
|
+
console.log("");
|
|
8556
|
+
console.log(chalk6.bold(` VibeDrift CLI v${version}`));
|
|
8557
|
+
console.log("");
|
|
8558
|
+
if (!resolved) {
|
|
8559
|
+
console.log(` Status: ${chalk6.dim("not logged in")}`);
|
|
8560
|
+
console.log(` Config: ${chalk6.dim(getConfigPath())}`);
|
|
8561
|
+
console.log("");
|
|
8562
|
+
console.log(chalk6.dim(" Run `vibedrift login` to authenticate."));
|
|
8563
|
+
console.log("");
|
|
8564
|
+
return;
|
|
8565
|
+
}
|
|
8566
|
+
console.log(` Status: ${chalk6.green("authenticated")}`);
|
|
8567
|
+
console.log(` Source: ${chalk6.dim(describeSource(resolved.source))}`);
|
|
8568
|
+
console.log(` Token: ${chalk6.dim(previewToken(resolved.token))}`);
|
|
8569
|
+
if (resolved.source === "config") {
|
|
8570
|
+
if (config.email) console.log(` Account: ${chalk6.bold(config.email)}`);
|
|
8571
|
+
if (config.plan) console.log(` Plan: ${chalk6.bold(config.plan)}`);
|
|
8572
|
+
if (config.expiresAt) console.log(` Expires: ${chalk6.dim(config.expiresAt)}`);
|
|
8573
|
+
console.log(` Config: ${chalk6.dim(getConfigPath())}`);
|
|
8574
|
+
}
|
|
8575
|
+
console.log("");
|
|
8576
|
+
process.stdout.write(chalk6.dim(" Validating token with server... "));
|
|
8577
|
+
try {
|
|
8578
|
+
const result = await validateToken(resolved.token, { apiUrl: config.apiUrl });
|
|
8579
|
+
if (result.valid) {
|
|
8580
|
+
console.log(chalk6.green("ok"));
|
|
8581
|
+
if (result.email && result.email !== config.email) {
|
|
8582
|
+
console.log(chalk6.dim(` Server account: ${result.email} (config out of sync \u2014 run \`vibedrift login\` to refresh)`));
|
|
8583
|
+
}
|
|
8584
|
+
if (result.plan && result.plan !== config.plan) {
|
|
8585
|
+
console.log(chalk6.dim(` Server plan: ${result.plan} (config out of sync \u2014 run \`vibedrift login\` to refresh)`));
|
|
8586
|
+
}
|
|
8587
|
+
} else {
|
|
8588
|
+
console.log(chalk6.red("invalid"));
|
|
8589
|
+
console.log(chalk6.dim(" Run `vibedrift login` to re-authenticate."));
|
|
8590
|
+
}
|
|
8591
|
+
} catch (err) {
|
|
8592
|
+
console.log(chalk6.yellow("offline"));
|
|
8593
|
+
if (err instanceof VibeDriftApiError) {
|
|
8594
|
+
console.log(chalk6.dim(` ${err.message}`));
|
|
8595
|
+
}
|
|
8596
|
+
}
|
|
8597
|
+
console.log("");
|
|
8598
|
+
}
|
|
8599
|
+
function describeSource(source) {
|
|
8600
|
+
switch (source) {
|
|
8601
|
+
case "flag":
|
|
8602
|
+
return "command-line flag";
|
|
8603
|
+
case "env":
|
|
8604
|
+
return "VIBEDRIFT_TOKEN environment variable";
|
|
8605
|
+
case "config":
|
|
8606
|
+
return "~/.vibedrift/config.json";
|
|
8607
|
+
}
|
|
8608
|
+
}
|
|
8609
|
+
|
|
8610
|
+
// src/cli/commands/usage.ts
|
|
8611
|
+
init_esm_shims();
|
|
8612
|
+
import chalk7 from "chalk";
|
|
8613
|
+
async function runUsage() {
|
|
8614
|
+
const resolved = await resolveToken();
|
|
8615
|
+
if (!resolved) {
|
|
8616
|
+
console.error(chalk7.red("\n \u2717 Not logged in. Run `vibedrift login` first.\n"));
|
|
8617
|
+
process.exit(1);
|
|
8618
|
+
}
|
|
8619
|
+
const config = await readConfig();
|
|
8620
|
+
let data;
|
|
8621
|
+
try {
|
|
8622
|
+
data = await fetchUsage(resolved.token, { apiUrl: config.apiUrl });
|
|
8623
|
+
} catch (err) {
|
|
8624
|
+
if (err instanceof VibeDriftApiError && err.status === 401) {
|
|
8625
|
+
console.error(chalk7.red("\n \u2717 Your token is invalid or expired. Run `vibedrift login` to re-authenticate.\n"));
|
|
8626
|
+
process.exit(1);
|
|
8627
|
+
}
|
|
8628
|
+
console.error(chalk7.red(`
|
|
8629
|
+
\u2717 Could not fetch usage: ${err instanceof Error ? err.message : String(err)}
|
|
8630
|
+
`));
|
|
8631
|
+
process.exit(1);
|
|
8632
|
+
}
|
|
8633
|
+
console.log("");
|
|
8634
|
+
console.log(chalk7.bold(" Account"));
|
|
8635
|
+
console.log(` Email: ${chalk7.bold(data.user.email)}`);
|
|
8636
|
+
console.log(` Plan: ${chalk7.bold(data.user.plan)}`);
|
|
8637
|
+
console.log("");
|
|
8638
|
+
console.log(chalk7.bold(" Current period"));
|
|
8639
|
+
console.log(` From: ${chalk7.dim(formatDate(data.current_period.start))}`);
|
|
8640
|
+
console.log(` To: ${chalk7.dim(formatDate(data.current_period.end))}`);
|
|
8641
|
+
console.log(` Scans: ${chalk7.bold(data.current_period.scans.toString())}`);
|
|
8642
|
+
console.log(` Deep: ${chalk7.bold(data.current_period.deep_scans.toString())}`);
|
|
8643
|
+
console.log("");
|
|
8644
|
+
console.log(chalk7.bold(" Limits"));
|
|
8645
|
+
const deepLimit = data.limits.deep_scans_per_month;
|
|
8646
|
+
console.log(` Deep: ${deepLimit === null ? chalk7.green("unlimited") : `${deepLimit}/month`}`);
|
|
8647
|
+
console.log(` Rate: ${data.limits.rate_limit_per_min} requests/minute`);
|
|
8648
|
+
console.log("");
|
|
8649
|
+
if (data.recent_scans.length > 0) {
|
|
8650
|
+
console.log(chalk7.bold(` Recent scans (${data.recent_scans.length})`));
|
|
8651
|
+
for (const scan of data.recent_scans.slice(0, 10)) {
|
|
8652
|
+
const flag = scan.is_deep ? chalk7.cyan("deep") : chalk7.dim("std ");
|
|
8653
|
+
const score = scan.score === null ? chalk7.dim("\u2014") : chalk7.bold(String(Math.round(scan.score))).padEnd(3);
|
|
8654
|
+
console.log(` ${flag} ${score} ${chalk7.dim(formatDateTime(scan.created_at))} ${chalk7.dim(scan.project_hash.slice(0, 12))}`);
|
|
8655
|
+
}
|
|
8656
|
+
console.log("");
|
|
8657
|
+
}
|
|
8658
|
+
}
|
|
8659
|
+
function formatDate(iso) {
|
|
8660
|
+
try {
|
|
8661
|
+
return new Date(iso).toISOString().slice(0, 10);
|
|
8662
|
+
} catch {
|
|
8663
|
+
return iso;
|
|
8664
|
+
}
|
|
8665
|
+
}
|
|
8666
|
+
function formatDateTime(iso) {
|
|
8667
|
+
try {
|
|
8668
|
+
const d = new Date(iso);
|
|
8669
|
+
return d.toISOString().slice(0, 16).replace("T", " ");
|
|
8670
|
+
} catch {
|
|
8671
|
+
return iso;
|
|
8672
|
+
}
|
|
8673
|
+
}
|
|
8674
|
+
|
|
8675
|
+
// src/cli/commands/upgrade.ts
|
|
8676
|
+
init_esm_shims();
|
|
8677
|
+
import chalk8 from "chalk";
|
|
8678
|
+
var PRICING_URL = "https://vibedrift.ai/pricing";
|
|
8679
|
+
async function runUpgrade() {
|
|
8680
|
+
console.log("");
|
|
8681
|
+
console.log(chalk8.bold(" Upgrade your VibeDrift plan"));
|
|
8682
|
+
console.log("");
|
|
8683
|
+
console.log(` ${chalk8.cyan(PRICING_URL)}`);
|
|
8684
|
+
console.log("");
|
|
8685
|
+
const opened = openInBrowser(PRICING_URL);
|
|
8686
|
+
if (opened) {
|
|
8687
|
+
console.log(chalk8.dim(" Opened in your browser."));
|
|
8688
|
+
} else {
|
|
8689
|
+
console.log(chalk8.dim(" Open the link above in your browser."));
|
|
8690
|
+
}
|
|
8691
|
+
console.log("");
|
|
8692
|
+
console.log(chalk8.dim(" After upgrading, run `vibedrift login` to refresh your plan locally."));
|
|
8693
|
+
console.log("");
|
|
8694
|
+
}
|
|
8695
|
+
|
|
8696
|
+
// src/cli/commands/billing.ts
|
|
8697
|
+
init_esm_shims();
|
|
8698
|
+
import chalk9 from "chalk";
|
|
8699
|
+
async function runBilling() {
|
|
8700
|
+
const resolved = await resolveToken();
|
|
8701
|
+
if (!resolved) {
|
|
8702
|
+
console.error(chalk9.red("\n \u2717 Not logged in. Run `vibedrift login` first.\n"));
|
|
8703
|
+
process.exit(1);
|
|
8704
|
+
}
|
|
8705
|
+
const config = await readConfig();
|
|
8706
|
+
let portal;
|
|
8707
|
+
try {
|
|
8708
|
+
portal = await createPortalSession(resolved.token, { apiUrl: config.apiUrl });
|
|
8709
|
+
} catch (err) {
|
|
8710
|
+
if (err instanceof VibeDriftApiError) {
|
|
8711
|
+
if (err.status === 401) {
|
|
8712
|
+
console.error(chalk9.red("\n \u2717 Your token is invalid or expired. Run `vibedrift login`.\n"));
|
|
8713
|
+
process.exit(1);
|
|
8714
|
+
}
|
|
8715
|
+
if (err.status === 402 || err.status === 404) {
|
|
8716
|
+
console.error(chalk9.yellow("\n \u26A0 No billing account found for this user."));
|
|
8717
|
+
console.error(chalk9.dim(" Run `vibedrift upgrade` to start a paid plan first.\n"));
|
|
8718
|
+
process.exit(1);
|
|
8719
|
+
}
|
|
8720
|
+
}
|
|
8721
|
+
console.error(chalk9.red(`
|
|
8722
|
+
\u2717 Could not open billing portal: ${err instanceof Error ? err.message : String(err)}
|
|
8723
|
+
`));
|
|
8724
|
+
process.exit(1);
|
|
8725
|
+
}
|
|
8726
|
+
console.log("");
|
|
8727
|
+
console.log(chalk9.bold(" Stripe Customer Portal"));
|
|
8728
|
+
console.log("");
|
|
8729
|
+
console.log(` ${chalk9.cyan(portal.url)}`);
|
|
8730
|
+
console.log("");
|
|
8731
|
+
const opened = openInBrowser(portal.url);
|
|
8732
|
+
if (opened) {
|
|
8733
|
+
console.log(chalk9.dim(" Opened in your browser. The link is single-use and expires shortly."));
|
|
8734
|
+
} else {
|
|
8735
|
+
console.log(chalk9.dim(" Open the link above in your browser. It's single-use and expires shortly."));
|
|
8736
|
+
}
|
|
8737
|
+
console.log("");
|
|
8738
|
+
}
|
|
8739
|
+
|
|
8740
|
+
// src/cli/commands/doctor.ts
|
|
8741
|
+
init_esm_shims();
|
|
8742
|
+
import chalk10 from "chalk";
|
|
8743
|
+
import { homedir as homedir3, platform, arch } from "os";
|
|
8744
|
+
import { join as join7 } from "path";
|
|
8745
|
+
import { stat as stat4, access, constants } from "fs/promises";
|
|
8746
|
+
init_version();
|
|
8747
|
+
async function runDoctor() {
|
|
8748
|
+
let failures = 0;
|
|
8749
|
+
console.log("");
|
|
8750
|
+
console.log(chalk10.bold(" VibeDrift Doctor"));
|
|
8751
|
+
console.log("");
|
|
8752
|
+
console.log(chalk10.bold(" Environment"));
|
|
8753
|
+
ok("CLI version", getVersion());
|
|
8754
|
+
ok("Node", process.version);
|
|
8755
|
+
ok("Platform", `${platform()} ${arch()}`);
|
|
8756
|
+
ok("HOME", homedir3());
|
|
8757
|
+
console.log("");
|
|
8758
|
+
console.log(chalk10.bold(" Config"));
|
|
8759
|
+
const configDir = getConfigDir();
|
|
8760
|
+
const configPath = getConfigPath();
|
|
8761
|
+
let configDirOk = false;
|
|
8762
|
+
try {
|
|
8763
|
+
const info2 = await stat4(configDir);
|
|
8764
|
+
if (info2.isDirectory()) {
|
|
8765
|
+
configDirOk = true;
|
|
8766
|
+
const mode = (info2.mode & 511).toString(8);
|
|
8767
|
+
ok("Config dir", `${configDir} (mode ${mode})`);
|
|
8768
|
+
} else {
|
|
8769
|
+
bad(`Config dir exists but is not a directory: ${configDir}`);
|
|
8770
|
+
failures++;
|
|
8771
|
+
}
|
|
8772
|
+
} catch {
|
|
8773
|
+
info("Config dir", `${configDir} (will be created on first login)`);
|
|
8774
|
+
configDirOk = true;
|
|
8775
|
+
}
|
|
8776
|
+
if (configDirOk) {
|
|
8777
|
+
try {
|
|
8778
|
+
await access(configPath, constants.R_OK);
|
|
8779
|
+
const info2 = await stat4(configPath);
|
|
8780
|
+
const mode = (info2.mode & 511).toString(8);
|
|
8781
|
+
if ((info2.mode & 63) !== 0) {
|
|
8782
|
+
warn("Config file", `${configPath} (mode ${mode}, world/group readable \u2014 should be 600)`);
|
|
8783
|
+
} else {
|
|
8784
|
+
ok("Config file", `${configPath} (mode ${mode})`);
|
|
8785
|
+
}
|
|
8786
|
+
} catch {
|
|
8787
|
+
info("Config file", "absent (not logged in)");
|
|
8788
|
+
}
|
|
8789
|
+
}
|
|
8790
|
+
const historyDir = join7(homedir3(), ".vibedrift", "scans");
|
|
8791
|
+
try {
|
|
8792
|
+
const info2 = await stat4(historyDir);
|
|
8793
|
+
if (info2.isDirectory()) ok("Scan history", historyDir);
|
|
8794
|
+
else warn("Scan history", `${historyDir} exists but is not a directory`);
|
|
8795
|
+
} catch {
|
|
8796
|
+
info("Scan history", "empty (no scans run yet)");
|
|
8797
|
+
}
|
|
8798
|
+
console.log("");
|
|
8799
|
+
console.log(chalk10.bold(" Authentication"));
|
|
8800
|
+
const config = await readConfig();
|
|
8801
|
+
const resolved = await resolveToken();
|
|
8802
|
+
if (!resolved) {
|
|
8803
|
+
info("Login state", "not logged in");
|
|
8804
|
+
} else {
|
|
8805
|
+
ok("Token source", describeSource2(resolved.source));
|
|
8806
|
+
ok("Token preview", previewToken(resolved.token));
|
|
8807
|
+
if (resolved.source === "config") {
|
|
8808
|
+
if (config.email) ok("Email", config.email);
|
|
8809
|
+
if (config.plan) ok("Plan", config.plan);
|
|
8810
|
+
if (config.expiresAt) {
|
|
8811
|
+
const expires = new Date(config.expiresAt).getTime();
|
|
8812
|
+
const now = Date.now();
|
|
8813
|
+
if (expires < now) {
|
|
8814
|
+
bad(`Token expired ${Math.floor((now - expires) / 864e5)} days ago`);
|
|
8815
|
+
failures++;
|
|
8816
|
+
} else {
|
|
8817
|
+
ok("Token expires", `${config.expiresAt} (${Math.ceil((expires - now) / 864e5)} days)`);
|
|
8818
|
+
}
|
|
8819
|
+
}
|
|
8820
|
+
}
|
|
8821
|
+
}
|
|
8822
|
+
console.log("");
|
|
8823
|
+
console.log(chalk10.bold(" API"));
|
|
8824
|
+
const apiUrl = await resolveApiUrl();
|
|
8825
|
+
ok("API URL", apiUrl);
|
|
8826
|
+
if (resolved) {
|
|
8827
|
+
process.stdout.write(` ${chalk10.dim("\u2192 Validating token... ")}`);
|
|
8828
|
+
try {
|
|
8829
|
+
const result = await validateToken(resolved.token, { apiUrl });
|
|
8830
|
+
if (result.valid) {
|
|
8831
|
+
console.log(chalk10.green("ok"));
|
|
8832
|
+
} else {
|
|
8833
|
+
console.log(chalk10.red("invalid token"));
|
|
8834
|
+
failures++;
|
|
8835
|
+
}
|
|
8836
|
+
} catch (err) {
|
|
8837
|
+
console.log(chalk10.red("unreachable"));
|
|
8838
|
+
console.log(` ${chalk10.dim(" ")}${err instanceof VibeDriftApiError ? err.message : String(err)}`);
|
|
8839
|
+
failures++;
|
|
8840
|
+
}
|
|
8841
|
+
} else {
|
|
8842
|
+
process.stdout.write(` ${chalk10.dim("\u2192 Pinging API... ")}`);
|
|
8843
|
+
try {
|
|
8844
|
+
const res = await fetch(`${apiUrl}/health`, { signal: AbortSignal.timeout(1e4) });
|
|
8845
|
+
if (res.ok) console.log(chalk10.green("ok"));
|
|
8846
|
+
else {
|
|
8847
|
+
console.log(chalk10.yellow(`HTTP ${res.status}`));
|
|
8848
|
+
failures++;
|
|
8849
|
+
}
|
|
8850
|
+
} catch (err) {
|
|
8851
|
+
console.log(chalk10.red("unreachable"));
|
|
8852
|
+
console.log(` ${chalk10.dim(" ")}${err instanceof Error ? err.message : String(err)}`);
|
|
8853
|
+
failures++;
|
|
8854
|
+
}
|
|
8855
|
+
}
|
|
8856
|
+
console.log("");
|
|
8857
|
+
if (failures === 0) {
|
|
8858
|
+
console.log(chalk10.green(" \u2713 All checks passed."));
|
|
8859
|
+
} else {
|
|
8860
|
+
console.log(chalk10.red(` \u2717 ${failures} check${failures === 1 ? "" : "s"} failed.`));
|
|
8861
|
+
}
|
|
8862
|
+
console.log("");
|
|
8863
|
+
process.exit(failures === 0 ? 0 : 1);
|
|
8864
|
+
}
|
|
8865
|
+
function ok(label, value) {
|
|
8866
|
+
console.log(` ${chalk10.green("\u2713")} ${label.padEnd(14)} ${chalk10.dim(value)}`);
|
|
8867
|
+
}
|
|
8868
|
+
function warn(label, value) {
|
|
8869
|
+
console.log(` ${chalk10.yellow("\u26A0")} ${label.padEnd(14)} ${chalk10.dim(value)}`);
|
|
8870
|
+
}
|
|
8871
|
+
function bad(value) {
|
|
8872
|
+
console.log(` ${chalk10.red("\u2717")} ${chalk10.red(value)}`);
|
|
8873
|
+
}
|
|
8874
|
+
function info(label, value) {
|
|
8875
|
+
console.log(` ${chalk10.dim("\xB7")} ${label.padEnd(14)} ${chalk10.dim(value)}`);
|
|
8876
|
+
}
|
|
8877
|
+
function describeSource2(source) {
|
|
8878
|
+
switch (source) {
|
|
8879
|
+
case "flag":
|
|
8880
|
+
return "command-line flag";
|
|
8881
|
+
case "env":
|
|
8882
|
+
return "VIBEDRIFT_TOKEN environment variable";
|
|
8883
|
+
case "config":
|
|
8884
|
+
return "~/.vibedrift/config.json";
|
|
8885
|
+
}
|
|
8886
|
+
}
|
|
8887
|
+
|
|
7731
8888
|
// src/cli/index.ts
|
|
7732
8889
|
init_version();
|
|
7733
8890
|
var VERSION = getVersion();
|
|
@@ -7741,10 +8898,14 @@ function parseScoreThreshold(value) {
|
|
|
7741
8898
|
}
|
|
7742
8899
|
return n;
|
|
7743
8900
|
}
|
|
8901
|
+
function collect(value, previous) {
|
|
8902
|
+
return previous.concat([value]);
|
|
8903
|
+
}
|
|
7744
8904
|
var program = new Command();
|
|
7745
8905
|
program.name("vibedrift").description(
|
|
7746
8906
|
"Detect drift, contradictions, and security gaps in AI-generated codebases."
|
|
7747
|
-
).version(VERSION, "-V, --version", "show the installed version").helpOption("-h, --help", "show this help")
|
|
8907
|
+
).version(VERSION, "-V, --version", "show the installed version").helpOption("-h, --help", "show this help");
|
|
8908
|
+
program.command("scan", { isDefault: true }).description("Scan a project for vibe drift (default command)").argument("[path]", "path to project directory", ".").option(
|
|
7748
8909
|
"--format <type>",
|
|
7749
8910
|
"output format: html, terminal, json, csv, docx",
|
|
7750
8911
|
"html"
|
|
@@ -7752,8 +8913,27 @@ program.name("vibedrift").description(
|
|
|
7752
8913
|
"--fail-on-score <n>",
|
|
7753
8914
|
"exit with code 1 if composite score is below this threshold",
|
|
7754
8915
|
parseScoreThreshold
|
|
7755
|
-
).option("--no-codedna", "skip Code DNA semantic analysis").option(
|
|
7756
|
-
|
|
8916
|
+
).option("--no-codedna", "skip Code DNA semantic analysis").option(
|
|
8917
|
+
"--deep",
|
|
8918
|
+
"enable AI-powered deep analysis (requires `vibedrift login`)"
|
|
8919
|
+
).option(
|
|
8920
|
+
"--project-name <name>",
|
|
8921
|
+
"override the auto-detected project name shown in the dashboard"
|
|
8922
|
+
).option(
|
|
8923
|
+
"--include <pattern>",
|
|
8924
|
+
"only scan files matching this glob (repeatable)",
|
|
8925
|
+
collect,
|
|
8926
|
+
[]
|
|
8927
|
+
).option(
|
|
8928
|
+
"--exclude <pattern>",
|
|
8929
|
+
"exclude files matching this glob (repeatable)",
|
|
8930
|
+
collect,
|
|
8931
|
+
[]
|
|
8932
|
+
).option(
|
|
8933
|
+
"--update",
|
|
8934
|
+
"update the VibeDrift CLI to the latest version (alias for `vibedrift update`)"
|
|
8935
|
+
).option("--verbose", "show timing breakdown and analyzer details").addOption(
|
|
8936
|
+
new Option("--api-url <url>", "override the VibeDrift API base URL").hideHelp()
|
|
7757
8937
|
).action(async (path2, options) => {
|
|
7758
8938
|
if (options.update) {
|
|
7759
8939
|
await runUpdate(VERSION);
|
|
@@ -7765,12 +8945,43 @@ program.name("vibedrift").description(
|
|
|
7765
8945
|
output: options.output,
|
|
7766
8946
|
failOnScore: options.failOnScore,
|
|
7767
8947
|
codedna: options.codedna,
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
8948
|
+
deep: options.deep,
|
|
8949
|
+
apiUrl: options.apiUrl,
|
|
8950
|
+
include: options.include,
|
|
8951
|
+
exclude: options.exclude,
|
|
8952
|
+
verbose: options.verbose,
|
|
8953
|
+
projectName: options.projectName
|
|
8954
|
+
});
|
|
8955
|
+
});
|
|
8956
|
+
program.command("login").description("Log in to your VibeDrift account").option("--no-browser", "don't open the browser automatically").addOption(
|
|
8957
|
+
new Option("--api-url <url>", "override the API base URL").hideHelp()
|
|
8958
|
+
).action(async (options) => {
|
|
8959
|
+
await runLogin({
|
|
8960
|
+
apiUrl: options.apiUrl,
|
|
8961
|
+
noBrowser: options.browser === false
|
|
7772
8962
|
});
|
|
7773
8963
|
});
|
|
8964
|
+
program.command("logout").description("Log out and revoke the current token").action(async () => {
|
|
8965
|
+
await runLogout();
|
|
8966
|
+
});
|
|
8967
|
+
program.command("status").description("Show the current account, plan, and token").action(async () => {
|
|
8968
|
+
await runStatus();
|
|
8969
|
+
});
|
|
8970
|
+
program.command("usage").description("Show your current billing period's scan usage").action(async () => {
|
|
8971
|
+
await runUsage();
|
|
8972
|
+
});
|
|
8973
|
+
program.command("upgrade").description("Open the VibeDrift pricing page").action(async () => {
|
|
8974
|
+
await runUpgrade();
|
|
8975
|
+
});
|
|
8976
|
+
program.command("billing").description("Open the Stripe Customer Portal to manage your subscription").action(async () => {
|
|
8977
|
+
await runBilling();
|
|
8978
|
+
});
|
|
8979
|
+
program.command("doctor").description("Diagnose CLI installation, auth, and API connectivity").action(async () => {
|
|
8980
|
+
await runDoctor();
|
|
8981
|
+
});
|
|
8982
|
+
program.command("update").description("Update the VibeDrift CLI to the latest version").action(async () => {
|
|
8983
|
+
await runUpdate(VERSION);
|
|
8984
|
+
});
|
|
7774
8985
|
program.addHelpText(
|
|
7775
8986
|
"after",
|
|
7776
8987
|
`
|
|
@@ -7780,8 +8991,20 @@ Examples:
|
|
|
7780
8991
|
$ vibedrift --format terminal print results to the terminal
|
|
7781
8992
|
$ vibedrift --json > report.json pipe JSON output to a file
|
|
7782
8993
|
$ vibedrift --fail-on-score 70 fail CI if score drops below 70
|
|
7783
|
-
$ vibedrift --
|
|
7784
|
-
$ vibedrift --
|
|
8994
|
+
$ vibedrift --include "src/**" only scan files under src/
|
|
8995
|
+
$ vibedrift --exclude "**/*.spec.*" skip test files
|
|
8996
|
+
$ vibedrift --deep run premium AI-powered deep analysis
|
|
8997
|
+
$ vibedrift login sign in to enable --deep
|
|
8998
|
+
$ vibedrift status check current auth state
|
|
8999
|
+
$ vibedrift usage view this month's scan usage
|
|
9000
|
+
$ vibedrift upgrade open the pricing page
|
|
9001
|
+
$ vibedrift billing manage your Stripe subscription
|
|
9002
|
+
$ vibedrift update update to the latest CLI version
|
|
9003
|
+
|
|
9004
|
+
Environment:
|
|
9005
|
+
VIBEDRIFT_TOKEN bearer token (overrides ~/.vibedrift/config.json)
|
|
9006
|
+
VIBEDRIFT_API_URL override the API base URL
|
|
9007
|
+
VIBEDRIFT_NO_BROWSER if "1", never auto-open the browser
|
|
7785
9008
|
|
|
7786
9009
|
Learn more: https://vibedrift.ai`
|
|
7787
9010
|
);
|