@tiens.nguyen/gu-cli 1.0.863 → 1.0.865
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 +28 -2
- package/client-expert-run.mjs +22 -7
- package/env-check.mjs +228 -0
- package/gu-cli.mjs +47 -10
- package/gu-repl.mjs +144 -8
- package/install-path.mjs +10 -6
- package/mongo-native.mjs +15 -0
- package/package.json +4 -1
- package/support-bundle.mjs +389 -0
- package/support-command.mjs +106 -0
- package/terminal-layout.mjs +41 -1
package/README.md
CHANGED
|
@@ -9,9 +9,13 @@ gu # open the REPL in the current folder
|
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
The package installs three commands: `gu` (the terminal REPL), `gu-cli` and `gu-local-worker`
|
|
12
|
-
(the same worker CLI). Requires Node.js 18 or newer. The install
|
|
13
|
-
npm
|
|
14
|
-
|
|
12
|
+
(the same worker CLI). Requires Node.js 18 or newer. The install tries to put npm's global bin
|
|
13
|
+
folder on your PATH, but npm 11 skips install scripts it was not told to allow (it prints
|
|
14
|
+
"install scripts not yet covered by allowScripts"). If `gu` is "command not found" afterwards, run
|
|
15
|
+
`npx @tiens.nguyen/gu-cli fix-path` (works even when `gu-cli` itself is not found). `gu-cli fix-path`
|
|
16
|
+
also finds what an old install left behind - broken `gu` links, a second gu from another npm, a
|
|
17
|
+
shell-profile PATH line pinned to a versioned Homebrew folder - and asks before fixing each
|
|
18
|
+
(`--check` only reports). `gu support` sends the support team a request with gu's logs attached.
|
|
15
19
|
On Windows, PowerShell's built-in `gu` alias (Get-Unique) shadows this CLI — run
|
|
16
20
|
`gu-cli fix-powershell` to drop it.
|
|
17
21
|
|
package/cli-token.mjs
CHANGED
|
@@ -238,6 +238,19 @@ export function shortToken(token) {
|
|
|
238
238
|
return t.length <= 24 ? t : `${t.slice(0, 16)}…${t.slice(-6)}`;
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
+
/**
|
|
242
|
+
* The ONE short yellow line for "there is no token to offer" (#222). The caller's `why` (a licence
|
|
243
|
+
* rule's full sentence) is cut to its first sentence; with none, what the trial lookup said.
|
|
244
|
+
*/
|
|
245
|
+
export function noTokenShort({ why = "", trial = {}, dev = null } = {}) {
|
|
246
|
+
const first = (t) => String(t).trim().split(/(?<=[.!?])\s+/)[0].trim();
|
|
247
|
+
if (why) return first(why);
|
|
248
|
+
if (trial.why === "used" && trial.endedAt) return `Free trial ended ${shortDate(trial.endedAt)} — a gu token is needed.`;
|
|
249
|
+
if (trial.why === "unreachable") return "Can't reach the gu service right now.";
|
|
250
|
+
if (/^http-\d+$/.test(String(trial.why ?? "")) || trial.why === "bad-response") return "The gu service gave no free trial right now.";
|
|
251
|
+
return "This computer needs a gu token to start the agent.";
|
|
252
|
+
}
|
|
253
|
+
|
|
241
254
|
/**
|
|
242
255
|
* THE TOKEN QUESTION, WITH THE TRIAL AS ITS DEFAULT (#305/#76, placed by #314).
|
|
243
256
|
*
|
|
@@ -285,8 +298,10 @@ export async function promptForToken({
|
|
|
285
298
|
const dev = device === undefined ? await trialDevice().catch(() => null) : device;
|
|
286
299
|
const getOne = `For a free token for 1 computer, log in at ${accountUrl} and create one — or buy Pro there for more computers.`;
|
|
287
300
|
let trial, trialOk, shown, hint;
|
|
288
|
-
let details = []; // #219: the
|
|
301
|
+
let details = []; // #219/#222: the detail, printed only when the person asks ("?" or an empty Enter)
|
|
302
|
+
let compactShort = ""; // #222: the ONE yellow line when there is no default token to offer
|
|
289
303
|
const offer = async () => {
|
|
304
|
+
details = [];
|
|
290
305
|
trial = dev ? await claim({ hostedBase, device: dev }) : { ok: false, why: "no-device" };
|
|
291
306
|
trialOk = Boolean(trial.ok && verifyCliToken(trial.token, publicKeyB64).ok);
|
|
292
307
|
shown = trialOk ? shortToken(trial.token) : "";
|
|
@@ -326,9 +341,18 @@ export async function promptForToken({
|
|
|
326
341
|
...(trialOk ? [] : ["Paste a Pro token here, then Enter."]),
|
|
327
342
|
];
|
|
328
343
|
}
|
|
344
|
+
// #222: NO DEFAULT TOKEN to offer (trial ended, nothing yet, service unreachable): the same
|
|
345
|
+
// advice used to be said three times — the banner, a title, and 3-4 yellow lines under the
|
|
346
|
+
// prompt. Now ONE yellow line; everything else is behind "?" (or an empty Enter).
|
|
347
|
+
compactShort = "";
|
|
348
|
+
if (!trialOk && !overLimit) {
|
|
349
|
+
compactShort = noTokenShort({ why, trial, dev });
|
|
350
|
+
details = [...(why ? [why] : []), ...hint.filter((l) => !why || l !== why)];
|
|
351
|
+
hint = [];
|
|
352
|
+
}
|
|
329
353
|
};
|
|
330
354
|
await offer();
|
|
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");
|
|
355
|
+
log(overLimit && why ? `${short || why} Type ? for details.` : compactShort ? `${compactShort} Type ? for details.` : why || "This computer needs a gu token before the agent can start — one time only.", "title");
|
|
332
356
|
const entitled = async (token) => {
|
|
333
357
|
if (!accept) return true;
|
|
334
358
|
const r = await accept(token).catch(() => ({ ok: true })); // unknown = soft, like every other check here
|
|
@@ -341,6 +365,8 @@ export async function promptForToken({
|
|
|
341
365
|
// A way OUT of a required question (task #17): "/exit" is not a token, and treating it as one
|
|
342
366
|
// ("That token was not saved…" and the same question again) left no way to leave gu at all.
|
|
343
367
|
if (/^\/(exit|quit)$/i.test(typed)) return { ok: false, why: "quit" };
|
|
368
|
+
// An empty Enter with no default is also "try again": the service may be back, the trial may exist now (#222).
|
|
369
|
+
if (!typed && required && !trialOk && !overLimit) await offer();
|
|
344
370
|
// #219: the over-limit detail, on request only — "?" or an empty Enter, then the same question again.
|
|
345
371
|
if (details.length && (typed === "?" || (!typed && required))) {
|
|
346
372
|
for (const l of details) log(l, "info");
|
package/client-expert-run.mjs
CHANGED
|
@@ -324,15 +324,18 @@ export async function ensureNativeMongoMac(ask, mongo, { log = (l) => console.lo
|
|
|
324
324
|
// a yes to something unseen. These are exactly the lines a yes runs.
|
|
325
325
|
log(dim(installing ? "\n It is not installed here. This installs it (Homebrew):\n" : "\n It is installed but not running. This starts it:\n"));
|
|
326
326
|
for (const s of steps) log(` brew ${s.join(" ")}`);
|
|
327
|
+
// How long and how big, BEFORE the yes (#221): the install took minutes and pulled a second Node
|
|
328
|
+
// and a dozen libraries, with nothing said about it up front.
|
|
329
|
+
if (installing) log(dim("\n This downloads a few hundred MB and takes a few minutes — Homebrew may also\n add or update some of its own libraries (a Node, OpenSSL…) that MongoDB's tools need."));
|
|
327
330
|
log("");
|
|
328
331
|
const yes = await readYesNo(ask, installing ? " Install MongoDB now? [Y/n]: " : " Start MongoDB now? [Y/n]: ", true, log);
|
|
329
332
|
if (!yes) {
|
|
330
333
|
log(dim(`\n Not ${installing ? "installed" : "started"} — run the commands above yourself.`));
|
|
331
334
|
return { ok: false, explained: true };
|
|
332
335
|
}
|
|
333
|
-
log("\n " + bold(installing ? "Installing MongoDB Community" : "Starting MongoDB") + dim(" Homebrew, a
|
|
336
|
+
log("\n " + bold(installing ? "Installing MongoDB Community" : "Starting MongoDB") + dim(installing ? " Homebrew, a few minutes" : " Homebrew"));
|
|
334
337
|
const done = await mn.runBrewSteps(brew, steps, {
|
|
335
|
-
onLine: (l) => l.length < 200 && log(dim(" " + l)),
|
|
338
|
+
onLine: (l) => l.length < 200 && mn.brewLineWorthShowing(l) && log(dim(" " + l)),
|
|
336
339
|
});
|
|
337
340
|
if (!done) {
|
|
338
341
|
log("\n " + red("✗ ") + "Homebrew did not finish — the lines above say why.");
|
|
@@ -771,6 +774,19 @@ export async function readYesNo(ask, prompt, defaultYes, say = (l) => console.lo
|
|
|
771
774
|
return defaultYes;
|
|
772
775
|
}
|
|
773
776
|
|
|
777
|
+
/**
|
|
778
|
+
* Does setup still need to ASK whose MongoDB this is? (#221) → "custom" | "default" | "forced" | "ask".
|
|
779
|
+
* A resumed run reuses what an earlier run was told: custom credentials (kept as a uri), or a yes
|
|
780
|
+
* to the default empty pair (kept as mongoCredsConfirmed — it used to be forgotten, so an
|
|
781
|
+
* interrupted setup asked the same question again). A uri forced through the environment is deliberate.
|
|
782
|
+
*/
|
|
783
|
+
export function mongoCredsDecision(state = {}, env = process.env) {
|
|
784
|
+
if (typeof state.localMongoUri === "string" && state.localMongoUri) return "custom";
|
|
785
|
+
if (state.mongoCredsConfirmed === true) return "default";
|
|
786
|
+
if (env.GONEXT_LOCAL_MONGO_URI) return "forced";
|
|
787
|
+
return "ask";
|
|
788
|
+
}
|
|
789
|
+
|
|
774
790
|
export async function askMongoCredentials(ask, askSecret = null) {
|
|
775
791
|
const { host, port } = mongoHostPort(LOCAL_MONGO_URI);
|
|
776
792
|
console.log(dim(` MongoDB is answering at ${host}:${port}.`));
|
|
@@ -1167,12 +1183,13 @@ export async function runClientExpertSetup({
|
|
|
1167
1183
|
// A uri already in state — a resumed run — is reused silently, like every other answered
|
|
1168
1184
|
// question, and a URI forced via GONEXT_LOCAL_MONGO_URI is taken as deliberate.
|
|
1169
1185
|
let mongoUri = LOCAL_MONGO_URI;
|
|
1170
|
-
|
|
1186
|
+
const credsDecision = mongoCredsDecision(state);
|
|
1187
|
+
if (credsDecision === "custom") {
|
|
1171
1188
|
mongoUri = state.localMongoUri;
|
|
1172
1189
|
if (mongoUri !== LOCAL_MONGO_URI) {
|
|
1173
1190
|
console.log(dim(" using the MongoDB credentials from the previous run"));
|
|
1174
1191
|
}
|
|
1175
|
-
} else if (
|
|
1192
|
+
} else if (credsDecision === "ask") {
|
|
1176
1193
|
const credsChoice = await askMongoCredentials(ask, askSecret);
|
|
1177
1194
|
if (credsChoice.exit) {
|
|
1178
1195
|
console.log("\n " + dim("No working MongoDB credentials — setup stops here."));
|
|
@@ -1184,9 +1201,7 @@ export async function runClientExpertSetup({
|
|
|
1184
1201
|
return { complete: false, stoppedAt: "mongo" };
|
|
1185
1202
|
}
|
|
1186
1203
|
mongoUri = credsChoice.uri;
|
|
1187
|
-
|
|
1188
|
-
state = { ...state, localMongoUri: mongoUri };
|
|
1189
|
-
}
|
|
1204
|
+
state = credsChoice.custom ? { ...state, localMongoUri: mongoUri } : { ...state, mongoCredsConfirmed: true };
|
|
1190
1205
|
}
|
|
1191
1206
|
state = recordStep(state, "mongo", "done");
|
|
1192
1207
|
await saveState(state);
|
package/env-check.mjs
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What is wrong with gu's footprint on THIS machine's PATH (#221) — found on a Mac that was
|
|
3
|
+
* "uninstalled" and reinstalled:
|
|
4
|
+
*
|
|
5
|
+
* · a second gu from ANOTHER npm (Homebrew's, beside nvm's) survived the uninstall and kept
|
|
6
|
+
* answering `which gu` — two copies, two versions, no hint which one runs;
|
|
7
|
+
* · `gu`/`gu-cli` links whose package was removed (broken) are left in a bin folder;
|
|
8
|
+
* · the shell profile's PATH block was written with a VERSIONED Homebrew path
|
|
9
|
+
* (…/Cellar/node/26.10.0_1/bin), which stops existing at the next `brew upgrade node`.
|
|
10
|
+
*
|
|
11
|
+
* Pure of prompts: this file looks and fixes ONE thing at a time; `gu-cli fix-path` asks. Everything
|
|
12
|
+
* takes `pathDirs` / `home`, so the tests run on a temp folder.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, lstatSync, readFileSync, readlinkSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
16
|
+
|
|
17
|
+
export const GU_BIN_NAMES = ["gu", "gu-cli"];
|
|
18
|
+
export const RC_BEGIN = "# >>> gu-cli PATH >>>";
|
|
19
|
+
export const RC_END = "# <<< gu-cli PATH <<<";
|
|
20
|
+
const PACKAGE_MARK = "@tiens.nguyen/gu-cli";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A Homebrew Cellar path names ONE version of node and disappears when it is upgraded; the same
|
|
24
|
+
* folder is reachable as <brew prefix>/bin for good. Anything else is returned as it was.
|
|
25
|
+
* /opt/homebrew/Cellar/node/26.10.0_1/bin → /opt/homebrew/bin
|
|
26
|
+
*/
|
|
27
|
+
export function stableBinDir(binDir) {
|
|
28
|
+
const m = /^(.*)\/Cellar\/[^/]+\/[^/]+\/bin\/?$/.exec(String(binDir ?? ""));
|
|
29
|
+
return m ? join(m[1] || "/", "bin") : String(binDir ?? "");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const isGuTarget = (t) => String(t).includes(PACKAGE_MARK) || /gu-cli[\\/](gu-repl|gu-cli)\.mjs$/.test(String(t));
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Every gu / gu-cli launcher in the PATH folders (POSIX symlinks, what npm makes).
|
|
36
|
+
* → [{ dir, name, path, target, broken, pkgDir }] (`pkgDir` = the package folder it points into, when it works)
|
|
37
|
+
*/
|
|
38
|
+
export function scanGuLinks({ pathDirs = [] } = {}) {
|
|
39
|
+
const out = [];
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
for (const dir of pathDirs) {
|
|
42
|
+
if (!dir || seen.has(dir)) continue;
|
|
43
|
+
seen.add(dir);
|
|
44
|
+
for (const name of GU_BIN_NAMES) {
|
|
45
|
+
const path = join(dir, name);
|
|
46
|
+
let st;
|
|
47
|
+
try {
|
|
48
|
+
st = lstatSync(path);
|
|
49
|
+
} catch {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (!st.isSymbolicLink()) continue;
|
|
53
|
+
let target = "";
|
|
54
|
+
try {
|
|
55
|
+
target = readlinkSync(path);
|
|
56
|
+
} catch {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const abs = isAbsolute(target) ? target : resolve(dir, target);
|
|
60
|
+
if (!isGuTarget(abs)) continue; // somebody else's `gu` is none of our business
|
|
61
|
+
let real = "";
|
|
62
|
+
try {
|
|
63
|
+
real = realpathSync(path);
|
|
64
|
+
} catch {
|
|
65
|
+
/* broken */
|
|
66
|
+
}
|
|
67
|
+
out.push({ dir, name, path, target: abs, broken: !real, pkgDir: real ? dirname(real) : "" });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The npm prefix a package folder belongs to: <prefix>/lib/node_modules/@tiens.nguyen/gu-cli → <prefix>. */
|
|
74
|
+
export function prefixOfPackageDir(pkgDir) {
|
|
75
|
+
const m = /^(.*)\/lib\/node_modules\/@tiens\.nguyen\/gu-cli$/.exec(String(pkgDir ?? ""));
|
|
76
|
+
return m ? m[1] || "/" : "";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Working gu installs other than `here` (the one running this).
|
|
81
|
+
* → [{ pkgDir, prefix, links: [path] }]
|
|
82
|
+
*/
|
|
83
|
+
export function otherGuCopies(links, here = "") {
|
|
84
|
+
const mine = here ? safeReal(here) : "";
|
|
85
|
+
const byPkg = new Map();
|
|
86
|
+
for (const l of links) {
|
|
87
|
+
if (l.broken || !l.pkgDir) continue;
|
|
88
|
+
if (mine && safeReal(l.pkgDir) === mine) continue;
|
|
89
|
+
const e = byPkg.get(l.pkgDir) ?? { pkgDir: l.pkgDir, prefix: prefixOfPackageDir(l.pkgDir), links: [] };
|
|
90
|
+
e.links.push(l.path);
|
|
91
|
+
byPkg.set(l.pkgDir, e);
|
|
92
|
+
}
|
|
93
|
+
return [...byPkg.values()];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function safeReal(p) {
|
|
97
|
+
try {
|
|
98
|
+
return realpathSync(p);
|
|
99
|
+
} catch {
|
|
100
|
+
return resolve(p);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The gu block in a shell profile, read. → { has, binDir, versioned, missing }
|
|
106
|
+
* versioned = the path names a Homebrew Cellar version (breaks on upgrade)
|
|
107
|
+
* missing = the folder it adds is not there any more
|
|
108
|
+
*/
|
|
109
|
+
export function readRcBlock(text, { exists = existsSync } = {}) {
|
|
110
|
+
const s = String(text ?? "");
|
|
111
|
+
const a = s.indexOf(RC_BEGIN);
|
|
112
|
+
const b = s.indexOf(RC_END);
|
|
113
|
+
if (a < 0 || b < a) return { has: false, binDir: "", versioned: false, missing: false };
|
|
114
|
+
const m = /export PATH="([^"$]+?):\$PATH"/.exec(s.slice(a, b));
|
|
115
|
+
const binDir = m ? m[1] : "";
|
|
116
|
+
return { has: true, binDir, versioned: Boolean(binDir) && stableBinDir(binDir) !== binDir, missing: Boolean(binDir) && !exists(binDir) };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The block, rewritten to add `binDir` instead (everything outside the markers untouched). */
|
|
120
|
+
export function rewriteRcBlock(text, binDir) {
|
|
121
|
+
const s = String(text ?? "");
|
|
122
|
+
const a = s.indexOf(RC_BEGIN);
|
|
123
|
+
const b = s.indexOf(RC_END);
|
|
124
|
+
if (a < 0 || b < a) return s;
|
|
125
|
+
const end = b + RC_END.length;
|
|
126
|
+
const block = `${RC_BEGIN}\n# npm's global bin dir was not on PATH; added by @tiens.nguyen/gu-cli.\nexport PATH="${binDir}:$PATH"\n${RC_END}`;
|
|
127
|
+
return s.slice(0, a) + block + s.slice(end);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const RC_FILES = [".zshrc", ".bashrc", ".bash_profile", ".profile"];
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Everything found, as findings the caller can print and (when `fix`) apply:
|
|
134
|
+
* { id, text, fix?: { kind: "remove-link" | "rewrite-rc" | "uninstall-copy", … , label } }
|
|
135
|
+
*/
|
|
136
|
+
export function checkEnvironment({ pathDirs = [], home = "", here = "", exists = existsSync } = {}) {
|
|
137
|
+
const findings = [];
|
|
138
|
+
const links = scanGuLinks({ pathDirs });
|
|
139
|
+
for (const l of links.filter((x) => x.broken)) {
|
|
140
|
+
findings.push({
|
|
141
|
+
id: "broken-link",
|
|
142
|
+
text: `${l.path} points to a gu that is no longer installed.`,
|
|
143
|
+
fix: { kind: "remove-link", path: l.path, label: `remove ${l.path}` },
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
for (const c of otherGuCopies(links, here)) {
|
|
147
|
+
let version = "?";
|
|
148
|
+
try {
|
|
149
|
+
version = JSON.parse(readFileSync(join(c.pkgDir, "package.json"), "utf8")).version;
|
|
150
|
+
} catch {
|
|
151
|
+
/* shown as ? */
|
|
152
|
+
}
|
|
153
|
+
findings.push({
|
|
154
|
+
id: "second-copy",
|
|
155
|
+
text: `A second gu (${version}) is installed at ${c.pkgDir} — which one runs depends on PATH order.`,
|
|
156
|
+
fix: c.prefix ? { kind: "uninstall-copy", prefix: c.prefix, label: `npm uninstall -g ${PACKAGE_MARK} --prefix ${c.prefix}` } : undefined,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
for (const f of RC_FILES) {
|
|
160
|
+
const file = join(home, f);
|
|
161
|
+
let text = "";
|
|
162
|
+
try {
|
|
163
|
+
text = readFileSync(file, "utf8");
|
|
164
|
+
} catch {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const rc = readRcBlock(text, { exists });
|
|
168
|
+
if (!rc.has) continue;
|
|
169
|
+
if (rc.versioned) {
|
|
170
|
+
const stable = stableBinDir(rc.binDir);
|
|
171
|
+
findings.push({
|
|
172
|
+
id: "versioned-path",
|
|
173
|
+
text: `${file} adds ${rc.binDir}, a Homebrew version folder that goes away on \`brew upgrade\`${rc.missing ? " (it already has — gu is not on PATH from there)" : ""}.`,
|
|
174
|
+
fix: { kind: "rewrite-rc", file, binDir: stable, label: `change it to ${stable}` },
|
|
175
|
+
});
|
|
176
|
+
} else if (rc.missing) {
|
|
177
|
+
findings.push({ id: "missing-path", text: `${file} adds ${rc.binDir}, which does not exist.` });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return findings;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Apply one finding's fix. → { ok, why } — never throws. */
|
|
184
|
+
export function applyFix(fix, { run = null } = {}) {
|
|
185
|
+
try {
|
|
186
|
+
if (fix.kind === "remove-link") {
|
|
187
|
+
rmSync(fix.path, { force: true });
|
|
188
|
+
return { ok: !lstatOk(fix.path), why: "" };
|
|
189
|
+
}
|
|
190
|
+
if (fix.kind === "rewrite-rc") {
|
|
191
|
+
const text = readFileSync(fix.file, "utf8");
|
|
192
|
+
writeFileSync(fix.file, rewriteRcBlock(text, fix.binDir));
|
|
193
|
+
return { ok: true, why: "" };
|
|
194
|
+
}
|
|
195
|
+
if (fix.kind === "uninstall-copy") {
|
|
196
|
+
if (!run) return { ok: false, why: "no runner" };
|
|
197
|
+
const r = run("npm", ["uninstall", "-g", PACKAGE_MARK, "--prefix", fix.prefix]);
|
|
198
|
+
return { ok: r.status === 0, why: r.status === 0 ? "" : String(r.stderr || r.error || `exit ${r.status}`).trim().slice(0, 200) };
|
|
199
|
+
}
|
|
200
|
+
} catch (e) {
|
|
201
|
+
return { ok: false, why: e?.message || String(e) };
|
|
202
|
+
}
|
|
203
|
+
return { ok: false, why: "unknown fix" };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function lstatOk(p) {
|
|
207
|
+
try {
|
|
208
|
+
lstatSync(p);
|
|
209
|
+
return true;
|
|
210
|
+
} catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* One line for gu's start when gu runs but its own npm folder is NOT on PATH (#221). npm 11 no
|
|
217
|
+
* longer runs install scripts it was not told to allow, so the postinstall that adds the folder
|
|
218
|
+
* may never have run: gu was started by its full path (or npx) and the next terminal will say
|
|
219
|
+
* "gu: command not found". The line gives the command, by full path since `gu-cli` is not found either.
|
|
220
|
+
* → "" when there is nothing to say (on PATH, not a global install, Windows, or CI).
|
|
221
|
+
*/
|
|
222
|
+
export function pathNoticeLine({ prefix = "", pathEnv = "", platform = process.platform, ci = false } = {}) {
|
|
223
|
+
if (!prefix || platform === "win32" || ci) return "";
|
|
224
|
+
const binDir = join(prefix, "bin");
|
|
225
|
+
const dirs = String(pathEnv).split(":");
|
|
226
|
+
if (dirs.includes(binDir) || dirs.includes(stableBinDir(binDir))) return "";
|
|
227
|
+
return `${binDir} is not on your PATH, so a new terminal will not find \`gu\`. Fix it: ${join(binDir, "gu-cli")} fix-path`;
|
|
228
|
+
}
|
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
|
|
|
@@ -1253,13 +1286,17 @@ if (args[0] === "fix-powershell") {
|
|
|
1253
1286
|
}
|
|
1254
1287
|
|
|
1255
1288
|
if (args[0] === "support") {
|
|
1256
|
-
//
|
|
1257
|
-
|
|
1258
|
-
const {
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
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);
|
|
1263
1300
|
}
|
|
1264
1301
|
|
|
1265
1302
|
if (args[0] === "clear" || args[0] === "reset") {
|