@modelstatus/cli 0.1.34 → 0.1.36
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/api.js +6 -0
- package/src/ci.js +2 -2
- package/src/index.js +219 -12
- package/src/integrations.js +121 -0
- package/src/sources/aws-lambda.js +95 -0
- package/src/sources/configscan.js +8 -2
- package/src/sources/filesystem.js +0 -0
- package/src/sources/github-actions.js +156 -0
- package/src/sources/index.js +70 -13
- package/src/sources/scan-process.js +238 -0
- package/src/sources/scan-runner.js +127 -0
- package/src/sources/scan-worker.js +148 -0
- package/src/sources/supabase-edge.js +183 -0
- package/src/sources/supabase.js +5 -0
- package/src/sources/vercel.js +74 -0
- package/src/tui/app.js +45 -2
- package/src/tui/game/DkGame.js +21 -0
- package/src/tui/game/dk-core.js +688 -0
- package/src/tui/game/dk-render.js +160 -0
- package/src/tui/game/input.js +169 -0
- package/src/tui/game/loop.js +337 -0
- package/src/tui/game/term.js +330 -0
- package/src/tui/views/add.js +1 -1
- package/src/tui/views/integrations.js +224 -0
- package/src/tui/views/inventory.js +31 -2
- package/src/tui/views/scan.js +116 -6
package/src/tui/views/scan.js
CHANGED
|
@@ -3,9 +3,23 @@
|
|
|
3
3
|
* retirement display, and lets you select + upload to your inventory. Server
|
|
4
4
|
* model_ids are resolved only at upload time so uploads stay correct.
|
|
5
5
|
* Scrollable (j/k/↑↓), filterable (/), with a full-width detail panel; ↵ drills
|
|
6
|
-
* into the highlighted model's usage locations and ↵ opens one in your editor.
|
|
6
|
+
* into the highlighted model's usage locations and ↵ opens one in your editor.
|
|
7
|
+
*
|
|
8
|
+
* BACKGROUND RUNNER: the live view's walk runs on the shared cooperative engine
|
|
9
|
+
* via useStreamingScan (scan-stream.js) → scanFilesystemStreaming.
|
|
10
|
+
*
|
|
11
|
+
* PLAY-WHILE-YOU-WAIT: P launches Donkey Kong via a clean Ink↔game handoff (NOT
|
|
12
|
+
* an Ink overlay anymore). We (1) start a TRUE background scan SUBPROCESS
|
|
13
|
+
* (scan-process.js) that survives Ink being unmounted because it's a separate OS
|
|
14
|
+
* process, (2) UNMOUNT the whole Ink tree (releasing raw mode + stdin), (3) run
|
|
15
|
+
* the direct-ANSI 60Hz game loop (loop.js — its own raw mode + alt screen +
|
|
16
|
+
* diff renderer, zero flicker), then (4) on game exit REMOUNT the TUI at the
|
|
17
|
+
* Scan tab. When the subprocess scan finishes during play we persist its
|
|
18
|
+
* candidates to the on-disk scan cache, so the remounted Scan view loads the
|
|
19
|
+
* fresh results on its fast path. Mid-scan only: P opens the game. */
|
|
7
20
|
import React from "react";
|
|
8
21
|
import fs from "node:fs";
|
|
22
|
+
import os from "node:os";
|
|
9
23
|
import path from "node:path";
|
|
10
24
|
import { Box, Text, useInput } from "ink";
|
|
11
25
|
import {
|
|
@@ -20,6 +34,7 @@ import { readSnippet } from "../snippet.js";
|
|
|
20
34
|
import { buildUsages, assignProjects } from "../../upload.js";
|
|
21
35
|
import { track } from "../../telemetry.js";
|
|
22
36
|
import { openUrl, openLocation } from "../../openUrl.js";
|
|
37
|
+
import { boardSize, MIN_W, MIN_H } from "../game/dk-core.js";
|
|
23
38
|
|
|
24
39
|
export const meta = {
|
|
25
40
|
keys: [
|
|
@@ -30,12 +45,13 @@ export const meta = {
|
|
|
30
45
|
{ k: "/", label: "search" },
|
|
31
46
|
{ k: "g", label: "rescan" },
|
|
32
47
|
{ k: "u", label: "upload all" },
|
|
48
|
+
{ k: "P", label: "play" }, // only meaningful mid-scan; shown when running
|
|
33
49
|
],
|
|
34
50
|
};
|
|
35
51
|
|
|
36
52
|
const PANEL_H = 6;
|
|
37
53
|
|
|
38
|
-
export function ScanView({ client, dir, ui, active, width = 78, height = 14, fresh = false }) {
|
|
54
|
+
export function ScanView({ client, dir, ui, active, width = 78, height = 14, fresh = false, launchGameImpl = null }) {
|
|
39
55
|
// Reuse the SHARED cached scan engine (same module-level cache as the Here
|
|
40
56
|
// tab) instead of re-walking the repo on every visit. Switching to Scan after
|
|
41
57
|
// Here is now instant; `g` forces a fresh walk for both.
|
|
@@ -51,6 +67,85 @@ export function ScanView({ client, dir, ui, active, width = 78, height = 14, fre
|
|
|
51
67
|
const tick = useTick(80, running || busy);
|
|
52
68
|
const search = useSearch();
|
|
53
69
|
|
|
70
|
+
// "Play Donkey Kong while you wait." The board fits only if the terminal is
|
|
71
|
+
// large enough (boardSize ≥ MIN_W×MIN_H), so the affordance is hidden on tiny
|
|
72
|
+
// terminals and the open is a no-op there. launchingRef guards against a
|
|
73
|
+
// double-launch (the handoff is async).
|
|
74
|
+
const { BOARD_W, BOARD_H } = boardSize(width, height);
|
|
75
|
+
const canPlay = BOARD_W >= MIN_W && BOARD_H >= MIN_H;
|
|
76
|
+
const launchingRef = React.useRef(false);
|
|
77
|
+
|
|
78
|
+
async function launchGame() {
|
|
79
|
+
if (launchingRef.current) return;
|
|
80
|
+
if (!canPlay) return ui?.showToast?.("terminal too small for the game — resize a bit", "#d97706");
|
|
81
|
+
launchingRef.current = true;
|
|
82
|
+
track("game_opened", { game: "donkey_kong", scan_phase: scan.phase });
|
|
83
|
+
// Test seam: a caller may inject the launcher so the Ink↔game handoff (which
|
|
84
|
+
// grabs the real terminal + spawns a subprocess) can be asserted without
|
|
85
|
+
// either. Production passes nothing → the real handoff below runs.
|
|
86
|
+
if (launchGameImpl) {
|
|
87
|
+
try { await launchGameImpl({ dir, width, height }); }
|
|
88
|
+
finally { launchingRef.current = false; }
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const [{ runGame }, { startScanProcess }, { appController }, { writeDiskScan, loadRegistry }] = await Promise.all([
|
|
93
|
+
import("../game/loop.js"),
|
|
94
|
+
import("../../sources/scan-process.js"),
|
|
95
|
+
import("../app.js"),
|
|
96
|
+
import("../scan-stream.js"),
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
// (1) Start a TRUE background scan SUBPROCESS over the same dir. It survives
|
|
100
|
+
// the Ink unmount (separate OS process). Pre-fetch + cache the registry so
|
|
101
|
+
// the worker skips the network; on done, persist candidates to the disk
|
|
102
|
+
// cache so the remounted Scan view loads the fresh results.
|
|
103
|
+
let cacheFile = null;
|
|
104
|
+
try {
|
|
105
|
+
const snapshot = await loadRegistry().catch(() => null);
|
|
106
|
+
if (snapshot) {
|
|
107
|
+
cacheFile = path.join(os.tmpdir(), `mm-reg-game-${process.pid}.json`);
|
|
108
|
+
fs.writeFileSync(cacheFile, JSON.stringify(snapshot));
|
|
109
|
+
}
|
|
110
|
+
} catch { /* worker fetches itself */ }
|
|
111
|
+
|
|
112
|
+
let handle = null;
|
|
113
|
+
try {
|
|
114
|
+
handle = startScanProcess(
|
|
115
|
+
{ root: dir, registryCachePath: cacheFile || undefined },
|
|
116
|
+
{ onDone: (r) => { try { if (r?.candidates?.length) writeDiskScan(dir, r.candidates); } catch { /* best effort */ } } },
|
|
117
|
+
);
|
|
118
|
+
} catch { handle = null; }
|
|
119
|
+
|
|
120
|
+
// (2) Unmount the Ink tree (releases raw mode + stdin). Wait a tick so the
|
|
121
|
+
// teardown fully settles before the game grabs raw mode + alt screen.
|
|
122
|
+
appController.unmount();
|
|
123
|
+
await new Promise((r) => setImmediate(r));
|
|
124
|
+
|
|
125
|
+
// (3) Run the game. Its loop owns raw mode / alt screen / the diff renderer
|
|
126
|
+
// and restores the terminal (cooked, main buffer, cursor shown) on exit.
|
|
127
|
+
try {
|
|
128
|
+
await runGame({
|
|
129
|
+
width: process.stdout.columns || width,
|
|
130
|
+
height: process.stdout.rows || height,
|
|
131
|
+
level: 1,
|
|
132
|
+
scanStore: handle,
|
|
133
|
+
});
|
|
134
|
+
} finally {
|
|
135
|
+
try { handle && handle.abort(); } catch { /* ignore */ }
|
|
136
|
+
try { if (cacheFile) fs.unlinkSync(cacheFile); } catch { /* ignore */ }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// (4) Remount the TUI at the Scan tab. fresh:false → the view loads the
|
|
140
|
+
// (now-updated) disk scan cache on its fast path.
|
|
141
|
+
appController.remount({ initialView: "scan", fresh: false });
|
|
142
|
+
} catch (e) {
|
|
143
|
+
ui?.showToast?.(`couldn't start the game: ${e?.message || e}`, "#dc2626");
|
|
144
|
+
} finally {
|
|
145
|
+
launchingRef.current = false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
54
149
|
// Upload-target projects (cheap; independent of the scan).
|
|
55
150
|
const projQ = useAsync(() => client.listProjects().then((r) => r.data || []).catch(() => []), []);
|
|
56
151
|
const projects = projQ.data || [];
|
|
@@ -123,7 +218,7 @@ export function ScanView({ client, dir, ui, active, width = 78, height = 14, fre
|
|
|
123
218
|
}, [items, selCount, project, ui]);
|
|
124
219
|
|
|
125
220
|
// Tell the shell when backspace should back out *within* this view (drilled
|
|
126
|
-
// into refs
|
|
221
|
+
// into refs or an active filter) rather than stepping to the previous tab.
|
|
127
222
|
const setHandlesBack = ui?.setHandlesBack;
|
|
128
223
|
React.useEffect(() => {
|
|
129
224
|
setHandlesBack?.(focus === "refs" || !!search.query);
|
|
@@ -227,7 +322,13 @@ export function ScanView({ client, dir, ui, active, width = 78, height = 14, fre
|
|
|
227
322
|
if (input === "a") return setItems((its) => its.map((it) => ({ ...it, selected: true })));
|
|
228
323
|
if (input === "x") return setItems((its) => its.map((it) => ({ ...it, selected: false })));
|
|
229
324
|
if (input === "p") return setProjectIdx((i) => (i + 1) % projOptions.length);
|
|
230
|
-
if (input === "P")
|
|
325
|
+
if (input === "P") {
|
|
326
|
+
// Mid-scan, P launches "Play Donkey Kong while you wait" (a clean Ink↔
|
|
327
|
+
// game handoff); otherwise it keeps its original meaning (create a new
|
|
328
|
+
// upload-target project).
|
|
329
|
+
if (running) { launchGame(); return; }
|
|
330
|
+
return ui.askPrompt("New project", { onSubmit: createProject });
|
|
331
|
+
}
|
|
231
332
|
if (input === "g") return scan.reload();
|
|
232
333
|
if (input === "u") return upload();
|
|
233
334
|
if (input === "l") {
|
|
@@ -239,7 +340,13 @@ export function ScanView({ client, dir, ui, active, width = 78, height = 14, fre
|
|
|
239
340
|
);
|
|
240
341
|
|
|
241
342
|
if (scan.phase === "error") return h(StateLine, { kind: "error", text: scan.error });
|
|
242
|
-
if (running && !items.length)
|
|
343
|
+
if (running && !items.length)
|
|
344
|
+
return h(
|
|
345
|
+
Box,
|
|
346
|
+
{ flexDirection: "column" },
|
|
347
|
+
h(StateLine, { kind: "scanning", spin: SPINNER[tick % SPINNER.length], text: `scanning ${dir}…` }),
|
|
348
|
+
canPlay ? h(Text, { color: C.FG_FAINT }, ` ${GLYPH.spark} press P to play Donkey Kong while you wait`) : null,
|
|
349
|
+
);
|
|
243
350
|
if (!items.length)
|
|
244
351
|
return h(EmptyCard, { icon: GLYPH.spark, title: `No models found in ${path.basename(dir)} — yet`, lines: ["We looked through code, config, and prompt files for model ids.", "Press g to rescan, 1 Here to try another project, or 5 Add to enter one by hand."], width });
|
|
245
352
|
|
|
@@ -264,10 +371,13 @@ export function ScanView({ client, dir, ui, active, width = 78, height = 14, fre
|
|
|
264
371
|
return h(ListRow, { key: it.key + realIdx, active: isCur, selected: it.selected, cells, width });
|
|
265
372
|
});
|
|
266
373
|
|
|
374
|
+
// While the walk is still running (and the terminal fits a board), nudge the
|
|
375
|
+
// "Play Donkey Kong while you wait" affordance on the showing line.
|
|
376
|
+
const playHint = running && canPlay ? " · P play" : "";
|
|
267
377
|
const showingLine = h(
|
|
268
378
|
Text,
|
|
269
379
|
{ color: C.FG_FAINT },
|
|
270
|
-
` ${fmtNum(filtered.length)} ref${filtered.length === 1 ? "" : "s"}${filtered.length > pageSize ? ` · ${nav.start + 1}-${Math.min(nav.end, filtered.length)}` : ""}${search.query ? ` · filter "${search.query}"` : ""}${scan.fromCache ? " · cached" : ""} · ${selCount === items.length ? `all ${selCount}` : `${selCount}/${items.length}`} selected · u uploads them all`,
|
|
380
|
+
` ${fmtNum(filtered.length)} ref${filtered.length === 1 ? "" : "s"}${filtered.length > pageSize ? ` · ${nav.start + 1}-${Math.min(nav.end, filtered.length)}` : ""}${search.query ? ` · filter "${search.query}"` : ""}${scan.fromCache ? " · cached" : ""} · ${selCount === items.length ? `all ${selCount}` : `${selCount}/${items.length}`} selected · u uploads them all${playHint}`,
|
|
271
381
|
);
|
|
272
382
|
const footer = busy
|
|
273
383
|
? h(Text, { color: C.ACCENT }, ` ${SPINNER[tick % SPINNER.length]} uploading…`)
|