@gorlitzer-labs/seldon 0.1.0

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 ADDED
@@ -0,0 +1,18 @@
1
+ # @gorlitzer-labs/seldon
2
+
3
+ The installer for **the Seldon stack**. Pick the tools you want from a checklist;
4
+ each installs via its native method (npm for the node tools, the shell installer
5
+ for bifrost, a venv for demerzel).
6
+
7
+ ```bash
8
+ npm i -g @gorlitzer-labs/seldon
9
+ seldon # open the checklist — space to pick, enter to install
10
+ seldon install factory # or install named modules (pulls in apiary + foundation)
11
+ seldon doctor # check external deps (tmux, sops, age, tailscale, python)
12
+ seldon list
13
+ seldon --dev # link node modules from a monorepo checkout instead of npm
14
+ ```
15
+
16
+ Modules: `apiary` · `foundation` · `comb` · `factory` · `bifrost` · `demerzel`.
17
+ Ticking `factory` auto-includes `apiary` + `foundation` (it needs them on PATH).
18
+ `demerzel` is offered only on macOS / Apple silicon.
package/bin/seldon.js ADDED
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env node
2
+ // seldon — the Seldon stack installer. Pick your tools; install each via its
3
+ // native method. Zero dependencies: a raw-terminal checklist, no build step.
4
+ import { execSync, spawnSync } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
6
+ import path from "node:path";
7
+ import fs from "node:fs";
8
+ import readline from "node:readline";
9
+ import { MODULES, byId, DEPS, RAW, MONOREPO, withRequires, platformOk } from "../modules.mjs";
10
+
11
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
12
+ // The monorepo root when running from a checkout (tools/seldon/bin -> ../../..)
13
+ const REPO_ROOT = path.resolve(HERE, "..", "..", "..");
14
+ const IN_CHECKOUT = fs.existsSync(path.join(REPO_ROOT, "modules", "apiary", "package.json"));
15
+
16
+ const C = {
17
+ dim: (s) => `\x1b[2m${s}\x1b[0m`, bold: (s) => `\x1b[1m${s}\x1b[0m`,
18
+ gold: (s) => `\x1b[33m${s}\x1b[0m`, green: (s) => `\x1b[32m${s}\x1b[0m`,
19
+ red: (s) => `\x1b[31m${s}\x1b[0m`, cyan: (s) => `\x1b[36m${s}\x1b[0m`,
20
+ };
21
+
22
+ function have(dep) {
23
+ const probe = DEPS[dep]?.probe;
24
+ if (!probe) return true;
25
+ try { execSync(probe, { stdio: "ignore" }); return true; } catch { return false; }
26
+ }
27
+ const depCache = {};
28
+ const depOk = (d) => (depCache[d] ??= have(d));
29
+
30
+ // ---- doctor -----------------------------------------------------------------
31
+ function doctorFor(ids) {
32
+ const needed = new Set();
33
+ for (const id of ids) for (const d of byId[id].needs) needed.add(d);
34
+ const rows = [...needed].map((d) => ({ d, ok: depOk(d), hint: DEPS[d]?.hint }));
35
+ return rows;
36
+ }
37
+ function printDoctor(ids) {
38
+ const rows = doctorFor(ids);
39
+ if (!rows.length) { console.log(C.dim(" no external deps for this selection")); return true; }
40
+ let allOk = true;
41
+ for (const r of rows) {
42
+ if (r.ok) console.log(` ${C.green("✓")} ${r.d}`);
43
+ else { allOk = false; console.log(` ${C.red("✗")} ${r.d} ${C.dim("— " + r.hint)}`); }
44
+ }
45
+ return allOk;
46
+ }
47
+
48
+ // ---- install actions --------------------------------------------------------
49
+ function run(cmd, args, opts = {}) {
50
+ console.log(C.dim(` $ ${cmd} ${args.join(" ")}`));
51
+ const r = spawnSync(cmd, args, { stdio: "inherit", ...opts });
52
+ return r.status === 0;
53
+ }
54
+
55
+ function installNpm(m, dev) {
56
+ if (dev && IN_CHECKOUT) {
57
+ const dir = path.join(REPO_ROOT, m.dir);
58
+ const pj = JSON.parse(fs.readFileSync(path.join(dir, "package.json")));
59
+ if (pj.scripts?.build) run("npm", ["--prefix", dir, "run", "build"]);
60
+ return run("npm", ["link"], { cwd: dir });
61
+ }
62
+ return run("npm", ["install", "-g", m.pkg]);
63
+ }
64
+
65
+ function installShell(m, dev) {
66
+ // bifrost: run its installer. In a checkout, run the local file; otherwise
67
+ // fetch it from the monorepo (the standalone repo is retired).
68
+ if (dev && IN_CHECKOUT) return run("bash", [path.join(REPO_ROOT, m.installer)]);
69
+ return run("bash", ["-c", `curl -fsSL ${RAW}/${m.installer} | bash`]);
70
+ }
71
+
72
+ function installPython(m, dev) {
73
+ if (!platformOk(m)) {
74
+ console.log(C.red(` ${m.id} needs macOS on Apple silicon — skipping`));
75
+ return false;
76
+ }
77
+ const dest = dev && IN_CHECKOUT
78
+ ? path.join(REPO_ROOT, m.dir)
79
+ : path.join(process.env.HOME, ".seldon", m.id);
80
+ if (!(dev && IN_CHECKOUT)) {
81
+ console.log(C.dim(` fetching ${m.id} into ${dest}`));
82
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
83
+ if (!fs.existsSync(path.join(dest, ".git"))) {
84
+ if (!run("git", ["clone", "--depth", "1", `https://github.com/${MONOREPO}.git`, dest + ".tmp"])) return false;
85
+ fs.renameSync(path.join(dest + ".tmp", m.dir), dest);
86
+ fs.rmSync(dest + ".tmp", { recursive: true, force: true });
87
+ }
88
+ }
89
+ const venv = path.join(dest, ".venv");
90
+ run("python3", ["-m", "venv", venv]);
91
+ const ok = run(path.join(venv, "bin", "pip"), ["install", "-r", path.join(dest, "requirements.txt")]);
92
+ if (ok) console.log(C.dim(` run it: ${path.join(dest, m.bin)} (venv at ${venv})`));
93
+ return ok;
94
+ }
95
+
96
+ function installOne(m, dev) {
97
+ console.log("\n" + C.bold(`▸ ${m.id}`) + C.dim(` — ${m.title}`));
98
+ const fn = { npm: installNpm, shell: installShell, python: installPython }[m.method];
99
+ const ok = fn(m, dev);
100
+ console.log(ok ? C.green(` ✓ ${m.id} installed`) : C.red(` ✗ ${m.id} failed`));
101
+ return ok;
102
+ }
103
+
104
+ function doInstall(ids, { dev } = {}) {
105
+ ids = withRequires(ids);
106
+ console.log(C.bold(`\nInstalling: `) + ids.join(", ") + (dev ? C.dim(" (dev: link from checkout)") : ""));
107
+ console.log(C.bold("\nPreflight:"));
108
+ const ok = printDoctor(ids);
109
+ if (!ok) console.log(C.gold("\n ⚠ some deps are missing — install them above, then the tool will work."));
110
+ const results = ids.map((id) => [id, installOne(byId[id], dev)]);
111
+ const good = results.filter(([, r]) => r).map(([i]) => i);
112
+ const bad = results.filter(([, r]) => !r).map(([i]) => i);
113
+ console.log("\n" + C.bold("Done. ") + C.green(good.join(", ") || "—") + (bad.length ? " " + C.red("failed: " + bad.join(", ")) : ""));
114
+ }
115
+
116
+ // ---- the checklist TUI ------------------------------------------------------
117
+ function tui() {
118
+ return new Promise((resolve) => {
119
+ const rows = MODULES.map((m) => ({ m, on: platformOk(m) && ["apiary", "foundation", "factory"].includes(m.id) }));
120
+ let cur = 0;
121
+ const methodTag = (m) => ({ npm: "npm", shell: "shell", python: "python" }[m.method]);
122
+ const draw = () => {
123
+ readline.cursorTo(process.stdout, 0, 0);
124
+ readline.clearScreenDown(process.stdout);
125
+ const out = [];
126
+ out.push(C.bold(C.gold(" The Seldon stack")) + C.dim(" — pick your tools"));
127
+ out.push(C.dim(" ↑↓ move · space toggle · a all · enter install · q quit\n"));
128
+ rows.forEach((r, i) => {
129
+ const sel = r.on ? C.green("[x]") : "[ ]";
130
+ const cursor = i === cur ? C.gold("❯") : " ";
131
+ const okp = !platformOk(r.m);
132
+ const name = (i === cur ? C.bold(r.m.id) : r.m.id).padEnd(11);
133
+ const deps = r.m.needs.map((d) => (depOk(d) ? d : C.red(d))).join(" ");
134
+ const platNote = okp ? C.red(" (unsupported OS)") : "";
135
+ out.push(` ${cursor} ${sel} ${name} ${C.dim(methodTag(r.m).padEnd(7))} ${C.dim(r.m.blurb)}${platNote}`);
136
+ if (i === cur) out.push(` ${C.dim("needs: " + (deps || "nothing"))}${r.m.requires.length ? C.dim(" · pulls in: " + r.m.requires.join(", ")) : ""}`);
137
+ });
138
+ process.stdout.write(out.join("\n") + "\n");
139
+ };
140
+ readline.emitKeypressEvents(process.stdin);
141
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
142
+ draw();
143
+ const onKey = (str, key) => {
144
+ if (key.name === "up") cur = (cur - 1 + rows.length) % rows.length;
145
+ else if (key.name === "down") cur = (cur + 1) % rows.length;
146
+ else if (key.name === "space") { if (platformOk(rows[cur].m)) rows[cur].on = !rows[cur].on; }
147
+ else if (str === "a") { const all = rows.every((r) => r.on || !platformOk(r.m)); rows.forEach((r) => { if (platformOk(r.m)) r.on = !all; }); }
148
+ else if (key.name === "return") return finish(rows.filter((r) => r.on).map((r) => r.m.id));
149
+ else if (key.name === "q" || (key.ctrl && key.name === "c")) return finish(null);
150
+ draw();
151
+ };
152
+ const finish = (ids) => {
153
+ process.stdin.off("keypress", onKey);
154
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
155
+ readline.cursorTo(process.stdout, 0); readline.clearScreenDown(process.stdout);
156
+ resolve(ids);
157
+ };
158
+ process.stdin.on("keypress", onKey);
159
+ });
160
+ }
161
+
162
+ // ---- cli --------------------------------------------------------------------
163
+ function list() {
164
+ console.log(C.bold("\nThe Seldon stack\n"));
165
+ for (const m of MODULES) {
166
+ const plat = platformOk(m) ? "" : C.red(" (unsupported here)");
167
+ console.log(` ${C.bold(m.id.padEnd(11))} ${C.dim(m.method.padEnd(7))} ${m.blurb}${plat}`);
168
+ }
169
+ console.log("");
170
+ }
171
+ function help() {
172
+ console.log(`
173
+ ${C.bold("seldon")} — install the Seldon stack, pick what you use.
174
+
175
+ ${C.bold("seldon")} open the checklist (space to pick, enter to install)
176
+ ${C.bold("seldon install")} [ids…] install everything picked, or the named modules
177
+ ${C.bold("seldon doctor")} [ids…] check external deps (tmux, sops, age, tailscale, python…)
178
+ ${C.bold("seldon list")} list the modules
179
+ ${C.bold("seldon --dev")} link node modules from this checkout instead of npm
180
+
181
+ modules: ${MODULES.map((m) => m.id).join(", ")}
182
+ `);
183
+ }
184
+
185
+ const argv = process.argv.slice(2);
186
+ const dev = argv.includes("--dev");
187
+ const args = argv.filter((a) => !a.startsWith("--"));
188
+ const cmd = args[0];
189
+ const ids = args.slice(1).filter((id) => byId[id]);
190
+
191
+ (async () => {
192
+ if (argv.includes("--help") || cmd === "help") return help();
193
+ if (cmd === "list") return list();
194
+ if (cmd === "doctor") { printDoctor(ids.length ? withRequires(ids) : MODULES.map((m) => m.id)); return; }
195
+ if (cmd === "install") {
196
+ if (ids.length) return doInstall(ids, { dev });
197
+ // install with no ids -> fall through to the picker
198
+ }
199
+ if (cmd && cmd !== "install") { help(); process.exitCode = 1; return; }
200
+ // interactive
201
+ if (!process.stdin.isTTY) { console.log(C.red("no TTY — use: seldon install <ids…>")); process.exitCode = 1; return; }
202
+ const picked = await tui();
203
+ if (!picked) { console.log(C.dim("nothing installed.")); return; }
204
+ if (!picked.length) { console.log(C.dim("nothing selected.")); return; }
205
+ doInstall(picked, { dev });
206
+ })();
package/modules.mjs ADDED
@@ -0,0 +1,114 @@
1
+ // The Seldon stack — the module registry the installer reads.
2
+ // One entry per tool: how it ships, what it needs, what it pulls in.
3
+
4
+ export const MONOREPO = "gorlitzer-labs/seldon";
5
+ export const RAW = `https://raw.githubusercontent.com/${MONOREPO}/main`;
6
+
7
+ /** @typedef {"npm"|"shell"|"python"} Method */
8
+
9
+ export const MODULES = [
10
+ {
11
+ id: "apiary",
12
+ title: "the conversation",
13
+ blurb: "shared rooms where AI agents talk, coordinate, hand off",
14
+ method: "npm",
15
+ pkg: "@gorlitzer-labs/apiary",
16
+ dir: "modules/apiary",
17
+ bin: "apiary",
18
+ needs: ["node", "tmux"], // tmux only for the terminal agents, still worth flagging
19
+ requires: [],
20
+ platforms: null, // any
21
+ },
22
+ {
23
+ id: "foundation",
24
+ title: "the seam",
25
+ blurb: "the deterministic project workflow beneath it all",
26
+ method: "npm",
27
+ pkg: "@gorlitzer-labs/foundation",
28
+ dir: "modules/foundation",
29
+ bin: "foundation",
30
+ needs: ["node"],
31
+ requires: [],
32
+ platforms: null,
33
+ },
34
+ {
35
+ id: "comb",
36
+ title: "the vault",
37
+ blurb: "keys by name; a leak audit; multi-machine secrets (SOPS + age)",
38
+ method: "npm",
39
+ pkg: "@gorlitzer-labs/comb",
40
+ dir: "modules/comb",
41
+ bin: "comb",
42
+ needs: ["node", "sops", "age"],
43
+ requires: [],
44
+ platforms: null,
45
+ },
46
+ {
47
+ id: "factory",
48
+ title: "the floor",
49
+ blurb: "the 24/7 supervisor — new · watch · board · box · realms",
50
+ method: "npm",
51
+ pkg: "@gorlitzer-labs/factory",
52
+ dir: "modules/factory",
53
+ bin: "factory",
54
+ needs: ["node"],
55
+ requires: ["apiary", "foundation"], // factory needs both on PATH
56
+ platforms: null,
57
+ },
58
+ {
59
+ id: "bifrost",
60
+ title: "the bridge",
61
+ blurb: "tmux + Tailscale; sessions survive; phone access; agent state",
62
+ method: "shell",
63
+ dir: "modules/bifrost",
64
+ installer: "modules/bifrost/install.sh",
65
+ bin: "bifrost",
66
+ needs: ["tmux", "tailscale"],
67
+ requires: [],
68
+ platforms: null, // macos / linux / termux
69
+ },
70
+ {
71
+ id: "demerzel",
72
+ title: "the voice",
73
+ blurb: "a fully-local voice you talk to (MLX, Apple silicon)",
74
+ method: "python",
75
+ dir: "modules/demerzel",
76
+ bin: "demerzel",
77
+ needs: ["python3"],
78
+ requires: [],
79
+ platforms: { platform: "darwin", arch: "arm64" }, // macOS + Apple silicon only
80
+ },
81
+ ];
82
+
83
+ export const byId = Object.fromEntries(MODULES.map((m) => [m.id, m]));
84
+
85
+ /** External tools we can probe, and how a user installs each (macOS-first hint). */
86
+ export const DEPS = {
87
+ node: { probe: "node --version", hint: "install Node 20+ (nvm, or brew install node)" },
88
+ tmux: { probe: "tmux -V", hint: "brew install tmux" },
89
+ sops: { probe: "sops --version", hint: "brew install sops" },
90
+ age: { probe: "age --version", hint: "brew install age" },
91
+ tailscale:{ probe: "tailscale version", hint: "brew install --cask tailscale (or tailscale.com/download)" },
92
+ python3: { probe: "python3 --version", hint: "brew install python@3.13" },
93
+ };
94
+
95
+ /** Expand a selection to include hard requires (factory -> apiary, foundation). */
96
+ export function withRequires(ids) {
97
+ const out = new Set(ids);
98
+ let changed = true;
99
+ while (changed) {
100
+ changed = false;
101
+ for (const id of [...out]) {
102
+ for (const req of byId[id]?.requires ?? []) {
103
+ if (!out.has(req)) { out.add(req); changed = true; }
104
+ }
105
+ }
106
+ }
107
+ return MODULES.filter((m) => out.has(m.id)).map((m) => m.id);
108
+ }
109
+
110
+ export function platformOk(m) {
111
+ if (!m.platforms) return true;
112
+ const { platform, arch } = m.platforms;
113
+ return (!platform || process.platform === platform) && (!arch || process.arch === arch);
114
+ }
package/package.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@gorlitzer-labs/seldon",
3
+ "version": "0.1.0",
4
+ "description": "Install the Seldon stack — pick your tools from a checklist; each installs via its native method.",
5
+ "type": "module",
6
+ "bin": { "seldon": "bin/seldon.js" },
7
+ "files": ["bin", "modules.mjs", "README.md"],
8
+ "engines": { "node": ">=20" },
9
+ "keywords": ["ai", "agents", "installer", "cli", "seldon", "apiary", "factory"],
10
+ "author": "Franco Berardi",
11
+ "license": "MIT",
12
+ "publishConfig": { "access": "public" }
13
+ }