@tiens.nguyen/gu-cli 1.0.865 → 1.0.866
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 +2 -1
- package/banner-name.mjs +110 -0
- package/client-expert-run.mjs +39 -3
- package/gu-repl.mjs +23 -4
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -35,10 +35,11 @@ meaning the whole stack runs on this machine.
|
|
|
35
35
|
## The `gu` REPL
|
|
36
36
|
|
|
37
37
|
```
|
|
38
|
-
gu [-v|--verbose] [--log] [--license <pro token>]
|
|
38
|
+
gu [-v|--verbose] [--log] [--name <text>] [--license <pro token>]
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
- `-v, --verbose` shows RAG index-file activity in the bottom bar.
|
|
42
|
+
- `--name <text>` draws `<text>` instead of the gu logo in the startup banner (this run only). Letters, digits, space, `-` and `_` become block art; anything else, or a name over 12 characters, is shown as plain text.
|
|
42
43
|
- `--log` logs this run in detail to `~/.gonext/worker.log`. Without it only starts, stops and errors are logged.
|
|
43
44
|
- Ctrl-C stops following a turn.
|
|
44
45
|
|
package/banner-name.mjs
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gu --name <text>` (#224): the startup banner draws <text> instead of the "gu" mark.
|
|
3
|
+
*
|
|
4
|
+
* Same material as the built-in mark: half-block elements only (▀ ▄ █), so it renders on any
|
|
5
|
+
* terminal without a font to install. Two rows of x-height, plus a third row used only by letters
|
|
6
|
+
* with a descender (g j p q y). Glyphs are separated by one blank column.
|
|
7
|
+
*
|
|
8
|
+
* ANYTHING THE FONT CANNOT DRAW (accents, emoji, punctuation) is shown as plain text on a single
|
|
9
|
+
* row instead of being dropped — a name must never silently turn into a different name.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const G = {
|
|
13
|
+
a: ["█▀▀█", "█▀▀█"],
|
|
14
|
+
b: ["█▀▀▄", "█▄▄▀"],
|
|
15
|
+
c: ["█▀▀▀", "█▄▄▄"],
|
|
16
|
+
d: ["▄▀▀█", "▀▄▄█"],
|
|
17
|
+
e: ["█▀▀█", "█▄▄▄"],
|
|
18
|
+
f: ["█▀▀▀", "█▀▀ "],
|
|
19
|
+
g: ["█▀▀█", "█▄▄█", "▄▄▄█"],
|
|
20
|
+
h: ["█ █", "█▀▀█"],
|
|
21
|
+
i: ["▄", "█"],
|
|
22
|
+
j: [" █", " █", "▄▄█"],
|
|
23
|
+
k: ["█ ▄▀", "█▀▄ "],
|
|
24
|
+
l: ["█ ", "█▄▄"],
|
|
25
|
+
m: ["█▀▄▀█", "█ █"],
|
|
26
|
+
n: ["█▀▀█", "█ █"],
|
|
27
|
+
o: ["█▀▀█", "█▄▄█"],
|
|
28
|
+
p: ["█▀▀█", "█▀▀▀", "▀ "],
|
|
29
|
+
q: ["█▀▀█", "█▄▄█", " █"],
|
|
30
|
+
r: ["█▀▀█", "█▀▀▄"],
|
|
31
|
+
s: ["▄▀▀▀", "▄▄▄▀"],
|
|
32
|
+
t: ["▀█▀", " █ "],
|
|
33
|
+
u: ["█ █", "█▄▄█"],
|
|
34
|
+
v: ["█ █", "▀▄▄▀"],
|
|
35
|
+
w: ["█ █", "█▄█▄█"],
|
|
36
|
+
x: ["▀▄▀", "▄▀▄"],
|
|
37
|
+
y: ["█ █", "▀▄▄█", "▄▄▄▀"],
|
|
38
|
+
z: ["▀▀▀█", "█▄▄▄"],
|
|
39
|
+
0: ["█▀▀█", "█▄▀█"],
|
|
40
|
+
1: ["▄█", " █"],
|
|
41
|
+
2: ["▄▀▀█", "█▄▄▄"],
|
|
42
|
+
3: ["▀▀▀█", "▄▄▄█"],
|
|
43
|
+
4: ["█ █", "▀▀▀█"],
|
|
44
|
+
5: ["█▀▀▀", "▄▄▄█"],
|
|
45
|
+
6: ["█▀▀▀", "█▄▄█"],
|
|
46
|
+
7: ["▀▀▀█", " █ "],
|
|
47
|
+
8: ["▄▀▀▄", "▀▄▄▀"],
|
|
48
|
+
9: ["█▀▀█", "▀▀▀█"],
|
|
49
|
+
"-": [" ", "▄▄▄"],
|
|
50
|
+
_: [" ", "▄▄▄▄"],
|
|
51
|
+
" ": [" ", " "],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** Longest name accepted at all; longer is cut (and the cut is reported by the caller). */
|
|
55
|
+
export const NAME_MAX = 24;
|
|
56
|
+
/** Most characters drawn as block art; a longer name is shown as plain text. */
|
|
57
|
+
export const ART_MAX = 12;
|
|
58
|
+
|
|
59
|
+
/** What the user typed → what is shown: trimmed, control chars removed, capped. */
|
|
60
|
+
export function cleanName(raw) {
|
|
61
|
+
return String(raw ?? "")
|
|
62
|
+
.replace(/[\u0000-\u001f\u007f]/g, "")
|
|
63
|
+
.trim()
|
|
64
|
+
.slice(0, NAME_MAX);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Draw `name` as rows of equal visible width. Returns { rows, width, art }; null for an empty name. */
|
|
68
|
+
export function logoFor(raw, maxWidth = Infinity) {
|
|
69
|
+
const name = cleanName(raw);
|
|
70
|
+
if (!name) return null;
|
|
71
|
+
const lower = name.toLowerCase();
|
|
72
|
+
const drawable = [...lower].every((ch) => G[ch]);
|
|
73
|
+
if (drawable && lower.length <= ART_MAX) {
|
|
74
|
+
const rows = ["", "", ""];
|
|
75
|
+
[...lower].forEach((ch, i) => {
|
|
76
|
+
const g = G[ch];
|
|
77
|
+
const w = Math.max(...g.map((r) => r.length));
|
|
78
|
+
for (let r = 0; r < 3; r++) {
|
|
79
|
+
rows[r] += (i ? " " : "") + (g[r] ?? "").padEnd(w);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
// The third row only exists for descenders; drop it when it is all blanks.
|
|
83
|
+
const out = rows[2].trim() ? rows : rows.slice(0, 2);
|
|
84
|
+
const width = Math.max(...out.map((r) => r.length));
|
|
85
|
+
if (width <= maxWidth) return { rows: out.map((r) => r.padEnd(width)), width, art: true };
|
|
86
|
+
}
|
|
87
|
+
const text = name.slice(0, Math.max(1, Math.min(name.length, maxWidth)));
|
|
88
|
+
return { rows: [text], width: text.length, art: false };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Read `--name <x>` / `--name=<x>` from argv. { value } | { missing: true } | {} when absent.
|
|
93
|
+
* A following token that is itself a flag is NOT taken as the value.
|
|
94
|
+
*/
|
|
95
|
+
export function parseNameArg(argv) {
|
|
96
|
+
const args = Array.isArray(argv) ? argv : [];
|
|
97
|
+
for (let i = 0; i < args.length; i++) {
|
|
98
|
+
const a = String(args[i]);
|
|
99
|
+
if (a.startsWith("--name=")) {
|
|
100
|
+
const value = cleanName(a.slice(7));
|
|
101
|
+
return value ? { value } : { missing: true };
|
|
102
|
+
}
|
|
103
|
+
if (a === "--name") {
|
|
104
|
+
const next = args[i + 1];
|
|
105
|
+
const value = next === undefined || String(next).startsWith("-") ? "" : cleanName(next);
|
|
106
|
+
return value ? { value } : { missing: true };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return {};
|
|
110
|
+
}
|
package/client-expert-run.mjs
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* that does not work — and that state is much harder to get out of than an unfinished setup.
|
|
15
15
|
*/
|
|
16
16
|
import { execFile, spawn } from "node:child_process";
|
|
17
|
-
import { readFile, writeFile, mkdir, rm, rename } from "node:fs/promises";
|
|
17
|
+
import { readFile, writeFile, mkdir, rm, rename, cp } from "node:fs/promises";
|
|
18
18
|
import { writeFileSync } from "node:fs";
|
|
19
19
|
import { homedir } from "node:os";
|
|
20
20
|
import { join } from "node:path";
|
|
@@ -867,6 +867,43 @@ export async function probeApiInstall(version) {
|
|
|
867
867
|
}
|
|
868
868
|
}
|
|
869
869
|
|
|
870
|
+
/** Errors Windows raises while another process (antivirus, indexer) holds a handle in a folder. */
|
|
871
|
+
const LOCKED = new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Put the unpacked folder at its final path. WINDOWS REFUSES TO RENAME OR DELETE A DIRECTORY
|
|
875
|
+
* while any process has a handle inside it, and the files `tar` has just written are exactly what
|
|
876
|
+
* Defender and the indexer open (issue #223: "EPERM: operation not permitted, rename ...incoming").
|
|
877
|
+
* So retry with backoff, then fall back to copy + remove, then say which folder is locked.
|
|
878
|
+
* `fs` is injectable for the tests.
|
|
879
|
+
*/
|
|
880
|
+
export async function moveIntoPlace(staging, dir, fs = {}) {
|
|
881
|
+
const { rename: mv = rename, rm: del = rm, cp: copy = cp, sleep = (ms) => new Promise((r) => setTimeout(r, ms)) } = fs;
|
|
882
|
+
const tries = fs.tries ?? 10;
|
|
883
|
+
let last;
|
|
884
|
+
for (let i = 0; i < tries; i++) {
|
|
885
|
+
try {
|
|
886
|
+
await del(dir, { recursive: true, force: true });
|
|
887
|
+
await mv(staging, dir);
|
|
888
|
+
return;
|
|
889
|
+
} catch (e) {
|
|
890
|
+
last = e;
|
|
891
|
+
if (!LOCKED.has(e?.code)) throw e;
|
|
892
|
+
await sleep(Math.min(250 * 2 ** i, 3000));
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
try {
|
|
896
|
+
await del(dir, { recursive: true, force: true });
|
|
897
|
+
await copy(staging, dir, { recursive: true });
|
|
898
|
+
await del(staging, { recursive: true, force: true }).catch(() => {});
|
|
899
|
+
} catch (e) {
|
|
900
|
+
throw new Error(
|
|
901
|
+
`Could not move the unpacked API into ${dir} (${e?.code ?? last?.code}): a program is holding a file in it. ` +
|
|
902
|
+
"Close gu and any terminal or editor open in that folder, pause antivirus scanning for it, and run the install again."
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
870
907
|
/**
|
|
871
908
|
* Download and unpack the published API.
|
|
872
909
|
*
|
|
@@ -908,9 +945,8 @@ export async function downloadApiPackage(manifest, { onProgress = () => {} } = {
|
|
|
908
945
|
// ends in "gzip: stdin: unexpected end of file". A path with no drive letter works for GNU and bsdtar.
|
|
909
946
|
await run("tar", ["xzf", "package.tar.gz", "-C", "."], { cwd: staging });
|
|
910
947
|
await rm(tmpTar, { force: true });
|
|
911
|
-
await rm(dir, { recursive: true, force: true });
|
|
912
948
|
await mkdir(join(homedir(), ".gonext", "api"), { recursive: true });
|
|
913
|
-
await
|
|
949
|
+
await moveIntoPlace(staging, dir);
|
|
914
950
|
onProgress(`installed ${manifest.version} → ${dir}`);
|
|
915
951
|
return { dir, version: manifest.version };
|
|
916
952
|
}
|
package/gu-repl.mjs
CHANGED
|
@@ -193,6 +193,7 @@ import { EditCardStream, isEditCardHeader } from "./edit-card-render.mjs";
|
|
|
193
193
|
// Task #240: what a failed agent-payload call means, and when a rejected key may be recovered
|
|
194
194
|
// from in place instead of ending the session.
|
|
195
195
|
import { payloadError, recoveryPlan } from "./credential-recovery.mjs";
|
|
196
|
+
import { logoFor, parseNameArg } from "./banner-name.mjs";
|
|
196
197
|
import { isNoCodeRetry, splitVerboseLine, stepHasCode } from "./verbose-lines.mjs";
|
|
197
198
|
import {
|
|
198
199
|
createTranscript,
|
|
@@ -382,8 +383,9 @@ const vwidth = (s) => String(s).replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
|
382
383
|
// square, read as a box; with an all-caps "GU AGENT" beside it the banner shouted its name twice.
|
|
383
384
|
// The product is written "gu", so the mark is too: a g with a real descender and a u — a wordmark,
|
|
384
385
|
// one row shorter, and the only coloured thing on the banner.
|
|
385
|
-
|
|
386
|
-
|
|
386
|
+
// `let`: `gu --name <text>` (#224) swaps in that name's art once argv has been read.
|
|
387
|
+
let LOGO = ["█▀▀█ █ █", "█▄▄█ █▄▄█", "▄▄▄█"];
|
|
388
|
+
let LOGO_W = 9;
|
|
387
389
|
// Lay the LOGO and the status rows SIDE BY SIDE (logo left, facts right), or STACK them (logo
|
|
388
390
|
// then facts) when the terminal is too narrow to fit both without wrapping — so the banner is
|
|
389
391
|
// never the soft-wrapped jumble the plain "· … · …" lines used to be (task #131). Side-by-side
|
|
@@ -402,7 +404,7 @@ const renderBanner = (statusRows) => {
|
|
|
402
404
|
out.push(GUTTER + left + gap + (statusRows[i] ?? ""));
|
|
403
405
|
}
|
|
404
406
|
} else {
|
|
405
|
-
for (const r of LOGO) out.push(GUTTER + accent(r));
|
|
407
|
+
for (const r of LOGO) out.push(GUTTER + accent(clipVisible(r, cols - 1 - GUTTER.length)));
|
|
406
408
|
for (const r of statusRows) out.push(GUTTER + clipVisible(r, cols - 1 - GUTTER.length));
|
|
407
409
|
}
|
|
408
410
|
return out.join("\n");
|
|
@@ -622,7 +624,7 @@ if (cliVerdict.kind === "run") {
|
|
|
622
624
|
} else if (argv.includes("--help") || argv.includes("-h")) {
|
|
623
625
|
console.log(
|
|
624
626
|
"gu — terminal agent REPL for the current folder\n\n" +
|
|
625
|
-
"Usage: gu [-v|--verbose] [--log] [--color] [--license <pro token>]\n" +
|
|
627
|
+
"Usage: gu [-v|--verbose] [--log] [--color] [--name <text>] [--license <pro token>]\n" +
|
|
626
628
|
" gu status # worker + a table of the running model servers\n" +
|
|
627
629
|
" gu start|stop|restart|logs # manage the background worker\n" +
|
|
628
630
|
" gu logs <port|pid> [-f] # tail a running model server's log\n" +
|
|
@@ -633,6 +635,8 @@ if (cliVerdict.kind === "run") {
|
|
|
633
635
|
" stops and errors are logged. Ends when this gu closes.\n" +
|
|
634
636
|
" --color colour the thinking spinner and its playful word (cycling colours).\n" +
|
|
635
637
|
" Without it they are plain white.\n" +
|
|
638
|
+
" --name <text> show <text> instead of the gu logo in the startup banner\n" +
|
|
639
|
+
" (this run only; letters, digits, space - _ are drawn as art)\n" +
|
|
636
640
|
" --license <token> pro license: a first install ASKS how this machine runs (hosted\n" +
|
|
637
641
|
" client / MLX host / private). Without it, a first install is\n" +
|
|
638
642
|
" private by default — the whole stack on this machine.\n" +
|
|
@@ -660,6 +664,21 @@ const REPL_LOG = argv.includes("--log");
|
|
|
660
664
|
// --color (#216): the thinking spinner and its playful word cycle through colours only on a run
|
|
661
665
|
// started with this flag; by default they are plain white.
|
|
662
666
|
const REPL_COLOR = argv.includes("--color");
|
|
667
|
+
// --name <text> (#224): the startup banner draws <text> in place of the "gu" mark. Per run only.
|
|
668
|
+
{
|
|
669
|
+
const named = parseNameArg(argv);
|
|
670
|
+
if (named.missing) {
|
|
671
|
+
console.error(red("gu: --name needs a name, e.g. gu --name acme"));
|
|
672
|
+
process.exit(2);
|
|
673
|
+
}
|
|
674
|
+
if (named.value) {
|
|
675
|
+
const l = logoFor(named.value, Math.max(8, (process.stdout.columns || 80) - 6));
|
|
676
|
+
if (l) {
|
|
677
|
+
LOGO = l.rows;
|
|
678
|
+
LOGO_W = l.width;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
663
682
|
if (REPL_LOG) {
|
|
664
683
|
const ll = await import("./log-level.mjs");
|
|
665
684
|
await ll.takeLease().catch(() => {});
|
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.866",
|
|
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",
|
|
@@ -101,6 +101,7 @@
|
|
|
101
101
|
"rag-job-config.mjs",
|
|
102
102
|
"completed-flush.mjs",
|
|
103
103
|
"verbose-lines.mjs",
|
|
104
|
+
"banner-name.mjs",
|
|
104
105
|
"agent-model-label.mjs",
|
|
105
106
|
"coding-choice-tags.mjs",
|
|
106
107
|
"coding-provider-setup.mjs",
|