@tiny-fish/cli 0.28.1-next.233 → 0.28.2-next.236
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/README.md +5 -3
- package/dist/commands/connect.d.ts +2 -0
- package/dist/commands/connect.js +22 -8
- package/dist/lib/connect-all.js +63 -19
- package/dist/lib/connect-auth.d.ts +3 -1
- package/dist/lib/connect-auth.js +4 -1
- package/dist/lib/connect-clients.d.ts +2 -0
- package/dist/lib/connect-clients.js +1 -0
- package/dist/lib/connect-runtime.d.ts +5 -1
- package/dist/lib/connect-runtime.js +11 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,9 +24,11 @@ them all, or use the arrow keys and space to narrow the set first; `q` cancels w
|
|
|
24
24
|
anything. Terminals without raw-key support get a numbered prompt instead.
|
|
25
25
|
|
|
26
26
|
Without a terminal (agents, CI), bare `connect` behaves exactly like `tinyfish connect --all`
|
|
27
|
-
and says so on its first line:
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
and says so on its first line: harnesses that can take your API key (via `TINYFISH_API_KEY` or
|
|
28
|
+
`--api-key`) connect first, then the first harness that needs a browser sign-in opens your
|
|
29
|
+
browser and waits up to 90 seconds for you to finish it. Any further sign-ins are reported as
|
|
30
|
+
waiting on you, with the single-harness command that finishes each one.
|
|
31
|
+
`tinyfish connect --all` itself skips the picker; `--dry-run`, `--uninstall`
|
|
30
32
|
and `--skip-launch` still require it.
|
|
31
33
|
|
|
32
34
|
### Connect Claude Code
|
|
@@ -9,6 +9,8 @@ interface NativeConnectOptions {
|
|
|
9
9
|
attemptId?: string;
|
|
10
10
|
/** Headless: refuse rather than fall back to an unfinishable browser sign-in. */
|
|
11
11
|
keyAuthOnly?: boolean;
|
|
12
|
+
/** Headless --all: bounds sign-in spawns and bars interactive CLI login. */
|
|
13
|
+
authTimeoutMs?: number;
|
|
12
14
|
/** Internal only: false leaves the global npm tree untouched. */
|
|
13
15
|
cliInstall?: boolean;
|
|
14
16
|
/** Set by `--all`: the CLI install already ran once, upfront. */
|
package/dist/commands/connect.js
CHANGED
|
@@ -4,7 +4,7 @@ import { CLAUDE_CODE, CODEX, CURSOR_SKILL_TARGET, GROK, HERMES, OPENCLAW, OPENCL
|
|
|
4
4
|
import { captureStdio, capturedOutput, HoistedFailureReportedError, installTinyFishCli, replay, } from "../lib/cli-install.js";
|
|
5
5
|
import { ensureCliAuthenticated } from "../lib/connect-auth.js";
|
|
6
6
|
import { installWebSkill } from "../lib/skill-install.js";
|
|
7
|
-
import { ConnectInterruptedError, ConnectStepError, createConnectTelemetry, finishSetupCommand, requireCommandSupport, runGuarded, settle, spawnStepError, throwIfInterrupted, NON_INTERACTIVE_TIMEOUT_MS, } from "../lib/connect-runtime.js";
|
|
7
|
+
import { ConnectInterruptedError, ConnectStepError, createConnectTelemetry, finishSetupCommand, requireCommandSupport, runGuarded, settle, SignInTimeoutError, spawnStepError, throwIfInterrupted, NON_INTERACTIVE_TIMEOUT_MS, } from "../lib/connect-runtime.js";
|
|
8
8
|
import { cursorInstallDeeplink, cursorMcpPath, readCursorTinyfishEntry, writeCursorMcpConfig, } from "../lib/cursor-config.js";
|
|
9
9
|
import { runConnectAll } from "../lib/connect-all.js";
|
|
10
10
|
import { AuthMode } from "../lib/harness-detect.js";
|
|
@@ -51,6 +51,13 @@ function exportApiKey(apiKey, displayName, deferOutro) {
|
|
|
51
51
|
}
|
|
52
52
|
return true;
|
|
53
53
|
}
|
|
54
|
+
/** A bounded spawn's timeout means the sign-in never finished. */
|
|
55
|
+
function signInStepError(message, result, bounded) {
|
|
56
|
+
const error = spawnStepError(message, result);
|
|
57
|
+
return bounded && error.failureReason === "timeout"
|
|
58
|
+
? new SignInTimeoutError(error.message, error.cause)
|
|
59
|
+
: error;
|
|
60
|
+
}
|
|
54
61
|
async function connectNativeMcpClient(client, options) {
|
|
55
62
|
const telemetry = createConnectTelemetry(options.mcpUrl, client.connectClient, options);
|
|
56
63
|
const state = { stage: "prerequisite_check", settled: false };
|
|
@@ -100,12 +107,13 @@ async function connectNativeMcpClient(client, options) {
|
|
|
100
107
|
? keyedAdd.spec.addArgs(mcpUrl.toString(), keyedAdd.key)
|
|
101
108
|
: client.addArgs(mcpUrl.toString(), options.mcpUrl);
|
|
102
109
|
// Capturing an inline-OAuth add hides the sign-in URL and hangs.
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
110
|
+
const interactiveAdd = !(client.nonInteractiveAdd || keyedAdd);
|
|
111
|
+
const addResult = spawn.sync(client.command, addArgs, interactiveAdd
|
|
112
|
+
? { stdio: "inherit", timeout: options.authTimeoutMs }
|
|
113
|
+
: captureStdio(options.verbose ?? false));
|
|
106
114
|
if (addResult.error || addResult.status !== 0) {
|
|
107
115
|
// Built first: its interrupt check must run before any replay.
|
|
108
|
-
const error =
|
|
116
|
+
const error = signInStepError(`Could not add TinyFish to ${client.displayName}`, addResult, interactiveAdd && options.authTimeoutMs !== undefined);
|
|
109
117
|
replay(capturedOutput(addResult));
|
|
110
118
|
throw error;
|
|
111
119
|
}
|
|
@@ -130,9 +138,12 @@ async function connectNativeMcpClient(client, options) {
|
|
|
130
138
|
if (pendingLogin) {
|
|
131
139
|
state.stage = "client_oauth";
|
|
132
140
|
errLine("Signing in to TinyFish...");
|
|
133
|
-
const loginResult = spawn.sync(client.command, pendingLogin, {
|
|
141
|
+
const loginResult = spawn.sync(client.command, pendingLogin, {
|
|
142
|
+
stdio: "inherit",
|
|
143
|
+
timeout: options.authTimeoutMs,
|
|
144
|
+
});
|
|
134
145
|
if (loginResult.error || loginResult.status !== 0) {
|
|
135
|
-
throw
|
|
146
|
+
throw signInStepError(`Could not authenticate TinyFish in ${client.displayName}`, loginResult, options.authTimeoutMs !== undefined);
|
|
136
147
|
}
|
|
137
148
|
if (probeAuthenticated?.() === false)
|
|
138
149
|
signInDeferred = true;
|
|
@@ -190,7 +201,10 @@ function runPostInstallSteps(client, options, state, telemetry, signInDeferred)
|
|
|
190
201
|
// Before auth: a failed sign-in must not lose the harness record.
|
|
191
202
|
saveConnectContext(client.connectClient, telemetry.attemptId);
|
|
192
203
|
attempt("authentication", () => {
|
|
193
|
-
|
|
204
|
+
// A set timeout means headless; never shell an interactive login.
|
|
205
|
+
ensureCliAuthenticated(client.connectClient, options.apiKey, {
|
|
206
|
+
interactiveLogin: options.authTimeoutMs === undefined,
|
|
207
|
+
});
|
|
194
208
|
telemetry.track("checkpoint", { phase: "authenticated" });
|
|
195
209
|
});
|
|
196
210
|
if (client.postConnectNote)
|
package/dist/lib/connect-all.js
CHANGED
|
@@ -6,7 +6,7 @@ import { TINYFISH_API_KEY_VAR } from "./constants.js";
|
|
|
6
6
|
import { installTinyFishCli, } from "./cli-install.js";
|
|
7
7
|
import { NATIVE_MCP_CLIENTS } from "./connect-clients.js";
|
|
8
8
|
import { ensureCliAuthenticated, TINYFISH_CLI_NOT_FOUND_MESSAGE } from "./connect-auth.js";
|
|
9
|
-
import { commandNotFound, ConnectInterruptedError, ConnectStepError, finishSetupCommand, throwIfInterrupted, } from "./connect-runtime.js";
|
|
9
|
+
import { commandNotFound, ConnectInterruptedError, ConnectStepError, SignInTimeoutError, finishSetupCommand, throwIfInterrupted, } from "./connect-runtime.js";
|
|
10
10
|
import { pickHarnesses } from "./connect-picker.js";
|
|
11
11
|
import { commandOnPath, detectInstalledHarnesses, HARNESS_COMMANDS, HARNESS_DISPLAY_NAMES as DISPLAY_NAMES, ALL_HARNESSES, } from "./harness-detect.js";
|
|
12
12
|
import { detectHumanInitiated } from "./harness.js";
|
|
@@ -23,8 +23,21 @@ const TOO_OLD_REASONS = new Set([
|
|
|
23
23
|
"harness_too_old",
|
|
24
24
|
"harness_command_unsupported",
|
|
25
25
|
]);
|
|
26
|
+
// These failures land before any browser could have opened.
|
|
27
|
+
const NO_BROWSER_REASONS = new Set([
|
|
28
|
+
...TOO_OLD_REASONS,
|
|
29
|
+
"harness_not_installed",
|
|
30
|
+
"command_not_found",
|
|
31
|
+
]);
|
|
26
32
|
const AUTH_CHECK_CAPABLE = new Set(["cursor", "openclaw"]);
|
|
27
33
|
const STEP_INDENT = " ";
|
|
34
|
+
// Must undercut agent tool-call budgets (Claude Code Bash defaults to 120s).
|
|
35
|
+
const OAUTH_SIGN_IN_TIMEOUT_MS = 90_000;
|
|
36
|
+
// Derived: these block `connect` on a browser sign-in when keyless.
|
|
37
|
+
const BROWSER_SIGN_IN = new Set(NATIVE_MCP_CLIENTS.filter((client) => client.loginArgs || client.oauthInAdd)
|
|
38
|
+
.map((client) => client.connectClient)
|
|
39
|
+
.filter((client) => ALL_HARNESSES.includes(client))
|
|
40
|
+
.map((client) => client));
|
|
28
41
|
// Descriptor-derived so a harness that cannot carry a key is skipped, not failed, when headless.
|
|
29
42
|
// Cursor and OpenClaw hold the key themselves rather than handing it to a harness `mcp add`.
|
|
30
43
|
const KEY_AUTH_CAPABLE = new Set([
|
|
@@ -46,12 +59,13 @@ const UNINSTALL_POINTERS = {
|
|
|
46
59
|
// OpenCode ships no `mcp remove`, so the only removal is editing the config it wrote.
|
|
47
60
|
opencode: "remove the tinyfish entry from ~/.config/opencode/opencode.json",
|
|
48
61
|
};
|
|
49
|
-
async function runHarnessConnect(harness, opts, keyAuthOnly, hoistedCliInstall, onPostInstallFailed, deferOutro) {
|
|
62
|
+
async function runHarnessConnect(harness, opts, keyAuthOnly, authTimeoutMs, hoistedCliInstall, onPostInstallFailed, deferOutro) {
|
|
50
63
|
const base = {
|
|
51
64
|
apiKey: opts.apiKey,
|
|
52
65
|
mcpUrl: opts.mcpUrl,
|
|
53
66
|
launch: false,
|
|
54
67
|
keyAuthOnly,
|
|
68
|
+
authTimeoutMs,
|
|
55
69
|
hoistedCliInstall,
|
|
56
70
|
onPostInstallFailed,
|
|
57
71
|
verbose: opts.verbose,
|
|
@@ -147,7 +161,7 @@ function uninstallHarness(harness, configPath) {
|
|
|
147
161
|
}
|
|
148
162
|
return { ...detection, outcome: "uninstalled" };
|
|
149
163
|
}
|
|
150
|
-
async function processHarness(detection, opts, prompt, hoistedCliInstall, deferOutro) {
|
|
164
|
+
async function processHarness(detection, opts, prompt, oauthBudget, hoistedCliInstall, deferOutro) {
|
|
151
165
|
const { harness, configPath, detected } = detection;
|
|
152
166
|
const base = { harness, detected, configPath, installed: false };
|
|
153
167
|
if (!detected)
|
|
@@ -168,13 +182,23 @@ async function processHarness(detection, opts, prompt, hoistedCliInstall, deferO
|
|
|
168
182
|
const apiKey = validatedApiKey(opts.apiKey);
|
|
169
183
|
// A key only leaves the own-OAuth set where the harness can actually carry one.
|
|
170
184
|
const keyed = !!apiKey && KEY_AUTH_CAPABLE.has(harness);
|
|
185
|
+
const fixCommand = `tinyfish connect ${harness}`;
|
|
186
|
+
let headlessAuth = false;
|
|
187
|
+
let spentBrowserSlot = false;
|
|
171
188
|
if (!isTTY && !keyed) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
outcome: "
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
189
|
+
// OpenClaw signs in through the CLI's own interactive login — unboundable.
|
|
190
|
+
if (harness === "openclaw")
|
|
191
|
+
return { ...base, outcome: "auth_pending", fixCommand };
|
|
192
|
+
if (BROWSER_SIGN_IN.has(harness)) {
|
|
193
|
+
// One browser sign-in per run; the rest wait for a re-run.
|
|
194
|
+
if (oauthBudget.spent)
|
|
195
|
+
return { ...base, outcome: "auth_pending", fixCommand };
|
|
196
|
+
oauthBudget.spent = true;
|
|
197
|
+
spentBrowserSlot = true;
|
|
198
|
+
errLine(`${DISPLAY_NAMES[harness]} needs a browser sign-in — finish it when your browser ` +
|
|
199
|
+
`opens; waiting up to ${OAUTH_SIGN_IN_TIMEOUT_MS / 1000} seconds.`);
|
|
200
|
+
}
|
|
201
|
+
headlessAuth = true;
|
|
178
202
|
}
|
|
179
203
|
if (isTTY && !keyed && prompt) {
|
|
180
204
|
errLine(`step: waiting for browser sign-in to TinyFish for ${DISPLAY_NAMES[harness]}`);
|
|
@@ -191,12 +215,24 @@ async function processHarness(detection, opts, prompt, hoistedCliInstall, deferO
|
|
|
191
215
|
let authMode;
|
|
192
216
|
let postInstallFailed = false;
|
|
193
217
|
try {
|
|
194
|
-
//
|
|
195
|
-
authMode = await runHarnessConnect(harness, opts, !isTTY, hoistedCliInstall, () => {
|
|
218
|
+
// A keyed install too old for key auth fails, no browser.
|
|
219
|
+
authMode = await runHarnessConnect(harness, opts, keyed && !isTTY, headlessAuth ? OAUTH_SIGN_IN_TIMEOUT_MS : undefined, hoistedCliInstall, () => {
|
|
196
220
|
postInstallFailed = true;
|
|
197
221
|
}, deferOutro);
|
|
198
222
|
}
|
|
199
223
|
catch (e) {
|
|
224
|
+
if (e instanceof SignInTimeoutError) {
|
|
225
|
+
return {
|
|
226
|
+
...base,
|
|
227
|
+
outcome: "auth_pending",
|
|
228
|
+
errorMessage: "sign-in did not finish in time",
|
|
229
|
+
fixCommand,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
// A pre-browser failure must not burn the run's only browser slot.
|
|
233
|
+
if (spentBrowserSlot && e instanceof ConnectStepError && NO_BROWSER_REASONS.has(e.failureReason)) {
|
|
234
|
+
oauthBudget.spent = false;
|
|
235
|
+
}
|
|
200
236
|
// Both reasons carry the same "flag not listed" message; the upgrade nudge is in it.
|
|
201
237
|
if (e instanceof ConnectStepError && TOO_OLD_REASONS.has(e.failureReason)) {
|
|
202
238
|
return {
|
|
@@ -248,10 +284,12 @@ function summaryLine(result, uninstall) {
|
|
|
248
284
|
return `… ${name} — ${result.fixCommand}`;
|
|
249
285
|
}
|
|
250
286
|
if (result.outcome === "no_tty_auth_skip") {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
return
|
|
287
|
+
return `… ${name} — skipped, no stored credentials and no terminal to sign in. Fix: ${result.fixCommand}`;
|
|
288
|
+
}
|
|
289
|
+
if (result.outcome === "auth_pending") {
|
|
290
|
+
return result.errorMessage
|
|
291
|
+
? `… ${name} — ${result.errorMessage}. Finish it in a terminal: ${result.fixCommand}`
|
|
292
|
+
: `… ${name} — needs a browser sign-in. Run in a terminal: ${result.fixCommand}`;
|
|
255
293
|
}
|
|
256
294
|
if (result.outcome === "deselected") {
|
|
257
295
|
return `… ${name} — skipped, not selected. Fix: ${result.fixCommand}`;
|
|
@@ -405,6 +443,11 @@ export async function runConnectAll(opts) {
|
|
|
405
443
|
}
|
|
406
444
|
: undefined;
|
|
407
445
|
const detections = detectInstalledHarnesses();
|
|
446
|
+
const gateKey = validatedApiKey(opts.apiKey);
|
|
447
|
+
// Keyed installs first: working tools land before sign-ins can block.
|
|
448
|
+
if (gateKey) {
|
|
449
|
+
detections.sort((a, b) => Number(KEY_AUTH_CAPABLE.has(b.harness)) - Number(KEY_AUTH_CAPABLE.has(a.harness)));
|
|
450
|
+
}
|
|
408
451
|
let deselected = new Set();
|
|
409
452
|
let mode = "all";
|
|
410
453
|
// Before willAttempt/cliNeeded: a cursor-only pick must not hoist the CLI.
|
|
@@ -422,9 +465,9 @@ export async function runConnectAll(opts) {
|
|
|
422
465
|
const chosen = new Set(pick.selection);
|
|
423
466
|
deselected = new Set(offerable.filter((d) => !chosen.has(d.harness)).map((d) => d.harness));
|
|
424
467
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
468
|
+
// Mirrors processHarness's gate: headless, only keyless OpenClaw skips outright.
|
|
469
|
+
const willAttempt = (harness) => !deselected.has(harness) &&
|
|
470
|
+
(isTTY || harness !== "openclaw" || (!!gateKey && KEY_AUTH_CAPABLE.has(harness)));
|
|
428
471
|
// Machine-global, so once per invocation; cursor's connect never installs it.
|
|
429
472
|
const cliNeeded = !opts.dryRun &&
|
|
430
473
|
!opts.uninstall &&
|
|
@@ -448,6 +491,7 @@ export async function runConnectAll(opts) {
|
|
|
448
491
|
// A Set dedupes: Codex and Grok defer the same export line.
|
|
449
492
|
const outro = new Set();
|
|
450
493
|
const results = [];
|
|
494
|
+
const oauthBudget = { spent: false };
|
|
451
495
|
const stepTotal = detections.length - deselected.size;
|
|
452
496
|
let step = 0;
|
|
453
497
|
for (const detection of detections) {
|
|
@@ -466,7 +510,7 @@ export async function runConnectAll(opts) {
|
|
|
466
510
|
let result;
|
|
467
511
|
try {
|
|
468
512
|
setErrIndent(STEP_INDENT);
|
|
469
|
-
result = await processHarness(detection, opts, prompt, takeHoistedCli, (line) => outro.add(line));
|
|
513
|
+
result = await processHarness(detection, opts, prompt, oauthBudget, takeHoistedCli, (line) => outro.add(line));
|
|
470
514
|
}
|
|
471
515
|
catch (e) {
|
|
472
516
|
// Isolation: one harness throwing never stops the others.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import { type AgentClient } from "./connect-runtime.js";
|
|
2
2
|
export declare const TINYFISH_CLI_NOT_FOUND_MESSAGE = "TinyFish CLI installed but is not available on PATH. Open a new terminal and retry.";
|
|
3
|
-
export declare function ensureCliAuthenticated(source: AgentClient | "cli", apiKey?: string
|
|
3
|
+
export declare function ensureCliAuthenticated(source: AgentClient | "cli", apiKey?: string, opts?: {
|
|
4
|
+
interactiveLogin?: boolean;
|
|
5
|
+
}): void;
|
package/dist/lib/connect-auth.js
CHANGED
|
@@ -4,7 +4,7 @@ import { persistApiKeyToEnvironment, validateKeyFormat, writeConfig } from "./au
|
|
|
4
4
|
import { commandNotFound, ConnectStepError, spawnStepError, throwIfInterrupted, NON_INTERACTIVE_TIMEOUT_MS, } from "./connect-runtime.js";
|
|
5
5
|
import { errLine } from "./output.js";
|
|
6
6
|
export const TINYFISH_CLI_NOT_FOUND_MESSAGE = "TinyFish CLI installed but is not available on PATH. Open a new terminal and retry.";
|
|
7
|
-
export function ensureCliAuthenticated(source, apiKey) {
|
|
7
|
+
export function ensureCliAuthenticated(source, apiKey, opts) {
|
|
8
8
|
const envKey = apiKey ?? process.env[TINYFISH_API_KEY_VAR];
|
|
9
9
|
if (envKey) {
|
|
10
10
|
if (!validateKeyFormat(envKey)) {
|
|
@@ -30,6 +30,9 @@ export function ensureCliAuthenticated(source, apiKey) {
|
|
|
30
30
|
if (authStatus.error) {
|
|
31
31
|
throw spawnStepError("Could not read TinyFish CLI auth status", authStatus);
|
|
32
32
|
}
|
|
33
|
+
if (opts?.interactiveLogin === false) {
|
|
34
|
+
throw new Error("The TinyFish CLI is not signed in. Run: tinyfish auth login");
|
|
35
|
+
}
|
|
33
36
|
errLine("Signing in to the TinyFish CLI...");
|
|
34
37
|
const loginResult = spawn.sync("tinyfish", ["auth", "login", "--source", source], {
|
|
35
38
|
stdio: "inherit",
|
|
@@ -25,6 +25,8 @@ export interface NativeMcpClient extends SupportedCommand {
|
|
|
25
25
|
/** Shown when a keyless install leaves sign-in to the harness. */
|
|
26
26
|
signInHint?: string;
|
|
27
27
|
loginArgs?: string[];
|
|
28
|
+
/** Keyless `mcp add` itself blocks on the browser OAuth. */
|
|
29
|
+
oauthInAdd?: boolean;
|
|
28
30
|
/** Replaces the `loginArgs` step when `supportCheck.optionalPatterns` miss. */
|
|
29
31
|
degradedAuthNotes?: {
|
|
30
32
|
keyed: string;
|
|
@@ -228,6 +228,7 @@ export const HERMES = {
|
|
|
228
228
|
// No `keyAuth`: `hermes mcp add` has no --header, and `--auth header` prompts interactively.
|
|
229
229
|
removals: [{ args: ["mcp", "remove", "tinyfish"], label: "TinyFish registration from Hermes" }],
|
|
230
230
|
// Hermes completes OAuth while adding the server; a separate `mcp login` would authenticate twice.
|
|
231
|
+
oauthInAdd: true,
|
|
231
232
|
addArgs: (mcpUrl) => [...urlAddArgs(mcpUrl), "--auth", "oauth"],
|
|
232
233
|
launchWalkthrough: launchHermesWalkthrough,
|
|
233
234
|
};
|
|
@@ -19,11 +19,15 @@ export declare class ConnectStepError extends Error {
|
|
|
19
19
|
cause?: unknown;
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
|
+
/** A bounded sign-in wait elapsed — unfinished, not broken. */
|
|
23
|
+
export declare class SignInTimeoutError extends ConnectStepError {
|
|
24
|
+
constructor(message: string, cause?: unknown);
|
|
25
|
+
}
|
|
22
26
|
export declare function throwIfInterrupted(result: {
|
|
23
27
|
signal?: string | null;
|
|
24
28
|
error?: Error;
|
|
25
29
|
}): void;
|
|
26
|
-
interface SpawnStepResult {
|
|
30
|
+
export interface SpawnStepResult {
|
|
27
31
|
status?: number | null;
|
|
28
32
|
signal?: string | null;
|
|
29
33
|
error?: Error;
|
|
@@ -37,6 +37,12 @@ export class ConnectStepError extends Error {
|
|
|
37
37
|
}
|
|
38
38
|
class PrerequisiteError extends ConnectStepError {
|
|
39
39
|
}
|
|
40
|
+
/** A bounded sign-in wait elapsed — unfinished, not broken. */
|
|
41
|
+
export class SignInTimeoutError extends ConnectStepError {
|
|
42
|
+
constructor(message, cause) {
|
|
43
|
+
super(message, "timeout", { cause });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
40
46
|
// A maxBuffer overflow is also killed with SIGTERM, and is a failure too.
|
|
41
47
|
const NOT_INTERRUPTIONS = new Set(["ETIMEDOUT", "ENOBUFS"]);
|
|
42
48
|
export function throwIfInterrupted(result) {
|
|
@@ -107,6 +113,11 @@ export async function runGuarded(state, telemetry, body) {
|
|
|
107
113
|
process.exitCode = 130;
|
|
108
114
|
return;
|
|
109
115
|
}
|
|
116
|
+
// An elapsed sign-in wait is abandonment, not breakage.
|
|
117
|
+
if (error instanceof SignInTimeoutError) {
|
|
118
|
+
settle(state, telemetry, "aborted", { failedStage: state.stage, failureReason: "timeout" });
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
110
121
|
settle(state, telemetry, "failed", {
|
|
111
122
|
failedStage: state.stage,
|
|
112
123
|
failureReason: error instanceof ConnectStepError ? error.failureReason : "unexpected_error",
|