aegiscode 6.0.0 → 6.1.0
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 +69 -45
- package/package.json +2 -2
- package/scripts/demo.mjs +1 -1
- package/src/app.js +100 -24
- package/src/art.js +219 -29
- package/src/commands.js +150 -50
- package/src/fuzzy.js +116 -0
- package/src/markdown.js +303 -0
- package/src/overlays.js +286 -0
- package/src/render.js +235 -120
- package/src/screen.js +196 -21
- package/src/theme.js +102 -66
package/README.md
CHANGED
|
@@ -72,42 +72,60 @@ Every command above that talks to AEGIS names a tool from `mcp/tools.js`, and
|
|
|
72
72
|
the test suite asserts both directions: no command points at a tool that does
|
|
73
73
|
not exist, and no tool is unreachable from the prompt.
|
|
74
74
|
|
|
75
|
+
Command names, aliases and categories follow `aegiscodex-dev`'s registry, so the
|
|
76
|
+
vocabulary matches the other terminal host: the AEGIS family is spelled
|
|
77
|
+
`/aegis-ask`, `/aegis-status`, `/aegis-recall`, `/aegis-remember`, `/aegis-import`
|
|
78
|
+
(with `/ask`, `/status`, `/recall`, `/remember` kept as aliases), and the session
|
|
79
|
+
and model controls are its `help`/`?`/`h`, `exit`/`quit`, `clear`/`cls`,
|
|
80
|
+
`theme`/`t`, `version`/`v` and `model`/`m`. Commands that need a local agent loop
|
|
81
|
+
this client deliberately does not have — `/login`, `/doctor`, `/permissions`,
|
|
82
|
+
`/mcp`, `/skills`, `/hooks`, `/agents`, `/resume` and a few more — are listed but
|
|
83
|
+
answer honestly with the reason and the nearest working alternative rather than
|
|
84
|
+
pretending to work.
|
|
85
|
+
|
|
75
86
|
## What it looks like
|
|
76
87
|
|
|
77
88
|
```
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
89
|
+
━────────────────────────────────────────────────────────────────────────────━
|
|
90
|
+
|
|
91
|
+
✦ █████▓▓░ ▓▓▓▓▓▓▓▓▓ ✦ ▒▒▒▒▒▒▒▒▒▒▒▒
|
|
92
|
+
✦ ███▓░ ░░ ▓▓▓▓▓▓▓░░░ ▒▒▒▒▒▒▒▒▒▒▒▒▒
|
|
93
|
+
░░░░ ███▓░ ▓▓▓▓░░░░░░ ██▒▒▒▒▒▒▒▒▒▒▒▒▓▓
|
|
94
|
+
░░░░░░░░ ███▓░ ░░░░░░ ██▒▒▒▒▒▒▒▒▒▒▒▒▓
|
|
95
|
+
░░░░░░░░░░░░ ███▓░ ✦ · · ▒▒▒▒▒▒▒▒▒▒▒▒▒▒
|
|
96
|
+
██▓░░ ▓ ░░░░░░░░░░░░
|
|
97
|
+
░▓▓███▓▓░
|
|
98
|
+
▐▛███▜▌
|
|
99
|
+
▝▜█████▛▘
|
|
100
|
+
▘▘ ▝▝
|
|
101
|
+
|
|
102
|
+
Welcome to AEGIS Code
|
|
103
|
+
v6.1.0 · Cloud brain in your shell.
|
|
104
|
+
|
|
105
|
+
───────────────────────────────── aegiscode ──────────────────────────────────
|
|
106
|
+
version v6.1.0
|
|
107
|
+
model nexus-brain
|
|
108
|
+
base https://aegiscloud.org
|
|
109
|
+
key aegis_••••4f2a
|
|
110
|
+
render streaming
|
|
111
|
+
|
|
112
|
+
type /help for commands · /quit to exit
|
|
113
|
+
|
|
114
|
+
❯ summarise what changed in the token accounting
|
|
115
|
+
● Three things changed, and one of them was costing you money:
|
|
116
|
+
|
|
117
|
+
- the pool merges worker usage instead of overwriting it
|
|
118
|
+
- cache reads and writes are billed, not ignored
|
|
119
|
+
- a zero-token call no longer refunds its reservation
|
|
120
|
+
|
|
121
|
+
merge_usage({input_tokens: 1250}, {output_tokens: 312})
|
|
122
|
+
=> 1562 total
|
|
123
|
+
|
|
124
|
+
Use /balance to see tokens beside € on every row.
|
|
125
|
+
⎿ nexus-brain · 1,562 tok · 1,250/312 · €0.0007 · 4.2s · 4 calls
|
|
126
|
+
|
|
127
|
+
* Consulting… (2.1s) esc to interrupt
|
|
128
|
+
aegis · nexus-brain · 1,562 tok · €0.0007 · stream ctrl+c quit
|
|
111
129
|
```
|
|
112
130
|
|
|
113
131
|
Render it yourself, with no key and no network:
|
|
@@ -119,12 +137,15 @@ node cli/scripts/demo.mjs # from a source checkout
|
|
|
119
137
|
|
|
120
138
|
## Design notes
|
|
121
139
|
|
|
122
|
-
**It
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
140
|
+
**It looks like `aegiscodex-dev` on purpose.** The gold/coral/lavender palette,
|
|
141
|
+
the `✦`-studded welcome mark (mascot, crescent moon and diving whale), the `❯`
|
|
142
|
+
prompt, the `⎿` hook rows and the `✻` spinner line are the `aegiscodex-dev`
|
|
143
|
+
design system, adopted wholesale rather than approximated: the palette, glyphs,
|
|
144
|
+
welcome art, working verbs and command vocabulary all come from it, and
|
|
145
|
+
`test/cli-conformance.test.mjs` pins every RGB triple, glyph, spinner frame, verb
|
|
146
|
+
and art row to that source so the two hosts cannot drift apart. (An earlier
|
|
147
|
+
revision of this CLI was a deliberate divergence — a violet/cyan "Signal" theme
|
|
148
|
+
with a test asserting it. That direction is gone.)
|
|
128
149
|
|
|
129
150
|
**A linear transcript, not a full-screen TUI.** Finished turns are written once
|
|
130
151
|
to the scrollback; only the one live line (spinner, elapsed time) is redrawn.
|
|
@@ -139,12 +160,15 @@ free usage next to a token count.
|
|
|
139
160
|
## Tests
|
|
140
161
|
|
|
141
162
|
```bash
|
|
142
|
-
node ../../test/cli-
|
|
143
|
-
node ../../test/cli-render.test.mjs
|
|
144
|
-
node ../../test/cli-
|
|
145
|
-
node ../../test/cli-
|
|
146
|
-
node ../../test/cli-
|
|
147
|
-
|
|
163
|
+
node ../../test/cli-conformance.test.mjs # the design guard: palette, glyphs, art, verbs
|
|
164
|
+
node ../../test/cli-render.test.mjs # width safety, accounting, live region
|
|
165
|
+
node ../../test/cli-overlays.test.mjs # the / palette, model + effort pickers, resume list
|
|
166
|
+
node ../../test/cli-fuzzy.test.mjs # palette ranking and match positions
|
|
167
|
+
node ../../test/cli-markdown.test.mjs # span-line markdown, cell widths
|
|
168
|
+
node ../../test/cli-tools.test.mjs # registry parity with the MCP host + dispatch
|
|
169
|
+
node ../../test/cli-run.test.mjs # the real binary against a real backend
|
|
170
|
+
node ../../test/cli-package.test.mjs # the published layout, isolated from the repo
|
|
171
|
+
npm test # all of the above
|
|
148
172
|
```
|
|
149
173
|
|
|
150
174
|
`npm run predist` stages `client/`, `mcp/tools.js` and the desktop's `usage.js`
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aegiscode",
|
|
3
3
|
"productName": "AEGIS Code",
|
|
4
|
-
"version": "6.
|
|
4
|
+
"version": "6.1.0",
|
|
5
5
|
"description": "aegiscode — the command-line version of AEGIS Desktop. The shared tool surface in your shell, over the same thin transport and tool registry as the MCP plugin and the desktop app. Ships transport + UI only; no brain.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "AEGIS Code",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"start": "node bin/aegiscode.js",
|
|
32
32
|
"predist": "node scripts/predist.mjs",
|
|
33
33
|
"prepublishOnly": "npm run predist",
|
|
34
|
-
"check": "node --check bin/aegiscode.js && node --check src/theme.js && node --check src/art.js && node --check src/screen.js && node --check src/format.js && node --check src/render.js && node --check src/commands.js && node --check src/deps.js && node --check src/app.js && node --check scripts/predist.mjs",
|
|
34
|
+
"check": "node --check bin/aegiscode.js && node --check src/theme.js && node --check src/art.js && node --check src/screen.js && node --check src/format.js && node --check src/fuzzy.js && node --check src/markdown.js && node --check src/render.js && node --check src/overlays.js && node --check src/commands.js && node --check src/deps.js && node --check src/app.js && node --check scripts/predist.mjs",
|
|
35
35
|
"test": "for f in ../../test/cli-*.test.mjs; do node \"$f\" || exit 1; done"
|
|
36
36
|
},
|
|
37
37
|
"keywords": [
|
package/scripts/demo.mjs
CHANGED
|
@@ -87,6 +87,6 @@ push(render.renderStatus(ctx, { model: 'nexus-brain', tokens: 1562, spend: 0.000
|
|
|
87
87
|
process.stdout.write((plain ? out.map(stripAnsi) : out).join('\n') + '\n\n');
|
|
88
88
|
if (!plain) {
|
|
89
89
|
process.stdout.write(
|
|
90
|
-
stripAnsi(`${art.
|
|
90
|
+
stripAnsi(`${art.WELCOME_TITLE} — ${out.length} lines rendered at ${width} cols${ctx.light ? ' (light)' : ''}\n`)
|
|
91
91
|
);
|
|
92
92
|
}
|
package/src/app.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
const readline = require('node:readline');
|
|
15
15
|
const { createTools, createClient, usageTokens } = require('./deps.js');
|
|
16
|
-
const { GLYPH, VERBS, themeOf, RESET } = require('./theme.js');
|
|
16
|
+
const { GLYPH, VERBS, themeOf, RESET, BOLD } = require('./theme.js');
|
|
17
17
|
const { LiveRegion, termWidth, EC, w } = require('./screen.js');
|
|
18
18
|
const { parseLine, findCommand, COMMANDS } = require('./commands.js');
|
|
19
19
|
const render = require('./render.js');
|
|
@@ -289,21 +289,67 @@ function createApp(options = {}) {
|
|
|
289
289
|
emit(render.renderToolResult(ctx(), name, text, width()));
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
// Category order and labels, matching aegiscodex-dev's palette.
|
|
293
|
+
const CATEGORY_ORDER = ['aegis', 'model', 'session', 'data', 'auth', 'support', 'workspace'];
|
|
294
|
+
const CATEGORY_LABEL = {
|
|
295
|
+
aegis: 'Aegis plugin',
|
|
296
|
+
model: 'Model & behavior',
|
|
297
|
+
session: 'Session & context',
|
|
298
|
+
data: 'Data',
|
|
299
|
+
auth: 'Auth',
|
|
300
|
+
support: 'Support',
|
|
301
|
+
workspace: 'Workspace',
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* The nearest routable name to a mistyped one: a prefix of it, or a name it
|
|
306
|
+
* is a prefix of. Deliberately simple — `cli/src/fuzzy.js` is a separate
|
|
307
|
+
* workstream and may not exist, so this must not depend on it.
|
|
308
|
+
*/
|
|
309
|
+
function nearest(name) {
|
|
310
|
+
const n = String(name || '').toLowerCase();
|
|
311
|
+
if (!n) return null;
|
|
312
|
+
let best = null;
|
|
313
|
+
for (const c of COMMANDS) {
|
|
314
|
+
for (const cand of [c.name, ...(c.aliases || [])]) {
|
|
315
|
+
if (cand === n) continue;
|
|
316
|
+
if (cand.startsWith(n) || n.startsWith(cand)) {
|
|
317
|
+
if (best == null || Math.abs(cand.length - n.length) < Math.abs(best.length - n.length)) {
|
|
318
|
+
best = cand;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return best;
|
|
324
|
+
}
|
|
325
|
+
|
|
292
326
|
function printHelp() {
|
|
327
|
+
const t = themeOf(ctx());
|
|
293
328
|
emit([render.renderHeading(ctx(), 'commands', width())]);
|
|
294
|
-
const rows = COMMANDS.map((c) => {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
329
|
+
const rows = COMMANDS.filter((c) => !c.unavailable).map((c) => ({
|
|
330
|
+
cat: c.category || 'other',
|
|
331
|
+
usage: `/${c.name}${c.args ? ' ' + c.args : ''}`,
|
|
332
|
+
desc: c.desc,
|
|
333
|
+
aliases: (c.aliases || []).map((a) => `/${a}`).join(' '),
|
|
334
|
+
}));
|
|
298
335
|
const widest = rows.reduce((m, r) => Math.max(m, r.usage.length), 0);
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
emit([
|
|
303
|
-
|
|
304
|
-
|
|
336
|
+
for (const cat of CATEGORY_ORDER) {
|
|
337
|
+
const group = rows.filter((r) => r.cat === cat);
|
|
338
|
+
if (!group.length) continue;
|
|
339
|
+
emit(['', `${t.dim}${BOLD}${CATEGORY_LABEL[cat] || cat}${RESET}`]);
|
|
340
|
+
for (const r of group) {
|
|
341
|
+
const alias = r.aliases ? `${t.dim} (${r.aliases})${RESET}` : '';
|
|
342
|
+
emit([
|
|
343
|
+
` ${t.gold}${r.usage}${RESET}${' '.repeat(Math.max(1, widest - r.usage.length + 2))}` +
|
|
344
|
+
`${t.white}${r.desc}${RESET}${alias}`,
|
|
345
|
+
]);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const unavail = COMMANDS.filter((c) => c.unavailable).map((c) => `/${c.name}`);
|
|
349
|
+
if (unavail.length) {
|
|
350
|
+
emit(['', `${t.dim}not available in this client: ${unavail.join(' ')}${RESET}`]);
|
|
305
351
|
}
|
|
306
|
-
emit(['', render.renderNotice(ctx(), 'info', `plain text is a prompt ${GLYPH.bullet} /
|
|
352
|
+
emit(['', render.renderNotice(ctx(), 'info', `plain text is a prompt ${GLYPH.bullet} /exit exits`)]);
|
|
307
353
|
}
|
|
308
354
|
|
|
309
355
|
/** Handle one line of input. Returns false when the session should end. */
|
|
@@ -316,7 +362,24 @@ function createApp(options = {}) {
|
|
|
316
362
|
return true;
|
|
317
363
|
}
|
|
318
364
|
if (parsed.kind === 'unknown') {
|
|
319
|
-
|
|
365
|
+
const alt = nearest(parsed.name);
|
|
366
|
+
emit(
|
|
367
|
+
render.renderNotice(
|
|
368
|
+
ctx(),
|
|
369
|
+
'error',
|
|
370
|
+
`unknown command: /${parsed.name}${alt ? ` — did you mean /${alt}?` : ` — try /help`}`
|
|
371
|
+
)
|
|
372
|
+
);
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
if (parsed.kind === 'unavailable') {
|
|
376
|
+
const c = parsed.command;
|
|
377
|
+
emit(render.renderNotice(ctx(), 'warn', `/${c.name} is not available in aegiscode — ${c.why}`));
|
|
378
|
+
if (c.alt) emit(render.renderNotice(ctx(), 'info', `try ${c.alt} instead`));
|
|
379
|
+
else {
|
|
380
|
+
const alt = nearest(c.name);
|
|
381
|
+
if (alt) emit(render.renderNotice(ctx(), 'info', `try /${alt} instead`));
|
|
382
|
+
}
|
|
320
383
|
return true;
|
|
321
384
|
}
|
|
322
385
|
|
|
@@ -325,15 +388,27 @@ function createApp(options = {}) {
|
|
|
325
388
|
|
|
326
389
|
if (cmd.local) {
|
|
327
390
|
switch (cmd.local) {
|
|
328
|
-
case '
|
|
391
|
+
case 'exit':
|
|
329
392
|
return false;
|
|
330
393
|
case 'clear':
|
|
331
394
|
out.write(EC.clearScreen);
|
|
395
|
+
// aegiscodex-dev's /clear starts a new session with empty context, so
|
|
396
|
+
// the session tallies reset too (the transcript is not persisted here).
|
|
397
|
+
session.turns = 0;
|
|
398
|
+
session.calls = 0;
|
|
399
|
+
session.tokens = 0;
|
|
400
|
+
session.inputTokens = 0;
|
|
401
|
+
session.outputTokens = 0;
|
|
402
|
+
session.cost = 0;
|
|
403
|
+
session.startedAt = Date.now();
|
|
332
404
|
emit(bannerLines());
|
|
333
405
|
return true;
|
|
334
406
|
case 'help':
|
|
335
407
|
printHelp();
|
|
336
408
|
return true;
|
|
409
|
+
case 'version':
|
|
410
|
+
emit(render.renderNotice(ctx(), 'info', `aegiscode v${VERSION}`));
|
|
411
|
+
return true;
|
|
337
412
|
case 'model':
|
|
338
413
|
if (!arg) {
|
|
339
414
|
emit(render.renderNotice(ctx(), 'info', `model: ${opts.model || 'server default'}`));
|
|
@@ -353,13 +428,14 @@ function createApp(options = {}) {
|
|
|
353
428
|
opts.light = arg ? /^light/i.test(arg) : !opts.light;
|
|
354
429
|
emit(render.renderNotice(ctx(), 'ok', `theme: ${opts.light ? 'light' : 'dark'}`));
|
|
355
430
|
return true;
|
|
356
|
-
case 'cost':
|
|
431
|
+
case 'cost':
|
|
432
|
+
case 'tokens': {
|
|
357
433
|
const t = themeOf(ctx());
|
|
358
434
|
emit([render.renderHeading(ctx(), 'session', width())]);
|
|
359
435
|
emit([
|
|
360
|
-
` tokens ${
|
|
361
|
-
|
|
362
|
-
` spend ${
|
|
436
|
+
` tokens ${t.white}${fmtTokens(session.tokens)}${RESET} ` +
|
|
437
|
+
t.gray + `(${fmtTokens(session.inputTokens)} in / ${fmtTokens(session.outputTokens)} out)` + RESET,
|
|
438
|
+
` spend ${t.green}${fmtEur(session.cost)}${RESET}`,
|
|
363
439
|
` calls ${session.calls}`,
|
|
364
440
|
` balance ${session.balance == null ? 'unknown' : fmtEur(session.balance)}`,
|
|
365
441
|
` elapsed ${fmtElapsed(Date.now() - session.startedAt)}`,
|
|
@@ -397,7 +473,7 @@ function createApp(options = {}) {
|
|
|
397
473
|
}
|
|
398
474
|
args = { ...args, api_key: key };
|
|
399
475
|
}
|
|
400
|
-
if (cmd.
|
|
476
|
+
if (cmd.tool === 'aegis_ask') return await runPrompt(args.prompt);
|
|
401
477
|
await runTool(cmd.tool, args);
|
|
402
478
|
} catch (e) {
|
|
403
479
|
emit(render.renderNotice(ctx(), 'error', e.message));
|
|
@@ -412,7 +488,7 @@ function createApp(options = {}) {
|
|
|
412
488
|
}
|
|
413
489
|
return new Promise((resolve) => {
|
|
414
490
|
const t = themeOf(ctx());
|
|
415
|
-
out.write(
|
|
491
|
+
out.write(t.gray + promptText + RESET);
|
|
416
492
|
const stdin = process.stdin;
|
|
417
493
|
let buf = '';
|
|
418
494
|
stdin.setRawMode(true);
|
|
@@ -477,9 +553,9 @@ function createApp(options = {}) {
|
|
|
477
553
|
if (tokens != null) {
|
|
478
554
|
const t = themeOf(ctx());
|
|
479
555
|
out.write(
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
556
|
+
t.dim + GLYPH.spend + ' ' + t.blue + (res.model || 'aegis') + RESET +
|
|
557
|
+
t.dim + ' ' + GLYPH.bullet + ' ' + RESET +
|
|
558
|
+
t.white + `${fmtTokens(tokens)} tok` + RESET + '\n'
|
|
483
559
|
);
|
|
484
560
|
}
|
|
485
561
|
}
|
|
@@ -493,7 +569,7 @@ function createApp(options = {}) {
|
|
|
493
569
|
out.write('\n');
|
|
494
570
|
|
|
495
571
|
const rl = options.readline || readline.createInterface({ input: process.stdin, output: out, terminal: true });
|
|
496
|
-
const promptStr =
|
|
572
|
+
const promptStr = themeOf(ctx()).gold + GLYPH.prompt + ' ' + RESET;
|
|
497
573
|
rl.setPrompt(promptStr);
|
|
498
574
|
rl.prompt();
|
|
499
575
|
|
package/src/art.js
CHANGED
|
@@ -1,45 +1,235 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Welcome art — the aegiscodex-dev mark, adopted wholesale.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* The composition is copied from `aegiscodex-dev/src/art.js`, which took the
|
|
7
|
+
* glyph rows from the shipped Claude Code binary: a feather plume (░), a top hat
|
|
8
|
+
* (█ ▓ ▒), the spark face (▐▛███▜▌ / ▝▜█████▛▘ / ▘▘ ▝▝), plus a crescent moon and
|
|
9
|
+
* a diving whale for the second brain. Side-by-side when the terminal is wide
|
|
10
|
+
* enough, stacked when it isn't.
|
|
10
11
|
*
|
|
11
|
-
* Every row is the same display width (
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* spacing the shorter `AEGIS` mark used, now that the host and the CLI have been
|
|
16
|
-
* folded onto one name. `test/cli-identity.test.mjs` strips the spacing and
|
|
17
|
-
* asserts the mark still names the product.
|
|
12
|
+
* Every row of a block is the same display width, so `center()` and
|
|
13
|
+
* `joinSideBySide()` stay exact. `test/cli-conformance.test.mjs` asserts both
|
|
14
|
+
* that property and that these rows still match aegiscodex-dev's source, so the
|
|
15
|
+
* mark can't drift.
|
|
18
16
|
*/
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
'
|
|
26
|
-
'
|
|
18
|
+
// The mascot face — exact glyphs from the binary (msS template).
|
|
19
|
+
const FACE = [' ▐▛███▜▌', '▝▜█████▛▘', ' ▘▘ ▝▝'];
|
|
20
|
+
|
|
21
|
+
// Top hat + feather plume, composed from the extracted row glyphs.
|
|
22
|
+
const HAT = [
|
|
23
|
+
' ✦ ░░░░░░░░░░ ✦',
|
|
24
|
+
' ░░░░░░░░░░░░░░░░',
|
|
25
|
+
' █████▓▓░',
|
|
26
|
+
' ███▓░',
|
|
27
|
+
' ███▓░',
|
|
28
|
+
' ███▓░',
|
|
29
|
+
' ░▓▓███▓▓░',
|
|
30
|
+
' ▒▒░░▒▒ ▒ ▒▒',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// Plume + hat + face: the left half of the welcome mark.
|
|
34
|
+
const MASCOT_ART = [
|
|
35
|
+
' ✦ █████▓▓░',
|
|
36
|
+
' ✦ ███▓░ ░░',
|
|
37
|
+
' ░░░░ ███▓░',
|
|
38
|
+
' ░░░░░░░░ ███▓░',
|
|
39
|
+
' ░░░░░░░░░░░░ ███▓░',
|
|
40
|
+
' ██▓░░ ▓',
|
|
41
|
+
' ░▓▓███▓▓░',
|
|
42
|
+
...FACE,
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// Crescent moon in a starry night sky — lit rim (▓) with a faint earthshine
|
|
46
|
+
// body (░). Rows are equal code-point length (13).
|
|
47
|
+
const MOON = [
|
|
48
|
+
' ✦ ▓▓▓ ✦ ',
|
|
49
|
+
' ▓▓▓▓▓ ✦',
|
|
50
|
+
' ▓▓▓▓▓▓▓▓▓ ✦',
|
|
51
|
+
' ▓▓▓▓▓▓▓░░░ ',
|
|
52
|
+
' ▓▓▓▓░░░░░░ ',
|
|
53
|
+
' ░░░░░░ ',
|
|
54
|
+
' ✦ · · ',
|
|
27
55
|
];
|
|
28
56
|
|
|
29
|
-
|
|
30
|
-
|
|
57
|
+
// The diving whale: spout (░), eye (██) on the rounded head, body (▒) sweeping
|
|
58
|
+
// right to a small forked fluke (▓), water line (░) beneath. Rows are equal
|
|
59
|
+
// code-point length (18).
|
|
60
|
+
const WHALE = [
|
|
61
|
+
' ✦ ░ ░ ░ ',
|
|
62
|
+
' ✦ ░░░░░ ',
|
|
63
|
+
' ▒▒▒▒▒▒▒▒▒▒▒▒ ',
|
|
64
|
+
' ▒▒▒▒▒▒▒▒▒▒▒▒▒ ',
|
|
65
|
+
' ██▒▒▒▒▒▒▒▒▒▒▒▒▓▓',
|
|
66
|
+
' ██▒▒▒▒▒▒▒▒▒▒▒▒▓ ',
|
|
67
|
+
' ▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ',
|
|
68
|
+
' ░░░░░░░░░░░░ ',
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
// Moon above the whale — the right half of the mark, 2-col gutter. The moon is
|
|
72
|
+
// padded with a trailing blank row so joinSideBySide keeps both blocks aligned.
|
|
73
|
+
const MOON_WHALE = joinSideBySide([...MOON, ''], WHALE, 2);
|
|
74
|
+
|
|
75
|
+
/** Full welcome mark: mascot + (moon + whale) side by side, bottom-aligned. */
|
|
76
|
+
const WELCOME_ART = joinSideBySide(MASCOT_ART, MOON_WHALE, 2);
|
|
31
77
|
|
|
32
|
-
|
|
78
|
+
/** Narrow-terminal fallback: mascot stacked above (moon + whale). */
|
|
79
|
+
const WELCOME_ART_STACKED = stackVertical(MASCOT_ART, MOON_WHALE, 1);
|
|
80
|
+
|
|
81
|
+
/** Welcome copy, in aegiscode-dev's frame: coral title over the mark. */
|
|
82
|
+
const WELCOME_TITLE = 'Welcome to AEGIS Code';
|
|
83
|
+
const WELCOME_BACK = 'Welcome back!';
|
|
33
84
|
const TAGLINE = 'Cloud brain in your shell.';
|
|
34
85
|
|
|
86
|
+
// ── Terminal portability ─────────────────────────────────────────────────────
|
|
87
|
+
// The block-glyph palette and ✦ stars are single-width on Linux terminals, but
|
|
88
|
+
// not everywhere: Windows fonts often lack the U+259B..U+259F face edges and
|
|
89
|
+
// mangle █▓▒░ entirely; on macOS ✦ renders as a double-width emoji. Each pass
|
|
90
|
+
// swaps offending code points 1:1 (same character count per row) so the width
|
|
91
|
+
// maths stays exact and the mark keeps its shape.
|
|
92
|
+
const ASCII_STENCIL = new Map([
|
|
93
|
+
['█', '#'],
|
|
94
|
+
['▓', '@'],
|
|
95
|
+
['▒', '='],
|
|
96
|
+
['░', '.'],
|
|
97
|
+
['▐', '#'],
|
|
98
|
+
['▛', '#'],
|
|
99
|
+
['▜', '#'],
|
|
100
|
+
['▌', '#'],
|
|
101
|
+
['▝', '#'],
|
|
102
|
+
['▘', '#'],
|
|
103
|
+
['✦', '*'],
|
|
104
|
+
['·', '.'],
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
const DARWIN_STENCIL = new Map([
|
|
108
|
+
['▐', '#'],
|
|
109
|
+
['▛', '#'],
|
|
110
|
+
['▜', '#'],
|
|
111
|
+
['▌', '#'],
|
|
112
|
+
['▝', '#'],
|
|
113
|
+
['▘', '#'],
|
|
114
|
+
['✦', '*'],
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
/** One platform pass over an art block. */
|
|
118
|
+
function portability(rows) {
|
|
119
|
+
if (process.platform === 'win32') {
|
|
120
|
+
return rows.map((r) => [...r].map((ch) => ASCII_STENCIL.get(ch) ?? ch).join(''));
|
|
121
|
+
}
|
|
122
|
+
if (process.platform === 'darwin') {
|
|
123
|
+
return rows.map((r) => [...r].map((ch) => DARWIN_STENCIL.get(ch) ?? ch).join(''));
|
|
124
|
+
}
|
|
125
|
+
return rows;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The mark that fits the current terminal: side-by-side when there's room. */
|
|
129
|
+
function welcomeArtFor(cols) {
|
|
130
|
+
const wide = portability(WELCOME_ART);
|
|
131
|
+
const stacked = portability(WELCOME_ART_STACKED);
|
|
132
|
+
const W = Math.max(...wide.map((r) => [...r].length));
|
|
133
|
+
return cols >= W + 4 ? wide : stacked;
|
|
134
|
+
}
|
|
135
|
+
|
|
35
136
|
/**
|
|
36
|
-
*
|
|
37
|
-
* @returns {string
|
|
137
|
+
* Split the mark into its two halves so each can carry its own theme ink.
|
|
138
|
+
* @returns {{rows: Array<[string,string]>, width: number, gutter: number}}
|
|
38
139
|
*/
|
|
39
|
-
function
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
140
|
+
function welcomeArtParts(cols) {
|
|
141
|
+
const mascot = portability(MASCOT_ART);
|
|
142
|
+
const moonWhale = portability(MOON_WHALE);
|
|
143
|
+
const L = Math.max(...mascot.map((r) => [...r].length));
|
|
144
|
+
const R = Math.max(...moonWhale.map((r) => [...r].length));
|
|
145
|
+
const wide = cols >= L + R + 2 + 4;
|
|
146
|
+
const gutter = wide ? 2 : 1;
|
|
147
|
+
if (wide) {
|
|
148
|
+
// The right half carries the moon *and* the whale. Using WHALE alone here —
|
|
149
|
+
// which the reference does, because it only wants the per-mascot ink split —
|
|
150
|
+
// would silently drop the crescent from the welcome screen this CLI renders.
|
|
151
|
+
const n = Math.max(mascot.length, moonWhale.length);
|
|
152
|
+
const rows = [];
|
|
153
|
+
for (let i = 0; i < n; i++) {
|
|
154
|
+
const li = i + (n - mascot.length);
|
|
155
|
+
const ri = i + (n - moonWhale.length);
|
|
156
|
+
rows.push([(mascot[li] ?? '').padEnd(L), (moonWhale[ri] ?? '').padEnd(R)]);
|
|
157
|
+
}
|
|
158
|
+
return { rows, width: L + gutter + R, gutter };
|
|
159
|
+
}
|
|
160
|
+
const w = Math.max(L, R);
|
|
161
|
+
const centerBlock = (rows) =>
|
|
162
|
+
rows.map((r) => ' '.repeat(Math.max(0, Math.floor((w - [...r].length) / 2))) + r);
|
|
163
|
+
return {
|
|
164
|
+
rows: [
|
|
165
|
+
...centerBlock(mascot).map((c) => [c, '']),
|
|
166
|
+
['', ''],
|
|
167
|
+
...centerBlock(moonWhale).map((r) => ['', r]),
|
|
168
|
+
],
|
|
169
|
+
width: w,
|
|
170
|
+
gutter,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Stack two art blocks vertically, each centred, separated by `gap` rows. */
|
|
175
|
+
function stackVertical(top, bottom, gap = 1) {
|
|
176
|
+
const w = Math.max(...[...top, ...bottom].map((r) => [...r].length));
|
|
177
|
+
const centerBlock = (rows) =>
|
|
178
|
+
rows.map((r) => {
|
|
179
|
+
const pad = Math.max(0, Math.floor((w - [...r].length) / 2));
|
|
180
|
+
return ' '.repeat(pad) + r;
|
|
181
|
+
});
|
|
182
|
+
return [...centerBlock(top), ...Array(gap).fill(''), ...centerBlock(bottom)];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Join two art stacks row-aligned (bottom-aligned by default). */
|
|
186
|
+
function joinSideBySide(left, right, gutter = 2, align = 'bottom') {
|
|
187
|
+
const L = Math.max(...left.map((r) => [...r].length));
|
|
188
|
+
const R = Math.max(...right.map((r) => [...r].length));
|
|
189
|
+
const n = Math.max(left.length, right.length);
|
|
190
|
+
return Array.from({ length: n }, (_, i) => {
|
|
191
|
+
const li = align === 'bottom' ? i + (n - left.length) : i;
|
|
192
|
+
const ri = align === 'bottom' ? i + (n - right.length) : i;
|
|
193
|
+
const l = (left[li] ?? '').padEnd(L);
|
|
194
|
+
const r = (right[ri] ?? '').padEnd(R);
|
|
195
|
+
return l + ' '.repeat(gutter) + r;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Centre every row of an art block inside `width` (left pad + right fill). */
|
|
200
|
+
function center(rows, width) {
|
|
201
|
+
return rows.map((r) => {
|
|
202
|
+
const pad = Math.max(0, Math.floor((width - [...r].length) / 2));
|
|
203
|
+
return ' '.repeat(pad) + r + ' '.repeat(Math.max(0, width - [...r].length - pad));
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Centre each row against the block's own max width (no trailing fill). */
|
|
208
|
+
function padBoth(rows, width) {
|
|
209
|
+
const w = Math.max(...rows.map((r) => [...r].length));
|
|
210
|
+
return rows.map((r) => {
|
|
211
|
+
const pad = Math.floor((width - w) / 2);
|
|
212
|
+
return ' '.repeat(Math.max(0, pad)) + r;
|
|
213
|
+
});
|
|
43
214
|
}
|
|
44
215
|
|
|
45
|
-
module.exports = {
|
|
216
|
+
module.exports = {
|
|
217
|
+
FACE,
|
|
218
|
+
HAT,
|
|
219
|
+
MASCOT_ART,
|
|
220
|
+
MOON,
|
|
221
|
+
WHALE,
|
|
222
|
+
MOON_WHALE,
|
|
223
|
+
WELCOME_ART,
|
|
224
|
+
WELCOME_ART_STACKED,
|
|
225
|
+
WELCOME_TITLE,
|
|
226
|
+
WELCOME_BACK,
|
|
227
|
+
TAGLINE,
|
|
228
|
+
portability,
|
|
229
|
+
welcomeArtFor,
|
|
230
|
+
welcomeArtParts,
|
|
231
|
+
stackVertical,
|
|
232
|
+
joinSideBySide,
|
|
233
|
+
center,
|
|
234
|
+
padBoth,
|
|
235
|
+
};
|