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