@modelstatus/cli 0.1.40 → 0.1.42
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 +7 -18
- package/src/sources/shell.js +43 -1
- package/src/sources/supabase-edge.js +7 -3
- package/src/sources/vercel.js +3 -1
- package/src/tui/game/dk-core.js +166 -17
- package/src/tui/source-meta.js +83 -0
- package/src/tui/views/inventory.js +21 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modelstatus/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.42",
|
|
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
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
getEnvTag, setEnabled, setEnvTag,
|
|
11
11
|
} from "./integrations.js";
|
|
12
12
|
import { redactValue } from "./redact.js";
|
|
13
|
-
import { assignProjects } from "./upload.js";
|
|
13
|
+
import { assignProjects, buildUsages } from "./upload.js";
|
|
14
14
|
import { loginViaBrowser } from "./auth.js";
|
|
15
15
|
import { maybeCheckForUpdate } from "./updater.js";
|
|
16
16
|
import { track, maybeAnalyticsNotice } from "./telemetry.js";
|
|
@@ -109,12 +109,6 @@ function parseArgs(argv) {
|
|
|
109
109
|
|
|
110
110
|
const uuidish = (s) => /^[0-9a-f]{8}-[0-9a-f]{4}-/i.test(s || "");
|
|
111
111
|
|
|
112
|
-
/** The "owner/name" repo slug for source deep-links. In GitHub Actions
|
|
113
|
-
* GITHUB_REPOSITORY is exactly that and the checkout is repo-root-relative (so
|
|
114
|
-
* our source_path lines up). Outside CI we return "" (omit it) rather than guess
|
|
115
|
-
* from a git remote, since a local scan root may not be the repo root. */
|
|
116
|
-
const ghRepoSlug = () => (process.env.GITHUB_REPOSITORY || "").trim();
|
|
117
|
-
|
|
118
112
|
/** The set of source ids the user named VERBATIM in --sources (empty for the
|
|
119
113
|
* default / "all"). Naming a live integration here overrides its enabled-gate. */
|
|
120
114
|
function explicitSources(flags) {
|
|
@@ -312,17 +306,12 @@ async function cmdScan(positional, flags) {
|
|
|
312
306
|
return true;
|
|
313
307
|
});
|
|
314
308
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
location_label: r.location_label,
|
|
322
|
-
source_repo: ghRepoSlug() || undefined,
|
|
323
|
-
source_path: r.source_path,
|
|
324
|
-
source_line: r.source_line ?? undefined,
|
|
325
|
-
}));
|
|
309
|
+
// Build the upload payload via the SHARED helper so the command, the TUI Scan
|
|
310
|
+
// tab, and the Here push produce byte-identical usages (model resolution +
|
|
311
|
+
// redaction + the location_label scheme prefix that carries provenance). Parity-
|
|
312
|
+
// only: buildUsages reads the same fields the inline map did and falls back to
|
|
313
|
+
// GITHUB_REPOSITORY for source_repo. No new data is uploaded.
|
|
314
|
+
const usages = await buildUsages(client, rows);
|
|
326
315
|
|
|
327
316
|
// --dry-run: show exactly what WOULD upload (secret-source safety check).
|
|
328
317
|
if (flags["dry-run"]) {
|
package/src/sources/shell.js
CHANGED
|
@@ -2,6 +2,31 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
+
/** Test-only fixture file name for a command+args, e.g. run("vercel",["env","ls",
|
|
6
|
+
* "production","--scope","team"]) → "vercel.env.ls.production.txt". Per-run scoping
|
|
7
|
+
* flags (--scope/--project-ref/--region) AND the value that follows each are
|
|
8
|
+
* dropped so a fixture is keyed on the stable command shape. Lowercased, path
|
|
9
|
+
* separators → dots. */
|
|
10
|
+
const FIXTURE_SKIP_FLAGS = new Set(["--scope", "--project-ref", "--region"]);
|
|
11
|
+
export function fixtureName(cmd, args = []) {
|
|
12
|
+
const kept = [];
|
|
13
|
+
for (let i = 0; i < args.length; i++) {
|
|
14
|
+
if (FIXTURE_SKIP_FLAGS.has(String(args[i]))) {
|
|
15
|
+
i++; // also skip the flag's value
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
kept.push(args[i]);
|
|
19
|
+
}
|
|
20
|
+
return (
|
|
21
|
+
[cmd, ...kept]
|
|
22
|
+
.join(".")
|
|
23
|
+
.toLowerCase()
|
|
24
|
+
.replace(/[/\\]/g, ".")
|
|
25
|
+
.replace(/[^a-z0-9._-]/g, "")
|
|
26
|
+
.slice(0, 120) + ".txt"
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
5
30
|
/** Is `name` an executable on PATH? Pure PATH scan — no shell, no spawn. */
|
|
6
31
|
export function hasCmd(name) {
|
|
7
32
|
const PATH = process.env.PATH || "";
|
|
@@ -18,8 +43,25 @@ export function hasCmd(name) {
|
|
|
18
43
|
}
|
|
19
44
|
|
|
20
45
|
/** Run a command WITHOUT a shell (execFile → no injection). Read-only by
|
|
21
|
-
* convention. Never throws; resolves { ok, stdout, stderr, code }.
|
|
46
|
+
* convention. Never throws; resolves { ok, stdout, stderr, code }.
|
|
47
|
+
*
|
|
48
|
+
* TEST-ONLY: when process.env.MM_SOURCE_FIXTURE is set (a directory of canned
|
|
49
|
+
* vendor outputs), this REPLAYS a fixture file instead of spawning — so the full
|
|
50
|
+
* vercel/supabase collect() flow runs with zero cloud access. It is strictly less
|
|
51
|
+
* capable than the real path (read-only, no spawn / write / network), a pure no-op
|
|
52
|
+
* when the env var is unset (prod default), and a missing fixture degrades exactly
|
|
53
|
+
* like a missing CLI ({ok:false, code:127}). It is the symmetric twin of the
|
|
54
|
+
* LLMSTATUS_INTEGRATIONS_FILE / LLMSTATUS_IGNORE_FILE test redirects. */
|
|
22
55
|
export function run(cmd, args, { timeout = 25000, input, maxBuffer = 48 * 1024 * 1024 } = {}) {
|
|
56
|
+
const fixtureDir = process.env.MM_SOURCE_FIXTURE;
|
|
57
|
+
if (fixtureDir) {
|
|
58
|
+
try {
|
|
59
|
+
const stdout = fs.readFileSync(path.join(fixtureDir, fixtureName(cmd, args)), "utf8");
|
|
60
|
+
return Promise.resolve({ ok: true, stdout, stderr: "", code: 0 });
|
|
61
|
+
} catch {
|
|
62
|
+
return Promise.resolve({ ok: false, stdout: "", stderr: "no fixture", code: 127 });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
23
65
|
return new Promise((resolve) => {
|
|
24
66
|
const child = execFile(cmd, args, { timeout, maxBuffer }, (err, stdout, stderr) => {
|
|
25
67
|
resolve({ ok: !err, stdout: stdout || "", stderr: stderr || "", code: err?.code ?? 0 });
|
|
@@ -114,7 +114,9 @@ export const supabaseEdgeSource = {
|
|
|
114
114
|
integration: true,
|
|
115
115
|
envTag: "unknown",
|
|
116
116
|
async available() {
|
|
117
|
-
|
|
117
|
+
// MM_SOURCE_FIXTURE is a test-only OR (no CLI / token needed for the no-cred
|
|
118
|
+
// fixture flow); the real hasCmd PATH check is unchanged.
|
|
119
|
+
return hasCmd("supabase") || !!process.env.SUPABASE_ACCESS_TOKEN || !!process.env.MM_SOURCE_FIXTURE;
|
|
118
120
|
},
|
|
119
121
|
/** Read-only identity probe (MAY spawn). Used by the TUI "test" key + verbose
|
|
120
122
|
* `mm sources`, NOT by the hot collect path. */
|
|
@@ -135,8 +137,10 @@ export const supabaseEdgeSource = {
|
|
|
135
137
|
const out = [];
|
|
136
138
|
|
|
137
139
|
// (a) Secret NAMES only (never values). Requires the CLI; skipped gracefully
|
|
138
|
-
// when only a token is present (no shell to list through).
|
|
139
|
-
|
|
140
|
+
// when only a token is present (no shell to list through). Under the test-only
|
|
141
|
+
// MM_SOURCE_FIXTURE, `run` replays a canned table so this branch is exercised
|
|
142
|
+
// end-to-end without the CLI.
|
|
143
|
+
if (hasCmd("supabase") || process.env.MM_SOURCE_FIXTURE) {
|
|
140
144
|
const refArg = opts?.supabaseProjectRef ? ["--project-ref", opts.supabaseProjectRef] : [];
|
|
141
145
|
const secrets = await run("supabase", ["secrets", "list", ...refArg]);
|
|
142
146
|
if (secrets.ok) {
|
package/src/sources/vercel.js
CHANGED
|
@@ -44,7 +44,9 @@ export const vercelSource = {
|
|
|
44
44
|
integration: true,
|
|
45
45
|
envTag: "unknown",
|
|
46
46
|
async available() {
|
|
47
|
-
|
|
47
|
+
// MM_SOURCE_FIXTURE is a test-only OR (lights up the spawn surface with no CLI
|
|
48
|
+
// / token so the no-cred fixture flow runs); the real hasCmd PATH check stays.
|
|
49
|
+
return hasCmd("vercel") || !!process.env.VERCEL_TOKEN || !!process.env.MM_SOURCE_FIXTURE;
|
|
48
50
|
},
|
|
49
51
|
async authState() {
|
|
50
52
|
const r = await run("vercel", ["whoami"]);
|
package/src/tui/game/dk-core.js
CHANGED
|
@@ -85,7 +85,7 @@ export function bonusForLevel(level) {
|
|
|
85
85
|
return 5000 + level * 1000;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
const SCHEMA =
|
|
88
|
+
const SCHEMA = 3; // bumped: board contract now varies per level (N/slope/ladder placement)
|
|
89
89
|
|
|
90
90
|
// ---- Level scaling (pure, tested monotonic) --------------------------------
|
|
91
91
|
/** DK barrel-throw cadence in TICKS — faster (shorter) as you climb. */
|
|
@@ -121,16 +121,145 @@ export function boardSize(width, height) {
|
|
|
121
121
|
return { BOARD_W, BOARD_H };
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
function buildSlope(baseRow, w, down, amp) {
|
|
124
|
+
function buildSlope(baseRow, w, down, amp, slopeEvery = SLOPE_EVERY) {
|
|
125
125
|
const offs = new Array(w);
|
|
126
126
|
for (let x = 0; x < w; x++) {
|
|
127
|
-
const steps = Math.floor(x /
|
|
127
|
+
const steps = Math.floor(x / slopeEvery);
|
|
128
128
|
const off = down ? steps : -steps;
|
|
129
129
|
offs[x] = baseRow + Math.max(-amp, Math.min(amp, off));
|
|
130
130
|
}
|
|
131
131
|
return offs;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
// ---- Per-level layout generator (PURE, deterministic) ----------------------
|
|
135
|
+
/* `levelLayout(level, BOARD_W, BOARD_H)` returns the few geometry knobs initGame
|
|
136
|
+
* reads instead of hardcoded values, keyed on k = (level-1) % 10 so levels 1-10
|
|
137
|
+
* are distinct and 11+ cycle (L11 === L1). It is a pure function of its three
|
|
138
|
+
* args only — no Math.random / Date.now / rngSeed — so the same (size, level)
|
|
139
|
+
* yields a byte-identical board on node and the bun binary (INVARIANT 4).
|
|
140
|
+
*
|
|
141
|
+
* Every knob is CLAMPED to the feasible window so it can never break an invariant:
|
|
142
|
+
* - N is clamped to [3, Nmax(BOARD_H)] (Nmax = largest N<=5 with gap>=GAP_MIN),
|
|
143
|
+
* so on the minimum 28x12 board every level collapses to N=3 (== today) and a
|
|
144
|
+
* small terminal never gets an unsafe extra girder. (INVARIANT 3.)
|
|
145
|
+
* - amp is only ever LOWERED below the existing floor((gap-1)/2) clamp, AND
|
|
146
|
+
* further capped so every ladder spans >=2 rows (gap-2*amp>=2 → amp<=(gap-2)/2)
|
|
147
|
+
* — so girders never cross (amp<gap/2, INVARIANT 2) and ladders are never the
|
|
148
|
+
* 1-row fragile latches the critic flagged (B3).
|
|
149
|
+
* - ladder columns are clamped to [2, BOARD_W-3] (double-wide [col,col+1] fits,
|
|
150
|
+
* INVARIANT 1) AND nudged so no two ladders' footprints overlap (>=2 apart),
|
|
151
|
+
* which the critic flagged would otherwise shadow a ladder in ladderAt (B2).
|
|
152
|
+
* - dkCol / princessCol clamped to [2, BOARD_W-3].
|
|
153
|
+
* The slope DIRECTION/cadence knobs (slopeDownFor, slopeEvery) only move points
|
|
154
|
+
* WITHIN the ±amp band, so no pattern can induce a crossing. Up-slope traversal
|
|
155
|
+
* (the critic's B1) is handled in stepPlaying's grounded branch (riser auto-mount).
|
|
156
|
+
*/
|
|
157
|
+
const GAP_MIN = 4; // min girder spacing for the N-delta to take effect on tall boards
|
|
158
|
+
const clampInt = (v, lo, hi) => (v < lo ? lo : v > hi ? hi : v);
|
|
159
|
+
// Per-k knob tables (k = (level-1) % 10). All bounded; clamps make them safe.
|
|
160
|
+
const L_NDELTA = [0, 0, 1, 0, 1, 0, 2, 1, 0, 2]; // extra girders (capped at Nmax)
|
|
161
|
+
const L_AMPCAP = [2, 1, 2, 0, 2, 1, 2, 1, 2, 0]; // some levels flatter (amp 0/1)
|
|
162
|
+
const L_SLOPEEV = [6, 5, 7, 6, 4, 6, 8, 5, 6, 4]; // stair cadence (cosmetic)
|
|
163
|
+
const L_SLOPEPAT = [0, 0, 1, 2, 3, 4, 5, 6, 7, 1]; // slope-direction pattern id
|
|
164
|
+
const L_LADSTY = [0, 1, 0, 5, 4, 0, 0, 1, 5, 0]; // ladder placement style id
|
|
165
|
+
const L_STAGMAG = [3, 2, 4, 3, 5, 2, 3, 4, 2, 3]; // ladder stagger magnitude
|
|
166
|
+
const L_DKSTY = [0, 0, 1, 0, 2, 3, 1, 0, 2, 3]; // DK column style id (cosmetic)
|
|
167
|
+
const L_PRSTY = [0, 0, 1, 2, 0, 3, 4, 0, 5, 0]; // princess column style id
|
|
168
|
+
|
|
169
|
+
/** Largest girder count N in [3,5] whose even spacing gap>=GAP_MIN; else 3. The
|
|
170
|
+
* Math.max(3,…) floor (not the gap gate) is what guarantees the small board. */
|
|
171
|
+
function nMaxForHeight(BOARD_H) {
|
|
172
|
+
let best = 3;
|
|
173
|
+
for (let n = 3; n <= 5; n++) if (Math.floor((BOARD_H - 2) / n) >= GAP_MIN) best = n;
|
|
174
|
+
return best;
|
|
175
|
+
}
|
|
176
|
+
/** Per-girder slope direction (down-rightward) for a slope pattern. ANY value is
|
|
177
|
+
* safe (clamped to ±amp around distinct baseRows → never crosses). */
|
|
178
|
+
function slopeDownFor(pat, i) {
|
|
179
|
+
switch (pat) {
|
|
180
|
+
case 1: return i % 2 === 1; // alternating from odd (phase flip)
|
|
181
|
+
case 2: return true; // all-down
|
|
182
|
+
case 3: return false; // all-up
|
|
183
|
+
case 4: return Math.floor(i / 2) % 2 === 0; // zig-zag pairs
|
|
184
|
+
case 5: return true; // all-down (gentle via slopeEvery)
|
|
185
|
+
case 6: return false; // all-up
|
|
186
|
+
case 7: return i % 2 === 0; // alternating from even
|
|
187
|
+
case 0:
|
|
188
|
+
default: return i % 2 === 0; // alternating from even (classic)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
export function levelLayout(level, BOARD_W, BOARD_H) {
|
|
192
|
+
const k = (((level - 1) % 10) + 10) % 10;
|
|
193
|
+
const Nmax = nMaxForHeight(BOARD_H);
|
|
194
|
+
const N = clampInt(3 + L_NDELTA[k], 3, Nmax);
|
|
195
|
+
const gap = Math.floor((BOARD_H - 2) / N);
|
|
196
|
+
// amp: never above today's floor((gap-1)/2); also cap so every ladder spans
|
|
197
|
+
// >=2 rows (gap - 2*amp >= 2 ⇒ amp <= (gap-2)/2). amp<gap/2 ⇒ no girder cross.
|
|
198
|
+
let amp = Math.max(0, Math.min(L_AMPCAP[k], Math.floor((gap - 1) / 2)));
|
|
199
|
+
amp = Math.max(0, Math.min(amp, Math.floor((gap - 2) / 2)));
|
|
200
|
+
const slopeEvery = L_SLOPEEV[k];
|
|
201
|
+
const pat = L_SLOPEPAT[k];
|
|
202
|
+
const sty = L_LADSTY[k];
|
|
203
|
+
const stag = L_STAGMAG[k];
|
|
204
|
+
const lo = 2, hi = BOARD_W - 3;
|
|
205
|
+
|
|
206
|
+
// Ladder columns: one per adjacent pair, per-level placement style, then nudged
|
|
207
|
+
// so no two footprints [col,col+1] overlap (>=2 apart) and all in [lo,hi].
|
|
208
|
+
const cols = [];
|
|
209
|
+
for (let i = 0; i < N - 1; i++) {
|
|
210
|
+
const frac = (i + 1) / N;
|
|
211
|
+
let base;
|
|
212
|
+
switch (sty) {
|
|
213
|
+
case 1: base = lo + 2 + i * 2 + (i % 2 ? 0 : 1); break; // clustered-left
|
|
214
|
+
case 2: base = hi - 2 - i * 2; break; // clustered-right
|
|
215
|
+
case 3: base = Math.floor(frac * BOARD_W) + (i % 2 ? stag : -stag); break; // reversed stagger
|
|
216
|
+
case 4: base = hi - 3 - (N - 2 - i) * 2 - (i % 2 ? stag : 0); break; // clustered-right staggered
|
|
217
|
+
case 5: base = (i % 2 === 0) ? lo + 1 + i : hi - 1 - i; break; // near-edges alternating
|
|
218
|
+
case 0:
|
|
219
|
+
default: base = Math.floor(frac * BOARD_W) + (i % 2 ? -stag : stag); // spread (classic)
|
|
220
|
+
}
|
|
221
|
+
cols.push(clampInt(base, lo, hi));
|
|
222
|
+
}
|
|
223
|
+
for (let i = 1; i < cols.length; i++) {
|
|
224
|
+
for (let j = 0; j < i; j++) {
|
|
225
|
+
let guard = 0;
|
|
226
|
+
while (Math.abs(cols[i] - cols[j]) < 2 && guard++ < 256) {
|
|
227
|
+
cols[i] = cols[i] + 2 <= hi ? cols[i] + 2 : cols[i] - 2;
|
|
228
|
+
if (cols[i] < lo) { cols[i] = lo; break; }
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
cols[i] = clampInt(cols[i], lo, hi);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let dkCol;
|
|
235
|
+
switch (L_DKSTY[k]) {
|
|
236
|
+
case 1: dkCol = BOARD_W - 3; break;
|
|
237
|
+
case 2: dkCol = BOARD_W - 4; break;
|
|
238
|
+
case 3: dkCol = 3; break;
|
|
239
|
+
case 0:
|
|
240
|
+
default: dkCol = 2;
|
|
241
|
+
}
|
|
242
|
+
const cen = Math.floor(BOARD_W / 2);
|
|
243
|
+
let princessCol;
|
|
244
|
+
switch (L_PRSTY[k]) {
|
|
245
|
+
case 1: princessCol = Math.floor(BOARD_W / 3); break;
|
|
246
|
+
case 2: princessCol = Math.floor((2 * BOARD_W) / 3); break;
|
|
247
|
+
case 3: princessCol = Math.floor(BOARD_W / 4); break;
|
|
248
|
+
case 4: princessCol = Math.floor((3 * BOARD_W) / 4); break;
|
|
249
|
+
case 5: princessCol = cen + 4; break;
|
|
250
|
+
case 0:
|
|
251
|
+
default: princessCol = cen;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
k, N, gap, amp, slopeEvery, slopePat: pat,
|
|
256
|
+
slopeDownFor: (i) => slopeDownFor(pat, i),
|
|
257
|
+
ladderCols: cols,
|
|
258
|
+
dkCol: clampInt(dkCol, lo, hi),
|
|
259
|
+
princessCol: clampInt(princessCol, lo, hi),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
134
263
|
/** Girder index whose standing row is at-or-just-below row y at column x. */
|
|
135
264
|
function girderBelow(platforms, x, y) {
|
|
136
265
|
let best = -1, bestRow = Infinity;
|
|
@@ -182,34 +311,40 @@ export function initGame({
|
|
|
182
311
|
const { BOARD_W, BOARD_H } = boardSize(width, height);
|
|
183
312
|
if (BOARD_W < MIN_W || BOARD_H < MIN_H) return { tooSmall: true, BOARD_W, BOARD_H };
|
|
184
313
|
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const
|
|
314
|
+
// Per-level layout: a PURE function of (level, BOARD_W, BOARD_H) — see
|
|
315
|
+
// levelLayout for the clamps that keep every knob within its invariant window.
|
|
316
|
+
// Fewer girders → bigger vertical gaps → real jump headroom (~4+ rows apart so
|
|
317
|
+
// the higher JUMP_VY reads as a tall jump); levelLayout's Nmax(BOARD_H) gate
|
|
318
|
+
// preserves that on small boards (N collapses to 3 exactly as the old gen did).
|
|
319
|
+
const layout = levelLayout(level, BOARD_W, BOARD_H);
|
|
320
|
+
const { N, gap, amp, slopeEvery } = layout;
|
|
191
321
|
const platforms = [];
|
|
192
322
|
for (let i = 0; i < N; i++) {
|
|
193
323
|
const baseRow = BOARD_H - 2 - amp - i * gap; // 0 = bottom-most girder
|
|
194
|
-
const down = i
|
|
195
|
-
platforms.push({ row: baseRow, slopeOffsets: buildSlope(baseRow, BOARD_W, down, amp) });
|
|
324
|
+
const down = layout.slopeDownFor(i);
|
|
325
|
+
platforms.push({ row: baseRow, slopeOffsets: buildSlope(baseRow, BOARD_W, down, amp, slopeEvery) });
|
|
196
326
|
}
|
|
197
327
|
// platforms[0] is the LOWEST (largest row); platforms[N-1] the HIGHEST.
|
|
198
328
|
|
|
329
|
+
// One ladder per adjacent pair, IN ORDER (ladders[i] connects platforms[i]→[i+1])
|
|
330
|
+
// so the autopilot's ladders[gi] assumption holds verbatim. The column comes from
|
|
331
|
+
// the layout (already clamped to [2,BOARD_W-3] + footprint-deconflicted); the
|
|
332
|
+
// endpoints are recomputed from the slope AT THAT column so both rails touch both
|
|
333
|
+
// standing rows even on sloped girders.
|
|
199
334
|
const ladders = [];
|
|
200
335
|
for (let i = 0; i < N - 1; i++) {
|
|
201
336
|
const lower = platforms[i];
|
|
202
337
|
const upper = platforms[i + 1];
|
|
203
|
-
const col =
|
|
338
|
+
const col = layout.ladderCols[i];
|
|
204
339
|
const yBottom = lower.slopeOffsets[col] - 1;
|
|
205
340
|
const yTop = upper.slopeOffsets[col] - 1;
|
|
206
341
|
ladders.push({ col, yTop: Math.min(yTop, yBottom), yBottom: Math.max(yTop, yBottom) });
|
|
207
342
|
}
|
|
208
343
|
|
|
209
344
|
const top = platforms[N - 1];
|
|
210
|
-
const dkCol =
|
|
345
|
+
const dkCol = layout.dkCol;
|
|
211
346
|
const donkeyKong = { x: dkCol, y: top.slopeOffsets[dkCol] - 1, throwCooldown: 2, animPhase: 0 };
|
|
212
|
-
const princessCol =
|
|
347
|
+
const princessCol = layout.princessCol;
|
|
213
348
|
const princess = { x: princessCol, y: top.slopeOffsets[princessCol] - 1, animPhase: 0 };
|
|
214
349
|
|
|
215
350
|
const player = makePlayer(platforms);
|
|
@@ -498,10 +633,24 @@ function stepPlaying(s, input, dt) {
|
|
|
498
633
|
}
|
|
499
634
|
} else {
|
|
500
635
|
// Grounded: follow the slope at the current column, detect walk-off.
|
|
636
|
+
// A sloped girder steps ±1 row every `slopeEvery` columns, so when walking
|
|
637
|
+
// INTO a riser the girder's stand row at the next column is one ABOVE the
|
|
638
|
+
// player's current cell (girderIndexAt at cell(py)+1 then misses → the
|
|
639
|
+
// player walks off the riser and falls — the critic's B1). Re-snap to the
|
|
640
|
+
// girder whose stand row is within ±1 of the player's cell so the player
|
|
641
|
+
// auto-mounts a one-row riser (and follows a one-row drop) instead of
|
|
642
|
+
// falling off. This is the same riser the player was already standing on
|
|
643
|
+
// (girders are >=2 rows apart, so at most one girder qualifies), so it
|
|
644
|
+
// never teleports between platforms — it just walks the staircase.
|
|
501
645
|
const c = cell(p.px);
|
|
502
|
-
const
|
|
646
|
+
const cy = cell(p.py);
|
|
647
|
+
// girder cell one below the player (flat / down-step → stand row stays cy).
|
|
648
|
+
let gi = girderIndexAt(platforms, c, cy + 1);
|
|
649
|
+
// up-step: the girder cell is AT the player's current row (stand row cy-1),
|
|
650
|
+
// so the next-column girder stepped up one — auto-mount it.
|
|
651
|
+
if (gi < 0) gi = girderIndexAt(platforms, c, cy);
|
|
503
652
|
if (gi >= 0) {
|
|
504
|
-
p.py = toFx(platforms[gi].slopeOffsets[c] - 1); // re-snap to slope
|
|
653
|
+
p.py = toFx(platforms[gi].slopeOffsets[c] - 1); // re-snap to slope (auto-mount the riser)
|
|
505
654
|
p.vy = 0;
|
|
506
655
|
} else {
|
|
507
656
|
// walked off the edge → coyote window, begin falling
|
|
@@ -686,5 +835,5 @@ export function deserialize(obj) {
|
|
|
686
835
|
// Expose internals for focused unit tests.
|
|
687
836
|
export const _internals = {
|
|
688
837
|
girderBelow, isGirderAt, girderIndexAt, ladderAt, rng, between,
|
|
689
|
-
makePlayer, advanceBarrels, syncCell,
|
|
838
|
+
makePlayer, advanceBarrels, syncCell, levelLayout, nMaxForHeight,
|
|
690
839
|
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/* Shared, PURE provenance mapping for the CLI (no React, no I/O). It answers one
|
|
2
|
+
* question for any tracked usage: WHERE was this model discovered? — filesystem,
|
|
3
|
+
* an env var, Vercel, Supabase Edge, an AWS Lambda / Bedrock / Secrets / SSM, a
|
|
4
|
+
* GitHub Actions config, Kubernetes, Helm, a SQL/config table, or added by hand.
|
|
5
|
+
*
|
|
6
|
+
* STORAGE: we DERIVE provenance with zero migration. Two non-secret signals that
|
|
7
|
+
* already reach + persist in the DB drive it:
|
|
8
|
+
* (1) the scheme prefix every integration source stamps into `location_label`
|
|
9
|
+
* (vercel://, supabase-edge://, aws-lambda://, aws-bedrock://, aws-secrets://,
|
|
10
|
+
* aws-ssm://, github-actions://, env://, k8s://, helm://, sql://), and
|
|
11
|
+
* (2) `discovered_by` (manual vs cli) for the prefix-less filesystem/manual case.
|
|
12
|
+
* The scheme prefix + the labels/badges below are NON-SECRET — we never surface a
|
|
13
|
+
* secret VALUE, so this keeps the redaction invariant intact.
|
|
14
|
+
*
|
|
15
|
+
* This file MUST stay byte-for-byte equivalent in keys/labels/badges to the web
|
|
16
|
+
* counterpart apps/web/lib/source-meta.ts — CLI↔web parity is the contract. */
|
|
17
|
+
|
|
18
|
+
import { GLYPH, C } from "./ui.js";
|
|
19
|
+
|
|
20
|
+
// scheme (from location_label `^scheme://`) → canonical source_type.
|
|
21
|
+
// NOTE: the AWS Lambda source emits TWO schemes — `aws-lambda://` for an env var
|
|
22
|
+
// found on a Lambda and `aws-bedrock://` for an enabled Bedrock foundation model —
|
|
23
|
+
// so they map to DISTINCT source types. Likewise aws.js emits aws-secrets:// AND
|
|
24
|
+
// aws-ssm://. An unknown/future scheme falls through to "filesystem" (safe default).
|
|
25
|
+
const SCHEME_TO_SOURCE = {
|
|
26
|
+
vercel: "vercel",
|
|
27
|
+
"supabase-edge": "supabase-edge",
|
|
28
|
+
"aws-lambda": "aws-lambda",
|
|
29
|
+
"aws-bedrock": "aws-bedrock",
|
|
30
|
+
"aws-secrets": "aws-secrets",
|
|
31
|
+
"aws-ssm": "aws-ssm",
|
|
32
|
+
"github-actions": "github-actions",
|
|
33
|
+
env: "env",
|
|
34
|
+
k8s: "k8s",
|
|
35
|
+
helm: "helm",
|
|
36
|
+
sql: "sql",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Derive a usage's source_type from its persisted location_label + discovered_by.
|
|
41
|
+
* - a known `scheme://` prefix → that scheme's source_type
|
|
42
|
+
* - literal "manual" label OR discovered_by === "manual" → manual
|
|
43
|
+
* - otherwise (plain repo path, discovered_by "cli", or empty) → filesystem
|
|
44
|
+
*/
|
|
45
|
+
export function sourceOf(locationLabel, discoveredBy) {
|
|
46
|
+
const label = String(locationLabel || "");
|
|
47
|
+
// Scheme allows digits (k8s://). Unknown scheme → falls through to filesystem.
|
|
48
|
+
const m = /^([a-z][a-z0-9-]*):\/\//.exec(label);
|
|
49
|
+
if (m && SCHEME_TO_SOURCE[m[1]]) return SCHEME_TO_SOURCE[m[1]];
|
|
50
|
+
if (label === "manual" || discoveredBy === "manual") return "manual";
|
|
51
|
+
return "filesystem";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// source_type → display metadata. `badge` is ≤8 chars (the inventory SOURCE
|
|
55
|
+
// column width). `glyph`/`ascii` are a unicode/ASCII pair so the column degrades
|
|
56
|
+
// on dumb terminals exactly like the rest of the TUI. Colors reuse the SAME hexes
|
|
57
|
+
// as tui/views/integrations.js KIND_COLOR so the Sources tab + Inventory match.
|
|
58
|
+
export const SOURCE_META = {
|
|
59
|
+
filesystem: { label: "Repo file", badge: "repo", glyph: GLYPH.dot, color: C.FG_DIM },
|
|
60
|
+
env: { label: "Env var", badge: "env", glyph: "=", color: C.FG_DIM },
|
|
61
|
+
vercel: { label: "Vercel env", badge: "vercel", glyph: GLYPH.retiring, color: C.FG_STRONG },
|
|
62
|
+
"supabase-edge": { label: "Supabase Edge", badge: "supabase", glyph: "~", color: "#3ecf8e" },
|
|
63
|
+
"aws-lambda": { label: "AWS Lambda", badge: "aws", glyph: "λ", ascii: "L", color: "#ff9900" },
|
|
64
|
+
"aws-bedrock": { label: "AWS Bedrock", badge: "bedrock", glyph: "λ", ascii: "L", color: "#ff9900" },
|
|
65
|
+
"aws-secrets": { label: "AWS Secrets", badge: "secrets", glyph: "λ", ascii: "L", color: "#ff9900" },
|
|
66
|
+
"aws-ssm": { label: "AWS SSM", badge: "ssm", glyph: "λ", ascii: "L", color: "#ff9900" },
|
|
67
|
+
"github-actions": { label: "GitHub Actions", badge: "github", glyph: "⎇", ascii: "gh", color: "#a78bfa" },
|
|
68
|
+
k8s: { label: "Kubernetes", badge: "k8s", glyph: "⎈", ascii: "k8s", color: "#326ce5" },
|
|
69
|
+
helm: { label: "Helm", badge: "helm", glyph: "⎈", ascii: "helm", color: "#0f6fd1" },
|
|
70
|
+
sql: { label: "SQL/config", badge: "sql", glyph: "▦", ascii: "sql", color: C.FG_DIM },
|
|
71
|
+
manual: { label: "Manual", badge: "manual", glyph: GLYPH.repl === "->" ? "+" : "✎", ascii: "+", color: C.FG_FAINT },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Whether the GLYPH table is the ASCII fallback (MM_ASCII=1 / TERM=dumb). When it
|
|
75
|
+
// is, prefer each meta's `ascii` variant for the non-ASCII-safe unicode glyphs
|
|
76
|
+
// (λ/⎇/⎈/▦/✎). GLYPH.repl is "->" only in the ASCII table, so it's a stable probe.
|
|
77
|
+
const ASCII = GLYPH.repl === "->";
|
|
78
|
+
|
|
79
|
+
/** The terminal-safe glyph for a source_type (ASCII fallback when MM_ASCII). */
|
|
80
|
+
export function sourceGlyph(sourceType) {
|
|
81
|
+
const meta = SOURCE_META[sourceType] || SOURCE_META.filesystem;
|
|
82
|
+
return ASCII && meta.ascii ? meta.ascii : meta.glyph;
|
|
83
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
relativeTime, envTag, cell, cellE, snippetLines, SPINNER, useTick, useAsync, clampCursor,
|
|
11
11
|
} from "../ui.js";
|
|
12
12
|
import { readSnippet, findSourceFile } from "../snippet.js";
|
|
13
|
+
import { sourceOf, SOURCE_META, sourceGlyph } from "../source-meta.js";
|
|
13
14
|
|
|
14
15
|
const ENV_ORDER = ["prod", "staging", "dev", "unknown"];
|
|
15
16
|
|
|
@@ -158,9 +159,13 @@ export function InventoryView({ client, ui, dir = ".", active, width = 78, heigh
|
|
|
158
159
|
// ----- layout: left list pane + right detail drawer -----
|
|
159
160
|
const drawerW = 34;
|
|
160
161
|
const leftWidth = Math.max(24, width - drawerW - 2);
|
|
161
|
-
// left columns: glyph(2) + slug + sp(1) + env(7) + sp(1) +
|
|
162
|
+
// left columns: glyph(2) + slug + sp(1) + env(7) + sp(1) + SOURCE + sp(1) + project.
|
|
163
|
+
// SOURCE is a fixed badge column on wide layouts, a 1-char glyph when tight, so it
|
|
164
|
+
// shrinks projW (which keeps a floor) rather than overflowing on narrow terminals.
|
|
162
165
|
const ENV_W = 7;
|
|
163
|
-
const
|
|
166
|
+
const wideSrc = leftWidth >= 70;
|
|
167
|
+
const SOURCE_W = wideSrc ? 8 : 1; // badge (≤8) vs glyph-only
|
|
168
|
+
const FIXED = 2 + 1 + ENV_W + 1 + SOURCE_W + 1; // glyph+sp, sp, env, sp, source, sp
|
|
164
169
|
const slugW = Math.max(10, Math.floor((leftWidth - 1 - FIXED) * 0.5));
|
|
165
170
|
const projW = Math.max(8, leftWidth - 1 - FIXED - slugW);
|
|
166
171
|
|
|
@@ -187,19 +192,24 @@ export function InventoryView({ client, ui, dir = ".", active, width = 78, heigh
|
|
|
187
192
|
const header = h(
|
|
188
193
|
Text,
|
|
189
194
|
{ color: C.FG_FAINT },
|
|
190
|
-
" " + cell("MODEL", 2 + slugW) + cell("ENV", 1 + ENV_W) + "PROJECT",
|
|
195
|
+
" " + cell("MODEL", 2 + slugW) + cell("ENV", 1 + ENV_W) + cell(wideSrc ? "SRC" : "S", SOURCE_W + 1) + "PROJECT",
|
|
191
196
|
);
|
|
192
197
|
|
|
193
198
|
const rowNodes = view.map((u, i) => {
|
|
194
199
|
const isCur = start + i === c;
|
|
195
200
|
const et = envTag(u.environment);
|
|
196
201
|
const slug = u.model_display || u.custom_model_name || "?";
|
|
202
|
+
const st = sourceOf(u.location_label, u.discovered_by);
|
|
203
|
+
const meta = SOURCE_META[st];
|
|
204
|
+
const srcText = wideSrc ? meta.badge : sourceGlyph(st);
|
|
197
205
|
const cells = [
|
|
198
206
|
{ text: `${healthGlyph(u.health)} `, color: healthColor(u.health) },
|
|
199
207
|
{ text: cellE(slug, slugW), color: isCur ? C.FG_STRONG : C.FG, bold: isCur },
|
|
200
208
|
{ text: " ", color: C.FG },
|
|
201
209
|
{ text: cell(et.text, ENV_W), color: et.color },
|
|
202
210
|
{ text: " ", color: C.FG },
|
|
211
|
+
{ text: cell(srcText, SOURCE_W), color: meta.color },
|
|
212
|
+
{ text: " ", color: C.FG },
|
|
203
213
|
{ text: cellE(projName(u.project_id), projW), color: C.FG_DIM },
|
|
204
214
|
];
|
|
205
215
|
return h(ListRow, { key: u.id, active: isCur, cells, width: leftWidth });
|
|
@@ -242,6 +252,14 @@ export function InventoryView({ client, ui, dir = ".", active, width = 78, heigh
|
|
|
242
252
|
h(Text, {}, h(Text, { color: C.FG_DIM }, "project "), h(Text, { color: C.FG }, projName(cur.project_id))),
|
|
243
253
|
cur.source_repo ? h(Text, {}, h(Text, { color: C.FG_DIM }, "repo "), h(Text, { color: C.FG }, cur.source_repo)) : null,
|
|
244
254
|
h(Text, {}, h(Text, { color: C.FG_DIM }, "where "), h(Text, { color: C.FG_DIM }, where)),
|
|
255
|
+
(() => {
|
|
256
|
+
// Human SOURCE label (derived from the location_label scheme prefix +
|
|
257
|
+
// discovered_by). The raw scheme stays in `where` (it's the locator);
|
|
258
|
+
// this is the friendly name, and `via <discovered_by>` below reads as a
|
|
259
|
+
// sub-detail. Non-secret — never surfaces a value.
|
|
260
|
+
const meta = SOURCE_META[sourceOf(cur.location_label, cur.discovered_by)];
|
|
261
|
+
return h(Text, {}, h(Text, { color: C.FG_DIM }, "source "), h(Text, { color: meta.color }, meta.label));
|
|
262
|
+
})(),
|
|
245
263
|
rt && rt.text
|
|
246
264
|
? h(Text, {}, h(Text, { color: C.FG_DIM }, "retires "), h(Text, { color: rt.color, bold: rt.bold }, rt.text))
|
|
247
265
|
: null,
|