@tiny-fish/cli 0.50.1-next.399 → 0.50.1-next.401

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.
@@ -0,0 +1,4 @@
1
+ /** Connect installs the CLI for the skills; only a missing or older PATH copy needs one. */
2
+ export declare function cliInstallNeeded(): boolean;
3
+ /** SemVer order: a prerelease sorts before its release (`0.51.0-next.3` < `0.51.0`). */
4
+ export declare function isOlder(installed: string, current: string): boolean;
@@ -0,0 +1,54 @@
1
+ import * as path from 'node:path';
2
+ import spawn from 'cross-spawn';
3
+ import which from 'which';
4
+ import { CLI_VERSION } from './constants.js';
5
+ import { installRoot } from './install-root.js';
6
+ const VERSION_PROBE_TIMEOUT_MS = 10_000;
7
+ const SEMVER = /^\d+\.\d+\.\d+(?:-\S+)?$/;
8
+ /** Connect installs the CLI for the skills; only a missing or older PATH copy needs one. */
9
+ export function cliInstallNeeded() {
10
+ // Running from the global prefix means PATH already has this exact version.
11
+ if (installRoot())
12
+ return false;
13
+ // npx puts its own cache bin first on PATH, and that copy vanishes after the run.
14
+ const candidates = (which.sync('tinyfish', { all: true, nothrow: true }) ?? []).filter((candidate) => !candidate.split(path.sep).includes('_npx') && dirOnPath(candidate));
15
+ if (candidates.length === 0)
16
+ return true;
17
+ const installed = pathCliVersion(candidates[0]);
18
+ if (!installed)
19
+ return true;
20
+ return isOlder(installed, CLI_VERSION);
21
+ }
22
+ /** SemVer order: a prerelease sorts before its release (`0.51.0-next.3` < `0.51.0`). */
23
+ export function isOlder(installed, current) {
24
+ const [installedCore, installedPre] = installed.split(/-(.*)/);
25
+ const [currentCore, currentPre] = current.split(/-(.*)/);
26
+ const order = installedCore.localeCompare(currentCore, 'en', { numeric: true });
27
+ if (order !== 0)
28
+ return order < 0;
29
+ if (!installedPre || !currentPre)
30
+ return Boolean(installedPre) && !currentPre;
31
+ return installedPre.localeCompare(currentPre, 'en', { numeric: true }) < 0;
32
+ }
33
+ // which searches the cwd first on Windows; a skill run elsewhere would miss that copy.
34
+ function dirOnPath(candidate) {
35
+ const key = (dir) => {
36
+ const resolved = path.resolve(dir);
37
+ return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
38
+ };
39
+ const dirs = (process.env['PATH'] ?? '').split(path.delimiter).filter(Boolean).map(key);
40
+ return dirs.includes(key(path.dirname(candidate)));
41
+ }
42
+ function pathCliVersion(binary) {
43
+ const result = spawn.sync(binary, ['--version'], {
44
+ encoding: 'utf8',
45
+ timeout: VERSION_PROBE_TIMEOUT_MS,
46
+ });
47
+ if (result.error || result.status !== 0)
48
+ return undefined;
49
+ // Unparseable output stays undefined, which installs: today's behaviour.
50
+ return result.stdout
51
+ ?.split(/\r?\n/)
52
+ .map((line) => line.trim())
53
+ .find((line) => SEMVER.test(line));
54
+ }
@@ -4,6 +4,7 @@ import spawn from 'cross-spawn';
4
4
  import { connectHarness } from './connect-harness.js';
5
5
  import { loadConfig, validatedApiKey } from './auth.js';
6
6
  import { installTinyFishCli, } from './cli-install.js';
7
+ import { cliInstallNeeded } from './cli-presence.js';
7
8
  import { authGate, mapConnectError, OAUTH_SIGN_IN_TIMEOUT_MS } from './connect-all-auth.js';
8
9
  import { actionable, computeExitCode, reloadHint, renderSummary } from './connect-all-summary.js';
9
10
  import { planText, uninstallHarness, uninstallPlanText } from './connect-all-uninstall.js';
@@ -167,8 +168,12 @@ function hoistCliIfNeeded(detections, opts, willAttempt) {
167
168
  willAttempt(d.harness));
168
169
  if (!cliNeeded)
169
170
  return undefined;
170
- errLine('First: the TinyFish CLI, shared by every harness.');
171
- const outcome = runHoistedCliInstall(opts.verbose ?? false);
171
+ // A skip still reports cli_installed; the route rejects unknown phases.
172
+ let outcome = { ok: true, durationMs: 0 };
173
+ if (cliInstallNeeded()) {
174
+ errLine('First: the TinyFish CLI, shared by every harness.');
175
+ outcome = runHoistedCliInstall(opts.verbose ?? false);
176
+ }
172
177
  if (!outcome)
173
178
  return 'interrupted';
174
179
  let taken = false;
@@ -2,6 +2,7 @@ import * as readline from 'node:readline/promises';
2
2
  import spawn from 'cross-spawn';
3
3
  import { resolvedApiKey, validateKeyFormat } from './auth.js';
4
4
  import { installTinyFishCli } from './cli-install.js';
5
+ import { cliInstallNeeded } from './cli-presence.js';
5
6
  import { ensureCliAuthenticated, TINYFISH_CLI_NOT_FOUND_MESSAGE } from './connect-auth.js';
6
7
  import { TINYFISH_API_KEY_VAR } from './constants.js';
7
8
  import { commandNotFound, ConnectInterruptedError, ConnectStepError, throwIfInterrupted, } from './connect-runtime.js';
@@ -50,6 +51,8 @@ export function createStdinPrompt() {
50
51
  };
51
52
  }
52
53
  function installStep(opts) {
54
+ if (!cliInstallNeeded())
55
+ return null;
53
56
  try {
54
57
  installTinyFishCli({ verbose: opts.verbose, announce: true });
55
58
  return null;
@@ -1,5 +1,6 @@
1
1
  import { verifyMcpHealth } from './verify.js';
2
2
  import { installTinyFishCli } from './cli-install.js';
3
+ import { cliInstallNeeded } from './cli-presence.js';
3
4
  import { createStdinPrompt, runCliFallback } from './connect-fallback.js';
4
5
  import { detectHumanInitiated } from './harness.js';
5
6
  import { errLine } from './output.js';
@@ -77,7 +78,8 @@ export function runCliInstallStep(options, telemetry) {
77
78
  takeHoistedCliInstall(options.hoistedCliInstall, telemetry);
78
79
  return;
79
80
  }
80
- installTinyFishCli({ verbose: options.verbose ?? false, announce: true });
81
+ if (cliInstallNeeded())
82
+ installTinyFishCli({ verbose: options.verbose ?? false, announce: true });
81
83
  telemetry.track('checkpoint', { phase: 'cli_installed' });
82
84
  }
83
85
  export function walkthroughPhase(outcome) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.50.1-next.399",
3
+ "version": "0.50.1-next.401",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,7 +32,8 @@
32
32
  "type-check": "tsc --noEmit --project tsconfig.all.json",
33
33
  "prepublishOnly": "npm run build",
34
34
  "lint:budget": "bash ../../.github/scripts/oxlint-budget.sh sdk/cli",
35
- "format:check": "oxfmt --check ."
35
+ "format:check": "oxfmt --check .",
36
+ "test:e2e": "vitest --run --config vitest.e2e.config.ts"
36
37
  },
37
38
  "dependencies": {
38
39
  "@tiny-fish/sdk": "^0.8.0",