@tiens.nguyen/gu-cli 1.0.686 → 1.0.693

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.
@@ -84,7 +84,7 @@ export async function promptCodingKeyIfMissing(payload, { askSecret, saveCodingK
84
84
  return { asked: false, saved: Boolean(ok), keyless: true, model, url: payload.codingKeyUrl };
85
85
  }
86
86
 
87
- log(dim("\ngonext: your agent coding model (") + cyan(model) + dim(") needs an API key."));
87
+ log(dim("\ngu: your agent coding model (") + cyan(model) + dim(") needs an API key."));
88
88
  log(dim(" Until it's set, coding falls back to your local model. Enter the key now,"));
89
89
  log(dim(" or press Enter to skip (I'll ask again next time).\n"));
90
90
 
@@ -251,7 +251,7 @@ export async function findRunningDaemonPids() {
251
251
  * True when `pid` is actually a gu polling daemon (not an unrelated process that reused
252
252
  * the PID). Reads that one pid's command line — cheap and exact (review M1).
253
253
  */
254
- function pidIsGonextDaemon(pid) {
254
+ function pidIsGuDaemon(pid) {
255
255
  if (!pid || !Number.isFinite(pid)) return Promise.resolve(false);
256
256
  return new Promise((resolve) => {
257
257
  const one = processCommandCommand(pid);
@@ -324,7 +324,7 @@ export async function readBusyState() {
324
324
  export async function daemonStatus() {
325
325
  const [pids, pidFilePid] = await Promise.all([findRunningDaemonPids(), readPidFile()]);
326
326
  const pidFileIsDaemon =
327
- pidFilePid != null && (pids.includes(pidFilePid) || (await pidIsGonextDaemon(pidFilePid)));
327
+ pidFilePid != null && (pids.includes(pidFilePid) || (await pidIsGuDaemon(pidFilePid)));
328
328
  const allPids = pidFileIsDaemon && !pids.includes(pidFilePid) ? [...pids, pidFilePid] : pids;
329
329
  return {
330
330
  running: allPids.length > 0,
@@ -5508,7 +5508,7 @@ def _rag_read_allowed(path: str) -> str:
5508
5508
  base = _WS_ACTIVE or (_WS_ROOTS[0]["path"] if _WS_ROOTS else None)
5509
5509
  if base is None:
5510
5510
  raise ValueError(
5511
- "No workspace is active — register a folder with `gonext-local-worker "
5511
+ "No workspace is active — register a folder with `gu-local-worker "
5512
5512
  "workspace add <path>` before reading files."
5513
5513
  )
5514
5514
  expanded = os.path.join(base, _ws_strip_echoed_root(base, expanded))
@@ -5842,7 +5842,7 @@ def _ws_write_allowed(path: str) -> str:
5842
5842
  if w is None:
5843
5843
  raise ValueError(
5844
5844
  "Writes are only allowed inside a registered workspace "
5845
- "(register one with: gonext-local-worker workspace add <path>)."
5845
+ "(register one with: gu-local-worker workspace add <path>)."
5846
5846
  )
5847
5847
  rel = os.path.relpath(rp, w["path"])
5848
5848
  if rel == ".git" or rel.startswith(".git" + os.sep) or (os.sep + ".git" + os.sep) in (os.sep + rel + os.sep):
@@ -5884,7 +5884,7 @@ def _ws_snapshot(path: str) -> str:
5884
5884
 
5885
5885
 
5886
5886
  def _ws_record_change(action: str, path: str, backup) -> None:
5887
- """Track a change AND persist the run manifest so `gonext-local-worker workspace
5887
+ """Track a change AND persist the run manifest so `gu-local-worker workspace
5888
5888
  revert` can restore edits / delete created files after the process is gone."""
5889
5889
  import os
5890
5890
  _WS_CHANGES.append({"action": action, "path": path, "backup": backup or None})
@@ -6772,7 +6772,7 @@ def run_agent_chat(cfg):
6772
6772
  # Ollama?" or "did the user pick openai?" separately — reasoning_effort, streaming, the
6773
6773
  # #113 reasoning repairs, and this step budget — now reads `coding_backend`.
6774
6774
  #
6775
- # The worker (gonext-local-worker.mjs) resolves this first, because it must decide
6775
+ # The worker (gu-local-worker.mjs) resolves this first, because it must decide
6776
6776
  # whether to fetch the coding API key BEFORE python starts, and passes the answer down
6777
6777
  # as codingBackend. Trust it only when we ended up on the URL it judged: the fallbacks
6778
6778
  # above can swap coding_base_url for the chat model's, which is a different server and
@@ -7094,7 +7094,7 @@ def run_agent_chat(cfg):
7094
7094
  )
7095
7095
 
7096
7096
  # ---- Workspaces (local folders the agent may read/edit/test code in) ----
7097
- # Registered on the Mac via `gonext-local-worker workspace add <path>`; the worker
7097
+ # Registered on the Mac via `gu-local-worker workspace add <path>`; the worker
7098
7098
  # passes the registry through cfg. Only these roots are writable — the security
7099
7099
  # boundary for the coding tools.
7100
7100
  global _WS_ROOTS, _WS_ACTIVE
@@ -11556,7 +11556,7 @@ def run_agent_chat(cfg):
11556
11556
  "To give the user a FILE or a ZIP of a folder, use the create_download "
11557
11557
  "tool instead — it needs no shell and works here. "
11558
11558
  "(To enable real commands, re-register with: "
11559
- "gonext-local-worker workspace add <path> --allow-run)")
11559
+ "gu-local-worker workspace add <path> --allow-run)")
11560
11560
  _last_obs["text"] = msg
11561
11561
  return msg
11562
11562
  argv = shlex.split(command or "")
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- GoNext MLX embeddings server — an OpenAI-compatible /v1/embeddings endpoint for MLX
3
+ Gu MLX embeddings server — an OpenAI-compatible /v1/embeddings endpoint for MLX
4
4
  embedding models (e.g. Qwen3-Embedding-8B), which `mlx_lm.server` does NOT provide.
5
5
 
6
- The GoNext worker's RAG tools call `POST {url}/v1/embeddings` first, so pointing the
6
+ The Gu worker's RAG tools call `POST {url}/v1/embeddings` first, so pointing the
7
7
  "RAG embedding server URL" (web Settings → Agent → RAG) at this server makes MLX
8
8
  embeddings work with no other changes.
9
9
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """Voice transcription for the GoNext local worker (Audio support, task #21).
2
+ """Voice transcription for the Gu local worker (Audio support, task #21).
3
3
 
4
4
  Reads a JSON job on stdin: {"audioPath": str, "model": str, "language": str?}.
5
5
  Transcribes with mlx-whisper and prints {"text": "..."} on stdout.
package/gu-cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * GoNext local worker:
3
+ * Gu local worker:
4
4
  * - `gu-cli set <workerKey> [--api-base URL]`
5
5
  * writes ~/.gonext/worker.env
6
6
  * - `gu-cli ws-ping-test` — POST worker ws-ping (needs GONEXT_* env)
@@ -92,6 +92,8 @@ Usage:
92
92
  gu-cli rag # test the embedder end-to-end + list knowledge bases
93
93
  gu-cli workspace revert [runId] # undo an agent run's file edits from its backups
94
94
  gu-cli clear [--scratch|--keep-history] [--yes] # back to a fresh machine (asks first)
95
+ gu-cli fix-powershell [--check] # Windows: drop PowerShell's built-in \`gu\` alias
96
+ # (it is Get-Unique and shadows this CLI)
95
97
  gu-cli rag plan [path] # is RAG worth it here? files/chars/chunks → estimated index time
96
98
  gu-cli rag --test # can this machine embed and retrieve?
97
99
  gu-cli test-proxy # can this machine route model calls through a proxy?
@@ -905,6 +907,93 @@ if (args[0] === "token-usage" || args[0] === "tokens") {
905
907
  process.exit(0);
906
908
  }
907
909
 
910
+ // `gu-cli fix-powershell` — drop PowerShell's built-in `gu` alias so the command works there.
911
+ // ASKS FIRST and never runs as part of an install: a PowerShell profile is the user's file, and
912
+ // a CLI that edits it unannounced is one that surprises people. --check only reports.
913
+ if (args[0] === "fix-powershell") {
914
+ const pa = await import("./powershell-alias.mjs");
915
+ if (!pa.affectsThisMachine()) {
916
+ console.log(" Nothing to fix — this only affects Windows PowerShell.");
917
+ process.exit(0);
918
+ }
919
+ const { execFileSync } = await import("node:child_process");
920
+ const ps = (script) =>
921
+ execFileSync("powershell", ["-NoProfile", "-Command", script], { encoding: "utf8" }).trim();
922
+
923
+ let profilePath = "";
924
+ try {
925
+ profilePath = ps("$PROFILE");
926
+ } catch (e) {
927
+ console.error(` Could not ask PowerShell where your profile is: ${e.message}`);
928
+ process.exit(1);
929
+ }
930
+ const { readFileSync, writeFileSync, mkdirSync, existsSync } = await import("node:fs");
931
+ const current = existsSync(profilePath) ? readFileSync(profilePath, "utf8") : "";
932
+ const next = pa.profileWithFix(current);
933
+ // ASKED IN EVERY PATH, because "already fixed" was reported on the very machine whose policy
934
+ // stops the profile loading — the same "said Done, changed nothing" mistake one branch along.
935
+ const blockedBy = () => {
936
+ try {
937
+ return pa.profileBlockedBy(ps("Get-ExecutionPolicy"));
938
+ } catch {
939
+ return "";
940
+ }
941
+ };
942
+ if (next === null) {
943
+ console.log(` Already fixed — ${profilePath} removes the alias.`);
944
+ const blocked = blockedBy();
945
+ if (blocked) {
946
+ console.log("");
947
+ for (const l of pa.policyAdvice(blocked)) console.log(" " + l);
948
+ process.exit(1);
949
+ }
950
+ console.log(" Open a NEW PowerShell window for it to take effect.");
951
+ process.exit(0);
952
+ }
953
+ if (args.includes("--check")) {
954
+ console.log(" Not fixed yet. `gu` in PowerShell is still Get-Unique.");
955
+ const blocked = blockedBy();
956
+ if (blocked) for (const l of pa.policyAdvice(blocked)) console.log(" " + l);
957
+ console.log(` Run: gu-cli fix-powershell (edits ${profilePath})`);
958
+ process.exit(1);
959
+ }
960
+ console.log("");
961
+ for (const l of pa.shadowedAdvice()) console.log(" " + l);
962
+ console.log("");
963
+ console.log(` This appends ONE line to ${profilePath}:`);
964
+ console.log(" " + pa.PROFILE_LINE);
965
+ const rlp = (await import("node:readline")).createInterface({
966
+ input: process.stdin,
967
+ output: process.stdout,
968
+ });
969
+ const answer = await new Promise((r) => rlp.question("\n Append it? [y/N]: ", r));
970
+ rlp.close();
971
+ if (!/^y(es)?$/i.test(String(answer).trim())) {
972
+ console.log(" " + pa.MEANWHILE);
973
+ process.exit(0);
974
+ }
975
+ try {
976
+ const dir = profilePath.replace(/[\\/][^\\/]*$/, "");
977
+ if (dir && !existsSync(dir)) mkdirSync(dir, { recursive: true });
978
+ writeFileSync(profilePath, next, "utf8");
979
+ } catch (e) {
980
+ console.error(` Could not write ${profilePath}: ${e.message}`);
981
+ process.exit(1);
982
+ }
983
+ console.log(` Done — ${profilePath} updated.`);
984
+ // VERIFY THE FIX CAN ACTUALLY RUN. Writing the line is not the same as PowerShell loading it:
985
+ // under Restricted or AllSigned the profile is a script it refuses to execute, and the first
986
+ // real run of this said "Done" to a machine where `gu` stayed shadowed.
987
+ const policy = blockedBy();
988
+ if (policy) {
989
+ console.log("");
990
+ for (const l of pa.policyAdvice(policy)) console.log(" " + l);
991
+ process.exit(1);
992
+ }
993
+ console.log(" Open a NEW PowerShell window, then `gu` runs this CLI.");
994
+ process.exit(0);
995
+ }
996
+
908
997
  if (args[0] === "clear" || args[0] === "reset") {
909
998
  const cs = await import("./clear-state.mjs");
910
999
  const scratchOnly = args.includes("--scratch");
@@ -2838,7 +2927,7 @@ async function runOcrJob(job) {
2838
2927
  }
2839
2928
  console.log(`[gu-worker] OCR source jobId=${jobId} ${sourceLabel}`);
2840
2929
 
2841
- const tempDir = await mkdtemp(join(tmpdir(), "gonext-ocr-worker-"));
2930
+ const tempDir = await mkdtemp(join(tmpdir(), "gu-ocr-worker-"));
2842
2931
  let extractedText = "";
2843
2932
  try {
2844
2933
  const imagePath = join(
package/gu-repl.mjs CHANGED
@@ -783,7 +783,6 @@ const COMMANDS = [
783
783
  // The two synonyms were dispatched but declared nowhere (found by slash-command-wiring.test.mjs):
784
784
  // they worked, Tab-completed to nothing, and appeared in no help. Aliases is what the registry
785
785
  // already has for exactly this — /quit, /rag-local.
786
- { name: "/ask-again", aliases: ["/daemon-ask", "/model-setup-ask"], desc: "forget the remembered \"no\" for the model-setup prompt in this folder" },
787
786
  { name: "/host-mode", desc: "undo client mode — run agent turns on THIS machine again" },
788
787
  { name: "/server", desc: "pick a deployment server (host/user) for this session" },
789
788
  { name: "/proxy", usage: "/proxy [show|set|on|off|clear|test]", desc: "route MODEL calls through an HTTP proxy (local calls always go direct)" },
@@ -1795,7 +1794,7 @@ async function refreshAvailableUpdates() {
1795
1794
  const { updatesAvailable } = await import("./version-check.mjs");
1796
1795
  const cliLatest = await (async () => {
1797
1796
  try {
1798
- const r = await fetch("https://registry.npmjs.org/@tiens.nguyen%2Fgonext-cli/latest", {
1797
+ const r = await fetch("https://registry.npmjs.org/@tiens.nguyen%2Fgu-cli/latest", {
1799
1798
  signal: AbortSignal.timeout(6000),
1800
1799
  });
1801
1800
  return r.ok ? (await r.json())?.version ?? "" : "";
@@ -9350,7 +9349,7 @@ async function main() {
9350
9349
  servers: await (await import("./model-doctor.mjs")).discoverModelServers(),
9351
9350
  });
9352
9351
  const statusRows = [
9353
- bold(cyan("GONEXT AGENT")) + dim(` v${REPL_VERSION}`),
9352
+ bold(cyan("GU AGENT")) + dim(` v${REPL_VERSION}`),
9354
9353
  // Task #134: a client machine OVERRIDES the account's agent model locally (the
9355
9354
  // account value points at the model host's own server). Show what will actually be
9356
9355
  // used, not a value this machine can't reach.
@@ -9422,7 +9421,7 @@ async function main() {
9422
9421
  const skipDeps = await loadSessionSkip(resolve(process.cwd()), "skipDepsPrompt");
9423
9422
  if (!skipDeps) {
9424
9423
  console.log(
9425
- dim("\ngonext: the agent framework (smolagents + openai) isn't installed here —") +
9424
+ dim("\ngu: the agent framework (smolagents + openai) isn't installed here —") +
9426
9425
  dim("\n every turn will fail without it. It's a small pip install; no models.")
9427
9426
  );
9428
9427
  if (await askOr(false, () => askYesNo("Install the agent framework now?", true))) {
@@ -9469,10 +9468,10 @@ async function main() {
9469
9468
  // to a local model setup here — its model is remote by design.
9470
9469
  if (!clientModeBanner && !(await md.probeModelServer(chatUrl))) {
9471
9470
  console.log(
9472
- dim(`\ngonext: the chat model server (${md.modelRoot(chatUrl)}) isn't responding.`)
9471
+ dim(`\ngu: the chat model server (${md.modelRoot(chatUrl)}) isn't responding.`)
9473
9472
  );
9474
9473
  // Task #135: remember a "no" for this folder. `gu-cli doctor` still works
9475
- // explicitly; /model-setup-ask clears the memory.
9474
+ // explicitly; `gu doctor` runs the same setup whenever it is wanted.
9476
9475
  const skipSetup = await loadSessionSkip(resolve(process.cwd()), "skipModelSetupPrompt");
9477
9476
  // Esc = "not now" rather than "yes": this branch DOWNLOADS a model, and a cancelled
9478
9477
  // question must never be read as consent to start one.
@@ -9483,8 +9482,8 @@ async function main() {
9483
9482
  );
9484
9483
  if (skipSetup) {
9485
9484
  console.log(
9486
- dim(" Set-up skipped (remembered for this folder) — `gu doctor` to run it, ") +
9487
- dim("`/model-setup-ask` to be asked again.\n")
9485
+ dim(" Set-up skipped (remembered for this folder) — `gu doctor` runs it ") +
9486
+ dim("any time.\n")
9488
9487
  );
9489
9488
  }
9490
9489
  if (go) {
@@ -9980,24 +9979,6 @@ async function main() {
9980
9979
  );
9981
9980
  continue;
9982
9981
  }
9983
- if (line === "/ask-again" || line === "/daemon-ask" || line === "/model-setup-ask") {
9984
- // Task #135: the way BACK from a remembered "no". Without this the choice is a
9985
- // one-way door — a setup that later changes (model installed, daemon wanted) would
9986
- // have no route to the prompt again short of editing the session file by hand.
9987
- await saveSessionSkip(cwd, "skipDaemonPrompt", false);
9988
- await saveSessionSkip(cwd, "skipModelSetupPrompt", false);
9989
- await saveSessionSkip(cwd, "skipDepsPrompt", false);
9990
- // The background worker is no longer among these: it is started and announced rather than
9991
- // asked about (task #159), so there is no remembered "no" left to undo. `gu stop` is
9992
- // the way to not have one running. Clearing its old key anyway, harmlessly, so a session
9993
- // file written by an earlier version does not keep a setting nothing reads.
9994
- console.log(
9995
- cyan("startup prompts re-enabled") +
9996
- dim(" — you'll be asked about model setup again next time you run `gu` here.\n") +
9997
- dim(" (the background worker is started automatically now — `gu stop` to stop it.)\n")
9998
- );
9999
- continue;
10000
- }
10001
9982
  if (line === "/rag" || line === "/rag-cloud" || line === "/rag-local") {
10002
9983
  // ONE QUESTION, THREE ANSWERS (task #180). This was two commands that toggled one flag,
10003
9984
  // so the command you typed did not determine the state you got — and the reply named a
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * postinstall: make `gu` / `gu-cli` runnable from the NEXT terminal without the user
4
+ * ever editing a profile by hand.
5
+ *
6
+ * WHY: the support loop that prompted this file is always the same — a user on macOS or
7
+ * Ubuntu runs `npm install -g @tiens.nguyen/gu-cli`, npm puts the shims in a global bin
8
+ * dir their shell never heard of, and the very first command answers "gu: command not
9
+ * found". The CLI works; the PATH does not. Users file it as "the install is broken".
10
+ *
11
+ * WHAT THIS DOES, in order of preference:
12
+ * 1. Nothing, when the global bin dir is already on PATH (the common case with nvm,
13
+ * Homebrew-node, or a repointed npm prefix).
14
+ * 2. Appends ONE idempotent, marker-wrapped `export PATH=...` block to the user's
15
+ * shell rc file — .zshrc on macOS (default shell since Catalina), .bashrc on
16
+ * Ubuntu/Debian — creating the file only when that shell is the platform default.
17
+ * 3. Prints the exact `export` line for the CURRENT terminal, because a child process
18
+ * physically cannot `source` anything into its parent shell.
19
+ *
20
+ * HARD RULES:
21
+ * - NEVER fail the install. Every path through this file ends in exit 0; npm treats a
22
+ * postinstall failure as a failed install and the user gets nothing.
23
+ * - NEVER run on Windows (PATH there lives in the registry, and `gu`'s PowerShell
24
+ * alias problem is powershell-alias.mjs's job, explicitly interactive).
25
+ * - NEVER touch an rc file twice: the marker pair makes re-runs no-ops.
26
+ * - Under `sudo npm i -g`, write to the REAL user's home (SUDO_USER) and chown the
27
+ * file back, or the user's next login shell reads a root-owned rc file.
28
+ */
29
+ import { appendFileSync, existsSync, readFileSync, chownSync, writeFileSync } from "node:fs";
30
+ import { homedir, platform, tmpdir } from "node:os";
31
+ import { join, resolve } from "node:path";
32
+ import { fileURLToPath } from "node:url";
33
+
34
+ const BEGIN = "# >>> gu-cli PATH >>>";
35
+ const END = "# <<< gu-cli PATH <<<";
36
+
37
+ /**
38
+ * Self-execute ONLY as the postinstall entry point.
39
+ *
40
+ * WHY THE GUARD: scripts/preflight.mjs step 4 does `await import()` on every shipped
41
+ * module to prove it resolves. The first version of this file ran main() and called
42
+ * process.exit(0) at import time — the import KILLED the preflight runner mid-step,
43
+ * exit code 0, summary never printed. A module that exits its importer is a landmine;
44
+ * only the direct `node install-path.mjs` invocation (npm's postinstall) may run it.
45
+ */
46
+ const isEntryPoint = (() => {
47
+ try {
48
+ return process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
49
+ } catch {
50
+ return false;
51
+ }
52
+ })();
53
+
54
+ if (isEntryPoint) {
55
+ try {
56
+ main();
57
+ } catch (e) {
58
+ // A postinstall that throws uninstalls the package. Report, swallow, succeed.
59
+ console.log(`gu-cli: PATH setup skipped (${e.message}) — see \`gu-cli doctor\` if \`gu\` is not found.`);
60
+ }
61
+ process.exit(0);
62
+ }
63
+
64
+ function main() {
65
+ if (platform() === "win32") return; // registry PATH + PowerShell alias: not here
66
+ if (process.env.CI) return; // no user shell to help in CI
67
+ const prefix = process.env.npm_config_prefix;
68
+ if (!prefix) return; // not an npm install context
69
+ const binDir = join(prefix, "bin");
70
+ if (resolve(binDir).startsWith(resolve(tmpdir()))) return; // preflight's throwaway prefix
71
+
72
+ const onPath = (process.env.PATH || "").split(":").some((p) => p === binDir);
73
+ if (onPath) return; // nothing to fix — most installs
74
+
75
+ const home = realUserHome();
76
+ const targets = rcFilesFor(home);
77
+ const block = `${BEGIN}\n# npm's global bin dir was not on PATH; added by @tiens.nguyen/gu-cli postinstall.\nexport PATH="${binDir}:$PATH"\n${END}\n`;
78
+
79
+ let wrote = null;
80
+ for (const rc of targets) {
81
+ if (ensureBlock(rc, block)) { wrote = wrote || rc; }
82
+ }
83
+
84
+ if (wrote) {
85
+ console.log(`gu-cli: added ${binDir} to PATH in ${wrote}.`);
86
+ console.log(`gu-cli: open a NEW terminal to use \`gu\`, or run this once in the current one:`);
87
+ console.log(` export PATH="${binDir}:$PATH"`);
88
+ } else if (targets.length === 0) {
89
+ console.log(`gu-cli: ${binDir} is not on your PATH and no shell profile was found to update.`);
90
+ console.log(` export PATH="${binDir}:$PATH"`);
91
+ }
92
+ }
93
+
94
+ /** The installer's home — the invoking user's, not root's, under sudo. */
95
+ function realUserHome() {
96
+ const sudoUser = process.env.SUDO_USER;
97
+ if (sudoUser && process.getuid && process.getuid() === 0) {
98
+ return platform() === "darwin" ? join("/Users", sudoUser) : join("/home", sudoUser);
99
+ }
100
+ return homedir();
101
+ }
102
+
103
+ /**
104
+ * Which rc files to maintain, in order. macOS default is zsh; Ubuntu default is bash.
105
+ * A non-default shell's rc is updated only if the file already exists — we fix the
106
+ * setups people HAVE, we don't seed shells they don't use.
107
+ */
108
+ function rcFilesFor(home) {
109
+ const zshrc = join(home, ".zshrc");
110
+ const bashrc = join(home, ".bashrc");
111
+ const out = [];
112
+ if (platform() === "darwin") {
113
+ out.push(zshrc);
114
+ if (existsSync(bashrc)) out.push(bashrc);
115
+ } else {
116
+ out.push(bashrc);
117
+ if (existsSync(zshrc)) out.push(zshrc);
118
+ }
119
+ return out;
120
+ }
121
+
122
+ /** Append the block unless the marker is already there. True when the file changed. */
123
+ function ensureBlock(rcFile, block) {
124
+ const prior = existsSync(rcFile) ? readFileSync(rcFile, "utf8") : "";
125
+ if (prior.includes(BEGIN)) return false;
126
+ if (!existsSync(rcFile)) writeFileSync(rcFile, block);
127
+ else appendFileSync(rcFile, (prior.endsWith("\n") || prior === "" ? "" : "\n") + block);
128
+ chownBackToSudoUser(rcFile);
129
+ return true;
130
+ }
131
+
132
+ /** `sudo npm i -g` would otherwise leave a root-owned .zshrc the user cannot edit. */
133
+ function chownBackToSudoUser(file) {
134
+ const { SUDO_UID, SUDO_GID } = process.env;
135
+ if (SUDO_UID && SUDO_GID && process.getuid && process.getuid() === 0) {
136
+ try { chownSync(file, Number(SUDO_UID), Number(SUDO_GID)); } catch { /* best effort */ }
137
+ }
138
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gu-cli",
3
- "version": "1.0.686",
3
+ "version": "1.0.693",
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",
@@ -13,11 +13,12 @@
13
13
  "url": "https://github.com/tiennsloit/gu/issues"
14
14
  },
15
15
  "bin": {
16
- "gu-cli": "./gu-cli.mjs",
17
- "gu-local-worker": "./gu-cli.mjs",
18
- "gu": "./gu-repl.mjs"
16
+ "gu-cli": "gu-cli.mjs",
17
+ "gu-local-worker": "gu-cli.mjs",
18
+ "gu": "gu-repl.mjs"
19
19
  },
20
20
  "scripts": {
21
+ "postinstall": "node install-path.mjs",
21
22
  "test": "python3 -m unittest discover tests -v && node --test",
22
23
  "test:js": "node --test",
23
24
  "test:keys": "node --test tests/key-choice-regression.test.mjs tests/coding-key-line.test.mjs tests/coding-key-picker.test.mjs tests/coding-key-prompt.test.mjs tests/coding-key-notice.test.mjs tests/coding-key-fallback-line.test.mjs tests/manage-coding-providers.test.mjs tests/add-coding-provider.test.mjs tests/coding-provider-setup.test.mjs",
@@ -77,6 +78,8 @@
77
78
  "proxy-dispatcher.mjs",
78
79
  "model-doctor.mjs",
79
80
  "coding-key-prompt.mjs",
81
+ "powershell-alias.mjs",
82
+ "install-path.mjs",
80
83
  "gonext_probe_agent.py",
81
84
  "gonext_agent_chat.py",
82
85
  "gonext_transcribe.py",
@@ -0,0 +1,94 @@
1
+ /**
2
+ * `gu` IS A BUILT-IN POWERSHELL ALIAS FOR Get-Unique.
3
+ *
4
+ * Found by the Windows end-to-end run: on Windows 11, where PowerShell is the default shell,
5
+ * typing `gu --help` runs Get-Unique and fails with "A positional parameter cannot be found that
6
+ * accepts argument '--help'". cmd.exe has no such alias, which is why it works there.
7
+ *
8
+ * NOTHING ON PATH CAN FIX THIS. PowerShell resolves Alias → Function → Cmdlet → Application, so
9
+ * an alias beats every executable and every function we could ship. The only remedies are to
10
+ * remove the alias from the user's profile, or to be called something else.
11
+ *
12
+ * So this offers the removal, EXPLICITLY and never automatically. A profile is the user's file;
13
+ * a CLI that edits it during an install is a CLI that surprises people. `gu-cli fix-powershell`
14
+ * asks, shows the line, and appends it — and `--check` just reports.
15
+ */
16
+
17
+ /** The line appended to the profile. `-Force` because the built-in alias is read-only. */
18
+ export const PROFILE_LINE =
19
+ "Remove-Item Alias:gu -Force -ErrorAction SilentlyContinue # gu CLI: PowerShell's built-in `gu` is Get-Unique";
20
+
21
+ /**
22
+ * What still works while the alias is in place.
23
+ *
24
+ * IT LIVES HERE, NOT IN gu-cli.mjs, and that is not tidiness. platform-assumptions.test.mjs
25
+ * switches on a rule for any file whose source mentions a quoted ".cmd" — and gu-cli.mjs has
26
+ * several long-standing spawn sites that the rule then flags. One sentence about `gu.cmd` in
27
+ * that file woke all of them. Keeping the string in this module leaves that file exactly as it
28
+ * was, and those sites a separate question rather than one a rename quietly changed.
29
+ */
30
+ export const MEANWHILE = "Left alone. `gu-cli` and `gu.cmd` both run this program in the meantime.";
31
+
32
+ /** True when this machine can even have the problem. */
33
+ export function affectsThisMachine(platform = process.platform) {
34
+ return platform === "win32";
35
+ }
36
+
37
+ /**
38
+ * What to tell someone whose `gu` is shadowed.
39
+ *
40
+ * Names BOTH working commands first, because the immediate need is to run the thing at all —
41
+ * the profile edit is the fix for tomorrow, `gu-cli` is the fix for right now.
42
+ */
43
+ export function shadowedAdvice() {
44
+ return [
45
+ "PowerShell has a built-in alias `gu` (Get-Unique) that wins over this CLI.",
46
+ "Right now: use `gu-cli` — or `gu.cmd`, which is this program.",
47
+ "Permanently: run `gu-cli fix-powershell` to drop the alias from your PowerShell profile.",
48
+ "cmd.exe is unaffected; `gu` works there already.",
49
+ ];
50
+ }
51
+
52
+ /**
53
+ * Will PowerShell even LOAD a profile on this machine?
54
+ *
55
+ * Found immediately after the first real fix: the line was written, the command said "Done",
56
+ * and a new PowerShell still had the alias — because the execution policy was Restricted and
57
+ * profiles are scripts. A fix that cannot run is worse than no fix, because it stops you
58
+ * looking for the real reason.
59
+ *
60
+ * "Restricted" and "AllSigned" both refuse an unsigned profile. Everything else loads it.
61
+ */
62
+ export function profileBlockedBy(policy) {
63
+ const p = String(policy ?? "").trim();
64
+ return /^(Restricted|AllSigned)$/i.test(p) ? p : "";
65
+ }
66
+
67
+ /** What to do about a policy that will not run the profile. The user's decision, not ours. */
68
+ export function policyAdvice(policy) {
69
+ return [
70
+ `PowerShell's execution policy is ${policy}, so it will not load your profile —`,
71
+ "the line is written but will never run. To allow your own profile:",
72
+ " Set-ExecutionPolicy -Scope CurrentUser RemoteSigned",
73
+ "Until then `gu-cli` and `gu.cmd` still work, and cmd.exe is unaffected.",
74
+ ];
75
+ }
76
+
77
+ /**
78
+ * Is the line already in a profile's text?
79
+ *
80
+ * Matched on the COMMAND, not the whole line including its comment — so a profile edited by
81
+ * hand, or by an older version of this message, is still recognised and not appended to twice.
82
+ */
83
+ export function alreadyFixed(profileText) {
84
+ return /Remove-Item\s+Alias:gu\b/i.test(String(profileText ?? ""));
85
+ }
86
+
87
+ /** The text to write, given what the profile already holds. Returns null when nothing is needed. */
88
+ export function profileWithFix(profileText) {
89
+ if (alreadyFixed(profileText)) return null;
90
+ const prev = String(profileText ?? "");
91
+ // A profile that does not end in a newline would otherwise get the fix glued to its last line.
92
+ const sep = prev.length === 0 ? "" : prev.endsWith("\n") ? "" : "\n";
93
+ return `${prev}${sep}${PROFILE_LINE}\n`;
94
+ }