geneseed 3.14.0 → 3.15.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 +54 -0
- package/SHIPPED.md +1 -1
- package/adapters/opencode/plugins/geneseed-context.js +23 -9
- 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/harness.config.json +1 -1
- package/js/README.md +9 -5
- package/js/build/args.mjs +4 -3
- package/js/build/bundle.mjs +10 -10
- package/js/build/driver.mjs +8 -7
- 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 +15 -11
- package/js/build/render.mjs +1 -1
- package/js/build/source.mjs +6 -6
- package/js/build/stubs.mjs +6 -5
- package/js/build/version.mjs +21 -2
- package/js/hosts/hooks-context.mjs +2 -1
- package/js/hosts/hooks-prims.mjs +2 -2
- package/js/hosts/hosts.mjs +24 -26
- package/js/hosts/installs.mjs +33 -14
- 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 +44 -36
- package/js/hosts/shim.mjs +4 -2
- package/js/inspect/diff.mjs +5 -6
- package/js/inspect/excludes.mjs +10 -5
- package/js/inspect/status.mjs +18 -21
- package/js/inspect/validate.mjs +2 -2
- 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 +9 -131
- 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 +82 -386
- 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/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/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).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 '';
|
package/js/ui/cli.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* copy in it.
|
|
11
11
|
*
|
|
12
12
|
* P10c's answer was to make the metadata DATA — generated into `cli.json` at the repo root by
|
|
13
|
-
*
|
|
13
|
+
* a since-deleted Python script and read by both sides. P2 finishes the thought: the table is
|
|
14
14
|
* no longer GENERATED from anything, it is the OWNED document, and it lives here on a product
|
|
15
15
|
* path because that is where a document npm ships belongs. `package.json`'s `files[]` already
|
|
16
16
|
* carries `js/`, so it needs no row of its own, and nothing has to import out of `tests/`.
|
|
@@ -31,13 +31,9 @@
|
|
|
31
31
|
* a file filtered for the page breaks the CLI, and a file in `cliSpec`'s shape cannot render
|
|
32
32
|
* the docs. It stays exactly what the argparse walk produced.
|
|
33
33
|
*
|
|
34
|
-
* WHAT
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* argparse-vs-table comparison, and it can only ever cover the rows argparse HAS: `catalog`,
|
|
38
|
-
* `mcp` and `memory` never had a subparser and are outside it by construction. The successor
|
|
39
|
-
* that checks the WHOLE table, and outlives the parser, is `tests/unit/cli_table.test.mjs`;
|
|
40
|
-
* after that the table is simply the source of truth and is edited directly.
|
|
34
|
+
* WHAT GUARDS IT. The Python parser it was walked from is gone, and with it every comparison
|
|
35
|
+
* against argparse; the table is now simply the source of truth and is edited directly.
|
|
36
|
+
* `tests/unit/cli_table.test.mjs` checks the WHOLE table.
|
|
41
37
|
*
|
|
42
38
|
* THE COST, NAMED: this entry cannot parse anything without this file. `js/build/source.mjs`
|
|
43
39
|
* already reads `harness.config.json` at import for every render, so a tracked data document
|
|
@@ -48,6 +44,7 @@ import { readFileSync } from 'node:fs';
|
|
|
48
44
|
import path from 'node:path';
|
|
49
45
|
|
|
50
46
|
import { ROOT } from '../build/source.mjs';
|
|
47
|
+
import { printOut } from '../lib/fs.mjs';
|
|
51
48
|
import { parseIntStrict, stripWhitespace } from '../lib/text.mjs';
|
|
52
49
|
|
|
53
50
|
const CLI_JSON = path.join(ROOT, 'js', 'cli-table.json');
|
|
@@ -156,8 +153,8 @@ function actionInvocation(a, positional) {
|
|
|
156
153
|
// hyphenated word into two chunks, so it can break a line inside `back-compat` where a
|
|
157
154
|
// space-only wrap cannot. Measured before deciding: every help string then in the table disagreed
|
|
158
155
|
// with a greedy wrap at some width, the widest at 175 — so "the branch is dead" was simply
|
|
159
|
-
// false, and `
|
|
160
|
-
// first asserted it. The rules are transcribed instead.
|
|
156
|
+
// false, and the former Python suite's every-width comparison against `textwrap` said so on
|
|
157
|
+
// the run that first asserted it. The rules are transcribed instead.
|
|
161
158
|
//
|
|
162
159
|
// Python's classes, spelled for a `u`-flagged JavaScript regex: `\w` is letters, numbers and
|
|
163
160
|
// underscore; `[^\d\W]` is that minus the DECIMAL digits, which leaves the letter-ish half
|
|
@@ -181,9 +178,8 @@ const WORDSEP = new RegExp(
|
|
|
181
178
|
* `drop_whitespace`, `break_long_words` and `break_on_hyphens` all on, and the text already
|
|
182
179
|
* whitespace-collapsed and stripped by the caller.
|
|
183
180
|
*
|
|
184
|
-
* ponytail: lives here rather than in `js/lib/` because `formatHelp` is its only consumer
|
|
185
|
-
*
|
|
186
|
-
* move this when that changes, not before.
|
|
181
|
+
* ponytail: lives here rather than in `js/lib/` because `formatHelp` is its only consumer;
|
|
182
|
+
* move it when a second one appears, not before.
|
|
187
183
|
*/
|
|
188
184
|
export function wrapText(text, width) {
|
|
189
185
|
const chunks = text.split(WORDSEP).filter(Boolean).reverse();
|
|
@@ -310,7 +306,7 @@ function formatAction(a, positional, helpPosition, width) {
|
|
|
310
306
|
* `parser.format_help()` for one subcommand, byte for byte with argparse.
|
|
311
307
|
*
|
|
312
308
|
* WHY THE PORT RENDERS THIS AT ALL. Task 10 compared the two implementations' `--help` for the
|
|
313
|
-
* first time —
|
|
309
|
+
* first time — the former Python CLI matrix had zero `--help` cells — and found the port answering
|
|
314
310
|
* `geneseed: error: unrecognized arguments: --help`, exit 2, on all 26 verbs. That is not the
|
|
315
311
|
* declared wording divergence `parse`'s docblock reserves for argparse's ERROR text: it is the
|
|
316
312
|
* absence of the surface, on the binary that survives the reference's deletion.
|
|
@@ -318,19 +314,19 @@ function formatAction(a, positional, helpPosition, width) {
|
|
|
318
314
|
* `prog` IS AN ARGUMENT AND THAT IS THE ONE DELIBERATE DIVERGENCE. The reference names itself
|
|
319
315
|
* `harness`, after the Python file this migration deletes; a user who typed `geneseed status
|
|
320
316
|
* --help` is shown `usage: geneseed status`, which is also how every error this entry prints
|
|
321
|
-
* already spells itself (`die`).
|
|
322
|
-
*
|
|
323
|
-
*
|
|
317
|
+
* already spells itself (`die`). The former Python suite replayed the reference's recorded
|
|
318
|
+
* answers through here with `prog` set to the reference's own, so the layout was gated byte for
|
|
319
|
+
* byte and the rename was the only thing that moved. That corpus is gone; the layout is now
|
|
320
|
+
* pinned by the written-out `formatHelp` rows in `tests/unit/text_layout.test.mjs`.
|
|
324
321
|
*
|
|
325
|
-
* NOT EVERY VERB
|
|
326
|
-
* no subparser for the recorder to render from, so
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
* what keeps "no recorded text" from meaning "no gate".
|
|
322
|
+
* NOT EVERY VERB HAD A RECORDED ANSWER, AND THREE NEVER COULD. `catalog`, `mcp` and `memory` had
|
|
323
|
+
* no subparser for the recorder to render from, so they were held ABSOLUTELY instead — and
|
|
324
|
+
* `tests/unit/hook_cli.test.mjs`'s `NATIVE` list still names that population and fails a verb
|
|
325
|
+
* that falls into neither, which is what keeps "no recorded text" from meaning "no gate".
|
|
330
326
|
*
|
|
331
327
|
* The alias rides on the same argument: argparse gives `update` the parser object it built for
|
|
332
|
-
* `upgrade`, so `harness.py update --help`
|
|
333
|
-
* map
|
|
328
|
+
* `upgrade`, so `harness.py update --help` printed `usage: harness upgrade`. The recorded prog
|
|
329
|
+
* map reproduced that; the shipped call passes `geneseed update`, which is what the user typed.
|
|
334
330
|
*
|
|
335
331
|
* `width` is argparse's `shutil.get_terminal_size().columns - 2`, passed in rather than read,
|
|
336
332
|
* because a recorded help text whose wrap column came from the recorder's terminal is a corpus
|
|
@@ -407,12 +403,13 @@ export function formatHelp(cmd, prog, width) {
|
|
|
407
403
|
*
|
|
408
404
|
* `parseIntStrict`, NOT `Number.parseInt`, and the tree already owned it. `get_terminal_size` spells
|
|
409
405
|
* this `int(os.environ['COLUMNS'])` inside `except (KeyError, ValueError)`, and `int` is not
|
|
410
|
-
* `parseInt`. Measured against the reference over a corpus of `$COLUMNS` values
|
|
411
|
-
* (`
|
|
406
|
+
* `parseInt`. Measured against the reference over a corpus of `$COLUMNS` values in the former
|
|
407
|
+
* Python suite (`tests/unit/cli_table.test.mjs`'s 'the help width is the terminal width less
|
|
408
|
+
* two, and COLUMNS wins' keeps a written-out part of it): `COLUMNS=1_0` is 10 to Python and
|
|
412
409
|
* 1 to `parseInt`, `COLUMNS=٣` is 3 and NaN, `COLUMNS=' 80 '` is 80 and NaN. `COLUMNS=80abc`
|
|
413
410
|
* is a ValueError to Python and 80 to `parseInt` — the same answer down a PIPE, where the
|
|
414
411
|
* fallback is 80 either way, and a different one on any terminal that is not 80 wide, which is
|
|
415
|
-
* why
|
|
412
|
+
* why a unit test is the gate rather than a cell. A non-positive value falls through on both
|
|
416
413
|
* sides, which is Python's `if columns <= 0` and was already right here.
|
|
417
414
|
*
|
|
418
415
|
* `stripWhitespace` because THIS is the caller `parseIntStrict`'s docblock did not have: its other two
|
|
@@ -443,8 +440,7 @@ export function printVerbList(prog, verbs, extra = {}) {
|
|
|
443
440
|
`A bare \`${prog}\` runs \`home\`. \`${prog} <command> --help\` describes one command.`, '',
|
|
444
441
|
'commands:'];
|
|
445
442
|
for (const n of names) lines.push(` ${n.padEnd(pad)}${helpOf.get(n) || ''}`.trimEnd());
|
|
446
|
-
|
|
447
|
-
process.stdout.write(process.platform === 'win32' ? text.replaceAll('\n', '\r\n') : text);
|
|
443
|
+
printOut(`${lines.join('\n')}\n`);
|
|
448
444
|
return 0;
|
|
449
445
|
}
|
|
450
446
|
|
|
@@ -453,18 +449,16 @@ export function printVerbList(prog, verbs, extra = {}) {
|
|
|
453
449
|
* describe — the caller already owns that refusal and its message.
|
|
454
450
|
*
|
|
455
451
|
* ONE OWNER FOR TWO ENTRIES. `bin/geneseed-hook.mjs` carries the four hook verbs and
|
|
456
|
-
* `bin/geneseed-cli.mjs` the rest of the table, and
|
|
457
|
-
*
|
|
452
|
+
* `bin/geneseed-cli.mjs` the rest of the table, and `tests/unit/hook_cli.test.mjs`'s
|
|
453
|
+
* "the two entry points carry disjoint verb sets" keeps them from learning each other's
|
|
458
454
|
* tables — so the help each prints has to come from here rather than from a copy in each.
|
|
459
455
|
*
|
|
460
|
-
* CRLF ON WINDOWS
|
|
461
|
-
* translates, and this text lands in the same terminals and the same pipes as that one.
|
|
456
|
+
* CRLF ON WINDOWS through `printOut`, the one funnel every CLI print takes.
|
|
462
457
|
*/
|
|
463
458
|
export function printHelp(prog, verb) {
|
|
464
459
|
const cmd = cliCommand(verb);
|
|
465
460
|
if (cmd === null) return null;
|
|
466
|
-
|
|
467
|
-
process.stdout.write(process.platform === 'win32' ? text.replaceAll('\n', '\r\n') : text);
|
|
461
|
+
printOut(formatHelp(cmd, `${prog} ${verb}`, helpWidth()));
|
|
468
462
|
return 0;
|
|
469
463
|
}
|
|
470
464
|
|
package/js/ui/menu.mjs
CHANGED
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
* still the right shape; only its explanation moved. The alternative that was considered and
|
|
23
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
|
-
* screen, unreachable from every cell (no cell has a TTY),
|
|
26
|
-
*
|
|
25
|
+
* screen, unreachable from every cell (no cell has a TTY), for a panel that was rejected
|
|
26
|
+
* outright rather than deferred. The fallback is the reference's own contract for "no
|
|
27
27
|
* full-screen menu available here"; a new UI would not be.
|
|
28
28
|
*
|
|
29
29
|
* `cmd_home` is the default for a bare `geneseed`, and it crosses WHOLE. Its two arms are
|
|
@@ -32,10 +32,9 @@
|
|
|
32
32
|
*
|
|
33
33
|
* `_web_first_ok`'s FIVE REFUSALS ARE THE PAYLOAD, and only two of them are reachable from a
|
|
34
34
|
* cell: a cell's stdin is a pipe, so `isTTY` is false and everything past it is dead. The
|
|
35
|
-
* other three (SSH, the Linux display server, the browser) are gated by
|
|
36
|
-
* `tests/
|
|
37
|
-
* one input no cell can vary.
|
|
38
|
-
* declares.
|
|
35
|
+
* other three (SSH, the Linux display server, the browser) are gated by
|
|
36
|
+
* `tests/unit/web_first.test.mjs`, which drives this function through
|
|
37
|
+
* `tests/fixtures/pure_probe.mjs` with `isTTY` FAKED — the one input no cell can vary.
|
|
39
38
|
*/
|
|
40
39
|
import { printOut, printErr } from '../lib/fs.mjs';
|
|
41
40
|
import { which } from '../lib/paths.mjs';
|
|
@@ -93,13 +92,8 @@ export function cmdMenu() {
|
|
|
93
92
|
// The reference's own `except Exception` arm, with a reason that is permanent here rather
|
|
94
93
|
// than machine-dependent. stderr, and CRLF on Windows, because `sys.stderr.write` does.
|
|
95
94
|
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
// to be false where it matters: `rituals/_harness_core.py` installs `rituals/_winterm.py`
|
|
99
|
-
// as `sys.modules["curses"]` when the stdlib module is missing, so the reference opens its
|
|
100
|
-
// panel on this machine and `tests/test_tui_boundary.py` drives it doing so. What is
|
|
101
|
-
// missing is on THIS side — there is no window implementation in the port — and that is
|
|
102
|
-
// what the message now says.
|
|
95
|
+
// The reason is on THIS side: there is no window implementation, by decision, and that is
|
|
96
|
+
// what the message says (`tests/unit/no_panel.test.mjs` keeps it so).
|
|
103
97
|
// NOTHING TO POINT AT ANY MORE. The message used to end by sending the reader at the
|
|
104
98
|
// Python panel, and P2 took the pointer out rather than re-aiming it: the panel it named
|
|
105
99
|
// is the Python this migration deletes, and a refusal that sends a user to a file which
|