@modelstatus/cli 0.1.82 → 0.1.83
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 +12 -2
- package/package.json +1 -1
- package/src/changelog-data.js +9 -0
- package/src/index.js +16 -3
- package/src/spinner.js +45 -0
- package/src/telemetry.js +51 -3
- package/src/updater.js +4 -3
package/README.md
CHANGED
|
@@ -29,10 +29,20 @@ Or skip install entirely and one-shot it: `npx @modelstatus/cli status`.
|
|
|
29
29
|
|
|
30
30
|
## Quick start
|
|
31
31
|
|
|
32
|
-
### Free:
|
|
32
|
+
### Free: open the dashboard
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
mm
|
|
35
|
+
mm [dir] # the full TUI — inventory · scan · what's new · alerts
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Just run `mm` (optionally on a folder) for the interactive dashboard: it scans
|
|
39
|
+
locally, shows every model in use with its health, and lets you fix the dying
|
|
40
|
+
ones — no account needed.
|
|
41
|
+
|
|
42
|
+
### Free: a one-shot health check (great for CI / pipes)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
mm status [dir] # quick offline check, prints + exits
|
|
36
46
|
npx @modelstatus/cli status [dir] # zero install (needs Node)
|
|
37
47
|
```
|
|
38
48
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modelstatus/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.83",
|
|
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/changelog-data.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
/* GENERATED by scripts/gen-changelog.mjs from apps/web/lib/changelog.json — do not edit.
|
|
2
2
|
* Release notes baked into the binary (in-TUI + the on-load what's-new card). */
|
|
3
3
|
export const CHANGELOG = [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.83",
|
|
6
|
+
"date": "2026-06-14",
|
|
7
|
+
"title": "Live progress on mm status",
|
|
8
|
+
"items": [
|
|
9
|
+
"`mm status` and `mm fix` now show live progress while they pull the registry and scan your repo — a cold run no longer sits silent and looks frozen.",
|
|
10
|
+
"Docs and the post-install quick start now lead with `mm` (the full dashboard); `mm status` is the quick, scriptable, no-account check that drops into CI."
|
|
11
|
+
]
|
|
12
|
+
},
|
|
4
13
|
{
|
|
5
14
|
"version": "0.1.82",
|
|
6
15
|
"date": "2026-06-13",
|
package/src/index.js
CHANGED
|
@@ -14,7 +14,8 @@ import { redactValue } from "./redact.js";
|
|
|
14
14
|
import { assignProjects, buildUsages } from "./upload.js";
|
|
15
15
|
import { loginViaBrowser } from "./auth.js";
|
|
16
16
|
import { maybeCheckForUpdate, forceUpdate } from "./updater.js";
|
|
17
|
-
import { track, analyticsState } from "./telemetry.js";
|
|
17
|
+
import { track, analyticsState, maybeFirstRun } from "./telemetry.js";
|
|
18
|
+
import { startProgress } from "./spinner.js";
|
|
18
19
|
import { BUILD_VERSION } from "./version.js";
|
|
19
20
|
|
|
20
21
|
// TRUE BACKGROUND SCAN — hidden worker dispatch. MUST be the first executable
|
|
@@ -544,9 +545,14 @@ async function cmdFix(positional, flags) {
|
|
|
544
545
|
const { getRegistry } = await import("./registry/fetch.js");
|
|
545
546
|
const { resolveLocal, computeHealth } = await import("./registry/local.js");
|
|
546
547
|
const { planFixes, applyFixes, terminalReplacement, recordFixes } = await import("./fix.js");
|
|
547
|
-
|
|
548
|
+
// Same cold-start silence as `mm status`: registry download + repo walk with
|
|
549
|
+
// no feedback. Spinner is stderr-only and auto-off under --json.
|
|
550
|
+
const prog = startProgress(!flags.json, "fetching the model registry…");
|
|
551
|
+
const snapshot = await getRegistry({ offline: !!flags.offline, log: (m) => prog.log(m) });
|
|
548
552
|
|
|
553
|
+
prog.update("scanning for model references…");
|
|
549
554
|
const candidates = await collectFrom(["filesystem"], { root: dir }, snapshot.detection);
|
|
555
|
+
prog.stop();
|
|
550
556
|
const resolved = resolveLocal(snapshot, [...new Set(candidates.map((c) => c.model_string))]);
|
|
551
557
|
const byStr = new Map(resolved.map((r) => [r.input.toLowerCase(), r]));
|
|
552
558
|
const today = new Date();
|
|
@@ -688,9 +694,15 @@ async function cmdStatus(positional, flags) {
|
|
|
688
694
|
const dir = path.resolve(positional[1] || flags.dir || ".");
|
|
689
695
|
const { getRegistry } = await import("./registry/fetch.js");
|
|
690
696
|
const { resolveLocal, computeHealth, dropResolvedFragments } = await import("./registry/local.js");
|
|
691
|
-
|
|
697
|
+
// A cold `mm status` downloads the ~225 kB registry snapshot then walks the
|
|
698
|
+
// repo with zero output — it reads as frozen. Show live stderr progress
|
|
699
|
+
// (auto-off for --json / pipes so machine output stays byte-identical).
|
|
700
|
+
const prog = startProgress(!flags.json, "fetching the model registry…");
|
|
701
|
+
const snapshot = await getRegistry({ offline: !!flags.offline, log: (m) => prog.log(m) });
|
|
692
702
|
|
|
703
|
+
prog.update("scanning for model references…");
|
|
693
704
|
let candidates = await collectFrom(parseSources(flags), scanOpts(flags, dir), snapshot.detection, explicitSources(flags));
|
|
705
|
+
prog.stop();
|
|
694
706
|
const resolved = resolveLocal(snapshot, [...new Set(candidates.map((c) => c.model_string))]);
|
|
695
707
|
const byStr = new Map(resolved.map((r) => [r.input.toLowerCase(), r]));
|
|
696
708
|
// Suppress detector fragments of resolved aliases (see dropResolvedFragments).
|
|
@@ -854,6 +866,7 @@ async function main() {
|
|
|
854
866
|
|
|
855
867
|
// Anonymous, opt-out usage analytics (one-time disclosure, then a single
|
|
856
868
|
// event per invocation). No-op without a baked key / when opted out.
|
|
869
|
+
maybeFirstRun(); // one-time cli_first_run per install (same opt-out as below)
|
|
857
870
|
track("cli_command", { command: cmd || "tui" });
|
|
858
871
|
|
|
859
872
|
// Explicit self-update: `mm update` (command) or `--update` (flag on any
|
package/src/spinner.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/* Minimal dependency-free progress spinner for stderr. A silent command that
|
|
2
|
+
* spends a few seconds downloading the registry / scanning a repo reads as
|
|
3
|
+
* frozen — this gives live feedback. No-op when stderr isn't a TTY (piped, CI,
|
|
4
|
+
* --json) so machine output is never polluted. */
|
|
5
|
+
|
|
6
|
+
const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
7
|
+
const CLEAR = "\r\x1b[2K"; // carriage return + clear the whole line
|
|
8
|
+
|
|
9
|
+
/** Start a spinner. `active` lets callers force it off (e.g. --json). Returns
|
|
10
|
+
* { update(text), log(line), stop() }. `log` prints a line ABOVE the spinner
|
|
11
|
+
* (clears, writes, resumes) so warnings from the work aren't clobbered. */
|
|
12
|
+
export function startProgress(active, text) {
|
|
13
|
+
const isTTY = !!process.stderr.isTTY;
|
|
14
|
+
if (!active || !isTTY) {
|
|
15
|
+
return { update() {}, log: (m) => process.stderr.write(`! ${m}\n`), stop() {} };
|
|
16
|
+
}
|
|
17
|
+
let i = 0;
|
|
18
|
+
let msg = text;
|
|
19
|
+
let timer = null;
|
|
20
|
+
const frame = () => process.stderr.write(`${CLEAR}${FRAMES[i = (i + 1) % FRAMES.length]} ${msg}`);
|
|
21
|
+
const start = () => {
|
|
22
|
+
frame();
|
|
23
|
+
timer = setInterval(frame, 80);
|
|
24
|
+
if (timer.unref) timer.unref(); // don't keep the process alive
|
|
25
|
+
};
|
|
26
|
+
const clear = () => {
|
|
27
|
+
if (timer) clearInterval(timer);
|
|
28
|
+
timer = null;
|
|
29
|
+
process.stderr.write(CLEAR);
|
|
30
|
+
};
|
|
31
|
+
start();
|
|
32
|
+
return {
|
|
33
|
+
update(m) {
|
|
34
|
+
msg = m;
|
|
35
|
+
},
|
|
36
|
+
log(m) {
|
|
37
|
+
clear();
|
|
38
|
+
process.stderr.write(`! ${m}\n`);
|
|
39
|
+
start();
|
|
40
|
+
},
|
|
41
|
+
stop() {
|
|
42
|
+
clear();
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
package/src/telemetry.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* --define __POSTHOG_KEY__; with no key, every function here is a silent no-op. */
|
|
9
9
|
import crypto from "node:crypto";
|
|
10
10
|
import { loadConfig, setConfigValue } from "./config.js";
|
|
11
|
-
import { BUILD_VERSION, UPDATE_CHANNEL } from "./version.js";
|
|
11
|
+
import { BUILD_VERSION, UPDATE_CHANNEL, IS_SHELL_INSTALL } from "./version.js";
|
|
12
|
+
import { isBrewManaged } from "./updater.js";
|
|
12
13
|
|
|
13
14
|
// eslint-disable-next-line no-undef
|
|
14
15
|
const POSTHOG_KEY = typeof __POSTHOG_KEY__ !== "undefined" ? __POSTHOG_KEY__ : (process.env.MM_POSTHOG_KEY || "");
|
|
@@ -20,6 +21,33 @@ const optedOut = () => {
|
|
|
20
21
|
};
|
|
21
22
|
const enabled = () => !!POSTHOG_KEY && !optedOut();
|
|
22
23
|
|
|
24
|
+
/** How this CLI got onto the machine, for real-user-metrics segmentation.
|
|
25
|
+
* - "homebrew": the binary lives under a brew prefix (shared with updater.js).
|
|
26
|
+
* - "npm": running through Node from a node_modules tree (npm -g / npx).
|
|
27
|
+
* - "binary": the curl|bash CDN binary (Bun-compiled, IS_SHELL_INSTALL=true) or
|
|
28
|
+
* anything else we can't otherwise place.
|
|
29
|
+
* Small + pure: takes its inputs so it's trivially testable. */
|
|
30
|
+
export function installMethod(execPath = process.execPath, scriptPath = process.argv[1] || "") {
|
|
31
|
+
if (isBrewManaged(execPath)) return "homebrew";
|
|
32
|
+
// npm/npx run Node directly with our script under a node_modules tree; the
|
|
33
|
+
// compiled CDN binary is its own execPath and never lives under node_modules.
|
|
34
|
+
if (!IS_SHELL_INSTALL && (/node_modules/.test(scriptPath) || /node_modules/.test(execPath))) return "npm";
|
|
35
|
+
return "binary";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Properties stamped onto EVERY captured event — version/os/install method, plus
|
|
39
|
+
* internal:true for our own dev machines (set MM_INTERNAL=1) so they can be
|
|
40
|
+
* excluded from real-user metrics. Kept tiny + side-effect free. */
|
|
41
|
+
function baseProps() {
|
|
42
|
+
const p = {
|
|
43
|
+
cli_version: BUILD_VERSION,
|
|
44
|
+
os: process.platform,
|
|
45
|
+
install_method: installMethod(),
|
|
46
|
+
};
|
|
47
|
+
if (process.env.MM_INTERNAL) p.internal = true;
|
|
48
|
+
return p;
|
|
49
|
+
}
|
|
50
|
+
|
|
23
51
|
let cachedId = null;
|
|
24
52
|
function distinctId() {
|
|
25
53
|
if (cachedId) return cachedId;
|
|
@@ -56,10 +84,9 @@ export function track(event, properties = {}) {
|
|
|
56
84
|
distinct_id: distinctId(),
|
|
57
85
|
properties: {
|
|
58
86
|
$process_person_profile: false, // anonymous events, no person profiles
|
|
59
|
-
cli_version: BUILD_VERSION,
|
|
60
87
|
channel: UPDATE_CHANNEL,
|
|
61
|
-
os: process.platform,
|
|
62
88
|
arch: process.arch,
|
|
89
|
+
...baseProps(), // cli_version, os, install_method, internal (every event)
|
|
63
90
|
...properties,
|
|
64
91
|
},
|
|
65
92
|
timestamp: new Date().toISOString(),
|
|
@@ -69,3 +96,24 @@ export function track(event, properties = {}) {
|
|
|
69
96
|
}).catch(() => {}).finally(() => clearTimeout(timer));
|
|
70
97
|
} catch { /* analytics must never break the CLI */ }
|
|
71
98
|
}
|
|
99
|
+
|
|
100
|
+
/** Emit a one-time `cli_first_run` event the first time the CLI runs per install.
|
|
101
|
+
* "First run" is marked by a `firstRunAt` ISO timestamp persisted to the config
|
|
102
|
+
* file — written on the first invocation that gets here and never re-emitted.
|
|
103
|
+
* Honors the SAME opt-out as every other event (track() gates on enabled(): no
|
|
104
|
+
* key, MM_NO_ANALYTICS / DO_NOT_TRACK / CI, or a persisted analyticsOptOut). The
|
|
105
|
+
* marker is still written when opted-out so flipping analytics on later doesn't
|
|
106
|
+
* retroactively fire a "first run". Never throws. */
|
|
107
|
+
export function maybeFirstRun() {
|
|
108
|
+
try {
|
|
109
|
+
let cfg;
|
|
110
|
+
try { cfg = loadConfig(); } catch { cfg = {}; }
|
|
111
|
+
if (cfg.firstRunAt) return; // already seen this install
|
|
112
|
+
try { setConfigValue("firstRunAt", new Date().toISOString()); } catch { /* best effort */ }
|
|
113
|
+
track("cli_first_run", {
|
|
114
|
+
install_method: installMethod(),
|
|
115
|
+
cli_version: BUILD_VERSION,
|
|
116
|
+
os: process.platform,
|
|
117
|
+
});
|
|
118
|
+
} catch { /* analytics must never break the CLI */ }
|
|
119
|
+
}
|
package/src/updater.js
CHANGED
|
@@ -127,9 +127,10 @@ function dirWritable(dir) {
|
|
|
127
127
|
|
|
128
128
|
/** True when the running binary lives under a Homebrew prefix. Brew owns the
|
|
129
129
|
* binary's lifecycle (`brew upgrade`), and silently swapping it out from under
|
|
130
|
-
* brew would break its integrity tracking — so we never self-update there.
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
* brew would break its integrity tracking — so we never self-update there.
|
|
131
|
+
* Exported so telemetry's install-method detection shares one definition. */
|
|
132
|
+
export function isBrewManaged(execPath = process.execPath) {
|
|
133
|
+
const p = execPath;
|
|
133
134
|
return p.includes("/Cellar/") || p.includes("/homebrew/") || p.includes("/linuxbrew/");
|
|
134
135
|
}
|
|
135
136
|
|