fix(uri): include index in custom qmd links
This commit is contained in:
parent
c2f3a40372
commit
8404cc3bb1
@ -110,6 +110,7 @@ enableProductionMode();
|
|||||||
|
|
||||||
let store: ReturnType<typeof createStore> | null = null;
|
let store: ReturnType<typeof createStore> | null = null;
|
||||||
let storeDbPathOverride: string | undefined;
|
let storeDbPathOverride: string | undefined;
|
||||||
|
let currentIndexName = "index";
|
||||||
|
|
||||||
function getStore(): ReturnType<typeof createStore> {
|
function getStore(): ReturnType<typeof createStore> {
|
||||||
if (!store) {
|
if (!store) {
|
||||||
@ -160,6 +161,10 @@ function getDbPath(): string {
|
|||||||
return store?.dbPath ?? storeDbPathOverride ?? getDefaultDbPath();
|
return store?.dbPath ?? storeDbPathOverride ?? getDefaultDbPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getActiveIndexName(): string {
|
||||||
|
return currentIndexName;
|
||||||
|
}
|
||||||
|
|
||||||
function setIndexName(name: string | null): void {
|
function setIndexName(name: string | null): void {
|
||||||
let normalizedName = name;
|
let normalizedName = name;
|
||||||
// Normalize relative paths to prevent malformed database paths
|
// 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
|
// Replace path separators with underscores to create a valid filename
|
||||||
normalizedName = absolutePath.replace(/\//g, '_').replace(/^_/, '');
|
normalizedName = absolutePath.replace(/\//g, '_').replace(/^_/, '');
|
||||||
}
|
}
|
||||||
|
currentIndexName = normalizedName || "index";
|
||||||
storeDbPathOverride = normalizedName ? getDefaultDbPath(normalizedName) : undefined;
|
storeDbPathOverride = normalizedName ? getDefaultDbPath(normalizedName) : undefined;
|
||||||
// Reset open handle so next use opens the new index
|
// Reset open handle so next use opens the new index
|
||||||
closeDb();
|
closeDb();
|
||||||
@ -818,8 +824,6 @@ function contextRemove(pathArg: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getDocument(filename: string, fromLine?: number, maxLines?: number, lineNumbers?: boolean): 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")
|
// Parse :linenum suffix from filename (e.g., "file.md:100")
|
||||||
let inputPath = filename;
|
let inputPath = filename;
|
||||||
const colonMatch = inputPath.match(/:(\d+)$/);
|
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.)
|
// Handle docid lookup (#abc123, abc123, "#abc123", "abc123", etc.)
|
||||||
if (isDocid(inputPath)) {
|
if (isDocid(inputPath)) {
|
||||||
const docidMatch = findDocumentByDocid(db, inputPath);
|
const docidMatch = findDocumentByDocid(db, inputPath);
|
||||||
@ -842,7 +854,6 @@ function getDocument(filename: string, fromLine?: number, maxLines?: number, lin
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let doc: { collectionName: string; path: string; body: string } | null = null;
|
let doc: { collectionName: string; path: string; body: string } | null = null;
|
||||||
let virtualPath: string;
|
let virtualPath: string;
|
||||||
|
|
||||||
@ -1925,7 +1936,18 @@ function outputResults(results: OutputRow[], query: string, opts: OutputOptions)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper to create qmd:// URI from displayPath
|
// 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") {
|
if (opts.format === "json") {
|
||||||
// JSON output for LLM consumption
|
// JSON output for LLM consumption
|
||||||
|
|||||||
11
src/store.ts
11
src/store.ts
@ -566,6 +566,7 @@ export function getRealPath(path: string): string {
|
|||||||
export type VirtualPath = {
|
export type VirtualPath = {
|
||||||
collectionName: string;
|
collectionName: string;
|
||||||
path: string; // relative path within collection
|
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 {
|
export function parseVirtualPath(virtualPath: string): VirtualPath | null {
|
||||||
// Normalize the path first
|
// Normalize the path first
|
||||||
const normalized = normalizeVirtualPath(virtualPath);
|
const normalized = normalizeVirtualPath(virtualPath);
|
||||||
|
const [pathPart = normalized, queryString = ""] = normalized.split("?");
|
||||||
|
|
||||||
// Match: qmd://collection-name[/optional-path]
|
// Match: qmd://collection-name[/optional-path]
|
||||||
// Allows: qmd://name, qmd://name/, qmd://name/path
|
// Allows: qmd://name, qmd://name/, qmd://name/path
|
||||||
const match = normalized.match(/^qmd:\/\/([^\/]+)\/?(.*)$/);
|
const match = pathPart.match(/^qmd:\/\/([^\/]+)\/?(.*)$/);
|
||||||
if (!match?.[1]) return null;
|
if (!match?.[1]) return null;
|
||||||
|
const indexName = new URLSearchParams(queryString).get("index")?.trim() || undefined;
|
||||||
return {
|
return {
|
||||||
collectionName: match[1],
|
collectionName: match[1],
|
||||||
path: match[2] ?? '', // Empty string for collection root
|
path: match[2] ?? '', // Empty string for collection root
|
||||||
|
...(indexName ? { indexName } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a virtual path from collection name and relative path.
|
* Build a virtual path from collection name and relative path.
|
||||||
*/
|
*/
|
||||||
export function buildVirtualPath(collectionName: string, path: string): string {
|
export function buildVirtualPath(collectionName: string, path: string, indexName?: string): string {
|
||||||
return `qmd://${collectionName}/${path}`;
|
const base = `qmd://${collectionName}/${path}`;
|
||||||
|
return indexName ? `${base}?index=${encodeURIComponent(indexName)}` : base;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1130,6 +1130,42 @@ describe("search output formats", () => {
|
|||||||
expect(result.file).not.toMatch(/^\/home\//);
|
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 () => {
|
test("search --files includes qmd:// path, docid, and context", async () => {
|
||||||
const { stdout, exitCode } = await runQmd(["search", "test", "--files", "-n", "1"], { dbPath: localDbPath, configDir: localConfigDir });
|
const { stdout, exitCode } = await runQmd(["search", "test", "--files", "-n", "1"], { dbPath: localDbPath, configDir: localConfigDir });
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|||||||
@ -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", () => {
|
test("returns null for non-virtual paths", () => {
|
||||||
expect(parseVirtualPath("/absolute/path.md")).toBe(null);
|
expect(parseVirtualPath("/absolute/path.md")).toBe(null);
|
||||||
expect(parseVirtualPath("~/home/path.md")).toBe(null);
|
expect(parseVirtualPath("~/home/path.md")).toBe(null);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user