geneseed 3.14.0 → 3.16.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/CHANGELOG.md +66 -0
- package/SHIPPED.md +1 -1
- package/adapters/opencode/plugins/geneseed-context.js +45 -11
- package/adapters/opencode/plugins/geneseed-guard.js +16 -4
- package/adapters/opencode/plugins/geneseed-learn.js +37 -11
- package/adapters/opencode/plugins/geneseed-notify.js +37 -19
- package/adapters/opencode/plugins/geneseed-workflow.js +3 -1
- package/adapters/opencode/workflows/_runtime.js +16 -3
- package/bin/geneseed-cli.mjs +22 -20
- package/bin/geneseed-hook.mjs +6 -3
- package/docs/concepts/exclusions.md +21 -9
- package/docs/guides/install-scripted.md +1 -1
- package/docs/guides/project-context.md +1 -1
- package/docs/token-footprint.md +10 -7
- package/harness.config.json +1 -1
- package/js/README.md +9 -5
- package/js/build/args.mjs +38 -6
- package/js/build/bundle.mjs +10 -10
- package/js/build/driver.mjs +16 -14
- package/js/build/emit-claude.mjs +3 -2
- package/js/build/emit-common.mjs +2 -14
- package/js/build/emit-opencode.mjs +12 -8
- package/js/build/generate.mjs +26 -16
- package/js/build/render.mjs +34 -4
- package/js/build/source.mjs +43 -7
- package/js/build/stubs.mjs +6 -5
- package/js/build/version.mjs +21 -2
- package/js/cli-table.json +15 -0
- package/js/hosts/hooks-context.mjs +85 -34
- package/js/hosts/hooks-prims.mjs +2 -2
- package/js/hosts/hosts.mjs +24 -26
- package/js/hosts/installs.mjs +58 -16
- package/js/hosts/link.mjs +75 -26
- package/js/hosts/memory-files.mjs +13 -4
- package/js/hosts/native.mjs +106 -48
- package/js/hosts/opencode.mjs +46 -36
- package/js/hosts/shim.mjs +4 -2
- package/js/inspect/diff.mjs +9 -7
- package/js/inspect/excludes.mjs +10 -5
- package/js/inspect/status.mjs +20 -22
- package/js/inspect/validate.mjs +5 -3
- package/js/lib/fs.mjs +12 -11
- package/js/lib/json.mjs +4 -4
- package/js/lib/paths.mjs +27 -33
- package/js/lib/proc.mjs +3 -3
- package/js/lib/prompt.mjs +132 -0
- package/js/lib/text.mjs +18 -4
- package/js/lib/udiff.mjs +3 -2
- package/js/maintain/memory.mjs +5 -6
- package/js/maintain/migrate.mjs +4 -3
- package/js/maintain/setup.mjs +20 -135
- package/js/maintain/uninstall.mjs +10 -9
- package/js/maintain/update.mjs +115 -34
- package/js/ui/anim.mjs +8 -12
- package/js/ui/cli.mjs +29 -35
- package/js/ui/menu.mjs +7 -13
- package/js/ui/tui.mjs +14 -123
- package/js/web/actions.mjs +87 -389
- package/js/web/activity.mjs +1 -1
- package/js/web/api.mjs +33 -717
- package/js/web/catalog.mjs +527 -0
- package/js/web/daemon.mjs +21 -3
- package/js/web/handler.mjs +51 -27
- package/js/web/jobs.mjs +24 -2
- package/js/web/routes.mjs +61 -6
- package/js/web/server.mjs +4 -4
- package/js/web/state.mjs +138 -0
- package/js/web/user-files.mjs +339 -0
- package/package.json +1 -1
- package/src/AGENT.md.tmpl +1 -1
- package/web/dist/assets/{Activity-CjRqZnr-.js → Activity-kpWxd70F.js} +1 -1
- package/web/dist/assets/{ActivityDetail-OQTxKBj2.js → ActivityDetail-Ccr942VL.js} +1 -1
- package/web/dist/assets/Dashboard-D1lE7jcg.js +2 -0
- package/web/dist/assets/{Docs-CvOZ8akL.js → Docs-BHyaggFy.js} +3 -3
- package/web/dist/assets/{GreenhouseView-6KDjX8da.js → GreenhouseView-Sc_UDMOE.js} +1 -1
- package/web/dist/assets/Installs-Gw-pJ3Zi.js +2 -0
- package/web/dist/assets/JournalView-_gxpsxXi.js +1 -0
- package/web/dist/assets/{Laws-Du0-Zyp2.js → Laws-S9fq1r9T.js} +1 -1
- package/web/dist/assets/{Library-DOkO0TtB.js → Library-MBxadRzn.js} +1 -1
- package/web/dist/assets/{Loops-Dv_aLByJ.js → Loops-Da_JhlzM.js} +2 -2
- package/web/dist/assets/{OperatorHudView-BPP1dtLz.js → OperatorHudView--TkvDybH.js} +1 -1
- package/web/dist/assets/{Personal-26VPYkK0.js → Personal-CTXKHVbV.js} +1 -1
- package/web/dist/assets/Tabs-B0cdf8k6.js +1 -0
- package/web/dist/assets/api-CxgfYu18.js +1 -0
- package/web/dist/assets/drawable-BbWQxe82.js +1 -0
- package/web/dist/assets/index-ujKG1ZNf.js +2 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/Dashboard-J-xCe_Gk.js +0 -2
- package/web/dist/assets/Installs-MWuhNjSm.js +0 -2
- package/web/dist/assets/JournalView-C8MrOz2c.js +0 -1
- package/web/dist/assets/Tabs-BzKuFskD.js +0 -1
- package/web/dist/assets/api-D_ixdyEY.js +0 -1
- package/web/dist/assets/drawable-zVVDbsyw.js +0 -1
- package/web/dist/assets/index-KHt3wE_K.js +0 -2
package/js/maintain/setup.mjs
CHANGED
|
@@ -31,17 +31,17 @@
|
|
|
31
31
|
* and a cell's stdin is a pipe. So the whole wizard is unreachable from the acceptance
|
|
32
32
|
* matrix in the same way `_confirm` was in P5h — except that here it is not one branch, it
|
|
33
33
|
* is the verb. The `setup/*` cells therefore gate the REFUSAL, absolutely and on both sides,
|
|
34
|
-
* and everything past it is gated as a CORPUS in `tests/
|
|
34
|
+
* and everything past it is gated as a CORPUS in `tests/unit/wizard.test.mjs`.
|
|
35
35
|
*
|
|
36
36
|
* The corpus is a new shape and the brief predicted the reason: a corpus over a stdin reader
|
|
37
|
-
* needs a SEEDED FD, not a string. `tests/fixtures/pure_probe.
|
|
38
|
-
* redirected from a file of answers and
|
|
37
|
+
* needs a SEEDED FD, not a string. `tests/fixtures/pure_probe.mjs` is run with stdin
|
|
38
|
+
* redirected from a file of answers and its WHOLE stdout compared — so the prompts, the
|
|
39
39
|
* numbered menus, the default markers, the `About to run:` line and the returned selection
|
|
40
40
|
* are all one byte comparison. That is a stricter gate than a cell would have been, because
|
|
41
41
|
* a cell could not have varied the answers at all.
|
|
42
42
|
*/
|
|
43
43
|
import { spawnSync } from 'node:child_process';
|
|
44
|
-
import { existsSync
|
|
44
|
+
import { existsSync } from 'node:fs';
|
|
45
45
|
import path from 'node:path';
|
|
46
46
|
|
|
47
47
|
import { main as driverMain, resolveOut } from '../build/driver.mjs';
|
|
@@ -57,132 +57,10 @@ import {
|
|
|
57
57
|
} from '../hosts/installs.mjs';
|
|
58
58
|
import { printOut, printErr } from '../lib/fs.mjs';
|
|
59
59
|
import { which } from '../lib/paths.mjs';
|
|
60
|
+
import { ask, askChoice, confirm } from '../lib/prompt.mjs';
|
|
60
61
|
import { parseIntStrict } from '../lib/text.mjs';
|
|
61
62
|
import { NO_WINDOW } from '../lib/proc.mjs';
|
|
62
63
|
|
|
63
|
-
// --------------------------------------------------------------------------------------
|
|
64
|
-
// the three readers
|
|
65
|
-
// --------------------------------------------------------------------------------------
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* `_harness_setup._ask` — `input(f"{prompt}{suffix}: ").strip()`, or the default at EOF.
|
|
69
|
-
*
|
|
70
|
-
* WHY IT READS A BYTE AT A TIME. Node has no synchronous line read: `readFileSync(0)` blocks
|
|
71
|
-
* until EOF rather than until Enter, so a wizard built on it would hang on a real terminal
|
|
72
|
-
* until the user pressed Ctrl-D. Reading fd 0 one byte at a time to the first `\n` is what
|
|
73
|
-
* `input()` does, and it is the only synchronous shape that stops where Enter stops. The
|
|
74
|
-
* body arrived in P5h inside `js/maintain/uninstall.mjs`'s `confirm`; `setup` is the second caller,
|
|
75
|
-
* so it moved to where the Python's own owner is — `_harness_setup` — and `confirm` is built
|
|
76
|
-
* on it here exactly as `_confirm` is built on `_ask` there.
|
|
77
|
-
*
|
|
78
|
-
* The EOF branch is the one a piped caller sees, and it agrees with Python by two different
|
|
79
|
-
* routes that happen to produce one answer: `input()` RAISES `EOFError` and the `except`
|
|
80
|
-
* returns the default, while a zero-length read here leaves `line` empty and `|| dflt` does
|
|
81
|
-
* the same. A partial line with no trailing newline is not EOF to either — Python returns
|
|
82
|
-
* the characters and so does this.
|
|
83
|
-
*
|
|
84
|
-
* `.trim()` is `str.strip()` in every case but U+FEFF, which JS counts as whitespace and
|
|
85
|
-
* Python does not; that is a standing item of this port and not this function's to settle.
|
|
86
|
-
*/
|
|
87
|
-
export function ask(prompt, dflt = '') {
|
|
88
|
-
const suffix = dflt ? ` [${dflt}]` : '';
|
|
89
|
-
// `input()` writes its prompt to stdout with no newline — nothing for `printOut` to
|
|
90
|
-
// translate, and it must NOT gain one.
|
|
91
|
-
process.stdout.write(`${prompt}${suffix}: `);
|
|
92
|
-
// `?? ''` because `readLine` reports EOF-with-nothing-read as null since P6h; `_ask`
|
|
93
|
-
// catches `EOFError` and returns the default, which is what the empty string produces
|
|
94
|
-
// here through `|| dflt`. The two routes to one answer are the docblock's own point.
|
|
95
|
-
const ans = (readLine() ?? '').trim();
|
|
96
|
-
return ans || dflt;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* `input(prompt)` with EOF told apart from an empty line — `null` where Python RAISES.
|
|
101
|
-
*
|
|
102
|
-
* `ask` above cannot express that difference and does not need to: its EOF and its empty
|
|
103
|
-
* answer both mean "take the default". `_web_server.serve`'s npm prompt is the first
|
|
104
|
-
* caller where they differ, and they differ dangerously — `""` is in its accepted set, so
|
|
105
|
-
* a port that read EOF as an empty line would answer YES to "run npm install now?" on
|
|
106
|
-
* every non-interactive run. The reference catches `(EOFError, KeyboardInterrupt)` and
|
|
107
|
-
* answers "n"; this returns null so the caller can.
|
|
108
|
-
*
|
|
109
|
-
* No `.trim()`: `input()` does not strip, and both callers strip for themselves.
|
|
110
|
-
*/
|
|
111
|
-
export function promptLine(prompt) {
|
|
112
|
-
process.stdout.write(prompt);
|
|
113
|
-
return readLine();
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Fd 0 to the first `\n`, dropping a `\r` before it — the CRLF a Windows console sends.
|
|
118
|
-
*
|
|
119
|
-
* THE BYTES ARE COLLECTED AND DECODED ONCE, and that is a fix rather than a style. The
|
|
120
|
-
* version this arrived as decoded EACH BYTE on its own — `buf.toString('utf8', 0, n)` inside
|
|
121
|
-
* the loop — which turns every multi-byte character into two or three U+FFFD. It was
|
|
122
|
-
* invisible for a phase because its only caller was `confirm`, whose answer is `y` or `n`;
|
|
123
|
-
* the wizard's first non-ASCII answer is what exposed it, and the corpus caught it on the
|
|
124
|
-
* first run. Scanning for the terminators a byte at a time is still correct: `\n` and `\r`
|
|
125
|
-
* are ASCII, and no continuation byte of a UTF-8 sequence is below 0x80.
|
|
126
|
-
*/
|
|
127
|
-
function readLine() {
|
|
128
|
-
const buf = Buffer.alloc(1);
|
|
129
|
-
const bytes = [];
|
|
130
|
-
let eof = false;
|
|
131
|
-
for (;;) {
|
|
132
|
-
let n = 0;
|
|
133
|
-
try { n = readSync(0, buf, 0, 1, null); } catch { eof = true; break; }
|
|
134
|
-
if (n === 0) { eof = true; break; }
|
|
135
|
-
if (buf[0] === 0x0a) break;
|
|
136
|
-
if (buf[0] !== 0x0d) bytes.push(buf[0]);
|
|
137
|
-
}
|
|
138
|
-
// NULL ONLY FOR THE EMPTY EOF, which is the one case `input()` raises on. A partial line
|
|
139
|
-
// with no trailing newline is not EOF to Python — it returns the characters — and the
|
|
140
|
-
// `bytes.length` test is what keeps that arm returning a string here too.
|
|
141
|
-
if (eof && bytes.length === 0) return null;
|
|
142
|
-
return Buffer.from(bytes).toString('utf8');
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** `_harness_setup._confirm` — `_ask` with a Y/n suffix; the default on an empty answer. */
|
|
146
|
-
export function confirm(prompt, dflt = true) {
|
|
147
|
-
const ans = ask(`${prompt} (${dflt ? 'Y/n' : 'y/N'})`).toLowerCase();
|
|
148
|
-
return ans === '' ? dflt : ans[0] === 'y';
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* `_harness_setup._ask_choice` — a numbered menu; the chosen KEY, or the default.
|
|
153
|
-
*
|
|
154
|
-
* THREE THINGS ABOUT THE FALLBACK ORDER, all of them observable and none of them obvious:
|
|
155
|
-
*
|
|
156
|
-
* * The key match lives in Python's `except ValueError`, so an answer that PARSES as an
|
|
157
|
-
* integer never reaches it. `99` against a five-option menu returns the default; it does
|
|
158
|
-
* not go looking for an option literally named `99`.
|
|
159
|
-
* * An in-range index wins, a parsed out-of-range index falls to the default, and only an
|
|
160
|
-
* UNPARSEABLE answer is matched against the keys. `parseIntStrict` is what separates the second
|
|
161
|
-
* case from the third, and it is not `Number()` — see its own docblock.
|
|
162
|
-
* * The default's index is computed with `next(...)` and no fallback, so a default that is
|
|
163
|
-
* not in `options` raises `StopIteration` on the reference. That is reachable (a
|
|
164
|
-
* deployed install can carry a theme this checkout no longer ships) and it is mirrored
|
|
165
|
-
* as a throw rather than smoothed over: the two implementations must fail in the same
|
|
166
|
-
* place, and a silent `-1` here would be a divergence dressed as robustness.
|
|
167
|
-
*/
|
|
168
|
-
export function askChoice(prompt, options, dflt) {
|
|
169
|
-
printOut(`\n${prompt}:\n`);
|
|
170
|
-
options.forEach(([key, desc], i) => {
|
|
171
|
-
const label = desc ? `${key} — ${desc}` : key;
|
|
172
|
-
printOut(` ${i + 1}) ${label}${key === dflt ? ' (default)' : ''}\n`);
|
|
173
|
-
});
|
|
174
|
-
const defaultIdx = options.findIndex(([k]) => k === dflt);
|
|
175
|
-
if (defaultIdx < 0) throw new Error(`StopIteration: no option named ${dflt}`);
|
|
176
|
-
const raw = ask('Choose', String(defaultIdx + 1));
|
|
177
|
-
const idx = parseIntStrict(raw);
|
|
178
|
-
if (idx !== null) {
|
|
179
|
-
if (idx >= 1 && idx <= options.length) return options[idx - 1][0];
|
|
180
|
-
} else {
|
|
181
|
-
for (const [key] of options) if (raw === key) return key;
|
|
182
|
-
}
|
|
183
|
-
return dflt;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
64
|
// --------------------------------------------------------------------------------------
|
|
187
65
|
// the option tables
|
|
188
66
|
// --------------------------------------------------------------------------------------
|
|
@@ -372,7 +250,7 @@ export function collectSetupLines() {
|
|
|
372
250
|
const mode = askChoice('Mode', modeOptions(), inst.mode || defaultMode());
|
|
373
251
|
const doctrines = askDoctrines(inst.doctrines);
|
|
374
252
|
const emit = askChoice('Install mode', EMIT_OPTIONS, inst.emit || 'opencode-global');
|
|
375
|
-
const footprint = askChoice('Footprint', FOOTPRINT_OPTIONS
|
|
253
|
+
const footprint = askChoice('Footprint', FOOTPRINT_OPTIONS,
|
|
376
254
|
inst.footprint || 'lean');
|
|
377
255
|
let out = null;
|
|
378
256
|
let root = null;
|
|
@@ -399,11 +277,18 @@ export function collectSetupLines() {
|
|
|
399
277
|
// `setupBuildArgs` spell `--exclude-rules none`, and that string would land in the
|
|
400
278
|
// "About to run:" line every recorded wizard probe compares byte for byte.
|
|
401
279
|
const excludeRules = inst.excludeRules?.length ? inst.excludeRules : null;
|
|
280
|
+
// Skills left out are preserved the same way, and elide when there are none.
|
|
281
|
+
const excludeSkills = inst.excludeSkills?.length ? inst.excludeSkills : null;
|
|
402
282
|
printOut(`\nAbout to run: geneseed build ${
|
|
403
283
|
setupBuildArgs(theme, emit, out, root, footprint, posture, mode, doctrines, allPacks,
|
|
404
|
-
excludeRules).join(' ')}\n`);
|
|
284
|
+
excludeRules, undefined, excludeSkills).join(' ')}\n`);
|
|
405
285
|
if (!confirm('Proceed?', true)) return null;
|
|
406
|
-
|
|
286
|
+
// The key only when there is something to carry, so a selection that excludes no skill is
|
|
287
|
+
// the same object it was before the flag existed.
|
|
288
|
+
return {
|
|
289
|
+
theme, posture, mode, doctrines, excludeRules, ...(excludeSkills ? { excludeSkills } : {}),
|
|
290
|
+
emit, out, root, footprint,
|
|
291
|
+
};
|
|
407
292
|
}
|
|
408
293
|
|
|
409
294
|
// --------------------------------------------------------------------------------------
|
|
@@ -430,8 +315,8 @@ export function javaMajorOk(versionOutput, minimum = 21) {
|
|
|
430
315
|
* `_harness_setup._lsp_prereqs` — `(label, present, hint)` for what OpenCode cannot install
|
|
431
316
|
* for itself. Today that is a JDK 21+ for `jdtls`; the JS-runtime servers self-download.
|
|
432
317
|
*
|
|
433
|
-
* THE SECOND SPAWN ON THIS ENTRY POINT, and the reason `
|
|
434
|
-
*
|
|
318
|
+
* THE SECOND SPAWN ON THIS ENTRY POINT, and the reason `ALLOWED_SPAWNS` in
|
|
319
|
+
* `tests/unit/hook_cli.test.mjs` became a TABLE. `js/inspect/doctor.mjs` was the sole allowed importer
|
|
435
320
|
* for one binding and one argv; this is the second, and the port's rule is that the second
|
|
436
321
|
* instance of anything turns a special case into a table cross-checked against the source.
|
|
437
322
|
* The argv is asserted literally there, so an allow-list entry cannot be reused to start an
|
|
@@ -572,7 +457,7 @@ export function setupSummaryLines(theme, emit, out, root, ok) {
|
|
|
572
457
|
* command the reader can type, and names one that survives the deletion.
|
|
573
458
|
*
|
|
574
459
|
* `theme_anim.play_line` USED TO BE THE SECOND, and P7c crossed it — `js/ui/anim.mjs`, gated by
|
|
575
|
-
*
|
|
460
|
+
* the theme-animation jobs in `tests/unit/wizard.test.mjs`. The bare `catch` around it
|
|
576
461
|
* is the reference's own, labelled there "cosmetic only — never block a successful install",
|
|
577
462
|
* and it is kept for the same reason: the animation runs AFTER the build has already
|
|
578
463
|
* succeeded, so nothing it can do may change the exit code.
|
|
@@ -585,7 +470,7 @@ export function setupLines() {
|
|
|
585
470
|
}
|
|
586
471
|
const {
|
|
587
472
|
theme, emit, out, root, footprint = 'lean', posture = 'peer', mode = 'direct',
|
|
588
|
-
doctrines = null, excludeRules = null,
|
|
473
|
+
doctrines = null, excludeRules = null, excludeSkills = null,
|
|
589
474
|
} = sel;
|
|
590
475
|
if (emit === 'opencode-global') {
|
|
591
476
|
// The build below overwrites the deployed global harness, and the self-improvement loops
|
|
@@ -604,7 +489,7 @@ export function setupLines() {
|
|
|
604
489
|
// runs must carry the pack selection the plan line just previewed, or the wizard would print
|
|
605
490
|
// one command and run another.
|
|
606
491
|
const argv = setupBuildArgs(theme, emit, out, root, footprint, posture, mode, doctrines,
|
|
607
|
-
doctrineOptions().map(([n]) => n), excludeRules);
|
|
492
|
+
doctrineOptions().map(([n]) => n), excludeRules, undefined, excludeSkills);
|
|
608
493
|
printOut(`Running: geneseed build ${argv.join(' ')}\n`);
|
|
609
494
|
const rc = driverMain(argv);
|
|
610
495
|
if (rc !== 0) {
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
* 2. AN EMPTY DIRECTORY WAS INVISIBLE TO EVERY CELL. The ancestor-climb prune is a quarter of
|
|
18
18
|
* this verb and `_snapshot` could not see it: a port that unlinked the files and left
|
|
19
19
|
* `skills/<name>/` behind was byte-identical everywhere. Closed by a `<dirs>` column in
|
|
20
|
-
* `tests/
|
|
21
|
-
* entry closes it for all 219 cells written before it existed —
|
|
22
|
-
* order sat in the same hole. The ABSOLUTE half is a different axis and is the sixth
|
|
20
|
+
* the CLI matrix's snapshot (`tests/helpers/cli_golden.mjs`) rather than a per-cell
|
|
21
|
+
* expectation, because one entry closes it for all 219 cells written before it existed —
|
|
22
|
+
* `cmdTheme`'s statement order sat in the same hole. The ABSOLUTE half is a different axis and is the sixth
|
|
23
23
|
* expectation kind, `expect_absent_files`.
|
|
24
24
|
* 3. `--archive-memory` TOUCHES THE MEMORY STORE, the one thing here a user cannot rebuild.
|
|
25
25
|
* It is why `_archive_store` MOVES and never deletes, and why the Python's `memory=delete`
|
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
import path from 'node:path';
|
|
49
49
|
|
|
50
50
|
import { hookRunnerEntry } from '../hosts/shim.mjs';
|
|
51
|
-
import { confirm } from '
|
|
51
|
+
import { confirm } from '../lib/prompt.mjs';
|
|
52
52
|
import {
|
|
53
53
|
claudeCfg, claudeReadManifest, doctrinesOfDir, emitHostScopeOf, excludedRulesOfDir, installKind,
|
|
54
54
|
installState,
|
|
@@ -374,7 +374,7 @@ export function installAgentEntry(root, kind) {
|
|
|
374
374
|
* A cell can only ever observe the entries a seeded `opencode.json` holds, and the shape
|
|
375
375
|
* that separates the two `is_absolute` rules is a Windows ROOTLESS path — which a cell CAN
|
|
376
376
|
* seed but whose answer then only differs on one platform, in one branch, with no other
|
|
377
|
-
* observable effect.
|
|
377
|
+
* observable effect. The former Python corpus drove the list directly; nothing does now.
|
|
378
378
|
*/
|
|
379
379
|
export function installAgentEntryOf(instr) {
|
|
380
380
|
if (Array.isArray(instr)) {
|
|
@@ -732,9 +732,10 @@ export function cmdUninstall(args) {
|
|
|
732
732
|
// ALL-OR-NOTHING IS THE PROPERTY, and it is the one a port breaks silently. A move that fails
|
|
733
733
|
// puts every earlier move back and reports, leaving the install fully `active`; a port that
|
|
734
734
|
// merely stopped on the failure would leave a half-gutted config dir that neither state
|
|
735
|
-
// answers for. `tests/
|
|
736
|
-
// drives it
|
|
737
|
-
//
|
|
735
|
+
// answers for. `tests/unit/web_api.test.mjs`'s `deactivate rolls back on a failed move`
|
|
736
|
+
// drives it with a file planted where the stash dir belongs, which fails the FIRST move; the
|
|
737
|
+
// former web matrix's SECOND-move collision (a manifest naming a file AND its parent
|
|
738
|
+
// directory) has no successor.
|
|
738
739
|
|
|
739
740
|
/**
|
|
740
741
|
* `_harness_mcp._move_tree` — move a file or dir, refusing an existing destination.
|
|
@@ -926,7 +927,7 @@ function restoreAll(stash, base, skip = null) {
|
|
|
926
927
|
* The config edit is the LAST step and the only non-move mutation, so a move failure rolls
|
|
927
928
|
* back cleanly with the `instructions` entry still intact. The prune climbs from each moved
|
|
928
929
|
* file's PARENT so a `skills/<name>/` husk goes too — the half of this function no file
|
|
929
|
-
* snapshot can see, and the reason
|
|
930
|
+
* snapshot can see, and the reason the former web matrix grew a `<dirs>` column in P6i.
|
|
930
931
|
*/
|
|
931
932
|
export function installDeactivate(root, host = 'opencode', scope = 'global') {
|
|
932
933
|
if (CLAUDE_STYLE.includes(host)) return claudeDeactivate(root, scope, host);
|
package/js/maintain/update.mjs
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Self-update: `git pull` the install's own origin, validate, rebuild.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
* release
|
|
6
|
-
* because a Node install that shelled out to the Python harness to update itself
|
|
7
|
-
* would have Python as a hard dependency of the only command a user MUST be able to run.
|
|
4
|
+
* The one operation a user MUST always be able to run: every other verb can be broken by a bad
|
|
5
|
+
* release, and this is the verb that fetches the fix.
|
|
8
6
|
*
|
|
9
7
|
* WHY THIS MODULE SPAWNS, AND WHY THE ARGUMENT IS NOT THE JOB RUNNER'S. `js/web/jobs.mjs`
|
|
10
8
|
* re-executes this program for ISOLATION — a synchronous verb in the daemon's event loop
|
|
@@ -15,21 +13,23 @@
|
|
|
15
13
|
* the modules this process loaded before the merge — it would validate the old source, pass,
|
|
16
14
|
* and then render the old source into the bundle, reporting success. That is not a slower
|
|
17
15
|
* version of the right answer, it is the wrong one. So `runDoctor`, `rebuildBundle` and
|
|
18
|
-
* `rebuildInstalls` each start a fresh interpreter over the tree as it now is,
|
|
19
|
-
*
|
|
16
|
+
* `rebuildInstalls` each start a fresh interpreter over the tree as it now is, and
|
|
17
|
+
* `ALLOWED_SPAWNS` in `tests/unit/hook_cli.test.mjs` carries the argv. The same reason is why
|
|
18
|
+
* the two modules this file reaches lazily (`js/build/generate.mjs`, `js/web/daemon.mjs`) are
|
|
19
|
+
* loaded BEFORE the pull and not at their call sites: an `import()` after the merge would mix
|
|
20
|
+
* pulled files with the ones already cached, and a renamed export kills the upgrade mid-way.
|
|
20
21
|
*
|
|
21
22
|
* `git` is the fourth spawn and the plainest: there is no git library in the Node stdlib,
|
|
22
23
|
* and the port will not vendor one. Every git call goes through the `gitRun` seam —
|
|
23
24
|
* which-guarded, credential-redacted, and it never throws.
|
|
24
25
|
*
|
|
25
|
-
* WHAT THE GATE CANNOT SEE, stated rather than implied. `
|
|
26
|
-
* `tests/
|
|
27
|
-
* fixture builds, so no
|
|
26
|
+
* WHAT THE GATE CANNOT SEE, stated rather than implied. `tests/unit/update_git.test.mjs` and
|
|
27
|
+
* `tests/unit/update_flow.test.mjs` run each arm against a private clone of a bare origin the
|
|
28
|
+
* fixture builds, so no test can reach a network — which is a safety property and a blind
|
|
28
29
|
* spot at once: `git fetch --progress` over a local path prints NOTHING, so
|
|
29
30
|
* `fetchStreaming`'s reader, its phase filter and its 15s heartbeat are reachable in every
|
|
30
|
-
*
|
|
31
|
-
* `tests/
|
|
32
|
-
* neither and are declared in the P8a handoff.
|
|
31
|
+
* run and varied in none. The phase filter is gated over a recorded transcript in
|
|
32
|
+
* `tests/unit/update_git.test.mjs`; the heartbeat and the hard deadline are gated by nothing.
|
|
33
33
|
*
|
|
34
34
|
* ALSO HERE, SINCE P8b: `syncSelf`/`cmdSyncSelf` and the bootstrap step runner
|
|
35
35
|
* (`cmdBootstrap`, `bootstrapPlain`) at the bottom of this file. Still NOT here: `main`, the
|
|
@@ -47,10 +47,9 @@ import { ROOT } from '../build/source.mjs';
|
|
|
47
47
|
import { exportImprovements, flushExportNotes } from '../inspect/diff.mjs';
|
|
48
48
|
import { opencodeConfigDir } from '../hosts/hosts.mjs';
|
|
49
49
|
import { appendText, copyFile, printOut, printErr, readText, writeText } from '../lib/fs.mjs';
|
|
50
|
-
import { toPlatformPath, which } from '../lib/paths.mjs';
|
|
50
|
+
import { comparePaths, toPlatformPath, which } from '../lib/paths.mjs';
|
|
51
51
|
import { WHITESPACE, parseIntStrict, stripWhitespace } from '../lib/text.mjs';
|
|
52
52
|
import { NO_WINDOW } from '../lib/proc.mjs';
|
|
53
|
-
import { bounceDaemonIfRunning } from '../web/daemon.mjs';
|
|
54
53
|
|
|
55
54
|
/**
|
|
56
55
|
* `_CREDS_RE` / `_redact_url_creds` — strip a `user[:token]@` userinfo out of any URL in
|
|
@@ -248,8 +247,14 @@ export function countOccurrences(s) {
|
|
|
248
247
|
export function killTree(child) {
|
|
249
248
|
try {
|
|
250
249
|
if (process.platform !== 'win32') process.kill(-child.pid, 'SIGKILL');
|
|
251
|
-
else
|
|
252
|
-
|
|
250
|
+
else {
|
|
251
|
+
// `which`, never a bare name (a bare name resolves against the cwd first on Windows);
|
|
252
|
+
// no taskkill on PATH falls through to the `child.kill()` below.
|
|
253
|
+
const taskkill = which('taskkill');
|
|
254
|
+
if (!taskkill) throw new Error('taskkill not on PATH');
|
|
255
|
+
spawnSync(taskkill, ['/F', '/T', '/PID', String(child.pid)],
|
|
256
|
+
{ encoding: 'utf8', timeout: 15000, ...NO_WINDOW });
|
|
257
|
+
}
|
|
253
258
|
} catch {
|
|
254
259
|
try { child.kill(); } catch { /* already gone */ }
|
|
255
260
|
}
|
|
@@ -262,7 +267,7 @@ export function killTree(child) {
|
|
|
262
267
|
* times and universal-newline decoding turns every `\r` repaint into its own line, so
|
|
263
268
|
* without this a 30-second fetch writes a thousand lines into the install log. No cell can
|
|
264
269
|
* reach it — a local-path fetch prints nothing at all — so
|
|
265
|
-
* `tests/
|
|
270
|
+
* `tests/unit/update_git.test.mjs` runs it over a recorded fetch transcript instead.
|
|
266
271
|
*
|
|
267
272
|
* Returns `[linesToLog, lastPhase]`, splitting the reference's loop from its side effects.
|
|
268
273
|
*/
|
|
@@ -366,13 +371,19 @@ export async function fetchStreaming(log = null) {
|
|
|
366
371
|
|
|
367
372
|
/**
|
|
368
373
|
* `_measure_upstream(log)` — Phase B: fetch (streamed), then classify. Returns
|
|
369
|
-
* `[code, behind, err]` with `code` ∈ {ready, fetch_failed, unrelated, diverged,
|
|
374
|
+
* `[code, behind, err]` with `code` ∈ {ready, fetch_failed, no_upstream, unrelated, diverged,
|
|
375
|
+
* uptodate}.
|
|
376
|
+
*
|
|
377
|
+
* `no_upstream` HERE, AFTER PREFLIGHT SAID THERE WAS ONE: the fetch itself can take it away.
|
|
378
|
+
* An upstream branch deleted on the remote, fetched with `fetch.prune`, leaves `@{u}` naming
|
|
379
|
+
* nothing — both counts then fail, and read as two zeros they said "already up to date".
|
|
370
380
|
*/
|
|
371
381
|
export async function measureUpstream(log = null) {
|
|
372
382
|
const [rc, err] = await fetchStreaming(log);
|
|
373
383
|
if (rc !== 0) return ['fetch_failed', 0, err];
|
|
374
|
-
const [, aheadRaw] = gitRun(['rev-list', '--count', '@{u}..HEAD']);
|
|
375
|
-
const [, behindRaw] = gitRun(['rev-list', '--count', 'HEAD..@{u}']);
|
|
384
|
+
const [arc, aheadRaw] = gitRun(['rev-list', '--count', '@{u}..HEAD']);
|
|
385
|
+
const [brc, behindRaw] = gitRun(['rev-list', '--count', 'HEAD..@{u}']);
|
|
386
|
+
if (arc !== 0 || brc !== 0) return ['no_upstream', 0, ''];
|
|
376
387
|
const ahead = countOccurrences(aheadRaw);
|
|
377
388
|
const behind = countOccurrences(behindRaw);
|
|
378
389
|
if (ahead > 0) {
|
|
@@ -419,16 +430,31 @@ export function runDoctor(cand) {
|
|
|
419
430
|
return [r.status === 0, universal(r.stdout) + universal(r.stderr)];
|
|
420
431
|
}
|
|
421
432
|
|
|
422
|
-
/**
|
|
433
|
+
/**
|
|
434
|
+
* `_pull_and_validate(log)` — fast-forward to `@{u}`, then doctor-gate with exact rollback.
|
|
435
|
+
*
|
|
436
|
+
* `collision` ONLY WHEN GIT SAYS SO. A fast-forward can fail for other reasons — a lock file, a
|
|
437
|
+
* permission, a merge refused outright — and calling every one an untracked-file collision
|
|
438
|
+
* sent the user hunting for a file that is not there, with an info exit (3) over an error.
|
|
439
|
+
*
|
|
440
|
+
* THE ROLLBACK IS `reset --keep`, NEVER `--hard`. The doctor window lasts up to 300 s, and an
|
|
441
|
+
* edit made in the checkout meanwhile is a tracked edit `--hard` would destroy without a word.
|
|
442
|
+
* `--keep` refuses instead when such an edit touches a file the rollback must change; that
|
|
443
|
+
* refusal is reported as itself — the user is left on the pulled commit, with the command to
|
|
444
|
+
* finish the rollback — rather than as a rollback that did not happen.
|
|
445
|
+
*/
|
|
423
446
|
export function pullAndValidate(log) {
|
|
424
447
|
const [rc0, old] = gitRun(['rev-parse', 'HEAD']);
|
|
425
448
|
if (rc0 !== 0 || !old) return [false, 'not_git', 'could not read HEAD'];
|
|
426
449
|
log('[geneseed] fast-forwarding to upstream ...');
|
|
427
450
|
const [rc1, , err] = gitRun(['merge', '--ff-only', '@{u}'], { timeout: 60 });
|
|
428
451
|
if (rc1 !== 0) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
452
|
+
if (/would be overwritten/.test(err)) {
|
|
453
|
+
return [false, 'collision',
|
|
454
|
+
'Update blocked — a new upstream file collides with a local untracked '
|
|
455
|
+
+ 'file. Move or remove it, then update.\n' + err];
|
|
456
|
+
}
|
|
457
|
+
return [false, 'merge_failed', `Update failed — could not fast-forward to upstream.\n${err}`];
|
|
432
458
|
}
|
|
433
459
|
const [rc2, pulled] = gitRun(['log', '--oneline', '--no-decorate', '-20', `${old}..HEAD`]);
|
|
434
460
|
if (rc2 === 0 && pulled) {
|
|
@@ -439,8 +465,15 @@ export function pullAndValidate(log) {
|
|
|
439
465
|
const [passed, output] = runDoctor(ROOT);
|
|
440
466
|
log(output.replace(/\n+$/, ''));
|
|
441
467
|
if (!passed) {
|
|
442
|
-
gitRun(['reset', '--
|
|
468
|
+
const [rc3, , rerr] = gitRun(['reset', '--keep', old], { timeout: 60 });
|
|
443
469
|
for (const line of DOCTOR_LEGEND) log(line);
|
|
470
|
+
if (rc3 !== 0) {
|
|
471
|
+
return [false, 'doctor_fail',
|
|
472
|
+
'the pulled source FAILS validation, and the rollback was REFUSED — a file it must '
|
|
473
|
+
+ 'restore was edited while the doctor ran, and those edits were kept. You are still '
|
|
474
|
+
+ `on the pulled commit; save your edits, then roll back with: git reset --keep ${old}\n`
|
|
475
|
+
+ rerr];
|
|
476
|
+
}
|
|
444
477
|
return [false, 'doctor_fail',
|
|
445
478
|
'the pulled source FAILS validation — rolled back to the previous commit. '
|
|
446
479
|
+ 'Fix the problems listed above.'];
|
|
@@ -505,8 +538,9 @@ export function markerTheme(cfg, out) {
|
|
|
505
538
|
}
|
|
506
539
|
|
|
507
540
|
/**
|
|
508
|
-
* `_config_theme(here)` — the theme the
|
|
509
|
-
*
|
|
541
|
+
* `_config_theme(here)` — the theme the checkout's `harness.config.json` asks for. Fallback
|
|
542
|
+
* only; a bundle marker still wins. Read before the pull, but nothing local is at stake: the
|
|
543
|
+
* file is tracked and `preflight` refuses a dirty tree, so the value read is the committed one.
|
|
510
544
|
*/
|
|
511
545
|
export function configTheme(here) {
|
|
512
546
|
try {
|
|
@@ -518,11 +552,16 @@ export function configTheme(here) {
|
|
|
518
552
|
/**
|
|
519
553
|
* `_migrate_stray_bundle` — move host state (context.json, memory/) from an OLD in-folder
|
|
520
554
|
* bundle to the canonical sibling `out` BEFORE rebuilding, then drop the stray.
|
|
555
|
+
*
|
|
556
|
+
* "IS `out` THE STRAY ITSELF?" IS A PATH COMPARISON, NOT A STRING ONE. `GENESEED_OUT` is typed
|
|
557
|
+
* by a user, so on Windows `c:\x\Geneseed\harness` is the same directory as the stray and was a
|
|
558
|
+
* different string: the rescue was skipped as "nothing to move into", and the `rmSync` at the
|
|
559
|
+
* bottom then deleted the LIVE bundle — memory/ and context.json with it.
|
|
521
560
|
*/
|
|
522
561
|
export function migrateStrayBundle(here, out, log) {
|
|
523
562
|
const stray = path.join(String(here), 'Harness');
|
|
524
563
|
const outStr = String(out);
|
|
525
|
-
if (outStr ===
|
|
564
|
+
if (comparePaths(path.resolve(outStr), path.resolve(stray)) === 0) return;
|
|
526
565
|
try { if (!statSync(stray).isDirectory()) return; } catch { return; }
|
|
527
566
|
mkdirSync(outStr, { recursive: true });
|
|
528
567
|
const ctx = path.join(stray, 'context.json');
|
|
@@ -559,15 +598,40 @@ async function drainStdout() {
|
|
|
559
598
|
await new Promise((resolve) => { process.stdout.write('', resolve); });
|
|
560
599
|
}
|
|
561
600
|
|
|
601
|
+
/**
|
|
602
|
+
* The flags that re-emit the install at `dir` on its OWN axes — footprint, posture, mode,
|
|
603
|
+
* trust, doctrine packs, excluded rules — or `[]` when no build ever wrote a marker there.
|
|
604
|
+
*
|
|
605
|
+
* WITHOUT THEM THE REBUILD RESETS THE INSTALL. The generator's defaults are not the install's:
|
|
606
|
+
* a missing `--footprint` is `lean`, and posture, mode, packs and excluded rules come from
|
|
607
|
+
* `harness.config.json`. So a `full`, mentor, narrowed or rule-excluded install was re-emitted
|
|
608
|
+
* as the config default — markers included — and `rebuild-all` right after reads those
|
|
609
|
+
* rewritten markers back through `installProfile`, so the loss stuck. `installProfile` is the
|
|
610
|
+
* one reader of an install's axes; this takes its answer rather than growing a second one.
|
|
611
|
+
*
|
|
612
|
+
* Only the TAIL of the argv is kept: theme and emit are `upgrade`'s to decide (a theme argument
|
|
613
|
+
* may override the marker), and `setupBuildArgs` opens with exactly those four tokens when it
|
|
614
|
+
* is given no out and no root.
|
|
615
|
+
*/
|
|
616
|
+
export async function installAxes(dir, { installProfile, setupBuildArgs }) {
|
|
617
|
+
if (!existsSync(path.join(String(dir), '.geneseed-emit'))) return [];
|
|
618
|
+
const p = installProfile('opencode', 'global', String(dir));
|
|
619
|
+
return setupBuildArgs('', 'files', null, null, p.footprint, p.posture, p.mode, p.doctrines,
|
|
620
|
+
undefined, p.excludeRules, p.trust, p.excludeSkills).slice(4);
|
|
621
|
+
}
|
|
622
|
+
|
|
562
623
|
/**
|
|
563
624
|
* `_rebuild_bundle` — render the bundle from the (already-updated) source and bounce a
|
|
564
|
-
* running web daemon. Returns the build subprocess returncode (0 = ok).
|
|
625
|
+
* running web daemon. Returns the build subprocess returncode (0 = ok). `axes` is
|
|
626
|
+
* `installAxes` of the target, read before the pull; `bounce` is the daemon module's
|
|
627
|
+
* `bounceDaemonIfRunning`, loaded before the pull for the reason the file header gives.
|
|
565
628
|
*/
|
|
566
|
-
export async function rebuildBundle(here, out, theme, emit, rootDir, log) {
|
|
629
|
+
export async function rebuildBundle(here, out, theme, emit, rootDir, log, axes, bounce) {
|
|
567
630
|
const buildArgs = ['--out', String(out)];
|
|
568
631
|
if (theme) buildArgs.push('--theme', theme);
|
|
569
632
|
if (emit === 'opencode') buildArgs.push('--emit', 'opencode', '--root', String(rootDir));
|
|
570
633
|
else if (emit === 'opencode-global') buildArgs.push('--emit', 'opencode-global');
|
|
634
|
+
buildArgs.push(...axes);
|
|
571
635
|
log(`[geneseed] rebuilding bundle -> ${out} (theme: ${theme || 'config default'}, emit: ${emit}) ...`);
|
|
572
636
|
await drainStdout();
|
|
573
637
|
// THE GENERATOR, RE-EXECUTED. `js/build/generate.mjs`'s `cmdBuild` calls `driverMain` in-process
|
|
@@ -580,7 +644,7 @@ export async function rebuildBundle(here, out, theme, emit, rootDir, log) {
|
|
|
580
644
|
// If a web daemon is running, bounce it so the new source and the freshly rebuilt web/dist
|
|
581
645
|
// take effect — otherwise the open PWA keeps hitting the old code. The web-job exception
|
|
582
646
|
// (`upgrade/the-web-job-marker-...` gates it) lives with the helper.
|
|
583
|
-
await
|
|
647
|
+
await bounce(theme, log);
|
|
584
648
|
return 0;
|
|
585
649
|
}
|
|
586
650
|
|
|
@@ -636,6 +700,12 @@ export async function upgrade(ref = null, themeArg = null) {
|
|
|
636
700
|
return p.kind === 'info' ? 3 : 1;
|
|
637
701
|
}
|
|
638
702
|
|
|
703
|
+
// Loaded and read BEFORE the pull — see the file header for why the imports cannot wait, and
|
|
704
|
+
// `installAxes` for why the target's own axes must be carried into the rebuild.
|
|
705
|
+
const [generate, { bounceDaemonIfRunning }] = await Promise.all([
|
|
706
|
+
import('../build/generate.mjs'), import('../web/daemon.mjs')]);
|
|
707
|
+
const axes = await installAxes(emit === 'opencode-global' ? cfg : out, generate);
|
|
708
|
+
|
|
639
709
|
log(`[geneseed] fetching from origin (git: ${which('git') || 'git'}, `
|
|
640
710
|
+ `timeout: ${fetchTimeout()}s) ...`);
|
|
641
711
|
const [code, behind, err] = await measureUpstream(log);
|
|
@@ -647,6 +717,10 @@ export async function upgrade(ref = null, themeArg = null) {
|
|
|
647
717
|
+ 'Kerberos credentials). Restart it from that terminal: `geneseed web restart`.');
|
|
648
718
|
return 1;
|
|
649
719
|
}
|
|
720
|
+
if (code === 'no_upstream') {
|
|
721
|
+
log(`[geneseed] ${PRE_MSG.no_upstream[1]}`);
|
|
722
|
+
return 3;
|
|
723
|
+
}
|
|
650
724
|
if (code === 'unrelated') {
|
|
651
725
|
log('[geneseed] Upstream history was rewritten; back up local work, then re-clone '
|
|
652
726
|
+ 'or `git reset --hard @{u}`.');
|
|
@@ -676,7 +750,8 @@ export async function upgrade(ref = null, themeArg = null) {
|
|
|
676
750
|
} catch (e) {
|
|
677
751
|
log(`[geneseed] ⚠️ could not migrate the old in-folder bundle (${e && e.message ? e.message : e}) — continuing.`);
|
|
678
752
|
}
|
|
679
|
-
let rc = await rebuildBundle(here, out, theme, emit, rootDir, log
|
|
753
|
+
let rc = await rebuildBundle(here, out, theme, emit, rootDir, log, axes,
|
|
754
|
+
bounceDaemonIfRunning);
|
|
680
755
|
if (rc !== 0) {
|
|
681
756
|
log(`[geneseed][E-BUILD] ✗ the bundle build FAILED (theme: ${theme || 'default'}, emit: ${emit}).`);
|
|
682
757
|
return 1;
|
|
@@ -886,13 +961,19 @@ function reexec(argv) {
|
|
|
886
961
|
* `flushExportNotes()` runs BEFORE the handoff, deliberately: the re-exec replaces this process
|
|
887
962
|
* and the update step's own improvements notice has scrolled past (or, under the reference's
|
|
888
963
|
* curses arm, died with the alternate screen).
|
|
964
|
+
*
|
|
965
|
+
* A FAILED UPDATE STILL EXITS 1 AFTER THE HANDOFF. `setup` still runs — the user asked for it,
|
|
966
|
+
* and it can repair what the update could not — but its exit code used to become bootstrap's,
|
|
967
|
+
* so a script saw 0 over an update that never happened. `handoff` is the seam a test needs:
|
|
968
|
+
* the real `setup` exits 1 off a terminal, which would make the two answers indistinguishable.
|
|
889
969
|
*/
|
|
890
|
-
export async function cmdBootstrap(args) {
|
|
970
|
+
export async function cmdBootstrap(args, { handoff = reexec } = {}) {
|
|
891
971
|
const failed = await bootstrapPlain();
|
|
892
972
|
flushExportNotes();
|
|
893
973
|
if (!args.noSetup) {
|
|
894
|
-
|
|
974
|
+
const rc = handoff([process.execPath,
|
|
895
975
|
path.join(String(ROOT), 'bin', 'geneseed-cli.mjs'), 'setup']);
|
|
976
|
+
return failed ? 1 : rc;
|
|
896
977
|
}
|
|
897
978
|
// Scripted `bootstrap --no-setup` must not exit 0 over a failed update.
|
|
898
979
|
return failed ? 1 : 0;
|
package/js/ui/anim.mjs
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
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
|
|
6
|
-
* layer" — and the rest of the terminal layer turned out to be the panel, which
|
|
7
|
-
*
|
|
6
|
+
* layer" — and the rest of the terminal layer turned out to be the panel, which was rejected
|
|
7
|
+
* (`tests/unit/no_panel.test.mjs` asserts it stays gone). This module is the part
|
|
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
|
*
|
|
@@ -23,16 +23,12 @@
|
|
|
23
23
|
* needed — which is fortunate, because it is a `child_process` spawn and this port is barred
|
|
24
24
|
* from those. The consequence is that the reference's `except Exception: return False` arm
|
|
25
25
|
* around it has no counterpart here. That arm needs `cmd.exe` to be unlaunchable on a machine
|
|
26
|
-
* that has already reached an interactive wizard
|
|
27
|
-
* environments in `tests/test_pure_function_parity.py` — none of which can produce it on
|
|
28
|
-
* either side.
|
|
26
|
+
* that has already reached an interactive wizard.
|
|
29
27
|
*
|
|
30
|
-
* WHAT GATES IT.
|
|
31
|
-
* `
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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.
|
|
28
|
+
* WHAT GATES IT. Little, now: the recorded snapshot corpus that replayed `art_for`, `_place`,
|
|
29
|
+
* `_tile`, `_height`, `anim_ok` and `play_line` is gone with the reference.
|
|
30
|
+
* `tests/unit/wizard.test.mjs` asserts the wizard still plays the animation, and in the right
|
|
31
|
+
* place; `tests/unit/no_panel.test.mjs` that this file is the one allowed its cursor escapes.
|
|
36
32
|
*/
|
|
37
33
|
import { printOut } from '../lib/fs.mjs';
|
|
38
34
|
import { parseIntStrict, codePointLength, padEndToWidth } from '../lib/text.mjs';
|
|
@@ -235,7 +231,7 @@ export function place(row, x, width) {
|
|
|
235
231
|
* Python turns into an empty string and `String.repeat` turns into a RangeError, hence the
|
|
236
232
|
* clamp. And `phase % len(tile)` is non-negative in Python for a positive divisor while
|
|
237
233
|
* JavaScript keeps the sign of the dividend, so a negative phase indexes off the front of the
|
|
238
|
-
* string instead of wrapping to the end.
|
|
234
|
+
* string instead of wrapping to the end.
|
|
239
235
|
*/
|
|
240
236
|
export function tile(tileStr, width, phase = 0) {
|
|
241
237
|
if (!tileStr) return '';
|