@tobilu/qmd 2.5.1 → 2.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/bin/qmd +105 -62
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [2.5.2] - 2026-05-22
6
+
7
+ ### Fixes
8
+
9
+ - Launcher: Rewrite `bin/qmd` as a Node-based shebang polyglot to fix global npm installation execution failures on Windows (#668 / #452), while supporting seamless fallback to Bun in Node-less environments.
10
+
11
+
5
12
  ## [2.5.1] - 2026-05-20
6
13
 
7
14
  ### Changes
package/bin/qmd CHANGED
@@ -1,68 +1,111 @@
1
- #!/bin/sh
2
- # Resolve symlinks so global installs (npm link / npm install -g) can find the
3
- # actual package directory instead of the global bin directory.
4
- SOURCE="$0"
5
- while [ -L "$SOURCE" ]; do
6
- SOURCE_DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
7
- TARGET="$(readlink "$SOURCE")"
8
- case "$TARGET" in
9
- /*) SOURCE="$TARGET" ;;
10
- *) SOURCE="$SOURCE_DIR/$TARGET" ;;
11
- esac
12
- done
1
+ #!/usr/bin/env node
2
+ // 2>/dev/null; if command -v node >/dev/null 2>&1; then exec node "$0" "$@"; else exec bun "$0" "$@"; fi
3
+ // Cross-platform launcher for qmd.
4
+ //
5
+ // Previously this was a POSIX shell script with `#!/bin/sh`, which meant npm
6
+ // on Windows generated shims that tried to route through `/bin/sh` — a path
7
+ // that doesn't exist on Windows, so `qmd` failed immediately after a global
8
+ // install. Rewriting the launcher in Node.js lets npm generate native
9
+ // cmd/ps1/sh shims that invoke `node` directly on every platform.
13
10
 
14
- # Detect the runtime used to install this package and use the matching one
15
- # to avoid native module ABI mismatches (e.g., better-sqlite3 compiled for bun vs node)
16
- DIR="$(cd -P "$(dirname "$SOURCE")/.." && pwd)"
11
+ import { spawn, spawnSync } from "node:child_process";
12
+ import { existsSync, realpathSync } from "node:fs";
13
+ import { dirname, resolve } from "node:path";
14
+ import { fileURLToPath } from "node:url";
17
15
 
18
- # MCP stdio reserves stdout exclusively for JSON-RPC frames. node-llama-cpp
19
- # / llama.cpp / ggml can write native logs directly to stdout before JS-level
20
- # log handlers are attached, so seed the native quiet env before Node/Bun imports
21
- # the CLI and its LLM modules. Preserve explicit user values when provided.
22
- if [ "$1" = "mcp" ]; then
23
- export LLAMA_LOG_LEVEL="${LLAMA_LOG_LEVEL:-error}"
24
- export GGML_LOG_LEVEL="${GGML_LOG_LEVEL:-error}"
25
- export GGML_BACKEND_SILENT="${GGML_BACKEND_SILENT:-1}"
26
- fi
16
+ // Resolve symlinks so global installs (npm link / npm install -g) can find
17
+ // the actual package directory instead of the global bin directory.
18
+ const self = realpathSync(fileURLToPath(import.meta.url));
19
+ const pkgDir = resolve(dirname(self), "..");
20
+ const jsEntry = resolve(pkgDir, "dist/cli/qmd.js");
21
+ const tsEntry = resolve(pkgDir, "src/cli/qmd.ts");
27
22
 
28
- JS="$DIR/dist/cli/qmd.js"
29
- TS="$DIR/src/cli/qmd.ts"
23
+ // MCP stdio reserves stdout exclusively for JSON-RPC frames. node-llama-cpp
24
+ // / llama.cpp / ggml can write native logs directly to stdout before JS-level
25
+ // log handlers are attached, so seed the native quiet env before Node/Bun imports
26
+ // the CLI and its LLM modules. Preserve explicit user values when provided.
27
+ if (process.argv[2] === "mcp") {
28
+ process.env.LLAMA_LOG_LEVEL = process.env.LLAMA_LOG_LEVEL || "error";
29
+ process.env.GGML_LOG_LEVEL = process.env.GGML_LOG_LEVEL || "error";
30
+ process.env.GGML_BACKEND_SILENT = process.env.GGML_BACKEND_SILENT || "1";
31
+ }
30
32
 
31
- # In published packages, bin/qmd must run dist/. In a git checkout, however,
32
- # dist/ is often ignored and can be stale after git reset or branch switches.
33
- # Prefer source mode only for checkouts so ./bin/qmd reflects the checked-out
34
- # source without changing packaged/runtime behavior.
35
- if [ -e "$DIR/.git" ] && [ -f "$TS" ]; then
36
- if [ -f "$DIR/bun.lock" ] || [ -f "$DIR/bun.lockb" ]; then
37
- if command -v bun >/dev/null 2>&1; then
38
- exec bun "$TS" "$@"
39
- fi
40
- fi
41
- if [ -f "$DIR/node_modules/tsx/dist/cli.mjs" ]; then
42
- exec node "$DIR/node_modules/tsx/dist/cli.mjs" "$TS" "$@"
43
- fi
44
- fi
33
+ function hasBun() {
34
+ try {
35
+ const res = spawnSync("bun", ["--version"], { stdio: "ignore", shell: process.platform === "win32" });
36
+ return res.status === 0;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
45
41
 
46
- if [ ! -f "$JS" ]; then
47
- echo "qmd is not built: missing $JS" >&2
48
- echo "Run: bun install && bun run build" >&2
49
- echo "Or: npm install && npm run build" >&2
50
- echo "After building, run: qmd doctor" >&2
51
- exit 1
52
- fi
42
+ // In published packages, bin/qmd must run dist/. In a git checkout, however,
43
+ // dist/ is often ignored and can be stale after git reset or branch switches.
44
+ // Prefer source mode only for checkouts so ./bin/qmd reflects the checked-out
45
+ // source without changing packaged/runtime behavior.
46
+ let useSourceMode = false;
47
+ let sourceRunner = null;
48
+ let sourceArgs = [];
53
49
 
54
- # Detect the package manager that installed dependencies by checking lockfiles.
55
- # $BUN_INSTALL is intentionally NOT checked — it only indicates that bun exists
56
- # on the system, not that it was used to install this package (see #361).
57
- #
58
- # package-lock.json takes priority: if it exists, npm installed the native
59
- # modules for Node. The repo ships bun.lock, so without this check, source
60
- # builds that use npm would be incorrectly routed to bun, causing ABI
61
- # mismatches with better-sqlite3 / sqlite-vec (see #381).
62
- if [ -f "$DIR/package-lock.json" ]; then
63
- exec node "$JS" "$@"
64
- elif [ -f "$DIR/bun.lock" ] || [ -f "$DIR/bun.lockb" ]; then
65
- exec bun "$JS" "$@"
66
- else
67
- exec node "$JS" "$@"
68
- fi
50
+ if (existsSync(resolve(pkgDir, ".git")) && existsSync(tsEntry)) {
51
+ if (existsSync(resolve(pkgDir, "bun.lock")) || existsSync(resolve(pkgDir, "bun.lockb"))) {
52
+ if (hasBun()) {
53
+ useSourceMode = true;
54
+ sourceRunner = "bun";
55
+ sourceArgs = [tsEntry, ...process.argv.slice(2)];
56
+ }
57
+ }
58
+ if (!useSourceMode && existsSync(resolve(pkgDir, "node_modules/tsx/dist/cli.mjs"))) {
59
+ useSourceMode = true;
60
+ sourceRunner = "node";
61
+ sourceArgs = [resolve(pkgDir, "node_modules/tsx/dist/cli.mjs"), tsEntry, ...process.argv.slice(2)];
62
+ }
63
+ }
64
+
65
+ if (!useSourceMode && !existsSync(jsEntry)) {
66
+ console.error(`qmd is not built: missing ${jsEntry}`);
67
+ console.error("Run: bun install && bun run build");
68
+ console.error("Or: npm install && npm run build");
69
+ console.error("After building, run: qmd doctor");
70
+ process.exit(1);
71
+ }
72
+
73
+ // Detect the package manager that installed dependencies by checking lockfiles.
74
+ // $BUN_INSTALL is intentionally NOT checked — it only indicates that bun exists
75
+ // on the system, not that it was used to install this package (see #361).
76
+ //
77
+ // package-lock.json takes priority: if it exists, npm installed the native
78
+ // modules for Node. The repo ships bun.lock, so without this check, source
79
+ // builds that use npm would be incorrectly routed to bun, causing ABI
80
+ // mismatches with better-sqlite3 / sqlite-vec (see #381).
81
+ let runnerName = "node";
82
+ if (existsSync(resolve(pkgDir, "package-lock.json"))) {
83
+ runnerName = "node";
84
+ } else if (existsSync(resolve(pkgDir, "bun.lock")) || existsSync(resolve(pkgDir, "bun.lockb"))) {
85
+ runnerName = "bun";
86
+ } else {
87
+ runnerName = "node";
88
+ }
89
+
90
+ const runner = useSourceMode ? sourceRunner : (runnerName === "node" ? "node" : "bun");
91
+ const args = useSourceMode ? sourceArgs : [jsEntry, ...process.argv.slice(2)];
92
+ const needsShell = (runner === "bun") && process.platform === "win32";
93
+
94
+ const child = spawn(runner, args, {
95
+ stdio: "inherit",
96
+ shell: needsShell,
97
+ });
98
+
99
+ child.on("exit", (code, signal) => {
100
+ if (signal) {
101
+ process.kill(process.pid, signal);
102
+ } else {
103
+ process.exit(code ?? 0);
104
+ }
105
+ });
106
+
107
+ child.on("error", (err) => {
108
+ const name = useSourceMode ? sourceRunner : runnerName;
109
+ console.error(`qmd: failed to launch ${name}: ${err.message}`);
110
+ process.exit(1);
111
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tobilu/qmd",
3
- "version": "2.5.1",
3
+ "version": "2.5.2",
4
4
  "description": "Query Markup Documents - On-device hybrid search for markdown files with BM25, vector search, and LLM reranking",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",