slopie 1.7.5 → 1.8.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.
@@ -1,9 +1,13 @@
1
1
  // Instant first frame for `slopie audit`.
2
2
  //
3
- // Loading the CLI bundle takes ~300 ms. For the interactive audit we paint a
4
- // skeleton frame first (in well under 100 ms), so the screen responds at once;
5
- // the real wallboard takes over the alternate screen as soon as it loads and
6
- // calls `globalThis.__slopieSplash.handOff()`.
3
+ // Loading the CLI bundle takes ~300 ms. For the interactive audit we paint the
4
+ // loading story's first frame (Slopie, the brand, the privacy promise) before
5
+ // the bundle loads, so the screen responds at once; the real loading story
6
+ // takes over the alternate screen in exactly the same place and calls
7
+ // `globalThis.__slopieSplash.handOff()`.
8
+ //
9
+ // The pixel art and geometry mirror packages/cli/src/audit-ui/{mascot,loading}.ts;
10
+ // tests/audit-ui/splash.test.ts keeps them identical.
7
11
  //
8
12
  // If anything else ends up writing (an error, --help), the alternate screen is
9
13
  // left first so the message stays visible, and the terminal is always restored
@@ -11,36 +15,108 @@
11
15
 
12
16
  const PLAIN_FLAGS = new Set(['--json', '--plain', '--export', '--legacy', '--sweep', '--verify', '--help', '-h', '--version', '-V']);
13
17
 
18
+ /** Slopie, idle. Rows of palette letters; '.' is transparent. First row is pixel row -2 (the tip). */
19
+ export const SPLASH_ART = {
20
+ big: {
21
+ top: -2,
22
+ palette: ['#ff7a1a', '#e16011', '#c94a0a', '#ffb070', '#fff4e6', '#ffffff', '#1c0f08', '#8a2e05', '#ff7027'],
23
+ rows: ['........ab....', '.......aa.....', '.....aaaa.....', '...aaddaaaa...', '..adedaaaaaa..', '.aadaaaaaaaab.', '.adaaaaaaaabb.', 'aaaaaaaaaaaabb', 'aaafgaaaafgabb', 'aaaggaaaaggabb', 'aaaaaaaaaaaabb', 'aiiaahaahaaiib', 'caaaaahhaaaabc', 'ccaaaaaaaaaacc', '.cccccccccccc.', '..cccccccccc..', '...c.......c..', '...........b..'],
24
+ },
25
+ compact: {
26
+ top: 0,
27
+ palette: ['#ff7a1a', '#e16011', '#c94a0a', '#ffb070', '#fff4e6', '#1c0f08', '#8a2e05'],
28
+ rows: ['..aaaa..', '.adaaaa.', 'aeaaaaab', 'aafaafab', 'aafaafab', 'aagaagab', 'caaggaac', '.cccccc.'],
29
+ },
30
+ };
31
+
32
+ /** Mirrors loadingGeometry() in audit-ui/loading.ts. */
33
+ export function splashGeometry(w, h) {
34
+ const L = w >= 100 && h >= 30;
35
+ const S = w < 76 || h < 23;
36
+ const mx = L ? 4 : S ? 2 : 3;
37
+ const scale = L ? 2 : 1;
38
+ const mascotW = S ? 10 : 18 * scale;
39
+ const mascotH = S ? 4 : 8 * scale;
40
+ const panelX = S ? mx : Math.max(mx + mascotW + 6, Math.floor(w * (L ? 0.46 : 0.42)));
41
+ const sx = S ? Math.floor((w - 10) / 2) : Math.max(mx, Math.floor((panelX - mascotW) / 2) - 1);
42
+ const sy = S ? 2 : Math.max(2, Math.floor((h - mascotH) / 2) - 2);
43
+ const rowsY = S ? sy + mascotH + 2 : Math.max(3, Math.floor(h / 2) - (L ? 6 : 5));
44
+ return { L, S, mx, scale, mascotW, mascotH, panelX, sx, sy, rowsY };
45
+ }
46
+
47
+ const INK = [13, 11, 10];
48
+ const TUNED_256 = { '#ff7a1a': 208, '#ffb070': 215, '#fff4e6': 230, '#ffffff': 231, '#c94a0a': 166, '#8a2e05': 94, '#1c0f08': 232 };
49
+ const hexRgb = hex => [1, 3, 5].map(i => parseInt(hex.slice(i, i + 2), 16));
50
+ function to256(hex) {
51
+ if (TUNED_256[hex]) return TUNED_256[hex];
52
+ const cube = [0, 95, 135, 175, 215, 255];
53
+ const near = v => cube.reduce((best, c, i) => (Math.abs(c - v) < Math.abs(cube[best] - v) ? i : best), 0);
54
+ const [r, g, b] = hexRgb(hex);
55
+ return 16 + 36 * near(r) + 6 * near(g) + near(b);
56
+ }
57
+
58
+ /** Cells for the art: Map "x,y" → [topHex|null, bottomHex|null], in cell coordinates. */
59
+ export function splashCells(size, x, y, scale) {
60
+ const art = SPLASH_ART[size];
61
+ const offset = size === 'big' ? 2 : 0;
62
+ const top = size === 'big' ? 2 : 0;
63
+ const cells = new Map();
64
+ art.rows.forEach((row, ry) => {
65
+ for (let rx = 0; rx < row.length; rx++) {
66
+ if (row[rx] === '.') continue;
67
+ const color = art.palette['abcdefghij'.indexOf(row[rx])];
68
+ const px = rx, py = ry + art.top;
69
+ for (let sy = 0; sy < scale; sy++) for (let sx = 0; sx < scale; sx++) {
70
+ const cx = x + (px + offset) * scale + sx;
71
+ const pyy = y * 2 + (py + top) * scale + sy;
72
+ const key = `${cx},${pyy >> 1}`;
73
+ const cell = cells.get(key) ?? [null, null];
74
+ cell[pyy & 1] = color;
75
+ cells.set(key, cell);
76
+ }
77
+ }
78
+ });
79
+ return cells;
80
+ }
81
+
14
82
  export function auditSplash(argv = process.argv.slice(2)) {
15
83
  const { stdin, stdout, stderr } = process;
16
84
  if (argv[0] !== 'audit' || argv.some(arg => PLAIN_FLAGS.has(arg.split('=')[0]))) return;
17
85
  if (!stdin.isTTY || !stdout.isTTY || process.env.SLOPIE_NO_SPLASH === '1') return;
18
- const columns = stdout.columns || 80;
19
- const rows = stdout.rows || 24;
20
- if (columns < 40 || rows < 12) return;
86
+ const w = stdout.columns || 80;
87
+ const h = stdout.rows || 24;
88
+ if (w < 60 || h < 20) return;
21
89
 
22
90
  const color = !('NO_COLOR' in process.env) && !argv.includes('--no-color') && process.env.TERM !== 'dumb';
23
- const ascii = argv.includes('--ascii');
24
- const paint = (code, text) => (color ? `\x1b[${code}m${text}\x1b[0m` : text);
25
- const orange = '1;38;5;215';
26
- const gray = '38;5;248';
27
- const dark = '38;5;241';
91
+ const truecolor = /^(truecolor|24bit)$/i.test(process.env.COLORTERM ?? '') || /^(iTerm\.app|WezTerm|ghostty|vscode)$/i.test(process.env.TERM_PROGRAM ?? '');
92
+ const deep = color && (truecolor || /256/.test(process.env.TERM ?? '') || process.env.TERM_PROGRAM === 'Apple_Terminal');
93
+ const fg = hex => (!color ? '' : truecolor ? `\x1b[38;2;${hexRgb(hex).join(';')}m` : `\x1b[38;5;${to256(hex)}m`);
94
+ const bgOf = hex => (!deep ? '' : truecolor ? `\x1b[48;2;${hexRgb(hex).join(';')}m` : `\x1b[48;5;${to256(hex)}m`);
95
+ const ink = !deep ? '' : truecolor ? `\x1b[48;2;${INK.join(';')}m` : '\x1b[48;5;233m';
96
+ const reset = `\x1b[0m${ink}`;
28
97
 
29
- const width = Math.min(132, columns - 4);
30
- const margin = ' '.repeat(Math.floor((columns - width) / 2));
31
- const status = 'reading your history…';
32
- const block = ascii ? '.' : '░';
33
- const lines = [];
34
- lines[0] = margin + paint(orange, 'slopie') + ' '.repeat(Math.max(1, width - 6 - status.length)) + paint(gray, status);
35
- for (let i = 2; i <= 4; i++) lines[i] = margin + paint(dark, block.repeat(20));
36
- lines[5] = margin + paint(gray, 'what your tokens would cost at API prices');
37
- lines[rows - 1] = margin + paint(orange, ascii ? '-' : '⠋') + ' ' + paint(gray, 'starting…');
38
-
39
- let out = '\x1b[?1049h\x1b[H\x1b[2J\x1b[?25l';
40
- lines.forEach((line, row) => {
41
- if (line) out += `\x1b[${row + 1};1H${line}`;
42
- });
43
- stdout.write(out);
98
+ const g = splashGeometry(w, h);
99
+ let out = `\x1b[?1049h\x1b[?25l${ink}\x1b[H\x1b[2J`;
100
+ const at = (x, y, text) => { out += `\x1b[${y + 1};${x + 1}H${text}${reset}`; };
101
+ at(g.mx, g.L ? 1 : 0, `\x1b[1m${fg('#ff7a1a')}slopie${reset} ${fg('#5c5c5c')}audit`);
102
+ if (deep) {
103
+ const cells = splashCells(g.S ? 'compact' : 'big', g.sx, g.sy, g.S ? 1 : g.scale);
104
+ for (const [key, [top, bottom]] of cells) {
105
+ const [x, y] = key.split(',').map(Number);
106
+ if (top && bottom) at(x, y, top === bottom ? `${fg(top)}█` : `${fg(top)}${bgOf(bottom)}▀`);
107
+ else if (top) at(x, y, `${fg(top)}▀`);
108
+ else if (bottom) at(x, y, `${fg(bottom)}▄`);
109
+ }
110
+ } else {
111
+ at(g.sx + 4, g.sy + Math.floor(g.mascotH / 2), `${fg('#ff7a1a')}(•ᴗ•)`);
112
+ }
113
+ const privacy = 'all local · nothing leaves this machine';
114
+ const py = h - (g.L ? 2 : 1);
115
+ const px = Math.round(w / 2 - (privacy.length + 2) / 2);
116
+ at(px, py, `${fg('#ff7a1a')}●${reset} ${fg('#5c5c5c')}${privacy}`);
117
+ const cy = g.rowsY + (g.L ? 13 : 9);
118
+ at(g.panelX, cy, `${fg('#ffb070')}finding your agents`);
119
+ stdout.write(out + '\x1b[0m');
44
120
  try { stdin.setRawMode(true); } catch { /* not a raw-capable tty */ }
45
121
 
46
122
  let active = true;
@@ -54,7 +130,7 @@ export function auditSplash(argv = process.argv.slice(2)) {
54
130
  try { stdin.setRawMode(false); } catch { /* ignore */ }
55
131
  originalOut('\x1b[0m\x1b[?25h\x1b[?1049l');
56
132
  };
57
- // Anything other than the wallboard writing first means we must get out of the way.
133
+ // Anything other than the story writing first means we must get out of the way.
58
134
  stdout.write = (...args) => { leave(); return stdout.write(...args); };
59
135
  stderr.write = (...args) => { leave(); return stderr.write(...args); };
60
136
  process.once('exit', leave);