@tiny-fish/cli 0.39.1-next.311 → 0.40.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.
@@ -1,14 +1,15 @@
1
+ import * as fs from 'node:fs';
2
+ import * as os from 'node:os';
1
3
  import * as path from 'node:path';
2
4
  import spawn from 'cross-spawn';
3
- import { z } from 'zod';
4
5
  import { loadConfig } from './auth.js';
5
6
  import { captureStdio, capturedOutput, replay, SKILL_INSTALL_TIMEOUT_MS, STEP_MAX_BUFFER, } from './cli-install.js';
6
7
  import { CURSOR_SKILL_TARGET, NATIVE_MCP_CLIENTS, OPENCLAW, openclawSkillInstallArgs, } from './connect-clients.js';
7
- import { ConnectInterruptedError, ConnectStepError, probeSupportVariant, spawnFailureDetail, spawnStepError, throwIfInterrupted, } from './connect-runtime.js';
8
+ import { ConnectInterruptedError, ConnectStepError, probeSupportVariant, spawnStepError, throwIfInterrupted, } from './connect-runtime.js';
8
9
  import { CLI_AGENT_IDENTITY } from './constants.js';
9
- import { errLine, parseJson } from './output.js';
10
+ import { errLine, sanitizeLine } from './output.js';
10
11
  // Supports Hermes without node:util.styleText, so the installer still runs on Node 20.11.
11
- const SKILLS_CLI_PACKAGE = 'skills@1.5.15';
12
+ export const SKILLS_CLI_PACKAGE = 'skills@1.5.15';
12
13
  const TINYFISH_WEB_SKILL_SOURCE = 'tinyfish-io/tinyfish-cookbook';
13
14
  const TINYFISH_WEB_SKILL = 'use-tinyfish';
14
15
  /** `skills add` is an unconditional overwrite, so it doubles as the refresh path. */
@@ -44,27 +45,34 @@ const SKILL_ALREADY_CURRENT_PATTERN = /All global skills are up to date/;
44
45
  // A lock entry with no recorded hash is untrackable, so `skills` reports it as skipped rather
45
46
  // than failed. Left undetected that reads as "up to date" while nothing was refreshed.
46
47
  const SKILL_UNCHECKABLE_PATTERN = /cannot be checked automatically/;
47
- const skillListSchema = z.array(z.object({ name: z.string() }));
48
- // `add` exits 0 on per-skill failure; the piped list is the verdict.
49
- function failedListVerdict() {
50
- const list = spawn.sync('npx', ['-y', SKILLS_CLI_PACKAGE, 'list', '--global', '--json'], {
51
- encoding: 'utf8',
52
- env: skillSpawnEnv(),
53
- maxBuffer: STEP_MAX_BUFFER,
54
- timeout: SKILL_INSTALL_TIMEOUT_MS,
55
- });
56
- const listRan = !list.error && list.status === 0;
57
- if (listRan && skillListed(list.stdout ?? ''))
58
- return null;
59
- // A clean list that omits the skill is not a real exit (PF-3680).
60
- const tag = listRan ? 'skill_absent_after_add' : `skill_list_failed ${spawnFailureDetail(list)}`;
61
- return { result: list, tag };
48
+ // `skills` writes here for the universal agents, whose own config dirs it never touches.
49
+ function canonicalSkillsDir() {
50
+ return path.join(os.homedir(), '.agents', 'skills'); // nosemgrep: path-join-resolve-traversal -- fixed dir names under os.homedir()
62
51
  }
63
- // Entry presence is the whole verdict: `agents` names the harnesses `skills` detects,
64
- // so an install for a harness with no config dir yet lists none (PF-3680).
65
- function skillListed(stdout) {
66
- const parsed = skillListSchema.safeParse(parseJson(stdout));
67
- return parsed.success && parsed.data.some((skill) => skill.name === TINYFISH_WEB_SKILL);
52
+ // Where the harness reads. Both of the modes `add` picks land the skill here.
53
+ const SKILL_DIR_BY_AGENT = {
54
+ 'claude-code': () => path.join(agentHome('CLAUDE_CONFIG_DIR', '.claude'), 'skills'),
55
+ // The env value, not resolveHermesHome(): the child we spawn reads the env.
56
+ 'hermes-agent': () => path.join(agentHome('HERMES_HOME', '.hermes'), 'skills'),
57
+ codex: canonicalSkillsDir,
58
+ cursor: canonicalSkillsDir,
59
+ opencode: canonicalSkillsDir,
60
+ // `skills` writes pi's here whatever PI_CODING_AGENT_DIR says.
61
+ pi: () => path.join(os.homedir(), '.pi', 'agent', 'skills'), // nosemgrep: path-join-resolve-traversal -- fixed dir names under os.homedir()
62
+ };
63
+ function agentHome(override, fallback) {
64
+ return process.env[override]?.trim() || path.join(os.homedir(), fallback); // nosemgrep: path-join-resolve-traversal -- fixed dir names under os.homedir()
65
+ }
66
+ /** `add` exits 0 on per-agent failure, so the file it should have written is the verdict. */
67
+ function skillOnDisk(agent) {
68
+ // SKILL.md, not the dir: `skills` mkdirs before it copies, so a failed copy leaves one.
69
+ return fs.existsSync(path.join(SKILL_DIR_BY_AGENT[agent](), TINYFISH_WEB_SKILL, 'SKILL.md'));
70
+ }
71
+ // The route tail-slices to 500, so an over-long tail would cut the tag off the front.
72
+ const SKILL_DETAIL_TAIL_MAX_CHARS = 425;
73
+ function absentDetail(addOutput) {
74
+ const tail = sanitizeLine(addOutput).trim().slice(-SKILL_DETAIL_TAIL_MAX_CHARS);
75
+ return tail ? `skill_absent_after_add | ${tail}` : 'skill_absent_after_add';
68
76
  }
69
77
  export function installWebSkill(client, { verbose }) {
70
78
  if (!client.skillAgent)
@@ -74,14 +82,13 @@ export function installWebSkill(client, { verbose }) {
74
82
  ...captureStdio(verbose),
75
83
  env: skillSpawnEnv(),
76
84
  });
85
+ const output = capturedOutput(add);
77
86
  const addFailed = Boolean(add.error) || add.status !== 0;
78
- const verdict = addFailed ? null : failedListVerdict();
79
- const failed = addFailed ? add : verdict?.result;
80
- if (failed) {
87
+ const absent = !addFailed && !skillOnDisk(client.skillAgent);
88
+ if (addFailed || absent) {
81
89
  // Built first: its interrupt check must run before any replay.
82
- const error = spawnStepError(`Could not install the TinyFish web skill in ${client.displayName}`, failed, verdict?.tag);
83
- // The add carries the failure prose; the list is JSON only.
84
- replay(capturedOutput(add));
90
+ const error = spawnStepError(`Could not install the TinyFish web skill in ${client.displayName}`, add, absent ? absentDetail(output) : undefined);
91
+ replay(output);
85
92
  throw error;
86
93
  }
87
94
  }
@@ -168,18 +175,18 @@ function reinstallWebSkill(skillAgents, verbose) {
168
175
  timeout: SKILL_INSTALL_TIMEOUT_MS,
169
176
  });
170
177
  const output = capturedOutput(result);
171
- // Quiet capture stays (#4434); the list verdict replaced prose matching.
178
+ // Quiet capture stays (#4434); the on-disk check replaced prose matching.
172
179
  const addFailed = Boolean(result.error) || result.status !== 0;
173
- const failedList = addFailed ? null : failedListVerdict();
174
- const failed = addFailed || failedList !== null;
180
+ const missing = addFailed ? undefined : skillAgents.find((agent) => !skillOnDisk(agent));
181
+ const failed = addFailed || missing !== undefined;
175
182
  if (failed)
176
- throwIfInterrupted(failedList?.result ?? result);
183
+ throwIfInterrupted(result);
177
184
  if (verbose || failed)
178
185
  replay(output);
179
186
  if (failed) {
180
- throw new Error('Could not refresh the TinyFish web skill', {
181
- cause: (failedList?.result ?? result).error,
182
- });
187
+ // Upgrade telemetry carries no detail, so the agent has to ride the message.
188
+ const scope = missing ? ` for ${missing}` : '';
189
+ throw new Error(`Could not refresh the TinyFish web skill${scope}`, { cause: result.error });
183
190
  }
184
191
  }
185
192
  /** Fallback when no skill-bearing harness is recorded: refresh whatever `skills` tracks. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.39.1-next.311",
3
+ "version": "0.40.0",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {