@tiens.nguyen/gu-cli 1.0.862 → 1.0.864
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/README.md +7 -3
- package/cli-token.mjs +47 -6
- package/client-expert-run.mjs +22 -7
- package/env-check.mjs +228 -0
- package/gu-cli.mjs +130 -77
- package/gu-repl.mjs +153 -12
- package/install-path.mjs +11 -7
- package/mongo-native.mjs +15 -0
- package/package.json +4 -1
- package/powershell-alias.mjs +62 -4
- package/support-bundle.mjs +389 -0
- package/support-command.mjs +106 -0
- package/terminal-layout.mjs +41 -1
package/gu-cli.mjs
CHANGED
|
@@ -139,7 +139,8 @@ Usage:
|
|
|
139
139
|
gu-cli workspace revert [runId] # undo an agent run's file edits from its backups
|
|
140
140
|
gu-cli mongo migrate-from-docker # macOS: move a Docker MongoDB to native (Homebrew)
|
|
141
141
|
gu-cli clear [--scratch|--keep-history] [--yes] # back to a fresh machine (asks first)
|
|
142
|
-
gu-cli support #
|
|
142
|
+
gu-cli support # send the support team a request with gu's logs (masked) attached
|
|
143
|
+
gu-cli support --no-send # only make the logs zip (~/.gonext/support/) to email yourself
|
|
143
144
|
gu-cli fix-path [--check] # put npm's global folder on PATH (Windows: check, then ask)
|
|
144
145
|
# (what the install does, if npm skipped it)
|
|
145
146
|
gu-cli fix-powershell [--check] # Windows: drop PowerShell's built-in \`gu\` alias
|
|
@@ -1111,14 +1112,45 @@ if (args[0] === "fix-path") {
|
|
|
1111
1112
|
// #94: under `npx @tiens.nguyen/gu-cli fix-path` this file runs from npx's cache; fixPathPrefix
|
|
1112
1113
|
// then falls back to npm's global prefix when gu is installed there.
|
|
1113
1114
|
const prefix = fixPathPrefix({ here });
|
|
1115
|
+
// #221: besides the PATH line, look for what an old or second install left behind — broken
|
|
1116
|
+
// `gu` links, a second copy from another npm, a profile line pinned to a versioned Homebrew
|
|
1117
|
+
// folder. Each is explained and asked about; `--check` only reports.
|
|
1118
|
+
const scanEnvironment = async () => {
|
|
1119
|
+
const { checkEnvironment, applyFix } = await import("./env-check.mjs");
|
|
1120
|
+
const { homedir } = await import("node:os");
|
|
1121
|
+
const findings = checkEnvironment({ pathDirs: (process.env.PATH || "").split(":"), home: homedir(), here });
|
|
1122
|
+
if (!findings.length) return;
|
|
1123
|
+
console.log("\n Also found:");
|
|
1124
|
+
const { spawnSync } = await import("node:child_process");
|
|
1125
|
+
let rl = null;
|
|
1126
|
+
const check = args.includes("--check") || !process.stdin.isTTY;
|
|
1127
|
+
for (const f of findings) {
|
|
1128
|
+
console.log(` • ${f.text}`);
|
|
1129
|
+
if (!f.fix || check) {
|
|
1130
|
+
if (f.fix) console.log(` Fix: ${f.fix.label} (run \`gu-cli fix-path\` in a terminal to be asked)`);
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
rl ??= (await import("node:readline/promises")).createInterface({ input: process.stdin, output: process.stdout });
|
|
1134
|
+
// Removing another gu install is the one that costs something: default No.
|
|
1135
|
+
const dflt = f.fix.kind === "uninstall-copy" ? "N" : "Y";
|
|
1136
|
+
const a = (await rl.question(` ${f.fix.label}? [${dflt === "Y" ? "Y/n" : "y/N"}] `)).trim().toLowerCase();
|
|
1137
|
+
if (!(a === "y" || a === "yes" || (a === "" && dflt === "Y"))) continue;
|
|
1138
|
+
const r = applyFix(f.fix, { run: (cmd, a2) => spawnSync(cmd, a2, { encoding: "utf8" }) });
|
|
1139
|
+
console.log(r.ok ? " Done." : ` Could not: ${r.why}`);
|
|
1140
|
+
}
|
|
1141
|
+
rl?.close();
|
|
1142
|
+
};
|
|
1114
1143
|
if (!prefix) {
|
|
1115
1144
|
console.log(` gu is not installed globally with npm (${here}) — there is no PATH to fix.`);
|
|
1116
1145
|
console.log(" Install it with: npm install -g @tiens.nguyen/gu-cli");
|
|
1146
|
+
await scanEnvironment();
|
|
1117
1147
|
process.exit(0);
|
|
1118
1148
|
}
|
|
1149
|
+
const { stableBinDir } = await import("./env-check.mjs");
|
|
1119
1150
|
const binDir = join(prefix, "bin");
|
|
1120
|
-
if ((process.env.PATH || "").split(":").
|
|
1151
|
+
if ((process.env.PATH || "").split(":").some((d) => d === binDir || d === stableBinDir(binDir))) {
|
|
1121
1152
|
console.log(` Nothing to fix — ${binDir} is already on PATH.`);
|
|
1153
|
+
await scanEnvironment();
|
|
1122
1154
|
process.exit(0);
|
|
1123
1155
|
}
|
|
1124
1156
|
const { spawnSync } = await import("node:child_process");
|
|
@@ -1143,9 +1175,10 @@ if (args[0] === "fix-path") {
|
|
|
1143
1175
|
if (has) console.log(` Your shell profile already adds ${binDir} to PATH — open a new terminal.`);
|
|
1144
1176
|
else {
|
|
1145
1177
|
console.log(` ${binDir} is not on your PATH and was not added to a shell profile. Add this line to it:`);
|
|
1146
|
-
console.log(` export PATH="${binDir}:$PATH"`);
|
|
1178
|
+
console.log(` export PATH="${stableBinDir(binDir)}:$PATH"`);
|
|
1147
1179
|
}
|
|
1148
1180
|
}
|
|
1181
|
+
await scanEnvironment();
|
|
1149
1182
|
process.exit(r.status ?? 0);
|
|
1150
1183
|
}
|
|
1151
1184
|
|
|
@@ -1159,91 +1192,111 @@ if (args[0] === "fix-powershell") {
|
|
|
1159
1192
|
process.exit(0);
|
|
1160
1193
|
}
|
|
1161
1194
|
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 = () => {
|
|
1195
|
+
const runPs = (host, script) =>
|
|
1196
|
+
execFileSync(host, ["-NoProfile", "-Command", script], {
|
|
1197
|
+
encoding: "utf8",
|
|
1198
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1199
|
+
}).trim();
|
|
1200
|
+
|
|
1201
|
+
// EVERY installed PowerShell has its OWN profile: Windows PowerShell 5.1 (`powershell`) and
|
|
1202
|
+
// PowerShell 7 (`pwsh`, VS Code's usual terminal) read different files. Asking only
|
|
1203
|
+
// `powershell` wrote the fix where a pwsh terminal never looked.
|
|
1204
|
+
const found = [];
|
|
1205
|
+
for (const host of pa.POWERSHELL_HOSTS) {
|
|
1178
1206
|
try {
|
|
1179
|
-
|
|
1207
|
+
found.push({ host, profilePath: runPs(host, "$PROFILE.CurrentUserAllHosts") });
|
|
1180
1208
|
} catch {
|
|
1181
|
-
|
|
1182
|
-
}
|
|
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);
|
|
1209
|
+
/* that PowerShell is not installed */
|
|
1191
1210
|
}
|
|
1192
|
-
console.log(" Open a NEW PowerShell window for it to take effect.");
|
|
1193
|
-
process.exit(0);
|
|
1194
|
-
}
|
|
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
1211
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
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}`);
|
|
1212
|
+
const targets = pa.uniqueProfiles(found);
|
|
1213
|
+
if (targets.length === 0) {
|
|
1214
|
+
console.error(" Could not ask PowerShell where your profile is (neither `powershell` nor `pwsh` answered).");
|
|
1223
1215
|
process.exit(1);
|
|
1224
1216
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1217
|
+
const { readFileSync, writeFileSync, mkdirSync, existsSync } = await import("node:fs");
|
|
1218
|
+
const checkOnly = args.includes("--check");
|
|
1219
|
+
let rlp = null;
|
|
1220
|
+
let worst = 0;
|
|
1221
|
+
|
|
1222
|
+
for (const { host, profilePath } of targets) {
|
|
1231
1223
|
console.log("");
|
|
1232
|
-
|
|
1233
|
-
|
|
1224
|
+
console.log(` ${pa.hostLabel(host)} — ${profilePath}`);
|
|
1225
|
+
const current = existsSync(profilePath) ? readFileSync(profilePath, "utf8") : "";
|
|
1226
|
+
const next = pa.profileWithFix(current);
|
|
1227
|
+
// ASKED IN EVERY PATH, because "already fixed" was reported on the very machine whose policy
|
|
1228
|
+
// stops the profile loading — the same "said Done, changed nothing" mistake one branch along.
|
|
1229
|
+
const blockedBy = () => {
|
|
1230
|
+
try {
|
|
1231
|
+
return pa.profileBlockedBy(runPs(host, "Get-ExecutionPolicy"));
|
|
1232
|
+
} catch {
|
|
1233
|
+
return "";
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
1236
|
+
if (next === null) {
|
|
1237
|
+
console.log(" Already fixed — the profile removes the alias.");
|
|
1238
|
+
const blocked = blockedBy();
|
|
1239
|
+
if (blocked) {
|
|
1240
|
+
for (const l of pa.policyAdvice(blocked)) console.log(" " + l);
|
|
1241
|
+
worst = 1;
|
|
1242
|
+
}
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
if (checkOnly) {
|
|
1246
|
+
console.log(" Not fixed yet. `gu` in this PowerShell is still Get-Unique.");
|
|
1247
|
+
const blocked = blockedBy();
|
|
1248
|
+
if (blocked) for (const l of pa.policyAdvice(blocked)) console.log(" " + l);
|
|
1249
|
+
worst = 1;
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
for (const l of pa.shadowedAdvice()) console.log(" " + l);
|
|
1253
|
+
console.log(` This appends ONE line to ${profilePath}:`);
|
|
1254
|
+
console.log(" " + pa.PROFILE_LINE);
|
|
1255
|
+
rlp ??= (await import("node:readline")).createInterface({
|
|
1256
|
+
input: process.stdin,
|
|
1257
|
+
output: process.stdout,
|
|
1258
|
+
});
|
|
1259
|
+
const answer = await new Promise((r) => rlp.question("\n Append it? [y/N]: ", r));
|
|
1260
|
+
if (!/^y(es)?$/i.test(String(answer).trim())) {
|
|
1261
|
+
console.log(" " + pa.MEANWHILE);
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1264
|
+
try {
|
|
1265
|
+
const dir = profilePath.replace(/[\\/][^\\/]*$/, "");
|
|
1266
|
+
if (dir && !existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
1267
|
+
writeFileSync(profilePath, next, "utf8");
|
|
1268
|
+
} catch (e) {
|
|
1269
|
+
console.error(` Could not write ${profilePath}: ${e.message}`);
|
|
1270
|
+
worst = 1;
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
console.log(` Done — ${profilePath} updated.`);
|
|
1274
|
+
// VERIFY THE FIX CAN ACTUALLY RUN. Writing the line is not the same as PowerShell loading it:
|
|
1275
|
+
// under Restricted or AllSigned the profile is a script it refuses to execute, and the first
|
|
1276
|
+
// real run of this said "Done" to a machine where `gu` stayed shadowed.
|
|
1277
|
+
const policy = blockedBy();
|
|
1278
|
+
if (policy) {
|
|
1279
|
+
for (const l of pa.policyAdvice(policy)) console.log(" " + l);
|
|
1280
|
+
worst = 1;
|
|
1281
|
+
}
|
|
1234
1282
|
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1283
|
+
rlp?.close();
|
|
1284
|
+
if (worst === 0 && !checkOnly) console.log("\n Open a NEW PowerShell window (in VS Code too), then `gu` runs this CLI.");
|
|
1285
|
+
process.exit(worst);
|
|
1237
1286
|
}
|
|
1238
1287
|
|
|
1239
1288
|
if (args[0] === "support") {
|
|
1240
|
-
//
|
|
1241
|
-
|
|
1242
|
-
const {
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1289
|
+
// #220: the masked logs + health report as one zip, shown, then sent to the support team with a
|
|
1290
|
+
// reference back (support-command.mjs). `--report` is the old text-only report.
|
|
1291
|
+
const { runSupportCommand } = await import("./support-command.mjs");
|
|
1292
|
+
let rl = null;
|
|
1293
|
+
const ask = async (q) => {
|
|
1294
|
+
rl ??= (await import("node:readline/promises")).createInterface({ input: process.stdin, output: process.stdout });
|
|
1295
|
+
return rl.question(q).catch(() => "");
|
|
1296
|
+
};
|
|
1297
|
+
const code = await runSupportCommand({ args: args.slice(1), version: WORKER_VERSION, ask });
|
|
1298
|
+
rl?.close();
|
|
1299
|
+
process.exit(code);
|
|
1247
1300
|
}
|
|
1248
1301
|
|
|
1249
1302
|
if (args[0] === "clear" || args[0] === "reset") {
|
package/gu-repl.mjs
CHANGED
|
@@ -105,6 +105,8 @@ import {
|
|
|
105
105
|
physicalRows,
|
|
106
106
|
inputPhysicalRows,
|
|
107
107
|
buildFooterRows,
|
|
108
|
+
buildFooter,
|
|
109
|
+
SUPPORT_LINK_TEXT,
|
|
108
110
|
pasteHint,
|
|
109
111
|
hintRows as questionHintRows,
|
|
110
112
|
footerDrawSeq,
|
|
@@ -707,6 +709,33 @@ let openPresetsAtStart = false;
|
|
|
707
709
|
// #132 follow-up: a hosted provider picked in first-run setup; its key and model are asked once the terminal exists.
|
|
708
710
|
let addProviderAtStart = null;
|
|
709
711
|
|
|
712
|
+
// #220: a gu that STOPS ON AN ERROR says where help is — a start that failed (exit 1 before the
|
|
713
|
+
// prompt: no key, API unreachable, MongoDB down…) or a crash. One dim line, never during a session
|
|
714
|
+
// that started fine, where /support is in the command list.
|
|
715
|
+
let supportHintAtExit = true;
|
|
716
|
+
const SUPPORT_HINT = "Need help? Run `gu support` — it sends the support team gu's logs (secrets masked).";
|
|
717
|
+
process.on("exit", (code) => {
|
|
718
|
+
if (code !== 1 || !supportHintAtExit) return;
|
|
719
|
+
try {
|
|
720
|
+
process.stderr.write("\n" + dim(SUPPORT_HINT) + "\n");
|
|
721
|
+
} catch {
|
|
722
|
+
/* stderr gone: nothing to tell */
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
function crashed(e) {
|
|
726
|
+
try {
|
|
727
|
+
releaseScreenAtExit();
|
|
728
|
+
} catch {
|
|
729
|
+
/* the screen may not be ours yet */
|
|
730
|
+
}
|
|
731
|
+
console.error(e?.stack || String(e));
|
|
732
|
+
supportHintAtExit = true;
|
|
733
|
+
process.exit(1);
|
|
734
|
+
}
|
|
735
|
+
process.on("uncaughtException", crashed);
|
|
736
|
+
process.on("unhandledRejection", crashed);
|
|
737
|
+
|
|
738
|
+
|
|
710
739
|
if (!workerKey || !apiBase) {
|
|
711
740
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
712
741
|
try {
|
|
@@ -977,6 +1006,7 @@ const COMMANDS = [
|
|
|
977
1006
|
{ name: "/shells", desc: "long commands the agent left running in the background — status, runtime, command, output; stop one" },
|
|
978
1007
|
{ name: "/budget", desc: "short view of your API provider budgets and balances (Pro) — the same items as the web /providers page" },
|
|
979
1008
|
{ name: "/tokens", desc: "break down the last prompt sent to the coding model — how much of it was RAG" },
|
|
1009
|
+
{ name: "/support", desc: "send the gu support team a request with gu's logs attached (secrets masked) — shows what goes first, then asks" },
|
|
980
1010
|
{ name: "/clear-logs", desc: "show what gu's logs, RAG indexes and old API versions take on disk, then ask what to clear — the logs first" },
|
|
981
1011
|
{ name: "/prices", usage: "/prices [update]", desc: "the price table and the cache table, and what the last turn and this session cost — \"update\" applies gu's newest published tables" },
|
|
982
1012
|
// ONE name, /new (user, #278). /reset and /clear still work (HIDDEN_COMMANDS) for habit and
|
|
@@ -1573,6 +1603,12 @@ function applyClick({ row, col }) {
|
|
|
1573
1603
|
void runClearLogs();
|
|
1574
1604
|
return;
|
|
1575
1605
|
}
|
|
1606
|
+
// The footer's "? support" link (#220): the footer is the last rows of the screen, so count from the bottom.
|
|
1607
|
+
if (supportLinkTarget && !zonePanel && row === viewportRows() - supportLinkTarget.fromEnd + 1 &&
|
|
1608
|
+
col - 1 >= supportLinkTarget.from && col - 1 < supportLinkTarget.to) {
|
|
1609
|
+
void runSupport();
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1576
1612
|
if (agentsRowTarget && row === updateRowAbs &&
|
|
1577
1613
|
col - 1 >= agentsRowTarget.from && col - 1 < agentsRowTarget.to) {
|
|
1578
1614
|
void showOtherAgents(); // async now — it offers stop / switch-to (task #211)
|
|
@@ -2488,6 +2524,8 @@ async function refreshAvailableUpdates() {
|
|
|
2488
2524
|
* paid to see the warning go.
|
|
2489
2525
|
*/
|
|
2490
2526
|
let tokenNotice = null;
|
|
2527
|
+
/** #219: true while the token question is on screen — the bar must not repeat its warning. */
|
|
2528
|
+
let tokenQuestionOpen = false;
|
|
2491
2529
|
let tokenBusy = false;
|
|
2492
2530
|
let startupRenewDone = false;
|
|
2493
2531
|
/**
|
|
@@ -2866,8 +2904,12 @@ const compactLockNote = () =>
|
|
|
2866
2904
|
at: lastPromptBreakdown?.compactedAt,
|
|
2867
2905
|
})
|
|
2868
2906
|
: "";
|
|
2869
|
-
|
|
2870
|
-
|
|
2907
|
+
// #220: where the footer's "? support" link is on screen (set by frozenZone, read by applyClick).
|
|
2908
|
+
let supportLinkTarget = null;
|
|
2909
|
+
const footerBuilt = () =>
|
|
2910
|
+
buildFooter({
|
|
2911
|
+
link: SUPPORT_LINK_TEXT,
|
|
2912
|
+
linkPaint: (t) => dim("\x1b[4m" + t + "\x1b[24m"),
|
|
2871
2913
|
width: process.stdout.columns || 80,
|
|
2872
2914
|
gutter: GUTTER,
|
|
2873
2915
|
rag: REPL_VERBOSE && (lastRagFile || lastRagState) ? ragLineText() : "",
|
|
@@ -2893,6 +2935,7 @@ const footerRows = () =>
|
|
|
2893
2935
|
// where the red rows above mean something is not set up at all.
|
|
2894
2936
|
missing: [...missingNoticeRows().map((r) => red(r)), ...fallbackNoticeRows().map((r) => yellow(r))],
|
|
2895
2937
|
});
|
|
2938
|
+
const footerRows = () => footerBuilt().rows;
|
|
2896
2939
|
// An ask()'s hint lines (#305), drawn in yellow under its input while it waits. Plain text.
|
|
2897
2940
|
let askHint = [];
|
|
2898
2941
|
const askHintRows = (width) => questionHintRows(askHint, { width, gutter: GUTTER, paint: yellow });
|
|
@@ -3006,9 +3049,11 @@ function zoneNotice(width) {
|
|
|
3006
3049
|
const up = updateRow({ ...availableUpdates, width: 400 });
|
|
3007
3050
|
if (up) items.push({ key: "update", clickable: true, paint: accent, text: up.text.slice(up.from) });
|
|
3008
3051
|
}
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
if (
|
|
3052
|
+
// While the token question is open it is the ONLY thing on the screen (#222): the housekeeping
|
|
3053
|
+
// notices (new prices, new token limits) wait, and the token notice would repeat the question.
|
|
3054
|
+
if (priceUpdate && !tokenQuestionOpen) items.push({ key: "prices", clickable: true, paint: accent, text: `${priceUpdateText()} (click)` });
|
|
3055
|
+
if (limitsUpdate && !tokenQuestionOpen) items.push({ key: "limits", clickable: true, paint: accent, text: `${limitsUpdate.label} (click)` });
|
|
3056
|
+
if (tokenNotice?.text && !tokenQuestionOpen) items.push({ key: "token", paint: tokenNotice.urgent ? yellow : dim, text: tokenNotice.text });
|
|
3012
3057
|
const agents = agentsIndicator(otherAgents);
|
|
3013
3058
|
if (agents) items.push({ key: "agents", clickable: true, paint: accent, text: `${agents} running (click)` });
|
|
3014
3059
|
const shells = shellsNotice(shellEntries, { cwd: process.cwd() });
|
|
@@ -3123,7 +3168,10 @@ function frozenZone() {
|
|
|
3123
3168
|
selection: secretInput ? null : inputSelection,
|
|
3124
3169
|
highlight: highlightRow,
|
|
3125
3170
|
});
|
|
3126
|
-
const
|
|
3171
|
+
const footer = footerBuilt();
|
|
3172
|
+
const rows = status.concat(notice ? [notice.row] : [], zone.rows, askHintRows(width), footer.rows);
|
|
3173
|
+
// The footer is the LAST rows of the zone, so the link's row counts from the end.
|
|
3174
|
+
supportLinkTarget = footer.link ? { fromEnd: footer.rows.length - footer.link.rowIndex, from: footer.link.from, to: footer.link.to } : null;
|
|
3127
3175
|
// The separator is `status.length (+1 for the notice row)` rows down — everything that writes to
|
|
3128
3176
|
// or hit-tests a row by index counts from here.
|
|
3129
3177
|
const sep = status.length + (notice ? 1 : 0);
|
|
@@ -8585,6 +8633,83 @@ async function refreshLogsSize() {
|
|
|
8585
8633
|
}
|
|
8586
8634
|
}
|
|
8587
8635
|
|
|
8636
|
+
// /support (#220): gu's logs, masked and zipped, sent to the support team with a reference back.
|
|
8637
|
+
// What is collected and how it is sent is support-bundle.mjs; this is the terminal half only.
|
|
8638
|
+
let supportBusy = false;
|
|
8639
|
+
async function runSupport() {
|
|
8640
|
+
if (following) {
|
|
8641
|
+
console.log("\n" + GUTTER + dim("A turn is running - /support is available when it finishes (Ctrl-C stops it)."));
|
|
8642
|
+
return;
|
|
8643
|
+
}
|
|
8644
|
+
if (supportBusy || listPickerActive || approvalActive || askWaiting) return;
|
|
8645
|
+
supportBusy = true;
|
|
8646
|
+
try {
|
|
8647
|
+
const sb = await import("./support-bundle.mjs");
|
|
8648
|
+
const width = Math.max(20, process.stdout.columns || 80);
|
|
8649
|
+
const tilde = (p) => (p.startsWith(homedir()) ? "~" + p.slice(homedir().length) : p);
|
|
8650
|
+
const note = (l) => console.log(GUTTER + l);
|
|
8651
|
+
console.log("\n" + GUTTER + dim("Collecting gu's logs (secrets are masked)…"));
|
|
8652
|
+
const bundle = await sb.prepareSupportBundle({ version: REPL_VERSION });
|
|
8653
|
+
const where = tilde(bundle.file);
|
|
8654
|
+
const fallback = (opts = {}) => sb.supportFallbackLines({ file: where, ...opts });
|
|
8655
|
+
console.log("");
|
|
8656
|
+
note(bold("What would be sent") + dim(` · ${sb.bundleSize(bundle)} zip`));
|
|
8657
|
+
for (const row of sb.bundleSummaryLines(bundle)) note(" " + dim(row));
|
|
8658
|
+
note(dim("saved at ") + dim(where));
|
|
8659
|
+
console.log("");
|
|
8660
|
+
// The answers are typed in the input row, which empties after Enter: echo them so the
|
|
8661
|
+
// scrollback still shows what was sent.
|
|
8662
|
+
const typedMessage = (await ask("What went wrong? (Enter to skip): ")).trim();
|
|
8663
|
+
const message = sb.supportMessage(typedMessage);
|
|
8664
|
+
note(dim("what went wrong: ") + (typedMessage || dim("(not said)")));
|
|
8665
|
+
let email = await sb.defaultReplyEmail();
|
|
8666
|
+
const typed = (await ask(`Email for the answer${email ? ` [${email}]` : ""}: `)).trim();
|
|
8667
|
+
if (typed) email = typed;
|
|
8668
|
+
if (!sb.isEmail(email)) {
|
|
8669
|
+
note(yellow("No reply email — the support team could not answer you. Nothing sent."));
|
|
8670
|
+
for (const l of fallback()) note(dim(l));
|
|
8671
|
+
console.log("");
|
|
8672
|
+
return;
|
|
8673
|
+
}
|
|
8674
|
+
note(dim("answer to: ") + email);
|
|
8675
|
+
// A question never wraps: at 80 columns over SSH the picker hint goes on a row of its own.
|
|
8676
|
+
const q = `Send it to the gu support team (${sb.SUPPORT_EMAIL})?`;
|
|
8677
|
+
const hint = `(${PICK_HINT}):`;
|
|
8678
|
+
console.log("");
|
|
8679
|
+
if (3 + q.length + 2 + hint.length < width) console.log(dim(` ${q} ${hint}`));
|
|
8680
|
+
else console.log(dim(` ${q}\n ${hint}`));
|
|
8681
|
+
const go = await pickFromList(["Yes — send the request and the logs", "No — keep the zip, I will email it myself"], 0);
|
|
8682
|
+
if (go !== 0) {
|
|
8683
|
+
for (const l of fallback()) note(dim(l));
|
|
8684
|
+
console.log("");
|
|
8685
|
+
return;
|
|
8686
|
+
}
|
|
8687
|
+
note(dim("Sending…"));
|
|
8688
|
+
const r = await sb.sendSupportRequest({ base: sb.supportApiBase(), message, email, version: REPL_VERSION, zip: bundle.zip });
|
|
8689
|
+
if (!r.ok) {
|
|
8690
|
+
const [first, ...rest] = fallback({ why: r.why });
|
|
8691
|
+
note(yellow(first));
|
|
8692
|
+
for (const l of rest) note(l);
|
|
8693
|
+
console.log("");
|
|
8694
|
+
return;
|
|
8695
|
+
}
|
|
8696
|
+
note(accent(`✓ Sent — your reference is ${bold(r.ref)}`));
|
|
8697
|
+
note(dim(`the answer goes to ${email}`));
|
|
8698
|
+
if (r.uploaded) note(dim(`logs attached · to add more, email ${sb.SUPPORT_EMAIL} with ${r.ref}`));
|
|
8699
|
+
else {
|
|
8700
|
+
note(yellow(`The logs did not go with it (${r.uploadWhy}).`));
|
|
8701
|
+
for (const l of fallback({ ref: r.ref })) note(l);
|
|
8702
|
+
}
|
|
8703
|
+
console.log("");
|
|
8704
|
+
} catch (e) {
|
|
8705
|
+
if (wasPromptCancelled(e)) console.log(dim(" (cancelled — nothing sent)\n"));
|
|
8706
|
+
else console.log(GUTTER + red(`couldn't finish — ${e?.message ?? e}`) + "\n");
|
|
8707
|
+
} finally {
|
|
8708
|
+
supportBusy = false;
|
|
8709
|
+
if (barPinned()) paintBar();
|
|
8710
|
+
}
|
|
8711
|
+
}
|
|
8712
|
+
|
|
8588
8713
|
async function runClearLogs() {
|
|
8589
8714
|
// ONE AT A TIME, and never over a turn or another picker — reached from a fire-and-forget click
|
|
8590
8715
|
// (see showOtherAgents for what a second picker over the first one does).
|
|
@@ -11744,12 +11869,12 @@ async function localScaffoldState() {
|
|
|
11744
11869
|
/** Print the startup scaffold line — deferred while a turn owns the transcript. */
|
|
11745
11870
|
function announceScaffold() {
|
|
11746
11871
|
if (!scaffoldNotice || following) return;
|
|
11747
|
-
const { kind, text
|
|
11872
|
+
const { kind, text } = scaffoldNotice;
|
|
11748
11873
|
scaffoldNotice = null;
|
|
11749
11874
|
if (kind === "link") {
|
|
11750
11875
|
// A setup step, said calmly (#292): the headline, then the steps on their own lines.
|
|
11876
|
+
// ONE yellow line (#222): the steps are the token question's own, behind "?" there.
|
|
11751
11877
|
console.log(GUTTER + yellow("! " + text));
|
|
11752
|
-
for (const s of steps ?? []) console.log(GUTTER + " " + dim(s));
|
|
11753
11878
|
} else {
|
|
11754
11879
|
console.log(GUTTER + (kind === "ok" ? accent("✓ " + text) : kind === "warn" ? yellow("⚠ " + text) : red("✗ " + text)));
|
|
11755
11880
|
}
|
|
@@ -11780,13 +11905,15 @@ async function ensureTokenForTurn() {
|
|
|
11780
11905
|
* (Esc does nothing), one Backspace clears the default for a paste, and a valid token turns the
|
|
11781
11906
|
* row back into ">> ". → true once a token is saved.
|
|
11782
11907
|
*/
|
|
11783
|
-
async function askForCliToken(ct, dir, { why = "", overLimit = false } = {}) {
|
|
11908
|
+
async function askForCliToken(ct, dir, { why = "", overLimit = false, short = "" } = {}) {
|
|
11784
11909
|
console.log("");
|
|
11785
11910
|
tokenPromptActive = true; // Ctrl+C now quits gu (see onInterrupt) — task #17
|
|
11911
|
+
tokenQuestionOpen = true;
|
|
11786
11912
|
try {
|
|
11787
|
-
return await askForCliTokenInner(ct, dir, why, overLimit);
|
|
11913
|
+
return await askForCliTokenInner(ct, dir, why, overLimit, short);
|
|
11788
11914
|
} finally {
|
|
11789
11915
|
tokenPromptActive = false;
|
|
11916
|
+
tokenQuestionOpen = false;
|
|
11790
11917
|
}
|
|
11791
11918
|
}
|
|
11792
11919
|
|
|
@@ -11804,7 +11931,7 @@ function quitFromTokenPrompt() {
|
|
|
11804
11931
|
process.exit(0);
|
|
11805
11932
|
}
|
|
11806
11933
|
|
|
11807
|
-
async function askForCliTokenInner(ct, dir, why, overLimit = false) {
|
|
11934
|
+
async function askForCliTokenInner(ct, dir, why, overLimit = false, short = "") {
|
|
11808
11935
|
const hostedBase = await tokenHostedBase();
|
|
11809
11936
|
const pk = ct.publicKey();
|
|
11810
11937
|
const site = ct.siteUrl();
|
|
@@ -11816,6 +11943,7 @@ async function askForCliTokenInner(ct, dir, why, overLimit = false) {
|
|
|
11816
11943
|
hostedBase,
|
|
11817
11944
|
why,
|
|
11818
11945
|
overLimit, // #209: the computer limit, not a missing plan — no "paste a Pro token"
|
|
11946
|
+
short, // #219: the one short yellow line; the rest is behind "?"
|
|
11819
11947
|
// #315: a token is only good if THIS computer may use it — renew it from here (the server then
|
|
11820
11948
|
// says how much free time the computer has), save what came back, and run the licence rule.
|
|
11821
11949
|
accept: async (token) => {
|
|
@@ -11893,7 +12021,7 @@ async function askTokenAtStart() {
|
|
|
11893
12021
|
return;
|
|
11894
12022
|
}
|
|
11895
12023
|
}
|
|
11896
|
-
await askForCliToken(ct, dir, { why: d.why, overLimit: d.reason === "over-limit" });
|
|
12024
|
+
await askForCliToken(ct, dir, { why: d.why, overLimit: d.reason === "over-limit", short: d.short ?? "" });
|
|
11897
12025
|
}
|
|
11898
12026
|
/** The token refresh started at launch — askTokenAtStart waits for it (see there). */
|
|
11899
12027
|
let startupTokenRefresh = null;
|
|
@@ -14220,6 +14348,15 @@ async function main() {
|
|
|
14220
14348
|
process.exit(1);
|
|
14221
14349
|
}
|
|
14222
14350
|
console.log(wrapGutter("Ask about this repo. Type / to see commands (Tab completes). Ctrl-C aborts a running turn.", dim) + "\n");
|
|
14351
|
+
// #221: gu runs, but its own npm folder is not on PATH (npm skipped the postinstall that adds it).
|
|
14352
|
+
try {
|
|
14353
|
+
const { pathNoticeLine } = await import("./env-check.mjs");
|
|
14354
|
+
const note = pathNoticeLine({ prefix: globalPrefixOf(DIR), pathEnv: process.env.PATH || "", ci: Boolean(process.env.CI) });
|
|
14355
|
+
if (note) console.log(wrapGutter(note, yellow) + "\n");
|
|
14356
|
+
} catch {
|
|
14357
|
+
/* a hint, never a reason to fail to start */
|
|
14358
|
+
}
|
|
14359
|
+
supportHintAtExit = false; // started: from here /support is in the command list
|
|
14223
14360
|
// Ask whether anything is newer — in the BACKGROUND, so a slow registry never delays the
|
|
14224
14361
|
// prompt (task #181). The answer paints itself onto the separator row when it arrives, and
|
|
14225
14362
|
// the interval keeps a long-lived session honest. unref'd: a pending timer must not be the
|
|
@@ -14788,6 +14925,10 @@ async function main() {
|
|
|
14788
14925
|
await runClearLogs();
|
|
14789
14926
|
continue;
|
|
14790
14927
|
}
|
|
14928
|
+
if (line === "/support") {
|
|
14929
|
+
await runSupport();
|
|
14930
|
+
continue;
|
|
14931
|
+
}
|
|
14791
14932
|
if (line === "/budget") {
|
|
14792
14933
|
// #205: the short terminal version of web-v2's /providers budgets. The server judges Pro,
|
|
14793
14934
|
// the account email and "has items" (api/src/providers/terminalBudget.ts).
|
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.
|
|
@@ -30,6 +30,7 @@ import { appendFileSync, existsSync, readFileSync, chownSync, writeFileSync } fr
|
|
|
30
30
|
import { homedir, platform, tmpdir } from "node:os";
|
|
31
31
|
import { join, resolve } from "node:path";
|
|
32
32
|
import { fileURLToPath } from "node:url";
|
|
33
|
+
import { stableBinDir } from "./env-check.mjs";
|
|
33
34
|
|
|
34
35
|
const BEGIN = "# >>> gu-cli PATH >>>";
|
|
35
36
|
const END = "# <<< gu-cli PATH <<<";
|
|
@@ -121,12 +122,15 @@ function main() {
|
|
|
121
122
|
const binDir = join(prefix, "bin");
|
|
122
123
|
if (resolve(binDir).startsWith(resolve(tmpdir()))) return; // preflight's throwaway prefix
|
|
123
124
|
|
|
124
|
-
|
|
125
|
+
// On PATH under EITHER spelling: Homebrew's npm can report its prefix as the versioned Cellar
|
|
126
|
+
// folder while PATH holds the stable <brew prefix>/bin — the same folder (#221).
|
|
127
|
+
const stable = stableBinDir(binDir);
|
|
128
|
+
const onPath = (process.env.PATH || "").split(":").some((p) => p === binDir || p === stable);
|
|
125
129
|
if (onPath) return; // nothing to fix — most installs
|
|
126
130
|
|
|
127
131
|
const home = realUserHome();
|
|
128
132
|
const targets = rcFilesFor(home);
|
|
129
|
-
const block = `${BEGIN}\n# npm's global bin dir was not on PATH; added by @tiens.nguyen/gu-cli postinstall.\nexport PATH="${
|
|
133
|
+
const block = `${BEGIN}\n# npm's global bin dir was not on PATH; added by @tiens.nguyen/gu-cli postinstall.\nexport PATH="${stable}:$PATH"\n${END}\n`;
|
|
130
134
|
|
|
131
135
|
let wrote = null;
|
|
132
136
|
for (const rc of targets) {
|
|
@@ -134,12 +138,12 @@ function main() {
|
|
|
134
138
|
}
|
|
135
139
|
|
|
136
140
|
if (wrote) {
|
|
137
|
-
console.log(`gu-cli: added ${
|
|
141
|
+
console.log(`gu-cli: added ${stable} to PATH in ${wrote}.`);
|
|
138
142
|
console.log(`gu-cli: open a NEW terminal to use \`gu\`, or run this once in the current one:`);
|
|
139
|
-
console.log(` export PATH="${
|
|
143
|
+
console.log(` export PATH="${stable}:$PATH"`);
|
|
140
144
|
} else if (targets.length === 0) {
|
|
141
|
-
console.log(`gu-cli: ${
|
|
142
|
-
console.log(` export PATH="${
|
|
145
|
+
console.log(`gu-cli: ${stable} is not on your PATH and no shell profile was found to update.`);
|
|
146
|
+
console.log(` export PATH="${stable}:$PATH"`);
|
|
143
147
|
}
|
|
144
148
|
}
|
|
145
149
|
|
package/mongo-native.mjs
CHANGED
|
@@ -246,6 +246,21 @@ export function captureOutput(cmd, args, timeoutMs = 10000) {
|
|
|
246
246
|
});
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Which of Homebrew's output lines are worth showing (#221). A MongoDB install prints ~100 lines
|
|
251
|
+
* (every library it pours, a mug per bottle) over several minutes; shown whole they bury the one
|
|
252
|
+
* that matters. Kept: the `==>` stage lines ("Installing mongodb-community", "Downloading…"), what
|
|
253
|
+
* gu itself prints (`$ brew …`), and anything that looks like a failure or warning.
|
|
254
|
+
*/
|
|
255
|
+
export function brewLineWorthShowing(line) {
|
|
256
|
+
const l = String(line ?? "").trim();
|
|
257
|
+
if (!l) return false;
|
|
258
|
+
if (l.startsWith("$ ")) return true;
|
|
259
|
+
if (/\b(error|fail(ed|ure)?|warning|cannot|denied|not found|conflict)\b/i.test(l)) return true;
|
|
260
|
+
if (l.startsWith("==>")) return !/^==> (Pouring|Fetching downloads? for)\b/.test(l);
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
249
264
|
/**
|
|
250
265
|
* Homebrew steps, one after another, output shown. → true when all succeeded. HOMEBREW_NO_AUTO_UPDATE
|
|
251
266
|
* keeps `brew install` from first updating every formula on the machine — minutes nobody asked for.
|
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.864",
|
|
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",
|
|
@@ -89,6 +89,9 @@
|
|
|
89
89
|
"node-version.mjs",
|
|
90
90
|
"clear-state.mjs",
|
|
91
91
|
"support-report.mjs",
|
|
92
|
+
"env-check.mjs",
|
|
93
|
+
"support-bundle.mjs",
|
|
94
|
+
"support-command.mjs",
|
|
92
95
|
"clear-logs.mjs",
|
|
93
96
|
"platform-tools.mjs",
|
|
94
97
|
"job-pools.mjs",
|