@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.
Files changed (63) hide show
  1. package/dist/cli/commands/fulltext/check.d.ts +4 -0
  2. package/dist/cli/commands/fulltext/check.d.ts.map +1 -1
  3. package/dist/cli/commands/fulltext/check.js +41 -4
  4. package/dist/cli/commands/fulltext/check.js.map +1 -1
  5. package/dist/cli/commands/fulltext/index.d.ts.map +1 -1
  6. package/dist/cli/commands/fulltext/index.js +10 -0
  7. package/dist/cli/commands/fulltext/index.js.map +1 -1
  8. package/dist/cli/commands/fulltext/verify-pmcid.d.ts +28 -0
  9. package/dist/cli/commands/fulltext/verify-pmcid.d.ts.map +1 -0
  10. package/dist/cli/commands/fulltext/verify-pmcid.js +51 -0
  11. package/dist/cli/commands/fulltext/verify-pmcid.js.map +1 -0
  12. package/dist/cli/commands/register.d.ts +4 -0
  13. package/dist/cli/commands/register.d.ts.map +1 -1
  14. package/dist/cli/commands/register.js +62 -13
  15. package/dist/cli/commands/register.js.map +1 -1
  16. package/dist/cli/commands/upgrade.d.ts +28 -0
  17. package/dist/cli/commands/upgrade.d.ts.map +1 -0
  18. package/dist/cli/commands/upgrade.js +96 -0
  19. package/dist/cli/commands/upgrade.js.map +1 -0
  20. package/dist/cli/index.d.ts.map +1 -1
  21. package/dist/cli/index.js +11 -2
  22. package/dist/cli/index.js.map +1 -1
  23. package/dist/cli/utils/argv.d.ts +34 -0
  24. package/dist/cli/utils/argv.d.ts.map +1 -0
  25. package/dist/cli/utils/argv.js +63 -0
  26. package/dist/cli/utils/argv.js.map +1 -0
  27. package/dist/config/paths.js +4 -0
  28. package/dist/config/paths.js.map +1 -1
  29. package/dist/integration/csl-json.d.ts +10 -0
  30. package/dist/integration/csl-json.d.ts.map +1 -1
  31. package/dist/integration/csl-json.js +12 -0
  32. package/dist/integration/csl-json.js.map +1 -1
  33. package/dist/integration/register.d.ts.map +1 -1
  34. package/dist/integration/register.js +1 -1
  35. package/dist/integration/register.js.map +1 -1
  36. package/dist/node_modules/@ncukondo/academic-fulltext/dist/convert/jats-parser.js +25 -7
  37. package/dist/node_modules/@ncukondo/academic-fulltext/dist/convert/jats-parser.js.map +1 -1
  38. package/dist/node_modules/@ncukondo/academic-fulltext/dist/discovery/index.js +67 -25
  39. package/dist/node_modules/@ncukondo/academic-fulltext/dist/discovery/index.js.map +1 -1
  40. package/dist/node_modules/@ncukondo/academic-fulltext/dist/discovery/pmc.js +3 -3
  41. package/dist/node_modules/@ncukondo/academic-fulltext/dist/discovery/pmc.js.map +1 -1
  42. package/dist/package.json.js +1 -1
  43. package/dist/upgrade/apply-binary.d.ts +37 -0
  44. package/dist/upgrade/apply-binary.d.ts.map +1 -0
  45. package/dist/upgrade/apply-binary.js +200 -0
  46. package/dist/upgrade/apply-binary.js.map +1 -0
  47. package/dist/upgrade/apply-npm.d.ts +23 -0
  48. package/dist/upgrade/apply-npm.d.ts.map +1 -0
  49. package/dist/upgrade/apply-npm.js +96 -0
  50. package/dist/upgrade/apply-npm.js.map +1 -0
  51. package/dist/upgrade/check.d.ts +21 -0
  52. package/dist/upgrade/check.d.ts.map +1 -0
  53. package/dist/upgrade/check.js +91 -0
  54. package/dist/upgrade/check.js.map +1 -0
  55. package/dist/upgrade/detect.d.ts +16 -0
  56. package/dist/upgrade/detect.d.ts.map +1 -0
  57. package/dist/upgrade/detect.js +65 -0
  58. package/dist/upgrade/detect.js.map +1 -0
  59. package/dist/upgrade/notifier.d.ts +26 -0
  60. package/dist/upgrade/notifier.d.ts.map +1 -0
  61. package/dist/upgrade/notifier.js +111 -0
  62. package/dist/upgrade/notifier.js.map +1 -0
  63. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"pmc.js","sources":["../../../../../../node_modules/@ncukondo/academic-fulltext/dist/discovery/pmc.js"],"sourcesContent":["/**\n * PMC OA discovery client.\n * Checks PubMed Central availability and generates download URLs.\n *\n * PDF: https://www.ncbi.nlm.nih.gov/pmc/articles/PMC{id}/pdf/\n * XML: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=pmc&id={pmcid}&rettype=xml\n * PMID→PMCID: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/elink.fcgi?dbfrom=pubmed&db=pmc&id={pmid}&retmode=json\n */\nconst EUTILS_BASE = \"https://eutils.ncbi.nlm.nih.gov/entrez/eutils\";\n/** Strip \"PMC\" prefix from PMCID, returning just the numeric part */\nfunction stripPmcPrefix(pmcid) {\n return pmcid.replace(/^PMC/i, \"\");\n}\n/** Ensure PMCID has the \"PMC\" prefix */\nfunction ensurePmcPrefix(pmcid) {\n return pmcid.startsWith(\"PMC\") ? pmcid : `PMC${pmcid}`;\n}\n/**\n * Generate PMC download URLs from a known PMCID.\n */\nexport function getPmcUrls(pmcid) {\n const numericId = stripPmcPrefix(pmcid);\n const fullPmcid = ensurePmcPrefix(pmcid);\n return [\n {\n source: \"pmc\",\n url: `https://www.ncbi.nlm.nih.gov/pmc/articles/${fullPmcid}/pdf/`,\n urlType: \"pdf\",\n version: \"published\",\n },\n {\n source: \"pmc\",\n url: `${EUTILS_BASE}/efetch.fcgi?db=pmc&id=${numericId}&rettype=xml`,\n urlType: \"xml\",\n version: \"published\",\n },\n ];\n}\n/**\n * Look up PMCID from PMID via E-utilities elink API.\n * @returns PMCID string or null if not in PMC\n */\nasync function lookupPmcid(pmid, options) {\n let url = `${EUTILS_BASE}/elink.fcgi?dbfrom=pubmed&db=pmc&id=${pmid}&retmode=json`;\n if (options?.apiKey) {\n url += `&api_key=${options.apiKey}`;\n }\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(`PMC elink API error: HTTP ${response.status}`);\n }\n const data = (await response.json());\n const linksets = data.linksets;\n if (!linksets || linksets.length === 0)\n return null;\n const firstLinkset = linksets[0];\n if (!firstLinkset?.linksetdbs)\n return null;\n const pmcLink = firstLinkset.linksetdbs.find((db) => db.dbto === \"pmc\");\n if (!pmcLink?.links || pmcLink.links.length === 0)\n return null;\n const pmcNumericId = pmcLink.links[0];\n return pmcNumericId ? `PMC${pmcNumericId}` : null;\n}\n/**\n * Check PMC availability for an article.\n * If PMCID is known, generates URLs directly.\n * If only PMID is known, looks up PMCID via E-utilities elink.\n */\nexport async function checkPmc(ids, options) {\n // If PMCID is already known, generate URLs directly\n if (ids.pmcid) {\n return getPmcUrls(ids.pmcid);\n }\n // If only PMID is known, look up PMCID\n if (ids.pmid) {\n const pmcid = await lookupPmcid(ids.pmid, options);\n if (!pmcid)\n return null;\n return getPmcUrls(pmcid);\n }\n // No identifiers provided\n return null;\n}\n//# sourceMappingURL=pmc.js.map"],"names":[],"mappings":"AAQA,MAAM,cAAc;AAEpB,SAAS,eAAe,OAAO;AAC3B,SAAO,MAAM,QAAQ,SAAS,EAAE;AACpC;AAEA,SAAS,gBAAgB,OAAO;AAC5B,SAAO,MAAM,WAAW,KAAK,IAAI,QAAQ,MAAM,KAAK;AACxD;AAIO,SAAS,WAAW,OAAO;AAC9B,QAAM,YAAY,eAAe,KAAK;AACtC,QAAM,YAAY,gBAAgB,KAAK;AACvC,SAAO;AAAA,IACH;AAAA,MACI,QAAQ;AAAA,MACR,KAAK,6CAA6C,SAAS;AAAA,MAC3D,SAAS;AAAA,MACT,SAAS;AAAA,IACrB;AAAA,IACQ;AAAA,MACI,QAAQ;AAAA,MACR,KAAK,GAAG,WAAW,0BAA0B,SAAS;AAAA,MACtD,SAAS;AAAA,MACT,SAAS;AAAA,IACrB;AAAA,EACA;AACA;AAKA,eAAe,YAAY,MAAM,SAAS;AACtC,MAAI,MAAM,GAAG,WAAW,uCAAuC,IAAI;AAInE,QAAM,WAAW,MAAM,MAAM,GAAG;AAChC,MAAI,CAAC,SAAS,IAAI;AACd,UAAM,IAAI,MAAM,6BAA6B,SAAS,MAAM,EAAE;AAAA,EAClE;AACA,QAAM,OAAQ,MAAM,SAAS;AAC7B,QAAM,WAAW,KAAK;AACtB,MAAI,CAAC,YAAY,SAAS,WAAW;AACjC,WAAO;AACX,QAAM,eAAe,SAAS,CAAC;AAC/B,MAAI,CAAC,cAAc;AACf,WAAO;AACX,QAAM,UAAU,aAAa,WAAW,KAAK,CAAC,OAAO,GAAG,SAAS,KAAK;AACtE,MAAI,CAAC,SAAS,SAAS,QAAQ,MAAM,WAAW;AAC5C,WAAO;AACX,QAAM,eAAe,QAAQ,MAAM,CAAC;AACpC,SAAO,eAAe,MAAM,YAAY,KAAK;AACjD;AAMO,eAAe,SAAS,KAAK,SAAS;AAEzC,MAAI,IAAI,OAAO;AACX,WAAO,WAAW,IAAI,KAAK;AAAA,EAC/B;AAEA,MAAI,IAAI,MAAM;AACV,UAAM,QAAQ,MAAM,YAAY,IAAI,IAAa;AACjD,QAAI,CAAC;AACD,aAAO;AACX,WAAO,WAAW,KAAK;AAAA,EAC3B;AAEA,SAAO;AACX;","x_google_ignoreList":[0]}
1
+ {"version":3,"file":"pmc.js","sources":["../../../../../../node_modules/@ncukondo/academic-fulltext/dist/discovery/pmc.js"],"sourcesContent":["/**\n * PMC OA discovery client.\n * Checks PubMed Central availability and generates download URLs.\n *\n * PDF: https://www.ncbi.nlm.nih.gov/pmc/articles/PMC{id}/pdf/\n * XML: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=pmc&id={pmcid}&rettype=xml\n * PMID→PMCID: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/elink.fcgi?dbfrom=pubmed&db=pmc&id={pmid}&retmode=json\n */\nconst EUTILS_BASE = \"https://eutils.ncbi.nlm.nih.gov/entrez/eutils\";\n/** Strip \"PMC\" prefix from PMCID, returning just the numeric part */\nfunction stripPmcPrefix(pmcid) {\n return pmcid.replace(/^PMC/i, \"\");\n}\n/** Ensure PMCID has the \"PMC\" prefix */\nfunction ensurePmcPrefix(pmcid) {\n return pmcid.startsWith(\"PMC\") ? pmcid : `PMC${pmcid}`;\n}\n/**\n * Generate PMC download URLs from a known PMCID.\n */\nexport function getPmcUrls(pmcid) {\n const numericId = stripPmcPrefix(pmcid);\n const fullPmcid = ensurePmcPrefix(pmcid);\n return [\n {\n source: \"pmc\",\n url: `https://www.ncbi.nlm.nih.gov/pmc/articles/${fullPmcid}/pdf/`,\n urlType: \"pdf\",\n version: \"published\",\n },\n {\n source: \"pmc\",\n url: `${EUTILS_BASE}/efetch.fcgi?db=pmc&id=${numericId}&rettype=xml`,\n urlType: \"xml\",\n version: \"published\",\n },\n ];\n}\n/**\n * Look up PMCID from PMID via E-utilities elink API.\n * @returns PMCID string or null if not in PMC\n */\nasync function lookupPmcid(pmid, options) {\n let url = `${EUTILS_BASE}/elink.fcgi?dbfrom=pubmed&db=pmc&id=${pmid}&retmode=json`;\n if (options?.apiKey) {\n url += `&api_key=${options.apiKey}`;\n }\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(`PMC elink API error: HTTP ${response.status}`);\n }\n const data = (await response.json());\n const linksets = data.linksets;\n if (!linksets || linksets.length === 0)\n return null;\n const firstLinkset = linksets[0];\n if (!firstLinkset?.linksetdbs)\n return null;\n const pmcLink = firstLinkset.linksetdbs.find((db) => db.dbto === \"pmc\" && db.linkname === \"pubmed_pmc\");\n if (!pmcLink?.links || pmcLink.links.length === 0)\n return null;\n const pmcNumericId = pmcLink.links[0];\n return pmcNumericId ? `PMC${pmcNumericId}` : null;\n}\n/**\n * Check PMC availability for an article.\n * If PMCID is known, generates URLs directly.\n * If only PMID is known, looks up PMCID via E-utilities elink.\n */\nexport async function checkPmc(ids, options) {\n // If PMCID is already known, generate URLs directly\n if (ids.pmcid) {\n return { locations: getPmcUrls(ids.pmcid) };\n }\n // If only PMID is known, look up PMCID\n if (ids.pmid) {\n const pmcid = await lookupPmcid(ids.pmid, options);\n if (!pmcid)\n return null;\n return { locations: getPmcUrls(pmcid), discoveredPmcid: pmcid };\n }\n // No identifiers provided\n return null;\n}\n//# sourceMappingURL=pmc.js.map"],"names":[],"mappings":"AAQA,MAAM,cAAc;AAEpB,SAAS,eAAe,OAAO;AAC3B,SAAO,MAAM,QAAQ,SAAS,EAAE;AACpC;AAEA,SAAS,gBAAgB,OAAO;AAC5B,SAAO,MAAM,WAAW,KAAK,IAAI,QAAQ,MAAM,KAAK;AACxD;AAIO,SAAS,WAAW,OAAO;AAC9B,QAAM,YAAY,eAAe,KAAK;AACtC,QAAM,YAAY,gBAAgB,KAAK;AACvC,SAAO;AAAA,IACH;AAAA,MACI,QAAQ;AAAA,MACR,KAAK,6CAA6C,SAAS;AAAA,MAC3D,SAAS;AAAA,MACT,SAAS;AAAA,IACrB;AAAA,IACQ;AAAA,MACI,QAAQ;AAAA,MACR,KAAK,GAAG,WAAW,0BAA0B,SAAS;AAAA,MACtD,SAAS;AAAA,MACT,SAAS;AAAA,IACrB;AAAA,EACA;AACA;AAKA,eAAe,YAAY,MAAM,SAAS;AACtC,MAAI,MAAM,GAAG,WAAW,uCAAuC,IAAI;AAInE,QAAM,WAAW,MAAM,MAAM,GAAG;AAChC,MAAI,CAAC,SAAS,IAAI;AACd,UAAM,IAAI,MAAM,6BAA6B,SAAS,MAAM,EAAE;AAAA,EAClE;AACA,QAAM,OAAQ,MAAM,SAAS;AAC7B,QAAM,WAAW,KAAK;AACtB,MAAI,CAAC,YAAY,SAAS,WAAW;AACjC,WAAO;AACX,QAAM,eAAe,SAAS,CAAC;AAC/B,MAAI,CAAC,cAAc;AACf,WAAO;AACX,QAAM,UAAU,aAAa,WAAW,KAAK,CAAC,OAAO,GAAG,SAAS,SAAS,GAAG,aAAa,YAAY;AACtG,MAAI,CAAC,SAAS,SAAS,QAAQ,MAAM,WAAW;AAC5C,WAAO;AACX,QAAM,eAAe,QAAQ,MAAM,CAAC;AACpC,SAAO,eAAe,MAAM,YAAY,KAAK;AACjD;AAMO,eAAe,SAAS,KAAK,SAAS;AAEzC,MAAI,IAAI,OAAO;AACX,WAAO,EAAE,WAAW,WAAW,IAAI,KAAK,EAAC;AAAA,EAC7C;AAEA,MAAI,IAAI,MAAM;AACV,UAAM,QAAQ,MAAM,YAAY,IAAI,IAAa;AACjD,QAAI,CAAC;AACD,aAAO;AACX,WAAO,EAAE,WAAW,WAAW,KAAK,GAAG,iBAAiB,MAAK;AAAA,EACjE;AAEA,SAAO;AACX;","x_google_ignoreList":[0]}
@@ -1,4 +1,4 @@
1
- const version = "0.23.1";
1
+ const version = "0.24.0";
2
2
  const pkg = {
3
3
  version
4
4
  };
@@ -0,0 +1,37 @@
1
+ import { ReleaseInfo } from './check.js';
2
+ export type UpgradeStatus = 'success' | 'already-up-to-date' | 'guidance' | 'error';
3
+ export interface UpgradeResult {
4
+ status: UpgradeStatus;
5
+ fromVersion?: string;
6
+ toVersion?: string;
7
+ url?: string;
8
+ message?: string;
9
+ error?: string;
10
+ }
11
+ export interface UpgradeBinaryOptions {
12
+ /** Absolute path to the binary that should be replaced. */
13
+ destPath: string;
14
+ /** The version of the currently running `search-hub`, without leading `v`. */
15
+ currentVersion: string;
16
+ /** Explicit release tag (e.g. "v0.24.0" or "0.24.0"). When unset, uses `getLatest`. */
17
+ version?: string;
18
+ /** Report mode only: do not mutate anything. */
19
+ check?: boolean;
20
+ /** Defaults to `process.platform`. */
21
+ platform?: NodeJS.Platform;
22
+ /** Defaults to `process.arch`. */
23
+ arch?: string;
24
+ /** Defaults to `process.pid`. */
25
+ pid?: number;
26
+ /** Injection points. */
27
+ fetch?: typeof globalThis.fetch;
28
+ getLatest?: () => Promise<ReleaseInfo | null>;
29
+ /**
30
+ * Runs the downloaded binary's `--version` command and returns the stdout
31
+ * (or null on failure). Injected so unit tests don't execute real children.
32
+ */
33
+ verifyBinary?: (path: string) => Promise<string | null>;
34
+ }
35
+ export declare function computeAssetName(platform: NodeJS.Platform, arch: string): string;
36
+ export declare function upgradeBinary(options: UpgradeBinaryOptions): Promise<UpgradeResult>;
37
+ //# sourceMappingURL=apply-binary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-binary.d.ts","sourceRoot":"","sources":["../../src/upgrade/apply-binary.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,KAAK,WAAW,EAAoB,MAAM,YAAY,CAAC;AAMhE,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,oBAAoB,GAAG,UAAU,GAAG,OAAO,CAAC;AAEpF,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,aAAa,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,cAAc,EAAE,MAAM,CAAC;IACvB,uFAAuF;IACvF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gDAAgD;IAChD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,kCAAkC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iCAAiC;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,wBAAwB;IACxB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC9C;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACzD;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAKhF;AAkKD,wBAAsB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,aAAa,CAAC,CAgFzF"}
@@ -0,0 +1,200 @@
1
+ import { execFile } from "node:child_process";
2
+ import { writeFileSync, chmodSync, existsSync, rmSync, renameSync } from "node:fs";
3
+ import { promisify } from "node:util";
4
+ import { getLatestVersion } from "./check.js";
5
+ const execFileAsync = promisify(execFile);
6
+ const REPO = "ncukondo/search-hub";
7
+ function computeAssetName(platform, arch) {
8
+ const osName = mapPlatform(platform);
9
+ const archName = mapArch(arch);
10
+ const suffix = osName === "windows" ? ".exe" : "";
11
+ return `search-hub-${osName}-${archName}${suffix}`;
12
+ }
13
+ function mapPlatform(platform) {
14
+ if (platform === "linux") return "linux";
15
+ if (platform === "darwin") return "darwin";
16
+ if (platform === "win32") return "windows";
17
+ throw new Error(`unsupported platform: ${platform}`);
18
+ }
19
+ function mapArch(arch) {
20
+ if (arch === "x64") return "x64";
21
+ if (arch === "arm64") return "arm64";
22
+ throw new Error(`unsupported arch: ${arch}`);
23
+ }
24
+ function normalizeTag(tag) {
25
+ return tag.startsWith("v") ? tag : `v${tag}`;
26
+ }
27
+ function stripV(tag) {
28
+ return tag.startsWith("v") ? tag.slice(1) : tag;
29
+ }
30
+ function buildAssetUrl(tag, assetName) {
31
+ return `https://github.com/${REPO}/releases/download/${tag}/${assetName}`;
32
+ }
33
+ function errorResult(fromVersion, error, extras = {}) {
34
+ return {
35
+ status: "error",
36
+ fromVersion,
37
+ ...extras.toVersion !== void 0 && { toVersion: extras.toVersion },
38
+ ...extras.url !== void 0 && { url: extras.url },
39
+ error
40
+ };
41
+ }
42
+ async function defaultVerifyBinary(path) {
43
+ try {
44
+ const { stdout } = await execFileAsync(path, ["--version"], {
45
+ timeout: 5e3
46
+ });
47
+ const trimmed = stdout.trim();
48
+ return trimmed.length > 0 ? trimmed : null;
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+ async function resolveTarget(pinnedVersion, getLatest) {
54
+ if (pinnedVersion) {
55
+ const targetTag2 = normalizeTag(pinnedVersion);
56
+ return { targetTag: targetTag2, targetVersion: stripV(targetTag2) };
57
+ }
58
+ const release = await getLatest();
59
+ if (!release) {
60
+ return { error: "Could not determine the latest release. Check your network connection." };
61
+ }
62
+ const targetTag = normalizeTag(release.latest);
63
+ return {
64
+ targetTag,
65
+ targetVersion: stripV(targetTag),
66
+ releaseHtmlUrl: release.url
67
+ };
68
+ }
69
+ async function downloadAsset(url, tmpPath, fetchFn) {
70
+ let response;
71
+ try {
72
+ response = await fetchFn(url);
73
+ } catch (error) {
74
+ return {
75
+ error: `Download failed: ${error instanceof Error ? error.message : String(error)}`
76
+ };
77
+ }
78
+ if (response.status === 404) {
79
+ return { error: `Download returned 404. Asset not found at: ${url}` };
80
+ }
81
+ if (!response.ok) {
82
+ return { error: `Download failed with HTTP ${response.status}: ${url}` };
83
+ }
84
+ try {
85
+ const buffer = Buffer.from(await response.arrayBuffer());
86
+ writeFileSync(tmpPath, buffer);
87
+ chmodSync(tmpPath, 493);
88
+ } catch (error) {
89
+ if (existsSync(tmpPath)) {
90
+ try {
91
+ rmSync(tmpPath, { force: true });
92
+ } catch {
93
+ }
94
+ }
95
+ return {
96
+ error: `Failed to write downloaded binary: ${error instanceof Error ? error.message : String(error)}`
97
+ };
98
+ }
99
+ return {};
100
+ }
101
+ function atomicReplace(tmpPath, destPath, platform) {
102
+ try {
103
+ if (platform === "win32") {
104
+ const oldPath = `${destPath}.old`;
105
+ if (existsSync(oldPath)) {
106
+ try {
107
+ rmSync(oldPath, { force: true });
108
+ } catch {
109
+ }
110
+ }
111
+ if (existsSync(destPath)) {
112
+ renameSync(destPath, oldPath);
113
+ }
114
+ renameSync(tmpPath, destPath);
115
+ } else {
116
+ renameSync(tmpPath, destPath);
117
+ }
118
+ } catch (error) {
119
+ return {
120
+ error: `Failed to replace ${destPath}: ${error instanceof Error ? error.message : String(error)}`
121
+ };
122
+ }
123
+ return {};
124
+ }
125
+ async function upgradeBinary(options) {
126
+ const {
127
+ destPath,
128
+ currentVersion,
129
+ version: pinnedVersion,
130
+ check = false,
131
+ platform = process.platform,
132
+ arch = process.arch,
133
+ pid = process.pid,
134
+ fetch: fetchFn = globalThis.fetch,
135
+ getLatest = getLatestVersion,
136
+ verifyBinary = defaultVerifyBinary
137
+ } = options;
138
+ const resolved = await resolveTarget(pinnedVersion, getLatest);
139
+ if ("error" in resolved) {
140
+ return errorResult(currentVersion, resolved.error);
141
+ }
142
+ const { targetTag, targetVersion, releaseHtmlUrl } = resolved;
143
+ if (!pinnedVersion && currentVersion === targetVersion && !check) {
144
+ return {
145
+ status: "already-up-to-date",
146
+ fromVersion: currentVersion,
147
+ toVersion: targetVersion,
148
+ ...releaseHtmlUrl !== void 0 && { url: releaseHtmlUrl }
149
+ };
150
+ }
151
+ let assetName;
152
+ try {
153
+ assetName = computeAssetName(platform, arch);
154
+ } catch (error) {
155
+ return errorResult(currentVersion, error instanceof Error ? error.message : String(error), {
156
+ toVersion: targetVersion
157
+ });
158
+ }
159
+ const assetUrl = buildAssetUrl(targetTag, assetName);
160
+ if (check) {
161
+ return {
162
+ status: currentVersion === targetVersion && !pinnedVersion ? "already-up-to-date" : "guidance",
163
+ fromVersion: currentVersion,
164
+ toVersion: targetVersion,
165
+ url: assetUrl
166
+ };
167
+ }
168
+ const tmpPath = `${destPath}.tmp.${pid}`;
169
+ const dl = await downloadAsset(assetUrl, tmpPath, fetchFn);
170
+ if (dl.error) {
171
+ return errorResult(currentVersion, dl.error, { toVersion: targetVersion, url: assetUrl });
172
+ }
173
+ const verified = await verifyBinary(tmpPath);
174
+ if (!verified) {
175
+ return errorResult(
176
+ currentVersion,
177
+ `Verification failed: downloaded binary at ${tmpPath} did not report a version. The file has been left in place for inspection.`,
178
+ { toVersion: targetVersion, url: assetUrl }
179
+ );
180
+ }
181
+ const replaced = atomicReplace(tmpPath, destPath, platform);
182
+ if (replaced.error) {
183
+ return errorResult(currentVersion, replaced.error, {
184
+ toVersion: targetVersion,
185
+ url: assetUrl
186
+ });
187
+ }
188
+ return {
189
+ status: "success",
190
+ fromVersion: currentVersion,
191
+ toVersion: targetVersion,
192
+ url: assetUrl,
193
+ message: verified
194
+ };
195
+ }
196
+ export {
197
+ computeAssetName,
198
+ upgradeBinary
199
+ };
200
+ //# sourceMappingURL=apply-binary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-binary.js","sources":["../../src/upgrade/apply-binary.ts"],"sourcesContent":["/**\n * Binary upgrade strategy for `search-hub upgrade` on single-binary installs.\n *\n * Downloads the release asset for the current platform to\n * `{dest}.tmp.{pid}`, verifies it by invoking `--version`, then atomically\n * replaces the running binary. Replacing the running binary on Unix is safe\n * because the kernel keeps the inode alive until open file descriptors close;\n * on Windows the running `.exe` is rotated to `{dest}.old` instead.\n */\nimport { execFile } from 'node:child_process';\nimport { chmodSync, existsSync, renameSync, rmSync, writeFileSync } from 'node:fs';\nimport { promisify } from 'node:util';\nimport { type ReleaseInfo, getLatestVersion } from './check.js';\n\nconst execFileAsync = promisify(execFile);\n\nconst REPO = 'ncukondo/search-hub';\n\nexport type UpgradeStatus = 'success' | 'already-up-to-date' | 'guidance' | 'error';\n\nexport interface UpgradeResult {\n status: UpgradeStatus;\n fromVersion?: string;\n toVersion?: string;\n url?: string;\n message?: string;\n error?: string;\n}\n\nexport interface UpgradeBinaryOptions {\n /** Absolute path to the binary that should be replaced. */\n destPath: string;\n /** The version of the currently running `search-hub`, without leading `v`. */\n currentVersion: string;\n /** Explicit release tag (e.g. \"v0.24.0\" or \"0.24.0\"). When unset, uses `getLatest`. */\n version?: string;\n /** Report mode only: do not mutate anything. */\n check?: boolean;\n /** Defaults to `process.platform`. */\n platform?: NodeJS.Platform;\n /** Defaults to `process.arch`. */\n arch?: string;\n /** Defaults to `process.pid`. */\n pid?: number;\n /** Injection points. */\n fetch?: typeof globalThis.fetch;\n getLatest?: () => Promise<ReleaseInfo | null>;\n /**\n * Runs the downloaded binary's `--version` command and returns the stdout\n * (or null on failure). Injected so unit tests don't execute real children.\n */\n verifyBinary?: (path: string) => Promise<string | null>;\n}\n\nexport function computeAssetName(platform: NodeJS.Platform, arch: string): string {\n const osName = mapPlatform(platform);\n const archName = mapArch(arch);\n const suffix = osName === 'windows' ? '.exe' : '';\n return `search-hub-${osName}-${archName}${suffix}`;\n}\n\nfunction mapPlatform(platform: NodeJS.Platform): 'linux' | 'darwin' | 'windows' {\n if (platform === 'linux') return 'linux';\n if (platform === 'darwin') return 'darwin';\n if (platform === 'win32') return 'windows';\n throw new Error(`unsupported platform: ${platform}`);\n}\n\nfunction mapArch(arch: string): 'x64' | 'arm64' {\n if (arch === 'x64') return 'x64';\n if (arch === 'arm64') return 'arm64';\n throw new Error(`unsupported arch: ${arch}`);\n}\n\nfunction normalizeTag(tag: string): string {\n return tag.startsWith('v') ? tag : `v${tag}`;\n}\n\nfunction stripV(tag: string): string {\n return tag.startsWith('v') ? tag.slice(1) : tag;\n}\n\nfunction buildAssetUrl(tag: string, assetName: string): string {\n return `https://github.com/${REPO}/releases/download/${tag}/${assetName}`;\n}\n\nfunction errorResult(\n fromVersion: string,\n error: string,\n extras: { toVersion?: string; url?: string } = {}\n): UpgradeResult {\n return {\n status: 'error',\n fromVersion,\n ...(extras.toVersion !== undefined && { toVersion: extras.toVersion }),\n ...(extras.url !== undefined && { url: extras.url }),\n error,\n };\n}\n\n/**\n * Default verifier: execs `{path} --version` with a short timeout. Returns the\n * trimmed stdout on success, or null if the binary fails to run.\n */\nasync function defaultVerifyBinary(path: string): Promise<string | null> {\n try {\n const { stdout } = await execFileAsync(path, ['--version'], {\n timeout: 5000,\n });\n const trimmed = stdout.trim();\n return trimmed.length > 0 ? trimmed : null;\n } catch {\n return null;\n }\n}\n\ninterface ResolvedTarget {\n targetTag: string;\n targetVersion: string;\n releaseHtmlUrl?: string;\n}\n\nasync function resolveTarget(\n pinnedVersion: string | undefined,\n getLatest: () => Promise<ReleaseInfo | null>\n): Promise<ResolvedTarget | { error: string }> {\n if (pinnedVersion) {\n const targetTag = normalizeTag(pinnedVersion);\n return { targetTag, targetVersion: stripV(targetTag) };\n }\n const release = await getLatest();\n if (!release) {\n return { error: 'Could not determine the latest release. Check your network connection.' };\n }\n const targetTag = normalizeTag(release.latest);\n return {\n targetTag,\n targetVersion: stripV(targetTag),\n releaseHtmlUrl: release.url,\n };\n}\n\nasync function downloadAsset(\n url: string,\n tmpPath: string,\n fetchFn: typeof globalThis.fetch\n): Promise<{ error?: string }> {\n let response: Response;\n try {\n response = await fetchFn(url);\n } catch (error) {\n return {\n error: `Download failed: ${error instanceof Error ? error.message : String(error)}`,\n };\n }\n\n if (response.status === 404) {\n return { error: `Download returned 404. Asset not found at: ${url}` };\n }\n if (!response.ok) {\n return { error: `Download failed with HTTP ${response.status}: ${url}` };\n }\n\n try {\n const buffer = Buffer.from(await response.arrayBuffer());\n writeFileSync(tmpPath, buffer);\n chmodSync(tmpPath, 0o755);\n } catch (error) {\n if (existsSync(tmpPath)) {\n try {\n rmSync(tmpPath, { force: true });\n } catch {\n // ignore\n }\n }\n return {\n error: `Failed to write downloaded binary: ${\n error instanceof Error ? error.message : String(error)\n }`,\n };\n }\n return {};\n}\n\nfunction atomicReplace(\n tmpPath: string,\n destPath: string,\n platform: NodeJS.Platform\n): { error?: string } {\n try {\n if (platform === 'win32') {\n // The running .exe cannot be deleted on Windows: rotate it to `.old`\n // (best-effort cleaned up on the next upgrade) before moving the new\n // binary into place.\n const oldPath = `${destPath}.old`;\n if (existsSync(oldPath)) {\n try {\n rmSync(oldPath, { force: true });\n } catch {\n // Best-effort: ignore if .old cannot be removed.\n }\n }\n if (existsSync(destPath)) {\n renameSync(destPath, oldPath);\n }\n renameSync(tmpPath, destPath);\n } else {\n // POSIX rename atomically replaces an existing dest: there is never a\n // moment with no binary on disk, unlike an unlink-then-rename sequence.\n renameSync(tmpPath, destPath);\n }\n } catch (error) {\n return {\n error: `Failed to replace ${destPath}: ${\n error instanceof Error ? error.message : String(error)\n }`,\n };\n }\n return {};\n}\n\nexport async function upgradeBinary(options: UpgradeBinaryOptions): Promise<UpgradeResult> {\n const {\n destPath,\n currentVersion,\n version: pinnedVersion,\n check = false,\n platform = process.platform,\n arch = process.arch,\n pid = process.pid,\n fetch: fetchFn = globalThis.fetch,\n getLatest = getLatestVersion,\n verifyBinary = defaultVerifyBinary,\n } = options;\n\n const resolved = await resolveTarget(pinnedVersion, getLatest);\n if ('error' in resolved) {\n return errorResult(currentVersion, resolved.error);\n }\n const { targetTag, targetVersion, releaseHtmlUrl } = resolved;\n\n // Fast path: already up-to-date (skipped in check mode so we still report).\n if (!pinnedVersion && currentVersion === targetVersion && !check) {\n return {\n status: 'already-up-to-date',\n fromVersion: currentVersion,\n toVersion: targetVersion,\n ...(releaseHtmlUrl !== undefined && { url: releaseHtmlUrl }),\n };\n }\n\n let assetName: string;\n try {\n assetName = computeAssetName(platform, arch);\n } catch (error) {\n return errorResult(currentVersion, error instanceof Error ? error.message : String(error), {\n toVersion: targetVersion,\n });\n }\n const assetUrl = buildAssetUrl(targetTag, assetName);\n\n if (check) {\n return {\n status:\n currentVersion === targetVersion && !pinnedVersion ? 'already-up-to-date' : 'guidance',\n fromVersion: currentVersion,\n toVersion: targetVersion,\n url: assetUrl,\n };\n }\n\n const tmpPath = `${destPath}.tmp.${pid}`;\n const dl = await downloadAsset(assetUrl, tmpPath, fetchFn);\n if (dl.error) {\n return errorResult(currentVersion, dl.error, { toVersion: targetVersion, url: assetUrl });\n }\n\n const verified = await verifyBinary(tmpPath);\n if (!verified) {\n return errorResult(\n currentVersion,\n `Verification failed: downloaded binary at ${tmpPath} did not report a version. The file has been left in place for inspection.`,\n { toVersion: targetVersion, url: assetUrl }\n );\n }\n\n const replaced = atomicReplace(tmpPath, destPath, platform);\n if (replaced.error) {\n return errorResult(currentVersion, replaced.error, {\n toVersion: targetVersion,\n url: assetUrl,\n });\n }\n\n return {\n status: 'success',\n fromVersion: currentVersion,\n toVersion: targetVersion,\n url: assetUrl,\n message: verified,\n };\n}\n"],"names":["targetTag"],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import { UpgradeResult } from './apply-binary.js';
2
+ import { ReleaseInfo } from './check.js';
3
+ export interface RunCommandResult {
4
+ exitCode: number;
5
+ stdout: string;
6
+ stderr: string;
7
+ }
8
+ export interface UpgradeNpmOptions {
9
+ currentVersion: string;
10
+ /** Explicit tag like "v0.24.0" or "0.24.0". Defaults to the getLatest result. */
11
+ version?: string;
12
+ /** `--check`: report only, no action. */
13
+ check?: boolean;
14
+ /** `--yes`: run npm instead of printing the command. */
15
+ yes?: boolean;
16
+ /** Injection points. */
17
+ getLatest?: () => Promise<ReleaseInfo | null>;
18
+ runCommand?: (command: string, args: string[]) => Promise<RunCommandResult>;
19
+ }
20
+ export type { UpgradeResult };
21
+ export declare function defaultRunCommand(command: string, args: string[]): Promise<RunCommandResult>;
22
+ export declare function upgradeNpmGlobal(options: UpgradeNpmOptions): Promise<UpgradeResult>;
23
+ //# sourceMappingURL=apply-npm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-npm.d.ts","sourceRoot":"","sources":["../../src/upgrade/apply-npm.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,KAAK,WAAW,EAAoB,MAAM,YAAY,CAAC;AAMhE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,iBAAiB;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yCAAyC;IACzC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,wDAAwD;IACxD,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,wBAAwB;IACxB,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC9C,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAC;CAC7E;AAED,YAAY,EAAE,aAAa,EAAE,CAAC;AAM9B,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,GACb,OAAO,CAAC,gBAAgB,CAAC,CAwB3B;AAED,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,aAAa,CAAC,CAmEzF"}
@@ -0,0 +1,96 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { getLatestVersion } from "./check.js";
4
+ const execFileAsync = promisify(execFile);
5
+ const PACKAGE_NAME = "@ncukondo/search-hub";
6
+ function stripV(tag) {
7
+ return tag.startsWith("v") ? tag.slice(1) : tag;
8
+ }
9
+ async function defaultRunCommand(command, args) {
10
+ try {
11
+ const { stdout, stderr } = await execFileAsync(command, args, { encoding: "utf-8" });
12
+ return { exitCode: 0, stdout, stderr };
13
+ } catch (error) {
14
+ const err = error;
15
+ if (err.code === "ENOENT") {
16
+ return {
17
+ exitCode: 127,
18
+ stdout: "",
19
+ stderr: `${command} not found in PATH — install Node.js to use the npm-global upgrade path`
20
+ };
21
+ }
22
+ const exitCode = typeof err.code === "number" ? err.code : 1;
23
+ return {
24
+ exitCode,
25
+ stdout: err.stdout ?? "",
26
+ stderr: err.stderr ?? err.message ?? ""
27
+ };
28
+ }
29
+ }
30
+ async function upgradeNpmGlobal(options) {
31
+ const {
32
+ currentVersion,
33
+ version: pinnedVersion,
34
+ check = false,
35
+ yes = false,
36
+ getLatest = getLatestVersion,
37
+ runCommand = defaultRunCommand
38
+ } = options;
39
+ let targetVersion;
40
+ let specifier;
41
+ if (pinnedVersion) {
42
+ targetVersion = stripV(pinnedVersion);
43
+ specifier = targetVersion;
44
+ } else {
45
+ const release = await getLatest();
46
+ if (!release) {
47
+ return {
48
+ status: "error",
49
+ fromVersion: currentVersion,
50
+ error: "Could not determine the latest release. Check your network connection."
51
+ };
52
+ }
53
+ targetVersion = stripV(release.latest);
54
+ specifier = "latest";
55
+ }
56
+ const commandArgs = ["i", "-g", `${PACKAGE_NAME}@${specifier}`];
57
+ const commandLine = `npm ${commandArgs.join(" ")}`;
58
+ if (!pinnedVersion && currentVersion === targetVersion && !check) {
59
+ return {
60
+ status: "already-up-to-date",
61
+ fromVersion: currentVersion,
62
+ toVersion: targetVersion,
63
+ message: commandLine
64
+ };
65
+ }
66
+ if (check || !yes) {
67
+ return {
68
+ status: "guidance",
69
+ fromVersion: currentVersion,
70
+ toVersion: targetVersion,
71
+ message: commandLine
72
+ };
73
+ }
74
+ const result = await runCommand("npm", commandArgs);
75
+ if (result.exitCode !== 0) {
76
+ const details = result.stderr.trim() || result.stdout.trim();
77
+ return {
78
+ status: "error",
79
+ fromVersion: currentVersion,
80
+ toVersion: targetVersion,
81
+ message: commandLine,
82
+ error: `npm exited with code ${result.exitCode}${details ? `: ${details}` : ""}`
83
+ };
84
+ }
85
+ return {
86
+ status: "success",
87
+ fromVersion: currentVersion,
88
+ toVersion: targetVersion,
89
+ message: commandLine
90
+ };
91
+ }
92
+ export {
93
+ defaultRunCommand,
94
+ upgradeNpmGlobal
95
+ };
96
+ //# sourceMappingURL=apply-npm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-npm.js","sources":["../../src/upgrade/apply-npm.ts"],"sourcesContent":["/**\n * npm-global upgrade strategy.\n *\n * Without `--yes`: prints the exact `npm i -g …` command and exits without\n * mutating anything. With `--yes`: runs the command directly.\n */\nimport { execFile } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport type { UpgradeResult } from './apply-binary.js';\nimport { type ReleaseInfo, getLatestVersion } from './check.js';\n\nconst execFileAsync = promisify(execFile);\n\nconst PACKAGE_NAME = '@ncukondo/search-hub';\n\nexport interface RunCommandResult {\n exitCode: number;\n stdout: string;\n stderr: string;\n}\n\nexport interface UpgradeNpmOptions {\n currentVersion: string;\n /** Explicit tag like \"v0.24.0\" or \"0.24.0\". Defaults to the getLatest result. */\n version?: string;\n /** `--check`: report only, no action. */\n check?: boolean;\n /** `--yes`: run npm instead of printing the command. */\n yes?: boolean;\n /** Injection points. */\n getLatest?: () => Promise<ReleaseInfo | null>;\n runCommand?: (command: string, args: string[]) => Promise<RunCommandResult>;\n}\n\nexport type { UpgradeResult };\n\nfunction stripV(tag: string): string {\n return tag.startsWith('v') ? tag.slice(1) : tag;\n}\n\nexport async function defaultRunCommand(\n command: string,\n args: string[]\n): Promise<RunCommandResult> {\n try {\n const { stdout, stderr } = await execFileAsync(command, args, { encoding: 'utf-8' });\n return { exitCode: 0, stdout, stderr };\n } catch (error) {\n const err = error as NodeJS.ErrnoException & {\n code?: number | string;\n stdout?: string;\n stderr?: string;\n };\n if (err.code === 'ENOENT') {\n return {\n exitCode: 127,\n stdout: '',\n stderr: `${command} not found in PATH — install Node.js to use the npm-global upgrade path`,\n };\n }\n const exitCode = typeof err.code === 'number' ? err.code : 1;\n return {\n exitCode,\n stdout: err.stdout ?? '',\n stderr: err.stderr ?? err.message ?? '',\n };\n }\n}\n\nexport async function upgradeNpmGlobal(options: UpgradeNpmOptions): Promise<UpgradeResult> {\n const {\n currentVersion,\n version: pinnedVersion,\n check = false,\n yes = false,\n getLatest = getLatestVersion,\n runCommand = defaultRunCommand,\n } = options;\n\n let targetVersion: string;\n let specifier: string;\n if (pinnedVersion) {\n targetVersion = stripV(pinnedVersion);\n specifier = targetVersion;\n } else {\n const release = await getLatest();\n if (!release) {\n return {\n status: 'error',\n fromVersion: currentVersion,\n error: 'Could not determine the latest release. Check your network connection.',\n };\n }\n targetVersion = stripV(release.latest);\n specifier = 'latest';\n }\n\n const commandArgs = ['i', '-g', `${PACKAGE_NAME}@${specifier}`];\n const commandLine = `npm ${commandArgs.join(' ')}`;\n\n if (!pinnedVersion && currentVersion === targetVersion && !check) {\n return {\n status: 'already-up-to-date',\n fromVersion: currentVersion,\n toVersion: targetVersion,\n message: commandLine,\n };\n }\n\n if (check || !yes) {\n return {\n status: 'guidance',\n fromVersion: currentVersion,\n toVersion: targetVersion,\n message: commandLine,\n };\n }\n\n const result = await runCommand('npm', commandArgs);\n if (result.exitCode !== 0) {\n const details = result.stderr.trim() || result.stdout.trim();\n return {\n status: 'error',\n fromVersion: currentVersion,\n toVersion: targetVersion,\n message: commandLine,\n error: `npm exited with code ${result.exitCode}${details ? `: ${details}` : ''}`,\n };\n }\n\n return {\n status: 'success',\n fromVersion: currentVersion,\n toVersion: targetVersion,\n message: commandLine,\n };\n}\n"],"names":[],"mappings":";;;AAWA,MAAM,gBAAgB,UAAU,QAAQ;AAExC,MAAM,eAAe;AAuBrB,SAAS,OAAO,KAAqB;AACnC,SAAO,IAAI,WAAW,GAAG,IAAI,IAAI,MAAM,CAAC,IAAI;AAC9C;AAEA,eAAsB,kBACpB,SACA,MAC2B;AAC3B,MAAI;AACF,UAAM,EAAE,QAAQ,OAAA,IAAW,MAAM,cAAc,SAAS,MAAM,EAAE,UAAU,SAAS;AACnF,WAAO,EAAE,UAAU,GAAG,QAAQ,OAAA;AAAA,EAChC,SAAS,OAAO;AACd,UAAM,MAAM;AAKZ,QAAI,IAAI,SAAS,UAAU;AACzB,aAAO;AAAA,QACL,UAAU;AAAA,QACV,QAAQ;AAAA,QACR,QAAQ,GAAG,OAAO;AAAA,MAAA;AAAA,IAEtB;AACA,UAAM,WAAW,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO;AAC3D,WAAO;AAAA,MACL;AAAA,MACA,QAAQ,IAAI,UAAU;AAAA,MACtB,QAAQ,IAAI,UAAU,IAAI,WAAW;AAAA,IAAA;AAAA,EAEzC;AACF;AAEA,eAAsB,iBAAiB,SAAoD;AACzF,QAAM;AAAA,IACJ;AAAA,IACA,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,YAAY;AAAA,IACZ,aAAa;AAAA,EAAA,IACX;AAEJ,MAAI;AACJ,MAAI;AACJ,MAAI,eAAe;AACjB,oBAAgB,OAAO,aAAa;AACpC,gBAAY;AAAA,EACd,OAAO;AACL,UAAM,UAAU,MAAM,UAAA;AACtB,QAAI,CAAC,SAAS;AACZ,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,aAAa;AAAA,QACb,OAAO;AAAA,MAAA;AAAA,IAEX;AACA,oBAAgB,OAAO,QAAQ,MAAM;AACrC,gBAAY;AAAA,EACd;AAEA,QAAM,cAAc,CAAC,KAAK,MAAM,GAAG,YAAY,IAAI,SAAS,EAAE;AAC9D,QAAM,cAAc,OAAO,YAAY,KAAK,GAAG,CAAC;AAEhD,MAAI,CAAC,iBAAiB,mBAAmB,iBAAiB,CAAC,OAAO;AAChE,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,WAAW;AAAA,MACX,SAAS;AAAA,IAAA;AAAA,EAEb;AAEA,MAAI,SAAS,CAAC,KAAK;AACjB,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,WAAW;AAAA,MACX,SAAS;AAAA,IAAA;AAAA,EAEb;AAEA,QAAM,SAAS,MAAM,WAAW,OAAO,WAAW;AAClD,MAAI,OAAO,aAAa,GAAG;AACzB,UAAM,UAAU,OAAO,OAAO,UAAU,OAAO,OAAO,KAAA;AACtD,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,WAAW;AAAA,MACX,SAAS;AAAA,MACT,OAAO,wBAAwB,OAAO,QAAQ,GAAG,UAAU,KAAK,OAAO,KAAK,EAAE;AAAA,IAAA;AAAA,EAElF;AAEA,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,WAAW;AAAA,IACX,SAAS;AAAA,EAAA;AAEb;"}
@@ -0,0 +1,21 @@
1
+ export interface ReleaseInfo {
2
+ checkedAt: string;
3
+ latest: string;
4
+ url: string;
5
+ }
6
+ export interface GetLatestVersionOptions {
7
+ force?: boolean;
8
+ cachePath?: string;
9
+ fetch?: typeof globalThis.fetch;
10
+ now?: () => Date;
11
+ ttlMs?: number;
12
+ /**
13
+ * Abort the HTTP request after this many milliseconds. Unset means no
14
+ * timeout (the interactive `upgrade` command can afford to wait); the
15
+ * notifier passes a short timeout so a hung connection never keeps the
16
+ * process alive after the user's command has finished.
17
+ */
18
+ timeoutMs?: number;
19
+ }
20
+ export declare function getLatestVersion(options?: GetLatestVersionOptions): Promise<ReleaseInfo | null>;
21
+ //# sourceMappingURL=check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/upgrade/check.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,uBAAuB;IACtC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAiDD,wBAAsB,gBAAgB,CACpC,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CA+D7B"}
@@ -0,0 +1,91 @@
1
+ import { readFile, mkdir, writeFile, rename } from "node:fs/promises";
2
+ import { join, dirname } from "node:path";
3
+ import { getDataDir } from "../config/paths.js";
4
+ const DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
5
+ const RELEASES_API_URL = "https://api.github.com/repos/ncukondo/search-hub/releases/latest";
6
+ function defaultCachePath() {
7
+ return join(getDataDir(), "update-check.json");
8
+ }
9
+ function normalizeTag(tag) {
10
+ return tag.startsWith("v") ? tag.slice(1) : tag;
11
+ }
12
+ async function writeFileAtomic(filePath, content) {
13
+ await mkdir(dirname(filePath), { recursive: true });
14
+ const tmpPath = `${filePath}.tmp.${process.pid}`;
15
+ await writeFile(tmpPath, content, "utf-8");
16
+ await rename(tmpPath, filePath);
17
+ }
18
+ async function readCache(cachePath) {
19
+ try {
20
+ const text = await readFile(cachePath, "utf-8");
21
+ const parsed = JSON.parse(text);
22
+ if (typeof parsed.checkedAt === "string" && typeof parsed.latest === "string" && typeof parsed.url === "string") {
23
+ return { checkedAt: parsed.checkedAt, latest: parsed.latest, url: parsed.url };
24
+ }
25
+ return null;
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+ function isFresh(cache, now, ttlMs) {
31
+ const checkedAt = Date.parse(cache.checkedAt);
32
+ if (Number.isNaN(checkedAt)) return false;
33
+ return now.getTime() - checkedAt < ttlMs;
34
+ }
35
+ async function getLatestVersion(options = {}) {
36
+ const {
37
+ force = false,
38
+ cachePath = defaultCachePath(),
39
+ fetch: fetchFn = globalThis.fetch,
40
+ now = () => /* @__PURE__ */ new Date(),
41
+ ttlMs = DEFAULT_TTL_MS,
42
+ timeoutMs
43
+ } = options;
44
+ const currentTime = now();
45
+ const cached = await readCache(cachePath);
46
+ if (!force && cached && isFresh(cached, currentTime, ttlMs)) {
47
+ return cached;
48
+ }
49
+ let response;
50
+ try {
51
+ response = await fetchFn(RELEASES_API_URL, {
52
+ headers: {
53
+ accept: "application/vnd.github+json",
54
+ "user-agent": "search-hub-update-check"
55
+ },
56
+ ...timeoutMs !== void 0 && { signal: AbortSignal.timeout(timeoutMs) }
57
+ });
58
+ } catch {
59
+ return null;
60
+ }
61
+ if (response.status === 403 || response.status === 429) {
62
+ return cached ?? null;
63
+ }
64
+ if (!response.ok) {
65
+ return null;
66
+ }
67
+ let payload;
68
+ try {
69
+ payload = await response.json();
70
+ } catch {
71
+ return null;
72
+ }
73
+ if (typeof payload.tag_name !== "string" || typeof payload.html_url !== "string") {
74
+ return null;
75
+ }
76
+ const info = {
77
+ checkedAt: currentTime.toISOString(),
78
+ latest: normalizeTag(payload.tag_name),
79
+ url: payload.html_url
80
+ };
81
+ try {
82
+ await writeFileAtomic(cachePath, `${JSON.stringify(info, null, 2)}
83
+ `);
84
+ } catch {
85
+ }
86
+ return info;
87
+ }
88
+ export {
89
+ getLatestVersion
90
+ };
91
+ //# sourceMappingURL=check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.js","sources":["../../src/upgrade/check.ts"],"sourcesContent":["/**\n * Update-check cache + GitHub Releases API client.\n *\n * Fetches the latest release tag from GitHub and caches the result for\n * 24 hours at `{data}/update-check.json` (search-hub's platform data dir).\n * Network/parse failures are swallowed (returns null) so the caller can\n * ignore them silently.\n */\nimport { mkdir, readFile, rename, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { getDataDir } from '../config/paths.js';\n\nexport interface ReleaseInfo {\n checkedAt: string;\n latest: string;\n url: string;\n}\n\nexport interface GetLatestVersionOptions {\n force?: boolean;\n cachePath?: string;\n fetch?: typeof globalThis.fetch;\n now?: () => Date;\n ttlMs?: number;\n /**\n * Abort the HTTP request after this many milliseconds. Unset means no\n * timeout (the interactive `upgrade` command can afford to wait); the\n * notifier passes a short timeout so a hung connection never keeps the\n * process alive after the user's command has finished.\n */\n timeoutMs?: number;\n}\n\nconst DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;\nconst RELEASES_API_URL = 'https://api.github.com/repos/ncukondo/search-hub/releases/latest';\n\nfunction defaultCachePath(): string {\n return join(getDataDir(), 'update-check.json');\n}\n\nfunction normalizeTag(tag: string): string {\n return tag.startsWith('v') ? tag.slice(1) : tag;\n}\n\n/** Write via a temp file + rename so a concurrent reader never sees a torn cache. */\nasync function writeFileAtomic(filePath: string, content: string): Promise<void> {\n await mkdir(dirname(filePath), { recursive: true });\n const tmpPath = `${filePath}.tmp.${process.pid}`;\n await writeFile(tmpPath, content, 'utf-8');\n await rename(tmpPath, filePath);\n}\n\nasync function readCache(cachePath: string): Promise<ReleaseInfo | null> {\n try {\n const text = await readFile(cachePath, 'utf-8');\n const parsed = JSON.parse(text) as Partial<ReleaseInfo>;\n if (\n typeof parsed.checkedAt === 'string' &&\n typeof parsed.latest === 'string' &&\n typeof parsed.url === 'string'\n ) {\n return { checkedAt: parsed.checkedAt, latest: parsed.latest, url: parsed.url };\n }\n return null;\n } catch {\n return null;\n }\n}\n\nfunction isFresh(cache: ReleaseInfo, now: Date, ttlMs: number): boolean {\n const checkedAt = Date.parse(cache.checkedAt);\n if (Number.isNaN(checkedAt)) return false;\n return now.getTime() - checkedAt < ttlMs;\n}\n\ninterface ReleaseApiResponse {\n tag_name?: unknown;\n html_url?: unknown;\n}\n\nexport async function getLatestVersion(\n options: GetLatestVersionOptions = {}\n): Promise<ReleaseInfo | null> {\n const {\n force = false,\n cachePath = defaultCachePath(),\n fetch: fetchFn = globalThis.fetch,\n now = () => new Date(),\n ttlMs = DEFAULT_TTL_MS,\n timeoutMs,\n } = options;\n\n const currentTime = now();\n const cached = await readCache(cachePath);\n\n if (!force && cached && isFresh(cached, currentTime, ttlMs)) {\n return cached;\n }\n\n let response: Response;\n try {\n response = await fetchFn(RELEASES_API_URL, {\n headers: {\n accept: 'application/vnd.github+json',\n 'user-agent': 'search-hub-update-check',\n },\n ...(timeoutMs !== undefined && { signal: AbortSignal.timeout(timeoutMs) }),\n });\n } catch {\n return null;\n }\n\n // On rate-limit (403/429), fall back to any existing cache per spec.\n if (response.status === 403 || response.status === 429) {\n return cached ?? null;\n }\n\n if (!response.ok) {\n return null;\n }\n\n let payload: ReleaseApiResponse;\n try {\n payload = (await response.json()) as ReleaseApiResponse;\n } catch {\n return null;\n }\n\n if (typeof payload.tag_name !== 'string' || typeof payload.html_url !== 'string') {\n return null;\n }\n\n const info: ReleaseInfo = {\n checkedAt: currentTime.toISOString(),\n latest: normalizeTag(payload.tag_name),\n url: payload.html_url,\n };\n\n try {\n await writeFileAtomic(cachePath, `${JSON.stringify(info, null, 2)}\\n`);\n } catch {\n // Cache write failure should not affect the returned value.\n }\n\n return info;\n}\n"],"names":[],"mappings":";;;AAiCA,MAAM,iBAAiB,KAAK,KAAK,KAAK;AACtC,MAAM,mBAAmB;AAEzB,SAAS,mBAA2B;AAClC,SAAO,KAAK,WAAA,GAAc,mBAAmB;AAC/C;AAEA,SAAS,aAAa,KAAqB;AACzC,SAAO,IAAI,WAAW,GAAG,IAAI,IAAI,MAAM,CAAC,IAAI;AAC9C;AAGA,eAAe,gBAAgB,UAAkB,SAAgC;AAC/E,QAAM,MAAM,QAAQ,QAAQ,GAAG,EAAE,WAAW,MAAM;AAClD,QAAM,UAAU,GAAG,QAAQ,QAAQ,QAAQ,GAAG;AAC9C,QAAM,UAAU,SAAS,SAAS,OAAO;AACzC,QAAM,OAAO,SAAS,QAAQ;AAChC;AAEA,eAAe,UAAU,WAAgD;AACvE,MAAI;AACF,UAAM,OAAO,MAAM,SAAS,WAAW,OAAO;AAC9C,UAAM,SAAS,KAAK,MAAM,IAAI;AAC9B,QACE,OAAO,OAAO,cAAc,YAC5B,OAAO,OAAO,WAAW,YACzB,OAAO,OAAO,QAAQ,UACtB;AACA,aAAO,EAAE,WAAW,OAAO,WAAW,QAAQ,OAAO,QAAQ,KAAK,OAAO,IAAA;AAAA,IAC3E;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,QAAQ,OAAoB,KAAW,OAAwB;AACtE,QAAM,YAAY,KAAK,MAAM,MAAM,SAAS;AAC5C,MAAI,OAAO,MAAM,SAAS,EAAG,QAAO;AACpC,SAAO,IAAI,YAAY,YAAY;AACrC;AAOA,eAAsB,iBACpB,UAAmC,IACN;AAC7B,QAAM;AAAA,IACJ,QAAQ;AAAA,IACR,YAAY,iBAAA;AAAA,IACZ,OAAO,UAAU,WAAW;AAAA,IAC5B,MAAM,MAAM,oBAAI,KAAA;AAAA,IAChB,QAAQ;AAAA,IACR;AAAA,EAAA,IACE;AAEJ,QAAM,cAAc,IAAA;AACpB,QAAM,SAAS,MAAM,UAAU,SAAS;AAExC,MAAI,CAAC,SAAS,UAAU,QAAQ,QAAQ,aAAa,KAAK,GAAG;AAC3D,WAAO;AAAA,EACT;AAEA,MAAI;AACJ,MAAI;AACF,eAAW,MAAM,QAAQ,kBAAkB;AAAA,MACzC,SAAS;AAAA,QACP,QAAQ;AAAA,QACR,cAAc;AAAA,MAAA;AAAA,MAEhB,GAAI,cAAc,UAAa,EAAE,QAAQ,YAAY,QAAQ,SAAS,EAAA;AAAA,IAAE,CACzE;AAAA,EACH,QAAQ;AACN,WAAO;AAAA,EACT;AAGA,MAAI,SAAS,WAAW,OAAO,SAAS,WAAW,KAAK;AACtD,WAAO,UAAU;AAAA,EACnB;AAEA,MAAI,CAAC,SAAS,IAAI;AAChB,WAAO;AAAA,EACT;AAEA,MAAI;AACJ,MAAI;AACF,cAAW,MAAM,SAAS,KAAA;AAAA,EAC5B,QAAQ;AACN,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,QAAQ,aAAa,YAAY,OAAO,QAAQ,aAAa,UAAU;AAChF,WAAO;AAAA,EACT;AAEA,QAAM,OAAoB;AAAA,IACxB,WAAW,YAAY,YAAA;AAAA,IACvB,QAAQ,aAAa,QAAQ,QAAQ;AAAA,IACrC,KAAK,QAAQ;AAAA,EAAA;AAGf,MAAI;AACF,UAAM,gBAAgB,WAAW,GAAG,KAAK,UAAU,MAAM,MAAM,CAAC,CAAC;AAAA,CAAI;AAAA,EACvE,QAAQ;AAAA,EAER;AAEA,SAAO;AACT;"}
@@ -0,0 +1,16 @@
1
+ export type InstallMethod = 'binary' | 'npm-global' | 'dev' | 'npx';
2
+ /**
3
+ * True when `path` is a virtual path inside a Bun-compiled executable's
4
+ * embedded filesystem. Verified against Bun 1.x behavior: `process.argv[1]`
5
+ * is `/$bunfs/root/<outfile>` on Unix and `B:\~BUN\root\<outfile>.exe` on
6
+ * Windows, while `process.execPath` points at the real compiled binary.
7
+ */
8
+ export declare function isBunVirtualPath(path: string): boolean;
9
+ /**
10
+ * Resolve the effective on-disk invocation path: `argv1` for regular node
11
+ * invocations, `execPath` when running inside a Bun-compiled binary (where
12
+ * `argv1` is a virtual bunfs path) or when `argv1` is missing.
13
+ */
14
+ export declare function resolveInvocationPath(argv1?: string | undefined, execPath?: string): string;
15
+ export declare function detectInstallMethod(argv1?: string, execPath?: string): InstallMethod;
16
+ //# sourceMappingURL=detect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../../src/upgrade/detect.ts"],"names":[],"mappings":"AAiBA,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,YAAY,GAAG,KAAK,GAAG,KAAK,CAAC;AAcpE;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,GAAE,MAAM,GAAG,SAA2B,EAC3C,QAAQ,GAAE,MAAyB,GAClC,MAAM,CAGR;AAiDD,wBAAgB,mBAAmB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,aAAa,CAWpF"}