@modelstatus/cli 0.1.86 → 0.1.88
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 +16 -0
- package/package.json +7 -2
- package/src/api.js +41 -11
- package/src/changelog-data.js +25 -0
- package/src/ci.js +8 -4
- package/src/clipboard.js +42 -0
- package/src/detect/core.js +17 -6
- package/src/fix-prompt.js +106 -0
- package/src/fix.js +132 -46
- package/src/index.js +258 -38
- package/src/integrations.js +18 -3
- package/src/registry/fetch.js +16 -1
- package/src/sources/aws-lambda.js +9 -2
- package/src/sources/aws.js +7 -1
- package/src/sources/filesystem.js +0 -0
- package/src/sources/github-actions.js +9 -2
- package/src/sources/helm.js +9 -3
- package/src/sources/index.js +45 -3
- package/src/sources/k8s.js +6 -2
- package/src/sources/shell.js +126 -14
- package/src/sources/sql.js +6 -2
- package/src/sources/supabase-edge.js +19 -5
- package/src/sources/vercel.js +64 -6
- package/src/telemetry.js +21 -0
- package/src/tui/app.js +55 -13
- package/src/tui/game/launch.js +14 -2
- package/src/tui/signin.js +43 -7
- package/src/tui/views/account.js +19 -2
- package/src/tui/views/alerts.js +24 -9
- package/src/tui/views/inventory.js +57 -0
- package/src/tui/views/local.js +37 -1
- package/src/updater.js +160 -39
- package/src/upgrade.js +37 -8
package/src/fix.js
CHANGED
|
@@ -9,15 +9,23 @@
|
|
|
9
9
|
* - Replace ONLY on the recorded line. If the string is no longer there (file
|
|
10
10
|
* changed since the scan), the ref is reported "stale" and skipped — never
|
|
11
11
|
* guess. A rescan refreshes the locations.
|
|
12
|
-
* - Boundary-aware matching
|
|
13
|
-
* "gpt-4
|
|
14
|
-
*
|
|
12
|
+
* - Boundary-aware matching in LOCKSTEP with detect/core.js (same predicates,
|
|
13
|
+
* same case-insensitivity): "gpt-4" must never rewrite inside "gpt-4o" /
|
|
14
|
+
* "gpt-4-turbo", but provider prefixes ("anthropic.claude-…", "openai/…")
|
|
15
|
+
* are fine on the left — exactly what the scan matched, fix can rewrite.
|
|
16
|
+
* - Version-pinned occurrences ("claude-3-sonnet@20240229", Bedrock "-v1:0",
|
|
17
|
+
* ft ":acme", template "-${ver}") are NEVER base-swapped — the old model's
|
|
18
|
+
* pin glued to the new id is a nonexistent id. They skip with an explicit
|
|
19
|
+
* "pinned variant — review manually" note.
|
|
20
|
+
* - Never corrupt bytes: non-UTF-8 files are skipped loudly, and every line
|
|
21
|
+
* keeps its own \n / \r\n ending (one stray CRLF never converts the file).
|
|
15
22
|
* - Style-preserving replacement: if the code says "openai/gpt-4" the new id
|
|
16
23
|
* keeps the provider prefix; if it says "gpt-4" it stays bare.
|
|
17
24
|
*/
|
|
18
25
|
import fs from "node:fs";
|
|
19
26
|
import os from "node:os";
|
|
20
27
|
import path from "node:path";
|
|
28
|
+
import { isTokenChar, isPrefixSep, MODEL_SUFFIX } from "./detect/core.js";
|
|
21
29
|
|
|
22
30
|
/**
|
|
23
31
|
* Follow a replacement chain to the first CURRENT model. Replacements can
|
|
@@ -25,6 +33,12 @@ import path from "node:path";
|
|
|
25
33
|
* rewriting to a dying model just means fixing twice — land on the live one.
|
|
26
34
|
* `getModel(slug)` → model|null; `isCurrent(model)` → bool. Cycle-guarded;
|
|
27
35
|
* unknown slugs end the walk (best known answer wins).
|
|
36
|
+
*
|
|
37
|
+
* Returns the WRITE-FORM of the target — "provider/canonical_id" when the
|
|
38
|
+
* model is known (e.g. "openai/gpt-5.5"), else the registry slug. Registry
|
|
39
|
+
* slugs dash-encode dots ("openai/gpt-5-5"), and providers reject the slug
|
|
40
|
+
* form ("gpt-5-5" 404s where the API id is "gpt-5.5") — canonical_id is the
|
|
41
|
+
* provider's real API id. styleReplacement() derives both styles from it.
|
|
28
42
|
*/
|
|
29
43
|
export function terminalReplacement(startSlug, getModel, isCurrent) {
|
|
30
44
|
let cur = startSlug;
|
|
@@ -35,7 +49,14 @@ export function terminalReplacement(startSlug, getModel, isCurrent) {
|
|
|
35
49
|
if (!m || isCurrent(m) || !m.replacement_slug) break;
|
|
36
50
|
cur = m.replacement_slug;
|
|
37
51
|
}
|
|
38
|
-
|
|
52
|
+
const slug = cur || startSlug;
|
|
53
|
+
const m = getModel(slug);
|
|
54
|
+
if (m?.canonical_id) {
|
|
55
|
+
const provider = m.provider_slug
|
|
56
|
+
|| (typeof m.slug === "string" && m.slug.includes("/") ? m.slug.slice(0, m.slug.indexOf("/")) : "");
|
|
57
|
+
return provider ? `${provider}/${m.canonical_id}` : m.canonical_id;
|
|
58
|
+
}
|
|
59
|
+
return slug; // no canonical known — the slug is the best answer we have
|
|
39
60
|
}
|
|
40
61
|
|
|
41
62
|
/* ------------------------------------------------------------ fix history */
|
|
@@ -73,43 +94,94 @@ export function readFixes() {
|
|
|
73
94
|
}
|
|
74
95
|
}
|
|
75
96
|
|
|
76
|
-
/** The string to write into the file
|
|
77
|
-
*
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
97
|
+
/** The string to write into the file, styled to match how the old id was
|
|
98
|
+
* written (provider-prefixed vs bare). `replacement` is terminalReplacement's
|
|
99
|
+
* write-form ("provider/canonical_id", e.g. "openai/gpt-5.5") — bare originals
|
|
100
|
+
* get the canonical id after the first "/", prefixed originals the whole thing.
|
|
101
|
+
* (A plain registry slug still styles correctly, just without canonical ids.) */
|
|
102
|
+
export function styleReplacement(oldStr, replacement) {
|
|
103
|
+
if (!replacement) return null;
|
|
104
|
+
const i = replacement.indexOf("/");
|
|
105
|
+
const bare = i >= 0 ? replacement.slice(i + 1) : replacement;
|
|
106
|
+
return oldStr.includes("/") ? replacement : bare;
|
|
83
107
|
}
|
|
84
108
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
*
|
|
109
|
+
/** Boundary-safe replace of every occurrence of `from` in one line of text,
|
|
110
|
+
* with the SAME boundary semantics (and case-insensitivity) as detection —
|
|
111
|
+
* whatever the scan matched, this can find. Occurrences whose right context is
|
|
112
|
+
* a version pin of the OLD model ("@20240229", "-v1:0", ":acme", "-${ver}")
|
|
113
|
+
* are never base-swapped (the old pin glued to the new id is a nonexistent
|
|
114
|
+
* id); they're returned in `pinned` so callers can report them honestly.
|
|
115
|
+
* Returns { out, n, pinned } — the rewritten line, how many occurrences
|
|
116
|
+
* changed, and the pinned occurrences (with a bit of right context). */
|
|
90
117
|
export function replaceOnLine(lineText, from, to) {
|
|
91
|
-
const
|
|
118
|
+
const lower = lineText.toLowerCase();
|
|
119
|
+
const f = String(from ?? "").toLowerCase();
|
|
120
|
+
let out = "";
|
|
121
|
+
let last = 0;
|
|
92
122
|
let n = 0;
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
123
|
+
const pinned = [];
|
|
124
|
+
let at = 0;
|
|
125
|
+
while (f && (at = lower.indexOf(f, at)) >= 0) {
|
|
126
|
+
const before = at > 0 ? lower[at - 1] : "";
|
|
127
|
+
if (before && isTokenChar(before) && !isPrefixSep(before)) {
|
|
128
|
+
at += 1; // inside a longer identifier — a later occurrence may be clean
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const rest = lower.slice(at + f.length);
|
|
132
|
+
const after = rest[0] ?? "";
|
|
133
|
+
// Trailing separators the generic detector trims ("claude-3-5-sonnet-${ver}",
|
|
134
|
+
// "gpt-4."): a [-._]+ run NOT followed by more token chars.
|
|
135
|
+
const trimmedTail = rest.replace(/^[-._]+/, "");
|
|
136
|
+
const isPinned =
|
|
137
|
+
(after === "@" && /^@[a-z0-9]/.test(rest)) || // Vertex-style @-pin
|
|
138
|
+
(isTokenChar(after) && MODEL_SUFFIX.test(rest)) || // ':0' / '-v1' / '-20240229'
|
|
139
|
+
(/^[-._]/.test(rest) && !(trimmedTail && isTokenChar(trimmedTail[0])));
|
|
140
|
+
if (isPinned) {
|
|
141
|
+
const ctx = /^[^\s"'`]{0,16}/.exec(lineText.slice(at + f.length))?.[0] ?? "";
|
|
142
|
+
pinned.push(lineText.slice(at, at + f.length) + ctx);
|
|
143
|
+
at += f.length;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (after === "" || !isTokenChar(after)) {
|
|
147
|
+
out += lineText.slice(last, at) + to;
|
|
148
|
+
last = at + f.length;
|
|
149
|
+
n += 1;
|
|
150
|
+
at += f.length;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
at += 1; // embedded in a longer id (gpt-4 in gpt-4o) — keep looking
|
|
154
|
+
}
|
|
155
|
+
return { out: out + lineText.slice(last), n, pinned };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The skip reason for a line where nothing was rewritten. */
|
|
159
|
+
function skipError(pinned) {
|
|
160
|
+
return pinned.length
|
|
161
|
+
? `pinned variant "${pinned[0]}" — review manually`
|
|
162
|
+
: "string not on that line anymore — rescan";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Decode a buffer as UTF-8 only if it round-trips byte-for-byte — otherwise
|
|
166
|
+
* null (writing a lossy decode back would corrupt the file: 0xE9 → U+FFFD). */
|
|
167
|
+
function decodeUtf8Strict(buf) {
|
|
168
|
+
const text = buf.toString("utf8");
|
|
169
|
+
return Buffer.from(text, "utf8").equals(buf) ? text : null;
|
|
98
170
|
}
|
|
99
171
|
|
|
100
172
|
/**
|
|
101
173
|
* Build fix plans from scan refs. `refs` = [{ model_string, source_path,
|
|
102
|
-
* source_line }] (the shape the scanner emits); `
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* filesystem refs are rewritable.
|
|
174
|
+
* source_line }] (the shape the scanner emits); `replacement` is
|
|
175
|
+
* terminalReplacement's write-form ("provider/canonical_id") for the model
|
|
176
|
+
* those refs belong to. Refs without a real file path (integration scheme
|
|
177
|
+
* labels like vercel://) are skipped — only filesystem refs are rewritable.
|
|
106
178
|
*/
|
|
107
|
-
export function planFixes(refs,
|
|
179
|
+
export function planFixes(refs, replacement) {
|
|
108
180
|
const plans = [];
|
|
109
181
|
const seen = new Set();
|
|
110
182
|
for (const r of refs || []) {
|
|
111
183
|
if (!r?.source_path || !r.source_line || !r.model_string) continue;
|
|
112
|
-
const to = styleReplacement(r.model_string,
|
|
184
|
+
const to = styleReplacement(r.model_string, replacement);
|
|
113
185
|
if (!to || to === r.model_string) continue;
|
|
114
186
|
const key = `${r.source_path}:${r.source_line}:${r.model_string}`;
|
|
115
187
|
if (seen.has(key)) continue;
|
|
@@ -133,24 +205,29 @@ export function planFixes(refs, replacementSlug) {
|
|
|
133
205
|
export function previewFixes(dir, plans) {
|
|
134
206
|
const previews = [];
|
|
135
207
|
const stale = [];
|
|
136
|
-
const cache = new Map(); // file -> lines
|
|
208
|
+
const cache = new Map(); // file -> { lines } | { error } | {}
|
|
137
209
|
for (const p of plans) {
|
|
138
210
|
if (!cache.has(p.file)) {
|
|
211
|
+
let entry = {}; // unreadable → "file or line missing" below
|
|
139
212
|
try {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
213
|
+
const text = decodeUtf8Strict(fs.readFileSync(path.resolve(dir, p.file)));
|
|
214
|
+
entry = text == null ? { error: "not valid UTF-8 — skipped" } : { lines: text.split(/\r?\n/) };
|
|
215
|
+
} catch { /* keep {} */ }
|
|
216
|
+
cache.set(p.file, entry);
|
|
144
217
|
}
|
|
145
|
-
const
|
|
146
|
-
|
|
218
|
+
const ent = cache.get(p.file);
|
|
219
|
+
if (ent.error) {
|
|
220
|
+
stale.push({ ...p, error: ent.error });
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const before = ent.lines?.[p.line - 1];
|
|
147
224
|
if (before == null) {
|
|
148
225
|
stale.push({ ...p, error: "file or line missing — rescan" });
|
|
149
226
|
continue;
|
|
150
227
|
}
|
|
151
|
-
const { out, n } = replaceOnLine(before, p.from, p.to);
|
|
228
|
+
const { out, n, pinned } = replaceOnLine(before, p.from, p.to);
|
|
152
229
|
if (n === 0) {
|
|
153
|
-
stale.push({ ...p, error:
|
|
230
|
+
stale.push({ ...p, error: skipError(pinned) });
|
|
154
231
|
continue;
|
|
155
232
|
}
|
|
156
233
|
previews.push({ ...p, before, after: out });
|
|
@@ -177,13 +254,20 @@ export function applyFixes(dir, plans) {
|
|
|
177
254
|
const abs = path.resolve(dir, file);
|
|
178
255
|
let text;
|
|
179
256
|
try {
|
|
180
|
-
text = fs.readFileSync(abs
|
|
257
|
+
text = decodeUtf8Strict(fs.readFileSync(abs));
|
|
258
|
+
if (text == null) {
|
|
259
|
+
// A lossy decode written back would corrupt every non-UTF-8 byte in
|
|
260
|
+
// the file (0xE9 → U+FFFD) — refuse loudly instead.
|
|
261
|
+
for (const p of filePlans) failed.push({ ...p, error: "not valid UTF-8 — skipped" });
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
181
264
|
} catch (e) {
|
|
182
265
|
for (const p of filePlans) failed.push({ ...p, error: e.code === "ENOENT" ? "file not found" : e.message });
|
|
183
266
|
continue;
|
|
184
267
|
}
|
|
185
|
-
|
|
186
|
-
|
|
268
|
+
// Split KEEPING each line's own terminator so untouched lines round-trip
|
|
269
|
+
// byte-identical — one stray CRLF line must never convert the whole file.
|
|
270
|
+
const lines = text.split(/(?<=\n)/);
|
|
187
271
|
let dirty = false;
|
|
188
272
|
for (const p of filePlans) {
|
|
189
273
|
const idx = p.line - 1;
|
|
@@ -191,19 +275,21 @@ export function applyFixes(dir, plans) {
|
|
|
191
275
|
stale.push({ ...p, error: "line out of range — rescan" });
|
|
192
276
|
continue;
|
|
193
277
|
}
|
|
194
|
-
const
|
|
195
|
-
const
|
|
278
|
+
const raw = lines[idx];
|
|
279
|
+
const nl = /\r?\n$/.exec(raw)?.[0] ?? "";
|
|
280
|
+
const body = nl ? raw.slice(0, -nl.length) : raw;
|
|
281
|
+
const { out, n, pinned } = replaceOnLine(body, p.from, p.to);
|
|
196
282
|
if (n === 0) {
|
|
197
|
-
stale.push({ ...p, error:
|
|
283
|
+
stale.push({ ...p, error: skipError(pinned) });
|
|
198
284
|
continue;
|
|
199
285
|
}
|
|
200
|
-
lines[idx] = out;
|
|
286
|
+
lines[idx] = out + nl;
|
|
201
287
|
dirty = true;
|
|
202
|
-
applied.push({ ...p, count: n, before, after: out });
|
|
288
|
+
applied.push({ ...p, count: n, before: body, after: out });
|
|
203
289
|
}
|
|
204
290
|
if (dirty) {
|
|
205
291
|
try {
|
|
206
|
-
fs.writeFileSync(abs, lines.join(
|
|
292
|
+
fs.writeFileSync(abs, lines.join(""));
|
|
207
293
|
} catch (e) {
|
|
208
294
|
// Roll the bookkeeping back: everything in this file actually failed.
|
|
209
295
|
for (let i = applied.length - 1; i >= 0; i--) {
|