@onesi/genesis-cli 0.1.13 → 0.2.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.
Files changed (63) hide show
  1. package/README.md +29 -2
  2. package/dist/beacons.js +68 -0
  3. package/dist/dials.js +7 -4
  4. package/dist/fleets.js +30 -0
  5. package/dist/flocks.d.ts +7 -1
  6. package/dist/flocks.js +257 -3
  7. package/dist/henry.d.ts +5 -1
  8. package/dist/henry.js +60 -1
  9. package/dist/hives-coverage.d.ts +12 -0
  10. package/dist/hives-coverage.js +116 -0
  11. package/dist/hives-node-transport.js +3 -0
  12. package/dist/hives-operations.d.ts +1 -0
  13. package/dist/hives-operations.js +2 -0
  14. package/dist/hives-platform.d.ts +13 -0
  15. package/dist/hives-platform.js +133 -0
  16. package/dist/iam-directory.d.ts +15 -0
  17. package/dist/iam-directory.js +71 -0
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.js +255 -23
  20. package/dist/minds.d.ts +1 -1
  21. package/dist/minds.js +37 -2
  22. package/dist/onboard/art.d.ts +50 -0
  23. package/dist/onboard/art.js +139 -0
  24. package/dist/onboard/brand.d.ts +56 -0
  25. package/dist/onboard/brand.js +134 -0
  26. package/dist/onboard/caps.d.ts +90 -0
  27. package/dist/onboard/caps.js +232 -0
  28. package/dist/onboard/encode.d.ts +23 -0
  29. package/dist/onboard/encode.js +235 -0
  30. package/dist/onboard/eye.d.ts +103 -0
  31. package/dist/onboard/eye.js +147 -0
  32. package/dist/onboard/index.d.ts +61 -0
  33. package/dist/onboard/index.js +562 -0
  34. package/dist/onboard/mcp.d.ts +35 -0
  35. package/dist/onboard/mcp.js +118 -0
  36. package/dist/onboard/raster.d.ts +43 -0
  37. package/dist/onboard/raster.js +253 -0
  38. package/dist/onboard/sound.d.ts +2 -0
  39. package/dist/onboard/sound.js +54 -0
  40. package/dist/onboard/state.d.ts +24 -0
  41. package/dist/onboard/state.js +48 -0
  42. package/dist/onboard/term.d.ts +69 -0
  43. package/dist/onboard/term.js +292 -0
  44. package/dist/onboard/ui.d.ts +97 -0
  45. package/dist/onboard/ui.js +615 -0
  46. package/dist/orgs.d.ts +16 -0
  47. package/dist/orgs.js +89 -0
  48. package/dist/products.d.ts +6 -2
  49. package/dist/products.js +238 -12
  50. package/dist/relays-commands/certificates.d.ts +3 -0
  51. package/dist/relays-commands/certificates.js +20 -0
  52. package/dist/relays-commands/domains.d.ts +3 -0
  53. package/dist/relays-commands/domains.js +23 -0
  54. package/dist/relays-commands/index.d.ts +4 -0
  55. package/dist/relays-commands/index.js +10 -0
  56. package/dist/relays-commands/monitoring.d.ts +3 -0
  57. package/dist/relays-commands/monitoring.js +41 -0
  58. package/dist/relays-commands/telemetry.d.ts +3 -0
  59. package/dist/relays-commands/telemetry.js +28 -0
  60. package/dist/relays-commands/zones.d.ts +5 -0
  61. package/dist/relays-commands/zones.js +9 -0
  62. package/dist/runner.js +2 -0
  63. package/package.json +2 -2
@@ -0,0 +1,615 @@
1
+ /**
2
+ * Setup's pieces: the welcome (the hybrid ONE drawing itself, its O waking as One's eye and hopping through the Ones),
3
+ * type that arrives, choices with a rail that glides between rows in eighth-cell steps, a confirmation, a text field
4
+ * whose caret is the emblem's bar, and a waiting card where One's eye keeps you company.
5
+ *
6
+ * Every piece has a still form that is the same words in the same order, so reduced motion, screen readers, NO_COLOR,
7
+ * TERM=dumb and pipes get exactly the same setup.
8
+ */
9
+ import { Canvas, hex, mix } from './raster.js';
10
+ import { drawEmblem, drawLogotype, LOGO_ASPECT, LOGO_REST } from './art.js';
11
+ import { ONES } from './brand.js';
12
+ import { toCells, fg, kittyImage, itermImage, png, sixel, kittyDelete } from './encode.js';
13
+ import { animate, Interrupted, width } from './term.js';
14
+ import { BLINK, IdleEye, LIGHT, REST, SMILE, ease, sample } from './eye.js';
15
+ const ESC = '\x1b';
16
+ const FPS = 60;
17
+ export class Style {
18
+ term;
19
+ tone;
20
+ ink;
21
+ mutedColor;
22
+ constructor(term) {
23
+ this.term = term;
24
+ const c = term.caps;
25
+ this.tone = c.background ? (c.dark ? 'dark' : 'light') : 'unknown';
26
+ this.ink = c.background && c.color >= 24 ? hex(c.dark ? '#f2f1ea' : '#191614') : null;
27
+ this.mutedColor = c.background && c.color >= 24 ? hex(c.dark ? '#b2afad' : '#625f5d') : null;
28
+ }
29
+ sgr(open, s) { return this.term.caps.color ? `${open}${s}${ESC}[0m` : s; }
30
+ bold = (s) => this.sgr(`${ESC}[1m`, s);
31
+ muted = (s) => this.mutedColor ? this.sgr(fg(this.mutedColor, 24), s) : this.sgr(`${ESC}[2m`, s);
32
+ /** Text partway through arriving: from the background toward ink, in linear light. */
33
+ arriving(s, k) {
34
+ if (k >= 1 || !this.term.caps.color)
35
+ return s;
36
+ if (this.ink && this.term.caps.background)
37
+ return `${fg(mix(this.term.caps.background, this.ink, ease('enter', k)), 24)}${s}${ESC}[0m`;
38
+ return k < 0.5 ? `${ESC}[2m${s}${ESC}[0m` : s;
39
+ }
40
+ error = (s) => this.sgr(`${ESC}[31m`, s);
41
+ get check() { return this.term.caps.unicode ? '✓' : 'ok'; }
42
+ get dot() { return this.term.caps.unicode ? '·' : '-'; }
43
+ }
44
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
45
+ /** Resolve when a key arrives or `ms` passes; true if a key cut it short. */
46
+ async function wait(term, ms) {
47
+ const ac = new AbortController();
48
+ const k = term.key(ac.signal).then((key) => key.name !== 'abort');
49
+ const r = await Promise.race([k, sleep(ms).then(() => false)]);
50
+ ac.abort();
51
+ return r;
52
+ }
53
+ // ── welcome ────────────────────────────────────────────────────────────────
54
+ /** Pick three Ones for the parade: random per run, fixed by GENESIS_DEMO_SEED so recordings repeat. */
55
+ export function paradeCast(seed) {
56
+ let s = (seed ?? Math.floor(Math.random() * 2 ** 31)) >>> 0 || 1;
57
+ const rnd = () => { s = (s * 1103515245 + 12345) >>> 0; return s / 2 ** 32; };
58
+ const pool = [...ONES];
59
+ for (let i = pool.length - 1; i > 0; i--) {
60
+ const j = Math.floor(rnd() * (i + 1));
61
+ [pool[i], pool[j]] = [pool[j], pool[i]];
62
+ }
63
+ return pool.slice(0, 3);
64
+ }
65
+ /** The logotype's state at time t of the welcome. Pure, so the tests can check any frame. */
66
+ export function welcomeAt(t, cast, short = false) {
67
+ const k = (a, d, e = 'enter') => ease(e, Math.min(1, Math.max(0, (t - a) / d)));
68
+ const reveal = { ring: k(0, 0.6), fill: 0, head: null, n: k(0.15, 0.52), e: k(0.26, 0.52) };
69
+ if (short) {
70
+ // Later runs: the logotype arrives and the O blinks once as One, then rests.
71
+ if (t < 0.85)
72
+ return { ...reveal, done: false };
73
+ if (t < 1.09)
74
+ return { ...LOGO_REST, fill: k(0.85, 0.24), done: false };
75
+ if (t < 1.6)
76
+ return { ...LOGO_REST, head: { one: null, pose: { ...REST, open: sample(BLINK, t - 1.12) } }, done: false };
77
+ if (t < 1.73)
78
+ return { ...LOGO_REST, fill: 1 - k(1.6, 0.13, 'leave'), done: false };
79
+ return { ...LOGO_REST, done: true };
80
+ }
81
+ if (t < 0.9)
82
+ return { ...reveal, done: false };
83
+ if (t < 1.14)
84
+ return { ...LOGO_REST, fill: k(0.9, 0.24), done: false };
85
+ const one = (pose, extra = {}) => ({ ...LOGO_REST, head: { one: null, pose: { ...REST, ...pose }, ...extra }, done: false });
86
+ const light = t < 3.0 ? sample(LIGHT, t - 1.2) : 1;
87
+ // wake: open from shut with the blink's snap, then glance right and back, then blink.
88
+ if (t < 2.9) {
89
+ const open = t < 1.31 ? sample(BLINK, 0.13 + (t - 1.14)) : t > 2.6 ? sample(BLINK, t - 2.6) : 1;
90
+ const look = t < 1.6 ? 0 : t < 2.3 ? 0.6 * k(1.6, 0.22, 'snap') : 0.6 * (1 - k(2.3, 0.22, 'snap'));
91
+ return one({ open, look }, { light });
92
+ }
93
+ // the parade: hop into each One (the cut on landing), then home.
94
+ const HOP = 0.42, start = 2.9, stops = [...cast, null];
95
+ if (t < start + HOP * stops.length) {
96
+ const i = Math.floor((t - start) / HOP), p = (t - start - i * HOP) / HOP;
97
+ const from = i === 0 ? null : stops[i - 1], to = stops[i];
98
+ const squat = p < 0.16 ? 1 - 0.12 * ease('glide', p / 0.16) : p < 0.24 ? 0.88 + 0.2 * ease('snap', (p - 0.16) / 0.08) : 1;
99
+ const air = p >= 0.2 && p < 0.62 ? Math.sin((Math.PI * (p - 0.2)) / 0.42) : 0;
100
+ const land = p >= 0.62 && p < 0.9 ? 1 - 0.1 * Math.sin((Math.PI * (p - 0.62)) / 0.28) : 1;
101
+ const shape = p < 0.62 ? from : to;
102
+ return { ...LOGO_REST, head: { one: shape, pose: { ...REST, lift: -0.8 * air, open: 1 }, hop: -16 * air, squash: squat * land }, done: false };
103
+ }
104
+ const after = t - (start + HOP * stops.length);
105
+ if (after < 0.5)
106
+ return one({ open: sample(BLINK, after - 0.08) });
107
+ if (after < 0.63)
108
+ return { ...LOGO_REST, fill: 1 - k(start + HOP * stops.length + 0.5, 0.13, 'leave'), done: false };
109
+ return { ...LOGO_REST, done: true };
110
+ }
111
+ export function hero(term, style) {
112
+ const cols = term.columns;
113
+ if (cols < 40 || !term.caps.unicode)
114
+ return null;
115
+ let px = Math.floor((cols - 6) / LOGO_ASPECT / 2) * 2;
116
+ px = Math.max(10, Math.min(20, px));
117
+ const w = Math.ceil(px * LOGO_ASPECT) + 2;
118
+ return {
119
+ rows: px / 2, px,
120
+ draw(state) {
121
+ const cv = new Canvas(w, px);
122
+ drawLogotype(cv, 1, 0, px, state, style.tone);
123
+ return toCells(cv, { depth: term.caps.color, background: term.caps.background, unicode: true });
124
+ },
125
+ };
126
+ }
127
+ /**
128
+ * The still logotype at device resolution where the terminal can show images (Kitty, iTerm2/WezTerm, sixel): the
129
+ * half-block animation pulls into focus as a crisp drawing in exactly the same cells.
130
+ */
131
+ function heroImage(term, style, h) {
132
+ const g = term.caps.graphics;
133
+ if (!g)
134
+ return null;
135
+ const cell = term.caps.cell ?? { width: 9, height: 18 };
136
+ const cols = Math.ceil(h.px * LOGO_ASPECT) + 2, rows = h.rows;
137
+ const W = cols * cell.width, H = rows * cell.height;
138
+ const cv = new Canvas(W, H, 3);
139
+ const k = H / h.px; // device pixels per half-block pixel
140
+ drawLogotype(cv, k, 0, H, LOGO_REST, style.tone);
141
+ if (g === 'kitty')
142
+ return kittyImage(png(cv), cols, rows);
143
+ if (g === 'iterm')
144
+ return itermImage(png(cv), cols, rows);
145
+ return term.caps.background ? sixel(cv, term.caps.background) : null;
146
+ }
147
+ export async function welcome(term, style, opts) {
148
+ const h = hero(term, style);
149
+ const tag = opts.tagline;
150
+ if (!term.caps.motion || !h) {
151
+ if (h && !term.caps.screenReader) {
152
+ for (const l of h.draw(LOGO_REST))
153
+ term.say(l);
154
+ term.say();
155
+ }
156
+ for (const l of tag)
157
+ term.say(l);
158
+ return;
159
+ }
160
+ const cast = paradeCast(opts.seed);
161
+ let skipped = false, interrupted = false;
162
+ const ac = new AbortController();
163
+ term.key(ac.signal).then((k) => { if (k.name !== 'abort')
164
+ skipped = true; }, () => { skipped = interrupted = true; });
165
+ term.say();
166
+ await animate(FPS, (t) => {
167
+ const s = skipped ? { ...LOGO_REST, done: true } : welcomeAt(t, cast, opts.short);
168
+ term.frame(h.draw(s));
169
+ return !s.done;
170
+ });
171
+ ac.abort();
172
+ if (interrupted) {
173
+ term.commit();
174
+ throw new Interrupted();
175
+ }
176
+ const img = heroImage(term, style, h);
177
+ if (img) {
178
+ // Clear the half-block drawing and put the image in its cells; the cursor stays where text continues.
179
+ term.frame(Array(h.rows).fill(''));
180
+ term.write(`\r${h.rows > 1 ? `${ESC}[${h.rows - 1}A` : ''}${term.margin}${img}${ESC}[${h.rows - 1}B\r`);
181
+ }
182
+ term.commit();
183
+ term.say();
184
+ await typeLines(term, style, tag);
185
+ }
186
+ /** Lines that type themselves in: a little faster on spaces, a breath at punctuation, each glyph fading up to ink. */
187
+ export async function typeLines(term, style, lines) {
188
+ if (!term.caps.motion) {
189
+ for (const l of lines)
190
+ term.say(l);
191
+ return;
192
+ }
193
+ for (const line of lines) {
194
+ const plain = [...line];
195
+ if (/\x1b/.test(line) || !plain.length) {
196
+ term.say(line);
197
+ continue;
198
+ }
199
+ const at = [];
200
+ let clock = 0;
201
+ for (const ch of plain) {
202
+ at.push(clock);
203
+ clock += ch === ' ' ? 0.008 : /[.,:;—]/.test(ch) ? 0.05 : 0.014;
204
+ }
205
+ let skipped = false, interrupted = false;
206
+ const ac = new AbortController();
207
+ term.key(ac.signal).then((k) => { if (k.name !== 'abort')
208
+ skipped = true; }, () => { skipped = interrupted = true; });
209
+ const FADE = 0.12;
210
+ await animate(FPS, (t) => {
211
+ if (skipped)
212
+ t = clock + FADE;
213
+ let s = '';
214
+ for (let i = 0; i < plain.length; i++) {
215
+ if (at[i] > t)
216
+ break;
217
+ s += style.arriving(plain[i], (t - at[i]) / FADE);
218
+ }
219
+ term.frame([s]);
220
+ return t < clock + FADE;
221
+ });
222
+ ac.abort();
223
+ term.commit();
224
+ if (interrupted)
225
+ throw new Interrupted();
226
+ }
227
+ }
228
+ const EIGHTHS = ['', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
229
+ /**
230
+ * One row's rail cell when the rail (one row tall) sits at fractional row `pos`. Lower eighths draw the rail coming in
231
+ * from below; reverse video of the same glyphs draws it leaving through the bottom (the terminal swaps its own ink
232
+ * and paper, so this works without knowing either).
233
+ */
234
+ export function railCell(row, pos) {
235
+ const d = pos - row;
236
+ if (d <= -1 || d >= 1)
237
+ return ' ';
238
+ if (d >= 0) {
239
+ const n = Math.round((1 - d) * 8);
240
+ return n <= 0 ? ' ' : n >= 8 ? '█' : EIGHTHS[n];
241
+ }
242
+ // The rail's foot is in this cell's top part: reverse video of a lower block leaves the top (8 − n)/8 in ink.
243
+ const n = Math.round(-d * 8);
244
+ return n <= 0 ? '█' : n >= 8 ? ' ' : `${ESC}[7m${EIGHTHS[n]}${ESC}[27m`;
245
+ }
246
+ export async function select(term, style, q) {
247
+ const opts = q.options;
248
+ let idx = Math.max(0, Math.min(opts.length - 1, q.initial ?? 0));
249
+ while (opts[idx]?.disabled && idx < opts.length - 1)
250
+ idx++;
251
+ const footer = ' ' + style.muted(`↑↓ choose enter ok${q.skippable ? ' s skip' : ''}`);
252
+ if (term.caps.screenReader || !term.caps.interactive) {
253
+ term.say(q.title);
254
+ opts.forEach((o, i) => term.say(` ${i + 1}. ${o.label}${o.hint ? ` (${o.hint})` : ''}${i === idx ? ' [default]' : ''}`));
255
+ if (!term.caps.interactive)
256
+ return opts[idx].value;
257
+ term.say(`Type a number and press enter${q.skippable ? ', or s to skip' : ''}. Enter alone takes the default.`);
258
+ let buf = '';
259
+ for (;;) {
260
+ const k = await term.key();
261
+ if (k.name === 'enter') {
262
+ const n = buf ? Number(buf) - 1 : idx;
263
+ if (opts[n] && !opts[n].disabled) {
264
+ term.say(` ${opts[n].label}`);
265
+ return opts[n].value;
266
+ }
267
+ buf = '';
268
+ continue;
269
+ }
270
+ if (q.skippable && k.name === 's') {
271
+ term.say(' Skipped');
272
+ return 'skip';
273
+ }
274
+ if (k.ch && /\d/.test(k.ch))
275
+ buf += k.ch;
276
+ if (k.name === 'backspace')
277
+ buf = buf.slice(0, -1);
278
+ }
279
+ }
280
+ let pos = idx, from = idx, moveAt = -1;
281
+ const t0 = performance.now();
282
+ const now = () => (performance.now() - t0) / 1000;
283
+ const render = () => {
284
+ const t = now();
285
+ if (moveAt >= 0 && term.caps.motion) {
286
+ const p = Math.min(1, (t - moveAt) / 0.16);
287
+ pos = from + (idx - from) * ease('snap', p);
288
+ if (p >= 1)
289
+ moveAt = -1;
290
+ }
291
+ else
292
+ pos = idx;
293
+ term.frame([style.bold(q.title), ...opts.map((o, i) => {
294
+ const label = o.disabled ? style.muted(o.label) : i === idx ? style.bold(o.label) : o.label;
295
+ return ` ${railCell(i, pos)} ${label}${o.hint ? ' ' + style.muted(o.hint) : ''}`;
296
+ }), footer]);
297
+ };
298
+ render();
299
+ let running = true;
300
+ const loop = term.caps.motion ? animate(FPS, () => { if (moveAt >= 0)
301
+ render(); return running; }) : Promise.resolve();
302
+ try {
303
+ for (;;) {
304
+ const k = await term.key();
305
+ const step = (d) => { let n = idx; do
306
+ n = (n + d + opts.length) % opts.length;
307
+ while (opts[n].disabled && n !== idx); from = pos; idx = n; moveAt = now(); render(); };
308
+ if (k.name === 'up' || k.name === 'k' || k.name === 'backtab')
309
+ step(-1);
310
+ else if (k.name === 'down' || k.name === 'j' || k.name === 'tab')
311
+ step(1);
312
+ else if (k.ch && /^[1-9]$/.test(k.ch) && opts[Number(k.ch) - 1] && !opts[Number(k.ch) - 1].disabled) {
313
+ from = pos;
314
+ idx = Number(k.ch) - 1;
315
+ moveAt = now();
316
+ render();
317
+ }
318
+ else if (k.name === 'enter' || k.name === 'space') {
319
+ running = false;
320
+ await loop;
321
+ term.frame([`${style.bold(q.title)} ${style.muted(opts[idx].label)}`]);
322
+ term.commit();
323
+ return opts[idx].value;
324
+ }
325
+ else if (q.skippable && (k.name === 's' || k.name === 'escape')) {
326
+ running = false;
327
+ await loop;
328
+ term.frame([`${style.bold(q.title)} ${style.muted('skipped')}`]);
329
+ term.commit();
330
+ return 'skip';
331
+ }
332
+ }
333
+ }
334
+ finally {
335
+ running = false;
336
+ }
337
+ }
338
+ export async function confirm(term, style, question, initial) {
339
+ const hint = initial ? 'Y/n' : 'y/N';
340
+ if (!term.caps.interactive) {
341
+ term.say(`${question} ${initial ? 'yes' : 'no'}`);
342
+ return initial;
343
+ }
344
+ if (term.caps.screenReader)
345
+ term.say(`${question} (${hint})`);
346
+ else
347
+ term.frame([`${style.bold(question)} ${style.muted(hint)}`]);
348
+ for (;;) {
349
+ const k = await term.key();
350
+ const answer = k.name === 'enter' ? initial : k.name === 'y' ? true : k.name === 'n' || k.name === 'escape' || k.name === 's' ? false : null;
351
+ if (answer === null)
352
+ continue;
353
+ if (term.caps.screenReader)
354
+ term.say(answer ? 'yes' : 'no');
355
+ else {
356
+ term.frame([`${style.bold(question)} ${style.muted(answer ? 'yes' : 'no')}`]);
357
+ term.commit();
358
+ }
359
+ return answer;
360
+ }
361
+ }
362
+ /** A one-line field. The caret is a steady bar (DECSCUSR 6), the emblem's bar; typed letters fade up to ink. */
363
+ export async function input(term, style, q) {
364
+ if (!term.caps.interactive) {
365
+ term.say(`${q.label} ${q.initial}`);
366
+ return q.initial;
367
+ }
368
+ let value = '', error = null;
369
+ const born = [];
370
+ const t0 = performance.now(), now = () => (performance.now() - t0) / 1000;
371
+ term.cursor(true);
372
+ term.cursorShape(6);
373
+ const render = () => {
374
+ const shown = value ? [...value].map((ch, i) => style.arriving(ch, (now() - born[i]) / 0.12)).join('') : style.muted(q.initial);
375
+ const line = `${style.bold(q.label)} ${shown}`;
376
+ const lines = [line, error ? style.error(error) : style.muted('enter ok')];
377
+ term.frame(lines);
378
+ // Put the real caret after the typed text: up one line, to the column.
379
+ const col = width(`${q.label} `) + width(value);
380
+ term.write(`${ESC}[1A\r${col ? `${ESC}[${col}C` : ''}`);
381
+ };
382
+ let running = true;
383
+ const anim = term.caps.motion ? animate(FPS, () => { if (born.length && now() - born[born.length - 1] < 0.15) {
384
+ term.write(`${ESC}[1B`);
385
+ render();
386
+ } return running; }) : Promise.resolve();
387
+ render();
388
+ try {
389
+ for (;;) {
390
+ const k = await term.key();
391
+ if (k.name === 'enter') {
392
+ const v = value || q.initial, e = q.validate?.(v) ?? null;
393
+ if (e) {
394
+ error = e;
395
+ term.write(`${ESC}[1B`);
396
+ render();
397
+ continue;
398
+ }
399
+ running = false;
400
+ await anim;
401
+ term.write(`${ESC}[1B`);
402
+ term.frame([`${style.bold(q.label)} ${style.muted(v)}`]);
403
+ term.commit();
404
+ return v;
405
+ }
406
+ if (k.name === 'backspace') {
407
+ value = [...value].slice(0, -1).join('');
408
+ born.pop();
409
+ }
410
+ else if (k.name === 'paste' && k.ch) {
411
+ for (const ch of k.ch.replace(/[\r\n\t\x00-\x1f\x7f]/g, '')) {
412
+ value += ch;
413
+ born.push(now());
414
+ }
415
+ }
416
+ else if (k.ch && !k.ctrl && k.name !== 'space') {
417
+ value += k.ch;
418
+ born.push(now());
419
+ }
420
+ else if (k.name === 'space') {
421
+ value += ' ';
422
+ born.push(now());
423
+ }
424
+ else if (k.name === 'escape') {
425
+ value = '';
426
+ born.length = 0;
427
+ }
428
+ error = null;
429
+ term.write(`${ESC}[1B`);
430
+ render();
431
+ }
432
+ }
433
+ finally {
434
+ running = false;
435
+ term.cursorShape(0);
436
+ term.cursor(!term.caps.motion);
437
+ }
438
+ }
439
+ /**
440
+ * Wait for `task` beside One's eye. The eye idles as on the site (blinks, glances at the text), turns to look at you
441
+ * when you come back to the window, and the tab shows indeterminate progress. Keys in `keys` end the wait early and
442
+ * are returned (e.g. "d" to switch to a device code); Ctrl-C stops setup, resumably.
443
+ */
444
+ export async function waitBeside(term, style, lines, task, keys = []) {
445
+ term.progress(3);
446
+ if (!term.caps.interactive || !term.caps.motion || term.caps.screenReader || !term.caps.unicode) {
447
+ for (const l of lines())
448
+ term.say(l);
449
+ const ac = new AbortController();
450
+ const keyP = term.caps.interactive ? keyMatching(term, keys, ac.signal) : new Promise(() => { });
451
+ try {
452
+ return await Promise.race([task.then((value) => ({ value })), keyP.then((key) => ({ key }))]);
453
+ }
454
+ finally {
455
+ ac.abort();
456
+ term.progress(0);
457
+ }
458
+ }
459
+ const eye = new IdleEye(11);
460
+ const t0 = performance.now(), now = () => (performance.now() - t0) / 1000;
461
+ const onFocus = (f) => { if (f) {
462
+ eye.look(now(), 0);
463
+ eye.blink(now() + 0.25);
464
+ } };
465
+ term.focusListeners.add(onFocus);
466
+ let result;
467
+ task.then((value) => { result = { value }; }, () => { result = result ?? {}; });
468
+ const ac = new AbortController();
469
+ let interrupted = false;
470
+ keyMatching(term, keys, ac.signal).then((key) => { if (key.name !== 'abort')
471
+ result = { key }; }, () => { interrupted = true; });
472
+ const PX = 13; // odd, centred on a pixel, so the bar lands on whole pixel columns
473
+ try {
474
+ await animate(30, (t) => {
475
+ // PX is odd and the disc is centred on a pixel's middle, so the eye lands on a whole column.
476
+ const cv = new Canvas(PX, PX + 1);
477
+ drawEmblem(cv, PX / 2, (PX + 1) / 2, PX, { pose: eye.pose(t) }, style.tone);
478
+ const art = toCells(cv, { depth: term.caps.color, background: term.caps.background, unicode: true });
479
+ const text = lines();
480
+ const top = Math.max(0, Math.floor((art.length - text.length) / 2));
481
+ term.frame(art.map((a, i) => `${a}${' '.repeat(Math.max(0, PX - width(a)))} ${text[i - top] ?? ''}`).concat(text.slice(art.length - top)));
482
+ return !result && !interrupted;
483
+ });
484
+ }
485
+ finally {
486
+ term.focusListeners.delete(onFocus);
487
+ ac.abort();
488
+ term.progress(0);
489
+ }
490
+ if (interrupted)
491
+ throw new Interrupted();
492
+ if (result?.value !== undefined || result?.key) {
493
+ term.commit();
494
+ return result;
495
+ }
496
+ term.commit();
497
+ return { value: await task };
498
+ }
499
+ async function keyMatching(term, names, signal) {
500
+ for (;;) {
501
+ const k = await term.key(signal);
502
+ if (k.name === 'abort' || names.includes(k.name))
503
+ return k;
504
+ }
505
+ }
506
+ /** The moment it works: One smiles, and the light fills the eye and leaves. */
507
+ export async function delight(term, style, line) {
508
+ if (!term.caps.motion || !term.caps.unicode) {
509
+ term.say(line);
510
+ return;
511
+ }
512
+ const PX = 13;
513
+ await animate(FPS, (t) => {
514
+ const cv = new Canvas(PX, PX + 1);
515
+ const pose = { ...REST, smile: sample(SMILE.smile, t), mouth: sample(SMILE.mouth, t) };
516
+ drawEmblem(cv, PX / 2, (PX + 1) / 2, PX, { pose, light: sample(LIGHT, t - 0.1) }, style.tone);
517
+ const art = toCells(cv, { depth: term.caps.color, background: term.caps.background, unicode: true });
518
+ const mid = Math.floor(art.length / 2);
519
+ term.frame(art.map((a, i) => `${a}${' '.repeat(Math.max(0, PX - width(a)))} ${i === mid ? style.arriving(line, t / 0.3) : ''}`));
520
+ return t < 2.0;
521
+ });
522
+ term.commit();
523
+ }
524
+ export { kittyDelete };
525
+ /** Several choices at once (space toggles, a selects all, enter confirms). Screen readers type numbers. */
526
+ export async function pick(term, style, q) {
527
+ const opts = q.options.map((o) => ({ ...o }));
528
+ const box = (on) => term.caps.unicode ? (on ? '◉' : '○') : on ? '[x]' : '[ ]';
529
+ if (term.caps.screenReader || !term.caps.interactive) {
530
+ term.say(q.title);
531
+ opts.forEach((o, i) => term.say(` ${i + 1}. ${o.label}${o.hint ? ` (${o.hint})` : ''}${o.checked ? ' [selected]' : ''}`));
532
+ if (!term.caps.interactive)
533
+ return opts.filter((o) => o.checked && !o.disabled).map((o) => o.value);
534
+ term.say('Type numbers to toggle, then press enter. Enter alone keeps the selection.');
535
+ let buf = '';
536
+ for (;;) {
537
+ const k = await term.key();
538
+ if (k.ch && /\d/.test(k.ch)) {
539
+ buf += k.ch;
540
+ continue;
541
+ }
542
+ if (k.name === 'enter') {
543
+ if (!buf) {
544
+ const chosen = opts.filter((o) => o.checked && !o.disabled);
545
+ term.say(` ${chosen.map((o) => o.label).join(', ') || 'none'}`);
546
+ return chosen.map((o) => o.value);
547
+ }
548
+ const o = opts[Number(buf) - 1];
549
+ buf = '';
550
+ if (o && !o.disabled) {
551
+ o.checked = !o.checked;
552
+ term.say(` ${o.label} ${o.checked ? 'selected' : 'not selected'}`);
553
+ }
554
+ }
555
+ }
556
+ }
557
+ let idx = Math.max(0, opts.findIndex((o) => !o.disabled));
558
+ const render = () => term.frame([style.bold(q.title), ...opts.map((o, i) => {
559
+ const mark = o.disabled ? style.muted(box(false)) : box(o.checked);
560
+ const label = o.disabled ? style.muted(o.label) : i === idx ? style.bold(o.label) : o.label;
561
+ return ` ${railCell(i, idx)} ${mark} ${label}${o.hint ? ' ' + style.muted(o.hint) : ''}`;
562
+ }), ' ' + style.muted('space toggle a all enter ok')]);
563
+ render();
564
+ for (;;) {
565
+ const k = await term.key();
566
+ const move = (d) => { let n = idx; do
567
+ n = (n + d + opts.length) % opts.length;
568
+ while (opts[n].disabled && n !== idx); idx = n; };
569
+ if (k.name === 'up' || k.name === 'k')
570
+ move(-1);
571
+ else if (k.name === 'down' || k.name === 'j' || k.name === 'tab')
572
+ move(1);
573
+ else if (k.name === 'space' && !opts[idx].disabled)
574
+ opts[idx].checked = !opts[idx].checked;
575
+ else if (k.name === 'a') {
576
+ const all = opts.every((o) => o.disabled || o.checked);
577
+ for (const o of opts)
578
+ if (!o.disabled)
579
+ o.checked = !all;
580
+ }
581
+ else if (k.name === 'enter') {
582
+ const chosen = opts.filter((o) => o.checked && !o.disabled);
583
+ term.frame([`${style.bold(q.title)} ${style.muted(chosen.map((o) => o.label).join(', ') || 'none')}`]);
584
+ term.commit();
585
+ return chosen.map((o) => o.value);
586
+ }
587
+ else if (k.name === 'escape' || k.name === 's') {
588
+ term.frame([`${style.bold(q.title)} ${style.muted('skipped')}`]);
589
+ term.commit();
590
+ return [];
591
+ }
592
+ render();
593
+ }
594
+ }
595
+ /**
596
+ * A short wait on one line: the emblem's bar blinking on agent-eye's timing (its lid closing as lower eighth blocks),
597
+ * then the line resolves to its result. Still terminals print the label once.
598
+ */
599
+ export async function busy(term, style, label, task) {
600
+ if (!term.caps.motion || !term.caps.unicode) {
601
+ term.say(style.muted(label + '…'));
602
+ return task;
603
+ }
604
+ let settled = false;
605
+ task.then(() => { settled = true; }, () => { settled = true; });
606
+ const eye = new IdleEye(5);
607
+ await animate(30, (t) => {
608
+ const open = Math.max(0, Math.min(1, eye.pose(t).open));
609
+ const n = Math.round(open * 8);
610
+ term.frame([`${n >= 8 ? '█' : EIGHTHS[n] || ' '} ${style.muted(label)}`]);
611
+ return !settled;
612
+ });
613
+ term.frame([]);
614
+ return task;
615
+ }
package/dist/orgs.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import type { Command } from 'commander';
2
+ import type { Genesis } from '@onesi/genesis';
3
+ type Dependencies = {
4
+ client: () => Promise<Genesis>;
5
+ emit: (data: unknown, pretty: () => string) => void;
6
+ signal?: AbortSignal;
7
+ };
8
+ /** Orgs accepts documents of 1 byte to 5 MiB (sent as base64 in JSON). */
9
+ export declare const ORGS_MAX_DOCUMENT_BYTES: number;
10
+ /**
11
+ * The Orgs commands that are more than one JSON request: uploading a document from a file, downloading
12
+ * a document's bytes to a path (checked against its sha256), and requesting formation with --dry-run.
13
+ * Every other Orgs operation is a generic command under `genesis orgs` (products.ts).
14
+ */
15
+ export declare function registerOrgs(program: Command, deps: Dependencies): void;
16
+ export {};