bitfab-cli 0.2.10 → 0.2.12

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 +608 -94
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6805,6 +6805,307 @@ import { cancel as cancel2 } from "@clack/prompts";
6805
6805
  import { execSync as execSync3 } from "child_process";
6806
6806
  import * as p4 from "@clack/prompts";
6807
6807
 
6808
+ // ../bitfab-plugin-lib/dist/errors.js
6809
+ var NotAuthenticatedError = class extends Error {
6810
+ constructor(message) {
6811
+ super(message ?? "Not authenticated. Run the login command to connect your Bitfab account.");
6812
+ this.name = "NotAuthenticatedError";
6813
+ }
6814
+ };
6815
+
6816
+ // ../bitfab-plugin-lib/dist/studioRoutes.js
6817
+ var STUDIO_ROUTE_PATTERNS = [
6818
+ /^\/studio$/,
6819
+ /^\/studio\/experiments$/,
6820
+ /^\/studio\/close$/,
6821
+ /^\/studio\/sign-in$/,
6822
+ /^\/studio\/trace-plan\/[^/]+$/,
6823
+ /^\/studio\/trace-functions\/[^/]+\/datasets\/labeled$/,
6824
+ /^\/studio\/trace-functions\/[^/]+\/datasets\/[^/]+$/,
6825
+ /^\/studio\/trace-functions\/[^/]+\/template-preview$/,
6826
+ /^\/studio\/trace-functions\/[^/]+\/template-preview\/[^/]+$/,
6827
+ /^\/studio\/auth\/[^/]+$/
6828
+ ];
6829
+ function isValidStudioRoute(input) {
6830
+ const pathname = input.split("?")[0];
6831
+ return STUDIO_ROUTE_PATTERNS.some((pattern) => pattern.test(pathname));
6832
+ }
6833
+
6834
+ // ../bitfab-plugin-lib/dist/agentSessionChannel.js
6835
+ var DEFAULT_POLL_INTERVAL_MS = 1500;
6836
+ async function createAgentSession(opts) {
6837
+ if (!opts.apiKey) {
6838
+ throw new NotAuthenticatedError();
6839
+ }
6840
+ const url2 = `${opts.serviceUrl}/api/studio/sessions`;
6841
+ const res = await fetch(url2, {
6842
+ method: "POST",
6843
+ headers: {
6844
+ Authorization: `Bearer ${opts.apiKey}`,
6845
+ "Content-Type": "application/json",
6846
+ ...opts.extraHeaders
6847
+ },
6848
+ body: JSON.stringify({ sessionId: opts.sessionId })
6849
+ });
6850
+ if (!res.ok) {
6851
+ if (res.status === 401) {
6852
+ throw new NotAuthenticatedError("Session expired or invalid. Run the login command to re-authenticate.");
6853
+ }
6854
+ throw new Error(`createAgentSession failed (${res.status}): ${await res.text()}`);
6855
+ }
6856
+ return await res.json();
6857
+ }
6858
+ async function pushAgentSessionEvent(client, event) {
6859
+ const url2 = `${client.serviceUrl}/api/studio/events`;
6860
+ const res = await fetch(url2, {
6861
+ method: "POST",
6862
+ headers: {
6863
+ Authorization: `Bearer ${client.apiKey}`,
6864
+ "Content-Type": "application/json"
6865
+ },
6866
+ body: JSON.stringify({
6867
+ session: client.sessionId,
6868
+ type: event.type,
6869
+ data: event.data
6870
+ })
6871
+ });
6872
+ if (!res.ok) {
6873
+ throw new Error(`pushAgentSessionEvent failed (${res.status}): ${await res.text()}`);
6874
+ }
6875
+ return await res.json();
6876
+ }
6877
+ async function pollAgentSessionEvents(opts) {
6878
+ const interval = opts.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
6879
+ const client = {
6880
+ serviceUrl: opts.serviceUrl,
6881
+ apiKey: opts.apiKey,
6882
+ sessionId: opts.sessionId
6883
+ };
6884
+ let since = opts.startCursor ?? null;
6885
+ while (!opts.abortSignal.aborted) {
6886
+ try {
6887
+ const url2 = new URL(`${opts.serviceUrl}/api/studio/events`);
6888
+ url2.searchParams.set("session", opts.sessionId);
6889
+ if (since) {
6890
+ url2.searchParams.set("since", since);
6891
+ }
6892
+ const res = await fetch(url2.toString(), {
6893
+ headers: { Authorization: `Bearer ${opts.apiKey}` },
6894
+ signal: opts.abortSignal
6895
+ });
6896
+ if (res.ok) {
6897
+ const body = await res.json();
6898
+ for (const event of body.events) {
6899
+ if (event.type === "studio:ping") {
6900
+ sendPong(client, event.data.ts).catch(() => {
6901
+ });
6902
+ }
6903
+ opts.onEvent(event);
6904
+ since = event.id;
6905
+ }
6906
+ if (opts.onBrowserStatus && typeof body.browserConnected === "boolean") {
6907
+ opts.onBrowserStatus(body.browserConnected);
6908
+ }
6909
+ } else if (opts.onError) {
6910
+ opts.onError(new Error(`agent-session poll failed (${res.status})`));
6911
+ }
6912
+ } catch (err) {
6913
+ if (opts.abortSignal.aborted) {
6914
+ return;
6915
+ }
6916
+ if (opts.onError) {
6917
+ opts.onError(err instanceof Error ? err : new Error(String(err)));
6918
+ }
6919
+ }
6920
+ await new Promise((resolve) => {
6921
+ const onAbort = () => {
6922
+ clearTimeout(timer);
6923
+ resolve();
6924
+ };
6925
+ const timer = setTimeout(() => {
6926
+ opts.abortSignal.removeEventListener("abort", onAbort);
6927
+ resolve();
6928
+ }, interval);
6929
+ opts.abortSignal.addEventListener("abort", onAbort, { once: true });
6930
+ });
6931
+ }
6932
+ }
6933
+ async function pollLoginEvents(opts) {
6934
+ const interval = opts.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
6935
+ let since = null;
6936
+ while (!opts.abortSignal.aborted) {
6937
+ try {
6938
+ const url2 = new URL(`${opts.serviceUrl}/api/studio/sessions/poll`);
6939
+ url2.searchParams.set("session", opts.sessionId);
6940
+ if (since) {
6941
+ url2.searchParams.set("since", since);
6942
+ }
6943
+ const res = await fetch(url2.toString(), {
6944
+ signal: opts.abortSignal
6945
+ });
6946
+ if (res.ok) {
6947
+ const body = await res.json();
6948
+ for (const event of body.events) {
6949
+ since = event.id;
6950
+ if (event.type === "studio:authenticated" && typeof event.data.token === "string") {
6951
+ return event.data.token;
6952
+ }
6953
+ }
6954
+ }
6955
+ } catch {
6956
+ if (opts.abortSignal.aborted) {
6957
+ break;
6958
+ }
6959
+ }
6960
+ await new Promise((resolve) => {
6961
+ const onAbort = () => {
6962
+ clearTimeout(timer);
6963
+ resolve();
6964
+ };
6965
+ const timer = setTimeout(() => {
6966
+ opts.abortSignal.removeEventListener("abort", onAbort);
6967
+ resolve();
6968
+ }, interval);
6969
+ opts.abortSignal.addEventListener("abort", onAbort, { once: true });
6970
+ });
6971
+ }
6972
+ throw new Error("Login polling aborted");
6973
+ }
6974
+ async function navigateStudio(client, path14) {
6975
+ if (!isValidStudioRoute(path14)) {
6976
+ throw new Error(`Studio route not allowed: ${path14}`);
6977
+ }
6978
+ return pushAgentSessionEvent(client, {
6979
+ type: "agent:navigate",
6980
+ data: { path: path14 }
6981
+ });
6982
+ }
6983
+ var STUDIO_NAVIGATE_ACK_TIMEOUT_MS = 12e3;
6984
+ var PING_PONG_TIMEOUT_MS = 5e3;
6985
+ function awaitNavigateAck(client, path14, ackTimeoutMs) {
6986
+ const abortController = new AbortController();
6987
+ let timer = null;
6988
+ const result = new Promise((resolve) => {
6989
+ timer = setTimeout(() => {
6990
+ resolve({ acked: false, reason: "timeout" });
6991
+ abortController.abort();
6992
+ }, ackTimeoutMs);
6993
+ const pathOnly = path14.split("?")[0];
6994
+ pollAgentSessionEvents({
6995
+ serviceUrl: client.serviceUrl,
6996
+ apiKey: client.apiKey,
6997
+ sessionId: client.sessionId,
6998
+ abortSignal: abortController.signal,
6999
+ onEvent: (event) => {
7000
+ if (event.type === "studio:navigated" && event.data.path === pathOnly) {
7001
+ if (timer) {
7002
+ clearTimeout(timer);
7003
+ }
7004
+ resolve({ acked: true });
7005
+ abortController.abort();
7006
+ return;
7007
+ }
7008
+ if (event.type === "studio:navigation-blocked" && event.data.path === pathOnly) {
7009
+ if (timer) {
7010
+ clearTimeout(timer);
7011
+ }
7012
+ resolve({
7013
+ acked: false,
7014
+ reason: "blocked",
7015
+ blockedReason: event.data.reason
7016
+ });
7017
+ abortController.abort();
7018
+ return;
7019
+ }
7020
+ if (event.type === "studio:session-ended" || event.type === "studio:session-closed") {
7021
+ if (timer) {
7022
+ clearTimeout(timer);
7023
+ }
7024
+ resolve({ acked: false, reason: "session-ended" });
7025
+ abortController.abort();
7026
+ }
7027
+ },
7028
+ onError: () => {
7029
+ }
7030
+ }).catch(() => {
7031
+ if (abortController.signal.aborted) {
7032
+ return;
7033
+ }
7034
+ if (timer) {
7035
+ clearTimeout(timer);
7036
+ }
7037
+ resolve({ acked: false, reason: "timeout" });
7038
+ });
7039
+ });
7040
+ return result;
7041
+ }
7042
+ function awaitPong(client, pingTs) {
7043
+ return new Promise((resolve) => {
7044
+ const abortController = new AbortController();
7045
+ let received = false;
7046
+ const timer = setTimeout(() => {
7047
+ abortController.abort();
7048
+ resolve(false);
7049
+ }, PING_PONG_TIMEOUT_MS);
7050
+ pollAgentSessionEvents({
7051
+ serviceUrl: client.serviceUrl,
7052
+ apiKey: client.apiKey,
7053
+ sessionId: client.sessionId,
7054
+ abortSignal: abortController.signal,
7055
+ onEvent: (event) => {
7056
+ if (event.type === "studio:pong" && event.data.replyTo === pingTs) {
7057
+ received = true;
7058
+ clearTimeout(timer);
7059
+ abortController.abort();
7060
+ resolve(true);
7061
+ }
7062
+ },
7063
+ onError: () => {
7064
+ }
7065
+ }).catch(() => {
7066
+ if (!received) {
7067
+ clearTimeout(timer);
7068
+ resolve(false);
7069
+ }
7070
+ });
7071
+ });
7072
+ }
7073
+ async function navigateStudioAndAwaitAck(client, path14, opts = {}) {
7074
+ const ackTimeoutMs = opts.ackTimeoutMs ?? STUDIO_NAVIGATE_ACK_TIMEOUT_MS;
7075
+ if (!isValidStudioRoute(path14)) {
7076
+ return { acked: false, reason: "invalid-route" };
7077
+ }
7078
+ try {
7079
+ await navigateStudio(client, path14);
7080
+ } catch {
7081
+ return { acked: false, reason: "push-failed" };
7082
+ }
7083
+ const result = await awaitNavigateAck(client, path14, ackTimeoutMs);
7084
+ if (result.acked || result.reason !== "timeout") {
7085
+ return result;
7086
+ }
7087
+ const pingTs = Date.now();
7088
+ try {
7089
+ await pushAgentSessionEvent(client, {
7090
+ type: "agent:ping",
7091
+ data: { ts: pingTs }
7092
+ });
7093
+ } catch {
7094
+ return result;
7095
+ }
7096
+ const pongReceived = await awaitPong(client, pingTs);
7097
+ if (pongReceived) {
7098
+ return { acked: true };
7099
+ }
7100
+ return result;
7101
+ }
7102
+ async function sendPong(client, replyTo) {
7103
+ return pushAgentSessionEvent(client, {
7104
+ type: "agent:pong",
7105
+ data: { ts: Date.now(), replyTo }
7106
+ });
7107
+ }
7108
+
6808
7109
  // ../bitfab-plugin-lib/dist/buildInfo.js
6809
7110
  import crypto from "crypto";
6810
7111
  import fs from "fs";
@@ -6840,9 +7141,9 @@ var GLOBAL_CONFIG_FILE = path3.join(GLOBAL_CONFIG_DIR, "config.json");
6840
7141
  var GLOBAL_CREDENTIALS_FILE = path3.join(GLOBAL_CONFIG_DIR, "credentials.json");
6841
7142
  var PROJECT_CONFIG_RELATIVE = path3.join(".bitfab", "config.local.json");
6842
7143
  var PROJECT_CREDENTIALS_RELATIVE = path3.join(".bitfab", "credentials.local.json");
6843
- function readJsonFile(filePath) {
7144
+ function readJsonFile(filePath2) {
6844
7145
  try {
6845
- const content = fs3.readFileSync(filePath, "utf-8");
7146
+ const content = fs3.readFileSync(filePath2, "utf-8");
6846
7147
  return JSON.parse(content);
6847
7148
  } catch {
6848
7149
  return null;
@@ -6863,18 +7164,18 @@ function findProjectFile(relativePath, startDir = process.cwd()) {
6863
7164
  }
6864
7165
  }
6865
7166
  function getProjectConfigData() {
6866
- const filePath = findProjectFile(PROJECT_CONFIG_RELATIVE);
6867
- if (!filePath) {
7167
+ const filePath2 = findProjectFile(PROJECT_CONFIG_RELATIVE);
7168
+ if (!filePath2) {
6868
7169
  return null;
6869
7170
  }
6870
- return readJsonFile(filePath);
7171
+ return readJsonFile(filePath2);
6871
7172
  }
6872
7173
  function getProjectCredentialsData() {
6873
- const filePath = findProjectFile(PROJECT_CREDENTIALS_RELATIVE);
6874
- if (!filePath) {
7174
+ const filePath2 = findProjectFile(PROJECT_CREDENTIALS_RELATIVE);
7175
+ if (!filePath2) {
6875
7176
  return null;
6876
7177
  }
6877
- return readJsonFile(filePath) ?? {};
7178
+ return readJsonFile(filePath2) ?? {};
6878
7179
  }
6879
7180
  function getConfigData() {
6880
7181
  return readJsonFile(GLOBAL_CONFIG_FILE) ?? {};
@@ -6941,6 +7242,12 @@ function getConfig() {
6941
7242
  };
6942
7243
  }
6943
7244
  function saveCredentials(apiKey) {
7245
+ const projectFile = findProjectFile(PROJECT_CREDENTIALS_RELATIVE);
7246
+ if (projectFile) {
7247
+ fs3.writeFileSync(projectFile, `${JSON.stringify({ apiKey }, null, 2)}
7248
+ `);
7249
+ return;
7250
+ }
6944
7251
  fs3.mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true });
6945
7252
  fs3.writeFileSync(GLOBAL_CREDENTIALS_FILE, `${JSON.stringify({ apiKey }, null, 2)}
6946
7253
  `);
@@ -6996,36 +7303,62 @@ function buildBitfabRequestHeaders(apiKey, pluginVersion, platform2) {
6996
7303
  };
6997
7304
  }
6998
7305
 
6999
- // ../bitfab-plugin-lib/dist/commands/login.js
7306
+ // ../bitfab-plugin-lib/dist/activeStudioSession.js
7000
7307
  import crypto3 from "crypto";
7001
-
7002
- // ../bitfab-plugin-lib/dist/analytics.js
7003
- async function reportHandoff(apiKey, pluginVersion, platform2, body) {
7004
- const config2 = getConfig();
7005
- const headers = buildBitfabRequestHeaders(apiKey, pluginVersion, platform2);
7308
+ import fs6 from "fs";
7309
+ import os6 from "os";
7310
+ import path5 from "path";
7311
+ function cwdHash() {
7312
+ return crypto3.createHash("sha256").update(process.cwd()).digest("hex").slice(0, 16);
7313
+ }
7314
+ function filePath() {
7315
+ return path5.join(os6.homedir(), ".config", "bitfab", `active-studio-session.${cwdHash()}.json`);
7316
+ }
7317
+ function writeActiveStudioSession(session) {
7318
+ const target = filePath();
7319
+ fs6.mkdirSync(path5.dirname(target), { recursive: true });
7320
+ const payload = {
7321
+ ...session,
7322
+ pid: process.pid,
7323
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
7324
+ };
7325
+ const tmp = `${target}.${process.pid}.tmp`;
7326
+ fs6.writeFileSync(tmp, `${JSON.stringify(payload, null, 2)}
7327
+ `);
7328
+ fs6.renameSync(tmp, target);
7329
+ }
7330
+ function clearActiveStudioSession() {
7006
7331
  try {
7007
- const response = await fetch(`${config2.serviceUrl}/api/plugin/analytics/handoff`, {
7008
- method: "POST",
7009
- headers,
7010
- body: JSON.stringify(body)
7011
- });
7012
- if (config2.debug && !response.ok) {
7013
- console.error(`reportHandoff: server returned ${response.status} for ${body.flow}`);
7332
+ const current = readActiveStudioSessionRaw();
7333
+ if (current && current.pid !== process.pid) {
7334
+ return;
7014
7335
  }
7015
- } catch (err) {
7016
- if (config2.debug) {
7017
- const message = err instanceof Error ? err.message : String(err);
7018
- console.error(`reportHandoff: request failed for ${body.flow}: ${message}`);
7336
+ fs6.rmSync(filePath(), { force: true });
7337
+ } catch {
7338
+ }
7339
+ }
7340
+ function readActiveStudioSessionRaw() {
7341
+ try {
7342
+ const raw = fs6.readFileSync(filePath(), "utf-8");
7343
+ const parsed = JSON.parse(raw);
7344
+ if (typeof parsed.sessionId === "string" && typeof parsed.serviceUrl === "string" && typeof parsed.pid === "number" && typeof parsed.startedAt === "string") {
7345
+ return parsed;
7019
7346
  }
7347
+ return null;
7348
+ } catch {
7349
+ return null;
7020
7350
  }
7021
7351
  }
7352
+ function readActiveStudioSession() {
7353
+ return readActiveStudioSessionRaw();
7354
+ }
7022
7355
 
7023
7356
  // ../bitfab-plugin-lib/dist/browser.js
7024
7357
  import { execSync, spawn } from "child_process";
7025
- import fs6 from "fs";
7026
- import os6 from "os";
7358
+ import fs7 from "fs";
7359
+ import os7 from "os";
7027
7360
  function getChromiumBrowsers() {
7028
- const platform2 = os6.platform();
7361
+ const platform2 = os7.platform();
7029
7362
  if (platform2 === "darwin") {
7030
7363
  return [
7031
7364
  {
@@ -7113,7 +7446,7 @@ function getChromiumBrowsers() {
7113
7446
  function findExistingBinary(paths) {
7114
7447
  for (const candidate of paths) {
7115
7448
  if (candidate.includes("/") || candidate.includes("\\")) {
7116
- if (fs6.existsSync(candidate)) {
7449
+ if (fs7.existsSync(candidate)) {
7117
7450
  return candidate;
7118
7451
  }
7119
7452
  } else {
@@ -7127,10 +7460,10 @@ function findExistingBinary(paths) {
7127
7460
  return null;
7128
7461
  }
7129
7462
  function getDefaultBrowserId() {
7130
- const platform2 = os6.platform();
7463
+ const platform2 = os7.platform();
7131
7464
  try {
7132
7465
  if (platform2 === "darwin") {
7133
- const plistPath = `${os6.homedir()}/Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist`;
7466
+ const plistPath = `${os7.homedir()}/Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist`;
7134
7467
  const json2 = execSync(`plutil -convert json -o - "${plistPath}"`, {
7135
7468
  stdio: ["ignore", "pipe", "ignore"],
7136
7469
  encoding: "utf-8",
@@ -7171,7 +7504,7 @@ function matchChromiumBrowser(defaultId, browsers) {
7171
7504
  var SCREEN_FRACTION = 0.95;
7172
7505
  function getWorkArea() {
7173
7506
  try {
7174
- const platform2 = os6.platform();
7507
+ const platform2 = os7.platform();
7175
7508
  if (platform2 === "darwin") {
7176
7509
  const out = execSync(`osascript -l JavaScript -e 'ObjC.import("AppKit"); const s = $.NSScreen.mainScreen; const f = s.frame; const v = s.visibleFrame; const topY = f.size.height - (v.origin.y + v.size.height); JSON.stringify([v.origin.x, topY, v.size.width, v.size.height])'`, {
7177
7510
  stdio: ["ignore", "pipe", "ignore"],
@@ -7240,7 +7573,7 @@ function openChromiumApp(binaryPath, url2, sizeArgs) {
7240
7573
  spawnDetached(binaryPath, [`--app=${url2}`, ...sizeArgs]);
7241
7574
  }
7242
7575
  function openOsDefault(url2) {
7243
- const platform2 = os6.platform();
7576
+ const platform2 = os7.platform();
7244
7577
  if (platform2 === "darwin") {
7245
7578
  spawnDetached("open", [url2]);
7246
7579
  } else if (platform2 === "win32") {
@@ -7291,9 +7624,61 @@ function openChromelessWindow(url2) {
7291
7624
  openOsDefault(url2);
7292
7625
  }
7293
7626
 
7627
+ // ../bitfab-plugin-lib/dist/commands/createStudioSession.js
7628
+ function ensureStudioPath(p5) {
7629
+ if (!p5.startsWith("/studio")) {
7630
+ return "/studio";
7631
+ }
7632
+ return p5;
7633
+ }
7634
+ async function createStudioSession({ serviceUrl, apiKey, sessionId, initialPath = "/studio", clientHeaders }) {
7635
+ if (!apiKey) {
7636
+ throw new NotAuthenticatedError();
7637
+ }
7638
+ initialPath = ensureStudioPath(initialPath);
7639
+ if (!isValidStudioRoute(initialPath)) {
7640
+ throw new Error(`Studio route not allowed: ${initialPath}`);
7641
+ }
7642
+ await createAgentSession({
7643
+ serviceUrl,
7644
+ apiKey,
7645
+ sessionId,
7646
+ extraHeaders: clientHeaders
7647
+ });
7648
+ writeActiveStudioSession({ sessionId, serviceUrl });
7649
+ const separator = initialPath.includes("?") ? "&" : "?";
7650
+ const url2 = `${serviceUrl}${initialPath}${separator}session=${encodeURIComponent(sessionId)}`;
7651
+ openChromelessWindow(url2);
7652
+ return { sessionId, serviceUrl };
7653
+ }
7654
+
7655
+ // ../bitfab-plugin-lib/dist/commands/login.js
7656
+ import crypto5 from "crypto";
7657
+
7658
+ // ../bitfab-plugin-lib/dist/analytics.js
7659
+ async function reportHandoff(apiKey, pluginVersion, platform2, body) {
7660
+ const config2 = getConfig();
7661
+ const headers = buildBitfabRequestHeaders(apiKey, pluginVersion, platform2);
7662
+ try {
7663
+ const response = await fetch(`${config2.serviceUrl}/api/plugin/analytics/handoff`, {
7664
+ method: "POST",
7665
+ headers,
7666
+ body: JSON.stringify(body)
7667
+ });
7668
+ if (config2.debug && !response.ok) {
7669
+ console.error(`reportHandoff: server returned ${response.status} for ${body.flow}`);
7670
+ }
7671
+ } catch (err) {
7672
+ if (config2.debug) {
7673
+ const message = err instanceof Error ? err.message : String(err);
7674
+ console.error(`reportHandoff: request failed for ${body.flow}: ${message}`);
7675
+ }
7676
+ }
7677
+ }
7678
+
7294
7679
  // ../bitfab-plugin-lib/dist/frontmostApp.js
7295
7680
  import { execFileSync as execFileSync2, spawn as spawn2 } from "child_process";
7296
- import os7 from "os";
7681
+ import os8 from "os";
7297
7682
  function findAncestorApp() {
7298
7683
  try {
7299
7684
  let pid = process.ppid;
@@ -7354,7 +7739,7 @@ function focusItermSession(sessionId) {
7354
7739
  spawnQuiet("osascript", ["-e", script]);
7355
7740
  }
7356
7741
  function getFrontmostApp() {
7357
- const platform2 = os7.platform();
7742
+ const platform2 = os8.platform();
7358
7743
  try {
7359
7744
  if (platform2 === "darwin") {
7360
7745
  return execFileSync2("osascript", [
@@ -7388,7 +7773,7 @@ function focusApp(identifier) {
7388
7773
  if (!identifier) {
7389
7774
  return;
7390
7775
  }
7391
- const platform2 = os7.platform();
7776
+ const platform2 = os8.platform();
7392
7777
  if (platform2 === "darwin") {
7393
7778
  spawnQuiet("osascript", [
7394
7779
  "-e",
@@ -7418,7 +7803,7 @@ function focusApp(identifier) {
7418
7803
  }
7419
7804
  }
7420
7805
  function recordFocus() {
7421
- const platform2 = os7.platform();
7806
+ const platform2 = os8.platform();
7422
7807
  if (platform2 === "linux") {
7423
7808
  const windowId = process.env.WINDOWID;
7424
7809
  if (windowId) {
@@ -7436,9 +7821,151 @@ function recordFocus() {
7436
7821
  return () => focusApp(frontmost);
7437
7822
  }
7438
7823
 
7439
- // ../bitfab-plugin-lib/dist/commands/login.js
7824
+ // ../bitfab-plugin-lib/dist/commands/openStudioTo.js
7825
+ import crypto4 from "crypto";
7826
+
7827
+ // ../bitfab-plugin-lib/dist/commands/loginForSession.js
7440
7828
  var LOGIN_TIMEOUT_MS = 10 * 60 * 1e3;
7441
- var POLL_INTERVAL_MS = 1500;
7829
+
7830
+ // ../bitfab-plugin-lib/dist/commands/openStudioTo.js
7831
+ var SESSION_CLOSE_GRACE_PERIOD_MS = 1e4;
7832
+ var StudioNavigationError = class extends Error {
7833
+ reason;
7834
+ blockedReason;
7835
+ staleSessionId;
7836
+ constructor(reason, blockedReason, staleSessionId) {
7837
+ super(blockedReason ? `Studio navigation failed (${reason}): ${blockedReason}` : `Studio navigation failed: ${reason}`);
7838
+ this.reason = reason;
7839
+ this.blockedReason = blockedReason;
7840
+ this.staleSessionId = staleSessionId;
7841
+ this.name = "StudioNavigationError";
7842
+ }
7843
+ };
7844
+ async function openStudioTo(path14, opts) {
7845
+ const sessionId = opts.sessionId ?? crypto4.randomUUID();
7846
+ const noop = () => {
7847
+ };
7848
+ const restoreFocus = recordFocus();
7849
+ if (!opts.apiKey) {
7850
+ const separator = path14.includes("?") ? "&" : "?";
7851
+ const url2 = `${opts.serviceUrl}${path14}${separator}session=${encodeURIComponent(sessionId)}`;
7852
+ openChromelessWindow(url2);
7853
+ return {
7854
+ sessionId,
7855
+ serviceUrl: opts.serviceUrl,
7856
+ opened: true,
7857
+ abort: noop,
7858
+ done: Promise.resolve()
7859
+ };
7860
+ }
7861
+ if (opts.forceNew) {
7862
+ clearActiveStudioSession();
7863
+ }
7864
+ const existing = opts.forceNew ? null : readActiveStudioSession();
7865
+ if (existing) {
7866
+ const client = {
7867
+ serviceUrl: existing.serviceUrl,
7868
+ apiKey: opts.apiKey,
7869
+ sessionId: existing.sessionId
7870
+ };
7871
+ const ack = await navigateStudioAndAwaitAck(client, path14);
7872
+ if (ack.acked) {
7873
+ const poller2 = opts.onEvent ? startEventPoller(existing.serviceUrl, opts.apiKey, existing.sessionId, opts.onEvent, restoreFocus) : { abort: noop, done: Promise.resolve() };
7874
+ return {
7875
+ sessionId: existing.sessionId,
7876
+ serviceUrl: existing.serviceUrl,
7877
+ opened: false,
7878
+ ...poller2
7879
+ };
7880
+ }
7881
+ if (ack.reason === "session-ended" || ack.reason === "push-failed") {
7882
+ if (ack.reason === "session-ended") {
7883
+ clearActiveStudioSession();
7884
+ }
7885
+ } else {
7886
+ throw new StudioNavigationError(ack.reason ?? "unknown", ack.blockedReason, existing.sessionId);
7887
+ }
7888
+ }
7889
+ const session = await createStudioSession({
7890
+ serviceUrl: opts.serviceUrl,
7891
+ apiKey: opts.apiKey,
7892
+ sessionId,
7893
+ initialPath: path14,
7894
+ clientHeaders: opts.clientHeaders
7895
+ });
7896
+ const poller = opts.onEvent ? startEventPoller(session.serviceUrl, opts.apiKey, session.sessionId, opts.onEvent, restoreFocus) : { abort: noop, done: Promise.resolve() };
7897
+ return {
7898
+ sessionId: session.sessionId,
7899
+ serviceUrl: session.serviceUrl,
7900
+ opened: true,
7901
+ ...poller
7902
+ };
7903
+ }
7904
+ function startEventPoller(serviceUrl, apiKey, sessionId, onEvent, restoreFocus) {
7905
+ const abortController = new AbortController();
7906
+ let sessionEndGraceTimer = null;
7907
+ const done = pollAgentSessionEvents({
7908
+ serviceUrl,
7909
+ apiKey,
7910
+ sessionId,
7911
+ abortSignal: abortController.signal,
7912
+ onEvent: (event) => {
7913
+ if (event.type === "studio:session-closed") {
7914
+ if (sessionEndGraceTimer) {
7915
+ return;
7916
+ }
7917
+ sessionEndGraceTimer = setTimeout(() => {
7918
+ sessionEndGraceTimer = null;
7919
+ clearActiveStudioSession();
7920
+ restoreFocus();
7921
+ onEvent({ ...event, type: "studio:session-ended" });
7922
+ abortController.abort();
7923
+ }, SESSION_CLOSE_GRACE_PERIOD_MS);
7924
+ return;
7925
+ }
7926
+ if (event.type === "studio:session-started") {
7927
+ if (sessionEndGraceTimer) {
7928
+ clearTimeout(sessionEndGraceTimer);
7929
+ sessionEndGraceTimer = null;
7930
+ console.error("Studio reconnected after page refresh");
7931
+ }
7932
+ return;
7933
+ }
7934
+ if (event.type === "studio:session-ended") {
7935
+ if (sessionEndGraceTimer) {
7936
+ clearTimeout(sessionEndGraceTimer);
7937
+ sessionEndGraceTimer = null;
7938
+ }
7939
+ clearActiveStudioSession();
7940
+ restoreFocus();
7941
+ onEvent(event);
7942
+ abortController.abort();
7943
+ return;
7944
+ }
7945
+ if (event.type === "studio:not-member") {
7946
+ restoreFocus();
7947
+ onEvent(event);
7948
+ abortController.abort();
7949
+ return;
7950
+ }
7951
+ if (event.type === "studio:return-to-agent" || event.type === "studio:edit-with-agent") {
7952
+ restoreFocus();
7953
+ }
7954
+ onEvent(event);
7955
+ },
7956
+ onError: (err) => {
7957
+ console.error(`studio agent-event poll: ${err.message}`);
7958
+ }
7959
+ }).catch((err) => {
7960
+ if (!abortController.signal.aborted) {
7961
+ console.error(`studio agent-event poll terminated: ${err.message}`);
7962
+ }
7963
+ });
7964
+ return { abort: () => abortController.abort(), done };
7965
+ }
7966
+
7967
+ // ../bitfab-plugin-lib/dist/commands/login.js
7968
+ var LOGIN_TIMEOUT_MS2 = 10 * 60 * 1e3;
7442
7969
  async function verifyToken(serviceUrl, token) {
7443
7970
  try {
7444
7971
  const res = await fetch(`${serviceUrl}/api/plugin/whoami`, {
@@ -7452,58 +7979,50 @@ async function verifyToken(serviceUrl, token) {
7452
7979
  return null;
7453
7980
  }
7454
7981
  }
7455
- async function pollLoginStatus(serviceUrl, loginId, signal) {
7456
- while (!signal.aborted) {
7457
- try {
7458
- const url2 = `${serviceUrl}/api/studio/login-status?loginId=${encodeURIComponent(loginId)}`;
7459
- const res = await fetch(url2, { signal });
7460
- if (res.ok) {
7461
- const body = await res.json();
7462
- if (body.status === "complete" && body.token) {
7463
- return body.token;
7464
- }
7465
- }
7466
- } catch {
7467
- if (signal.aborted) {
7468
- throw new Error("Login polling aborted");
7469
- }
7470
- }
7471
- await new Promise((resolve) => {
7472
- const timer = setTimeout(resolve, POLL_INTERVAL_MS);
7473
- signal.addEventListener("abort", () => {
7474
- clearTimeout(timer);
7475
- resolve();
7476
- }, { once: true });
7477
- });
7478
- }
7479
- throw new Error("Login polling aborted");
7480
- }
7481
7982
  async function runLogin(platform2, pluginVersion, options) {
7482
7983
  const exitOnComplete = options?.exitOnComplete ?? true;
7483
7984
  const config2 = getConfig();
7484
7985
  const restoreFocus = recordFocus();
7485
- const loginId = crypto3.randomUUID();
7486
- const redirectUrl = `/studio/close?pluginLogin=true&loginId=${loginId}`;
7487
- const browserUrl = `${config2.serviceUrl}/studio/sign-in?redirect_url=${encodeURIComponent(redirectUrl)}`;
7986
+ const sessionId = crypto5.randomUUID();
7987
+ const redirectPath = `/studio/close?pluginLogin=true&session=${sessionId}&autoClose=true&message=${encodeURIComponent("Login complete")}`;
7988
+ const signInPath = `/studio/sign-in?redirect_url=${encodeURIComponent(redirectPath)}`;
7488
7989
  console.log("\nOpening Studio to sign in...");
7489
- console.log(` ${browserUrl}`);
7490
- console.log("");
7491
- console.log("(If the browser didn't open, visit the URL above manually.)");
7492
- openChromelessWindow(browserUrl);
7990
+ console.log("(If the browser didn't open, visit the Studio URL manually.)");
7991
+ try {
7992
+ await openStudioTo(signInPath, {
7993
+ apiKey: hasCredentials() ? config2.apiKey : null,
7994
+ serviceUrl: config2.serviceUrl,
7995
+ sessionId
7996
+ });
7997
+ } catch (err) {
7998
+ if (err instanceof StudioNavigationError) {
7999
+ await openStudioTo(signInPath, {
8000
+ apiKey: null,
8001
+ serviceUrl: config2.serviceUrl,
8002
+ sessionId
8003
+ });
8004
+ } else {
8005
+ throw err;
8006
+ }
8007
+ }
7493
8008
  const abortController = new AbortController();
7494
8009
  let loginTimer;
7495
8010
  const timeoutPromise = new Promise((_resolve, reject) => {
7496
8011
  loginTimer = setTimeout(() => {
7497
8012
  abortController.abort();
7498
8013
  reject(new Error("Authentication timed out after 10 minutes. Run login again."));
7499
- }, LOGIN_TIMEOUT_MS);
8014
+ }, LOGIN_TIMEOUT_MS2);
7500
8015
  });
7501
8016
  const keepaliveTimer = setInterval(() => {
7502
8017
  process.stdout.write("");
7503
8018
  }, 3e4);
7504
8019
  try {
7505
8020
  const token = await Promise.race([
7506
- pollLoginStatus(config2.serviceUrl, loginId, abortController.signal),
8021
+ pollLoginEvents({
8022
+ serviceUrl: config2.serviceUrl,
8023
+ sessionId,
8024
+ abortSignal: abortController.signal
8025
+ }),
7507
8026
  timeoutPromise
7508
8027
  ]);
7509
8028
  if (loginTimer) {
@@ -7511,11 +8030,6 @@ async function runLogin(platform2, pluginVersion, options) {
7511
8030
  }
7512
8031
  clearInterval(keepaliveTimer);
7513
8032
  saveCredentials(token);
7514
- const who = await verifyToken(config2.serviceUrl, token);
7515
- const greeting = who?.user.email ? `Logged in as ${who.user.email}.` : "Authentication successful.";
7516
- console.log(`
7517
- ${greeting}`);
7518
- console.log("Bitfab MCP tools are now active. (via studio)");
7519
8033
  await reportHandoff(token, pluginVersion, platform2, {
7520
8034
  flow: "auth.login",
7521
8035
  wonBy: "studio",
@@ -7523,6 +8037,11 @@ ${greeting}`);
7523
8037
  ticketAvailable: false
7524
8038
  });
7525
8039
  restoreFocus();
8040
+ const who = await verifyToken(config2.serviceUrl, token);
8041
+ const greeting = who?.user.email ? `Logged in as ${who.user.email}.` : "Authentication successful.";
8042
+ console.log(`
8043
+ ${greeting}`);
8044
+ console.log("Bitfab MCP tools are now active. (via studio)");
7526
8045
  if (exitOnComplete) {
7527
8046
  process.exit(0);
7528
8047
  }
@@ -7552,19 +8071,8 @@ function runLogout() {
7552
8071
  console.log("Logged out of Bitfab.");
7553
8072
  }
7554
8073
 
7555
- // ../bitfab-plugin-lib/dist/commands/openStudio.js
7556
- import crypto5 from "crypto";
7557
-
7558
- // ../bitfab-plugin-lib/dist/activeStudioSession.js
7559
- import crypto4 from "crypto";
7560
- import fs7 from "fs";
7561
- import os8 from "os";
7562
- import path5 from "path";
7563
-
7564
- // ../bitfab-plugin-lib/dist/withStudioSession.js
7565
- import crypto6 from "crypto";
7566
-
7567
8074
  // ../bitfab-plugin-lib/dist/commands/openTracePlan.js
8075
+ import crypto6 from "crypto";
7568
8076
  var OPEN_TRACE_PLAN_TIMEOUT_MS = 30 * 60 * 1e3;
7569
8077
 
7570
8078
  // ../bitfab-plugin-lib/dist/commands/persistReplayLabels.js
@@ -21364,6 +21872,12 @@ var inputFileSchema = external_exports.object({
21364
21872
  verdicts: external_exports.array(external_exports.unknown()).min(1)
21365
21873
  });
21366
21874
 
21875
+ // ../bitfab-plugin-lib/dist/commands/startDataset.js
21876
+ import crypto7 from "crypto";
21877
+
21878
+ // ../bitfab-plugin-lib/dist/commands/startTemplatePreview.js
21879
+ import crypto8 from "crypto";
21880
+
21367
21881
  // ../bitfab-plugin-lib/dist/activePreviewSession.js
21368
21882
  import fs8 from "fs";
21369
21883
  import os9 from "os";
@@ -27390,7 +27904,7 @@ Examples:
27390
27904
  bitfab setup --skip-permissions Setup without permission prompts
27391
27905
  bitfab assistant --skip-permissions Run assistant autonomously
27392
27906
  `;
27393
- function parseArgs(argv) {
27907
+ function parseArgs2(argv) {
27394
27908
  const [command, ...tail] = argv;
27395
27909
  let editor;
27396
27910
  let skipPermissions;
@@ -27417,7 +27931,7 @@ function parseArgs(argv) {
27417
27931
  return { command, editor, skipPermissions, rest };
27418
27932
  }
27419
27933
  async function main() {
27420
- const { command, editor, skipPermissions, rest } = parseArgs(
27934
+ const { command, editor, skipPermissions, rest } = parseArgs2(
27421
27935
  process.argv.slice(2)
27422
27936
  );
27423
27937
  const abortUpdateCheck = startUpdateCheck();
@@ -27473,5 +27987,5 @@ main().catch((err) => {
27473
27987
  process.exit(1);
27474
27988
  });
27475
27989
  export {
27476
- parseArgs
27990
+ parseArgs2 as parseArgs
27477
27991
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bitfab-cli",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
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",