@modelstatus/cli 0.1.52 → 0.1.54

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelstatus/cli",
3
- "version": "0.1.52",
3
+ "version": "0.1.54",
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 upgrade Open Stripe checkout and poll until Pro is active
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 the new version.\n`);
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
@@ -305,14 +305,21 @@ export const appController = {
305
305
  try { this._instance && this._instance.unmount(); } catch { /* already gone */ }
306
306
  this._instance = null;
307
307
  },
308
- /** Mount a fresh Ink tree, merging the base opts with `next` (e.g. a tab). */
308
+ /** Mount a fresh Ink tree, merging the base opts with `next` (e.g. a tab).
309
+ * Only called after the in-TUI game (launch.js). */
309
310
  remount(next = {}) {
310
311
  const opts = { ...(this._opts || {}), ...next };
311
312
  this._opts = opts;
312
- // We're still in the alt screen (entered by runApp; the game kept it). Clear +
313
- // home so the fresh Ink tree paints the whole screen from the top with no
314
- // leftover game frame and no inline-cursor drift.
315
- try { process.stdout.write("\x1b[2J\x1b[H"); } catch { /* ignore */ }
313
+ // RECYCLE the alt screen: leave (?1049l) then re-enter (?1049h) + clear + home.
314
+ // We're still in the alt screen the game kept, but block-model terminals (Warp)
315
+ // drop their clean full-screen presentation during the game round-trip and the
316
+ // host's block header bleeds back over the top rows. A plain clear can't undo
317
+ // that. Leaving + re-entering reproduces the EXACT main→alt transition that
318
+ // runApp does on first launch (which renders perfectly), so Warp re-enters
319
+ // full-screen mode and the window paints flush from the top again. The two
320
+ // sequences are written together with no render between them, so there's no
321
+ // visible flash of the host screen.
322
+ try { process.stdout.write("\x1b[?1049l\x1b[?1049h\x1b[2J\x1b[H"); } catch { /* ignore */ }
316
323
  this._instance = render(h(Bootstrap, opts));
317
324
  return this._instance;
318
325
  },
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 {