@tiens.nguyen/gu-cli 1.0.862 → 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
@@ -2488,6 +2488,8 @@ async function refreshAvailableUpdates() {
2488
2488
  * paid to see the warning go.
2489
2489
  */
2490
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;
2491
2493
  let tokenBusy = false;
2492
2494
  let startupRenewDone = false;
2493
2495
  /**
@@ -3008,7 +3010,7 @@ function zoneNotice(width) {
3008
3010
  }
3009
3011
  if (priceUpdate) items.push({ key: "prices", clickable: true, paint: accent, text: `${priceUpdateText()} (click)` });
3010
3012
  if (limitsUpdate) items.push({ key: "limits", clickable: true, paint: accent, text: `${limitsUpdate.label} (click)` });
3011
- 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 });
3012
3014
  const agents = agentsIndicator(otherAgents);
3013
3015
  if (agents) items.push({ key: "agents", clickable: true, paint: accent, text: `${agents} running (click)` });
3014
3016
  const shells = shellsNotice(shellEntries, { cwd: process.cwd() });
@@ -11780,13 +11782,15 @@ async function ensureTokenForTurn() {
11780
11782
  * (Esc does nothing), one Backspace clears the default for a paste, and a valid token turns the
11781
11783
  * row back into ">> ". → true once a token is saved.
11782
11784
  */
11783
- async function askForCliToken(ct, dir, { why = "", overLimit = false } = {}) {
11785
+ async function askForCliToken(ct, dir, { why = "", overLimit = false, short = "" } = {}) {
11784
11786
  console.log("");
11785
11787
  tokenPromptActive = true; // Ctrl+C now quits gu (see onInterrupt) — task #17
11788
+ tokenQuestionOpen = true;
11786
11789
  try {
11787
- return await askForCliTokenInner(ct, dir, why, overLimit);
11790
+ return await askForCliTokenInner(ct, dir, why, overLimit, short);
11788
11791
  } finally {
11789
11792
  tokenPromptActive = false;
11793
+ tokenQuestionOpen = false;
11790
11794
  }
11791
11795
  }
11792
11796
 
@@ -11804,7 +11808,7 @@ function quitFromTokenPrompt() {
11804
11808
  process.exit(0);
11805
11809
  }
11806
11810
 
11807
- async function askForCliTokenInner(ct, dir, why, overLimit = false) {
11811
+ async function askForCliTokenInner(ct, dir, why, overLimit = false, short = "") {
11808
11812
  const hostedBase = await tokenHostedBase();
11809
11813
  const pk = ct.publicKey();
11810
11814
  const site = ct.siteUrl();
@@ -11816,6 +11820,7 @@ async function askForCliTokenInner(ct, dir, why, overLimit = false) {
11816
11820
  hostedBase,
11817
11821
  why,
11818
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 "?"
11819
11824
  // #315: a token is only good if THIS computer may use it — renew it from here (the server then
11820
11825
  // says how much free time the computer has), save what came back, and run the licence rule.
11821
11826
  accept: async (token) => {
@@ -11893,7 +11898,7 @@ async function askTokenAtStart() {
11893
11898
  return;
11894
11899
  }
11895
11900
  }
11896
- 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 ?? "" });
11897
11902
  }
11898
11903
  /** The token refresh started at launch — askTokenAtStart waits for it (see there). */
11899
11904
  let startupTokenRefresh = null;
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.862",
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
  }