@sweny-ai/core 0.1.65 → 0.1.67
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/__tests__/diagram.test.d.ts +1 -0
- package/dist/cli/__tests__/diagram.test.js +201 -0
- package/dist/cli/__tests__/upgrade.test.d.ts +1 -0
- package/dist/cli/__tests__/upgrade.test.js +306 -0
- package/dist/cli/__tests__/version-check.test.d.ts +1 -0
- package/dist/cli/__tests__/version-check.test.js +192 -0
- package/dist/cli/diagram.d.ts +30 -0
- package/dist/cli/diagram.js +74 -0
- package/dist/cli/main.js +69 -53
- package/dist/cli/upgrade.d.ts +74 -0
- package/dist/cli/upgrade.js +219 -0
- package/dist/cli/version-check.d.ts +62 -0
- package/dist/cli/version-check.js +123 -0
- package/package.json +1 -1
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
/**
|
|
4
|
+
* Detect the package manager that owns this install by walking the parent
|
|
5
|
+
* chain of the CLI entrypoint. The rules are ordered by specificity — more
|
|
6
|
+
* distinctive roots (bun, volta, pnpm, homebrew) beat the generic
|
|
7
|
+
* `lib/node_modules` npm layout.
|
|
8
|
+
*/
|
|
9
|
+
export function detectPackageManager(installPath) {
|
|
10
|
+
// Normalize to forward slashes so Windows paths match the same patterns.
|
|
11
|
+
const p = installPath.replace(/\\/g, "/").toLowerCase();
|
|
12
|
+
if (p.includes("/homebrew/") || p.includes("/cellar/"))
|
|
13
|
+
return "brew";
|
|
14
|
+
if (p.includes("/.volta/"))
|
|
15
|
+
return "volta";
|
|
16
|
+
if (p.includes("/.bun/"))
|
|
17
|
+
return "bun";
|
|
18
|
+
// pnpm global store lives under ~/.local/share/pnpm or ~/Library/pnpm, and
|
|
19
|
+
// the actual binary is a symlink — the resolved path typically contains
|
|
20
|
+
// either `/pnpm/` or the global store marker `/pnpm-global/`.
|
|
21
|
+
if (p.includes("/pnpm/") || p.includes("/pnpm-global/") || p.includes("/.pnpm-store/"))
|
|
22
|
+
return "pnpm";
|
|
23
|
+
// Yarn's global install folder is ~/.config/yarn/global or
|
|
24
|
+
// ~/.yarn/global — check for both.
|
|
25
|
+
if (p.includes("/.yarn/") || p.includes("/yarn/global/"))
|
|
26
|
+
return "yarn";
|
|
27
|
+
// Default: assume npm if we see node_modules — covers nvm, fnm, asdf,
|
|
28
|
+
// vanilla Node, and every CI runner.
|
|
29
|
+
if (p.includes("/node_modules/"))
|
|
30
|
+
return "npm";
|
|
31
|
+
return "unknown";
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the install-root directory for writability checks. For npm-style
|
|
35
|
+
* layouts, walk up until we find `node_modules/@sweny-ai/core` and return
|
|
36
|
+
* the parent of `node_modules` (the writable prefix). Returns null if the
|
|
37
|
+
* install path doesn't look like a package-manager install.
|
|
38
|
+
*/
|
|
39
|
+
export function resolveInstallRoot(installPath) {
|
|
40
|
+
let cur = path.dirname(installPath);
|
|
41
|
+
const visited = new Set();
|
|
42
|
+
while (cur && !visited.has(cur) && path.dirname(cur) !== cur) {
|
|
43
|
+
visited.add(cur);
|
|
44
|
+
if (path.basename(cur) === "@sweny-ai") {
|
|
45
|
+
// cur = .../node_modules/@sweny-ai — parent of node_modules is the root
|
|
46
|
+
const nodeModules = path.dirname(cur);
|
|
47
|
+
if (path.basename(nodeModules) === "node_modules") {
|
|
48
|
+
return path.dirname(nodeModules);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
cur = path.dirname(cur);
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
/** Build the (cmd, args) pair for a given package manager + tag. */
|
|
56
|
+
export function installCommandFor(pm, tag) {
|
|
57
|
+
const ref = `@sweny-ai/core@${tag}`;
|
|
58
|
+
switch (pm) {
|
|
59
|
+
case "npm":
|
|
60
|
+
return { cmd: "npm", args: ["install", "-g", ref] };
|
|
61
|
+
case "pnpm":
|
|
62
|
+
return { cmd: "pnpm", args: ["add", "-g", ref] };
|
|
63
|
+
case "yarn":
|
|
64
|
+
return { cmd: "yarn", args: ["global", "add", ref] };
|
|
65
|
+
case "bun":
|
|
66
|
+
return { cmd: "bun", args: ["add", "-g", ref] };
|
|
67
|
+
case "volta":
|
|
68
|
+
return { cmd: "volta", args: ["install", `@sweny-ai/core@${tag}`] };
|
|
69
|
+
case "brew":
|
|
70
|
+
case "unknown":
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Compare two semver-ish version strings. Returns:
|
|
76
|
+
* - `>0` when `a > b`
|
|
77
|
+
* - `<0` when `a < b`
|
|
78
|
+
* - `0` when equal
|
|
79
|
+
*
|
|
80
|
+
* Handles numeric core (`major.minor.patch`) and pre-release strings
|
|
81
|
+
* conservatively: any pre-release (`-alpha.1`) is treated as *lower* than
|
|
82
|
+
* the same core without one, matching npm's semver ordering at the level
|
|
83
|
+
* of precision we need for "is the registry newer than me."
|
|
84
|
+
*/
|
|
85
|
+
export function compareVersions(a, b) {
|
|
86
|
+
const parse = (v) => {
|
|
87
|
+
const [core, pre] = v.split("-", 2);
|
|
88
|
+
const nums = core.split(".").map((n) => {
|
|
89
|
+
const parsed = Number.parseInt(n, 10);
|
|
90
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
91
|
+
});
|
|
92
|
+
return { nums, pre: pre ?? "" };
|
|
93
|
+
};
|
|
94
|
+
const pa = parse(a);
|
|
95
|
+
const pb = parse(b);
|
|
96
|
+
const len = Math.max(pa.nums.length, pb.nums.length);
|
|
97
|
+
for (let i = 0; i < len; i++) {
|
|
98
|
+
const av = pa.nums[i] ?? 0;
|
|
99
|
+
const bv = pb.nums[i] ?? 0;
|
|
100
|
+
if (av !== bv)
|
|
101
|
+
return av - bv;
|
|
102
|
+
}
|
|
103
|
+
// Core equal — a pre-release ranks lower than a release.
|
|
104
|
+
if (pa.pre && !pb.pre)
|
|
105
|
+
return -1;
|
|
106
|
+
if (!pa.pre && pb.pre)
|
|
107
|
+
return 1;
|
|
108
|
+
if (pa.pre < pb.pre)
|
|
109
|
+
return -1;
|
|
110
|
+
if (pa.pre > pb.pre)
|
|
111
|
+
return 1;
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Fetch the latest published version of `@sweny-ai/core` from the npm
|
|
116
|
+
* registry. Uses the dist-tag endpoint which returns a small, cacheable JSON
|
|
117
|
+
* document — no tarball download.
|
|
118
|
+
*/
|
|
119
|
+
export async function fetchLatestFromNpm(tag) {
|
|
120
|
+
const controller = new AbortController();
|
|
121
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
122
|
+
try {
|
|
123
|
+
const res = await fetch(`https://registry.npmjs.org/@sweny-ai/core/${encodeURIComponent(tag)}`, {
|
|
124
|
+
signal: controller.signal,
|
|
125
|
+
headers: { accept: "application/json" },
|
|
126
|
+
});
|
|
127
|
+
if (!res.ok) {
|
|
128
|
+
throw new Error(`npm registry returned ${res.status} ${res.statusText}`);
|
|
129
|
+
}
|
|
130
|
+
const body = (await res.json());
|
|
131
|
+
if (typeof body.version !== "string" || body.version.length === 0) {
|
|
132
|
+
throw new Error("npm registry response missing `version` field");
|
|
133
|
+
}
|
|
134
|
+
return body.version;
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
clearTimeout(timeout);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const BREW_HINT = "This CLI looks like it was installed via Homebrew. Run `brew upgrade sweny` or `brew update && brew upgrade sweny`.";
|
|
141
|
+
const UNKNOWN_HINT = "Couldn't detect your package manager. Install the latest manually with:\n npm install -g @sweny-ai/core@latest";
|
|
142
|
+
/**
|
|
143
|
+
* Main `sweny upgrade` entrypoint. Resolves the latest version, decides
|
|
144
|
+
* whether an install is needed, dispatches to the right package manager,
|
|
145
|
+
* and surfaces permission problems as actionable copy-paste commands.
|
|
146
|
+
*/
|
|
147
|
+
export async function runUpgrade(options, deps) {
|
|
148
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
149
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
150
|
+
const exit = deps.exit ?? ((code) => process.exit(code));
|
|
151
|
+
const tag = options.tag && options.tag.length > 0 ? options.tag : "latest";
|
|
152
|
+
let latest;
|
|
153
|
+
try {
|
|
154
|
+
latest = await deps.fetchLatestVersion(tag);
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
stderr.write(chalk.red(` Error: couldn't reach the npm registry — ${err instanceof Error ? err.message : String(err)}`) +
|
|
158
|
+
"\n");
|
|
159
|
+
stderr.write(chalk.dim(" Check your network connection and try again.") + "\n");
|
|
160
|
+
exit(1);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const current = deps.currentVersion;
|
|
164
|
+
const cmp = compareVersions(current, latest);
|
|
165
|
+
if (cmp >= 0 && !options.force) {
|
|
166
|
+
if (cmp === 0) {
|
|
167
|
+
stdout.write(chalk.green(` ✓ sweny ${current} is already the latest.`) + "\n");
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
stdout.write(chalk.yellow(` You're running sweny ${current}, which is newer than the published ${latest}. Nothing to do.`) +
|
|
171
|
+
"\n");
|
|
172
|
+
}
|
|
173
|
+
exit(0);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const pm = detectPackageManager(deps.installPath);
|
|
177
|
+
// Homebrew has its own update cadence — we don't try to drive it from here.
|
|
178
|
+
if (pm === "brew") {
|
|
179
|
+
stdout.write(chalk.cyan(` sweny ${latest} is available (you have ${current}).`) + "\n");
|
|
180
|
+
stderr.write(chalk.dim(` ${BREW_HINT}`) + "\n");
|
|
181
|
+
exit(options.check ? 0 : 1);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const installCmd = installCommandFor(pm, tag);
|
|
185
|
+
if (!installCmd) {
|
|
186
|
+
stdout.write(chalk.cyan(` sweny ${latest} is available (you have ${current}).`) + "\n");
|
|
187
|
+
stderr.write(chalk.dim(` ${UNKNOWN_HINT}`) + "\n");
|
|
188
|
+
exit(options.check ? 0 : 1);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const fullCmd = [installCmd.cmd, ...installCmd.args].join(" ");
|
|
192
|
+
if (options.check) {
|
|
193
|
+
stdout.write(chalk.cyan(` sweny ${latest} is available (you have ${current}).`) + "\n");
|
|
194
|
+
stdout.write(chalk.dim(` Run: ${fullCmd}`) + "\n");
|
|
195
|
+
exit(0);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// Permissions probe — for npm-style layouts, the user needs write access to
|
|
199
|
+
// the prefix. If not, we print the exact sudo-prefixed command so they can
|
|
200
|
+
// copy-paste without guessing.
|
|
201
|
+
const root = resolveInstallRoot(deps.installPath);
|
|
202
|
+
if (root && !deps.canWrite(root)) {
|
|
203
|
+
stderr.write(chalk.red(` Error: no write permission on ${root}`) + "\n");
|
|
204
|
+
stderr.write(chalk.dim(` Try: sudo ${fullCmd}`) + "\n");
|
|
205
|
+
stderr.write(chalk.dim(` Or reinstall Node with a user-writable prefix (nvm, fnm, volta) to avoid sudo.`) + "\n");
|
|
206
|
+
exit(1);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
stdout.write(chalk.cyan(` Upgrading sweny ${current} → ${latest} via ${pm}…`) + "\n");
|
|
210
|
+
stdout.write(chalk.dim(` $ ${fullCmd}`) + "\n");
|
|
211
|
+
const code = deps.runInstall(installCmd.cmd, installCmd.args);
|
|
212
|
+
if (code !== 0) {
|
|
213
|
+
stderr.write(chalk.red(` Error: ${installCmd.cmd} exited with code ${code}`) + "\n");
|
|
214
|
+
exit(code);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
stdout.write(chalk.green(` ✓ Upgraded sweny to ${latest}.`) + "\n");
|
|
218
|
+
exit(0);
|
|
219
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passive "a new sweny is out" nudge. Runs as a commander `postAction` hook
|
|
3
|
+
* so every command prints at most one dim footer line when behind.
|
|
4
|
+
*
|
|
5
|
+
* Design goals:
|
|
6
|
+
* 1. Never block the user's command. Cache reads are sync; network refresh
|
|
7
|
+
* happens inline but bounded to a tight budget — if the registry is slow
|
|
8
|
+
* we swallow the error silently and try again in 24h.
|
|
9
|
+
* 2. Never nag in contexts where nobody's watching — CI, pipes, or when the
|
|
10
|
+
* user has explicitly opted out.
|
|
11
|
+
* 3. Never fire from inside `sweny upgrade` itself (would double-print the
|
|
12
|
+
* upgrade hint right before/after running the install).
|
|
13
|
+
*/
|
|
14
|
+
export declare const CACHE_TTL_MS: number;
|
|
15
|
+
/** Budget for the background registry refresh. Short on purpose. */
|
|
16
|
+
export declare const REFRESH_BUDGET_MS = 1500;
|
|
17
|
+
export interface CacheEntry {
|
|
18
|
+
latest: string;
|
|
19
|
+
checkedAt: number;
|
|
20
|
+
}
|
|
21
|
+
export interface VersionCheckDeps {
|
|
22
|
+
currentVersion: string;
|
|
23
|
+
cachePath: string;
|
|
24
|
+
now: number;
|
|
25
|
+
env: NodeJS.ProcessEnv;
|
|
26
|
+
isTty: boolean;
|
|
27
|
+
/** The command the user ran, so we can suppress the nudge for self-referential commands. */
|
|
28
|
+
commandName?: string;
|
|
29
|
+
stderr?: NodeJS.WritableStream;
|
|
30
|
+
fetchLatestVersion?: (tag: string) => Promise<string>;
|
|
31
|
+
/** Injected so tests can make the refresh synchronous + deterministic. */
|
|
32
|
+
now_fn?: () => number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Decide whether any version-check work should run at all. We bail early for:
|
|
36
|
+
* - `CI=1`: noise in pipelines and reads tokens
|
|
37
|
+
* - `SWENY_NO_UPDATE_CHECK=1`: explicit opt-out
|
|
38
|
+
* - `SWENY_OFFLINE=1`: users behind airgaps
|
|
39
|
+
* - Non-TTY stderr: scripts and pipes
|
|
40
|
+
* - `upgrade` / `update` commands: the command itself handles the hint
|
|
41
|
+
*/
|
|
42
|
+
export declare function shouldSkip(env: NodeJS.ProcessEnv, isTty: boolean, commandName: string | undefined): boolean;
|
|
43
|
+
export declare function readCache(cachePath: string): CacheEntry | null;
|
|
44
|
+
export declare function writeCache(cachePath: string, entry: CacheEntry): void;
|
|
45
|
+
export declare function isCacheFresh(entry: CacheEntry, now: number, ttl?: number): boolean;
|
|
46
|
+
export declare function formatNudge(current: string, latest: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the cache path on disk. Honors `XDG_CACHE_HOME` and falls back to
|
|
49
|
+
* `~/.cache/sweny/version-check.json`. We isolate this so tests can inject a
|
|
50
|
+
* tmpdir without touching the real cache.
|
|
51
|
+
*/
|
|
52
|
+
export declare function defaultCachePath(env?: NodeJS.ProcessEnv, home?: string): string;
|
|
53
|
+
/**
|
|
54
|
+
* Run the check. Prints a nudge if the cache shows a newer version, then
|
|
55
|
+
* refreshes the cache in the background if it's stale or missing.
|
|
56
|
+
*
|
|
57
|
+
* The refresh is awaited with a short timeout so we don't hang the CLI, but
|
|
58
|
+
* we also don't surface its result on *this* run — that keeps the code paths
|
|
59
|
+
* simple and avoids "first run after install" printing a nudge before the
|
|
60
|
+
* user's command finishes rendering.
|
|
61
|
+
*/
|
|
62
|
+
export declare function maybeNudge(deps: VersionCheckDeps): Promise<void>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { compareVersions, fetchLatestFromNpm } from "./upgrade.js";
|
|
6
|
+
/**
|
|
7
|
+
* Passive "a new sweny is out" nudge. Runs as a commander `postAction` hook
|
|
8
|
+
* so every command prints at most one dim footer line when behind.
|
|
9
|
+
*
|
|
10
|
+
* Design goals:
|
|
11
|
+
* 1. Never block the user's command. Cache reads are sync; network refresh
|
|
12
|
+
* happens inline but bounded to a tight budget — if the registry is slow
|
|
13
|
+
* we swallow the error silently and try again in 24h.
|
|
14
|
+
* 2. Never nag in contexts where nobody's watching — CI, pipes, or when the
|
|
15
|
+
* user has explicitly opted out.
|
|
16
|
+
* 3. Never fire from inside `sweny upgrade` itself (would double-print the
|
|
17
|
+
* upgrade hint right before/after running the install).
|
|
18
|
+
*/
|
|
19
|
+
export const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
20
|
+
/** Budget for the background registry refresh. Short on purpose. */
|
|
21
|
+
export const REFRESH_BUDGET_MS = 1500;
|
|
22
|
+
/**
|
|
23
|
+
* Decide whether any version-check work should run at all. We bail early for:
|
|
24
|
+
* - `CI=1`: noise in pipelines and reads tokens
|
|
25
|
+
* - `SWENY_NO_UPDATE_CHECK=1`: explicit opt-out
|
|
26
|
+
* - `SWENY_OFFLINE=1`: users behind airgaps
|
|
27
|
+
* - Non-TTY stderr: scripts and pipes
|
|
28
|
+
* - `upgrade` / `update` commands: the command itself handles the hint
|
|
29
|
+
*/
|
|
30
|
+
export function shouldSkip(env, isTty, commandName) {
|
|
31
|
+
if (env.CI)
|
|
32
|
+
return true;
|
|
33
|
+
if (env.SWENY_NO_UPDATE_CHECK)
|
|
34
|
+
return true;
|
|
35
|
+
if (env.SWENY_OFFLINE)
|
|
36
|
+
return true;
|
|
37
|
+
if (!isTty)
|
|
38
|
+
return true;
|
|
39
|
+
if (commandName === "upgrade" || commandName === "update")
|
|
40
|
+
return true;
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
export function readCache(cachePath) {
|
|
44
|
+
try {
|
|
45
|
+
const raw = fs.readFileSync(cachePath, "utf8");
|
|
46
|
+
const parsed = JSON.parse(raw);
|
|
47
|
+
if (typeof parsed.latest !== "string" || typeof parsed.checkedAt !== "number") {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (parsed.latest.length === 0)
|
|
51
|
+
return null;
|
|
52
|
+
return { latest: parsed.latest, checkedAt: parsed.checkedAt };
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function writeCache(cachePath, entry) {
|
|
59
|
+
try {
|
|
60
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
61
|
+
fs.writeFileSync(cachePath, JSON.stringify(entry));
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Cache-write failures are non-fatal — we'll just retry next run.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export function isCacheFresh(entry, now, ttl = CACHE_TTL_MS) {
|
|
68
|
+
return now - entry.checkedAt < ttl;
|
|
69
|
+
}
|
|
70
|
+
export function formatNudge(current, latest) {
|
|
71
|
+
return chalk.dim(` › sweny ${latest} is available (you have ${current}). Run \`sweny upgrade\``);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolve the cache path on disk. Honors `XDG_CACHE_HOME` and falls back to
|
|
75
|
+
* `~/.cache/sweny/version-check.json`. We isolate this so tests can inject a
|
|
76
|
+
* tmpdir without touching the real cache.
|
|
77
|
+
*/
|
|
78
|
+
export function defaultCachePath(env = process.env, home = os.homedir()) {
|
|
79
|
+
const xdg = env.XDG_CACHE_HOME && env.XDG_CACHE_HOME.length > 0 ? env.XDG_CACHE_HOME : path.join(home, ".cache");
|
|
80
|
+
return path.join(xdg, "sweny", "version-check.json");
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Run the check. Prints a nudge if the cache shows a newer version, then
|
|
84
|
+
* refreshes the cache in the background if it's stale or missing.
|
|
85
|
+
*
|
|
86
|
+
* The refresh is awaited with a short timeout so we don't hang the CLI, but
|
|
87
|
+
* we also don't surface its result on *this* run — that keeps the code paths
|
|
88
|
+
* simple and avoids "first run after install" printing a nudge before the
|
|
89
|
+
* user's command finishes rendering.
|
|
90
|
+
*/
|
|
91
|
+
export async function maybeNudge(deps) {
|
|
92
|
+
if (shouldSkip(deps.env, deps.isTty, deps.commandName))
|
|
93
|
+
return;
|
|
94
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
95
|
+
const cache = readCache(deps.cachePath);
|
|
96
|
+
if (cache && compareVersions(deps.currentVersion, cache.latest) < 0) {
|
|
97
|
+
stderr.write(formatNudge(deps.currentVersion, cache.latest) + "\n");
|
|
98
|
+
}
|
|
99
|
+
if (cache && isCacheFresh(cache, deps.now))
|
|
100
|
+
return;
|
|
101
|
+
// Stale or missing — refresh in the background with a hard timeout. We
|
|
102
|
+
// race the fetch against a timer so the process can exit promptly even if
|
|
103
|
+
// the registry hangs.
|
|
104
|
+
const fetchFn = deps.fetchLatestVersion ?? fetchLatestFromNpm;
|
|
105
|
+
try {
|
|
106
|
+
const latest = await Promise.race([
|
|
107
|
+
fetchFn("latest").catch(() => null),
|
|
108
|
+
new Promise((resolve) => {
|
|
109
|
+
const t = setTimeout(() => resolve(null), REFRESH_BUDGET_MS);
|
|
110
|
+
// Don't keep the event loop alive purely for this timer.
|
|
111
|
+
if (typeof t.unref === "function") {
|
|
112
|
+
t.unref();
|
|
113
|
+
}
|
|
114
|
+
}),
|
|
115
|
+
]);
|
|
116
|
+
if (latest) {
|
|
117
|
+
writeCache(deps.cachePath, { latest, checkedAt: deps.now });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Swallow — network hiccups shouldn't be user-visible here.
|
|
122
|
+
}
|
|
123
|
+
}
|