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)`);