@ornncompute/cli 0.2.11 → 0.2.12
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/README.md +13 -1
- package/bin/ornn.js +2 -10
- package/package.json +4 -1
- package/src/cli.mjs +3 -13
- package/src/command-entry.mjs +13 -0
- package/src/signals.mjs +17 -0
- package/src/startup.mjs +288 -0
- package/src/update.mjs +344 -164
package/README.md
CHANGED
|
@@ -357,7 +357,19 @@ between Public and Private VPN preserves the peer.
|
|
|
357
357
|
|
|
358
358
|
## Updating
|
|
359
359
|
|
|
360
|
-
|
|
360
|
+
Interactive global installations check for new releases in the background,
|
|
361
|
+
using a daily interval. When a newer release has been discovered, Ornn
|
|
362
|
+
installs it before your next eligible command and then runs that command
|
|
363
|
+
with the updated CLI. The first check has a one-day grace period.
|
|
364
|
+
|
|
365
|
+
Automatic updates are skipped in CI, with redirected input or output,
|
|
366
|
+
for machine-readable output, and for help or version commands. Project
|
|
367
|
+
dependencies, development checkouts, and package-runner invocations do
|
|
368
|
+
not update automatically. Run `ornn update` to check and update immediately.
|
|
369
|
+
|
|
370
|
+
If an installation fails, Ornn continues only when it can verify that
|
|
371
|
+
the remaining CLI loads successfully. Otherwise it prints reinstall
|
|
372
|
+
instructions. If another update is running, retry after it finishes.
|
|
361
373
|
|
|
362
374
|
## Deployment
|
|
363
375
|
|
package/bin/ornn.js
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const exitCode = await run(process.argv.slice(2), {
|
|
6
|
-
env: process.env,
|
|
7
|
-
stderr: process.stderr,
|
|
8
|
-
stdout: process.stdout,
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
process.exitCode = exitCode;
|
|
2
|
+
import { bootstrap } from "../src/startup.mjs";
|
|
3
|
+
process.exitCode = await bootstrap();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ornncompute/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"description": "Command-line interface for Ornn compute access workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,6 +33,9 @@
|
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@ornn/capabilities": "0.1.0"
|
|
35
35
|
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"update-notifier": "7.3.1"
|
|
38
|
+
},
|
|
36
39
|
"engines": {
|
|
37
40
|
"node": ">=20"
|
|
38
41
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
tokensListCommand,
|
|
52
52
|
usersQueryCommand,
|
|
53
53
|
} from "./catalog-dispatch.mjs";
|
|
54
|
-
import {
|
|
54
|
+
import { updateCommand } from "./update.mjs";
|
|
55
55
|
|
|
56
56
|
const { version: VERSION } = createRequire(import.meta.url)("../package.json");
|
|
57
57
|
const DEFAULT_BUY_NOW_USD_PER_GPU_HOUR = 10;
|
|
@@ -358,17 +358,7 @@ const GPU_COMMANDS = new Set(["gpu", "gpus", "reservations"]);
|
|
|
358
358
|
const RESERVATION_OP_SUBCOMMANDS = new Set(["transfer", "deploy", "commerce"]);
|
|
359
359
|
|
|
360
360
|
export async function run(argv = [], io = {}) {
|
|
361
|
-
|
|
362
|
-
if (argv[0] !== "update") {
|
|
363
|
-
await maybeNotifyUpdate({
|
|
364
|
-
binPath: io.binPath ?? process.argv[1],
|
|
365
|
-
currentVersion: VERSION,
|
|
366
|
-
env: io.env ?? process.env,
|
|
367
|
-
fetchImpl: io.fetch ?? fetch,
|
|
368
|
-
stderr: io.stderr ?? process.stderr,
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
return exitCode;
|
|
361
|
+
return await dispatch(argv, io);
|
|
372
362
|
}
|
|
373
363
|
|
|
374
364
|
async function dispatch(argv = [], io = {}) {
|
|
@@ -3874,7 +3864,7 @@ async function storage(rawArgs, context) {
|
|
|
3874
3864
|
status: response.status,
|
|
3875
3865
|
});
|
|
3876
3866
|
}
|
|
3877
|
-
// Atomic tmp+rename
|
|
3867
|
+
// Atomic tmp+rename: a mid-stream
|
|
3878
3868
|
// failure must never truncate or replace an existing file at outputPath.
|
|
3879
3869
|
const partialPath = `${outputPath}.${process.pid}.partial`;
|
|
3880
3870
|
try {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
|
|
3
|
+
const { version } = createRequire(import.meta.url)("../package.json");
|
|
4
|
+
|
|
5
|
+
export async function execute(argv, io) {
|
|
6
|
+
const { run } = await import("./cli.mjs");
|
|
7
|
+
return await run(argv, io);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function probe() {
|
|
11
|
+
await import("./cli.mjs");
|
|
12
|
+
return version;
|
|
13
|
+
}
|
package/src/signals.mjs
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function forwardSignals(signalSource, child, onSignal) {
|
|
2
|
+
const listeners = ["SIGINT", "SIGTERM"].map((signal) => {
|
|
3
|
+
const listener = () => {
|
|
4
|
+
onSignal(signal);
|
|
5
|
+
try {
|
|
6
|
+
child?.kill?.(signal);
|
|
7
|
+
} catch {}
|
|
8
|
+
};
|
|
9
|
+
signalSource.once(signal, listener);
|
|
10
|
+
return [signal, listener];
|
|
11
|
+
});
|
|
12
|
+
return () => {
|
|
13
|
+
for (const [signal, listener] of listeners.splice(0)) {
|
|
14
|
+
signalSource.off?.(signal, listener) ?? signalSource.removeListener?.(signal, listener);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
package/src/startup.mjs
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { constants } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { forwardSignals } from "./signals.mjs";
|
|
7
|
+
import {
|
|
8
|
+
acquireUpdateLock,
|
|
9
|
+
compareVersions,
|
|
10
|
+
detectInstallContext,
|
|
11
|
+
installVersion,
|
|
12
|
+
isValidReleaseVersion,
|
|
13
|
+
resolveInstalledBin,
|
|
14
|
+
} from "./update.mjs";
|
|
15
|
+
|
|
16
|
+
const PACKAGE_NAME = "@ornncompute/cli";
|
|
17
|
+
const { version: VERSION } = createRequire(import.meta.url)("../package.json");
|
|
18
|
+
const UPDATE_CHECK_INTERVAL = 24 * 60 * 60 * 1000;
|
|
19
|
+
const PROBE_TIMEOUT_MS = 5000;
|
|
20
|
+
const PROBE_KILL_GRACE_MS = 250;
|
|
21
|
+
const GLOBAL_INSTALL_KINDS = new Set(["bun", "npm", "pnpm", "yarn"]);
|
|
22
|
+
const REINSTALL_GUIDANCE = `Reinstall with: npm install -g ${PACKAGE_NAME}@latest`;
|
|
23
|
+
const PROBE_PROGRAM = `const entry = await import(process.argv[1]);
|
|
24
|
+
process.stdout.write(await entry.probe());`;
|
|
25
|
+
const RESTART_PROGRAM = `const entry = await import(process.argv[1]);
|
|
26
|
+
process.exitCode = await entry.execute(process.argv.slice(3), {
|
|
27
|
+
binPath: process.argv[2], env: process.env,
|
|
28
|
+
stdin: process.stdin, stdout: process.stdout, stderr: process.stderr,
|
|
29
|
+
});`;
|
|
30
|
+
|
|
31
|
+
function hasHumanOnlyArgument(argv) {
|
|
32
|
+
return argv.some(
|
|
33
|
+
(arg) =>
|
|
34
|
+
arg === "--json" ||
|
|
35
|
+
arg.startsWith("--json=") ||
|
|
36
|
+
arg === "--raw" ||
|
|
37
|
+
arg === "--help" ||
|
|
38
|
+
arg === "-h" ||
|
|
39
|
+
arg === "help" ||
|
|
40
|
+
arg === "--version" ||
|
|
41
|
+
arg === "-v" ||
|
|
42
|
+
arg === "version",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function shouldAutoUpdate(argv, { env = process.env, stdin, stdout, stderr, context } = {}) {
|
|
47
|
+
if (!GLOBAL_INSTALL_KINDS.has(context?.kind)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
if ((env.CI && env.CI !== "false") || !stdin?.isTTY || !stdout?.isTTY || !stderr?.isTTY) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
if (argv.length === 0 || argv[0] === "update" || hasHumanOnlyArgument(argv)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function defaultNotifierFactory(options) {
|
|
60
|
+
const { default: updateNotifier } = await import("update-notifier");
|
|
61
|
+
return updateNotifier(options);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function warning(stderr, message) {
|
|
65
|
+
stderr?.write(`${message}\n`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function failedProbe(stderr) {
|
|
69
|
+
warning(stderr, `The installed ornn CLI could not be verified. ${REINSTALL_GUIDANCE}`);
|
|
70
|
+
return { kind: "exit", code: 1 };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function unavailableLock(stderr) {
|
|
74
|
+
warning(
|
|
75
|
+
stderr,
|
|
76
|
+
"Automatic update unavailable; retry after any other ornn update finishes. If none is running, remove the stale update-install lock from your Ornn config directory or check that directory's permissions.",
|
|
77
|
+
);
|
|
78
|
+
return { kind: "exit", code: 1 };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function exitForSignal(signal) {
|
|
82
|
+
return { kind: "exit", code: 128 + (constants.signals[signal] ?? 1) };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function entryUrl(binPath) {
|
|
86
|
+
return pathToFileURL(join(dirname(binPath), "..", "src", "command-entry.mjs")).href;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function probeInstalledCli(
|
|
90
|
+
binPath,
|
|
91
|
+
{ spawnProcess = spawn, env = process.env, signalSource = process, timeoutMs = PROBE_TIMEOUT_MS } = {},
|
|
92
|
+
) {
|
|
93
|
+
return await new Promise((resolve) => {
|
|
94
|
+
let output = "";
|
|
95
|
+
let settled = false;
|
|
96
|
+
let timedOut = false;
|
|
97
|
+
let forceKillTimer;
|
|
98
|
+
let timeoutTimer;
|
|
99
|
+
const child = spawnProcess(process.execPath, ["--input-type=module", "--eval", PROBE_PROGRAM, entryUrl(binPath)], {
|
|
100
|
+
env,
|
|
101
|
+
shell: false,
|
|
102
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
103
|
+
});
|
|
104
|
+
const scheduleForceKill = () => {
|
|
105
|
+
forceKillTimer ??= setTimeout(() => child.kill?.("SIGKILL"), PROBE_KILL_GRACE_MS);
|
|
106
|
+
};
|
|
107
|
+
const removeSignals = forwardSignals(signalSource, child, scheduleForceKill);
|
|
108
|
+
const finish = (result) => {
|
|
109
|
+
if (settled) return;
|
|
110
|
+
settled = true;
|
|
111
|
+
clearTimeout(timeoutTimer);
|
|
112
|
+
clearTimeout(forceKillTimer);
|
|
113
|
+
removeSignals();
|
|
114
|
+
resolve(result);
|
|
115
|
+
};
|
|
116
|
+
timeoutTimer = setTimeout(() => {
|
|
117
|
+
timedOut = true;
|
|
118
|
+
child.kill?.("SIGTERM");
|
|
119
|
+
scheduleForceKill();
|
|
120
|
+
}, timeoutMs);
|
|
121
|
+
child.stdout?.on("data", (chunk) => {
|
|
122
|
+
output += String(chunk);
|
|
123
|
+
});
|
|
124
|
+
child.on("error", () => finish({ ok: false, version: null }));
|
|
125
|
+
child.on("close", (code, signal) => {
|
|
126
|
+
const version = output.trim();
|
|
127
|
+
const ok = !timedOut && code === 0 && !signal && isValidReleaseVersion(version);
|
|
128
|
+
finish({
|
|
129
|
+
ok,
|
|
130
|
+
version: ok ? version : null,
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function restartCli(
|
|
137
|
+
binPath,
|
|
138
|
+
argv,
|
|
139
|
+
{ spawnProcess = spawn, env = process.env, signalSource = process } = {},
|
|
140
|
+
) {
|
|
141
|
+
return await new Promise((resolve, reject) => {
|
|
142
|
+
let parentSignal = null;
|
|
143
|
+
const child = spawnProcess(
|
|
144
|
+
process.execPath,
|
|
145
|
+
["--input-type=module", "--eval", RESTART_PROGRAM, entryUrl(binPath), binPath, ...argv],
|
|
146
|
+
{ env, shell: false, stdio: "inherit" },
|
|
147
|
+
);
|
|
148
|
+
const removeSignals = forwardSignals(signalSource, child, (signal) => {
|
|
149
|
+
parentSignal ??= signal;
|
|
150
|
+
});
|
|
151
|
+
child.on("error", (error) => {
|
|
152
|
+
removeSignals();
|
|
153
|
+
reject(error);
|
|
154
|
+
});
|
|
155
|
+
child.on("exit", (code, signal) => {
|
|
156
|
+
removeSignals();
|
|
157
|
+
const interruption = parentSignal ?? signal;
|
|
158
|
+
resolve(interruption ? 128 + (constants.signals[interruption] ?? 1) : (code ?? 1));
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function unexpectedVersion(stderr, version, currentVersion, target) {
|
|
164
|
+
const detail = isValidReleaseVersion(version)
|
|
165
|
+
? `The installer left ornn CLI at ${version}, expected ${target}.`
|
|
166
|
+
: "The installer left an invalid ornn CLI version.";
|
|
167
|
+
const downgrade = isValidReleaseVersion(version) && compareVersions(version, currentVersion) < 0;
|
|
168
|
+
warning(stderr, `${detail}${downgrade ? ` It is older than ${currentVersion}.` : ""} ${REINSTALL_GUIDANCE}`);
|
|
169
|
+
return { kind: "exit", code: 1 };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function prepareStartup(argv, options = {}) {
|
|
173
|
+
const env = options.env ?? process.env;
|
|
174
|
+
const stdin = options.stdin ?? process.stdin;
|
|
175
|
+
const stdout = options.stdout ?? process.stdout;
|
|
176
|
+
const stderr = options.stderr ?? process.stderr;
|
|
177
|
+
const binPath = options.binPath ?? process.argv[1];
|
|
178
|
+
const context = options.context ?? detectInstallContext(binPath, env);
|
|
179
|
+
|
|
180
|
+
if (!shouldAutoUpdate(argv, { env, stdin, stdout, stderr, context })) {
|
|
181
|
+
return { kind: "continue" };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let interruptedSignal = null;
|
|
185
|
+
const signalSource = options.signalSource ?? process;
|
|
186
|
+
const removeSignals = forwardSignals(signalSource, null, (signal) => {
|
|
187
|
+
interruptedSignal ??= signal;
|
|
188
|
+
});
|
|
189
|
+
const acquireLock = options.acquireLock ?? acquireUpdateLock;
|
|
190
|
+
const notifierFactory = options.notifierFactory ?? defaultNotifierFactory;
|
|
191
|
+
const install = options.install ?? installVersion;
|
|
192
|
+
const resolveBin = options.resolveBin ?? resolveInstalledBin;
|
|
193
|
+
const probe = options.probe ?? probeInstalledCli;
|
|
194
|
+
const subprocessOptions = {
|
|
195
|
+
env,
|
|
196
|
+
platform: options.platform ?? process.platform,
|
|
197
|
+
signalSource,
|
|
198
|
+
spawnProcess: options.spawnProcess ?? spawn,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
let release;
|
|
202
|
+
try {
|
|
203
|
+
try {
|
|
204
|
+
release = await acquireLock({ env, binPath: context.binPath ?? binPath });
|
|
205
|
+
} catch {}
|
|
206
|
+
if (interruptedSignal) return exitForSignal(interruptedSignal);
|
|
207
|
+
if (typeof release !== "function") return unavailableLock(stderr);
|
|
208
|
+
|
|
209
|
+
let target;
|
|
210
|
+
try {
|
|
211
|
+
const notifier = await notifierFactory({
|
|
212
|
+
pkg: { name: PACKAGE_NAME, version: options.currentVersion },
|
|
213
|
+
updateCheckInterval: UPDATE_CHECK_INTERVAL,
|
|
214
|
+
distTag: "latest",
|
|
215
|
+
});
|
|
216
|
+
target = notifier?.update?.latest;
|
|
217
|
+
} catch {}
|
|
218
|
+
if (interruptedSignal) return exitForSignal(interruptedSignal);
|
|
219
|
+
if (!isValidReleaseVersion(target) || compareVersions(target, options.currentVersion) <= 0) {
|
|
220
|
+
return { kind: "continue" };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
warning(stderr, `Updating ornn CLI ${options.currentVersion} -> ${target}...`);
|
|
224
|
+
let stableBin;
|
|
225
|
+
try {
|
|
226
|
+
stableBin = await resolveBin({ context, ...subprocessOptions });
|
|
227
|
+
} catch {}
|
|
228
|
+
if (interruptedSignal) return exitForSignal(interruptedSignal);
|
|
229
|
+
if (!stableBin) {
|
|
230
|
+
return failedProbe(stderr);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let installResult;
|
|
234
|
+
try {
|
|
235
|
+
installResult = await install({ context, version: target, ...subprocessOptions });
|
|
236
|
+
} catch {}
|
|
237
|
+
if (interruptedSignal || installResult?.signal) {
|
|
238
|
+
return exitForSignal(interruptedSignal ?? installResult.signal);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let replacementBin;
|
|
242
|
+
try {
|
|
243
|
+
replacementBin = await resolveBin({ context, ...subprocessOptions });
|
|
244
|
+
} catch {}
|
|
245
|
+
if (interruptedSignal) return exitForSignal(interruptedSignal);
|
|
246
|
+
if (!replacementBin) {
|
|
247
|
+
return failedProbe(stderr);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let result;
|
|
251
|
+
try {
|
|
252
|
+
result = await probe(replacementBin, subprocessOptions);
|
|
253
|
+
} catch {}
|
|
254
|
+
if (interruptedSignal) return exitForSignal(interruptedSignal);
|
|
255
|
+
if (!result?.ok) {
|
|
256
|
+
return failedProbe(stderr);
|
|
257
|
+
}
|
|
258
|
+
if (installResult?.exitCode !== 0) {
|
|
259
|
+
warning(stderr, "The automatic update failed; restarting the verified installed CLI.");
|
|
260
|
+
return { kind: "restart", binPath: replacementBin };
|
|
261
|
+
}
|
|
262
|
+
if (result.version !== target) {
|
|
263
|
+
return unexpectedVersion(stderr, result.version, options.currentVersion, target);
|
|
264
|
+
}
|
|
265
|
+
return { kind: "restart", binPath: replacementBin };
|
|
266
|
+
} finally {
|
|
267
|
+
removeSignals();
|
|
268
|
+
if (typeof release === "function") await Promise.resolve(release()).catch(() => {});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export async function bootstrap(argv = process.argv.slice(2), io = {}) {
|
|
273
|
+
const binPath = io.binPath ?? process.argv[1];
|
|
274
|
+
const env = io.env ?? process.env;
|
|
275
|
+
const result = await prepareStartup(argv, { ...io, binPath, currentVersion: VERSION, env });
|
|
276
|
+
if (result.kind === "exit") {
|
|
277
|
+
return result.code;
|
|
278
|
+
}
|
|
279
|
+
if (result.kind === "restart") {
|
|
280
|
+
return await restartCli(result.binPath, argv, {
|
|
281
|
+
env,
|
|
282
|
+
signalSource: io.signalSource ?? process,
|
|
283
|
+
spawnProcess: io.spawnProcess ?? spawn,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
const entry = await import("./command-entry.mjs");
|
|
287
|
+
return await entry.execute(argv, { ...io, binPath, env });
|
|
288
|
+
}
|
package/src/update.mjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
1
2
|
import { existsSync, realpathSync } from "node:fs";
|
|
2
|
-
import { mkdir,
|
|
3
|
+
import { mkdir, open, readFile, rm } from "node:fs/promises";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
|
-
import { dirname, isAbsolute, join } from "node:path";
|
|
5
|
+
import { dirname, isAbsolute, join, posix, win32 } from "node:path";
|
|
5
6
|
import { CliApiError } from "./api-client.mjs";
|
|
6
7
|
import { getAuthConfigDir } from "./auth-store.mjs";
|
|
8
|
+
import { forwardSignals } from "./signals.mjs";
|
|
7
9
|
|
|
8
|
-
export const UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
9
10
|
export const UPDATE_CHECK_TIMEOUT_MS = 3000;
|
|
10
|
-
const
|
|
11
|
+
const UPDATE_LOCK_PREFIX = "update-install";
|
|
11
12
|
const PACKAGE_NAME = "@ornncompute/cli";
|
|
12
13
|
const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
13
14
|
const INSTALLED_KINDS = new Set(["bun", "npm", "pnpm", "yarn"]);
|
|
@@ -19,6 +20,13 @@ const INSTALL_COMMANDS = {
|
|
|
19
20
|
yarn: (spec) => ["yarn", ["global", "add", spec]],
|
|
20
21
|
};
|
|
21
22
|
|
|
23
|
+
const GLOBAL_ROOT_COMMANDS = {
|
|
24
|
+
bun: ["pm", "ls", "-g"],
|
|
25
|
+
npm: ["root", "-g"],
|
|
26
|
+
pnpm: ["root", "-g"],
|
|
27
|
+
yarn: ["global", "dir"],
|
|
28
|
+
};
|
|
29
|
+
|
|
22
30
|
function parseVersion(value) {
|
|
23
31
|
if (typeof value !== "string") {
|
|
24
32
|
return null;
|
|
@@ -77,6 +85,10 @@ export function compareVersions(a, b) {
|
|
|
77
85
|
return 0;
|
|
78
86
|
}
|
|
79
87
|
|
|
88
|
+
export function isValidReleaseVersion(version) {
|
|
89
|
+
return typeof version === "string" && VERSION_PATTERN.test(version);
|
|
90
|
+
}
|
|
91
|
+
|
|
80
92
|
export function detectInstallContext(binPath, env = process.env) {
|
|
81
93
|
if (!binPath) {
|
|
82
94
|
return { kind: "unknown", binPath: null };
|
|
@@ -100,18 +112,30 @@ export function detectInstallContext(binPath, env = process.env) {
|
|
|
100
112
|
if (!has("node_modules")) {
|
|
101
113
|
return { kind: "dev", binPath: resolved };
|
|
102
114
|
}
|
|
103
|
-
if (
|
|
115
|
+
if (isBunGlobalInstall(lower, env)) {
|
|
104
116
|
return { kind: "bun", binPath: resolved };
|
|
105
117
|
}
|
|
106
|
-
if (
|
|
118
|
+
if (isPnpmGlobalInstall(lower, env)) {
|
|
107
119
|
return { kind: "pnpm", binPath: resolved };
|
|
108
120
|
}
|
|
109
|
-
if (
|
|
121
|
+
if (isYarnGlobalInstall(lower)) {
|
|
110
122
|
return { kind: "yarn", binPath: resolved };
|
|
111
123
|
}
|
|
124
|
+
if (isNpmGlobalInstall(lower, env)) {
|
|
125
|
+
return { kind: "npm", binPath: resolved };
|
|
126
|
+
}
|
|
112
127
|
if (isProjectInstall(resolved, lower)) {
|
|
113
128
|
return { kind: "project", binPath: resolved };
|
|
114
129
|
}
|
|
130
|
+
if (has(".bun") || isUnderDir(lower, env.BUN_INSTALL)) {
|
|
131
|
+
return { kind: "bun", binPath: resolved };
|
|
132
|
+
}
|
|
133
|
+
if (has(".pnpm") || has("pnpm") || isUnderDir(lower, env.PNPM_HOME)) {
|
|
134
|
+
return { kind: "pnpm", binPath: resolved };
|
|
135
|
+
}
|
|
136
|
+
if (has("yarn") || has(".yarn")) {
|
|
137
|
+
return { kind: "yarn", binPath: resolved };
|
|
138
|
+
}
|
|
115
139
|
return { kind: "npm", binPath: resolved };
|
|
116
140
|
}
|
|
117
141
|
|
|
@@ -136,114 +160,274 @@ function isUnderDir(lowerPath, dir) {
|
|
|
136
160
|
return lowerPath.startsWith(`${trimmed.replaceAll("\\", "/").toLowerCase().replace(/\/+$/, "")}/`);
|
|
137
161
|
}
|
|
138
162
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
163
|
+
function isBunGlobalInstall(lowerPath, env) {
|
|
164
|
+
return (
|
|
165
|
+
lowerPath.includes("/.bun/install/global/node_modules/") ||
|
|
166
|
+
(isUnderDir(lowerPath, env.BUN_INSTALL) && lowerPath.includes("/install/global/node_modules/"))
|
|
167
|
+
);
|
|
142
168
|
}
|
|
143
169
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
170
|
+
function isPnpmGlobalInstall(lowerPath, env) {
|
|
171
|
+
return /\/pnpm\/global\/\d+\/node_modules\//.test(lowerPath) || isUnderDir(lowerPath, env.PNPM_HOME);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isYarnGlobalInstall(lowerPath) {
|
|
175
|
+
return lowerPath.includes("/yarn/global/node_modules/") || lowerPath.includes("/yarn/data/global/node_modules/");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isNpmGlobalInstall(lowerPath, env) {
|
|
179
|
+
const configuredPrefix = env.npm_config_prefix?.trim();
|
|
180
|
+
if (configuredPrefix) {
|
|
181
|
+
let resolvedPrefix = configuredPrefix;
|
|
182
|
+
try {
|
|
183
|
+
resolvedPrefix = realpathSync(configuredPrefix);
|
|
184
|
+
} catch {}
|
|
185
|
+
const prefix = resolvedPrefix.replaceAll("\\", "/").toLowerCase().replace(/\/+$/, "");
|
|
186
|
+
if (lowerPath.startsWith(`${prefix}/node_modules/`) || lowerPath.startsWith(`${prefix}/lib/node_modules/`)) {
|
|
187
|
+
return true;
|
|
156
188
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (process.platform !== "win32") {
|
|
192
|
+
const executable = realpathSync(process.execPath).replaceAll("\\", "/").toLowerCase();
|
|
193
|
+
const suffix = "/bin/node";
|
|
194
|
+
if (executable.endsWith(suffix)) {
|
|
195
|
+
const prefix = executable.slice(0, -suffix.length);
|
|
196
|
+
return lowerPath.startsWith(`${prefix}/lib/node_modules/`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function pathApi(platform) {
|
|
203
|
+
return platform === "win32" ? win32 : posix;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function stableInstallRoot(binPath) {
|
|
207
|
+
if (!binPath) {
|
|
161
208
|
return null;
|
|
162
209
|
}
|
|
210
|
+
let resolved = binPath;
|
|
211
|
+
try {
|
|
212
|
+
resolved = realpathSync(binPath);
|
|
213
|
+
} catch {}
|
|
214
|
+
const normalized = resolved.replaceAll("\\", "/");
|
|
215
|
+
const markerIndex = normalized.toLowerCase().indexOf("/node_modules/");
|
|
216
|
+
return markerIndex > 0 ? normalized.slice(0, markerIndex) : null;
|
|
163
217
|
}
|
|
164
218
|
|
|
165
|
-
function
|
|
166
|
-
|
|
219
|
+
function pathIdentity(path) {
|
|
220
|
+
const normalized = path.replaceAll("\\", "/").replace(/\/+$/, "");
|
|
221
|
+
return /^[A-Za-z]:\//.test(normalized) ? normalized.toLowerCase() : normalized;
|
|
167
222
|
}
|
|
168
223
|
|
|
169
|
-
|
|
224
|
+
function canonicalPathIdentity(path) {
|
|
170
225
|
try {
|
|
171
|
-
|
|
172
|
-
return parsed && typeof parsed === "object" ? parsed : null;
|
|
226
|
+
return pathIdentity(realpathSync(path));
|
|
173
227
|
} catch {
|
|
228
|
+
return pathIdentity(path);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function updateLockPath(env, binPath) {
|
|
233
|
+
const configDir = getAuthConfigDir(env);
|
|
234
|
+
const installRoot = stableInstallRoot(binPath);
|
|
235
|
+
if (!installRoot || !isAbsolute(configDir)) {
|
|
174
236
|
return null;
|
|
175
237
|
}
|
|
238
|
+
const key = createHash("sha256").update(pathIdentity(installRoot)).digest("hex").slice(0, 24);
|
|
239
|
+
return join(configDir, `${UPDATE_LOCK_PREFIX}-${key}.lock`);
|
|
176
240
|
}
|
|
177
241
|
|
|
178
|
-
async function
|
|
179
|
-
|
|
180
|
-
|
|
242
|
+
export async function acquireUpdateLock({ env = process.env, binPath }) {
|
|
243
|
+
const lockPath = updateLockPath(env, binPath);
|
|
244
|
+
if (!lockPath) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let handle;
|
|
181
249
|
try {
|
|
182
|
-
await mkdir(dirname(
|
|
183
|
-
await
|
|
184
|
-
await rename(tmpPath, path);
|
|
185
|
-
return true;
|
|
250
|
+
await mkdir(dirname(lockPath), { mode: 0o700, recursive: true });
|
|
251
|
+
handle = await open(lockPath, "wx", 0o600);
|
|
186
252
|
} catch {
|
|
187
|
-
|
|
188
|
-
return false;
|
|
253
|
+
return null;
|
|
189
254
|
}
|
|
190
|
-
}
|
|
191
255
|
|
|
192
|
-
|
|
193
|
-
binPath,
|
|
194
|
-
currentVersion,
|
|
195
|
-
env = process.env,
|
|
196
|
-
fetchImpl = fetch,
|
|
197
|
-
now = Date.now(),
|
|
198
|
-
stderr,
|
|
199
|
-
}) {
|
|
256
|
+
const token = randomUUID();
|
|
200
257
|
try {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (
|
|
258
|
+
await handle.writeFile(`${JSON.stringify({ pid: process.pid, token })}\n`, "utf8");
|
|
259
|
+
await handle.close();
|
|
260
|
+
} catch {
|
|
261
|
+
await handle.close().catch(() => {});
|
|
262
|
+
await rm(lockPath, { force: true }).catch(() => {});
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let released = false;
|
|
267
|
+
return async () => {
|
|
268
|
+
if (released) {
|
|
212
269
|
return;
|
|
213
270
|
}
|
|
271
|
+
released = true;
|
|
272
|
+
try {
|
|
273
|
+
const current = JSON.parse(await readFile(lockPath, "utf8"));
|
|
274
|
+
if (current?.token === token) {
|
|
275
|
+
await rm(lockPath);
|
|
276
|
+
}
|
|
277
|
+
} catch {}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
214
280
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
281
|
+
function installInvocation(context, version) {
|
|
282
|
+
if (version !== "latest" && !isValidReleaseVersion(version)) {
|
|
283
|
+
throw new CliApiError("The update target must be a valid release version.");
|
|
284
|
+
}
|
|
285
|
+
const commandFactory = INSTALL_COMMANDS[context?.kind];
|
|
286
|
+
if (!commandFactory) {
|
|
287
|
+
throw new CliApiError("Could not determine which package manager should install the update.");
|
|
288
|
+
}
|
|
289
|
+
return commandFactory(`${PACKAGE_NAME}@${version}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export async function installVersion({
|
|
293
|
+
context,
|
|
294
|
+
version,
|
|
295
|
+
spawnProcess,
|
|
296
|
+
platform = process.platform,
|
|
297
|
+
env = process.env,
|
|
298
|
+
signalSource = process,
|
|
299
|
+
}) {
|
|
300
|
+
const [command, args] = installInvocation(context, version);
|
|
301
|
+
return await new Promise((resolve, reject) => {
|
|
302
|
+
let parentSignal = null;
|
|
303
|
+
let settled = false;
|
|
304
|
+
const child = spawnProcess(command, args, {
|
|
305
|
+
env,
|
|
306
|
+
shell: platform === "win32",
|
|
307
|
+
stdio: "inherit",
|
|
308
|
+
});
|
|
309
|
+
const cleanup = forwardSignals(signalSource, child, (signal) => {
|
|
310
|
+
parentSignal ??= signal;
|
|
311
|
+
});
|
|
312
|
+
child.on("error", (error) => {
|
|
313
|
+
if (settled) return;
|
|
314
|
+
settled = true;
|
|
315
|
+
cleanup();
|
|
316
|
+
reject(
|
|
317
|
+
new CliApiError(
|
|
318
|
+
error?.code === "ENOENT"
|
|
319
|
+
? `${command} was not found on PATH. Install it or run: npm install -g ${PACKAGE_NAME}@latest`
|
|
320
|
+
: `Failed to run ${command}: ${error?.message ?? error}`,
|
|
321
|
+
),
|
|
322
|
+
);
|
|
323
|
+
});
|
|
324
|
+
child.on("exit", (code, signal) => {
|
|
325
|
+
if (settled) return;
|
|
326
|
+
settled = true;
|
|
327
|
+
cleanup();
|
|
328
|
+
resolve({
|
|
329
|
+
exitCode: parentSignal || signal ? 1 : (code ?? 0),
|
|
330
|
+
signal: parentSignal ?? signal ?? null,
|
|
228
331
|
});
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function resolveGlobalRoot({ kind, spawnProcess, env, platform, signalSource }) {
|
|
337
|
+
return await new Promise((resolve) => {
|
|
338
|
+
let output = "";
|
|
339
|
+
let interrupted = false;
|
|
340
|
+
const child = spawnProcess(kind, GLOBAL_ROOT_COMMANDS[kind], {
|
|
341
|
+
env,
|
|
342
|
+
shell: platform === "win32",
|
|
343
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
344
|
+
});
|
|
345
|
+
const cleanup = forwardSignals(signalSource, child, () => {
|
|
346
|
+
interrupted = true;
|
|
347
|
+
});
|
|
348
|
+
child.stdout?.on("data", (chunk) => {
|
|
349
|
+
output += String(chunk);
|
|
350
|
+
});
|
|
351
|
+
child.on("error", () => {
|
|
352
|
+
cleanup();
|
|
353
|
+
resolve(null);
|
|
354
|
+
});
|
|
355
|
+
child.on("exit", (code, signal) => {
|
|
356
|
+
cleanup();
|
|
357
|
+
resolve(code === 0 && !signal && !interrupted ? output.trim() || null : null);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
}
|
|
241
361
|
|
|
242
|
-
|
|
243
|
-
|
|
362
|
+
export async function resolveInstalledBin({
|
|
363
|
+
context,
|
|
364
|
+
spawnProcess,
|
|
365
|
+
env = process.env,
|
|
366
|
+
platform = process.platform,
|
|
367
|
+
signalSource = process,
|
|
368
|
+
}) {
|
|
369
|
+
if (!INSTALLED_KINDS.has(context?.kind)) {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
const installRoot = stableInstallRoot(context.binPath);
|
|
373
|
+
if (!installRoot) {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const paths = pathApi(platform);
|
|
378
|
+
const packageRoot = paths.join(installRoot, "node_modules");
|
|
379
|
+
const output = await resolveGlobalRoot({
|
|
380
|
+
kind: context.kind,
|
|
381
|
+
spawnProcess,
|
|
382
|
+
env,
|
|
383
|
+
platform,
|
|
384
|
+
signalSource,
|
|
385
|
+
});
|
|
386
|
+
let configuredRoot = output;
|
|
387
|
+
if (context.kind === "bun") {
|
|
388
|
+
// Bun lists the effective global directory before its dependency tree.
|
|
389
|
+
configuredRoot = /^(.+) node_modules \(\d+(?: installed)?\)$/.exec(output?.split(/\r?\n/)[0] ?? "")?.[1];
|
|
390
|
+
}
|
|
391
|
+
if (!configuredRoot || !paths.isAbsolute(configuredRoot)) {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
if (context.kind === "bun" || context.kind === "yarn") {
|
|
395
|
+
configuredRoot = paths.join(configuredRoot, "node_modules");
|
|
396
|
+
}
|
|
397
|
+
if (canonicalPathIdentity(configuredRoot) !== canonicalPathIdentity(packageRoot)) {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const candidate = paths.join(packageRoot, "@ornncompute", "cli", "bin", "ornn.js");
|
|
402
|
+
if (!paths.isAbsolute(candidate) || !existsSync(candidate)) {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
return candidate;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function resolveUpdateRegistry(env = process.env) {
|
|
409
|
+
const registry = env.npm_config_registry?.trim() || "https://registry.npmjs.org";
|
|
410
|
+
return registry.replace(/\/+$/, "");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export async function fetchLatestVersion({
|
|
414
|
+
env = process.env,
|
|
415
|
+
fetchImpl = fetch,
|
|
416
|
+
timeoutMs = UPDATE_CHECK_TIMEOUT_MS,
|
|
417
|
+
} = {}) {
|
|
418
|
+
try {
|
|
419
|
+
const response = await fetchImpl(`${resolveUpdateRegistry(env)}/${PACKAGE_NAME}/latest`, {
|
|
420
|
+
headers: { Accept: "application/json" },
|
|
421
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
422
|
+
});
|
|
423
|
+
if (!response.ok) {
|
|
424
|
+
return null;
|
|
244
425
|
}
|
|
426
|
+
const payload = await response.json();
|
|
427
|
+
const version = payload?.version;
|
|
428
|
+
return typeof version === "string" && VERSION_PATTERN.test(version) ? version : null;
|
|
245
429
|
} catch {
|
|
246
|
-
|
|
430
|
+
return null;
|
|
247
431
|
}
|
|
248
432
|
}
|
|
249
433
|
|
|
@@ -265,95 +449,91 @@ export async function updateCommand({
|
|
|
265
449
|
throw new CliApiError("ornn is running from a development checkout; update it with git pull.");
|
|
266
450
|
}
|
|
267
451
|
if (context.kind === "project") {
|
|
268
|
-
throw new CliApiError(
|
|
452
|
+
throw new CliApiError(
|
|
453
|
+
"ornn is installed as a project dependency; update it there with: npm update @ornncompute/cli",
|
|
454
|
+
);
|
|
269
455
|
}
|
|
270
456
|
if (context.kind === "unknown") {
|
|
271
|
-
throw new CliApiError(
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const latest = await fetchLatestVersion({ env, fetchImpl });
|
|
275
|
-
if (latest && compareVersions(latest, currentVersion) <= 0) {
|
|
276
|
-
await writeUpdateCache(updateCachePath(env), {
|
|
277
|
-
lastCheckedAt: new Date().toISOString(),
|
|
278
|
-
latestVersion: latest,
|
|
279
|
-
});
|
|
280
|
-
stdout.write(`ornn CLI is already up to date (${currentVersion}).\n`);
|
|
281
|
-
return 0;
|
|
282
|
-
}
|
|
283
|
-
if (!latest) {
|
|
284
|
-
stderr.write("Could not check the npm registry for the latest version; attempting the update anyway.\n");
|
|
457
|
+
throw new CliApiError(
|
|
458
|
+
"Could not determine how ornn was installed. Reinstall with: npm install -g @ornncompute/cli@latest",
|
|
459
|
+
);
|
|
285
460
|
}
|
|
286
461
|
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const exitCode = await new Promise((resolve, reject) => {
|
|
291
|
-
const child = spawnProcess(command, commandArgs, {
|
|
292
|
-
shell: platform === "win32",
|
|
293
|
-
stdio: "inherit",
|
|
294
|
-
});
|
|
295
|
-
child.on("error", (error) =>
|
|
296
|
-
reject(
|
|
297
|
-
new CliApiError(
|
|
298
|
-
error?.code === "ENOENT"
|
|
299
|
-
? `${command} was not found on PATH. Install it or run: npm install -g ${PACKAGE_NAME}@latest`
|
|
300
|
-
: `Failed to run ${command}: ${error?.message ?? error}`,
|
|
301
|
-
),
|
|
302
|
-
),
|
|
303
|
-
);
|
|
304
|
-
child.on("exit", (code, signal) => resolve(signal ? 1 : code ?? 0));
|
|
305
|
-
});
|
|
306
|
-
if (exitCode !== 0) {
|
|
462
|
+
const releaseLock = await acquireUpdateLock({ env, binPath: context.binPath });
|
|
463
|
+
if (!releaseLock) {
|
|
464
|
+
const lockPath = updateLockPath(env, context.binPath);
|
|
307
465
|
throw new CliApiError(
|
|
308
|
-
|
|
466
|
+
`Could not acquire the update lock. If another ornn update is running, wait for it to finish and retry.${
|
|
467
|
+
lockPath
|
|
468
|
+
? ` Otherwise remove the stale lock at ${lockPath} and retry.`
|
|
469
|
+
: " Check the config directory permissions and retry."
|
|
470
|
+
}`,
|
|
309
471
|
);
|
|
310
472
|
}
|
|
311
473
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
474
|
+
try {
|
|
475
|
+
const installedBin = await resolveInstalledBin({ context, spawnProcess, env, platform });
|
|
476
|
+
if (!installedBin) {
|
|
477
|
+
throw new CliApiError(
|
|
478
|
+
`Could not verify the ${context.kind} global installation root. No files were changed; reinstall with: npm install -g ${PACKAGE_NAME}@latest`,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
315
481
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
|
|
482
|
+
const latest = await fetchLatestVersion({ env, fetchImpl });
|
|
483
|
+
if (latest && compareVersions(latest, currentVersion) <= 0) {
|
|
484
|
+
stdout.write(`ornn CLI is already up to date (${currentVersion}).\n`);
|
|
485
|
+
return 0;
|
|
486
|
+
}
|
|
487
|
+
if (!latest) {
|
|
488
|
+
stderr.write("Could not check the npm registry for the latest version; attempting the update anyway.\n");
|
|
322
489
|
}
|
|
323
|
-
await writeUpdateCache(updateCachePath(env), {
|
|
324
|
-
lastCheckedAt: new Date().toISOString(),
|
|
325
|
-
latestVersion: latest ?? null,
|
|
326
|
-
});
|
|
327
|
-
return 0;
|
|
328
|
-
}
|
|
329
490
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
491
|
+
const version = latest ?? "latest";
|
|
492
|
+
const [command, commandArgs] = installInvocation(context, version);
|
|
493
|
+
stderr.write(`Running: ${command} ${commandArgs.join(" ")}\n`);
|
|
494
|
+
const result = await installVersion({ context, version, spawnProcess, platform, env });
|
|
495
|
+
if (result.exitCode !== 0) {
|
|
496
|
+
throw new CliApiError(
|
|
497
|
+
updateFailureMessage({
|
|
498
|
+
binPath: context.binPath,
|
|
499
|
+
command,
|
|
500
|
+
commandArgs,
|
|
501
|
+
exitCode: result.exitCode,
|
|
502
|
+
kind: context.kind,
|
|
503
|
+
platform,
|
|
504
|
+
}),
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (platform === "win32") {
|
|
509
|
+
stderr.write("If this terminal reports an error after updating, it is safe to ignore; open a new terminal.\n");
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const installedVersion = await readInstalledVersion(installedBin);
|
|
513
|
+
if (!installedVersion) {
|
|
514
|
+
stdout.write("Update finished. Run `ornn --version` to confirm the installed version.\n");
|
|
515
|
+
} else if (installedVersion === currentVersion && latest) {
|
|
516
|
+
stderr.write(
|
|
517
|
+
`The installer exited successfully, but this ornn still runs ${currentVersion} from ${context.binPath}. Your registry mirror may be behind, or another install shadows this one; check your PATH.\n`,
|
|
518
|
+
);
|
|
519
|
+
return 1;
|
|
520
|
+
} else if (installedVersion === currentVersion) {
|
|
521
|
+
stdout.write(`ornn CLI is still ${currentVersion}; the registry may not have a newer version.\n`);
|
|
522
|
+
} else if (compareVersions(installedVersion, currentVersion) < 0) {
|
|
523
|
+
stderr.write(
|
|
524
|
+
`The installer replaced ornn CLI ${currentVersion} with the older ${installedVersion}; your registry mirror may be behind. Reinstall with: npm install -g ${PACKAGE_NAME}@${currentVersion}\n`,
|
|
525
|
+
);
|
|
526
|
+
return 1;
|
|
527
|
+
} else {
|
|
528
|
+
stdout.write(`Updated ornn CLI ${currentVersion} -> ${installedVersion}.\n`);
|
|
529
|
+
if (latest && compareVersions(installedVersion, latest) < 0) {
|
|
530
|
+
stderr.write(`Note: ${latest} is the latest on the registry; your mirror may be behind.\n`);
|
|
531
|
+
}
|
|
350
532
|
}
|
|
533
|
+
return 0;
|
|
534
|
+
} finally {
|
|
535
|
+
await releaseLock();
|
|
351
536
|
}
|
|
352
|
-
await writeUpdateCache(updateCachePath(env), {
|
|
353
|
-
lastCheckedAt: new Date().toISOString(),
|
|
354
|
-
latestVersion: latest ?? installedVersion ?? null,
|
|
355
|
-
});
|
|
356
|
-
return 0;
|
|
357
537
|
}
|
|
358
538
|
|
|
359
539
|
function updateFailureMessage({ binPath, command, commandArgs, exitCode, kind, platform }) {
|