bitfab-cli 0.2.186 → 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.
- package/dist/index.js +374 -58
- 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;
|
|
@@ -9566,6 +9572,40 @@ function updateActiveStudioSession(updates) {
|
|
|
9566
9572
|
writeStateFile(sessionFilePath(), updated);
|
|
9567
9573
|
return updated;
|
|
9568
9574
|
}
|
|
9575
|
+
function clearActiveStudioSession(sessionId, opts = {}) {
|
|
9576
|
+
try {
|
|
9577
|
+
const current = readStateFile(sessionFilePath());
|
|
9578
|
+
if (!current) {
|
|
9579
|
+
return clearLegacySession(sessionId, opts);
|
|
9580
|
+
}
|
|
9581
|
+
if (current.sessionId !== sessionId) {
|
|
9582
|
+
return false;
|
|
9583
|
+
}
|
|
9584
|
+
if (!opts.force && current.pollerPid != null && current.pollerPid !== process.pid && isProcessAlive(current.pollerPid)) {
|
|
9585
|
+
return false;
|
|
9586
|
+
}
|
|
9587
|
+
fs7.unlinkSync(sessionFilePath());
|
|
9588
|
+
return true;
|
|
9589
|
+
} catch {
|
|
9590
|
+
return false;
|
|
9591
|
+
}
|
|
9592
|
+
}
|
|
9593
|
+
function clearLegacySession(sessionId, opts = {}) {
|
|
9594
|
+
try {
|
|
9595
|
+
const raw = fs7.readFileSync(legacyFilePath(), "utf-8");
|
|
9596
|
+
const parsed = JSON.parse(raw);
|
|
9597
|
+
if (parsed.sessionId !== sessionId) {
|
|
9598
|
+
return false;
|
|
9599
|
+
}
|
|
9600
|
+
if (!opts.force && typeof parsed.pid === "number" && parsed.pid !== process.pid && isProcessAlive(parsed.pid)) {
|
|
9601
|
+
return false;
|
|
9602
|
+
}
|
|
9603
|
+
fs7.unlinkSync(legacyFilePath());
|
|
9604
|
+
return true;
|
|
9605
|
+
} catch {
|
|
9606
|
+
return false;
|
|
9607
|
+
}
|
|
9608
|
+
}
|
|
9569
9609
|
function isProcessAlive(pid) {
|
|
9570
9610
|
try {
|
|
9571
9611
|
process.kill(pid, 0);
|
|
@@ -9948,6 +9988,9 @@ function closeStudioWindowsBySession(sessionId) {
|
|
|
9948
9988
|
}
|
|
9949
9989
|
}
|
|
9950
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
|
+
}
|
|
9951
9994
|
function openChromelessWindow(url2) {
|
|
9952
9995
|
if (process.env.VITEST || process.env.NODE_ENV === "test") {
|
|
9953
9996
|
throw new Error("openChromelessWindow called in a test environment without being mocked");
|
|
@@ -9958,12 +10001,16 @@ function openChromelessWindow(url2) {
|
|
|
9958
10001
|
}
|
|
9959
10002
|
if (recentOpens.length >= WINDOW_OPEN_RATE_LIMIT) {
|
|
9960
10003
|
console.error(`[browser] rate limit: ${recentOpens.length} windows opened in the last ${WINDOW_OPEN_RATE_WINDOW_MS / 1e3}s, refusing to open another`);
|
|
9961
|
-
return
|
|
10004
|
+
return {
|
|
10005
|
+
pid: null,
|
|
10006
|
+
launched: false,
|
|
10007
|
+
method: "none",
|
|
10008
|
+
reason: "rate-limited"
|
|
10009
|
+
};
|
|
9962
10010
|
}
|
|
9963
10011
|
recentOpens.push(now);
|
|
9964
10012
|
if (shouldDisableChromelessWindows()) {
|
|
9965
|
-
openOsDefault(url2);
|
|
9966
|
-
return null;
|
|
10013
|
+
return osDefaultResult(openOsDefault(url2));
|
|
9967
10014
|
}
|
|
9968
10015
|
const browsers = getChromiumBrowsers();
|
|
9969
10016
|
const defaultId = getDefaultBrowserId();
|
|
@@ -9974,29 +10021,31 @@ function openChromelessWindow(url2) {
|
|
|
9974
10021
|
if (binary) {
|
|
9975
10022
|
const pid = openChromiumApp(binary, url2, sizeArgs);
|
|
9976
10023
|
if (pid !== null) {
|
|
9977
|
-
return pid;
|
|
10024
|
+
return { pid, launched: true, method: "app" };
|
|
9978
10025
|
}
|
|
9979
|
-
openOsDefault(url2);
|
|
9980
|
-
return null;
|
|
10026
|
+
return osDefaultResult(openOsDefault(url2));
|
|
9981
10027
|
}
|
|
9982
10028
|
}
|
|
9983
10029
|
if (defaultId !== null && defaultChromium === null) {
|
|
9984
|
-
openOsDefault(url2);
|
|
9985
|
-
return null;
|
|
10030
|
+
return osDefaultResult(openOsDefault(url2));
|
|
9986
10031
|
}
|
|
9987
10032
|
for (const browser of browsers) {
|
|
9988
10033
|
const binary = findExistingBinary(browser.binaryPaths);
|
|
9989
10034
|
if (binary) {
|
|
9990
10035
|
const pid = openChromiumApp(binary, url2, sizeArgs);
|
|
9991
10036
|
if (pid !== null) {
|
|
9992
|
-
return pid;
|
|
10037
|
+
return { pid, launched: true, method: "app" };
|
|
9993
10038
|
}
|
|
9994
|
-
openOsDefault(url2);
|
|
9995
|
-
return null;
|
|
10039
|
+
return osDefaultResult(openOsDefault(url2));
|
|
9996
10040
|
}
|
|
9997
10041
|
}
|
|
9998
|
-
openOsDefault(url2);
|
|
9999
|
-
|
|
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
|
+
`);
|
|
10000
10049
|
}
|
|
10001
10050
|
|
|
10002
10051
|
// ../bitfab-plugin-lib/dist/parseArgs.js
|
|
@@ -10112,14 +10161,19 @@ async function createStudioSession({ serviceUrl, apiKey, sessionId, initialPath
|
|
|
10112
10161
|
sessionId,
|
|
10113
10162
|
extraHeaders: clientHeaders
|
|
10114
10163
|
});
|
|
10115
|
-
const
|
|
10164
|
+
const open = openChromelessWindow(target.url);
|
|
10116
10165
|
writeActiveStudioSession({
|
|
10117
10166
|
sessionId,
|
|
10118
10167
|
serviceUrl,
|
|
10119
|
-
windowPid,
|
|
10168
|
+
windowPid: open.pid,
|
|
10120
10169
|
currentPath: target.path
|
|
10121
10170
|
});
|
|
10122
|
-
return {
|
|
10171
|
+
return {
|
|
10172
|
+
sessionId,
|
|
10173
|
+
serviceUrl,
|
|
10174
|
+
launched: open.launched,
|
|
10175
|
+
launchReason: open.reason
|
|
10176
|
+
};
|
|
10123
10177
|
}
|
|
10124
10178
|
|
|
10125
10179
|
// ../bitfab-plugin-lib/dist/daemon/client.js
|
|
@@ -10163,6 +10217,8 @@ var BROWSER_HEALTH_INTERVAL_MS = 10 * 1e3;
|
|
|
10163
10217
|
var ENSURE_POLL_INTERVAL_MS = 200;
|
|
10164
10218
|
var ENSURE_TIMEOUT_MS = 5e3;
|
|
10165
10219
|
var PING_TIMEOUT_MS = 2e3;
|
|
10220
|
+
var OPEN_TIMEOUT_MS = 1e4;
|
|
10221
|
+
var COMMAND_TIMEOUT_MS = 3e4;
|
|
10166
10222
|
function serialize(msg) {
|
|
10167
10223
|
return `${JSON.stringify(msg)}
|
|
10168
10224
|
`;
|
|
@@ -10336,7 +10392,7 @@ async function waitForLockHolderDaemon(socketPath, timeoutMs, localBuild = local
|
|
|
10336
10392
|
const deadline = Date.now() + timeoutMs;
|
|
10337
10393
|
while (Date.now() < deadline) {
|
|
10338
10394
|
const ping = await pingSocket(socketPath);
|
|
10339
|
-
if (ping.alive && (localBuild
|
|
10395
|
+
if (ping.alive && buildMatches(localBuild, ping.build)) {
|
|
10340
10396
|
return;
|
|
10341
10397
|
}
|
|
10342
10398
|
await new Promise((r) => setTimeout(r, ENSURE_POLL_INTERVAL_MS));
|
|
@@ -10347,6 +10403,12 @@ async function waitForLockHolderDaemon(socketPath, timeoutMs, localBuild = local
|
|
|
10347
10403
|
}
|
|
10348
10404
|
throw new Error(`daemon did not become ready within ${timeoutMs}ms`);
|
|
10349
10405
|
}
|
|
10406
|
+
function buildMatches(localBuild, remoteBuild) {
|
|
10407
|
+
if (localBuild === "unknown") {
|
|
10408
|
+
return true;
|
|
10409
|
+
}
|
|
10410
|
+
return remoteBuild === localBuild;
|
|
10411
|
+
}
|
|
10350
10412
|
function localBuildFingerprint() {
|
|
10351
10413
|
try {
|
|
10352
10414
|
const thisDir = path9.dirname(fileURLToPath3(import.meta.url));
|
|
@@ -10498,11 +10560,10 @@ async function ensureDaemon(paths = SINGLETON_DAEMON_PATHS) {
|
|
|
10498
10560
|
const ping = await pingSocket(socketPath);
|
|
10499
10561
|
if (ping.alive) {
|
|
10500
10562
|
const localBuild = localBuildFingerprint();
|
|
10501
|
-
if (
|
|
10502
|
-
console.error(`[daemon] build mismatch (running: ${ping.build}, local: ${localBuild}), restarting`);
|
|
10503
|
-
} else {
|
|
10563
|
+
if (buildMatches(localBuild, ping.build)) {
|
|
10504
10564
|
return;
|
|
10505
10565
|
}
|
|
10566
|
+
console.error(`[daemon] build mismatch (running: ${ping.build ?? "unreported"}, local: ${localBuild}), restarting`);
|
|
10506
10567
|
}
|
|
10507
10568
|
}
|
|
10508
10569
|
if (!acquireLock(socketPath)) {
|
|
@@ -10527,9 +10588,19 @@ var DaemonClient = class {
|
|
|
10527
10588
|
disconnectHandler = null;
|
|
10528
10589
|
paths;
|
|
10529
10590
|
socketPath;
|
|
10530
|
-
|
|
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 = {}) {
|
|
10531
10600
|
this.paths = paths;
|
|
10532
10601
|
this.socketPath = paths.socketPath;
|
|
10602
|
+
this.openTimeoutMs = timeouts.openTimeoutMs ?? OPEN_TIMEOUT_MS;
|
|
10603
|
+
this.commandTimeoutMs = timeouts.commandTimeoutMs ?? COMMAND_TIMEOUT_MS;
|
|
10533
10604
|
}
|
|
10534
10605
|
async connect() {
|
|
10535
10606
|
await ensureDaemon(this.paths);
|
|
@@ -10579,28 +10650,97 @@ var DaemonClient = class {
|
|
|
10579
10650
|
idx = this.buffer.indexOf("\n");
|
|
10580
10651
|
}
|
|
10581
10652
|
}
|
|
10582
|
-
|
|
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 = {}) {
|
|
10583
10659
|
if (!this.socket) {
|
|
10584
10660
|
return Promise.reject(new Error("not connected"));
|
|
10585
10661
|
}
|
|
10662
|
+
const timeoutMs = opts.timeoutMs ?? this.commandTimeoutMs;
|
|
10663
|
+
const cmdName = cmd.cmd ?? "command";
|
|
10586
10664
|
return new Promise((resolve2, reject) => {
|
|
10587
|
-
|
|
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);
|
|
10588
10694
|
try {
|
|
10589
10695
|
this.socket.write(serialize(cmd));
|
|
10590
10696
|
} catch (err) {
|
|
10591
|
-
|
|
10697
|
+
settled = true;
|
|
10698
|
+
clearTimeout(timer);
|
|
10699
|
+
const idx = this.responseQueue.indexOf(resolver);
|
|
10700
|
+
if (idx !== -1) {
|
|
10701
|
+
this.responseQueue.splice(idx, 1);
|
|
10702
|
+
}
|
|
10592
10703
|
reject(err);
|
|
10593
10704
|
}
|
|
10594
10705
|
});
|
|
10595
10706
|
}
|
|
10596
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) {
|
|
10597
10721
|
const cmd = { cmd: "open", ...opts };
|
|
10598
|
-
const res = await this.sendCommand(cmd
|
|
10722
|
+
const res = await this.sendCommand(cmd, {
|
|
10723
|
+
timeoutMs: this.openTimeoutMs,
|
|
10724
|
+
onTimeout: () => new DaemonOpenTimeoutError()
|
|
10725
|
+
});
|
|
10599
10726
|
if (!res.ok) {
|
|
10600
10727
|
throw new Error(res.error);
|
|
10601
10728
|
}
|
|
10602
10729
|
return res;
|
|
10603
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
|
+
}
|
|
10604
10744
|
async navigate(key, navPath) {
|
|
10605
10745
|
const res = await this.sendCommand({ cmd: "navigate", key, path: navPath });
|
|
10606
10746
|
if (!res.ok) {
|
|
@@ -10780,11 +10920,11 @@ var DirectChannel = class {
|
|
|
10780
10920
|
path: initialPath,
|
|
10781
10921
|
sessionId: sessionId2
|
|
10782
10922
|
});
|
|
10783
|
-
const
|
|
10923
|
+
const open = openChromelessWindow(url2);
|
|
10784
10924
|
writeActiveStudioSession({
|
|
10785
10925
|
sessionId: sessionId2,
|
|
10786
10926
|
serviceUrl: this.serviceUrl,
|
|
10787
|
-
windowPid,
|
|
10927
|
+
windowPid: open.pid,
|
|
10788
10928
|
currentPath: initialPath,
|
|
10789
10929
|
// A keyless login window is pre-auth: writeActiveStudioSession defaults
|
|
10790
10930
|
// authenticated to true, but this window has no token yet. Record it as
|
|
@@ -10793,7 +10933,12 @@ var DirectChannel = class {
|
|
|
10793
10933
|
// (openStudioTo) is what later excludes it from reuse.
|
|
10794
10934
|
authenticated: false
|
|
10795
10935
|
});
|
|
10796
|
-
return {
|
|
10936
|
+
return {
|
|
10937
|
+
sessionId: sessionId2,
|
|
10938
|
+
opened: true,
|
|
10939
|
+
launched: open.launched,
|
|
10940
|
+
launchReason: open.reason
|
|
10941
|
+
};
|
|
10797
10942
|
}
|
|
10798
10943
|
const existing = readActiveStudioSession();
|
|
10799
10944
|
if (existing && existing.windowState !== "closed") {
|
|
@@ -10817,7 +10962,12 @@ var DirectChannel = class {
|
|
|
10817
10962
|
initialPath: path21,
|
|
10818
10963
|
clientHeaders: opts?.clientHeaders
|
|
10819
10964
|
});
|
|
10820
|
-
return {
|
|
10965
|
+
return {
|
|
10966
|
+
sessionId: session.sessionId,
|
|
10967
|
+
opened: true,
|
|
10968
|
+
launched: session.launched,
|
|
10969
|
+
launchReason: session.launchReason
|
|
10970
|
+
};
|
|
10821
10971
|
}
|
|
10822
10972
|
async close(sessionId, message) {
|
|
10823
10973
|
await closeStudio({ serviceUrl: this.serviceUrl, apiKey: this.apiKey, sessionId }, message);
|
|
@@ -10899,7 +11049,12 @@ var DaemonChannel = class _DaemonChannel {
|
|
|
10899
11049
|
}
|
|
10900
11050
|
return { sessionId: openRes.sessionId, opened: false };
|
|
10901
11051
|
}
|
|
10902
|
-
return {
|
|
11052
|
+
return {
|
|
11053
|
+
sessionId: openRes.sessionId,
|
|
11054
|
+
opened: true,
|
|
11055
|
+
launched: openRes.launched,
|
|
11056
|
+
launchReason: openRes.launchReason
|
|
11057
|
+
};
|
|
10903
11058
|
}
|
|
10904
11059
|
async close(sessionId, message) {
|
|
10905
11060
|
await Promise.all([
|
|
@@ -11003,6 +11158,48 @@ async function resolveChannel(apiKey, serviceUrl) {
|
|
|
11003
11158
|
return new DirectChannel(apiKey, serviceUrl);
|
|
11004
11159
|
}
|
|
11005
11160
|
|
|
11161
|
+
// ../bitfab-plugin-lib/dist/studioTeardown.js
|
|
11162
|
+
function killStudioWindow(sessionId) {
|
|
11163
|
+
const record2 = readActiveStudioSession();
|
|
11164
|
+
if (record2?.sessionId !== sessionId || record2.windowPid == null) {
|
|
11165
|
+
return;
|
|
11166
|
+
}
|
|
11167
|
+
try {
|
|
11168
|
+
process.kill(record2.windowPid, "SIGTERM");
|
|
11169
|
+
} catch {
|
|
11170
|
+
}
|
|
11171
|
+
}
|
|
11172
|
+
function killPollerProcess(sessionId) {
|
|
11173
|
+
const record2 = readActiveStudioSession();
|
|
11174
|
+
if (record2?.sessionId !== sessionId || record2.pollerPid == null || record2.pollerPid === process.pid) {
|
|
11175
|
+
return;
|
|
11176
|
+
}
|
|
11177
|
+
try {
|
|
11178
|
+
process.kill(record2.pollerPid, "SIGTERM");
|
|
11179
|
+
} catch {
|
|
11180
|
+
}
|
|
11181
|
+
}
|
|
11182
|
+
|
|
11183
|
+
// ../bitfab-plugin-lib/dist/commands/clearStudioSession.js
|
|
11184
|
+
async function clearStudioSessionById(sessionId, record2) {
|
|
11185
|
+
killStudioWindow(sessionId);
|
|
11186
|
+
const alreadyClosed = record2?.sessionId === sessionId && record2.windowState === "closed";
|
|
11187
|
+
if (!alreadyClosed) {
|
|
11188
|
+
closeStudioWindowsBySession(sessionId);
|
|
11189
|
+
}
|
|
11190
|
+
killPollerProcess(sessionId);
|
|
11191
|
+
try {
|
|
11192
|
+
const channel = await resolveChannel("", record2?.serviceUrl ?? "");
|
|
11193
|
+
try {
|
|
11194
|
+
await channel.clearSession(sessionId);
|
|
11195
|
+
} finally {
|
|
11196
|
+
channel.destroy();
|
|
11197
|
+
}
|
|
11198
|
+
} catch {
|
|
11199
|
+
}
|
|
11200
|
+
return clearActiveStudioSession(sessionId, { force: true });
|
|
11201
|
+
}
|
|
11202
|
+
|
|
11006
11203
|
// ../bitfab-plugin-lib/dist/replayCapabilities.js
|
|
11007
11204
|
var semver = __toESM(require_semver2(), 1);
|
|
11008
11205
|
|
|
@@ -11210,6 +11407,20 @@ function applyFocusTarget(target) {
|
|
|
11210
11407
|
}
|
|
11211
11408
|
}
|
|
11212
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
|
+
|
|
11213
11424
|
// ../bitfab-plugin-lib/dist/commands/openStudioTo.js
|
|
11214
11425
|
function lineToAgentEvent(line) {
|
|
11215
11426
|
const name = typeof line.event === "string" ? line.event : "";
|
|
@@ -11219,17 +11430,31 @@ function lineToAgentEvent(line) {
|
|
|
11219
11430
|
var WINDOW_CLOSE_GRACE_PERIOD_MS = 1e4;
|
|
11220
11431
|
var LOGIN_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
11221
11432
|
function formatStudioUrlMessage(url2) {
|
|
11222
|
-
return `Studio
|
|
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
|
+
}
|
|
11223
11440
|
}
|
|
11224
|
-
function createEventSubscription(channel, sessionId, onEvent, restoreFocus) {
|
|
11441
|
+
function createEventSubscription(channel, sessionId, onEvent, restoreFocus, freshOpenGraceMs = null) {
|
|
11225
11442
|
let graceTimer = null;
|
|
11443
|
+
let connectWatchdog = null;
|
|
11226
11444
|
let endedNotified = false;
|
|
11445
|
+
const ownsPointer = !channel.isPersistent();
|
|
11227
11446
|
const clearGraceTimer = () => {
|
|
11228
11447
|
if (graceTimer) {
|
|
11229
11448
|
clearTimeout(graceTimer);
|
|
11230
11449
|
graceTimer = null;
|
|
11231
11450
|
}
|
|
11232
11451
|
};
|
|
11452
|
+
const clearConnectWatchdog = () => {
|
|
11453
|
+
if (connectWatchdog) {
|
|
11454
|
+
clearTimeout(connectWatchdog);
|
|
11455
|
+
connectWatchdog = null;
|
|
11456
|
+
}
|
|
11457
|
+
};
|
|
11233
11458
|
const notifyEnded = (baseEvent) => {
|
|
11234
11459
|
if (!endedNotified) {
|
|
11235
11460
|
endedNotified = true;
|
|
@@ -11237,14 +11462,20 @@ function createEventSubscription(channel, sessionId, onEvent, restoreFocus) {
|
|
|
11237
11462
|
onEvent({ ...baseEvent, type: "studio:session-ended" });
|
|
11238
11463
|
}
|
|
11239
11464
|
clearGraceTimer();
|
|
11465
|
+
clearConnectWatchdog();
|
|
11240
11466
|
sub.abort();
|
|
11241
11467
|
};
|
|
11242
11468
|
const sub = channel.subscribe(sessionId, (event) => {
|
|
11469
|
+
if (!event.type.startsWith("agent:")) {
|
|
11470
|
+
clearConnectWatchdog();
|
|
11471
|
+
}
|
|
11243
11472
|
if (event.type === "studio:window-closed") {
|
|
11244
11473
|
clearGraceTimer();
|
|
11245
11474
|
graceTimer = setTimeout(() => {
|
|
11246
11475
|
graceTimer = null;
|
|
11247
|
-
|
|
11476
|
+
if (ownsPointer) {
|
|
11477
|
+
updateActiveStudioSession({ windowState: "closed" });
|
|
11478
|
+
}
|
|
11248
11479
|
notifyEnded(event);
|
|
11249
11480
|
}, WINDOW_CLOSE_GRACE_PERIOD_MS);
|
|
11250
11481
|
return;
|
|
@@ -11277,9 +11508,36 @@ function createEventSubscription(channel, sessionId, onEvent, restoreFocus) {
|
|
|
11277
11508
|
}, (err) => {
|
|
11278
11509
|
console.error(`studio event stream: ${err.message}`);
|
|
11279
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
|
+
}
|
|
11280
11537
|
return {
|
|
11281
11538
|
abort: () => {
|
|
11282
11539
|
clearGraceTimer();
|
|
11540
|
+
clearConnectWatchdog();
|
|
11283
11541
|
sub.abort();
|
|
11284
11542
|
},
|
|
11285
11543
|
done: sub.done
|
|
@@ -11293,7 +11551,19 @@ async function openStudioTo(path21, opts = {}) {
|
|
|
11293
11551
|
const focusTarget = captureFocusTarget();
|
|
11294
11552
|
const restoreFocus = () => applyFocusTarget(focusTarget);
|
|
11295
11553
|
const channel = await resolveChannel(apiKey ?? "", serviceUrl);
|
|
11296
|
-
const
|
|
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) => {
|
|
11297
11567
|
const persistent = channel.isPersistent();
|
|
11298
11568
|
const base = {
|
|
11299
11569
|
sessionId: sessionId2,
|
|
@@ -11304,6 +11574,7 @@ async function openStudioTo(path21, opts = {}) {
|
|
|
11304
11574
|
url: url3
|
|
11305
11575
|
};
|
|
11306
11576
|
const wantsEvents = opts.tail || Boolean(opts.onEvent);
|
|
11577
|
+
const freshOpenGraceMs = opened2 && !windowProven ? launched2 === false ? preAuthConnectGraceMs() : connectGraceMs() : null;
|
|
11307
11578
|
if (persistent) {
|
|
11308
11579
|
if (!wantsEvents || opts.tail) {
|
|
11309
11580
|
if (!opts.tail) {
|
|
@@ -11316,7 +11587,7 @@ async function openStudioTo(path21, opts = {}) {
|
|
|
11316
11587
|
};
|
|
11317
11588
|
}
|
|
11318
11589
|
const subscription = createEventSubscription(channel, sessionId2, opts.onEvent ?? (() => {
|
|
11319
|
-
}), restoreFocus);
|
|
11590
|
+
}), restoreFocus, freshOpenGraceMs);
|
|
11320
11591
|
return { ...base, ...subscription };
|
|
11321
11592
|
}
|
|
11322
11593
|
if (!wantsEvents) {
|
|
@@ -11334,7 +11605,7 @@ async function openStudioTo(path21, opts = {}) {
|
|
|
11334
11605
|
appendStudioEvent(key, studioEventToLogLine(event, sessionId2));
|
|
11335
11606
|
userOnEvent2(event);
|
|
11336
11607
|
};
|
|
11337
|
-
const subscription = createEventSubscription(channel, sessionId2, sink, restoreFocus);
|
|
11608
|
+
const subscription = createEventSubscription(channel, sessionId2, sink, restoreFocus, freshOpenGraceMs);
|
|
11338
11609
|
return {
|
|
11339
11610
|
...base,
|
|
11340
11611
|
abort: subscription.abort,
|
|
@@ -11378,9 +11649,11 @@ async function openStudioTo(path21, opts = {}) {
|
|
|
11378
11649
|
const sessionId2 = openResult.sessionId;
|
|
11379
11650
|
const signInUrl = `${serviceUrl}${buildSignInPath(sessionId2)}`;
|
|
11380
11651
|
if (openResult.opened) {
|
|
11381
|
-
|
|
11652
|
+
reportFreshOpen(signInUrl, openResult.launched, openResult.launchReason);
|
|
11653
|
+
} else {
|
|
11654
|
+
emit({ event: "navigated", sessionId: sessionId2, path: buildSignInPath(sessionId2) });
|
|
11382
11655
|
}
|
|
11383
|
-
|
|
11656
|
+
emit({ event: "auth-required", sessionId: sessionId2, signInUrl });
|
|
11384
11657
|
const loginApiKey = await new Promise((resolve2, reject) => {
|
|
11385
11658
|
const timer = setTimeout(() => {
|
|
11386
11659
|
sub.abort();
|
|
@@ -11404,19 +11677,30 @@ async function openStudioTo(path21, opts = {}) {
|
|
|
11404
11677
|
});
|
|
11405
11678
|
saveCredentials(loginApiKey);
|
|
11406
11679
|
updateActiveStudioSession({ authenticated: true });
|
|
11407
|
-
|
|
11680
|
+
emit({ event: "authenticated", sessionId: sessionId2 });
|
|
11408
11681
|
channel.setApiKey(loginApiKey);
|
|
11409
|
-
return finish(
|
|
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
|
+
);
|
|
11410
11692
|
}
|
|
11411
|
-
const { sessionId, opened } = await channel.openOrNavigate(path21, {
|
|
11693
|
+
const { sessionId, opened, launched, launchReason } = await channel.openOrNavigate(path21, {
|
|
11412
11694
|
clientHeaders: opts.clientHeaders,
|
|
11413
11695
|
focusTarget: focusTarget ?? void 0
|
|
11414
11696
|
});
|
|
11415
11697
|
const url2 = opened ? resolveStudioSessionTarget({ serviceUrl, path: path21, sessionId }).url : void 0;
|
|
11416
|
-
if (url2) {
|
|
11417
|
-
|
|
11698
|
+
if (opened && url2) {
|
|
11699
|
+
reportFreshOpen(url2, launched, launchReason);
|
|
11700
|
+
} else {
|
|
11701
|
+
emit({ event: "navigated", sessionId, path: path21 });
|
|
11418
11702
|
}
|
|
11419
|
-
return finish(sessionId, opened, apiKey, url2);
|
|
11703
|
+
return finish(sessionId, opened, apiKey, url2, launched);
|
|
11420
11704
|
}
|
|
11421
11705
|
|
|
11422
11706
|
// ../bitfab-plugin-lib/dist/commands/login.js
|
|
@@ -11451,20 +11735,33 @@ Already logged in as ${identity}${endpoint}. Run the login command with --force
|
|
|
11451
11735
|
return;
|
|
11452
11736
|
}
|
|
11453
11737
|
}
|
|
11738
|
+
if (force) {
|
|
11739
|
+
const record2 = readActiveStudioSession();
|
|
11740
|
+
if (record2) {
|
|
11741
|
+
await clearStudioSessionById(record2.sessionId, record2).catch(() => {
|
|
11742
|
+
});
|
|
11743
|
+
}
|
|
11744
|
+
}
|
|
11454
11745
|
try {
|
|
11455
11746
|
let printedSignInUrl = false;
|
|
11456
11747
|
const result = await openStudioTo("/studio", {
|
|
11457
11748
|
forceLogin: true,
|
|
11458
11749
|
freshWindowPath: `/studio/close?autoClose=true&message=${encodeURIComponent("Login complete")}`,
|
|
11459
|
-
|
|
11460
|
-
|
|
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") {
|
|
11461
11756
|
printedSignInUrl = true;
|
|
11462
|
-
console.log(
|
|
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));
|
|
11463
11764
|
}
|
|
11464
|
-
},
|
|
11465
|
-
onWindowOpened: (url2) => {
|
|
11466
|
-
printedSignInUrl = true;
|
|
11467
|
-
console.log(formatStudioUrlMessage(url2));
|
|
11468
11765
|
}
|
|
11469
11766
|
});
|
|
11470
11767
|
const apiKey = getConfig().apiKey;
|
|
@@ -11495,7 +11792,7 @@ ${greeting}`);
|
|
|
11495
11792
|
} catch (err) {
|
|
11496
11793
|
if (exitOnComplete) {
|
|
11497
11794
|
if (err instanceof StudioNavigationError && err.staleSessionId) {
|
|
11498
|
-
console.error("\nA Studio window is recorded as open but is not responding. Close
|
|
11795
|
+
console.error("\nA Studio window is recorded as open but is not responding. Close the Studio window and try again. To force-clear a stale session, a user can re-run with `bitfab login --force` (manual recovery - automated agents should not run it).");
|
|
11499
11796
|
} else {
|
|
11500
11797
|
console.error(`
|
|
11501
11798
|
${err.message}`);
|
|
@@ -33004,7 +33301,7 @@ var GLOBAL_HELP_TEXT = `Usage: bitfab <command> [options]
|
|
|
33004
33301
|
Commands:
|
|
33005
33302
|
init [--editor <name>] Full onboarding: plugin-install, login, and setup
|
|
33006
33303
|
plugin-install [--editor <name>] Install the Bitfab plugin in one editor
|
|
33007
|
-
login
|
|
33304
|
+
login [--force] Authenticate with Bitfab (opens browser)
|
|
33008
33305
|
logout Remove stored credentials
|
|
33009
33306
|
session-logs [status|enable|disable] Read or update session log collection
|
|
33010
33307
|
setup [--editor <name>] Launch /bitfab:setup in the editor
|
|
@@ -33021,6 +33318,7 @@ Options:
|
|
|
33021
33318
|
--no-upload-logs analyze-repo: keep session logs local (skips the prompt)
|
|
33022
33319
|
--limit <n> analyze-repo: cap how many draft trace plans to upload (default 5)
|
|
33023
33320
|
--prompt, -p <text> analyze-repo: free-text guidance steering what to focus on
|
|
33321
|
+
--force login: re-authenticate and clear any stale Studio session first
|
|
33024
33322
|
|
|
33025
33323
|
Examples:
|
|
33026
33324
|
bitfab init Full setup (detect editor, install, login, setup)
|
|
@@ -33028,6 +33326,7 @@ Examples:
|
|
|
33028
33326
|
bitfab analyze-repo Scan the repo and upload draft trace plans (headless)
|
|
33029
33327
|
bitfab analyze-repo --limit 3 Scan the repo and upload at most 3 draft trace plans
|
|
33030
33328
|
bitfab analyze-repo "focus on billing" Scan with free-text guidance on what to prioritize
|
|
33329
|
+
bitfab login --force Re-authenticate and clear a stale Studio session
|
|
33031
33330
|
bitfab session-logs enable Enable session log collection
|
|
33032
33331
|
bitfab session-logs disable Disable session log collection
|
|
33033
33332
|
bitfab assistant investigate Investigate traces
|
|
@@ -33060,15 +33359,16 @@ Examples:
|
|
|
33060
33359
|
bitfab plugin-install
|
|
33061
33360
|
bitfab plugin-install --editor cursor
|
|
33062
33361
|
`,
|
|
33063
|
-
login: `Usage: bitfab login
|
|
33362
|
+
login: `Usage: bitfab login [--force]
|
|
33064
33363
|
|
|
33065
33364
|
Authenticate with Bitfab in the browser and save credentials locally.
|
|
33066
33365
|
|
|
33067
33366
|
Options:
|
|
33068
|
-
|
|
33367
|
+
--force Re-authenticate even when already signed in, and clear any stale Studio session first
|
|
33069
33368
|
|
|
33070
33369
|
Examples:
|
|
33071
33370
|
bitfab login
|
|
33371
|
+
bitfab login --force
|
|
33072
33372
|
`,
|
|
33073
33373
|
logout: `Usage: bitfab logout
|
|
33074
33374
|
|
|
@@ -33230,7 +33530,7 @@ function wantsHelp(argv) {
|
|
|
33230
33530
|
var COMMAND_SPECS = {
|
|
33231
33531
|
init: { flags: ["editor", "skipPermissions"], unknownOption: "error" },
|
|
33232
33532
|
"plugin-install": { flags: ["editor"], unknownOption: "error" },
|
|
33233
|
-
login: { flags: [], unknownOption: "error" },
|
|
33533
|
+
login: { flags: ["force"], unknownOption: "error" },
|
|
33234
33534
|
logout: { flags: [], unknownOption: "error" },
|
|
33235
33535
|
"session-logs": { flags: [], unknownOption: "error" },
|
|
33236
33536
|
setup: { flags: ["editor", "skipPermissions"], unknownOption: "error" },
|
|
@@ -33304,6 +33604,12 @@ function applyFlag(key, token, next, values) {
|
|
|
33304
33604
|
return 1;
|
|
33305
33605
|
}
|
|
33306
33606
|
return null;
|
|
33607
|
+
case "force":
|
|
33608
|
+
if (token === "--force") {
|
|
33609
|
+
values.force = true;
|
|
33610
|
+
return 1;
|
|
33611
|
+
}
|
|
33612
|
+
return null;
|
|
33307
33613
|
}
|
|
33308
33614
|
}
|
|
33309
33615
|
function parseArgs2(argv) {
|
|
@@ -33350,6 +33656,7 @@ function parseArgs2(argv) {
|
|
|
33350
33656
|
uploadLogs: values.uploadLogs,
|
|
33351
33657
|
limit: values.limit,
|
|
33352
33658
|
prompt: values.prompt,
|
|
33659
|
+
force: values.force,
|
|
33353
33660
|
rest
|
|
33354
33661
|
};
|
|
33355
33662
|
}
|
|
@@ -33369,7 +33676,16 @@ ${GLOBAL_HELP_TEXT}`
|
|
|
33369
33676
|
process.stdout.write(helpText);
|
|
33370
33677
|
return;
|
|
33371
33678
|
}
|
|
33372
|
-
const {
|
|
33679
|
+
const {
|
|
33680
|
+
command,
|
|
33681
|
+
editor,
|
|
33682
|
+
skipPermissions,
|
|
33683
|
+
uploadLogs,
|
|
33684
|
+
limit,
|
|
33685
|
+
prompt,
|
|
33686
|
+
force,
|
|
33687
|
+
rest
|
|
33688
|
+
} = parseArgs2(argv);
|
|
33373
33689
|
const abortUpdateCheck = startUpdateCheck();
|
|
33374
33690
|
if (command === "init") {
|
|
33375
33691
|
await runInit({ editor, skipPermissions });
|
|
@@ -33382,7 +33698,7 @@ ${GLOBAL_HELP_TEXT}`
|
|
|
33382
33698
|
return;
|
|
33383
33699
|
}
|
|
33384
33700
|
if (command === "login") {
|
|
33385
|
-
await runLoginCommand();
|
|
33701
|
+
await runLoginCommand({ force });
|
|
33386
33702
|
abortUpdateCheck();
|
|
33387
33703
|
return;
|
|
33388
33704
|
}
|