modelstat 0.0.31 → 0.0.33
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/dist/cli.mjs +57 -62
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.mjs +127 -20
- package/vendor/tray-mac/Sources/ModelstatTray/main.swift +18 -18
package/package.json
CHANGED
package/scripts/postinstall.mjs
CHANGED
|
@@ -47,23 +47,38 @@ function inThisMonorepo() {
|
|
|
47
47
|
async function main() {
|
|
48
48
|
if (process.env.MODELSTAT_SKIP_POSTINSTALL === "1") {
|
|
49
49
|
console.log(
|
|
50
|
-
"[modelstat] postinstall skipped (MODELSTAT_SKIP_POSTINSTALL=1)
|
|
51
|
-
);
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
if (process.env.CI === "true" || process.env.CI === "1") {
|
|
55
|
-
console.log(
|
|
56
|
-
"[modelstat] postinstall skipped (CI=1) — model downloads lazily on first scan",
|
|
50
|
+
"[modelstat] postinstall skipped (MODELSTAT_SKIP_POSTINSTALL=1)",
|
|
57
51
|
);
|
|
58
52
|
return;
|
|
59
53
|
}
|
|
60
54
|
if (inThisMonorepo()) {
|
|
61
|
-
// Dev environment — don't pull 2.7 GB on every workspace install
|
|
55
|
+
// Dev environment — don't pull 2.7 GB on every workspace install,
|
|
56
|
+
// and don't muck with the developer's running daemon.
|
|
62
57
|
return;
|
|
63
58
|
}
|
|
64
|
-
|
|
59
|
+
|
|
60
|
+
// ── Always-run path: refresh the existing service if there is one ─
|
|
61
|
+
// The bundle at ~/.modelstat/bin/modelstat.mjs and its sibling
|
|
62
|
+
// node_modules need to track every upgrade, otherwise the daemon
|
|
63
|
+
// launches against stale code (or, worse, code that imports a
|
|
64
|
+
// native module the local install doesn't have anymore). This MUST
|
|
65
|
+
// run regardless of TTY/CI — the previous behaviour gated it on
|
|
66
|
+
// an interactive shell, which left every npm-cache / npx-prewarm
|
|
67
|
+
// install with a half-installed bundle the daemon couldn't load.
|
|
68
|
+
await rebootServiceIfInstalled();
|
|
69
|
+
|
|
70
|
+
// ── TTY-only path: pre-download the heavy summariser model ───────
|
|
71
|
+
// The 2.7 GB GGUF is the only thing we want to skip in non-TTY
|
|
72
|
+
// installs (CI, npm cache, packagers). The agent always preflights
|
|
73
|
+
// the summariser before producing segments (see src/pipeline.ts),
|
|
74
|
+
// so users in skip-mode just see the download then, instead of now.
|
|
75
|
+
const skipModelDownload =
|
|
76
|
+
process.env.CI === "true" ||
|
|
77
|
+
process.env.CI === "1" ||
|
|
78
|
+
!process.stdout.isTTY;
|
|
79
|
+
if (skipModelDownload) {
|
|
65
80
|
console.log(
|
|
66
|
-
"[modelstat]
|
|
81
|
+
"[modelstat] non-interactive install — summariser model will download lazily on first scan",
|
|
67
82
|
);
|
|
68
83
|
return;
|
|
69
84
|
}
|
|
@@ -110,16 +125,6 @@ async function main() {
|
|
|
110
125
|
"[modelstat] the model will download lazily on first `modelstat scan` instead",
|
|
111
126
|
);
|
|
112
127
|
}
|
|
113
|
-
|
|
114
|
-
// ── Auto-replace + restart the existing service ───────────────
|
|
115
|
-
// If a previous version of the agent is already installed as a
|
|
116
|
-
// launchd / systemd service, its bundle copy at
|
|
117
|
-
// ~/.modelstat/bin/modelstat.mjs is now STALE — the new code we
|
|
118
|
-
// just installed lives in <global node_modules>/modelstat/dist.
|
|
119
|
-
// Stop the service, refresh the bundle copy, restart. Without
|
|
120
|
-
// this step the user has to manually `modelstat stop && modelstat
|
|
121
|
-
// connect` after every upgrade.
|
|
122
|
-
await rebootServiceIfInstalled();
|
|
123
128
|
console.log(
|
|
124
129
|
"[modelstat] all set — your dashboard already has the new agent running",
|
|
125
130
|
);
|
|
@@ -196,6 +201,16 @@ async function rebootServiceIfInstalled() {
|
|
|
196
201
|
);
|
|
197
202
|
}
|
|
198
203
|
|
|
204
|
+
// The bundle is portable EXCEPT for `node-llama-cpp` and its
|
|
205
|
+
// platform-specific native sub-packages — they're marked external
|
|
206
|
+
// in tsup so they're resolved at runtime via Node's `node_modules`
|
|
207
|
+
// walk-up. From `~/.modelstat/bin/modelstat.mjs` there's no parent
|
|
208
|
+
// `node_modules` to find them in, so the bundled summariser dies
|
|
209
|
+
// at preflight with "Cannot find package 'node-llama-cpp'". Set up
|
|
210
|
+
// a sibling `node_modules` directory containing the runtime
|
|
211
|
+
// dependencies, sourced from this package's own install location.
|
|
212
|
+
await setupNativeNodeModules(installedBundle);
|
|
213
|
+
|
|
199
214
|
// Start back up. `modelstat start` re-runs preflight (incl. the
|
|
200
215
|
// processing-version reconcile that wipes cursors when we ship a
|
|
201
216
|
// new pipeline), so the upgrade picks up the new behaviour
|
|
@@ -209,6 +224,98 @@ async function rebootServiceIfInstalled() {
|
|
|
209
224
|
console.log("[modelstat] ✓ background service restarted with new build");
|
|
210
225
|
}
|
|
211
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Make `node-llama-cpp` and its platform-specific native sibling
|
|
229
|
+
* packages reachable from `~/.modelstat/bin/modelstat.mjs` so the
|
|
230
|
+
* bundle's runtime `import("node-llama-cpp")` resolves.
|
|
231
|
+
*
|
|
232
|
+
* Strategy:
|
|
233
|
+
* - Find the source `node_modules` directory in this package's
|
|
234
|
+
* install (npm flat layout puts it at <pkg>/.. for global
|
|
235
|
+
* installs; pnpm/bun layouts vary but the same walk-up works
|
|
236
|
+
* since postinstall always runs from the package root).
|
|
237
|
+
* - For each runtime dep we care about (`node-llama-cpp` plus
|
|
238
|
+
* every `@node-llama-cpp/*` and `@reflink/*` sub-package),
|
|
239
|
+
* symlink it into `~/.modelstat/bin/node_modules/<name>`.
|
|
240
|
+
* Symlinks are free + fast; if symlinking fails (filesystem
|
|
241
|
+
* boundary, permissions) fall back to a recursive copy with
|
|
242
|
+
* `dereference:true` so the resulting tree has no dangling
|
|
243
|
+
* refs into the source location.
|
|
244
|
+
*
|
|
245
|
+
* Idempotent: each upgrade clears the destination first.
|
|
246
|
+
*/
|
|
247
|
+
async function setupNativeNodeModules(installedBundle) {
|
|
248
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
249
|
+
// Find a parent `node_modules` containing `node-llama-cpp`. The
|
|
250
|
+
// walk handles npm flat (<prefix>/lib/node_modules), pnpm flat
|
|
251
|
+
// (<prefix>/lib/node_modules with peer hoisting), and the npx
|
|
252
|
+
// cache layout (~/.npm/_npx/<hash>/node_modules).
|
|
253
|
+
let srcNodeModules = null;
|
|
254
|
+
for (let dir = here, i = 0; i < 8; i++) {
|
|
255
|
+
const probe = join(dir, "node_modules", "node-llama-cpp", "package.json");
|
|
256
|
+
if (existsSync(probe)) {
|
|
257
|
+
srcNodeModules = join(dir, "node_modules");
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
const up = resolve(dir, "..");
|
|
261
|
+
if (up === dir) break;
|
|
262
|
+
dir = up;
|
|
263
|
+
}
|
|
264
|
+
if (!srcNodeModules) {
|
|
265
|
+
console.warn(
|
|
266
|
+
"[modelstat] couldn't locate node-llama-cpp in this install — bundled summariser will fail at preflight. Re-install with `npm i -g modelstat` or report a bug.",
|
|
267
|
+
);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const fs = await import("node:fs/promises");
|
|
272
|
+
const destDir = join(dirname(installedBundle), "node_modules");
|
|
273
|
+
await fs.mkdir(destDir, { recursive: true });
|
|
274
|
+
|
|
275
|
+
// Discover every native package we need: the main one + every
|
|
276
|
+
// platform-specific sibling (the binary blob is in one of the
|
|
277
|
+
// `@node-llama-cpp/*` packages chosen by node-llama-cpp's
|
|
278
|
+
// optional-dependencies resolution at install time).
|
|
279
|
+
const wanted = new Set(["node-llama-cpp"]);
|
|
280
|
+
const all = await fs
|
|
281
|
+
.readdir(srcNodeModules, { withFileTypes: true })
|
|
282
|
+
.catch(() => []);
|
|
283
|
+
for (const e of all) {
|
|
284
|
+
if (!e.isDirectory()) continue;
|
|
285
|
+
if (!e.name.startsWith("@")) continue;
|
|
286
|
+
if (
|
|
287
|
+
e.name === "@node-llama-cpp" ||
|
|
288
|
+
e.name === "@reflink"
|
|
289
|
+
) {
|
|
290
|
+
// Scoped dirs contain multiple packages — copy/symlink the
|
|
291
|
+
// whole scope dir so all platform variants are reachable.
|
|
292
|
+
wanted.add(e.name);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
for (const pkg of wanted) {
|
|
297
|
+
const src = join(srcNodeModules, pkg);
|
|
298
|
+
const dest = join(destDir, pkg);
|
|
299
|
+
if (!existsSync(src)) continue;
|
|
300
|
+
try {
|
|
301
|
+
await fs.rm(dest, { recursive: true, force: true });
|
|
302
|
+
} catch {
|
|
303
|
+
/* ignore */
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
await fs.symlink(src, dest, "dir");
|
|
307
|
+
} catch {
|
|
308
|
+
try {
|
|
309
|
+
await fs.cp(src, dest, { recursive: true, dereference: true });
|
|
310
|
+
} catch (err2) {
|
|
311
|
+
console.warn(
|
|
312
|
+
`[modelstat] couldn't link ${pkg}: ${err2 && err2.message ? err2.message : err2}`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
212
319
|
/**
|
|
213
320
|
* Kill any daemon process the service supervisor didn't reap.
|
|
214
321
|
* Reads ~/.modelstat/daemon.lock for the PID, sends SIGTERM, then
|
|
@@ -439,33 +439,33 @@ final class TrayController: NSObject {
|
|
|
439
439
|
// anyone signed up for. We set it here as a belt — the plist is the
|
|
440
440
|
// braces — so a malformed bundle still behaves.
|
|
441
441
|
//
|
|
442
|
-
// TrayController is @MainActor, so its init must run on the main
|
|
443
|
-
// actor. We wrap the bootstrap in a main-actor function to satisfy
|
|
444
|
-
// Swift 6's strict concurrency without bloating the controller.
|
|
445
|
-
//
|
|
446
442
|
// IMPORTANT: nothing in AppKit retains TrayController for us. The
|
|
447
443
|
// NSStatusItem holds the menu, and NSMenuItem.target is a weak
|
|
448
444
|
// reference, so the controller has no strong owners. Without a
|
|
449
445
|
// global anchor, ARC deallocates the controller as soon as init
|
|
450
446
|
// returns — which leaves the timer's `[weak self]` callback firing
|
|
451
|
-
// against nil and the menu title frozen on "
|
|
447
|
+
// against nil and the menu title frozen on "Loading…" forever.
|
|
452
448
|
// The `controller` global below is the strong reference that keeps
|
|
453
449
|
// the controller alive for the entire app lifetime.
|
|
450
|
+
//
|
|
451
|
+
// CRITICAL: NSApplication.run() must NOT be called from inside a
|
|
452
|
+
// `DispatchQueue.main.async { ... }` closure. NSApplication.run()
|
|
453
|
+
// blocks for the lifetime of the app, and from libdispatch's
|
|
454
|
+
// perspective the wrapping closure is "still executing" the entire
|
|
455
|
+
// time — which means every other main-queue async block (including
|
|
456
|
+
// the stats-poll completion that updates the menu title) gets queued
|
|
457
|
+
// behind it and never runs. Schedule the controller setup separately
|
|
458
|
+
// and call app.run() directly from top-level code so libdispatch's
|
|
459
|
+
// main queue stays free to drain.
|
|
454
460
|
@MainActor
|
|
455
461
|
private var controller: TrayController?
|
|
456
462
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
controller = TrayController()
|
|
462
|
-
app.run()
|
|
463
|
+
DispatchQueue.main.async {
|
|
464
|
+
MainActor.assumeIsolated {
|
|
465
|
+
controller = TrayController()
|
|
466
|
+
}
|
|
463
467
|
}
|
|
464
468
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
// becomes a no-op; we keep the current thread alive with RunLoop.main
|
|
469
|
-
// until AppKit takes over.
|
|
470
|
-
DispatchQueue.main.async { bootstrap() }
|
|
471
|
-
RunLoop.main.run()
|
|
469
|
+
let app = NSApplication.shared
|
|
470
|
+
app.setActivationPolicy(.accessory)
|
|
471
|
+
app.run()
|