@tiny-fish/cli 0.29.1-next.239 → 0.29.1-next.241

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.
@@ -26,6 +26,8 @@ const NO_BROWSER_REASONS = new Set([
26
26
  "harness_not_installed",
27
27
  "command_not_found",
28
28
  ]);
29
+ // Naming a harness the user does not have is the noise PF-3708 cut.
30
+ const HIDDEN_ONCE_SOMETHING_WORKED = new Set(["deselected", "not_detected", "not_installed"]);
29
31
  const STEP_INDENT = " ";
30
32
  // Must undercut agent tool-call budgets (Claude Code Bash defaults to 120s).
31
33
  const OAUTH_SIGN_IN_TIMEOUT_MS = 90_000;
@@ -140,11 +142,11 @@ function uninstallHarness(harness, configPath) {
140
142
  async function processHarness(detection, opts, prompt, oauthBudget, hoistedCliInstall, deferOutro) {
141
143
  const { harness, configPath, detected } = detection;
142
144
  const base = { harness, detected, configPath, installed: false };
143
- if (!detected)
144
- return { ...base, outcome: "not_detected" };
145
- if (!opts.uninstall && isStaleConfig(detection)) {
146
- return { ...base, outcome: "stale_config", fixCommand: `tinyfish connect ${harness}` };
147
- }
145
+ const noWork = noWorkOutcome(detection, opts.uninstall);
146
+ if (noWork === "not_detected")
147
+ return { ...base, outcome: noWork };
148
+ if (noWork)
149
+ return { ...base, outcome: noWork, fixCommand: `tinyfish connect ${harness}` };
148
150
  // dryRun first: --dry-run --uninstall must plan, never reach removeCursorMcpServer().
149
151
  if (opts.dryRun) {
150
152
  const plan = opts.uninstall
@@ -210,6 +212,10 @@ async function processHarness(detection, opts, prompt, oauthBudget, hoistedCliIn
210
212
  if (spentBrowserSlot && e instanceof ConnectStepError && NO_BROWSER_REASONS.has(e.failureReason)) {
211
213
  oauthBudget.spent = false;
212
214
  }
215
+ // Detection saw a name on PATH; the probe proved nothing is behind it.
216
+ if (e instanceof ConnectStepError && e.failureReason === "harness_not_installed") {
217
+ return { ...base, detected: false, outcome: "not_installed" };
218
+ }
213
219
  // Both reasons carry the same "flag not listed" message; the upgrade nudge is in it.
214
220
  if (e instanceof ConnectStepError && TOO_OLD_REASONS.has(e.failureReason)) {
215
221
  return {
@@ -245,6 +251,12 @@ async function processHarness(detection, opts, prompt, oauthBudget, hoistedCliIn
245
251
  }
246
252
  function summaryLine(result, uninstall) {
247
253
  const name = DISPLAY_NAMES[result.harness];
254
+ if (result.outcome === "not_installed") {
255
+ const next = uninstall
256
+ ? "nothing to remove"
257
+ : `Install ${name}, then run: tinyfish connect ${result.harness}`;
258
+ return `- ${name} — not installed (\`${HARNESS_COMMANDS[result.harness]}\` is on PATH but does not run). ${next}`;
259
+ }
248
260
  if (!result.detected) {
249
261
  const next = uninstall
250
262
  ? "nothing to remove"
@@ -268,9 +280,6 @@ function summaryLine(result, uninstall) {
268
280
  ? `… ${name} — ${result.errorMessage}. Finish it in a terminal: ${result.fixCommand}`
269
281
  : `… ${name} — needs a browser sign-in. Run in a terminal: ${result.fixCommand}`;
270
282
  }
271
- if (result.outcome === "deselected") {
272
- return `… ${name} — skipped, not selected. Fix: ${result.fixCommand}`;
273
- }
274
283
  if (result.outcome === "uninstall_pointer") {
275
284
  return `… ${name} — not removed automatically (config is owned by ${name}). Run: ${result.fixCommand}`;
276
285
  }
@@ -307,6 +316,28 @@ function summaryLine(result, uninstall) {
307
316
  }
308
317
  return `✓ ${name} — installed, ${verifyLabel}`;
309
318
  }
319
+ /** Absent and unpicked harnesses are noise once something real happened. */
320
+ function renderSummary(results, uninstall) {
321
+ if (!results.some(actionable))
322
+ return results.map((r) => summaryLine(r, uninstall));
323
+ // Ids, not display names: they double as the `connect` argument.
324
+ const skipped = results.filter((r) => r.outcome === "deselected").map((r) => r.harness);
325
+ const lines = results
326
+ .filter((r) => !HIDDEN_ONCE_SOMETHING_WORKED.has(r.outcome))
327
+ .map((r) => summaryLine(r, uninstall));
328
+ if (skipped.length === 0)
329
+ return lines;
330
+ lines.push("", `Skipped (not selected): ${skipped.join(", ")}`);
331
+ return lines;
332
+ }
333
+ /** The states processHarness settles without work; stepTotal must agree with it. */
334
+ function noWorkOutcome(d, uninstall) {
335
+ if (!d.detected)
336
+ return "not_detected";
337
+ if (!uninstall && isStaleConfig(d))
338
+ return "stale_config";
339
+ return undefined;
340
+ }
310
341
  /** A config-writing connect needs no binary; every other harness spawns one. */
311
342
  function isStaleConfig(detection) {
312
343
  return (detection.detected &&
@@ -475,7 +506,9 @@ export async function runConnectAll(opts) {
475
506
  const outro = new Set();
476
507
  const results = [];
477
508
  const oauthBudget = { spent: false };
478
- const stepTotal = detections.length - deselected.size;
509
+ // Shares processHarness's no-work states so stepTotal cannot desync.
510
+ const stepped = (d) => !deselected.has(d.harness) && !noWorkOutcome(d, opts.uninstall);
511
+ const stepTotal = detections.filter(stepped).length;
479
512
  let step = 0;
480
513
  for (const detection of detections) {
481
514
  const { harness } = detection;
@@ -488,6 +521,9 @@ export async function runConnectAll(opts) {
488
521
  });
489
522
  continue;
490
523
  }
524
+ // The pad loop below records these; announcing them is the noise this command had.
525
+ if (!stepped(detection))
526
+ continue;
491
527
  step += 1;
492
528
  errLine(`step ${step} of ${stepTotal} — ${DISPLAY_NAMES[harness]}`);
493
529
  let result;
@@ -533,8 +569,8 @@ export async function runConnectAll(opts) {
533
569
  }
534
570
  outLine("");
535
571
  outLine("TinyFish setup summary:");
536
- for (const result of results)
537
- outLine(summaryLine(result, opts.uninstall));
572
+ for (const line of renderSummary(results, opts.uninstall))
573
+ outLine(line);
538
574
  // Before the launch handover: anything printed after it is never seen.
539
575
  for (const line of outro)
540
576
  errLine(line);
@@ -137,7 +137,8 @@ export function requireCommandSupport(client) {
137
137
  env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
138
138
  timeout: NON_INTERACTIVE_TIMEOUT_MS,
139
139
  });
140
- if (commandNotFound(result.error)) {
140
+ // 127 is the shell's not-found code; a wrapper shim with no real binary behind it exits that.
141
+ if (commandNotFound(result.error) || result.status === 127) {
141
142
  throw new PrerequisiteError(`${client.displayName} is not installed or not available on PATH.`, "harness_not_installed", { cause: result.error });
142
143
  }
143
144
  if (result.error || result.status !== 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.29.1-next.239",
3
+ "version": "0.29.1-next.241",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {