@tiny-fish/cli 0.29.1-next.240 → 0.29.1-next.242
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 +1 -1
- package/dist/commands/doctor.js +9 -0
- package/dist/lib/auth.d.ts +6 -2
- package/dist/lib/auth.js +16 -5
- package/dist/lib/connect-all.js +25 -9
- package/dist/lib/doctor-report.d.ts +10 -0
- package/dist/lib/doctor-report.js +2 -0
- package/dist/lib/registration-detect.d.ts +3 -0
- package/dist/lib/registration-detect.js +2 -0
- package/package.json +1 -1
package/dist/commands/connect.js
CHANGED
|
@@ -199,7 +199,7 @@ function runPostInstallSteps(client, options, state, telemetry, signInDeferred)
|
|
|
199
199
|
});
|
|
200
200
|
}
|
|
201
201
|
// Before auth: a failed sign-in must not lose the harness record.
|
|
202
|
-
saveConnectContext(client.connectClient, telemetry.attemptId);
|
|
202
|
+
saveConnectContext(client.connectClient, telemetry.attemptId, state.authMode);
|
|
203
203
|
attempt("authentication", () => {
|
|
204
204
|
// A set timeout means headless; never shell an interactive login.
|
|
205
205
|
ensureCliAuthenticated(client.connectClient, options.apiKey, {
|
package/dist/commands/doctor.js
CHANGED
|
@@ -130,6 +130,14 @@ function registrationVerdict(status, mcpUrl, keyAuth, cliKeyMissing) {
|
|
|
130
130
|
// Widening past api-key would warn every healthy oauth install.
|
|
131
131
|
if (status.authMode === AuthMode.ApiKey)
|
|
132
132
|
return keyedVerdict(status, keyAuth, cliKeyMissing);
|
|
133
|
+
// Recorded, not verified: it never feeds provesHarnessReach.
|
|
134
|
+
if (status.recordedAuthMode) {
|
|
135
|
+
return {
|
|
136
|
+
status: "pass",
|
|
137
|
+
detail: `registered, auth mode ${status.recordedAuthMode} ` +
|
|
138
|
+
"(recorded at connect; this harness does not report its headers)",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
133
141
|
// A probe that had to explain itself to reach `yes` is the only thing that explains `unknown`.
|
|
134
142
|
const because = status.reason ? ` (${status.reason})` : "";
|
|
135
143
|
return { status: "pass", detail: `registered, auth mode ${status.authMode}${because}` };
|
|
@@ -258,6 +266,7 @@ export async function runDoctor(options) {
|
|
|
258
266
|
detected: status.detected,
|
|
259
267
|
registered: status.registered,
|
|
260
268
|
auth_mode: status.authMode,
|
|
269
|
+
...(status.recordedAuthMode ? { recorded_auth_mode: status.recordedAuthMode } : {}),
|
|
261
270
|
proves_harness_reach: provesHarnessReach(status, keyAuths[i], healthOk, options.mcpUrl),
|
|
262
271
|
}));
|
|
263
272
|
const body = {
|
package/dist/lib/auth.d.ts
CHANGED
|
@@ -3,8 +3,11 @@ export declare function configDir(): string;
|
|
|
3
3
|
/** Install channel baked onto connect context; the connect flow is always CLI-driven. */
|
|
4
4
|
export declare const CONNECT_SOURCE = "tinyfish_cli";
|
|
5
5
|
export declare const CONNECT_ATTEMPT_ENV = "TINYFISH_CONNECT_ATTEMPT_ID";
|
|
6
|
-
|
|
6
|
+
/** Mirrors ConnectAuthMode; duplicated to keep auth.ts free of connect-runtime imports. */
|
|
7
|
+
export type RecordedAuthMode = "api-key" | "oauth" | "deferred";
|
|
8
|
+
export interface ConnectEntry {
|
|
7
9
|
attempt_id: string;
|
|
10
|
+
auth_mode?: RecordedAuthMode;
|
|
8
11
|
}
|
|
9
12
|
interface PendingConnectAttempt {
|
|
10
13
|
id: string;
|
|
@@ -27,7 +30,8 @@ export declare function takePendingConnectAttempt(): string | undefined;
|
|
|
27
30
|
/** Throwing variant for callers with their own cleanup (connect's telemetry flush). */
|
|
28
31
|
export declare function writeConfig(apiKey: string): void;
|
|
29
32
|
export declare function saveConfig(apiKey: string): void;
|
|
30
|
-
|
|
33
|
+
/** `authMode` omitted rewrites the entry without one: a reconnect that degraded must not keep a stale key claim. */
|
|
34
|
+
export declare function saveConnectContext(client: string, attemptId: string, authMode?: RecordedAuthMode): void;
|
|
31
35
|
interface EnvironmentOptions {
|
|
32
36
|
platform?: typeof process.platform;
|
|
33
37
|
shell?: string;
|
package/dist/lib/auth.js
CHANGED
|
@@ -19,6 +19,7 @@ export const CONNECT_SOURCE = "tinyfish_cli";
|
|
|
19
19
|
// An env var, not a flag: released CLIs ignore an unknown one, and the installer's PATH
|
|
20
20
|
// prepend means a copied command can still run a CLI far older than the page that wrote it.
|
|
21
21
|
export const CONNECT_ATTEMPT_ENV = "TINYFISH_CONNECT_ATTEMPT_ID";
|
|
22
|
+
const RECORDED_AUTH_MODES = ["api-key", "oauth", "deferred"];
|
|
22
23
|
export function loadConfig() {
|
|
23
24
|
try {
|
|
24
25
|
const raw = JSON.parse(fs.readFileSync(configFile(), "utf8"));
|
|
@@ -45,9 +46,15 @@ function parseConnectMap(value) {
|
|
|
45
46
|
return undefined;
|
|
46
47
|
const out = {};
|
|
47
48
|
for (const [client, entry] of Object.entries(value)) {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
const record = entry;
|
|
50
|
+
const attemptId = record?.attempt_id;
|
|
51
|
+
if (typeof attemptId !== "string")
|
|
52
|
+
continue;
|
|
53
|
+
const authMode = record?.auth_mode;
|
|
54
|
+
out[client] =
|
|
55
|
+
typeof authMode === "string" && RECORDED_AUTH_MODES.includes(authMode)
|
|
56
|
+
? { attempt_id: attemptId, auth_mode: authMode }
|
|
57
|
+
: { attempt_id: attemptId };
|
|
51
58
|
}
|
|
52
59
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
53
60
|
}
|
|
@@ -118,11 +125,15 @@ export function saveConfig(apiKey) {
|
|
|
118
125
|
process.exit(1);
|
|
119
126
|
}
|
|
120
127
|
}
|
|
121
|
-
|
|
128
|
+
/** `authMode` omitted rewrites the entry without one: a reconnect that degraded must not keep a stale key claim. */
|
|
129
|
+
export function saveConnectContext(client, attemptId, authMode) {
|
|
130
|
+
const entry = authMode
|
|
131
|
+
? { attempt_id: attemptId, auth_mode: authMode }
|
|
132
|
+
: { attempt_id: attemptId };
|
|
122
133
|
try {
|
|
123
134
|
updateConfig((config) => ({
|
|
124
135
|
...config,
|
|
125
|
-
connect: { ...config.connect, [client]:
|
|
136
|
+
connect: { ...config.connect, [client]: entry },
|
|
126
137
|
}));
|
|
127
138
|
}
|
|
128
139
|
catch (e) {
|
package/dist/lib/connect-all.js
CHANGED
|
@@ -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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
@@ -212,7 +214,7 @@ async function processHarness(detection, opts, prompt, oauthBudget, hoistedCliIn
|
|
|
212
214
|
}
|
|
213
215
|
// Detection saw a name on PATH; the probe proved nothing is behind it.
|
|
214
216
|
if (e instanceof ConnectStepError && e.failureReason === "harness_not_installed") {
|
|
215
|
-
return { ...base, detected: false, outcome: "
|
|
217
|
+
return { ...base, detected: false, outcome: "not_installed" };
|
|
216
218
|
}
|
|
217
219
|
// Both reasons carry the same "flag not listed" message; the upgrade nudge is in it.
|
|
218
220
|
if (e instanceof ConnectStepError && TOO_OLD_REASONS.has(e.failureReason)) {
|
|
@@ -249,6 +251,12 @@ async function processHarness(detection, opts, prompt, oauthBudget, hoistedCliIn
|
|
|
249
251
|
}
|
|
250
252
|
function summaryLine(result, uninstall) {
|
|
251
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
|
+
}
|
|
252
260
|
if (!result.detected) {
|
|
253
261
|
const next = uninstall
|
|
254
262
|
? "nothing to remove"
|
|
@@ -315,13 +323,21 @@ function renderSummary(results, uninstall) {
|
|
|
315
323
|
// Ids, not display names: they double as the `connect` argument.
|
|
316
324
|
const skipped = results.filter((r) => r.outcome === "deselected").map((r) => r.harness);
|
|
317
325
|
const lines = results
|
|
318
|
-
.filter((r) =>
|
|
326
|
+
.filter((r) => !HIDDEN_ONCE_SOMETHING_WORKED.has(r.outcome))
|
|
319
327
|
.map((r) => summaryLine(r, uninstall));
|
|
320
328
|
if (skipped.length === 0)
|
|
321
329
|
return lines;
|
|
322
330
|
lines.push("", `Skipped (not selected): ${skipped.join(", ")}`);
|
|
323
331
|
return lines;
|
|
324
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
|
+
}
|
|
325
341
|
/** A config-writing connect needs no binary; every other harness spawns one. */
|
|
326
342
|
function isStaleConfig(detection) {
|
|
327
343
|
return (detection.detected &&
|
|
@@ -490,8 +506,8 @@ export async function runConnectAll(opts) {
|
|
|
490
506
|
const outro = new Set();
|
|
491
507
|
const results = [];
|
|
492
508
|
const oauthBudget = { spent: false };
|
|
493
|
-
//
|
|
494
|
-
const stepped = (d) => !deselected.has(d.harness) && d
|
|
509
|
+
// Shares processHarness's no-work states so stepTotal cannot desync.
|
|
510
|
+
const stepped = (d) => !deselected.has(d.harness) && !noWorkOutcome(d, opts.uninstall);
|
|
495
511
|
const stepTotal = detections.filter(stepped).length;
|
|
496
512
|
let step = 0;
|
|
497
513
|
for (const detection of detections) {
|
|
@@ -51,6 +51,11 @@ declare const doctorHarnessSchema: z.ZodObject<{
|
|
|
51
51
|
detected: z.ZodBoolean;
|
|
52
52
|
registered: z.ZodEnum<typeof Registered>;
|
|
53
53
|
auth_mode: z.ZodEnum<typeof AuthMode>;
|
|
54
|
+
recorded_auth_mode: z.ZodOptional<z.ZodEnum<{
|
|
55
|
+
"api-key": "api-key";
|
|
56
|
+
oauth: "oauth";
|
|
57
|
+
deferred: "deferred";
|
|
58
|
+
}>>;
|
|
54
59
|
proves_harness_reach: z.ZodBoolean;
|
|
55
60
|
}, z.core.$strip>;
|
|
56
61
|
declare const doctorRepairSchema: z.ZodObject<{
|
|
@@ -114,6 +119,11 @@ export declare const doctorReportSchema: z.ZodObject<{
|
|
|
114
119
|
detected: z.ZodBoolean;
|
|
115
120
|
registered: z.ZodEnum<typeof Registered>;
|
|
116
121
|
auth_mode: z.ZodEnum<typeof AuthMode>;
|
|
122
|
+
recorded_auth_mode: z.ZodOptional<z.ZodEnum<{
|
|
123
|
+
"api-key": "api-key";
|
|
124
|
+
oauth: "oauth";
|
|
125
|
+
deferred: "deferred";
|
|
126
|
+
}>>;
|
|
117
127
|
proves_harness_reach: z.ZodBoolean;
|
|
118
128
|
}, z.core.$strip>>;
|
|
119
129
|
repairs: z.ZodArray<z.ZodObject<{
|
|
@@ -19,6 +19,8 @@ const doctorHarnessSchema = z.object({
|
|
|
19
19
|
detected: z.boolean(),
|
|
20
20
|
registered: z.enum(Registered),
|
|
21
21
|
auth_mode: z.enum(AuthMode),
|
|
22
|
+
// Additive under schema 3: what connect wrote when the harness cannot echo it back.
|
|
23
|
+
recorded_auth_mode: z.enum(["api-key", "oauth", "deferred"]).optional(),
|
|
22
24
|
proves_harness_reach: z.boolean(),
|
|
23
25
|
});
|
|
24
26
|
// `action` is what `--fix` dispatches on, not the harness field: keying off a null harness
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type RecordedAuthMode } from "./auth.js";
|
|
1
2
|
import type { AgentClient } from "./connect-runtime.js";
|
|
2
3
|
import { AuthMode, type Harness, Registered } from "./harness-detect.js";
|
|
3
4
|
export interface RegistrationStatus {
|
|
@@ -15,6 +16,8 @@ export interface RegistrationStatus {
|
|
|
15
16
|
connected?: boolean;
|
|
16
17
|
/** Why a keyed registration's key could not be read; not `reason`. */
|
|
17
18
|
keyReason?: string;
|
|
19
|
+
/** What connect wrote, for harnesses whose CLI never echoes it back. Recorded, not verified. */
|
|
20
|
+
recordedAuthMode?: RecordedAuthMode;
|
|
18
21
|
reason?: string;
|
|
19
22
|
}
|
|
20
23
|
export declare function codexOauthCompleted(): boolean | undefined;
|
|
@@ -458,10 +458,12 @@ export function detectRegistrations(harnesses) {
|
|
|
458
458
|
return detectInstalledHarnesses()
|
|
459
459
|
.filter((detection) => !harnesses || harnesses.includes(detection.harness))
|
|
460
460
|
.map((detection) => {
|
|
461
|
+
const recordedAuthMode = connect[detection.harness]?.auth_mode;
|
|
461
462
|
const base = {
|
|
462
463
|
harness: detection.harness,
|
|
463
464
|
detected: detection.detected,
|
|
464
465
|
connectedBefore: detection.harness in connect,
|
|
466
|
+
...(recordedAuthMode ? { recordedAuthMode } : {}),
|
|
465
467
|
};
|
|
466
468
|
// Undetected means nothing to probe: no spawn cost, and no misleading "unknown".
|
|
467
469
|
if (!detection.detected) {
|