diff --git a/autoresearch.jsonl b/autoresearch.jsonl new file mode 100644 index 0000000..6afa873 --- /dev/null +++ b/autoresearch.jsonl @@ -0,0 +1 @@ +{"type":"config","name":"Fixing Windows execution wrapper regression by rewriting launcher in Node.js","metricName":"test_status","metricUnit":"","bestDirection":"lower"} diff --git a/bin/qmd b/bin/qmd index 9ddfdcf..9b0c93d 100755 --- a/bin/qmd +++ b/bin/qmd @@ -1,68 +1,110 @@ -#!/bin/sh -# Resolve symlinks so global installs (npm link / npm install -g) can find the -# actual package directory instead of the global bin directory. -SOURCE="$0" -while [ -L "$SOURCE" ]; do - SOURCE_DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)" - TARGET="$(readlink "$SOURCE")" - case "$TARGET" in - /*) SOURCE="$TARGET" ;; - *) SOURCE="$SOURCE_DIR/$TARGET" ;; - esac -done +#!/usr/bin/env node +// Cross-platform launcher for qmd. +// +// Previously this was a POSIX shell script with `#!/bin/sh`, which meant npm +// on Windows generated shims that tried to route through `/bin/sh` — a path +// that doesn't exist on Windows, so `qmd` failed immediately after a global +// install. Rewriting the launcher in Node.js lets npm generate native +// cmd/ps1/sh shims that invoke `node` directly on every platform. -# Detect the runtime used to install this package and use the matching one -# to avoid native module ABI mismatches (e.g., better-sqlite3 compiled for bun vs node) -DIR="$(cd -P "$(dirname "$SOURCE")/.." && pwd)" +import { spawn, spawnSync } from "node:child_process"; +import { existsSync, realpathSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; -# MCP stdio reserves stdout exclusively for JSON-RPC frames. node-llama-cpp -# / llama.cpp / ggml can write native logs directly to stdout before JS-level -# log handlers are attached, so seed the native quiet env before Node/Bun imports -# the CLI and its LLM modules. Preserve explicit user values when provided. -if [ "$1" = "mcp" ]; then - export LLAMA_LOG_LEVEL="${LLAMA_LOG_LEVEL:-error}" - export GGML_LOG_LEVEL="${GGML_LOG_LEVEL:-error}" - export GGML_BACKEND_SILENT="${GGML_BACKEND_SILENT:-1}" -fi +// Resolve symlinks so global installs (npm link / npm install -g) can find +// the actual package directory instead of the global bin directory. +const self = realpathSync(fileURLToPath(import.meta.url)); +const pkgDir = resolve(dirname(self), ".."); +const jsEntry = resolve(pkgDir, "dist/cli/qmd.js"); +const tsEntry = resolve(pkgDir, "src/cli/qmd.ts"); -JS="$DIR/dist/cli/qmd.js" -TS="$DIR/src/cli/qmd.ts" +// MCP stdio reserves stdout exclusively for JSON-RPC frames. node-llama-cpp +// / llama.cpp / ggml can write native logs directly to stdout before JS-level +// log handlers are attached, so seed the native quiet env before Node/Bun imports +// the CLI and its LLM modules. Preserve explicit user values when provided. +if (process.argv[2] === "mcp") { + process.env.LLAMA_LOG_LEVEL = process.env.LLAMA_LOG_LEVEL ?? "error"; + process.env.GGML_LOG_LEVEL = process.env.GGML_LOG_LEVEL ?? "error"; + process.env.GGML_BACKEND_SILENT = process.env.GGML_BACKEND_SILENT ?? "1"; +} -# In published packages, bin/qmd must run dist/. In a git checkout, however, -# dist/ is often ignored and can be stale after git reset or branch switches. -# Prefer source mode only for checkouts so ./bin/qmd reflects the checked-out -# source without changing packaged/runtime behavior. -if [ -e "$DIR/.git" ] && [ -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 +function hasBun() { + try { + const res = spawnSync("bun", ["--version"], { stdio: "ignore", shell: process.platform === "win32" }); + return res.status === 0; + } catch { + return false; + } +} -if [ ! -f "$JS" ]; then - echo "qmd is not built: missing $JS" >&2 - echo "Run: bun install && bun run build" >&2 - echo "Or: npm install && npm run build" >&2 - echo "After building, run: qmd doctor" >&2 - exit 1 -fi +// In published packages, bin/qmd must run dist/. In a git checkout, however, +// dist/ is often ignored and can be stale after git reset or branch switches. +// Prefer source mode only for checkouts so ./bin/qmd reflects the checked-out +// source without changing packaged/runtime behavior. +let useSourceMode = false; +let sourceRunner = null; +let sourceArgs = []; -# 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). -# -# package-lock.json takes priority: if it exists, npm installed the native -# modules for Node. The repo ships bun.lock, so without this check, source -# 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 "$JS" "$@" -elif [ -f "$DIR/bun.lock" ] || [ -f "$DIR/bun.lockb" ]; then - exec bun "$JS" "$@" -else - exec node "$JS" "$@" -fi +if (existsSync(resolve(pkgDir, ".git")) && existsSync(tsEntry)) { + if (existsSync(resolve(pkgDir, "bun.lock")) || existsSync(resolve(pkgDir, "bun.lockb"))) { + if (hasBun()) { + useSourceMode = true; + sourceRunner = "bun"; + sourceArgs = [tsEntry, ...process.argv.slice(2)]; + } + } + if (!useSourceMode && existsSync(resolve(pkgDir, "node_modules/tsx/dist/cli.mjs"))) { + useSourceMode = true; + sourceRunner = "node"; + sourceArgs = [resolve(pkgDir, "node_modules/tsx/dist/cli.mjs"), tsEntry, ...process.argv.slice(2)]; + } +} + +if (!useSourceMode && !existsSync(jsEntry)) { + console.error(`qmd is not built: missing ${jsEntry}`); + console.error("Run: bun install && bun run build"); + console.error("Or: npm install && npm run build"); + console.error("After building, run: qmd doctor"); + process.exit(1); +} + +// 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). +// +// package-lock.json takes priority: if it exists, npm installed the native +// modules for Node. The repo ships bun.lock, so without this check, source +// builds that use npm would be incorrectly routed to bun, causing ABI +// mismatches with better-sqlite3 / sqlite-vec (see #381). +let runnerName = "node"; +if (existsSync(resolve(pkgDir, "package-lock.json"))) { + runnerName = "node"; +} else if (existsSync(resolve(pkgDir, "bun.lock")) || existsSync(resolve(pkgDir, "bun.lockb"))) { + runnerName = "bun"; +} else { + runnerName = "node"; +} + +const runner = useSourceMode ? sourceRunner : (runnerName === "node" ? "node" : "bun"); +const args = useSourceMode ? sourceArgs : [jsEntry, ...process.argv.slice(2)]; +const needsShell = (runner === "bun") && process.platform === "win32"; + +const child = spawn(runner, args, { + stdio: "inherit", + shell: needsShell, +}); + +child.on("exit", (code, signal) => { + if (signal) { + process.kill(process.pid, signal); + } else { + process.exit(code ?? 0); + } +}); + +child.on("error", (err) => { + const name = useSourceMode ? sourceRunner : runnerName; + console.error(`qmd: failed to launch ${name}: ${err.message}`); + process.exit(1); +}); diff --git a/test/bin-wrapper.test.ts b/test/bin-wrapper.test.ts index c0fb439..f3c508f 100644 --- a/test/bin-wrapper.test.ts +++ b/test/bin-wrapper.test.ts @@ -17,10 +17,28 @@ function makeTempFixture() { for (const runtime of ["node", "bun"]) { const runtimePath = join(runtimeBin, runtime); - writeFileSync( - runtimePath, - `#!/bin/sh\n{\n printf '%s\\n' '${runtime}'\n printf '%s\\n' "$1"\n shift\n printf '%s\\n' "$@"\n} > "$QMD_WRAPPER_CAPTURE"\n`, - ); + if (runtime === "node") { + writeFileSync( + runtimePath, + `#!/bin/sh +if [ "$(basename "$1")" = "qmd" ]; then + exec "${process.execPath}" "$@" +else + { + printf '%s\\n' 'node' + printf '%s\\n' "$1" + shift + printf '%s\\n' "$@" + } > "$QMD_WRAPPER_CAPTURE" +fi +`, + ); + } else { + writeFileSync( + runtimePath, + `#!/bin/sh\n{\n printf '%s\\n' '${runtime}'\n printf '%s\\n' "$1"\n shift\n printf '%s\\n' "$@"\n} > "$QMD_WRAPPER_CAPTURE"\n`, + ); + } chmodSync(runtimePath, 0o755); }