geneseed 3.1.0 → 3.1.2
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 +110 -0
- package/DESIGN.md +31 -20
- package/README.md +2 -2
- package/SETUP.md +30 -3
- package/SHIPPED.md +1 -1
- package/bin/{geneseed.mjs → build-driver.mjs} +88 -42
- package/bin/geneseed-cli.mjs +33 -32
- package/bin/geneseed-hook.mjs +7 -7
- package/docs/design-history.md +40 -40
- package/docs/extending.md +113 -85
- package/docs/web/rules.md +2 -0
- package/docs/web/setup-choices.md +13 -1
- package/harness.config.json +1 -1
- package/js/README.md +203 -0
- package/js/build/bundle.mjs +231 -0
- package/js/{catalog.mjs → build/catalog.mjs} +7 -7
- package/js/build/emit-claude.mjs +287 -0
- package/js/build/emit-common.mjs +255 -0
- package/js/build/emit-opencode.mjs +214 -0
- package/js/{generate.mjs → build/generate.mjs} +45 -31
- package/js/{render.mjs → build/render.mjs} +72 -11
- package/js/{checkout.mjs → build/source.mjs} +52 -11
- package/js/build/stubs.mjs +256 -0
- package/js/{themes.mjs → build/themes.mjs} +24 -24
- package/js/build/version.mjs +155 -0
- package/js/{hooks.mjs → hosts/hooks.mjs} +47 -44
- package/js/{hosts.mjs → hosts/hosts.mjs} +30 -28
- package/js/{installs.mjs → hosts/installs.mjs} +68 -16
- package/js/{link.mjs → hosts/link.mjs} +29 -28
- package/js/{mcp.mjs → hosts/mcp.mjs} +10 -9
- package/js/{native.mjs → hosts/native.mjs} +24 -22
- package/js/{opencode.mjs → hosts/opencode.mjs} +12 -7
- package/js/{settings.mjs → hosts/settings.mjs} +94 -69
- package/js/inspect/checks-authoring.mjs +761 -0
- package/js/inspect/checks-build.mjs +231 -0
- package/js/inspect/checks-repo.mjs +308 -0
- package/js/{diff.mjs → inspect/diff.mjs} +34 -31
- package/js/inspect/doctor.mjs +286 -0
- package/js/{excludes.mjs → inspect/excludes.mjs} +30 -33
- package/js/{inventory.mjs → inspect/inventory.mjs} +34 -20
- package/js/{registry.mjs → inspect/registry.mjs} +12 -10
- package/js/inspect/scan.mjs +125 -0
- package/js/{status.mjs → inspect/status.mjs} +27 -25
- package/js/inspect/validate.mjs +199 -0
- package/js/lib/fs.mjs +37 -759
- package/js/lib/json.mjs +344 -0
- package/js/lib/paths.mjs +242 -0
- package/js/lib/proc.mjs +3 -3
- package/js/lib/text.mjs +181 -0
- package/js/lib/udiff.mjs +1 -1
- package/js/{memory.mjs → maintain/memory.mjs} +10 -10
- package/js/{migrate.mjs → maintain/migrate.mjs} +28 -26
- package/js/{setup.mjs → maintain/setup.mjs} +49 -37
- package/js/{uninstall.mjs → maintain/uninstall.mjs} +62 -58
- package/js/{update.mjs → maintain/update.mjs} +51 -51
- package/js/{anim.mjs → ui/anim.mjs} +30 -28
- package/js/{cli.mjs → ui/cli.mjs} +10 -10
- package/js/{menu.mjs → ui/menu.mjs} +10 -9
- package/js/{tui.mjs → ui/tui.mjs} +36 -34
- package/js/web/actions.mjs +124 -89
- package/js/web/activity.mjs +22 -21
- package/js/web/api.mjs +88 -58
- package/js/web/daemon.mjs +319 -0
- package/js/web/docs.mjs +24 -21
- package/js/web/graph.mjs +0 -0
- package/js/web/handler.mjs +442 -0
- package/js/web/jobs.mjs +29 -21
- package/js/web/routes.mjs +223 -0
- package/js/web/server.mjs +56 -987
- package/package.json +11 -3
- package/src/AGENT.md.tmpl +1 -1
- package/src/doctrines/README.md +1 -1
- package/themes/opencode/README.md +1 -1
- package/web/dist/assets/{About-_MnJcwb_.js → About-CPvIFZGh.js} +1 -1
- package/web/dist/assets/{Activity-DpjqO-FR.js → Activity-z0uXdS7J.js} +1 -1
- package/web/dist/assets/{ActivityDetail-Bvrjv8oW.js → ActivityDetail-CEj5IJpF.js} +1 -1
- package/web/dist/assets/{Diff-DZ0IehBV.js → Diff-BMR8fPuG.js} +1 -1
- package/web/dist/assets/{Doctor-cu4krooO.js → Doctor-Bz5qE4r9.js} +1 -1
- package/web/dist/assets/{Graph-DUOPY65j.js → Graph-BV6a6t2l.js} +1 -1
- package/web/dist/assets/{Harnesses-B5Osg-BF.js → Harnesses-C5Ag5PaQ.js} +1 -1
- package/web/dist/assets/Laws-BK07NKw9.js +3 -0
- package/web/dist/assets/{Library-8y69I3_z.js → Library-CiXN9sC3.js} +1 -1
- package/web/dist/assets/{Markdown-Ds5GS1OD.js → Markdown-DG6FjUz-.js} +1 -1
- package/web/dist/assets/{Profile-7Jxa7ZUX.js → Profile-B2oSvdxH.js} +1 -1
- package/web/dist/assets/{Rules-UZ0jWkjg.js → Rules-DobVPd80.js} +1 -1
- package/web/dist/assets/{Skills-Bgkl0xLd.js → Skills-DQPsvsn9.js} +1 -1
- package/web/dist/assets/{StatusBadge-Cx4caF_2.js → StatusBadge-BhJ3HhdI.js} +1 -1
- package/web/dist/assets/{Themes-De8az7Sv.js → Themes-BP38FBU9.js} +1 -1
- package/web/dist/assets/{activity-VDAdVQ3q.js → activity-D9mfv7pY.js} +1 -1
- package/web/dist/assets/index-BE226Gv6.css +1 -0
- package/web/dist/assets/{index-BKrjBRV4.js → index-BZ7cPLMI.js} +1 -1
- package/web/dist/assets/index-BgIs5eIa.js +12 -0
- package/web/dist/assets/{index-wNiNzFFQ.js → index-DGM5j2hB.js} +1 -1
- package/web/dist/index.html +2 -2
- package/web/src/pages/Laws.jsx +135 -69
- package/docs/declined.md +0 -200
- package/docs/limits.md +0 -329
- package/js/doctor.mjs +0 -1712
- package/js/emit.mjs +0 -1410
- package/web/dist/assets/Laws-D8DJgTBp.js +0 -3
- package/web/dist/assets/index-1pPSz7Kl.css +0 -1
- package/web/dist/assets/index-C-gb-q-r.js +0 -12
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The per-theme install animation the LINE wizard plays.
|
|
3
3
|
*
|
|
4
4
|
* P7c, and the one leftover of that phase that is NOT the declared curses panel. P5i ported
|
|
5
5
|
* `_setup_lines` and left this out with a note — "belongs to P7 with the rest of the terminal
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* that has nothing to do with curses: it draws on the ORDINARY terminal, with cursor-up
|
|
9
9
|
* escapes and a sleep, and its only caller is a function that already crossed.
|
|
10
10
|
*
|
|
11
|
-
* SO IT IS NOT IN `js/tui.mjs`, DELIBERATELY. That module carries a structural assertion that
|
|
11
|
+
* SO IT IS NOT IN `js/ui/tui.mjs`, DELIBERATELY. That module carries a structural assertion that
|
|
12
12
|
* it contains no escape sequence at all — the thing that makes "the panel is declared" a
|
|
13
13
|
* measurement instead of a promise. This file necessarily contains `\x1b[{n}A`, so putting it
|
|
14
14
|
* there would have turned that assertion red and invited someone to weaken it. A line-mode
|
|
@@ -27,13 +27,15 @@
|
|
|
27
27
|
* environments in `tests/test_pure_function_parity.py` — none of which can produce it on
|
|
28
28
|
* either side.
|
|
29
29
|
*
|
|
30
|
-
* WHAT GATES IT. `tests/
|
|
31
|
-
* every key plus the prototype-chain
|
|
32
|
-
* `
|
|
33
|
-
*
|
|
34
|
-
* inside the
|
|
30
|
+
* WHAT GATES IT. `tests/__snapshots__/primitives/{posix,win32}.json`, replayed by
|
|
31
|
+
* `tests/snapshot/pure_snapshot.test.mjs`: `art_for` over every key plus the prototype-chain
|
|
32
|
+
* names, `_place`/`_tile`/`_height` over their arithmetic, `anim_ok` over its whole decision
|
|
33
|
+
* table, and `play_line` itself over 24 environments — captured on the reference side through
|
|
34
|
+
* a real `TextIOWrapper`, so the CRLF translation is inside the recording rather than outside
|
|
35
|
+
* it. There is no recorder left, so a red run means these functions moved, never the corpus.
|
|
35
36
|
*/
|
|
36
|
-
import {
|
|
37
|
+
import { printOut } from '../lib/fs.mjs';
|
|
38
|
+
import { parseIntStrict, codePointLength, padEndToWidth } from '../lib/text.mjs';
|
|
37
39
|
|
|
38
40
|
export const ART = {
|
|
39
41
|
imperial: {
|
|
@@ -184,14 +186,14 @@ const cut = (s, a, b) => Array.from(s).slice(a, b).join('');
|
|
|
184
186
|
/**
|
|
185
187
|
* `shutil.get_terminal_size((80, 24)).columns`.
|
|
186
188
|
*
|
|
187
|
-
* `
|
|
189
|
+
* `parseIntStrict` and not `parseInt`, and the corpus has a case for exactly that: Python's `int()`
|
|
188
190
|
* refuses `'80x'` outright and `get_terminal_size` swallows the ValueError into the fallback,
|
|
189
191
|
* where `parseInt('80x')` is a cheerful 80. A width one column off is invisible in a terminal
|
|
190
192
|
* and loud in a byte comparison. `<= 0` is the reference's own guard, which is why `'0'` and
|
|
191
193
|
* `'-5'` fall through to the fallback rather than producing a zero-width canvas.
|
|
192
194
|
*/
|
|
193
195
|
function terminalColumns() {
|
|
194
|
-
const env =
|
|
196
|
+
const env = parseIntStrict(process.env.COLUMNS);
|
|
195
197
|
if (env !== null && env > 0) return env;
|
|
196
198
|
// `sys.__stdout__` over there, `process.stdout` here. Both are undefined-ish off a
|
|
197
199
|
// terminal, and both then take the (80, 24) fallback.
|
|
@@ -237,7 +239,7 @@ export function place(row, x, width) {
|
|
|
237
239
|
*/
|
|
238
240
|
export function tile(tileStr, width, phase = 0) {
|
|
239
241
|
if (!tileStr) return '';
|
|
240
|
-
const n =
|
|
242
|
+
const n = codePointLength(tileStr);
|
|
241
243
|
const base = tileStr.repeat(Math.max(0, Math.floor(width / n) + 2));
|
|
242
244
|
const off = ((phase % n) + n) % n;
|
|
243
245
|
return cut(base, off, off + width);
|
|
@@ -256,10 +258,10 @@ export function height(poses) {
|
|
|
256
258
|
* both odd the extra column goes on the LEFT, and otherwise on the right. Nothing about
|
|
257
259
|
* "centre this" implies it. The title line of every animation is the only caller.
|
|
258
260
|
*/
|
|
259
|
-
function
|
|
260
|
-
const marg = width -
|
|
261
|
+
function centerText(s, width) {
|
|
262
|
+
const marg = width - codePointLength(s);
|
|
261
263
|
if (marg <= 0) return s;
|
|
262
|
-
|
|
264
|
+
|
|
263
265
|
const left = Math.floor(marg / 2) + (marg & width & 1);
|
|
264
266
|
return ' '.repeat(left) + s + ' '.repeat(marg - left);
|
|
265
267
|
}
|
|
@@ -280,21 +282,21 @@ const sleep = (ms) => {
|
|
|
280
282
|
export function playLine(theme, ok = true) {
|
|
281
283
|
const art = artFor(theme);
|
|
282
284
|
const width = Math.min(terminalColumns() - 1, 56);
|
|
283
|
-
|
|
284
|
-
|
|
285
|
+
printOut('\n');
|
|
286
|
+
printOut(`${centerText(art.title, width)}\n`);
|
|
285
287
|
if (!ok) return;
|
|
286
288
|
if (!animOk()) {
|
|
287
|
-
for (const r of art.card)
|
|
288
|
-
|
|
289
|
+
for (const r of art.card) printOut(`${cut(r, 0, width)}\n`);
|
|
290
|
+
printOut('\n');
|
|
289
291
|
return;
|
|
290
292
|
}
|
|
291
293
|
const poses = art.sprite;
|
|
292
294
|
const ground = art.ground || '';
|
|
293
295
|
const h = height(poses);
|
|
294
296
|
const canvasH = h + (ground ? 1 : 0);
|
|
295
|
-
const spriteW = Math.max(0, ...poses.flatMap((p) => p.map((r) =>
|
|
297
|
+
const spriteW = Math.max(0, ...poses.flatMap((p) => p.map((r) => codePointLength(r))));
|
|
296
298
|
try {
|
|
297
|
-
|
|
299
|
+
printOut('\n'.repeat(canvasH));
|
|
298
300
|
const steps = Math.min(width + spriteW, 64); // cap to ~1.6s of motion
|
|
299
301
|
for (let i = 0; i <= steps; i += 1) {
|
|
300
302
|
const pose = poses[Math.floor(i / 3) % poses.length];
|
|
@@ -302,25 +304,25 @@ export function playLine(theme, ok = true) {
|
|
|
302
304
|
const lines = [];
|
|
303
305
|
for (let r = 0; r < h; r += 1) lines.push(place(r < pose.length ? pose[r] : '', x, width));
|
|
304
306
|
if (ground) lines.push(tile(ground, width, i));
|
|
305
|
-
|
|
306
|
-
for (const ln of lines)
|
|
307
|
+
printOut(`\x1b[${canvasH}A`);
|
|
308
|
+
for (const ln of lines) printOut(`\r${cut(padEndToWidth(ln, width), 0, width)}\n`);
|
|
307
309
|
sleep(25);
|
|
308
310
|
}
|
|
309
311
|
// settle on the card
|
|
310
|
-
|
|
312
|
+
printOut(`\x1b[${canvasH}A`);
|
|
311
313
|
const card = art.card;
|
|
312
314
|
for (let r = 0; r < canvasH; r += 1) {
|
|
313
315
|
const row = r < card.length ? cut(card[r], 0, width) : '';
|
|
314
|
-
|
|
316
|
+
printOut(`\r${cut(padEndToWidth(row, width), 0, width)}\n`);
|
|
315
317
|
}
|
|
316
318
|
// any extra card rows beyond the canvas
|
|
317
|
-
for (let r = canvasH; r < card.length; r += 1)
|
|
318
|
-
|
|
319
|
+
for (let r = canvasH; r < card.length; r += 1) printOut(`${cut(card[r], 0, width)}\n`);
|
|
320
|
+
printOut('\n');
|
|
319
321
|
} catch {
|
|
320
322
|
// Terminal didn't cooperate — leave a clean static card.
|
|
321
323
|
try {
|
|
322
|
-
for (const r of art.card)
|
|
323
|
-
|
|
324
|
+
for (const r of art.card) printOut(`${cut(r, 0, width)}\n`);
|
|
325
|
+
printOut('\n');
|
|
324
326
|
} catch { /* nothing left to try */ }
|
|
325
327
|
}
|
|
326
328
|
}
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* that checks the WHOLE table, and outlives the parser, is `tests/unit/cli_table.test.mjs`;
|
|
40
40
|
* after that the table is simply the source of truth and is edited directly.
|
|
41
41
|
*
|
|
42
|
-
* THE COST, NAMED: this entry cannot parse anything without this file. `js/
|
|
42
|
+
* THE COST, NAMED: this entry cannot parse anything without this file. `js/build/source.mjs`
|
|
43
43
|
* already reads `harness.config.json` at import for every render, so a tracked data document
|
|
44
44
|
* the code needs is not a new shape here — and this is not the hook entry, whose latency
|
|
45
45
|
* budget is `bin/geneseed-hook.mjs`'s and is untouched.
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
import { readFileSync } from 'node:fs';
|
|
48
48
|
import path from 'node:path';
|
|
49
49
|
|
|
50
|
-
import { ROOT } from '
|
|
51
|
-
import {
|
|
50
|
+
import { ROOT } from '../build/source.mjs';
|
|
51
|
+
import { parseIntStrict, stripWhitespace } from '../lib/text.mjs';
|
|
52
52
|
|
|
53
53
|
const CLI_JSON = path.join(ROOT, 'js', 'cli-table.json');
|
|
54
54
|
|
|
@@ -150,7 +150,7 @@ function actionInvocation(a, positional) {
|
|
|
150
150
|
return args ? `${a.names.join(', ')} ${args}` : a.names.join(', ');
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
// `textwrap`'s `wordsep_re`, which is what makes `
|
|
153
|
+
// `textwrap`'s `wordsep_re`, which is what makes `wrapText` below a port rather than a guess.
|
|
154
154
|
//
|
|
155
155
|
// A GREEDY SPACE-ONLY WRAP WAS THE FIRST DRAFT AND THE GATE REFUSED IT. `textwrap` splits a
|
|
156
156
|
// hyphenated word into two chunks, so it can break a line inside `back-compat` where a
|
|
@@ -185,7 +185,7 @@ const WORDSEP = new RegExp(
|
|
|
185
185
|
* `rituals/_harness_tui*.py` calls `textwrap.wrap` in seven places and is declared unported;
|
|
186
186
|
* move this when that changes, not before.
|
|
187
187
|
*/
|
|
188
|
-
export function
|
|
188
|
+
export function wrapText(text, width) {
|
|
189
189
|
const chunks = text.split(WORDSEP).filter(Boolean).reverse();
|
|
190
190
|
const blank = (s) => s.trim() === '';
|
|
191
191
|
const lines = [];
|
|
@@ -297,7 +297,7 @@ function formatAction(a, positional, helpPosition, width) {
|
|
|
297
297
|
// `_whitespace_matcher` is `re.compile(r'\s+', re.ASCII)` — the ASCII class, so a help
|
|
298
298
|
// string carrying U+00A0 keeps it rather than having it collapsed to a break opportunity.
|
|
299
299
|
const collapsed = help.replace(/[ \t\n\v\f\r]+/g, ' ').trim();
|
|
300
|
-
const lines =
|
|
300
|
+
const lines = wrapText(collapsed, Math.max(width - helpPosition, 11));
|
|
301
301
|
parts.push(`${' '.repeat(indentFirst)}${lines[0]}\n`);
|
|
302
302
|
for (const line of lines.slice(1)) parts.push(`${' '.repeat(helpPosition)}${line}\n`);
|
|
303
303
|
} else if (!parts[0].endsWith('\n')) {
|
|
@@ -405,7 +405,7 @@ export function formatHelp(cmd, prog, width) {
|
|
|
405
405
|
* reads `COLUMNS` before it asks the tty, and falls back to 80 when neither answers, which is
|
|
406
406
|
* every pipe both implementations write help into.)
|
|
407
407
|
*
|
|
408
|
-
* `
|
|
408
|
+
* `parseIntStrict`, NOT `Number.parseInt`, and the tree already owned it. `get_terminal_size` spells
|
|
409
409
|
* this `int(os.environ['COLUMNS'])` inside `except (KeyError, ValueError)`, and `int` is not
|
|
410
410
|
* `parseInt`. Measured against the reference over a corpus of `$COLUMNS` values
|
|
411
411
|
* (`test_the_ports_help_width_is_shutil_get_terminal_size`): `COLUMNS=1_0` is 10 to Python and
|
|
@@ -415,11 +415,11 @@ export function formatHelp(cmd, prog, width) {
|
|
|
415
415
|
* why the corpus is the gate rather than a cell. A non-positive value falls through on both
|
|
416
416
|
* sides, which is Python's `if columns <= 0` and was already right here.
|
|
417
417
|
*
|
|
418
|
-
* `
|
|
418
|
+
* `stripWhitespace` because THIS is the caller `parseIntStrict`'s docblock did not have: its other two
|
|
419
419
|
* strip before calling, and `$COLUMNS` arrives however the shell set it — `int(' 80 ')` is 80.
|
|
420
420
|
*/
|
|
421
421
|
export function helpWidth() {
|
|
422
|
-
const env =
|
|
422
|
+
const env = parseIntStrict(stripWhitespace(process.env.COLUMNS ?? ''));
|
|
423
423
|
if (env !== null && env > 0) return env - 2;
|
|
424
424
|
return (process.stdout.columns > 0 ? process.stdout.columns : 80) - 2;
|
|
425
425
|
}
|
|
@@ -486,7 +486,7 @@ export function cliSpec(verb) {
|
|
|
486
486
|
}
|
|
487
487
|
for (const a of cmd.options ?? []) {
|
|
488
488
|
const dest = camel(a.dest);
|
|
489
|
-
|
|
489
|
+
|
|
490
490
|
const table = a.is_flag ? 'flags' : (a.nargs === '?' ? 'optValue' : 'options');
|
|
491
491
|
for (const name of a.names ?? []) {
|
|
492
492
|
(spec[table] ??= {})[name] = dest;
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* `rituals/_winterm.py` under the name `curses` when it does — so inside the harness the
|
|
21
21
|
* import succeeds, the panel opens, and it is the PORT that has no window. The fallback is
|
|
22
22
|
* still the right shape; only its explanation moved. The alternative that was considered and
|
|
23
|
-
* refused: building a numbered line-menu out of `js/setup.mjs`'s `askChoice`. That is not a
|
|
23
|
+
* refused: building a numbered line-menu out of `js/maintain/setup.mjs`'s `askChoice`. That is not a
|
|
24
24
|
* port of `_main_menu` — it is a THIRD user interface, differing from the reference on every
|
|
25
25
|
* screen, unreachable from every cell (no cell has a TTY), and it would have to be deleted
|
|
26
26
|
* the day P7c lands the real panel. The fallback is the reference's own contract for "no
|
|
@@ -37,14 +37,15 @@
|
|
|
37
37
|
* one input no cell can vary. See that file's P7a section for what it reaches and what it
|
|
38
38
|
* declares.
|
|
39
39
|
*/
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
40
|
+
import { printOut, printErr } from '../lib/fs.mjs';
|
|
41
|
+
import { which } from '../lib/paths.mjs';
|
|
42
|
+
import { startDaemon } from '../web/daemon.mjs';
|
|
42
43
|
|
|
43
44
|
/** `_menu_help` — the off-TTY arm, and the fallback both TTY arms end in. */
|
|
44
45
|
export function menuHelp() {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
printOut('Geneseed — no interactive menu here. Get started with: geneseed setup\n');
|
|
47
|
+
printOut('Other commands: bootstrap · update · build · doctor · diff · tui · web\n');
|
|
48
|
+
printOut('On a VT-capable terminal, a bare `./geneseed` opens the interactive menu of these.\n');
|
|
48
49
|
return 0;
|
|
49
50
|
}
|
|
50
51
|
|
|
@@ -66,7 +67,7 @@ export function menuHelp() {
|
|
|
66
67
|
*/
|
|
67
68
|
function browserAvailable() {
|
|
68
69
|
if (process.platform === 'win32' || process.platform === 'darwin') return true;
|
|
69
|
-
return
|
|
70
|
+
return which('xdg-open') !== null;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
/**
|
|
@@ -103,14 +104,14 @@ export function cmdMenu() {
|
|
|
103
104
|
// Python panel, and P2 took the pointer out rather than re-aiming it: the panel it named
|
|
104
105
|
// is the Python this migration deletes, and a refusal that sends a user to a file which
|
|
105
106
|
// will not be there is worse than one that simply says the screen does not exist.
|
|
106
|
-
|
|
107
|
+
printErr('[menu] TUI unavailable (this entry has no full-screen menu).\n');
|
|
107
108
|
return menuHelp();
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
/** `cmd_home` — the default for a bare `geneseed`. */
|
|
111
112
|
export async function cmdHome(args) {
|
|
112
113
|
if (webFirstOk()) {
|
|
113
|
-
|
|
114
|
+
printOut('[geneseed] opening the web console — `geneseed menu` for the terminal UI, '
|
|
114
115
|
+ 'GENESEED_NO_WEB=1 to disable.\n');
|
|
115
116
|
return startDaemon(null, 4747, true);
|
|
116
117
|
}
|
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
* `cmd_tui` (`rituals/_harness_tui.py`) is twenty-six lines and three arms. Off a TTY it
|
|
7
7
|
* prints one line and returns 1; when `import curses` fails it prints one line and returns
|
|
8
8
|
* 1; otherwise it builds the inventory and hands the screen to `curses.wrapper(_tui_loop)`.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* NO CELL COULD EVER GIVE `cmd_tui` A TTY while the acceptance matrix was the gate, so the
|
|
10
|
+
* off-TTY arm was the whole of its reachable behaviour. That changed with the matrix:
|
|
11
|
+
* `tests/unit/no_panel.test.mjs` fakes `isTTY` true in a child and asserts BOTH arms — the
|
|
12
|
+
* exit code, and that the refusal names the console rather than pretending to draw.
|
|
11
13
|
*
|
|
12
14
|
* THE PANEL ITSELF IS P7C'S, AND THIS ENTRY FALLS BACK RATHER THAN INVENTING ONE — the same
|
|
13
|
-
* decision `js/menu.mjs` made in P7a, for the same reason. `cmd_tui` already owns a fourth
|
|
15
|
+
* decision `js/ui/menu.mjs` made in P7a, for the same reason. `cmd_tui` already owns a fourth
|
|
14
16
|
* path: an `except Exception` around `curses.wrapper` that prints `[tui] full-screen panel
|
|
15
17
|
* unavailable (<reason>)` and returns 1, written for the Windows shim's `Unsupported`. That
|
|
16
18
|
* is the reference's own contract for "no full-screen panel is available here", so the port
|
|
@@ -27,25 +29,24 @@
|
|
|
27
29
|
* this port has shipped code before whose absence nothing would have noticed, and these are
|
|
28
30
|
* not that.
|
|
29
31
|
*
|
|
30
|
-
* WHAT IS DELIBERATELY NOT HERE: `_bx` (its non-ASCII arm
|
|
31
|
-
* which
|
|
32
|
-
*
|
|
33
|
-
*
|
|
32
|
+
* WHAT IS DELIBERATELY NOT HERE: `_bx` (its non-ASCII arm returned `curses.ACS_*` chtype
|
|
33
|
+
* ints, which belonged with the window) and every `(stdscr, curses, pal)` drawing helper.
|
|
34
|
+
* `_wrap_lines` is NOT deferred but RETIRED — it reflowed a body into a fixed-width detail
|
|
35
|
+
* pane, the pane is gone, and the web console does its own wrapping.
|
|
34
36
|
*
|
|
35
37
|
* **DO NOT WRITE A SECOND LINE BREAKER.** This block used to argue that `textwrap.wrap` was
|
|
36
38
|
* "a second `difflib` — a stdlib payload with no Node twin". It has one since P1:
|
|
37
|
-
* `
|
|
38
|
-
* required it, frozen at `tests/__snapshots__/textwrap.json`
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* `_tui_inventory` crossed in P6c and live in `js/inventory.mjs`.
|
|
39
|
+
* `wrapText` in `js/ui/cli.mjs`, written because `--help` was the caller that finally
|
|
40
|
+
* required it, frozen at `tests/__snapshots__/textwrap.json` at every width from 11 to 198.
|
|
41
|
+
* Anything that needs wrapping wraps THAT. `_parse_laws`, `load_registry`, `entity_status` and
|
|
42
|
+
* `_tui_inventory` crossed in P6c and live in `js/inspect/inventory.mjs`.
|
|
42
43
|
*/
|
|
43
44
|
import path from 'node:path';
|
|
44
45
|
|
|
45
|
-
import { THEMES } from '
|
|
46
|
-
import { readJsonMaybe } from '
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
46
|
+
import { THEMES } from '../build/source.mjs';
|
|
47
|
+
import { readJsonMaybe } from '../hosts/installs.mjs';
|
|
48
|
+
import { splitLines } from '../lib/udiff.mjs';
|
|
49
|
+
import { printOut } from '../lib/fs.mjs';
|
|
49
50
|
|
|
50
51
|
// ---- display tiers -------------------------------------------------------------------
|
|
51
52
|
//
|
|
@@ -84,12 +85,11 @@ export const GLYPH = glyphs(TUI_ASCII);
|
|
|
84
85
|
// are the port's own, and no dependency was added to get them.
|
|
85
86
|
//
|
|
86
87
|
// A HAND-CARRIED UNICODE TABLE IS THE KIND OF THING THAT IS RIGHT UNTIL IT IS NOT, so the
|
|
87
|
-
// gate over it is not a case list. `tests/
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
// instead of agreeing with itself.
|
|
88
|
+
// gate over it is not a case list. `tests/__snapshots__/dwidth.json` holds the whole
|
|
89
|
+
// U+0000..U+2FFFF sweep run-length encoded — 196 608 inputs, and a range this table gets wrong
|
|
90
|
+
// has nowhere to hide — replayed by `tests/snapshot/pure_snapshot.test.mjs`. It was RECORDED
|
|
91
|
+
// from Python's live `unicodedata` while that was still here; it cannot be re-recorded now, so
|
|
92
|
+
// a run that goes red is a change in these tables and never a change in the reference.
|
|
93
93
|
//
|
|
94
94
|
// THE VERSION IS DECLARED, not left in a comment, and that changed on the day the gate fired
|
|
95
95
|
// for real. `python3.14` ships unidata 16.0.0, which assigned U+0897 a combining class these
|
|
@@ -97,9 +97,11 @@ export const GLYPH = glyphs(TUI_ASCII);
|
|
|
97
97
|
// as designed, on a machine whose only difference was the interpreter. That is not a defect
|
|
98
98
|
// in either implementation and it must not read as one: the sweep is a comparison against a
|
|
99
99
|
// SPECIFIC Unicode version, and a comparison is meaningless against a different one. So the
|
|
100
|
-
// version is a constant
|
|
101
|
-
//
|
|
102
|
-
//
|
|
100
|
+
// version is a constant, `tests/__snapshots__/dwidth.json` records the version its runs were
|
|
101
|
+
// measured at, and `tests/snapshot/pure_snapshot.test.mjs` asserts the two still agree —
|
|
102
|
+
// reading the constant both by import and by regex out of this file, so the declaration
|
|
103
|
+
// cannot be satisfied by deleting it. There is no interpreter left to pin: CI now FAILS if
|
|
104
|
+
// python is findable at all.
|
|
103
105
|
export const DWIDTH_UNIDATA = '15.1.0';
|
|
104
106
|
|
|
105
107
|
//: East_Asian_Width W or F, below U+1F000 — above it the reference's own `>= 0x1F000` rule
|
|
@@ -332,10 +334,10 @@ const BAR_EIGHTHS = ' ▏▎▍▌▋▊▉█';
|
|
|
332
334
|
export function progressBar(frac, width = 24) {
|
|
333
335
|
const f = Math.max(0.0, Math.min(1.0, frac));
|
|
334
336
|
if (TUI_ASCII) {
|
|
335
|
-
const filled =
|
|
337
|
+
const filled = roundHalfEven(f * width);
|
|
336
338
|
return '#'.repeat(filled) + '-'.repeat(width - filled);
|
|
337
339
|
}
|
|
338
|
-
const eighths =
|
|
340
|
+
const eighths = roundHalfEven(f * width * 8);
|
|
339
341
|
const full = Math.floor(eighths / 8);
|
|
340
342
|
const rem = eighths % 8;
|
|
341
343
|
if (full >= width) return '█'.repeat(width);
|
|
@@ -347,7 +349,7 @@ export function progressBar(frac, width = 24) {
|
|
|
347
349
|
* one place in the file where that shows: a bar at exactly 0.5 of a cell is the frontier
|
|
348
350
|
* case the eighths resolution exists to draw, and `round(0.5)` is 0 there and 1 here.
|
|
349
351
|
*/
|
|
350
|
-
function
|
|
352
|
+
function roundHalfEven(x) {
|
|
351
353
|
const f = Math.floor(x);
|
|
352
354
|
const d = x - f;
|
|
353
355
|
if (d > 0.5) return f + 1;
|
|
@@ -382,7 +384,7 @@ export function themeFlair(theme) {
|
|
|
382
384
|
// the empty string where `''.split('\n')` answers `['']`, which would put a blank row
|
|
383
385
|
// in the banner of every theme that omits one; and its boundary set is Python's rather
|
|
384
386
|
// than `\s`, which P6d measured apart.
|
|
385
|
-
banner:
|
|
387
|
+
banner: splitLines(banner),
|
|
386
388
|
benediction: data.BENEDICTION ?? '',
|
|
387
389
|
};
|
|
388
390
|
}
|
|
@@ -462,9 +464,9 @@ export function detailLines(kind, label, data) {
|
|
|
462
464
|
// because all three are a titled rule rather than a rendered spec. New kinds only: the nine
|
|
463
465
|
// recorded cases name kinds that already existed, so no recorded answer moves.
|
|
464
466
|
if (kind === 'law' || kind === 'doctrine' || kind === 'ontology') {
|
|
465
|
-
return [label, ''].concat(data ?
|
|
467
|
+
return [label, ''].concat(data ? splitLines(data.body) : []);
|
|
466
468
|
}
|
|
467
|
-
if ((kind === 'agent' || kind === 'skill') && data) return
|
|
469
|
+
if ((kind === 'agent' || kind === 'skill') && data) return splitLines(data.body);
|
|
468
470
|
return null;
|
|
469
471
|
}
|
|
470
472
|
|
|
@@ -476,12 +478,12 @@ export function detailLines(kind, label, data) {
|
|
|
476
478
|
*/
|
|
477
479
|
export function cmdTui() {
|
|
478
480
|
if (!process.stdin.isTTY) {
|
|
479
|
-
|
|
481
|
+
printOut('[tui] not an interactive terminal. Use `geneseed setup`, `geneseed doctor`, or `geneseed build`.\n');
|
|
480
482
|
return 1;
|
|
481
483
|
}
|
|
482
|
-
// The pointer to the Python panel is gone for `js/menu.mjs`'s reason: the
|
|
484
|
+
// The pointer to the Python panel is gone for `js/ui/menu.mjs`'s reason: the
|
|
483
485
|
// panel it named is the Python being deleted. What is left is true and stays true.
|
|
484
|
-
|
|
486
|
+
printOut('[tui] full-screen panel unavailable (this entry carries the TUI\'s layout '
|
|
485
487
|
+ 'half, not its screens). Use `geneseed setup`, `geneseed doctor`, or `geneseed build`.\n');
|
|
486
488
|
return 1;
|
|
487
489
|
}
|