@vibedrift/cli 0.2.0 → 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
@@ -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, {
@@ -1269,9 +1358,22 @@ async function runMlAnalysis(ctx, codeDnaResult, findings, options) {
1269
1358
  console.error(`[deep] Sending ${sampled.length} functions + ${deviations.length} deviations (${Math.round(payloadSize / 1024)}KB) to VibeDrift API...`);
1270
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: []
@@ -1283,6 +1385,9 @@ async function runMlAnalysis(ctx, codeDnaResult, findings, options) {
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(
@@ -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, {
@@ -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);
@@ -7658,6 +7917,15 @@ async function runScan(targetPath, options) {
7658
7917
  }
7659
7918
  bearerToken = resolved.token;
7660
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
+ }
7661
7929
  }
7662
7930
  const startTime = Date.now();
7663
7931
  const timings = {};
@@ -7735,7 +8003,8 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
7735
8003
  token: bearerToken,
7736
8004
  apiUrl,
7737
8005
  verbose: options.verbose,
7738
- driftFindings: driftResult.driftFindings
8006
+ driftFindings: driftResult.driftFindings,
8007
+ projectName: options.projectName
7739
8008
  });
7740
8009
  allFindings.push(...mlResult.highConfidence);
7741
8010
  mlMediumConfidence = mlResult.mediumConfidence;
@@ -7806,6 +8075,62 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
7806
8075
  }
7807
8076
  }
7808
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
+ }
7809
8134
  const format = options.format ?? (options.json ? "json" : "html");
7810
8135
  if (format === "html") {
7811
8136
  const html = renderHtmlReport(result);
@@ -8416,7 +8741,7 @@ async function runBilling() {
8416
8741
  init_esm_shims();
8417
8742
  import chalk10 from "chalk";
8418
8743
  import { homedir as homedir3, platform, arch } from "os";
8419
- import { join as join6 } from "path";
8744
+ import { join as join7 } from "path";
8420
8745
  import { stat as stat4, access, constants } from "fs/promises";
8421
8746
  init_version();
8422
8747
  async function runDoctor() {
@@ -8462,7 +8787,7 @@ async function runDoctor() {
8462
8787
  info("Config file", "absent (not logged in)");
8463
8788
  }
8464
8789
  }
8465
- const historyDir = join6(homedir3(), ".vibedrift", "scans");
8790
+ const historyDir = join7(homedir3(), ".vibedrift", "scans");
8466
8791
  try {
8467
8792
  const info2 = await stat4(historyDir);
8468
8793
  if (info2.isDirectory()) ok("Scan history", historyDir);
@@ -8591,6 +8916,9 @@ program.command("scan", { isDefault: true }).description("Scan a project for vib
8591
8916
  ).option("--no-codedna", "skip Code DNA semantic analysis").option(
8592
8917
  "--deep",
8593
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"
8594
8922
  ).option(
8595
8923
  "--include <pattern>",
8596
8924
  "only scan files matching this glob (repeatable)",
@@ -8621,7 +8949,8 @@ program.command("scan", { isDefault: true }).description("Scan a project for vib
8621
8949
  apiUrl: options.apiUrl,
8622
8950
  include: options.include,
8623
8951
  exclude: options.exclude,
8624
- verbose: options.verbose
8952
+ verbose: options.verbose,
8953
+ projectName: options.projectName
8625
8954
  });
8626
8955
  });
8627
8956
  program.command("login").description("Log in to your VibeDrift account").option("--no-browser", "don't open the browser automatically").addOption(