diff --git a/src/__tests__/tokscale.test.ts b/src/__tests__/tokscale.test.ts index d039d70..d092bea 100644 --- a/src/__tests__/tokscale.test.ts +++ b/src/__tests__/tokscale.test.ts @@ -29,6 +29,7 @@ import { versionAtLeast, reportToStats, resetDetectionCache, + TokscaleNotFoundError, } from "../tokscale" const mockExecFile = vi.mocked(execFile) @@ -282,6 +283,26 @@ describe("fetchPeriodStats", () => { mockExecFileError(error) await expect(fetchPeriodStats("today")).rejects.toThrow() }) + + it("throws TokscaleNotFoundError and resets detection cache on ENOENT", async () => { + // Prime cache as installed + mockDetectSequence("tokscale 4.0.5") + await detectTokscale() + vi.clearAllMocks() + + // Now simulate binary gone (ENOENT) + const error = Object.assign(new Error("spawn tokscale ENOENT"), { + code: "ENOENT" as unknown as number, + }) + mockExecFileError(error) + + await expect(fetchPeriodStats("today")).rejects.toThrow(TokscaleNotFoundError) + + // Detection cache should be reset — next detectTokscale() must re-probe + mockExecFileError(Object.assign(new Error("not found"), { code: 1 })) + const result = await detectTokscale() + expect(result).toBe(false) + }) }) describe("parseModelReport", () => { diff --git a/src/tokscale.ts b/src/tokscale.ts index 36d606e..64528b1 100644 --- a/src/tokscale.ts +++ b/src/tokscale.ts @@ -4,6 +4,13 @@ import { PERIOD_FLAGS } from "./types" type SemVer = readonly [number, number, number] +export class TokscaleNotFoundError extends Error { + constructor() { + super("tokscale binary not found") + this.name = "TokscaleNotFoundError" + } +} + let cachedDetection: boolean | null = null let cachedVersion: SemVer | null = null @@ -70,6 +77,11 @@ export function fetchPeriodStats( { timeout: 15000, maxBuffer: 1024 * 1024 }, (error, stdout) => { if (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + resetDetectionCache() + reject(new TokscaleNotFoundError()) + return + } reject(new Error(`tokscale CLI failed: ${error.message}`)) return } diff --git a/src/tui.tsx b/src/tui.tsx index d4bd00c..7714c5f 100644 --- a/src/tui.tsx +++ b/src/tui.tsx @@ -4,7 +4,7 @@ import { createSignal } from "solid-js" import type { TimePeriod, PeriodState, TokscalePluginOptions } from "./types" import { TIME_PERIODS, PERIOD_LABELS } from "./types" import { formatTokens, formatCost } from "./format" -import { detectTokscale, fetchPeriodStats } from "./tokscale" +import { detectTokscale, fetchPeriodStats, TokscaleNotFoundError } from "./tokscale" const TOKSCALE_BLUE = "#0073FF" @@ -45,6 +45,13 @@ const tui: TuiPlugin = async (api, options, _meta) => { }) setState({ status: "success", stats, error: null }) } catch (e) { + if (e instanceof TokscaleNotFoundError) { + setInstalled(false) + for (const p of TIME_PERIODS) { + signals[p][1]({ status: "not-installed", stats: null, error: null }) + } + return + } setState({ status: "error", stats: signals[period][0]().stats, error: String(e) }) } })