@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/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${escapeRegex2(name)}\\b`, "g"), placeholder);
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 escapeRegex2(s) {
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, apiKey, apiUrl) {
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 (apiKey) {
1021
- headers["X-API-Key"] = apiKey;
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(`ML API error ${response.status}: ${errorBody.slice(0, 200)}`);
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(`[ml] Sending ${sampled.length} functions + ${deviations.length} deviations (${Math.round(payloadSize / 1024)}KB) to ML API...`);
1270
- console.error(`[ml] No full files transmitted \u2014 only function snippets and structural metadata.`);
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.mlKey, options.mlUrl);
1381
+ const response = await callMlApi(request, options.token, options.apiUrl);
1280
1382
  if (options.verbose) {
1281
1383
  console.error(
1282
- `[ml] 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`
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, mlKey) {
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 (mlKey) headers["X-API-Key"] = mlKey;
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 writeFile2 } from "fs/promises";
1973
- import { stat as stat2 } from "fs/promises";
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 info = await stat(fullPath);
2090
- if (info.size > MAX_FILE_SIZE) continue;
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 basename = file.path.split("/").pop()?.replace(/\.[^.]+$/, "") ?? "";
4921
- if (basename.length <= 1) continue;
4922
- if (/(?:test|spec|config|setup|__)/i.test(basename)) continue;
4923
- const conv = classifyName(basename);
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, premiumUsed) {
5966
- if (premiumUsed) return [];
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 --ml for premium ML-powered context analysis`
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 --ml for premium architectural recommendations`
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 --ml for premium semantic analysis`
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 --ml for premium architectural pattern detection and intent analysis`
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">ML ANALYSIS <span style="font-size:11px;font-weight:400;letter-spacing:0;text-transform:none;color:var(--info-blue);margin-left:8px">Layer 2 &middot; UniXcoder Embeddings &middot; ${mlFindings.length} findings</span></div>
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 &middot; Code Embeddings &middot; ${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 the ML inference API using code embeddings. Only function snippets (not full files) were sent for analysis &mdash; snippets are processed in memory and not stored.
7137
+ These findings were detected by VibeDrift&rsquo;s AI inference API. Only function snippets (not full files) were sent for analysis &mdash; 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 hasMl = result.findings.some((f) => f.tags?.includes("ml")) || !!result.aiSummary;
6986
- const premiumUpsell = !hasMl ? `<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">
6987
- Want premium ML-powered analysis? Get a VibeDrift API key at <a href="https://vibedrift.ai" style="color:var(--text-primary);text-decoration:underline">vibedrift.ai</a><br>
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 . --ml --ml-key vd_...</code>
6989
- <span data-copy="vibedrift . --ml --ml-key vd_..." style="cursor:pointer;margin-left:6px;font-size:11px;color:var(--text-tertiary)">[copy]</span>
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 &middot; ${result.context.totalLines.toLocaleString()} lines &middot; ${(result.scanTimeMs / 1e3).toFixed(1)}s</p>
6999
- <p style="margin:4px 0;font-size:12px">${hasMl ? "Function snippets were sent to VibeDrift&rsquo;s ML API for analysis. No full files transmitted. Snippets processed in memory and not stored." : "No data sent externally."}</p>
7261
+ <p style="margin:4px 0;font-size:12px">${hasDeep ? "Function snippets were sent to VibeDrift&rsquo;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,'&amp;').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 HISTORY_DIR = ".vibedrift";
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 = join4(rootDir, HISTORY_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 = join4(rootDir, HISTORY_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 info = await stat2(rootDir);
7442
- if (!info.isDirectory()) {
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.ml) {
7997
+ if (options.deep && bearerToken) {
7507
7998
  const t5 = Date.now();
7508
- if (spinner) spinner.text = "Connecting to ML service (may take ~30s on cold start)...";
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
- mlKey: options.mlKey,
7513
- mlUrl: options.mlUrl,
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(`[ml] ${mlResult.highConfidence.length} high-confidence findings shipped, ${mlResult.mediumConfidence.length} sent to LLM, ${mlResult.droppedCount} dropped`);
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(`[ml] ML analysis failed: ${err.message}`);
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.ml = Date.now() - t5;
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, false);
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.ml) parts.push(`ML API: ${(timings.ml / 1e3).toFixed(1)}s`);
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.ml) {
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 apiUrl = options.mlUrl ?? "https://vibedrift-api.fly.dev";
7572
- if (options.verbose) console.error(`[summary] Calling ${apiUrl}/v1/summarize...`);
7573
- const summary = await fetchAiSummary2(result, apiUrl, options.mlKey);
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 writeFile2(outputPath, html);
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 writeFile2(outputPath, csv);
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 writeFile2(outputPath, docx);
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 writeFile2(options.output, json);
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").argument("[path]", "path to project directory", ".").option(
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("--ml", "enable premium ML-powered analysis (requires VibeDrift key)").option("--ml-key <key>", "VibeDrift API key for premium features").option("--update", "update VibeDrift CLI to the latest version").option("--verbose", "show timing breakdown and analyzer details").addOption(
7756
- new Option("--ml-url <url>", "VibeDrift API endpoint").default("https://vibedrift-api.fly.dev").hideHelp()
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
- ml: options.ml,
7769
- mlKey: options.mlKey,
7770
- mlUrl: options.mlUrl,
7771
- verbose: options.verbose
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 --ml --ml-key vd_... run premium ML-powered analysis
7784
- $ vibedrift --update update to the latest version
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
  );