@tiny-fish/cli 0.33.1-next.279 → 0.33.1-next.281
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.
- package/dist/commands/connect.js +9 -4
- package/dist/commands/search.js +15 -1
- package/dist/lib/connect-auth.js +3 -1
- package/dist/lib/connect-clients.d.ts +2 -0
- package/dist/lib/connect-clients.js +3 -2
- package/dist/lib/connect-runtime.d.ts +9 -1
- package/dist/lib/connect-runtime.js +61 -33
- package/dist/lib/skill-install.js +13 -7
- package/package.json +1 -1
package/dist/commands/connect.js
CHANGED
|
@@ -72,7 +72,7 @@ function refuseKeylessInstall(client, keyAuthSupported) {
|
|
|
72
72
|
`Update it, then ${rerun}`, 'harness_too_old');
|
|
73
73
|
}
|
|
74
74
|
throw new ConnectStepError(`Connecting ${client.displayName} needs a TinyFish API key, and ${noSignIn}. ` +
|
|
75
|
-
`Run: tinyfish auth login, then ${rerun}`, 'invalid_config');
|
|
75
|
+
`Run: tinyfish auth login, then ${rerun}`, 'invalid_config', { failureDetail: 'keyless_install_refused' });
|
|
76
76
|
}
|
|
77
77
|
/** Resolved before the removals, so a refusal strands no registration. */
|
|
78
78
|
function keylessAddArgs(client, keyAuthSupported, oauthResource) {
|
|
@@ -92,8 +92,9 @@ async function verifyKeyBeforeSeeding(apiKey, displayName, mcpUrl) {
|
|
|
92
92
|
throw new ConnectStepError(`Could not reach TinyFish to check your API key (${verified.code ?? 'unknown'}), so ` +
|
|
93
93
|
`nothing was written to ${displayName}. Check your connection and retry.`, 'timeout');
|
|
94
94
|
}
|
|
95
|
+
// The tag carries the HTTP status; `verified.code` is authored prose.
|
|
95
96
|
throw new ConnectStepError(`Your TinyFish API key was rejected (${verified.code ?? 'unknown'}), so it was not ` +
|
|
96
|
-
`written to ${displayName}. Run: tinyfish auth login`, 'invalid_config');
|
|
97
|
+
`written to ${displayName}. Run: tinyfish auth login`, 'invalid_config', { failureDetail: `key_rejected status=${verified.status}` });
|
|
97
98
|
}
|
|
98
99
|
/** A bounded spawn's timeout means the sign-in never finished. */
|
|
99
100
|
function signInStepError(message, result, bounded) {
|
|
@@ -213,7 +214,9 @@ function performMcpAdd(client, options, plan, attemptId) {
|
|
|
213
214
|
const registration = seeded?.confirmRegistered();
|
|
214
215
|
if (registration && !registration.ok) {
|
|
215
216
|
failAdd(() => new ConnectStepError(`${client.displayName} reported success but TinyFish is not enabled there` +
|
|
216
|
-
`${registration.detail ? `: ${registration.detail}` : ''}.`, 'invalid_config'
|
|
217
|
+
`${registration.detail ? `: ${registration.detail}` : ''}.`, 'invalid_config',
|
|
218
|
+
// The tag keeps the path-bearing prose out of telemetry.
|
|
219
|
+
{ failureDetail: registration.tag }));
|
|
217
220
|
}
|
|
218
221
|
return seeded?.note;
|
|
219
222
|
}
|
|
@@ -423,6 +426,7 @@ function trackPostInstallFailure(displayName, state, telemetry, error) {
|
|
|
423
426
|
telemetry.track('post_install_failed', {
|
|
424
427
|
failedStage: state.stage,
|
|
425
428
|
failureReason: error instanceof ConnectStepError ? error.failureReason : 'unexpected_error',
|
|
429
|
+
failureDetail: error instanceof ConnectStepError ? (error.failureDetail ?? error.message) : undefined,
|
|
426
430
|
stageDurationMs: stageDurationOf(error),
|
|
427
431
|
});
|
|
428
432
|
}
|
|
@@ -570,8 +574,9 @@ export async function connectCursor(options) {
|
|
|
570
574
|
mcpUrl.searchParams.set('connect_attempt_id', telemetry.attemptId);
|
|
571
575
|
const result = writeCursorMcpConfig(mcpUrl.toString(), resolvedKey);
|
|
572
576
|
if (result.status === 'corrupt_skip') {
|
|
577
|
+
// The message fallback would leak the config path; tag instead.
|
|
573
578
|
throw new ConnectStepError(`Could not update ${cursorMcpPath()}: existing file could not be read or is not valid JSON (${result.error}). ` +
|
|
574
|
-
`Fix the file, then re-run: tinyfish connect cursor`, 'invalid_config');
|
|
579
|
+
`Fix the file, then re-run: tinyfish connect cursor`, 'invalid_config', { failureDetail: 'cursor_mcp_json_corrupt' });
|
|
575
580
|
}
|
|
576
581
|
if (result.backupPath) {
|
|
577
582
|
errLine(`Backed up existing Cursor MCP config to ${result.backupPath}`);
|
package/dist/commands/search.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { getApiKey } from '../lib/auth.js';
|
|
2
2
|
import { searchQuery } from '../lib/client.js';
|
|
3
|
-
import { handleApiError, out, outLine } from '../lib/output.js';
|
|
3
|
+
import { err, handleApiError, out, outLine } from '../lib/output.js';
|
|
4
4
|
function printPrettySearch(response) {
|
|
5
5
|
outLine(`Query: ${response.query}`);
|
|
6
6
|
outLine(`Total results: ${response.total_results}`);
|
|
7
|
+
outLine(`Page: ${response.page}`);
|
|
7
8
|
outLine('');
|
|
8
9
|
if (response.results.length === 0) {
|
|
9
10
|
outLine('No results.');
|
|
@@ -16,6 +17,16 @@ function printPrettySearch(response) {
|
|
|
16
17
|
outLine('');
|
|
17
18
|
}
|
|
18
19
|
}
|
|
20
|
+
// Blank strings coerce to 0, a valid page.
|
|
21
|
+
function parsePageOption(value) {
|
|
22
|
+
if (value === undefined)
|
|
23
|
+
return undefined;
|
|
24
|
+
if (!/^\d+$/.test(value) || Number(value) > 10) {
|
|
25
|
+
err({ error: `Invalid --page "${value}". Must be an integer between 0 and 10.` });
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
return Number(value);
|
|
29
|
+
}
|
|
19
30
|
export function registerSearch(program) {
|
|
20
31
|
const searchCmd = program
|
|
21
32
|
.command('search')
|
|
@@ -29,8 +40,10 @@ export function registerSearch(program) {
|
|
|
29
40
|
.option('--language <value>', 'Search language hint')
|
|
30
41
|
.option('--include-domains <value>', 'Comma-separated list of domains to restrict results to')
|
|
31
42
|
.option('--exclude-domains <value>', 'Comma-separated list of domains to exclude from results')
|
|
43
|
+
.option('--page <value>', 'Page number for pagination, starting from 0')
|
|
32
44
|
.option('--pretty', 'Human-readable output')
|
|
33
45
|
.action(async (query, opts) => {
|
|
46
|
+
const page = parsePageOption(opts.page);
|
|
34
47
|
const apiKey = getApiKey();
|
|
35
48
|
try {
|
|
36
49
|
const response = await searchQuery({
|
|
@@ -39,6 +52,7 @@ export function registerSearch(program) {
|
|
|
39
52
|
...(opts.language !== undefined ? { language: opts.language } : {}),
|
|
40
53
|
...(opts.includeDomains !== undefined ? { include_domains: opts.includeDomains } : {}),
|
|
41
54
|
...(opts.excludeDomains !== undefined ? { exclude_domains: opts.excludeDomains } : {}),
|
|
55
|
+
...(page !== undefined ? { page } : {}),
|
|
42
56
|
}, apiKey);
|
|
43
57
|
if (opts.pretty) {
|
|
44
58
|
printPrettySearch(response);
|
package/dist/lib/connect-auth.js
CHANGED
|
@@ -8,7 +8,9 @@ export function ensureCliAuthenticated(source, apiKey, opts) {
|
|
|
8
8
|
const envKey = apiKey ?? process.env[TINYFISH_API_KEY_VAR];
|
|
9
9
|
if (envKey) {
|
|
10
10
|
if (!validateKeyFormat(envKey)) {
|
|
11
|
-
throw new ConnectStepError('TINYFISH_API_KEY has invalid format', 'invalid_config'
|
|
11
|
+
throw new ConnectStepError('TINYFISH_API_KEY has invalid format', 'invalid_config', {
|
|
12
|
+
failureDetail: 'env_key_invalid_format',
|
|
13
|
+
});
|
|
12
14
|
}
|
|
13
15
|
// Throwing variant: a write failure must reach connect's catch/flush, not exit(1) past it.
|
|
14
16
|
writeConfig(envKey);
|
|
@@ -32,6 +32,7 @@ export interface SeededInstall {
|
|
|
32
32
|
confirmRegistered: () => {
|
|
33
33
|
ok: boolean;
|
|
34
34
|
detail?: string;
|
|
35
|
+
tag?: string;
|
|
35
36
|
};
|
|
36
37
|
}
|
|
37
38
|
interface BaseMcpClient extends SupportedCommand {
|
|
@@ -97,6 +98,7 @@ export declare const GROK: NativeMcpClient;
|
|
|
97
98
|
export declare function hermesRegistrationEnabled(home: string): {
|
|
98
99
|
ok: boolean;
|
|
99
100
|
detail?: string;
|
|
101
|
+
tag?: string;
|
|
100
102
|
};
|
|
101
103
|
export declare const HERMES: KeyRequiredMcpClient;
|
|
102
104
|
export declare const OPENCODE: NativeMcpClient;
|
|
@@ -256,13 +256,14 @@ export function hermesRegistrationEnabled(home) {
|
|
|
256
256
|
unreadable: `${hermesConfigPath(home)} could not be read`,
|
|
257
257
|
unparseable: `${hermesConfigPath(home)} could not be parsed`,
|
|
258
258
|
}[entry.state];
|
|
259
|
-
|
|
259
|
+
// The tag keeps the path-bearing prose out of telemetry.
|
|
260
|
+
return { ok: false, detail, tag: `hermes_entry_${entry.state}` };
|
|
260
261
|
}
|
|
261
262
|
function seedHermesKey(apiKey) {
|
|
262
263
|
const home = resolveHermesHome();
|
|
263
264
|
// A plain Error settles unexpected_error; cli_connect_stage is load-bearing.
|
|
264
265
|
if (!home) {
|
|
265
|
-
throw new ConnectStepError("Could not determine Hermes' home directory from `hermes dump`", 'invalid_config');
|
|
266
|
+
throw new ConnectStepError("Could not determine Hermes' home directory from `hermes dump`", 'invalid_config', { failureDetail: 'hermes_home_undetermined' });
|
|
266
267
|
}
|
|
267
268
|
const restore = captureHermesKeyRestore(home);
|
|
268
269
|
writeHermesKey(home, apiKey);
|
|
@@ -18,9 +18,11 @@ export declare class HoistedFailureReportedError extends Error {
|
|
|
18
18
|
export declare class ConnectStepError extends Error {
|
|
19
19
|
readonly failureReason: ConnectFailureReason;
|
|
20
20
|
readonly harnessVersion?: string;
|
|
21
|
+
readonly failureDetail?: string;
|
|
21
22
|
constructor(message: string, failureReason: ConnectFailureReason, opts?: {
|
|
22
23
|
harnessVersion?: string;
|
|
23
24
|
cause?: unknown;
|
|
25
|
+
failureDetail?: string;
|
|
24
26
|
});
|
|
25
27
|
}
|
|
26
28
|
/** Hoisted install predates this attempt's telemetry; the gap reads ~0 ms. */
|
|
@@ -38,11 +40,15 @@ export interface SpawnStepResult {
|
|
|
38
40
|
status?: number | null;
|
|
39
41
|
signal?: string | null;
|
|
40
42
|
error?: Error;
|
|
43
|
+
/** Inherited stdio leaves this null; only piped runs yield text. */
|
|
44
|
+
stderr?: unknown;
|
|
41
45
|
}
|
|
42
46
|
/** Bounded removal spawn shared by install cleanup and --uninstall; Ctrl+C throws. */
|
|
43
47
|
export declare function spawnRemoval(command: string, args: readonly string[], timeoutMs?: number): import("child_process").SpawnSyncReturns<string>;
|
|
48
|
+
/** Outcome plus a bounded stderr tail; the route sanitizes and caps the rest. */
|
|
49
|
+
export declare function spawnFailureDetail(result: SpawnStepResult): string;
|
|
44
50
|
/** Classifies a spawn failure; interrupts throw instead. */
|
|
45
|
-
export declare function spawnStepError(message: string, result: SpawnStepResult): ConnectStepError;
|
|
51
|
+
export declare function spawnStepError(message: string, result: SpawnStepResult, failureDetail?: string): ConnectStepError;
|
|
46
52
|
export declare function commandNotFound(error: unknown): boolean;
|
|
47
53
|
/** One command redoes every post-install step for a client. */
|
|
48
54
|
export declare function finishSetupCommand(client: AgentClient): string;
|
|
@@ -64,6 +70,8 @@ interface ConnectStageDetail {
|
|
|
64
70
|
harnessDegraded?: boolean;
|
|
65
71
|
/** Overrides the event-gap duration; the hoisted CLI install measures itself. */
|
|
66
72
|
stageDurationMs?: number;
|
|
73
|
+
/** Dropped from the wire unless `failedStage` is set (route refine). */
|
|
74
|
+
failureDetail?: string;
|
|
67
75
|
}
|
|
68
76
|
export interface ConnectTelemetry {
|
|
69
77
|
attemptId: string;
|
|
@@ -32,10 +32,12 @@ export class HoistedFailureReportedError extends Error {
|
|
|
32
32
|
export class ConnectStepError extends Error {
|
|
33
33
|
failureReason;
|
|
34
34
|
harnessVersion;
|
|
35
|
+
failureDetail;
|
|
35
36
|
constructor(message, failureReason, opts) {
|
|
36
37
|
super(message, { cause: opts?.cause });
|
|
37
38
|
this.failureReason = failureReason;
|
|
38
39
|
this.harnessVersion = opts?.harnessVersion;
|
|
40
|
+
this.failureDetail = opts?.failureDetail;
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
/** Hoisted install predates this attempt's telemetry; the gap reads ~0 ms. */
|
|
@@ -76,8 +78,19 @@ export function spawnRemoval(command, args, timeoutMs = NON_INTERACTIVE_TIMEOUT_
|
|
|
76
78
|
throwIfInterrupted(result);
|
|
77
79
|
return result;
|
|
78
80
|
}
|
|
81
|
+
// Sanitized before the cut so the count is not spent on invisible bytes.
|
|
82
|
+
const STDERR_TAIL_MAX_CHARS = 400;
|
|
83
|
+
/** Outcome plus a bounded stderr tail; the route sanitizes and caps the rest. */
|
|
84
|
+
export function spawnFailureDetail(result) {
|
|
85
|
+
const stderr = typeof result.stderr === 'string'
|
|
86
|
+
? sanitizeLine(result.stderr.trim()).slice(-STDERR_TAIL_MAX_CHARS)
|
|
87
|
+
: '';
|
|
88
|
+
// A signal kill leaves status null, where the signal is the diagnosis.
|
|
89
|
+
const outcome = result.signal ? `signal=${result.signal}` : `exit=${result.status}`;
|
|
90
|
+
return `${outcome}${stderr ? ` | ${stderr}` : ''}`;
|
|
91
|
+
}
|
|
79
92
|
/** Classifies a spawn failure; interrupts throw instead. */
|
|
80
|
-
export function spawnStepError(message, result) {
|
|
93
|
+
export function spawnStepError(message, result, failureDetail) {
|
|
81
94
|
throwIfInterrupted(result);
|
|
82
95
|
const code = result.error?.code;
|
|
83
96
|
const reason = !result.error
|
|
@@ -87,7 +100,8 @@ export function spawnStepError(message, result) {
|
|
|
87
100
|
: code === 'ETIMEDOUT'
|
|
88
101
|
? 'timeout'
|
|
89
102
|
: 'spawn_error';
|
|
90
|
-
|
|
103
|
+
const detail = failureDetail ?? (reason === 'nonzero_exit' ? spawnFailureDetail(result) : undefined);
|
|
104
|
+
return new ConnectStepError(message, reason, { cause: result.error, failureDetail: detail });
|
|
91
105
|
}
|
|
92
106
|
export function commandNotFound(error) {
|
|
93
107
|
return error?.code === 'ENOENT';
|
|
@@ -136,7 +150,11 @@ export async function runGuarded(state, telemetry, body) {
|
|
|
136
150
|
}
|
|
137
151
|
// An elapsed sign-in wait is abandonment, not breakage.
|
|
138
152
|
if (error instanceof SignInTimeoutError) {
|
|
139
|
-
settle(state, telemetry, 'aborted', {
|
|
153
|
+
settle(state, telemetry, 'aborted', {
|
|
154
|
+
failedStage: state.stage,
|
|
155
|
+
failureReason: 'timeout',
|
|
156
|
+
failureDetail: error.failureDetail ?? error.message,
|
|
157
|
+
});
|
|
140
158
|
throw error;
|
|
141
159
|
}
|
|
142
160
|
// An earlier attempt owns the shared failure's terminal row.
|
|
@@ -145,7 +163,12 @@ export async function runGuarded(state, telemetry, body) {
|
|
|
145
163
|
settle(state, telemetry, 'failed', {
|
|
146
164
|
failedStage: state.stage,
|
|
147
165
|
failureReason: error instanceof ConnectStepError ? error.failureReason : 'unexpected_error',
|
|
148
|
-
...(error instanceof ConnectStepError
|
|
166
|
+
...(error instanceof ConnectStepError
|
|
167
|
+
? {
|
|
168
|
+
harnessVersion: error.harnessVersion,
|
|
169
|
+
failureDetail: error.failureDetail ?? error.message,
|
|
170
|
+
}
|
|
171
|
+
: {}),
|
|
149
172
|
stageDurationMs: stageDurationOf(error),
|
|
150
173
|
});
|
|
151
174
|
throw error;
|
|
@@ -227,6 +250,12 @@ function takeSeedAttemptId() {
|
|
|
227
250
|
const parked = takePendingConnectAttempt();
|
|
228
251
|
return fromEnv && isAttemptId(fromEnv) ? fromEnv : parked;
|
|
229
252
|
}
|
|
253
|
+
// postConnectEvent is total, so this only fires on a bug inside deliver.
|
|
254
|
+
function reportDeliverBug(error) {
|
|
255
|
+
if (!process.env['TINYFISH_DEBUG'])
|
|
256
|
+
return;
|
|
257
|
+
errLine(`connect telemetry failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
258
|
+
}
|
|
230
259
|
export function createConnectTelemetry(mcpUrl, client, opts) {
|
|
231
260
|
// Consumed even when --url already supplied an id: leaving it behind would let a stale
|
|
232
261
|
// seed reach the agent this connect launches, or the next connect on this machine.
|
|
@@ -252,6 +281,33 @@ export function createConnectTelemetry(mcpUrl, client, opts) {
|
|
|
252
281
|
label: 'connect',
|
|
253
282
|
});
|
|
254
283
|
}
|
|
284
|
+
function eventBody(stage, detail, stageDurationMs) {
|
|
285
|
+
return JSON.stringify({
|
|
286
|
+
attempt_id: attemptId,
|
|
287
|
+
client,
|
|
288
|
+
stage,
|
|
289
|
+
failed_stage: detail?.failedStage,
|
|
290
|
+
phase: detail?.phase,
|
|
291
|
+
failure_reason: detail?.failureReason,
|
|
292
|
+
// Only with failed_stage: the route refine 400s the event otherwise.
|
|
293
|
+
failure_detail: detail?.failedStage ? detail.failureDetail : undefined,
|
|
294
|
+
// Terminal rows only: rescue checkpoints stay wire-identical to normal ones.
|
|
295
|
+
fallback_outcome: stage === 'failed' || stage === 'aborted' ? fallbackOutcome : undefined,
|
|
296
|
+
harness_version: detail?.harnessVersion ?? harnessVersion,
|
|
297
|
+
auth_mode: detail?.authMode,
|
|
298
|
+
harness_degraded: detail?.harnessDegraded,
|
|
299
|
+
stage_duration_ms: stageDurationMs,
|
|
300
|
+
runtime_platform: process.platform,
|
|
301
|
+
node_version: process.version,
|
|
302
|
+
cli_version: CLI_VERSION,
|
|
303
|
+
// Same signal the usage events carry, so TTY and headless connects can be split.
|
|
304
|
+
is_human_initiated: detectHumanInitiated(),
|
|
305
|
+
// "none", not absent — absent has to keep meaning "CLI too old".
|
|
306
|
+
calling_harness: detectHarness() ?? 'none',
|
|
307
|
+
cli_invocation_id: CLI_INVOCATION_ID,
|
|
308
|
+
runtime_env: detectRuntimeEnv(),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
255
311
|
return {
|
|
256
312
|
attemptId,
|
|
257
313
|
// Fire-and-forget; awaiting each event stalls setup when telemetry down.
|
|
@@ -269,35 +325,7 @@ export function createConnectTelemetry(mcpUrl, client, opts) {
|
|
|
269
325
|
lastTrackAt = now;
|
|
270
326
|
// Absorbed here, not in flush: flush runs in runGuarded's finally, so a rejection would
|
|
271
327
|
// replace the error the flow is already reporting.
|
|
272
|
-
pending.push(deliver(
|
|
273
|
-
attempt_id: attemptId,
|
|
274
|
-
client,
|
|
275
|
-
stage,
|
|
276
|
-
failed_stage: detail?.failedStage,
|
|
277
|
-
phase: detail?.phase,
|
|
278
|
-
failure_reason: detail?.failureReason,
|
|
279
|
-
// Terminal rows only: rescue checkpoints stay wire-identical to normal ones.
|
|
280
|
-
fallback_outcome: stage === 'failed' || stage === 'aborted' ? fallbackOutcome : undefined,
|
|
281
|
-
harness_version: detail?.harnessVersion ?? harnessVersion,
|
|
282
|
-
auth_mode: detail?.authMode,
|
|
283
|
-
harness_degraded: detail?.harnessDegraded,
|
|
284
|
-
stage_duration_ms: stageDurationMs,
|
|
285
|
-
runtime_platform: process.platform,
|
|
286
|
-
node_version: process.version,
|
|
287
|
-
cli_version: CLI_VERSION,
|
|
288
|
-
// Same signal the usage events carry, so TTY and headless connects can be split.
|
|
289
|
-
is_human_initiated: detectHumanInitiated(),
|
|
290
|
-
// "none", not absent — absent has to keep meaning "CLI too old".
|
|
291
|
-
calling_harness: detectHarness() ?? 'none',
|
|
292
|
-
cli_invocation_id: CLI_INVOCATION_ID,
|
|
293
|
-
runtime_env: detectRuntimeEnv(),
|
|
294
|
-
})).catch((error) => {
|
|
295
|
-
// postConnectEvent is total, so this only fires if something inside deliver starts
|
|
296
|
-
// throwing. Same channel postConnectEvent uses for its own failures.
|
|
297
|
-
if (!process.env['TINYFISH_DEBUG'])
|
|
298
|
-
return;
|
|
299
|
-
errLine(`connect telemetry failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
300
|
-
}));
|
|
328
|
+
pending.push(deliver(eventBody(stage, detail, stageDurationMs)).catch(reportDeliverBug));
|
|
301
329
|
},
|
|
302
330
|
// Awaited in finally so in-flight events land before exit. Drains, so the signal-guard
|
|
303
331
|
// flush and the finally flush do not re-await the same events.
|
|
@@ -4,7 +4,7 @@ import { z } from 'zod';
|
|
|
4
4
|
import { loadConfig } from './auth.js';
|
|
5
5
|
import { captureStdio, capturedOutput, replay, SKILL_INSTALL_TIMEOUT_MS, STEP_MAX_BUFFER, } from './cli-install.js';
|
|
6
6
|
import { CURSOR_SKILL_TARGET, NATIVE_MCP_CLIENTS, OPENCLAW_SKILL_INSTALL_ARGS, } from './connect-clients.js';
|
|
7
|
-
import { commandNotFound, spawnStepError, throwIfInterrupted } from './connect-runtime.js';
|
|
7
|
+
import { commandNotFound, spawnFailureDetail, spawnStepError, throwIfInterrupted, } from './connect-runtime.js';
|
|
8
8
|
import { CLI_AGENT_IDENTITY } from './constants.js';
|
|
9
9
|
import { errLine } from './output.js';
|
|
10
10
|
// Supports Hermes without node:util.styleText, so the installer still runs on Node 20.11.
|
|
@@ -53,8 +53,12 @@ function failedListVerdict() {
|
|
|
53
53
|
maxBuffer: STEP_MAX_BUFFER,
|
|
54
54
|
timeout: SKILL_INSTALL_TIMEOUT_MS,
|
|
55
55
|
});
|
|
56
|
-
const
|
|
57
|
-
|
|
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 };
|
|
58
62
|
}
|
|
59
63
|
// Entry presence is the whole verdict: `agents` names the harnesses `skills` detects,
|
|
60
64
|
// so an install for a harness with no config dir yet lists none (PF-3680).
|
|
@@ -78,10 +82,12 @@ export function installWebSkill(client, { verbose }) {
|
|
|
78
82
|
...captureStdio(verbose),
|
|
79
83
|
env: skillSpawnEnv(),
|
|
80
84
|
});
|
|
81
|
-
const
|
|
85
|
+
const addFailed = Boolean(add.error) || add.status !== 0;
|
|
86
|
+
const verdict = addFailed ? null : failedListVerdict();
|
|
87
|
+
const failed = addFailed ? add : verdict?.result;
|
|
82
88
|
if (failed) {
|
|
83
89
|
// 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);
|
|
90
|
+
const error = spawnStepError(`Could not install the TinyFish web skill in ${client.displayName}`, failed, verdict?.tag);
|
|
85
91
|
// The add carries the failure prose; the list is JSON only.
|
|
86
92
|
replay(capturedOutput(add));
|
|
87
93
|
throw error;
|
|
@@ -154,12 +160,12 @@ function reinstallWebSkill(skillAgents, verbose) {
|
|
|
154
160
|
const failedList = addFailed ? null : failedListVerdict();
|
|
155
161
|
const failed = addFailed || failedList !== null;
|
|
156
162
|
if (failed)
|
|
157
|
-
throwIfInterrupted(failedList ?? result);
|
|
163
|
+
throwIfInterrupted(failedList?.result ?? result);
|
|
158
164
|
if (verbose || failed)
|
|
159
165
|
replay(output);
|
|
160
166
|
if (failed) {
|
|
161
167
|
throw new Error('Could not refresh the TinyFish web skill', {
|
|
162
|
-
cause: (failedList ?? result).error,
|
|
168
|
+
cause: (failedList?.result ?? result).error,
|
|
163
169
|
});
|
|
164
170
|
}
|
|
165
171
|
}
|