@ornncompute/cli 0.2.11 → 0.2.13
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 +26 -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 +20 -0
- package/src/startup.mjs +291 -0
- package/src/update-lock.mjs +100 -0
- package/src/update.mjs +314 -167
package/README.md
CHANGED
|
@@ -357,7 +357,32 @@ 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.
|
|
373
|
+
|
|
374
|
+
Updates publish an initialized `update-install-*.lock` directory in the Ornn
|
|
375
|
+
config directory. Its unique owner file prevents competing processes from
|
|
376
|
+
removing a replacement lock during recovery or release. Directory locks from
|
|
377
|
+
processes that exit before installation are reclaimed on the next update
|
|
378
|
+
attempt; live or inaccessible PIDs and malformed metadata remain locked. Legacy regular-file locks require
|
|
379
|
+
manual cleanup after confirming that no updater is running: deleting them
|
|
380
|
+
automatically could remove a live lock created by an older CLI. PID reuse can
|
|
381
|
+
also require manual cleanup. An installation marker keeps the lock held if the
|
|
382
|
+
CLI is killed during installation, because its package installer may still be
|
|
383
|
+
running. Confirm that installer has stopped before removing the lock. An
|
|
384
|
+
interrupted package installation may still require reinstalling the CLI if the package
|
|
385
|
+
manager has already removed the previous version.
|
|
361
386
|
|
|
362
387
|
## Deployment
|
|
363
388
|
|
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.13",
|
|
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,20 @@
|
|
|
1
|
+
const FORWARDABLE_SIGNALS =
|
|
2
|
+
process.platform === "win32" ? ["SIGINT", "SIGTERM"] : ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
3
|
+
|
|
4
|
+
export function forwardSignals(signalSource, child, onSignal) {
|
|
5
|
+
const listeners = FORWARDABLE_SIGNALS.map((signal) => {
|
|
6
|
+
const listener = () => {
|
|
7
|
+
onSignal(signal);
|
|
8
|
+
try {
|
|
9
|
+
child?.kill?.(signal);
|
|
10
|
+
} catch {}
|
|
11
|
+
};
|
|
12
|
+
signalSource.once(signal, listener);
|
|
13
|
+
return [signal, listener];
|
|
14
|
+
});
|
|
15
|
+
return () => {
|
|
16
|
+
for (const [signal, listener] of listeners.splice(0)) {
|
|
17
|
+
signalSource.off?.(signal, listener) ?? signalSource.removeListener?.(signal, listener);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
package/src/startup.mjs
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
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 no updater or package installer 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 lock;
|
|
202
|
+
try {
|
|
203
|
+
try {
|
|
204
|
+
lock = await acquireLock({ env, binPath: context.binPath ?? binPath });
|
|
205
|
+
} catch {}
|
|
206
|
+
if (interruptedSignal) return exitForSignal(interruptedSignal);
|
|
207
|
+
if (!lock) 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 lock.runInstall(() => {
|
|
236
|
+
if (interruptedSignal) return { signal: interruptedSignal };
|
|
237
|
+
return install({ context, version: target, ...subprocessOptions });
|
|
238
|
+
});
|
|
239
|
+
} catch {}
|
|
240
|
+
if (interruptedSignal || installResult?.signal) {
|
|
241
|
+
return exitForSignal(interruptedSignal ?? installResult.signal);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let replacementBin;
|
|
245
|
+
try {
|
|
246
|
+
replacementBin = await resolveBin({ context, ...subprocessOptions });
|
|
247
|
+
} catch {}
|
|
248
|
+
if (interruptedSignal) return exitForSignal(interruptedSignal);
|
|
249
|
+
if (!replacementBin) {
|
|
250
|
+
return failedProbe(stderr);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
let result;
|
|
254
|
+
try {
|
|
255
|
+
result = await probe(replacementBin, subprocessOptions);
|
|
256
|
+
} catch {}
|
|
257
|
+
if (interruptedSignal) return exitForSignal(interruptedSignal);
|
|
258
|
+
if (!result?.ok) {
|
|
259
|
+
return failedProbe(stderr);
|
|
260
|
+
}
|
|
261
|
+
if (installResult?.exitCode !== 0) {
|
|
262
|
+
warning(stderr, "The automatic update failed; restarting the verified installed CLI.");
|
|
263
|
+
return { kind: "restart", binPath: replacementBin };
|
|
264
|
+
}
|
|
265
|
+
if (result.version !== target) {
|
|
266
|
+
return unexpectedVersion(stderr, result.version, options.currentVersion, target);
|
|
267
|
+
}
|
|
268
|
+
return { kind: "restart", binPath: replacementBin };
|
|
269
|
+
} finally {
|
|
270
|
+
removeSignals();
|
|
271
|
+
if (lock) await Promise.resolve(lock.release()).catch(() => {});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export async function bootstrap(argv = process.argv.slice(2), io = {}) {
|
|
276
|
+
const binPath = io.binPath ?? process.argv[1];
|
|
277
|
+
const env = io.env ?? process.env;
|
|
278
|
+
const result = await prepareStartup(argv, { ...io, binPath, currentVersion: VERSION, env });
|
|
279
|
+
if (result.kind === "exit") {
|
|
280
|
+
return result.code;
|
|
281
|
+
}
|
|
282
|
+
if (result.kind === "restart") {
|
|
283
|
+
return await restartCli(result.binPath, argv, {
|
|
284
|
+
env,
|
|
285
|
+
signalSource: io.signalSource ?? process,
|
|
286
|
+
spawnProcess: io.spawnProcess ?? spawn,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
const entry = await import("./command-entry.mjs");
|
|
290
|
+
return await entry.execute(argv, { ...io, binPath, env });
|
|
291
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
mkdir,
|
|
4
|
+
mkdtemp,
|
|
5
|
+
readFile,
|
|
6
|
+
readdir,
|
|
7
|
+
rename,
|
|
8
|
+
rm,
|
|
9
|
+
rmdir,
|
|
10
|
+
unlink,
|
|
11
|
+
writeFile,
|
|
12
|
+
} from "node:fs/promises";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
|
|
15
|
+
async function isStale(path) {
|
|
16
|
+
let pid;
|
|
17
|
+
try {
|
|
18
|
+
({ pid } = JSON.parse(await readFile(path, "utf8")));
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
23
|
+
try {
|
|
24
|
+
process.kill(pid, 0);
|
|
25
|
+
return false;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return error.code === "ESRCH";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function removeOwner(lockPath, owner) {
|
|
32
|
+
await unlink(join(lockPath, owner)).catch(() => {});
|
|
33
|
+
// A replacement has a different owner file, so rmdir cannot remove it.
|
|
34
|
+
await rmdir(lockPath).catch(() => {});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function reclaim(lockPath) {
|
|
38
|
+
let owners;
|
|
39
|
+
try {
|
|
40
|
+
owners = await readdir(lockPath);
|
|
41
|
+
} catch {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (owners.length === 0) {
|
|
45
|
+
await rmdir(lockPath).catch(() => {});
|
|
46
|
+
} else if (owners.length === 1 && (await isStale(join(lockPath, owners[0])))) {
|
|
47
|
+
await removeOwner(lockPath, owners[0]);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function acquireFileLock(lockPath) {
|
|
52
|
+
let pending;
|
|
53
|
+
const owner = randomUUID();
|
|
54
|
+
try {
|
|
55
|
+
await mkdir(dirname(lockPath), { mode: 0o700, recursive: true });
|
|
56
|
+
pending = await mkdtemp(`${lockPath}-`);
|
|
57
|
+
await writeFile(
|
|
58
|
+
join(pending, owner),
|
|
59
|
+
`${JSON.stringify({ pid: process.pid })}\n`,
|
|
60
|
+
{
|
|
61
|
+
mode: 0o600,
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
65
|
+
try {
|
|
66
|
+
// Publish metadata atomically; an occupied nonempty directory cannot be replaced.
|
|
67
|
+
await rename(pending, lockPath);
|
|
68
|
+
pending = null;
|
|
69
|
+
let released = false;
|
|
70
|
+
return {
|
|
71
|
+
async release() {
|
|
72
|
+
if (released) return;
|
|
73
|
+
released = true;
|
|
74
|
+
await removeOwner(lockPath, owner);
|
|
75
|
+
},
|
|
76
|
+
async runInstall(install) {
|
|
77
|
+
if (released) throw new Error("The update lock has already been released.");
|
|
78
|
+
const marker = join(lockPath, `${randomUUID()}.install`);
|
|
79
|
+
// A killed coordinator cannot prove its installer exited. Keep that lock held.
|
|
80
|
+
try {
|
|
81
|
+
await writeFile(marker, "", { mode: 0o600, flag: "wx" });
|
|
82
|
+
return await install();
|
|
83
|
+
} finally {
|
|
84
|
+
await unlink(marker).catch(() => {});
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (!["EEXIST", "ENOTEMPTY", "ENOTDIR", "EISDIR", "EPERM"].includes(error.code))
|
|
90
|
+
return null;
|
|
91
|
+
if (attempt === 0) await reclaim(lockPath);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
} finally {
|
|
97
|
+
if (pending) await rm(pending, { recursive: true, force: true }).catch(() => {});
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
package/src/update.mjs
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { existsSync, realpathSync } from "node:fs";
|
|
2
|
-
import {
|
|
3
|
+
import { readFile } 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";
|
|
9
|
+
import { acquireFileLock } from "./update-lock.mjs";
|
|
7
10
|
|
|
8
|
-
export const UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
9
11
|
export const UPDATE_CHECK_TIMEOUT_MS = 3000;
|
|
10
|
-
const
|
|
12
|
+
const UPDATE_LOCK_PREFIX = "update-install";
|
|
11
13
|
const PACKAGE_NAME = "@ornncompute/cli";
|
|
12
14
|
const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
13
15
|
const INSTALLED_KINDS = new Set(["bun", "npm", "pnpm", "yarn"]);
|
|
@@ -19,6 +21,13 @@ const INSTALL_COMMANDS = {
|
|
|
19
21
|
yarn: (spec) => ["yarn", ["global", "add", spec]],
|
|
20
22
|
};
|
|
21
23
|
|
|
24
|
+
const GLOBAL_ROOT_COMMANDS = {
|
|
25
|
+
bun: ["pm", "ls", "-g"],
|
|
26
|
+
npm: ["root", "-g"],
|
|
27
|
+
pnpm: ["root", "-g"],
|
|
28
|
+
yarn: ["global", "dir"],
|
|
29
|
+
};
|
|
30
|
+
|
|
22
31
|
function parseVersion(value) {
|
|
23
32
|
if (typeof value !== "string") {
|
|
24
33
|
return null;
|
|
@@ -77,6 +86,10 @@ export function compareVersions(a, b) {
|
|
|
77
86
|
return 0;
|
|
78
87
|
}
|
|
79
88
|
|
|
89
|
+
export function isValidReleaseVersion(version) {
|
|
90
|
+
return typeof version === "string" && VERSION_PATTERN.test(version);
|
|
91
|
+
}
|
|
92
|
+
|
|
80
93
|
export function detectInstallContext(binPath, env = process.env) {
|
|
81
94
|
if (!binPath) {
|
|
82
95
|
return { kind: "unknown", binPath: null };
|
|
@@ -100,18 +113,30 @@ export function detectInstallContext(binPath, env = process.env) {
|
|
|
100
113
|
if (!has("node_modules")) {
|
|
101
114
|
return { kind: "dev", binPath: resolved };
|
|
102
115
|
}
|
|
103
|
-
if (
|
|
116
|
+
if (isBunGlobalInstall(lower, env)) {
|
|
104
117
|
return { kind: "bun", binPath: resolved };
|
|
105
118
|
}
|
|
106
|
-
if (
|
|
119
|
+
if (isPnpmGlobalInstall(lower, env)) {
|
|
107
120
|
return { kind: "pnpm", binPath: resolved };
|
|
108
121
|
}
|
|
109
|
-
if (
|
|
122
|
+
if (isYarnGlobalInstall(lower)) {
|
|
110
123
|
return { kind: "yarn", binPath: resolved };
|
|
111
124
|
}
|
|
125
|
+
if (isNpmGlobalInstall(lower, env)) {
|
|
126
|
+
return { kind: "npm", binPath: resolved };
|
|
127
|
+
}
|
|
112
128
|
if (isProjectInstall(resolved, lower)) {
|
|
113
129
|
return { kind: "project", binPath: resolved };
|
|
114
130
|
}
|
|
131
|
+
if (has(".bun") || isUnderDir(lower, env.BUN_INSTALL)) {
|
|
132
|
+
return { kind: "bun", binPath: resolved };
|
|
133
|
+
}
|
|
134
|
+
if (has(".pnpm") || has("pnpm") || isUnderDir(lower, env.PNPM_HOME)) {
|
|
135
|
+
return { kind: "pnpm", binPath: resolved };
|
|
136
|
+
}
|
|
137
|
+
if (has("yarn") || has(".yarn")) {
|
|
138
|
+
return { kind: "yarn", binPath: resolved };
|
|
139
|
+
}
|
|
115
140
|
return { kind: "npm", binPath: resolved };
|
|
116
141
|
}
|
|
117
142
|
|
|
@@ -136,114 +161,240 @@ function isUnderDir(lowerPath, dir) {
|
|
|
136
161
|
return lowerPath.startsWith(`${trimmed.replaceAll("\\", "/").toLowerCase().replace(/\/+$/, "")}/`);
|
|
137
162
|
}
|
|
138
163
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
164
|
+
function isBunGlobalInstall(lowerPath, env) {
|
|
165
|
+
return (
|
|
166
|
+
lowerPath.includes("/.bun/install/global/node_modules/") ||
|
|
167
|
+
(isUnderDir(lowerPath, env.BUN_INSTALL) && lowerPath.includes("/install/global/node_modules/"))
|
|
168
|
+
);
|
|
142
169
|
}
|
|
143
170
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
171
|
+
function isPnpmGlobalInstall(lowerPath, env) {
|
|
172
|
+
return /\/pnpm\/global\/\d+\/node_modules\//.test(lowerPath) || isUnderDir(lowerPath, env.PNPM_HOME);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function isYarnGlobalInstall(lowerPath) {
|
|
176
|
+
return lowerPath.includes("/yarn/global/node_modules/") || lowerPath.includes("/yarn/data/global/node_modules/");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function isNpmGlobalInstall(lowerPath, env) {
|
|
180
|
+
const configuredPrefix = env.npm_config_prefix?.trim();
|
|
181
|
+
if (configuredPrefix) {
|
|
182
|
+
let resolvedPrefix = configuredPrefix;
|
|
183
|
+
try {
|
|
184
|
+
resolvedPrefix = realpathSync(configuredPrefix);
|
|
185
|
+
} catch {}
|
|
186
|
+
const prefix = resolvedPrefix.replaceAll("\\", "/").toLowerCase().replace(/\/+$/, "");
|
|
187
|
+
if (lowerPath.startsWith(`${prefix}/node_modules/`) || lowerPath.startsWith(`${prefix}/lib/node_modules/`)) {
|
|
188
|
+
return true;
|
|
156
189
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (process.platform !== "win32") {
|
|
193
|
+
const executable = realpathSync(process.execPath).replaceAll("\\", "/").toLowerCase();
|
|
194
|
+
const suffix = "/bin/node";
|
|
195
|
+
if (executable.endsWith(suffix)) {
|
|
196
|
+
const prefix = executable.slice(0, -suffix.length);
|
|
197
|
+
return lowerPath.startsWith(`${prefix}/lib/node_modules/`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function pathApi(platform) {
|
|
204
|
+
return platform === "win32" ? win32 : posix;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function stableInstallRoot(binPath) {
|
|
208
|
+
if (!binPath) {
|
|
161
209
|
return null;
|
|
162
210
|
}
|
|
211
|
+
let resolved = binPath;
|
|
212
|
+
try {
|
|
213
|
+
resolved = realpathSync(binPath);
|
|
214
|
+
} catch {}
|
|
215
|
+
const normalized = resolved.replaceAll("\\", "/");
|
|
216
|
+
const markerIndex = normalized.toLowerCase().indexOf("/node_modules/");
|
|
217
|
+
return markerIndex > 0 ? normalized.slice(0, markerIndex) : null;
|
|
163
218
|
}
|
|
164
219
|
|
|
165
|
-
function
|
|
166
|
-
|
|
220
|
+
function pathIdentity(path) {
|
|
221
|
+
const normalized = path.replaceAll("\\", "/").replace(/\/+$/, "");
|
|
222
|
+
return /^[A-Za-z]:\//.test(normalized) ? normalized.toLowerCase() : normalized;
|
|
167
223
|
}
|
|
168
224
|
|
|
169
|
-
|
|
225
|
+
function canonicalPathIdentity(path) {
|
|
170
226
|
try {
|
|
171
|
-
|
|
172
|
-
return parsed && typeof parsed === "object" ? parsed : null;
|
|
227
|
+
return pathIdentity(realpathSync(path));
|
|
173
228
|
} catch {
|
|
229
|
+
return pathIdentity(path);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function updateLockPath(env, binPath) {
|
|
234
|
+
const configDir = getAuthConfigDir(env);
|
|
235
|
+
const installRoot = stableInstallRoot(binPath);
|
|
236
|
+
if (!installRoot || !isAbsolute(configDir)) {
|
|
174
237
|
return null;
|
|
175
238
|
}
|
|
239
|
+
const key = createHash("sha256").update(pathIdentity(installRoot)).digest("hex").slice(0, 24);
|
|
240
|
+
return join(configDir, `${UPDATE_LOCK_PREFIX}-${key}.lock`);
|
|
176
241
|
}
|
|
177
242
|
|
|
178
|
-
async function
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
243
|
+
export async function acquireUpdateLock({ env = process.env, binPath }) {
|
|
244
|
+
const lockPath = updateLockPath(env, binPath);
|
|
245
|
+
return lockPath ? acquireFileLock(lockPath) : null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function installInvocation(context, version) {
|
|
249
|
+
if (version !== "latest" && !isValidReleaseVersion(version)) {
|
|
250
|
+
throw new CliApiError("The update target must be a valid release version.");
|
|
251
|
+
}
|
|
252
|
+
const commandFactory = INSTALL_COMMANDS[context?.kind];
|
|
253
|
+
if (!commandFactory) {
|
|
254
|
+
throw new CliApiError("Could not determine which package manager should install the update.");
|
|
189
255
|
}
|
|
256
|
+
return commandFactory(`${PACKAGE_NAME}@${version}`);
|
|
190
257
|
}
|
|
191
258
|
|
|
192
|
-
export async function
|
|
193
|
-
|
|
194
|
-
|
|
259
|
+
export async function installVersion({
|
|
260
|
+
context,
|
|
261
|
+
version,
|
|
262
|
+
spawnProcess,
|
|
263
|
+
platform = process.platform,
|
|
195
264
|
env = process.env,
|
|
196
|
-
|
|
197
|
-
now = Date.now(),
|
|
198
|
-
stderr,
|
|
265
|
+
signalSource = process,
|
|
199
266
|
}) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
267
|
+
const [command, args] = installInvocation(context, version);
|
|
268
|
+
return await new Promise((resolve, reject) => {
|
|
269
|
+
let parentSignal = null;
|
|
270
|
+
let settled = false;
|
|
271
|
+
const child = spawnProcess(command, args, {
|
|
272
|
+
env,
|
|
273
|
+
shell: platform === "win32",
|
|
274
|
+
stdio: "inherit",
|
|
275
|
+
});
|
|
276
|
+
const cleanup = forwardSignals(signalSource, child, (signal) => {
|
|
277
|
+
parentSignal ??= signal;
|
|
278
|
+
});
|
|
279
|
+
child.on("error", (error) => {
|
|
280
|
+
if (settled) return;
|
|
281
|
+
settled = true;
|
|
282
|
+
cleanup();
|
|
283
|
+
reject(
|
|
284
|
+
new CliApiError(
|
|
285
|
+
error?.code === "ENOENT"
|
|
286
|
+
? `${command} was not found on PATH. Install it or run: npm install -g ${PACKAGE_NAME}@latest`
|
|
287
|
+
: `Failed to run ${command}: ${error?.message ?? error}`,
|
|
288
|
+
),
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
child.on("exit", (code, signal) => {
|
|
292
|
+
if (settled) return;
|
|
293
|
+
settled = true;
|
|
294
|
+
cleanup();
|
|
295
|
+
resolve({
|
|
296
|
+
exitCode: parentSignal || signal ? 1 : (code ?? 0),
|
|
297
|
+
signal: parentSignal ?? signal ?? null,
|
|
228
298
|
});
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function resolveGlobalRoot({ kind, spawnProcess, env, platform, signalSource }) {
|
|
304
|
+
return await new Promise((resolve) => {
|
|
305
|
+
let output = "";
|
|
306
|
+
let interrupted = false;
|
|
307
|
+
const child = spawnProcess(kind, GLOBAL_ROOT_COMMANDS[kind], {
|
|
308
|
+
env,
|
|
309
|
+
shell: platform === "win32",
|
|
310
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
311
|
+
});
|
|
312
|
+
const cleanup = forwardSignals(signalSource, child, () => {
|
|
313
|
+
interrupted = true;
|
|
314
|
+
});
|
|
315
|
+
child.stdout?.on("data", (chunk) => {
|
|
316
|
+
output += String(chunk);
|
|
317
|
+
});
|
|
318
|
+
child.on("error", () => {
|
|
319
|
+
cleanup();
|
|
320
|
+
resolve(null);
|
|
321
|
+
});
|
|
322
|
+
child.on("exit", (code, signal) => {
|
|
323
|
+
cleanup();
|
|
324
|
+
resolve(code === 0 && !signal && !interrupted ? output.trim() || null : null);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export async function resolveInstalledBin({
|
|
330
|
+
context,
|
|
331
|
+
spawnProcess,
|
|
332
|
+
env = process.env,
|
|
333
|
+
platform = process.platform,
|
|
334
|
+
signalSource = process,
|
|
335
|
+
}) {
|
|
336
|
+
if (!INSTALLED_KINDS.has(context?.kind)) {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
const installRoot = stableInstallRoot(context.binPath);
|
|
340
|
+
if (!installRoot) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
241
343
|
|
|
242
|
-
|
|
243
|
-
|
|
344
|
+
const paths = pathApi(platform);
|
|
345
|
+
const packageRoot = paths.join(installRoot, "node_modules");
|
|
346
|
+
const output = await resolveGlobalRoot({
|
|
347
|
+
kind: context.kind,
|
|
348
|
+
spawnProcess,
|
|
349
|
+
env,
|
|
350
|
+
platform,
|
|
351
|
+
signalSource,
|
|
352
|
+
});
|
|
353
|
+
let configuredRoot = output;
|
|
354
|
+
if (context.kind === "bun") {
|
|
355
|
+
// Bun lists the effective global directory before its dependency tree.
|
|
356
|
+
configuredRoot = /^(.+) node_modules \(\d+(?: installed)?\)$/.exec(output?.split(/\r?\n/)[0] ?? "")?.[1];
|
|
357
|
+
}
|
|
358
|
+
if (!configuredRoot || !paths.isAbsolute(configuredRoot)) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
if (context.kind === "bun" || context.kind === "yarn") {
|
|
362
|
+
configuredRoot = paths.join(configuredRoot, "node_modules");
|
|
363
|
+
}
|
|
364
|
+
if (canonicalPathIdentity(configuredRoot) !== canonicalPathIdentity(packageRoot)) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const candidate = paths.join(packageRoot, "@ornncompute", "cli", "bin", "ornn.js");
|
|
369
|
+
if (!paths.isAbsolute(candidate) || !existsSync(candidate)) {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
return candidate;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function resolveUpdateRegistry(env = process.env) {
|
|
376
|
+
const registry = env.npm_config_registry?.trim() || "https://registry.npmjs.org";
|
|
377
|
+
return registry.replace(/\/+$/, "");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export async function fetchLatestVersion({
|
|
381
|
+
env = process.env,
|
|
382
|
+
fetchImpl = fetch,
|
|
383
|
+
timeoutMs = UPDATE_CHECK_TIMEOUT_MS,
|
|
384
|
+
} = {}) {
|
|
385
|
+
try {
|
|
386
|
+
const response = await fetchImpl(`${resolveUpdateRegistry(env)}/${PACKAGE_NAME}/latest`, {
|
|
387
|
+
headers: { Accept: "application/json" },
|
|
388
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
389
|
+
});
|
|
390
|
+
if (!response.ok) {
|
|
391
|
+
return null;
|
|
244
392
|
}
|
|
393
|
+
const payload = await response.json();
|
|
394
|
+
const version = payload?.version;
|
|
395
|
+
return typeof version === "string" && VERSION_PATTERN.test(version) ? version : null;
|
|
245
396
|
} catch {
|
|
246
|
-
|
|
397
|
+
return null;
|
|
247
398
|
}
|
|
248
399
|
}
|
|
249
400
|
|
|
@@ -265,95 +416,91 @@ export async function updateCommand({
|
|
|
265
416
|
throw new CliApiError("ornn is running from a development checkout; update it with git pull.");
|
|
266
417
|
}
|
|
267
418
|
if (context.kind === "project") {
|
|
268
|
-
throw new CliApiError(
|
|
419
|
+
throw new CliApiError(
|
|
420
|
+
"ornn is installed as a project dependency; update it there with: npm update @ornncompute/cli",
|
|
421
|
+
);
|
|
269
422
|
}
|
|
270
423
|
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");
|
|
424
|
+
throw new CliApiError(
|
|
425
|
+
"Could not determine how ornn was installed. Reinstall with: npm install -g @ornncompute/cli@latest",
|
|
426
|
+
);
|
|
285
427
|
}
|
|
286
428
|
|
|
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) {
|
|
429
|
+
const lock = await acquireUpdateLock({ env, binPath: context.binPath });
|
|
430
|
+
if (!lock) {
|
|
431
|
+
const lockPath = updateLockPath(env, context.binPath);
|
|
307
432
|
throw new CliApiError(
|
|
308
|
-
|
|
433
|
+
`Could not acquire the update lock. If another ornn update is running, wait for it to finish and retry.${
|
|
434
|
+
lockPath
|
|
435
|
+
? ` If no updater or package installer is running, remove the stale lock at ${lockPath} and retry.`
|
|
436
|
+
: " Check the config directory permissions and retry."
|
|
437
|
+
}`,
|
|
309
438
|
);
|
|
310
439
|
}
|
|
311
440
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
441
|
+
try {
|
|
442
|
+
const installedBin = await resolveInstalledBin({ context, spawnProcess, env, platform });
|
|
443
|
+
if (!installedBin) {
|
|
444
|
+
throw new CliApiError(
|
|
445
|
+
`Could not verify the ${context.kind} global installation root. No files were changed; reinstall with: npm install -g ${PACKAGE_NAME}@latest`,
|
|
446
|
+
);
|
|
447
|
+
}
|
|
315
448
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
|
|
449
|
+
const latest = await fetchLatestVersion({ env, fetchImpl });
|
|
450
|
+
if (latest && compareVersions(latest, currentVersion) <= 0) {
|
|
451
|
+
stdout.write(`ornn CLI is already up to date (${currentVersion}).\n`);
|
|
452
|
+
return 0;
|
|
453
|
+
}
|
|
454
|
+
if (!latest) {
|
|
455
|
+
stderr.write("Could not check the npm registry for the latest version; attempting the update anyway.\n");
|
|
322
456
|
}
|
|
323
|
-
await writeUpdateCache(updateCachePath(env), {
|
|
324
|
-
lastCheckedAt: new Date().toISOString(),
|
|
325
|
-
latestVersion: latest ?? null,
|
|
326
|
-
});
|
|
327
|
-
return 0;
|
|
328
|
-
}
|
|
329
457
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
return 1;
|
|
346
|
-
} else {
|
|
347
|
-
stdout.write(`Updated ornn CLI ${currentVersion} -> ${installedVersion}.\n`);
|
|
348
|
-
if (latest && compareVersions(installedVersion, latest) < 0) {
|
|
349
|
-
stderr.write(`Note: ${latest} is the latest on the registry; your mirror may be behind.\n`);
|
|
458
|
+
const version = latest ?? "latest";
|
|
459
|
+
const [command, commandArgs] = installInvocation(context, version);
|
|
460
|
+
stderr.write(`Running: ${command} ${commandArgs.join(" ")}\n`);
|
|
461
|
+
const result = await lock.runInstall(() => installVersion({ context, version, spawnProcess, platform, env }));
|
|
462
|
+
if (result.exitCode !== 0) {
|
|
463
|
+
throw new CliApiError(
|
|
464
|
+
updateFailureMessage({
|
|
465
|
+
binPath: context.binPath,
|
|
466
|
+
command,
|
|
467
|
+
commandArgs,
|
|
468
|
+
exitCode: result.exitCode,
|
|
469
|
+
kind: context.kind,
|
|
470
|
+
platform,
|
|
471
|
+
}),
|
|
472
|
+
);
|
|
350
473
|
}
|
|
474
|
+
|
|
475
|
+
if (platform === "win32") {
|
|
476
|
+
stderr.write("If this terminal reports an error after updating, it is safe to ignore; open a new terminal.\n");
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const installedVersion = await readInstalledVersion(installedBin);
|
|
480
|
+
if (!installedVersion) {
|
|
481
|
+
stdout.write("Update finished. Run `ornn --version` to confirm the installed version.\n");
|
|
482
|
+
} else if (installedVersion === currentVersion && latest) {
|
|
483
|
+
stderr.write(
|
|
484
|
+
`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`,
|
|
485
|
+
);
|
|
486
|
+
return 1;
|
|
487
|
+
} else if (installedVersion === currentVersion) {
|
|
488
|
+
stdout.write(`ornn CLI is still ${currentVersion}; the registry may not have a newer version.\n`);
|
|
489
|
+
} else if (compareVersions(installedVersion, currentVersion) < 0) {
|
|
490
|
+
stderr.write(
|
|
491
|
+
`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`,
|
|
492
|
+
);
|
|
493
|
+
return 1;
|
|
494
|
+
} else {
|
|
495
|
+
stdout.write(`Updated ornn CLI ${currentVersion} -> ${installedVersion}.\n`);
|
|
496
|
+
if (latest && compareVersions(installedVersion, latest) < 0) {
|
|
497
|
+
stderr.write(`Note: ${latest} is the latest on the registry; your mirror may be behind.\n`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return 0;
|
|
501
|
+
} finally {
|
|
502
|
+
await lock.release();
|
|
351
503
|
}
|
|
352
|
-
await writeUpdateCache(updateCachePath(env), {
|
|
353
|
-
lastCheckedAt: new Date().toISOString(),
|
|
354
|
-
latestVersion: latest ?? installedVersion ?? null,
|
|
355
|
-
});
|
|
356
|
-
return 0;
|
|
357
504
|
}
|
|
358
505
|
|
|
359
506
|
function updateFailureMessage({ binPath, command, commandArgs, exitCode, kind, platform }) {
|