bitfab-cli 0.2.9 → 0.2.11

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 +670 -93
  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";
@@ -6839,18 +7140,19 @@ var GLOBAL_CONFIG_DIR = path3.join(os3.homedir(), ".config", "bitfab");
6839
7140
  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
- function readJsonFile(filePath) {
7143
+ var PROJECT_CREDENTIALS_RELATIVE = path3.join(".bitfab", "credentials.local.json");
7144
+ function readJsonFile(filePath2) {
6843
7145
  try {
6844
- const content = fs3.readFileSync(filePath, "utf-8");
7146
+ const content = fs3.readFileSync(filePath2, "utf-8");
6845
7147
  return JSON.parse(content);
6846
7148
  } catch {
6847
7149
  return null;
6848
7150
  }
6849
7151
  }
6850
- function findProjectConfigPath(startDir = process.cwd()) {
7152
+ function findProjectFile(relativePath, startDir = process.cwd()) {
6851
7153
  let dir = startDir;
6852
7154
  while (true) {
6853
- const candidate = path3.join(dir, PROJECT_CONFIG_RELATIVE);
7155
+ const candidate = path3.join(dir, relativePath);
6854
7156
  if (fs3.existsSync(candidate)) {
6855
7157
  return candidate;
6856
7158
  }
@@ -6862,11 +7164,18 @@ function findProjectConfigPath(startDir = process.cwd()) {
6862
7164
  }
6863
7165
  }
6864
7166
  function getProjectConfigData() {
6865
- const filePath = findProjectConfigPath();
6866
- if (!filePath) {
7167
+ const filePath2 = findProjectFile(PROJECT_CONFIG_RELATIVE);
7168
+ if (!filePath2) {
7169
+ return null;
7170
+ }
7171
+ return readJsonFile(filePath2);
7172
+ }
7173
+ function getProjectCredentialsData() {
7174
+ const filePath2 = findProjectFile(PROJECT_CREDENTIALS_RELATIVE);
7175
+ if (!filePath2) {
6867
7176
  return null;
6868
7177
  }
6869
- return readJsonFile(filePath);
7178
+ return readJsonFile(filePath2) ?? {};
6870
7179
  }
6871
7180
  function getConfigData() {
6872
7181
  return readJsonFile(GLOBAL_CONFIG_FILE) ?? {};
@@ -6889,6 +7198,10 @@ function getApiKey() {
6889
7198
  if (process.env.BITFAB_API_KEY) {
6890
7199
  return process.env.BITFAB_API_KEY;
6891
7200
  }
7201
+ const projectCreds = getProjectCredentialsData();
7202
+ if (projectCreds !== null) {
7203
+ return typeof projectCreds.apiKey === "string" ? projectCreds.apiKey : null;
7204
+ }
6892
7205
  const creds = getCredentialsData();
6893
7206
  return typeof creds.apiKey === "string" ? creds.apiKey : null;
6894
7207
  }
@@ -6929,6 +7242,12 @@ function getConfig() {
6929
7242
  };
6930
7243
  }
6931
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
+ }
6932
7251
  fs3.mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true });
6933
7252
  fs3.writeFileSync(GLOBAL_CREDENTIALS_FILE, `${JSON.stringify({ apiKey }, null, 2)}
6934
7253
  `);
@@ -6984,36 +7303,62 @@ function buildBitfabRequestHeaders(apiKey, pluginVersion, platform2) {
6984
7303
  };
6985
7304
  }
6986
7305
 
6987
- // ../bitfab-plugin-lib/dist/commands/login.js
7306
+ // ../bitfab-plugin-lib/dist/activeStudioSession.js
6988
7307
  import crypto3 from "crypto";
6989
-
6990
- // ../bitfab-plugin-lib/dist/analytics.js
6991
- async function reportHandoff(apiKey, pluginVersion, platform2, body) {
6992
- const config2 = getConfig();
6993
- 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() {
6994
7331
  try {
6995
- const response = await fetch(`${config2.serviceUrl}/api/plugin/analytics/handoff`, {
6996
- method: "POST",
6997
- headers,
6998
- body: JSON.stringify(body)
6999
- });
7000
- if (config2.debug && !response.ok) {
7001
- console.error(`reportHandoff: server returned ${response.status} for ${body.flow}`);
7332
+ const current = readActiveStudioSessionRaw();
7333
+ if (current && current.pid !== process.pid) {
7334
+ return;
7002
7335
  }
7003
- } catch (err) {
7004
- if (config2.debug) {
7005
- const message = err instanceof Error ? err.message : String(err);
7006
- 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;
7007
7346
  }
7347
+ return null;
7348
+ } catch {
7349
+ return null;
7008
7350
  }
7009
7351
  }
7352
+ function readActiveStudioSession() {
7353
+ return readActiveStudioSessionRaw();
7354
+ }
7010
7355
 
7011
7356
  // ../bitfab-plugin-lib/dist/browser.js
7012
7357
  import { execSync, spawn } from "child_process";
7013
- import fs6 from "fs";
7014
- import os6 from "os";
7358
+ import fs7 from "fs";
7359
+ import os7 from "os";
7015
7360
  function getChromiumBrowsers() {
7016
- const platform2 = os6.platform();
7361
+ const platform2 = os7.platform();
7017
7362
  if (platform2 === "darwin") {
7018
7363
  return [
7019
7364
  {
@@ -7101,7 +7446,7 @@ function getChromiumBrowsers() {
7101
7446
  function findExistingBinary(paths) {
7102
7447
  for (const candidate of paths) {
7103
7448
  if (candidate.includes("/") || candidate.includes("\\")) {
7104
- if (fs6.existsSync(candidate)) {
7449
+ if (fs7.existsSync(candidate)) {
7105
7450
  return candidate;
7106
7451
  }
7107
7452
  } else {
@@ -7115,10 +7460,10 @@ function findExistingBinary(paths) {
7115
7460
  return null;
7116
7461
  }
7117
7462
  function getDefaultBrowserId() {
7118
- const platform2 = os6.platform();
7463
+ const platform2 = os7.platform();
7119
7464
  try {
7120
7465
  if (platform2 === "darwin") {
7121
- 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`;
7122
7467
  const json2 = execSync(`plutil -convert json -o - "${plistPath}"`, {
7123
7468
  stdio: ["ignore", "pipe", "ignore"],
7124
7469
  encoding: "utf-8",
@@ -7159,7 +7504,7 @@ function matchChromiumBrowser(defaultId, browsers) {
7159
7504
  var SCREEN_FRACTION = 0.95;
7160
7505
  function getWorkArea() {
7161
7506
  try {
7162
- const platform2 = os6.platform();
7507
+ const platform2 = os7.platform();
7163
7508
  if (platform2 === "darwin") {
7164
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])'`, {
7165
7510
  stdio: ["ignore", "pipe", "ignore"],
@@ -7228,7 +7573,7 @@ function openChromiumApp(binaryPath, url2, sizeArgs) {
7228
7573
  spawnDetached(binaryPath, [`--app=${url2}`, ...sizeArgs]);
7229
7574
  }
7230
7575
  function openOsDefault(url2) {
7231
- const platform2 = os6.platform();
7576
+ const platform2 = os7.platform();
7232
7577
  if (platform2 === "darwin") {
7233
7578
  spawnDetached("open", [url2]);
7234
7579
  } else if (platform2 === "win32") {
@@ -7279,9 +7624,61 @@ function openChromelessWindow(url2) {
7279
7624
  openOsDefault(url2);
7280
7625
  }
7281
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
+
7282
7679
  // ../bitfab-plugin-lib/dist/frontmostApp.js
7283
7680
  import { execFileSync as execFileSync2, spawn as spawn2 } from "child_process";
7284
- import os7 from "os";
7681
+ import os8 from "os";
7285
7682
  function findAncestorApp() {
7286
7683
  try {
7287
7684
  let pid = process.ppid;
@@ -7342,7 +7739,7 @@ function focusItermSession(sessionId) {
7342
7739
  spawnQuiet("osascript", ["-e", script]);
7343
7740
  }
7344
7741
  function getFrontmostApp() {
7345
- const platform2 = os7.platform();
7742
+ const platform2 = os8.platform();
7346
7743
  try {
7347
7744
  if (platform2 === "darwin") {
7348
7745
  return execFileSync2("osascript", [
@@ -7376,7 +7773,7 @@ function focusApp(identifier) {
7376
7773
  if (!identifier) {
7377
7774
  return;
7378
7775
  }
7379
- const platform2 = os7.platform();
7776
+ const platform2 = os8.platform();
7380
7777
  if (platform2 === "darwin") {
7381
7778
  spawnQuiet("osascript", [
7382
7779
  "-e",
@@ -7406,7 +7803,7 @@ function focusApp(identifier) {
7406
7803
  }
7407
7804
  }
7408
7805
  function recordFocus() {
7409
- const platform2 = os7.platform();
7806
+ const platform2 = os8.platform();
7410
7807
  if (platform2 === "linux") {
7411
7808
  const windowId = process.env.WINDOWID;
7412
7809
  if (windowId) {
@@ -7424,9 +7821,151 @@ function recordFocus() {
7424
7821
  return () => focusApp(frontmost);
7425
7822
  }
7426
7823
 
7427
- // ../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
7428
7828
  var LOGIN_TIMEOUT_MS = 10 * 60 * 1e3;
7429
- 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;
7430
7969
  async function verifyToken(serviceUrl, token) {
7431
7970
  try {
7432
7971
  const res = await fetch(`${serviceUrl}/api/plugin/whoami`, {
@@ -7440,58 +7979,50 @@ async function verifyToken(serviceUrl, token) {
7440
7979
  return null;
7441
7980
  }
7442
7981
  }
7443
- async function pollLoginStatus(serviceUrl, loginId, signal) {
7444
- while (!signal.aborted) {
7445
- try {
7446
- const url2 = `${serviceUrl}/api/studio/login-status?loginId=${encodeURIComponent(loginId)}`;
7447
- const res = await fetch(url2, { signal });
7448
- if (res.ok) {
7449
- const body = await res.json();
7450
- if (body.status === "complete" && body.token) {
7451
- return body.token;
7452
- }
7453
- }
7454
- } catch {
7455
- if (signal.aborted) {
7456
- throw new Error("Login polling aborted");
7457
- }
7458
- }
7459
- await new Promise((resolve) => {
7460
- const timer = setTimeout(resolve, POLL_INTERVAL_MS);
7461
- signal.addEventListener("abort", () => {
7462
- clearTimeout(timer);
7463
- resolve();
7464
- }, { once: true });
7465
- });
7466
- }
7467
- throw new Error("Login polling aborted");
7468
- }
7469
7982
  async function runLogin(platform2, pluginVersion, options) {
7470
7983
  const exitOnComplete = options?.exitOnComplete ?? true;
7471
7984
  const config2 = getConfig();
7472
7985
  const restoreFocus = recordFocus();
7473
- const loginId = crypto3.randomUUID();
7474
- const redirectUrl = `/studio/close?pluginLogin=true&loginId=${loginId}`;
7475
- 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)}`;
7476
7989
  console.log("\nOpening Studio to sign in...");
7477
- console.log(` ${browserUrl}`);
7478
- console.log("");
7479
- console.log("(If the browser didn't open, visit the URL above manually.)");
7480
- 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
+ }
7481
8008
  const abortController = new AbortController();
7482
8009
  let loginTimer;
7483
8010
  const timeoutPromise = new Promise((_resolve, reject) => {
7484
8011
  loginTimer = setTimeout(() => {
7485
8012
  abortController.abort();
7486
8013
  reject(new Error("Authentication timed out after 10 minutes. Run login again."));
7487
- }, LOGIN_TIMEOUT_MS);
8014
+ }, LOGIN_TIMEOUT_MS2);
7488
8015
  });
7489
8016
  const keepaliveTimer = setInterval(() => {
7490
8017
  process.stdout.write("");
7491
8018
  }, 3e4);
7492
8019
  try {
7493
8020
  const token = await Promise.race([
7494
- pollLoginStatus(config2.serviceUrl, loginId, abortController.signal),
8021
+ pollLoginEvents({
8022
+ serviceUrl: config2.serviceUrl,
8023
+ sessionId,
8024
+ abortSignal: abortController.signal
8025
+ }),
7495
8026
  timeoutPromise
7496
8027
  ]);
7497
8028
  if (loginTimer) {
@@ -7499,11 +8030,6 @@ async function runLogin(platform2, pluginVersion, options) {
7499
8030
  }
7500
8031
  clearInterval(keepaliveTimer);
7501
8032
  saveCredentials(token);
7502
- const who = await verifyToken(config2.serviceUrl, token);
7503
- const greeting = who?.user.email ? `Logged in as ${who.user.email}.` : "Authentication successful.";
7504
- console.log(`
7505
- ${greeting}`);
7506
- console.log("Bitfab MCP tools are now active. (via studio)");
7507
8033
  await reportHandoff(token, pluginVersion, platform2, {
7508
8034
  flow: "auth.login",
7509
8035
  wonBy: "studio",
@@ -7511,6 +8037,11 @@ ${greeting}`);
7511
8037
  ticketAvailable: false
7512
8038
  });
7513
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)");
7514
8045
  if (exitOnComplete) {
7515
8046
  process.exit(0);
7516
8047
  }
@@ -7540,19 +8071,8 @@ function runLogout() {
7540
8071
  console.log("Logged out of Bitfab.");
7541
8072
  }
7542
8073
 
7543
- // ../bitfab-plugin-lib/dist/commands/openStudio.js
7544
- import crypto5 from "crypto";
7545
-
7546
- // ../bitfab-plugin-lib/dist/activeStudioSession.js
7547
- import crypto4 from "crypto";
7548
- import fs7 from "fs";
7549
- import os8 from "os";
7550
- import path5 from "path";
7551
-
7552
- // ../bitfab-plugin-lib/dist/withStudioSession.js
7553
- import crypto6 from "crypto";
7554
-
7555
8074
  // ../bitfab-plugin-lib/dist/commands/openTracePlan.js
8075
+ import crypto6 from "crypto";
7556
8076
  var OPEN_TRACE_PLAN_TIMEOUT_MS = 30 * 60 * 1e3;
7557
8077
 
7558
8078
  // ../bitfab-plugin-lib/dist/commands/persistReplayLabels.js
@@ -21352,6 +21872,12 @@ var inputFileSchema = external_exports.object({
21352
21872
  verdicts: external_exports.array(external_exports.unknown()).min(1)
21353
21873
  });
21354
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
+
21355
21881
  // ../bitfab-plugin-lib/dist/activePreviewSession.js
21356
21882
  import fs8 from "fs";
21357
21883
  import os9 from "os";
@@ -27312,6 +27838,47 @@ function runLogoutCommand() {
27312
27838
  runLogout();
27313
27839
  }
27314
27840
 
27841
+ // src/updateCheck.ts
27842
+ var NPM_REGISTRY_URL = "https://registry.npmjs.org/bitfab-cli/latest";
27843
+ var TIMEOUT_MS = 3e3;
27844
+ function isNewer(latest, current) {
27845
+ const parse3 = (v) => v.split(".").map(Number);
27846
+ const [lMaj = 0, lMin = 0, lPat = 0] = parse3(latest);
27847
+ const [cMaj = 0, cMin = 0, cPat = 0] = parse3(current);
27848
+ if (lMaj !== cMaj) {
27849
+ return lMaj > cMaj;
27850
+ }
27851
+ if (lMin !== cMin) {
27852
+ return lMin > cMin;
27853
+ }
27854
+ return lPat > cPat;
27855
+ }
27856
+ function startUpdateCheck() {
27857
+ const controller = new AbortController();
27858
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
27859
+ const current = getVersion();
27860
+ const done = fetch(NPM_REGISTRY_URL, { signal: controller.signal }).then(
27861
+ (res) => res.ok ? res.json() : null
27862
+ ).then((data) => {
27863
+ const latest = data?.version;
27864
+ if (latest && isNewer(latest, current)) {
27865
+ process.stderr.write(
27866
+ `
27867
+ Update available: ${current} -> ${latest}
27868
+ Run \`npx bitfab-cli@latest\` to get the newest version.
27869
+
27870
+ `
27871
+ );
27872
+ }
27873
+ }).catch(() => {
27874
+ }).finally(() => clearTimeout(timer));
27875
+ return () => {
27876
+ controller.abort();
27877
+ done.catch(() => {
27878
+ });
27879
+ };
27880
+ }
27881
+
27315
27882
  // src/index.ts
27316
27883
  var HELP_TEXT = `Usage: bitfab <command> [options]
27317
27884
 
@@ -27337,7 +27904,7 @@ Examples:
27337
27904
  bitfab setup --skip-permissions Setup without permission prompts
27338
27905
  bitfab assistant --skip-permissions Run assistant autonomously
27339
27906
  `;
27340
- function parseArgs(argv) {
27907
+ function parseArgs2(argv) {
27341
27908
  const [command, ...tail] = argv;
27342
27909
  let editor;
27343
27910
  let skipPermissions;
@@ -27364,44 +27931,54 @@ function parseArgs(argv) {
27364
27931
  return { command, editor, skipPermissions, rest };
27365
27932
  }
27366
27933
  async function main() {
27367
- const { command, editor, skipPermissions, rest } = parseArgs(
27934
+ const { command, editor, skipPermissions, rest } = parseArgs2(
27368
27935
  process.argv.slice(2)
27369
27936
  );
27937
+ const abortUpdateCheck = startUpdateCheck();
27370
27938
  if (!command || command === "help" || command === "--help" || command === "-h") {
27371
27939
  process.stdout.write(HELP_TEXT);
27940
+ abortUpdateCheck();
27372
27941
  return;
27373
27942
  }
27374
27943
  if (command === "init") {
27375
27944
  await runInit({ editor, skipPermissions });
27945
+ abortUpdateCheck();
27376
27946
  return;
27377
27947
  }
27378
27948
  if (command === "plugin-install") {
27379
27949
  await runInstall({ editor, skipPermissions });
27950
+ abortUpdateCheck();
27380
27951
  return;
27381
27952
  }
27382
27953
  if (command === "login") {
27383
27954
  await runLoginCommand();
27955
+ abortUpdateCheck();
27384
27956
  return;
27385
27957
  }
27386
27958
  if (command === "logout") {
27387
27959
  runLogoutCommand();
27960
+ abortUpdateCheck();
27388
27961
  return;
27389
27962
  }
27390
27963
  if (command === "setup") {
27391
27964
  await runSetup({ editor, skipPermissions });
27965
+ abortUpdateCheck();
27392
27966
  return;
27393
27967
  }
27394
27968
  if (command === "assistant") {
27395
27969
  await runAssistant({ editor, skipPermissions }, rest);
27970
+ abortUpdateCheck();
27396
27971
  return;
27397
27972
  }
27398
27973
  if (command === "update") {
27399
27974
  await runUpdate2({ editor, skipPermissions }, rest);
27975
+ abortUpdateCheck();
27400
27976
  return;
27401
27977
  }
27402
27978
  process.stderr.write(`Unknown command: ${command}
27403
27979
 
27404
27980
  ${HELP_TEXT}`);
27981
+ abortUpdateCheck();
27405
27982
  process.exit(1);
27406
27983
  }
27407
27984
  main().catch((err) => {
@@ -27410,5 +27987,5 @@ main().catch((err) => {
27410
27987
  process.exit(1);
27411
27988
  });
27412
27989
  export {
27413
- parseArgs
27990
+ parseArgs2 as parseArgs
27414
27991
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bitfab-cli",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
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",