@modelstatus/cli 0.1.71 → 0.1.73
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/package.json +1 -1
- package/src/index.js +1 -0
- package/src/updater.js +42 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modelstatus/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.73",
|
|
4
4
|
"description": "Track which AI models you use, where, and never get surprised by a retirement. Free offline model-health for any repo (mm status), browser sign-in for cloud inventory + alerts.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"llm",
|
package/src/index.js
CHANGED
|
@@ -824,6 +824,7 @@ async function applyUpdate(rerunArgs) {
|
|
|
824
824
|
if (r.status === "latest") process.stderr.write(`✓ Already on the latest mm (${r.version}).\n`);
|
|
825
825
|
else if (r.status === "manual")
|
|
826
826
|
process.stderr.write(`✦ mm ${r.to} is available (you're on ${r.from}), but auto-update can't write ${process.execPath}. Reinstall:\n curl -fsSL https://llmstatus.ai/install.sh | bash\n`);
|
|
827
|
+
else if (r.status === "brew") process.stderr.write(`✦ mm ${r.to} is available (you're on ${r.from}). This is a Homebrew install — update with:\n brew upgrade modelstatus-cli\n`);
|
|
827
828
|
else if (r.status === "npm") process.stderr.write(`This is an npm-managed install. Update with:\n npm update -g @modelstatus/cli\n`);
|
|
828
829
|
else if (r.status === "unsupported") process.stderr.write(`Self-update isn't supported for this build. Reinstall:\n curl -fsSL https://llmstatus.ai/install.sh | bash\n`);
|
|
829
830
|
else if (r.status === "error") process.stderr.write(`! Update check failed: ${r.message}\n`);
|
package/src/updater.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* same-run doesn't re-exec, so any in-flight state stays sane.
|
|
14
14
|
*/
|
|
15
15
|
import crypto from "node:crypto";
|
|
16
|
+
import { spawnSync } from "node:child_process";
|
|
16
17
|
import fs from "node:fs";
|
|
17
18
|
import os from "node:os";
|
|
18
19
|
import path from "node:path";
|
|
@@ -97,6 +98,14 @@ function dirWritable(dir) {
|
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
/** True when the running binary lives under a Homebrew prefix. Brew owns the
|
|
102
|
+
* binary's lifecycle (`brew upgrade`), and silently swapping it out from under
|
|
103
|
+
* brew would break its integrity tracking — so we never self-update there. */
|
|
104
|
+
function isBrewManaged() {
|
|
105
|
+
const p = process.execPath;
|
|
106
|
+
return p.includes("/Cellar/") || p.includes("/homebrew/") || p.includes("/linuxbrew/");
|
|
107
|
+
}
|
|
108
|
+
|
|
100
109
|
/** Replace the executable via the atomic tmp+rename pattern ONLY (a new inode).
|
|
101
110
|
* We must NOT writeFileSync over the live executable's inode: truncating +
|
|
102
111
|
* rewriting a running, memory-mapped Mach-O can make macOS SIGKILL the running
|
|
@@ -114,6 +123,24 @@ function replaceBinary(exe, buf) {
|
|
|
114
123
|
}
|
|
115
124
|
}
|
|
116
125
|
|
|
126
|
+
// Our Developer ID team. A self-update download must be signed by us before we
|
|
127
|
+
// swap it in — otherwise a CDN compromise would auto-propagate to every
|
|
128
|
+
// installed macOS user (worse than a one-time install compromise). The sha256
|
|
129
|
+
// is from the same CDN as the binary, so it can't prove provenance; the code
|
|
130
|
+
// signature is the real anchor.
|
|
131
|
+
const TEAM_ID = "9H8U98V2UU";
|
|
132
|
+
|
|
133
|
+
/** macOS only: verify a downloaded binary file carries a valid Developer ID
|
|
134
|
+
* signature under OUR team, anchored to Apple. Throws if codesign is missing or
|
|
135
|
+
* the check fails (fail closed). No-op on Linux/Windows (no equivalent). */
|
|
136
|
+
function verifyMacSignature(file) {
|
|
137
|
+
if (process.platform !== "darwin") return;
|
|
138
|
+
const req = `anchor apple generic and certificate leaf[subject.OU] = "${TEAM_ID}"`;
|
|
139
|
+
const r = spawnSync("codesign", ["--verify", "--strict", `-R=${req}`, file], { stdio: "ignore" });
|
|
140
|
+
if (r.error) throw new Error("codesign unavailable — refusing to self-update");
|
|
141
|
+
if (r.status !== 0) throw new Error(`downloaded binary failed Developer ID verification (team ${TEAM_ID})`);
|
|
142
|
+
}
|
|
143
|
+
|
|
117
144
|
async function downloadAndReplace(version, key, expectedSha) {
|
|
118
145
|
const exe = process.execPath;
|
|
119
146
|
const ext = process.platform === "win32" ? ".exe" : "";
|
|
@@ -125,6 +152,17 @@ async function downloadAndReplace(version, key, expectedSha) {
|
|
|
125
152
|
if (actualSha !== expectedSha) {
|
|
126
153
|
throw new Error(`sha256 mismatch: expected ${expectedSha.slice(0, 12)}…, got ${actualSha.slice(0, 12)}…`);
|
|
127
154
|
}
|
|
155
|
+
// On macOS, verify the SIGNATURE before trusting these bytes as our binary.
|
|
156
|
+
// Stage to a temp file, codesign-verify it, then swap — never swap unverified.
|
|
157
|
+
if (process.platform === "darwin") {
|
|
158
|
+
const staged = path.join(path.dirname(exe), `.${path.basename(exe)}.verify.${process.pid}`);
|
|
159
|
+
try {
|
|
160
|
+
fs.writeFileSync(staged, buf, { mode: 0o755 });
|
|
161
|
+
verifyMacSignature(staged);
|
|
162
|
+
} finally {
|
|
163
|
+
try { fs.unlinkSync(staged); } catch { /* best effort */ }
|
|
164
|
+
}
|
|
165
|
+
}
|
|
128
166
|
replaceBinary(exe, buf);
|
|
129
167
|
}
|
|
130
168
|
|
|
@@ -158,6 +196,9 @@ export async function forceUpdate() {
|
|
|
158
196
|
|
|
159
197
|
const expectedSha = manifest.sha256?.[key];
|
|
160
198
|
if (!expectedSha) return { status: "error", message: `no sha256 for ${key}` };
|
|
199
|
+
if (isBrewManaged()) {
|
|
200
|
+
return { status: "brew", from: dispVer(BUILD_VERSION), to: dispVer(manifest.version) };
|
|
201
|
+
}
|
|
161
202
|
if (!dirWritable(path.dirname(process.execPath))) {
|
|
162
203
|
return { status: "manual", from: dispVer(BUILD_VERSION), to: dispVer(manifest.version) };
|
|
163
204
|
}
|
|
@@ -173,6 +214,7 @@ export async function maybeCheckForUpdate(flags = {}) {
|
|
|
173
214
|
try {
|
|
174
215
|
if (!IS_SHELL_INSTALL) return null;
|
|
175
216
|
if (process.env.MM_NO_AUTO_UPDATE) return null;
|
|
217
|
+
if (isBrewManaged()) return null; // brew upgrade owns the lifecycle
|
|
176
218
|
if (flags.json || flags.ci) return null;
|
|
177
219
|
|
|
178
220
|
const cache = readCache() || {};
|