@tiny-fish/cli 0.30.1-next.247 → 0.30.1-next.249
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/commands/connect.d.ts +4 -0
- package/dist/commands/connect.js +59 -4
- package/dist/lib/connect-all.js +18 -94
- package/dist/lib/connect-fallback.d.ts +24 -0
- package/dist/lib/connect-fallback.js +136 -0
- package/dist/lib/connect-runtime.d.ts +4 -2
- package/dist/lib/connect-runtime.js +6 -0
- package/package.json +1 -1
|
@@ -21,6 +21,8 @@ export interface NativeConnectOptions {
|
|
|
21
21
|
verbose?: boolean;
|
|
22
22
|
/** Set by `--all`: repeated outro lines print once, after the summary. */
|
|
23
23
|
deferOutro?: (line: string) => void;
|
|
24
|
+
/** Targeted connects only: --all keeps its per-harness failed row. */
|
|
25
|
+
fallbackWhenMissing?: boolean;
|
|
24
26
|
}
|
|
25
27
|
export declare function connectClaudeCode(options: NativeConnectOptions): Promise<ConnectAuthMode | undefined>;
|
|
26
28
|
export declare function connectCodex(options: NativeConnectOptions): Promise<ConnectAuthMode | undefined>;
|
|
@@ -38,6 +40,8 @@ export declare function connectOpenClaw(options: {
|
|
|
38
40
|
verbose?: boolean;
|
|
39
41
|
/** Set by `--all`: repeated outro lines print once, after the summary. */
|
|
40
42
|
deferOutro?: (line: string) => void;
|
|
43
|
+
/** Targeted connects only: --all keeps its per-harness failed row. */
|
|
44
|
+
fallbackWhenMissing?: boolean;
|
|
41
45
|
}): Promise<void>;
|
|
42
46
|
/** Writes mcp.json directly — no `cursor mcp add` exists. */
|
|
43
47
|
export declare function connectCursor(options: {
|
package/dist/commands/connect.js
CHANGED
|
@@ -3,6 +3,7 @@ import { CONNECT_SOURCE, persistApiKeyToEnvironment, saveConnectContext, validat
|
|
|
3
3
|
import { CLAUDE_CODE, CODEX, CURSOR_SKILL_TARGET, GROK, HERMES, OPENCLAW, OPENCLAW_SKILL_INSTALL_ARGS, OPENCODE, launchNativeMcpClient, launchOpenClawWalkthrough, openExternalUrl, } from "../lib/connect-clients.js";
|
|
4
4
|
import { captureStdio, capturedOutput, HoistedFailureReportedError, installTinyFishCli, replay, } from "../lib/cli-install.js";
|
|
5
5
|
import { ensureCliAuthenticated } from "../lib/connect-auth.js";
|
|
6
|
+
import { createStdinPrompt, runCliFallback } from "../lib/connect-fallback.js";
|
|
6
7
|
import { installWebSkill } from "../lib/skill-install.js";
|
|
7
8
|
import { ConnectInterruptedError, ConnectStepError, createConnectTelemetry, finishSetupCommand, requireCommandSupport, runGuarded, settle, SignInTimeoutError, spawnStepError, throwIfInterrupted, NON_INTERACTIVE_TIMEOUT_MS, } from "../lib/connect-runtime.js";
|
|
8
9
|
import { cursorInstallDeeplink, cursorMcpPath, readCursorTinyfishEntry, writeCursorMcpConfig, } from "../lib/cursor-config.js";
|
|
@@ -58,13 +59,62 @@ function signInStepError(message, result, bounded) {
|
|
|
58
59
|
? new SignInTimeoutError(error.message, error.cause)
|
|
59
60
|
: error;
|
|
60
61
|
}
|
|
62
|
+
function isMissingHarness(error) {
|
|
63
|
+
return error instanceof ConnectStepError && error.failureReason === "harness_not_installed";
|
|
64
|
+
}
|
|
65
|
+
/** Undefined means the rescue ran; the caller returns immediately. */
|
|
66
|
+
async function requireSupportOrRescue(client, connectClient, state, telemetry, options) {
|
|
67
|
+
try {
|
|
68
|
+
return requireCommandSupport(client);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (!options.fallbackWhenMissing || !isMissingHarness(error))
|
|
72
|
+
throw error;
|
|
73
|
+
await runMissingHarnessFallback(client.displayName, connectClient, state, telemetry, options);
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// The harness attempt stays failed; the rescue rides fallback_outcome (PF-3707).
|
|
78
|
+
async function runMissingHarnessFallback(displayName, connectClient, state, telemetry, options) {
|
|
79
|
+
// Stamped before any step: Ctrl+C settles before an outcome exists.
|
|
80
|
+
telemetry.setFallbackOutcome("abandoned");
|
|
81
|
+
errLine(`${displayName} was not found on PATH. Setting up the TinyFish CLI instead.`);
|
|
82
|
+
const outcome = await runCliFallback({
|
|
83
|
+
apiKey: options.apiKey,
|
|
84
|
+
verbose: options.verbose ?? false,
|
|
85
|
+
prompt: detectHumanInitiated() ? createStdinPrompt() : undefined,
|
|
86
|
+
retryCommand: `npx @tiny-fish/cli connect ${connectClient}`,
|
|
87
|
+
hooks: {
|
|
88
|
+
// Terminal detail is explicit below; abort rows still name the step.
|
|
89
|
+
onStepStart: (stage) => {
|
|
90
|
+
state.stage = stage;
|
|
91
|
+
},
|
|
92
|
+
onStepDone: (phase) => telemetry.track("checkpoint", { phase }),
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
telemetry.setFallbackOutcome(outcome);
|
|
96
|
+
// Never `completed`: dashboards read that as "the harness install worked".
|
|
97
|
+
settle(state, telemetry, "failed", {
|
|
98
|
+
failedStage: "prerequisite_check",
|
|
99
|
+
failureReason: "harness_not_installed",
|
|
100
|
+
});
|
|
101
|
+
if (outcome !== "cli_verified") {
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
errLine("The TinyFish CLI is installed and working.");
|
|
106
|
+
errLine("Using a different agent? Run: tinyfish connect --all");
|
|
107
|
+
}
|
|
61
108
|
async function connectNativeMcpClient(client, options) {
|
|
62
109
|
const telemetry = createConnectTelemetry(options.mcpUrl, client.connectClient, options);
|
|
63
110
|
const state = { stage: "prerequisite_check", settled: false };
|
|
64
111
|
const probeAuthenticated = SIGN_IN_PROBES[client.connectClient];
|
|
65
|
-
await runGuarded(state, telemetry, () => {
|
|
112
|
+
await runGuarded(state, telemetry, async () => {
|
|
66
113
|
telemetry.track("started");
|
|
67
|
-
const
|
|
114
|
+
const support = await requireSupportOrRescue(client, client.connectClient, state, telemetry, options);
|
|
115
|
+
if (!support)
|
|
116
|
+
return;
|
|
117
|
+
const { optionalSupported, keyAuthSupported, harnessVersion } = support;
|
|
68
118
|
telemetry.setHarnessVersion(harnessVersion);
|
|
69
119
|
state.harnessDegraded = !!client.loginArgs && !optionalSupported;
|
|
70
120
|
// Prod MCP accepts X-API-Key, so a stored key replaces the browser OAuth hop.
|
|
@@ -293,9 +343,12 @@ export async function connectOpenClaw(options) {
|
|
|
293
343
|
settled: false,
|
|
294
344
|
harnessDegraded: false,
|
|
295
345
|
};
|
|
296
|
-
await runGuarded(state, telemetry, () => {
|
|
346
|
+
await runGuarded(state, telemetry, async () => {
|
|
297
347
|
telemetry.track("started");
|
|
298
|
-
|
|
348
|
+
const support = await requireSupportOrRescue(OPENCLAW, "openclaw", state, telemetry, options);
|
|
349
|
+
if (!support)
|
|
350
|
+
return;
|
|
351
|
+
telemetry.setHarnessVersion(support.harnessVersion);
|
|
299
352
|
telemetry.track("checkpoint", { phase: "prerequisite_ok" });
|
|
300
353
|
if (options.hoistedCliInstall) {
|
|
301
354
|
state.stage = "cli_install";
|
|
@@ -570,6 +623,8 @@ export function registerConnect(program) {
|
|
|
570
623
|
attemptId,
|
|
571
624
|
// A key that the install cannot use leaves only a browser hop no agent can finish.
|
|
572
625
|
keyAuthOnly: !detectHumanInitiated() && !!validatedApiKey(options.apiKey),
|
|
626
|
+
// Only here: a missing harness rescues via the CLI (PF-3707).
|
|
627
|
+
fallbackWhenMissing: true,
|
|
573
628
|
verbose: options.verbose ?? false,
|
|
574
629
|
};
|
|
575
630
|
const connector = CLIENT_CONNECTORS[client];
|
package/dist/lib/connect-all.js
CHANGED
|
@@ -2,11 +2,10 @@ import * as readline from "node:readline/promises";
|
|
|
2
2
|
import spawn from "cross-spawn";
|
|
3
3
|
import { connectClaudeCode, connectCodex, connectCursor, connectGrok, connectHermes, connectOpenClaw, connectOpencode, } from "../commands/connect.js";
|
|
4
4
|
import { loadConfig, validatedApiKey } from "./auth.js";
|
|
5
|
-
import { TINYFISH_API_KEY_VAR } from "./constants.js";
|
|
6
5
|
import { installTinyFishCli, } from "./cli-install.js";
|
|
7
6
|
import { HARNESS_CAPABILITIES, NATIVE_BY_HARNESS } from "./connect-clients.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import { createStdinPrompt, runCliFallback } from "./connect-fallback.js";
|
|
8
|
+
import { ConnectInterruptedError, ConnectStepError, SignInTimeoutError, finishSetupCommand, } from "./connect-runtime.js";
|
|
10
9
|
import { pickHarnesses } from "./connect-picker.js";
|
|
11
10
|
import { commandOnPath, detectInstalledHarnesses, HARNESS_COMMANDS, HARNESS_DISPLAY_NAMES as DISPLAY_NAMES, ALL_HARNESSES, } from "./harness-detect.js";
|
|
12
11
|
import { detectHumanInitiated } from "./harness.js";
|
|
@@ -184,9 +183,14 @@ async function processHarness(detection, opts, prompt, oauthBudget, hoistedCliIn
|
|
|
184
183
|
try {
|
|
185
184
|
await prompt(`Press enter to open the browser and sign in to TinyFish for ${DISPLAY_NAMES[harness]}... `);
|
|
186
185
|
}
|
|
187
|
-
catch {
|
|
188
|
-
// Ctrl+
|
|
189
|
-
|
|
186
|
+
catch (error) {
|
|
187
|
+
// Ctrl+C stops the run; Ctrl+D only skips this harness.
|
|
188
|
+
if (error instanceof ConnectInterruptedError) {
|
|
189
|
+
errLine("Setup interrupted — run the command again to finish.");
|
|
190
|
+
process.exitCode = 130;
|
|
191
|
+
return { ...base, outcome: "interrupted", fixCommand };
|
|
192
|
+
}
|
|
193
|
+
return { ...base, outcome: "no_tty_auth_skip", fixCommand };
|
|
190
194
|
}
|
|
191
195
|
}
|
|
192
196
|
// runGuarded swallows Ctrl+C and sets exitCode 130 instead of throwing.
|
|
@@ -361,96 +365,10 @@ function computeExitCode(results) {
|
|
|
361
365
|
return 0;
|
|
362
366
|
return attempted.some((r) => r.outcome === "installed") ? 0 : 1;
|
|
363
367
|
}
|
|
364
|
-
const SEARCH_RETRY = 'tinyfish search query --pretty -- "<your query>"';
|
|
365
|
-
function searchRetry(query) {
|
|
366
|
-
if (process.platform === "win32")
|
|
367
|
-
return SEARCH_RETRY;
|
|
368
|
-
return `tinyfish search query --pretty -- '${query.replaceAll("'", "'\\''")}'`;
|
|
369
|
-
}
|
|
370
|
-
async function runCliFallback(opts, prompt) {
|
|
371
|
-
try {
|
|
372
|
-
installTinyFishCli({ verbose: opts.verbose ?? false, announce: true });
|
|
373
|
-
}
|
|
374
|
-
catch (error) {
|
|
375
|
-
if (error instanceof ConnectInterruptedError)
|
|
376
|
-
throw error;
|
|
377
|
-
errLine(error instanceof Error ? error.message : String(error));
|
|
378
|
-
outLine("Retry: npx @tiny-fish/cli connect --all");
|
|
379
|
-
return 1;
|
|
380
|
-
}
|
|
381
|
-
const apiKey = opts.apiKey ?? process.env[TINYFISH_API_KEY_VAR];
|
|
382
|
-
if (!prompt && !apiKey) {
|
|
383
|
-
outLine("Retry in a terminal: npx @tiny-fish/cli connect --all");
|
|
384
|
-
return 1;
|
|
385
|
-
}
|
|
386
|
-
try {
|
|
387
|
-
ensureCliAuthenticated("cli", apiKey);
|
|
388
|
-
}
|
|
389
|
-
catch (error) {
|
|
390
|
-
if (error instanceof ConnectInterruptedError)
|
|
391
|
-
throw error;
|
|
392
|
-
errLine(error instanceof Error ? error.message : String(error));
|
|
393
|
-
outLine(error instanceof ConnectStepError && error.failureReason === "command_not_found"
|
|
394
|
-
? "Retry in a new terminal: tinyfish auth login"
|
|
395
|
-
: "Retry: tinyfish auth login");
|
|
396
|
-
return 1;
|
|
397
|
-
}
|
|
398
|
-
if (!prompt) {
|
|
399
|
-
outLine(`Retry in a terminal: ${SEARCH_RETRY}`);
|
|
400
|
-
return 1;
|
|
401
|
-
}
|
|
402
|
-
let query;
|
|
403
|
-
try {
|
|
404
|
-
query = (await prompt("What would you like to search? ")).trim();
|
|
405
|
-
}
|
|
406
|
-
catch {
|
|
407
|
-
outLine(`Retry: ${SEARCH_RETRY}`);
|
|
408
|
-
return 1;
|
|
409
|
-
}
|
|
410
|
-
if (!query) {
|
|
411
|
-
outLine(`Retry: ${SEARCH_RETRY}`);
|
|
412
|
-
return 1;
|
|
413
|
-
}
|
|
414
|
-
const search = spawn.sync("tinyfish", ["search", "query", "--pretty", "--", query], {
|
|
415
|
-
stdio: "inherit",
|
|
416
|
-
env: { ...process.env, TF_SETUP_VERIFICATION: "1" },
|
|
417
|
-
});
|
|
418
|
-
if (commandNotFound(search.error)) {
|
|
419
|
-
errLine(TINYFISH_CLI_NOT_FOUND_MESSAGE);
|
|
420
|
-
outLine(`Retry in a new terminal: ${searchRetry(query)}`);
|
|
421
|
-
return 1;
|
|
422
|
-
}
|
|
423
|
-
throwIfInterrupted(search);
|
|
424
|
-
if (search.error || search.status !== 0) {
|
|
425
|
-
outLine(`Retry: ${searchRetry(query)}`);
|
|
426
|
-
return 1;
|
|
427
|
-
}
|
|
428
|
-
return 0;
|
|
429
|
-
}
|
|
430
368
|
export async function runConnectAll(opts) {
|
|
431
369
|
const isTTY = detectHumanInitiated();
|
|
432
|
-
let stdinClosed = false;
|
|
433
370
|
// Open interface holds stdin raw; hermes' input() then wedges.
|
|
434
|
-
const prompt = isTTY
|
|
435
|
-
? async (question) => {
|
|
436
|
-
if (stdinClosed)
|
|
437
|
-
throw new Error("stdin is closed");
|
|
438
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
439
|
-
// Without a close-linked signal, question() never settles on Ctrl+D.
|
|
440
|
-
const closed = new AbortController();
|
|
441
|
-
rl.on("close", () => closed.abort());
|
|
442
|
-
try {
|
|
443
|
-
return await rl.question(question, { signal: closed.signal });
|
|
444
|
-
}
|
|
445
|
-
catch (e) {
|
|
446
|
-
stdinClosed = true;
|
|
447
|
-
throw e;
|
|
448
|
-
}
|
|
449
|
-
finally {
|
|
450
|
-
rl.close();
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
: undefined;
|
|
371
|
+
const prompt = isTTY ? createStdinPrompt() : undefined;
|
|
454
372
|
const detections = detectInstalledHarnesses();
|
|
455
373
|
const gateKey = validatedApiKey(opts.apiKey);
|
|
456
374
|
// Keyed installs first: working tools land before sign-ins can block.
|
|
@@ -577,7 +495,13 @@ export async function runConnectAll(opts) {
|
|
|
577
495
|
if (!results.some(actionable)) {
|
|
578
496
|
if (!opts.dryRun && !opts.uninstall) {
|
|
579
497
|
try {
|
|
580
|
-
|
|
498
|
+
const outcome = await runCliFallback({
|
|
499
|
+
apiKey: opts.apiKey,
|
|
500
|
+
verbose: opts.verbose ?? false,
|
|
501
|
+
prompt,
|
|
502
|
+
retryCommand: "npx @tiny-fish/cli connect --all",
|
|
503
|
+
});
|
|
504
|
+
return outcome === "cli_verified" ? 0 : 1;
|
|
581
505
|
}
|
|
582
506
|
catch (error) {
|
|
583
507
|
if (!(error instanceof ConnectInterruptedError))
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type CliFallbackOutcome, type ConnectCheckpoint, type ConnectFailureStage } from "./connect-runtime.js";
|
|
2
|
+
export type Prompt = (question: string) => Promise<string>;
|
|
3
|
+
export declare const SEARCH_RETRY = "tinyfish search query --pretty -- \"<your query>\"";
|
|
4
|
+
export declare const DEFAULT_VERIFICATION_QUERY = "today's top Hacker News story";
|
|
5
|
+
export declare const MALFORMED_ENV_KEY_MESSAGE = "TINYFISH_API_KEY is set but malformed; it will override any stored key. Fix or unset it, then re-run.";
|
|
6
|
+
export declare const MALFORMED_STORED_KEY_MESSAGE = "The stored TinyFish API key is malformed. Run: tinyfish auth login";
|
|
7
|
+
export interface CliFallbackHooks {
|
|
8
|
+
onStepStart?: (stage: ConnectFailureStage) => void;
|
|
9
|
+
onStepDone?: (phase: ConnectCheckpoint) => void;
|
|
10
|
+
}
|
|
11
|
+
export interface CliFallbackOptions {
|
|
12
|
+
apiKey?: string;
|
|
13
|
+
verbose: boolean;
|
|
14
|
+
/** Absent means headless: no interactive login, no query prompt. */
|
|
15
|
+
prompt?: Prompt;
|
|
16
|
+
/** Whole-command retry line, per entry point. */
|
|
17
|
+
retryCommand: string;
|
|
18
|
+
/** Absent under --all's zero-harness path, which has no telemetry object. */
|
|
19
|
+
hooks?: CliFallbackHooks;
|
|
20
|
+
}
|
|
21
|
+
/** One readline per question; a held-open interface wedges raw-stdin children. */
|
|
22
|
+
export declare function createStdinPrompt(): Prompt;
|
|
23
|
+
/** Steps print retry lines and return outcomes; only interrupts throw. */
|
|
24
|
+
export declare function runCliFallback(opts: CliFallbackOptions): Promise<CliFallbackOutcome>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import * as readline from "node:readline/promises";
|
|
2
|
+
import spawn from "cross-spawn";
|
|
3
|
+
import { resolvedApiKey, validateKeyFormat } from "./auth.js";
|
|
4
|
+
import { installTinyFishCli } from "./cli-install.js";
|
|
5
|
+
import { ensureCliAuthenticated, TINYFISH_CLI_NOT_FOUND_MESSAGE } from "./connect-auth.js";
|
|
6
|
+
import { TINYFISH_API_KEY_VAR } from "./constants.js";
|
|
7
|
+
import { commandNotFound, ConnectInterruptedError, ConnectStepError, throwIfInterrupted, } from "./connect-runtime.js";
|
|
8
|
+
import { errLine, outLine } from "./output.js";
|
|
9
|
+
export const SEARCH_RETRY = 'tinyfish search query --pretty -- "<your query>"';
|
|
10
|
+
// Echoes the finale suggestion, so headless rescues rehearse the same task.
|
|
11
|
+
export const DEFAULT_VERIFICATION_QUERY = "today's top Hacker News story";
|
|
12
|
+
// Must undercut agent tool-call budgets (Claude Code Bash defaults to 120s).
|
|
13
|
+
const HEADLESS_VERIFY_TIMEOUT_MS = 90_000;
|
|
14
|
+
// An exported var must be unset, not re-logged-in.
|
|
15
|
+
export const MALFORMED_ENV_KEY_MESSAGE = "TINYFISH_API_KEY is set but malformed; it will override any stored key. Fix or unset it, then re-run.";
|
|
16
|
+
export const MALFORMED_STORED_KEY_MESSAGE = "The stored TinyFish API key is malformed. Run: tinyfish auth login";
|
|
17
|
+
function searchRetry(query) {
|
|
18
|
+
if (process.platform === "win32")
|
|
19
|
+
return SEARCH_RETRY;
|
|
20
|
+
return `tinyfish search query --pretty -- '${query.replaceAll("'", "'\\''")}'`;
|
|
21
|
+
}
|
|
22
|
+
/** One readline per question; a held-open interface wedges raw-stdin children. */
|
|
23
|
+
export function createStdinPrompt() {
|
|
24
|
+
let stdinClosed = false;
|
|
25
|
+
return async (question) => {
|
|
26
|
+
if (stdinClosed)
|
|
27
|
+
throw new Error("stdin is closed");
|
|
28
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
29
|
+
// Without a close-linked signal, question() never settles on Ctrl+D.
|
|
30
|
+
const closed = new AbortController();
|
|
31
|
+
rl.on("close", () => closed.abort());
|
|
32
|
+
// Raw mode swallows Ctrl+C here; resurface it as the interrupt.
|
|
33
|
+
let interrupted = false;
|
|
34
|
+
rl.on("SIGINT", () => {
|
|
35
|
+
interrupted = true;
|
|
36
|
+
rl.close();
|
|
37
|
+
});
|
|
38
|
+
try {
|
|
39
|
+
return await rl.question(question, { signal: closed.signal });
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (interrupted)
|
|
43
|
+
throw new ConnectInterruptedError("Setup interrupted");
|
|
44
|
+
stdinClosed = true;
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
rl.close();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Steps print retry lines and return outcomes; only interrupts throw. */
|
|
53
|
+
export async function runCliFallback(opts) {
|
|
54
|
+
const { hooks, prompt } = opts;
|
|
55
|
+
hooks?.onStepStart?.("cli_install");
|
|
56
|
+
try {
|
|
57
|
+
installTinyFishCli({ verbose: opts.verbose, announce: true });
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error instanceof ConnectInterruptedError)
|
|
61
|
+
throw error;
|
|
62
|
+
errLine(error instanceof Error ? error.message : String(error));
|
|
63
|
+
outLine(`Retry: ${opts.retryCommand}`);
|
|
64
|
+
return "cli_install_failed";
|
|
65
|
+
}
|
|
66
|
+
hooks?.onStepDone?.("cli_installed");
|
|
67
|
+
hooks?.onStepStart?.("authentication");
|
|
68
|
+
// Resolved raw: validatedApiKey collapses a malformed key into "no key".
|
|
69
|
+
const rawKey = resolvedApiKey(opts.apiKey);
|
|
70
|
+
if (rawKey && !validateKeyFormat(rawKey)) {
|
|
71
|
+
// Terminal even in a TTY: login writes config; env shadows it.
|
|
72
|
+
errLine(process.env[TINYFISH_API_KEY_VAR] ? MALFORMED_ENV_KEY_MESSAGE : MALFORMED_STORED_KEY_MESSAGE);
|
|
73
|
+
outLine(`Retry: ${opts.retryCommand}`);
|
|
74
|
+
return "invalid_credentials";
|
|
75
|
+
}
|
|
76
|
+
if (!prompt && !rawKey) {
|
|
77
|
+
outLine(`Retry in a terminal: ${opts.retryCommand}`);
|
|
78
|
+
return "no_credentials";
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
// Gate-only widening: a config-only key must not re-persist anywhere.
|
|
82
|
+
// Headless has no one to answer a login prompt; never inherit stdio there.
|
|
83
|
+
ensureCliAuthenticated("cli", opts.apiKey ?? process.env[TINYFISH_API_KEY_VAR], {
|
|
84
|
+
interactiveLogin: !!prompt,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (error instanceof ConnectInterruptedError)
|
|
89
|
+
throw error;
|
|
90
|
+
errLine(error instanceof Error ? error.message : String(error));
|
|
91
|
+
outLine(error instanceof ConnectStepError && error.failureReason === "command_not_found"
|
|
92
|
+
? "Retry in a new terminal: tinyfish auth login"
|
|
93
|
+
: "Retry: tinyfish auth login");
|
|
94
|
+
return "authentication_failed";
|
|
95
|
+
}
|
|
96
|
+
hooks?.onStepDone?.("authenticated");
|
|
97
|
+
hooks?.onStepStart?.("cli_verification");
|
|
98
|
+
let query;
|
|
99
|
+
if (prompt) {
|
|
100
|
+
try {
|
|
101
|
+
query = (await prompt("What would you like to search? ")).trim();
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (error instanceof ConnectInterruptedError)
|
|
105
|
+
throw error;
|
|
106
|
+
outLine(`Retry: ${SEARCH_RETRY}`);
|
|
107
|
+
return "abandoned";
|
|
108
|
+
}
|
|
109
|
+
if (!query) {
|
|
110
|
+
outLine(`Retry: ${SEARCH_RETRY}`);
|
|
111
|
+
return "abandoned";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
// Headless with a key: a canned query still proves the pipe.
|
|
116
|
+
query = DEFAULT_VERIFICATION_QUERY;
|
|
117
|
+
}
|
|
118
|
+
const search = spawn.sync("tinyfish", ["search", "query", "--pretty", "--", query], {
|
|
119
|
+
stdio: "inherit",
|
|
120
|
+
env: { ...process.env, TF_SETUP_VERIFICATION: "1" },
|
|
121
|
+
// A user-driven search must never be cut off.
|
|
122
|
+
timeout: prompt ? undefined : HEADLESS_VERIFY_TIMEOUT_MS,
|
|
123
|
+
});
|
|
124
|
+
if (commandNotFound(search.error)) {
|
|
125
|
+
errLine(TINYFISH_CLI_NOT_FOUND_MESSAGE);
|
|
126
|
+
outLine(`Retry in a new terminal: ${searchRetry(query)}`);
|
|
127
|
+
return "verification_failed";
|
|
128
|
+
}
|
|
129
|
+
throwIfInterrupted(search);
|
|
130
|
+
if (search.error || search.status !== 0) {
|
|
131
|
+
outLine(`Retry: ${searchRetry(query)}`);
|
|
132
|
+
return "verification_failed";
|
|
133
|
+
}
|
|
134
|
+
hooks?.onStepDone?.("cli_verified");
|
|
135
|
+
return "cli_verified";
|
|
136
|
+
}
|
|
@@ -2,11 +2,12 @@ import { AuthMode } from "./harness-detect.js";
|
|
|
2
2
|
export declare const NON_INTERACTIVE_TIMEOUT_MS = 10000;
|
|
3
3
|
export type AgentClient = "claude-code" | "codex" | "cursor" | "grok" | "hermes" | "openclaw" | "opencode";
|
|
4
4
|
type ConnectStage = "started" | "checkpoint" | "completed" | "failed" | "aborted" | "post_install_failed";
|
|
5
|
-
type ConnectFailureStage = "prerequisite_check" | "registration_cleanup" | "registration" | "registration_or_authentication" | "client_oauth" | "cli_install" | "skill_install" | "authentication" | "walkthrough_launch";
|
|
5
|
+
export type ConnectFailureStage = "prerequisite_check" | "registration_cleanup" | "registration" | "registration_or_authentication" | "client_oauth" | "cli_install" | "skill_install" | "authentication" | "walkthrough_launch" | "cli_verification";
|
|
6
6
|
export declare const CONNECT_FAILURE_REASONS: readonly ["harness_not_installed", "harness_command_unsupported", "harness_too_old", "key_auth_unsupported", "command_not_found", "timeout", "spawn_error", "nonzero_exit", "invalid_config", "user_abandoned", "process_terminated", "unexpected_error"];
|
|
7
7
|
export type ConnectFailureReason = (typeof CONNECT_FAILURE_REASONS)[number];
|
|
8
8
|
export type ConnectAuthMode = Exclude<AuthMode, AuthMode.Unknown> | "deferred";
|
|
9
|
-
type ConnectCheckpoint = "prerequisite_ok" | "registered" | "oauth_done" | "cli_installed" | "skill_installed" | "authenticated";
|
|
9
|
+
export type ConnectCheckpoint = "prerequisite_ok" | "registered" | "oauth_done" | "cli_installed" | "skill_installed" | "authenticated" | "cli_verified";
|
|
10
|
+
export type CliFallbackOutcome = "cli_verified" | "cli_install_failed" | "no_credentials" | "invalid_credentials" | "authentication_failed" | "verification_failed" | "abandoned";
|
|
10
11
|
/** Ctrl+C killed a setup child — abandonment, not error. */
|
|
11
12
|
export declare class ConnectInterruptedError extends Error {
|
|
12
13
|
}
|
|
@@ -60,6 +61,7 @@ export interface ConnectTelemetry {
|
|
|
60
61
|
attemptId: string;
|
|
61
62
|
track(stage: ConnectStage, detail?: ConnectStageDetail): void;
|
|
62
63
|
setHarnessVersion(version?: string): void;
|
|
64
|
+
setFallbackOutcome(outcome: CliFallbackOutcome): void;
|
|
63
65
|
flush(): Promise<void>;
|
|
64
66
|
}
|
|
65
67
|
/** Terminal events exactly once, whichever of flow or signal handler wins. */
|
|
@@ -210,6 +210,7 @@ export function createConnectTelemetry(mcpUrl, client, opts) {
|
|
|
210
210
|
const pending = [];
|
|
211
211
|
let lastTrackAt;
|
|
212
212
|
let harnessVersion;
|
|
213
|
+
let fallbackOutcome;
|
|
213
214
|
async function deliver(body) {
|
|
214
215
|
// Ahead of the key read; postConnectEvent's own opt-out check is too late to skip it.
|
|
215
216
|
if (telemetryDisabled())
|
|
@@ -229,6 +230,9 @@ export function createConnectTelemetry(mcpUrl, client, opts) {
|
|
|
229
230
|
setHarnessVersion(version) {
|
|
230
231
|
harnessVersion = version;
|
|
231
232
|
},
|
|
233
|
+
setFallbackOutcome(outcome) {
|
|
234
|
+
fallbackOutcome = outcome;
|
|
235
|
+
},
|
|
232
236
|
track(stage, detail) {
|
|
233
237
|
const now = Date.now();
|
|
234
238
|
// Clamped: a backward clock step would send a route-rejected negative.
|
|
@@ -244,6 +248,8 @@ export function createConnectTelemetry(mcpUrl, client, opts) {
|
|
|
244
248
|
failed_stage: detail?.failedStage,
|
|
245
249
|
phase: detail?.phase,
|
|
246
250
|
failure_reason: detail?.failureReason,
|
|
251
|
+
// Terminal rows only: rescue checkpoints stay wire-identical to normal ones.
|
|
252
|
+
fallback_outcome: stage === "failed" || stage === "aborted" ? fallbackOutcome : undefined,
|
|
247
253
|
harness_version: detail?.harnessVersion ?? harnessVersion,
|
|
248
254
|
auth_mode: detail?.authMode,
|
|
249
255
|
harness_degraded: detail?.harnessDegraded,
|