@ncukondo/search-hub 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) 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/query/init.d.ts +0 -1
  13. package/dist/cli/commands/query/init.d.ts.map +1 -1
  14. package/dist/cli/commands/query/init.js +2 -3
  15. package/dist/cli/commands/query/init.js.map +1 -1
  16. package/dist/cli/commands/query/resolve.d.ts.map +1 -1
  17. package/dist/cli/commands/query/resolve.js +5 -2
  18. package/dist/cli/commands/query/resolve.js.map +1 -1
  19. package/dist/cli/commands/register.d.ts +4 -0
  20. package/dist/cli/commands/register.d.ts.map +1 -1
  21. package/dist/cli/commands/register.js +62 -13
  22. package/dist/cli/commands/register.js.map +1 -1
  23. package/dist/cli/commands/upgrade.d.ts +28 -0
  24. package/dist/cli/commands/upgrade.d.ts.map +1 -0
  25. package/dist/cli/commands/upgrade.js +96 -0
  26. package/dist/cli/commands/upgrade.js.map +1 -0
  27. package/dist/cli/index.d.ts.map +1 -1
  28. package/dist/cli/index.js +12 -3
  29. package/dist/cli/index.js.map +1 -1
  30. package/dist/cli/suggestions/rules.js +1 -1
  31. package/dist/cli/suggestions/rules.js.map +1 -1
  32. package/dist/cli/utils/argv.d.ts +34 -0
  33. package/dist/cli/utils/argv.d.ts.map +1 -0
  34. package/dist/cli/utils/argv.js +63 -0
  35. package/dist/cli/utils/argv.js.map +1 -0
  36. package/dist/config/index.d.ts +1 -1
  37. package/dist/config/index.d.ts.map +1 -1
  38. package/dist/config/paths.d.ts +1 -1
  39. package/dist/config/paths.d.ts.map +1 -1
  40. package/dist/config/paths.js +6 -2
  41. package/dist/config/paths.js.map +1 -1
  42. package/dist/index.js +2 -2
  43. package/dist/integration/csl-json.d.ts +10 -0
  44. package/dist/integration/csl-json.d.ts.map +1 -1
  45. package/dist/integration/csl-json.js +12 -0
  46. package/dist/integration/csl-json.js.map +1 -1
  47. package/dist/integration/register.d.ts.map +1 -1
  48. package/dist/integration/register.js +1 -1
  49. package/dist/integration/register.js.map +1 -1
  50. package/dist/node_modules/@ncukondo/academic-fulltext/dist/convert/jats-parser.js +25 -7
  51. package/dist/node_modules/@ncukondo/academic-fulltext/dist/convert/jats-parser.js.map +1 -1
  52. package/dist/node_modules/@ncukondo/academic-fulltext/dist/discovery/index.js +67 -25
  53. package/dist/node_modules/@ncukondo/academic-fulltext/dist/discovery/index.js.map +1 -1
  54. package/dist/node_modules/@ncukondo/academic-fulltext/dist/discovery/pmc.js +3 -3
  55. package/dist/node_modules/@ncukondo/academic-fulltext/dist/discovery/pmc.js.map +1 -1
  56. package/dist/package.json.js +1 -1
  57. package/dist/upgrade/apply-binary.d.ts +37 -0
  58. package/dist/upgrade/apply-binary.d.ts.map +1 -0
  59. package/dist/upgrade/apply-binary.js +200 -0
  60. package/dist/upgrade/apply-binary.js.map +1 -0
  61. package/dist/upgrade/apply-npm.d.ts +23 -0
  62. package/dist/upgrade/apply-npm.d.ts.map +1 -0
  63. package/dist/upgrade/apply-npm.js +96 -0
  64. package/dist/upgrade/apply-npm.js.map +1 -0
  65. package/dist/upgrade/check.d.ts +21 -0
  66. package/dist/upgrade/check.d.ts.map +1 -0
  67. package/dist/upgrade/check.js +91 -0
  68. package/dist/upgrade/check.js.map +1 -0
  69. package/dist/upgrade/detect.d.ts +16 -0
  70. package/dist/upgrade/detect.d.ts.map +1 -0
  71. package/dist/upgrade/detect.js +65 -0
  72. package/dist/upgrade/detect.js.map +1 -0
  73. package/dist/upgrade/notifier.d.ts +26 -0
  74. package/dist/upgrade/notifier.d.ts.map +1 -0
  75. package/dist/upgrade/notifier.js +111 -0
  76. package/dist/upgrade/notifier.js.map +1 -0
  77. package/package.json +2 -2
@@ -0,0 +1,65 @@
1
+ import { realpathSync, statSync, existsSync } from "node:fs";
2
+ import { sep, dirname } from "node:path";
3
+ const BINARY_PATH_CHAINS = [
4
+ [".local", "bin"],
5
+ ["usr", "local", "bin"],
6
+ ["opt", "homebrew", "bin"],
7
+ ["opt", "local", "bin"]
8
+ ];
9
+ function isBunVirtualPath(path) {
10
+ return path.startsWith("/$bunfs/") || /^[A-Za-z]:\\~BUN\\/.test(path) || path.includes("/~BUN/");
11
+ }
12
+ function resolveInvocationPath(argv1 = process.argv[1], execPath = process.execPath) {
13
+ if (!argv1 || isBunVirtualPath(argv1)) return execPath;
14
+ return argv1;
15
+ }
16
+ function safeRealpath(p) {
17
+ try {
18
+ return realpathSync(p);
19
+ } catch {
20
+ return p;
21
+ }
22
+ }
23
+ function containsSegment(path, segment) {
24
+ const wrapped = `${sep}${segment}${sep}`;
25
+ return path.includes(wrapped) || path.endsWith(`${sep}${segment}`);
26
+ }
27
+ function containsPathChain(path, chain) {
28
+ const wrapped = `${sep}${chain.join(sep)}${sep}`;
29
+ return path.includes(wrapped);
30
+ }
31
+ function isTypicalBinaryPath(path) {
32
+ return BINARY_PATH_CHAINS.some((chain) => containsPathChain(path, chain));
33
+ }
34
+ function isInsideGitWorktree(startPath) {
35
+ let current;
36
+ try {
37
+ current = statSync(startPath).isDirectory() ? startPath : dirname(startPath);
38
+ } catch {
39
+ current = dirname(startPath);
40
+ }
41
+ while (true) {
42
+ if (existsSync(`${current}${sep}.git`)) {
43
+ return existsSync(`${current}${sep}package.json`);
44
+ }
45
+ const parent = dirname(current);
46
+ if (parent === current) return false;
47
+ current = parent;
48
+ }
49
+ }
50
+ function detectInstallMethod(argv1, execPath) {
51
+ const source = resolveInvocationPath(argv1, process.execPath);
52
+ if (!source) return "binary";
53
+ const resolved = safeRealpath(source);
54
+ if (containsSegment(resolved, "_npx")) return "npx";
55
+ if (isTypicalBinaryPath(resolved)) return "binary";
56
+ if (isInsideGitWorktree(resolved)) return "dev";
57
+ if (containsSegment(resolved, "node_modules")) return "npm-global";
58
+ return "binary";
59
+ }
60
+ export {
61
+ detectInstallMethod,
62
+ isBunVirtualPath,
63
+ resolveInvocationPath
64
+ };
65
+ //# sourceMappingURL=detect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detect.js","sources":["../../src/upgrade/detect.ts"],"sourcesContent":["/**\n * Install-method detection for `search-hub upgrade`.\n *\n * Resolves the invocation path (normally `process.argv[1]`) through\n * `realpathSync` and pattern-matches the resolved path to determine how the\n * user installed search-hub.\n *\n * Bun-compiled binaries (`bun build --compile`, entry `src/cli/entry-bun.ts`)\n * are a special case: `process.argv[1]` is a virtual bunfs path\n * (`/$bunfs/root/...` on Unix, `B:\\~BUN\\root\\...` on Windows) that does not\n * exist on disk and cannot be resolved with `realpathSync`. In that case the\n * real on-disk location of the running executable is `process.execPath`, so\n * detection falls back to it (see {@link resolveInvocationPath}).\n */\nimport { existsSync, realpathSync, statSync } from 'node:fs';\nimport { dirname, sep } from 'node:path';\n\nexport type InstallMethod = 'binary' | 'npm-global' | 'dev' | 'npx';\n\n/**\n * Typical install locations for the single-binary `search-hub`. Matched as an\n * embedded path chain so a resolved path like `/home/user/.local/bin/search-hub`\n * wins over a `.git` directory in an ancestor (e.g. a dotfiles repo at $HOME).\n */\nconst BINARY_PATH_CHAINS: readonly string[][] = [\n ['.local', 'bin'],\n ['usr', 'local', 'bin'],\n ['opt', 'homebrew', 'bin'],\n ['opt', 'local', 'bin'],\n];\n\n/**\n * True when `path` is a virtual path inside a Bun-compiled executable's\n * embedded filesystem. Verified against Bun 1.x behavior: `process.argv[1]`\n * is `/$bunfs/root/<outfile>` on Unix and `B:\\~BUN\\root\\<outfile>.exe` on\n * Windows, while `process.execPath` points at the real compiled binary.\n */\nexport function isBunVirtualPath(path: string): boolean {\n return path.startsWith('/$bunfs/') || /^[A-Za-z]:\\\\~BUN\\\\/.test(path) || path.includes('/~BUN/');\n}\n\n/**\n * Resolve the effective on-disk invocation path: `argv1` for regular node\n * invocations, `execPath` when running inside a Bun-compiled binary (where\n * `argv1` is a virtual bunfs path) or when `argv1` is missing.\n */\nexport function resolveInvocationPath(\n argv1: string | undefined = process.argv[1],\n execPath: string = process.execPath\n): string {\n if (!argv1 || isBunVirtualPath(argv1)) return execPath;\n return argv1;\n}\n\nfunction safeRealpath(p: string): string {\n try {\n return realpathSync(p);\n } catch {\n return p;\n }\n}\n\nfunction containsSegment(path: string, segment: string): boolean {\n const wrapped = `${sep}${segment}${sep}`;\n return path.includes(wrapped) || path.endsWith(`${sep}${segment}`);\n}\n\nfunction containsPathChain(path: string, chain: readonly string[]): boolean {\n const wrapped = `${sep}${chain.join(sep)}${sep}`;\n return path.includes(wrapped);\n}\n\nfunction isTypicalBinaryPath(path: string): boolean {\n return BINARY_PATH_CHAINS.some((chain) => containsPathChain(path, chain));\n}\n\n/**\n * True only when `startPath` is inside a git worktree that looks like a\n * search-hub checkout (i.e. the repo root contains `package.json`).\n *\n * The `package.json` check avoids false positives for unrelated ancestor\n * repos — e.g. a dotfiles repo at $HOME picking up a plain binary installed\n * at `~/.local/bin/search-hub`.\n */\nfunction isInsideGitWorktree(startPath: string): boolean {\n let current: string;\n try {\n current = statSync(startPath).isDirectory() ? startPath : dirname(startPath);\n } catch {\n current = dirname(startPath);\n }\n while (true) {\n if (existsSync(`${current}${sep}.git`)) {\n return existsSync(`${current}${sep}package.json`);\n }\n const parent = dirname(current);\n if (parent === current) return false;\n current = parent;\n }\n}\n\nexport function detectInstallMethod(argv1?: string, execPath?: string): InstallMethod {\n const source = resolveInvocationPath(argv1, execPath ?? process.execPath);\n if (!source) return 'binary';\n\n const resolved = safeRealpath(source);\n\n if (containsSegment(resolved, '_npx')) return 'npx';\n if (isTypicalBinaryPath(resolved)) return 'binary';\n if (isInsideGitWorktree(resolved)) return 'dev';\n if (containsSegment(resolved, 'node_modules')) return 'npm-global';\n return 'binary';\n}\n"],"names":[],"mappings":";;AAwBA,MAAM,qBAA0C;AAAA,EAC9C,CAAC,UAAU,KAAK;AAAA,EAChB,CAAC,OAAO,SAAS,KAAK;AAAA,EACtB,CAAC,OAAO,YAAY,KAAK;AAAA,EACzB,CAAC,OAAO,SAAS,KAAK;AACxB;AAQO,SAAS,iBAAiB,MAAuB;AACtD,SAAO,KAAK,WAAW,UAAU,KAAK,qBAAqB,KAAK,IAAI,KAAK,KAAK,SAAS,QAAQ;AACjG;AAOO,SAAS,sBACd,QAA4B,QAAQ,KAAK,CAAC,GAC1C,WAAmB,QAAQ,UACnB;AACR,MAAI,CAAC,SAAS,iBAAiB,KAAK,EAAG,QAAO;AAC9C,SAAO;AACT;AAEA,SAAS,aAAa,GAAmB;AACvC,MAAI;AACF,WAAO,aAAa,CAAC;AAAA,EACvB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,gBAAgB,MAAc,SAA0B;AAC/D,QAAM,UAAU,GAAG,GAAG,GAAG,OAAO,GAAG,GAAG;AACtC,SAAO,KAAK,SAAS,OAAO,KAAK,KAAK,SAAS,GAAG,GAAG,GAAG,OAAO,EAAE;AACnE;AAEA,SAAS,kBAAkB,MAAc,OAAmC;AAC1E,QAAM,UAAU,GAAG,GAAG,GAAG,MAAM,KAAK,GAAG,CAAC,GAAG,GAAG;AAC9C,SAAO,KAAK,SAAS,OAAO;AAC9B;AAEA,SAAS,oBAAoB,MAAuB;AAClD,SAAO,mBAAmB,KAAK,CAAC,UAAU,kBAAkB,MAAM,KAAK,CAAC;AAC1E;AAUA,SAAS,oBAAoB,WAA4B;AACvD,MAAI;AACJ,MAAI;AACF,cAAU,SAAS,SAAS,EAAE,gBAAgB,YAAY,QAAQ,SAAS;AAAA,EAC7E,QAAQ;AACN,cAAU,QAAQ,SAAS;AAAA,EAC7B;AACA,SAAO,MAAM;AACX,QAAI,WAAW,GAAG,OAAO,GAAG,GAAG,MAAM,GAAG;AACtC,aAAO,WAAW,GAAG,OAAO,GAAG,GAAG,cAAc;AAAA,IAClD;AACA,UAAM,SAAS,QAAQ,OAAO;AAC9B,QAAI,WAAW,QAAS,QAAO;AAC/B,cAAU;AAAA,EACZ;AACF;AAEO,SAAS,oBAAoB,OAAgB,UAAkC;AACpF,QAAM,SAAS,sBAAsB,OAAmB,QAAQ,QAAQ;AACxE,MAAI,CAAC,OAAQ,QAAO;AAEpB,QAAM,WAAW,aAAa,MAAM;AAEpC,MAAI,gBAAgB,UAAU,MAAM,EAAG,QAAO;AAC9C,MAAI,oBAAoB,QAAQ,EAAG,QAAO;AAC1C,MAAI,oBAAoB,QAAQ,EAAG,QAAO;AAC1C,MAAI,gBAAgB,UAAU,cAAc,EAAG,QAAO;AACtD,SAAO;AACT;"}
@@ -0,0 +1,26 @@
1
+ import { ReleaseInfo } from './check.js';
2
+ export interface NotifierOptions {
3
+ isTty?: boolean;
4
+ env?: NodeJS.ProcessEnv;
5
+ currentVersion?: string;
6
+ getLatest?: () => Promise<ReleaseInfo | null>;
7
+ output?: NodeJS.WritableStream;
8
+ /** When true, suppress the check (e.g. user passed `--no-update-check`). */
9
+ noUpdateCheck?: boolean;
10
+ /** When true, suppress the check (global `--quiet`: only errors may print). */
11
+ quiet?: boolean;
12
+ }
13
+ /**
14
+ * Kicks off the async update check. Returns a promise that resolves once the
15
+ * check is done (or immediately, if the check was suppressed). Production
16
+ * callers typically ignore the returned promise; tests can await it.
17
+ */
18
+ export declare function maybeStartUpdateCheck(command: string, options?: NotifierOptions): Promise<void>;
19
+ /**
20
+ * True when `candidate` is a strictly newer semver than `current`. Falls back
21
+ * to plain inequality when either side is not parseable as semver, so odd
22
+ * tags still produce a notice rather than being silently ignored.
23
+ */
24
+ export declare function isNewerVersion(candidate: string, current: string): boolean;
25
+ export declare function flushUpdateNotice(): void;
26
+ //# sourceMappingURL=notifier.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifier.d.ts","sourceRoot":"","sources":["../../src/upgrade/notifier.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,KAAK,WAAW,EAAoB,MAAM,YAAY,CAAC;AAgBhE,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC9C,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC/B,4EAA4E;IAC5E,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAmCD;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,IAAI,CAAC,CAgCf;AAqDD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAQ1E;AAED,wBAAgB,iBAAiB,IAAI,IAAI,CAWxC"}
@@ -0,0 +1,111 @@
1
+ import { VERSION } from "../version.js";
2
+ import { getLatestVersion } from "./check.js";
3
+ const SUPPRESSED_COMMANDS = /* @__PURE__ */ new Set(["upgrade"]);
4
+ const CHECK_TIMEOUT_MS = 3e3;
5
+ let state = null;
6
+ let exitListenerRegistered = false;
7
+ function ensureExitListener() {
8
+ if (exitListenerRegistered) return;
9
+ exitListenerRegistered = true;
10
+ process.on("exit", () => {
11
+ flushUpdateNotice();
12
+ });
13
+ }
14
+ function isSuppressed(command, env, isTty, noUpdateCheckFlag, quiet) {
15
+ if (!isTty) return true;
16
+ if (noUpdateCheckFlag) return true;
17
+ if (quiet) return true;
18
+ if (env["SEARCH_HUB_NO_UPDATE_CHECK"] === "1") return true;
19
+ if (SUPPRESSED_COMMANDS.has(command)) return true;
20
+ return false;
21
+ }
22
+ function maybeStartUpdateCheck(command, options = {}) {
23
+ state = null;
24
+ const env = options.env ?? process.env;
25
+ const isTty = options.isTty ?? process.stdout.isTTY === true;
26
+ const noUpdateCheck = options.noUpdateCheck ?? false;
27
+ const quiet = options.quiet ?? false;
28
+ if (isSuppressed(command, env, isTty, noUpdateCheck, quiet)) return Promise.resolve();
29
+ const currentVersion = options.currentVersion ?? VERSION;
30
+ const output = options.output ?? process.stderr;
31
+ const getLatest = options.getLatest ?? (() => getLatestVersion({ timeoutMs: CHECK_TIMEOUT_MS }));
32
+ const localState = {
33
+ result: null,
34
+ currentVersion,
35
+ output,
36
+ printed: false
37
+ };
38
+ state = localState;
39
+ ensureExitListener();
40
+ return getLatest().then(
41
+ (releaseInfo) => {
42
+ localState.result = releaseInfo;
43
+ },
44
+ () => {
45
+ }
46
+ );
47
+ }
48
+ const SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
49
+ function parseSemver(version) {
50
+ const match = SEMVER_RE.exec(version);
51
+ if (!match) return null;
52
+ const [, major, minor, patch, prerelease] = match;
53
+ if (major === void 0 || minor === void 0 || patch === void 0) return null;
54
+ return {
55
+ major: Number(major),
56
+ minor: Number(minor),
57
+ patch: Number(patch),
58
+ prerelease: prerelease ? prerelease.split(".") : []
59
+ };
60
+ }
61
+ function comparePrerelease(a, b) {
62
+ if (a.length === 0 && b.length === 0) return 0;
63
+ if (a.length === 0) return 1;
64
+ if (b.length === 0) return -1;
65
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
66
+ const idA = a[i];
67
+ const idB = b[i];
68
+ if (idA === void 0) return -1;
69
+ if (idB === void 0) return 1;
70
+ const numA = /^\d+$/.test(idA) ? Number(idA) : null;
71
+ const numB = /^\d+$/.test(idB) ? Number(idB) : null;
72
+ if (numA !== null && numB !== null) {
73
+ if (numA !== numB) return numA < numB ? -1 : 1;
74
+ } else if (numA !== null) {
75
+ return -1;
76
+ } else if (numB !== null) {
77
+ return 1;
78
+ } else if (idA !== idB) {
79
+ return idA < idB ? -1 : 1;
80
+ }
81
+ }
82
+ return 0;
83
+ }
84
+ function isNewerVersion(candidate, current) {
85
+ const a = parseSemver(candidate);
86
+ const b = parseSemver(current);
87
+ if (!a || !b) return candidate !== current;
88
+ if (a.major !== b.major) return a.major > b.major;
89
+ if (a.minor !== b.minor) return a.minor > b.minor;
90
+ if (a.patch !== b.patch) return a.patch > b.patch;
91
+ return comparePrerelease(a.prerelease, b.prerelease) > 0;
92
+ }
93
+ function flushUpdateNotice() {
94
+ if (!state || state.printed) return;
95
+ const { result, currentVersion, output } = state;
96
+ if (!result) return;
97
+ if (!isNewerVersion(result.latest, currentVersion)) return;
98
+ state.printed = true;
99
+ output.write(
100
+ `
101
+ >>> New version available: ${currentVersion} -> ${result.latest}
102
+ Run: search-hub upgrade
103
+ `
104
+ );
105
+ }
106
+ export {
107
+ flushUpdateNotice,
108
+ isNewerVersion,
109
+ maybeStartUpdateCheck
110
+ };
111
+ //# sourceMappingURL=notifier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifier.js","sources":["../../src/upgrade/notifier.ts"],"sourcesContent":["/**\n * Update-check notifier.\n *\n * Kicks off an async version check at CLI entry, then prints a one-line\n * ASCII notice to stderr after the user's command completes — but only if\n * the check has already resolved by then. The user's command is never\n * delayed, and machine-readable stdout (JSON/YAML output) is never touched:\n * the notice goes to stderr and only when stdout is a TTY.\n */\nimport { VERSION } from '../version.js';\nimport { type ReleaseInfo, getLatestVersion } from './check.js';\n\n/**\n * Commands that never trigger the check. search-hub has no long-running or\n * completion commands; only `upgrade` itself is suppressed (redundant there).\n * Machine-facing output paths are covered by the non-TTY suppression rule.\n */\nconst SUPPRESSED_COMMANDS = new Set(['upgrade']);\n\n/**\n * Abort the notifier's version check after this long. The check is fire-and\n * -forget, but an in-flight fetch keeps the event loop alive — without a\n * timeout, a hung connection would delay process exit for minutes.\n */\nconst CHECK_TIMEOUT_MS = 3000;\n\nexport interface NotifierOptions {\n isTty?: boolean;\n env?: NodeJS.ProcessEnv;\n currentVersion?: string;\n getLatest?: () => Promise<ReleaseInfo | null>;\n output?: NodeJS.WritableStream;\n /** When true, suppress the check (e.g. user passed `--no-update-check`). */\n noUpdateCheck?: boolean;\n /** When true, suppress the check (global `--quiet`: only errors may print). */\n quiet?: boolean;\n}\n\ninterface NotifierState {\n result: ReleaseInfo | null;\n currentVersion: string;\n output: NodeJS.WritableStream;\n printed: boolean;\n}\n\nlet state: NotifierState | null = null;\nlet exitListenerRegistered = false;\n\nfunction ensureExitListener(): void {\n if (exitListenerRegistered) return;\n exitListenerRegistered = true;\n process.on('exit', () => {\n flushUpdateNotice();\n });\n}\n\nfunction isSuppressed(\n command: string,\n env: NodeJS.ProcessEnv,\n isTty: boolean,\n noUpdateCheckFlag: boolean,\n quiet: boolean\n): boolean {\n if (!isTty) return true;\n if (noUpdateCheckFlag) return true;\n if (quiet) return true;\n if (env['SEARCH_HUB_NO_UPDATE_CHECK'] === '1') return true;\n if (SUPPRESSED_COMMANDS.has(command)) return true;\n return false;\n}\n\n/**\n * Kicks off the async update check. Returns a promise that resolves once the\n * check is done (or immediately, if the check was suppressed). Production\n * callers typically ignore the returned promise; tests can await it.\n */\nexport function maybeStartUpdateCheck(\n command: string,\n options: NotifierOptions = {}\n): Promise<void> {\n state = null;\n\n const env = options.env ?? process.env;\n const isTty = options.isTty ?? process.stdout.isTTY === true;\n const noUpdateCheck = options.noUpdateCheck ?? false;\n const quiet = options.quiet ?? false;\n\n if (isSuppressed(command, env, isTty, noUpdateCheck, quiet)) return Promise.resolve();\n\n const currentVersion = options.currentVersion ?? VERSION;\n const output = options.output ?? process.stderr;\n const getLatest =\n options.getLatest ?? (() => getLatestVersion({ timeoutMs: CHECK_TIMEOUT_MS }));\n\n const localState: NotifierState = {\n result: null,\n currentVersion,\n output,\n printed: false,\n };\n state = localState;\n ensureExitListener();\n\n return getLatest().then(\n (releaseInfo) => {\n localState.result = releaseInfo;\n },\n () => {\n // Errors are silent; nothing will be printed.\n }\n );\n}\n\ninterface ParsedVersion {\n major: number;\n minor: number;\n patch: number;\n prerelease: string[];\n}\n\nconst SEMVER_RE =\n /^v?(\\d+)\\.(\\d+)\\.(\\d+)(?:-([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$/;\n\nfunction parseSemver(version: string): ParsedVersion | null {\n const match = SEMVER_RE.exec(version);\n if (!match) return null;\n const [, major, minor, patch, prerelease] = match;\n if (major === undefined || minor === undefined || patch === undefined) return null;\n return {\n major: Number(major),\n minor: Number(minor),\n patch: Number(patch),\n prerelease: prerelease ? prerelease.split('.') : [],\n };\n}\n\n/** Semver precedence for prerelease identifier lists (empty = full release). */\nfunction comparePrerelease(a: string[], b: string[]): number {\n if (a.length === 0 && b.length === 0) return 0;\n // A version without prerelease identifiers outranks one with them.\n if (a.length === 0) return 1;\n if (b.length === 0) return -1;\n for (let i = 0; i < Math.max(a.length, b.length); i++) {\n const idA = a[i];\n const idB = b[i];\n // The longer identifier list wins once the shared prefix is equal.\n if (idA === undefined) return -1;\n if (idB === undefined) return 1;\n const numA = /^\\d+$/.test(idA) ? Number(idA) : null;\n const numB = /^\\d+$/.test(idB) ? Number(idB) : null;\n if (numA !== null && numB !== null) {\n if (numA !== numB) return numA < numB ? -1 : 1;\n } else if (numA !== null) {\n // Numeric identifiers rank below alphanumeric ones.\n return -1;\n } else if (numB !== null) {\n return 1;\n } else if (idA !== idB) {\n return idA < idB ? -1 : 1;\n }\n }\n return 0;\n}\n\n/**\n * True when `candidate` is a strictly newer semver than `current`. Falls back\n * to plain inequality when either side is not parseable as semver, so odd\n * tags still produce a notice rather than being silently ignored.\n */\nexport function isNewerVersion(candidate: string, current: string): boolean {\n const a = parseSemver(candidate);\n const b = parseSemver(current);\n if (!a || !b) return candidate !== current;\n if (a.major !== b.major) return a.major > b.major;\n if (a.minor !== b.minor) return a.minor > b.minor;\n if (a.patch !== b.patch) return a.patch > b.patch;\n return comparePrerelease(a.prerelease, b.prerelease) > 0;\n}\n\nexport function flushUpdateNotice(): void {\n if (!state || state.printed) return;\n const { result, currentVersion, output } = state;\n if (!result) return;\n // Only a strictly newer release warrants a notice; a local version that is\n // ahead of the latest release (e.g. bumped but unreleased) must stay quiet.\n if (!isNewerVersion(result.latest, currentVersion)) return;\n state.printed = true;\n output.write(\n `\\n>>> New version available: ${currentVersion} -> ${result.latest}\\n Run: search-hub upgrade\\n`\n );\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ncukondo/search-hub",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "A CLI tool for systematic literature searching across multiple academic databases",
5
5
  "type": "module",
6
6
  "engines": {
@@ -63,7 +63,7 @@
63
63
  "license": "MIT",
64
64
  "dependencies": {
65
65
  "@iarna/toml": "^3.0.0",
66
- "@ncukondo/academic-fulltext": "^0.2.0",
66
+ "@ncukondo/academic-fulltext": "^0.2.6",
67
67
  "cli-progress": "^3.12.0",
68
68
  "commander": "^13.0.0",
69
69
  "dotenv": "^17.2.3",