@ncukondo/search-hub 0.23.1 → 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/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 +11 -2
- package/dist/cli/index.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/paths.js +4 -0
- package/dist/config/paths.js.map +1 -1
- 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
|
@@ -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
|
|
@@ -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];
|