bitfab-cli 0.2.187 → 0.2.188

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 +265 -51
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -8837,6 +8837,12 @@ var DaemonUnavailableError = class extends Error {
8837
8837
  this.name = "DaemonUnavailableError";
8838
8838
  }
8839
8839
  };
8840
+ var DaemonOpenTimeoutError = class extends Error {
8841
+ constructor(message = "daemon did not respond to open") {
8842
+ super(message);
8843
+ this.name = "DaemonOpenTimeoutError";
8844
+ }
8845
+ };
8840
8846
  var StudioNavigationError = class extends Error {
8841
8847
  reason;
8842
8848
  blockedReason;
@@ -9982,6 +9988,9 @@ function closeStudioWindowsBySession(sessionId) {
9982
9988
  }
9983
9989
  }
9984
9990
  }
9991
+ function osDefaultResult(pid) {
9992
+ return pid !== null ? { pid, launched: true, method: "os-default" } : { pid: null, launched: false, method: "none", reason: "spawn-failed" };
9993
+ }
9985
9994
  function openChromelessWindow(url2) {
9986
9995
  if (process.env.VITEST || process.env.NODE_ENV === "test") {
9987
9996
  throw new Error("openChromelessWindow called in a test environment without being mocked");
@@ -9992,12 +10001,16 @@ function openChromelessWindow(url2) {
9992
10001
  }
9993
10002
  if (recentOpens.length >= WINDOW_OPEN_RATE_LIMIT) {
9994
10003
  console.error(`[browser] rate limit: ${recentOpens.length} windows opened in the last ${WINDOW_OPEN_RATE_WINDOW_MS / 1e3}s, refusing to open another`);
9995
- return null;
10004
+ return {
10005
+ pid: null,
10006
+ launched: false,
10007
+ method: "none",
10008
+ reason: "rate-limited"
10009
+ };
9996
10010
  }
9997
10011
  recentOpens.push(now);
9998
10012
  if (shouldDisableChromelessWindows()) {
9999
- openOsDefault(url2);
10000
- return null;
10013
+ return osDefaultResult(openOsDefault(url2));
10001
10014
  }
10002
10015
  const browsers = getChromiumBrowsers();
10003
10016
  const defaultId = getDefaultBrowserId();
@@ -10008,29 +10021,31 @@ function openChromelessWindow(url2) {
10008
10021
  if (binary) {
10009
10022
  const pid = openChromiumApp(binary, url2, sizeArgs);
10010
10023
  if (pid !== null) {
10011
- return pid;
10024
+ return { pid, launched: true, method: "app" };
10012
10025
  }
10013
- openOsDefault(url2);
10014
- return null;
10026
+ return osDefaultResult(openOsDefault(url2));
10015
10027
  }
10016
10028
  }
10017
10029
  if (defaultId !== null && defaultChromium === null) {
10018
- openOsDefault(url2);
10019
- return null;
10030
+ return osDefaultResult(openOsDefault(url2));
10020
10031
  }
10021
10032
  for (const browser of browsers) {
10022
10033
  const binary = findExistingBinary(browser.binaryPaths);
10023
10034
  if (binary) {
10024
10035
  const pid = openChromiumApp(binary, url2, sizeArgs);
10025
10036
  if (pid !== null) {
10026
- return pid;
10037
+ return { pid, launched: true, method: "app" };
10027
10038
  }
10028
- openOsDefault(url2);
10029
- return null;
10039
+ return osDefaultResult(openOsDefault(url2));
10030
10040
  }
10031
10041
  }
10032
- openOsDefault(url2);
10033
- return null;
10042
+ return osDefaultResult(openOsDefault(url2));
10043
+ }
10044
+
10045
+ // ../bitfab-plugin-lib/dist/output.js
10046
+ function emitJson(event) {
10047
+ process.stdout.write(`${JSON.stringify(event)}
10048
+ `);
10034
10049
  }
10035
10050
 
10036
10051
  // ../bitfab-plugin-lib/dist/parseArgs.js
@@ -10146,14 +10161,19 @@ async function createStudioSession({ serviceUrl, apiKey, sessionId, initialPath
10146
10161
  sessionId,
10147
10162
  extraHeaders: clientHeaders
10148
10163
  });
10149
- const windowPid = openChromelessWindow(target.url);
10164
+ const open = openChromelessWindow(target.url);
10150
10165
  writeActiveStudioSession({
10151
10166
  sessionId,
10152
10167
  serviceUrl,
10153
- windowPid,
10168
+ windowPid: open.pid,
10154
10169
  currentPath: target.path
10155
10170
  });
10156
- return { sessionId, serviceUrl };
10171
+ return {
10172
+ sessionId,
10173
+ serviceUrl,
10174
+ launched: open.launched,
10175
+ launchReason: open.reason
10176
+ };
10157
10177
  }
10158
10178
 
10159
10179
  // ../bitfab-plugin-lib/dist/daemon/client.js
@@ -10197,6 +10217,8 @@ var BROWSER_HEALTH_INTERVAL_MS = 10 * 1e3;
10197
10217
  var ENSURE_POLL_INTERVAL_MS = 200;
10198
10218
  var ENSURE_TIMEOUT_MS = 5e3;
10199
10219
  var PING_TIMEOUT_MS = 2e3;
10220
+ var OPEN_TIMEOUT_MS = 1e4;
10221
+ var COMMAND_TIMEOUT_MS = 3e4;
10200
10222
  function serialize(msg) {
10201
10223
  return `${JSON.stringify(msg)}
10202
10224
  `;
@@ -10370,7 +10392,7 @@ async function waitForLockHolderDaemon(socketPath, timeoutMs, localBuild = local
10370
10392
  const deadline = Date.now() + timeoutMs;
10371
10393
  while (Date.now() < deadline) {
10372
10394
  const ping = await pingSocket(socketPath);
10373
- if (ping.alive && (localBuild === "unknown" || !ping.build || ping.build === localBuild)) {
10395
+ if (ping.alive && buildMatches(localBuild, ping.build)) {
10374
10396
  return;
10375
10397
  }
10376
10398
  await new Promise((r) => setTimeout(r, ENSURE_POLL_INTERVAL_MS));
@@ -10381,6 +10403,12 @@ async function waitForLockHolderDaemon(socketPath, timeoutMs, localBuild = local
10381
10403
  }
10382
10404
  throw new Error(`daemon did not become ready within ${timeoutMs}ms`);
10383
10405
  }
10406
+ function buildMatches(localBuild, remoteBuild) {
10407
+ if (localBuild === "unknown") {
10408
+ return true;
10409
+ }
10410
+ return remoteBuild === localBuild;
10411
+ }
10384
10412
  function localBuildFingerprint() {
10385
10413
  try {
10386
10414
  const thisDir = path9.dirname(fileURLToPath3(import.meta.url));
@@ -10532,11 +10560,10 @@ async function ensureDaemon(paths = SINGLETON_DAEMON_PATHS) {
10532
10560
  const ping = await pingSocket(socketPath);
10533
10561
  if (ping.alive) {
10534
10562
  const localBuild = localBuildFingerprint();
10535
- if (ping.build && ping.build !== localBuild && localBuild !== "unknown") {
10536
- console.error(`[daemon] build mismatch (running: ${ping.build}, local: ${localBuild}), restarting`);
10537
- } else {
10563
+ if (buildMatches(localBuild, ping.build)) {
10538
10564
  return;
10539
10565
  }
10566
+ console.error(`[daemon] build mismatch (running: ${ping.build ?? "unreported"}, local: ${localBuild}), restarting`);
10540
10567
  }
10541
10568
  }
10542
10569
  if (!acquireLock(socketPath)) {
@@ -10561,9 +10588,19 @@ var DaemonClient = class {
10561
10588
  disconnectHandler = null;
10562
10589
  paths;
10563
10590
  socketPath;
10564
- constructor(paths = SINGLETON_DAEMON_PATHS) {
10591
+ openTimeoutMs;
10592
+ commandTimeoutMs;
10593
+ // The single in-flight wedged-daemon reap, shared by every timeout path.
10594
+ // Sharing it is load-bearing: `open`'s retry must AWAIT the same reap the
10595
+ // command timeout fire-and-forgot, or a trailing reap's file cleanup could
10596
+ // finish after the respawned daemon bound its socket and wrote its pid
10597
+ // file, unlinking the new daemon's files and orphaning it.
10598
+ reapInFlight = null;
10599
+ constructor(paths = SINGLETON_DAEMON_PATHS, timeouts = {}) {
10565
10600
  this.paths = paths;
10566
10601
  this.socketPath = paths.socketPath;
10602
+ this.openTimeoutMs = timeouts.openTimeoutMs ?? OPEN_TIMEOUT_MS;
10603
+ this.commandTimeoutMs = timeouts.commandTimeoutMs ?? COMMAND_TIMEOUT_MS;
10567
10604
  }
10568
10605
  async connect() {
10569
10606
  await ensureDaemon(this.paths);
@@ -10613,28 +10650,97 @@ var DaemonClient = class {
10613
10650
  idx = this.buffer.indexOf("\n");
10614
10651
  }
10615
10652
  }
10616
- sendCommand(cmd) {
10653
+ // Every command runs under a timeout so a wedged daemon can never hang a
10654
+ // caller indefinitely: a never-resolving promise is not a rejection, so it
10655
+ // slips straight past callers' .catch() guards. `open` passes its own tighter
10656
+ // timeout + typed error to drive the reap-and-retry path; everything else
10657
+ // gets the generous default ceiling.
10658
+ sendCommand(cmd, opts = {}) {
10617
10659
  if (!this.socket) {
10618
10660
  return Promise.reject(new Error("not connected"));
10619
10661
  }
10662
+ const timeoutMs = opts.timeoutMs ?? this.commandTimeoutMs;
10663
+ const cmdName = cmd.cmd ?? "command";
10620
10664
  return new Promise((resolve2, reject) => {
10621
- this.responseQueue.push(resolve2);
10665
+ let settled = false;
10666
+ const timer = setTimeout(() => {
10667
+ if (settled) {
10668
+ return;
10669
+ }
10670
+ settled = true;
10671
+ const pending = this.responseQueue;
10672
+ this.responseQueue = [];
10673
+ for (const other of pending) {
10674
+ if (other !== resolver) {
10675
+ other({
10676
+ ok: false,
10677
+ error: `daemon connection reset after ${cmdName} timed out`
10678
+ });
10679
+ }
10680
+ }
10681
+ this.destroy();
10682
+ void this.reapWedgedDaemon();
10683
+ reject(opts.onTimeout?.() ?? new Error(`daemon did not respond to ${cmdName} within ${timeoutMs}ms`));
10684
+ }, timeoutMs);
10685
+ const resolver = (msg) => {
10686
+ if (settled) {
10687
+ return;
10688
+ }
10689
+ settled = true;
10690
+ clearTimeout(timer);
10691
+ resolve2(msg);
10692
+ };
10693
+ this.responseQueue.push(resolver);
10622
10694
  try {
10623
10695
  this.socket.write(serialize(cmd));
10624
10696
  } catch (err) {
10625
- this.responseQueue.pop();
10697
+ settled = true;
10698
+ clearTimeout(timer);
10699
+ const idx = this.responseQueue.indexOf(resolver);
10700
+ if (idx !== -1) {
10701
+ this.responseQueue.splice(idx, 1);
10702
+ }
10626
10703
  reject(err);
10627
10704
  }
10628
10705
  });
10629
10706
  }
10630
10707
  async open(opts) {
10708
+ try {
10709
+ return await this.openOnce(opts);
10710
+ } catch (err) {
10711
+ if (!(err instanceof DaemonOpenTimeoutError)) {
10712
+ throw err;
10713
+ }
10714
+ this.destroy();
10715
+ await this.reapWedgedDaemon();
10716
+ await this.connect();
10717
+ return await this.openOnce(opts);
10718
+ }
10719
+ }
10720
+ async openOnce(opts) {
10631
10721
  const cmd = { cmd: "open", ...opts };
10632
- const res = await this.sendCommand(cmd);
10722
+ const res = await this.sendCommand(cmd, {
10723
+ timeoutMs: this.openTimeoutMs,
10724
+ onTimeout: () => new DaemonOpenTimeoutError()
10725
+ });
10633
10726
  if (!res.ok) {
10634
10727
  throw new Error(res.error);
10635
10728
  }
10636
10729
  return res;
10637
10730
  }
10731
+ // Start (or join) the wedged-daemon reap. All timeout paths funnel through
10732
+ // this so at most one reap runs at a time and `open`'s retry can await the
10733
+ // in-flight one instead of racing it - a reap that finished AFTER the retry
10734
+ // respawned would unlink the new daemon's socket/pid files and orphan it.
10735
+ reapWedgedDaemon() {
10736
+ if (!this.reapInFlight) {
10737
+ this.reapInFlight = reapAllDaemons(this.paths).catch(() => {
10738
+ }).finally(() => {
10739
+ this.reapInFlight = null;
10740
+ });
10741
+ }
10742
+ return this.reapInFlight;
10743
+ }
10638
10744
  async navigate(key, navPath) {
10639
10745
  const res = await this.sendCommand({ cmd: "navigate", key, path: navPath });
10640
10746
  if (!res.ok) {
@@ -10814,11 +10920,11 @@ var DirectChannel = class {
10814
10920
  path: initialPath,
10815
10921
  sessionId: sessionId2
10816
10922
  });
10817
- const windowPid = openChromelessWindow(url2);
10923
+ const open = openChromelessWindow(url2);
10818
10924
  writeActiveStudioSession({
10819
10925
  sessionId: sessionId2,
10820
10926
  serviceUrl: this.serviceUrl,
10821
- windowPid,
10927
+ windowPid: open.pid,
10822
10928
  currentPath: initialPath,
10823
10929
  // A keyless login window is pre-auth: writeActiveStudioSession defaults
10824
10930
  // authenticated to true, but this window has no token yet. Record it as
@@ -10827,7 +10933,12 @@ var DirectChannel = class {
10827
10933
  // (openStudioTo) is what later excludes it from reuse.
10828
10934
  authenticated: false
10829
10935
  });
10830
- return { sessionId: sessionId2, opened: true };
10936
+ return {
10937
+ sessionId: sessionId2,
10938
+ opened: true,
10939
+ launched: open.launched,
10940
+ launchReason: open.reason
10941
+ };
10831
10942
  }
10832
10943
  const existing = readActiveStudioSession();
10833
10944
  if (existing && existing.windowState !== "closed") {
@@ -10851,7 +10962,12 @@ var DirectChannel = class {
10851
10962
  initialPath: path21,
10852
10963
  clientHeaders: opts?.clientHeaders
10853
10964
  });
10854
- return { sessionId: session.sessionId, opened: true };
10965
+ return {
10966
+ sessionId: session.sessionId,
10967
+ opened: true,
10968
+ launched: session.launched,
10969
+ launchReason: session.launchReason
10970
+ };
10855
10971
  }
10856
10972
  async close(sessionId, message) {
10857
10973
  await closeStudio({ serviceUrl: this.serviceUrl, apiKey: this.apiKey, sessionId }, message);
@@ -10933,7 +11049,12 @@ var DaemonChannel = class _DaemonChannel {
10933
11049
  }
10934
11050
  return { sessionId: openRes.sessionId, opened: false };
10935
11051
  }
10936
- return { sessionId: openRes.sessionId, opened: true };
11052
+ return {
11053
+ sessionId: openRes.sessionId,
11054
+ opened: true,
11055
+ launched: openRes.launched,
11056
+ launchReason: openRes.launchReason
11057
+ };
10937
11058
  }
10938
11059
  async close(sessionId, message) {
10939
11060
  await Promise.all([
@@ -11286,6 +11407,20 @@ function applyFocusTarget(target) {
11286
11407
  }
11287
11408
  }
11288
11409
 
11410
+ // ../bitfab-plugin-lib/dist/studioConnectGrace.js
11411
+ var CONNECT_GRACE_MS = 45e3;
11412
+ function connectGraceMs() {
11413
+ const raw = process.env.BITFAB_STUDIO_CONNECT_GRACE_MS;
11414
+ const parsed = raw ? Number(raw) : Number.NaN;
11415
+ return Number.isFinite(parsed) ? parsed : CONNECT_GRACE_MS;
11416
+ }
11417
+ var PRE_AUTH_CONNECT_GRACE_MS = 10 * 60 * 1e3;
11418
+ function preAuthConnectGraceMs() {
11419
+ const raw = process.env.BITFAB_STUDIO_PREAUTH_CONNECT_GRACE_MS;
11420
+ const parsed = raw ? Number(raw) : Number.NaN;
11421
+ return Number.isFinite(parsed) ? parsed : PRE_AUTH_CONNECT_GRACE_MS;
11422
+ }
11423
+
11289
11424
  // ../bitfab-plugin-lib/dist/commands/openStudioTo.js
11290
11425
  function lineToAgentEvent(line) {
11291
11426
  const name = typeof line.event === "string" ? line.event : "";
@@ -11295,17 +11430,31 @@ function lineToAgentEvent(line) {
11295
11430
  var WINDOW_CLOSE_GRACE_PERIOD_MS = 1e4;
11296
11431
  var LOGIN_TIMEOUT_MS = 10 * 60 * 1e3;
11297
11432
  function formatStudioUrlMessage(url2) {
11298
- return `Studio opened at: ${url2}`;
11433
+ return `Opening Studio at: ${url2}`;
11434
+ }
11435
+ function defaultLifecycleEmit(event) {
11436
+ emitJson(event);
11437
+ if (event.event === "window-open-requested" && typeof event.url === "string") {
11438
+ console.error(formatStudioUrlMessage(event.url));
11439
+ }
11299
11440
  }
11300
- function createEventSubscription(channel, sessionId, onEvent, restoreFocus) {
11441
+ function createEventSubscription(channel, sessionId, onEvent, restoreFocus, freshOpenGraceMs = null) {
11301
11442
  let graceTimer = null;
11443
+ let connectWatchdog = null;
11302
11444
  let endedNotified = false;
11445
+ const ownsPointer = !channel.isPersistent();
11303
11446
  const clearGraceTimer = () => {
11304
11447
  if (graceTimer) {
11305
11448
  clearTimeout(graceTimer);
11306
11449
  graceTimer = null;
11307
11450
  }
11308
11451
  };
11452
+ const clearConnectWatchdog = () => {
11453
+ if (connectWatchdog) {
11454
+ clearTimeout(connectWatchdog);
11455
+ connectWatchdog = null;
11456
+ }
11457
+ };
11309
11458
  const notifyEnded = (baseEvent) => {
11310
11459
  if (!endedNotified) {
11311
11460
  endedNotified = true;
@@ -11313,14 +11462,20 @@ function createEventSubscription(channel, sessionId, onEvent, restoreFocus) {
11313
11462
  onEvent({ ...baseEvent, type: "studio:session-ended" });
11314
11463
  }
11315
11464
  clearGraceTimer();
11465
+ clearConnectWatchdog();
11316
11466
  sub.abort();
11317
11467
  };
11318
11468
  const sub = channel.subscribe(sessionId, (event) => {
11469
+ if (!event.type.startsWith("agent:")) {
11470
+ clearConnectWatchdog();
11471
+ }
11319
11472
  if (event.type === "studio:window-closed") {
11320
11473
  clearGraceTimer();
11321
11474
  graceTimer = setTimeout(() => {
11322
11475
  graceTimer = null;
11323
- updateActiveStudioSession({ windowState: "closed" });
11476
+ if (ownsPointer) {
11477
+ updateActiveStudioSession({ windowState: "closed" });
11478
+ }
11324
11479
  notifyEnded(event);
11325
11480
  }, WINDOW_CLOSE_GRACE_PERIOD_MS);
11326
11481
  return;
@@ -11353,9 +11508,36 @@ function createEventSubscription(channel, sessionId, onEvent, restoreFocus) {
11353
11508
  }, (err) => {
11354
11509
  console.error(`studio event stream: ${err.message}`);
11355
11510
  });
11511
+ if (freshOpenGraceMs != null) {
11512
+ connectWatchdog = setTimeout(() => {
11513
+ connectWatchdog = null;
11514
+ if (endedNotified) {
11515
+ return;
11516
+ }
11517
+ endedNotified = true;
11518
+ restoreFocus();
11519
+ if (ownsPointer) {
11520
+ updateActiveStudioSession({ windowState: "closed" });
11521
+ }
11522
+ onEvent({
11523
+ id: "",
11524
+ type: "studio:session-ended",
11525
+ data: { reason: "never-connected" }
11526
+ });
11527
+ clearGraceTimer();
11528
+ void (async () => {
11529
+ try {
11530
+ await channel.clearSession(sessionId);
11531
+ } catch {
11532
+ }
11533
+ sub.abort();
11534
+ })();
11535
+ }, freshOpenGraceMs);
11536
+ }
11356
11537
  return {
11357
11538
  abort: () => {
11358
11539
  clearGraceTimer();
11540
+ clearConnectWatchdog();
11359
11541
  sub.abort();
11360
11542
  },
11361
11543
  done: sub.done
@@ -11369,7 +11551,19 @@ async function openStudioTo(path21, opts = {}) {
11369
11551
  const focusTarget = captureFocusTarget();
11370
11552
  const restoreFocus = () => applyFocusTarget(focusTarget);
11371
11553
  const channel = await resolveChannel(apiKey ?? "", serviceUrl);
11372
- const finish = (sessionId2, opened2, resolvedApiKey, url3) => {
11554
+ const emit = opts.emit ?? defaultLifecycleEmit;
11555
+ const reportFreshOpen = (url3, launched2, reason) => {
11556
+ if (launched2 === false) {
11557
+ emit({
11558
+ event: "open-failed",
11559
+ reason: reason ?? "could not launch a browser",
11560
+ url: url3
11561
+ });
11562
+ } else {
11563
+ emit({ event: "window-open-requested", url: url3 });
11564
+ }
11565
+ };
11566
+ const finish = (sessionId2, opened2, resolvedApiKey, url3, launched2, windowProven = false) => {
11373
11567
  const persistent = channel.isPersistent();
11374
11568
  const base = {
11375
11569
  sessionId: sessionId2,
@@ -11380,6 +11574,7 @@ async function openStudioTo(path21, opts = {}) {
11380
11574
  url: url3
11381
11575
  };
11382
11576
  const wantsEvents = opts.tail || Boolean(opts.onEvent);
11577
+ const freshOpenGraceMs = opened2 && !windowProven ? launched2 === false ? preAuthConnectGraceMs() : connectGraceMs() : null;
11383
11578
  if (persistent) {
11384
11579
  if (!wantsEvents || opts.tail) {
11385
11580
  if (!opts.tail) {
@@ -11392,7 +11587,7 @@ async function openStudioTo(path21, opts = {}) {
11392
11587
  };
11393
11588
  }
11394
11589
  const subscription = createEventSubscription(channel, sessionId2, opts.onEvent ?? (() => {
11395
- }), restoreFocus);
11590
+ }), restoreFocus, freshOpenGraceMs);
11396
11591
  return { ...base, ...subscription };
11397
11592
  }
11398
11593
  if (!wantsEvents) {
@@ -11410,7 +11605,7 @@ async function openStudioTo(path21, opts = {}) {
11410
11605
  appendStudioEvent(key, studioEventToLogLine(event, sessionId2));
11411
11606
  userOnEvent2(event);
11412
11607
  };
11413
- const subscription = createEventSubscription(channel, sessionId2, sink, restoreFocus);
11608
+ const subscription = createEventSubscription(channel, sessionId2, sink, restoreFocus, freshOpenGraceMs);
11414
11609
  return {
11415
11610
  ...base,
11416
11611
  abort: subscription.abort,
@@ -11454,9 +11649,11 @@ async function openStudioTo(path21, opts = {}) {
11454
11649
  const sessionId2 = openResult.sessionId;
11455
11650
  const signInUrl = `${serviceUrl}${buildSignInPath(sessionId2)}`;
11456
11651
  if (openResult.opened) {
11457
- opts.onWindowOpened?.(signInUrl);
11652
+ reportFreshOpen(signInUrl, openResult.launched, openResult.launchReason);
11653
+ } else {
11654
+ emit({ event: "navigated", sessionId: sessionId2, path: buildSignInPath(sessionId2) });
11458
11655
  }
11459
- opts.onLoginRequired?.(sessionId2, signInUrl);
11656
+ emit({ event: "auth-required", sessionId: sessionId2, signInUrl });
11460
11657
  const loginApiKey = await new Promise((resolve2, reject) => {
11461
11658
  const timer = setTimeout(() => {
11462
11659
  sub.abort();
@@ -11480,19 +11677,30 @@ async function openStudioTo(path21, opts = {}) {
11480
11677
  });
11481
11678
  saveCredentials(loginApiKey);
11482
11679
  updateActiveStudioSession({ authenticated: true });
11483
- opts.onAuthenticated?.(sessionId2);
11680
+ emit({ event: "authenticated", sessionId: sessionId2 });
11484
11681
  channel.setApiKey(loginApiKey);
11485
- return finish(sessionId2, openResult.opened, loginApiKey, openResult.opened ? signInUrl : void 0);
11682
+ return finish(
11683
+ sessionId2,
11684
+ openResult.opened,
11685
+ loginApiKey,
11686
+ openResult.opened ? signInUrl : void 0,
11687
+ openResult.launched,
11688
+ // The sign-in page pushed studio:authenticated to get here, so the
11689
+ // window demonstrably exists: never arm the connect watchdog on it.
11690
+ true
11691
+ );
11486
11692
  }
11487
- const { sessionId, opened } = await channel.openOrNavigate(path21, {
11693
+ const { sessionId, opened, launched, launchReason } = await channel.openOrNavigate(path21, {
11488
11694
  clientHeaders: opts.clientHeaders,
11489
11695
  focusTarget: focusTarget ?? void 0
11490
11696
  });
11491
11697
  const url2 = opened ? resolveStudioSessionTarget({ serviceUrl, path: path21, sessionId }).url : void 0;
11492
- if (url2) {
11493
- opts.onWindowOpened?.(url2);
11698
+ if (opened && url2) {
11699
+ reportFreshOpen(url2, launched, launchReason);
11700
+ } else {
11701
+ emit({ event: "navigated", sessionId, path: path21 });
11494
11702
  }
11495
- return finish(sessionId, opened, apiKey, url2);
11703
+ return finish(sessionId, opened, apiKey, url2, launched);
11496
11704
  }
11497
11705
 
11498
11706
  // ../bitfab-plugin-lib/dist/commands/login.js
@@ -11539,15 +11747,21 @@ Already logged in as ${identity}${endpoint}. Run the login command with --force
11539
11747
  const result = await openStudioTo("/studio", {
11540
11748
  forceLogin: true,
11541
11749
  freshWindowPath: `/studio/close?autoClose=true&message=${encodeURIComponent("Login complete")}`,
11542
- onLoginRequired: (_sessionId, signInUrl) => {
11543
- if (!printedSignInUrl) {
11750
+ // login is a human-facing command, not a JSONL stream, so it renders the
11751
+ // Studio lifecycle events as text instead of the default stdout JSON. It
11752
+ // only cares about surfacing a sign-in link (once) and reporting a failed
11753
+ // browser launch; navigated/authenticated are handled by the flow below.
11754
+ emit: (event) => {
11755
+ if (event.event === "open-failed") {
11544
11756
  printedSignInUrl = true;
11545
- console.log(formatStudioUrlMessage(signInUrl));
11757
+ console.log(`Could not open a browser (${event.reason}). Open this link to sign in: ${event.url}`);
11758
+ return;
11759
+ }
11760
+ const url2 = event.event === "window-open-requested" ? event.url : event.event === "auth-required" ? event.signInUrl : null;
11761
+ if (url2 && !printedSignInUrl) {
11762
+ printedSignInUrl = true;
11763
+ console.log(formatStudioUrlMessage(url2));
11546
11764
  }
11547
- },
11548
- onWindowOpened: (url2) => {
11549
- printedSignInUrl = true;
11550
- console.log(formatStudioUrlMessage(url2));
11551
11765
  }
11552
11766
  });
11553
11767
  const apiKey = getConfig().apiKey;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bitfab-cli",
3
- "version": "0.2.187",
3
+ "version": "0.2.188",
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",