ispbills-icli 6.0.0 → 6.1.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/README.md +6 -0
- package/bin/icli.js +22 -13
- package/package.json +1 -1
- package/src/banner.js +8 -4
- package/src/loader.js +3 -4
- package/src/renderer.js +20 -18
- package/src/ui.js +49 -0
package/README.md
CHANGED
|
@@ -93,6 +93,7 @@ icli --reasoning "audit the config on olt#1" # stream reasoning inline
|
|
|
93
93
|
| `icli logout` | Log out and clear saved credentials |
|
|
94
94
|
| `icli update` | Update iCli to the latest version |
|
|
95
95
|
| `icli whoami` | Show current login info |
|
|
96
|
+
| `icli --version` | Show the installed iCli version |
|
|
96
97
|
| `icli --help` | Show help |
|
|
97
98
|
|
|
98
99
|
### Flags
|
|
@@ -101,6 +102,10 @@ icli --reasoning "audit the config on olt#1" # stream reasoning inline
|
|
|
101
102
|
|---|---|
|
|
102
103
|
| `--reasoning` | Stream the model's reasoning inline |
|
|
103
104
|
| `--loader-style STYLE` | Thinking indicator: `spinner` (default), `gradient`, or `minimal` |
|
|
105
|
+
| `--ascii` | Force ASCII-safe glyphs (use if symbols show as boxes) |
|
|
106
|
+
| `--unicode` | Force full Unicode glyphs |
|
|
107
|
+
|
|
108
|
+
> On classic Windows `cmd.exe` iCli auto-switches to ASCII-safe symbols. If it guesses wrong, force it with `icli --ascii` (or set the `ICLI_ASCII=1` environment variable). For the full Unicode look, use **Windows Terminal** or pass `--unicode`.
|
|
104
109
|
|
|
105
110
|
### Slash commands & keys (REPL)
|
|
106
111
|
|
|
@@ -133,6 +138,7 @@ iCli connects to the same AI engine as the in-browser terminal:
|
|
|
133
138
|
- **Streaming output** — answers stream live with a thinking indicator, reasoning, and step-by-step server progress
|
|
134
139
|
- **Plans & connect cards** — proposed fixes render as a reviewable plan (`apply fix` to confirm); device connections show as a connect card
|
|
135
140
|
- **Zero dependencies** — pure Node.js (built-ins only), so it installs instantly and runs the same on Windows, macOS, and Linux
|
|
141
|
+
- **Terminal-aware rendering** — full-width separators, and automatic ASCII-safe glyphs on classic Windows consoles (`cmd.exe` / legacy PowerShell) so nothing shows as broken boxes
|
|
136
142
|
|
|
137
143
|
---
|
|
138
144
|
|
package/bin/icli.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
SLASH_COMMANDS, slashCompleter, slashToQuestion, printSlashMenu, printHistory,
|
|
17
17
|
} from '../src/commands.js';
|
|
18
18
|
import {
|
|
19
|
-
RESET, BOLD, DIM, GRAY, WHITE, ACCENT, GREEN, YELLOW, RED, CYAN, rule,
|
|
19
|
+
RESET, BOLD, DIM, GRAY, WHITE, ACCENT, GREEN, YELLOW, RED, CYAN, rule, glyphs,
|
|
20
20
|
} from '../src/ui.js';
|
|
21
21
|
|
|
22
22
|
const PKG_NAME = 'ispbills-icli';
|
|
@@ -48,6 +48,11 @@ for (let i = 0; i < rawArgs.length; i++) {
|
|
|
48
48
|
|
|
49
49
|
const cmd = positional[0] ?? '';
|
|
50
50
|
|
|
51
|
+
// Glyph overrides — set the env vars the (lazy) glyph resolver in ui.js reads,
|
|
52
|
+
// before anything is rendered. Useful when auto-detection guesses wrong.
|
|
53
|
+
if (has('--ascii')) process.env.ICLI_ASCII = '1';
|
|
54
|
+
if (has('--unicode')) process.env.ICLI_UNICODE = '1';
|
|
55
|
+
|
|
51
56
|
const display = {
|
|
52
57
|
reasoning: has('--reasoning'),
|
|
53
58
|
loader: {
|
|
@@ -78,10 +83,10 @@ function selfUpdate() {
|
|
|
78
83
|
});
|
|
79
84
|
|
|
80
85
|
if (res.status === 0) {
|
|
81
|
-
process.stdout.write(`\n ${GREEN}
|
|
86
|
+
process.stdout.write(`\n ${GREEN}${glyphs.check}${RESET} iCli updated. ${DIM}Restart iCli to use the new version.${RESET}\n\n`);
|
|
82
87
|
return true;
|
|
83
88
|
}
|
|
84
|
-
process.stdout.write(`\n ${RED}
|
|
89
|
+
process.stdout.write(`\n ${RED}${glyphs.cross}${RESET} Update failed. Try manually:\n`);
|
|
85
90
|
process.stdout.write(` ${ACCENT}npm install -g ${PKG_NAME}@latest${RESET}\n`);
|
|
86
91
|
process.stdout.write(` ${DIM}On some systems this needs elevated privileges (sudo / Administrator).${RESET}\n\n`);
|
|
87
92
|
return false;
|
|
@@ -98,6 +103,7 @@ function printHelp() {
|
|
|
98
103
|
['icli logout', 'Log out and clear saved credentials'],
|
|
99
104
|
['icli update', 'Update iCli to the latest version'],
|
|
100
105
|
['icli whoami', 'Show current session info'],
|
|
106
|
+
['icli --version', 'Show the installed iCli version'],
|
|
101
107
|
['icli --help', 'Show this help'],
|
|
102
108
|
].forEach(([c, d]) => console.log(` ${ACCENT}${c.padEnd(20)}${RESET}${DIM}${d}${RESET}`));
|
|
103
109
|
|
|
@@ -105,6 +111,8 @@ function printHelp() {
|
|
|
105
111
|
[
|
|
106
112
|
['--reasoning', 'Stream the model\'s reasoning inline'],
|
|
107
113
|
['--loader-style STYLE', 'spinner | gradient | minimal'],
|
|
114
|
+
['--ascii', 'Force ASCII-safe glyphs (classic Windows cmd.exe)'],
|
|
115
|
+
['--unicode', 'Force full Unicode glyphs'],
|
|
108
116
|
].forEach(([c, d]) => console.log(` ${ACCENT}${c.padEnd(24)}${RESET}${DIM}${d}${RESET}`));
|
|
109
117
|
|
|
110
118
|
console.log(`\n ${BOLD}Slash commands${RESET}`);
|
|
@@ -132,7 +140,7 @@ async function ensureAuth() {
|
|
|
132
140
|
if (!cfg) {
|
|
133
141
|
process.stdout.write(`\n ${YELLOW}No saved credentials. Setting up iCli.${RESET}\n\n`);
|
|
134
142
|
cfg = await loginFlow();
|
|
135
|
-
process.stdout.write(`\n ${GREEN}
|
|
143
|
+
process.stdout.write(`\n ${GREEN}${glyphs.check}${RESET} Logged in as ${WHITE}${cfg.user?.name ?? cfg.user?.email}${RESET}` +
|
|
136
144
|
`${DIM} · ${cfg.user?.role ?? 'operator'}${RESET}\n\n`);
|
|
137
145
|
}
|
|
138
146
|
return cfg;
|
|
@@ -140,7 +148,7 @@ async function ensureAuth() {
|
|
|
140
148
|
|
|
141
149
|
// ── One conversation turn ───────────────────────────────────────────────────────
|
|
142
150
|
async function runTurn(cfg, history, renderer, loader, question, { echo = false } = {}) {
|
|
143
|
-
if (echo) console.log(`\n ${GRAY}
|
|
151
|
+
if (echo) console.log(`\n ${GRAY}${glyphs.prompt}${RESET} ${DIM}${question}${RESET}`);
|
|
144
152
|
history.push({ role: 'user', content: question });
|
|
145
153
|
|
|
146
154
|
const abort = new AbortController();
|
|
@@ -168,7 +176,7 @@ async function runTurn(cfg, history, renderer, loader, question, { echo = false
|
|
|
168
176
|
if (abort.signal.aborted) {
|
|
169
177
|
console.log(`\n ${DIM}Interrupted.${RESET}\n`);
|
|
170
178
|
} else {
|
|
171
|
-
console.log(`\n ${RED}
|
|
179
|
+
console.log(`\n ${RED}${glyphs.cross}${RESET} ${WHITE}${e.message}${RESET}\n`);
|
|
172
180
|
}
|
|
173
181
|
} finally {
|
|
174
182
|
process.removeListener('SIGINT', onSigint);
|
|
@@ -187,7 +195,7 @@ async function interactiveRepl(cfg) {
|
|
|
187
195
|
const history = [];
|
|
188
196
|
const renderer = new TuiRenderer({ display });
|
|
189
197
|
const loader = new Loader(display.loader);
|
|
190
|
-
const PROMPT = () => ` ${ACCENT}
|
|
198
|
+
const PROMPT = () => ` ${ACCENT}${glyphs.prompt}${RESET} `;
|
|
191
199
|
let busy = false;
|
|
192
200
|
|
|
193
201
|
printBanner(cfg, { model: cfg._model });
|
|
@@ -227,7 +235,7 @@ async function interactiveRepl(cfg) {
|
|
|
227
235
|
}
|
|
228
236
|
if (q === '/new') {
|
|
229
237
|
history.length = 0;
|
|
230
|
-
console.log(`\n ${GREEN}
|
|
238
|
+
console.log(`\n ${GREEN}${glyphs.check}${RESET} ${DIM}New conversation started.${RESET}\n`); rl.prompt(); return;
|
|
231
239
|
}
|
|
232
240
|
if (q === '/help' || q === 'help') { printHelp(); rl.prompt(); return; }
|
|
233
241
|
if (q === '/history' || q === 'history') { printHistory(history); rl.prompt(); return; }
|
|
@@ -237,7 +245,7 @@ async function interactiveRepl(cfg) {
|
|
|
237
245
|
}
|
|
238
246
|
if (q === '/logout' || q === 'logout') {
|
|
239
247
|
const cleared = clearConfig();
|
|
240
|
-
console.log(`\n ${cleared ? GREEN +
|
|
248
|
+
console.log(`\n ${cleared ? GREEN + glyphs.check + RESET + ' Logged out — credentials cleared.' : YELLOW + 'Already logged out.' + RESET}`);
|
|
241
249
|
console.log(` ${DIM}Run ${RESET}${ACCENT}icli login${RESET}${DIM} to sign in again.${RESET}\n`);
|
|
242
250
|
rl.close(); return;
|
|
243
251
|
}
|
|
@@ -260,11 +268,12 @@ async function interactiveRepl(cfg) {
|
|
|
260
268
|
|
|
261
269
|
// ── Main ─────────────────────────────────────────────────────────────────────────
|
|
262
270
|
async function main() {
|
|
263
|
-
if (
|
|
271
|
+
if (has('--help') || has('-h') || cmd === 'help') { printHelp(); return; }
|
|
272
|
+
if (has('--version') || has('-v') || cmd === 'version') { console.log(pkgVersion()); return; }
|
|
264
273
|
|
|
265
274
|
if (cmd === 'login') {
|
|
266
275
|
const cfg = await loginFlow();
|
|
267
|
-
console.log(`\n ${GREEN}
|
|
276
|
+
console.log(`\n ${GREEN}${glyphs.check}${RESET} Logged in as ${WHITE}${cfg.user?.name ?? cfg.user?.email}${RESET}${DIM} · ${cfg.user?.role ?? 'operator'}${RESET}`);
|
|
268
277
|
console.log(` ${DIM}Config: ${configPath()}${RESET}\n`);
|
|
269
278
|
return;
|
|
270
279
|
}
|
|
@@ -274,7 +283,7 @@ async function main() {
|
|
|
274
283
|
if (cmd === 'logout') {
|
|
275
284
|
const cleared = clearConfig();
|
|
276
285
|
if (cleared) {
|
|
277
|
-
console.log(`\n ${GREEN}
|
|
286
|
+
console.log(`\n ${GREEN}${glyphs.check}${RESET} Logged out — credentials cleared.`);
|
|
278
287
|
console.log(` ${DIM}Run ${RESET}${ACCENT}icli login${RESET}${DIM} to sign in again.${RESET}\n`);
|
|
279
288
|
} else {
|
|
280
289
|
console.log(`\n ${YELLOW}Not logged in.${RESET} Nothing to clear.\n`);
|
|
@@ -305,6 +314,6 @@ async function main() {
|
|
|
305
314
|
}
|
|
306
315
|
|
|
307
316
|
main().catch((e) => {
|
|
308
|
-
console.log(`\n ${RED}
|
|
317
|
+
console.log(`\n ${RED}${glyphs.cross}${RESET} ${WHITE}${e.message}${RESET}\n`);
|
|
309
318
|
exit(1);
|
|
310
319
|
});
|
package/package.json
CHANGED
package/src/banner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RESET, BOLD, DIM, CYAN, WHITE, ACCENT, GRAY, cols, shortModel, rule } from './ui.js';
|
|
1
|
+
import { RESET, BOLD, DIM, CYAN, WHITE, ACCENT, GRAY, cols, shortModel, rule, hr } from './ui.js';
|
|
2
2
|
|
|
3
3
|
const LOGO = [
|
|
4
4
|
' ██╗ ██████╗ ██████╗ ██████╗ ██╗██╗ ██████╗ ████████╗',
|
|
@@ -9,8 +9,12 @@ const LOGO = [
|
|
|
9
9
|
' ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ',
|
|
10
10
|
];
|
|
11
11
|
|
|
12
|
+
// Widest visible logo line — used to decide when to fall back to a compact header.
|
|
13
|
+
const LOGO_WIDTH = Math.max(...LOGO.map((l) => l.length));
|
|
14
|
+
|
|
12
15
|
/**
|
|
13
16
|
* Full iCopilot banner. On narrow terminals falls back to a compact header.
|
|
17
|
+
* A full-width rule frames the header so it uses the whole window.
|
|
14
18
|
*
|
|
15
19
|
* @param {object} cfg Loaded config ({ url, user, _model }).
|
|
16
20
|
* @param {object} opts { model }
|
|
@@ -20,13 +24,11 @@ export function printBanner(cfg = {}, opts = {}) {
|
|
|
20
24
|
const model = opts.model || cfg._model || 'IspBills AI';
|
|
21
25
|
|
|
22
26
|
console.log();
|
|
23
|
-
if (width >=
|
|
27
|
+
if (width >= LOGO_WIDTH) {
|
|
24
28
|
for (const line of LOGO) console.log(ACCENT + BOLD + line + RESET);
|
|
25
29
|
console.log(` ${DIM}iCopilot — network engineer in your terminal${RESET}`);
|
|
26
30
|
} else {
|
|
27
|
-
console.log(rule());
|
|
28
31
|
console.log(` ${BOLD}iCopilot${RESET} ${DIM}— IspBills AI terminal${RESET}`);
|
|
29
|
-
console.log(rule());
|
|
30
32
|
}
|
|
31
33
|
console.log();
|
|
32
34
|
|
|
@@ -39,4 +41,6 @@ export function printBanner(cfg = {}, opts = {}) {
|
|
|
39
41
|
console.log();
|
|
40
42
|
console.log(` ${DIM}Type a message to start. ${RESET}${ACCENT}/help${RESET}${DIM} for commands · ${RESET}${ACCENT}Ctrl+C${RESET}${DIM} to exit${RESET}`);
|
|
41
43
|
console.log();
|
|
44
|
+
hr();
|
|
45
|
+
console.log();
|
|
42
46
|
}
|
package/src/loader.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { DIM, RESET } from './ui.js';
|
|
2
|
-
|
|
3
|
-
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
1
|
+
import { DIM, RESET, getSpinner } from './ui.js';
|
|
4
2
|
|
|
5
3
|
const GRADIENT_COLORS = [
|
|
6
4
|
'\x1b[38;5;240m',
|
|
@@ -68,7 +66,8 @@ export class Loader {
|
|
|
68
66
|
}
|
|
69
67
|
case 'spinner':
|
|
70
68
|
default: {
|
|
71
|
-
const
|
|
69
|
+
const frames = getSpinner();
|
|
70
|
+
const char = frames[this.frame % frames.length];
|
|
72
71
|
process.stdout.write(`\r ${DIM}${char} ${text}…${RESET}`);
|
|
73
72
|
break;
|
|
74
73
|
}
|
package/src/renderer.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
RESET, BOLD, DIM, ITALIC, CYAN, GREEN, YELLOW, RED, GRAY, WHITE, ACCENT,
|
|
3
|
-
cols, shortModel,
|
|
3
|
+
cols, shortModel, glyphs, hr,
|
|
4
4
|
} from './ui.js';
|
|
5
5
|
|
|
6
6
|
/** Pick a status icon from the content of a server progress line. */
|
|
7
7
|
function statusIcon(line) {
|
|
8
8
|
const l = line.toLowerCase();
|
|
9
|
-
if (/unreachable|skip|fail|error|denied|timeout/.test(l)) return `${RED}
|
|
10
|
-
if (/done|success|found|complet|online|ready|ok\b/.test(l)) return `${GREEN}
|
|
11
|
-
if (/batch|sub.?agent|fleet|parallel/.test(l)) return `${ACCENT}
|
|
12
|
-
if (/\[\d+\/\d+\]/.test(l)) return `${ACCENT}
|
|
13
|
-
if (/probe|reachab|ping/.test(l)) return `${YELLOW}
|
|
14
|
-
if (/connect|ssh|telnet|api|session/.test(l)) return `${ACCENT}
|
|
15
|
-
if (/read|fetch|get|query|look|search/.test(l)) return `${GRAY}
|
|
16
|
-
return `${GRAY}
|
|
9
|
+
if (/unreachable|skip|fail|error|denied|timeout/.test(l)) return `${RED}${glyphs.cross}${RESET}`;
|
|
10
|
+
if (/done|success|found|complet|online|ready|ok\b/.test(l)) return `${GREEN}${glyphs.check}${RESET}`;
|
|
11
|
+
if (/batch|sub.?agent|fleet|parallel/.test(l)) return `${ACCENT}${glyphs.ring}${RESET}`;
|
|
12
|
+
if (/\[\d+\/\d+\]/.test(l)) return `${ACCENT}${glyphs.diamond}${RESET}`;
|
|
13
|
+
if (/probe|reachab|ping/.test(l)) return `${YELLOW}${glyphs.target}${RESET}`;
|
|
14
|
+
if (/connect|ssh|telnet|api|session/.test(l)) return `${ACCENT}${glyphs.arrow}${RESET}`;
|
|
15
|
+
if (/read|fetch|get|query|look|search/.test(l)) return `${GRAY}${glyphs.down}${RESET}`;
|
|
16
|
+
return `${GRAY}${glyphs.bullet}${RESET}`;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -67,7 +67,7 @@ export class TuiRenderer {
|
|
|
67
67
|
let out = line
|
|
68
68
|
.replace(/\*\*(.+?)\*\*/g, `${BOLD}$1${RESET}`)
|
|
69
69
|
.replace(/`([^`]+)`/g, `${CYAN}$1${RESET}`)
|
|
70
|
-
.replace(/^(\s*)[-*]\s+/, `$1${ACCENT}
|
|
70
|
+
.replace(/^(\s*)[-*]\s+/, `$1${ACCENT}${glyphs.bullet}${RESET} `);
|
|
71
71
|
return ' ' + out;
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -89,7 +89,7 @@ export class TuiRenderer {
|
|
|
89
89
|
if (!this.display.reasoning) return;
|
|
90
90
|
this.endStreaming();
|
|
91
91
|
if (!this.reasoningOpen) {
|
|
92
|
-
process.stdout.write(`\n ${ACCENT}
|
|
92
|
+
process.stdout.write(`\n ${ACCENT}${glyphs.reasoning}${RESET} ${DIM}Reasoning${RESET}\n ${GRAY}`);
|
|
93
93
|
this.reasoningOpen = true;
|
|
94
94
|
}
|
|
95
95
|
process.stdout.write(delta.replace(/\n/g, `\n ${GRAY}`));
|
|
@@ -126,16 +126,16 @@ export class TuiRenderer {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
renderPlan(reply) {
|
|
129
|
-
console.log(`\n ${ACCENT}${BOLD}
|
|
129
|
+
console.log(`\n ${ACCENT}${BOLD}${glyphs.diamond} Plan${RESET}`);
|
|
130
130
|
if (reply.summary) console.log(` ${DIM}${reply.summary}${RESET}`);
|
|
131
131
|
const steps = reply.steps || [];
|
|
132
132
|
steps.forEach((s, i) => {
|
|
133
133
|
const last = i === steps.length - 1;
|
|
134
|
-
const branch = `${GRAY}${last ?
|
|
134
|
+
const branch = `${GRAY}${last ? glyphs.branchEnd : glyphs.branchMid}${RESET}`;
|
|
135
135
|
const risk =
|
|
136
|
-
(s.risk === 'destructive' || s.risk === 'high') ? ` ${RED}
|
|
137
|
-
(s.risk === 'caution' || s.risk === 'medium') ? ` ${YELLOW}
|
|
138
|
-
const blocked = s.blocked ? ` ${RED}
|
|
136
|
+
(s.risk === 'destructive' || s.risk === 'high') ? ` ${RED}${glyphs.warn} destructive${RESET}` :
|
|
137
|
+
(s.risk === 'caution' || s.risk === 'medium') ? ` ${YELLOW}${glyphs.bolt} caution${RESET}` : '';
|
|
138
|
+
const blocked = s.blocked ? ` ${RED}${glyphs.block} blocked${RESET}` : '';
|
|
139
139
|
console.log(` ${branch} ${DIM}${i + 1}.${RESET} ${YELLOW}${s.cmd || ''}${RESET}${risk}${blocked}`);
|
|
140
140
|
const why = s.why || s.purpose;
|
|
141
141
|
if (why) console.log(` ${GRAY}${why}${RESET}`);
|
|
@@ -147,7 +147,7 @@ export class TuiRenderer {
|
|
|
147
147
|
const d = reply.device ?? reply ?? {};
|
|
148
148
|
const proto = d.proto ? ` ${DIM}via ${d.proto}${RESET}` : '';
|
|
149
149
|
console.log(
|
|
150
|
-
`\n ${GREEN}
|
|
150
|
+
`\n ${GREEN}${glyphs.plug}${RESET} ${BOLD}${(d.kind ?? '?').toUpperCase()} #${d.id ?? '?'}${RESET}${proto}`
|
|
151
151
|
);
|
|
152
152
|
if (reply.note) console.log(` ${DIM}${reply.note}${RESET}`);
|
|
153
153
|
console.log(` ${DIM}Establishing session…${RESET}`);
|
|
@@ -155,8 +155,10 @@ export class TuiRenderer {
|
|
|
155
155
|
|
|
156
156
|
renderFooter(meta) {
|
|
157
157
|
const parts = [meta.model ? shortModel(meta.model) : null, meta.user, meta.role].filter(Boolean);
|
|
158
|
+
console.log();
|
|
159
|
+
hr();
|
|
158
160
|
if (parts.length) {
|
|
159
|
-
console.log(
|
|
161
|
+
console.log(` ${GRAY}${parts.join(' · ')}${RESET}`);
|
|
160
162
|
}
|
|
161
163
|
}
|
|
162
164
|
}
|
package/src/ui.js
CHANGED
|
@@ -53,6 +53,55 @@ export function rule(color = GRAY) {
|
|
|
53
53
|
return paint(color, '─'.repeat(cols()));
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// ── Glyphs ─────────────────────────────────────────────────────────────────
|
|
57
|
+
// Classic Windows consoles (cmd.exe / legacy PowerShell using the Consolas or
|
|
58
|
+
// raster font) lack many Unicode dingbats, geometric shapes, arrows, braille
|
|
59
|
+
// and emoji — they render as "tofu" boxes. Windows Terminal sets WT_SESSION,
|
|
60
|
+
// so we fall back to ASCII on a classic Windows console. This can be forced
|
|
61
|
+
// either way with the ICLI_ASCII / ICLI_UNICODE env vars (or --ascii/--unicode).
|
|
62
|
+
//
|
|
63
|
+
// Selection is evaluated lazily (per access) so the CLI can set the env vars
|
|
64
|
+
// from command-line flags before the first glyph is rendered.
|
|
65
|
+
function truthy(v) {
|
|
66
|
+
return v === '1' || v === 'true' || v === 'yes' || v === 'on';
|
|
67
|
+
}
|
|
68
|
+
export function asciiSafe() {
|
|
69
|
+
if (truthy(process.env.ICLI_UNICODE)) return false;
|
|
70
|
+
if (truthy(process.env.ICLI_ASCII)) return true;
|
|
71
|
+
return process.platform === 'win32' && !process.env.WT_SESSION;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const ASCII_GLYPHS = {
|
|
75
|
+
prompt: '>', bullet: '-', diamond: '*', ring: '*', target: 'o',
|
|
76
|
+
arrow: '->', down: 'v', check: 'OK', cross: 'XX', warn: '!!',
|
|
77
|
+
bolt: '!', block: 'X', plug: '>>', reasoning: '~',
|
|
78
|
+
branchMid: '|-', branchEnd: '`-', vbar: '|',
|
|
79
|
+
};
|
|
80
|
+
const UNICODE_GLYPHS = {
|
|
81
|
+
prompt: '❯', bullet: '·', diamond: '◆', ring: '◈', target: '◎',
|
|
82
|
+
arrow: '⇢', down: '↓', check: '✓', cross: '✗', warn: '⚠',
|
|
83
|
+
bolt: '⚡', block: '⛔', plug: '🔌', reasoning: '◆',
|
|
84
|
+
branchMid: '├─', branchEnd: '└─', vbar: '│',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Proxy resolves each glyph at access time, honouring runtime env overrides.
|
|
88
|
+
export const glyphs = new Proxy({}, {
|
|
89
|
+
get: (_t, key) => (asciiSafe() ? ASCII_GLYPHS : UNICODE_GLYPHS)[key],
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const ASCII_SPINNER = ['|', '/', '-', '\\'];
|
|
93
|
+
const UNICODE_SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
94
|
+
|
|
95
|
+
/** Spinner frames — braille on modern terminals, ASCII on classic Windows. */
|
|
96
|
+
export function getSpinner() {
|
|
97
|
+
return asciiSafe() ? ASCII_SPINNER : UNICODE_SPINNER;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Print a full-width horizontal rule (dim by default). */
|
|
101
|
+
export function hr(color = GRAY) {
|
|
102
|
+
process.stdout.write(paint(color, '─'.repeat(cols())) + '\n');
|
|
103
|
+
}
|
|
104
|
+
|
|
56
105
|
/** Shorten a fully-qualified model id for display (e.g. "anthropic/claude-…"). */
|
|
57
106
|
export function shortModel(m) {
|
|
58
107
|
if (!m) return '';
|