@ikyyofc/gemini-cli 3.0.0 → 3.0.1
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/package.json +1 -1
- package/src/renderer.js +205 -88
package/package.json
CHANGED
package/src/renderer.js
CHANGED
|
@@ -13,16 +13,18 @@ const tw = () => Math.min(process.stdout.columns || 72, 84);
|
|
|
13
13
|
const bw = () => Math.min(tw() - 4, 68);
|
|
14
14
|
const vlen = s => s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
// Word-wrap a PLAIN (no ANSI) string to maxW, returns array of lines
|
|
17
|
+
function wrapPlain(text, maxW) {
|
|
18
|
+
if (!text || text.length <= maxW) return [text || ""];
|
|
18
19
|
const words = text.split(" ");
|
|
19
20
|
const lines = [];
|
|
20
21
|
let cur = "";
|
|
21
22
|
for (const w of words) {
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
23
|
+
const candidate = cur ? cur + " " + w : w;
|
|
24
|
+
if (candidate.length <= maxW) { cur = candidate; continue; }
|
|
24
25
|
if (cur) lines.push(cur);
|
|
25
|
-
if (
|
|
26
|
+
if (w.length > maxW) {
|
|
27
|
+
// hard-wrap single very long word (e.g. bare URL)
|
|
26
28
|
for (let i = 0; i < w.length; i += maxW) lines.push(w.slice(i, i + maxW));
|
|
27
29
|
cur = "";
|
|
28
30
|
} else { cur = w; }
|
|
@@ -31,37 +33,34 @@ function wrapText(text, maxW) {
|
|
|
31
33
|
return lines.length ? lines : [""];
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
// garbage numbers appearing in rendered code).
|
|
36
|
+
// ─────────────────────────────────────────────────────────────────
|
|
37
|
+
// Syntax highlighting — applyToRaw prevents placeholder corruption
|
|
38
|
+
// ─────────────────────────────────────────────────────────────────
|
|
38
39
|
const KW = {
|
|
39
|
-
js
|
|
40
|
-
ts
|
|
41
|
-
py
|
|
42
|
-
go
|
|
43
|
-
sh
|
|
44
|
-
rs
|
|
40
|
+
js:/\b(const|let|var|function|return|if|else|for|while|do|switch|case|break|continue|new|this|class|extends|import|export|default|async|await|try|catch|finally|throw|typeof|instanceof|of|in|null|undefined|true|false|void|delete|yield|from|static|super)\b/g,
|
|
41
|
+
ts:/\b(const|let|var|function|return|if|else|for|while|switch|case|class|extends|import|export|default|async|await|try|catch|type|interface|enum|implements|declare|readonly|abstract|as|keyof|never|any|string|number|boolean|null|undefined|true|false)\b/g,
|
|
42
|
+
py:/\b(def|class|return|if|elif|else|for|while|import|from|as|with|try|except|finally|raise|pass|break|continue|and|or|not|in|is|None|True|False|lambda|yield|global|async|await)\b/g,
|
|
43
|
+
go:/\b(func|return|if|else|for|range|switch|var|const|type|struct|interface|import|package|defer|go|chan|map|make|new|nil|true|false)\b/g,
|
|
44
|
+
sh:/\b(if|then|else|elif|fi|for|while|do|done|case|esac|function|return|exit|export|echo|local|source|cd|mkdir|rm|cp|mv|sudo|npm|pip|git)\b/g,
|
|
45
|
+
rs:/\b(fn|let|mut|return|if|else|for|match|use|mod|pub|struct|enum|impl|trait|type|const|async|await|true|false|None|Some|Ok|Err)\b/g,
|
|
45
46
|
};
|
|
46
47
|
const LANGMAP = {
|
|
47
|
-
javascript:"js",
|
|
48
|
-
python:"py",
|
|
49
|
-
rust:"rs",
|
|
48
|
+
javascript:"js",js:"js",typescript:"ts",ts:"ts",
|
|
49
|
+
python:"py",py:"py",go:"go",golang:"go",
|
|
50
|
+
rust:"rs",rs:"rs",bash:"sh",sh:"sh",shell:"sh",zsh:"sh",fish:"sh",
|
|
50
51
|
};
|
|
51
52
|
|
|
52
53
|
function applyToRaw(str, re, fn) {
|
|
53
|
-
return str.split(/(\x00\d+\x00)/).map((p, i) =>
|
|
54
|
-
i % 2 === 0 ? p.replace(re, fn) : p
|
|
55
|
-
).join("");
|
|
54
|
+
return str.split(/(\x00\d+\x00)/).map((p, i) => i % 2 === 0 ? p.replace(re, fn) : p).join("");
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
function highlight(code, lang = "") {
|
|
59
58
|
const l = LANGMAP[lang.toLowerCase()] || "";
|
|
60
59
|
if (!l) return code;
|
|
61
60
|
const saved = [];
|
|
62
|
-
const save
|
|
61
|
+
const save = s => { const id = `\x00${saved.length}\x00`; saved.push(s); return id; };
|
|
63
62
|
let r = code;
|
|
64
|
-
r = applyToRaw(r, /(\/\/.*$|#.*$|\/\*[\s\S]*?\*\/)/gm,
|
|
63
|
+
r = applyToRaw(r, /(\/\/.*$|#.*$|\/\*[\s\S]*?\*\/)/gm, m => save(chalk.hex(C.comment).italic(m)));
|
|
65
64
|
r = applyToRaw(r, /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g, m => save(chalk.hex(C.str)(m)));
|
|
66
65
|
if (KW[l]) { KW[l].lastIndex = 0; r = applyToRaw(r, KW[l], m => save(chalk.hex(C.kw).bold(m))); }
|
|
67
66
|
r = applyToRaw(r, /\b(\d+\.?\d*)\b/g, m => save(chalk.hex(C.num)(m)));
|
|
@@ -69,53 +68,162 @@ function highlight(code, lang = "") {
|
|
|
69
68
|
return r.replace(/\x00(\d+)\x00/g, (_, i) => saved[+i] ?? "");
|
|
70
69
|
}
|
|
71
70
|
|
|
72
|
-
//
|
|
71
|
+
// ─────────────────────────────────────────────────────────────────
|
|
72
|
+
// Markdown → array of display lines (with wrapping baked in)
|
|
73
|
+
// Returns: Array<{ text: string, raw: boolean }>
|
|
74
|
+
// raw=true → ANSI already applied (code blocks), print as-is
|
|
75
|
+
// raw=false → plain text, will receive border + chalk in printBox
|
|
76
|
+
// ─────────────────────────────────────────────────────────────────
|
|
73
77
|
export function renderMarkdown(text, contentW) {
|
|
74
78
|
const cw = contentW ?? bw() - 2;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
79
|
+
const out = []; // { text, raw }
|
|
80
|
+
|
|
81
|
+
const pushPlain = (s) => out.push({ text: s, raw: false });
|
|
82
|
+
const pushStyled = (s) => out.push({ text: s, raw: true });
|
|
83
|
+
const pushBlank = () => out.push({ text: "", raw: false });
|
|
84
|
+
|
|
85
|
+
// Normalise line endings
|
|
86
|
+
const src = text.replace(/\r\n/g, "\n").trimEnd();
|
|
87
|
+
|
|
88
|
+
// Split into tokens: code blocks vs everything else
|
|
89
|
+
const segments = src.split(/(```[\s\S]*?```)/g);
|
|
90
|
+
|
|
91
|
+
for (const seg of segments) {
|
|
92
|
+
// ── Fenced code block ──────────────────────────────────────
|
|
93
|
+
if (seg.startsWith("```")) {
|
|
94
|
+
const m = seg.match(/^```(\w*)\n?([\s\S]*?)```$/);
|
|
95
|
+
const lang = m?.[1] ?? "";
|
|
96
|
+
const code = m?.[2]?.trimEnd() ?? seg.slice(3);
|
|
97
|
+
const lines = code.split("\n");
|
|
98
|
+
const hl = highlight(code, lang);
|
|
99
|
+
const hlLines = hl.split("\n");
|
|
100
|
+
const gw = String(lines.length).length;
|
|
101
|
+
const bw2 = Math.min(cw + 2, tw() - 6);
|
|
102
|
+
const lbl = lang ? chalk.hex(C.blue).bold(` ${lang} `) : "";
|
|
103
|
+
const lblLen = lang ? lang.length + 2 : 0;
|
|
104
|
+
const dashes = Math.max(2, bw2 - lblLen - 1);
|
|
105
|
+
const maxCode = bw2 - gw - 4;
|
|
106
|
+
|
|
107
|
+
pushBlank();
|
|
108
|
+
pushStyled(chalk.hex(C.dim)("┌─") + lbl + chalk.hex(C.dimmer)("─".repeat(dashes)) + chalk.hex(C.dim)("┐"));
|
|
109
|
+
hlLines.forEach((hl_line, i) => {
|
|
110
|
+
const ln = chalk.hex(C.dimmer)(String(i + 1).padStart(gw));
|
|
111
|
+
const raw = lines[i] ?? "";
|
|
112
|
+
const disp = raw.length > maxCode ? hl_line.slice(0, maxCode * 3) + chalk.hex(C.dimmer)("…") : hl_line;
|
|
113
|
+
pushStyled(chalk.hex(C.dim)("│") + chalk.hex(C.dimmer)(" " + ln + " ╎ ") + disp);
|
|
114
|
+
});
|
|
115
|
+
pushStyled(chalk.hex(C.dim)("└" + "─".repeat(bw2 + 1) + "┘"));
|
|
116
|
+
pushBlank();
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Text segment — process line by line ───────────────────
|
|
121
|
+
for (const rawLine of seg.split("\n")) {
|
|
122
|
+
const line = rawLine;
|
|
123
|
+
|
|
124
|
+
// Blank line
|
|
125
|
+
if (!line.trim()) { pushBlank(); continue; }
|
|
126
|
+
|
|
127
|
+
// === Header === (non-standard but model uses it)
|
|
128
|
+
if (/^={3,}\s*.+\s*={3,}$/.test(line)) {
|
|
129
|
+
const t = line.replace(/^=+\s*/, "").replace(/\s*=+$/, "");
|
|
130
|
+
pushBlank();
|
|
131
|
+
pushStyled(chalk.hex(C.blue).bold.underline(t));
|
|
132
|
+
pushBlank();
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ATX headers
|
|
137
|
+
if (/^### /.test(line)) { pushStyled(chalk.hex(C.yellow).bold("◈ " + line.slice(4))); continue; }
|
|
138
|
+
if (/^## /.test(line)) { pushStyled(chalk.hex(C.blue).bold.underline(line.slice(3))); continue; }
|
|
139
|
+
if (/^# /.test(line)) { pushStyled(chalk.hex(C.teal).bold("◉ " + line.slice(2).toUpperCase())); continue; }
|
|
140
|
+
|
|
141
|
+
// HR
|
|
142
|
+
if (/^---+$/.test(line)) { pushStyled(chalk.hex(C.dimmer)("╌".repeat(Math.min(cw, 48)))); continue; }
|
|
143
|
+
|
|
144
|
+
// Blockquote
|
|
145
|
+
if (/^> /.test(line)) {
|
|
146
|
+
const t = line.slice(2);
|
|
147
|
+
wrapPlain(t, cw - 2).forEach((wl, i) =>
|
|
148
|
+
pushStyled(chalk.hex(C.purple)("▎ ") + chalk.hex(C.muted).italic(wl))
|
|
149
|
+
);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Unordered list
|
|
154
|
+
const ulM = line.match(/^(\s*)[*\-+] (.+)$/);
|
|
155
|
+
if (ulM) {
|
|
156
|
+
const [, ind, content] = ulM;
|
|
157
|
+
const indW = ind.length;
|
|
158
|
+
const itemW = cw - indW - 4; // "◆ " = 2 chars visible + 2 padding
|
|
159
|
+
const plain = stripInline(content);
|
|
160
|
+
wrapPlain(plain, itemW).forEach((wl, i) => {
|
|
161
|
+
if (i === 0)
|
|
162
|
+
pushStyled(ind + chalk.hex(C.teal)("◆ ") + chalk.hex(C.white)(applyInline(wl)));
|
|
163
|
+
else
|
|
164
|
+
pushStyled(ind + " " + chalk.hex(C.white)(applyInline(wl)));
|
|
165
|
+
});
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Ordered list
|
|
170
|
+
const olM = line.match(/^(\s*)(\d+)\. (.+)$/);
|
|
171
|
+
if (olM) {
|
|
172
|
+
const [, ind, num, content] = olM;
|
|
173
|
+
const markerW = num.length + 2;
|
|
174
|
+
const itemW = cw - ind.length - markerW;
|
|
175
|
+
const plain = stripInline(content);
|
|
176
|
+
wrapPlain(plain, itemW).forEach((wl, i) => {
|
|
177
|
+
if (i === 0)
|
|
178
|
+
pushStyled(ind + chalk.hex(C.blue)(chalk.bold(num + ".") + " ") + chalk.hex(C.white)(applyInline(wl)));
|
|
179
|
+
else
|
|
180
|
+
pushStyled(ind + " ".repeat(markerW) + chalk.hex(C.white)(applyInline(wl)));
|
|
181
|
+
});
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Plain paragraph — strip inline markup for wrapping, then re-apply
|
|
186
|
+
const plain = stripInline(line);
|
|
187
|
+
wrapPlain(plain, cw).forEach(wl =>
|
|
188
|
+
pushStyled(chalk.hex(C.white)(applyInline(wl)))
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Strip inline markdown to get plain text for accurate wrapping
|
|
197
|
+
function stripInline(s) {
|
|
198
|
+
return s
|
|
199
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") // links → text only
|
|
200
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
201
|
+
.replace(/\*\*\*(.+?)\*\*\*/g, "$1")
|
|
202
|
+
.replace(/\*\*(.+?)\*\*/g, "$1")
|
|
203
|
+
.replace(/\*(.+?)\*/g, "$1")
|
|
204
|
+
.replace(/_(.+?)_/g, "$1");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Apply inline markdown styling to already-wrapped line
|
|
208
|
+
function applyInline(s) {
|
|
209
|
+
return s
|
|
210
|
+
// Links → show "text (url truncated)" — URLs not clickable in terminal
|
|
211
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) => {
|
|
212
|
+
const short = url.length > 40 ? url.slice(0, 40) + "…" : url;
|
|
213
|
+
return chalk.hex(C.teal)(text) + chalk.hex(C.dimmer)(" (" + short + ")");
|
|
214
|
+
})
|
|
215
|
+
.replace(/`([^`]+)`/g, (_, c) => chalk.bgHex("#2A2A3E")(chalk.hex(C.orange)(" " + c + " ")))
|
|
216
|
+
.replace(/\*\*\*(.+?)\*\*\*/g, (_, t) => chalk.bold.italic(t))
|
|
217
|
+
.replace(/\*\*(.+?)\*\*/g, (_, t) => chalk.bold(t))
|
|
218
|
+
.replace(/\*(.+?)\*/g, (_, t) => chalk.italic(t))
|
|
219
|
+
.replace(/_(.+?)_/g, (_, t) => chalk.italic(t));
|
|
113
220
|
}
|
|
114
221
|
|
|
115
|
-
//
|
|
116
|
-
|
|
222
|
+
// ─────────────────────────────────────────────────────────────────
|
|
223
|
+
// Box printer — all lines are pre-wrapped, just add border
|
|
224
|
+
// ─────────────────────────────────────────────────────────────────
|
|
225
|
+
function printBox(renderedLines, borderHex, label, extra = "") {
|
|
117
226
|
const W = bw();
|
|
118
|
-
const inner = W - 2;
|
|
119
227
|
const lblLen = vlen(label) + vlen(extra);
|
|
120
228
|
const dashes = Math.max(0, W - lblLen - 1);
|
|
121
229
|
|
|
@@ -124,37 +232,43 @@ function printBox(contentLines, borderHex, label, extra = "") {
|
|
|
124
232
|
chalk.hex(C.dim)("─".repeat(dashes)) + extra + "\n"
|
|
125
233
|
);
|
|
126
234
|
|
|
127
|
-
for (const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
} else {
|
|
132
|
-
wrapText(line || " ", inner).forEach(wl =>
|
|
133
|
-
process.stdout.write(chalk.hex(borderHex)("│ ") + wl + "\n")
|
|
134
|
-
);
|
|
135
|
-
}
|
|
235
|
+
for (const { text, raw } of renderedLines) {
|
|
236
|
+
// raw lines (code blocks, headers with box-drawing) — print as-is
|
|
237
|
+
// non-raw lines — already plain/styled text, just add border
|
|
238
|
+
process.stdout.write(chalk.hex(borderHex)("│ ") + text + "\n");
|
|
136
239
|
}
|
|
137
240
|
|
|
138
241
|
process.stdout.write(chalk.hex(borderHex)("╰" + "─".repeat(W + 1)) + "\n");
|
|
139
242
|
}
|
|
140
243
|
|
|
141
|
-
//
|
|
244
|
+
// ─────────────────────────────────────────────────────────────────
|
|
245
|
+
// Messages
|
|
246
|
+
// ─────────────────────────────────────────────────────────────────
|
|
142
247
|
export function printUser(text) {
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
const
|
|
248
|
+
const W = bw();
|
|
249
|
+
const inner = W - 2;
|
|
250
|
+
const rawLines = text.split("\n");
|
|
251
|
+
const multi = rawLines.length > 1;
|
|
252
|
+
const label = chalk.hex(C.blue).bold(" you ");
|
|
253
|
+
const extra = multi ? chalk.hex(C.muted)(` (${rawLines.length} lines) `) : "";
|
|
254
|
+
|
|
255
|
+
const lines = multi
|
|
256
|
+
? rawLines.flatMap(l => wrapPlain(l, inner).map(wl => ({ text: chalk.hex(C.white)(wl), raw: true })))
|
|
257
|
+
: wrapPlain(text, inner).map(wl => ({ text: chalk.hex(C.white)(wl), raw: true }));
|
|
258
|
+
|
|
147
259
|
printBox(lines, C.blue, label, extra);
|
|
148
260
|
}
|
|
149
261
|
|
|
150
262
|
export function printAssistant(text) {
|
|
151
|
-
const inner
|
|
152
|
-
const
|
|
153
|
-
printBox(
|
|
263
|
+
const inner = bw() - 2;
|
|
264
|
+
const lines = renderMarkdown(text.trimEnd(), inner);
|
|
265
|
+
printBox(lines, C.teal, chalk.hex(C.teal).bold(" gemini "));
|
|
154
266
|
process.stdout.write("\n");
|
|
155
267
|
}
|
|
156
268
|
|
|
157
|
-
//
|
|
269
|
+
// ─────────────────────────────────────────────────────────────────
|
|
270
|
+
// Agent step blocks
|
|
271
|
+
// ─────────────────────────────────────────────────────────────────
|
|
158
272
|
export function printStepHeader(step) {
|
|
159
273
|
const W = bw();
|
|
160
274
|
const label = chalk.hex(C.yellow).bold(" working ");
|
|
@@ -173,7 +287,7 @@ export function printStepFooter() {
|
|
|
173
287
|
export function printToolCall(name, args = {}) {
|
|
174
288
|
const argStr = Object.entries(args).map(([k, v]) => {
|
|
175
289
|
const raw = String(v).replace(/\n/g, "↵");
|
|
176
|
-
const val =
|
|
290
|
+
const val = raw.length > 42 ? raw.slice(0, 42) + "…" : raw;
|
|
177
291
|
return chalk.hex(C.muted)(k + ":") + chalk.hex(C.orange)(val);
|
|
178
292
|
}).join(" ");
|
|
179
293
|
process.stdout.write(
|
|
@@ -199,15 +313,19 @@ export function printToolResult(result) {
|
|
|
199
313
|
if (extra > 0) process.stdout.write(border + chalk.hex(C.dim)(`… +${extra} more lines`) + "\n");
|
|
200
314
|
}
|
|
201
315
|
|
|
202
|
-
//
|
|
316
|
+
// ─────────────────────────────────────────────────────────────────
|
|
317
|
+
// Status
|
|
318
|
+
// ─────────────────────────────────────────────────────────────────
|
|
203
319
|
export function printError(msg) { process.stdout.write("\n" + chalk.hex(C.red)("╳ ") + chalk.hex(C.red).bold("error ") + chalk.hex(C.muted)(msg) + "\n\n"); }
|
|
204
320
|
export function printInfo(msg) { process.stdout.write(chalk.hex(C.dim)("· ") + chalk.hex(C.white)(msg) + "\n"); }
|
|
205
321
|
export function printSuccess(msg) { process.stdout.write(chalk.hex(C.teal)("✓ ") + chalk.hex(C.teal)(msg) + "\n"); }
|
|
206
322
|
export function printWarning(msg) { process.stdout.write(chalk.hex(C.yellow)("⚠ ") + chalk.hex(C.yellow)(msg) + "\n"); }
|
|
207
323
|
|
|
208
|
-
//
|
|
324
|
+
// ─────────────────────────────────────────────────────────────────
|
|
325
|
+
// Welcome & Help
|
|
326
|
+
// ─────────────────────────────────────────────────────────────────
|
|
209
327
|
export function renderWelcome(memCount = 0, extCount = 0) {
|
|
210
|
-
const W
|
|
328
|
+
const W = bw();
|
|
211
329
|
const stats = [
|
|
212
330
|
memCount ? `${memCount} context file${memCount > 1 ? "s" : ""}` : null,
|
|
213
331
|
extCount ? `${extCount} extension${extCount > 1 ? "s" : ""}` : null,
|
|
@@ -224,7 +342,6 @@ export function renderWelcome(memCount = 0, extCount = 0) {
|
|
|
224
342
|
].join("\n");
|
|
225
343
|
}
|
|
226
344
|
|
|
227
|
-
// ─── Help ────────────────────────────────────────────────────────
|
|
228
345
|
export function renderHelp(customCommands = {}) {
|
|
229
346
|
const sep = chalk.hex(C.dimmer)(" " + "─".repeat(Math.min(tw() - 4, 52)));
|
|
230
347
|
const row = (cmd, desc) => " " + chalk.hex(C.blue).bold(cmd.padEnd(24)) + chalk.hex(C.muted)(desc);
|