@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 +21 -6
- package/gu-cli.mjs +83 -67
- package/gu-repl.mjs +23 -9
- package/gu_browser.py +33 -4
- package/install-path.mjs +1 -1
- package/package.json +1 -1
- package/powershell-alias.mjs +62 -4
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
|
|
1163
|
-
execFileSync(
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
const
|
|
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
|
-
|
|
1174
|
+
found.push({ host, profilePath: runPs(host, "$PROFILE.CurrentUserAllHosts") });
|
|
1180
1175
|
} catch {
|
|
1181
|
-
|
|
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
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
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
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
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
|
-
|
|
1233
|
-
|
|
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
|
-
|
|
1236
|
-
|
|
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(
|
|
4269
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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",
|
package/powershell-alias.mjs
CHANGED
|
@@ -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
|
|
13
|
-
* a CLI that edits it
|
|
14
|
-
* asks,
|
|
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
|
}
|