@tiens.nguyen/gu-cli 1.0.764 → 1.0.765

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.
@@ -46,6 +46,9 @@ import {
46
46
  clientExpertAvailable,
47
47
  mongoInstallPlan,
48
48
  mongoDockerPlan,
49
+ buildMongoUri,
50
+ mongoShellPingCommand,
51
+ mongoAuthVerdict,
49
52
  MONGO_DOCKER_NAME,
50
53
  } from "./client-expert-setup.mjs";
51
54
 
@@ -299,6 +302,79 @@ export async function startMongoInDocker(ask, { platform = process.platform } =
299
302
  return false;
300
303
  }
301
304
 
305
+ /**
306
+ * Does MongoDB accept THIS uri — credentials included? (task #15)
307
+ *
308
+ * Asks a mongo shell, because the shell is all that exists at this point in setup: the API
309
+ * package (and the mongodb driver inside it) is downloaded LATER. `mongosh` first, the legacy
310
+ * `mongo` shell second. When neither exists the answer is { ok: null, unverifiable: true } —
311
+ * honestly "cannot check", which is a different thing from "check failed": the seed step still
312
+ * verifies the URI for real before anything is written.
313
+ */
314
+ export async function checkMongoConnection(uri) {
315
+ for (const shell of ["mongosh", "mongo"]) {
316
+ const has = await tryRun("sh", ["-c", `command -v ${shell} || true`]);
317
+ if (!has || !has.trim()) continue;
318
+ const out = await tryRun(shell, mongoShellPingCommand(uri), 12000);
319
+ return mongoAuthVerdict({ output: out });
320
+ }
321
+ return {
322
+ ok: null,
323
+ unverifiable: true,
324
+ why: "no mongo shell on this machine to check with — it will be verified for real " +
325
+ "when the database is seeded",
326
+ };
327
+ }
328
+
329
+ /**
330
+ * Confirm WHO the MongoDB on this machine belongs to, and collect credentials when it is not
331
+ * the one setup would have created (task #15).
332
+ *
333
+ * THE CASE: something is already answering on 27017 — a MongoDB the OS had before gu — and it
334
+ * has its OWN username/password, which we do not have admin access to. Seeding into it with
335
+ * the default empty credentials then fails two steps later, after a 360-package install, with
336
+ * a message about "a permissions or connection-string problem". The question belongs at the
337
+ * mongo step, where the answer is still cheap to change.
338
+ *
339
+ * Returns { uri, custom } to continue with, or { exit: true } when the user declines — the
340
+ * caller ends setup with the `gu clear` → `gu` hint.
341
+ */
342
+ export async function askMongoCredentials(ask) {
343
+ const { host, port } = mongoHostPort(LOCAL_MONGO_URI);
344
+ console.log(dim(` MongoDB is answering at ${host}:${port}.`));
345
+ console.log(dim(" If that is a MongoDB this machine already had, it may have its OWN"));
346
+ console.log(dim(" username and password — which setup does not know, and cannot change."));
347
+ const yes = await ask(" Use the default (empty username and password)? [Y/n]: ");
348
+ if (!String(yes ?? "").trim().toLowerCase().startsWith("n")) {
349
+ return { uri: LOCAL_MONGO_URI, custom: false };
350
+ }
351
+ // NO → the instance has its own credentials. Ask for them, and CHECK before any installing
352
+ // continues: a typo found here costs ten seconds; found at the seed step it costs the run.
353
+ for (let attempt = 0; attempt < 3; attempt += 1) {
354
+ const username = String(await ask(" MongoDB username: ") ?? "").trim();
355
+ const password = String(await ask(" MongoDB password: ") ?? "");
356
+ const uri = buildMongoUri({ username, password, host, port });
357
+ process.stdout.write(dim(" checking the connection…"));
358
+ const verdict = await checkMongoConnection(uri);
359
+ if (verdict.ok === true) {
360
+ console.log("\n " + cyan("✓ Connection OK") + dim(" — those credentials work"));
361
+ return { uri, custom: true };
362
+ }
363
+ if (verdict.unverifiable) {
364
+ console.log("\n " + yellow("⚠ could not verify: ") + dim(verdict.why));
365
+ const carryOn = await ask(" Continue with these credentials anyway? [Y/n]: ");
366
+ if (!String(carryOn ?? "").trim().toLowerCase().startsWith("n")) {
367
+ return { uri, custom: true, unverified: true };
368
+ }
369
+ } else {
370
+ console.log("\n " + red("✗ Check Connection failed") + dim(` — ${verdict.why}`));
371
+ }
372
+ const again = await ask(" Re-enter the username and password? [y/N]: ");
373
+ if (!String(again ?? "").trim().toLowerCase().startsWith("y")) return { exit: true };
374
+ }
375
+ return { exit: true };
376
+ }
377
+
302
378
  /**
303
379
  * Where the published API packages live. Overridable so a fork, a staging bucket or an
304
380
  * air-gapped mirror does not need a code change.
@@ -616,6 +692,36 @@ export async function runClientExpertSetup({ ask }) {
616
692
  }
617
693
 
618
694
  console.log("\n " + cyan("✓ MongoDB is running") + dim(` (${mongo.how})`));
695
+
696
+ // --- whose MongoDB is it? (task #15) ---------------------------------------------------
697
+ // Something answering on 27017 is not necessarily a database setup can USE: an instance the
698
+ // OS already had may carry its own admin credentials, and seeding into it with the default
699
+ // empty pair fails two steps later, after the 360-package install, with a message pointing
700
+ // at the wrong thing. Confirm the credentials HERE, where changing the answer is cheap.
701
+ // A uri already in state — a resumed run — is reused silently, like every other answered
702
+ // question, and a URI forced via GONEXT_LOCAL_MONGO_URI is taken as deliberate.
703
+ let mongoUri = LOCAL_MONGO_URI;
704
+ if (typeof state.localMongoUri === "string" && state.localMongoUri) {
705
+ mongoUri = state.localMongoUri;
706
+ if (mongoUri !== LOCAL_MONGO_URI) {
707
+ console.log(dim(" using the MongoDB credentials from the previous run"));
708
+ }
709
+ } else if (!process.env.GONEXT_LOCAL_MONGO_URI) {
710
+ const credsChoice = await askMongoCredentials(ask);
711
+ if (credsChoice.exit) {
712
+ console.log("\n " + dim("No working MongoDB credentials — setup stops here."));
713
+ console.log(dim(" To start over: `gu clear`, then `gu` again.\n"));
714
+ state = recordStep(state, "mongo", "interrupted", {
715
+ reason: "declined at the MongoDB credentials check",
716
+ });
717
+ await saveState(state);
718
+ return { complete: false, stoppedAt: "mongo" };
719
+ }
720
+ mongoUri = credsChoice.uri;
721
+ if (credsChoice.custom) {
722
+ state = { ...state, localMongoUri: mongoUri };
723
+ }
724
+ }
619
725
  state = recordStep(state, "mongo", "done");
620
726
  await saveState(state);
621
727
 
@@ -687,6 +793,7 @@ export async function runClientExpertSetup({ ask }) {
687
793
  try {
688
794
  creds = await seedDatabase({
689
795
  apiDir,
796
+ mongoUri,
690
797
  userId: state.localUserId,
691
798
  workerKey: state.localWorkerKey,
692
799
  });
@@ -713,7 +820,7 @@ export async function runClientExpertSetup({ ask }) {
713
820
  let health = await probeLocalApi(LOCAL_API_PORT);
714
821
  if (!health.ok) {
715
822
  console.log("\n " + bold(`Starting the local API on port ${LOCAL_API_PORT}`));
716
- await startLocalApi({ apiDir });
823
+ await startLocalApi({ apiDir, mongoUri });
717
824
  health = await waitForApi({ port: LOCAL_API_PORT });
718
825
  }
719
826
  if (!health.ok) {
@@ -808,3 +808,51 @@ export async function promptForLicenseToken(ask, initialToken, {
808
808
  }
809
809
  return { token: "", result: null, declined: true };
810
810
  }
811
+
812
+ /* ------------------------- MongoDB credentials (task #15) ------------------------------ */
813
+
814
+ /**
815
+ * Build a mongodb:// URI from parts, with the credentials URL-encoded.
816
+ *
817
+ * Encoding is not decoration: a password of `p@ss/word#1` pasted verbatim into a URI breaks
818
+ * the parse in three different ways, and the failure surfaces two steps later as "MongoDB
819
+ * answered a moment ago, so this is usually a permissions problem" — a message that sends the
820
+ * user debugging the wrong thing entirely.
821
+ */
822
+ export function buildMongoUri({ username = "", password = "", host = "127.0.0.1", port = 27017 } = {}) {
823
+ const user = String(username ?? "").trim();
824
+ if (!user) return `mongodb://${host}:${port}`;
825
+ const pass = String(password ?? "");
826
+ return `mongodb://${encodeURIComponent(user)}:${encodeURIComponent(pass)}@${host}:${port}`;
827
+ }
828
+
829
+ /** The one-liner any mongo shell answers, credentialed or not. */
830
+ export function mongoShellPingCommand(uri) {
831
+ return ["--quiet", String(uri), "--eval", "db.runCommand({ping:1}).ok"];
832
+ }
833
+
834
+ /**
835
+ * What a mongo shell's ping output means for CREDENTIALS (task #15).
836
+ *
837
+ * → { ok } true when the ping answered, false when it definitely did not,
838
+ * { ok: null, unverifiable: true } when no shell exists on this machine to ask.
839
+ *
840
+ * A socket can prove a port answers; only something that speaks the wire protocol can prove a
841
+ * username/password pair is ACCEPTED — which is the whole question when the MongoDB on 27017
842
+ * is not one we installed. The shells are what is available before the API package (and its
843
+ * mongodb driver) is downloaded, so their absence means "cannot check", not "check failed" —
844
+ * and those two get different messages because they need different fixes.
845
+ */
846
+ export function mongoAuthVerdict({ output } = {}) {
847
+ const out = String(output ?? "");
848
+ if (out.trim().endsWith("1")) return { ok: true, why: "" };
849
+ const looksLikeAuth =
850
+ /auth|AuthenticationFailed|SCRAM|not authorized|requires authentication/i.test(out);
851
+ return {
852
+ ok: false,
853
+ why: looksLikeAuth
854
+ ? "the database rejected those credentials"
855
+ : "it answered, but the ping did not succeed",
856
+ };
857
+ }
858
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gu-cli",
3
- "version": "1.0.764",
3
+ "version": "1.0.765",
4
4
  "description": "Gu CLI — the gu terminal plus the local worker that runs agent / OCR / PDF / embedding jobs on your Mac (Ollama / MLX / OpenAI-compatible).",
5
5
  "type": "module",
6
6
  "license": "MIT",