@vibedrift/cli 0.2.0 → 0.3.1

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);
@@ -7630,6 +7889,104 @@ function previewToken(token) {
7630
7889
  return token.slice(0, 12) + "\u2026";
7631
7890
  }
7632
7891
 
7892
+ // src/auth/api.ts
7893
+ init_esm_shims();
7894
+ var REQUEST_TIMEOUT_MS = 3e4;
7895
+ var VibeDriftApiError = class extends Error {
7896
+ constructor(status, message) {
7897
+ super(message);
7898
+ this.status = status;
7899
+ this.name = "VibeDriftApiError";
7900
+ }
7901
+ status;
7902
+ };
7903
+ async function jsonFetch(url, init = {}) {
7904
+ const controller = new AbortController();
7905
+ const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
7906
+ try {
7907
+ const res = await fetch(url, {
7908
+ ...init,
7909
+ signal: controller.signal,
7910
+ headers: {
7911
+ Accept: "application/json",
7912
+ ...init.body ? { "Content-Type": "application/json" } : {},
7913
+ ...init.headers ?? {}
7914
+ }
7915
+ });
7916
+ if (!res.ok) {
7917
+ let detail = "";
7918
+ try {
7919
+ const body = await res.json();
7920
+ detail = body.detail ?? body.message ?? "";
7921
+ } catch {
7922
+ try {
7923
+ detail = await res.text();
7924
+ } catch {
7925
+ }
7926
+ }
7927
+ throw new VibeDriftApiError(res.status, detail || `HTTP ${res.status}`);
7928
+ }
7929
+ return await res.json();
7930
+ } catch (err) {
7931
+ if (err?.name === "AbortError") {
7932
+ throw new VibeDriftApiError(0, `Request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s`);
7933
+ }
7934
+ if (err instanceof VibeDriftApiError) throw err;
7935
+ throw new VibeDriftApiError(0, err?.message ?? String(err));
7936
+ } finally {
7937
+ clearTimeout(timer);
7938
+ }
7939
+ }
7940
+ async function startDeviceAuth(opts) {
7941
+ const base = await resolveApiUrl(opts?.apiUrl);
7942
+ return jsonFetch(`${base}/auth/device`, {
7943
+ method: "POST",
7944
+ body: JSON.stringify({ client_id: "vibedrift-cli" })
7945
+ });
7946
+ }
7947
+ async function pollDeviceAuth(deviceCode, opts) {
7948
+ const base = await resolveApiUrl(opts?.apiUrl);
7949
+ return jsonFetch(`${base}/auth/poll`, {
7950
+ method: "POST",
7951
+ body: JSON.stringify({ device_code: deviceCode })
7952
+ });
7953
+ }
7954
+ async function validateToken(token, opts) {
7955
+ const base = await resolveApiUrl(opts?.apiUrl);
7956
+ return jsonFetch(`${base}/auth/validate`, {
7957
+ method: "GET",
7958
+ headers: { Authorization: `Bearer ${token}` }
7959
+ });
7960
+ }
7961
+ async function revokeToken(token, opts) {
7962
+ const base = await resolveApiUrl(opts?.apiUrl);
7963
+ await jsonFetch(`${base}/auth/revoke`, {
7964
+ method: "POST",
7965
+ headers: { Authorization: `Bearer ${token}` }
7966
+ });
7967
+ }
7968
+ async function fetchUsage(token, opts) {
7969
+ const base = await resolveApiUrl(opts?.apiUrl);
7970
+ return jsonFetch(`${base}/account/usage`, {
7971
+ method: "GET",
7972
+ headers: { Authorization: `Bearer ${token}` }
7973
+ });
7974
+ }
7975
+ async function fetchCredits(token, opts) {
7976
+ const base = await resolveApiUrl(opts?.apiUrl);
7977
+ return jsonFetch(`${base}/account/credits`, {
7978
+ method: "GET",
7979
+ headers: { Authorization: `Bearer ${token}` }
7980
+ });
7981
+ }
7982
+ async function createPortalSession(token, opts) {
7983
+ const base = await resolveApiUrl(opts?.apiUrl);
7984
+ return jsonFetch(`${base}/account/portal`, {
7985
+ method: "POST",
7986
+ headers: { Authorization: `Bearer ${token}` }
7987
+ });
7988
+ }
7989
+
7633
7990
  // src/cli/commands/scan.ts
7634
7991
  async function runScan(targetPath, options) {
7635
7992
  const rootDir = resolve(targetPath);
@@ -7651,13 +8008,43 @@ async function runScan(targetPath, options) {
7651
8008
  console.error("");
7652
8009
  console.error(chalk2.red(" \u2717 Deep scans require a VibeDrift account."));
7653
8010
  console.error("");
7654
- console.error(" Run " + chalk2.bold("vibedrift login") + " to sign in.");
8011
+ console.error(chalk2.bgYellow.black.bold(" \u{1F381} But your first deep scan is FREE. "));
8012
+ console.error("");
8013
+ console.error(" Run " + chalk2.bold("vibedrift login") + " to sign in and claim it.");
7655
8014
  console.error(" Or set " + chalk2.bold("VIBEDRIFT_TOKEN") + " in your environment for CI.");
7656
8015
  console.error("");
7657
8016
  process.exit(1);
7658
8017
  }
7659
8018
  bearerToken = resolved.token;
7660
8019
  apiUrl = await resolveApiUrl(options.apiUrl);
8020
+ } else {
8021
+ try {
8022
+ const resolved = await resolveToken();
8023
+ if (resolved) {
8024
+ bearerToken = resolved.token;
8025
+ apiUrl = await resolveApiUrl(options.apiUrl);
8026
+ }
8027
+ } catch {
8028
+ }
8029
+ }
8030
+ if (!options.json && options.format !== "json" && !options.deep) {
8031
+ if (bearerToken) {
8032
+ try {
8033
+ const credits = await fetchCredits(bearerToken, { apiUrl });
8034
+ if (credits.has_free_deep_scan && !credits.unlimited) {
8035
+ console.log("");
8036
+ console.log(chalk2.bgYellow.black.bold(" \u{1F381} 1 FREE DEEP SCAN AVAILABLE "));
8037
+ console.log(chalk2.yellow(" Run with ") + chalk2.bold.cyan("--deep") + chalk2.yellow(" to use Claude-powered analysis (no card required)."));
8038
+ console.log("");
8039
+ }
8040
+ } catch {
8041
+ }
8042
+ } else {
8043
+ console.log("");
8044
+ console.log(chalk2.dim(" Tip: ") + chalk2.yellow("sign up free to get 1 deep scan included"));
8045
+ console.log(chalk2.dim(" Run ") + chalk2.bold("vibedrift login") + chalk2.dim(" to claim it (no card required)."));
8046
+ console.log("");
8047
+ }
7661
8048
  }
7662
8049
  const startTime = Date.now();
7663
8050
  const timings = {};
@@ -7735,7 +8122,8 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
7735
8122
  token: bearerToken,
7736
8123
  apiUrl,
7737
8124
  verbose: options.verbose,
7738
- driftFindings: driftResult.driftFindings
8125
+ driftFindings: driftResult.driftFindings,
8126
+ projectName: options.projectName
7739
8127
  });
7740
8128
  allFindings.push(...mlResult.highConfidence);
7741
8129
  mlMediumConfidence = mlResult.mediumConfidence;
@@ -7806,6 +8194,62 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
7806
8194
  }
7807
8195
  }
7808
8196
  await saveScanResult(rootDir, scores, compositeScore);
8197
+ if (bearerToken) {
8198
+ try {
8199
+ const { logScan: logScan2 } = await Promise.resolve().then(() => (init_log_scan(), log_scan_exports));
8200
+ const { detectProjectIdentity: detectProjectIdentity2 } = await Promise.resolve().then(() => (init_project_name(), project_name_exports));
8201
+ const { sanitizeResultForUpload: sanitizeResultForUpload2 } = await Promise.resolve().then(() => (init_sanitize_result(), sanitize_result_exports));
8202
+ const projectIdentity = await detectProjectIdentity2(
8203
+ rootDir,
8204
+ options.projectName
8205
+ );
8206
+ const mlDuplicates = allFindings.filter((f) => f.analyzerId === "ml-duplicate").length;
8207
+ const mlIntent = allFindings.filter((f) => f.analyzerId === "ml-intent").length;
8208
+ const mlAnomaly = allFindings.filter((f) => f.analyzerId === "ml-anomaly").length;
8209
+ let html;
8210
+ try {
8211
+ html = renderHtmlReport(result);
8212
+ } catch {
8213
+ html = void 0;
8214
+ }
8215
+ const pct = maxCompositeScore > 0 ? compositeScore / maxCompositeScore * 100 : 0;
8216
+ const grade = pct >= 90 ? "A" : pct >= 75 ? "B" : pct >= 50 ? "C" : pct >= 25 ? "D" : "F";
8217
+ const sanitizedResult = sanitizeResultForUpload2(result);
8218
+ sanitizedResult.project = {
8219
+ name: projectIdentity.name,
8220
+ hash: projectIdentity.hash
8221
+ };
8222
+ sanitizedResult.grade = grade;
8223
+ sanitizedResult.scanType = options.deep ? "deep" : "free";
8224
+ sanitizedResult.scannedAt = (/* @__PURE__ */ new Date()).toISOString();
8225
+ await logScan2({
8226
+ token: bearerToken,
8227
+ apiUrl,
8228
+ verbose: options.verbose,
8229
+ payload: {
8230
+ project_hash: projectIdentity.hash,
8231
+ project_name: projectIdentity.name,
8232
+ language: ctx.dominantLanguage ?? "unknown",
8233
+ file_count: ctx.files.length,
8234
+ function_count: codeDnaResult?.functions?.length ?? 0,
8235
+ finding_count: allFindings.length,
8236
+ score: compositeScore,
8237
+ grade,
8238
+ duplicates_found: mlDuplicates,
8239
+ intent_mismatches: mlIntent,
8240
+ anomalies_found: mlAnomaly,
8241
+ is_deep: !!options.deep,
8242
+ processing_time_ms: scanTimeMs,
8243
+ report_html: html,
8244
+ result_json: sanitizedResult
8245
+ }
8246
+ });
8247
+ } catch (err) {
8248
+ if (options.verbose) {
8249
+ console.error(chalk2.dim(`[scan-log] Failed: ${err.message}`));
8250
+ }
8251
+ }
8252
+ }
7809
8253
  const format = options.format ?? (options.json ? "json" : "html");
7810
8254
  if (format === "html") {
7811
8255
  const html = renderHtmlReport(result);
@@ -7956,97 +8400,6 @@ Update failed: ${message}`));
7956
8400
  init_esm_shims();
7957
8401
  import chalk4 from "chalk";
7958
8402
 
7959
- // src/auth/api.ts
7960
- init_esm_shims();
7961
- var REQUEST_TIMEOUT_MS = 3e4;
7962
- var VibeDriftApiError = class extends Error {
7963
- constructor(status, message) {
7964
- super(message);
7965
- this.status = status;
7966
- this.name = "VibeDriftApiError";
7967
- }
7968
- status;
7969
- };
7970
- async function jsonFetch(url, init = {}) {
7971
- const controller = new AbortController();
7972
- const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
7973
- try {
7974
- const res = await fetch(url, {
7975
- ...init,
7976
- signal: controller.signal,
7977
- headers: {
7978
- Accept: "application/json",
7979
- ...init.body ? { "Content-Type": "application/json" } : {},
7980
- ...init.headers ?? {}
7981
- }
7982
- });
7983
- if (!res.ok) {
7984
- let detail = "";
7985
- try {
7986
- const body = await res.json();
7987
- detail = body.detail ?? body.message ?? "";
7988
- } catch {
7989
- try {
7990
- detail = await res.text();
7991
- } catch {
7992
- }
7993
- }
7994
- throw new VibeDriftApiError(res.status, detail || `HTTP ${res.status}`);
7995
- }
7996
- return await res.json();
7997
- } catch (err) {
7998
- if (err?.name === "AbortError") {
7999
- throw new VibeDriftApiError(0, `Request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s`);
8000
- }
8001
- if (err instanceof VibeDriftApiError) throw err;
8002
- throw new VibeDriftApiError(0, err?.message ?? String(err));
8003
- } finally {
8004
- clearTimeout(timer);
8005
- }
8006
- }
8007
- async function startDeviceAuth(opts) {
8008
- const base = await resolveApiUrl(opts?.apiUrl);
8009
- return jsonFetch(`${base}/auth/device`, {
8010
- method: "POST",
8011
- body: JSON.stringify({ client_id: "vibedrift-cli" })
8012
- });
8013
- }
8014
- async function pollDeviceAuth(deviceCode, opts) {
8015
- const base = await resolveApiUrl(opts?.apiUrl);
8016
- return jsonFetch(`${base}/auth/poll`, {
8017
- method: "POST",
8018
- body: JSON.stringify({ device_code: deviceCode })
8019
- });
8020
- }
8021
- async function validateToken(token, opts) {
8022
- const base = await resolveApiUrl(opts?.apiUrl);
8023
- return jsonFetch(`${base}/auth/validate`, {
8024
- method: "GET",
8025
- headers: { Authorization: `Bearer ${token}` }
8026
- });
8027
- }
8028
- async function revokeToken(token, opts) {
8029
- const base = await resolveApiUrl(opts?.apiUrl);
8030
- await jsonFetch(`${base}/auth/revoke`, {
8031
- method: "POST",
8032
- headers: { Authorization: `Bearer ${token}` }
8033
- });
8034
- }
8035
- async function fetchUsage(token, opts) {
8036
- const base = await resolveApiUrl(opts?.apiUrl);
8037
- return jsonFetch(`${base}/account/usage`, {
8038
- method: "GET",
8039
- headers: { Authorization: `Bearer ${token}` }
8040
- });
8041
- }
8042
- async function createPortalSession(token, opts) {
8043
- const base = await resolveApiUrl(opts?.apiUrl);
8044
- return jsonFetch(`${base}/account/portal`, {
8045
- method: "POST",
8046
- headers: { Authorization: `Bearer ${token}` }
8047
- });
8048
- }
8049
-
8050
8403
  // src/auth/browser.ts
8051
8404
  init_esm_shims();
8052
8405
  import { spawn as spawn2 } from "child_process";
@@ -8153,11 +8506,43 @@ async function runLogin(options = {}) {
8153
8506
  console.log(` Account: ${chalk4.bold(result.email)}`);
8154
8507
  console.log(` Plan: ${chalk4.bold(result.plan)}`);
8155
8508
  console.log("");
8156
- if (result.plan === "free") {
8157
- console.log(chalk4.dim(" Run `vibedrift upgrade` to enable deep AI scans."));
8158
- console.log("");
8159
- } else {
8160
- console.log(chalk4.dim(" Run `vibedrift . --deep` to use AI-powered analysis."));
8509
+ try {
8510
+ const credits = await fetchCredits(result.access_token, {
8511
+ apiUrl: options.apiUrl
8512
+ });
8513
+ if (credits.has_free_deep_scan && !credits.unlimited) {
8514
+ console.log(
8515
+ chalk4.bgYellow.black.bold(" \u{1F381} 1 FREE deep scan included with your account ")
8516
+ );
8517
+ console.log("");
8518
+ console.log(
8519
+ chalk4.yellow(" Try the full pipeline (Claude analysis, security review,")
8520
+ );
8521
+ console.log(
8522
+ chalk4.yellow(" AI-powered drift detection) on any project \u2014 no card needed.")
8523
+ );
8524
+ console.log("");
8525
+ console.log(` ${chalk4.cyan("vibedrift . --deep")}`);
8526
+ console.log("");
8527
+ } else if (credits.unlimited) {
8528
+ console.log(chalk4.dim(" Run `vibedrift . --deep` to use AI-powered analysis."));
8529
+ console.log("");
8530
+ } else if (credits.available_total > 0) {
8531
+ console.log(
8532
+ chalk4.dim(` You have ${credits.available_total} deep scan credit${credits.available_total === 1 ? "" : "s"} available.`)
8533
+ );
8534
+ console.log(chalk4.dim(" Run `vibedrift . --deep` to use one."));
8535
+ console.log("");
8536
+ } else {
8537
+ console.log(chalk4.dim(" Run `vibedrift upgrade` to enable deep AI scans."));
8538
+ console.log("");
8539
+ }
8540
+ } catch {
8541
+ if (result.plan === "free") {
8542
+ console.log(chalk4.dim(" Run `vibedrift upgrade` to enable deep AI scans."));
8543
+ } else {
8544
+ console.log(chalk4.dim(" Run `vibedrift . --deep` to use AI-powered analysis."));
8545
+ }
8161
8546
  console.log("");
8162
8547
  }
8163
8548
  return;
@@ -8269,6 +8654,21 @@ async function runStatus() {
8269
8654
  console.log(chalk6.dim(` ${err.message}`));
8270
8655
  }
8271
8656
  }
8657
+ try {
8658
+ const credits = await fetchCredits(resolved.token, { apiUrl: config.apiUrl });
8659
+ console.log("");
8660
+ if (credits.unlimited) {
8661
+ console.log(` Deep scans: ${chalk6.bold.green("unlimited")} (${credits.plan})`);
8662
+ } else if (credits.has_free_deep_scan) {
8663
+ console.log(` Deep scans: ${chalk6.bold.yellow("1 free")} + ${credits.available_purchased} purchased`);
8664
+ console.log(chalk6.dim(" Run `vibedrift . --deep` to use your free credit."));
8665
+ } else if (credits.available_total > 0) {
8666
+ console.log(` Deep scans: ${chalk6.bold(credits.available_total)} credit${credits.available_total === 1 ? "" : "s"} available`);
8667
+ } else {
8668
+ console.log(` Deep scans: ${chalk6.dim("0 credits")} \u2014 run \`vibedrift upgrade\` for more`);
8669
+ }
8670
+ } catch {
8671
+ }
8272
8672
  console.log("");
8273
8673
  }
8274
8674
  function describeSource(source) {
@@ -8416,7 +8816,7 @@ async function runBilling() {
8416
8816
  init_esm_shims();
8417
8817
  import chalk10 from "chalk";
8418
8818
  import { homedir as homedir3, platform, arch } from "os";
8419
- import { join as join6 } from "path";
8819
+ import { join as join7 } from "path";
8420
8820
  import { stat as stat4, access, constants } from "fs/promises";
8421
8821
  init_version();
8422
8822
  async function runDoctor() {
@@ -8462,7 +8862,7 @@ async function runDoctor() {
8462
8862
  info("Config file", "absent (not logged in)");
8463
8863
  }
8464
8864
  }
8465
- const historyDir = join6(homedir3(), ".vibedrift", "scans");
8865
+ const historyDir = join7(homedir3(), ".vibedrift", "scans");
8466
8866
  try {
8467
8867
  const info2 = await stat4(historyDir);
8468
8868
  if (info2.isDirectory()) ok("Scan history", historyDir);
@@ -8591,6 +8991,9 @@ program.command("scan", { isDefault: true }).description("Scan a project for vib
8591
8991
  ).option("--no-codedna", "skip Code DNA semantic analysis").option(
8592
8992
  "--deep",
8593
8993
  "enable AI-powered deep analysis (requires `vibedrift login`)"
8994
+ ).option(
8995
+ "--project-name <name>",
8996
+ "override the auto-detected project name shown in the dashboard"
8594
8997
  ).option(
8595
8998
  "--include <pattern>",
8596
8999
  "only scan files matching this glob (repeatable)",
@@ -8621,7 +9024,8 @@ program.command("scan", { isDefault: true }).description("Scan a project for vib
8621
9024
  apiUrl: options.apiUrl,
8622
9025
  include: options.include,
8623
9026
  exclude: options.exclude,
8624
- verbose: options.verbose
9027
+ verbose: options.verbose,
9028
+ projectName: options.projectName
8625
9029
  });
8626
9030
  });
8627
9031
  program.command("login").description("Log in to your VibeDrift account").option("--no-browser", "don't open the browser automatically").addOption(