@modelstatus/cli 0.1.84 → 0.1.85

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.84",
3
+ "version": "0.1.85",
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/api.js CHANGED
@@ -90,7 +90,7 @@ export function createClient({ apiBase, apiKey }) {
90
90
  listNotifications: (params) => req("GET", `/notifications${qs(params)}`),
91
91
  readNotification: (id) => req("POST", `/notifications/${id}/read`),
92
92
 
93
- // billing
94
- checkout: () => req("POST", "/billing/checkout"),
93
+ // billing — plan: undefined → Pro ($5/yr subscription); "lifetime" → $29 one-time
94
+ checkout: (plan) => req("POST", `/billing/checkout${plan ? `?plan=${encodeURIComponent(plan)}` : ""}`),
95
95
  };
96
96
  }
@@ -1,6 +1,17 @@
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.85",
6
+ "date": "2026-06-14",
7
+ "title": "Command polish + safer billing",
8
+ "items": [
9
+ "`mm upgrade --lifetime` starts the one-time Lifetime checkout from the CLI (plain `mm upgrade` is still the $5/yr Pro plan).",
10
+ "`mm <command> --help` now prints per-command usage and flags; an unknown command exits non-zero instead of silently printing the help.",
11
+ "`mm clear` requires an explicit `--yes`/`--force` — it will no longer wipe your cloud inventory on `--ci` alone. `mm logout` no longer claims it removed a key when you weren't signed in, and warns if an env-var key still authenticates you.",
12
+ "Billing safety: checkout won't start a duplicate charge for an account that's already on Pro or Lifetime."
13
+ ]
14
+ },
4
15
  {
5
16
  "version": "0.1.84",
6
17
  "date": "2026-06-14",
package/src/index.js CHANGED
@@ -72,7 +72,7 @@ if (process.argv[2] === "__bench_frames") {
72
72
  const p = runGame({
73
73
  width: 80, height: 24, scanStore: scan,
74
74
  _inject: {
75
- out, inp: input, proc: process, now,
75
+ out, inp: input, proc: process, now, noPersist: true, // bench: never write dkHighScore
76
76
  schedule: (fn, ms) => setTimeout(() => { const t = now(); if (prev != null) emit({ t: "frame", dtMs: t - prev }); prev = t; fn(); }, ms),
77
77
  cancel: (h) => clearTimeout(h),
78
78
  },
@@ -181,8 +181,14 @@ async function cmdSignup(_positional, flags) {
181
181
  }
182
182
 
183
183
  function cmdLogout() {
184
+ const had = !!loadConfig().apiKey;
184
185
  clearAuth();
185
- console.log("✓ Signed out (API key removed). Run `mm login` to sign back in.");
186
+ if (had) console.log("✓ Signed out (saved API key removed). Run `mm login` to sign back in.");
187
+ else console.log("Not signed in — no saved API key to remove.");
188
+ // clearAuth only touches the config file; an env key still authenticates.
189
+ if (process.env.LLMSTATUS_API_KEY || process.env.MM_API_KEY) {
190
+ console.log("Note: an API key is still set via LLMSTATUS_API_KEY/MM_API_KEY — unset it to fully sign out.");
191
+ }
186
192
  }
187
193
 
188
194
  async function cmdUpgrade(_positional, flags) {
@@ -193,7 +199,9 @@ async function cmdUpgrade(_positional, flags) {
193
199
  }
194
200
  const client = createClient({ apiBase, apiKey });
195
201
  const { upgradeViaBrowser } = await import("./upgrade.js");
196
- const plan = await upgradeViaBrowser({ client });
202
+ // --lifetime opens the one-time $29 checkout; default is the $5/yr Pro subscription.
203
+ const checkoutPlan = flags.lifetime ? "lifetime" : undefined;
204
+ const plan = await upgradeViaBrowser({ client, plan: checkoutPlan });
197
205
  if (!plan) {
198
206
  console.error("Upgrade not detected (timed out). Run `mm upgrade` again if you completed checkout.");
199
207
  process.exit(1);
@@ -554,9 +562,13 @@ async function cmdClear(_positional, flags) {
554
562
  }
555
563
  const all = !!flags.all;
556
564
  const scope = all ? "ALL usages, projects, alert rules + the in-app feed" : "ALL usages";
557
- if (!flags.yes && !flags.force) {
565
+ // A destructive delete requires an EXPLICIT --yes/--force. `--ci` implies --yes
566
+ // for scan/fix non-interactivity, but it must NOT silently skip the safety
567
+ // prompt here — so check the raw argv, not the --ci-derived flags.yes.
568
+ const explicitConfirm = process.argv.includes("--yes") || process.argv.includes("--force");
569
+ if (!explicitConfirm) {
558
570
  if (!process.stdin.isTTY) {
559
- console.error(`Refusing to delete ${scope} without confirmation. Re-run with --yes.`);
571
+ console.error(`Refusing to delete ${scope} without confirmation. Re-run with --yes (or --force).`);
560
572
  process.exit(1);
561
573
  }
562
574
  const ok = await confirm(`Delete ${scope} from your account? This cannot be undone. [y/N] `);
@@ -856,7 +868,7 @@ Usage:
856
868
  mm integrations Manage live integrations (list | enable <id> | disable <id> | env <id> <tag>)
857
869
  mm clear Delete all tracked usages from your inventory (--all also wipes projects/rules; --yes to skip the prompt)
858
870
  mm update Update to the latest version now and relaunch (or add --update to any command)
859
- mm upgrade Open Stripe checkout and poll until Pro is active (the paid plan, not the binary)
871
+ mm upgrade Open Stripe checkout and poll until active (--lifetime for the one-time plan; this is the paid plan, not the binary)
860
872
  mm play [dir] Play Donkey Kong while a background scan walks the dir (just for fun)
861
873
  mm tui Force-launch the TUI (needs an interactive terminal)
862
874
 
@@ -875,7 +887,82 @@ Flags: --update · --api <url> · --key <key> · --project <id|name> · --dir <p
875
887
  --sources <list> · --region <r> · --namespace <ns> · --kube-context <c> · --db <dsn> · --sql-table <t>
876
888
  --vercel-project <p> · --vercel-team <t> · --gh-repo <owner/name> · --supabase-ref <ref>
877
889
 
878
- Get started: \`mm login\` (opens your browser).`;
890
+ Get started: \`mm login\` (opens your browser).
891
+
892
+ Per-command help: \`mm <command> --help\` (e.g. \`mm ci --help\`).`;
893
+
894
+ /** Per-command usage. `mm <cmd> --help` / `mm help <cmd>` prints these; anything
895
+ * not listed falls back to the global HELP. Keep flags here in sync with parseArgs. */
896
+ const COMMAND_HELP = {
897
+ status: `mm status [dir] Offline, account-less model-health check (free).
898
+
899
+ Pulls the signed registry, scans the dir locally, prints each model in use with
900
+ its health + replacement, then the custom/unrecognized ids. Always exits 0 (it
901
+ informs; use \`mm ci\` to gate a build).
902
+
903
+ --json machine-readable {registry, scanned, references, files, models[], custom[], needs_attention}
904
+ --offline use the cached registry only (no network)
905
+ --sources <list> detection sources (default: filesystem + enabled integrations; "all" for everything)
906
+ --dir <path> directory to scan (alternative to the positional arg)`,
907
+ fix: `mm fix [dir] Rewrite dying model ids to their registry replacement, in place.
908
+
909
+ Boundary-safe, style-preserving, chain-resolved. Only filesystem refs with a known
910
+ replacement are touched. Asks before writing (a non-TTY needs --yes).
911
+
912
+ --dry-run preview the rewrites, write nothing
913
+ --json machine output ({planned, dryRun} for --dry-run; {applied, stale, failed} on apply)
914
+ --yes skip the confirmation prompt
915
+ --model <slug> only fix this one model (full provider/slug)
916
+ --offline use the cached registry only`,
917
+ ci: `mm ci [dir] CI gate: fail the build on deprecated/retiring models.
918
+
919
+ Exits non-zero when a finding is at/above --fail-on. Emits GitHub annotations +
920
+ a step summary under GITHUB_ACTIONS. Offline-capable, no account.
921
+
922
+ --fail-on <none|deprecating|retiring|retired> threshold (default: retired)
923
+ --json print the full report to stdout
924
+ --json-out <file> write clean findings JSON to a file (stdout stays annotations-only)
925
+ --diff <base> limit findings to files changed vs base (auto on PRs via GITHUB_BASE_REF)
926
+ --report (Pro) sync this run's usages + a CI-run row to your account
927
+ --offline use the cached registry only`,
928
+ scan: `mm scan [dir] Scan for model usage and upload to your account's inventory (needs login).
929
+
930
+ On a TTY with no flags it opens the interactive Scan tab. --ci/--json/--yes run
931
+ non-interactively.
932
+
933
+ --dry-run show exactly what WOULD upload, upload nothing
934
+ --json / --ci machine output (--ci implies --yes + --json)
935
+ --yes upload without the interactive TUI
936
+ --project <id|name> route everything to one project (created if the name is new)
937
+ --sources <list> detection sources ("all" for everything)
938
+ --dir <path> directory to scan`,
939
+ clear: `mm clear Delete tracked usages from your cloud inventory (DESTRUCTIVE, needs login).
940
+
941
+ Requires an explicit --yes or --force (it will NOT proceed on --ci alone).
942
+
943
+ --all also wipe projects, alert rules + the in-app feed (a full reset)
944
+ --yes / --force confirm the delete (required on a non-TTY)
945
+ --json print the result counts`,
946
+ upgrade: `mm upgrade Open Stripe checkout and poll until your plan is active (needs login).
947
+
948
+ --lifetime buy the one-time Lifetime plan ($29) instead of Pro ($5/yr)`,
949
+ integrations: `mm integrations [list | enable <id> | disable <id> | env <id> <tag>]
950
+
951
+ Manage the local on/off state of the live integrations (the gate for what
952
+ \`mm scan\`/\`mm status\` run by default). Ids: aws-lambda, vercel, supabase-edge,
953
+ github-actions. \`env <id> <prod|staging|dev|unknown>\` sets a declared env.
954
+
955
+ --json (list only) machine-readable integration state`,
956
+ config: `mm config [analytics on|off] View or change local settings.
957
+
958
+ Bare \`mm config\` lists settings (analytics, update channel, config path).
959
+ \`mm config analytics on|off\` toggles anonymous usage analytics (also honored:
960
+ MM_NO_ANALYTICS=1, DO_NOT_TRACK=1, CI=1).`,
961
+ login: `mm login [api_key] Sign in. With no key, opens the browser and polls; or paste a key.
962
+
963
+ --key <key> paste an API key directly
964
+ --api <url> override the API base`,
965
+ };
879
966
 
880
967
  /** Awaits the updater promise; prints a one-liner if an update completed. Never throws. */
881
968
  async function maybePrintUpdate(promise) {
@@ -932,8 +1019,10 @@ async function main() {
932
1019
 
933
1020
  // --help / -h / help: print usage + exit. MUST come before the no-arg → TUI
934
1021
  // fallthrough below (a bare `mm` launches the TUI, but `mm --help` must not).
1022
+ // `mm <cmd> --help` and `mm help <cmd>` print per-command usage when we have it.
935
1023
  if (cmd === "help" || flags.help || flags.h) {
936
- console.log(HELP);
1024
+ const topic = cmd === "help" ? positional[1] : cmd;
1025
+ console.log((topic && COMMAND_HELP[topic]) || HELP);
937
1026
  return;
938
1027
  }
939
1028
 
@@ -1007,8 +1096,13 @@ async function main() {
1007
1096
  flags.dir = cmd;
1008
1097
  await launchTui(positional[1], flags);
1009
1098
  }
1010
- else if (cmd === "help" || flags.help) console.log(HELP);
1011
- else console.log(HELP);
1099
+ else {
1100
+ // Unknown command / non-existent path: name it on stderr + exit non-zero so
1101
+ // a typo in a script fails loudly instead of silently printing help.
1102
+ console.error(`Unknown command or path: ${cmd}\n`);
1103
+ console.log(HELP);
1104
+ process.exit(1);
1105
+ }
1012
1106
  } catch (e) {
1013
1107
  console.error(`Error: ${e?.message ?? e}`);
1014
1108
  await maybePrintUpdate(updatePromise);
@@ -139,7 +139,9 @@ export function runGame({ width, height, level = 1, scanStore = null, onExit, in
139
139
  if (highSaved) return;
140
140
  highSaved = true;
141
141
  const score = (game && Math.max(game.best || 0, game.score || 0)) || 0;
142
- if (score > best) {
142
+ // _inject.noPersist (the __bench_frames seam) keeps a measurement run from
143
+ // writing dkHighScore to the user's config.
144
+ if (score > best && !_inject.noPersist) {
143
145
  try { setConfigValue("dkHighScore", score); best = score; } catch { /* best effort */ }
144
146
  }
145
147
  }
package/src/upgrade.js CHANGED
@@ -4,8 +4,8 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
4
4
 
5
5
  /** Open Stripe checkout and poll /me until the plan flips off "free". Mirrors
6
6
  * the browser-login poll UX. Returns the new plan, or null on timeout. */
7
- export async function upgradeViaBrowser({ client, log = console.error, onTick }) {
8
- const { url } = await client.checkout();
7
+ export async function upgradeViaBrowser({ client, plan, log = console.error, onTick }) {
8
+ const { url } = await client.checkout(plan);
9
9
  if (!url) throw new Error("Could not start checkout.");
10
10
  log(`\n Opening checkout in your browser…`);
11
11
  log(` If it doesn't open, visit:\n ${url}\n`);