@gorlitzer-labs/seldon 0.1.7 → 0.1.9
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 +38 -5
- package/bin/seldon.js +188 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Pick the tools you want from a checklist; each installs via its native method
|
|
|
16
16
|
## Install
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
|
|
19
|
+
pnpm add -g @gorlitzer-labs/seldon # or: bun add -g … · npm i -g …
|
|
20
20
|
seldon # open the checklist — ↑↓ move · space pick · a all · enter install
|
|
21
21
|
```
|
|
22
22
|
|
|
@@ -28,6 +28,34 @@ seldon doctor # check external deps (tmux, sops, age, tailsca
|
|
|
28
28
|
seldon list # everything available
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
## Start the stack
|
|
32
|
+
|
|
33
|
+
One command brings up the always-on services and prints where to go:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
seldon up # starts the voice (demerzel) + the supervisor (factory watch),
|
|
37
|
+
# then prints: Voice → http://localhost:8770, how to put agents
|
|
38
|
+
# to work, rooms, phone access
|
|
39
|
+
seldon status # what's running (pids)
|
|
40
|
+
seldon down # stop everything
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`seldon up` only starts what's installed. demerzel serves the voice UI on
|
|
44
|
+
`http://localhost:8770`; its first run needs a one-time model fetch (~25 GB) and
|
|
45
|
+
`up` prints the exact command if they're missing. Logs live in `~/.seldon/run/`.
|
|
46
|
+
|
|
47
|
+
## Uninstall
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
seldon uninstall comb demerzel # remove specific modules (asks to confirm)
|
|
51
|
+
seldon uninstall --all # remove the whole stack (also drops ~/.seldon)
|
|
52
|
+
seldon uninstall <ids> --yes # skip the confirm prompt
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Node tools are removed from the global store; demerzel's isolated venv and
|
|
56
|
+
bifrost's binary + config are deleted. Your system Python, tmux, tailscale,
|
|
57
|
+
sops/age are never touched.
|
|
58
|
+
|
|
31
59
|
## The stack
|
|
32
60
|
|
|
33
61
|
| module | what it is | ships via |
|
|
@@ -51,10 +79,15 @@ It's a stack, not a bundle — install only what you use:
|
|
|
51
79
|
|
|
52
80
|
## Package managers
|
|
53
81
|
|
|
54
|
-
Node tools install
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
82
|
+
Node tools install with **pnpm** when it can global-install (much faster than npm),
|
|
83
|
+
else **npm**. **bun** is supported but opt-in — `seldon install --pm=bun` (also
|
|
84
|
+
`--pm=pnpm|npm` to force any of them).
|
|
85
|
+
|
|
86
|
+
demerzel (Python) always installs into its **own isolated venv** — never your
|
|
87
|
+
system or active Python. It pins **CPython 3.12** (its `kokoro` pin caps Python at
|
|
88
|
+
`<3.13`) and uses **uv** to provision a standalone 3.12: if `uv` isn't present,
|
|
89
|
+
seldon bootstraps it into `~/.seldon/bin` with no shell/PATH changes. If uv can't
|
|
90
|
+
be installed, it falls back to a matching `python3.12` on PATH.
|
|
58
91
|
|
|
59
92
|
## From a checkout
|
|
60
93
|
|
package/bin/seldon.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// seldon — the Seldon stack installer. Pick your tools; install each via its
|
|
3
3
|
// native method. Zero dependencies: a raw-terminal checklist, no build step.
|
|
4
|
-
import { execSync, spawnSync } from "node:child_process";
|
|
4
|
+
import { execSync, spawnSync, spawn } from "node:child_process";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import fs from "node:fs";
|
|
@@ -170,7 +170,9 @@ function installPython(m, dev) {
|
|
|
170
170
|
run(py, ["-m", "venv", venv]);
|
|
171
171
|
ok = run(path.join(venv, "bin", "pip"), ["install", "-r", req]);
|
|
172
172
|
}
|
|
173
|
-
|
|
173
|
+
// demerzel has no standalone binary — it runs as a module from its venv.
|
|
174
|
+
if (ok && m.id === "demerzel") console.log(C.dim(` run it: seldon up (starts the voice on http://localhost:8770; first run fetches models)`));
|
|
175
|
+
else if (ok) console.log(C.dim(` run it: ${path.join(dest, m.bin)} (isolated venv at ${venv})`));
|
|
174
176
|
return ok;
|
|
175
177
|
}
|
|
176
178
|
|
|
@@ -194,6 +196,174 @@ function doInstall(ids, { dev } = {}) {
|
|
|
194
196
|
console.log("\n" + C.bold("Done. ") + C.green(good.join(", ") || "—") + (bad.length ? " " + C.red("failed: " + bad.join(", ")) : ""));
|
|
195
197
|
}
|
|
196
198
|
|
|
199
|
+
// ---- uninstall --------------------------------------------------------------
|
|
200
|
+
// Remove a node CLI from the global store. `rm -g` is the same verb for pnpm,
|
|
201
|
+
// npm and bun; try the chosen PM first, then any other present (a tool may have
|
|
202
|
+
// been installed under a different one). Removing an absent package is success.
|
|
203
|
+
function uninstallNpm(m) {
|
|
204
|
+
const pm = nodePM();
|
|
205
|
+
const tried = new Set();
|
|
206
|
+
for (const cand of [pm, "pnpm", "npm", "bun"]) {
|
|
207
|
+
if (tried.has(cand) || !has(cand)) continue;
|
|
208
|
+
tried.add(cand);
|
|
209
|
+
if (run(cand, ["rm", "-g", m.pkg])) return true;
|
|
210
|
+
}
|
|
211
|
+
console.log(C.dim(` ${m.id} not found in any global store (already gone)`));
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// demerzel & co: the isolated venv lives under ~/.seldon/<id>. Just delete it.
|
|
216
|
+
function uninstallPython(m) {
|
|
217
|
+
const dest = path.join(process.env.HOME, ".seldon", m.id);
|
|
218
|
+
if (fs.existsSync(dest)) { fs.rmSync(dest, { recursive: true, force: true }); console.log(C.dim(` removed ${dest}`)); }
|
|
219
|
+
else console.log(C.dim(` ${m.id} not present`));
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// bifrost: a single script in ~/bin or ~/.local/bin, plus ~/.config/bifrost.
|
|
224
|
+
function uninstallShell(m) {
|
|
225
|
+
let removed = false;
|
|
226
|
+
for (const p of [path.join(process.env.HOME, "bin", m.bin), path.join(process.env.HOME, ".local", "bin", m.bin)]) {
|
|
227
|
+
if (fs.existsSync(p)) { fs.rmSync(p, { force: true }); console.log(C.dim(` removed ${p}`)); removed = true; }
|
|
228
|
+
}
|
|
229
|
+
const cfg = path.join(process.env.HOME, ".config", m.id);
|
|
230
|
+
if (fs.existsSync(cfg)) { fs.rmSync(cfg, { recursive: true, force: true }); console.log(C.dim(` removed ${cfg}`)); removed = true; }
|
|
231
|
+
if (!removed) console.log(C.dim(` ${m.id} not present`));
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function uninstallOne(m) {
|
|
236
|
+
console.log("\n" + C.bold(`▸ ${m.id}`) + C.dim(` — remove`));
|
|
237
|
+
const fn = { npm: uninstallNpm, shell: uninstallShell, python: uninstallPython }[m.method];
|
|
238
|
+
const ok = fn(m);
|
|
239
|
+
console.log(ok ? C.green(` ✓ ${m.id} removed`) : C.red(` ✗ ${m.id} failed`));
|
|
240
|
+
return ok;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// A single y/N prompt. Built on the same raw-keypress mechanism as the picker
|
|
244
|
+
// (proven to release stdin and let the process exit) rather than
|
|
245
|
+
// readline.createInterface, whose stdin stayed entangled and hung after abort.
|
|
246
|
+
// One keypress: 'y' = yes, anything else (incl. Ctrl-C) = no.
|
|
247
|
+
function confirm(question) {
|
|
248
|
+
return new Promise((resolve) => {
|
|
249
|
+
if (!process.stdin.isTTY) return resolve(true); // non-interactive: assume yes (paired with an explicit ids/--all)
|
|
250
|
+
process.stdout.write(question);
|
|
251
|
+
readline.emitKeypressEvents(process.stdin);
|
|
252
|
+
process.stdin.setRawMode(true);
|
|
253
|
+
process.stdin.resume();
|
|
254
|
+
const onKey = (str, key) => {
|
|
255
|
+
process.stdin.off("keypress", onKey);
|
|
256
|
+
process.stdin.setRawMode(false);
|
|
257
|
+
process.stdin.pause();
|
|
258
|
+
process.stdin.unref();
|
|
259
|
+
process.stdout.write((str || "") + "\n");
|
|
260
|
+
resolve(/^y$/i.test(str || "") && !(key && key.ctrl));
|
|
261
|
+
};
|
|
262
|
+
process.stdin.on("keypress", onKey);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function doUninstall(ids, { yes = false, all = false } = {}) {
|
|
267
|
+
const targets = all ? MODULES.map((m) => m.id) : ids;
|
|
268
|
+
if (!targets.length) { console.log(C.red("nothing to uninstall — name modules or use `seldon uninstall --all`.")); process.exitCode = 1; return; }
|
|
269
|
+
console.log(C.bold("\nUninstall: ") + targets.join(", "));
|
|
270
|
+
console.log(C.dim(" removes their global CLI / isolated venv — never your system Python, tmux, tailscale, etc."));
|
|
271
|
+
if (!yes && !(await confirm(C.gold("\nProceed? [y/N] ")))) { console.log(C.dim("aborted.")); return; }
|
|
272
|
+
const results = targets.map((id) => [id, uninstallOne(byId[id])]);
|
|
273
|
+
// A full uninstall also drops the isolated ~/.seldon (demerzel + the uv/CPython it bootstrapped).
|
|
274
|
+
if (all) {
|
|
275
|
+
const root = path.join(process.env.HOME, ".seldon");
|
|
276
|
+
if (fs.existsSync(root)) { fs.rmSync(root, { recursive: true, force: true }); console.log(C.dim(`\n removed ${root} (isolated uv/venv store)`)); }
|
|
277
|
+
}
|
|
278
|
+
const good = results.filter(([, r]) => r).map(([i]) => i);
|
|
279
|
+
const bad = results.filter(([, r]) => !r).map(([i]) => i);
|
|
280
|
+
console.log("\n" + C.bold("Done. ") + C.green("removed: " + (good.join(", ") || "—")) + (bad.length ? " " + C.red("failed: " + bad.join(", ")) : ""));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ---- up / down / status : the friendly front door ---------------------------
|
|
284
|
+
// The stack's only always-on services are the voice (demerzel) and the
|
|
285
|
+
// supervisor loop (factory watch). We run them as detached background daemons,
|
|
286
|
+
// track them by pidfile under ~/.seldon/run, and print one map of where to go.
|
|
287
|
+
const RUN_DIR = path.join(process.env.HOME, ".seldon", "run");
|
|
288
|
+
const SELDON_HOME = path.join(process.env.HOME, ".seldon");
|
|
289
|
+
const pidFile = (name) => path.join(RUN_DIR, name + ".pid");
|
|
290
|
+
const readPid = (name) => { try { return parseInt(fs.readFileSync(pidFile(name), "utf8").trim(), 10) || 0; } catch { return 0; } };
|
|
291
|
+
const isAlive = (pid) => { try { process.kill(pid, 0); return true; } catch { return false; } };
|
|
292
|
+
const daemonUp = (name) => { const p = readPid(name); return p && isAlive(p) ? p : 0; };
|
|
293
|
+
|
|
294
|
+
function startDaemon(name, cmd, args, opts = {}) {
|
|
295
|
+
if (daemonUp(name)) return { already: true, pid: readPid(name) };
|
|
296
|
+
fs.mkdirSync(RUN_DIR, { recursive: true });
|
|
297
|
+
const log = fs.openSync(path.join(RUN_DIR, name + ".log"), "a");
|
|
298
|
+
const child = spawn(cmd, args, { detached: true, stdio: ["ignore", log, log], ...opts });
|
|
299
|
+
fs.writeFileSync(pidFile(name), String(child.pid));
|
|
300
|
+
child.unref();
|
|
301
|
+
return { pid: child.pid };
|
|
302
|
+
}
|
|
303
|
+
function stopDaemon(name) {
|
|
304
|
+
const pid = readPid(name);
|
|
305
|
+
if (!pid || !isAlive(pid)) { try { fs.rmSync(pidFile(name), { force: true }); } catch {} return false; }
|
|
306
|
+
try { process.kill(-pid, "SIGTERM"); } catch { try { process.kill(pid, "SIGTERM"); } catch {} }
|
|
307
|
+
try { fs.rmSync(pidFile(name), { force: true }); } catch {}
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
// demerzel needs its model set in the HF cache before it can serve.
|
|
311
|
+
function demerzelModelsReady() {
|
|
312
|
+
const hub = path.join(process.env.HOME, ".cache", "huggingface", "hub");
|
|
313
|
+
try { return fs.existsSync(hub) && fs.readdirSync(hub).some((d) => d.startsWith("models--")); } catch { return false; }
|
|
314
|
+
}
|
|
315
|
+
const demerzelInstalled = () => fs.existsSync(path.join(SELDON_HOME, "demerzel", ".venv", "bin", "python"));
|
|
316
|
+
|
|
317
|
+
function up() {
|
|
318
|
+
console.log(C.gold("\n seldon — bringing the stack up\n"));
|
|
319
|
+
const go = []; // where-to-go lines
|
|
320
|
+
const later = []; // things that need one action first
|
|
321
|
+
|
|
322
|
+
// Voice — demerzel (background daemon)
|
|
323
|
+
const dem = byId.demerzel;
|
|
324
|
+
if (demerzelInstalled()) {
|
|
325
|
+
const home = path.join(SELDON_HOME, "demerzel");
|
|
326
|
+
if (!platformOk(dem)) later.push("Voice (demerzel) needs macOS on Apple silicon — skipped here.");
|
|
327
|
+
else if (!demerzelModelsReady()) later.push(`Voice (demerzel) needs its models once (~25 GB):\n ${path.join(home, ".venv/bin/python")} ${path.join(home, "scripts/fetch-models.py")}\n then re-run seldon up`);
|
|
328
|
+
else {
|
|
329
|
+
const r = startDaemon("demerzel", path.join(home, ".venv/bin/python"), ["-m", "demerzel.server"], { cwd: home });
|
|
330
|
+
go.push(`${C.bold("Voice")} ${C.cyan("http://localhost:8770")} ${C.dim(r.already ? "(already up)" : "(starting — models load, ~30s)")}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Supervisor — factory watch (background daemon, the "let it work" loop)
|
|
335
|
+
if (has("factory")) {
|
|
336
|
+
const r = startDaemon("factory-watch", "factory", ["watch", "--all"]);
|
|
337
|
+
go.push(`${C.bold("Supervisor")} ${C.dim(r.already ? "already running" : "running")} — heals agents, catches stalls ${C.dim("· live view: factory board")}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// The rest are on-demand, not daemons — point at the command.
|
|
341
|
+
if (has("factory")) go.push(`${C.bold("Put agents to work")} ${C.cyan('factory new "<your idea>"')} ${C.dim("→ repo · foundation · apiary hive")}`);
|
|
342
|
+
if (has("apiary")) go.push(`${C.bold("Agent rooms")} ${C.cyan("apiary ps")} ${C.dim("(active rooms + join links)")}`);
|
|
343
|
+
if (has("bifrost")) go.push(`${C.bold("Across machines / phone")} ${C.cyan("bifrost sessions")}`);
|
|
344
|
+
|
|
345
|
+
if (go.length) { console.log(C.bold(" Where to go:")); for (const l of go) console.log(" • " + l); }
|
|
346
|
+
else console.log(C.dim(" nothing to start — install services first: seldon install factory demerzel"));
|
|
347
|
+
if (later.length) { console.log("\n" + C.gold(" First, one step:")); for (const l of later) console.log(" • " + l); }
|
|
348
|
+
console.log("\n " + C.dim("stop everything: seldon down · check state: seldon status") + "\n");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function down() {
|
|
352
|
+
const names = ["demerzel", "factory-watch"];
|
|
353
|
+
const stopped = names.filter((n) => stopDaemon(n));
|
|
354
|
+
console.log(stopped.length ? C.green("stopped: ") + stopped.join(", ") : C.dim("nothing was running."));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function statusCmd() {
|
|
358
|
+
console.log(C.bold("\nseldon services\n"));
|
|
359
|
+
const rows = [["Voice (demerzel)", "demerzel"], ["Supervisor (factory watch)", "factory-watch"]];
|
|
360
|
+
for (const [label, name] of rows) {
|
|
361
|
+
const pid = daemonUp(name);
|
|
362
|
+
console.log(` ${label.padEnd(28)} ${pid ? C.green("up") + C.dim(` (pid ${pid})`) : C.dim("down")}`);
|
|
363
|
+
}
|
|
364
|
+
console.log(C.dim(`\n logs: ${RUN_DIR}/<service>.log\n`));
|
|
365
|
+
}
|
|
366
|
+
|
|
197
367
|
// ---- the checklist TUI ------------------------------------------------------
|
|
198
368
|
const BANNER = [
|
|
199
369
|
" ███████╗███████╗██╗ ██████╗ ██████╗ ███╗ ██╗",
|
|
@@ -265,11 +435,16 @@ function help() {
|
|
|
265
435
|
${C.bold("seldon")} — install the Seldon stack, pick what you use.
|
|
266
436
|
|
|
267
437
|
${C.bold("seldon")} open the checklist (space to pick, enter to install)
|
|
438
|
+
${C.bold("seldon up")} start the stack (voice + supervisor) and print where to go
|
|
439
|
+
${C.bold("seldon status")} what's running · ${C.bold("seldon down")} stop it
|
|
268
440
|
${C.bold("seldon install")} [ids…] install everything picked, or the named modules
|
|
441
|
+
${C.bold("seldon uninstall")} ids… remove the named modules (global CLI / isolated venv)
|
|
442
|
+
${C.bold("seldon uninstall --all")} remove the whole stack (also drops ~/.seldon)
|
|
269
443
|
${C.bold("seldon doctor")} [ids…] check external deps (tmux, sops, age, tailscale, python…)
|
|
270
444
|
${C.bold("seldon list")} list the modules
|
|
271
445
|
${C.bold("seldon --dev")} link node modules from this checkout instead of npm
|
|
272
446
|
${C.bold("seldon install --pm=pnpm")} force the node package manager (pnpm|bun|npm; auto-detected)
|
|
447
|
+
${C.dim("uninstall takes --yes to skip the confirm prompt.")}
|
|
273
448
|
|
|
274
449
|
${C.dim("node tools install via pnpm/bun if present (faster), else npm.")}
|
|
275
450
|
${C.dim("python (demerzel) installs into its own isolated venv — never your system Python;")}
|
|
@@ -283,7 +458,8 @@ const argv = process.argv.slice(2);
|
|
|
283
458
|
const dev = argv.includes("--dev");
|
|
284
459
|
const args = argv.filter((a) => !a.startsWith("--"));
|
|
285
460
|
const cmd = args[0];
|
|
286
|
-
const
|
|
461
|
+
const tokens = args.slice(1); // raw names after the command
|
|
462
|
+
const ids = tokens.filter((id) => byId[id]); // only the valid module ids
|
|
287
463
|
|
|
288
464
|
(async () => {
|
|
289
465
|
if (argv.includes("--help") || cmd === "help") return help();
|
|
@@ -293,6 +469,15 @@ const ids = args.slice(1).filter((id) => byId[id]);
|
|
|
293
469
|
if (ids.length) return doInstall(ids, { dev });
|
|
294
470
|
// install with no ids -> fall through to the picker
|
|
295
471
|
}
|
|
472
|
+
if (cmd === "uninstall") {
|
|
473
|
+
const all = argv.includes("--all");
|
|
474
|
+
const bad = tokens.filter((t) => !byId[t]);
|
|
475
|
+
if (bad.length) { console.log(C.red(`unknown module(s): ${bad.join(", ")}`)); console.log(C.dim(`modules: ${MODULES.map((m) => m.id).join(", ")}`)); process.exitCode = 1; return; }
|
|
476
|
+
return doUninstall(ids, { all, yes: argv.includes("--yes") });
|
|
477
|
+
}
|
|
478
|
+
if (cmd === "up") return up();
|
|
479
|
+
if (cmd === "down") return down();
|
|
480
|
+
if (cmd === "status") return statusCmd();
|
|
296
481
|
if (cmd && cmd !== "install") { help(); process.exitCode = 1; return; }
|
|
297
482
|
// interactive
|
|
298
483
|
if (!process.stdin.isTTY) { console.log(C.red("no TTY — use: seldon install <ids…>")); process.exitCode = 1; return; }
|