@ncukondo/reference-manager 0.7.0 → 0.7.1
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.
- package/dist/chunks/{search-prompt-D67WyKrY.js → search-prompt-RtHDJFgL.js} +40 -11
- package/dist/chunks/search-prompt-RtHDJFgL.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/features/interactive/search-prompt.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/chunks/search-prompt-D67WyKrY.js.map +0 -1
|
@@ -94,8 +94,7 @@ function createChoices(results, terminalWidth) {
|
|
|
94
94
|
index,
|
|
95
95
|
item: result.reference
|
|
96
96
|
}),
|
|
97
|
-
message: formattedText
|
|
98
|
-
value: result.reference.id
|
|
97
|
+
message: formattedText
|
|
99
98
|
};
|
|
100
99
|
});
|
|
101
100
|
}
|
|
@@ -115,6 +114,30 @@ function parseSelectedValues(values) {
|
|
|
115
114
|
function getTerminalWidth() {
|
|
116
115
|
return process.stdout.columns ?? 80;
|
|
117
116
|
}
|
|
117
|
+
function collectEnabledIds(choices) {
|
|
118
|
+
const enabledIds = /* @__PURE__ */ new Set();
|
|
119
|
+
for (const choice of choices) {
|
|
120
|
+
if (choice.enabled) {
|
|
121
|
+
try {
|
|
122
|
+
const data = JSON.parse(choice.name);
|
|
123
|
+
enabledIds.add(data.item.id);
|
|
124
|
+
} catch {
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return enabledIds;
|
|
129
|
+
}
|
|
130
|
+
function restoreEnabledState(choices, enabledIds) {
|
|
131
|
+
for (const choice of choices) {
|
|
132
|
+
try {
|
|
133
|
+
const data = JSON.parse(choice.name);
|
|
134
|
+
if (enabledIds.has(data.item.id)) {
|
|
135
|
+
choice.enabled = true;
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
118
141
|
async function runSearchPrompt(allReferences, searchFn, config, initialQuery = "") {
|
|
119
142
|
const enquirer = await import("enquirer");
|
|
120
143
|
const AutoComplete = enquirer.default.AutoComplete;
|
|
@@ -139,23 +162,29 @@ async function runSearchPrompt(allReferences, searchFn, config, initialQuery = "
|
|
|
139
162
|
return choices;
|
|
140
163
|
}
|
|
141
164
|
lastQuery = input;
|
|
142
|
-
|
|
143
|
-
|
|
165
|
+
const enabledIds = collectEnabledIds(choices);
|
|
166
|
+
const newChoices = input.trim() ? createChoices(searchFn(input).slice(0, config.limit), terminalWidth) : createChoices(
|
|
167
|
+
allReferences.slice(0, config.limit).map((ref) => ({
|
|
144
168
|
reference: ref,
|
|
145
169
|
overallStrength: "exact",
|
|
146
170
|
tokenMatches: [],
|
|
147
171
|
score: 0
|
|
148
|
-
}))
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return
|
|
172
|
+
})),
|
|
173
|
+
terminalWidth
|
|
174
|
+
);
|
|
175
|
+
restoreEnabledState(newChoices, enabledIds);
|
|
176
|
+
return newChoices;
|
|
153
177
|
}
|
|
154
178
|
};
|
|
155
179
|
try {
|
|
156
180
|
const prompt = new AutoComplete(promptOptions);
|
|
157
181
|
const result = await prompt.run();
|
|
158
|
-
const
|
|
182
|
+
const promptAny = prompt;
|
|
183
|
+
let valuesToParse = result;
|
|
184
|
+
if (Array.isArray(result) && result.length === 0 && promptAny.focused?.enabled && promptAny.focused?.name) {
|
|
185
|
+
valuesToParse = [promptAny.focused.name];
|
|
186
|
+
}
|
|
187
|
+
const selected = parseSelectedValues(valuesToParse);
|
|
159
188
|
return {
|
|
160
189
|
selected,
|
|
161
190
|
cancelled: false
|
|
@@ -176,4 +205,4 @@ export {
|
|
|
176
205
|
parseSelectedValues,
|
|
177
206
|
runSearchPrompt
|
|
178
207
|
};
|
|
179
|
-
//# sourceMappingURL=search-prompt-
|
|
208
|
+
//# sourceMappingURL=search-prompt-RtHDJFgL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-prompt-RtHDJFgL.js","sources":["../../src/features/interactive/format.ts","../../src/features/interactive/search-prompt.ts"],"sourcesContent":["/**\n * Display format functions for interactive search\n */\n\nimport type { CslItem } from \"../../core/csl-json/types.js\";\n\n/**\n * CSL name type (author structure)\n */\ntype CslName = NonNullable<CslItem[\"author\"]>[number];\n\n/**\n * Format a single author name\n * - Personal: \"Smith, J.\" (family + initial of given)\n * - Institutional: \"World Health Organization\" (literal)\n */\nfunction formatSingleAuthor(author: CslName): string {\n if (author.literal) {\n return author.literal;\n }\n if (author.family) {\n if (author.given) {\n const initial = author.given.charAt(0).toUpperCase();\n return `${author.family}, ${initial}.`;\n }\n return author.family;\n }\n return \"\";\n}\n\n/**\n * Format author list for display\n * - Single author: \"Smith, J.\"\n * - Two authors: \"Smith, J., & Doe, A.\"\n * - Three authors: \"Smith, J., Doe, A., & Johnson, B.\"\n * - More than three: \"Smith, J., et al.\"\n *\n * @param authors - Array of CSL author objects\n * @returns Formatted author string\n */\nexport function formatAuthors(authors: CslName[] | undefined): string {\n if (!authors || authors.length === 0) {\n return \"\";\n }\n\n if (authors.length > 3) {\n const first = authors[0];\n if (!first) {\n return \"\";\n }\n return `${formatSingleAuthor(first)}, et al.`;\n }\n\n const formatted = authors.map(formatSingleAuthor);\n if (formatted.length === 1) {\n return formatted[0] ?? \"\";\n }\n\n // Join all but last with \", \" and append \"& \" + last\n const allButLast = formatted.slice(0, -1).join(\", \");\n const last = formatted[formatted.length - 1] ?? \"\";\n return `${allButLast}, & ${last}`;\n}\n\n/**\n * Truncate title to fit terminal width\n *\n * @param title - Title string\n * @param maxWidth - Maximum display width\n * @returns Truncated title with ellipsis if needed\n */\nexport function formatTitle(title: string | undefined, maxWidth: number): string {\n if (!title) {\n return \"\";\n }\n\n if (title.length <= maxWidth) {\n return title;\n }\n\n // Truncate and add ellipsis, keeping total length at maxWidth\n return `${title.slice(0, maxWidth - 3)}...`;\n}\n\n/**\n * Format identifiers (DOI, PMID, PMCID, ISBN) for display\n *\n * @param item - CSL item\n * @returns Formatted identifier string (e.g., \"DOI: 10.1000/example | PMID: 12345678\")\n */\nexport function formatIdentifiers(item: CslItem): string {\n const identifiers: string[] = [];\n\n if (item.DOI) {\n identifiers.push(`DOI: ${item.DOI}`);\n }\n if (item.PMID) {\n identifiers.push(`PMID: ${item.PMID}`);\n }\n if (item.PMCID) {\n identifiers.push(`PMCID: ${item.PMCID}`);\n }\n if (item.ISBN) {\n identifiers.push(`ISBN: ${item.ISBN}`);\n }\n\n return identifiers.join(\" | \");\n}\n\n/**\n * Extract year from CSL item\n */\nfunction extractYear(item: CslItem): number | undefined {\n const dateParts = item.issued?.[\"date-parts\"];\n if (!dateParts || dateParts.length === 0) {\n return undefined;\n }\n const firstDatePart = dateParts[0];\n if (!firstDatePart || firstDatePart.length === 0) {\n return undefined;\n }\n return firstDatePart[0];\n}\n\n/**\n * Compose a complete search result line\n *\n * Format:\n * ```\n * [1] Smith, J., & Doe, A. (2020)\n * Machine learning in medicine: A comprehensive review\n * DOI: 10.1000/example | PMID: 12345678\n * ```\n *\n * @param item - CSL item\n * @param index - Display index (1-based)\n * @param terminalWidth - Terminal width for title truncation\n * @returns Multi-line formatted string\n */\nexport function formatSearchResult(item: CslItem, index: number, terminalWidth: number): string {\n const lines: string[] = [];\n\n // Line 1: [index] Authors (year)\n const authors = formatAuthors(item.author);\n const year = extractYear(item);\n const yearPart = year !== undefined ? ` (${year})` : \"\";\n const line1 = `[${index}] ${authors}${yearPart}`;\n lines.push(line1);\n\n // Line 2: Title (indented, truncated)\n const indent = \" \";\n const titleMaxWidth = terminalWidth - indent.length;\n const title = formatTitle(item.title, titleMaxWidth);\n if (title) {\n lines.push(`${indent}${title}`);\n }\n\n // Line 3: Identifiers (indented)\n const identifiers = formatIdentifiers(item);\n if (identifiers) {\n lines.push(`${indent}${identifiers}`);\n }\n\n return lines.join(\"\\n\");\n}\n","/**\n * Interactive search prompt using Enquirer's AutoComplete\n *\n * Provides real-time incremental search with multiple selection support.\n */\n\nimport type { CslItem } from \"../../core/csl-json/types.js\";\nimport type { SearchResult } from \"../search/types.js\";\nimport type { AutoCompleteChoice } from \"./enquirer.js\";\nimport { formatSearchResult } from \"./format.js\";\n\n/**\n * Configuration for the search prompt\n */\nexport interface SearchPromptConfig {\n /** Maximum number of results to display */\n limit: number;\n /** Debounce delay in milliseconds */\n debounceMs: number;\n}\n\n/**\n * Search function type for filtering references\n */\nexport type SearchFunction = (query: string) => SearchResult[];\n\n/**\n * Result from the search prompt\n */\nexport interface SearchPromptResult {\n /** Selected references */\n selected: CslItem[];\n /** Whether the prompt was cancelled */\n cancelled: boolean;\n}\n\n/**\n * Maps internal choice value to CslItem\n */\ninterface ChoiceData {\n index: number;\n item: CslItem;\n}\n\n/**\n * Creates choices from search results\n */\nexport function createChoices(\n results: SearchResult[],\n terminalWidth: number\n): AutoCompleteChoice[] {\n return results.map((result, index) => {\n const displayIndex = index + 1;\n const formattedText = formatSearchResult(result.reference, displayIndex, terminalWidth);\n\n // Enquirer returns the 'name' property on selection when 'value' is not defined\n // So we store the JSON data in 'name' and use 'message' for display\n return {\n name: JSON.stringify({\n index,\n item: result.reference,\n } satisfies ChoiceData),\n message: formattedText,\n };\n });\n}\n\n/**\n * Parses selected values back to CslItems\n */\nexport function parseSelectedValues(values: string | string[]): CslItem[] {\n const valueArray = Array.isArray(values) ? values : [values];\n const items: CslItem[] = [];\n\n for (const value of valueArray) {\n if (!value) continue;\n try {\n const data = JSON.parse(value) as ChoiceData;\n items.push(data.item);\n } catch {\n // If parsing fails, the value might be just the id (name)\n // In this case, we can't recover the full item\n // This shouldn't happen in normal operation\n }\n }\n\n return items;\n}\n\n/**\n * Gets terminal width, falling back to 80 if not available\n */\nexport function getTerminalWidth(): number {\n return process.stdout.columns ?? 80;\n}\n\n/**\n * Collects enabled (selected) item IDs from choices\n */\nfunction collectEnabledIds(choices: AutoCompleteChoice[]): Set<string> {\n const enabledIds = new Set<string>();\n for (const choice of choices) {\n if ((choice as { enabled?: boolean }).enabled) {\n try {\n const data = JSON.parse(choice.name) as ChoiceData;\n enabledIds.add(data.item.id);\n } catch {\n // Ignore parse errors\n }\n }\n }\n return enabledIds;\n}\n\n/**\n * Restores enabled state for choices matching the given IDs\n */\nfunction restoreEnabledState(choices: AutoCompleteChoice[], enabledIds: Set<string>): void {\n for (const choice of choices) {\n try {\n const data = JSON.parse(choice.name) as ChoiceData;\n if (enabledIds.has(data.item.id)) {\n (choice as { enabled?: boolean }).enabled = true;\n }\n } catch {\n // Ignore parse errors\n }\n }\n}\n\n/**\n * Creates and runs an interactive search prompt\n */\nexport async function runSearchPrompt(\n allReferences: CslItem[],\n searchFn: SearchFunction,\n config: SearchPromptConfig,\n initialQuery = \"\"\n): Promise<SearchPromptResult> {\n // Dynamic import to allow mocking in tests\n // enquirer is a CommonJS module, so we must use default import\n const enquirer = await import(\"enquirer\");\n const AutoComplete = (enquirer.default as unknown as Record<string, unknown>)\n .AutoComplete as new (\n options: Record<string, unknown>\n ) => {\n run(): Promise<string | string[]>;\n };\n\n const terminalWidth = getTerminalWidth();\n\n // Create initial choices from all references (limited)\n const initialResults: SearchResult[] = initialQuery\n ? searchFn(initialQuery).slice(0, config.limit)\n : allReferences.slice(0, config.limit).map((ref) => ({\n reference: ref,\n overallStrength: \"exact\" as const,\n tokenMatches: [],\n score: 0,\n }));\n\n const initialChoices = createChoices(initialResults, terminalWidth);\n\n // Track last search query to avoid redundant searches\n let lastQuery = initialQuery;\n\n const promptOptions = {\n name: \"references\",\n message: \"Search references\",\n initial: initialQuery,\n choices: initialChoices,\n multiple: true,\n limit: config.limit,\n suggest: (input: string, choices: AutoCompleteChoice[]) => {\n // If input hasn't changed, return current choices\n if (input === lastQuery) {\n return choices;\n }\n lastQuery = input;\n\n // Collect enabled (selected) item IDs from current choices\n const enabledIds = collectEnabledIds(choices);\n\n // Create new choices based on input\n const newChoices = input.trim()\n ? createChoices(searchFn(input).slice(0, config.limit), terminalWidth)\n : createChoices(\n allReferences.slice(0, config.limit).map((ref) => ({\n reference: ref,\n overallStrength: \"exact\" as const,\n tokenMatches: [],\n score: 0,\n })),\n terminalWidth\n );\n\n // Restore enabled state for matching items\n restoreEnabledState(newChoices, enabledIds);\n\n return newChoices;\n },\n };\n\n try {\n const prompt = new AutoComplete(promptOptions);\n const result = await prompt.run();\n\n // Workaround for Enquirer bug: focused item with enabled=true may not be in result\n const promptAny = prompt as unknown as {\n focused?: { name: string; enabled?: boolean };\n selected?: Array<{ name: string }>;\n };\n\n let valuesToParse: string | string[] = result;\n\n // If result is empty but focused item is enabled, use focused item\n if (\n Array.isArray(result) &&\n result.length === 0 &&\n promptAny.focused?.enabled &&\n promptAny.focused?.name\n ) {\n valuesToParse = [promptAny.focused.name];\n }\n\n // Handle result\n const selected = parseSelectedValues(valuesToParse);\n\n return {\n selected,\n cancelled: false,\n };\n } catch (error) {\n // Enquirer throws an empty string when cancelled\n if (error === \"\" || (error instanceof Error && error.message === \"\")) {\n return {\n selected: [],\n cancelled: true,\n };\n }\n throw error;\n }\n}\n"],"names":[],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -13,7 +13,7 @@ import { l as loadConfig } from "./chunks/loader-BItrdVWG.js";
|
|
|
13
13
|
import { spawn } from "node:child_process";
|
|
14
14
|
import { serve } from "@hono/node-server";
|
|
15
15
|
const name = "@ncukondo/reference-manager";
|
|
16
|
-
const version$1 = "0.7.
|
|
16
|
+
const version$1 = "0.7.1";
|
|
17
17
|
const description$1 = "A local reference management tool using CSL-JSON as the single source of truth";
|
|
18
18
|
const packageJson = {
|
|
19
19
|
name,
|
|
@@ -21627,7 +21627,7 @@ function validateInteractiveOptions(options) {
|
|
|
21627
21627
|
async function executeInteractiveSearch(options, context, config2) {
|
|
21628
21628
|
validateInteractiveOptions(options);
|
|
21629
21629
|
const { checkTTY } = await import("./chunks/tty-CDBIQraQ.js");
|
|
21630
|
-
const { runSearchPrompt } = await import("./chunks/search-prompt-
|
|
21630
|
+
const { runSearchPrompt } = await import("./chunks/search-prompt-RtHDJFgL.js");
|
|
21631
21631
|
const { runActionMenu } = await import("./chunks/action-menu-CTtINmWd.js");
|
|
21632
21632
|
const { search } = await import("./chunks/file-watcher-D7oyc-9z.js").then((n) => n.y);
|
|
21633
21633
|
const { tokenize } = await import("./chunks/file-watcher-D7oyc-9z.js").then((n) => n.x);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search-prompt.d.ts","sourceRoot":"","sources":["../../../src/features/interactive/search-prompt.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,8BAA8B,CAAC;AAC5D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAGxD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,YAAY,EAAE,CAAC;AAE/D;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,0BAA0B;IAC1B,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,uCAAuC;IACvC,SAAS,EAAE,OAAO,CAAC;CACpB;AAUD;;GAEG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,YAAY,EAAE,EACvB,aAAa,EAAE,MAAM,GACpB,kBAAkB,EAAE,
|
|
1
|
+
{"version":3,"file":"search-prompt.d.ts","sourceRoot":"","sources":["../../../src/features/interactive/search-prompt.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,8BAA8B,CAAC;AAC5D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAGxD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,YAAY,EAAE,CAAC;AAE/D;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,0BAA0B;IAC1B,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,uCAAuC;IACvC,SAAS,EAAE,OAAO,CAAC;CACpB;AAUD;;GAEG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,YAAY,EAAE,EACvB,aAAa,EAAE,MAAM,GACpB,kBAAkB,EAAE,CAetB;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,EAAE,CAiBxE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAoCD;;GAEG;AACH,wBAAsB,eAAe,CACnC,aAAa,EAAE,OAAO,EAAE,EACxB,QAAQ,EAAE,cAAc,EACxB,MAAM,EAAE,kBAAkB,EAC1B,YAAY,SAAK,GAChB,OAAO,CAAC,kBAAkB,CAAC,CAwG7B"}
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"search-prompt-D67WyKrY.js","sources":["../../src/features/interactive/format.ts","../../src/features/interactive/search-prompt.ts"],"sourcesContent":["/**\n * Display format functions for interactive search\n */\n\nimport type { CslItem } from \"../../core/csl-json/types.js\";\n\n/**\n * CSL name type (author structure)\n */\ntype CslName = NonNullable<CslItem[\"author\"]>[number];\n\n/**\n * Format a single author name\n * - Personal: \"Smith, J.\" (family + initial of given)\n * - Institutional: \"World Health Organization\" (literal)\n */\nfunction formatSingleAuthor(author: CslName): string {\n if (author.literal) {\n return author.literal;\n }\n if (author.family) {\n if (author.given) {\n const initial = author.given.charAt(0).toUpperCase();\n return `${author.family}, ${initial}.`;\n }\n return author.family;\n }\n return \"\";\n}\n\n/**\n * Format author list for display\n * - Single author: \"Smith, J.\"\n * - Two authors: \"Smith, J., & Doe, A.\"\n * - Three authors: \"Smith, J., Doe, A., & Johnson, B.\"\n * - More than three: \"Smith, J., et al.\"\n *\n * @param authors - Array of CSL author objects\n * @returns Formatted author string\n */\nexport function formatAuthors(authors: CslName[] | undefined): string {\n if (!authors || authors.length === 0) {\n return \"\";\n }\n\n if (authors.length > 3) {\n const first = authors[0];\n if (!first) {\n return \"\";\n }\n return `${formatSingleAuthor(first)}, et al.`;\n }\n\n const formatted = authors.map(formatSingleAuthor);\n if (formatted.length === 1) {\n return formatted[0] ?? \"\";\n }\n\n // Join all but last with \", \" and append \"& \" + last\n const allButLast = formatted.slice(0, -1).join(\", \");\n const last = formatted[formatted.length - 1] ?? \"\";\n return `${allButLast}, & ${last}`;\n}\n\n/**\n * Truncate title to fit terminal width\n *\n * @param title - Title string\n * @param maxWidth - Maximum display width\n * @returns Truncated title with ellipsis if needed\n */\nexport function formatTitle(title: string | undefined, maxWidth: number): string {\n if (!title) {\n return \"\";\n }\n\n if (title.length <= maxWidth) {\n return title;\n }\n\n // Truncate and add ellipsis, keeping total length at maxWidth\n return `${title.slice(0, maxWidth - 3)}...`;\n}\n\n/**\n * Format identifiers (DOI, PMID, PMCID, ISBN) for display\n *\n * @param item - CSL item\n * @returns Formatted identifier string (e.g., \"DOI: 10.1000/example | PMID: 12345678\")\n */\nexport function formatIdentifiers(item: CslItem): string {\n const identifiers: string[] = [];\n\n if (item.DOI) {\n identifiers.push(`DOI: ${item.DOI}`);\n }\n if (item.PMID) {\n identifiers.push(`PMID: ${item.PMID}`);\n }\n if (item.PMCID) {\n identifiers.push(`PMCID: ${item.PMCID}`);\n }\n if (item.ISBN) {\n identifiers.push(`ISBN: ${item.ISBN}`);\n }\n\n return identifiers.join(\" | \");\n}\n\n/**\n * Extract year from CSL item\n */\nfunction extractYear(item: CslItem): number | undefined {\n const dateParts = item.issued?.[\"date-parts\"];\n if (!dateParts || dateParts.length === 0) {\n return undefined;\n }\n const firstDatePart = dateParts[0];\n if (!firstDatePart || firstDatePart.length === 0) {\n return undefined;\n }\n return firstDatePart[0];\n}\n\n/**\n * Compose a complete search result line\n *\n * Format:\n * ```\n * [1] Smith, J., & Doe, A. (2020)\n * Machine learning in medicine: A comprehensive review\n * DOI: 10.1000/example | PMID: 12345678\n * ```\n *\n * @param item - CSL item\n * @param index - Display index (1-based)\n * @param terminalWidth - Terminal width for title truncation\n * @returns Multi-line formatted string\n */\nexport function formatSearchResult(item: CslItem, index: number, terminalWidth: number): string {\n const lines: string[] = [];\n\n // Line 1: [index] Authors (year)\n const authors = formatAuthors(item.author);\n const year = extractYear(item);\n const yearPart = year !== undefined ? ` (${year})` : \"\";\n const line1 = `[${index}] ${authors}${yearPart}`;\n lines.push(line1);\n\n // Line 2: Title (indented, truncated)\n const indent = \" \";\n const titleMaxWidth = terminalWidth - indent.length;\n const title = formatTitle(item.title, titleMaxWidth);\n if (title) {\n lines.push(`${indent}${title}`);\n }\n\n // Line 3: Identifiers (indented)\n const identifiers = formatIdentifiers(item);\n if (identifiers) {\n lines.push(`${indent}${identifiers}`);\n }\n\n return lines.join(\"\\n\");\n}\n","/**\n * Interactive search prompt using Enquirer's AutoComplete\n *\n * Provides real-time incremental search with multiple selection support.\n */\n\nimport type { CslItem } from \"../../core/csl-json/types.js\";\nimport type { SearchResult } from \"../search/types.js\";\nimport type { AutoCompleteChoice } from \"./enquirer.js\";\nimport { formatSearchResult } from \"./format.js\";\n\n/**\n * Configuration for the search prompt\n */\nexport interface SearchPromptConfig {\n /** Maximum number of results to display */\n limit: number;\n /** Debounce delay in milliseconds */\n debounceMs: number;\n}\n\n/**\n * Search function type for filtering references\n */\nexport type SearchFunction = (query: string) => SearchResult[];\n\n/**\n * Result from the search prompt\n */\nexport interface SearchPromptResult {\n /** Selected references */\n selected: CslItem[];\n /** Whether the prompt was cancelled */\n cancelled: boolean;\n}\n\n/**\n * Maps internal choice value to CslItem\n */\ninterface ChoiceData {\n index: number;\n item: CslItem;\n}\n\n/**\n * Creates choices from search results\n */\nexport function createChoices(\n results: SearchResult[],\n terminalWidth: number\n): AutoCompleteChoice[] {\n return results.map((result, index) => {\n const displayIndex = index + 1;\n const formattedText = formatSearchResult(result.reference, displayIndex, terminalWidth);\n\n // Enquirer returns the 'name' property on selection, not 'value'\n // So we store the JSON data in 'name' and use 'message' for display\n return {\n name: JSON.stringify({\n index,\n item: result.reference,\n } satisfies ChoiceData),\n message: formattedText,\n value: result.reference.id,\n };\n });\n}\n\n/**\n * Parses selected values back to CslItems\n */\nexport function parseSelectedValues(values: string | string[]): CslItem[] {\n const valueArray = Array.isArray(values) ? values : [values];\n const items: CslItem[] = [];\n\n for (const value of valueArray) {\n if (!value) continue;\n try {\n const data = JSON.parse(value) as ChoiceData;\n items.push(data.item);\n } catch {\n // If parsing fails, the value might be just the id (name)\n // In this case, we can't recover the full item\n // This shouldn't happen in normal operation\n }\n }\n\n return items;\n}\n\n/**\n * Gets terminal width, falling back to 80 if not available\n */\nexport function getTerminalWidth(): number {\n return process.stdout.columns ?? 80;\n}\n\n/**\n * Creates and runs an interactive search prompt\n */\nexport async function runSearchPrompt(\n allReferences: CslItem[],\n searchFn: SearchFunction,\n config: SearchPromptConfig,\n initialQuery = \"\"\n): Promise<SearchPromptResult> {\n // Dynamic import to allow mocking in tests\n // enquirer is a CommonJS module, so we must use default import\n const enquirer = await import(\"enquirer\");\n const AutoComplete = (enquirer.default as unknown as Record<string, unknown>)\n .AutoComplete as new (\n options: Record<string, unknown>\n ) => {\n run(): Promise<string | string[]>;\n };\n\n const terminalWidth = getTerminalWidth();\n\n // Create initial choices from all references (limited)\n const initialResults: SearchResult[] = initialQuery\n ? searchFn(initialQuery).slice(0, config.limit)\n : allReferences.slice(0, config.limit).map((ref) => ({\n reference: ref,\n overallStrength: \"exact\" as const,\n tokenMatches: [],\n score: 0,\n }));\n\n const initialChoices = createChoices(initialResults, terminalWidth);\n\n // Track last search query to avoid redundant searches\n let lastQuery = initialQuery;\n\n const promptOptions = {\n name: \"references\",\n message: \"Search references\",\n initial: initialQuery,\n choices: initialChoices,\n multiple: true,\n limit: config.limit,\n suggest: (input: string, choices: AutoCompleteChoice[]) => {\n // If input hasn't changed, return current choices\n if (input === lastQuery) {\n return choices;\n }\n lastQuery = input;\n\n // If input is empty, show all references (limited)\n if (!input.trim()) {\n const defaultResults: SearchResult[] = allReferences.slice(0, config.limit).map((ref) => ({\n reference: ref,\n overallStrength: \"exact\" as const,\n tokenMatches: [],\n score: 0,\n }));\n return createChoices(defaultResults, terminalWidth);\n }\n\n // Search and create new choices\n const results = searchFn(input).slice(0, config.limit);\n return createChoices(results, terminalWidth);\n },\n };\n\n try {\n const prompt = new AutoComplete(promptOptions);\n const result = await prompt.run();\n\n // Handle result\n const selected = parseSelectedValues(result);\n\n return {\n selected,\n cancelled: false,\n };\n } catch (error) {\n // Enquirer throws an empty string when cancelled\n if (error === \"\" || (error instanceof Error && error.message === \"\")) {\n return {\n selected: [],\n cancelled: true,\n };\n }\n throw error;\n }\n}\n"],"names":[],"mappings":"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"}
|