@modelstatus/cli 0.1.51 → 0.1.53
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 +45 -4
- package/src/tui/app.js +25 -23
- package/src/updater.js +35 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modelstatus/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.53",
|
|
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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
5
6
|
import { resolveAuth, loadConfig, saveConfig, clearAuth, configFilePath } from "./config.js";
|
|
6
7
|
import { createClient } from "./api.js";
|
|
7
8
|
import { collectFrom, availability, ALL_SOURCE_IDS, getSource } from "./sources/index.js";
|
|
@@ -12,7 +13,7 @@ import {
|
|
|
12
13
|
import { redactValue } from "./redact.js";
|
|
13
14
|
import { assignProjects, buildUsages } from "./upload.js";
|
|
14
15
|
import { loginViaBrowser } from "./auth.js";
|
|
15
|
-
import { maybeCheckForUpdate } from "./updater.js";
|
|
16
|
+
import { maybeCheckForUpdate, forceUpdate } from "./updater.js";
|
|
16
17
|
import { track, maybeAnalyticsNotice } from "./telemetry.js";
|
|
17
18
|
import { BUILD_VERSION } from "./version.js";
|
|
18
19
|
|
|
@@ -691,7 +692,8 @@ Usage:
|
|
|
691
692
|
mm sources List detection sources and whether each can run here
|
|
692
693
|
mm integrations Manage live integrations (list | enable <id> | disable <id> | env <id> <tag>)
|
|
693
694
|
mm clear Delete all tracked usages from your inventory (--all also wipes projects/rules; --yes to skip the prompt)
|
|
694
|
-
mm
|
|
695
|
+
mm update Update to the latest version now and relaunch (or add --update to any command)
|
|
696
|
+
mm upgrade Open Stripe checkout and poll until Pro is active (the paid plan, not the binary)
|
|
695
697
|
mm play [dir] Play Donkey Kong while a background scan walks the dir (just for fun)
|
|
696
698
|
mm tui Force-launch the TUI (logs you in first if needed)
|
|
697
699
|
|
|
@@ -705,7 +707,7 @@ Scan sources (--sources; default = filesystem + enabled integrations; "all" for
|
|
|
705
707
|
Secret sources shell out to your already-authenticated CLIs, run read-only,
|
|
706
708
|
and only ever upload model ids — secret VALUES never leave your machine.
|
|
707
709
|
|
|
708
|
-
Flags: --api <url> · --key <key> · --project <id|name> · --yes · --json · --ci · --dry-run
|
|
710
|
+
Flags: --update · --api <url> · --key <key> · --project <id|name> · --yes · --json · --ci · --dry-run
|
|
709
711
|
--sources <list> · --region <r> · --namespace <ns> · --kube-context <c> · --db <dsn> · --sql-table <t>
|
|
710
712
|
--vercel-project <p> · --vercel-team <t> · --gh-repo <owner/name> · --supabase-ref <ref>
|
|
711
713
|
|
|
@@ -721,13 +723,38 @@ async function maybePrintUpdate(promise) {
|
|
|
721
723
|
// be unsafe. Point the user at a one-line reinstall to a user-owned dir.
|
|
722
724
|
process.stderr.write(`\n✦ mm ${r.to} is available (you're on ${r.from}). Auto-update can't write ${process.execPath} — reinstall:\n curl -fsSL https://llmstatus.ai/install.sh | bash\n`);
|
|
723
725
|
} else {
|
|
724
|
-
process.stderr.write(`\n✓ Updated mm ${r.from} → ${r.to} — your next run uses
|
|
726
|
+
process.stderr.write(`\n✓ Updated mm ${r.from} → ${r.to} — your next run uses it (or run \`mm update\` to relaunch on it now).\n`);
|
|
725
727
|
}
|
|
726
728
|
} catch {
|
|
727
729
|
/* swallow */
|
|
728
730
|
}
|
|
729
731
|
}
|
|
730
732
|
|
|
733
|
+
/**
|
|
734
|
+
* `mm update` (command) or `--update` (flag on any command): apply the in-place
|
|
735
|
+
* self-update NOW and RE-EXEC the fresh binary, so a single run lands on the new
|
|
736
|
+
* version — no opening the app twice. `rerunArgs` is what the new binary should
|
|
737
|
+
* run (the same invocation minus the update trigger). Re-execs + exits on a real
|
|
738
|
+
* update; otherwise prints status and returns (caller continues normally).
|
|
739
|
+
*/
|
|
740
|
+
async function applyUpdate(rerunArgs) {
|
|
741
|
+
const r = await forceUpdate();
|
|
742
|
+
if (r.status === "updated") {
|
|
743
|
+
process.stderr.write(`✓ Updated mm ${r.from} → ${r.to}. Relaunching…\n`);
|
|
744
|
+
const child = spawnSync(process.execPath, rerunArgs, {
|
|
745
|
+
stdio: "inherit",
|
|
746
|
+
env: { ...process.env, MM_JUST_UPDATED: r.to },
|
|
747
|
+
});
|
|
748
|
+
process.exit(child.status ?? 0);
|
|
749
|
+
}
|
|
750
|
+
if (r.status === "latest") process.stderr.write(`✓ Already on the latest mm (${r.version}).\n`);
|
|
751
|
+
else if (r.status === "manual")
|
|
752
|
+
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`);
|
|
753
|
+
else if (r.status === "npm") process.stderr.write(`This is an npm-managed install. Update with:\n npm update -g @modelstatus/cli\n`);
|
|
754
|
+
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`);
|
|
755
|
+
else if (r.status === "error") process.stderr.write(`! Update check failed: ${r.message}\n`);
|
|
756
|
+
}
|
|
757
|
+
|
|
731
758
|
async function main() {
|
|
732
759
|
const { positional, flags } = parseArgs(process.argv.slice(2));
|
|
733
760
|
const cmd = positional[0];
|
|
@@ -750,6 +777,20 @@ async function main() {
|
|
|
750
777
|
maybeAnalyticsNotice();
|
|
751
778
|
track("cli_command", { command: cmd || "tui" });
|
|
752
779
|
|
|
780
|
+
// Explicit self-update: `mm update` (command) or `--update` (flag on any
|
|
781
|
+
// command). Apply the update in place and RE-EXEC the fresh binary so this one
|
|
782
|
+
// run lands on the latest — no opening the app twice. `mm update` with nothing
|
|
783
|
+
// else then opens the TUI on the new version.
|
|
784
|
+
if (cmd === "update" || flags.update) {
|
|
785
|
+
const rerun = process.argv.slice(2).filter((a) => a !== "update" && a !== "--update");
|
|
786
|
+
await applyUpdate(rerun); // re-execs + exits when it actually updates
|
|
787
|
+
if (cmd === "update") {
|
|
788
|
+
await launchTui(positional[1], flags);
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
// A bare `--update` on another command falls through to run that command.
|
|
792
|
+
}
|
|
793
|
+
|
|
753
794
|
// Kick off the background self-update check — runs in parallel with the
|
|
754
795
|
// user's command, capped at 1/24h, only for shell-installed binaries.
|
|
755
796
|
const updatePromise = maybeCheckForUpdate(flags);
|
package/src/tui/app.js
CHANGED
|
@@ -40,37 +40,39 @@ const GATE_KEYS = [{ k: "1-8", label: "switch" }, { k: "7", label: "sign in" }];
|
|
|
40
40
|
// toast, status, keybar, bottomRule) — the body fills whatever rows remain.
|
|
41
41
|
const CHROME_ROWS = 10;
|
|
42
42
|
|
|
43
|
+
// Rows to reserve below the frame (termRows = rows - reserve). POSITIVE shrinks
|
|
44
|
+
// the frame, NEGATIVE grows it past the reported height.
|
|
45
|
+
// - 1 everywhere: a full-height ink frame write+newlines its bottom line and
|
|
46
|
+
// scrolls the buffer up 1/render; the 1-row reserve keeps the last line blank.
|
|
47
|
+
// - -7 in Warp: empirically (the user dialing it live in their Warp), the frame
|
|
48
|
+
// must be GROWN ~7 rows past stdout.rows or the top clips — Warp's block model
|
|
49
|
+
// reports/handles the alt-screen height such that a flush-height frame rides too
|
|
50
|
+
// high. So in Warp we render TALLER, which pushes the window down into view.
|
|
51
|
+
// Gated on Warp (a negative reserve elsewhere would over-grow + clip the top of
|
|
52
|
+
// a normal full-screen terminal).
|
|
53
|
+
const WARP_RESERVE = -7;
|
|
54
|
+
|
|
55
|
+
/** True when running under Warp (TERM_PROGRAM=WarpTerminal, or any WARP_* env, or
|
|
56
|
+
* TERM_PROGRAM merely containing "warp" — broad so detection doesn't silently
|
|
57
|
+
* miss a build/config that doesn't set the canonical value). */
|
|
58
|
+
function isWarp(env) {
|
|
59
|
+
if (String(env.TERM_PROGRAM || "").toLowerCase().includes("warp")) return true;
|
|
60
|
+
for (const k in env) if (k.startsWith("WARP_")) return true;
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
43
64
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* the bottom line, which scrolls the buffer up by 1 each render; reserving the
|
|
48
|
-
* last row keeps it blank so nothing ever scrolls.
|
|
49
|
-
* - 6 rows in Warp: Warp keeps ~5 rows of block chrome (sticky command header +
|
|
50
|
-
* pinned input) drawn OVER the alternate screen and over-reports stdout.rows by
|
|
51
|
-
* that much, so the TOP clips unless we also subtract them (1 guard + 5 chrome;
|
|
52
|
-
* measured against the user's Warp build — see Warp issues #6428/#2373, the
|
|
53
|
-
* chrome area can't be reclaimed via alt_screen_padding). Gated on TERM_PROGRAM
|
|
54
|
-
* so non-Warp terminals keep the plain 1-row reserve (an extra reserve elsewhere
|
|
55
|
-
* would leave a blank band).
|
|
56
|
-
*
|
|
57
|
-
* MM_TUI_RESERVE_ROWS (integer ≥ 0) overrides the whole reserve so a user on a
|
|
58
|
-
* different Warp version / input position / timing-line setting can self-tune
|
|
59
|
-
* (e.g. =5 or =7) without waiting for a release; 0 opts out entirely.
|
|
65
|
+
* MM_TUI_RESERVE_ROWS (any integer, may be negative) overrides the reserve so a
|
|
66
|
+
* user on a different terminal/version can self-tune in either direction without
|
|
67
|
+
* a release; blank/non-integer falls back to the gated default.
|
|
60
68
|
*/
|
|
61
69
|
export function rowsReserve(env = process.env) {
|
|
62
|
-
// Any non-blank INTEGER string overrides (so `MM_TUI_RESERVE_ROWS=` or stray
|
|
63
|
-
// whitespace falls back to the default rather than being read as 0 —
|
|
64
|
-
// `Number("")` is 0). The override may be NEGATIVE: a negative reserve GROWS the
|
|
65
|
-
// frame past the terminal height (termRows = rows - reserve), so the knob goes
|
|
66
|
-
// both ways — positive shrinks the frame, negative grows it — letting a user
|
|
67
|
-
// find the exact offset their terminal (e.g. Warp) needs in either direction.
|
|
68
70
|
const raw = env.MM_TUI_RESERVE_ROWS;
|
|
69
71
|
if (typeof raw === "string" && raw.trim() !== "") {
|
|
70
72
|
const n = Number(raw);
|
|
71
73
|
if (Number.isInteger(n)) return n;
|
|
72
74
|
}
|
|
73
|
-
return env
|
|
75
|
+
return isWarp(env) ? WARP_RESERVE : 1;
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
/** Frame height for a terminal of `rows` rows: reserve rows, then clamp to a
|
package/src/updater.js
CHANGED
|
@@ -133,6 +133,41 @@ async function downloadAndReplace(version, key, expectedSha) {
|
|
|
133
133
|
* "0.1.8 → v0.1.9" which looks like a typo. */
|
|
134
134
|
function dispVer(v) { return String(v).replace(/^v/, ""); }
|
|
135
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Force an update RIGHT NOW for an explicit `mm update` / `--update` — ignores
|
|
138
|
+
* the 30s throttle AND MM_NO_AUTO_UPDATE (the user asked for it). Swaps the
|
|
139
|
+
* binary in place but never re-execs (the caller does, so this one run can land
|
|
140
|
+
* on the new version without opening the app twice). Never throws; returns:
|
|
141
|
+
* { status: "updated", from, to } binary swapped — caller should re-exec
|
|
142
|
+
* { status: "latest", version } already the newest
|
|
143
|
+
* { status: "manual", from, to } newer exists but the dir isn't writable
|
|
144
|
+
* { status: "npm" } npm-managed install (use npm update -g)
|
|
145
|
+
* { status: "unsupported" } not a shell binary / unknown platform
|
|
146
|
+
* { status: "error", message } the check or download failed
|
|
147
|
+
*/
|
|
148
|
+
export async function forceUpdate() {
|
|
149
|
+
try {
|
|
150
|
+
if (!IS_SHELL_INSTALL) return { status: "npm" };
|
|
151
|
+
const key = platformKey();
|
|
152
|
+
if (!key) return { status: "unsupported" };
|
|
153
|
+
|
|
154
|
+
const manifest = await fetchJson(`${CDN}/cli/${CHANNEL_PATH}/version.json`);
|
|
155
|
+
writeCache({ ...(readCache() || {}), last_check: Date.now(), latest_known: manifest.version });
|
|
156
|
+
if (!manifest.version) return { status: "error", message: "manifest has no version" };
|
|
157
|
+
if (compareVer(manifest.version, BUILD_VERSION) <= 0) return { status: "latest", version: dispVer(BUILD_VERSION) };
|
|
158
|
+
|
|
159
|
+
const expectedSha = manifest.sha256?.[key];
|
|
160
|
+
if (!expectedSha) return { status: "error", message: `no sha256 for ${key}` };
|
|
161
|
+
if (!dirWritable(path.dirname(process.execPath))) {
|
|
162
|
+
return { status: "manual", from: dispVer(BUILD_VERSION), to: dispVer(manifest.version) };
|
|
163
|
+
}
|
|
164
|
+
await downloadAndReplace(manifest.version, key, expectedSha);
|
|
165
|
+
return { status: "updated", from: dispVer(BUILD_VERSION), to: dispVer(manifest.version) };
|
|
166
|
+
} catch (e) {
|
|
167
|
+
return { status: "error", message: e?.message ?? String(e) };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
136
171
|
/** Returns { from, to } if an update was completed, else null. Never throws. */
|
|
137
172
|
export async function maybeCheckForUpdate(flags = {}) {
|
|
138
173
|
try {
|