@tiens.nguyen/gu-cli 1.0.861 → 1.0.863

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/cli-token.mjs CHANGED
@@ -275,6 +275,8 @@ export async function promptForToken({
275
275
  why = "",
276
276
  // #209: the block is the computer LIMIT (not a missing/expired plan) — a Pro token is not the way out.
277
277
  overLimit = false,
278
+ // #219: with overLimit, the ONE short yellow line (the long `why` is shown only on request).
279
+ short = "",
278
280
  // #315: a saved token must also be ENTITLED on this computer. `accept(token)` → { ok } | { ok:false, reason }
279
281
  // — the REPL renews it from this computer (to learn its free time) and runs the licence rule.
280
282
  accept = null,
@@ -283,6 +285,7 @@ export async function promptForToken({
283
285
  const dev = device === undefined ? await trialDevice().catch(() => null) : device;
284
286
  const getOne = `For a free token for 1 computer, log in at ${accountUrl} and create one — or buy Pro there for more computers.`;
285
287
  let trial, trialOk, shown, hint;
288
+ let details = []; // #219: the over-limit detail, printed only when the person asks ("?" or an empty Enter)
286
289
  const offer = async () => {
287
290
  trial = dev ? await claim({ hostedBase, device: dev }) : { ok: false, why: "no-device" };
288
291
  trialOk = Boolean(trial.ok && verifyCliToken(trial.token, publicKeyB64).ok);
@@ -305,12 +308,16 @@ export async function promptForToken({
305
308
  ];
306
309
  if (why) {
307
310
  const pricingUrl = `${String(site).replace(/^https?:\/\//, "")}/pricing`;
311
+ if (overLimit) {
312
+ details = [
313
+ why,
314
+ `Remove the computers you no longer use at ${accountUrl}, then start gu again — or Pro allows more computers: ${pricingUrl}`,
315
+ `Think this is wrong? ${SUPPORT_EMAIL}`,
316
+ required ? "/exit or Ctrl+C quits gu." : "Enter to skip for now.",
317
+ ];
318
+ }
308
319
  hint = overLimit
309
- ? [
310
- `Remove the computers you no longer use at ${accountUrl}, then start gu again — or Pro allows more computers: ${pricingUrl}`,
311
- `Think this is wrong? ${SUPPORT_EMAIL}`,
312
- required ? "/exit or Ctrl+C quits gu." : "Enter to skip for now.",
313
- ]
320
+ ? [] // #219: one yellow line only — the rest is behind "?"
314
321
  : [
315
322
  // The reason (the title) already says what happened; a "trial ended on <date>" line beside it is
316
323
  // noise — and wrong for a computer that went straight to a free account and never had one.
@@ -321,7 +328,7 @@ export async function promptForToken({
321
328
  }
322
329
  };
323
330
  await offer();
324
- log(why || "This computer needs a gu token before the agent can start — one time only.", "title");
331
+ log(overLimit && why ? `${short || why} Type ? for details.` : why || "This computer needs a gu token before the agent can start — one time only.", "title");
325
332
  const entitled = async (token) => {
326
333
  if (!accept) return true;
327
334
  const r = await accept(token).catch(() => ({ ok: true })); // unknown = soft, like every other check here
@@ -334,6 +341,12 @@ export async function promptForToken({
334
341
  // A way OUT of a required question (task #17): "/exit" is not a token, and treating it as one
335
342
  // ("That token was not saved…" and the same question again) left no way to leave gu at all.
336
343
  if (/^\/(exit|quit)$/i.test(typed)) return { ok: false, why: "quit" };
344
+ // #219: the over-limit detail, on request only — "?" or an empty Enter, then the same question again.
345
+ if (details.length && (typed === "?" || (!typed && required))) {
346
+ for (const l of details) log(l, "info");
347
+ if (!required) round--; // "?" does not use up one of the two tries
348
+ continue;
349
+ }
337
350
  if (!typed) {
338
351
  if (!required) return { ok: false, why: "skipped" };
339
352
  if (!trialOk) await offer(); // no default: maybe the service is back, or the trial exists now
@@ -754,6 +767,8 @@ export function enforcementDecision({ token, publicKeyB64, now = new Date(), sit
754
767
  enforced: true,
755
768
  allowed: false,
756
769
  reason: "over-limit",
770
+ // #219: the ONE short line shown first (the long `why` is the detail, shown on request).
771
+ short: `${planName(p.plan)} allows ${computers(p.deviceLimit)}; this account is on ${p.activeDevices}.`,
757
772
  why:
758
773
  `This account has used gu on ${computers(p.activeDevices)} in the last 30 days, and the ` +
759
774
  `${planName(p.plan)} allows ${p.deviceLimit}. Sign out of one, or remove it at ${accountUrl}.`,
package/gu-cli.mjs CHANGED
@@ -1159,81 +1159,97 @@ if (args[0] === "fix-powershell") {
1159
1159
  process.exit(0);
1160
1160
  }
1161
1161
  const { execFileSync } = await import("node:child_process");
1162
- const ps = (script) =>
1163
- execFileSync("powershell", ["-NoProfile", "-Command", script], { encoding: "utf8" }).trim();
1164
-
1165
- let profilePath = "";
1166
- try {
1167
- profilePath = ps("$PROFILE");
1168
- } catch (e) {
1169
- console.error(` Could not ask PowerShell where your profile is: ${e.message}`);
1170
- process.exit(1);
1171
- }
1172
- const { readFileSync, writeFileSync, mkdirSync, existsSync } = await import("node:fs");
1173
- const current = existsSync(profilePath) ? readFileSync(profilePath, "utf8") : "";
1174
- const next = pa.profileWithFix(current);
1175
- // ASKED IN EVERY PATH, because "already fixed" was reported on the very machine whose policy
1176
- // stops the profile loading — the same "said Done, changed nothing" mistake one branch along.
1177
- const blockedBy = () => {
1162
+ const runPs = (host, script) =>
1163
+ execFileSync(host, ["-NoProfile", "-Command", script], {
1164
+ encoding: "utf8",
1165
+ stdio: ["ignore", "pipe", "ignore"],
1166
+ }).trim();
1167
+
1168
+ // EVERY installed PowerShell has its OWN profile: Windows PowerShell 5.1 (`powershell`) and
1169
+ // PowerShell 7 (`pwsh`, VS Code's usual terminal) read different files. Asking only
1170
+ // `powershell` wrote the fix where a pwsh terminal never looked.
1171
+ const found = [];
1172
+ for (const host of pa.POWERSHELL_HOSTS) {
1178
1173
  try {
1179
- return pa.profileBlockedBy(ps("Get-ExecutionPolicy"));
1174
+ found.push({ host, profilePath: runPs(host, "$PROFILE.CurrentUserAllHosts") });
1180
1175
  } catch {
1181
- return "";
1176
+ /* that PowerShell is not installed */
1182
1177
  }
1183
- };
1184
- if (next === null) {
1185
- console.log(` Already fixed — ${profilePath} removes the alias.`);
1186
- const blocked = blockedBy();
1187
- if (blocked) {
1188
- console.log("");
1189
- for (const l of pa.policyAdvice(blocked)) console.log(" " + l);
1190
- process.exit(1);
1191
- }
1192
- console.log(" Open a NEW PowerShell window for it to take effect.");
1193
- process.exit(0);
1194
1178
  }
1195
- if (args.includes("--check")) {
1196
- console.log(" Not fixed yet. `gu` in PowerShell is still Get-Unique.");
1197
- const blocked = blockedBy();
1198
- if (blocked) for (const l of pa.policyAdvice(blocked)) console.log(" " + l);
1199
- console.log(` Run: gu-cli fix-powershell (edits ${profilePath})`);
1200
- process.exit(1);
1201
- }
1202
- console.log("");
1203
- for (const l of pa.shadowedAdvice()) console.log(" " + l);
1204
- console.log("");
1205
- console.log(` This appends ONE line to ${profilePath}:`);
1206
- console.log(" " + pa.PROFILE_LINE);
1207
- const rlp = (await import("node:readline")).createInterface({
1208
- input: process.stdin,
1209
- output: process.stdout,
1210
- });
1211
- const answer = await new Promise((r) => rlp.question("\n Append it? [y/N]: ", r));
1212
- rlp.close();
1213
- if (!/^y(es)?$/i.test(String(answer).trim())) {
1214
- console.log(" " + pa.MEANWHILE);
1215
- process.exit(0);
1216
- }
1217
- try {
1218
- const dir = profilePath.replace(/[\\/][^\\/]*$/, "");
1219
- if (dir && !existsSync(dir)) mkdirSync(dir, { recursive: true });
1220
- writeFileSync(profilePath, next, "utf8");
1221
- } catch (e) {
1222
- console.error(` Could not write ${profilePath}: ${e.message}`);
1179
+ const targets = pa.uniqueProfiles(found);
1180
+ if (targets.length === 0) {
1181
+ console.error(" Could not ask PowerShell where your profile is (neither `powershell` nor `pwsh` answered).");
1223
1182
  process.exit(1);
1224
1183
  }
1225
- console.log(` Done — ${profilePath} updated.`);
1226
- // VERIFY THE FIX CAN ACTUALLY RUN. Writing the line is not the same as PowerShell loading it:
1227
- // under Restricted or AllSigned the profile is a script it refuses to execute, and the first
1228
- // real run of this said "Done" to a machine where `gu` stayed shadowed.
1229
- const policy = blockedBy();
1230
- if (policy) {
1184
+ const { readFileSync, writeFileSync, mkdirSync, existsSync } = await import("node:fs");
1185
+ const checkOnly = args.includes("--check");
1186
+ let rlp = null;
1187
+ let worst = 0;
1188
+
1189
+ for (const { host, profilePath } of targets) {
1231
1190
  console.log("");
1232
- for (const l of pa.policyAdvice(policy)) console.log(" " + l);
1233
- process.exit(1);
1191
+ console.log(` ${pa.hostLabel(host)} — ${profilePath}`);
1192
+ const current = existsSync(profilePath) ? readFileSync(profilePath, "utf8") : "";
1193
+ const next = pa.profileWithFix(current);
1194
+ // ASKED IN EVERY PATH, because "already fixed" was reported on the very machine whose policy
1195
+ // stops the profile loading — the same "said Done, changed nothing" mistake one branch along.
1196
+ const blockedBy = () => {
1197
+ try {
1198
+ return pa.profileBlockedBy(runPs(host, "Get-ExecutionPolicy"));
1199
+ } catch {
1200
+ return "";
1201
+ }
1202
+ };
1203
+ if (next === null) {
1204
+ console.log(" Already fixed — the profile removes the alias.");
1205
+ const blocked = blockedBy();
1206
+ if (blocked) {
1207
+ for (const l of pa.policyAdvice(blocked)) console.log(" " + l);
1208
+ worst = 1;
1209
+ }
1210
+ continue;
1211
+ }
1212
+ if (checkOnly) {
1213
+ console.log(" Not fixed yet. `gu` in this PowerShell is still Get-Unique.");
1214
+ const blocked = blockedBy();
1215
+ if (blocked) for (const l of pa.policyAdvice(blocked)) console.log(" " + l);
1216
+ worst = 1;
1217
+ continue;
1218
+ }
1219
+ for (const l of pa.shadowedAdvice()) console.log(" " + l);
1220
+ console.log(` This appends ONE line to ${profilePath}:`);
1221
+ console.log(" " + pa.PROFILE_LINE);
1222
+ rlp ??= (await import("node:readline")).createInterface({
1223
+ input: process.stdin,
1224
+ output: process.stdout,
1225
+ });
1226
+ const answer = await new Promise((r) => rlp.question("\n Append it? [y/N]: ", r));
1227
+ if (!/^y(es)?$/i.test(String(answer).trim())) {
1228
+ console.log(" " + pa.MEANWHILE);
1229
+ continue;
1230
+ }
1231
+ try {
1232
+ const dir = profilePath.replace(/[\\/][^\\/]*$/, "");
1233
+ if (dir && !existsSync(dir)) mkdirSync(dir, { recursive: true });
1234
+ writeFileSync(profilePath, next, "utf8");
1235
+ } catch (e) {
1236
+ console.error(` Could not write ${profilePath}: ${e.message}`);
1237
+ worst = 1;
1238
+ continue;
1239
+ }
1240
+ console.log(` Done — ${profilePath} updated.`);
1241
+ // VERIFY THE FIX CAN ACTUALLY RUN. Writing the line is not the same as PowerShell loading it:
1242
+ // under Restricted or AllSigned the profile is a script it refuses to execute, and the first
1243
+ // real run of this said "Done" to a machine where `gu` stayed shadowed.
1244
+ const policy = blockedBy();
1245
+ if (policy) {
1246
+ for (const l of pa.policyAdvice(policy)) console.log(" " + l);
1247
+ worst = 1;
1248
+ }
1234
1249
  }
1235
- console.log(" Open a NEW PowerShell window, then `gu` runs this CLI.");
1236
- process.exit(0);
1250
+ rlp?.close();
1251
+ if (worst === 0 && !checkOnly) console.log("\n Open a NEW PowerShell window (in VS Code too), then `gu` runs this CLI.");
1252
+ process.exit(worst);
1237
1253
  }
1238
1254
 
1239
1255
  if (args[0] === "support") {
package/gu-repl.mjs CHANGED
@@ -532,6 +532,10 @@ const SPINNERS = [
532
532
  // green, cyan, blue, indigo, pink, amber).
533
533
  const WAIT_COLORS_256 = [120, 87, 111, 147, 218, 221];
534
534
  const color256 = (n, s) => `\x1b[38;5;${n}m${s}\x1b[0m`;
535
+ // The thinking/waiting spinner + playful word (#216): plain white by default, the cycling
536
+ // WAIT_COLORS_256 colour only when this run was started with `gu --color`. White goes through
537
+ // paint(), so NO_COLOR / TERM=dumb still turn it off.
538
+ const waitPaint = (n, s) => (REPL_COLOR ? color256(n, s) : white(s));
535
539
  const orange = (s) => color256(208, s); // peak-token label (task #84)
536
540
  // Compact token count: 950 → "950", 12300 → "12.3k", 1500000 → "1.5M" (task #83).
537
541
  const fmtTokens = (n) => {
@@ -616,7 +620,7 @@ if (cliVerdict.kind === "run") {
616
620
  } else if (argv.includes("--help") || argv.includes("-h")) {
617
621
  console.log(
618
622
  "gu — terminal agent REPL for the current folder\n\n" +
619
- "Usage: gu [-v|--verbose] [--log] [--license <pro token>]\n" +
623
+ "Usage: gu [-v|--verbose] [--log] [--color] [--license <pro token>]\n" +
620
624
  " gu status # worker + a table of the running model servers\n" +
621
625
  " gu start|stop|restart|logs # manage the background worker\n" +
622
626
  " gu logs <port|pid> [-f] # tail a running model server's log\n" +
@@ -625,6 +629,8 @@ if (cliVerdict.kind === "run") {
625
629
  " -v, --verbose show RAG index-file activity in the bottom bar (each shard written)\n" +
626
630
  " --log log this run in detail (~/.gonext/worker.log). Without it only starts,\n" +
627
631
  " stops and errors are logged. Ends when this gu closes.\n" +
632
+ " --color colour the thinking spinner and its playful word (cycling colours).\n" +
633
+ " Without it they are plain white.\n" +
628
634
  " --license <token> pro license: a first install ASKS how this machine runs (hosted\n" +
629
635
  " client / MLX host / private). Without it, a first install is\n" +
630
636
  " private by default — the whole stack on this machine.\n" +
@@ -649,6 +655,9 @@ const REPL_VERBOSE = argv.includes("-v") || argv.includes("--verbose");
649
655
  // worker reads, and gives it back on exit. Per run by the owner's rule — nothing is saved, and a
650
656
  // run that is killed leaves a lease for a dead process, which is no lease.
651
657
  const REPL_LOG = argv.includes("--log");
658
+ // --color (#216): the thinking spinner and its playful word cycle through colours only on a run
659
+ // started with this flag; by default they are plain white.
660
+ const REPL_COLOR = argv.includes("--color");
652
661
  if (REPL_LOG) {
653
662
  const ll = await import("./log-level.mjs");
654
663
  await ll.takeLease().catch(() => {});
@@ -2479,6 +2488,8 @@ async function refreshAvailableUpdates() {
2479
2488
  * paid to see the warning go.
2480
2489
  */
2481
2490
  let tokenNotice = null;
2491
+ /** #219: true while the token question is on screen — the bar must not repeat its warning. */
2492
+ let tokenQuestionOpen = false;
2482
2493
  let tokenBusy = false;
2483
2494
  let startupRenewDone = false;
2484
2495
  /**
@@ -2999,7 +3010,7 @@ function zoneNotice(width) {
2999
3010
  }
3000
3011
  if (priceUpdate) items.push({ key: "prices", clickable: true, paint: accent, text: `${priceUpdateText()} (click)` });
3001
3012
  if (limitsUpdate) items.push({ key: "limits", clickable: true, paint: accent, text: `${limitsUpdate.label} (click)` });
3002
- if (tokenNotice?.text) items.push({ key: "token", paint: tokenNotice.urgent ? yellow : dim, text: tokenNotice.text });
3013
+ if (tokenNotice?.text && !(tokenQuestionOpen && tokenNotice.kind === "over-limit")) items.push({ key: "token", paint: tokenNotice.urgent ? yellow : dim, text: tokenNotice.text });
3003
3014
  const agents = agentsIndicator(otherAgents);
3004
3015
  if (agents) items.push({ key: "agents", clickable: true, paint: accent, text: `${agents} running (click)` });
3005
3016
  const shells = shellsNotice(shellEntries, { cwd: process.cwd() });
@@ -4265,8 +4276,8 @@ function pendingStatusRows() {
4265
4276
  const wc = WAIT_COLORS_256[Math.floor(now / 2500) % WAIT_COLORS_256.length];
4266
4277
  const secs = Math.max(0, Math.round((now - _pendingSince) / 1000));
4267
4278
  const line =
4268
- markLine(color256(wc, glyph)) +
4269
- color256(wc, `${_pendingLabel}…`) +
4279
+ markLine(waitPaint(wc, glyph)) +
4280
+ waitPaint(wc, `${_pendingLabel}…`) +
4270
4281
  (secs >= 2 ? dim(` (${secs}s)`) : "");
4271
4282
  return [clipVisible(line, width - 1)].concat(
4272
4283
  barPinned() || !process.stdout.isTTY ? [] : footerRows()
@@ -11771,13 +11782,15 @@ async function ensureTokenForTurn() {
11771
11782
  * (Esc does nothing), one Backspace clears the default for a paste, and a valid token turns the
11772
11783
  * row back into ">> ". → true once a token is saved.
11773
11784
  */
11774
- async function askForCliToken(ct, dir, { why = "", overLimit = false } = {}) {
11785
+ async function askForCliToken(ct, dir, { why = "", overLimit = false, short = "" } = {}) {
11775
11786
  console.log("");
11776
11787
  tokenPromptActive = true; // Ctrl+C now quits gu (see onInterrupt) — task #17
11788
+ tokenQuestionOpen = true;
11777
11789
  try {
11778
- return await askForCliTokenInner(ct, dir, why, overLimit);
11790
+ return await askForCliTokenInner(ct, dir, why, overLimit, short);
11779
11791
  } finally {
11780
11792
  tokenPromptActive = false;
11793
+ tokenQuestionOpen = false;
11781
11794
  }
11782
11795
  }
11783
11796
 
@@ -11795,7 +11808,7 @@ function quitFromTokenPrompt() {
11795
11808
  process.exit(0);
11796
11809
  }
11797
11810
 
11798
- async function askForCliTokenInner(ct, dir, why, overLimit = false) {
11811
+ async function askForCliTokenInner(ct, dir, why, overLimit = false, short = "") {
11799
11812
  const hostedBase = await tokenHostedBase();
11800
11813
  const pk = ct.publicKey();
11801
11814
  const site = ct.siteUrl();
@@ -11807,6 +11820,7 @@ async function askForCliTokenInner(ct, dir, why, overLimit = false) {
11807
11820
  hostedBase,
11808
11821
  why,
11809
11822
  overLimit, // #209: the computer limit, not a missing plan — no "paste a Pro token"
11823
+ short, // #219: the one short yellow line; the rest is behind "?"
11810
11824
  // #315: a token is only good if THIS computer may use it — renew it from here (the server then
11811
11825
  // says how much free time the computer has), save what came back, and run the licence rule.
11812
11826
  accept: async (token) => {
@@ -11884,7 +11898,7 @@ async function askTokenAtStart() {
11884
11898
  return;
11885
11899
  }
11886
11900
  }
11887
- await askForCliToken(ct, dir, { why: d.why, overLimit: d.reason === "over-limit" });
11901
+ await askForCliToken(ct, dir, { why: d.why, overLimit: d.reason === "over-limit", short: d.short ?? "" });
11888
11902
  }
11889
11903
  /** The token refresh started at launch — askTokenAtStart waits for it (see there). */
11890
11904
  let startupTokenRefresh = null;
@@ -12458,7 +12472,7 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
12458
12472
  for (const wl of expLines) contentRows.push(clipRow(GUTTER + dim(wl)));
12459
12473
  for (let i = 0; i < WORD_GAP; i++) contentRows.push(""); // see WORD_GAP
12460
12474
  contentRows.push(
12461
- clipRow(`${markLine(color256(wc, glyph))}${color256(wc, `${thinkWord}…`)}${outLabel}`)
12475
+ clipRow(`${markLine(waitPaint(wc, glyph))}${waitPaint(wc, `${thinkWord}…`)}${outLabel}`)
12462
12476
  );
12463
12477
  // PINNED → the block IS frozen rows; unpinned (a window too short to reserve any) there is
12464
12478
  // no zone, so it keeps the old in-place draw at the output position.
package/gu_browser.py CHANGED
@@ -291,12 +291,42 @@ def should_offer(now=None):
291
291
 
292
292
 
293
293
  def mark_offered():
294
+ """→ whether the marker was written (a failed write must not mean "offer again", #217)."""
294
295
  try:
295
296
  os.makedirs(os.path.dirname(_offered_path()), exist_ok=True)
296
297
  with open(_offered_path(), "w") as f:
297
298
  f.write(str(int(time.time())))
299
+ return True
298
300
  except OSError:
299
- pass
301
+ return False
302
+
303
+
304
+ def claim_offer(now=None):
305
+ """Atomically decide that THIS process may open the store page (#217): True for exactly one of
306
+ several gu processes racing, and only if it was not opened in the last OFFER_EVERY_S. False when
307
+ GU_NO_OPEN_APPS is set (tests) or when the marker cannot be written — never offer without a
308
+ record that we did."""
309
+ if os.environ.get("GU_NO_OPEN_APPS"):
310
+ return False
311
+ lock = _offered_path() + ".lock"
312
+ try:
313
+ os.makedirs(os.path.dirname(lock), exist_ok=True)
314
+ try:
315
+ if time.time() - os.path.getmtime(lock) > 60: # a crashed claimer's leftover
316
+ os.remove(lock)
317
+ except OSError:
318
+ pass
319
+ fd = os.open(lock, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
320
+ except OSError:
321
+ return False # another process holds the claim right now (or no writable folder)
322
+ try:
323
+ os.close(fd)
324
+ return should_offer(now) and mark_offered()
325
+ finally:
326
+ try:
327
+ os.remove(lock)
328
+ except OSError:
329
+ pass
300
330
 
301
331
 
302
332
  def open_store_page(url=None, platform=None):
@@ -369,9 +399,8 @@ def ensure_extension(log=None, emit=None, wait_s=120, installs=None):
369
399
  installs = find_installs() if installs is None else installs
370
400
  state = diagnose(installs)
371
401
  opened = False
372
- if state == "missing" and should_offer():
373
- opened = open_store_page()
374
- mark_offered()
402
+ if state == "missing" and claim_offer():
403
+ opened = open_store_page() # claim_offer already recorded it: at most once a day, never per call
375
404
  steps = setup_steps(state, installs, opened)
376
405
  say = emit or (lambda _t: None)
377
406
  for line in steps:
package/install-path.mjs CHANGED
@@ -21,7 +21,7 @@
21
21
  * - NEVER fail the install. Every path through this file ends in exit 0; npm treats a
22
22
  * postinstall failure as a failed install and the user gets nothing.
23
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).
24
+ * alias problem is install.ps1's job (asks once) and `gu-cli fix-powershell`'s, both interactive).
25
25
  * - NEVER touch an rc file twice: the marker pair makes re-runs no-ops.
26
26
  * - Under `sudo npm i -g`, write to the REAL user's home (SUDO_USER) and chown the
27
27
  * file back, or the user's next login shell reads a root-owned rc file.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gu-cli",
3
- "version": "1.0.861",
3
+ "version": "1.0.863",
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",
@@ -9,9 +9,12 @@
9
9
  * an alias beats every executable and every function we could ship. The only remedies are to
10
10
  * remove the alias from the user's profile, or to be called something else.
11
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.
12
+ * So this offers the removal, EXPLICITLY and never silently. A profile is the user's file;
13
+ * a CLI that edits it unannounced is a CLI that surprises people. The Windows installer
14
+ * (web-v2/public/install.ps1) asks ONCE, default Yes, for every PowerShell on the machine, so a
15
+ * new user never meets the alias; `gu-cli fix-powershell` is the same fix for an existing install
16
+ * (it asks, shows the line, and appends it — `--check` just reports). Both write the ALL-HOSTS
17
+ * profile, which VS Code's terminal reads too.
15
18
  */
16
19
 
17
20
  /** The line appended to the profile. `-Force` because the built-in alias is read-only. */
@@ -29,6 +32,61 @@ export const PROFILE_LINE =
29
32
  */
30
33
  export const MEANWHILE = "Left alone. `gu-cli` and `gu.cmd` both run this program in the meantime.";
31
34
 
35
+ /**
36
+ * The PowerShell hosts that can be installed side by side on Windows 11.
37
+ * `powershell` = Windows PowerShell 5.1 (built in); `pwsh` = PowerShell 7 (the default in many
38
+ * VS Code terminals). Each has ITS OWN profile and execution policy — fixing one leaves the other
39
+ * shadowed (issue #218).
40
+ */
41
+ export const POWERSHELL_HOSTS = ["powershell", "pwsh"];
42
+
43
+ /** The one PowerShell command that reports where a host's profiles are and its policy (3 lines). */
44
+ export const HOST_PROBE =
45
+ "$PROFILE.CurrentUserAllHosts; $PROFILE.CurrentUserCurrentHost; Get-ExecutionPolicy";
46
+
47
+ /** Parse HOST_PROBE output: { allHosts, currentHost, policy } (empty strings when missing). */
48
+ export function parseHostProbe(out) {
49
+ const [allHosts = "", currentHost = "", policy = ""] = String(out ?? "")
50
+ .split(/\r?\n/)
51
+ .map((s) => s.trim())
52
+ .filter(Boolean);
53
+ return { allHosts, currentHost, policy };
54
+ }
55
+
56
+ /**
57
+ * Is ONE host fixed, and can its profile run? Fixed if either of its profile files (all-hosts or
58
+ * this-host, the latter where older versions wrote) already drops the alias.
59
+ */
60
+ export function hostStatus({ allHostsText, currentHostText, policy }) {
61
+ return {
62
+ fixed: alreadyFixed(allHostsText) || alreadyFixed(currentHostText),
63
+ blocked: profileBlockedBy(policy),
64
+ };
65
+ }
66
+
67
+ /** A human name for a host command. */
68
+ export function hostLabel(host) {
69
+ return host === "pwsh" ? "PowerShell 7 (pwsh)" : "Windows PowerShell";
70
+ }
71
+
72
+ /**
73
+ * The profiles to fix, one per distinct file. `found` is [{host, profilePath}] in host order;
74
+ * entries with an empty path are dropped, and two hosts that share a file are fixed once.
75
+ */
76
+ export function uniqueProfiles(found) {
77
+ const seen = new Set();
78
+ const out = [];
79
+ for (const f of found ?? []) {
80
+ const p = String(f?.profilePath ?? "").trim();
81
+ if (!p) continue;
82
+ const key = p.toLowerCase();
83
+ if (seen.has(key)) continue;
84
+ seen.add(key);
85
+ out.push({ host: f.host, profilePath: p });
86
+ }
87
+ return out;
88
+ }
89
+
32
90
  /** True when this machine can even have the problem. */
33
91
  export function affectsThisMachine(platform = process.platform) {
34
92
  return platform === "win32";
@@ -44,7 +102,7 @@ export function shadowedAdvice() {
44
102
  return [
45
103
  "PowerShell has a built-in alias `gu` (Get-Unique) that wins over this CLI.",
46
104
  "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.",
105
+ "Permanently: run `gu-cli fix-powershell` to drop the alias from your PowerShell profile(s) —\n Windows PowerShell and PowerShell 7 (pwsh, the VS Code default) each have their own.",
48
106
  "cmd.exe is unaffected; `gu` works there already.",
49
107
  ];
50
108
  }