From 74b2bd74e91fc6ff8f65145a809dad7d22c5d158 Mon Sep 17 00:00:00 2001 From: Kurtis Tozer Date: Wed, 18 Mar 2026 12:31:53 +1100 Subject: [PATCH] fix: detect broken qmd installs and surface actionable errors The qmd binary shim can exist on PATH while the actual module is missing (bun install -g from GitHub doesn't always run the build step). Previously this silently passed dependency checks then failed at collection creation with no useful error message. Now checkQmd() verifies qmd can actually execute, not just that the shim exists. When a broken install is detected: - printDepsReport shows fix instructions (cd && bun run build) - installQmd auto-attempts the build step as recovery - createCollection returns the actual error reason for display Co-Authored-By: Claude Opus 4.6 --- src/deps.ts | 56 +++++++++++++++++++++++++++++++++++++++++++++++++--- src/qmd.ts | 17 ++++++++-------- src/setup.ts | 15 +++++++++----- 3 files changed, 72 insertions(+), 16 deletions(-) diff --git a/src/deps.ts b/src/deps.ts index e8a2cd1..b5e4a81 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -1,4 +1,5 @@ import { homedir } from "node:os"; +import { join } from "node:path"; import { $ } from "bun"; // ============================================================================= @@ -57,9 +58,28 @@ export function resetDepCache(): void { // Individual Dependency Checks // ============================================================================= -/** Check if qmd is installed */ +/** Check if qmd is installed and functional. + * The binary shim may exist on PATH but point to a missing dist/ directory + * (e.g. when `bun install -g` didn't run the build step). We verify by + * running `qmd collection list` which exercises the actual module. */ export async function checkQmd(): Promise { - return checkBinary("qmd"); + const base = await checkBinary("qmd"); + if (!base.available) return base; + // Verify qmd can actually execute — not just that the shim exists + try { + await $`qmd collection list`.quiet(); + return base; + } catch (err) { + const stderr = (err as { stderr?: { toString(): string } })?.stderr?.toString() ?? ""; + const msg = stderr || (err instanceof Error ? err.message : String(err)); + // Module not found = broken install (shim exists but dist/ missing) + if (msg.includes("Module not found") || msg.includes("Cannot find module")) { + _cache.qmd = { available: false, version: "broken install", path: base.path }; + return _cache.qmd!; + } + // Other errors (e.g. no collections yet) are fine — qmd itself works + return base; + } } /** Check if claude CLI is installed */ @@ -101,6 +121,28 @@ export async function installQmd(): Promise { console.log(` qmd installed successfully.`); return true; } + // Binary exists but module missing — try running the build step + if (check.version === "broken install") { + console.log(" qmd shim installed but build step did not run. Building..."); + const pkgDir = join(home, ".bun", "install", "global", "node_modules", "@tobilu", "qmd"); + try { + await $`cd ${pkgDir} && bun run build`.quiet(); + resetDepCache(); + const recheck = await checkQmd(); + if (recheck.available) { + console.log(` qmd built and installed successfully.`); + return true; + } + } catch (buildErr) { + const stderr = (buildErr as { stderr?: { toString(): string } })?.stderr?.toString() ?? ""; + console.error( + ` qmd build failed: ${stderr.trim() || (buildErr instanceof Error ? buildErr.message : String(buildErr))}`, + ); + } + console.error(` qmd build did not produce a working install.`); + console.error(` Try manually: cd ${pkgDir} && bun run build`); + return false; + } console.error(" qmd installation completed but binary not found on PATH."); console.error(" Ensure ~/.bun/bin is in your PATH."); return false; @@ -147,7 +189,15 @@ export function printDepsReport(report: DepsReport): void { const ver = (s: DepStatus) => (s.version ? ` (${s.version})` : ""); console.log(` bun: ${ok(report.bun)}${ver(report.bun)}`); - console.log(` qmd: ${ok(report.qmd)}${ver(report.qmd)}`); + if (report.qmd.version === "broken install") { + const qmdPkgDir = join(home, ".bun", "install", "global", "node_modules", "@tobilu", "qmd"); + console.log(` qmd: broken install (shim exists but module missing)`); + console.log(` The qmd build step may not have run during installation.`); + console.log(` Fix: cd ${qmdPkgDir} && bun run build`); + console.log(` Or reinstall: bun install -g github:tobi/qmd --force`); + } else { + console.log(` qmd: ${ok(report.qmd)}${ver(report.qmd)}`); + } console.log(` claude: ${ok(report.claude)}${ver(report.claude)}`); console.log(` sqlite (brew): ${ok(report.sqlite)}`); } diff --git a/src/qmd.ts b/src/qmd.ts index 4d8a1f6..eb3bc10 100644 --- a/src/qmd.ts +++ b/src/qmd.ts @@ -95,22 +95,23 @@ export async function searchSessions(query: string, opts?: { tag?: string; colle } } -/** Create initial QMD collection for the project. Returns true if successful. */ -export async function createCollection(root: string): Promise { - if (!(await isQmdAvailable())) return false; +/** Create initial QMD collection for the project. Returns { ok, reason } for diagnostic output. */ +export async function createCollection(root: string): Promise<{ ok: boolean; reason?: string }> { + if (!(await isQmdAvailable())) return { ok: false, reason: "qmd not available" }; const name = await collectionName(root); const dir = completedDir(root); try { - if (await collectionExists(root)) return true; + if (await collectionExists(root)) return { ok: true }; await $`qmd collection add ${dir} --name ${name}`.quiet(); await $`qmd context add ${dir} "AI coding session transcripts and reasoning"`.quiet(); - return true; + return { ok: true }; } catch (err) { // If collection already exists (collectionExists may have failed to detect it), // treat as success. Bun ShellError puts the actual message in stderr. - const msg = `${err instanceof Error ? err.message : String(err)} ${(err as { stderr?: { toString(): string } })?.stderr?.toString() ?? ""}`; - if (msg.includes("already exists")) return true; - return false; + const stderr = (err as { stderr?: { toString(): string } })?.stderr?.toString() ?? ""; + const msg = `${err instanceof Error ? err.message : String(err)} ${stderr}`; + if (msg.includes("already exists")) return { ok: true }; + return { ok: false, reason: stderr.trim() || (err instanceof Error ? err.message : String(msg)).trim() }; } } diff --git a/src/setup.ts b/src/setup.ts index 35d35c9..a93cdcd 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -174,9 +174,9 @@ export async function enable(root: string, opts?: { install?: boolean; genesis?: await chmod(postCommitPath, 0o755); // 9. Create initial QMD collection (if available, uses main repo root) - let qmdOk = false; + let qmdResult: { ok: boolean; reason?: string } = { ok: false }; if (report.qmd.available) { - qmdOk = await createCollection(mainRoot); + qmdResult = await createCollection(mainRoot); } // 10. Report results @@ -185,9 +185,14 @@ export async function enable(root: string, opts?: { install?: boolean; genesis?: console.log(` ${c.bold}Hooks:${c.reset} .claude/settings.json`); console.log(` ${c.bold}Git notes:${c.reset} refs/notes/ai-sessions`); if (report.qmd.available) { - console.log( - ` ${c.bold}QMD:${c.reset} ${name}${qmdOk ? ` ${c.green}(created)${c.reset}` : ` ${c.red}(failed to create)${c.reset}`}`, - ); + if (qmdResult.ok) { + console.log(` ${c.bold}QMD:${c.reset} ${name} ${c.green}(created)${c.reset}`); + } else { + console.log(` ${c.bold}QMD:${c.reset} ${name} ${c.red}(failed to create)${c.reset}`); + if (qmdResult.reason) { + console.log(` ${c.dim} ${qmdResult.reason}${c.reset}`); + } + } console.log(` ${c.bold}MCP server:${c.reset} ghost-sessions`); } else { console.log(` ${c.bold}QMD:${c.reset} ${c.yellow}not installed${c.reset} (search disabled)`);