@tiny-fish/cli 0.26.1-next.226 → 0.27.0

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.
@@ -15,6 +15,10 @@ interface NativeConnectOptions {
15
15
  hoistedCliInstall?: TakeHoistedCliInstall;
16
16
  /** Lets `--all` warn in its summary instead of all-green. */
17
17
  onPostInstallFailed?: () => void;
18
+ /** Inherit subprocess stdio instead of replaying it only on failure. */
19
+ verbose?: boolean;
20
+ /** Set by `--all`: repeated outro lines print once, after the summary. */
21
+ deferOutro?: (line: string) => void;
18
22
  }
19
23
  export declare function connectClaudeCode(options: NativeConnectOptions): Promise<ConnectAuthMode | undefined>;
20
24
  export declare function connectCodex(options: NativeConnectOptions): Promise<ConnectAuthMode | undefined>;
@@ -29,12 +33,16 @@ export declare function connectOpenClaw(options: {
29
33
  attemptId?: string;
30
34
  /** Set by `--all`: the CLI install already ran once, upfront. */
31
35
  hoistedCliInstall?: TakeHoistedCliInstall;
36
+ verbose?: boolean;
37
+ /** Set by `--all`: repeated outro lines print once, after the summary. */
38
+ deferOutro?: (line: string) => void;
32
39
  }): Promise<void>;
33
40
  /** Writes mcp.json directly — no `cursor mcp add` exists. */
34
41
  export declare function connectCursor(options: {
35
42
  apiKey?: string;
36
43
  mcpUrl: string;
37
44
  attemptId?: string;
45
+ verbose?: boolean;
38
46
  }): Promise<void>;
39
47
  export declare function launchAgent(client: AgentClient): void;
40
48
  export declare function registerConnect(program: Command): void;
@@ -1,7 +1,7 @@
1
1
  import spawn from "cross-spawn";
2
2
  import { CONNECT_SOURCE, persistApiKeyToEnvironment, saveConnectContext, validateKeyFormat, validatedApiKey, } from "../lib/auth.js";
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
- import { HoistedFailureReportedError, installTinyFishCli, SKILL_INSTALL_TIMEOUT_MS, UPGRADE_HINT, } from "../lib/cli-install.js";
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
7
  import { ConnectInterruptedError, ConnectStepError, createConnectTelemetry, finishSetupCommand, requireCommandSupport, runGuarded, settle, spawnStepError, throwIfInterrupted, NON_INTERACTIVE_TIMEOUT_MS, } from "../lib/connect-runtime.js";
@@ -29,7 +29,7 @@ function removeExistingRegistration(client, removal) {
29
29
  throw spawnStepError(`Could not remove existing ${removal.label}: ${details}`, result);
30
30
  }
31
31
  /** false: fall back to OAuth — a config naming an unset variable authenticates nothing. */
32
- function exportApiKey(apiKey, displayName) {
32
+ function exportApiKey(apiKey, displayName, deferOutro) {
33
33
  try {
34
34
  persistApiKeyToEnvironment(apiKey);
35
35
  }
@@ -39,8 +39,14 @@ function exportApiKey(apiKey, displayName) {
39
39
  return false;
40
40
  }
41
41
  process.env["TINYFISH_API_KEY"] = apiKey;
42
- errLine(`Exported TINYFISH_API_KEY to your shell profile — ${displayName} reads the key from there, ` +
43
- "so restart it (or open a new terminal) before use.");
42
+ if (deferOutro) {
43
+ deferOutro("Exported TINYFISH_API_KEY to your shell profile — restart your agents (or open a new " +
44
+ "terminal) before use.");
45
+ }
46
+ else {
47
+ errLine(`Exported TINYFISH_API_KEY to your shell profile — ${displayName} reads the key from there, ` +
48
+ "so restart it (or open a new terminal) before use.");
49
+ }
44
50
  return true;
45
51
  }
46
52
  async function connectNativeMcpClient(client, options) {
@@ -57,7 +63,8 @@ async function connectNativeMcpClient(client, options) {
57
63
  ? { key: storedKey, spec: client.keyAuth }
58
64
  : undefined;
59
65
  // Before the removals: a failed export must not strand the old registration.
60
- if (keyedAdd?.spec.viaEnv && !exportApiKey(keyedAdd.key, client.displayName)) {
66
+ if (keyedAdd?.spec.viaEnv &&
67
+ !exportApiKey(keyedAdd.key, client.displayName, options.deferOutro)) {
61
68
  keyedAdd = undefined;
62
69
  }
63
70
  const useKeyAuth = !!keyedAdd;
@@ -89,9 +96,15 @@ async function connectNativeMcpClient(client, options) {
89
96
  const addArgs = keyedAdd
90
97
  ? keyedAdd.spec.addArgs(mcpUrl.toString(), keyedAdd.key)
91
98
  : client.addArgs(mcpUrl.toString(), options.mcpUrl);
92
- const addResult = spawn.sync(client.command, addArgs, { stdio: "inherit" });
99
+ // Capturing an inline-OAuth add hides the sign-in URL and hangs.
100
+ const addResult = spawn.sync(client.command, addArgs, client.nonInteractiveAdd || keyedAdd
101
+ ? captureStdio(options.verbose ?? false)
102
+ : { stdio: "inherit" });
93
103
  if (addResult.error || addResult.status !== 0) {
94
- throw spawnStepError(`Could not add TinyFish to ${client.displayName}`, addResult);
104
+ // Built first: its interrupt check must run before any replay.
105
+ const error = spawnStepError(`Could not add TinyFish to ${client.displayName}`, addResult);
106
+ replay(capturedOutput(addResult));
107
+ throw error;
95
108
  }
96
109
  telemetry.track("checkpoint", { phase: "registered" });
97
110
  // Codex runs OAuth inside `mcp add` and exits 0 either way, so the recorded state decides
@@ -167,7 +180,7 @@ function runPostInstallSteps(client, options, state, telemetry, signInDeferred)
167
180
  }
168
181
  if (client.skillAgent) {
169
182
  attempt("skill_install", () => {
170
- installWebSkill(client);
183
+ installWebSkill(client, { verbose: options.verbose ?? false });
171
184
  telemetry.track("checkpoint", { phase: "skill_installed" });
172
185
  });
173
186
  }
@@ -177,11 +190,12 @@ function runPostInstallSteps(client, options, state, telemetry, signInDeferred)
177
190
  ensureCliAuthenticated(client.connectClient, options.apiKey);
178
191
  telemetry.track("checkpoint", { phase: "authenticated" });
179
192
  });
180
- errLine(UPGRADE_HINT);
181
193
  if (client.postConnectNote)
182
194
  errLine(client.postConnectNote);
183
195
  if (!options.launch) {
184
- errLine(connectedLine(client, signInDeferred));
196
+ // Under --all the summary replaces this; sign-in hints still print.
197
+ if (signInDeferred || !options.deferOutro)
198
+ errLine(connectedLine(client, signInDeferred));
185
199
  }
186
200
  else {
187
201
  attempt("walkthrough_launch", () => {
@@ -219,8 +233,7 @@ function runCliInstallStep(options, telemetry) {
219
233
  }
220
234
  return;
221
235
  }
222
- // Verbose on purpose: first-run setup must not go silent.
223
- installTinyFishCli({ verbose: true });
236
+ installTinyFishCli({ verbose: options.verbose ?? false, announce: true });
224
237
  telemetry.track("checkpoint", { phase: "cli_installed" });
225
238
  }
226
239
  function finishSetupHint(connectClient) {
@@ -282,17 +295,17 @@ export async function connectOpenClaw(options) {
282
295
  }
283
296
  else if (options.cliInstall !== false) {
284
297
  state.stage = "cli_install";
285
- installTinyFishCli({ verbose: true });
298
+ installTinyFishCli({ verbose: options.verbose ?? false, announce: true });
286
299
  telemetry.track("checkpoint", { phase: "cli_installed" });
287
300
  }
288
301
  state.stage = "skill_install";
289
302
  errLine("Installing the TinyFish skill in OpenClaw...");
290
- const skillInstallResult = spawn.sync("openclaw", OPENCLAW_SKILL_INSTALL_ARGS, {
291
- stdio: "inherit",
292
- timeout: SKILL_INSTALL_TIMEOUT_MS,
293
- });
303
+ const skillInstallResult = spawn.sync("openclaw", OPENCLAW_SKILL_INSTALL_ARGS, captureStdio(options.verbose ?? false));
294
304
  if (skillInstallResult.error || skillInstallResult.status !== 0) {
295
- throw spawnStepError("Could not install the TinyFish skill in OpenClaw", skillInstallResult);
305
+ // Built first: its interrupt check must run before any replay.
306
+ const error = spawnStepError("Could not install the TinyFish skill in OpenClaw", skillInstallResult);
307
+ replay(capturedOutput(skillInstallResult));
308
+ throw error;
296
309
  }
297
310
  telemetry.track("checkpoint", { phase: "skill_installed" });
298
311
  // Stores the key before probing, so a passed --api-key never reaches an interactive login.
@@ -300,11 +313,12 @@ export async function connectOpenClaw(options) {
300
313
  ensureCliAuthenticated("openclaw", options.apiKey);
301
314
  telemetry.track("checkpoint", { phase: "authenticated" });
302
315
  saveConnectContext("openclaw", telemetry.attemptId);
303
- errLine(UPGRADE_HINT);
304
316
  // For openclaw the skill install and auth ARE functional; only the walkthrough is cosmetic.
305
317
  settle(state, telemetry, "completed");
306
318
  if (!options.launch) {
307
- errLine("TinyFish is connected. Open OpenClaw to start using it.");
319
+ // Under --all the summary already lists OpenClaw.
320
+ if (!options.deferOutro)
321
+ errLine("TinyFish is connected. Open OpenClaw to start using it.");
308
322
  return;
309
323
  }
310
324
  state.stage = "walkthrough_launch";
@@ -360,7 +374,7 @@ export async function connectCursor(options) {
360
374
  telemetry.track("started");
361
375
  telemetry.track("checkpoint", { phase: "prerequisite_ok" });
362
376
  state.stage = "skill_install";
363
- installWebSkill(CURSOR_SKILL_TARGET);
377
+ installWebSkill(CURSOR_SKILL_TARGET, { verbose: options.verbose ?? false });
364
378
  telemetry.track("checkpoint", { phase: "skill_installed" });
365
379
  // Auth first: a key lets the config carry an X-API-Key header, removing the sign-in hand-back.
366
380
  state.stage = "authentication";
@@ -455,6 +469,7 @@ export function registerConnect(program) {
455
469
  .option("--api-key <apiKey>", "TinyFish API key to persist for the installed skill")
456
470
  .option("--launch", "Launch the agent and start the TinyFish walkthrough")
457
471
  .option("--url <mcpUrl>", "MCP endpoint override")
472
+ .option("--verbose", "Print the full npm and skills output instead of only on failure")
458
473
  .action(async (client, options) => {
459
474
  if (options.apiKey && !validateKeyFormat(options.apiKey)) {
460
475
  throw new Error("Invalid --api-key value");
@@ -496,6 +511,7 @@ export function registerConnect(program) {
496
511
  dryRun: options.dryRun ?? false,
497
512
  uninstall: options.uninstall ?? false,
498
513
  noLaunch: options.skipLaunch === true,
514
+ verbose: options.verbose ?? false,
499
515
  });
500
516
  // Preserve the 130 runGuarded set on Ctrl+C.
501
517
  if (!process.exitCode)
@@ -519,6 +535,7 @@ export function registerConnect(program) {
519
535
  dryRun: false,
520
536
  uninstall: false,
521
537
  pick: humanInitiated,
538
+ verbose: options.verbose ?? false,
522
539
  });
523
540
  // Preserve the 130 runGuarded set on Ctrl+C.
524
541
  if (!process.exitCode)
@@ -532,6 +549,7 @@ export function registerConnect(program) {
532
549
  attemptId,
533
550
  // A key that the install cannot use leaves only a browser hop no agent can finish.
534
551
  keyAuthOnly: !detectHumanInitiated() && !!validatedApiKey(options.apiKey),
552
+ verbose: options.verbose ?? false,
535
553
  };
536
554
  if (client === "claude-code") {
537
555
  await connectClaudeCode(connectOptions);
@@ -1,9 +1,10 @@
1
1
  export declare const SKILL_INSTALL_TIMEOUT_MS = 120000;
2
- export declare const UPGRADE_HINT = "Run `tinyfish upgrade` any time to update the CLI and skill.";
3
2
  export declare const STEP_MAX_BUFFER: number;
4
3
  /** Quiet captures subprocess output and replays it only on failure. */
5
4
  export type InstallOptions = {
6
5
  verbose: boolean;
6
+ /** Progress line even when quiet; defaults to `verbose`. */
7
+ announce?: boolean;
7
8
  };
8
9
  /** Outcome of `--all`'s one npm install; error keeps ConnectStepError.failureReason. */
9
10
  export type HoistedCliInstall = {
@@ -39,4 +40,4 @@ export declare function capturedOutput(result: {
39
40
  }): string;
40
41
  /** A verbose run captured nothing, so this is a no-op. */
41
42
  export declare function replay(output: string): void;
42
- export declare function installTinyFishCli({ verbose }: InstallOptions): void;
43
+ export declare function installTinyFishCli({ verbose, announce }: InstallOptions): void;
@@ -5,7 +5,6 @@ import { installRoot } from "./install-root.js";
5
5
  import { errLine } from "./output.js";
6
6
  export const SKILL_INSTALL_TIMEOUT_MS = 120_000;
7
7
  const TINYFISH_CLI_INSTALL_SPEC = `${TINYFISH_CLI_PACKAGE}@latest`;
8
- export const UPGRADE_HINT = "Run `tinyfish upgrade` any time to update the CLI and skill.";
9
8
  // spawnSync caps piped output at 1 MiB by default.
10
9
  export const STEP_MAX_BUFFER = 10 * 1024 * 1024;
11
10
  /** Carried failure an earlier harness already reported. */
@@ -28,8 +27,8 @@ export function replay(output) {
28
27
  if (output.trim())
29
28
  errLine(output.trimEnd());
30
29
  }
31
- export function installTinyFishCli({ verbose }) {
32
- if (verbose)
30
+ export function installTinyFishCli({ verbose, announce }) {
31
+ if (announce ?? verbose)
33
32
  errLine("Installing the TinyFish CLI...");
34
33
  // Without the prefix, a curl install upgrades npm's global tree and never the PATH binary.
35
34
  const root = installRoot();
@@ -7,5 +7,7 @@ export interface ConnectAllOptions {
7
7
  noLaunch?: boolean;
8
8
  /** Bare connect: offer the checkbox picker over the detected harnesses. */
9
9
  pick?: boolean;
10
+ /** Inherit subprocess stdio instead of replaying it only on failure. */
11
+ verbose?: boolean;
10
12
  }
11
13
  export declare function runConnectAll(opts: ConnectAllOptions): Promise<number>;
@@ -11,7 +11,7 @@ import { commandOnPath, detectInstalledHarnesses, HARNESS_COMMANDS, HARNESS_DISP
11
11
  import { detectHumanInitiated } from "./harness.js";
12
12
  import { emitNotice } from "./notice.js";
13
13
  import { removeCursorMcpServer, planCursorWrite } from "./cursor-config.js";
14
- import { errLine, outLine } from "./output.js";
14
+ import { errLine, outLine, setErrIndent } from "./output.js";
15
15
  import { sendSetupCompleted } from "./setup-telemetry.js";
16
16
  import { verifyMcpAuth, verifyMcpHealth } from "./verify.js";
17
17
  // `codex mcp add` succeeds unauthenticated; OAuth lands at first tool use.
@@ -23,6 +23,7 @@ const TOO_OLD_REASONS = new Set([
23
23
  "harness_command_unsupported",
24
24
  ]);
25
25
  const AUTH_CHECK_CAPABLE = new Set(["cursor", "openclaw"]);
26
+ const STEP_INDENT = " ";
26
27
  // Descriptor-derived so a harness that cannot carry a key is skipped, not failed, when headless.
27
28
  // Cursor and OpenClaw hold the key themselves rather than handing it to a harness `mcp add`.
28
29
  const KEY_AUTH_CAPABLE = new Set([
@@ -44,7 +45,7 @@ const UNINSTALL_POINTERS = {
44
45
  // OpenCode ships no `mcp remove`, so the only removal is editing the config it wrote.
45
46
  opencode: "remove the tinyfish entry from ~/.config/opencode/opencode.json",
46
47
  };
47
- async function runHarnessConnect(harness, opts, keyAuthOnly, hoistedCliInstall, onPostInstallFailed) {
48
+ async function runHarnessConnect(harness, opts, keyAuthOnly, hoistedCliInstall, onPostInstallFailed, deferOutro) {
48
49
  const base = {
49
50
  apiKey: opts.apiKey,
50
51
  mcpUrl: opts.mcpUrl,
@@ -52,6 +53,8 @@ async function runHarnessConnect(harness, opts, keyAuthOnly, hoistedCliInstall,
52
53
  keyAuthOnly,
53
54
  hoistedCliInstall,
54
55
  onPostInstallFailed,
56
+ verbose: opts.verbose,
57
+ deferOutro,
55
58
  };
56
59
  if (harness === "claude-code")
57
60
  return connectClaudeCode(base);
@@ -66,6 +69,8 @@ async function runHarnessConnect(harness, opts, keyAuthOnly, hoistedCliInstall,
66
69
  mcpUrl: opts.mcpUrl,
67
70
  launch: false,
68
71
  hoistedCliInstall,
72
+ verbose: opts.verbose,
73
+ deferOutro,
69
74
  });
70
75
  return undefined;
71
76
  }
@@ -74,17 +79,17 @@ async function runHarnessConnect(harness, opts, keyAuthOnly, hoistedCliInstall,
74
79
  if (harness === "grok")
75
80
  return connectGrok(base);
76
81
  if (harness === "cursor") {
77
- await connectCursor({ apiKey: opts.apiKey, mcpUrl: opts.mcpUrl });
82
+ await connectCursor({ apiKey: opts.apiKey, mcpUrl: opts.mcpUrl, verbose: opts.verbose });
78
83
  return undefined;
79
84
  }
80
85
  // Explicit: a new harness reaching an untested branch must fail, not write Cursor's config.
81
86
  throw new Error(`No installer is wired for ${harness}`);
82
87
  }
83
88
  /** Undefined means Ctrl+C: the caller stops the whole run. */
84
- function runHoistedCliInstall() {
89
+ function runHoistedCliInstall(verbose) {
85
90
  const startedAt = Date.now();
86
91
  try {
87
- installTinyFishCli({ verbose: true });
92
+ installTinyFishCli({ verbose, announce: true });
88
93
  // Clamped: a backward clock step would send a route-rejected negative.
89
94
  return { ok: true, durationMs: Math.max(0, Date.now() - startedAt) };
90
95
  }
@@ -141,7 +146,7 @@ function uninstallHarness(harness, configPath) {
141
146
  }
142
147
  return { ...detection, outcome: "uninstalled" };
143
148
  }
144
- async function processHarness(detection, opts, prompt, hoistedCliInstall) {
149
+ async function processHarness(detection, opts, prompt, hoistedCliInstall, deferOutro) {
145
150
  const { harness, configPath, detected } = detection;
146
151
  const base = { harness, detected, configPath, installed: false };
147
152
  if (!detected)
@@ -188,7 +193,7 @@ async function processHarness(detection, opts, prompt, hoistedCliInstall) {
188
193
  // Headless: an install too old for key auth must fail, not open a browser.
189
194
  authMode = await runHarnessConnect(harness, opts, !isTTY, hoistedCliInstall, () => {
190
195
  postInstallFailed = true;
191
- });
196
+ }, deferOutro);
192
197
  }
193
198
  catch (e) {
194
199
  // Both reasons carry the same "flag not listed" message; the upgrade nudge is in it.
@@ -316,7 +321,7 @@ function searchRetry(query) {
316
321
  }
317
322
  async function runCliFallback(opts, prompt) {
318
323
  try {
319
- installTinyFishCli({ verbose: true });
324
+ installTinyFishCli({ verbose: opts.verbose ?? false, announce: true });
320
325
  }
321
326
  catch (error) {
322
327
  if (error instanceof ConnectInterruptedError)
@@ -426,7 +431,7 @@ export async function runConnectAll(opts) {
426
431
  let takeHoistedCli;
427
432
  if (cliNeeded) {
428
433
  errLine("First: the TinyFish CLI, shared by every harness.");
429
- const outcome = runHoistedCliInstall();
434
+ const outcome = runHoistedCliInstall(opts.verbose ?? false);
430
435
  if (!outcome) {
431
436
  errLine("Setup interrupted — run the command again to finish.");
432
437
  return 130;
@@ -439,6 +444,8 @@ export async function runConnectAll(opts) {
439
444
  return { outcome, firstTake };
440
445
  };
441
446
  }
447
+ // A Set dedupes: Codex and Grok defer the same export line.
448
+ const outro = new Set();
442
449
  const results = [];
443
450
  const stepTotal = detections.length - deselected.size;
444
451
  let step = 0;
@@ -457,7 +464,8 @@ export async function runConnectAll(opts) {
457
464
  errLine(`step ${step} of ${stepTotal} — ${DISPLAY_NAMES[harness]}`);
458
465
  let result;
459
466
  try {
460
- result = await processHarness(detection, opts, prompt, takeHoistedCli);
467
+ setErrIndent(STEP_INDENT);
468
+ result = await processHarness(detection, opts, prompt, takeHoistedCli, (line) => outro.add(line));
461
469
  }
462
470
  catch (e) {
463
471
  // Isolation: one harness throwing never stops the others.
@@ -471,6 +479,10 @@ export async function runConnectAll(opts) {
471
479
  fixCommand: `tinyfish connect ${harness}`,
472
480
  };
473
481
  }
482
+ finally {
483
+ // The summary and the outro sit outside any step.
484
+ setErrIndent("");
485
+ }
474
486
  results.push(result);
475
487
  // Ctrl+C means stop, not continue to the next harness.
476
488
  if (result.outcome === "interrupted")
@@ -495,6 +507,9 @@ export async function runConnectAll(opts) {
495
507
  outLine("TinyFish setup summary:");
496
508
  for (const result of results)
497
509
  outLine(summaryLine(result, opts.uninstall));
510
+ // Before the launch handover: anything printed after it is never seen.
511
+ for (const line of outro)
512
+ errLine(line);
498
513
  if (!results.some(actionable)) {
499
514
  if (!opts.dryRun && !opts.uninstall) {
500
515
  try {
@@ -14,6 +14,8 @@ export interface NativeMcpClient extends SupportedCommand {
14
14
  connectClient: Exclude<AgentClient, "openclaw">;
15
15
  /** Omitted where the `skills` CLI has no agent for this harness (Grok ships its own plugin). */
16
16
  skillAgent?: SkillAgent;
17
+ /** Capturable: this `mcp add` never runs OAuth inline; unset stays inherited. */
18
+ nonInteractiveAdd?: true;
17
19
  /** Replaces `addArgs` when a stored key is available: key auth removes the OAuth hop. */
18
20
  keyAuth?: {
19
21
  /** Key reaches the client through TINYFISH_API_KEY, not argv. */
@@ -84,6 +84,7 @@ export const CLAUDE_CODE = {
84
84
  command: "claude",
85
85
  connectClient: "claude-code",
86
86
  skillAgent: "claude-code",
87
+ nonInteractiveAdd: true,
87
88
  displayName: "Claude Code",
88
89
  supportCheck: {
89
90
  args: ["mcp", "--help"],
@@ -171,6 +172,7 @@ const grokAddArgs = (mcpUrl) => [
171
172
  export const GROK = {
172
173
  command: "grok",
173
174
  connectClient: "grok",
175
+ nonInteractiveAdd: true,
174
176
  displayName: "Grok",
175
177
  supportCheck: {
176
178
  args: ["mcp", "add", "--help"],
@@ -240,6 +242,7 @@ export const OPENCODE = {
240
242
  command: "opencode",
241
243
  connectClient: "opencode",
242
244
  skillAgent: "opencode",
245
+ nonInteractiveAdd: true,
243
246
  displayName: "OpenCode",
244
247
  supportCheck: {
245
248
  args: ["mcp", "add", "--help"],
@@ -7,6 +7,8 @@ export declare function out(data: unknown): void;
7
7
  /** The only writer for untrusted text: strips ANSI and C0/C1 controls. */
8
8
  export declare function sanitizeLine(text: string): string;
9
9
  export declare function outLine(line: string): void;
10
+ /** `connect --all` nests a harness's own lines under its step header. */
11
+ export declare function setErrIndent(prefix: string): void;
10
12
  export declare function errLine(line: string): void;
11
13
  /** Advisory stderr line, yellow on a terminal and plain everywhere else so piped output stays clean. */
12
14
  export declare function warnLine(line: string): void;
@@ -20,8 +20,15 @@ export function sanitizeLine(text) {
20
20
  export function outLine(line) {
21
21
  process.stdout.write(sanitizeLine(line) + "\n");
22
22
  }
23
+ let errIndent = "";
24
+ /** `connect --all` nests a harness's own lines under its step header. */
25
+ export function setErrIndent(prefix) {
26
+ errIndent = prefix;
27
+ }
23
28
  export function errLine(line) {
24
- process.stderr.write(sanitizeLine(line) + "\n");
29
+ const text = sanitizeLine(line);
30
+ // Per line, not once: a replayed capture carries newlines.
31
+ process.stderr.write((errIndent ? text.replace(/^(?=.)/gm, errIndent) : text) + "\n");
25
32
  }
26
33
  const WARN_ON = "\x1b[33m";
27
34
  const WARN_OFF = "\x1b[39m";
@@ -3,7 +3,7 @@ import { type SkillAgent } from "./connect-clients.js";
3
3
  export declare function installWebSkill(client: {
4
4
  skillAgent?: SkillAgent;
5
5
  displayName: string;
6
- }): void;
6
+ }, { verbose }: Pick<InstallOptions, "verbose">): void;
7
7
  /**
8
8
  * Refresh the skill we installed, for the harnesses we installed it in.
9
9
  *
@@ -70,19 +70,21 @@ function parseJson(text) {
70
70
  return undefined;
71
71
  }
72
72
  }
73
- export function installWebSkill(client) {
73
+ export function installWebSkill(client, { verbose }) {
74
74
  if (!client.skillAgent)
75
75
  return;
76
76
  errLine(`Installing the TinyFish web skill in ${client.displayName}...`);
77
- // Inherited stdio renders the spinner live as one overwriting line.
78
77
  const add = spawn.sync("npx", skillAddArgs([client.skillAgent]), {
79
- stdio: "inherit",
78
+ ...captureStdio(verbose),
80
79
  env: skillSpawnEnv(),
81
- timeout: SKILL_INSTALL_TIMEOUT_MS,
82
80
  });
83
81
  const failed = add.error || add.status !== 0 ? add : failedListVerdict();
84
82
  if (failed) {
85
- throw spawnStepError(`Could not install the TinyFish web skill in ${client.displayName}`, failed);
83
+ // Built first: its interrupt check must run before any replay.
84
+ const error = spawnStepError(`Could not install the TinyFish web skill in ${client.displayName}`, failed);
85
+ // The add carries the failure prose; the list is JSON only.
86
+ replay(capturedOutput(add));
87
+ throw error;
86
88
  }
87
89
  }
88
90
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.26.1-next.226",
3
+ "version": "0.27.0",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {