@tiny-fish/cli 0.30.2-next.256 → 0.30.2-next.258

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.
Files changed (77) hide show
  1. package/dist/commands/auth.d.ts +1 -1
  2. package/dist/commands/auth.js +52 -52
  3. package/dist/commands/batch.d.ts +1 -1
  4. package/dist/commands/batch.js +54 -46
  5. package/dist/commands/browser.d.ts +1 -1
  6. package/dist/commands/browser.js +14 -14
  7. package/dist/commands/config-claude.d.ts +1 -1
  8. package/dist/commands/config-claude.js +22 -22
  9. package/dist/commands/connect.d.ts +3 -3
  10. package/dist/commands/connect.js +150 -150
  11. package/dist/commands/doctor.d.ts +4 -4
  12. package/dist/commands/doctor.js +104 -104
  13. package/dist/commands/fetch.d.ts +1 -1
  14. package/dist/commands/fetch.js +24 -27
  15. package/dist/commands/onboard.d.ts +1 -1
  16. package/dist/commands/onboard.js +50 -50
  17. package/dist/commands/profile.d.ts +3 -3
  18. package/dist/commands/profile.js +45 -45
  19. package/dist/commands/run.d.ts +1 -1
  20. package/dist/commands/run.js +78 -65
  21. package/dist/commands/runs.d.ts +1 -1
  22. package/dist/commands/runs.js +63 -50
  23. package/dist/commands/search.d.ts +1 -1
  24. package/dist/commands/search.js +16 -16
  25. package/dist/commands/upgrade.d.ts +2 -2
  26. package/dist/commands/upgrade.js +36 -36
  27. package/dist/commands/vault.d.ts +1 -1
  28. package/dist/commands/vault.js +48 -45
  29. package/dist/commands/wallet.d.ts +1 -1
  30. package/dist/commands/wallet.js +14 -14
  31. package/dist/index.js +2 -2
  32. package/dist/lib/auth.d.ts +5 -5
  33. package/dist/lib/auth.js +56 -56
  34. package/dist/lib/batch-store.d.ts +1 -1
  35. package/dist/lib/batch-store.js +4 -4
  36. package/dist/lib/chrome-cookies.d.ts +1 -1
  37. package/dist/lib/chrome-cookies.js +24 -24
  38. package/dist/lib/claude-config.js +37 -37
  39. package/dist/lib/cli-install.js +12 -12
  40. package/dist/lib/client.d.ts +2 -2
  41. package/dist/lib/client.js +61 -61
  42. package/dist/lib/connect-all.js +140 -134
  43. package/dist/lib/connect-auth.d.ts +2 -2
  44. package/dist/lib/connect-auth.js +15 -15
  45. package/dist/lib/connect-clients.d.ts +6 -6
  46. package/dist/lib/connect-clients.js +143 -143
  47. package/dist/lib/connect-fallback.d.ts +1 -1
  48. package/dist/lib/connect-fallback.js +38 -38
  49. package/dist/lib/connect-picker.d.ts +4 -4
  50. package/dist/lib/connect-picker.js +28 -28
  51. package/dist/lib/connect-runtime.d.ts +7 -7
  52. package/dist/lib/connect-runtime.js +61 -61
  53. package/dist/lib/constants.js +13 -13
  54. package/dist/lib/cursor-config.d.ts +1 -1
  55. package/dist/lib/cursor-config.js +30 -30
  56. package/dist/lib/doctor-report.d.ts +2 -2
  57. package/dist/lib/doctor-report.js +22 -22
  58. package/dist/lib/harness-detect.js +39 -39
  59. package/dist/lib/harness.js +9 -9
  60. package/dist/lib/hermes-env.js +26 -23
  61. package/dist/lib/install-root.js +12 -12
  62. package/dist/lib/notice.d.ts +1 -1
  63. package/dist/lib/notice.js +12 -12
  64. package/dist/lib/output.js +14 -14
  65. package/dist/lib/registration-detect.d.ts +3 -3
  66. package/dist/lib/registration-detect.js +82 -82
  67. package/dist/lib/setup-telemetry.d.ts +4 -4
  68. package/dist/lib/setup-telemetry.js +34 -34
  69. package/dist/lib/signals.js +1 -1
  70. package/dist/lib/skill-install.d.ts +3 -3
  71. package/dist/lib/skill-install.js +40 -40
  72. package/dist/lib/types.d.ts +8 -8
  73. package/dist/lib/verify.d.ts +1 -1
  74. package/dist/lib/verify.js +28 -28
  75. package/dist/program.d.ts +1 -1
  76. package/dist/program.js +28 -28
  77. package/package.json +18 -9
@@ -1,21 +1,21 @@
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";
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
9
  export const SEARCH_RETRY = 'tinyfish search query --pretty -- "<your query>"';
10
10
  // Echoes the finale suggestion, so headless rescues rehearse the same task.
11
11
  export const DEFAULT_VERIFICATION_QUERY = "today's top Hacker News story";
12
12
  // Must undercut agent tool-call budgets (Claude Code Bash defaults to 120s).
13
13
  const HEADLESS_VERIFY_TIMEOUT_MS = 90_000;
14
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";
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
17
  function searchRetry(query) {
18
- if (process.platform === "win32")
18
+ if (process.platform === 'win32')
19
19
  return SEARCH_RETRY;
20
20
  return `tinyfish search query --pretty -- '${query.replaceAll("'", "'\\''")}'`;
21
21
  }
@@ -24,14 +24,14 @@ export function createStdinPrompt() {
24
24
  let stdinClosed = false;
25
25
  return async (question) => {
26
26
  if (stdinClosed)
27
- throw new Error("stdin is closed");
27
+ throw new Error('stdin is closed');
28
28
  const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
29
29
  // Without a close-linked signal, question() never settles on Ctrl+D.
30
30
  const closed = new AbortController();
31
- rl.on("close", () => closed.abort());
31
+ rl.on('close', () => closed.abort());
32
32
  // Raw mode swallows Ctrl+C here; resurface it as the interrupt.
33
33
  let interrupted = false;
34
- rl.on("SIGINT", () => {
34
+ rl.on('SIGINT', () => {
35
35
  interrupted = true;
36
36
  rl.close();
37
37
  });
@@ -40,7 +40,7 @@ export function createStdinPrompt() {
40
40
  }
41
41
  catch (error) {
42
42
  if (interrupted)
43
- throw new ConnectInterruptedError("Setup interrupted");
43
+ throw new ConnectInterruptedError('Setup interrupted');
44
44
  stdinClosed = true;
45
45
  throw error;
46
46
  }
@@ -52,7 +52,7 @@ export function createStdinPrompt() {
52
52
  /** Steps print retry lines and return outcomes; only interrupts throw. */
53
53
  export async function runCliFallback(opts) {
54
54
  const { hooks, prompt } = opts;
55
- hooks?.onStepStart?.("cli_install");
55
+ hooks?.onStepStart?.('cli_install');
56
56
  try {
57
57
  installTinyFishCli({ verbose: opts.verbose, announce: true });
58
58
  }
@@ -61,26 +61,26 @@ export async function runCliFallback(opts) {
61
61
  throw error;
62
62
  errLine(error instanceof Error ? error.message : String(error));
63
63
  outLine(`Retry: ${opts.retryCommand}`);
64
- return "cli_install_failed";
64
+ return 'cli_install_failed';
65
65
  }
66
- hooks?.onStepDone?.("cli_installed");
67
- hooks?.onStepStart?.("authentication");
66
+ hooks?.onStepDone?.('cli_installed');
67
+ hooks?.onStepStart?.('authentication');
68
68
  // Resolved raw: validatedApiKey collapses a malformed key into "no key".
69
69
  const rawKey = resolvedApiKey(opts.apiKey);
70
70
  if (rawKey && !validateKeyFormat(rawKey)) {
71
71
  // Terminal even in a TTY: login writes config; env shadows it.
72
72
  errLine(process.env[TINYFISH_API_KEY_VAR] ? MALFORMED_ENV_KEY_MESSAGE : MALFORMED_STORED_KEY_MESSAGE);
73
73
  outLine(`Retry: ${opts.retryCommand}`);
74
- return "invalid_credentials";
74
+ return 'invalid_credentials';
75
75
  }
76
76
  if (!prompt && !rawKey) {
77
77
  outLine(`Retry in a terminal: ${opts.retryCommand}`);
78
- return "no_credentials";
78
+ return 'no_credentials';
79
79
  }
80
80
  try {
81
81
  // Gate-only widening: a config-only key must not re-persist anywhere.
82
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], {
83
+ ensureCliAuthenticated('cli', opts.apiKey ?? process.env[TINYFISH_API_KEY_VAR], {
84
84
  interactiveLogin: !!prompt,
85
85
  });
86
86
  }
@@ -88,49 +88,49 @@ export async function runCliFallback(opts) {
88
88
  if (error instanceof ConnectInterruptedError)
89
89
  throw error;
90
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";
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
95
  }
96
- hooks?.onStepDone?.("authenticated");
97
- hooks?.onStepStart?.("cli_verification");
96
+ hooks?.onStepDone?.('authenticated');
97
+ hooks?.onStepStart?.('cli_verification');
98
98
  let query;
99
99
  if (prompt) {
100
100
  try {
101
- query = (await prompt("What would you like to search? ")).trim();
101
+ query = (await prompt('What would you like to search? ')).trim();
102
102
  }
103
103
  catch (error) {
104
104
  if (error instanceof ConnectInterruptedError)
105
105
  throw error;
106
106
  outLine(`Retry: ${SEARCH_RETRY}`);
107
- return "abandoned";
107
+ return 'abandoned';
108
108
  }
109
109
  if (!query) {
110
110
  outLine(`Retry: ${SEARCH_RETRY}`);
111
- return "abandoned";
111
+ return 'abandoned';
112
112
  }
113
113
  }
114
114
  else {
115
115
  // Headless with a key: a canned query still proves the pipe.
116
116
  query = DEFAULT_VERIFICATION_QUERY;
117
117
  }
118
- const search = spawn.sync("tinyfish", ["search", "query", "--pretty", "--", query], {
119
- stdio: "inherit",
120
- env: { ...process.env, TF_SETUP_VERIFICATION: "1" },
118
+ const search = spawn.sync('tinyfish', ['search', 'query', '--pretty', '--', query], {
119
+ stdio: 'inherit',
120
+ env: { ...process.env, TF_SETUP_VERIFICATION: '1' },
121
121
  // A user-driven search must never be cut off.
122
122
  timeout: prompt ? undefined : HEADLESS_VERIFY_TIMEOUT_MS,
123
123
  });
124
124
  if (commandNotFound(search.error)) {
125
125
  errLine(TINYFISH_CLI_NOT_FOUND_MESSAGE);
126
126
  outLine(`Retry in a new terminal: ${searchRetry(query)}`);
127
- return "verification_failed";
127
+ return 'verification_failed';
128
128
  }
129
129
  throwIfInterrupted(search);
130
130
  if (search.error || search.status !== 0) {
131
131
  outLine(`Retry: ${searchRetry(query)}`);
132
- return "verification_failed";
132
+ return 'verification_failed';
133
133
  }
134
- hooks?.onStepDone?.("cli_verified");
135
- return "cli_verified";
134
+ hooks?.onStepDone?.('cli_verified');
135
+ return 'cli_verified';
136
136
  }
@@ -1,9 +1,9 @@
1
- import type { Readable } from "node:stream";
2
- import { type Harness, type HarnessDetection } from "./harness-detect.js";
3
- export type PickSelection = Harness[] | "cancel";
1
+ import type { Readable } from 'node:stream';
2
+ import { type Harness, type HarnessDetection } from './harness-detect.js';
3
+ export type PickSelection = Harness[] | 'cancel';
4
4
  export interface PickResult {
5
5
  selection: PickSelection;
6
- mode: "pick" | "pick_numeric";
6
+ mode: 'pick' | 'pick_numeric';
7
7
  }
8
8
  export type PickerStdin = Readable & {
9
9
  setRawMode?: (raw: boolean) => unknown;
@@ -1,23 +1,23 @@
1
- import * as readline from "node:readline";
2
- import { HARNESS_DISPLAY_NAMES } from "./harness-detect.js";
3
- const KEY_HELP = " ↑↓ move space toggle one a none/all q cancel";
1
+ import * as readline from 'node:readline';
2
+ import { HARNESS_DISPLAY_NAMES } from './harness-detect.js';
3
+ const KEY_HELP = ' ↑↓ move space toggle one a none/all q cancel';
4
4
  function header(count) {
5
- return `Found ${count} agent config${count === 1 ? "" : "s"} on this machine.\n\n`;
5
+ return `Found ${count} agent config${count === 1 ? '' : 's'} on this machine.\n\n`;
6
6
  }
7
7
  function connectedNote(item) {
8
- return item.connectedBefore ? " (previously connected)" : "";
8
+ return item.connectedBefore ? ' (previously connected)' : '';
9
9
  }
10
10
  function enterHint(checkedCount, total) {
11
11
  if (checkedCount === 0)
12
- return "press enter to cancel";
12
+ return 'press enter to cancel';
13
13
  if (checkedCount === total)
14
14
  return `press enter to connect all ${total}`;
15
15
  return `press enter to connect ${checkedCount} of ${total}`;
16
16
  }
17
17
  function frame(items, checked, cursor) {
18
- const rows = items.map((item, i) => ` ${i === cursor ? ">" : " "} [${checked[i] ? "x" : " "}] ${item.name}${connectedNote(item)}`);
18
+ const rows = items.map((item, i) => ` ${i === cursor ? '>' : ' '} [${checked[i] ? 'x' : ' '}] ${item.name}${connectedNote(item)}`);
19
19
  const count = checked.filter(Boolean).length;
20
- return [...rows, "", ` ${enterHint(count, items.length)}`, KEY_HELP, ""].join("\n");
20
+ return [...rows, '', ` ${enterHint(count, items.length)}`, KEY_HELP, ''].join('\n');
21
21
  }
22
22
  async function pickWithKeys(items, stdin, write) {
23
23
  const checked = items.map(() => true);
@@ -25,7 +25,7 @@ async function pickWithKeys(items, stdin, write) {
25
25
  let painted = 0;
26
26
  const paint = () => {
27
27
  const text = frame(items, checked, cursor);
28
- write((painted > 0 ? `\u001b[${painted}A\u001b[0J` : "") + text);
28
+ write((painted > 0 ? `\u001b[${painted}A\u001b[0J` : '') + text);
29
29
  painted = items.length + 3;
30
30
  };
31
31
  readline.emitKeypressEvents(stdin);
@@ -38,24 +38,24 @@ async function pickWithKeys(items, stdin, write) {
38
38
  return await new Promise((resolve, reject) => {
39
39
  onKeypress = (_chunk, key = {}) => {
40
40
  try {
41
- const name = key.name ?? "";
41
+ const name = key.name ?? '';
42
42
  // Raw mode eats terminal SIGINT, so Ctrl+C arrives only here.
43
- if ((key.ctrl && (name === "c" || name === "d")) || name === "q" || name === "escape") {
44
- resolve("cancel");
43
+ if ((key.ctrl && (name === 'c' || name === 'd')) || name === 'q' || name === 'escape') {
44
+ resolve('cancel');
45
45
  return;
46
46
  }
47
- if (name === "return" || name === "enter") {
47
+ if (name === 'return' || name === 'enter') {
48
48
  const picked = items.filter((_, i) => checked[i]).map((item) => item.harness);
49
- resolve(picked.length > 0 ? picked : "cancel");
49
+ resolve(picked.length > 0 ? picked : 'cancel');
50
50
  return;
51
51
  }
52
- if (name === "up" || name === "k")
52
+ if (name === 'up' || name === 'k')
53
53
  cursor = (cursor + items.length - 1) % items.length;
54
- else if (name === "down" || name === "j")
54
+ else if (name === 'down' || name === 'j')
55
55
  cursor = (cursor + 1) % items.length;
56
- else if (name === "space")
56
+ else if (name === 'space')
57
57
  checked[cursor] = !checked[cursor];
58
- else if (name === "a")
58
+ else if (name === 'a')
59
59
  checked.fill(!checked.every(Boolean));
60
60
  paint();
61
61
  }
@@ -63,19 +63,19 @@ async function pickWithKeys(items, stdin, write) {
63
63
  reject(error instanceof Error ? error : new Error(String(error)));
64
64
  }
65
65
  };
66
- stdin.on("keypress", onKeypress);
66
+ stdin.on('keypress', onKeypress);
67
67
  });
68
68
  }
69
69
  finally {
70
70
  // Restore before any child spawn can inherit raw stdin (PF-3635).
71
71
  if (onKeypress)
72
- stdin.removeListener("keypress", onKeypress);
72
+ stdin.removeListener('keypress', onKeypress);
73
73
  stdin.setRawMode(false);
74
74
  stdin.pause();
75
75
  }
76
76
  }
77
77
  function numberedRows(items) {
78
- return (items.map((item, i) => ` [${i + 1}] ${item.name}${connectedNote(item)}`).join("\n") + "\n");
78
+ return (items.map((item, i) => ` [${i + 1}] ${item.name}${connectedNote(item)}`).join('\n') + '\n');
79
79
  }
80
80
  function parseSubset(answer, items) {
81
81
  const tokens = answer.split(/[\s,]+/).filter(Boolean);
@@ -103,17 +103,17 @@ async function pickWithNumbers(items, prompt, write) {
103
103
  }
104
104
  catch {
105
105
  // Closed stdin: every later question would throw too.
106
- return "cancel";
106
+ return 'cancel';
107
107
  }
108
- if (answer === "")
108
+ if (answer === '')
109
109
  return items.map((item) => item.harness);
110
110
  if (/^[nq]$/i.test(answer))
111
- return "cancel";
111
+ return 'cancel';
112
112
  const subset = parseSubset(answer, items);
113
113
  if (subset)
114
114
  return subset;
115
115
  }
116
- return "cancel";
116
+ return 'cancel';
117
117
  }
118
118
  export async function pickHarnesses(detections, connectedBefore, io = {}) {
119
119
  const items = detections
@@ -125,12 +125,12 @@ export async function pickHarnesses(detections, connectedBefore, io = {}) {
125
125
  }));
126
126
  const stdin = io.stdin ?? process.stdin;
127
127
  const write = io.write ?? ((text) => void process.stderr.write(text));
128
- if (typeof stdin.setRawMode !== "function") {
128
+ if (typeof stdin.setRawMode !== 'function') {
129
129
  // No prompt means nothing to ask; keep --all semantics.
130
130
  const selection = io.prompt
131
131
  ? await pickWithNumbers(items, io.prompt, write)
132
132
  : items.map((item) => item.harness);
133
- return { selection, mode: "pick_numeric" };
133
+ return { selection, mode: 'pick_numeric' };
134
134
  }
135
- return { selection: await pickWithKeys(items, stdin, write), mode: "pick" };
135
+ return { selection: await pickWithKeys(items, stdin, write), mode: 'pick' };
136
136
  }
@@ -1,13 +1,13 @@
1
- import { AuthMode } from "./harness-detect.js";
1
+ import { AuthMode } from './harness-detect.js';
2
2
  export declare const NON_INTERACTIVE_TIMEOUT_MS = 10000;
3
- export type AgentClient = "claude-code" | "codex" | "cursor" | "grok" | "hermes" | "openclaw" | "opencode";
4
- type ConnectStage = "started" | "checkpoint" | "completed" | "failed" | "aborted" | "post_install_failed";
5
- export type ConnectFailureStage = "prerequisite_check" | "registration_cleanup" | "registration" | "registration_or_authentication" | "client_oauth" | "cli_install" | "skill_install" | "authentication" | "walkthrough_launch" | "cli_verification";
3
+ export type AgentClient = 'claude-code' | 'codex' | 'cursor' | 'grok' | 'hermes' | 'openclaw' | 'opencode';
4
+ type ConnectStage = 'started' | 'checkpoint' | 'completed' | 'failed' | 'aborted' | 'post_install_failed';
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
- export type ConnectAuthMode = Exclude<AuthMode, AuthMode.Unknown> | "deferred";
9
- export type ConnectCheckpoint = "prerequisite_ok" | "registered" | "oauth_done" | "cli_installed" | "skill_installed" | "authenticated" | "cli_verified" | "walkthrough_launched" | "walkthrough_prompt_printed";
10
- export type CliFallbackOutcome = "cli_verified" | "cli_install_failed" | "no_credentials" | "invalid_credentials" | "authentication_failed" | "verification_failed" | "abandoned";
8
+ export type ConnectAuthMode = Exclude<AuthMode, AuthMode.Unknown> | 'deferred';
9
+ export type ConnectCheckpoint = 'prerequisite_ok' | 'registered' | 'oauth_done' | 'cli_installed' | 'skill_installed' | 'authenticated' | 'cli_verified' | 'walkthrough_launched' | 'walkthrough_prompt_printed';
10
+ export type CliFallbackOutcome = 'cli_verified' | 'cli_install_failed' | 'no_credentials' | 'invalid_credentials' | 'authentication_failed' | 'verification_failed' | 'abandoned';
11
11
  /** Ctrl+C killed a setup child — abandonment, not error. */
12
12
  export declare class ConnectInterruptedError extends Error {
13
13
  }
@@ -1,26 +1,26 @@
1
- import { randomUUID } from "node:crypto";
2
- import spawn from "cross-spawn";
3
- import { CONNECT_ATTEMPT_ENV, isAttemptId, resolvedApiKey, takePendingConnectAttempt, } from "./auth.js";
4
- import { CLI_VERSION } from "./constants.js";
5
- import { detectHarness, detectHumanInitiated } from "./harness.js";
6
- import { errLine, sanitizeLine } from "./output.js";
7
- import { postConnectEvent, telemetryDisabled } from "./setup-telemetry.js";
8
- import { installSignalGuard } from "./signals.js";
1
+ import { randomUUID } from 'node:crypto';
2
+ import spawn from 'cross-spawn';
3
+ import { CONNECT_ATTEMPT_ENV, isAttemptId, resolvedApiKey, takePendingConnectAttempt, } from './auth.js';
4
+ import { CLI_VERSION } from './constants.js';
5
+ import { detectHarness, detectHumanInitiated } from './harness.js';
6
+ import { errLine, sanitizeLine } from './output.js';
7
+ import { postConnectEvent, telemetryDisabled } from './setup-telemetry.js';
8
+ import { installSignalGuard } from './signals.js';
9
9
  export const NON_INTERACTIVE_TIMEOUT_MS = 10_000;
10
10
  // Mirrors the route's Zod enum; tests both sides pin it.
11
11
  export const CONNECT_FAILURE_REASONS = [
12
- "harness_not_installed",
13
- "harness_command_unsupported",
14
- "harness_too_old",
15
- "key_auth_unsupported",
16
- "command_not_found",
17
- "timeout",
18
- "spawn_error",
19
- "nonzero_exit",
20
- "invalid_config",
21
- "user_abandoned",
22
- "process_terminated",
23
- "unexpected_error",
12
+ 'harness_not_installed',
13
+ 'harness_command_unsupported',
14
+ 'harness_too_old',
15
+ 'key_auth_unsupported',
16
+ 'command_not_found',
17
+ 'timeout',
18
+ 'spawn_error',
19
+ 'nonzero_exit',
20
+ 'invalid_config',
21
+ 'user_abandoned',
22
+ 'process_terminated',
23
+ 'unexpected_error',
24
24
  ];
25
25
  /** Ctrl+C killed a setup child — abandonment, not error. */
26
26
  export class ConnectInterruptedError extends Error {
@@ -40,25 +40,25 @@ class PrerequisiteError extends ConnectStepError {
40
40
  /** A bounded sign-in wait elapsed — unfinished, not broken. */
41
41
  export class SignInTimeoutError extends ConnectStepError {
42
42
  constructor(message, cause) {
43
- super(message, "timeout", { cause });
43
+ super(message, 'timeout', { cause });
44
44
  }
45
45
  }
46
46
  // A maxBuffer overflow is also killed with SIGTERM, and is a failure too.
47
- const NOT_INTERRUPTIONS = new Set(["ETIMEDOUT", "ENOBUFS"]);
47
+ const NOT_INTERRUPTIONS = new Set(['ETIMEDOUT', 'ENOBUFS']);
48
48
  export function throwIfInterrupted(result) {
49
49
  // spawn.sync kills a timed-out child with SIGTERM, so signal alone would misread a slow
50
50
  // network as the user walking away. A timeout is a failure and must report as one.
51
51
  const code = result.error?.code;
52
52
  if (code && NOT_INTERRUPTIONS.has(code))
53
53
  return;
54
- if (result.signal === "SIGINT") {
55
- throw new ConnectInterruptedError("Setup interrupted");
54
+ if (result.signal === 'SIGINT') {
55
+ throw new ConnectInterruptedError('Setup interrupted');
56
56
  }
57
57
  }
58
58
  /** Bounded removal spawn shared by install cleanup and --uninstall; Ctrl+C throws. */
59
59
  export function spawnRemoval(command, args, timeoutMs = NON_INTERACTIVE_TIMEOUT_MS) {
60
60
  const result = spawn.sync(command, [...args], {
61
- encoding: "utf8",
61
+ encoding: 'utf8',
62
62
  timeout: timeoutMs,
63
63
  });
64
64
  throwIfInterrupted(result);
@@ -69,16 +69,16 @@ export function spawnStepError(message, result) {
69
69
  throwIfInterrupted(result);
70
70
  const code = result.error?.code;
71
71
  const reason = !result.error
72
- ? "nonzero_exit"
73
- : code === "ENOENT"
74
- ? "command_not_found"
75
- : code === "ETIMEDOUT"
76
- ? "timeout"
77
- : "spawn_error";
72
+ ? 'nonzero_exit'
73
+ : code === 'ENOENT'
74
+ ? 'command_not_found'
75
+ : code === 'ETIMEDOUT'
76
+ ? 'timeout'
77
+ : 'spawn_error';
78
78
  return new ConnectStepError(message, reason, { cause: result.error });
79
79
  }
80
80
  export function commandNotFound(error) {
81
- return error?.code === "ENOENT";
81
+ return error?.code === 'ENOENT';
82
82
  }
83
83
  /** One command redoes every post-install step for a client. */
84
84
  export function finishSetupCommand(client) {
@@ -96,11 +96,11 @@ export function settle(state, telemetry, stage, detail) {
96
96
  function installConnectSignalGuard(state, telemetry) {
97
97
  return installSignalGuard(async (signal) => {
98
98
  if (!state.settled) {
99
- settle(state, telemetry, "aborted", {
99
+ settle(state, telemetry, 'aborted', {
100
100
  failedStage: state.stage,
101
- failureReason: signal === "SIGINT" ? "user_abandoned" : "process_terminated",
101
+ failureReason: signal === 'SIGINT' ? 'user_abandoned' : 'process_terminated',
102
102
  });
103
- errLine("Setup interrupted — run the command again to finish.");
103
+ errLine('Setup interrupted — run the command again to finish.');
104
104
  }
105
105
  // Settled spans the whole post-install phase; the `completed` POST may still be in flight.
106
106
  await telemetry.flush();
@@ -114,22 +114,22 @@ export async function runGuarded(state, telemetry, body) {
114
114
  }
115
115
  catch (error) {
116
116
  if (error instanceof ConnectInterruptedError) {
117
- settle(state, telemetry, "aborted", {
117
+ settle(state, telemetry, 'aborted', {
118
118
  failedStage: state.stage,
119
- failureReason: "user_abandoned",
119
+ failureReason: 'user_abandoned',
120
120
  });
121
- errLine("Setup interrupted — run the command again to finish.");
121
+ errLine('Setup interrupted — run the command again to finish.');
122
122
  process.exitCode = 130;
123
123
  return;
124
124
  }
125
125
  // An elapsed sign-in wait is abandonment, not breakage.
126
126
  if (error instanceof SignInTimeoutError) {
127
- settle(state, telemetry, "aborted", { failedStage: state.stage, failureReason: "timeout" });
127
+ settle(state, telemetry, 'aborted', { failedStage: state.stage, failureReason: 'timeout' });
128
128
  throw error;
129
129
  }
130
- settle(state, telemetry, "failed", {
130
+ settle(state, telemetry, 'failed', {
131
131
  failedStage: state.stage,
132
- failureReason: error instanceof ConnectStepError ? error.failureReason : "unexpected_error",
132
+ failureReason: error instanceof ConnectStepError ? error.failureReason : 'unexpected_error',
133
133
  ...(error instanceof ConnectStepError ? { harnessVersion: error.harnessVersion } : {}),
134
134
  });
135
135
  throw error;
@@ -141,38 +141,38 @@ export async function runGuarded(state, telemetry, body) {
141
141
  }
142
142
  export function requireCommandSupport(client) {
143
143
  const result = spawn.sync(client.command, client.supportCheck.args, {
144
- encoding: "utf8",
144
+ encoding: 'utf8',
145
145
  // Agent harnesses export FORCE_COLOR, which makes clients colourise even a piped --help.
146
- env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
146
+ env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' },
147
147
  timeout: NON_INTERACTIVE_TIMEOUT_MS,
148
148
  });
149
149
  // 127 is the shell's not-found code; a wrapper shim with no real binary behind it exits that.
150
150
  if (commandNotFound(result.error) || result.status === 127) {
151
- throw new PrerequisiteError(`${client.displayName} is not installed or not available on PATH.`, "harness_not_installed", { cause: result.error });
151
+ throw new PrerequisiteError(`${client.displayName} is not installed or not available on PATH.`, 'harness_not_installed', { cause: result.error });
152
152
  }
153
153
  if (result.error || result.status !== 0) {
154
154
  throwIfInterrupted(result);
155
155
  // A hung probe is a timeout, not a missing capability.
156
- if (result.error?.code === "ETIMEDOUT") {
157
- throw new ConnectStepError(client.supportCheck.unavailableMessage, "timeout", {
156
+ if (result.error?.code === 'ETIMEDOUT') {
157
+ throw new ConnectStepError(client.supportCheck.unavailableMessage, 'timeout', {
158
158
  cause: result.error,
159
159
  });
160
160
  }
161
161
  // It ran and exited non-zero, so the version is still answerable.
162
- throw new PrerequisiteError(client.supportCheck.unavailableMessage, "harness_command_unsupported", { cause: result.error, harnessVersion: probeHarnessVersion(client.command) });
162
+ throw new PrerequisiteError(client.supportCheck.unavailableMessage, 'harness_command_unsupported', { cause: result.error, harnessVersion: probeHarnessVersion(client.command) });
163
163
  }
164
164
  // Belt and braces with the colour env: a client that ignores NO_COLOR still has to match.
165
- const output = sanitizeLine(`${result.stdout ?? ""}\n${result.stderr ?? ""}`);
165
+ const output = sanitizeLine(`${result.stdout ?? ''}\n${result.stderr ?? ''}`);
166
166
  const listed = (patterns) => patterns.every((pattern) => pattern.test(output));
167
167
  const essential = listed(client.supportCheck.patterns);
168
168
  const optionalSupported = listed(client.supportCheck.optionalPatterns ?? []);
169
169
  const keyAuthPattern = client.supportCheck.keyAuthPattern;
170
170
  const keyAuthSupported = keyAuthPattern ? keyAuthPattern.test(output) : true;
171
- if ((!essential || !optionalSupported || !keyAuthSupported) && process.env["TINYFISH_DEBUG"]) {
172
- errLine(`${client.command} ${client.supportCheck.args.join(" ")} printed:\n${output.trim()}`);
171
+ if ((!essential || !optionalSupported || !keyAuthSupported) && process.env['TINYFISH_DEBUG']) {
172
+ errLine(`${client.command} ${client.supportCheck.args.join(' ')} printed:\n${output.trim()}`);
173
173
  }
174
174
  if (!essential) {
175
- throw new PrerequisiteError(client.supportCheck.unavailableMessage, "harness_too_old", {
175
+ throw new PrerequisiteError(client.supportCheck.unavailableMessage, 'harness_too_old', {
176
176
  harnessVersion: probeHarnessVersion(client.command),
177
177
  });
178
178
  }
@@ -184,16 +184,16 @@ export function requireCommandSupport(client) {
184
184
  }
185
185
  /** Best-effort; the server rejects non-printable characters and >32 chars. */
186
186
  function probeHarnessVersion(command) {
187
- const result = spawn.sync(command, ["--version"], {
188
- encoding: "utf8",
187
+ const result = spawn.sync(command, ['--version'], {
188
+ encoding: 'utf8',
189
189
  timeout: NON_INTERACTIVE_TIMEOUT_MS,
190
190
  });
191
191
  if (result.error || result.status !== 0)
192
192
  return undefined;
193
193
  // First line only: sanitizeLine keeps newlines, so stripping splices lines.
194
- const version = sanitizeLine(result.stdout ?? "")
195
- .split("\n")[0]
196
- .replace(/[^\x20-\x7E]/g, "")
194
+ const version = sanitizeLine(result.stdout ?? '')
195
+ .split('\n')[0]
196
+ .replace(/[^\x20-\x7E]/g, '')
197
197
  .trim();
198
198
  return version ? version.slice(0, 32) : undefined;
199
199
  }
@@ -215,7 +215,7 @@ export function createConnectTelemetry(mcpUrl, client, opts) {
215
215
  // A setup-page id (via --url, the env var, or one parked by login) joins "copied the
216
216
  // command" to this install attempt.
217
217
  const attemptId = opts?.attemptId ?? seeded ?? opts?.fallbackAttemptId ?? randomUUID();
218
- const endpoint = new URL("/api/cli/connect-event", mcpUrl).toString();
218
+ const endpoint = new URL('/api/cli/connect-event', mcpUrl).toString();
219
219
  const pending = [];
220
220
  let lastTrackAt;
221
221
  let harnessVersion;
@@ -230,7 +230,7 @@ export function createConnectTelemetry(mcpUrl, client, opts) {
230
230
  body,
231
231
  attempts: 2,
232
232
  apiKey: resolvedApiKey(opts?.apiKey),
233
- label: "connect",
233
+ label: 'connect',
234
234
  });
235
235
  }
236
236
  return {
@@ -258,7 +258,7 @@ export function createConnectTelemetry(mcpUrl, client, opts) {
258
258
  phase: detail?.phase,
259
259
  failure_reason: detail?.failureReason,
260
260
  // Terminal rows only: rescue checkpoints stay wire-identical to normal ones.
261
- fallback_outcome: stage === "failed" || stage === "aborted" ? fallbackOutcome : undefined,
261
+ fallback_outcome: stage === 'failed' || stage === 'aborted' ? fallbackOutcome : undefined,
262
262
  harness_version: detail?.harnessVersion ?? harnessVersion,
263
263
  auth_mode: detail?.authMode,
264
264
  harness_degraded: detail?.harnessDegraded,
@@ -269,11 +269,11 @@ export function createConnectTelemetry(mcpUrl, client, opts) {
269
269
  // Same signal the usage events carry, so TTY and headless connects can be split.
270
270
  is_human_initiated: detectHumanInitiated(),
271
271
  // "none", not absent — absent has to keep meaning "CLI too old".
272
- calling_harness: detectHarness() ?? "none",
272
+ calling_harness: detectHarness() ?? 'none',
273
273
  })).catch((error) => {
274
274
  // postConnectEvent is total, so this only fires if something inside deliver starts
275
275
  // throwing. Same channel postConnectEvent uses for its own failures.
276
- if (!process.env["TINYFISH_DEBUG"])
276
+ if (!process.env['TINYFISH_DEBUG'])
277
277
  return;
278
278
  errLine(`connect telemetry failed: ${error instanceof Error ? error.message : String(error)}`);
279
279
  }));