@gorlitzer-labs/seldon 0.1.6 → 0.1.8

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.
Files changed (2) hide show
  1. package/bin/seldon.js +97 -1
  2. package/package.json +1 -1
package/bin/seldon.js CHANGED
@@ -194,6 +194,90 @@ function doInstall(ids, { dev } = {}) {
194
194
  console.log("\n" + C.bold("Done. ") + C.green(good.join(", ") || "—") + (bad.length ? " " + C.red("failed: " + bad.join(", ")) : ""));
195
195
  }
196
196
 
197
+ // ---- uninstall --------------------------------------------------------------
198
+ // Remove a node CLI from the global store. `rm -g` is the same verb for pnpm,
199
+ // npm and bun; try the chosen PM first, then any other present (a tool may have
200
+ // been installed under a different one). Removing an absent package is success.
201
+ function uninstallNpm(m) {
202
+ const pm = nodePM();
203
+ const tried = new Set();
204
+ for (const cand of [pm, "pnpm", "npm", "bun"]) {
205
+ if (tried.has(cand) || !has(cand)) continue;
206
+ tried.add(cand);
207
+ if (run(cand, ["rm", "-g", m.pkg])) return true;
208
+ }
209
+ console.log(C.dim(` ${m.id} not found in any global store (already gone)`));
210
+ return true;
211
+ }
212
+
213
+ // demerzel & co: the isolated venv lives under ~/.seldon/<id>. Just delete it.
214
+ function uninstallPython(m) {
215
+ const dest = path.join(process.env.HOME, ".seldon", m.id);
216
+ if (fs.existsSync(dest)) { fs.rmSync(dest, { recursive: true, force: true }); console.log(C.dim(` removed ${dest}`)); }
217
+ else console.log(C.dim(` ${m.id} not present`));
218
+ return true;
219
+ }
220
+
221
+ // bifrost: a single script in ~/bin or ~/.local/bin, plus ~/.config/bifrost.
222
+ function uninstallShell(m) {
223
+ let removed = false;
224
+ for (const p of [path.join(process.env.HOME, "bin", m.bin), path.join(process.env.HOME, ".local", "bin", m.bin)]) {
225
+ if (fs.existsSync(p)) { fs.rmSync(p, { force: true }); console.log(C.dim(` removed ${p}`)); removed = true; }
226
+ }
227
+ const cfg = path.join(process.env.HOME, ".config", m.id);
228
+ if (fs.existsSync(cfg)) { fs.rmSync(cfg, { recursive: true, force: true }); console.log(C.dim(` removed ${cfg}`)); removed = true; }
229
+ if (!removed) console.log(C.dim(` ${m.id} not present`));
230
+ return true;
231
+ }
232
+
233
+ function uninstallOne(m) {
234
+ console.log("\n" + C.bold(`▸ ${m.id}`) + C.dim(` — remove`));
235
+ const fn = { npm: uninstallNpm, shell: uninstallShell, python: uninstallPython }[m.method];
236
+ const ok = fn(m);
237
+ console.log(ok ? C.green(` ✓ ${m.id} removed`) : C.red(` ✗ ${m.id} failed`));
238
+ return ok;
239
+ }
240
+
241
+ // A single y/N prompt. Built on the same raw-keypress mechanism as the picker
242
+ // (proven to release stdin and let the process exit) rather than
243
+ // readline.createInterface, whose stdin stayed entangled and hung after abort.
244
+ // One keypress: 'y' = yes, anything else (incl. Ctrl-C) = no.
245
+ function confirm(question) {
246
+ return new Promise((resolve) => {
247
+ if (!process.stdin.isTTY) return resolve(true); // non-interactive: assume yes (paired with an explicit ids/--all)
248
+ process.stdout.write(question);
249
+ readline.emitKeypressEvents(process.stdin);
250
+ process.stdin.setRawMode(true);
251
+ process.stdin.resume();
252
+ const onKey = (str, key) => {
253
+ process.stdin.off("keypress", onKey);
254
+ process.stdin.setRawMode(false);
255
+ process.stdin.pause();
256
+ process.stdin.unref();
257
+ process.stdout.write((str || "") + "\n");
258
+ resolve(/^y$/i.test(str || "") && !(key && key.ctrl));
259
+ };
260
+ process.stdin.on("keypress", onKey);
261
+ });
262
+ }
263
+
264
+ async function doUninstall(ids, { yes = false, all = false } = {}) {
265
+ const targets = all ? MODULES.map((m) => m.id) : ids;
266
+ if (!targets.length) { console.log(C.red("nothing to uninstall — name modules or use `seldon uninstall --all`.")); process.exitCode = 1; return; }
267
+ console.log(C.bold("\nUninstall: ") + targets.join(", "));
268
+ console.log(C.dim(" removes their global CLI / isolated venv — never your system Python, tmux, tailscale, etc."));
269
+ if (!yes && !(await confirm(C.gold("\nProceed? [y/N] ")))) { console.log(C.dim("aborted.")); return; }
270
+ const results = targets.map((id) => [id, uninstallOne(byId[id])]);
271
+ // A full uninstall also drops the isolated ~/.seldon (demerzel + the uv/CPython it bootstrapped).
272
+ if (all) {
273
+ const root = path.join(process.env.HOME, ".seldon");
274
+ if (fs.existsSync(root)) { fs.rmSync(root, { recursive: true, force: true }); console.log(C.dim(`\n removed ${root} (isolated uv/venv store)`)); }
275
+ }
276
+ const good = results.filter(([, r]) => r).map(([i]) => i);
277
+ const bad = results.filter(([, r]) => !r).map(([i]) => i);
278
+ console.log("\n" + C.bold("Done. ") + C.green("removed: " + (good.join(", ") || "—")) + (bad.length ? " " + C.red("failed: " + bad.join(", ")) : ""));
279
+ }
280
+
197
281
  // ---- the checklist TUI ------------------------------------------------------
198
282
  const BANNER = [
199
283
  " ███████╗███████╗██╗ ██████╗ ██████╗ ███╗ ██╗",
@@ -242,6 +326,8 @@ function tui() {
242
326
  const finish = (ids) => {
243
327
  process.stdin.off("keypress", onKey);
244
328
  if (process.stdin.isTTY) process.stdin.setRawMode(false);
329
+ process.stdin.pause(); // emitKeypressEvents resumed stdin; without this the
330
+ process.stdin.unref(); // event loop stays alive and the process hangs after install
245
331
  readline.cursorTo(process.stdout, 0); readline.clearScreenDown(process.stdout);
246
332
  resolve(ids);
247
333
  };
@@ -264,10 +350,13 @@ ${C.bold("seldon")} — install the Seldon stack, pick what you use.
264
350
 
265
351
  ${C.bold("seldon")} open the checklist (space to pick, enter to install)
266
352
  ${C.bold("seldon install")} [ids…] install everything picked, or the named modules
353
+ ${C.bold("seldon uninstall")} ids… remove the named modules (global CLI / isolated venv)
354
+ ${C.bold("seldon uninstall --all")} remove the whole stack (also drops ~/.seldon)
267
355
  ${C.bold("seldon doctor")} [ids…] check external deps (tmux, sops, age, tailscale, python…)
268
356
  ${C.bold("seldon list")} list the modules
269
357
  ${C.bold("seldon --dev")} link node modules from this checkout instead of npm
270
358
  ${C.bold("seldon install --pm=pnpm")} force the node package manager (pnpm|bun|npm; auto-detected)
359
+ ${C.dim("uninstall takes --yes to skip the confirm prompt.")}
271
360
 
272
361
  ${C.dim("node tools install via pnpm/bun if present (faster), else npm.")}
273
362
  ${C.dim("python (demerzel) installs into its own isolated venv — never your system Python;")}
@@ -281,7 +370,8 @@ const argv = process.argv.slice(2);
281
370
  const dev = argv.includes("--dev");
282
371
  const args = argv.filter((a) => !a.startsWith("--"));
283
372
  const cmd = args[0];
284
- const ids = args.slice(1).filter((id) => byId[id]);
373
+ const tokens = args.slice(1); // raw names after the command
374
+ const ids = tokens.filter((id) => byId[id]); // only the valid module ids
285
375
 
286
376
  (async () => {
287
377
  if (argv.includes("--help") || cmd === "help") return help();
@@ -291,6 +381,12 @@ const ids = args.slice(1).filter((id) => byId[id]);
291
381
  if (ids.length) return doInstall(ids, { dev });
292
382
  // install with no ids -> fall through to the picker
293
383
  }
384
+ if (cmd === "uninstall") {
385
+ const all = argv.includes("--all");
386
+ const bad = tokens.filter((t) => !byId[t]);
387
+ 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; }
388
+ return doUninstall(ids, { all, yes: argv.includes("--yes") });
389
+ }
294
390
  if (cmd && cmd !== "install") { help(); process.exitCode = 1; return; }
295
391
  // interactive
296
392
  if (!process.stdin.isTTY) { console.log(C.red("no TTY — use: seldon install <ids…>")); process.exitCode = 1; return; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gorlitzer-labs/seldon",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Install the Seldon stack — pick your tools from a checklist; each installs via its native method.",
5
5
  "type": "module",
6
6
  "bin": {