@tiny-fish/cli 0.52.5-next.436 → 0.52.5-next.437
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 +2 -1
- package/dist/lib/connect-all-auth.js +16 -1
- package/dist/lib/connect-clients.d.ts +1 -2
- package/dist/lib/connect-clients.js +3 -9
- package/dist/lib/connect-native.js +62 -26
- package/dist/lib/harness/harness-spec.d.ts +7 -1
- package/dist/lib/harness/harness-spec.js +4 -1
- package/dist/lib/sign-in-url.d.ts +8 -0
- package/dist/lib/sign-in-url.js +35 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,11 +26,12 @@ anything. Terminals without raw-key support get a numbered prompt instead.
|
|
|
26
26
|
Try Search without an account; run `tinyfish auth login` and reconnect to unlock all tools:
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
-
npx -y @tiny-fish/cli@latest connect opencode --launch
|
|
30
29
|
npx -y @tiny-fish/cli@latest connect hermes --launch
|
|
31
30
|
npx -y @tiny-fish/cli@latest connect omp --launch
|
|
32
31
|
```
|
|
33
32
|
|
|
33
|
+
`connect opencode` signs you in to a TinyFish account instead: it opens your browser, or prints a sign-in link when an agent runs it.
|
|
34
|
+
|
|
34
35
|
Without a terminal (agents, CI), bare `connect` behaves exactly like `tinyfish connect --all`
|
|
35
36
|
and says so on its first line: harnesses that can take your API key (via `TINYFISH_API_KEY` or
|
|
36
37
|
`--api-key`) connect first, then the first harness that needs a browser sign-in opens your
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NATIVE_BY_HARNESS } from './connect-clients.js';
|
|
2
2
|
import { harnessSpec } from './harness/harness-spec.js';
|
|
3
|
-
import { ConnectInterruptedError, ConnectStepError, SignInTimeoutError, } from './connect-runtime.js';
|
|
3
|
+
import { ConnectInterruptedError, ConnectStepError, SignInTimeoutError, probeSupportVariant, } from './connect-runtime.js';
|
|
4
4
|
import { ALL_HARNESSES, HARNESS_DISPLAY_NAMES as DISPLAY_NAMES, } from './harness/harness-detect.js';
|
|
5
5
|
import { errLine } from './output.js';
|
|
6
6
|
// Must undercut agent tool-call budgets (Claude Code Bash defaults to 120s).
|
|
@@ -23,6 +23,19 @@ const BROWSER_SIGN_IN = new Set(ALL_HARNESSES.filter((harness) => {
|
|
|
23
23
|
}));
|
|
24
24
|
// Derived: these refuse a keyless install, so no sign-in helps.
|
|
25
25
|
const KEY_REQUIRED = new Set(ALL_HARNESSES.filter((harness) => !!harnessSpec(harness).keyRequired));
|
|
26
|
+
/** OpenCode 2.x prints a sign-in link and exits, so it waits on no browser. */
|
|
27
|
+
function printsSignInLink(harness) {
|
|
28
|
+
const native = NATIVE_BY_HARNESS.get(harness);
|
|
29
|
+
if (!native?.detachedLoginVariant)
|
|
30
|
+
return false;
|
|
31
|
+
try {
|
|
32
|
+
return probeSupportVariant(native) === native.detachedLoginVariant;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Routing only: connect itself reports the probe failure.
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
26
39
|
function headlessGate(base, oauthBudget, fixCommand) {
|
|
27
40
|
const { harness } = base;
|
|
28
41
|
// A CLI-login sign-in is interactive-only — unboundable headless.
|
|
@@ -33,6 +46,8 @@ function headlessGate(base, oauthBudget, fixCommand) {
|
|
|
33
46
|
spentBrowserSlot: false,
|
|
34
47
|
};
|
|
35
48
|
}
|
|
49
|
+
if (printsSignInLink(harness))
|
|
50
|
+
return { headlessAuth: true, spentBrowserSlot: false };
|
|
36
51
|
let spentBrowserSlot = false;
|
|
37
52
|
if (BROWSER_SIGN_IN.has(harness)) {
|
|
38
53
|
// One browser sign-in per run; the rest wait for a re-run.
|
|
@@ -39,7 +39,7 @@ interface SeededInstall {
|
|
|
39
39
|
tag?: string;
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
|
-
interface BaseMcpClient extends SupportedCommand, Pick<HarnessSpec, 'skillAgent' | 'nonInteractiveAdd' | 'signInHint' | 'degradedAuthNotes' | 'postConnectNote' | 'verifyListed'> {
|
|
42
|
+
interface BaseMcpClient extends SupportedCommand, Pick<HarnessSpec, 'skillAgent' | 'nonInteractiveAdd' | 'signInHint' | 'detachedLoginVariant' | 'cliKeyOptional' | 'degradedAuthNotes' | 'postConnectNote' | 'verifyListed'> {
|
|
43
43
|
connectClient: Exclude<AgentClient, 'openclaw'>;
|
|
44
44
|
/** Replaces `addArgs` when a stored key is available: key auth removes the OAuth hop. */
|
|
45
45
|
keyAuth?: {
|
|
@@ -49,7 +49,6 @@ interface BaseMcpClient extends SupportedCommand, Pick<HarnessSpec, 'skillAgent'
|
|
|
49
49
|
/** Writes into the harness's store, so connect verifies the key first. */
|
|
50
50
|
seedKey?: (apiKey: string) => SeededInstall;
|
|
51
51
|
};
|
|
52
|
-
keylessAddArgs?: (mcpUrl: string) => string[];
|
|
53
52
|
prepareKeylessInstall?: () => DirectInstall;
|
|
54
53
|
loginArgs?: string[];
|
|
55
54
|
removals: {
|
|
@@ -4,7 +4,7 @@ import { detectHumanInitiated } from './harness.js';
|
|
|
4
4
|
import { harnessConfigPath } from './harness/harness-detect.js';
|
|
5
5
|
import { errLine, sanitizeLine, warnLine } from './output.js';
|
|
6
6
|
import { ConnectInterruptedError, ConnectStepError, spawnStepError, } from './connect-runtime.js';
|
|
7
|
-
import {
|
|
7
|
+
import { TINYFISH_API_KEY_VAR } from './constants.js';
|
|
8
8
|
import { HARNESS_SPECS, NATIVE_HARNESSES, harnessSpec, } from './harness/harness-spec.js';
|
|
9
9
|
import { HERMES_KEY_VAR, captureHermesKeyRestore, hermesEnvPath, resolveHermesHome, writeHermesKey, } from './hermes-env.js';
|
|
10
10
|
import { hermesConfigPath, readHermesEntry } from './config/hermes-config.js';
|
|
@@ -256,13 +256,6 @@ function specKeyAuth(spec) {
|
|
|
256
256
|
addArgs: (mcpUrl, apiKey) => [...specAddArgs(spec, mcpUrl), '--header', value(apiKey)],
|
|
257
257
|
};
|
|
258
258
|
}
|
|
259
|
-
function specKeylessAddArgs(spec) {
|
|
260
|
-
const { header } = spec;
|
|
261
|
-
if (!spec.keylessFallback || !header)
|
|
262
|
-
return undefined;
|
|
263
|
-
const value = `${TINYFISH_ACCESS_MODE_HEADER}${header.sep}${TINYFISH_KEYLESS_ACCESS_MODE}`;
|
|
264
|
-
return (mcpUrl) => specAddArgs(spec, mcpUrl).concat('--header', value);
|
|
265
|
-
}
|
|
266
259
|
/** Descriptor behaviour the spec cannot hold; every other field is spec data. */
|
|
267
260
|
const HARNESS_OVERRIDES = {
|
|
268
261
|
cline: { launchWalkthrough: launchCline },
|
|
@@ -291,9 +284,10 @@ function toNativeClient(harness) {
|
|
|
291
284
|
skillAgent: spec.skillAgent,
|
|
292
285
|
nonInteractiveAdd: spec.nonInteractiveAdd,
|
|
293
286
|
keyAuth,
|
|
294
|
-
keylessAddArgs: specKeylessAddArgs(spec),
|
|
295
287
|
prepareKeylessInstall: overrides.prepareKeylessInstall,
|
|
296
288
|
signInHint: spec.signInHint,
|
|
289
|
+
detachedLoginVariant: spec.detachedLoginVariant,
|
|
290
|
+
cliKeyOptional: spec.cliKeyOptional,
|
|
297
291
|
degradedAuthNotes: spec.degradedAuthNotes,
|
|
298
292
|
removals: spec.removals ?? [
|
|
299
293
|
{
|
|
@@ -4,6 +4,7 @@ import { KEYLESS_ONBOARDING_PROMPT, launchNativeMcpClient, } from './connect-cli
|
|
|
4
4
|
import { captureStdio, capturedOutput, replay, SKILL_INSTALL_TIMEOUT_MS, STEP_MAX_BUFFER, } from './cli-install.js';
|
|
5
5
|
import { ensureCliAuthenticated } from './connect-auth.js';
|
|
6
6
|
import { installWebSkill } from './skill-install.js';
|
|
7
|
+
import { waitForSignInUrl } from './sign-in-url.js';
|
|
7
8
|
import { ConnectInterruptedError, ConnectStepError, createConnectTelemetry, runGuarded, settle, SignInTimeoutError, spawnRemoval, spawnStepError, } from './connect-runtime.js';
|
|
8
9
|
import { DASHBOARD_URL, TINYFISH_API_KEY_VAR } from './constants.js';
|
|
9
10
|
import { strayProjectKeyFiles } from './config/opencode-config.js';
|
|
@@ -194,20 +195,12 @@ async function prepareInstallAuth(client, options, keyAuthSupported, matchedVari
|
|
|
194
195
|
// Prod MCP accepts X-API-Key, so a stored key replaces the browser OAuth hop.
|
|
195
196
|
const storedKey = validatedApiKey(options.apiKey);
|
|
196
197
|
const keyedAdd = resolveKeyedAdd(client, storedKey, keyAuthSupported, options.keyAuthOnly === true, options.deferOutro);
|
|
197
|
-
const keylessArgs = !storedKey ? client.keylessAddArgs : undefined;
|
|
198
198
|
const directInstall = !storedKey ? client.prepareKeylessInstall?.() : undefined;
|
|
199
|
-
|
|
199
|
+
const keyless = !!directInstall;
|
|
200
200
|
let buildAddArgs;
|
|
201
201
|
if (keyedAdd) {
|
|
202
202
|
buildAddArgs = (mcpUrl) => keyedAdd.spec.addArgs(mcpUrl, keyedAdd.key);
|
|
203
203
|
}
|
|
204
|
-
else if (keylessArgs && keyAuthSupported) {
|
|
205
|
-
buildAddArgs = keylessArgs;
|
|
206
|
-
keyless = true;
|
|
207
|
-
}
|
|
208
|
-
else if (keylessArgs && options.authTimeoutMs !== undefined) {
|
|
209
|
-
refuseKeylessInstall(client, false, true);
|
|
210
|
-
}
|
|
211
204
|
else if (!directInstall) {
|
|
212
205
|
buildAddArgs = keylessAddArgs(client, keyAuthSupported);
|
|
213
206
|
}
|
|
@@ -302,6 +295,24 @@ function performLoginStep(client, pendingLogin, options) {
|
|
|
302
295
|
throw signInStepError(`Could not authenticate TinyFish in ${client.displayName}`, loginResult, options.authTimeoutMs !== undefined);
|
|
303
296
|
}
|
|
304
297
|
}
|
|
298
|
+
// Upper bound only: the URL normally prints within two seconds.
|
|
299
|
+
const SIGN_IN_URL_WAIT_MS = 15_000;
|
|
300
|
+
// 2.x's service lags `mcp add` by under a second (measured on 2.1.4).
|
|
301
|
+
const STALE_REGISTRATION_RETRIES = 3;
|
|
302
|
+
const STALE_REGISTRATION_DELAY_MS = 1_000;
|
|
303
|
+
/** Relays the sign-in link; the harness service finishes it after we exit. */
|
|
304
|
+
async function printDetachedLoginUrl(client, pendingLogin) {
|
|
305
|
+
errLine('Starting TinyFish sign-in...');
|
|
306
|
+
let run = await waitForSignInUrl(client.command, pendingLogin, SIGN_IN_URL_WAIT_MS);
|
|
307
|
+
for (let retry = 0; retry < STALE_REGISTRATION_RETRIES && run.output.includes('Integration not found'); retry++) {
|
|
308
|
+
await new Promise((resolve) => setTimeout(resolve, STALE_REGISTRATION_DELAY_MS));
|
|
309
|
+
run = await waitForSignInUrl(client.command, pendingLogin, SIGN_IN_URL_WAIT_MS);
|
|
310
|
+
}
|
|
311
|
+
if (!run.url) {
|
|
312
|
+
throw signInStepError(`Could not start TinyFish sign-in in ${client.displayName}`, run.result, true);
|
|
313
|
+
}
|
|
314
|
+
errLine(`Sign in to TinyFish: ${run.url}`);
|
|
315
|
+
}
|
|
305
316
|
function announceAuthNotes(useKeyAuth, degradedNotes, seededNote) {
|
|
306
317
|
if (useKeyAuth) {
|
|
307
318
|
errLine(degradedNotes?.keyed ?? 'Using your stored TinyFish API key — no browser sign-in needed.');
|
|
@@ -314,7 +325,20 @@ function announceAuthNotes(useKeyAuth, degradedNotes, seededNote) {
|
|
|
314
325
|
errLine(degradedNotes.deferred);
|
|
315
326
|
}
|
|
316
327
|
}
|
|
317
|
-
|
|
328
|
+
/** Returns whether sign-in is still deferred afterwards. */
|
|
329
|
+
async function runPendingLogin(client, pendingLogin, run) {
|
|
330
|
+
run.state.stage = 'client_oauth';
|
|
331
|
+
if (run.ctx.detachedLogin) {
|
|
332
|
+
await printDetachedLoginUrl(client, pendingLogin);
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
performLoginStep(client, pendingLogin, run.options);
|
|
336
|
+
if (run.ctx.probeAuthenticated?.() === false)
|
|
337
|
+
return true;
|
|
338
|
+
run.telemetry.track('checkpoint', { phase: 'oauth_done' });
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
async function completeAuthFlow(client, options, state, telemetry, ctx) {
|
|
318
342
|
const { useKeyAuth, useKeyless, loginDegraded, loginArgs, probeAuthenticated } = ctx;
|
|
319
343
|
if (useKeyless) {
|
|
320
344
|
errLine('Using keyless TinyFish Search — no account or browser sign-in needed.');
|
|
@@ -329,15 +353,9 @@ function completeAuthFlow(client, options, state, telemetry, ctx) {
|
|
|
329
353
|
// A probed client signs in again only when its inline OAuth is known to have been abandoned.
|
|
330
354
|
const retryLogin = probeAuthenticated ? addSignedIn === false : true;
|
|
331
355
|
const pendingLogin = !useKeyAuth && retryLogin ? loginArgs : undefined;
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
performLoginStep(client, pendingLogin, options);
|
|
336
|
-
if (probeAuthenticated?.() === false)
|
|
337
|
-
signInDeferred = true;
|
|
338
|
-
else
|
|
339
|
-
telemetry.track('checkpoint', { phase: 'oauth_done' });
|
|
340
|
-
}
|
|
356
|
+
const signInDeferred = pendingLogin
|
|
357
|
+
? await runPendingLogin(client, pendingLogin, { options, state, telemetry, ctx })
|
|
358
|
+
: !useKeyAuth && !addSignedIn;
|
|
341
359
|
const authMode = useKeyAuth
|
|
342
360
|
? AuthMode.ApiKey
|
|
343
361
|
: signInDeferred
|
|
@@ -380,11 +398,12 @@ export async function connectNativeMcpClient(unpinned, options) {
|
|
|
380
398
|
for (const stray of twoPointX ? strayProjectKeyFiles() : []) {
|
|
381
399
|
warnLine(opencodeStrayKeyNote(stray, DASHBOARD_URL));
|
|
382
400
|
}
|
|
383
|
-
const { authMode, signInDeferred } = completeAuthFlow(client, options, state, telemetry, {
|
|
401
|
+
const { authMode, signInDeferred } = await completeAuthFlow(client, options, state, telemetry, {
|
|
384
402
|
useKeyAuth,
|
|
385
403
|
useKeyless,
|
|
386
404
|
loginDegraded,
|
|
387
405
|
loginArgs,
|
|
406
|
+
detachedLogin: !!client.detachedLoginVariant && matchedVariant === client.detachedLoginVariant,
|
|
388
407
|
probeAuthenticated,
|
|
389
408
|
seededNote,
|
|
390
409
|
});
|
|
@@ -411,6 +430,28 @@ function connectedLine(client, signInDeferred) {
|
|
|
411
430
|
return (`TinyFish is connected. Open ${client.displayName}; if it is already running, ` +
|
|
412
431
|
`${RELOAD_ACTION[client.connectClient]} to pick up the tools.`);
|
|
413
432
|
}
|
|
433
|
+
/** Right after OAuth, a key prompt or failure reads as a second sign-in. */
|
|
434
|
+
function offerCliSignIn(client, options, telemetry) {
|
|
435
|
+
try {
|
|
436
|
+
ensureCliAuthenticated(client.connectClient, options.apiKey, { interactiveLogin: false });
|
|
437
|
+
telemetry.track('checkpoint', { phase: 'authenticated' });
|
|
438
|
+
}
|
|
439
|
+
catch (error) {
|
|
440
|
+
if (error instanceof ConnectInterruptedError)
|
|
441
|
+
throw error;
|
|
442
|
+
errLine('To use TinyFish from the terminal too, run: tinyfish auth login');
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function authenticateCli(client, options, state, telemetry) {
|
|
446
|
+
if (client.cliKeyOptional && state.authMode !== AuthMode.ApiKey) {
|
|
447
|
+
offerCliSignIn(client, options, telemetry);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
ensureCliAuthenticated(client.connectClient, options.apiKey, {
|
|
451
|
+
interactiveLogin: options.authTimeoutMs === undefined,
|
|
452
|
+
});
|
|
453
|
+
telemetry.track('checkpoint', { phase: 'authenticated' });
|
|
454
|
+
}
|
|
414
455
|
// Runs after `completed` settled: steps fail alone, the rest still run.
|
|
415
456
|
async function runPostInstallSteps(client, options, state, telemetry, signInDeferred, seededHome) {
|
|
416
457
|
let failed = false;
|
|
@@ -450,12 +491,7 @@ async function runPostInstallSteps(client, options, state, telemetry, signInDefe
|
|
|
450
491
|
});
|
|
451
492
|
}
|
|
452
493
|
if (state.authMode !== AuthMode.Keyless) {
|
|
453
|
-
await attempt('authentication', () =>
|
|
454
|
-
ensureCliAuthenticated(client.connectClient, options.apiKey, {
|
|
455
|
-
interactiveLogin: options.authTimeoutMs === undefined,
|
|
456
|
-
});
|
|
457
|
-
telemetry.track('checkpoint', { phase: 'authenticated' });
|
|
458
|
-
});
|
|
494
|
+
await attempt('authentication', () => authenticateCli(client, options, state, telemetry));
|
|
459
495
|
}
|
|
460
496
|
if (client.postConnectNote)
|
|
461
497
|
errLine(client.postConnectNote);
|
|
@@ -56,6 +56,10 @@ export interface HarnessSpec {
|
|
|
56
56
|
keyRequired?: true;
|
|
57
57
|
keylessFallback?: true;
|
|
58
58
|
loginArgs?: string[];
|
|
59
|
+
/** After this harness's OAuth, connect never prompts for or requires the CLI key. */
|
|
60
|
+
cliKeyOptional?: true;
|
|
61
|
+
/** With this support variant, the sign-in callback outlives `loginArgs`. */
|
|
62
|
+
detachedLoginVariant?: string;
|
|
59
63
|
/** Default: one `mcp remove tinyfish` labeled from displayName. */
|
|
60
64
|
removals?: {
|
|
61
65
|
args: string[];
|
|
@@ -295,7 +299,6 @@ export declare const HARNESS_SPECS: {
|
|
|
295
299
|
reloadAction: string;
|
|
296
300
|
skillAgent: "opencode";
|
|
297
301
|
nonInteractiveAdd: true;
|
|
298
|
-
keylessFallback: true;
|
|
299
302
|
supportCheck: {
|
|
300
303
|
args: string[];
|
|
301
304
|
patterns: RegExp[];
|
|
@@ -310,6 +313,9 @@ export declare const HARNESS_SPECS: {
|
|
|
310
313
|
sep: "=";
|
|
311
314
|
};
|
|
312
315
|
loginArgs: string[];
|
|
316
|
+
detachedLoginVariant: string;
|
|
317
|
+
cliKeyOptional: true;
|
|
318
|
+
signInHint: string;
|
|
313
319
|
removals: never[];
|
|
314
320
|
postConnectNote: string;
|
|
315
321
|
verifyListed: true;
|
|
@@ -221,7 +221,6 @@ export const HARNESS_SPECS = {
|
|
|
221
221
|
reloadAction: 'restart it',
|
|
222
222
|
skillAgent: 'opencode',
|
|
223
223
|
nonInteractiveAdd: true,
|
|
224
|
-
keylessFallback: true,
|
|
225
224
|
supportCheck: {
|
|
226
225
|
args: ['mcp', 'add', '--help'],
|
|
227
226
|
patterns: [/--url(?:\s|$)/m],
|
|
@@ -237,6 +236,10 @@ export const HARNESS_SPECS = {
|
|
|
237
236
|
header: { name: 'X-API-Key', sep: '=' },
|
|
238
237
|
// OpenCode writes its config itself; `mcp auth` is the separate OAuth step.
|
|
239
238
|
loginArgs: ['mcp', 'auth', 'tinyfish'],
|
|
239
|
+
// 2.x's background service, not `mcp auth`, catches the OAuth callback.
|
|
240
|
+
detachedLoginVariant: '--global',
|
|
241
|
+
cliKeyOptional: true,
|
|
242
|
+
signInHint: 'TinyFish is configured in OpenCode. Open the sign-in link above, then restart OpenCode.',
|
|
240
243
|
// `opencode mcp add` upserts by name and ships no `mcp remove`.
|
|
241
244
|
removals: [],
|
|
242
245
|
postConnectNote: OPENCODE_MODEL_NOTE,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SpawnStepResult } from './connect-runtime.js';
|
|
2
|
+
export interface SignInUrlRun {
|
|
3
|
+
url?: string;
|
|
4
|
+
output: string;
|
|
5
|
+
result: SpawnStepResult;
|
|
6
|
+
}
|
|
7
|
+
/** Resolves on the first printed URL; SIGKILL, because SIGTERM cancels OpenCode's sign-in. */
|
|
8
|
+
export declare function waitForSignInUrl(command: string, args: readonly string[], timeoutMs: number): Promise<SignInUrlRun>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import spawn from 'cross-spawn';
|
|
2
|
+
// Trailing whitespace proves the URL arrived whole, not cut mid-chunk.
|
|
3
|
+
const SIGN_IN_URL = /https:\/\/\S+(?=\s)/;
|
|
4
|
+
/** Resolves on the first printed URL; SIGKILL, because SIGTERM cancels OpenCode's sign-in. */
|
|
5
|
+
export function waitForSignInUrl(command, args, timeoutMs) {
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
const child = spawn(command, [...args], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
8
|
+
let output = '';
|
|
9
|
+
let settled = false;
|
|
10
|
+
const settle = (result) => {
|
|
11
|
+
if (settled)
|
|
12
|
+
return;
|
|
13
|
+
settled = true;
|
|
14
|
+
clearTimeout(timer);
|
|
15
|
+
child.kill('SIGKILL');
|
|
16
|
+
resolve({
|
|
17
|
+
url: output.match(SIGN_IN_URL)?.[0],
|
|
18
|
+
output,
|
|
19
|
+
result: { ...result, stdout: output },
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
const timer = setTimeout(() => {
|
|
23
|
+
settle({ status: null, error: Object.assign(new Error('timed out'), { code: 'ETIMEDOUT' }) });
|
|
24
|
+
}, timeoutMs);
|
|
25
|
+
const onData = (chunk) => {
|
|
26
|
+
output += chunk.toString();
|
|
27
|
+
if (SIGN_IN_URL.test(output))
|
|
28
|
+
settle({ status: 0 });
|
|
29
|
+
};
|
|
30
|
+
child.stdout?.on('data', onData);
|
|
31
|
+
child.stderr?.on('data', onData);
|
|
32
|
+
child.on('error', (error) => settle({ status: null, error }));
|
|
33
|
+
child.on('close', (status, signal) => settle({ status, signal }));
|
|
34
|
+
});
|
|
35
|
+
}
|