bitfab-cli 0.2.185 → 0.2.187

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.
Files changed (2) hide show
  1. package/dist/index.js +734 -10
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -9566,6 +9566,40 @@ function updateActiveStudioSession(updates) {
9566
9566
  writeStateFile(sessionFilePath(), updated);
9567
9567
  return updated;
9568
9568
  }
9569
+ function clearActiveStudioSession(sessionId, opts = {}) {
9570
+ try {
9571
+ const current = readStateFile(sessionFilePath());
9572
+ if (!current) {
9573
+ return clearLegacySession(sessionId, opts);
9574
+ }
9575
+ if (current.sessionId !== sessionId) {
9576
+ return false;
9577
+ }
9578
+ if (!opts.force && current.pollerPid != null && current.pollerPid !== process.pid && isProcessAlive(current.pollerPid)) {
9579
+ return false;
9580
+ }
9581
+ fs7.unlinkSync(sessionFilePath());
9582
+ return true;
9583
+ } catch {
9584
+ return false;
9585
+ }
9586
+ }
9587
+ function clearLegacySession(sessionId, opts = {}) {
9588
+ try {
9589
+ const raw = fs7.readFileSync(legacyFilePath(), "utf-8");
9590
+ const parsed = JSON.parse(raw);
9591
+ if (parsed.sessionId !== sessionId) {
9592
+ return false;
9593
+ }
9594
+ if (!opts.force && typeof parsed.pid === "number" && parsed.pid !== process.pid && isProcessAlive(parsed.pid)) {
9595
+ return false;
9596
+ }
9597
+ fs7.unlinkSync(legacyFilePath());
9598
+ return true;
9599
+ } catch {
9600
+ return false;
9601
+ }
9602
+ }
9569
9603
  function isProcessAlive(pid) {
9570
9604
  try {
9571
9605
  process.kill(pid, 0);
@@ -11003,6 +11037,48 @@ async function resolveChannel(apiKey, serviceUrl) {
11003
11037
  return new DirectChannel(apiKey, serviceUrl);
11004
11038
  }
11005
11039
 
11040
+ // ../bitfab-plugin-lib/dist/studioTeardown.js
11041
+ function killStudioWindow(sessionId) {
11042
+ const record2 = readActiveStudioSession();
11043
+ if (record2?.sessionId !== sessionId || record2.windowPid == null) {
11044
+ return;
11045
+ }
11046
+ try {
11047
+ process.kill(record2.windowPid, "SIGTERM");
11048
+ } catch {
11049
+ }
11050
+ }
11051
+ function killPollerProcess(sessionId) {
11052
+ const record2 = readActiveStudioSession();
11053
+ if (record2?.sessionId !== sessionId || record2.pollerPid == null || record2.pollerPid === process.pid) {
11054
+ return;
11055
+ }
11056
+ try {
11057
+ process.kill(record2.pollerPid, "SIGTERM");
11058
+ } catch {
11059
+ }
11060
+ }
11061
+
11062
+ // ../bitfab-plugin-lib/dist/commands/clearStudioSession.js
11063
+ async function clearStudioSessionById(sessionId, record2) {
11064
+ killStudioWindow(sessionId);
11065
+ const alreadyClosed = record2?.sessionId === sessionId && record2.windowState === "closed";
11066
+ if (!alreadyClosed) {
11067
+ closeStudioWindowsBySession(sessionId);
11068
+ }
11069
+ killPollerProcess(sessionId);
11070
+ try {
11071
+ const channel = await resolveChannel("", record2?.serviceUrl ?? "");
11072
+ try {
11073
+ await channel.clearSession(sessionId);
11074
+ } finally {
11075
+ channel.destroy();
11076
+ }
11077
+ } catch {
11078
+ }
11079
+ return clearActiveStudioSession(sessionId, { force: true });
11080
+ }
11081
+
11006
11082
  // ../bitfab-plugin-lib/dist/replayCapabilities.js
11007
11083
  var semver = __toESM(require_semver2(), 1);
11008
11084
 
@@ -11451,6 +11527,13 @@ Already logged in as ${identity}${endpoint}. Run the login command with --force
11451
11527
  return;
11452
11528
  }
11453
11529
  }
11530
+ if (force) {
11531
+ const record2 = readActiveStudioSession();
11532
+ if (record2) {
11533
+ await clearStudioSessionById(record2.sessionId, record2).catch(() => {
11534
+ });
11535
+ }
11536
+ }
11454
11537
  try {
11455
11538
  let printedSignInUrl = false;
11456
11539
  const result = await openStudioTo("/studio", {
@@ -11495,7 +11578,7 @@ ${greeting}`);
11495
11578
  } catch (err) {
11496
11579
  if (exitOnComplete) {
11497
11580
  if (err instanceof StudioNavigationError && err.staleSessionId) {
11498
- console.error("\nA Studio window is recorded as open but is not responding. Close it (or run the clearStudioSession command), then try again.");
11581
+ console.error("\nA Studio window is recorded as open but is not responding. Close the Studio window and try again. To force-clear a stale session, a user can re-run with `bitfab login --force` (manual recovery - automated agents should not run it).");
11499
11582
  } else {
11500
11583
  console.error(`
11501
11584
  ${err.message}`);
@@ -30999,6 +31082,16 @@ var SETUP_COMMAND = "/bitfab:setup";
30999
31082
  var ANALYZE_REPO_COMMAND = "/bitfab:setup analyze-repo";
31000
31083
  var ASSISTANT_COMMAND = "/bitfab:assistant";
31001
31084
  var UPDATE_COMMAND = "/bitfab:update";
31085
+ var ANSI = {
31086
+ reset: "\x1B[0m",
31087
+ bold: "\x1B[1m",
31088
+ dim: "\x1B[2m",
31089
+ green: "\x1B[32m",
31090
+ yellow: "\x1B[33m",
31091
+ cyan: "\x1B[36m",
31092
+ magenta: "\x1B[35m",
31093
+ red: "\x1B[31m"
31094
+ };
31002
31095
  function isClaudeAuthStatusLoggedIn(output) {
31003
31096
  try {
31004
31097
  const status = JSON.parse(output);
@@ -31016,6 +31109,12 @@ var checkClaudeAuth = makeCliAuthCheck({
31016
31109
  });
31017
31110
  function runClaudeInstall() {
31018
31111
  const s = p3.spinner();
31112
+ const localPluginKey = activeLocalClaudeBitfabPlugin();
31113
+ if (localPluginKey !== null) {
31114
+ p3.log.step(`Local Bitfab plugin already enabled (${localPluginKey})`);
31115
+ p3.log.success("Bitfab plugin ready in Claude Code");
31116
+ return;
31117
+ }
31019
31118
  s.start("Adding bitfab marketplace");
31020
31119
  const marketplaceOut = runCli(CLI, ["plugin", "marketplace", "add", REPO]);
31021
31120
  if (marketplaceOut.includes("already")) {
@@ -31046,6 +31145,37 @@ function runClaudeInstall() {
31046
31145
  }
31047
31146
  p3.log.success("Bitfab plugin ready in Claude Code");
31048
31147
  }
31148
+ function activeLocalClaudeBitfabPlugin() {
31149
+ const settingsPath = path17.join(
31150
+ process.cwd(),
31151
+ ".claude",
31152
+ "settings.local.json"
31153
+ );
31154
+ if (!fs20.existsSync(settingsPath)) {
31155
+ return null;
31156
+ }
31157
+ let settings;
31158
+ try {
31159
+ settings = JSON.parse(fs20.readFileSync(settingsPath, "utf-8"));
31160
+ } catch {
31161
+ return null;
31162
+ }
31163
+ if (typeof settings !== "object" || settings === null) {
31164
+ return null;
31165
+ }
31166
+ const enabledPlugins = settings.enabledPlugins;
31167
+ if (typeof enabledPlugins !== "object" || enabledPlugins === null) {
31168
+ return null;
31169
+ }
31170
+ for (const [key, enabled] of Object.entries(
31171
+ enabledPlugins
31172
+ )) {
31173
+ if (key.startsWith("bitfab@") && key !== PLUGIN_KEY && enabled === true) {
31174
+ return key;
31175
+ }
31176
+ }
31177
+ return null;
31178
+ }
31049
31179
  function pullLatestClaudePlugin() {
31050
31180
  runCli(CLI, ["plugin", "marketplace", "update", MARKETPLACE]);
31051
31181
  return runCli(CLI, ["plugin", "update", PLUGIN_KEY, "--scope", "user"]);
@@ -31140,10 +31270,18 @@ async function runClaudeAnalyzeRepo(captureOverride, limit, prompt) {
31140
31270
  `analyze-repo exited with status ${exitCode}. See ${logPath}`
31141
31271
  );
31142
31272
  }
31143
- const uploaded = countUploadedTracePlans(stdoutChunks.join(""));
31273
+ const logText = stdoutChunks.join("");
31274
+ const uploaded = countUploadedTracePlans(logText);
31144
31275
  spinner6.stop(
31145
31276
  uploaded === 0 ? "No AI workflows found - nothing to upload" : `Uploaded ${uploaded} draft trace plan${uploaded === 1 ? "" : "s"} to Bitfab`
31146
31277
  );
31278
+ const report = extractAnalyzeRepoReport(logText);
31279
+ if (report !== null) {
31280
+ process.stdout.write(`
31281
+ ${formatAnalyzeRepoReport(report)}
31282
+
31283
+ `);
31284
+ }
31147
31285
  } catch (err) {
31148
31286
  spinner6.stop("analyze-repo failed");
31149
31287
  throw err;
@@ -31153,6 +31291,557 @@ async function runClaudeAnalyzeRepo(captureOverride, limit, prompt) {
31153
31291
  }
31154
31292
  p3.log.info(`Full run log: ${logPath}`);
31155
31293
  }
31294
+ function extractAnalyzeRepoReport(logText) {
31295
+ let lastAssistantText = null;
31296
+ let lastSubstantiveReport = null;
31297
+ let lastResultText = null;
31298
+ for (const line of logText.split("\n")) {
31299
+ if (line.trim() === "") {
31300
+ continue;
31301
+ }
31302
+ let event;
31303
+ try {
31304
+ event = JSON.parse(line);
31305
+ } catch {
31306
+ continue;
31307
+ }
31308
+ for (const assistantText of textCandidatesFromEvent(event)) {
31309
+ if (uploadedHeadingFromLine(assistantText) === null) {
31310
+ continue;
31311
+ }
31312
+ lastAssistantText = assistantText;
31313
+ lastSubstantiveReport = extractSubstantiveAnalyzeRepoReport(assistantText) ?? lastSubstantiveReport;
31314
+ }
31315
+ const resultText = resultTextFromEvent(event);
31316
+ if (resultText !== null && uploadedHeadingFromLine(resultText) !== null) {
31317
+ lastResultText = resultText;
31318
+ }
31319
+ }
31320
+ return lastSubstantiveReport ?? lastAssistantText ?? lastResultText;
31321
+ }
31322
+ function extractSubstantiveAnalyzeRepoReport(text) {
31323
+ const lines = text.trim().split(/\r?\n/);
31324
+ const tableStart = lines.findIndex((line, index) => {
31325
+ const next = lines[index + 1];
31326
+ return isMarkdownTableRow(line) && next !== void 0 && isTableDivider(next);
31327
+ });
31328
+ if (tableStart === -1) {
31329
+ return null;
31330
+ }
31331
+ const headingIndex = lines.findIndex((line, index) => {
31332
+ return index <= tableStart && uploadedHeadingFromLine(line) !== null;
31333
+ });
31334
+ if (headingIndex === -1) {
31335
+ return null;
31336
+ }
31337
+ let tableEnd = tableStart + 2;
31338
+ while (tableEnd < lines.length && isMarkdownTableRow(lines[tableEnd])) {
31339
+ tableEnd++;
31340
+ }
31341
+ const heading = uploadedHeadingFromLine(lines[headingIndex]);
31342
+ if (heading === null) {
31343
+ return null;
31344
+ }
31345
+ const followUp = usefulAnalyzeRepoFollowUp(lines.slice(tableEnd));
31346
+ return [heading, "", ...lines.slice(tableStart, tableEnd), ...followUp].join("\n").trim();
31347
+ }
31348
+ function formatAnalyzeRepoReport(report, options = {}) {
31349
+ const style = createAnalyzeRepoReportStyle(options);
31350
+ const lines = report.trim().split(/\r?\n/);
31351
+ const tableStart = lines.findIndex((line, index) => {
31352
+ const next = lines[index + 1];
31353
+ return isMarkdownTableRow(line) && next !== void 0 && isTableDivider(next);
31354
+ });
31355
+ if (tableStart === -1) {
31356
+ return styleNonTableReport(hideTracePlanLinks(report.trim()), style);
31357
+ }
31358
+ const headers = splitMarkdownTableRow(lines[tableStart]);
31359
+ const fieldValuePlan = formatFieldValuePlanTable(lines, tableStart, style);
31360
+ if (fieldValuePlan !== null) {
31361
+ return fieldValuePlan;
31362
+ }
31363
+ const indexes = {
31364
+ key: findColumnIndex(headers, ["trace function key", "function", "key"]),
31365
+ boundary: findColumnIndex(headers, ["boundary"]),
31366
+ description: findColumnIndex(headers, [
31367
+ "description of workflow",
31368
+ "workflow description",
31369
+ "description"
31370
+ ]),
31371
+ frameworksUsed: findColumnIndex(headers, ["frameworks used", "frameworks"]),
31372
+ refactorComplexity: findColumnIndex(headers, [
31373
+ "refactor complexity to instrument",
31374
+ "refactor complexity",
31375
+ "complexity"
31376
+ ]),
31377
+ methodsToCapture: findColumnIndex(headers, [
31378
+ "suggested methods to capture",
31379
+ "suggested methods to captured",
31380
+ "methods to capture",
31381
+ "methods captured"
31382
+ ]),
31383
+ methodsToMock: findColumnIndex(headers, [
31384
+ "number of methods that need to mocked",
31385
+ "number of methods that need to be mocked",
31386
+ "methods that need to be mocked",
31387
+ "methods to mock",
31388
+ "mocked methods"
31389
+ ]),
31390
+ realDataImprovement: findColumnIndex(headers, [
31391
+ "real data",
31392
+ "feature would improve",
31393
+ "improvement",
31394
+ "why",
31395
+ "worth tracing",
31396
+ "rationale"
31397
+ ])
31398
+ };
31399
+ if (indexes.key === -1) {
31400
+ return styleNonTableReport(hideTracePlanLinks(report.trim()), style);
31401
+ }
31402
+ let tableEnd = tableStart + 2;
31403
+ while (tableEnd < lines.length && isMarkdownTableRow(lines[tableEnd])) {
31404
+ tableEnd++;
31405
+ }
31406
+ const rows = lines.slice(tableStart + 2, tableEnd).map(splitMarkdownTableRow).filter((columns) => columns.length > indexes.key);
31407
+ if (rows.length === 0) {
31408
+ return styleNonTableReport(hideTracePlanLinks(report.trim()), style);
31409
+ }
31410
+ const before = lines.slice(0, tableStart).join("\n").trim();
31411
+ const after = formatTrailingReportBlock(lines.slice(tableEnd), style);
31412
+ const formattedRows = rows.map(
31413
+ (columns, index) => formatPlanRow(style, index + 1, {
31414
+ key: columns[indexes.key] ?? "",
31415
+ boundary: indexes.boundary === -1 ? "" : columns[indexes.boundary] ?? "",
31416
+ description: indexes.description === -1 ? "" : columns[indexes.description] ?? "",
31417
+ frameworksUsed: indexes.frameworksUsed === -1 ? "" : columns[indexes.frameworksUsed] ?? "",
31418
+ refactorComplexity: indexes.refactorComplexity === -1 ? "" : columns[indexes.refactorComplexity] ?? "",
31419
+ methodsToCapture: indexes.methodsToCapture === -1 ? "" : columns[indexes.methodsToCapture] ?? "",
31420
+ methodsToMock: indexes.methodsToMock === -1 ? "" : columns[indexes.methodsToMock] ?? "",
31421
+ realDataImprovement: indexes.realDataImprovement === -1 ? "" : columns[indexes.realDataImprovement] ?? ""
31422
+ })
31423
+ ).join("\n\n");
31424
+ return hideTracePlanLinks(
31425
+ [styleReportBlock(before, style), formattedRows, after].filter((part) => part !== "").join("\n\n")
31426
+ );
31427
+ }
31428
+ function formatPlanRow(style, number4, fields) {
31429
+ const lines = [
31430
+ `${number4}. ${style.bold(style.cyan(cleanInline(fields.key)))}`
31431
+ ];
31432
+ if (fields.description !== "") {
31433
+ lines.push(formatLabeledLine(style, "Workflow", fields.description));
31434
+ }
31435
+ if (fields.boundary !== "") {
31436
+ lines.push(formatLabeledLine(style, "Boundary", fields.boundary));
31437
+ }
31438
+ if (fields.frameworksUsed !== "") {
31439
+ lines.push(formatLabeledLine(style, "Uses", fields.frameworksUsed));
31440
+ }
31441
+ const captureParts = [
31442
+ fields.refactorComplexity === "" ? "" : instrumentationComplexityLabel(style, fields.refactorComplexity),
31443
+ fields.methodsToCapture === "" ? "" : `Methods: ${fields.methodsToCapture}`
31444
+ ].filter((part) => part !== "");
31445
+ if (captureParts.length > 0) {
31446
+ lines.push(
31447
+ formatLabeledLine(style, "Instrumentation", captureParts.join(". "))
31448
+ );
31449
+ }
31450
+ const replayParts = [
31451
+ fields.methodsToMock === "" ? "" : mockReplayLabel(fields.methodsToMock)
31452
+ ].filter((part) => part !== "");
31453
+ if (replayParts.length > 0) {
31454
+ lines.push(formatLabeledLine(style, "Mocks", replayParts.join(". ")));
31455
+ }
31456
+ if (fields.realDataImprovement !== "") {
31457
+ lines.push(formatLabeledLine(style, "Value", fields.realDataImprovement));
31458
+ }
31459
+ return lines.join("\n");
31460
+ }
31461
+ function mockReplayLabel(value) {
31462
+ const trimmed = value.trim();
31463
+ const countWithNames = trimmed.match(/^(\d+)\s*\((.+)\)$/);
31464
+ if (countWithNames) {
31465
+ return `${mockCountLabel(countWithNames[1])}: ${countWithNames[2]}`;
31466
+ }
31467
+ const countWithDetails = trimmed.match(/^(\d+)\s*[:—-]\s*(.+)$/);
31468
+ if (countWithDetails) {
31469
+ return `${mockCountLabel(countWithDetails[1])}: ${countWithDetails[2]}`;
31470
+ }
31471
+ return /^\d+$/.test(trimmed) ? mockCountLabel(trimmed) : trimmed;
31472
+ }
31473
+ function mockCountLabel(count) {
31474
+ return `${count} method${count === "1" ? "" : "s"} mocked on replay`;
31475
+ }
31476
+ function styleInlineValue(value, style) {
31477
+ const parts = value.split(/(`[^`]+`)/g);
31478
+ return parts.map((part) => {
31479
+ if (part.startsWith("`") && part.endsWith("`")) {
31480
+ return style.cyan(part.slice(1, -1));
31481
+ }
31482
+ return cleanInline(part);
31483
+ }).join("");
31484
+ }
31485
+ function cleanInline(value) {
31486
+ return value.replace(/[`*_]/g, "");
31487
+ }
31488
+ function formatFieldValuePlanTable(lines, tableStart, style) {
31489
+ const headers = splitMarkdownTableRow(lines[tableStart]).map(
31490
+ (header) => normalizeMarkdownText(header)
31491
+ );
31492
+ if (headers.length !== 2 || headers[0] !== "field" || headers[1] !== "value" && headers[1] !== "detail") {
31493
+ return null;
31494
+ }
31495
+ let tableEnd = tableStart + 2;
31496
+ while (tableEnd < lines.length && isMarkdownTableRow(lines[tableEnd])) {
31497
+ tableEnd++;
31498
+ }
31499
+ const values = /* @__PURE__ */ new Map();
31500
+ for (const row of lines.slice(tableStart + 2, tableEnd)) {
31501
+ const [field, value] = splitMarkdownTableRow(row);
31502
+ if (field === void 0 || value === void 0) {
31503
+ continue;
31504
+ }
31505
+ values.set(normalizeMarkdownText(field), value);
31506
+ }
31507
+ const key = firstFieldValue(values, [
31508
+ "trace function key",
31509
+ "trace function",
31510
+ "function",
31511
+ "key"
31512
+ ]);
31513
+ if (key === "") {
31514
+ return null;
31515
+ }
31516
+ return hideTracePlanLinks(
31517
+ [
31518
+ styleReportBlock(lines.slice(0, tableStart).join("\n").trim(), style),
31519
+ formatPlanRow(style, 1, {
31520
+ key,
31521
+ boundary: firstFieldValue(values, ["boundary"]),
31522
+ description: firstFieldValue(values, [
31523
+ "workflow",
31524
+ "description of workflow",
31525
+ "workflow description",
31526
+ "description"
31527
+ ]),
31528
+ frameworksUsed: firstFieldValue(values, [
31529
+ "frameworks used",
31530
+ "frameworks"
31531
+ ]),
31532
+ refactorComplexity: firstFieldValue(values, [
31533
+ "refactor complexity to instrument",
31534
+ "refactor complexity",
31535
+ "complexity"
31536
+ ]),
31537
+ methodsToCapture: firstFieldValue(values, [
31538
+ "suggested methods to capture",
31539
+ "suggested methods to captured",
31540
+ "methods to capture",
31541
+ "methods captured"
31542
+ ]),
31543
+ methodsToMock: firstFieldValue(values, [
31544
+ "methods to mock on replay",
31545
+ "number of methods that need to mocked",
31546
+ "number of methods that need to be mocked",
31547
+ "methods that need to be mocked",
31548
+ "methods to mock",
31549
+ "mocked methods"
31550
+ ]),
31551
+ realDataImprovement: firstFieldValue(values, [
31552
+ "how real data improves this",
31553
+ "value of running real data through it",
31554
+ "real data",
31555
+ "feature would improve",
31556
+ "improvement",
31557
+ "why",
31558
+ "worth tracing",
31559
+ "rationale"
31560
+ ])
31561
+ }),
31562
+ formatTrailingReportBlock(lines.slice(tableEnd), style)
31563
+ ].filter((part) => part !== "").join("\n\n")
31564
+ );
31565
+ }
31566
+ function firstFieldValue(values, fields) {
31567
+ for (const field of fields) {
31568
+ const value = values.get(field);
31569
+ if (value !== void 0) {
31570
+ return value;
31571
+ }
31572
+ }
31573
+ return "";
31574
+ }
31575
+ function createAnalyzeRepoReportStyle(options) {
31576
+ const enabled = options.color ?? (process.stdout.isTTY === true && process.env.NO_COLOR === void 0 && process.env.TERM !== "dumb");
31577
+ const wrap = (code, text) => enabled ? `${code}${text}${ANSI.reset}` : text;
31578
+ return {
31579
+ enabled,
31580
+ bold: (text) => wrap(ANSI.bold, text),
31581
+ dim: (text) => wrap(ANSI.dim, text),
31582
+ green: (text) => wrap(ANSI.green, text),
31583
+ yellow: (text) => wrap(ANSI.yellow, text),
31584
+ cyan: (text) => wrap(ANSI.cyan, text),
31585
+ magenta: (text) => wrap(ANSI.magenta, text),
31586
+ red: (text) => wrap(ANSI.red, text),
31587
+ width: normalizeReportWidth(
31588
+ options.width ?? (process.stdout.isTTY === true ? process.stdout.columns : void 0)
31589
+ )
31590
+ };
31591
+ }
31592
+ function formatLabeledLine(style, label, value) {
31593
+ const prefix = ` - ${style.dim(`${label}:`)} `;
31594
+ const continuationPrefix = " ";
31595
+ return wrapStyledLine(
31596
+ `${prefix}${styleInlineValue(value, style)}`,
31597
+ prefixVisibleLength(label),
31598
+ continuationPrefix,
31599
+ style.width
31600
+ );
31601
+ }
31602
+ function prefixVisibleLength(label) {
31603
+ return ` - ${label}: `.length;
31604
+ }
31605
+ function normalizeReportWidth(width) {
31606
+ if (width === void 0 || !Number.isFinite(width) || width <= 0) {
31607
+ return 1e4;
31608
+ }
31609
+ return Math.max(48, Math.min(110, Math.floor(width)));
31610
+ }
31611
+ function wrapStyledLine(line, firstPrefixLength, continuationPrefix, width) {
31612
+ if (visibleLength(line) <= width) {
31613
+ return line;
31614
+ }
31615
+ const segments = ansiSegments(line);
31616
+ const out = [];
31617
+ let current = "";
31618
+ let currentVisible = 0;
31619
+ let limit = width;
31620
+ for (const segment of segments) {
31621
+ if (segment.visible === 0) {
31622
+ current += segment.text;
31623
+ continue;
31624
+ }
31625
+ const words = segment.text.split(/(\s+)/);
31626
+ for (const word of words) {
31627
+ if (word === "") {
31628
+ continue;
31629
+ }
31630
+ const wordVisible = visibleLength(word);
31631
+ const isSpace = /^\s+$/.test(word);
31632
+ if (currentVisible > firstPrefixLength && currentVisible + wordVisible > limit && !isSpace) {
31633
+ out.push(current.trimEnd());
31634
+ current = continuationPrefix;
31635
+ currentVisible = continuationPrefix.length;
31636
+ limit = width;
31637
+ }
31638
+ if (!(currentVisible === continuationPrefix.length && isSpace)) {
31639
+ current += word;
31640
+ currentVisible += wordVisible;
31641
+ }
31642
+ }
31643
+ }
31644
+ if (current.trim() !== "") {
31645
+ out.push(current.trimEnd());
31646
+ }
31647
+ return out.join("\n");
31648
+ }
31649
+ function visibleLength(text) {
31650
+ return ansiSegments(text).reduce((sum, segment) => sum + segment.visible, 0);
31651
+ }
31652
+ function ansiSegments(text) {
31653
+ const segments = [];
31654
+ let index = 0;
31655
+ while (index < text.length) {
31656
+ const escapeIndex = text.indexOf("\x1B[", index);
31657
+ if (escapeIndex === -1) {
31658
+ const plain = text.slice(index);
31659
+ segments.push({ text: plain, visible: plain.length });
31660
+ break;
31661
+ }
31662
+ if (escapeIndex > index) {
31663
+ const plain = text.slice(index, escapeIndex);
31664
+ segments.push({ text: plain, visible: plain.length });
31665
+ }
31666
+ const endIndex = text.indexOf("m", escapeIndex);
31667
+ if (endIndex === -1) {
31668
+ const plain = text.slice(escapeIndex);
31669
+ segments.push({ text: plain, visible: plain.length });
31670
+ break;
31671
+ }
31672
+ segments.push({ text: text.slice(escapeIndex, endIndex + 1), visible: 0 });
31673
+ index = endIndex + 1;
31674
+ }
31675
+ return segments;
31676
+ }
31677
+ function usefulAnalyzeRepoFollowUp(lines) {
31678
+ const useful = [];
31679
+ let includeSkipped = false;
31680
+ for (const rawLine of lines) {
31681
+ const line = rawLine.trimEnd();
31682
+ const normalized = normalizeMarkdownText(line);
31683
+ if (line.trim() === "") {
31684
+ if (useful.length > 0 && useful[useful.length - 1] !== "") {
31685
+ useful.push("");
31686
+ }
31687
+ continue;
31688
+ }
31689
+ if (normalized === "skipped") {
31690
+ useful.push(line);
31691
+ includeSkipped = true;
31692
+ continue;
31693
+ }
31694
+ if (includeSkipped && line.trim().startsWith("- ")) {
31695
+ useful.push(line);
31696
+ continue;
31697
+ }
31698
+ includeSkipped = false;
31699
+ }
31700
+ while (useful[useful.length - 1] === "") {
31701
+ useful.pop();
31702
+ }
31703
+ return useful.length === 0 ? [] : ["", ...useful];
31704
+ }
31705
+ function formatTrailingReportBlock(lines, style) {
31706
+ return styleReportBlock(
31707
+ usefulAnalyzeRepoFollowUp(lines).join("\n").trim(),
31708
+ style
31709
+ );
31710
+ }
31711
+ function instrumentationComplexityLabel(style, raw) {
31712
+ const normalized = raw.trim().toLowerCase();
31713
+ if (normalized === "none" || normalized.startsWith("none ")) {
31714
+ return `${style.green("no refactor")}${raw.trim().slice(4)}`;
31715
+ }
31716
+ if (normalized === "low" || normalized.startsWith("low ")) {
31717
+ return `${style.green("low effort")}${raw.trim().slice(3)}`;
31718
+ }
31719
+ if (normalized === "med" || normalized === "medium") {
31720
+ return style.yellow("medium effort");
31721
+ }
31722
+ if (normalized.startsWith("med ") || normalized.startsWith("medium ")) {
31723
+ return `${style.yellow("medium effort")}${raw.trim().replace(/^medium|^med/i, "")}`;
31724
+ }
31725
+ if (normalized === "high" || normalized.startsWith("high ")) {
31726
+ return `${style.red("high effort")}${raw.trim().slice(4)}`;
31727
+ }
31728
+ return raw;
31729
+ }
31730
+ function styleReportBlock(block, style) {
31731
+ if (block === "") {
31732
+ return "";
31733
+ }
31734
+ return block.split(/\r?\n/).map((line) => styleReportLine(line, style)).join("\n");
31735
+ }
31736
+ function styleNonTableReport(report, style) {
31737
+ return report.split(/\r?\n/).map((line) => styleReportLine(line, style)).join("\n");
31738
+ }
31739
+ function styleReportLine(line, style) {
31740
+ const heading = uploadedHeadingFromLine(line);
31741
+ if (heading !== null) {
31742
+ return style.bold(style.green(heading));
31743
+ }
31744
+ if (line.trim() === "Skipped") {
31745
+ return style.bold(style.yellow(line));
31746
+ }
31747
+ if (line.startsWith("- ")) {
31748
+ return style.yellow(styleInlineValue(line, style));
31749
+ }
31750
+ if (line.startsWith("These are draft")) {
31751
+ return style.dim(styleInlineValue(line, style));
31752
+ }
31753
+ return styleInlineValue(line, style);
31754
+ }
31755
+ function uploadedHeadingFromLine(line) {
31756
+ const normalized = line.replace(/[`*_]/g, "").trim();
31757
+ const match = normalized.match(/Uploaded \d+ draft trace plans?/i);
31758
+ return match?.[0] ?? null;
31759
+ }
31760
+ function hideTracePlanLinks(report) {
31761
+ return report.split(/\r?\n/).map(
31762
+ (line) => line.replace(
31763
+ /\[([^\]]+)\]\(https:\/\/bitfab\.ai\/studio\/trace-plan\/[^\s)]+\)/g,
31764
+ "$1"
31765
+ ).replace(/https:\/\/bitfab\.ai\/studio\/trace-plan\/\S+/g, "").trimEnd()
31766
+ ).filter((line) => !/^\s*(plan|plan url):\s*$/i.test(line)).join("\n").trim();
31767
+ }
31768
+ function findColumnIndex(headers, needles) {
31769
+ return headers.findIndex((header) => {
31770
+ const normalized = normalizeMarkdownText(header);
31771
+ return needles.some((needle) => normalized.includes(needle));
31772
+ });
31773
+ }
31774
+ function normalizeMarkdownText(text) {
31775
+ return text.replace(/[`*_]/g, "").replace(/\s+/g, " ").trim().toLowerCase();
31776
+ }
31777
+ function isMarkdownTableRow(line) {
31778
+ const trimmed = line.trim();
31779
+ return trimmed.startsWith("|") && trimmed.endsWith("|");
31780
+ }
31781
+ function isTableDivider(line) {
31782
+ const cells = splitMarkdownTableRow(line);
31783
+ return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell.replace(/\s/g, "")));
31784
+ }
31785
+ function splitMarkdownTableRow(line) {
31786
+ return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
31787
+ }
31788
+ function assistantTextFromEvent(event) {
31789
+ const content = eventMessageContent(event);
31790
+ if (!content) {
31791
+ return null;
31792
+ }
31793
+ const blocks = [];
31794
+ for (const block of content) {
31795
+ if (typeof block !== "object" || block === null) {
31796
+ continue;
31797
+ }
31798
+ const b = block;
31799
+ if (b.type === "text" && typeof b.text === "string") {
31800
+ blocks.push(b.text);
31801
+ }
31802
+ }
31803
+ const text = blocks.join("\n").trim();
31804
+ return text === "" ? null : text;
31805
+ }
31806
+ function textCandidatesFromEvent(event) {
31807
+ const candidates = [];
31808
+ const assistantText = assistantTextFromEvent(event);
31809
+ if (assistantText !== null) {
31810
+ candidates.push(assistantText);
31811
+ }
31812
+ for (const text of stringValuesFromUnknown(event)) {
31813
+ const trimmed = text.trim();
31814
+ if (trimmed !== "" && !candidates.includes(trimmed) && !candidates.some((candidate) => candidate.includes(trimmed))) {
31815
+ candidates.push(trimmed);
31816
+ }
31817
+ }
31818
+ return candidates;
31819
+ }
31820
+ function stringValuesFromUnknown(value) {
31821
+ if (typeof value === "string") {
31822
+ return [value];
31823
+ }
31824
+ if (Array.isArray(value)) {
31825
+ return value.flatMap(stringValuesFromUnknown);
31826
+ }
31827
+ if (typeof value !== "object" || value === null) {
31828
+ return [];
31829
+ }
31830
+ return Object.values(value).flatMap(
31831
+ stringValuesFromUnknown
31832
+ );
31833
+ }
31834
+ function resultTextFromEvent(event) {
31835
+ if (typeof event !== "object" || event === null) {
31836
+ return null;
31837
+ }
31838
+ const e = event;
31839
+ if (e.type !== "result" || typeof e.result !== "string") {
31840
+ return null;
31841
+ }
31842
+ const text = e.result.trim();
31843
+ return text === "" ? null : text;
31844
+ }
31156
31845
  function latestActivityLabel(chunk2) {
31157
31846
  const lines = chunk2.split("\n").filter((l) => l.trim() !== "");
31158
31847
  for (let i = lines.length - 1; i >= 0; i--) {
@@ -31463,10 +32152,18 @@ async function runCodexAnalyzeRepo(captureOverride, limit, prompt) {
31463
32152
  `analyze-repo exited with status ${exitCode}. See ${logPath}`
31464
32153
  );
31465
32154
  }
31466
- const uploaded = countUploadedTracePlans2(stdoutChunks.join(""));
32155
+ const logText = stdoutChunks.join("");
32156
+ const uploaded = countUploadedTracePlans2(logText);
31467
32157
  spinner6.stop(
31468
32158
  uploaded === 0 ? "No AI workflows found - nothing to upload" : `Uploaded ${uploaded} draft trace plan${uploaded === 1 ? "" : "s"} to Bitfab`
31469
32159
  );
32160
+ const report = extractAnalyzeRepoReport(logText);
32161
+ if (report !== null) {
32162
+ process.stdout.write(`
32163
+ ${formatAnalyzeRepoReport(report)}
32164
+
32165
+ `);
32166
+ }
31470
32167
  } catch (err) {
31471
32168
  spinner6.stop("analyze-repo failed");
31472
32169
  throw err;
@@ -31834,10 +32531,18 @@ async function runCursorAnalyzeRepo(captureOverride, limit, prompt) {
31834
32531
  `analyze-repo exited with status ${exitCode}. See ${logPath}`
31835
32532
  );
31836
32533
  }
31837
- const uploaded = countUploadedTracePlans3(stdoutChunks.join(""));
32534
+ const logText = stdoutChunks.join("");
32535
+ const uploaded = countUploadedTracePlans3(logText);
31838
32536
  spinner6.stop(
31839
32537
  uploaded === 0 ? "No AI workflows found - nothing to upload" : `Uploaded ${uploaded} draft trace plan${uploaded === 1 ? "" : "s"} to Bitfab`
31840
32538
  );
32539
+ const report = extractAnalyzeRepoReport(logText);
32540
+ if (report !== null) {
32541
+ process.stdout.write(`
32542
+ ${formatAnalyzeRepoReport(report)}
32543
+
32544
+ `);
32545
+ }
31841
32546
  } catch (err) {
31842
32547
  spinner6.stop("analyze-repo failed");
31843
32548
  throw err;
@@ -32382,7 +33087,7 @@ var GLOBAL_HELP_TEXT = `Usage: bitfab <command> [options]
32382
33087
  Commands:
32383
33088
  init [--editor <name>] Full onboarding: plugin-install, login, and setup
32384
33089
  plugin-install [--editor <name>] Install the Bitfab plugin in one editor
32385
- login Authenticate with Bitfab (opens browser)
33090
+ login [--force] Authenticate with Bitfab (opens browser)
32386
33091
  logout Remove stored credentials
32387
33092
  session-logs [status|enable|disable] Read or update session log collection
32388
33093
  setup [--editor <name>] Launch /bitfab:setup in the editor
@@ -32399,6 +33104,7 @@ Options:
32399
33104
  --no-upload-logs analyze-repo: keep session logs local (skips the prompt)
32400
33105
  --limit <n> analyze-repo: cap how many draft trace plans to upload (default 5)
32401
33106
  --prompt, -p <text> analyze-repo: free-text guidance steering what to focus on
33107
+ --force login: re-authenticate and clear any stale Studio session first
32402
33108
 
32403
33109
  Examples:
32404
33110
  bitfab init Full setup (detect editor, install, login, setup)
@@ -32406,6 +33112,7 @@ Examples:
32406
33112
  bitfab analyze-repo Scan the repo and upload draft trace plans (headless)
32407
33113
  bitfab analyze-repo --limit 3 Scan the repo and upload at most 3 draft trace plans
32408
33114
  bitfab analyze-repo "focus on billing" Scan with free-text guidance on what to prioritize
33115
+ bitfab login --force Re-authenticate and clear a stale Studio session
32409
33116
  bitfab session-logs enable Enable session log collection
32410
33117
  bitfab session-logs disable Disable session log collection
32411
33118
  bitfab assistant investigate Investigate traces
@@ -32438,15 +33145,16 @@ Examples:
32438
33145
  bitfab plugin-install
32439
33146
  bitfab plugin-install --editor cursor
32440
33147
  `,
32441
- login: `Usage: bitfab login
33148
+ login: `Usage: bitfab login [--force]
32442
33149
 
32443
33150
  Authenticate with Bitfab in the browser and save credentials locally.
32444
33151
 
32445
33152
  Options:
32446
- This command has no options.
33153
+ --force Re-authenticate even when already signed in, and clear any stale Studio session first
32447
33154
 
32448
33155
  Examples:
32449
33156
  bitfab login
33157
+ bitfab login --force
32450
33158
  `,
32451
33159
  logout: `Usage: bitfab logout
32452
33160
 
@@ -32608,7 +33316,7 @@ function wantsHelp(argv) {
32608
33316
  var COMMAND_SPECS = {
32609
33317
  init: { flags: ["editor", "skipPermissions"], unknownOption: "error" },
32610
33318
  "plugin-install": { flags: ["editor"], unknownOption: "error" },
32611
- login: { flags: [], unknownOption: "error" },
33319
+ login: { flags: ["force"], unknownOption: "error" },
32612
33320
  logout: { flags: [], unknownOption: "error" },
32613
33321
  "session-logs": { flags: [], unknownOption: "error" },
32614
33322
  setup: { flags: ["editor", "skipPermissions"], unknownOption: "error" },
@@ -32682,6 +33390,12 @@ function applyFlag(key, token, next, values) {
32682
33390
  return 1;
32683
33391
  }
32684
33392
  return null;
33393
+ case "force":
33394
+ if (token === "--force") {
33395
+ values.force = true;
33396
+ return 1;
33397
+ }
33398
+ return null;
32685
33399
  }
32686
33400
  }
32687
33401
  function parseArgs2(argv) {
@@ -32728,6 +33442,7 @@ function parseArgs2(argv) {
32728
33442
  uploadLogs: values.uploadLogs,
32729
33443
  limit: values.limit,
32730
33444
  prompt: values.prompt,
33445
+ force: values.force,
32731
33446
  rest
32732
33447
  };
32733
33448
  }
@@ -32747,7 +33462,16 @@ ${GLOBAL_HELP_TEXT}`
32747
33462
  process.stdout.write(helpText);
32748
33463
  return;
32749
33464
  }
32750
- const { command, editor, skipPermissions, uploadLogs, limit, prompt, rest } = parseArgs2(argv);
33465
+ const {
33466
+ command,
33467
+ editor,
33468
+ skipPermissions,
33469
+ uploadLogs,
33470
+ limit,
33471
+ prompt,
33472
+ force,
33473
+ rest
33474
+ } = parseArgs2(argv);
32751
33475
  const abortUpdateCheck = startUpdateCheck();
32752
33476
  if (command === "init") {
32753
33477
  await runInit({ editor, skipPermissions });
@@ -32760,7 +33484,7 @@ ${GLOBAL_HELP_TEXT}`
32760
33484
  return;
32761
33485
  }
32762
33486
  if (command === "login") {
32763
- await runLoginCommand();
33487
+ await runLoginCommand({ force });
32764
33488
  abortUpdateCheck();
32765
33489
  return;
32766
33490
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bitfab-cli",
3
- "version": "0.2.185",
3
+ "version": "0.2.187",
4
4
  "description": "Install and configure the Bitfab plugin in Claude Code, Codex, or Cursor.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",