@tiny-fish/cli 0.15.1-next.160 → 0.15.1-next.161

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,9 +1,17 @@
1
1
  import type { Command } from "commander";
2
- import { type DoctorReport } from "../lib/doctor-report.js";
2
+ import { type DoctorRepair, type DoctorReport } from "../lib/doctor-report.js";
3
3
  import { type FiveHarness } from "../lib/harness-detect.js";
4
4
  export interface DoctorOptions {
5
5
  mcpUrl: string;
6
6
  harness?: FiveHarness;
7
+ fix?: boolean;
8
+ yes?: boolean;
7
9
  }
8
10
  export declare function runDoctor(options: DoctorOptions): Promise<DoctorReport>;
11
+ export interface RepairOutcome {
12
+ repair: DoctorRepair;
13
+ status: "repaired" | "skipped" | "failed";
14
+ reason?: string;
15
+ }
16
+ export declare function applyRepairs(repairs: DoctorRepair[], options: DoctorOptions): Promise<RepairOutcome[]>;
9
17
  export declare function registerDoctor(program: Command): void;
@@ -1,11 +1,13 @@
1
+ import spawn from "cross-spawn";
1
2
  import { apiKeyStatus } from "../lib/auth.js";
2
3
  import { CLI_VERSION } from "../lib/constants.js";
3
4
  import { DOCTOR_COULD_NOT_RUN, DOCTOR_SCHEMA_VERSION, doctorReportSchema, exitCodeFor, renderPretty, } from "../lib/doctor-report.js";
4
5
  import { FIVE_HARNESSES } from "../lib/harness-detect.js";
6
+ import { detectHumanInitiated } from "../lib/harness.js";
5
7
  import { err, errLine, out, outLine } from "../lib/output.js";
6
8
  import { detectRegistrations } from "../lib/registration-detect.js";
7
9
  import { verifyMcpAuth, verifyMcpHealth } from "../lib/verify.js";
8
- import { DEFAULT_MCP_URL } from "./connect.js";
10
+ import { connectClaudeCode, connectCodex, connectCursor, connectHermes, connectOpenClaw, DEFAULT_MCP_URL, } from "./connect.js";
9
11
  // A green auth mode with a red auth call proves nothing, so the call's verdict gates the claim.
10
12
  function provesHarnessReach(status, authCallPassed, healthOk, mcpUrl) {
11
13
  // An endpoint doctor could not reach proves nothing about a harness pointed at it.
@@ -117,6 +119,7 @@ async function checkAuthCall(key) {
117
119
  const UNATTENDED_SAFE = new Set(["cursor"]);
118
120
  function repairsFor(checks, statuses) {
119
121
  const repairs = [];
122
+ const credentialResolves = checks.some((c) => c.id === "cli-credential" && c.status === "pass");
120
123
  for (const status of statuses) {
121
124
  // `unknown` earns nothing: running connect off a failed probe contradicts the check's detail.
122
125
  // A registered-but-misaimed entry does earn one, since connect rewrites the URL.
@@ -132,7 +135,8 @@ function repairsFor(checks, statuses) {
132
135
  action: "connect",
133
136
  harness: status.harness,
134
137
  command: `tinyfish connect ${status.harness}`,
135
- unattended_safe: UNATTENDED_SAFE.has(status.harness),
138
+ // Without a credential even Cursor's repair shells an interactive `auth login`.
139
+ unattended_safe: UNATTENDED_SAFE.has(status.harness) && credentialResolves,
136
140
  });
137
141
  }
138
142
  // A revoked-but-well-formed key passes the credential check and fails the call; both need login.
@@ -178,6 +182,75 @@ export async function runDoctor(options) {
178
182
  // Nothing to scrub: every string above is authored here or enumerated at its source.
179
183
  return doctorReportSchema.parse(body);
180
184
  }
185
+ // Shell out rather than import the login flow: `auth login` owns its own browser and prompts.
186
+ // Re-invoke this same entry script, never a global `tinyfish` — under `npx` there is no such
187
+ // binary on PATH, and npx is the install-nothing path doctor exists to serve.
188
+ function runAuthLogin() {
189
+ const entry = process.argv[1];
190
+ if (!entry)
191
+ throw new Error("Could not locate the TinyFish CLI entry point to re-invoke");
192
+ const result = spawn.sync(process.execPath, [entry, "auth", "login", "--source", "doctor"], {
193
+ stdio: "inherit",
194
+ });
195
+ if (result.error)
196
+ throw new Error("Could not run `tinyfish auth login`", { cause: result.error });
197
+ if (result.status !== 0) {
198
+ throw new Error(`\`tinyfish auth login\` exited ${result.status ?? "unknown"}`);
199
+ }
200
+ }
201
+ const CONNECTORS = {
202
+ "claude-code": (opts) => connectClaudeCode({ ...opts, launch: false }),
203
+ codex: (opts) => connectCodex({ ...opts, launch: false }),
204
+ cursor: (opts) => connectCursor(opts),
205
+ hermes: (opts) => connectHermes({ ...opts, launch: false }),
206
+ openclaw: (opts) => connectOpenClaw({ ...opts, launch: false }),
207
+ };
208
+ /** Dispatches on `action`, never on a null harness: that conflation made the credential fix a no-op. */
209
+ async function runRepair(repair, mcpUrl) {
210
+ if (repair.action === "auth-login")
211
+ return runAuthLogin();
212
+ if (repair.harness)
213
+ await CONNECTORS[repair.harness]({ mcpUrl });
214
+ }
215
+ export async function applyRepairs(repairs, options) {
216
+ const interactive = detectHumanInitiated();
217
+ const outcomes = [];
218
+ for (const repair of repairs) {
219
+ if (!interactive && !options.yes) {
220
+ outcomes.push({
221
+ repair,
222
+ status: "skipped",
223
+ reason: "no terminal; re-run with --yes to repair unattended",
224
+ });
225
+ continue;
226
+ }
227
+ if (!interactive && !repair.unattended_safe) {
228
+ outcomes.push({
229
+ repair,
230
+ status: "skipped",
231
+ reason: "needs a browser sign-in, which has no unattended path",
232
+ });
233
+ continue;
234
+ }
235
+ try {
236
+ await runRepair(repair, options.mcpUrl);
237
+ outcomes.push({ repair, status: "repaired" });
238
+ }
239
+ catch (e) {
240
+ // Connect quotes absolute config paths in its errors, so the raw text stays behind
241
+ // --debug and the user is told where to find it rather than handed a generic dead end.
242
+ if (process.env["TINYFISH_DEBUG"]) {
243
+ errLine(e instanceof Error ? (e.stack ?? e.message) : String(e));
244
+ }
245
+ outcomes.push({
246
+ repair,
247
+ status: "failed",
248
+ reason: `\`${repair.command}\` failed; re-run it directly, or --debug for the raw error`,
249
+ });
250
+ }
251
+ }
252
+ return outcomes;
253
+ }
181
254
  export function registerDoctor(program) {
182
255
  program
183
256
  .command("doctor")
@@ -185,18 +258,31 @@ export function registerDoctor(program) {
185
258
  .option("--pretty", "Human-readable check list instead of JSON")
186
259
  .option("--harness <name>", `Restrict to one harness (${FIVE_HARNESSES.join(", ")})`)
187
260
  .option("--url <mcpUrl>", "MCP endpoint override")
261
+ .option("--fix", "Repair what can be repaired by re-running tinyfish connect")
262
+ .option("--yes", "Allow --fix to run without a terminal (unattended-safe repairs only)")
188
263
  .action(async (options) => {
189
264
  if (options.harness && !FIVE_HARNESSES.includes(options.harness)) {
190
265
  err({ error: `Unknown --harness value: ${options.harness}` });
191
266
  process.exitCode = DOCTOR_COULD_NOT_RUN;
192
267
  return;
193
268
  }
269
+ const doctorOptions = {
270
+ mcpUrl: options.url ?? DEFAULT_MCP_URL,
271
+ fix: Boolean(options.fix),
272
+ yes: Boolean(options.yes),
273
+ ...(options.harness ? { harness: options.harness } : {}),
274
+ };
194
275
  let report;
195
276
  try {
196
- report = await runDoctor({
197
- mcpUrl: options.url ?? DEFAULT_MCP_URL,
198
- ...(options.harness ? { harness: options.harness } : {}),
199
- });
277
+ report = await runDoctor(doctorOptions);
278
+ if (doctorOptions.fix && report.repairs.length > 0) {
279
+ for (const outcome of await applyRepairs(report.repairs, doctorOptions)) {
280
+ const reason = outcome.reason ? ` (${outcome.reason})` : "";
281
+ errLine(`${outcome.status}: ${outcome.repair.command}${reason}`);
282
+ }
283
+ // Re-diagnose so the emitted report describes the machine after repair, not before.
284
+ report = await runDoctor(doctorOptions);
285
+ }
200
286
  }
201
287
  catch (e) {
202
288
  // Exit 2 means doctor itself broke, a different claim from "your setup is broken".
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.15.1-next.160",
3
+ "version": "0.15.1-next.161",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {