@prplkane/roundtable 0.6.3 → 0.6.4
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/CHANGELOG.md +16 -0
- package/README.md +4 -3
- package/docs/workflow.md +23 -4
- package/package.json +1 -1
- package/skills/roundtable/SKILL.md +3 -1
- package/src/doctor.js +8 -3
- package/src/local-server.js +32 -4
- package/src/server.js +20 -5
- package/src/terminal.js +36 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.4
|
|
4
|
+
|
|
5
|
+
### Terminal
|
|
6
|
+
|
|
7
|
+
- Report only the provider CLIs found on this machine: `roundtable doctor` lists missing ones
|
|
8
|
+
in a single line instead of a block each (`--vendor <name>` still shows the full block), and
|
|
9
|
+
foreground startup prints a `providers:` line in the seat colours instead of a failed quota
|
|
10
|
+
read for every CLI that is not installed.
|
|
11
|
+
- Always print how to open the board and that Ctrl+C stops it, including with `--no-open`.
|
|
12
|
+
- Say plainly when the port cannot be taken: in use, or a sandbox that forbids a local server
|
|
13
|
+
(`EPERM`), with the `roundtable server start` way out. The MCP tools and `roundtable server
|
|
14
|
+
start` report that last line within seconds instead of waiting out the readiness timeout,
|
|
15
|
+
while two hosts starting the board at once both get the one that won the port.
|
|
16
|
+
- Blink the block cursor beside the wordmark where the terminal allows blinking text, and show
|
|
17
|
+
vendor names in their seat colours.
|
|
18
|
+
|
|
3
19
|
## 0.6.3
|
|
4
20
|
|
|
5
21
|
### Assistant integration
|
package/README.md
CHANGED
|
@@ -69,9 +69,10 @@ For a source snapshot before its npm release, use
|
|
|
69
69
|
`npm install -g https://github.com/prplkane/roundtable/archive/refs/heads/main.tar.gz`.
|
|
70
70
|
|
|
71
71
|
Interactive `roundtable --help`, `roundtable` and `roundtable fake` show the app's pixel logo
|
|
72
|
-
tile in orchid, with the wordmark and installed version beside it
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
tile in orchid, with the wordmark, a blinking cursor and the installed version beside it, then
|
|
73
|
+
the provider CLIs found on this machine in their seat colors. Narrow terminals and terminals
|
|
74
|
+
without UTF-8 use a short title; `NO_COLOR=1` disables color. Redirected output, CI, dumb
|
|
75
|
+
terminals, MCP and JSON commands omit the decoration.
|
|
75
76
|
|
|
76
77
|
### Try the demo
|
|
77
78
|
|
package/docs/workflow.md
CHANGED
|
@@ -160,6 +160,23 @@ requested action needs them. There is no Roundtable cloud connection or API key
|
|
|
160
160
|
Keep the provider CLI executables on the assistant's `PATH`; after changing a Node version or
|
|
161
161
|
moving its installation, rerun setup and reload the assistant.
|
|
162
162
|
|
|
163
|
+
Some assistants run commands in a sandbox that forbids opening a local port. A `roundtable`
|
|
164
|
+
command run inside such a shell cannot start the board: it dies with `listen EPERM
|
|
165
|
+
127.0.0.1:4747`, and the command reports that line. Codex on Linux has shown this. Check
|
|
166
|
+
first that the MCP connection itself is loaded (`codex mcp list`, then `/mcp` in Codex), since
|
|
167
|
+
the MCP tools are started by the assistant rather than by its sandboxed shell. If the tools
|
|
168
|
+
still cannot start the board, start it once from a normal terminal:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
roundtable server start
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
It keeps running in the background and `roundtable server status` shows it; the tools then
|
|
175
|
+
only connect to it. Stop it with `roundtable server stop` when no review is active. If a tool
|
|
176
|
+
still cannot reach a board that is running, that sandbox blocks loopback connections too, and
|
|
177
|
+
the assistant cannot use Roundtable from inside it; report it as an issue with the assistant,
|
|
178
|
+
version and OS.
|
|
179
|
+
|
|
163
180
|
To check the workflow without a provider account, use `roundtable setup --client codex --fake`
|
|
164
181
|
before restarting the assistant and request a scripted demo review. This uses port 4748 and
|
|
165
182
|
makes no model calls. Switch back with `roundtable setup --client codex --live` and reload again.
|
|
@@ -168,10 +185,12 @@ the board, or run `roundtable fake` for a foreground demo. If the board is alrea
|
|
|
168
185
|
the background, open its URL instead of starting a second server.
|
|
169
186
|
|
|
170
187
|
Foreground board startup and top-level help show the app's pixel logo tile in orchid, with
|
|
171
|
-
the wordmark and installed version beside it. The tile is drawn
|
|
172
|
-
so it needs a UTF-8 terminal; other locales, and
|
|
173
|
-
|
|
174
|
-
|
|
188
|
+
the wordmark, a blinking block cursor and the installed version beside it. The tile is drawn
|
|
189
|
+
with half-block characters, so it needs a UTF-8 terminal; other locales, and
|
|
190
|
+
`ROUNDTABLE_ASCII=1`, get the wordmark alone. Startup then lists the provider CLIs found on
|
|
191
|
+
`PATH`, each in its seat color; `roundtable doctor` names the missing ones in one line.
|
|
192
|
+
Narrow terminals use a short title, `NO_COLOR=1` removes color, and redirected output, CI,
|
|
193
|
+
`TERM=dumb`, MCP and JSON commands omit the decoration.
|
|
175
194
|
|
|
176
195
|
## 2. Install and log in to each vendor
|
|
177
196
|
|
package/package.json
CHANGED
|
@@ -16,7 +16,9 @@ one-time `roundtable setup --client codex` (or `claude` / `agy`) step.
|
|
|
16
16
|
The MCP server starts the local board when needed; offer its link without opening it.
|
|
17
17
|
For the CLI fallback, read `roundtable agent --help`, then `roundtable agent status --refresh`.
|
|
18
18
|
If no server answers, use `roundtable server start` to start a hidden board and retry
|
|
19
|
-
the read.
|
|
19
|
+
the read. If that reports `EPERM` or that the environment blocks local servers, your
|
|
20
|
+
sandbox forbids the port: ask the user to run `roundtable server start` in their own
|
|
21
|
+
terminal once, then retry. An incompatible running board needs a restart after its reviews finish;
|
|
20
22
|
`roundtable server restart` refuses active reviews. Never automatically retry a write
|
|
21
23
|
that might already have succeeded.
|
|
22
24
|
2. Choose distinct perspectives using the user's facts and constraints. Normally use two seats,
|
package/src/doctor.js
CHANGED
|
@@ -13,6 +13,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
13
13
|
import { ADAPTERS } from './adapters/config.js';
|
|
14
14
|
import { resolveBinary, runProcess, profileHome, dataDir } from './adapters/spawn.js';
|
|
15
15
|
import { dataDirectory } from './history.js';
|
|
16
|
+
import { paintVendor } from './terminal.js';
|
|
16
17
|
|
|
17
18
|
// Shell lines that log a profile in, per vendor, for the platform the user is on. Pure.
|
|
18
19
|
export function loginInstructions(vendor, profile, where = {}) {
|
|
@@ -46,9 +47,13 @@ async function main() {
|
|
|
46
47
|
const catalogue = await vendorCatalogue(ADAPTERS.agy.terms && !acknowledgeTermsRisk ? { agyModels: async () => [] } : {});
|
|
47
48
|
console.log(`roundtable doctor (${process.platform}, node ${process.version}, data dir ${dataDir()})`);
|
|
48
49
|
console.log(live ? 'live: one trivial model call per vendor' : 'static only; add --live for one trivial model call per vendor');
|
|
50
|
+
const missing = []; // CLIs not on PATH: one line at the end, a full block only when asked for by --vendor
|
|
49
51
|
for (const [vendor, a] of Object.entries(ADAPTERS)) {
|
|
50
52
|
if (only && only !== vendor) continue;
|
|
51
|
-
|
|
53
|
+
let bin = null, lookup = null;
|
|
54
|
+
if (a.kind === 'cli') try { bin = resolveBinary(a.binary, a.npmBin); } catch (e) { lookup = e.message; }
|
|
55
|
+
if (a.kind === 'cli' && !bin && only !== vendor) { missing.push(`${a.label} (${a.binary})`); continue; }
|
|
56
|
+
console.log(`\n== ${paintVendor(a)} (${vendor}) pool: ${a.pool}`);
|
|
52
57
|
if (a.kind === 'api') { // no binary: everything about an endpoint seat is on its card
|
|
53
58
|
console.log(' seat it on the board: base URL (…/v1), model id, and for a hosted API the NAME of the key variable');
|
|
54
59
|
console.log(` efforts ${a.effortLevels.join(', ')} (reasoning_effort, sent only when set)`);
|
|
@@ -58,8 +63,7 @@ async function main() {
|
|
|
58
63
|
}
|
|
59
64
|
const held = !!a.terms && !acknowledgeTermsRisk;
|
|
60
65
|
const heldLine = `held: nothing is sent to ${a.binary} without --acknowledge-terms-risk`;
|
|
61
|
-
|
|
62
|
-
try { bin = resolveBinary(a.binary, a.npmBin); console.log(` binary ${bin.prefix.length ? `${bin.prefix[0]} (via node)` : bin.file}`); } catch (e) { console.log(` binary NOT FOUND: ${e.message}`); }
|
|
66
|
+
console.log(` binary ${bin ? bin.prefix.length ? `${bin.prefix[0]} (via node)` : bin.file : `NOT FOUND: ${lookup}`}`);
|
|
63
67
|
if (bin) console.log(` version ${await version(bin, vendor)}`);
|
|
64
68
|
const c = catalogue[vendor];
|
|
65
69
|
if (held) console.log(` models ${heldLine}`);
|
|
@@ -88,6 +92,7 @@ async function main() {
|
|
|
88
92
|
}
|
|
89
93
|
if (profile) for (const l of loginInstructions(vendor, profile)) console.log(` profile ${l}`);
|
|
90
94
|
}
|
|
95
|
+
if (missing.length) console.log(`\nnot installed: ${missing.join(', ')}; install and sign in to seat them, or add --vendor <name> for that block`);
|
|
91
96
|
if (!profile) console.log('\nadd --profile <name> to see how to log a named account profile in for each vendor');
|
|
92
97
|
}
|
|
93
98
|
|
package/src/local-server.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// A cheap, versioned health check; provider catalogues are not server readiness checks.
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
+
import { mkdtempSync, openSync, closeSync, readFileSync, rmSync } from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
3
5
|
import { realpath } from 'node:fs/promises';
|
|
4
6
|
import path from 'node:path';
|
|
5
7
|
import { fileURLToPath } from 'node:url';
|
|
@@ -43,23 +45,49 @@ export async function inspectServer(config, { signal, allowOlder = false } = {})
|
|
|
43
45
|
// Readiness reads may be retried. A review write is never part of this loop.
|
|
44
46
|
export async function ensureServer(config, { signal, timeoutMs = 35000 } = {}) {
|
|
45
47
|
const end = Date.now() + timeoutMs;
|
|
46
|
-
let spawned = false, spawnFailed = false, last;
|
|
48
|
+
let spawned = false, spawnFailed = false, exited = null, ready = false, last;
|
|
49
|
+
// The board's stderr goes to a file, never a pipe: it outlives this process, and a pipe to a
|
|
50
|
+
// gone host would break its logging. The file sits in a fresh private temp directory (no
|
|
51
|
+
// predictable path for another user to plant), is read only when the board exits early, and
|
|
52
|
+
// goes once the board is ready (at once where an open file can be unlinked, else when the
|
|
53
|
+
// board exits while this process is still around).
|
|
54
|
+
let log = null;
|
|
55
|
+
const discard = () => { try { if (log) rmSync(path.dirname(log), { recursive: true, force: true }); } catch {} };
|
|
47
56
|
while (Date.now() < end) {
|
|
48
57
|
signal?.throwIfAborted();
|
|
49
58
|
try {
|
|
50
59
|
const status = await inspectServer(config, { signal });
|
|
51
|
-
if (status) return status;
|
|
60
|
+
if (status) { if (spawned) { ready = true; discard(); } return status; }
|
|
52
61
|
if (config.start === false) throw new Error('Roundtable is not running; start the local board or remove --no-start.');
|
|
53
62
|
if (!spawned) {
|
|
54
63
|
spawned = true;
|
|
55
64
|
// Survives the host connection, so closing chat cannot cancel a review or its timer.
|
|
65
|
+
let fd = null;
|
|
66
|
+
try { log = path.join(mkdtempSync(path.join(os.tmpdir(), 'roundtable-start-')), 'stderr.log'); fd = openSync(log, 'wx'); } catch {}
|
|
56
67
|
const child = spawn(process.execPath, [SERVER, '--no-open', ...(config.fake ? ['--fake'] : [])], {
|
|
57
68
|
env: { ...process.env, PORT: String(config.port), ...(config.dataDir ? { ROUNDTABLE_DEBATES_DIR: config.dataDir } : {}) },
|
|
58
|
-
detached: true, windowsHide: true, stdio: 'ignore',
|
|
69
|
+
detached: true, windowsHide: true, stdio: ['ignore', 'ignore', fd ?? 'ignore'],
|
|
59
70
|
});
|
|
60
|
-
|
|
71
|
+
if (fd != null) closeSync(fd);
|
|
72
|
+
child.once('error', () => { spawnFailed = true; });
|
|
73
|
+
child.once('exit', (code, sig) => { exited = { code, sig, at: Date.now() }; if (ready) discard(); });
|
|
74
|
+
child.unref();
|
|
61
75
|
}
|
|
62
76
|
} catch (e) { if (!e.transient) throw e; last = e; }
|
|
77
|
+
// A board that exits early may have lost the port or the data folder to one that another
|
|
78
|
+
// process started at the same moment (two hosts asking at once), so the port is still
|
|
79
|
+
// watched for a few seconds before the exit counts as the answer.
|
|
80
|
+
if (exited && Date.now() - exited.at > 5000) {
|
|
81
|
+
let tail = '';
|
|
82
|
+
// the board's own last word where it had one, else the message line of a crash, else the last line
|
|
83
|
+
try {
|
|
84
|
+
const lines = readFileSync(log, 'utf8').split(/\r?\n/).filter((l) => l.trim());
|
|
85
|
+
tail = (lines.findLast((l) => l.startsWith('[server]')) || lines.find((l) => /^\w*Error: /.test(l)) || lines.at(-1) || '').trim().slice(0, 200);
|
|
86
|
+
} catch {}
|
|
87
|
+
discard();
|
|
88
|
+
const sandboxed = /cannot listen.*\b(EPERM|EACCES)\b/.test(tail);
|
|
89
|
+
throw new Error(`The local board exited before it was ready${tail ? ` (${tail})` : ''}. ${sandboxed ? 'This environment blocks local servers: start the board from a normal terminal with roundtable server start, then retry.' : 'Run roundtable --no-open in a terminal for startup details.'} No review was started.`);
|
|
90
|
+
}
|
|
63
91
|
if (spawnFailed) break;
|
|
64
92
|
await delay(100, undefined, { signal });
|
|
65
93
|
}
|
package/src/server.js
CHANGED
|
@@ -29,7 +29,7 @@ import { pickFolder } from './folder-picker.js';
|
|
|
29
29
|
import { dataDirectory, legacyHistory, acquireDataLock, checkedHistoryPath, removeHistory, createUsageStore, createHistoryIndex } from './history.js';
|
|
30
30
|
import { VERSION } from './version.js';
|
|
31
31
|
import { LOCAL_API_VERSION } from './local-server.js';
|
|
32
|
-
import { terminalBanner } from './terminal.js';
|
|
32
|
+
import { terminalBanner, paintVendor } from './terminal.js';
|
|
33
33
|
|
|
34
34
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
35
35
|
const UI = path.join(ROOT, 'src', 'ui');
|
|
@@ -402,6 +402,9 @@ function synthesizeDebate(d, body = {}) {
|
|
|
402
402
|
const quota = {}; // pool key -> { windows, plan, at, groups? }
|
|
403
403
|
const readers = FAKE ? { claude: async () => fakeQuota('claude'), codex: async () => fakeQuota('codex'), agy: async () => fakeQuota('agy') } : quotaReaders;
|
|
404
404
|
const keyOf = (vendor, account) => poolKey(vendor, account, poolsByAccount(ADAPTERS[vendor]));
|
|
405
|
+
// A vendor whose CLI is not on PATH gets no quota read and no startup line: a CLI that is not
|
|
406
|
+
// here is not a failed read. Demo mode seats every vendor with scripted replies.
|
|
407
|
+
const installed = (vendor) => { if (FAKE || ADAPTERS[vendor]?.kind !== 'cli') return true; try { resolveBinary(ADAPTERS[vendor].binary, ADAPTERS[vendor].npmBin); return true; } catch { return false; } };
|
|
405
408
|
|
|
406
409
|
function setQuota(key, q) {
|
|
407
410
|
if (!q) return;
|
|
@@ -415,7 +418,7 @@ const reading = new Map();
|
|
|
415
418
|
function refreshQuota(key, { acknowledged = false } = {}) {
|
|
416
419
|
const vendor = poolVendor(key), account = poolAccount(key);
|
|
417
420
|
// a reader that stopped itself (agy's latch, quota.js) said why once; no read, no repeat
|
|
418
|
-
if (!readers[vendor] || readers[vendor].stopped?.() || termsHeld(vendor, acknowledged)) return Promise.resolve(null);
|
|
421
|
+
if (!readers[vendor] || readers[vendor].stopped?.() || termsHeld(vendor, acknowledged) || !installed(vendor)) return Promise.resolve(null);
|
|
419
422
|
if (reading.has(key)) return reading.get(key);
|
|
420
423
|
// The readers resolve null on every failure (timeout, spawn error, no result) rather than
|
|
421
424
|
// throwing, so say so here or a dead read is invisible on the console. Resolves to the figure
|
|
@@ -423,7 +426,7 @@ function refreshQuota(key, { acknowledged = false } = {}) {
|
|
|
423
426
|
const p = (async () => {
|
|
424
427
|
try {
|
|
425
428
|
const q = await readers[vendor]({ home: account ? profileHome(vendor, account) : null });
|
|
426
|
-
if (!q) { diagnostics.write({ code: 'status-failed', action: 'quota', vendor }); console.warn(`[quota] ${key}: no figure (read failed or timed out); the strip keeps the last one`); }
|
|
429
|
+
if (!q) { diagnostics.write({ code: 'status-failed', action: 'quota', vendor }); console.warn(`[quota] ${paintVendor(ADAPTERS[vendor], { text: key })}: no figure (not logged in, or the read failed or timed out); the strip keeps the last one`); }
|
|
427
430
|
setQuota(key, q);
|
|
428
431
|
return q;
|
|
429
432
|
} catch (e) { diagnostics.write({ code: 'status-failed', action: 'quota', vendor }, e); console.error(`[quota] ${key}: ${e.message}`); return null; } finally { reading.delete(key); }
|
|
@@ -876,15 +879,27 @@ function openBrowser(url) {
|
|
|
876
879
|
const cmd = process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]] : process.platform === 'darwin' ? ['open', [url]] : ['xdg-open', [url]];
|
|
877
880
|
try { spawn(cmd[0], cmd[1], { stdio: 'ignore', detached: true, windowsHide: true }).on('error', () => {}).unref(); return true; } catch { return false; }
|
|
878
881
|
}
|
|
882
|
+
// A port that cannot be taken is said plainly: in use, or a sandbox that forbids listening
|
|
883
|
+
// (Codex on Linux, for one), where the board has to be started from a normal terminal instead.
|
|
884
|
+
server.on('error', (e) => {
|
|
885
|
+
diagnostics.write({ code: 'server-start', action: 'startup' }, e);
|
|
886
|
+
const why = e.code === 'EADDRINUSE' ? `port ${PORT} is in use. Check roundtable server status, or start with --port`
|
|
887
|
+
: ['EPERM', 'EACCES'].includes(e.code) ? `this environment does not allow a local server on 127.0.0.1:${PORT} (${e.code}). Start the board from a normal terminal with roundtable server start; the assistant's tools then connect to it`
|
|
888
|
+
: e.message;
|
|
889
|
+
console.error(`[server] cannot listen: ${why}`);
|
|
890
|
+
process.exit(1);
|
|
891
|
+
});
|
|
879
892
|
server.listen(PORT, '127.0.0.1', () => {
|
|
880
893
|
const url = `http://localhost:${PORT}`;
|
|
881
894
|
const open = process.stdout.isTTY && !process.argv.includes('--no-open') && process.env.ROUNDTABLE_NO_OPEN !== '1' && !process.env.CI;
|
|
882
895
|
const opened = open && openBrowser(url);
|
|
883
896
|
process.stdout.write(terminalBanner());
|
|
884
897
|
console.log(`roundtable${FAKE ? ' (fake seats)' : ''}: ${url} token ${TOKEN}`);
|
|
885
|
-
console.log(`mode: ${FAKE ? 'demo (scripted replies and usage)' : 'live
|
|
898
|
+
console.log(`mode: ${FAKE ? 'demo (scripted replies and usage)' : 'live'}; data: ${DEBATES_DIR}`);
|
|
899
|
+
const found = Object.entries(ADAPTERS).filter(([v, a]) => a.kind === 'cli' && installed(v)).map(([, a]) => paintVendor(a));
|
|
900
|
+
if (!FAKE) console.log(found.length ? `providers: ${found.join(', ')} (CLIs on PATH; an API endpoint is seated on the board)` : 'providers: no Claude, Codex or Antigravity CLI on PATH; seat an API endpoint on the board, or install one and restart. roundtable doctor has details');
|
|
886
901
|
console.log('diagnostics: _diagnostics in the data folder; use Save error log on the board. Nothing is uploaded.');
|
|
887
|
-
|
|
902
|
+
console.log(`${opened ? 'the board is open in your browser' : `open ${url} in your browser`}; Ctrl+C here stops the server and every CLI it started`);
|
|
888
903
|
});
|
|
889
904
|
|
|
890
905
|
// No CLI outlives the server: on a signal or an uncaught error every registered child goes
|
package/src/terminal.js
CHANGED
|
@@ -12,17 +12,40 @@ const TILE = [
|
|
|
12
12
|
];
|
|
13
13
|
const TAGLINE = 'Local decision reviews';
|
|
14
14
|
const RESET = '\x1b[0m';
|
|
15
|
+
const BOLD = '\x1b[1m', DIM = '\x1b[2m', BLINK = '\x1b[5m';
|
|
15
16
|
|
|
16
17
|
// Half blocks need a UTF-8 terminal. Windows consoles take UTF-16 from Node whatever the code
|
|
17
18
|
// page; elsewhere the locale has to say so. ROUNDTABLE_ASCII=1 forces the plain form.
|
|
18
19
|
const unicode = (env, platform) => env.ROUNDTABLE_ASCII !== '1'
|
|
19
20
|
&& (platform === 'win32' || /utf-?8/i.test(env.LC_ALL || env.LC_CTYPE || env.LANG || ''));
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
// Which colours the terminal takes: true colour where it says so, 256 colours, or the basic 8.
|
|
22
|
+
const tier = (env, platform) => (/truecolor|24bit/i.test(env.COLORTERM || '') || env.WT_SESSION || platform === 'win32') ? 'true'
|
|
23
|
+
: env.TERM?.includes('256color') ? '256' : 'basic';
|
|
24
|
+
// Tile and ink colours per tier: [tile background, ink foreground, orchid foreground].
|
|
25
|
+
const PALETTE = {
|
|
26
|
+
true: ['\x1b[48;2;199;154;208m', '\x1b[38;2;43;29;56m', '\x1b[38;2;199;154;208m'],
|
|
27
|
+
256: ['\x1b[48;5;182m', '\x1b[38;5;234m', '\x1b[38;5;182m'],
|
|
28
|
+
basic: ['\x1b[45m', '\x1b[30m', '\x1b[35m'],
|
|
29
|
+
};
|
|
30
|
+
// The family accents the board gives each vendor (adapters/config.js colour, design/app.css
|
|
31
|
+
// --seat-*), per tier, so a vendor reads the same colour in the terminal as on its seat.
|
|
32
|
+
const ACCENTS = {
|
|
33
|
+
rust: ['\x1b[38;2;223;107;85m', '\x1b[38;5;173m', '\x1b[31m'],
|
|
34
|
+
olive: ['\x1b[38;2;156;188;106m', '\x1b[38;5;149m', '\x1b[32m'],
|
|
35
|
+
teal: ['\x1b[38;2;111;179;169m', '\x1b[38;5;73m', '\x1b[36m'],
|
|
36
|
+
steel: ['\x1b[38;2;134;169;196m', '\x1b[38;5;110m', '\x1b[34m'],
|
|
37
|
+
};
|
|
38
|
+
const TIERS = ['true', '256', 'basic'];
|
|
39
|
+
|
|
40
|
+
// Nothing is decorated for a pipe, CI or a dumb terminal; NO_COLOR keeps the text and drops
|
|
41
|
+
// the codes.
|
|
42
|
+
const decorated = (isTTY, env) => isTTY && !env.CI && env.TERM !== 'dumb';
|
|
43
|
+
const painter = (env) => (s, code) => env.NO_COLOR ? s : `${code}${s}${RESET}`;
|
|
44
|
+
|
|
45
|
+
// A vendor's name in its seat colour: paintVendor(ADAPTERS.codex) -> coloured "Codex".
|
|
46
|
+
export function paintVendor({ label, colour }, { text = label, isTTY = process.stdout.isTTY, env = process.env, platform = process.platform } = {}) {
|
|
47
|
+
if (!decorated(isTTY, env) || !ACCENTS[colour]) return text;
|
|
48
|
+
return painter(env)(text, ACCENTS[colour][TIERS.indexOf(tier(env, platform))]);
|
|
26
49
|
}
|
|
27
50
|
|
|
28
51
|
function tileRows(colour) {
|
|
@@ -39,15 +62,16 @@ function tileRows(colour) {
|
|
|
39
62
|
}
|
|
40
63
|
|
|
41
64
|
export function terminalBanner({ isTTY = process.stdout.isTTY, columns = process.stdout.columns ?? 80, env = process.env, platform = process.platform } = {}) {
|
|
42
|
-
if (!isTTY
|
|
43
|
-
const paint = (
|
|
44
|
-
const [tile, ink, orchid] =
|
|
65
|
+
if (!decorated(isTTY, env)) return '';
|
|
66
|
+
const paint = painter(env);
|
|
67
|
+
const [tile, ink, orchid] = PALETTE[tier(env, platform)];
|
|
45
68
|
const version = `v${VERSION}`;
|
|
46
|
-
const name = paint('ROUNDTABLE',
|
|
47
|
-
const compact = `${name}${columns >= 12 + version.length ? ` ${paint(version,
|
|
69
|
+
const name = paint('ROUNDTABLE', BOLD);
|
|
70
|
+
const compact = `${name}${columns >= 12 + version.length ? ` ${paint(version, DIM)}` : ''}`;
|
|
48
71
|
// 2 indent + 16 tile + 3 gap + the tagline, the widest line beside the tile
|
|
49
72
|
if (!unicode(env, platform) || columns < 21 + TAGLINE.length) return `\n${compact}\n\n`;
|
|
50
|
-
|
|
73
|
+
// the block cursor blinks like the board header's, where the terminal allows blinking text
|
|
74
|
+
const beside = { 2: `${name} ${paint('▌', orchid + BLINK)}`, 3: paint(version, DIM), 5: paint(TAGLINE, DIM) };
|
|
51
75
|
const lines = tileRows((row) => paint(row, tile + ink)).map((row, y) => ` ${row}${beside[y] ? ` ${beside[y]}` : ''}`);
|
|
52
76
|
return `\n${lines.join('\n')}\n\n`;
|
|
53
77
|
}
|