@runinfra/cli 0.1.1 → 0.2.2
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 +134 -3
- package/dist/http.js +31 -8
- package/dist/main.js +22 -6
- package/dist/update-notice.js +216 -0
- package/dist/version.js +1 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -3,9 +3,6 @@
|
|
|
3
3
|
Sign in from your terminal and pull the optimized model packages your workspace
|
|
4
4
|
owns.
|
|
5
5
|
|
|
6
|
-
Node 20 or newer. **Zero runtime dependencies** by design, so it installs
|
|
7
|
-
quickly and runs cleanly inside a slim container or a locked-down host.
|
|
8
|
-
|
|
9
6
|
```console
|
|
10
7
|
$ npm install -g @runinfra/cli
|
|
11
8
|
$ runinfra login
|
|
@@ -16,11 +13,145 @@ Replace `<slug>` with the slug shown on your package's page under
|
|
|
16
13
|
**Optimized models** on runinfra.ai, for example
|
|
17
14
|
`qwen3-6-27b-fp8cd-v3-mlponly-h100-vllm`.
|
|
18
15
|
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
<!-- Release note for the next editor: when the installer and the wheel are
|
|
21
|
+
actually published, the standalone line becomes the lead command in the
|
|
22
|
+
block above and its Status cell becomes "Live". Do not promote a row
|
|
23
|
+
before fetching its artifact and installing from it: the Status column
|
|
24
|
+
is the only thing standing between a reader and a command that 404s. -->
|
|
25
|
+
|
|
26
|
+
Run the npm line above if the machine already has Node 20 or newer. It is the
|
|
27
|
+
only channel published today.
|
|
28
|
+
|
|
29
|
+
Two more channels are on the way, because these kits get pulled onto GPU hosts
|
|
30
|
+
and a GPU host often has Python but no Node:
|
|
31
|
+
|
|
32
|
+
| Channel | Command | Reach for it when | Status |
|
|
33
|
+
| --- | --- | --- | --- |
|
|
34
|
+
| Standalone | `curl -fsSL https://raw.githubusercontent.com/RightNow-AI/runinfra-cli/main/install.sh \| sh` | The box is bare. No Node, no Python, the download brings its own runtime. | Not published yet |
|
|
35
|
+
| Python | `pip install runinfra-cli` | The machine already lives in Python. | Not published yet |
|
|
36
|
+
| Node | `npm install -g @runinfra/cli` | The machine already lives in Node 20 or newer. | **Live, 0.1.1** |
|
|
37
|
+
|
|
38
|
+
Only the npm row works right now. Checked on 2026-07-27:
|
|
39
|
+
`pip index versions runinfra-cli` answers "No matching distribution found",
|
|
40
|
+
and the installer URL returns 404 because the script is not on the release
|
|
41
|
+
repository's main branch yet. Both are written down with their real commands
|
|
42
|
+
rather than left out, so the release that publishes them only has to flip a
|
|
43
|
+
Status cell, and so a reader can tell today what works from what does not.
|
|
44
|
+
|
|
45
|
+
All three publish at the same version, **0.1.1**, and put the same `runinfra`
|
|
46
|
+
command on PATH: the two new channels repackage the client that is already on
|
|
47
|
+
npm rather than changing it. So `runinfra --version` reads the same however it
|
|
48
|
+
arrived, and every command in this document behaves identically. What differs
|
|
49
|
+
is only what has to be on the machine first:
|
|
50
|
+
|
|
51
|
+
- **Standalone and Python** install one compiled executable with the runtime
|
|
52
|
+
inside it. Nothing else is required, which is the whole point on a host you
|
|
53
|
+
did not build. Note the distribution name: `pip install runinfra-cli` is
|
|
54
|
+
this tool, `pip install runinfra` is the inference SDK, a different product
|
|
55
|
+
for calling an endpoint from your code.
|
|
56
|
+
- **npm** installs the same program as JavaScript and runs it on the Node you
|
|
57
|
+
already have. It has **zero runtime dependencies** by design, so it installs
|
|
58
|
+
quickly and runs cleanly inside a slim container or a locked-down host.
|
|
59
|
+
|
|
60
|
+
Rather not install anything globally? The npm channel also runs through npx,
|
|
61
|
+
for example `npx @runinfra/cli pull <slug>`.
|
|
62
|
+
|
|
19
63
|
If your registry cannot find `@runinfra/cli`, the release has not reached it
|
|
20
64
|
yet. Run the CLI from a checkout instead, see [Development](#development).
|
|
21
65
|
|
|
22
66
|
---
|
|
23
67
|
|
|
68
|
+
## Verifying a release
|
|
69
|
+
|
|
70
|
+
Every release publishes `SHA256SUMS`, which lists the sha256 of each binary,
|
|
71
|
+
and `SHA256SUMS.sig`, an Ed25519 signature over that file.
|
|
72
|
+
|
|
73
|
+
`install.sh` checks both for you. It carries the public key **inside the
|
|
74
|
+
script** rather than fetching it, because a key downloaded from the same host
|
|
75
|
+
as the binary proves nothing: whoever can replace one can replace the other and
|
|
76
|
+
serve a matching pair. It verifies the signature before it reads a single hash
|
|
77
|
+
out of `SHA256SUMS`, and a bad signature ends the install rather than printing
|
|
78
|
+
a warning.
|
|
79
|
+
|
|
80
|
+
A **missing** signature ends the install too, when the artifacts came from a
|
|
81
|
+
release. An attacker who can serve you a swapped binary can also delete the
|
|
82
|
+
signature that would expose it, so a check that disappears on request is not a
|
|
83
|
+
check. If you are deliberately mirroring a release, or installing one published
|
|
84
|
+
before the signing key existed, `RUNINFRA_ALLOW_UNSIGNED=1` skips it and says
|
|
85
|
+
so, and artifacts taken from your own `--base-url` are never held to this rule.
|
|
86
|
+
|
|
87
|
+
Being unable to **perform** the check is the one case that stays a warning. If
|
|
88
|
+
the host has no `openssl`, or none that can do Ed25519, `install.sh` says so on
|
|
89
|
+
one line, names that exact reason, and continues on the checksum alone.
|
|
90
|
+
Refusing to install on a minimal container would cost more than it buys, and
|
|
91
|
+
the checksum still refuses a bad download. On macOS it looks past the LibreSSL
|
|
92
|
+
at `/usr/bin/openssl`, which cannot do the job, and tries the Homebrew
|
|
93
|
+
`openssl@3` locations before it gives up.
|
|
94
|
+
|
|
95
|
+
`install.ps1` does not check the signature. Windows PowerShell 5.1 has no
|
|
96
|
+
Ed25519 and .NET only gained one in 8, so it verifies the checksum, prints the
|
|
97
|
+
command below, and tells you which check it skipped rather than letting you
|
|
98
|
+
assume both ran.
|
|
99
|
+
|
|
100
|
+
To check a release by hand, save this as `runinfra-release.pub`:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
-----BEGIN PUBLIC KEY-----
|
|
104
|
+
MCowBQYDK2VwAyEAYkEJIc7GfRAHAvUXcY/jrtnwFj53XZNDoXE6cAkOxYk=
|
|
105
|
+
-----END PUBLIC KEY-----
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Its fingerprint is sha256 over the raw 32 byte key, and you can derive it from
|
|
109
|
+
the file you just saved rather than taking this line on trust:
|
|
110
|
+
|
|
111
|
+
```console
|
|
112
|
+
$ openssl pkey -pubin -in runinfra-release.pub -outform DER | tail -c 32 | sha256sum
|
|
113
|
+
5b2c8f637c0cd00a61ec6f126e5a9493c022ef1ea5be70fdd3ef4adf55801532
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Then, in the directory holding the downloaded files:
|
|
117
|
+
|
|
118
|
+
```console
|
|
119
|
+
$ openssl pkeyutl -verify -pubin -inkey runinfra-release.pub -rawin -in SHA256SUMS -sigfile SHA256SUMS.sig
|
|
120
|
+
Signature Verified Successfully
|
|
121
|
+
|
|
122
|
+
$ sha256sum --ignore-missing -c SHA256SUMS
|
|
123
|
+
runinfra-linux-x64: OK
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Run them in that order. The signature is what makes the checksum worth
|
|
127
|
+
checking: `SHA256SUMS` arrives from the same place as the binary, so anyone who
|
|
128
|
+
can swap the binary can swap its recorded hash to match. Verify the signature
|
|
129
|
+
first, then let the verified `SHA256SUMS` speak for the bytes.
|
|
130
|
+
|
|
131
|
+
Two things that are a failed check rather than a bad file. If the signature is
|
|
132
|
+
published base64 encoded rather than as 64 raw bytes, decode it first and point
|
|
133
|
+
`-sigfile` at the result, `base64 -d SHA256SUMS.sig > SHA256SUMS.sig.raw`, with
|
|
134
|
+
`-D` instead of `-d` on macOS. And
|
|
135
|
+
`-rawin` needs OpenSSL 1.1.1 or newer: the `openssl` on a stock macOS is
|
|
136
|
+
LibreSSL, whose `pkeyutl` has no `-rawin` at all, so it answers with a usage
|
|
137
|
+
error rather than a verdict. Read that as "not checked", not as "bad", and
|
|
138
|
+
install OpenSSL through Homebrew if you want the real answer.
|
|
139
|
+
|
|
140
|
+
### What a good signature proves
|
|
141
|
+
|
|
142
|
+
It proves the RunInfra release pipeline signed that file with the private half
|
|
143
|
+
of the key above, and that nothing has altered it since.
|
|
144
|
+
|
|
145
|
+
It does **not** prove that pipeline was not subverted. The signing key lives in
|
|
146
|
+
CI, so anyone who could steal the release token or edit the release workflow
|
|
147
|
+
could also reach the key and sign whatever they wanted. What this closes is a
|
|
148
|
+
release asset altered after publication, and a mirror or CDN serving something
|
|
149
|
+
other than what was published. Both are real attacks and both are worth
|
|
150
|
+
closing. Neither is the same thing as the download being tamper proof, and we
|
|
151
|
+
do not claim it is.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
24
155
|
## What the CLI never does
|
|
25
156
|
|
|
26
157
|
- **It never sees your password.** Sign-in happens in your browser, in your
|
package/dist/http.js
CHANGED
|
@@ -2,6 +2,7 @@ import { request as httpRequest } from "node:http";
|
|
|
2
2
|
import { request as httpsRequest } from "node:https";
|
|
3
3
|
import { isLoopbackHostname } from "./endpoints.js";
|
|
4
4
|
import { CliError } from "./errors.js";
|
|
5
|
+
import { CLI_LATEST_VERSION_HEADER, recordLatest } from "./update-notice.js";
|
|
5
6
|
import { userAgent } from "./version.js";
|
|
6
7
|
export const DEFAULT_IDLE_TIMEOUT_MS = 30_000;
|
|
7
8
|
export const DEFAULT_MAX_BODY_BYTES = 2 * 1024 * 1024;
|
|
@@ -13,6 +14,7 @@ function normalizeHeaders(message) {
|
|
|
13
14
|
continue;
|
|
14
15
|
out[name.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
|
|
15
16
|
}
|
|
17
|
+
recordLatest(out[CLI_LATEST_VERSION_HEADER]);
|
|
16
18
|
return out;
|
|
17
19
|
}
|
|
18
20
|
function networkError(url, cause) {
|
|
@@ -45,7 +47,9 @@ async function dispatch(options) {
|
|
|
45
47
|
const maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
|
|
46
48
|
let currentUrl = options.url;
|
|
47
49
|
let method = options.method;
|
|
48
|
-
let body = options.json === undefined
|
|
50
|
+
let body = options.json === undefined
|
|
51
|
+
? null
|
|
52
|
+
: Buffer.from(JSON.stringify(options.json), "utf8");
|
|
49
53
|
const originalOrigin = new URL(options.url).origin;
|
|
50
54
|
for (let hop = 0; hop <= maxRedirects; hop += 1) {
|
|
51
55
|
const parsed = new URL(currentUrl);
|
|
@@ -82,7 +86,11 @@ async function dispatch(options) {
|
|
|
82
86
|
});
|
|
83
87
|
const status = message.statusCode ?? 0;
|
|
84
88
|
const location = message.headers.location;
|
|
85
|
-
const isRedirect = (status === 301 ||
|
|
89
|
+
const isRedirect = (status === 301 ||
|
|
90
|
+
status === 302 ||
|
|
91
|
+
status === 303 ||
|
|
92
|
+
status === 307 ||
|
|
93
|
+
status === 308) &&
|
|
86
94
|
typeof location === "string";
|
|
87
95
|
if (!isRedirect) {
|
|
88
96
|
return { message, finalUrl: currentUrl };
|
|
@@ -95,7 +103,8 @@ async function dispatch(options) {
|
|
|
95
103
|
if (next.protocol === "http:" && !isLoopbackHostname(next.hostname)) {
|
|
96
104
|
throw new CliError("network", `${parsed.host} redirected to a plaintext URL off this machine (${next.protocol}//${next.host}).`, "This is not something the CLI can safely follow. Report it to support.");
|
|
97
105
|
}
|
|
98
|
-
if (status === 303 ||
|
|
106
|
+
if (status === 303 ||
|
|
107
|
+
((status === 301 || status === 302) && method !== "HEAD")) {
|
|
99
108
|
method = "GET";
|
|
100
109
|
body = null;
|
|
101
110
|
}
|
|
@@ -122,8 +131,13 @@ async function bufferBody(message, url, maxBytes) {
|
|
|
122
131
|
}
|
|
123
132
|
export async function requestRaw(options) {
|
|
124
133
|
const { message, finalUrl } = await dispatch(options);
|
|
134
|
+
const headers = normalizeHeaders(message);
|
|
125
135
|
const body = await bufferBody(message, finalUrl, options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES);
|
|
126
|
-
return {
|
|
136
|
+
return {
|
|
137
|
+
status: message.statusCode ?? 0,
|
|
138
|
+
headers,
|
|
139
|
+
body,
|
|
140
|
+
};
|
|
127
141
|
}
|
|
128
142
|
export async function requestJson(options) {
|
|
129
143
|
const raw = await requestRaw(options);
|
|
@@ -145,18 +159,25 @@ export async function headResource(url, options = {}) {
|
|
|
145
159
|
const { message } = await dispatch({
|
|
146
160
|
method: "HEAD",
|
|
147
161
|
url,
|
|
148
|
-
...(options.idleTimeoutMs === undefined
|
|
162
|
+
...(options.idleTimeoutMs === undefined
|
|
163
|
+
? {}
|
|
164
|
+
: { idleTimeoutMs: options.idleTimeoutMs }),
|
|
149
165
|
...(options.signal ? { signal: options.signal } : {}),
|
|
150
166
|
});
|
|
151
167
|
message.resume();
|
|
152
|
-
return {
|
|
168
|
+
return {
|
|
169
|
+
status: message.statusCode ?? 0,
|
|
170
|
+
headers: normalizeHeaders(message),
|
|
171
|
+
};
|
|
153
172
|
}
|
|
154
173
|
export async function streamRange(request) {
|
|
155
174
|
const { message, finalUrl } = await dispatch({
|
|
156
175
|
method: "GET",
|
|
157
176
|
url: request.url,
|
|
158
177
|
headers: { range: `bytes=${request.start}-${request.endInclusive}` },
|
|
159
|
-
...(request.idleTimeoutMs === undefined
|
|
178
|
+
...(request.idleTimeoutMs === undefined
|
|
179
|
+
? {}
|
|
180
|
+
: { idleTimeoutMs: request.idleTimeoutMs }),
|
|
160
181
|
...(request.signal ? { signal: request.signal } : {}),
|
|
161
182
|
});
|
|
162
183
|
const status = message.statusCode ?? 0;
|
|
@@ -172,7 +193,9 @@ export async function streamWhole(request) {
|
|
|
172
193
|
const { message, finalUrl } = await dispatch({
|
|
173
194
|
method: "GET",
|
|
174
195
|
url: request.url,
|
|
175
|
-
...(request.idleTimeoutMs === undefined
|
|
196
|
+
...(request.idleTimeoutMs === undefined
|
|
197
|
+
? {}
|
|
198
|
+
: { idleTimeoutMs: request.idleTimeoutMs }),
|
|
176
199
|
...(request.signal ? { signal: request.signal } : {}),
|
|
177
200
|
});
|
|
178
201
|
const status = message.statusCode ?? 0;
|
package/dist/main.js
CHANGED
|
@@ -6,7 +6,8 @@ import { login } from "./login.js";
|
|
|
6
6
|
import { logout } from "./logout.js";
|
|
7
7
|
import { createOutput } from "./output.js";
|
|
8
8
|
import { pull } from "./pull.js";
|
|
9
|
-
import {
|
|
9
|
+
import { maybePrintUpdateNotice } from "./update-notice.js";
|
|
10
|
+
import { CLI_NAME, CLI_VERSION, isSupportedNodeVersion, MINIMUM_NODE_MAJOR, } from "./version.js";
|
|
10
11
|
import { whoami } from "./whoami.js";
|
|
11
12
|
export async function run(argv, options = {}) {
|
|
12
13
|
const output = options.output ?? createOutput();
|
|
@@ -35,22 +36,32 @@ export async function run(argv, options = {}) {
|
|
|
35
36
|
...(options.env ? { env: options.env } : {}),
|
|
36
37
|
...(options.platform ? { platform: options.platform } : {}),
|
|
37
38
|
});
|
|
39
|
+
let exitCode;
|
|
38
40
|
switch (parsed.args.command) {
|
|
39
41
|
case "login":
|
|
40
|
-
|
|
42
|
+
exitCode = await login(context, { device: parsed.args.device });
|
|
43
|
+
break;
|
|
41
44
|
case "logout":
|
|
42
|
-
|
|
45
|
+
exitCode = await logout(context);
|
|
46
|
+
break;
|
|
43
47
|
case "whoami":
|
|
44
|
-
|
|
48
|
+
exitCode = await whoami(context);
|
|
49
|
+
break;
|
|
45
50
|
case "pull": {
|
|
46
51
|
const slug = parsed.args.slug;
|
|
47
52
|
if (slug === null) {
|
|
48
53
|
output.error("runinfra pull needs a package slug.");
|
|
49
54
|
return 2;
|
|
50
55
|
}
|
|
51
|
-
|
|
56
|
+
exitCode = await pull(context, { slug, out: parsed.args.out, concurrency: parsed.args.concurrency }, signal);
|
|
57
|
+
break;
|
|
52
58
|
}
|
|
53
59
|
}
|
|
60
|
+
await maybePrintUpdateNotice(output, {
|
|
61
|
+
env: options.env ?? process.env,
|
|
62
|
+
platform: options.platform ?? process.platform,
|
|
63
|
+
});
|
|
64
|
+
return exitCode;
|
|
54
65
|
}
|
|
55
66
|
catch (error) {
|
|
56
67
|
output.endStatus();
|
|
@@ -61,7 +72,12 @@ export async function run(argv, options = {}) {
|
|
|
61
72
|
if (described.code === "unexpected") {
|
|
62
73
|
output.detail("This is a bug in the CLI. Report it with the command you ran and this message.");
|
|
63
74
|
}
|
|
64
|
-
|
|
75
|
+
const exitCode = exitCodeFor(error);
|
|
76
|
+
await maybePrintUpdateNotice(output, {
|
|
77
|
+
env: options.env ?? process.env,
|
|
78
|
+
platform: options.platform ?? process.platform,
|
|
79
|
+
});
|
|
80
|
+
return exitCode;
|
|
65
81
|
}
|
|
66
82
|
}
|
|
67
83
|
function neverAborted() {
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { chmod, mkdir, open, readFile, rename, rm, writeFile, } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { credentialLocation } from "./paths.js";
|
|
5
|
+
import { CLI_VERSION } from "./version.js";
|
|
6
|
+
export const CLI_LATEST_VERSION_HEADER = "x-runinfra-cli-latest";
|
|
7
|
+
export const UPDATE_CHECK_FILE_NAME = "update-check.json";
|
|
8
|
+
const NOTICE_INTERVAL_MS = 24 * 60 * 60 * 1_000;
|
|
9
|
+
const PYPI_CHANNEL = "pypi";
|
|
10
|
+
const POSIX_INSTALLER = "curl -fsSL https://raw.githubusercontent.com/RightNow-AI/runinfra-cli/main/install.sh | sh";
|
|
11
|
+
const WINDOWS_INSTALLER = "irm https://raw.githubusercontent.com/RightNow-AI/runinfra-cli/main/install.ps1 | iex";
|
|
12
|
+
let latestSeen = null;
|
|
13
|
+
function parseVersion(version) {
|
|
14
|
+
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/u.exec(version);
|
|
15
|
+
if (!match)
|
|
16
|
+
return null;
|
|
17
|
+
const core = [match[1], match[2], match[3]].map((part) => Number.parseInt(part ?? "", 10));
|
|
18
|
+
if (core.some((part) => !Number.isSafeInteger(part) || part < 0))
|
|
19
|
+
return null;
|
|
20
|
+
const rawPrerelease = match[4];
|
|
21
|
+
const prerelease = rawPrerelease?.split(".") ?? null;
|
|
22
|
+
if (prerelease?.some((part) => part.length === 0 ||
|
|
23
|
+
!/^[0-9A-Za-z-]+$/u.test(part) ||
|
|
24
|
+
(/^\d+$/u.test(part) && part.length > 1 && part.startsWith("0")))) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
core: [core[0] ?? 0, core[1] ?? 0, core[2] ?? 0],
|
|
29
|
+
prerelease,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function comparePrerelease(a, b) {
|
|
33
|
+
if (a === null && b === null)
|
|
34
|
+
return 0;
|
|
35
|
+
if (a === null)
|
|
36
|
+
return 1;
|
|
37
|
+
if (b === null)
|
|
38
|
+
return -1;
|
|
39
|
+
const length = Math.max(a.length, b.length);
|
|
40
|
+
for (let index = 0; index < length; index += 1) {
|
|
41
|
+
const aPart = a[index];
|
|
42
|
+
const bPart = b[index];
|
|
43
|
+
if (aPart === undefined)
|
|
44
|
+
return -1;
|
|
45
|
+
if (bPart === undefined)
|
|
46
|
+
return 1;
|
|
47
|
+
if (aPart === bPart)
|
|
48
|
+
continue;
|
|
49
|
+
const aNumeric = /^\d+$/u.test(aPart);
|
|
50
|
+
const bNumeric = /^\d+$/u.test(bPart);
|
|
51
|
+
if (aNumeric && bNumeric) {
|
|
52
|
+
if (aPart.length !== bPart.length) {
|
|
53
|
+
return aPart.length < bPart.length ? -1 : 1;
|
|
54
|
+
}
|
|
55
|
+
return aPart < bPart ? -1 : 1;
|
|
56
|
+
}
|
|
57
|
+
if (aNumeric !== bNumeric)
|
|
58
|
+
return aNumeric ? -1 : 1;
|
|
59
|
+
return aPart < bPart ? -1 : 1;
|
|
60
|
+
}
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
export function compareVersions(a, b) {
|
|
64
|
+
const parsedA = parseVersion(a);
|
|
65
|
+
const parsedB = parseVersion(b);
|
|
66
|
+
if (!parsedA || !parsedB)
|
|
67
|
+
return 0;
|
|
68
|
+
for (let index = 0; index < parsedA.core.length; index += 1) {
|
|
69
|
+
const aPart = parsedA.core[index] ?? 0;
|
|
70
|
+
const bPart = parsedB.core[index] ?? 0;
|
|
71
|
+
if (aPart === bPart)
|
|
72
|
+
continue;
|
|
73
|
+
return aPart < bPart ? -1 : 1;
|
|
74
|
+
}
|
|
75
|
+
return comparePrerelease(parsedA.prerelease, parsedB.prerelease);
|
|
76
|
+
}
|
|
77
|
+
export function recordLatest(version) {
|
|
78
|
+
if (version === undefined || parseVersion(version) === null)
|
|
79
|
+
return;
|
|
80
|
+
if (latestSeen === null || compareVersions(version, latestSeen) === 1) {
|
|
81
|
+
latestSeen = version;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export function __resetLatestForTest() {
|
|
85
|
+
latestSeen = null;
|
|
86
|
+
}
|
|
87
|
+
function isRecord(value) {
|
|
88
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
89
|
+
}
|
|
90
|
+
function parseState(raw) {
|
|
91
|
+
let value;
|
|
92
|
+
try {
|
|
93
|
+
value = JSON.parse(raw);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
if (!isRecord(value))
|
|
99
|
+
return null;
|
|
100
|
+
if (typeof value.version !== "string" ||
|
|
101
|
+
parseVersion(value.version) === null) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
if (typeof value.lastNotifiedAt !== "number" ||
|
|
105
|
+
!Number.isSafeInteger(value.lastNotifiedAt) ||
|
|
106
|
+
value.lastNotifiedAt < 0) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
return { version: value.version, lastNotifiedAt: value.lastNotifiedAt };
|
|
110
|
+
}
|
|
111
|
+
async function readState(file) {
|
|
112
|
+
let raw;
|
|
113
|
+
try {
|
|
114
|
+
raw = await readFile(file, "utf8");
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (error.code === "ENOENT") {
|
|
118
|
+
return { kind: "missing" };
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
const state = parseState(raw);
|
|
123
|
+
return state ? { kind: "valid", state } : { kind: "invalid" };
|
|
124
|
+
}
|
|
125
|
+
function shouldNotify(latest, state, nowMs) {
|
|
126
|
+
if (compareVersions(latest, CLI_VERSION) !== 1)
|
|
127
|
+
return false;
|
|
128
|
+
if (state.kind === "invalid")
|
|
129
|
+
return false;
|
|
130
|
+
if (state.kind === "missing")
|
|
131
|
+
return true;
|
|
132
|
+
const alreadyNotified = compareVersions(state.state.version, latest);
|
|
133
|
+
if (alreadyNotified === 1)
|
|
134
|
+
return false;
|
|
135
|
+
if (alreadyNotified === -1)
|
|
136
|
+
return true;
|
|
137
|
+
return nowMs - state.state.lastNotifiedAt >= NOTICE_INTERVAL_MS;
|
|
138
|
+
}
|
|
139
|
+
function updateMessage(latest, env, platform, versions) {
|
|
140
|
+
if (env.RUNINFRA_DIST_CHANNEL === PYPI_CHANNEL) {
|
|
141
|
+
return `RunInfra CLI ${latest} is available. Upgrade: pip install --upgrade runinfra-cli`;
|
|
142
|
+
}
|
|
143
|
+
if (versions.bun !== undefined) {
|
|
144
|
+
const installer = platform === "win32" ? WINDOWS_INSTALLER : POSIX_INSTALLER;
|
|
145
|
+
return `RunInfra CLI ${latest} is available. Re-run the RunInfra standalone installer: ${installer}`;
|
|
146
|
+
}
|
|
147
|
+
return `RunInfra CLI ${latest} is available. Upgrade: npm install -g @runinfra/cli@latest`;
|
|
148
|
+
}
|
|
149
|
+
async function persistState(file, temp, state, platform) {
|
|
150
|
+
try {
|
|
151
|
+
await writeFile(temp, `${JSON.stringify(state, null, 2)}\n`, {
|
|
152
|
+
encoding: "utf8",
|
|
153
|
+
mode: 0o600,
|
|
154
|
+
});
|
|
155
|
+
if (platform !== "win32")
|
|
156
|
+
await chmod(temp, 0o600);
|
|
157
|
+
await rename(temp, file);
|
|
158
|
+
if (platform !== "win32")
|
|
159
|
+
await chmod(file, 0o600);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
await rm(temp, { force: true }).catch(() => undefined);
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async function releaseLock(lock, lockFile) {
|
|
167
|
+
if (lock === null)
|
|
168
|
+
return;
|
|
169
|
+
await lock.close().catch(() => undefined);
|
|
170
|
+
await rm(lockFile, { force: true }).catch(() => undefined);
|
|
171
|
+
}
|
|
172
|
+
async function printUpdateNotice(output, deps) {
|
|
173
|
+
const latest = latestSeen;
|
|
174
|
+
if (latest === null || compareVersions(latest, CLI_VERSION) !== 1)
|
|
175
|
+
return;
|
|
176
|
+
const env = deps.env ?? process.env;
|
|
177
|
+
const platform = deps.platform ?? process.platform;
|
|
178
|
+
const location = credentialLocation({
|
|
179
|
+
platform,
|
|
180
|
+
env,
|
|
181
|
+
homeDir: deps.homeDir ?? homedir(),
|
|
182
|
+
});
|
|
183
|
+
const file = join(location.dir, UPDATE_CHECK_FILE_NAME);
|
|
184
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
185
|
+
const lockFile = `${file}.lock`;
|
|
186
|
+
const nowMs = (deps.now ?? Date.now)();
|
|
187
|
+
if (!Number.isSafeInteger(nowMs) || nowMs < 0)
|
|
188
|
+
return;
|
|
189
|
+
await mkdir(location.dir, {
|
|
190
|
+
recursive: true,
|
|
191
|
+
...(platform === "win32" ? {} : { mode: 0o700 }),
|
|
192
|
+
});
|
|
193
|
+
if (platform !== "win32")
|
|
194
|
+
await chmod(location.dir, 0o700);
|
|
195
|
+
let lock = null;
|
|
196
|
+
try {
|
|
197
|
+
lock = await open(lockFile, "wx", 0o600);
|
|
198
|
+
if (platform !== "win32")
|
|
199
|
+
await chmod(lockFile, 0o600);
|
|
200
|
+
const state = await readState(file);
|
|
201
|
+
if (!shouldNotify(latest, state, nowMs))
|
|
202
|
+
return;
|
|
203
|
+
await persistState(file, temp, { version: latest, lastNotifiedAt: nowMs }, platform);
|
|
204
|
+
output.info(updateMessage(latest, env, platform, deps.versions ?? process.versions));
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
await releaseLock(lock, lockFile);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
export async function maybePrintUpdateNotice(output, deps = {}) {
|
|
211
|
+
try {
|
|
212
|
+
await printUpdateNotice(output, deps);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
}
|
|
216
|
+
}
|
package/dist/version.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const CLI_NAME = "runinfra";
|
|
2
2
|
export const CLI_PACKAGE = "@runinfra/cli";
|
|
3
|
-
export const CLI_VERSION = "0.
|
|
3
|
+
export const CLI_VERSION = "0.2.2";
|
|
4
4
|
export const CLI_CLIENT_ID = "runinfra-cli";
|
|
5
5
|
export const MINIMUM_NODE_MAJOR = 20;
|
|
6
6
|
export function isSupportedNodeVersion(version) {
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runinfra/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "RunInfra CLI: browser-approved sign-in and resumable downloads for optimized model packages",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://runinfra.ai/catalog",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/RightNow-AI/runinfra-cli.git"
|
|
10
|
+
},
|
|
7
11
|
"bugs": {
|
|
8
12
|
"url": "https://runinfra.ai/contact"
|
|
9
13
|
},
|