From 2b250f3dca228253ebb99ecf351d3c48cc48af67 Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Tue, 19 May 2026 15:22:13 -0400 Subject: [PATCH] fix: run qmd from unbuilt checkouts --- bin/qmd | 31 +++++++++++++++++++-- test/bin-wrapper.test.ts | 60 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 84 insertions(+), 7 deletions(-) diff --git a/bin/qmd b/bin/qmd index 7522b2e..343421f 100755 --- a/bin/qmd +++ b/bin/qmd @@ -25,6 +25,31 @@ if [ "$1" = "mcp" ]; then export GGML_BACKEND_SILENT="${GGML_BACKEND_SILENT:-1}" fi +JS="$DIR/dist/cli/qmd.js" + +# In published packages dist/ is always present. In a fresh checkout, however, +# people often run ./bin/qmd before building. Prefer a source-mode fallback when +# dependencies are installed; otherwise fail with an actionable message instead +# of a low-level "Module not found" from Node/Bun. +if [ ! -f "$JS" ]; then + TS="$DIR/src/cli/qmd.ts" + if [ -f "$TS" ]; then + if [ -f "$DIR/bun.lock" ] || [ -f "$DIR/bun.lockb" ]; then + if command -v bun >/dev/null 2>&1; then + exec bun "$TS" "$@" + fi + fi + if [ -f "$DIR/node_modules/tsx/dist/cli.mjs" ]; then + exec node "$DIR/node_modules/tsx/dist/cli.mjs" "$TS" "$@" + fi + fi + + echo "qmd is not built: missing $JS" >&2 + echo "Run: bun install && bun run build" >&2 + echo "Or: npm install && npm run build" >&2 + exit 1 +fi + # Detect the package manager that installed dependencies by checking lockfiles. # $BUN_INSTALL is intentionally NOT checked — it only indicates that bun exists # on the system, not that it was used to install this package (see #361). @@ -34,9 +59,9 @@ fi # builds that use npm would be incorrectly routed to bun, causing ABI # mismatches with better-sqlite3 / sqlite-vec (see #381). if [ -f "$DIR/package-lock.json" ]; then - exec node "$DIR/dist/cli/qmd.js" "$@" + exec node "$JS" "$@" elif [ -f "$DIR/bun.lock" ] || [ -f "$DIR/bun.lockb" ]; then - exec bun "$DIR/dist/cli/qmd.js" "$@" + exec bun "$JS" "$@" else - exec node "$DIR/dist/cli/qmd.js" "$@" + exec node "$JS" "$@" fi diff --git a/test/bin-wrapper.test.ts b/test/bin-wrapper.test.ts index 82796d3..47e1255 100644 --- a/test/bin-wrapper.test.ts +++ b/test/bin-wrapper.test.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect, test } from "vitest"; import { chmodSync, copyFileSync, mkdtempSync, mkdirSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join, relative } from "node:path"; -import { execFileSync } from "node:child_process"; +import { execFileSync, spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; const repoRoot = fileURLToPath(new URL("..", import.meta.url)); @@ -27,13 +27,24 @@ function makeTempFixture() { return { root, capturePath, runtimeBin }; } -function makePackage(root: string, packagePath: string, lockfiles: string[] = []) { +function makePackage(root: string, packagePath: string, lockfiles: string[] = [], options: { dist?: boolean; source?: boolean; tsx?: boolean } = {}) { const packageRoot = join(root, packagePath); + const includeDist = options.dist ?? true; mkdirSync(join(packageRoot, "bin"), { recursive: true }); - mkdirSync(join(packageRoot, "dist", "cli"), { recursive: true }); copyFileSync(join(repoRoot, "bin", "qmd"), join(packageRoot, "bin", "qmd")); chmodSync(join(packageRoot, "bin", "qmd"), 0o755); - writeFileSync(join(packageRoot, "dist", "cli", "qmd.js"), "// fixture\n"); + if (includeDist) { + mkdirSync(join(packageRoot, "dist", "cli"), { recursive: true }); + writeFileSync(join(packageRoot, "dist", "cli", "qmd.js"), "// fixture\n"); + } + if (options.source) { + mkdirSync(join(packageRoot, "src", "cli"), { recursive: true }); + writeFileSync(join(packageRoot, "src", "cli", "qmd.ts"), "// source fixture\n"); + } + if (options.tsx) { + mkdirSync(join(packageRoot, "node_modules", "tsx", "dist"), { recursive: true }); + writeFileSync(join(packageRoot, "node_modules", "tsx", "dist", "cli.mjs"), "// tsx fixture\n"); + } for (const lockfile of lockfiles) { writeFileSync(join(packageRoot, lockfile), ""); } @@ -161,4 +172,45 @@ describe("bin/qmd package wrapper", () => { expect(result.runtime).toBe("node"); expect(result.scriptPath).toBe(realpathSync(join(packageRoot, "dist", "cli", "qmd.js"))); }); + + test("falls back to source with bun in an unbuilt Bun checkout", () => { + const { root, runtimeBin, capturePath } = makeTempFixture(); + const packageRoot = makePackage(root, "qmd", ["bun.lock"], { dist: false, source: true }); + + const result = runWrapper(join(packageRoot, "bin", "qmd"), runtimeBin, capturePath); + + expect(result.runtime).toBe("bun"); + expect(result.scriptPath).toBe(realpathSync(join(packageRoot, "src", "cli", "qmd.ts"))); + expect(result.args).toEqual(["--version"]); + }); + + test("falls back to source through tsx in an unbuilt Node checkout", () => { + const { root, runtimeBin, capturePath } = makeTempFixture(); + const packageRoot = makePackage(root, "qmd", [], { dist: false, source: true, tsx: true }); + + const result = runWrapper(join(packageRoot, "bin", "qmd"), runtimeBin, capturePath); + + expect(result.runtime).toBe("node"); + expect(result.scriptPath).toBe(realpathSync(join(packageRoot, "node_modules", "tsx", "dist", "cli.mjs"))); + expect(result.args).toEqual([realpathSync(join(packageRoot, "src", "cli", "qmd.ts")), "--version"]); + }); + + test("explains how to build when dist is missing and source cannot run", () => { + const { root, runtimeBin } = makeTempFixture(); + const packageRoot = makePackage(root, "qmd", [], { dist: false }); + + const result = spawnSync(join(packageRoot, "bin", "qmd"), ["--version"], { + env: { + ...process.env, + PATH: `${runtimeBin}:${process.env.PATH ?? ""}`, + }, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + + expect(result.status).toBe(1); + expect(result.stderr).toContain("qmd is not built"); + expect(result.stderr).toContain("bun install && bun run build"); + expect(result.stderr).toContain("npm install && npm run build"); + }); });