diff --git a/src/cli/qmd.ts b/src/cli/qmd.ts index a09ffb3..0e178c0 100755 --- a/src/cli/qmd.ts +++ b/src/cli/qmd.ts @@ -110,6 +110,7 @@ enableProductionMode(); let store: ReturnType | null = null; let storeDbPathOverride: string | undefined; +let currentIndexName = "index"; function getStore(): ReturnType { if (!store) { @@ -160,6 +161,10 @@ function getDbPath(): string { return store?.dbPath ?? storeDbPathOverride ?? getDefaultDbPath(); } +function getActiveIndexName(): string { + return currentIndexName; +} + function setIndexName(name: string | null): void { let normalizedName = name; // Normalize relative paths to prevent malformed database paths @@ -170,6 +175,7 @@ function setIndexName(name: string | null): void { // Replace path separators with underscores to create a valid filename normalizedName = absolutePath.replace(/\//g, '_').replace(/^_/, ''); } + currentIndexName = normalizedName || "index"; storeDbPathOverride = normalizedName ? getDefaultDbPath(normalizedName) : undefined; // Reset open handle so next use opens the new index closeDb(); @@ -818,8 +824,6 @@ function contextRemove(pathArg: string): void { } function getDocument(filename: string, fromLine?: number, maxLines?: number, lineNumbers?: boolean): void { - const db = getDb(); - // Parse :linenum suffix from filename (e.g., "file.md:100") let inputPath = filename; const colonMatch = inputPath.match(/:(\d+)$/); @@ -831,6 +835,14 @@ function getDocument(filename: string, fromLine?: number, maxLines?: number, lin } } + const parsedIndexPath = isVirtualPath(inputPath) ? parseVirtualPath(inputPath) : null; + if (parsedIndexPath?.indexName) { + setIndexName(parsedIndexPath.indexName); + setConfigIndexName(parsedIndexPath.indexName); + } + + const db = getDb(); + // Handle docid lookup (#abc123, abc123, "#abc123", "abc123", etc.) if (isDocid(inputPath)) { const docidMatch = findDocumentByDocid(db, inputPath); @@ -842,7 +854,6 @@ function getDocument(filename: string, fromLine?: number, maxLines?: number, lin process.exit(1); } } - let doc: { collectionName: string; path: string; body: string } | null = null; let virtualPath: string; @@ -1925,7 +1936,18 @@ function outputResults(results: OutputRow[], query: string, opts: OutputOptions) } // Helper to create qmd:// URI from displayPath - const toQmdPath = (displayPath: string) => `qmd://${displayPath}`; + const toQmdPath = (displayPath: string) => { + const [collectionName, ...segments] = displayPath.split("/"); + if (!collectionName || segments.length === 0) { + return `qmd://${displayPath}`; + } + const indexName = getActiveIndexName(); + return buildVirtualPath( + collectionName, + segments.join("/"), + indexName === "index" ? undefined : indexName, + ); + }; if (opts.format === "json") { // JSON output for LLM consumption diff --git a/src/store.ts b/src/store.ts index ab4cbf4..7ad78b0 100644 --- a/src/store.ts +++ b/src/store.ts @@ -566,6 +566,7 @@ export function getRealPath(path: string): string { export type VirtualPath = { collectionName: string; path: string; // relative path within collection + indexName?: string; }; /** @@ -609,22 +610,26 @@ export function normalizeVirtualPath(input: string): string { export function parseVirtualPath(virtualPath: string): VirtualPath | null { // Normalize the path first const normalized = normalizeVirtualPath(virtualPath); + const [pathPart = normalized, queryString = ""] = normalized.split("?"); // Match: qmd://collection-name[/optional-path] // Allows: qmd://name, qmd://name/, qmd://name/path - const match = normalized.match(/^qmd:\/\/([^\/]+)\/?(.*)$/); + const match = pathPart.match(/^qmd:\/\/([^\/]+)\/?(.*)$/); if (!match?.[1]) return null; + const indexName = new URLSearchParams(queryString).get("index")?.trim() || undefined; return { collectionName: match[1], path: match[2] ?? '', // Empty string for collection root + ...(indexName ? { indexName } : {}), }; } /** * Build a virtual path from collection name and relative path. */ -export function buildVirtualPath(collectionName: string, path: string): string { - return `qmd://${collectionName}/${path}`; +export function buildVirtualPath(collectionName: string, path: string, indexName?: string): string { + const base = `qmd://${collectionName}/${path}`; + return indexName ? `${base}?index=${encodeURIComponent(indexName)}` : base; } /** diff --git a/test/cli.test.ts b/test/cli.test.ts index c9c5644..a95d8de 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -1130,6 +1130,42 @@ describe("search output formats", () => { expect(result.file).not.toMatch(/^\/home\//); }); + test("custom-index search links include ?index= and can be passed back to qmd get", async () => { + const env = await createIsolatedTestEnv("custom-index-links"); + const customColl = "fixtures-alt"; + const customIndex = "release-notes"; + const customCacheDir = join(testDir, `cache-${Date.now()}-${Math.random().toString(16).slice(2)}`); + await mkdir(customCacheDir, { recursive: true }); + + const sharedEnv = { + INDEX_PATH: "", + XDG_CACHE_HOME: customCacheDir, + }; + + const addResult = await runQmd( + ["--index", customIndex, "collection", "add", fixturesDir, "--name", customColl], + { dbPath: env.dbPath, configDir: env.configDir, env: sharedEnv } + ); + expect(addResult.exitCode).toBe(0); + + const searchResult = await runQmd( + ["--index", customIndex, "search", "test", "--json", "-n", "1"], + { dbPath: env.dbPath, configDir: env.configDir, env: sharedEnv } + ); + expect(searchResult.exitCode).toBe(0); + + const results = JSON.parse(searchResult.stdout); + const file = results[0]?.file; + expect(file).toMatch(new RegExp(`^qmd://${customColl}/.+\\?index=${customIndex}$`)); + + const getResult = await runQmd( + ["get", file, "-l", "2"], + { dbPath: env.dbPath, configDir: env.configDir, env: sharedEnv } + ); + expect(getResult.exitCode).toBe(0); + expect(getResult.stdout.trim().length).toBeGreaterThan(0); + }); + test("search --files includes qmd:// path, docid, and context", async () => { const { stdout, exitCode } = await runQmd(["search", "test", "--files", "-n", "1"], { dbPath: localDbPath, configDir: localConfigDir }); expect(exitCode).toBe(0); diff --git a/test/store.test.ts b/test/store.test.ts index 47b481b..9503c15 100644 --- a/test/store.test.ts +++ b/test/store.test.ts @@ -3154,6 +3154,14 @@ describe("parseVirtualPath", () => { }); }); + test("parses qmd:// paths with index query parameters", () => { + expect(parseVirtualPath("qmd://collection/path.md?index=docs-v2")).toEqual({ + collectionName: "collection", + path: "path.md", + indexName: "docs-v2", + }); + }); + test("returns null for non-virtual paths", () => { expect(parseVirtualPath("/absolute/path.md")).toBe(null); expect(parseVirtualPath("~/home/path.md")).toBe(null);