diff --git a/package.json b/package.json index 4ac978a5..d2383b52 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Quick Markdown Search - Full-text and vector search for markdown files", "type": "module", "bin": { - "qmd": "./qmd" + "qmd": "./src/qmd.ts" }, "scripts": { "test": "bun test", diff --git a/src/qmd.ts b/src/qmd.ts index a6b4b312..91abd2d3 100755 --- a/src/qmd.ts +++ b/src/qmd.ts @@ -9,6 +9,8 @@ import { getRealPath, homedir, resolve, + normalizePathSeparators, + isAbsolutePath, enableProductionMode, searchFTS, searchVec, @@ -204,16 +206,17 @@ function computeDisplayPath( existingPaths: Set ): string { // Get path relative to collection (include collection dir name) - const collectionDir = collectionPath.replace(/\/$/, ''); + const collectionDir = normalizePathSeparators(collectionPath).replace(/\/$/, ''); const collectionName = collectionDir.split('/').pop() || ''; + const normalizedFilepath = normalizePathSeparators(filepath); let relativePath: string; - if (filepath.startsWith(collectionDir + '/')) { + if (normalizedFilepath.startsWith(collectionDir + '/')) { // filepath is under collection: use collection name + relative path - relativePath = collectionName + filepath.slice(collectionDir.length); + relativePath = collectionName + normalizedFilepath.slice(collectionDir.length); } else { // Fallback: just use the filepath - relativePath = filepath; + relativePath = normalizedFilepath; } const parts = relativePath.split('/').filter(p => p.length > 0); @@ -400,7 +403,10 @@ async function updateCollections(): Promise { if (yamlCol?.update) { console.log(`${c.dim} Running update command: ${yamlCol.update}${c.reset}`); try { - const proc = Bun.spawn(["/usr/bin/env", "bash", "-c", yamlCol.update], { + const shellCmd = process.platform === "win32" + ? ["cmd", "/c", yamlCol.update] + : ["/usr/bin/env", "bash", "-c", yamlCol.update]; + const proc = Bun.spawn(shellCmd, { cwd: col.pwd, stdout: "pipe", stderr: "pipe", @@ -447,7 +453,7 @@ async function updateCollections(): Promise { * Returns { collectionId, collectionName, relativePath } or null if not in any collection. */ function detectCollectionFromPath(db: Database, fsPath: string): { collectionName: string; relativePath: string } | null { - const realPath = getRealPath(fsPath); + const realPath = normalizePathSeparators(getRealPath(fsPath)); // Find collections that this path is under from YAML const allCollections = yamlListCollections(); @@ -455,9 +461,10 @@ function detectCollectionFromPath(db: Database, fsPath: string): { collectionNam // Find longest matching path let bestMatch: { name: string; path: string } | null = null; for (const coll of allCollections) { - if (realPath.startsWith(coll.path + '/') || realPath === coll.path) { - if (!bestMatch || coll.path.length > bestMatch.path.length) { - bestMatch = { name: coll.name, path: coll.path }; + const collPath = normalizePathSeparators(coll.path); + if (realPath.startsWith(collPath + '/') || realPath === collPath) { + if (!bestMatch || collPath.length > bestMatch.path.length) { + bestMatch = { name: coll.name, path: collPath }; } } } @@ -496,7 +503,7 @@ async function contextAdd(pathArg: string | undefined, contextText: string): Pro fsPath = getPwd(); } else if (fsPath.startsWith('~/')) { fsPath = homedir() + fsPath.slice(1); - } else if (!fsPath.startsWith('/') && !fsPath.startsWith('qmd://')) { + } else if (!isAbsolutePath(fsPath) && !fsPath.startsWith('qmd://')) { fsPath = resolve(getPwd(), fsPath); } @@ -608,7 +615,7 @@ function contextRemove(pathArg: string): void { fsPath = getPwd(); } else if (fsPath.startsWith('~/')) { fsPath = homedir() + fsPath.slice(1); - } else if (!fsPath.startsWith('/')) { + } else if (!isAbsolutePath(fsPath)) { fsPath = resolve(getPwd(), fsPath); } @@ -749,8 +756,8 @@ function getDocument(filename: string, fromLine?: number, maxLines?: number, lin } else { // Try to interpret as collection/path format first (before filesystem path) // If path is relative (no / or ~ prefix), check if first component is a collection name - if (!inputPath.startsWith('/') && !inputPath.startsWith('~')) { - const parts = inputPath.split('/'); + if (!isAbsolutePath(inputPath) && !inputPath.startsWith('~')) { + const parts = normalizePathSeparators(inputPath).split('/'); if (parts.length >= 2) { const possibleCollection = parts[0]; const possiblePath = parts.slice(1).join('/'); @@ -795,7 +802,7 @@ function getDocument(filename: string, fromLine?: number, maxLines?: number, lin // Expand ~ to home directory if (fsPath.startsWith('~/')) { fsPath = homedir() + fsPath.slice(1); - } else if (!fsPath.startsWith('/')) { + } else if (!isAbsolutePath(fsPath)) { // Relative path - resolve from current directory fsPath = resolve(getPwd(), fsPath); } @@ -816,7 +823,7 @@ function getDocument(filename: string, fromLine?: number, maxLines?: number, lin // Fuzzy match by filename (last component of path) if (!doc) { - const filename = inputPath.split('/').pop() || inputPath; + const filename = normalizePathSeparators(inputPath).split('/').pop() || inputPath; doc = db.prepare(` SELECT d.collection as collectionName, d.path, content.doc as body FROM documents d @@ -1154,7 +1161,7 @@ function listFiles(pathArg?: string): void { pathPrefix = parsed.path; } else { // Just collection name or collection/path - const parts = pathArg.split('/'); + const parts = normalizePathSeparators(pathArg).split('/'); collectionName = parts[0] || ''; if (parts.length > 1) { pathPrefix = parts.slice(1).join('/'); @@ -1275,7 +1282,7 @@ async function collectionAdd(pwd: string, globPattern: string, name?: string): P // If name not provided, generate from pwd basename let collName = name; if (!collName) { - const parts = pwd.split('/').filter(Boolean); + const parts = normalizePathSeparators(pwd).split('/').filter(Boolean); collName = parts[parts.length - 1] || 'root'; } diff --git a/src/store.ts b/src/store.ts index fb7dd70d..fd811103 100644 --- a/src/store.ts +++ b/src/store.ts @@ -13,7 +13,8 @@ import { Database } from "bun:sqlite"; import { Glob } from "bun"; -import { realpathSync, statSync } from "node:fs"; +import { mkdirSync, realpathSync, statSync } from "node:fs"; +import { fileURLToPath } from "node:url"; import * as sqliteVec from "sqlite-vec"; import { LlamaCpp, @@ -42,7 +43,7 @@ import { // Configuration // ============================================================================= -const HOME = Bun.env.HOME || "/tmp"; +const HOME = Bun.env.HOME || Bun.env.USERPROFILE || "/tmp"; export const DEFAULT_EMBED_MODEL = "embeddinggemma"; export const DEFAULT_RERANK_MODEL = "ExpedientFalcon/qwen3-reranker:0.6b-q8_0"; export const DEFAULT_QUERY_MODEL = "Qwen/Qwen3-1.7B"; @@ -254,7 +255,7 @@ export function getDefaultDbPath(indexName: string = "index"): string { const cacheDir = Bun.env.XDG_CACHE_HOME || resolve(homedir(), ".cache"); const qmdCacheDir = resolve(cacheDir, "qmd"); - try { Bun.spawnSync(["mkdir", "-p", qmdCacheDir]); } catch { } + try { mkdirSync(qmdCacheDir, { recursive: true }); } catch { } return resolve(qmdCacheDir, `${indexName}.sqlite`); } @@ -441,14 +442,26 @@ function initializeDatabase(db: Database): void { try { sqliteVec.load(db); } catch (err) { - if (err instanceof Error && err.message.includes("does not support dynamic extension loading")) { + // On Windows, sqlite-vec may fail to resolve the native DLL when running + // from outside the project directory. Fall back to loading it directly + // from this project's node_modules using an absolute path. + if (process.platform === "win32") { + try { + const scriptDir = resolve(fileURLToPath(import.meta.url), ".."); + const vecDll = resolve(scriptDir, "..", "node_modules", "sqlite-vec-windows-x64", "vec0"); + db.loadExtension(vecDll); + } catch (fallbackErr) { + throw err; // throw original error if fallback also fails + } + } else if (err instanceof Error && err.message.includes("does not support dynamic extension loading")) { throw new Error( "SQLite build does not support dynamic extension loading. " + "Install Homebrew SQLite so the sqlite-vec extension can be loaded, " + "and set BREW_PREFIX if Homebrew is installed in a non-standard location." ); + } else { + throw err; } - throw err; } db.exec("PRAGMA journal_mode = WAL"); db.exec("PRAGMA foreign_keys = ON");