@tiens.nguyen/gu-cli 1.0.789 → 1.0.791
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/client-expert-run.mjs +274 -47
- package/client-expert-setup.mjs +56 -7
- package/gonext_agent_chat.py +14 -252
- package/gu-cli.mjs +16 -3
- package/gu-repl.mjs +73 -311
- package/mongo-native.mjs +27 -2
- package/package.json +1 -2
- package/terminal-layout.mjs +2 -171
- package/clear-logs.mjs +0 -352
package/gu-repl.mjs
CHANGED
|
@@ -63,11 +63,6 @@ import {
|
|
|
63
63
|
ragSegmentLabel,
|
|
64
64
|
searchSegmentLabel,
|
|
65
65
|
runningDetailRows,
|
|
66
|
-
runningCommandLabel,
|
|
67
|
-
runningCommandRows,
|
|
68
|
-
dropRestatement,
|
|
69
|
-
isRestatement,
|
|
70
|
-
liveThoughtText,
|
|
71
66
|
formatThinkTime,
|
|
72
67
|
plainAnswer,
|
|
73
68
|
summarizeError,
|
|
@@ -133,7 +128,6 @@ import {
|
|
|
133
128
|
} from "./terminal-layout.mjs";
|
|
134
129
|
import { completedFlush, splitLiveStream } from "./completed-flush.mjs";
|
|
135
130
|
import { isScaffoldStep, isGroupableStep, stepGroupRows, relativizePaths } from "./step-group.mjs";
|
|
136
|
-
import { clearLogsNoticeText } from "./clear-logs.mjs";
|
|
137
131
|
import { discoverSkills, matchSkillCommand } from "./skills.mjs";
|
|
138
132
|
import { appendNew, archivePathFor, archivedWorkspaceKey, markReset, seedArchive } from "./conversation-archive.mjs";
|
|
139
133
|
import { addAllowRule, listServers as listMcpServers } from "./mcp-config.mjs";
|
|
@@ -897,7 +891,6 @@ const COMMANDS = [
|
|
|
897
891
|
{ name: "/use-presets", usage: "/use-presets [name]", desc: "pick a saved preset and apply it — shows what changes, backs up your setup, asks first" },
|
|
898
892
|
{ name: "/doctor", desc: "check every configured model ANSWERS — especially the embedder, which a turn only reaches after several steps" },
|
|
899
893
|
{ name: "/tokens", desc: "break down the last prompt sent to the coding model — how much of it was RAG" },
|
|
900
|
-
{ 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" },
|
|
901
894
|
{ 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" },
|
|
902
895
|
// ONE name, /new (user, #278). /reset and /clear still work (HIDDEN_COMMANDS) for habit and
|
|
903
896
|
// for Claude Code users — not to be confused with `gu clear` in a shell, which wipes gu's state.
|
|
@@ -1469,11 +1462,6 @@ function applyClick({ row, col }) {
|
|
|
1469
1462
|
void runPricesUpdate();
|
|
1470
1463
|
return;
|
|
1471
1464
|
}
|
|
1472
|
-
if (logsRowTarget && row === updateRowAbs &&
|
|
1473
|
-
col - 1 >= logsRowTarget.from && col - 1 < logsRowTarget.to) {
|
|
1474
|
-
void runClearLogs();
|
|
1475
|
-
return;
|
|
1476
|
-
}
|
|
1477
1465
|
if (agentsRowTarget && row === updateRowAbs &&
|
|
1478
1466
|
col - 1 >= agentsRowTarget.from && col - 1 < agentsRowTarget.to) {
|
|
1479
1467
|
void showOtherAgents(); // async now — it offers stop / switch-to (task #211)
|
|
@@ -2692,9 +2680,6 @@ function zoneNotice(width) {
|
|
|
2692
2680
|
if (tokenNotice?.text) items.push({ key: "token", paint: tokenNotice.urgent ? yellow : dim, text: tokenNotice.text });
|
|
2693
2681
|
const agents = agentsIndicator(otherAgents);
|
|
2694
2682
|
if (agents) items.push({ key: "agents", clickable: true, paint: accent, text: `${agents} running (click)` });
|
|
2695
|
-
// LAST, so it is the first to give way on a narrow window: housekeeping, never urgent (#329).
|
|
2696
|
-
const logs = clearLogsNoticeText(logsBytes);
|
|
2697
|
-
if (logs) items.push({ key: "logs", clickable: true, paint: dim, text: logs });
|
|
2698
2683
|
return noticeRow(items, { width, gutter: GUTTER });
|
|
2699
2684
|
}
|
|
2700
2685
|
|
|
@@ -2775,7 +2760,6 @@ function frozenZone() {
|
|
|
2775
2760
|
updateRowTarget = notice?.spans.update ?? null;
|
|
2776
2761
|
agentsRowTarget = notice?.spans.agents ?? null;
|
|
2777
2762
|
pricesRowTarget = notice?.spans.prices ?? null;
|
|
2778
|
-
logsRowTarget = notice?.spans.logs ?? null;
|
|
2779
2763
|
jumpSpan = notice?.spans.jump ?? null;
|
|
2780
2764
|
return { ...zone, rows, statusRows: sep, noticeRow: Boolean(notice) };
|
|
2781
2765
|
}
|
|
@@ -2845,7 +2829,7 @@ function paintBar() {
|
|
|
2845
2829
|
// THE NOTICE ROW is the one directly above the separator (#318) — never the separator itself.
|
|
2846
2830
|
const noticeAbs = zone.noticeRow ? sepAbs - 1 : 0;
|
|
2847
2831
|
jumpRowTarget = scrolledBack() && jumpSpan ? { row: noticeAbs, ...jumpSpan } : null;
|
|
2848
|
-
updateRowAbs = updateRowTarget || agentsRowTarget || pricesRowTarget
|
|
2832
|
+
updateRowAbs = updateRowTarget || agentsRowTarget || pricesRowTarget ? noticeAbs : 0;
|
|
2849
2833
|
// preserveCaret:false — the save slot holds the OUTPUT position (see regionUi), and the caret
|
|
2850
2834
|
// is parked on the input line right after, so there is nothing to preserve here.
|
|
2851
2835
|
ui(
|
|
@@ -3007,8 +2991,6 @@ let updateRowAbs = 0;
|
|
|
3007
2991
|
let agentsRowTarget = null;
|
|
3008
2992
|
// …and "new prices updated <date>" (#323).
|
|
3009
2993
|
let pricesRowTarget = null;
|
|
3010
|
-
// …and "clear logs · 175 MB" (#329), shown only once the logs are worth a row.
|
|
3011
|
-
let logsRowTarget = null;
|
|
3012
2994
|
// A terminal left with a scrolling region is a broken shell — a hard guarantee, like the
|
|
3013
2995
|
// bracketed-paste and mouse-reporting teardowns.
|
|
3014
2996
|
process.on("exit", () => {
|
|
@@ -3319,33 +3301,7 @@ const paintThoughtRows = (block, hovered, collapsed = block.collapsed) => {
|
|
|
3319
3301
|
collapsed,
|
|
3320
3302
|
lines: THOUGHT_COLLAPSED_LINES,
|
|
3321
3303
|
});
|
|
3322
|
-
return
|
|
3323
|
-
};
|
|
3324
|
-
|
|
3325
|
-
/**
|
|
3326
|
-
* A THOUGHT HAS ITS OWN MARK AND ITS OWN ROOM (task #334).
|
|
3327
|
-
*
|
|
3328
|
-
* Reported with a screenshot: "● listed files · read file 7×" and, flush under it in the same
|
|
3329
|
-
* grey at the same indent, "The user wants me to use the .env file…" — which read as the rest of
|
|
3330
|
-
* the bullet. Two things were missing, both since #326 made the look-ups ONE row that stays where
|
|
3331
|
-
* it is while thoughts print under it (before that a blank or the question always sat above):
|
|
3332
|
-
*
|
|
3333
|
-
* THE MARK — a dim rule down the margin, on every row of the block, so two or twenty rows read
|
|
3334
|
-
* as one thing and never as an action ("●" means something was DONE). It takes the gutter's own
|
|
3335
|
-
* three cells, so no row gets wider and nothing that measures rows changes.
|
|
3336
|
-
*
|
|
3337
|
-
* THE BLANK ROW — owned by the BLOCK (`lead`), not written beside it. The open group's row is
|
|
3338
|
-
* rewritten in place and everything under it is found by row arithmetic that expects the blocks
|
|
3339
|
-
* to touch (stepGroupStillOpen, the merge below, the shift when a block opens); a blank that
|
|
3340
|
-
* belonged to nobody would break each of those. As the block's first row it moves, repaints
|
|
3341
|
-
* and is replaced with the block.
|
|
3342
|
-
*/
|
|
3343
|
-
const THOUGHT_GUTTER = " " + dim("│") + " ";
|
|
3344
|
-
|
|
3345
|
-
/** Is the last finished line of the transcript empty (so a block printed now needs no blank)? */
|
|
3346
|
-
const transcriptEndsBlank = () => {
|
|
3347
|
-
const lines = transcript.lines();
|
|
3348
|
-
return lines.length < 2 || stripAnsiEscapes(lines[lines.length - 2] ?? "").trim() === "";
|
|
3304
|
+
return rows.map((r) => GUTTER + paint(r));
|
|
3349
3305
|
};
|
|
3350
3306
|
|
|
3351
3307
|
/** Width available to a thought row: the screen, less the gutter it hangs in. */
|
|
@@ -3361,9 +3317,7 @@ const thoughtWidth = () =>
|
|
|
3361
3317
|
*/
|
|
3362
3318
|
function printThoughtBlock(raw, before) {
|
|
3363
3319
|
if (!sessionShowThought) return false;
|
|
3364
|
-
|
|
3365
|
-
// directly under the question, and it was all the two collapsed rows had room for.
|
|
3366
|
-
const prose = dropRestatement(thoughtProse(raw), turnQuestionText);
|
|
3320
|
+
const prose = thoughtProse(raw);
|
|
3367
3321
|
if (!prose) return false; // a bare tool label or pure code — not what anyone means by a thought
|
|
3368
3322
|
const rows = thoughtBlockRows(prose, {
|
|
3369
3323
|
width: thoughtWidth(),
|
|
@@ -3390,7 +3344,12 @@ function printThoughtBlock(raw, before) {
|
|
|
3390
3344
|
const prev = thoughtBlocks[thoughtBlocks.length - 1];
|
|
3391
3345
|
if (prev && !prev.kind && prev.collapsed && prev.to === transcript.dropped() + transcript.length() - 2) {
|
|
3392
3346
|
const merged = prev.prose.replace(/\s+$/, "") + " " + prose;
|
|
3393
|
-
const
|
|
3347
|
+
const grown = thoughtBlockRows(merged, {
|
|
3348
|
+
width: thoughtWidth(),
|
|
3349
|
+
collapsed: true,
|
|
3350
|
+
lines: THOUGHT_COLLAPSED_LINES,
|
|
3351
|
+
});
|
|
3352
|
+
const newEnd = transcript.replaceRange(prev.from, prev.to, grown.map((r) => GUTTER + dim(r)));
|
|
3394
3353
|
if (newEnd !== null) {
|
|
3395
3354
|
prev.prose = merged;
|
|
3396
3355
|
prev.to = newEnd;
|
|
@@ -3403,16 +3362,11 @@ function printThoughtBlock(raw, before) {
|
|
|
3403
3362
|
}
|
|
3404
3363
|
// Scrolled out of the buffer — fall through and print a new block rather than lose it.
|
|
3405
3364
|
}
|
|
3406
|
-
|
|
3407
|
-
const block = { from: 0, to: 0, prose, collapsed: true, lead: !transcriptEndsBlank() };
|
|
3408
|
-
const painted = paintThoughtRows(block, false);
|
|
3409
|
-
process.stdout.write(painted.join("\n") + "\n");
|
|
3365
|
+
process.stdout.write(rows.map((r) => GUTTER + dim(r)).join("\n") + "\n");
|
|
3410
3366
|
// Record where it landed. Measured AFTER the write so a trim during it cannot skew the
|
|
3411
3367
|
// range: the last completed line is length-2 (the final entry is the still-open line).
|
|
3412
3368
|
const endAbs = transcript.dropped() + transcript.length() - 2;
|
|
3413
|
-
|
|
3414
|
-
block.to = endAbs;
|
|
3415
|
-
thoughtBlocks.push(block);
|
|
3369
|
+
thoughtBlocks.push({ from: endAbs - rows.length + 1, to: endAbs, prose, collapsed: true });
|
|
3416
3370
|
if (thoughtBlocks.length > 200) thoughtBlocks = thoughtBlocks.slice(-200);
|
|
3417
3371
|
return true;
|
|
3418
3372
|
}
|
|
@@ -7925,128 +7879,6 @@ async function runPricesUpdate() {
|
|
|
7925
7879
|
}
|
|
7926
7880
|
}
|
|
7927
7881
|
|
|
7928
|
-
// ---------------------------------------------------------------------------------------------
|
|
7929
|
-
// "CLEAR LOGS" (#329): a quiet item at the right of the notice row once gu's logs are worth a row,
|
|
7930
|
-
// and /clear-logs for where there is no mouse. A click deletes nothing: it MEASURES — the logs,
|
|
7931
|
-
// and beside them the two things that are actually big, the RAG indexes and the downloaded API
|
|
7932
|
-
// versions — shows that with a total, then asks about each. What each "clear" may and may not
|
|
7933
|
-
// touch is decided in clear-logs.mjs; this is the terminal half only.
|
|
7934
|
-
// ---------------------------------------------------------------------------------------------
|
|
7935
|
-
let logsBytes = 0; // what the notice shows; only the logs are measured for it (a handful of stats)
|
|
7936
|
-
let clearLogsBusy = false;
|
|
7937
|
-
|
|
7938
|
-
async function refreshLogsSize() {
|
|
7939
|
-
try {
|
|
7940
|
-
const { surveyLogs } = await import("./clear-logs.mjs");
|
|
7941
|
-
const was = clearLogsNoticeText(logsBytes);
|
|
7942
|
-
logsBytes = (await surveyLogs()).bytes;
|
|
7943
|
-
if (clearLogsNoticeText(logsBytes) !== was && barPinned()) paintBar();
|
|
7944
|
-
} catch {
|
|
7945
|
-
/* a housekeeping notice is never worth an error */
|
|
7946
|
-
}
|
|
7947
|
-
}
|
|
7948
|
-
|
|
7949
|
-
async function runClearLogs() {
|
|
7950
|
-
// ONE AT A TIME, and never over a turn or another picker — reached from a fire-and-forget click
|
|
7951
|
-
// (see showOtherAgents for what a second picker over the first one does).
|
|
7952
|
-
if (clearLogsBusy || listPickerActive || approvalActive || following) return;
|
|
7953
|
-
clearLogsBusy = true;
|
|
7954
|
-
try {
|
|
7955
|
-
const cl = await import("./clear-logs.mjs");
|
|
7956
|
-
const { LOCAL_API_PORT } = await import("./client-expert-api.mjs");
|
|
7957
|
-
const width = Math.max(20, process.stdout.columns || 80);
|
|
7958
|
-
console.log("\n" + GUTTER + dim("Measuring ~/.gonext…"));
|
|
7959
|
-
// Which API version is ANSWERING decides what "old" means — ask it, do not infer it.
|
|
7960
|
-
const runningVersion = await fetch(`http://127.0.0.1:${LOCAL_API_PORT}/api/health`, { signal: AbortSignal.timeout(2000) })
|
|
7961
|
-
.then((r) => r.json())
|
|
7962
|
-
.then((j) => String(j?.version ?? ""))
|
|
7963
|
-
.catch(() => "");
|
|
7964
|
-
const survey = await cl.surveyDisk({ runningVersion });
|
|
7965
|
-
console.log("");
|
|
7966
|
-
for (const row of cl.summaryRows(survey, { width: width - GUTTER.length - 1 })) {
|
|
7967
|
-
console.log(GUTTER + (row.startsWith("Total") ? bold(row) : row));
|
|
7968
|
-
}
|
|
7969
|
-
const freed = [];
|
|
7970
|
-
// A question never wraps: at 80 columns over SSH the picker hint goes on a row of its own.
|
|
7971
|
-
const question = (q) => {
|
|
7972
|
-
const hint = `(${PICK_HINT}):`;
|
|
7973
|
-
console.log("");
|
|
7974
|
-
if (2 + q.length + 2 + hint.length < width) console.log(dim(` ${q} ${hint}`));
|
|
7975
|
-
else console.log(dim(` ${q}\n ${hint}`));
|
|
7976
|
-
};
|
|
7977
|
-
const cancelled = () => {
|
|
7978
|
-
console.log(dim(` (stopped — nothing ${freed.length ? "further " : ""}removed)\n`));
|
|
7979
|
-
paintBar();
|
|
7980
|
-
};
|
|
7981
|
-
|
|
7982
|
-
// 1. THE LOGS — what was asked for, so Yes is the first row.
|
|
7983
|
-
if (survey.logs.bytes > 0) {
|
|
7984
|
-
question(`Clear the logs — ${cl.humanSize(survey.logs.bytes)}?`);
|
|
7985
|
-
const go = await pickFromList(["Yes — clear the logs" + dim(" · the worker and the API keep running"), "No"], 0);
|
|
7986
|
-
if (go < 0) return cancelled();
|
|
7987
|
-
if (go === 0) {
|
|
7988
|
-
const r = await cl.clearLogs(survey.logs);
|
|
7989
|
-
freed.push({ what: "logs", ...r });
|
|
7990
|
-
console.log(GUTTER + accent(`✓ logs cleared — ${cl.humanSize(r.freed)} freed`) +
|
|
7991
|
-
(r.failed.length ? yellow(` (still in use, left alone: ${r.failed.join(", ")})`) : ""));
|
|
7992
|
-
} else {
|
|
7993
|
-
console.log(GUTTER + dim("logs kept"));
|
|
7994
|
-
}
|
|
7995
|
-
} else {
|
|
7996
|
-
console.log("\n" + GUTTER + dim("No logs to clear."));
|
|
7997
|
-
}
|
|
7998
|
-
|
|
7999
|
-
// 2. RAG — costs a re-index, so No is the first row.
|
|
8000
|
-
if (survey.rag.indexes.length) {
|
|
8001
|
-
const gone = survey.rag.indexes.filter((i) => i.missing);
|
|
8002
|
-
const goneBytes = gone.reduce((n, i) => n + i.bytes, 0);
|
|
8003
|
-
const choices = [{ id: "no", label: "No — keep them" }];
|
|
8004
|
-
if (gone.length && gone.length < survey.rag.indexes.length) {
|
|
8005
|
-
choices.push({ id: "gone", label: `Only the ${gone.length} whose folder no longer exists — ${cl.humanSize(goneBytes)}` });
|
|
8006
|
-
}
|
|
8007
|
-
choices.push({ id: "all", label: `Yes — all ${survey.rag.indexes.length}` + dim(" · a folder is indexed again when it is next used") });
|
|
8008
|
-
question(`Also clear the RAG indexes — ${cl.humanSize(survey.rag.bytes)}?`);
|
|
8009
|
-
const pick = await pickFromList(choices.map((c) => c.label), 0);
|
|
8010
|
-
if (pick < 0) return cancelled();
|
|
8011
|
-
if (choices[pick].id !== "no") {
|
|
8012
|
-
const r = await cl.clearRagIndexes(choices[pick].id === "gone" ? gone : survey.rag.indexes);
|
|
8013
|
-
freed.push({ what: "rag", ...r });
|
|
8014
|
-
console.log(GUTTER + accent(`✓ ${r.removed} RAG index${r.removed === 1 ? "" : "es"} cleared — ${cl.humanSize(r.freed)} freed`) +
|
|
8015
|
-
(r.failed.length ? yellow(` (could not remove: ${r.failed.join(", ")})`) : ""));
|
|
8016
|
-
} else {
|
|
8017
|
-
console.log(GUTTER + dim("RAG indexes kept"));
|
|
8018
|
-
}
|
|
8019
|
-
}
|
|
8020
|
-
|
|
8021
|
-
// 3. THE API — only ever the OLDER versions; the one in use is not on offer.
|
|
8022
|
-
if (survey.api.old.length) {
|
|
8023
|
-
const n = survey.api.old.length;
|
|
8024
|
-
question(`Also remove the ${n} older API version${n === 1 ? "" : "s"} — ${cl.humanSize(survey.api.oldBytes)}?`);
|
|
8025
|
-
const go = await pickFromList(["No — keep them", `Yes — remove the ${n} older one${n === 1 ? "" : "s"}` +
|
|
8026
|
-
dim(survey.api.inUse ? ` · ${survey.api.inUse}, the one in use, stays` : "")], 0);
|
|
8027
|
-
if (go < 0) return cancelled();
|
|
8028
|
-
if (go === 1) {
|
|
8029
|
-
console.log(GUTTER + dim("Removing…"));
|
|
8030
|
-
const r = await cl.clearOldApiVersions(survey.api.old);
|
|
8031
|
-
freed.push({ what: "api", ...r });
|
|
8032
|
-
console.log(GUTTER + accent(`✓ ${r.removed} older API version${r.removed === 1 ? "" : "s"} removed — ${cl.humanSize(r.freed)} freed`) +
|
|
8033
|
-
(r.failed.length ? yellow(` (could not remove: ${r.failed.join(", ")})`) : ""));
|
|
8034
|
-
} else {
|
|
8035
|
-
console.log(GUTTER + dim("older API versions kept"));
|
|
8036
|
-
}
|
|
8037
|
-
}
|
|
8038
|
-
|
|
8039
|
-
const total = freed.reduce((n, f) => n + f.freed, 0);
|
|
8040
|
-
console.log("\n" + GUTTER + (freed.length ? bold(`${cl.humanSize(total)} freed in all.`) : dim("Nothing removed.")) + "\n");
|
|
8041
|
-
} catch (e) {
|
|
8042
|
-
console.log(GUTTER + red(`couldn't finish — ${e?.message ?? e}`) + "\n");
|
|
8043
|
-
} finally {
|
|
8044
|
-
clearLogsBusy = false;
|
|
8045
|
-
await refreshLogsSize();
|
|
8046
|
-
if (barPinned()) paintBar();
|
|
8047
|
-
}
|
|
8048
|
-
}
|
|
8049
|
-
|
|
8050
7882
|
/**
|
|
8051
7883
|
* `/embed-model` — switch the RAG embedder, the embed server, or the embed key (task #169).
|
|
8052
7884
|
*
|
|
@@ -9876,10 +9708,6 @@ const RAGSTATE_PREFIX = "__RAGSTATE__::";
|
|
|
9876
9708
|
// The tool ABOUT to run: "<label>\t<call>" (task #177). Swallowed from scrollback like
|
|
9877
9709
|
// "Running → …" — it belongs to the live block, and the finished bullet replaces it.
|
|
9878
9710
|
const RUNTOOL_PREFIX = "__RUNTOOL__::";
|
|
9879
|
-
// The command that is running, WHOLE (task #331): one line of JSON {cmd}, sent beside the
|
|
9880
|
-
// 70-character "Running → …" line — and how many lines it has printed so far. Both swallowed.
|
|
9881
|
-
const RUNCMD_PREFIX = "__RUNCMD__::";
|
|
9882
|
-
const RUNOUT_PREFIX = "__RUNOUT__::";
|
|
9883
9711
|
const MAXSTEP_PREFIX = "__MAXSTEP__::";
|
|
9884
9712
|
const TIMEOUT_PREFIX = "__TIMEOUT__::";
|
|
9885
9713
|
const COMPACT_PREFIX = "__COMPACT__::";
|
|
@@ -10796,25 +10624,7 @@ async function askTokenAtStart() {
|
|
|
10796
10624
|
/** The token refresh started at launch — askTokenAtStart waits for it (see there). */
|
|
10797
10625
|
let startupTokenRefresh = null;
|
|
10798
10626
|
|
|
10799
|
-
/**
|
|
10800
|
-
* The fuller live thought (what the status row is driven from once it is longer than the row),
|
|
10801
|
-
* held to the same rule as the short one (#333): from its first real sentence, without the
|
|
10802
|
-
* question or gu's own instructions said back — "" while that is all there is so far.
|
|
10803
|
-
*/
|
|
10804
|
-
const liveThoughtClean = (text) => {
|
|
10805
|
-
const kept = dropRestatement(text, turnQuestionText);
|
|
10806
|
-
return isRestatement(kept, turnQuestionText) ? "" : liveThoughtText(kept);
|
|
10807
|
-
};
|
|
10808
|
-
|
|
10809
|
-
// The question the running turn is answering — what a thought is compared with so that the
|
|
10810
|
-
// question said back is not printed under the question (#333). Display only.
|
|
10811
|
-
let turnQuestionText = "";
|
|
10812
|
-
|
|
10813
10627
|
async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {}) {
|
|
10814
|
-
{
|
|
10815
|
-
const asked = [...(Array.isArray(history) ? history : [])].reverse().find((m) => m?.role === "user");
|
|
10816
|
-
turnQuestionText = typeof asked?.content === "string" ? asked.content : "";
|
|
10817
|
-
}
|
|
10818
10628
|
// #238: when the licence is ENFORCED and this account is not entitled, no turn is sent. Decided at
|
|
10819
10629
|
// startup (see licenceBlock) — never re-checked here, so a turn cannot die halfway through.
|
|
10820
10630
|
if (licenceBlock) {
|
|
@@ -11032,12 +10842,6 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
|
|
|
11032
10842
|
let lastWordBucket = -1; // 12s window index → rotate the word once per window (task: 30-spinner)
|
|
11033
10843
|
let almostDone = false; // set from the worker's "…almost completed thinking…" heartbeat
|
|
11034
10844
|
let runningCmd = null; // the command currently executing (from a "Running → …" event)
|
|
11035
|
-
// …the same command WHOLE, what it has printed so far, when it started, and whether the status
|
|
11036
|
-
// row is hiding some of it (i.e. a click has something to open) — task #331.
|
|
11037
|
-
let runningCmdFull = "";
|
|
11038
|
-
let runningCmdLines = 0;
|
|
11039
|
-
let runningCmdAt = 0;
|
|
11040
|
-
let runningCmdMore = false;
|
|
11041
10845
|
let runningTool = null; // { label, detail } for the tool executing right now (task #177)
|
|
11042
10846
|
// The model's CURRENT Thought (task #64): while it streams its reasoning inside the
|
|
11043
10847
|
// hidden ~~~ fence, show a one-clause summary on the blinking line so a 300s CPU-bound
|
|
@@ -11057,28 +10861,6 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
|
|
|
11057
10861
|
// became of it. A no-code retry notice discards it (the model restates the plan on the retry);
|
|
11058
10862
|
// anything else prints it. Always null under --verbose, which shows everything as it comes.
|
|
11059
10863
|
let heldThought = null;
|
|
11060
|
-
// A THOUGHT INTERRUPTED BY THE HEARTBEAT IS STILL ONE THOUGHT (#333). The worker's
|
|
11061
|
-
// "…almost completed thinking… (180s)" is a step, and a step closes the stream fence — in the
|
|
11062
|
-
// MIDDLE of the model's sentence. The fence then reopens and the rest of the sentence arrived
|
|
11063
|
-
// as a new thought: the live line read `one, call use_skill(name) FIRST…` and
|
|
11064
|
-
// `` `, `bootstrap-api-service.mjs` — add… `` (both from stored streams), and the history got
|
|
11065
|
-
// the thought in two halves. So a closing fence is only PARKED here: if nothing but blanks and
|
|
11066
|
-
// heartbeats follow and the fence opens again, the thought carries on where it stopped;
|
|
11067
|
-
// anything else finishes it exactly as before.
|
|
11068
|
-
let pausedFence = null; // { step, fence } while a fence is closed but not yet finished
|
|
11069
|
-
const finishPausedFence = () => {
|
|
11070
|
-
if (!pausedFence) return;
|
|
11071
|
-
const step = pausedFence.step;
|
|
11072
|
-
pausedFence = null;
|
|
11073
|
-
// The step's model output is complete. Print its reasoning HERE, before the tools
|
|
11074
|
-
// report what they did, so the transcript reads in the order it happened: the model
|
|
11075
|
-
// says what it intends, then the actions it took appear under it.
|
|
11076
|
-
if (!REPL_VERBOSE && step.trim() && !stepHasCode(step)) {
|
|
11077
|
-
heldThought = step; // #45: decided by the next line — see releaseHeldThought
|
|
11078
|
-
} else if (printThoughtBlock(step, onContent)) {
|
|
11079
|
-
lastWasBlank = false;
|
|
11080
|
-
}
|
|
11081
|
-
};
|
|
11082
10864
|
const releaseHeldThought = () => {
|
|
11083
10865
|
if (heldThought === null) return;
|
|
11084
10866
|
const raw = heldThought;
|
|
@@ -11280,23 +11062,9 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
|
|
|
11280
11062
|
// to reveal). Line 1 (what's happening now): a live command > the model's current Thought >
|
|
11281
11063
|
// "almost done" > "Thinking". When clickable we drive line 1 from the SAME fuller string, so
|
|
11282
11064
|
// the grey expansion CONTINUES where line 1 was cut off instead of repeating the head.
|
|
11283
|
-
|
|
11284
|
-
// row, and a click opens the whole command underneath instead of a thought's remainder.
|
|
11285
|
-
const cmdView = runningCmd && !runningTool
|
|
11286
|
-
? runningCommandLabel(runningCmdFull || runningCmd, {
|
|
11287
|
-
room: max,
|
|
11288
|
-
secs: runningCmdAt ? Math.max(0, Math.round((now - runningCmdAt) / 1000)) : secs,
|
|
11289
|
-
lines: runningCmdLines,
|
|
11290
|
-
expanded: thoughtExpanded,
|
|
11291
|
-
})
|
|
11292
|
-
: null;
|
|
11293
|
-
runningCmdMore = Boolean(cmdView?.more);
|
|
11294
|
-
const clickable = cmdView ? cmdView.more : !!fullLiveThought && !runningCmd && fullLiveThought.length > max;
|
|
11065
|
+
const clickable = !!fullLiveThought && !runningCmd && fullLiveThought.length > max;
|
|
11295
11066
|
let label, rest;
|
|
11296
|
-
if (
|
|
11297
|
-
label = cmdView.label;
|
|
11298
|
-
rest = "";
|
|
11299
|
-
} else if (clickable) {
|
|
11067
|
+
if (clickable) {
|
|
11300
11068
|
// Split at a WORD boundary near the line-1 width so nothing is cut mid-word: the last
|
|
11301
11069
|
// space at/before max-1 (unless that snaps back too far into the line — then hard-cut).
|
|
11302
11070
|
// Line 1 shows the head; the grey expansion CONTINUES from the same point (no dup, no
|
|
@@ -11316,13 +11084,7 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
|
|
|
11316
11084
|
}
|
|
11317
11085
|
// When clicked, unfold the remainder as up to 4 wrapped GREY lines BETWEEN the thought and
|
|
11318
11086
|
// the playful-word line.
|
|
11319
|
-
const expLines =
|
|
11320
|
-
? thoughtExpanded
|
|
11321
|
-
? runningCommandRows(runningCmdFull || runningCmd, {
|
|
11322
|
-
width: Math.max(20, process.stdout.columns || 80) - 1 - GUTTER.length,
|
|
11323
|
-
})
|
|
11324
|
-
: []
|
|
11325
|
-
: thoughtExpanded && rest ? wrapThought(rest, max, 4) : [];
|
|
11087
|
+
const expLines = thoughtExpanded && rest ? wrapThought(rest, max, 4) : [];
|
|
11326
11088
|
// WHAT IS RUNNING, under the label (task #177). Capped at two rows and clipped, because
|
|
11327
11089
|
// this block is redrawn by walking the caret over PHYSICAL rows — a wrapped row here is
|
|
11328
11090
|
// the #109/#138 failure mode.
|
|
@@ -11453,14 +11215,14 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
|
|
|
11453
11215
|
if (row < top || row > top + statusContentLines - LEAD_ROWS - WORD_GAP - 2) return;
|
|
11454
11216
|
// Only meaningful when there IS a fuller thought to reveal; otherwise ignore the click
|
|
11455
11217
|
// so we don't flicker an empty expand.
|
|
11456
|
-
if (!thoughtExpanded && !(
|
|
11218
|
+
if (!thoughtExpanded && !(fullLiveThought && !runningCmd)) return;
|
|
11457
11219
|
toggleThought();
|
|
11458
11220
|
};
|
|
11459
11221
|
// Same expand/collapse, reachable without the mouse: Ctrl+T while a turn runs (see
|
|
11460
11222
|
// toggleActiveThought). Ignored when there's nothing more to reveal, so it never flickers an
|
|
11461
11223
|
// empty expansion.
|
|
11462
11224
|
const toggleThought = () => {
|
|
11463
|
-
if (!thoughtExpanded && !(
|
|
11225
|
+
if (!thoughtExpanded && !(fullLiveThought && !runningCmd)) return;
|
|
11464
11226
|
thoughtExpanded = !thoughtExpanded;
|
|
11465
11227
|
clearStatus(); // next tick (≤120ms) redraws at the new size; keeps cursor math correct
|
|
11466
11228
|
};
|
|
@@ -11496,9 +11258,6 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
|
|
|
11496
11258
|
clearStatus();
|
|
11497
11259
|
thinking = false;
|
|
11498
11260
|
runningCmd = null;
|
|
11499
|
-
runningCmdFull = "";
|
|
11500
|
-
runningCmdLines = 0;
|
|
11501
|
-
runningCmdAt = 0;
|
|
11502
11261
|
runningTool = null;
|
|
11503
11262
|
// A concrete action landed → the Thought that led to it is spent; clear it so the
|
|
11504
11263
|
// next step's blinking line starts from "Thinking", not last step's stale clause.
|
|
@@ -11622,9 +11381,6 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
|
|
|
11622
11381
|
almostDone = /almost/i.test(line);
|
|
11623
11382
|
continue;
|
|
11624
11383
|
}
|
|
11625
|
-
// A parked fence (see pausedFence) is finished by the first real line that is not the
|
|
11626
|
-
// fence opening again.
|
|
11627
|
-
if (pausedFence && line.trim() !== "" && !isFenceLine(line)) finishPausedFence();
|
|
11628
11384
|
// #45: retry notices and context-window bookkeeping are the agent's own machinery. Shown
|
|
11629
11385
|
// under --verbose only; otherwise swallowed, together with the no-code false start the
|
|
11630
11386
|
// retry notice is about.
|
|
@@ -11649,20 +11405,6 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
|
|
|
11649
11405
|
lastContentAt = Date.now();
|
|
11650
11406
|
continue;
|
|
11651
11407
|
}
|
|
11652
|
-
if (line.startsWith(RUNCMD_PREFIX)) {
|
|
11653
|
-
try {
|
|
11654
|
-
runningCmdFull = String(JSON.parse(line.slice(RUNCMD_PREFIX.length))?.cmd ?? "");
|
|
11655
|
-
} catch {
|
|
11656
|
-
runningCmdFull = ""; // the short "Running → …" line still names it
|
|
11657
|
-
}
|
|
11658
|
-
lastContentAt = Date.now();
|
|
11659
|
-
continue;
|
|
11660
|
-
}
|
|
11661
|
-
if (line.startsWith(RUNOUT_PREFIX)) {
|
|
11662
|
-
runningCmdLines = Number(line.slice(RUNOUT_PREFIX.length).trim()) || 0;
|
|
11663
|
-
lastContentAt = Date.now();
|
|
11664
|
-
continue;
|
|
11665
|
-
}
|
|
11666
11408
|
if (line.startsWith(RUNTOOL_PREFIX)) {
|
|
11667
11409
|
const [label, ...rest] = line.slice(RUNTOOL_PREFIX.length).split("\t");
|
|
11668
11410
|
runningTool = { label: (label || "Working").trim(), detail: rest.join("\t").trim() };
|
|
@@ -11680,19 +11422,17 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
|
|
|
11680
11422
|
if (isFenceLine(line)) {
|
|
11681
11423
|
inStreamFence = !inStreamFence;
|
|
11682
11424
|
if (inStreamFence) {
|
|
11683
|
-
|
|
11684
|
-
|
|
11685
|
-
// had none ("…When the TASK matches" / " one, call…"), so the two halves are joined
|
|
11686
|
-
// on the line they were cut in.
|
|
11687
|
-
stepThought = pausedFence.step.replace(/\n$/, "");
|
|
11688
|
-
fenceThought = pausedFence.fence.replace(/\n$/, "");
|
|
11689
|
-
pausedFence = null;
|
|
11690
|
-
} else {
|
|
11691
|
-
fenceThought = "";
|
|
11692
|
-
stepThought = "";
|
|
11693
|
-
}
|
|
11425
|
+
fenceThought = "";
|
|
11426
|
+
stepThought = "";
|
|
11694
11427
|
} else {
|
|
11695
|
-
|
|
11428
|
+
// The step's model output is complete. Print its reasoning HERE, before the tools
|
|
11429
|
+
// report what they did, so the transcript reads in the order it happened: the model
|
|
11430
|
+
// says what it intends, then the actions it took appear under it.
|
|
11431
|
+
if (!REPL_VERBOSE && stepThought.trim() && !stepHasCode(stepThought)) {
|
|
11432
|
+
heldThought = stepThought; // #45: decided by the next line — see releaseHeldThought
|
|
11433
|
+
} else if (printThoughtBlock(stepThought, onContent)) {
|
|
11434
|
+
lastWasBlank = false;
|
|
11435
|
+
}
|
|
11696
11436
|
stepThought = "";
|
|
11697
11437
|
}
|
|
11698
11438
|
continue;
|
|
@@ -11712,13 +11452,11 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
|
|
|
11712
11452
|
// Accumulate a bounded head of the stream (enough to see the Thought prose OR the
|
|
11713
11453
|
// tool call), capped so a 16k-char runaway can't grow the buffer. normalizeThought
|
|
11714
11454
|
// yields a clean label — never raw code/CSS.
|
|
11715
|
-
|
|
11716
|
-
// and a restatement alone can run to a few hundred characters)
|
|
11717
|
-
if (fenceThought.length < 1200) {
|
|
11455
|
+
if (fenceThought.length < 400) {
|
|
11718
11456
|
fenceThought += line + "\n";
|
|
11719
|
-
const th = normalizeThought(fenceThought
|
|
11457
|
+
const th = normalizeThought(fenceThought);
|
|
11720
11458
|
if (th) liveThought = th;
|
|
11721
|
-
fullLiveThought =
|
|
11459
|
+
fullLiveThought = thoughtFull(fenceThought); // #96: keep the fuller thought for expand
|
|
11722
11460
|
}
|
|
11723
11461
|
continue;
|
|
11724
11462
|
}
|
|
@@ -11837,17 +11575,7 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
|
|
|
11837
11575
|
// So the pair collapses to: "◑ Running <cmd>…" while it runs, then "● <result>".
|
|
11838
11576
|
if (!plainReplyFlow) {
|
|
11839
11577
|
const mRun = /^Running → (.+)$/.exec(line.trim());
|
|
11840
|
-
if (mRun) {
|
|
11841
|
-
runningCmd = mRun[1];
|
|
11842
|
-
// a NEW command: its own clock and count, and it starts as one row (#331)
|
|
11843
|
-
runningCmdFull = "";
|
|
11844
|
-
runningCmdLines = 0;
|
|
11845
|
-
runningCmdAt = Date.now();
|
|
11846
|
-
thoughtExpanded = false;
|
|
11847
|
-
thinking = false;
|
|
11848
|
-
lastContentAt = Date.now();
|
|
11849
|
-
continue;
|
|
11850
|
-
}
|
|
11578
|
+
if (mRun) { runningCmd = mRun[1]; thinking = false; lastContentAt = Date.now(); continue; }
|
|
11851
11579
|
}
|
|
11852
11580
|
// Skip the status-clear for a BUFFERED plain reply (STREAM_PLAIN_REPLY=false): nothing
|
|
11853
11581
|
// prints yet, so the thinking ticker should keep animating until completion.
|
|
@@ -11930,9 +11658,9 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
|
|
|
11930
11658
|
// live clause from the partial too, so the blinking line updates mid-sentence instead
|
|
11931
11659
|
// of only once the line finally breaks (task #64).
|
|
11932
11660
|
if (inStreamFence && fenceThought.length < 400) {
|
|
11933
|
-
const th = normalizeThought(fenceThought + carry
|
|
11661
|
+
const th = normalizeThought(fenceThought + carry);
|
|
11934
11662
|
if (th) liveThought = th;
|
|
11935
|
-
fullLiveThought =
|
|
11663
|
+
fullLiveThought = thoughtFull(fenceThought + carry); // #96: fuller thought for expand
|
|
11936
11664
|
}
|
|
11937
11665
|
// A non-empty remainder is a partial CONTENT line (heartbeats always arrive whole with
|
|
11938
11666
|
// a newline), so tokens are actively flowing — keep the ticker asleep. EXCEPT a buffered
|
|
@@ -12133,7 +11861,6 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
|
|
|
12133
11861
|
lastWasBlank = false;
|
|
12134
11862
|
}
|
|
12135
11863
|
}
|
|
12136
|
-
finishPausedFence(); // #333: a fence closed and nothing followed — it is finished now
|
|
12137
11864
|
releaseHeldThought(); // #45: nothing followed it, so it was not a false start
|
|
12138
11865
|
clearStatus();
|
|
12139
11866
|
// Task #104: expose this turn's code-model INPUT + the CUMULATIVE output-token
|
|
@@ -12350,6 +12077,29 @@ async function main() {
|
|
|
12350
12077
|
// Asking an established machine to re-pick its mode on the first run after an upgrade
|
|
12351
12078
|
// implies that choosing Host would do something. It would not. Reported live on an existing
|
|
12352
12079
|
// machine that had been working for months.
|
|
12080
|
+
//
|
|
12081
|
+
// THE DATABASE FIRST, on a machine whose Local setup is finished (task #329). Every line below
|
|
12082
|
+
// talks to the API, and the API cannot answer without MongoDB — but its health check says "up"
|
|
12083
|
+
// regardless, so a missing mongod surfaced as two 15s timeouts and "the API is stuck". One
|
|
12084
|
+
// handshake here says what is actually wrong, and on a Mac offers to install or start it.
|
|
12085
|
+
// BEFORE the mode block on purpose: a database that had to be INSTALLED is empty, the steps
|
|
12086
|
+
// that filled it are re-opened, and the resume just below then runs them in this same start.
|
|
12087
|
+
if (isClientExpert()) {
|
|
12088
|
+
let mongoOk = true;
|
|
12089
|
+
try {
|
|
12090
|
+
const cer = await import("./client-expert-run.mjs");
|
|
12091
|
+
const { localMongoUri } = await import("./client-expert-api.mjs");
|
|
12092
|
+
const st0 = await cer.loadState();
|
|
12093
|
+
if (st0.mongo === "done") {
|
|
12094
|
+
const pre = await cer.preflightLocalMongo({ ask, isTty: IS_TTY, uri: localMongoUri() });
|
|
12095
|
+
mongoOk = pre.ok;
|
|
12096
|
+
if (pre.installed) await cer.saveState(cer.reopenAfterEmptyDatabase(st0));
|
|
12097
|
+
}
|
|
12098
|
+
} catch {
|
|
12099
|
+
/* a check, never a new way for gu to fail to start — the errors below still name the API */
|
|
12100
|
+
}
|
|
12101
|
+
if (!mongoOk) process.exit(1);
|
|
12102
|
+
}
|
|
12353
12103
|
try {
|
|
12354
12104
|
const { loadState, saveState, askMode } = await import("./client-expert-run.mjs");
|
|
12355
12105
|
const { clientExpertAvailable, expertPlanPending, shouldAskMode, parseLicenseFlag, resolveFirstRunMode, promptForLicenseToken } =
|
|
@@ -13031,7 +12781,25 @@ async function main() {
|
|
|
13031
12781
|
dim("\n Local mode keeps your conversations in MongoDB on this machine, and that") +
|
|
13032
12782
|
dim("\n API is what reads it. It does not survive a reboot on its own.\n")
|
|
13033
12783
|
);
|
|
12784
|
+
// ASK MONGODB ITSELF before telling the reader to (task #329). It can die AFTER the startup
|
|
12785
|
+
// check passed, and "is mongod running?" is a question this process answers in milliseconds.
|
|
12786
|
+
let mongoDown = "";
|
|
13034
12787
|
if (hung) {
|
|
12788
|
+
try {
|
|
12789
|
+
const { localMongoDown } = await import("./client-expert-run.mjs");
|
|
12790
|
+
const { localMongoUri } = await import("./client-expert-api.mjs");
|
|
12791
|
+
mongoDown = await localMongoDown(localMongoUri());
|
|
12792
|
+
} catch {
|
|
12793
|
+
/* unknown — the general advice below still applies */
|
|
12794
|
+
}
|
|
12795
|
+
}
|
|
12796
|
+
if (mongoDown) {
|
|
12797
|
+
console.error(
|
|
12798
|
+
dim(` The API is running; MongoDB is not answering at ${mongoDown}, and the API waits on it.`) +
|
|
12799
|
+
dim("\n Start MongoDB (run `gu` again — it checks the database first and says how),") +
|
|
12800
|
+
dim("\n no API restart needed.\n")
|
|
12801
|
+
);
|
|
12802
|
+
} else if (hung) {
|
|
13035
12803
|
// Do NOT try to start it — the port is taken by the process that is failing to answer.
|
|
13036
12804
|
console.error(
|
|
13037
12805
|
dim(" Something IS listening there but did not reply within ") +
|
|
@@ -13076,8 +12844,6 @@ async function main() {
|
|
|
13076
12844
|
// #323: gu's newest published price + cache tables — same cadence, same silent failure.
|
|
13077
12845
|
void refreshPriceUpdate();
|
|
13078
12846
|
setInterval(() => void refreshPriceUpdate(), UPDATE_CHECK_MS).unref();
|
|
13079
|
-
void refreshLogsSize();
|
|
13080
|
-
setInterval(() => void refreshLogsSize(), UPDATE_CHECK_MS).unref();
|
|
13081
12847
|
// The gu token (#232): same arrangement — background, never delays the prompt, unref'd.
|
|
13082
12848
|
startupTokenRefresh = refreshCliToken();
|
|
13083
12849
|
setInterval(() => void refreshCliToken(), TOKEN_TICK_MS).unref();
|
|
@@ -13538,10 +13304,6 @@ async function main() {
|
|
|
13538
13304
|
await runPricesCommand(line);
|
|
13539
13305
|
continue;
|
|
13540
13306
|
}
|
|
13541
|
-
if (line === "/clear-logs") {
|
|
13542
|
-
await runClearLogs();
|
|
13543
|
-
continue;
|
|
13544
|
-
}
|
|
13545
13307
|
if (line === "/tokens") {
|
|
13546
13308
|
// Task #142: WHERE the last request's prompt went. The bar answers "how much is RAG";
|
|
13547
13309
|
// this answers "and what displaced it", which is the part that tells you whether RAG is
|
package/mongo-native.mjs
CHANGED
|
@@ -144,6 +144,28 @@ export const BREW_INSTALL_STEPS = [
|
|
|
144
144
|
];
|
|
145
145
|
export const BREW_START_STEPS = [["services", "start", "mongodb/brew/mongodb-community"]];
|
|
146
146
|
|
|
147
|
+
/**
|
|
148
|
+
* HOMEBREW 7 REFUSES AN UNTRUSTED TAP (task #329, seen live 2026-10-02 on a fresh Homebrew 7.0.7):
|
|
149
|
+
* Error: Refusing to load formula mongodb/brew/mongodb-database-tools from untrusted tap
|
|
150
|
+
* mongodb/brew. Run … `brew trust mongodb/brew` to trust it.
|
|
151
|
+
* so tap → install, the steps that worked two days earlier, stopped at the install on exactly the
|
|
152
|
+
* machine a first install runs on. An older Homebrew has no `trust` command and must not be
|
|
153
|
+
* shown one, so the step is added only where the command exists.
|
|
154
|
+
*/
|
|
155
|
+
export const BREW_TRUST_STEP = ["trust", "mongodb/brew"];
|
|
156
|
+
export function brewInstallSteps({ trust = false } = {}) {
|
|
157
|
+
return trust ? [BREW_INSTALL_STEPS[0], BREW_TRUST_STEP, ...BREW_INSTALL_STEPS.slice(1)] : BREW_INSTALL_STEPS;
|
|
158
|
+
}
|
|
159
|
+
/** Does this Homebrew have `brew trust`? (`brew trust --help` exits 0 only where it exists.) */
|
|
160
|
+
export async function brewHasTrust(brew, { run = runStreaming } = {}) {
|
|
161
|
+
if (!brew) return false;
|
|
162
|
+
return (await run(brew, ["trust", "--help"], { env: { ...process.env, HOMEBREW_NO_AUTO_UPDATE: "1" } })) === 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Homebrew's own one-line installer (https://brew.sh) — it asks for the password itself. */
|
|
166
|
+
export const HOMEBREW_INSTALL_COMMAND =
|
|
167
|
+
'/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"';
|
|
168
|
+
|
|
147
169
|
/** Where Homebrew is, or "". Apple Silicon first, then Intel. */
|
|
148
170
|
export function findBrew({ exists = existsSync } = {}) {
|
|
149
171
|
for (const p of ["/opt/homebrew/bin/brew", "/usr/local/bin/brew"]) if (exists(p)) return p;
|
|
@@ -153,8 +175,11 @@ export function findBrew({ exists = existsSync } = {}) {
|
|
|
153
175
|
/** The lines a Mac with no Homebrew is shown. */
|
|
154
176
|
export function noBrewLines() {
|
|
155
177
|
return [
|
|
156
|
-
"MongoDB is installed with Homebrew on a Mac, and Homebrew is not here.",
|
|
157
|
-
|
|
178
|
+
"MongoDB is installed with Homebrew on a Mac, and Homebrew is not here. Install Homebrew first:",
|
|
179
|
+
` ${HOMEBREW_INSTALL_COMMAND}`,
|
|
180
|
+
"then run gu again — it installs MongoDB from there with:",
|
|
181
|
+
// A Homebrew installed today is 7+, which needs the tap trusted.
|
|
182
|
+
...brewInstallSteps({ trust: true }).map((s) => ` brew ${s.join(" ")}`),
|
|
158
183
|
"Or install MongoDB Community yourself: https://www.mongodb.com/try/download/community",
|
|
159
184
|
];
|
|
160
185
|
}
|