@ncukondo/search-hub 0.23.0 → 0.24.0
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/cli/commands/fulltext/check.d.ts +4 -0
- package/dist/cli/commands/fulltext/check.d.ts.map +1 -1
- package/dist/cli/commands/fulltext/check.js +41 -4
- package/dist/cli/commands/fulltext/check.js.map +1 -1
- package/dist/cli/commands/fulltext/index.d.ts.map +1 -1
- package/dist/cli/commands/fulltext/index.js +10 -0
- package/dist/cli/commands/fulltext/index.js.map +1 -1
- package/dist/cli/commands/fulltext/verify-pmcid.d.ts +28 -0
- package/dist/cli/commands/fulltext/verify-pmcid.d.ts.map +1 -0
- package/dist/cli/commands/fulltext/verify-pmcid.js +51 -0
- package/dist/cli/commands/fulltext/verify-pmcid.js.map +1 -0
- package/dist/cli/commands/query/init.d.ts +0 -1
- package/dist/cli/commands/query/init.d.ts.map +1 -1
- package/dist/cli/commands/query/init.js +2 -3
- package/dist/cli/commands/query/init.js.map +1 -1
- package/dist/cli/commands/query/resolve.d.ts.map +1 -1
- package/dist/cli/commands/query/resolve.js +5 -2
- package/dist/cli/commands/query/resolve.js.map +1 -1
- package/dist/cli/commands/register.d.ts +4 -0
- package/dist/cli/commands/register.d.ts.map +1 -1
- package/dist/cli/commands/register.js +62 -13
- package/dist/cli/commands/register.js.map +1 -1
- package/dist/cli/commands/upgrade.d.ts +28 -0
- package/dist/cli/commands/upgrade.d.ts.map +1 -0
- package/dist/cli/commands/upgrade.js +96 -0
- package/dist/cli/commands/upgrade.js.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +12 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/suggestions/rules.js +1 -1
- package/dist/cli/suggestions/rules.js.map +1 -1
- package/dist/cli/utils/argv.d.ts +34 -0
- package/dist/cli/utils/argv.d.ts.map +1 -0
- package/dist/cli/utils/argv.js +63 -0
- package/dist/cli/utils/argv.js.map +1 -0
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/paths.d.ts +1 -1
- package/dist/config/paths.d.ts.map +1 -1
- package/dist/config/paths.js +6 -2
- package/dist/config/paths.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/integration/csl-json.d.ts +10 -0
- package/dist/integration/csl-json.d.ts.map +1 -1
- package/dist/integration/csl-json.js +12 -0
- package/dist/integration/csl-json.js.map +1 -1
- package/dist/integration/register.d.ts.map +1 -1
- package/dist/integration/register.js +1 -1
- package/dist/integration/register.js.map +1 -1
- package/dist/node_modules/@ncukondo/academic-fulltext/dist/convert/jats-parser.js +25 -7
- package/dist/node_modules/@ncukondo/academic-fulltext/dist/convert/jats-parser.js.map +1 -1
- package/dist/node_modules/@ncukondo/academic-fulltext/dist/discovery/index.js +67 -25
- package/dist/node_modules/@ncukondo/academic-fulltext/dist/discovery/index.js.map +1 -1
- package/dist/node_modules/@ncukondo/academic-fulltext/dist/discovery/pmc.js +3 -3
- package/dist/node_modules/@ncukondo/academic-fulltext/dist/discovery/pmc.js.map +1 -1
- package/dist/package.json.js +1 -1
- package/dist/upgrade/apply-binary.d.ts +37 -0
- package/dist/upgrade/apply-binary.d.ts.map +1 -0
- package/dist/upgrade/apply-binary.js +200 -0
- package/dist/upgrade/apply-binary.js.map +1 -0
- package/dist/upgrade/apply-npm.d.ts +23 -0
- package/dist/upgrade/apply-npm.d.ts.map +1 -0
- package/dist/upgrade/apply-npm.js +96 -0
- package/dist/upgrade/apply-npm.js.map +1 -0
- package/dist/upgrade/check.d.ts +21 -0
- package/dist/upgrade/check.d.ts.map +1 -0
- package/dist/upgrade/check.js +91 -0
- package/dist/upgrade/check.js.map +1 -0
- package/dist/upgrade/detect.d.ts +16 -0
- package/dist/upgrade/detect.d.ts.map +1 -0
- package/dist/upgrade/detect.js +65 -0
- package/dist/upgrade/detect.js.map +1 -0
- package/dist/upgrade/notifier.d.ts +26 -0
- package/dist/upgrade/notifier.d.ts.map +1 -0
- package/dist/upgrade/notifier.js +111 -0
- package/dist/upgrade/notifier.js.map +1 -0
- package/package.json +2 -2
|
@@ -3,6 +3,7 @@ import { access, readFile } from "node:fs/promises";
|
|
|
3
3
|
import { constants } from "node:fs";
|
|
4
4
|
import { createInterface } from "node:readline";
|
|
5
5
|
import { parse } from "yaml";
|
|
6
|
+
import { loadResults } from "../../session/results-io.js";
|
|
6
7
|
import { parseProviderNames } from "../utils/validation.js";
|
|
7
8
|
import { classifyStatus } from "./review/types.js";
|
|
8
9
|
function parseRegisterOptions(sessionId, options) {
|
|
@@ -50,6 +51,15 @@ function getRegistrationId(article) {
|
|
|
50
51
|
if (article.doi) {
|
|
51
52
|
return article.doi;
|
|
52
53
|
}
|
|
54
|
+
if (article.arxivId) {
|
|
55
|
+
return `arxiv:${article.arxivId}`;
|
|
56
|
+
}
|
|
57
|
+
if (article.ericId) {
|
|
58
|
+
return `eric:${article.ericId}`;
|
|
59
|
+
}
|
|
60
|
+
if (article.scopusId) {
|
|
61
|
+
return `scopus:${article.scopusId}`;
|
|
62
|
+
}
|
|
53
63
|
return null;
|
|
54
64
|
}
|
|
55
65
|
function formatDryRunOutput(articles) {
|
|
@@ -74,15 +84,13 @@ function formatDryRunOutput(articles) {
|
|
|
74
84
|
if (withoutId.length > 0) {
|
|
75
85
|
lines.push("");
|
|
76
86
|
lines.push(
|
|
77
|
-
`${withoutId.length} article${withoutId.length !== 1 ? "s" : ""} will be skipped (no
|
|
87
|
+
`${withoutId.length} article${withoutId.length !== 1 ? "s" : ""} will be skipped (no identifier):`
|
|
78
88
|
);
|
|
79
89
|
const maxDisplay = 10;
|
|
80
90
|
const displayed = withoutId.slice(0, maxDisplay);
|
|
81
91
|
for (const article of displayed) {
|
|
82
92
|
const truncatedTitle = article.title.length > 50 ? article.title.substring(0, 50) + "..." : article.title;
|
|
83
|
-
|
|
84
|
-
const hasAltIds = altIds.length > 0 ? `, has: ${altIds.join(", ")}` : "";
|
|
85
|
-
lines.push(` - "${truncatedTitle}" (source: ${article.source}${hasAltIds})`);
|
|
93
|
+
lines.push(` - "${truncatedTitle}" (source: ${article.source})`);
|
|
86
94
|
}
|
|
87
95
|
if (withoutId.length > maxDisplay) {
|
|
88
96
|
lines.push(` ... and ${withoutId.length - maxDisplay} more`);
|
|
@@ -90,13 +98,6 @@ function formatDryRunOutput(articles) {
|
|
|
90
98
|
}
|
|
91
99
|
return lines.join("\n");
|
|
92
100
|
}
|
|
93
|
-
function getAlternativeIds(article) {
|
|
94
|
-
const ids = [];
|
|
95
|
-
if (article.arxivId) ids.push(`arxiv:${article.arxivId}`);
|
|
96
|
-
if (article.ericId) ids.push(`eric:${article.ericId}`);
|
|
97
|
-
if (article.scopusId) ids.push(`scopus:${article.scopusId}`);
|
|
98
|
-
return ids;
|
|
99
|
-
}
|
|
100
101
|
async function hasReviewFile(sessionId, sessionsDir) {
|
|
101
102
|
const reviewsPath = join(sessionsDir, sessionId, ".internal", "reviews.yaml");
|
|
102
103
|
try {
|
|
@@ -139,14 +140,62 @@ function parseAuthorName(name) {
|
|
|
139
140
|
if (parts.length === 1) {
|
|
140
141
|
return { family: parts[0] ?? "" };
|
|
141
142
|
}
|
|
143
|
+
const last = parts[parts.length - 1];
|
|
144
|
+
if (/^[A-Z]{1,3}$/.test(last)) {
|
|
145
|
+
parts.pop();
|
|
146
|
+
return { family: parts.join(" "), given: last };
|
|
147
|
+
}
|
|
142
148
|
const family = parts.pop() ?? "";
|
|
143
149
|
const given = parts.join(" ");
|
|
144
150
|
return { family, given };
|
|
145
151
|
}
|
|
152
|
+
function authorLookupKeys(ref) {
|
|
153
|
+
const keys = [];
|
|
154
|
+
if (ref.pmid) keys.push(`pmid:${ref.pmid}`);
|
|
155
|
+
if (ref.doi) keys.push(`doi:${ref.doi.toLowerCase()}`);
|
|
156
|
+
if (ref.arxivId) keys.push(`arxiv:${ref.arxivId}`);
|
|
157
|
+
if (ref.scopusId) keys.push(`scopus:${ref.scopusId}`);
|
|
158
|
+
if (ref.ericId) keys.push(`eric:${ref.ericId}`);
|
|
159
|
+
return keys;
|
|
160
|
+
}
|
|
161
|
+
async function buildAuthorLookup(sessionDir, providers) {
|
|
162
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
163
|
+
for (const provider of providers) {
|
|
164
|
+
const articles = await loadResults(sessionDir, provider);
|
|
165
|
+
for (const article of articles) {
|
|
166
|
+
if (!article.authors || article.authors.length === 0) continue;
|
|
167
|
+
for (const key of authorLookupKeys(article)) {
|
|
168
|
+
if (!lookup.has(key)) {
|
|
169
|
+
lookup.set(key, article.authors);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return lookup;
|
|
175
|
+
}
|
|
176
|
+
function resolveAuthors(entry, lookup) {
|
|
177
|
+
const keys = [
|
|
178
|
+
...authorLookupKeys(entry),
|
|
179
|
+
...(entry.mergedFrom ?? []).flatMap((source) => authorLookupKeys(source))
|
|
180
|
+
];
|
|
181
|
+
for (const key of keys) {
|
|
182
|
+
const authors = lookup.get(key);
|
|
183
|
+
if (authors) return authors;
|
|
184
|
+
}
|
|
185
|
+
return entry.authors ? entry.authors.split(/,\s*/).map(parseAuthorName) : [];
|
|
186
|
+
}
|
|
146
187
|
async function getIncludedArticles(sessionId, sessionsDir) {
|
|
147
188
|
const reviewFile = await loadReviewFile(sessionId, sessionsDir);
|
|
148
189
|
const articles = reviewFile.articles ?? [];
|
|
149
|
-
|
|
190
|
+
const included = articles.filter((entry) => entry.finalDecision === "include");
|
|
191
|
+
const providers = /* @__PURE__ */ new Set();
|
|
192
|
+
for (const entry of included) {
|
|
193
|
+
for (const source of entry.mergedFrom ?? []) {
|
|
194
|
+
providers.add(source.source);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const authorLookup = await buildAuthorLookup(join(sessionsDir, sessionId), providers);
|
|
198
|
+
return included.map((entry) => {
|
|
150
199
|
if (!entry.mergedFrom) {
|
|
151
200
|
throw new Error(
|
|
152
201
|
`Article "${entry.title}" has mergedFrom missing. This may be a legacy review file created before source tracking was fixed. Please re-run 'review init' to regenerate the review file with source tracking.`
|
|
@@ -157,7 +206,7 @@ async function getIncludedArticles(sessionId, sessionsDir) {
|
|
|
157
206
|
`Article "${entry.title}" has empty mergedFrom array. This is an invalid state - please re-run 'review init' to regenerate.`
|
|
158
207
|
);
|
|
159
208
|
}
|
|
160
|
-
const authors = entry
|
|
209
|
+
const authors = resolveAuthors(entry, authorLookup);
|
|
161
210
|
const source = entry.mergedFrom[0].source;
|
|
162
211
|
const article = {
|
|
163
212
|
title: entry.title,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register.js","sources":["../../../src/cli/commands/register.ts"],"sourcesContent":["/**\n * Register command for reference-manager integration.\n * Registers search results with reference-manager CLI.\n */\n\nimport { join } from 'node:path';\nimport { readFile, access } from 'node:fs/promises';\nimport { constants } from 'node:fs';\nimport { createInterface } from 'node:readline';\nimport { parse as parseYaml } from 'yaml';\nimport type { ProviderName, Article, Author } from '../../providers/base/types.js';\nimport { parseProviderNames } from '../utils/validation.js';\nimport { classifyStatus, type ReviewFile } from './review/types.js';\n\nexport interface RegisterCommandOptions {\n sessionId: string;\n providers?: ProviderName[];\n dryRun: boolean;\n withAbstracts: boolean;\n /** Register only reviewed articles with finalDecision='include' */\n reviewed?: boolean;\n /** Register all articles, ignoring reviews */\n all?: boolean;\n /** Skip confirmation prompts */\n force?: boolean;\n /** Suppress tips and suggestions */\n quiet?: boolean;\n}\n\nexport interface CommandLineOptions {\n db?: string | undefined;\n dryRun?: boolean | undefined;\n withAbstracts?: boolean | undefined;\n reviewed?: boolean | undefined;\n all?: boolean | undefined;\n force?: boolean | undefined;\n quiet?: boolean | undefined;\n}\n\nexport interface ValidationResult {\n valid: boolean;\n error?: string;\n}\n\n/**\n * Parse command line options into RegisterCommandOptions.\n */\nexport function parseRegisterOptions(\n sessionId: string,\n options: CommandLineOptions\n): RegisterCommandOptions {\n const result: RegisterCommandOptions = {\n sessionId,\n dryRun: options.dryRun ?? false,\n withAbstracts: options.withAbstracts ?? false,\n reviewed: options.reviewed ?? false,\n all: options.all ?? false,\n force: options.force ?? false,\n quiet: options.quiet ?? false,\n };\n\n if (options.db) {\n result.providers = parseProviderNames(options.db);\n }\n\n return result;\n}\n\n/**\n * Validate register command input.\n */\nexport function validateRegisterInput(options: RegisterCommandOptions): ValidationResult {\n if (!options.sessionId || options.sessionId.trim() === '') {\n return {\n valid: false,\n error: 'A session ID is required',\n };\n }\n\n return { valid: true };\n}\n\n/**\n * Format registration summary for CLI output.\n */\nexport function formatRegistrationSummary(summary: {\n total: number;\n added: number;\n skipped: number;\n failed: number;\n noId: number;\n}): string {\n const lines: string[] = ['Registration complete:'];\n\n // Added\n lines.push(` ✓ ${summary.added} added`);\n\n // Duplicates (skipped)\n if (summary.skipped > 0) {\n lines.push(` ⚠ ${summary.skipped} duplicates (already in library)`);\n }\n\n // Failed\n if (summary.failed > 0) {\n lines.push(` ✗ ${summary.failed} failed`);\n }\n\n // No ID (skipped)\n if (summary.noId > 0) {\n lines.push(` - ${summary.noId} skipped (no identifier)`);\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Get registration identifier for an article.\n * PMID is preferred over DOI for better metadata quality.\n */\nfunction getRegistrationId(article: Article): string | null {\n if (article.pmid) {\n return `pmid:${article.pmid}`;\n }\n if (article.doi) {\n return article.doi;\n }\n return null;\n}\n\n/**\n * Format dry run output showing what would be registered.\n */\nexport function formatDryRunOutput(articles: Article[]): string {\n const withId: Array<{ article: Article; id: string }> = [];\n const withoutId: Article[] = [];\n\n for (const article of articles) {\n const id = getRegistrationId(article);\n if (id) {\n withId.push({ article, id });\n } else {\n withoutId.push(article);\n }\n }\n\n const lines: string[] = [];\n\n // Summary\n lines.push(\n `Would register ${withId.length} reference${withId.length !== 1 ? 's' : ''}:`\n );\n\n // List articles with IDs\n for (const { id, article } of withId) {\n const title = article.title.length > 60\n ? article.title.substring(0, 57) + '...'\n : article.title;\n lines.push(` - ${id}: ${title}`);\n }\n\n // Details about articles without DOI/PMID\n if (withoutId.length > 0) {\n lines.push('');\n lines.push(\n `${withoutId.length} article${withoutId.length !== 1 ? 's' : ''} will be skipped (no DOI or PMID):`\n );\n\n const maxDisplay = 10;\n const displayed = withoutId.slice(0, maxDisplay);\n\n for (const article of displayed) {\n const truncatedTitle = article.title.length > 50\n ? article.title.substring(0, 50) + '...'\n : article.title;\n\n const altIds = getAlternativeIds(article);\n const hasAltIds = altIds.length > 0 ? `, has: ${altIds.join(', ')}` : '';\n\n lines.push(` - \"${truncatedTitle}\" (source: ${article.source}${hasAltIds})`);\n }\n\n if (withoutId.length > maxDisplay) {\n lines.push(` ... and ${withoutId.length - maxDisplay} more`);\n }\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Get alternative (non-DOI/PMID) identifiers for an article.\n */\nfunction getAlternativeIds(article: Article): string[] {\n const ids: string[] = [];\n if (article.arxivId) ids.push(`arxiv:${article.arxivId}`);\n if (article.ericId) ids.push(`eric:${article.ericId}`);\n if (article.scopusId) ids.push(`scopus:${article.scopusId}`);\n return ids;\n}\n\n/**\n * Summary of review decisions for a session.\n */\nexport interface ReviewSummary {\n /** Total articles in review file */\n total: number;\n /** Articles with finalDecision='include' */\n included: number;\n /** Articles with finalDecision='exclude' */\n excluded: number;\n /** Articles without finalDecision (pending, incomplete, all-uncertain, agreed, divided) */\n pending: number;\n}\n\n/**\n * Check if a session has a reviews.yaml file.\n */\nexport async function hasReviewFile(sessionId: string, sessionsDir: string): Promise<boolean> {\n const reviewsPath = join(sessionsDir, sessionId, '.internal', 'reviews.yaml');\n try {\n await access(reviewsPath, constants.R_OK);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Load and parse the review file for a session.\n */\nasync function loadReviewFile(sessionId: string, sessionsDir: string): Promise<ReviewFile> {\n const reviewsPath = join(sessionsDir, sessionId, '.internal', 'reviews.yaml');\n const content = await readFile(reviewsPath, 'utf-8');\n return parseYaml(content) as ReviewFile;\n}\n\n/**\n * Get review summary (counts) for a session.\n * Throws if reviews.yaml does not exist.\n */\nexport async function getReviewSummary(sessionId: string, sessionsDir: string): Promise<ReviewSummary> {\n const reviewFile = await loadReviewFile(sessionId, sessionsDir);\n const articles = reviewFile.articles ?? [];\n\n const summary: ReviewSummary = {\n total: articles.length,\n included: 0,\n excluded: 0,\n pending: 0,\n };\n\n for (const article of articles) {\n const status = classifyStatus(article);\n\n if (status === 'finalized') {\n if (article.finalDecision === 'include') {\n summary.included++;\n } else {\n summary.excluded++;\n }\n } else {\n // pending, incomplete, all-uncertain, agreed, divided all count as pending for registration\n summary.pending++;\n }\n }\n\n return summary;\n}\n\n/**\n * Parse author name string into Author object.\n * Simple heuristic: last word is family name, rest is given name.\n */\nfunction parseAuthorName(name: string): Author {\n const parts = name.trim().split(/\\s+/);\n if (parts.length === 1) {\n return { family: parts[0] ?? '' };\n }\n // Last part is family name (most common pattern in scientific citations)\n const family = parts.pop() ?? '';\n const given = parts.join(' ');\n return { family, given };\n}\n\n/**\n * Get articles with finalDecision='include' from review file.\n * Converts from ArticleEntry format to Article format.\n *\n * @throws Error if mergedFrom is missing or empty (indicates legacy review file)\n */\nexport async function getIncludedArticles(sessionId: string, sessionsDir: string): Promise<Article[]> {\n const reviewFile = await loadReviewFile(sessionId, sessionsDir);\n const articles = reviewFile.articles ?? [];\n\n return articles\n .filter((entry) => entry.finalDecision === 'include')\n .map((entry): Article => {\n // Validate mergedFrom exists\n if (!entry.mergedFrom) {\n throw new Error(\n `Article \"${entry.title}\" has mergedFrom missing. ` +\n `This may be a legacy review file created before source tracking was fixed. ` +\n `Please re-run 'review init' to regenerate the review file with source tracking.`\n );\n }\n if (entry.mergedFrom.length === 0) {\n throw new Error(\n `Article \"${entry.title}\" has empty mergedFrom array. ` +\n `This is an invalid state - please re-run 'review init' to regenerate.`\n );\n }\n\n const authors: Author[] = entry.authors\n ? entry.authors.split(/,\\s*/).map(parseAuthorName)\n : [];\n\n // Get source from the first entry in mergedFrom\n const source = entry.mergedFrom[0]!.source as ProviderName;\n\n const article: Article = {\n title: entry.title,\n authors,\n source,\n retrievedAt: new Date().toISOString(),\n };\n // Only set optional fields if they have values\n if (entry.doi) article.doi = entry.doi;\n if (entry.pmid) article.pmid = entry.pmid;\n if (entry.scopusId) article.scopusId = entry.scopusId;\n if (entry.arxivId) article.arxivId = entry.arxivId;\n if (entry.ericId) article.ericId = entry.ericId;\n if (entry.abstract) article.abstract = entry.abstract;\n if (entry.year) article.publicationDate = entry.year;\n return article;\n });\n}\n\n/**\n * Format message when reviews exist but no flag specified.\n */\nexport function formatReviewRequiredMessage(summary: ReviewSummary, sessionId: string): string {\n return `This session has a review file.\n Status: ${summary.included} include / ${summary.excluded} exclude / ${summary.pending} pending\n\nPlease specify which articles to register:\n --reviewed Register ${summary.included} included articles\n --all Register all ${summary.total} articles (ignore reviews)\n\nExample:\n search-hub register ${sessionId} --reviewed`;\n}\n\n/**\n * Format error when --reviewed used but no articles are included.\n */\nexport function formatNoIncludedArticlesError(summary: ReviewSummary, sessionId: string): string {\n return `Error: No articles marked as 'include' in reviews.\n Status: ${summary.included} include / ${summary.excluded} exclude / ${summary.pending} pending\n\nRun 'search-hub review status ${sessionId}' for details.`;\n}\n\n/**\n * Format warning when pending articles exist with --reviewed.\n */\nexport function formatPendingWarning(summary: ReviewSummary): string {\n const articleWord = summary.pending === 1 ? 'article' : 'articles';\n return `Warning: ${summary.pending} ${articleWord} still pending review (will be skipped).\nRegistering ${summary.included} included articles...\n\nProceed? [Y/n]`;\n}\n\n\n/**\n * Format library path display for CLI output.\n */\nexport function formatLibraryPath(sessionDir: string): string {\n return `Library: ${join(sessionDir, 'references.json')}`;\n}\n\n/**\n * Format hint for importing into default ref library.\n */\nexport function formatDefaultLibraryHint(sessionDir: string): string {\n return `To also add to your default ref library:\\n ref add -i json \"${join(sessionDir, 'references.json')}\"`;\n}\n\n/**\n * Format note when --all is used with reviews.yaml present.\n */\nexport function formatIgnoringReviewsNote(total: number): string {\n return `Note: Ignoring review decisions. Registering all ${total} articles.`;\n}\n\n/**\n * Prompt user for Y/n confirmation.\n * Returns true if user confirms (Y/y/Enter), false otherwise.\n */\nexport async function confirmPrompt(\n input: NodeJS.ReadableStream = process.stdin,\n output: NodeJS.WritableStream = process.stdout\n): Promise<boolean> {\n const rl = createInterface({\n input,\n output,\n terminal: false,\n });\n\n return new Promise((resolve) => {\n rl.question('', (answer) => {\n rl.close();\n const trimmed = answer.trim().toLowerCase();\n // Empty (Enter) or 'y' or 'yes' means confirm\n resolve(trimmed === '' || trimmed === 'y' || trimmed === 'yes');\n });\n });\n}\n"],"names":["parseYaml"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"register.js","sources":["../../../src/cli/commands/register.ts"],"sourcesContent":["/**\n * Register command for reference-manager integration.\n * Registers search results with reference-manager CLI.\n */\n\nimport { join } from 'node:path';\nimport { readFile, access } from 'node:fs/promises';\nimport { constants } from 'node:fs';\nimport { createInterface } from 'node:readline';\nimport { parse as parseYaml } from 'yaml';\nimport type { ProviderName, Article, Author } from '../../providers/base/types.js';\nimport { loadResults } from '../../session/results-io.js';\nimport { parseProviderNames } from '../utils/validation.js';\nimport { classifyStatus, type ArticleEntry, type ReviewFile } from './review/types.js';\n\nexport interface RegisterCommandOptions {\n sessionId: string;\n providers?: ProviderName[];\n dryRun: boolean;\n withAbstracts: boolean;\n /** Register only reviewed articles with finalDecision='include' */\n reviewed?: boolean;\n /** Register all articles, ignoring reviews */\n all?: boolean;\n /** Skip confirmation prompts */\n force?: boolean;\n /** Suppress tips and suggestions */\n quiet?: boolean;\n}\n\nexport interface CommandLineOptions {\n db?: string | undefined;\n dryRun?: boolean | undefined;\n withAbstracts?: boolean | undefined;\n reviewed?: boolean | undefined;\n all?: boolean | undefined;\n force?: boolean | undefined;\n quiet?: boolean | undefined;\n}\n\nexport interface ValidationResult {\n valid: boolean;\n error?: string;\n}\n\n/**\n * Parse command line options into RegisterCommandOptions.\n */\nexport function parseRegisterOptions(\n sessionId: string,\n options: CommandLineOptions\n): RegisterCommandOptions {\n const result: RegisterCommandOptions = {\n sessionId,\n dryRun: options.dryRun ?? false,\n withAbstracts: options.withAbstracts ?? false,\n reviewed: options.reviewed ?? false,\n all: options.all ?? false,\n force: options.force ?? false,\n quiet: options.quiet ?? false,\n };\n\n if (options.db) {\n result.providers = parseProviderNames(options.db);\n }\n\n return result;\n}\n\n/**\n * Validate register command input.\n */\nexport function validateRegisterInput(options: RegisterCommandOptions): ValidationResult {\n if (!options.sessionId || options.sessionId.trim() === '') {\n return {\n valid: false,\n error: 'A session ID is required',\n };\n }\n\n return { valid: true };\n}\n\n/**\n * Format registration summary for CLI output.\n */\nexport function formatRegistrationSummary(summary: {\n total: number;\n added: number;\n skipped: number;\n failed: number;\n noId: number;\n}): string {\n const lines: string[] = ['Registration complete:'];\n\n // Added\n lines.push(` ✓ ${summary.added} added`);\n\n // Duplicates (skipped)\n if (summary.skipped > 0) {\n lines.push(` ⚠ ${summary.skipped} duplicates (already in library)`);\n }\n\n // Failed\n if (summary.failed > 0) {\n lines.push(` ✗ ${summary.failed} failed`);\n }\n\n // No ID (skipped)\n if (summary.noId > 0) {\n lines.push(` - ${summary.noId} skipped (no identifier)`);\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Get registration identifier for an article.\n * PMID is preferred over DOI for better metadata quality; alternative\n * identifiers (arXiv/ERIC/Scopus) are used when neither is present.\n */\nfunction getRegistrationId(article: Article): string | null {\n if (article.pmid) {\n return `pmid:${article.pmid}`;\n }\n if (article.doi) {\n return article.doi;\n }\n if (article.arxivId) {\n return `arxiv:${article.arxivId}`;\n }\n if (article.ericId) {\n return `eric:${article.ericId}`;\n }\n if (article.scopusId) {\n return `scopus:${article.scopusId}`;\n }\n return null;\n}\n\n/**\n * Format dry run output showing what would be registered.\n */\nexport function formatDryRunOutput(articles: Article[]): string {\n const withId: Array<{ article: Article; id: string }> = [];\n const withoutId: Article[] = [];\n\n for (const article of articles) {\n const id = getRegistrationId(article);\n if (id) {\n withId.push({ article, id });\n } else {\n withoutId.push(article);\n }\n }\n\n const lines: string[] = [];\n\n // Summary\n lines.push(\n `Would register ${withId.length} reference${withId.length !== 1 ? 's' : ''}:`\n );\n\n // List articles with IDs\n for (const { id, article } of withId) {\n const title = article.title.length > 60\n ? article.title.substring(0, 57) + '...'\n : article.title;\n lines.push(` - ${id}: ${title}`);\n }\n\n // Details about articles without any identifier\n if (withoutId.length > 0) {\n lines.push('');\n lines.push(\n `${withoutId.length} article${withoutId.length !== 1 ? 's' : ''} will be skipped (no identifier):`\n );\n\n const maxDisplay = 10;\n const displayed = withoutId.slice(0, maxDisplay);\n\n for (const article of displayed) {\n const truncatedTitle = article.title.length > 50\n ? article.title.substring(0, 50) + '...'\n : article.title;\n\n lines.push(` - \"${truncatedTitle}\" (source: ${article.source})`);\n }\n\n if (withoutId.length > maxDisplay) {\n lines.push(` ... and ${withoutId.length - maxDisplay} more`);\n }\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Summary of review decisions for a session.\n */\nexport interface ReviewSummary {\n /** Total articles in review file */\n total: number;\n /** Articles with finalDecision='include' */\n included: number;\n /** Articles with finalDecision='exclude' */\n excluded: number;\n /** Articles without finalDecision (pending, incomplete, all-uncertain, agreed, divided) */\n pending: number;\n}\n\n/**\n * Check if a session has a reviews.yaml file.\n */\nexport async function hasReviewFile(sessionId: string, sessionsDir: string): Promise<boolean> {\n const reviewsPath = join(sessionsDir, sessionId, '.internal', 'reviews.yaml');\n try {\n await access(reviewsPath, constants.R_OK);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Load and parse the review file for a session.\n */\nasync function loadReviewFile(sessionId: string, sessionsDir: string): Promise<ReviewFile> {\n const reviewsPath = join(sessionsDir, sessionId, '.internal', 'reviews.yaml');\n const content = await readFile(reviewsPath, 'utf-8');\n return parseYaml(content) as ReviewFile;\n}\n\n/**\n * Get review summary (counts) for a session.\n * Throws if reviews.yaml does not exist.\n */\nexport async function getReviewSummary(sessionId: string, sessionsDir: string): Promise<ReviewSummary> {\n const reviewFile = await loadReviewFile(sessionId, sessionsDir);\n const articles = reviewFile.articles ?? [];\n\n const summary: ReviewSummary = {\n total: articles.length,\n included: 0,\n excluded: 0,\n pending: 0,\n };\n\n for (const article of articles) {\n const status = classifyStatus(article);\n\n if (status === 'finalized') {\n if (article.finalDecision === 'include') {\n summary.included++;\n } else {\n summary.excluded++;\n }\n } else {\n // pending, incomplete, all-uncertain, agreed, divided all count as pending for registration\n summary.pending++;\n }\n }\n\n return summary;\n}\n\n/**\n * Parse author name string into Author object.\n *\n * Fallback for articles that cannot be matched to the session's structured\n * search results (e.g. manually added entries). reviews.yaml stores authors\n * as \"Family GivenInitial\" (see formatAuthors in review/init.ts), so a\n * trailing run of 1-3 uppercase letters is treated as the given-name initials\n * and everything before it as the family name. Otherwise falls back to\n * \"Given Family\" (e.g. manually edited entries).\n */\nfunction parseAuthorName(name: string): Author {\n const parts = name.trim().split(/\\s+/);\n if (parts.length === 1) {\n return { family: parts[0] ?? '' };\n }\n\n const last = parts[parts.length - 1]!;\n if (/^[A-Z]{1,3}$/.test(last)) {\n parts.pop();\n return { family: parts.join(' '), given: last };\n }\n\n const family = parts.pop() ?? '';\n const given = parts.join(' ');\n return { family, given };\n}\n\n/**\n * Build identifier lookup keys for matching a review entry (or one of its\n * mergedFrom sources) against articles in the session's result files.\n * Covers the same identifiers as getArticleKeys in session-utils.ts.\n */\nfunction authorLookupKeys(ref: {\n pmid?: string | undefined;\n doi?: string | undefined;\n arxivId?: string | undefined;\n scopusId?: string | undefined;\n ericId?: string | undefined;\n}): string[] {\n const keys: string[] = [];\n if (ref.pmid) keys.push(`pmid:${ref.pmid}`);\n if (ref.doi) keys.push(`doi:${ref.doi.toLowerCase()}`);\n if (ref.arxivId) keys.push(`arxiv:${ref.arxivId}`);\n if (ref.scopusId) keys.push(`scopus:${ref.scopusId}`);\n if (ref.ericId) keys.push(`eric:${ref.ericId}`);\n return keys;\n}\n\n/**\n * Load structured author data from the session's search result files\n * (results.jsonl / YAML mirror), indexed by article identifiers.\n *\n * Only articles with non-empty author lists are indexed; the first result\n * seen for a given identifier wins.\n */\nasync function buildAuthorLookup(\n sessionDir: string,\n providers: Iterable<ProviderName>\n): Promise<Map<string, Author[]>> {\n const lookup = new Map<string, Author[]>();\n\n for (const provider of providers) {\n const articles = await loadResults(sessionDir, provider);\n for (const article of articles) {\n if (!article.authors || article.authors.length === 0) continue;\n for (const key of authorLookupKeys(article)) {\n if (!lookup.has(key)) {\n lookup.set(key, article.authors);\n }\n }\n }\n }\n\n return lookup;\n}\n\n/**\n * Resolve authors for a review entry.\n *\n * Prefers the structured author data from the session's search results,\n * matched by identifier (both the entry's own identifiers and those\n * recorded in mergedFrom). Falls back to parsing the display string in\n * reviews.yaml for articles not found in the results (e.g. manually added\n * entries).\n */\nfunction resolveAuthors(entry: ArticleEntry, lookup: Map<string, Author[]>): Author[] {\n const keys = [\n ...authorLookupKeys(entry),\n ...(entry.mergedFrom ?? []).flatMap((source) => authorLookupKeys(source)),\n ];\n\n for (const key of keys) {\n const authors = lookup.get(key);\n if (authors) return authors;\n }\n\n return entry.authors ? entry.authors.split(/,\\s*/).map(parseAuthorName) : [];\n}\n\n/**\n * Get articles with finalDecision='include' from review file.\n * Converts from ArticleEntry format to Article format.\n *\n * Authors are taken from the session's structured search results when the\n * article can be matched by identifier; the authors string in reviews.yaml\n * is only used as a fallback.\n *\n * @throws Error if mergedFrom is missing or empty (indicates legacy review file)\n */\nexport async function getIncludedArticles(sessionId: string, sessionsDir: string): Promise<Article[]> {\n const reviewFile = await loadReviewFile(sessionId, sessionsDir);\n const articles = reviewFile.articles ?? [];\n\n const included = articles.filter((entry) => entry.finalDecision === 'include');\n\n // Collect providers referenced by the included articles and index their\n // structured author data from the session's result files.\n const providers = new Set<ProviderName>();\n for (const entry of included) {\n for (const source of entry.mergedFrom ?? []) {\n providers.add(source.source as ProviderName);\n }\n }\n const authorLookup = await buildAuthorLookup(join(sessionsDir, sessionId), providers);\n\n return included\n .map((entry): Article => {\n // Validate mergedFrom exists\n if (!entry.mergedFrom) {\n throw new Error(\n `Article \"${entry.title}\" has mergedFrom missing. ` +\n `This may be a legacy review file created before source tracking was fixed. ` +\n `Please re-run 'review init' to regenerate the review file with source tracking.`\n );\n }\n if (entry.mergedFrom.length === 0) {\n throw new Error(\n `Article \"${entry.title}\" has empty mergedFrom array. ` +\n `This is an invalid state - please re-run 'review init' to regenerate.`\n );\n }\n\n const authors = resolveAuthors(entry, authorLookup);\n\n // Get source from the first entry in mergedFrom\n const source = entry.mergedFrom[0]!.source as ProviderName;\n\n const article: Article = {\n title: entry.title,\n authors,\n source,\n retrievedAt: new Date().toISOString(),\n };\n // Only set optional fields if they have values\n if (entry.doi) article.doi = entry.doi;\n if (entry.pmid) article.pmid = entry.pmid;\n if (entry.scopusId) article.scopusId = entry.scopusId;\n if (entry.arxivId) article.arxivId = entry.arxivId;\n if (entry.ericId) article.ericId = entry.ericId;\n if (entry.abstract) article.abstract = entry.abstract;\n if (entry.year) article.publicationDate = entry.year;\n return article;\n });\n}\n\n/**\n * Format message when reviews exist but no flag specified.\n */\nexport function formatReviewRequiredMessage(summary: ReviewSummary, sessionId: string): string {\n return `This session has a review file.\n Status: ${summary.included} include / ${summary.excluded} exclude / ${summary.pending} pending\n\nPlease specify which articles to register:\n --reviewed Register ${summary.included} included articles\n --all Register all ${summary.total} articles (ignore reviews)\n\nExample:\n search-hub register ${sessionId} --reviewed`;\n}\n\n/**\n * Format error when --reviewed used but no articles are included.\n */\nexport function formatNoIncludedArticlesError(summary: ReviewSummary, sessionId: string): string {\n return `Error: No articles marked as 'include' in reviews.\n Status: ${summary.included} include / ${summary.excluded} exclude / ${summary.pending} pending\n\nRun 'search-hub review status ${sessionId}' for details.`;\n}\n\n/**\n * Format warning when pending articles exist with --reviewed.\n */\nexport function formatPendingWarning(summary: ReviewSummary): string {\n const articleWord = summary.pending === 1 ? 'article' : 'articles';\n return `Warning: ${summary.pending} ${articleWord} still pending review (will be skipped).\nRegistering ${summary.included} included articles...\n\nProceed? [Y/n]`;\n}\n\n\n/**\n * Format library path display for CLI output.\n */\nexport function formatLibraryPath(sessionDir: string): string {\n return `Library: ${join(sessionDir, 'references.json')}`;\n}\n\n/**\n * Format hint for importing into default ref library.\n */\nexport function formatDefaultLibraryHint(sessionDir: string): string {\n return `To also add to your default ref library:\\n ref add -i json \"${join(sessionDir, 'references.json')}\"`;\n}\n\n/**\n * Format note when --all is used with reviews.yaml present.\n */\nexport function formatIgnoringReviewsNote(total: number): string {\n return `Note: Ignoring review decisions. Registering all ${total} articles.`;\n}\n\n/**\n * Prompt user for Y/n confirmation.\n * Returns true if user confirms (Y/y/Enter), false otherwise.\n */\nexport async function confirmPrompt(\n input: NodeJS.ReadableStream = process.stdin,\n output: NodeJS.WritableStream = process.stdout\n): Promise<boolean> {\n const rl = createInterface({\n input,\n output,\n terminal: false,\n });\n\n return new Promise((resolve) => {\n rl.question('', (answer) => {\n rl.close();\n const trimmed = answer.trim().toLowerCase();\n // Empty (Enter) or 'y' or 'yes' means confirm\n resolve(trimmed === '' || trimmed === 'y' || trimmed === 'yes');\n });\n });\n}\n"],"names":["parseYaml"],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { UpgradeBinaryOptions, UpgradeResult } from '../../upgrade/apply-binary.js';
|
|
3
|
+
import { UpgradeNpmOptions } from '../../upgrade/apply-npm.js';
|
|
4
|
+
import { InstallMethod } from '../../upgrade/detect.js';
|
|
5
|
+
export interface UpgradeCommandOptions {
|
|
6
|
+
check?: boolean;
|
|
7
|
+
version?: string;
|
|
8
|
+
yes?: boolean;
|
|
9
|
+
installDir?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface RunUpgradeDeps {
|
|
12
|
+
installMethod?: InstallMethod;
|
|
13
|
+
argv1?: string;
|
|
14
|
+
currentVersion?: string;
|
|
15
|
+
upgradeBinaryFn?: (options: UpgradeBinaryOptions) => Promise<UpgradeResult>;
|
|
16
|
+
upgradeNpmFn?: (options: UpgradeNpmOptions) => Promise<UpgradeResult>;
|
|
17
|
+
stdout?: NodeJS.WritableStream;
|
|
18
|
+
stderr?: NodeJS.WritableStream;
|
|
19
|
+
}
|
|
20
|
+
export interface RunUpgradeResult {
|
|
21
|
+
exitCode: 0 | 1 | 2;
|
|
22
|
+
method: InstallMethod;
|
|
23
|
+
result?: UpgradeResult;
|
|
24
|
+
}
|
|
25
|
+
export declare function formatUpgradeResult(result: UpgradeResult): string;
|
|
26
|
+
export declare function runUpgrade(options: UpgradeCommandOptions, deps?: RunUpgradeDeps): Promise<RunUpgradeResult>;
|
|
27
|
+
export declare function registerUpgradeCommand(program: Command): void;
|
|
28
|
+
//# sourceMappingURL=upgrade.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upgrade.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/upgrade.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAEnB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,KAAK,iBAAiB,EAAoB,MAAM,4BAA4B,CAAC;AACtF,OAAO,EACL,KAAK,aAAa,EAGnB,MAAM,yBAAyB,CAAC;AAGjC,MAAM,WAAW,qBAAqB;IACpC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;IAC5E,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACpB,MAAM,EAAE,aAAa,CAAC;IACtB,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAgCD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAejE;AAyBD,wBAAsB,UAAU,CAC9B,OAAO,EAAE,qBAAqB,EAC9B,IAAI,GAAE,cAAmB,GACxB,OAAO,CAAC,gBAAgB,CAAC,CAyB3B;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAuB7D"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { upgradeBinary } from "../../upgrade/apply-binary.js";
|
|
4
|
+
import { upgradeNpmGlobal } from "../../upgrade/apply-npm.js";
|
|
5
|
+
import { resolveInvocationPath, detectInstallMethod } from "../../upgrade/detect.js";
|
|
6
|
+
import { VERSION } from "../../version.js";
|
|
7
|
+
function resolveDestPath(argv1, installDir) {
|
|
8
|
+
if (installDir) {
|
|
9
|
+
const basename = process.platform === "win32" ? "search-hub.exe" : "search-hub";
|
|
10
|
+
return join(installDir, basename);
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
return realpathSync(argv1);
|
|
14
|
+
} catch {
|
|
15
|
+
return argv1;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function devGuidance(method) {
|
|
19
|
+
if (method === "npx") {
|
|
20
|
+
return "Detected an npx invocation (cache-resident copy). `search-hub upgrade` does nothing here — npx fetches the latest on each run, or pin a version with `npx @ncukondo/search-hub@<tag>`.\n";
|
|
21
|
+
}
|
|
22
|
+
return "Detected a dev install (npm link or in-tree). `search-hub upgrade` does not modify dev trees. Use `git pull && npm run build` in the source checkout, or reinstall with `install.sh` or `npm i -g @ncukondo/search-hub`.\n";
|
|
23
|
+
}
|
|
24
|
+
function exitCodeFor(status) {
|
|
25
|
+
return status === "error" ? 1 : 0;
|
|
26
|
+
}
|
|
27
|
+
function formatUpgradeResult(result) {
|
|
28
|
+
const from = result.fromVersion ?? "?";
|
|
29
|
+
const to = result.toVersion ?? "?";
|
|
30
|
+
switch (result.status) {
|
|
31
|
+
case "success":
|
|
32
|
+
return `Upgraded search-hub ${from} -> ${to}`;
|
|
33
|
+
case "already-up-to-date":
|
|
34
|
+
return `Already up to date (${to})`;
|
|
35
|
+
case "guidance": {
|
|
36
|
+
const base = result.message ?? `Update available: ${from} -> ${to}`;
|
|
37
|
+
return result.url ? `${base} (${result.url})` : base;
|
|
38
|
+
}
|
|
39
|
+
case "error":
|
|
40
|
+
return `Error: ${result.error ?? "upgrade failed"}`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function buildBinaryOptions(options, argv1, currentVersion) {
|
|
44
|
+
const destPath = resolveDestPath(argv1, options.installDir);
|
|
45
|
+
const out = { destPath, currentVersion };
|
|
46
|
+
if (options.check !== void 0) out.check = options.check;
|
|
47
|
+
if (options.version !== void 0) out.version = options.version;
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
function buildNpmOptions(options, currentVersion) {
|
|
51
|
+
const out = { currentVersion };
|
|
52
|
+
if (options.check !== void 0) out.check = options.check;
|
|
53
|
+
if (options.yes !== void 0) out.yes = options.yes;
|
|
54
|
+
if (options.version !== void 0) out.version = options.version;
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
async function runUpgrade(options, deps = {}) {
|
|
58
|
+
const argv1 = deps.argv1 ?? resolveInvocationPath();
|
|
59
|
+
const installMethod = deps.installMethod ?? detectInstallMethod(deps.argv1);
|
|
60
|
+
const currentVersion = deps.currentVersion ?? VERSION;
|
|
61
|
+
const upgradeBinaryFn = deps.upgradeBinaryFn ?? upgradeBinary;
|
|
62
|
+
const upgradeNpmFn = deps.upgradeNpmFn ?? upgradeNpmGlobal;
|
|
63
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
64
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
65
|
+
if (installMethod === "dev" || installMethod === "npx") {
|
|
66
|
+
stderr.write(devGuidance(installMethod));
|
|
67
|
+
return { exitCode: 2, method: installMethod };
|
|
68
|
+
}
|
|
69
|
+
const result = installMethod === "binary" ? await upgradeBinaryFn(buildBinaryOptions(options, argv1, currentVersion)) : await upgradeNpmFn(buildNpmOptions(options, currentVersion));
|
|
70
|
+
const target = result.status === "error" ? stderr : stdout;
|
|
71
|
+
target.write(`${formatUpgradeResult(result)}
|
|
72
|
+
`);
|
|
73
|
+
return { exitCode: exitCodeFor(result.status), method: installMethod, result };
|
|
74
|
+
}
|
|
75
|
+
function registerUpgradeCommand(program) {
|
|
76
|
+
program.command("upgrade").description("Upgrade search-hub to the latest release (or a pinned version)").option("--check", "Report current vs. latest without applying any upgrade").option("--version <tag>", "Pin to a specific release tag (e.g. v0.23.1)").option("-y, --yes", "Skip confirmation prompts (applies to npm-global strategy)").option("--install-dir <path>", "Override install directory for the single-binary strategy").addHelpText("after", `
|
|
77
|
+
Exit codes:
|
|
78
|
+
0 Already up to date, or upgrade completed successfully
|
|
79
|
+
1 Upgrade failed (network, permissions, verification)
|
|
80
|
+
2 Install method cannot be upgraded automatically (dev/npx)
|
|
81
|
+
|
|
82
|
+
Examples:
|
|
83
|
+
$ search-hub upgrade # Upgrade to the latest release
|
|
84
|
+
$ search-hub upgrade --check # Report current vs. latest only
|
|
85
|
+
$ search-hub upgrade --version v0.23.1 # Pin to a specific release
|
|
86
|
+
$ search-hub upgrade -y # npm-global: run npm without prompting`).action(async (options) => {
|
|
87
|
+
const result = await runUpgrade(options);
|
|
88
|
+
process.exitCode = result.exitCode;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
export {
|
|
92
|
+
formatUpgradeResult,
|
|
93
|
+
registerUpgradeCommand,
|
|
94
|
+
runUpgrade
|
|
95
|
+
};
|
|
96
|
+
//# sourceMappingURL=upgrade.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upgrade.js","sources":["../../../src/cli/commands/upgrade.ts"],"sourcesContent":["/**\n * `search-hub upgrade` command — applies a new release via the detected\n * install method.\n *\n * Exit codes: 0 = success / already up to date, 1 = upgrade failed,\n * 2 = install method cannot be upgraded automatically (dev/npx).\n */\nimport { realpathSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { Command } from 'commander';\nimport {\n type UpgradeBinaryOptions,\n type UpgradeResult,\n upgradeBinary,\n} from '../../upgrade/apply-binary.js';\nimport { type UpgradeNpmOptions, upgradeNpmGlobal } from '../../upgrade/apply-npm.js';\nimport {\n type InstallMethod,\n detectInstallMethod,\n resolveInvocationPath,\n} from '../../upgrade/detect.js';\nimport { VERSION } from '../../version.js';\n\nexport interface UpgradeCommandOptions {\n check?: boolean;\n version?: string;\n yes?: boolean;\n installDir?: string;\n}\n\nexport interface RunUpgradeDeps {\n installMethod?: InstallMethod;\n argv1?: string;\n currentVersion?: string;\n upgradeBinaryFn?: (options: UpgradeBinaryOptions) => Promise<UpgradeResult>;\n upgradeNpmFn?: (options: UpgradeNpmOptions) => Promise<UpgradeResult>;\n stdout?: NodeJS.WritableStream;\n stderr?: NodeJS.WritableStream;\n}\n\nexport interface RunUpgradeResult {\n exitCode: 0 | 1 | 2;\n method: InstallMethod;\n result?: UpgradeResult;\n}\n\nfunction resolveDestPath(argv1: string, installDir: string | undefined): string {\n if (installDir) {\n const basename = process.platform === 'win32' ? 'search-hub.exe' : 'search-hub';\n return join(installDir, basename);\n }\n try {\n return realpathSync(argv1);\n } catch {\n return argv1;\n }\n}\n\nfunction devGuidance(method: 'dev' | 'npx'): string {\n if (method === 'npx') {\n return (\n 'Detected an npx invocation (cache-resident copy). `search-hub upgrade` does nothing here — ' +\n 'npx fetches the latest on each run, or pin a version with `npx @ncukondo/search-hub@<tag>`.\\n'\n );\n }\n return (\n 'Detected a dev install (npm link or in-tree). `search-hub upgrade` does not modify dev trees. ' +\n 'Use `git pull && npm run build` in the source checkout, or reinstall with `install.sh` ' +\n 'or `npm i -g @ncukondo/search-hub`.\\n'\n );\n}\n\nfunction exitCodeFor(status: UpgradeResult['status']): 0 | 1 {\n return status === 'error' ? 1 : 0;\n}\n\nexport function formatUpgradeResult(result: UpgradeResult): string {\n const from = result.fromVersion ?? '?';\n const to = result.toVersion ?? '?';\n switch (result.status) {\n case 'success':\n return `Upgraded search-hub ${from} -> ${to}`;\n case 'already-up-to-date':\n return `Already up to date (${to})`;\n case 'guidance': {\n const base = result.message ?? `Update available: ${from} -> ${to}`;\n return result.url ? `${base} (${result.url})` : base;\n }\n case 'error':\n return `Error: ${result.error ?? 'upgrade failed'}`;\n }\n}\n\nfunction buildBinaryOptions(\n options: UpgradeCommandOptions,\n argv1: string,\n currentVersion: string\n): UpgradeBinaryOptions {\n const destPath = resolveDestPath(argv1, options.installDir);\n const out: UpgradeBinaryOptions = { destPath, currentVersion };\n if (options.check !== undefined) out.check = options.check;\n if (options.version !== undefined) out.version = options.version;\n return out;\n}\n\nfunction buildNpmOptions(\n options: UpgradeCommandOptions,\n currentVersion: string\n): UpgradeNpmOptions {\n const out: UpgradeNpmOptions = { currentVersion };\n if (options.check !== undefined) out.check = options.check;\n if (options.yes !== undefined) out.yes = options.yes;\n if (options.version !== undefined) out.version = options.version;\n return out;\n}\n\nexport async function runUpgrade(\n options: UpgradeCommandOptions,\n deps: RunUpgradeDeps = {}\n): Promise<RunUpgradeResult> {\n // resolveInvocationPath handles the Bun-compiled binary case where\n // process.argv[1] is a virtual bunfs path (see src/upgrade/detect.ts).\n const argv1 = deps.argv1 ?? resolveInvocationPath();\n const installMethod = deps.installMethod ?? detectInstallMethod(deps.argv1);\n const currentVersion = deps.currentVersion ?? VERSION;\n const upgradeBinaryFn = deps.upgradeBinaryFn ?? upgradeBinary;\n const upgradeNpmFn = deps.upgradeNpmFn ?? upgradeNpmGlobal;\n const stdout = deps.stdout ?? process.stdout;\n const stderr = deps.stderr ?? process.stderr;\n\n if (installMethod === 'dev' || installMethod === 'npx') {\n stderr.write(devGuidance(installMethod));\n return { exitCode: 2, method: installMethod };\n }\n\n const result =\n installMethod === 'binary'\n ? await upgradeBinaryFn(buildBinaryOptions(options, argv1, currentVersion))\n : await upgradeNpmFn(buildNpmOptions(options, currentVersion));\n\n const target = result.status === 'error' ? stderr : stdout;\n target.write(`${formatUpgradeResult(result)}\\n`);\n\n return { exitCode: exitCodeFor(result.status), method: installMethod, result };\n}\n\nexport function registerUpgradeCommand(program: Command): void {\n program\n .command('upgrade')\n .description('Upgrade search-hub to the latest release (or a pinned version)')\n .option('--check', 'Report current vs. latest without applying any upgrade')\n .option('--version <tag>', 'Pin to a specific release tag (e.g. v0.23.1)')\n .option('-y, --yes', 'Skip confirmation prompts (applies to npm-global strategy)')\n .option('--install-dir <path>', 'Override install directory for the single-binary strategy')\n .addHelpText('after', `\nExit codes:\n 0 Already up to date, or upgrade completed successfully\n 1 Upgrade failed (network, permissions, verification)\n 2 Install method cannot be upgraded automatically (dev/npx)\n\nExamples:\n $ search-hub upgrade # Upgrade to the latest release\n $ search-hub upgrade --check # Report current vs. latest only\n $ search-hub upgrade --version v0.23.1 # Pin to a specific release\n $ search-hub upgrade -y # npm-global: run npm without prompting`)\n .action(async (options: UpgradeCommandOptions) => {\n const result = await runUpgrade(options);\n process.exitCode = result.exitCode;\n });\n}\n"],"names":[],"mappings":"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"}
|
package/dist/cli/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAQA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAQA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AA4N5C;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,0BAA0B;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gCAAgC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,KAAK,EAAE,OAAO,CAAC;IACf,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAo8FvC;AAED;;GAEG;AACH,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAgB1C"}
|
package/dist/cli/index.js
CHANGED
|
@@ -44,6 +44,9 @@ import { executeReviewFinalize, formatFinalizeOutput } from "./commands/review/f
|
|
|
44
44
|
import "zod";
|
|
45
45
|
import "./commands/review/schema.js";
|
|
46
46
|
import { registerFulltextCommands } from "./commands/fulltext/index.js";
|
|
47
|
+
import { registerUpgradeCommand } from "./commands/upgrade.js";
|
|
48
|
+
import { maybeStartUpdateCheck } from "../upgrade/notifier.js";
|
|
49
|
+
import { rewriteUpgradeVersionFlag, hasQuietFlag, hasNoUpdateCheckFlag, extractCommandName } from "./utils/argv.js";
|
|
47
50
|
import { parseRegisterOptions, validateRegisterInput, hasReviewFile, getReviewSummary, formatNoIncludedArticlesError, formatPendingWarning, confirmPrompt, getIncludedArticles, formatReviewRequiredMessage, formatIgnoringReviewsNote, formatDryRunOutput as formatDryRunOutput$1, formatRegistrationSummary, formatLibraryPath, formatDefaultLibraryHint } from "./commands/register.js";
|
|
48
51
|
import { formatSuggestion } from "./suggestions/index.js";
|
|
49
52
|
import { getSuggestion } from "./suggestions/rules.js";
|
|
@@ -68,7 +71,7 @@ function createProgram() {
|
|
|
68
71
|
const program = new Command();
|
|
69
72
|
program.name("search-hub").version(VERSION).description(
|
|
70
73
|
"CLI tool for systematic literature searching across multiple academic databases"
|
|
71
|
-
).option("-c, --config <path>", "path to config file").option("--session-dir <path>", "path to session directory").option("-v, --verbose", "enable verbose output", false).option("--quiet", "suppress all output except errors", false).option("--no-color", "disable color output").addHelpText("after", `
|
|
74
|
+
).option("-c, --config <path>", "path to config file").option("--session-dir <path>", "path to session directory").option("-v, --verbose", "enable verbose output", false).option("--quiet", "suppress all output except errors", false).option("--no-color", "disable color output").option("--no-update-check", "disable the update-version check").addHelpText("after", `
|
|
72
75
|
Workflow:
|
|
73
76
|
1. query init → edit → validate / --dry-run Query preparation
|
|
74
77
|
2. search --preview → search Preview & execute
|
|
@@ -494,7 +497,7 @@ Examples:
|
|
|
494
497
|
});
|
|
495
498
|
queryCommand.command("init").description("Generate a template query YAML file").argument("<title>", "query title (used for name field and filename)").option("-o, --output <path>", "write to specific file path").option("--stdout", "output to stdout instead of file").option("--force", "overwrite existing file", false).addHelpText("after", `
|
|
496
499
|
Examples:
|
|
497
|
-
$ search-hub query init "WBA pain mechanisms" # → queries/wba-pain-mechanisms.yaml
|
|
500
|
+
$ search-hub query init "WBA pain mechanisms" # → .search-hub/queries/wba-pain-mechanisms.yaml
|
|
498
501
|
$ search-hub query init "WBA pain" -o ./custom-path.yaml # Custom output path
|
|
499
502
|
$ search-hub query init "WBA pain" --stdout # Print to stdout`).action(async (title, options) => {
|
|
500
503
|
const globalOpts = program.opts();
|
|
@@ -2355,11 +2358,17 @@ Examples:
|
|
|
2355
2358
|
}
|
|
2356
2359
|
});
|
|
2357
2360
|
registerFulltextCommands(program, getSessionsDir);
|
|
2361
|
+
registerUpgradeCommand(program);
|
|
2358
2362
|
return program;
|
|
2359
2363
|
}
|
|
2360
2364
|
async function main() {
|
|
2361
2365
|
const program = createProgram();
|
|
2362
|
-
|
|
2366
|
+
const argv = rewriteUpgradeVersionFlag(process.argv, program);
|
|
2367
|
+
maybeStartUpdateCheck(extractCommandName(argv, program), {
|
|
2368
|
+
noUpdateCheck: hasNoUpdateCheckFlag(argv),
|
|
2369
|
+
quiet: hasQuietFlag(argv)
|
|
2370
|
+
});
|
|
2371
|
+
await program.parseAsync(argv);
|
|
2363
2372
|
}
|
|
2364
2373
|
const currentFile = fileURLToPath(import.meta.url);
|
|
2365
2374
|
const executedFile = process.argv[1];
|