@tiens.nguyen/gu-cli 1.0.789 → 1.0.790

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.
@@ -13,7 +13,7 @@
13
13
  * paired, so writing it before the API is proven usable leaves a terminal aimed at something
14
14
  * that does not work — and that state is much harder to get out of than an unfinished setup.
15
15
  */
16
- import { execFile } from "node:child_process";
16
+ import { execFile, spawn } from "node:child_process";
17
17
  import { readFile, writeFile, mkdir, rm, rename } from "node:fs/promises";
18
18
  import { writeFileSync } from "node:fs";
19
19
  import { homedir } from "node:os";
@@ -283,8 +283,9 @@ export async function probeDockerForMongo(name = MONGO_DOCKER_NAME) {
283
283
  * macOS: make a native MongoDB answer on 27017 (task #304). → { ok, explained }.
284
284
  * `explained` = it printed its own guidance, so the caller adds none.
285
285
  */
286
- export async function ensureNativeMongoMac(ask, mongo, { log = (l) => console.log(l) } = {}) {
286
+ export async function ensureNativeMongoMac(ask, mongo, { log = (l) => console.log(l), brewHasTrust = null } = {}) {
287
287
  const mn = await import("./mongo-native.mjs");
288
+ brewHasTrust ??= mn.brewHasTrust;
288
289
  const brew = mn.findBrew();
289
290
  const { action, holder } = mn.macMongoAction({
290
291
  state: mongo.silentPort ? "silent" : "closed",
@@ -299,22 +300,23 @@ export async function ensureNativeMongoMac(ask, mongo, { log = (l) => console.lo
299
300
  }
300
301
  if (action === "no-brew") {
301
302
  log("");
302
- for (const l of mn.noBrewLines()) log(dim(" " + l));
303
+ for (const l of mn.noBrewLines()) log(" " + (l.startsWith(" ") ? l : dim(l)));
303
304
  return { ok: false, explained: true };
304
305
  }
305
306
  const installing = action === "brew-install";
306
- const yes = await ask(
307
- installing
308
- ? " Install MongoDB Community with Homebrew now? [Y/n]: "
309
- : " MongoDB is installed but not running. Start it now (brew services)? [Y/n]: "
310
- );
307
+ const steps = installing ? mn.brewInstallSteps({ trust: await brewHasTrust(brew) }) : mn.BREW_START_STEPS;
308
+ // THE COMMANDS FIRST, THEN THE QUESTION (task #329): "install now?" with nothing shown asks for
309
+ // a yes to something unseen. These are exactly the lines a yes runs.
310
+ log(dim(installing ? "\n It is not installed here. This installs it (Homebrew):\n" : "\n It is installed but not running. This starts it:\n"));
311
+ for (const s of steps) log(` brew ${s.join(" ")}`);
312
+ log("");
313
+ const yes = await ask(installing ? " Install MongoDB now? [Y/n]: " : " Start MongoDB now? [Y/n]: ");
311
314
  if (String(yes ?? "").trim().toLowerCase().startsWith("n")) {
312
- log(dim("\n Not installed. To do it yourself:"));
313
- for (const s of installing ? mn.BREW_INSTALL_STEPS : mn.BREW_START_STEPS) log(dim(` brew ${s.join(" ")}`));
315
+ log(dim(`\n Not ${installing ? "installed" : "started"} — run the commands above yourself.`));
314
316
  return { ok: false, explained: true };
315
317
  }
316
318
  log("\n " + bold(installing ? "Installing MongoDB Community" : "Starting MongoDB") + dim(" Homebrew, a minute or two"));
317
- const done = await mn.runBrewSteps(brew, installing ? mn.BREW_INSTALL_STEPS : mn.BREW_START_STEPS, {
319
+ const done = await mn.runBrewSteps(brew, steps, {
318
320
  onLine: (l) => l.length < 200 && log(dim(" " + l)),
319
321
  });
320
322
  if (!done) {
@@ -331,6 +333,241 @@ export async function ensureNativeMongoMac(ask, mongo, { log = (l) => console.lo
331
333
  return { ok: true, explained: true };
332
334
  }
333
335
 
336
+ /** /etc/os-release → { id, codename, versionId }, or null where there is none (not Linux). */
337
+ export function readOsRelease({ read = (f) => readFileSync(f, "utf8") } = {}) {
338
+ let text = "";
339
+ try {
340
+ text = read("/etc/os-release");
341
+ } catch {
342
+ return null;
343
+ }
344
+ const kv = {};
345
+ for (const line of String(text).split(/\r?\n/)) {
346
+ const m = /^([A-Z_]+)=(.*)$/.exec(line.trim());
347
+ if (m) kv[m[1]] = m[2].replace(/^["']|["']$/g, "");
348
+ }
349
+ return {
350
+ id: String(kv.ID ?? "").toLowerCase(),
351
+ codename: String(kv.VERSION_CODENAME ?? kv.UBUNTU_CODENAME ?? "").toLowerCase(),
352
+ versionId: String(kv.VERSION_ID ?? ""),
353
+ };
354
+ }
355
+
356
+ /**
357
+ * Run one install command with THE TERMINAL ITSELF as its input and output. → exit code, or null
358
+ * when it could not start.
359
+ *
360
+ * `sudo` and winget ask their own questions (a password, a licence), so they get the real
361
+ * terminal rather than a pipe. gu's own reader is PAUSED for the duration: two readers on one
362
+ * terminal split the keystrokes between them, and half a sudo password typed into gu is both a
363
+ * failed install and a password in the wrong place.
364
+ */
365
+ export function runInTerminal(cmd, { platform = process.platform, spawnFn = spawn, stdin = process.stdin } = {}) {
366
+ return new Promise((resolve) => {
367
+ const wasRaw = Boolean(stdin.isTTY && stdin.isRaw);
368
+ const wasPaused = typeof stdin.isPaused === "function" ? stdin.isPaused() : true;
369
+ try {
370
+ if (wasRaw) stdin.setRawMode(false);
371
+ stdin.pause?.();
372
+ } catch { /* not a terminal — nothing to hand over */ }
373
+ const restore = (code) => {
374
+ try {
375
+ if (wasRaw) stdin.setRawMode(true);
376
+ if (!wasPaused) stdin.resume?.();
377
+ } catch { /* same */ }
378
+ resolve(code);
379
+ };
380
+ let child;
381
+ try {
382
+ child = platform === "win32"
383
+ ? spawnFn("cmd.exe", ["/d", "/s", "/c", cmd], { stdio: "inherit", windowsVerbatimArguments: true })
384
+ : spawnFn("sh", ["-c", cmd], { stdio: "inherit" });
385
+ } catch {
386
+ restore(null);
387
+ return;
388
+ }
389
+ child.on("error", () => restore(null));
390
+ child.on("close", (code) => restore(code));
391
+ });
392
+ }
393
+
394
+ /**
395
+ * Make a native MongoDB answer on this machine — SHOW the commands, ASK, then run them (#329).
396
+ * → { ok, explained }. macOS goes through Homebrew (ensureNativeMongoMac); Ubuntu through
397
+ * MongoDB's apt repo; Windows 11 through winget.
398
+ *
399
+ * WHY IT ASKS NOW. This step used to print the commands and stop — a deliberate choice, after an
400
+ * earlier "Install MongoDB now?" whose yes and no did the same thing. The answer to a question
401
+ * that changes nothing is to make it change something, not to stop asking: a first install that
402
+ * ends in "go and run these five lines, then come back" loses the people it was written for.
403
+ * So yes RUNS them, in the user's own terminal where sudo can ask for its password, and no
404
+ * leaves the same lines on screen to run by hand.
405
+ */
406
+ export async function ensureNativeMongo(ask, mongo, {
407
+ platform = process.platform,
408
+ log = (l) => console.log(l),
409
+ os = platform === "linux" ? readOsRelease() : null,
410
+ isRoot = typeof process.getuid === "function" && process.getuid() === 0,
411
+ runCmd = runInTerminal,
412
+ waitFor = null,
413
+ ensureMac = ensureNativeMongoMac,
414
+ } = {}) {
415
+ if (platform === "darwin") return ensureMac(ask, mongo, { log });
416
+ if (mongo.silentPort) {
417
+ const mn = await import("./mongo-native.mjs");
418
+ log("");
419
+ for (const l of mn.silentPortLines({ action: mn.isDockerHolder(mongo.holder) ? "docker-stuck" : "port-silent", holder: mongo.holder })) {
420
+ log(" " + (l.startsWith(" ") ? l : dim(l)));
421
+ }
422
+ return { ok: false, explained: true };
423
+ }
424
+ const plan = mongoInstallPlan(platform, { os, isRoot });
425
+ if (!plan) return { ok: false, explained: false };
426
+ const installing = !mongo.installed;
427
+ const commands = installing ? plan.commands : [plan.start];
428
+ if (!commands.length) {
429
+ // A Linux that is not Ubuntu: its package manager is not ours to guess at.
430
+ log(dim(`\n It is not installed here, and gu only knows the install commands for Ubuntu`));
431
+ log(dim(` (this is ${plan.distro || "another Linux"}). MongoDB's own instructions:`));
432
+ log(" " + plan.docs);
433
+ return { ok: false, explained: true };
434
+ }
435
+ log(dim(installing ? "\n It is not installed here. This installs it:\n" : "\n It is installed but not running. This starts it:\n"));
436
+ for (const c of commands) log(" " + c);
437
+ log("");
438
+ const needs = platform === "win32"
439
+ ? "Windows may ask for administrator permission"
440
+ : isRoot ? "" : "sudo will ask for your password";
441
+ const yes = await ask(` ${installing ? "Install" : "Start"} MongoDB now?${needs ? ` (${needs})` : ""} [Y/n]: `);
442
+ if (String(yes ?? "").trim().toLowerCase().startsWith("n")) {
443
+ log(dim(`\n Not ${installing ? "installed" : "started"} — run the commands above yourself.`));
444
+ return { ok: false, explained: true };
445
+ }
446
+ log("\n " + bold(installing ? "Installing MongoDB" : "Starting MongoDB") + dim(installing ? " a few minutes" : ""));
447
+ for (const c of commands) {
448
+ log(dim(` $ ${c}`));
449
+ const code = await runCmd(c, { platform });
450
+ if (code !== 0) {
451
+ log("\n " + red("✗ ") + (code === null ? "that command could not be started" : `that command failed (exit ${code})`) + " — the lines above say why.");
452
+ if (platform === "win32" && code === null) log(dim(` No winget here? Download the installer: ${plan.docs}`));
453
+ log(dim(" Fix it, or run the remaining commands yourself."));
454
+ return { ok: false, explained: true };
455
+ }
456
+ }
457
+ const { host, port } = mongoHostPort(LOCAL_MONGO_URI);
458
+ const wait = waitFor ?? (await import("./mongo-native.mjs")).waitForMongo;
459
+ if (!(await wait(host, port, { timeoutMs: 60000 }))) {
460
+ log("\n " + red("✗ ") + `MongoDB was ${installing ? "installed" : "started"} but is not answering at ${host}:${port} yet.`);
461
+ log(dim(platform === "win32"
462
+ ? " Check the service: sc query MongoDB — then run gu again."
463
+ : " Check the service: systemctl status mongod — then run gu again."));
464
+ return { ok: false, explained: true };
465
+ }
466
+ log(" " + cyan("✓ MongoDB is running") + dim(` at ${host}:${port} (a system service — it starts with the machine)`));
467
+ return { ok: true, explained: true };
468
+ }
469
+
470
+ /**
471
+ * "" when the database at `uri` answers a MongoDB handshake, else the "host:port" that does not.
472
+ * mongodb+srv names a cluster rather than a host:port, so it is never reported down from here.
473
+ */
474
+ export async function localMongoDown(uri = LOCAL_MONGO_URI, { check = null } = {}) {
475
+ if (/^mongodb\+srv:/i.test(String(uri))) return "";
476
+ const { host, port } = mongoHostPort(uri);
477
+ const answers = check ?? (await import("./mongo-native.mjs")).mongoAnswers;
478
+ return (await answers(host, port)) === "answers" ? "" : `${host}:${port}`;
479
+ }
480
+
481
+ /**
482
+ * A FINISHED Local-mode machine whose MongoDB has stopped answering (task #329).
483
+ * → { ok, fixed, installed }: ok = it answers now; installed = it had to be INSTALLED, so the
484
+ * database is new and empty.
485
+ *
486
+ * Setup only looks at MongoDB while setup is running. Once every step is "done" nothing looked
487
+ * again, and the API's /api/health answers "up, mongo, persistence on" without touching the
488
+ * database — so a machine whose mongod was gone (seen 2026-10-02: Homebrew reinstalled, MongoDB
489
+ * with it) got "(cannot access local mongodb…)", then a 15s wait, then "the API is running and
490
+ * stuck — is mongod running?". The question it asked the reader is one it can answer itself, in
491
+ * milliseconds, with the same handshake setup uses.
492
+ */
493
+ export async function preflightLocalMongo({
494
+ ask,
495
+ isTty = false,
496
+ platform = process.platform,
497
+ uri = LOCAL_MONGO_URI,
498
+ check = null,
499
+ probe = probeMongo,
500
+ ensure = ensureNativeMongo,
501
+ brewHasTrust = async (brew) => (await import("./mongo-native.mjs")).brewHasTrust(brew),
502
+ os = platform === "linux" ? readOsRelease() : null,
503
+ isRoot = typeof process.getuid === "function" && process.getuid() === 0,
504
+ log = (l) => console.log(l),
505
+ } = {}) {
506
+ if (!(await localMongoDown(uri, { check }))) return { ok: true, fixed: false, installed: false };
507
+ const { host, port } = mongoHostPort(uri);
508
+
509
+ const local = /^(127\.|localhost$|\[?::1\]?$)/i.test(host);
510
+ const mongo = local ? await probe() : { running: false, installed: false };
511
+ // The cheap handshake can lose a race with a mongod that is still starting; the full probe
512
+ // (mongosh ping, then the socket again) is the second opinion before anything is said.
513
+ if (mongo.running) return { ok: true, fixed: false, installed: false };
514
+
515
+ log("\n " + red("gu: MongoDB is not answering") + dim(` at ${host}:${port}`) +
516
+ red(mongo.installed || mongo.silentPort || !local ? "." : " — it is not installed on this machine."));
517
+ log(dim(" Local mode keeps your conversations, models and keys in that database, so"));
518
+ log(dim(" nothing can start without it. The API on :4100 is fine — it is waiting on this."));
519
+ if (!local) {
520
+ log(dim(`\n That database is on another machine (${host}). Start it there, then run gu again.\n`));
521
+ return { ok: false, fixed: false, installed: false };
522
+ }
523
+ if (isTty && ask) {
524
+ const r = await ensure(ask, mongo, { platform, log, os, isRoot });
525
+ if (r.ok || r.explained) {
526
+ if (!r.ok) log("\n " + dim("Then run `gu` again.") + "\n");
527
+ return { ok: r.ok, fixed: r.ok, installed: r.ok && !mongo.installed };
528
+ }
529
+ }
530
+ // NOBODY TO ASK (a pipe, CI): the same commands, printed, and nothing is run.
531
+ const plan = mongoInstallPlan(platform, { os, isRoot });
532
+ if (mongo.silentPort) {
533
+ const mn = await import("./mongo-native.mjs");
534
+ log("");
535
+ for (const l of mn.silentPortLines({ action: mn.isDockerHolder(mongo.holder) ? "docker-stuck" : "port-silent", holder: mongo.holder, port })) {
536
+ log(" " + (l.startsWith(" ") ? l : dim(l)));
537
+ }
538
+ } else if (mongo.installed && plan?.start) {
539
+ log(dim("\n It is installed but not running. Start it with:\n"));
540
+ log(" " + plan.start);
541
+ } else if (plan?.commands.length) {
542
+ log(dim("\n Install it with:\n"));
543
+ let commands = plan.commands;
544
+ if (platform === "darwin") {
545
+ // The same steps a yes would run — including `brew trust`, where this Homebrew needs it.
546
+ const mn = await import("./mongo-native.mjs");
547
+ commands = mn.brewInstallSteps({ trust: await brewHasTrust(mn.findBrew()) }).map((s) => `brew ${s.join(" ")}`);
548
+ }
549
+ for (const c of commands) log(" " + c);
550
+ } else {
551
+ log(dim("\n Install it — MongoDB's own instructions:\n"));
552
+ log(" " + (plan?.docs ?? "https://www.mongodb.com/docs/manual/installation/"));
553
+ }
554
+ log("\n " + dim("Then run `gu` again.") + "\n");
555
+ return { ok: false, fixed: false, installed: false };
556
+ }
557
+
558
+ /**
559
+ * A newly INSTALLED MongoDB is empty: the user, the worker key and the model settings this
560
+ * machine's setup wrote are not in it. Re-open exactly those steps, so the next thing `gu` does
561
+ * is its ordinary resume — which seeds the database under the SAME identity (seedDatabase reuses
562
+ * localUserId / localWorkerKey) and asks for the models again. Pure.
563
+ */
564
+ export function reopenAfterEmptyDatabase(state) {
565
+ const why = "MongoDB was reinstalled — its database is new and empty";
566
+ let next = recordStep(state, "database", "interrupted", { reason: why });
567
+ next = recordStep(next, "models", "interrupted", { reason: why });
568
+ return next;
569
+ }
570
+
334
571
  /**
335
572
  * Start MongoDB in a container and wait for it to answer. → true when it is up.
336
573
  *
@@ -734,53 +971,43 @@ export async function runClientExpertSetup({
734
971
  }
735
972
 
736
973
  if (plan.next === "mongo") {
737
- // WE ONLY CHECK. Installing a database is a system-wide change needing sudo, a package
738
- // manager and a working repo for this architecture — none of which a terminal should take
739
- // on unasked, and all of which the user can do better themselves. An earlier version asked
740
- // "Install MongoDB now? [y/N]", which was a question this code could not honour: it prints
741
- // commands either way, so "yes" and "no" did the same thing. A choice that changes nothing
742
- // is worse than no choice.
743
- console.log("\n " + bold("MongoDB is not running on this machine."));
974
+ // SHOW THE COMMANDS, ASK, THEN RUN THEM (task #329) — on macOS, Ubuntu and Windows 11 alike.
975
+ //
976
+ // This used to only check: "installing a database is a system-wide change needing sudo… the
977
+ // user can do better themselves", written after an earlier "Install MongoDB now? [y/N]" that
978
+ // printed the same commands whichever way it was answered. That was the right verdict on a
979
+ // question that changed nothing and the wrong fix for it — a first install that ends in "run
980
+ // these five lines and come back" is where people stop. So the question is back and a yes is
981
+ // honoured: the commands run in the user's own terminal, where sudo asks for its password.
982
+ console.log("\n " + bold(mongo.installed ? "MongoDB is not running on this machine." : "MongoDB is not installed on this machine."));
744
983
  console.log(dim(" Local mode keeps every conversation and token count in a database"));
745
984
  console.log(dim(" on THIS machine, so it needs one before setup can go further."));
746
985
  // A MAC GETS NATIVE MONGODB, NOT DOCKER (task #304): Homebrew's mongodb-community as a brew
747
986
  // service. Docker on a Mac puts every call through its VM, and a stuck Docker Desktop keeps
748
- // 27017 open while nothing answers — the failure that stopped this project's own Mac. The
749
- // Docker branch below stays for Linux and Windows.
987
+ // 27017 open while nothing answers — the failure that stopped this project's own Mac.
988
+ //
989
+ // ELSEWHERE, DOCKER IS OFFERED ONLY WHERE IT ALREADY ANSWERS — one container, no system
990
+ // service — and declining it, or not having it, leads to the native install rather than to
991
+ // "Docker is not available here": a machine without Docker is the ordinary case on a server
992
+ // and on Windows, not an error to open with.
750
993
  let explained = false;
751
- if (machine.platform === "darwin") {
752
- const r = await ensureNativeMongoMac(ask, mongo);
994
+ if (machine.platform !== "darwin" && !mongo.silentPort && (await probeDockerForMongo()).usable) {
995
+ if (await startMongoInDocker(ask)) {
996
+ mongo.running = true;
997
+ probe.mongoRunning = true;
998
+ }
999
+ }
1000
+ if (!probe.mongoRunning) {
1001
+ const r = await ensureNativeMongo(ask, mongo, { platform: machine.platform });
753
1002
  if (r.ok) {
754
1003
  mongo.running = true;
755
1004
  probe.mongoRunning = true;
756
1005
  }
757
1006
  explained = r.explained;
758
- } else
759
- // DOCKER FIRST, WHERE IT EXISTS. This is the one branch here that can actually finish the
760
- // job rather than print homework: one container, no tap, no system service, and the same
761
- // mongo:8 the project's own end-to-end tests run against. Only offered when `docker ps`
762
- // answers, so a machine with Docker installed-but-not-running is not sent down a dead end.
763
- // On success setup CONTINUES in this same run instead of asking the user to start over.
764
- if (await startMongoInDocker(ask)) {
765
- mongo.running = true;
766
- probe.mongoRunning = true;
767
1007
  }
768
- if (probe.mongoRunning || explained) {
769
- // fall through — running, or the Mac branch has already said what to do.
770
- } else if (mongo.installed) {
771
- console.log(dim("\n It looks installed but is not answering. Start it with:\n"));
772
- console.log(
773
- " " + (machine.platform === "darwin"
774
- ? "brew services start mongodb-community"
775
- : machine.platform === "win32"
776
- ? "net start MongoDB"
777
- : "sudo systemctl enable --now mongod")
778
- );
779
- } else {
780
- const install = mongoInstallPlan(machine.platform);
781
- console.log(dim("\n Install it however you prefer — for reference, the usual way here is:\n"));
782
- for (const c of install.commands) console.log(dim(" " + c));
783
- console.log("\n " + dim(`then check it answers: ${install.verify}`));
1008
+ if (!probe.mongoRunning && !explained) {
1009
+ // A platform with no plan at all — unreachable past the gate, kept so it is never silent.
1010
+ console.log(dim("\n Install MongoDB Community, then run gu again: https://www.mongodb.com/docs/manual/installation/"));
784
1011
  }
785
1012
  // STOP ONLY IF IT IS STILL NOT RUNNING. When Docker started one above there is nothing to
786
1013
  // wait for, so setup carries straight on in this run — sending someone away to re-run
@@ -446,7 +446,39 @@ export function mongoDockerPlan({ exists = false, port = 27017 } = {}) {
446
446
  }
447
447
 
448
448
  /** The install commands for a platform, or null when we do not support it. */
449
- export function mongoInstallPlan(platform) {
449
+ /**
450
+ * Which MongoDB apt repo an Ubuntu release uses → "focal" | "jammy" | "noble" | "" (no repo).
451
+ *
452
+ * THE CODENAME IS PART OF THE COMMAND (task #329). The plan used to say `jammy/mongodb-org/7.0`
453
+ * to every Linux machine, and that repo does not exist for Ubuntu 24.04 (noble has no 7.0 —
454
+ * checked: HTTP 404), so on the commonest server release `apt-get update` failed on the line
455
+ * setup had just printed. 8.0 is published for focal, jammy and noble. A release between two
456
+ * LTSes takes the LTS below it, which is what MongoDB's own instructions say to do.
457
+ */
458
+ export function ubuntuMongoRepo({ codename = "", versionId = "" } = {}) {
459
+ const c = String(codename).trim().toLowerCase();
460
+ if (MONGO_APT_CODENAMES.includes(c)) return c;
461
+ const v = parseFloat(String(versionId));
462
+ if (!Number.isFinite(v)) return "";
463
+ if (v >= 24.04) return "noble";
464
+ if (v >= 22.04) return "jammy";
465
+ if (v >= 20.04) return "focal";
466
+ return "";
467
+ }
468
+ export const MONGO_APT_CODENAMES = ["focal", "jammy", "noble"];
469
+ export const MONGO_APT_SERIES = "8.0";
470
+ export const MONGO_LINUX_DOCS = "https://www.mongodb.com/docs/manual/administration/install-on-linux/";
471
+
472
+ /**
473
+ * The commands that install MongoDB here, the one that starts an installed one, and how to check.
474
+ *
475
+ * `os` is /etc/os-release on Linux ({ id, codename, versionId }) and `isRoot` drops `sudo` — a
476
+ * root shell on a server often has no sudo at all, and a pasted `sudo …` then fails on the word.
477
+ * With no `os` given the Linux commands read the codename from the machine themselves, so they
478
+ * are still right when copied. A Linux that is not Ubuntu gets no commands, only `docs`: its
479
+ * package manager is not one this plan can speak for.
480
+ */
481
+ export function mongoInstallPlan(platform, { os = null, isRoot = false } = {}) {
450
482
  if (platform === "darwin") {
451
483
  return {
452
484
  platform,
@@ -455,6 +487,7 @@ export function mongoInstallPlan(platform) {
455
487
  "brew install mongodb-community",
456
488
  "brew services start mongodb-community",
457
489
  ],
490
+ start: "brew services start mongodb-community",
458
491
  verify: "mongosh --eval 'db.runCommand({ ping: 1 })'",
459
492
  };
460
493
  }
@@ -467,20 +500,36 @@ export function mongoInstallPlan(platform) {
467
500
  "winget install --id MongoDB.Server --accept-package-agreements --accept-source-agreements",
468
501
  "winget install --id MongoDB.Shell --accept-package-agreements --accept-source-agreements",
469
502
  ],
503
+ start: "net start MongoDB",
504
+ docs: "https://www.mongodb.com/try/download/community",
470
505
  verify: "mongosh --eval \"db.runCommand({ ping: 1 })\"",
471
506
  };
472
507
  }
473
508
  if (platform === "linux") {
509
+ const verify = "mongosh --eval 'db.runCommand({ ping: 1 })'";
510
+ const sudo = isRoot ? "" : "sudo ";
511
+ const start = `${sudo}systemctl enable --now mongod`;
512
+ const id = String(os?.id ?? "").trim().toLowerCase();
513
+ if (os && id && id !== "ubuntu") {
514
+ return { platform, commands: [], start, docs: MONGO_LINUX_DOCS, verify, distro: id };
515
+ }
516
+ const repo = os ? ubuntuMongoRepo(os) : '$(. /etc/os-release && echo "$VERSION_CODENAME")';
517
+ if (!repo) return { platform, commands: [], start, docs: MONGO_LINUX_DOCS, verify, distro: id || "ubuntu" };
518
+ const v = MONGO_APT_SERIES;
519
+ const key = `/usr/share/keyrings/mongodb-server-${v}.gpg`;
474
520
  return {
475
521
  platform,
476
522
  commands: [
477
- "sudo apt-get install -y gnupg curl",
478
- "curl -fsSL https://pgp.mongodb.com/server-7.0.asc | sudo gpg --dearmor -o /usr/share/keyrings/mongodb-server-7.0.gpg",
479
- 'echo "deb [signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list',
480
- "sudo apt-get update && sudo apt-get install -y mongodb-org",
481
- "sudo systemctl enable --now mongod",
523
+ `${sudo}apt-get install -y gnupg curl`,
524
+ // --yes: a second run must overwrite the key, not stop to ask whether it may.
525
+ `curl -fsSL https://pgp.mongodb.com/server-${v}.asc | ${sudo}gpg --dearmor --yes -o ${key}`,
526
+ `echo "deb [ arch=amd64,arm64 signed-by=${key} ] https://repo.mongodb.org/apt/ubuntu ${repo}/mongodb-org/${v} multiverse" | ${sudo}tee /etc/apt/sources.list.d/mongodb-org-${v}.list`,
527
+ `${sudo}apt-get update && ${sudo}apt-get install -y mongodb-org`,
528
+ start,
482
529
  ],
483
- verify: "mongosh --eval 'db.runCommand({ ping: 1 })'",
530
+ start,
531
+ docs: MONGO_LINUX_DOCS,
532
+ verify,
484
533
  };
485
534
  }
486
535
  return null;