@tiens.nguyen/gu-cli 1.0.806 → 1.0.808
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/gu-repl.mjs +61 -14
- package/package.json +2 -1
- package/update-verify.mjs +194 -0
package/gu-repl.mjs
CHANGED
|
@@ -144,6 +144,7 @@ import { readSkillSource, sourceLabel, SKILL_SOURCE_KEY } from "./skill-source.m
|
|
|
144
144
|
import { codingBannerLabel, isOllamaCloudUrl, SECRET_KEY_HINT } from "./coding-key-prompt.mjs";
|
|
145
145
|
import { questionEchoText, thoughtPrintSeq } from "./terminal-layout.mjs";
|
|
146
146
|
import { globalPrefixOf } from "./install-path.mjs";
|
|
147
|
+
import { firstGuOnPath, pathGuMismatch, guOnPathIsCurrent, updateAttemptRecord, repeatUpdateNotice } from "./update-verify.mjs";
|
|
147
148
|
import { embedNotice, embedState } from "./embed-model-command.mjs";
|
|
148
149
|
import {
|
|
149
150
|
MODELFALLBACK_PREFIX, fallbackAfterTurn, fallbackHistoryLine, fallbackNotice, keyMissingNotice,
|
|
@@ -1107,12 +1108,21 @@ const contentWrite = (raw, chunk, enc, cb) => {
|
|
|
1107
1108
|
// Any real output commits a held question first (see pendingEcho above).
|
|
1108
1109
|
if (_uiDepth === 0 && pendingEcho !== null) flushPendingEcho();
|
|
1109
1110
|
if (_uiDepth === 0 && (typeof chunk === "string" || Buffer.isBuffer(chunk))) {
|
|
1111
|
+
// #113 NEVER PULL A READER DOWN. If the user has scrolled back to read history, new output
|
|
1112
|
+
// (the model thinking, a tool's lines) is RECORDED but not drawn, and the view stays on the
|
|
1113
|
+
// same lines: the offset grows by the rows just added, so "↓ jump to bottom · N lines below"
|
|
1114
|
+
// counts up instead. Only the user brings the view home — typing, Enter, End, or a click on
|
|
1115
|
+
// the jump line. At the live tail (offset 0) output follows the bottom exactly as before.
|
|
1116
|
+
const holdView = barPinned() && scrolledBack();
|
|
1117
|
+
const rowsBefore = holdView ? screenRows().length : 0;
|
|
1110
1118
|
transcript.append(String(chunk), { followCursor: !zoneTookOver });
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1119
|
+
if (holdView) {
|
|
1120
|
+
scrollOffset += Math.max(0, screenRows().length - rowsBefore);
|
|
1121
|
+
paintBar(); // the jump line's "N lines below" changed; the history on screen did not
|
|
1122
|
+
if (typeof enc === "function") enc();
|
|
1123
|
+
else if (typeof cb === "function") cb();
|
|
1124
|
+
return true;
|
|
1125
|
+
}
|
|
1116
1126
|
}
|
|
1117
1127
|
if (_uiDepth === 0 && barPinned()) {
|
|
1118
1128
|
// Content belongs in the scrolling area, not where the caret is parked.
|
|
@@ -2441,11 +2451,26 @@ async function refreshCliToken() {
|
|
|
2441
2451
|
}
|
|
2442
2452
|
const TOKEN_TICK_MS = 60 * 60 * 1000;
|
|
2443
2453
|
|
|
2454
|
+
// The last in-app CLI update attempt (task #21): lets the next start say WHY an update did not
|
|
2455
|
+
// take effect instead of offering the very same update again, forever.
|
|
2456
|
+
const UPDATE_ATTEMPT_FILE = join(homedir(), ".gonext", "last-cli-update.json");
|
|
2457
|
+
function readUpdateAttempt() {
|
|
2458
|
+
try { return JSON.parse(readFileSync(UPDATE_ATTEMPT_FILE, "utf8")); } catch { return null; }
|
|
2459
|
+
}
|
|
2460
|
+
function saveUpdateAttempt(rec) {
|
|
2461
|
+
try { mkdirSync(dirname(UPDATE_ATTEMPT_FILE), { recursive: true }); writeFileSync(UPDATE_ATTEMPT_FILE, JSON.stringify(rec, null, 2)); } catch { /* best effort */ }
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2444
2464
|
let cliNoticeShown = false;
|
|
2445
2465
|
function announceCliUpdate() {
|
|
2446
2466
|
if (cliNoticeShown || !availableUpdates.cli) return;
|
|
2447
2467
|
if (following) return; // a turn owns the transcript — try again at the next prompt
|
|
2448
|
-
|
|
2468
|
+
let lines = [];
|
|
2469
|
+
try {
|
|
2470
|
+
lines = repeatUpdateNotice({ last: readUpdateAttempt(), cli: availableUpdates.cli, running: REPL_VERSION,
|
|
2471
|
+
onPath: firstGuOnPath({ PATH: process.env.PATH }), home: homedir(), env: process.env });
|
|
2472
|
+
} catch { lines = []; }
|
|
2473
|
+
if (!lines.length) lines = updateNotice({ cli: availableUpdates.cli, running: REPL_VERSION });
|
|
2449
2474
|
if (!lines.length) return;
|
|
2450
2475
|
cliNoticeShown = true;
|
|
2451
2476
|
console.log("");
|
|
@@ -2539,12 +2564,20 @@ async function runUpdate() {
|
|
|
2539
2564
|
}
|
|
2540
2565
|
const failed = describeInstallFailure(r);
|
|
2541
2566
|
if (failed) throw new Error(failed);
|
|
2567
|
+
const onPath = firstGuOnPath({ PATH: process.env.PATH });
|
|
2542
2568
|
// npm exiting 0 is not the outcome: read the version now on disk in THIS install.
|
|
2543
2569
|
let onDisk = "";
|
|
2544
2570
|
try { onDisk = JSON.parse(readFileSync(join(DIR, "package.json"), "utf8")).version ?? ""; } catch { /* unreadable: skip the check */ }
|
|
2545
2571
|
const short = installedVersionShortfall(onDisk, cli);
|
|
2546
|
-
|
|
2572
|
+
// nvm-windows: `nvm use` in ANY terminal re-points C:\nvm4w\nodejs, so npm may have updated the
|
|
2573
|
+
// now-active Node's copy instead of this one. What counts is the gu the next `gu` starts.
|
|
2574
|
+
if (short && !guOnPathIsCurrent({ wanted: cli, onPath })) throw new Error(short);
|
|
2575
|
+
// …and THIS install is not necessarily the gu the next `gu` starts (task #21): another copy
|
|
2576
|
+
// earlier on PATH (e.g. a Homebrew Cellar bin dir from ~/.zshrc) keeps running the old one.
|
|
2577
|
+
const mismatch = pathGuMismatch({ wanted: cli, installedDir: DIR, onPath, env: process.env });
|
|
2578
|
+
if (mismatch) throw new Error(mismatch);
|
|
2547
2579
|
});
|
|
2580
|
+
saveUpdateAttempt(updateAttemptRecord({ version: cli, running: REPL_VERSION, ok: true }));
|
|
2548
2581
|
// NOT "updated" — the files changed, THIS process did not. Claiming otherwise is the one
|
|
2549
2582
|
// way this feature could lie to someone.
|
|
2550
2583
|
console.log(accent(` ✓ gu ${cli} installed`) +
|
|
@@ -2552,7 +2585,13 @@ async function runUpdate() {
|
|
|
2552
2585
|
availableUpdates = { ...availableUpdates, cli: "", any: Boolean(availableUpdates.api) };
|
|
2553
2586
|
}
|
|
2554
2587
|
} catch (e) {
|
|
2555
|
-
|
|
2588
|
+
if (pick === "cli") {
|
|
2589
|
+
saveUpdateAttempt(updateAttemptRecord({ version: cli, running: REPL_VERSION, ok: false, reason: e?.message ?? String(e) }));
|
|
2590
|
+
console.log(red(` ✗ Update NOT successful — gu is still ${REPL_VERSION}`));
|
|
2591
|
+
console.log(red(` ${e?.message ?? e}\n`));
|
|
2592
|
+
} else {
|
|
2593
|
+
console.log(red(` update failed: ${e?.message ?? e}\n`));
|
|
2594
|
+
}
|
|
2556
2595
|
}
|
|
2557
2596
|
paintBar();
|
|
2558
2597
|
}
|
|
@@ -2644,7 +2683,9 @@ const tokenLineText = () => {
|
|
|
2644
2683
|
dim(" · ") +
|
|
2645
2684
|
accent(`↓ out ${fmtTokens(lastTurnOutputTokens)}`)
|
|
2646
2685
|
);
|
|
2647
|
-
|
|
2686
|
+
// #112: the lifetime "total ↓ N" is no longer shown on the bar (the user's call) — hidden, not
|
|
2687
|
+
// deleted: globalOutputTokens is still tracked and still feeds /tokens reset.
|
|
2688
|
+
const total = "";
|
|
2648
2689
|
// #323: what the last turn's prompt cost, LAST on the row after a "|" — "~" when the cached share
|
|
2649
2690
|
// was assumed; "no price" when the model has none (never a misleading "| $0"). It survives a
|
|
2650
2691
|
// narrow window: "total ↓" is dropped before it is (tokenRowWithPrice).
|
|
@@ -7829,6 +7870,13 @@ async function applyTransferDoc(value, opts = {}) {
|
|
|
7829
7870
|
* bar and /model kept showing the old coder), and the "no embed model" / "no agent model"
|
|
7830
7871
|
* warnings. /model already does this after a change; applying a preset must too.
|
|
7831
7872
|
*/
|
|
7873
|
+
function seedDefaultCoder(p) {
|
|
7874
|
+
// The ONE place the account-default coder fields are read (startup + after a preset apply).
|
|
7875
|
+
// Anything that wants THIS folder's coder uses sessionCodingModel / activeCoderName() instead.
|
|
7876
|
+
if (p?.codingModelId) defaultCodingModel = p.codingModelId;
|
|
7877
|
+
if (p?.codingBaseURL) defaultCodingUrl = p.codingBaseURL;
|
|
7878
|
+
}
|
|
7879
|
+
|
|
7832
7880
|
async function refreshAfterPresetApply(value, codingDefault, result) {
|
|
7833
7881
|
try {
|
|
7834
7882
|
// The preset's coder IS the account default now — a folder pick would shadow it.
|
|
@@ -7843,9 +7891,9 @@ async function refreshAfterPresetApply(value, codingDefault, result) {
|
|
|
7843
7891
|
// What the API now answers with is authoritative — read it back rather than assume.
|
|
7844
7892
|
const probe = await fetchAgentPayload([{ role: "user", content: "ping" }]).catch(() => null);
|
|
7845
7893
|
const p = probe?.payload ?? {};
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
activeCoderWhere = whereOfActiveCoder(probe?.codingChoices
|
|
7894
|
+
seedDefaultCoder(p);
|
|
7895
|
+
// codingChoices lives on the envelope, never inside payload.
|
|
7896
|
+
activeCoderWhere = whereOfActiveCoder(probe?.codingChoices, sessionCodingModel);
|
|
7849
7897
|
if (value?.agentModel?.name || p.agentMissing === false) agentMissing = false;
|
|
7850
7898
|
else if (p.agentMissing === true) agentMissing = true;
|
|
7851
7899
|
if (probe?.payload) noModelAtAll = nothingToAnswerWith(p);
|
|
@@ -12876,8 +12924,7 @@ async function main() {
|
|
|
12876
12924
|
// Remember the default coder id for the token footer's global-total label, and the
|
|
12877
12925
|
// default backend's coding URL so the GLOBAL output-token total is keyed to it (task
|
|
12878
12926
|
// #125) when no /model override is active.
|
|
12879
|
-
|
|
12880
|
-
if (p.codingBaseURL) defaultCodingUrl = p.codingBaseURL;
|
|
12927
|
+
seedDefaultCoder(p);
|
|
12881
12928
|
// Task #14: seed the bottom warnings from the AUTHORITATIVE payload. `agentMissing` is set
|
|
12882
12929
|
// by the API when no agent model is configured (and it fell back to the coding model);
|
|
12883
12930
|
// `ragEnabled` false means no embed model was chosen. Boot may have already set these from
|
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.808",
|
|
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",
|
|
@@ -115,6 +115,7 @@
|
|
|
115
115
|
"add-server.mjs",
|
|
116
116
|
"powershell-alias.mjs",
|
|
117
117
|
"install-path.mjs",
|
|
118
|
+
"update-verify.mjs",
|
|
118
119
|
"skills.mjs",
|
|
119
120
|
"skill-source.mjs",
|
|
120
121
|
"step-group.mjs",
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Did the gu update REALLY take effect? (task #21)
|
|
3
|
+
*
|
|
4
|
+
* Seen on a real Mac: `~/.zshrc` put /opt/homebrew/Cellar/node/<v>/bin first on PATH, so `gu`
|
|
5
|
+
* started the Cellar copy (1.0.797) while `npm install -g` wrote /opt/homebrew/lib/node_modules
|
|
6
|
+
* (1.0.806). The updater said "✓ installed", the next `gu` was still 1.0.797, and it asked again —
|
|
7
|
+
* forever. npm exiting 0 and the files in ONE prefix being new proves nothing about the gu the
|
|
8
|
+
* user's shell will start. So:
|
|
9
|
+
* · firstGuOnPath — the gu the next `gu` will actually run (first on PATH) and its version;
|
|
10
|
+
* · pathGuMismatch — the sentence to show when that gu is older than what we installed;
|
|
11
|
+
* · attempt memory — remember the last update, so the next start reports WHY instead of
|
|
12
|
+
* offering the same update again as if nothing had happened.
|
|
13
|
+
*
|
|
14
|
+
* ALL THREE PLATFORMS (follow-up: "make sure this won't happen on Ubuntu or Windows 11"):
|
|
15
|
+
* · macOS / Ubuntu: `<prefix>/bin/gu` is a SYMLINK to `<prefix>/lib/node_modules/@tiens.nguyen/
|
|
16
|
+
* gu-cli/gu-repl.mjs` — apt/NodeSource node (prefix /usr), nvm (~/.nvm/versions/node/<v>),
|
|
17
|
+
* Homebrew, a user prefix (~/.npm-global). Realpath tells us the package and its prefix.
|
|
18
|
+
* Ubuntu's usual failure is EACCES on the root-owned /usr prefix → the fix needs `sudo`.
|
|
19
|
+
* · Windows: PATH is `;`-separated, case-insensitive, and the shims (gu.cmd / gu.ps1 / gu) are
|
|
20
|
+
* small scripts, NOT symlinks — they sit directly in the prefix next to
|
|
21
|
+
* `<prefix>\node_modules\@tiens.nguyen\gu-cli`. Its usual failure is EPERM/EBUSY because the
|
|
22
|
+
* running gu's files are locked → the fix is "close every gu window first".
|
|
23
|
+
* Pure (fs + platform injected) so every branch is testable on any OS.
|
|
24
|
+
*/
|
|
25
|
+
import { existsSync, realpathSync, readFileSync } from "node:fs";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
|
|
28
|
+
const PKG = "@tiens.nguyen/gu-cli";
|
|
29
|
+
const pathFor = (platform) => (platform === "win32" ? path.win32 : path.posix);
|
|
30
|
+
|
|
31
|
+
/** Numeric compare of x.y.z (pre-release suffix ignored). <0, 0, >0; NaN parts → 0. */
|
|
32
|
+
export function compareVersions(a = "", b = "") {
|
|
33
|
+
const p = (v) => String(v ?? "").split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
|
|
34
|
+
const x = p(a), y = p(b);
|
|
35
|
+
for (let i = 0; i < Math.max(x.length, y.length); i++) {
|
|
36
|
+
const d = (x[i] ?? 0) - (y[i] ?? 0);
|
|
37
|
+
if (d) return d;
|
|
38
|
+
}
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const realFs = {
|
|
43
|
+
exists: (p) => existsSync(p),
|
|
44
|
+
realpath: (p) => realpathSync(p),
|
|
45
|
+
readVersion: (dir) => {
|
|
46
|
+
try { return JSON.parse(readFileSync(path.join(dir, "package.json"), "utf8")).version ?? ""; } catch { return ""; }
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Windows shim names, in the order cmd.exe / PowerShell would pick them. */
|
|
51
|
+
const WIN_SHIMS = ["gu.exe", "gu.cmd", "gu.bat", "gu.ps1", "gu"];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The first `gu` on PATH → { bin, dir, version, prefix } or null.
|
|
55
|
+
* `dir` is the package folder the bin resolves to; `prefix` the npm prefix owning it ("" if none).
|
|
56
|
+
*/
|
|
57
|
+
export function firstGuOnPath({ PATH = "", platform = process.platform, fs = realFs } = {}) {
|
|
58
|
+
const P = pathFor(platform);
|
|
59
|
+
const win = platform === "win32";
|
|
60
|
+
const dirs = String(PATH).split(win ? ";" : ":").map((d) => d.trim().replace(/^"(.*)"$/, "$1")).filter(Boolean);
|
|
61
|
+
for (const d of dirs) {
|
|
62
|
+
if (win) {
|
|
63
|
+
const shim = WIN_SHIMS.map((n) => P.join(d, n)).find((b) => fs.exists(b));
|
|
64
|
+
if (!shim) continue;
|
|
65
|
+
// npm's Windows layout: shims in <prefix>, package in <prefix>\node_modules\@tiens.nguyen\gu-cli.
|
|
66
|
+
const dir = P.join(d, "node_modules", "@tiens.nguyen", "gu-cli");
|
|
67
|
+
const version = fs.readVersion(dir);
|
|
68
|
+
// nvm-windows: d is a JUNCTION (C:\nvm4w\nodejs → %LOCALAPPDATA%\nvm\v22.x). The running gu's
|
|
69
|
+
// own folder is the resolved target, so keep both spellings to compare against.
|
|
70
|
+
let realDir = dir;
|
|
71
|
+
try { realDir = (version && fs.realpath(dir)) || dir; } catch { /* keep dir */ }
|
|
72
|
+
return { bin: shim, dir, realDir, version, prefix: version ? d : "" };
|
|
73
|
+
}
|
|
74
|
+
const bin = P.join(d, "gu");
|
|
75
|
+
if (!fs.exists(bin)) continue;
|
|
76
|
+
let real;
|
|
77
|
+
try { real = fs.realpath(bin); } catch { continue; }
|
|
78
|
+
const dir = P.dirname(real);
|
|
79
|
+
const m = /^(.*)\/lib\/node_modules\/@tiens\.nguyen\/gu-cli$/.exec(dir);
|
|
80
|
+
return { bin, dir, realDir: dir, version: fs.readVersion(dir), prefix: m ? (m[1] || "/") : "" };
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** EACCES / EPERM: the prefix is not writable by this user (root-owned /usr on Ubuntu). */
|
|
86
|
+
const isPermission = (reason) => /\b(EACCES|permission denied)\b/i.test(String(reason ?? ""));
|
|
87
|
+
/** EPERM / EBUSY on Windows: the running gu's files are locked. */
|
|
88
|
+
const isLocked = (reason) => /\b(EPERM|EBUSY|operation not permitted|resource busy)\b/i.test(String(reason ?? ""));
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The command that puts the latest gu into the prefix owning the gu on PATH.
|
|
92
|
+
* `reason` (the last npm failure) picks the platform's fix: `sudo` for EACCES on Linux/macOS when
|
|
93
|
+
* the prefix is outside the user's home (never for nvm / ~/.npm-global — sudo there makes it worse).
|
|
94
|
+
*/
|
|
95
|
+
export function fixCommandFor(onPath, { platform = process.platform, reason = "", home = "" } = {}) {
|
|
96
|
+
const win = platform === "win32";
|
|
97
|
+
const base = `npm install -g ${PKG}@latest`;
|
|
98
|
+
const bad = win ? /["%`\n]/ : /["$`\\\n]/;
|
|
99
|
+
const cmd = onPath?.prefix && !bad.test(onPath.prefix) ? `${base} --prefix "${onPath.prefix}"` : base;
|
|
100
|
+
if (!win && isPermission(reason)) {
|
|
101
|
+
const inHome = home && onPath?.prefix && (onPath.prefix === home || onPath.prefix.startsWith(home + "/"));
|
|
102
|
+
if (!inHome) return `sudo ${cmd}`;
|
|
103
|
+
}
|
|
104
|
+
return cmd;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Is this gu from nvm-windows? Its PATH folder is %NVM_SYMLINK% (C:\nvm4w\nodejs by default), a
|
|
109
|
+
* junction into %NVM_HOME%\v<version>. Two things differ from a normal install: every Node version
|
|
110
|
+
* has its OWN global packages (gu included), and `nvm use` re-points that junction for EVERY open
|
|
111
|
+
* terminal at once — so switching Node makes a different (or missing) gu start next time.
|
|
112
|
+
*/
|
|
113
|
+
export function isNvmWindows(onPath, env = {}) {
|
|
114
|
+
if (!onPath) return false;
|
|
115
|
+
const norm = (s) => String(s ?? "").toLowerCase().replace(/\//g, "\\").replace(/\\+$/, "");
|
|
116
|
+
const s = `${norm(onPath.bin)}\n${norm(onPath.realDir)}`;
|
|
117
|
+
const link = norm(env.NVM_SYMLINK), home = norm(env.NVM_HOME);
|
|
118
|
+
return Boolean((link && s.includes(link + "\\")) || (home && s.includes(home + "\\")) ||
|
|
119
|
+
/\\nvm4w\\|\\nvm\\v\d/.test(s));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const NVM_NOTE = "nvm-windows keeps a separate gu for each Node version, and `nvm use` switches EVERY terminal — " +
|
|
123
|
+
"after switching Node, install gu again for that version.";
|
|
124
|
+
|
|
125
|
+
/** What to do after running the fix command, per platform. */
|
|
126
|
+
export function fixFollowUp({ platform = process.platform, reason = "", nvm = false } = {}) {
|
|
127
|
+
if (platform === "win32") {
|
|
128
|
+
const base = isLocked(reason) || !reason
|
|
129
|
+
? "Close EVERY gu window first (Windows can't replace files in use), run that in a new terminal, then start gu again."
|
|
130
|
+
: "Run that in a new terminal (close gu first), then start gu again.";
|
|
131
|
+
return nvm ? `${base} ${NVM_NOTE}` : base;
|
|
132
|
+
}
|
|
133
|
+
return "Run that in a terminal, then start gu again.";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const sameDir = (a, b, platform) =>
|
|
137
|
+
platform === "win32" ? String(a).toLowerCase().replace(/[\\/]+$/, "") === String(b).toLowerCase().replace(/[\\/]+$/, "")
|
|
138
|
+
: a === b;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* After an install into `installedDir`: will the next `gu` start `wanted` (or newer)?
|
|
142
|
+
* → null when yes (or unknowable), otherwise the reason + the exact fix.
|
|
143
|
+
*/
|
|
144
|
+
/** Will the next `gu` start `wanted` or newer? (What an update is really for.) */
|
|
145
|
+
export function guOnPathIsCurrent({ wanted = "", onPath = null } = {}) {
|
|
146
|
+
return Boolean(wanted && onPath?.version && compareVersions(onPath.version, wanted) >= 0);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function pathGuMismatch({ wanted = "", installedDir = "", onPath = null, platform = process.platform, env = {} } = {}) {
|
|
150
|
+
if (!wanted || !onPath || !onPath.version) return null;
|
|
151
|
+
if (compareVersions(onPath.version, wanted) >= 0) return null;
|
|
152
|
+
const elsewhere = installedDir && !sameDir(installedDir, onPath.dir, platform) &&
|
|
153
|
+
!sameDir(installedDir, onPath.realDir ?? onPath.dir, platform);
|
|
154
|
+
const where = elsewhere ? `installed to ${installedDir}, but ` : "";
|
|
155
|
+
const profile = platform !== "win32"
|
|
156
|
+
? "remove the older folder from PATH in your shell profile"
|
|
157
|
+
: isNvmWindows(onPath, env)
|
|
158
|
+
? `run \`nvm use\` for the Node version you updated gu in. ${NVM_NOTE}`
|
|
159
|
+
: "remove the older folder from Path in Settings › System › About › Advanced system settings › Environment Variables";
|
|
160
|
+
return `${where}the gu on your PATH is ${onPath.bin} (v${onPath.version}) — the next \`gu\` would still be ` +
|
|
161
|
+
`v${onPath.version}. Fix: ${fixCommandFor(onPath, { platform })} (or ${profile})`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The record saved after an update attempt. */
|
|
165
|
+
export function updateAttemptRecord({ version = "", running = "", ok = false, reason = "", at = Date.now() } = {}) {
|
|
166
|
+
return { version, running, ok: Boolean(ok), reason: String(reason ?? ""), at };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* At startup, an update `cli` is available while `running` is old. If we ALREADY tried exactly
|
|
171
|
+
* this update from exactly this version, offering it again is the loop the user hit — say what
|
|
172
|
+
* happened instead. → notice lines like updateNotice ([] = nothing remembered, show the normal one).
|
|
173
|
+
*/
|
|
174
|
+
export function repeatUpdateNotice({ last = null, cli = "", running = "", onPath = null,
|
|
175
|
+
platform = process.platform, home = "", env = {} } = {}) {
|
|
176
|
+
if (!last || !cli || !running) return [];
|
|
177
|
+
if (last.version !== cli || last.running !== running) return [];
|
|
178
|
+
if (compareVersions(running, cli) >= 0) return [];
|
|
179
|
+
const head = last.ok
|
|
180
|
+
? `gu ${cli} was installed, but this gu is still ${running} — the update did NOT take effect`
|
|
181
|
+
: `gu ${cli} update did NOT succeed last time`;
|
|
182
|
+
const why = last.ok
|
|
183
|
+
? (onPath && onPath.version && compareVersions(onPath.version, cli) < 0
|
|
184
|
+
? `the gu on your PATH is ${onPath.bin} (v${onPath.version}), a different copy from the one updated`
|
|
185
|
+
: "a different gu copy than the one updated is starting")
|
|
186
|
+
: (last.reason || "unknown error");
|
|
187
|
+
const reason = last.ok ? "" : last.reason;
|
|
188
|
+
return [
|
|
189
|
+
{ kind: "head", text: head },
|
|
190
|
+
{ kind: "note", text: `Reason: ${why}` },
|
|
191
|
+
{ kind: "cmd", text: fixCommandFor(onPath, { platform, reason, home }) },
|
|
192
|
+
{ kind: "note", text: fixFollowUp({ platform, reason, nvm: platform === "win32" && isNvmWindows(onPath, env) }) },
|
|
193
|
+
];
|
|
194
|
+
}
|