geneseed 3.1.1 → 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 +68 -1
- package/DESIGN.md +15 -15
- package/README.md +1 -1
- package/SETUP.md +1 -1
- package/SHIPPED.md +1 -1
- package/bin/{geneseed.mjs → build-driver.mjs} +39 -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 +111 -83
- 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} +27 -28
- package/js/{render.mjs → build/render.mjs} +13 -9
- package/js/{checkout.mjs → build/source.mjs} +9 -9
- 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} +18 -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} +63 -55
- 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} +20 -17
- 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} +34 -32
- package/js/{uninstall.mjs → maintain/uninstall.mjs} +55 -56
- 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 +84 -85
- package/js/web/activity.mjs +22 -21
- package/js/web/api.mjs +72 -50
- 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 +21 -19
- package/js/web/routes.mjs +223 -0
- package/js/web/server.mjs +56 -987
- package/package.json +11 -3
- package/src/doctrines/README.md +1 -1
- package/themes/opencode/README.md +1 -1
- package/web/dist/assets/{About-DyK7yRy7.js → About-CPvIFZGh.js} +1 -1
- package/web/dist/assets/{Activity-D_sYBlW-.js → Activity-z0uXdS7J.js} +1 -1
- package/web/dist/assets/{ActivityDetail-BomKcMhy.js → ActivityDetail-CEj5IJpF.js} +1 -1
- package/web/dist/assets/{Diff-BSvi1WUg.js → Diff-BMR8fPuG.js} +1 -1
- package/web/dist/assets/{Doctor-Cz1s-y1n.js → Doctor-Bz5qE4r9.js} +1 -1
- package/web/dist/assets/{Graph-CuntzFJ0.js → Graph-BV6a6t2l.js} +1 -1
- package/web/dist/assets/{Harnesses-DgRyOviS.js → Harnesses-C5Ag5PaQ.js} +1 -1
- package/web/dist/assets/{Laws-CWk3Mpri.js → Laws-BK07NKw9.js} +1 -1
- package/web/dist/assets/{Library-DXhTN9ju.js → Library-CiXN9sC3.js} +1 -1
- package/web/dist/assets/{Markdown-BoiZcdUl.js → Markdown-DG6FjUz-.js} +1 -1
- package/web/dist/assets/{Profile-DfCHlZ03.js → Profile-B2oSvdxH.js} +1 -1
- package/web/dist/assets/{Rules-eND7hJv9.js → Rules-DobVPd80.js} +1 -1
- package/web/dist/assets/{Skills-CDl9opZO.js → Skills-DQPsvsn9.js} +1 -1
- package/web/dist/assets/{StatusBadge-DSDtcArX.js → StatusBadge-BhJ3HhdI.js} +1 -1
- package/web/dist/assets/{Themes-CoAWuIeM.js → Themes-BP38FBU9.js} +1 -1
- package/web/dist/assets/{activity-Dmb0FTAx.js → activity-D9mfv7pY.js} +1 -1
- package/web/dist/assets/index-BE226Gv6.css +1 -0
- package/web/dist/assets/{index-jrY4m9jP.js → index-BZ7cPLMI.js} +1 -1
- package/web/dist/assets/index-BgIs5eIa.js +12 -0
- package/web/dist/assets/{index-Dg58ScBe.js → index-DGM5j2hB.js} +1 -1
- package/web/dist/index.html +2 -2
- package/web/src/pages/Laws.jsx +1 -1
- package/docs/declined.md +0 -200
- package/docs/limits.md +0 -329
- package/js/doctor.mjs +0 -1750
- package/js/emit.mjs +0 -1411
- package/web/dist/assets/index-B-mXquBZ.css +0 -1
- package/web/dist/assets/index-CH_Z725C.js +0 -12
|
@@ -35,11 +35,11 @@
|
|
|
35
35
|
* `move` where it has `unlink`, plus an inverse; splitting them would have duplicated the
|
|
36
36
|
* owned-file walk and the ancestor prune a fourth time.
|
|
37
37
|
*
|
|
38
|
-
* NO SPAWN. `
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
38
|
+
* NO SPAWN. `ALLOWED_SPAWNS` in `tests/unit/hook_cli.test.mjs` is the table of every module
|
|
39
|
+
* allowed to reach `child_process` and the argv each may use; this module has no row and
|
|
40
|
+
* does not want one, because every operation here is a filesystem call. That matters more
|
|
41
|
+
* than usual for this verb: shelling the uninstall out to an interpreter would have been
|
|
42
|
+
* byte-identical in every cell of the matrix, so only the allow-list refutes it.
|
|
43
43
|
*/
|
|
44
44
|
import {
|
|
45
45
|
existsSync, lstatSync, mkdirSync, readdirSync, renameSync, rmSync, rmdirSync, statSync,
|
|
@@ -47,26 +47,25 @@ import {
|
|
|
47
47
|
} from 'node:fs';
|
|
48
48
|
import path from 'node:path';
|
|
49
49
|
|
|
50
|
-
import { hookRunnerEntry } from '
|
|
50
|
+
import { hookRunnerEntry } from '../../bin/build-driver.mjs';
|
|
51
51
|
import { confirm } from './setup.mjs';
|
|
52
52
|
import {
|
|
53
53
|
claudeCfg, claudeReadManifest, doctrinesOfDir, emitHostScopeOf, excludedRulesOfDir, installKind,
|
|
54
54
|
installState,
|
|
55
55
|
registeredTargets, readMaybe, DISABLED_STASH,
|
|
56
|
-
} from '
|
|
56
|
+
} from '../hosts/installs.mjs';
|
|
57
57
|
import {
|
|
58
|
-
GLOBAL_MANIFEST, HOSTS, VERSION_MARKER, expanduser, opencodeConfigDir,
|
|
59
|
-
} from '
|
|
60
|
-
import { mcpCommented } from '
|
|
58
|
+
GLOBAL_MANIFEST, HOSTS, VERSION_MARKER, expanduser, opencodeConfigDir, resolvePath,
|
|
59
|
+
} from '../hosts/hosts.mjs';
|
|
60
|
+
import { mcpCommented } from '../hosts/mcp.mjs';
|
|
61
61
|
import {
|
|
62
62
|
managedBlockRead, managedBlockRemove, managedBlockWrite, mergeClaudeSettings,
|
|
63
63
|
opencodeTarget, readJsonc, settingsIntegrityCheck, wireClaudeExcludes,
|
|
64
64
|
unwireClaudeExcludes, unwireClaudeSettings,
|
|
65
|
-
} from '
|
|
66
|
-
import {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
} from './lib/fs.mjs';
|
|
65
|
+
} from '../hosts/settings.mjs';
|
|
66
|
+
import { printOut, printErr, readText, writeText } from '../lib/fs.mjs';
|
|
67
|
+
import { indexOfDeepEqual, jsonDumps, jsonDumpsIndent, deepEquals } from '../lib/json.mjs';
|
|
68
|
+
import { comparePaths, isAbsolutePath, within } from '../lib/paths.mjs';
|
|
70
69
|
|
|
71
70
|
const isDir = (p) => { try { return statSync(p).isDirectory(); } catch { return false; } };
|
|
72
71
|
const isFile = (p) => { try { return statSync(p).isFile(); } catch { return false; } };
|
|
@@ -173,7 +172,7 @@ function unlinkOwned(base, owned, label = '') {
|
|
|
173
172
|
// `f"{rel} ({e})"`. `str(OSError)` and an `Error.message` word the same fault
|
|
174
173
|
// differently, and no cell can reach this: it needs a file that exists, is a file, and
|
|
175
174
|
// cannot be unlinked. Recorded rather than papered over — the same standing item
|
|
176
|
-
// `js/excludes.mjs` carries for its `could not remove <stub>` branch.
|
|
175
|
+
// `js/inspect/excludes.mjs` carries for its `could not remove <stub>` branch.
|
|
177
176
|
failed.push(`${label}${rel} (${e && e.message ? e.message : e})`);
|
|
178
177
|
}
|
|
179
178
|
}
|
|
@@ -182,12 +181,12 @@ function unlinkOwned(base, owned, label = '') {
|
|
|
182
181
|
|
|
183
182
|
/** The two WARNs every reversal prints when an owned file survives. Identical in all three. */
|
|
184
183
|
function warnSurvivors(failed) {
|
|
185
|
-
|
|
184
|
+
printErr('[uninstall] WARN: could not remove '
|
|
186
185
|
+ `${failed.length} owned file(s): ${failed.join(', ')}\n`);
|
|
187
186
|
}
|
|
188
187
|
|
|
189
188
|
function warnMarkersKept() {
|
|
190
|
-
|
|
189
|
+
printErr('[uninstall] WARN: the manifest and markers were KEPT so '
|
|
191
190
|
+ '`geneseed uninstall` can be retried once the file(s) are '
|
|
192
191
|
+ 'unlocked/removable.\n');
|
|
193
192
|
}
|
|
@@ -216,7 +215,7 @@ export function unmergeOpencodeJson(p, entry) {
|
|
|
216
215
|
const instr = cfg.instructions;
|
|
217
216
|
if (!Array.isArray(instr) || !instr.includes(entry)) return false;
|
|
218
217
|
if (path.extname(target) === '.jsonc' && hadComments) {
|
|
219
|
-
|
|
218
|
+
printOut(`[uninstall] ${path.basename(target)} has comments — not rewriting it. Remove `
|
|
220
219
|
+ `this from its "instructions" by hand: ${jsonDumps(entry)}\n`);
|
|
221
220
|
return false;
|
|
222
221
|
}
|
|
@@ -265,7 +264,7 @@ function settingsFile(cfg, managed) {
|
|
|
265
264
|
/** `_harness_mcp._claude_md_path` — where the manifest says the managed block lives. */
|
|
266
265
|
function claudeMdPath(cfg, managed) {
|
|
267
266
|
const rel = ((managed && managed.claude_md) || {}).rel || 'CLAUDE.md';
|
|
268
|
-
return
|
|
267
|
+
return resolvePath(path.join(cfg, rel));
|
|
269
268
|
}
|
|
270
269
|
|
|
271
270
|
/** A manifest's `managed` map, or `{}` — the Python's `isinstance(..., dict)` guard. */
|
|
@@ -361,7 +360,7 @@ function mcpLoadOpencode(p) {
|
|
|
361
360
|
*
|
|
362
361
|
* A global install wires the ABSOLUTE posix path; a project install wires the relative
|
|
363
362
|
* `…/AGENT.md` the emit recorded, read back off the live config so a bundle sub-dir layout
|
|
364
|
-
* round-trips. `
|
|
363
|
+
* round-trips. `isAbsolutePath` and not `path.isAbsolute`: they disagree on Windows for a
|
|
365
364
|
* ROOTLESS `/repo/AGENT.md`, which Python keeps and `path.isAbsolute` would have skipped.
|
|
366
365
|
*/
|
|
367
366
|
export function installAgentEntry(root, kind) {
|
|
@@ -383,7 +382,7 @@ export function installAgentEntry(root, kind) {
|
|
|
383
382
|
export function installAgentEntryOf(instr) {
|
|
384
383
|
if (Array.isArray(instr)) {
|
|
385
384
|
for (const e of instr) {
|
|
386
|
-
if (typeof e === 'string' && !
|
|
385
|
+
if (typeof e === 'string' && !isAbsolutePath(e) && path.basename(e) === 'AGENT.md') return e;
|
|
387
386
|
}
|
|
388
387
|
}
|
|
389
388
|
return 'AGENT.md';
|
|
@@ -509,7 +508,7 @@ export function installUninstall(root, host = 'opencode', scope = 'global', memo
|
|
|
509
508
|
if (survivors.length || failed.length) {
|
|
510
509
|
if (survivors.length && !failed.length) {
|
|
511
510
|
// With `failed` set the reversal already warned twice — no third overlapping WARN.
|
|
512
|
-
|
|
511
|
+
printErr('[uninstall] WARN: could not fully remove the install — still present: '
|
|
513
512
|
+ `${survivors.join(', ')}. The install marker was KEPT so you can retry `
|
|
514
513
|
+ '`geneseed uninstall` once the file(s) are unlocked/removable.\n');
|
|
515
514
|
}
|
|
@@ -545,7 +544,7 @@ export function projectQualifies(root, host) {
|
|
|
545
544
|
const cfg = path.join(root, spec.projectMarker);
|
|
546
545
|
if (!isDir(cfg)) return false;
|
|
547
546
|
try {
|
|
548
|
-
if (
|
|
547
|
+
if (resolvePath(cfg) === resolvePath(spec.configDir())) return false;
|
|
549
548
|
} catch { /* as the Python's bare `except Exception: pass` */ }
|
|
550
549
|
if (isFile(path.join(cfg, GLOBAL_MANIFEST))) return true;
|
|
551
550
|
const hs = emitHostScopeOf(root);
|
|
@@ -569,7 +568,7 @@ export function uninstallResolve(targetArg) {
|
|
|
569
568
|
const globalHit = (p) => {
|
|
570
569
|
for (const spec of HOSTS) {
|
|
571
570
|
try {
|
|
572
|
-
if (p ===
|
|
571
|
+
if (p === resolvePath(spec.configDir())) return [spec.host, 'global', p];
|
|
573
572
|
} catch { continue; }
|
|
574
573
|
}
|
|
575
574
|
return null;
|
|
@@ -581,7 +580,7 @@ export function uninstallResolve(targetArg) {
|
|
|
581
580
|
return null;
|
|
582
581
|
};
|
|
583
582
|
if (targetArg) {
|
|
584
|
-
const p =
|
|
583
|
+
const p = resolvePath(expanduser(targetArg));
|
|
585
584
|
const hit = globalHit(p);
|
|
586
585
|
if (hit) return hit;
|
|
587
586
|
for (const spec of HOSTS) {
|
|
@@ -595,7 +594,7 @@ export function uninstallResolve(targetArg) {
|
|
|
595
594
|
// `.resolve()` so the cwd fallback matches every other branch and the registry, which
|
|
596
595
|
// stores resolved paths — a short-form (8.3) cwd, as Windows CI hands back for %TEMP%,
|
|
597
596
|
// would otherwise return a root that LOOKS different from the identical directory.
|
|
598
|
-
const hit = projectHit(
|
|
597
|
+
const hit = projectHit(resolvePath(process.cwd()));
|
|
599
598
|
if (hit) return hit;
|
|
600
599
|
return ['opencode', 'global', opencodeConfigDir()];
|
|
601
600
|
}
|
|
@@ -609,11 +608,11 @@ export function uninstallResolve(targetArg) {
|
|
|
609
608
|
* rediscovered from, and the just-removed root is excluded.
|
|
610
609
|
*/
|
|
611
610
|
export function survivingProjectInstalls(removedRoot) {
|
|
612
|
-
const rroot =
|
|
611
|
+
const rroot = resolvePath(removedRoot);
|
|
613
612
|
const out = [];
|
|
614
613
|
for (const [host, scope, root] of registeredTargets()) {
|
|
615
614
|
if (scope !== 'project') continue;
|
|
616
|
-
try { if (
|
|
615
|
+
try { if (resolvePath(root) === rroot) continue; } catch { /* as the Python's except OSError */ }
|
|
617
616
|
out.push([host, scope, root]);
|
|
618
617
|
}
|
|
619
618
|
return out;
|
|
@@ -623,10 +622,10 @@ export function survivingProjectInstalls(removedRoot) {
|
|
|
623
622
|
export function printSurvivingProjectInventory(removedRoot) {
|
|
624
623
|
const survivors = survivingProjectInstalls(removedRoot);
|
|
625
624
|
if (!survivors.length) return;
|
|
626
|
-
|
|
625
|
+
printOut(`[uninstall] ${survivors.length} project install(s) remain — the global removal `
|
|
627
626
|
+ 'does not affect them (each is self-contained):\n');
|
|
628
627
|
for (const [host, scope, root] of survivors) {
|
|
629
|
-
|
|
628
|
+
printOut(` - ${root} (${host}:${scope}) — remove with: `
|
|
630
629
|
+ `geneseed uninstall --target "${root}" --yes\n`);
|
|
631
630
|
}
|
|
632
631
|
}
|
|
@@ -640,8 +639,8 @@ export function printSurvivingProjectInventory(removedRoot) {
|
|
|
640
639
|
export function printOtherHostHits(root, removedHost) {
|
|
641
640
|
for (const spec of HOSTS) {
|
|
642
641
|
if (spec.host !== removedHost && projectQualifies(root, spec.host)) {
|
|
643
|
-
|
|
644
|
-
+ 'uninstall
|
|
642
|
+
printOut(`[uninstall] also found ${spec.host}:project here — run \`harness `
|
|
643
|
+
+ 'uninstall` again to remove it.\n');
|
|
645
644
|
}
|
|
646
645
|
}
|
|
647
646
|
}
|
|
@@ -649,7 +648,7 @@ export function printOtherHostHits(root, removedHost) {
|
|
|
649
648
|
/**
|
|
650
649
|
* `_harness_setup._confirm`, and the only interactive read this verb makes.
|
|
651
650
|
*
|
|
652
|
-
* MOVED TO `js/setup.mjs` IN P5i, where the Python's own owner is: `_confirm` lives in
|
|
651
|
+
* MOVED TO `js/maintain/setup.mjs` IN P5i, where the Python's own owner is: `_confirm` lives in
|
|
653
652
|
* `_harness_setup` beside the `_ask` it is built on, and `setup` is its second caller. The
|
|
654
653
|
* body stayed here for one phase because a helper with one caller is not shared yet; the
|
|
655
654
|
* rule this port uses is that the SECOND owner is what moves it, and a byte gate is what
|
|
@@ -674,8 +673,8 @@ export function cmdUninstall(args) {
|
|
|
674
673
|
if (hit === null) {
|
|
675
674
|
// `uninstallResolve(null)` never returns null — it falls back to the OpenCode global
|
|
676
675
|
// default — so reaching here means `--target` was given and was not recognised.
|
|
677
|
-
const targetDesc =
|
|
678
|
-
|
|
676
|
+
const targetDesc = resolvePath(expanduser(args.target));
|
|
677
|
+
printErr(`[uninstall] no Geneseed install detected at ${targetDesc}.\n`
|
|
679
678
|
+ '[uninstall] pass --target <repo> for a project install (.opencode/.claude/'
|
|
680
679
|
+ '.bob/.github) or --target <config dir> for a global one.\n');
|
|
681
680
|
return 1;
|
|
@@ -684,60 +683,60 @@ export function cmdUninstall(args) {
|
|
|
684
683
|
if (installState(root, host, scope) === 'absent') {
|
|
685
684
|
const where = scope === 'project' && host !== 'opencode'
|
|
686
685
|
? ` under ${hostSpec(host).projectMarker}/` : '';
|
|
687
|
-
|
|
686
|
+
printErr(`[uninstall] no ${host}:${scope} Geneseed install at ${root} `
|
|
688
687
|
+ `(no ${GLOBAL_MANIFEST}${where}).\n`);
|
|
689
688
|
return 1;
|
|
690
689
|
}
|
|
691
690
|
const data = installDataDir(root, host, scope);
|
|
692
691
|
const stores = ['memory', 'notebook'].filter((n) => isDir(path.join(data, n)));
|
|
693
|
-
|
|
692
|
+
printOut(`[uninstall] target: ${root} (${host}:${scope})\n`);
|
|
694
693
|
if (host === 'copilot') {
|
|
695
|
-
|
|
694
|
+
printOut('[uninstall] removes: agents/, skills/, markers, and the '
|
|
696
695
|
+ `${hostSpec(host).agentFile} managed block (Copilot has no `
|
|
697
696
|
+ 'settings.json/hooks to unwire; your own .github files are kept).\n');
|
|
698
697
|
} else if (host === 'claude' || host === 'bob') {
|
|
699
|
-
|
|
698
|
+
printOut('[uninstall] removes: agents/, skills/, markers, the '
|
|
700
699
|
+ `${hostSpec(host).agentFile} managed block, and Geneseed's `
|
|
701
700
|
+ 'settings.json hooks/excludes (your own keys/hooks are kept).\n');
|
|
702
701
|
} else if (scope === 'global') {
|
|
703
|
-
|
|
702
|
+
printOut('[uninstall] removes: AGENT.md, agents/, skills/, plugins/, markers, and the '
|
|
704
703
|
+ 'opencode.json instructions entry.\n');
|
|
705
704
|
} else {
|
|
706
|
-
|
|
705
|
+
printOut('[uninstall] removes: AGENT.md, .opencode/, laws/, agents/, skills/, and the '
|
|
707
706
|
+ 'opencode.json instructions entry.\n');
|
|
708
707
|
}
|
|
709
|
-
|
|
708
|
+
printOut('[uninstall] memory/ and notebook/ are kept in place (never deleted here)'
|
|
710
709
|
+ (stores.length ? ' — --archive-memory sets both aside.' : '.') + '\n');
|
|
711
710
|
if (stores.length && args.archiveMemory) {
|
|
712
|
-
|
|
711
|
+
printOut(`[uninstall] ${stores.join(' + ')}: will be ARCHIVED to a sibling `
|
|
713
712
|
+ 'archived-<name>/<timestamp>/ (never deleted)\n');
|
|
714
713
|
}
|
|
715
714
|
if (!args.yes) {
|
|
716
715
|
if (!process.stdin.isTTY) {
|
|
717
|
-
|
|
716
|
+
printErr('[uninstall] refusing to proceed without --yes (non-interactive).\n');
|
|
718
717
|
return 1;
|
|
719
718
|
}
|
|
720
719
|
if (!confirm('Proceed with uninstall?', false)) {
|
|
721
|
-
|
|
720
|
+
printOut('[uninstall] cancelled — nothing removed.\n');
|
|
722
721
|
return 0;
|
|
723
722
|
}
|
|
724
723
|
}
|
|
725
724
|
const memory = args.archiveMemory ? 'archive' : 'keep';
|
|
726
725
|
const s = installUninstall(root, host, scope, memory);
|
|
727
726
|
if (!(s.ok ?? true)) {
|
|
728
|
-
|
|
727
|
+
printErr(`[uninstall] failed: ${s.error || 'unknown error'}\n`);
|
|
729
728
|
return 1;
|
|
730
729
|
}
|
|
731
730
|
const archived = s.archived || [];
|
|
732
731
|
const mem = archived.length ? `archived -> ${archived.join(', ')}` : 'kept in place';
|
|
733
732
|
const cfgfile = host === 'opencode' ? 'opencode.json' : 'settings.json';
|
|
734
733
|
if (s.incomplete && s.incomplete.length) {
|
|
735
|
-
|
|
734
|
+
printOut(`[uninstall] INCOMPLETE — removed ${s.removed} file(s), but `
|
|
736
735
|
+ `${s.incomplete.length} item(s) survived (see the WARN above); `
|
|
737
736
|
+ 'the install marker was kept — retry `geneseed uninstall` once they\'re '
|
|
738
737
|
+ `removable. ${cfgfile} updated where needed; memory/notebook ${mem}.\n`);
|
|
739
738
|
} else {
|
|
740
|
-
|
|
739
|
+
printOut(`[uninstall] done — removed ${s.removed} file(s); ${cfgfile} updated where `
|
|
741
740
|
+ `needed; memory/notebook ${mem}. Start a new session to apply.\n`);
|
|
742
741
|
}
|
|
743
742
|
if (scope === 'global') printSurvivingProjectInventory(root);
|
|
@@ -836,8 +835,8 @@ function installMoveList(root, kind) {
|
|
|
836
835
|
} else {
|
|
837
836
|
rels = ownedOf(claudeReadManifest(root)).filter((r) => r !== VERSION_MARKER);
|
|
838
837
|
}
|
|
839
|
-
const rroot =
|
|
840
|
-
return rels.filter((r) => r && within(
|
|
838
|
+
const rroot = resolvePath(root);
|
|
839
|
+
return rels.filter((r) => r && within(resolvePath(path.resolve(rroot, r)), rroot));
|
|
841
840
|
}
|
|
842
841
|
|
|
843
842
|
/**
|
|
@@ -863,9 +862,9 @@ export function installReaddEntry(target, entry) {
|
|
|
863
862
|
const [cfg, hadComments] = readJsonc(raw);
|
|
864
863
|
if (typeof cfg !== 'object' || cfg === null || Array.isArray(cfg)) return false;
|
|
865
864
|
const instr = Array.isArray(cfg.instructions) ? cfg.instructions : [];
|
|
866
|
-
if (
|
|
865
|
+
if (indexOfDeepEqual(instr, entry) >= 0) return false;
|
|
867
866
|
if (path.extname(target) === '.jsonc' && hadComments) {
|
|
868
|
-
|
|
867
|
+
printOut(`[activate] ${path.basename(target)} has comments — not rewriting it. Add this `
|
|
869
868
|
+ `to its "instructions" by hand: ${jsonDumps(entry)}\n`);
|
|
870
869
|
return false;
|
|
871
870
|
}
|
|
@@ -1040,7 +1039,7 @@ function remergeClaudeHooks(cfg, root = cfg) {
|
|
|
1040
1039
|
const [, claims] = mergeClaudeSettings(settingsFile(cfg, managed), 'global',
|
|
1041
1040
|
managed.settings_hooks ?? null, hookRunnerEntry(), doctrines, excluded);
|
|
1042
1041
|
// `and data` — a manifest that did not parse is not one to write back.
|
|
1043
|
-
if (!
|
|
1042
|
+
if (!deepEquals(claims, managed.settings_hooks ?? null) && Object.keys(data).length > 0) {
|
|
1044
1043
|
managed.settings_hooks = claims;
|
|
1045
1044
|
data.managed = managed;
|
|
1046
1045
|
const tmp = path.join(cfg, `${GLOBAL_MANIFEST}.tmp`);
|
|
@@ -1063,9 +1062,9 @@ function claudeDeactivate(root, scope = 'global', host = 'claude') {
|
|
|
1063
1062
|
const man = claudeReadManifest(cfg);
|
|
1064
1063
|
const managed = managedOf(man);
|
|
1065
1064
|
const stash = path.join(cfg, DISABLED_STASH, host);
|
|
1066
|
-
const rroot =
|
|
1065
|
+
const rroot = resolvePath(cfg);
|
|
1067
1066
|
const rels = ownedOf(man).filter((r) => r && r !== VERSION_MARKER
|
|
1068
|
-
&& within(
|
|
1067
|
+
&& within(resolvePath(path.resolve(rroot, r)), rroot));
|
|
1069
1068
|
const done = [];
|
|
1070
1069
|
for (const rel of rels) {
|
|
1071
1070
|
const src = path.join(cfg, rel);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Self-update: `git pull` the install's own origin, validate, rebuild.
|
|
3
3
|
*
|
|
4
4
|
* The verb this whole port exists for. Every other subcommand can be Python for one more
|
|
5
5
|
* release; `upgrade` is the one operation a "no Python needed" install cannot delegate,
|
|
@@ -31,9 +31,10 @@
|
|
|
31
31
|
* `tests/test_pure_function_parity.py`; the heartbeat and the hard deadline are gated by
|
|
32
32
|
* neither and are declared in the P8a handoff.
|
|
33
33
|
*
|
|
34
|
-
*
|
|
35
|
-
* `
|
|
36
|
-
*
|
|
34
|
+
* ALSO HERE, SINCE P8b: `syncSelf`/`cmdSyncSelf` and the bootstrap step runner
|
|
35
|
+
* (`cmdBootstrap`, `bootstrapPlain`) at the bottom of this file. Still NOT here: `main`, the
|
|
36
|
+
* launchers' self-heal entry. `NOT_PORTED_ACTIONS` in `js/web/jobs.mjs` is empty now — every
|
|
37
|
+
* action row crosses. `parseOrigin` is ported in full here even though only `upgrade`
|
|
37
38
|
* needs its fallback, because P8c's docs `about` kind needs the slug half and splitting one
|
|
38
39
|
* pure function across two phases is how a second classifier gets written.
|
|
39
40
|
*/
|
|
@@ -42,33 +43,32 @@ import { cpSync, existsSync, mkdirSync, rmSync, statSync } from 'node:fs';
|
|
|
42
43
|
import os from 'node:os';
|
|
43
44
|
import path from 'node:path';
|
|
44
45
|
|
|
45
|
-
import { ROOT } from '
|
|
46
|
-
import { exportImprovements, flushExportNotes } from '
|
|
47
|
-
import { opencodeConfigDir } from '
|
|
48
|
-
import {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
} from '
|
|
52
|
-
import {
|
|
53
|
-
import { restartDaemon } from './web/server.mjs';
|
|
46
|
+
import { ROOT } from '../build/source.mjs';
|
|
47
|
+
import { exportImprovements, flushExportNotes } from '../inspect/diff.mjs';
|
|
48
|
+
import { opencodeConfigDir } from '../hosts/hosts.mjs';
|
|
49
|
+
import { appendText, copyFile, printOut, printErr, readText, writeText } from '../lib/fs.mjs';
|
|
50
|
+
import { toPlatformPath, which } from '../lib/paths.mjs';
|
|
51
|
+
import { WHITESPACE, parseIntStrict, stripWhitespace } from '../lib/text.mjs';
|
|
52
|
+
import { NO_WINDOW } from '../lib/proc.mjs';
|
|
53
|
+
import { restartDaemon } from '../web/daemon.mjs';
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
56
|
* `_CREDS_RE` / `_redact_url_creds` — strip a `user[:token]@` userinfo out of any URL in
|
|
57
57
|
* `text` so a tokened remote never reaches a log line or an HTTP response.
|
|
58
58
|
*
|
|
59
|
-
* `
|
|
59
|
+
* `WHITESPACE` rather than `\s`: the class is NEGATED, so the two definitions disagree about
|
|
60
60
|
* whether a `\uFEFF` or a `\u001c` ends the userinfo. Unreachable through a git remote and
|
|
61
61
|
* spelled correctly anyway, for the reason `js/lib/fs.mjs` gives — an approximated
|
|
62
62
|
* primitive is the one that is wrong the day something reaches it.
|
|
63
63
|
*/
|
|
64
|
-
const CREDS_RE = new RegExp(`(://)[^/@${
|
|
64
|
+
const CREDS_RE = new RegExp(`(://)[^/@${WHITESPACE}]+@`, 'g');
|
|
65
65
|
|
|
66
66
|
export function redactUrlCreds(text) {
|
|
67
67
|
return (text || '').replace(CREDS_RE, '$1');
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
// `_NO_WINDOW` was defined here and PRIVATE to this module, which is how `js/doctor.mjs`
|
|
71
|
-
// and `js/hooks.mjs` came to spawn without it — see `js/lib/proc.mjs` for the flag, the
|
|
70
|
+
// `_NO_WINDOW` was defined here and PRIVATE to this module, which is how `js/inspect/doctor.mjs`
|
|
71
|
+
// and `js/hosts/hooks.mjs` came to spawn without it — see `js/lib/proc.mjs` for the flag, the
|
|
72
72
|
// capturing-only rule behind it, and why a miss here is invisible to every byte gate.
|
|
73
73
|
|
|
74
74
|
/** `subprocess.run(..., capture_output=True, encoding="utf-8")` reads in UNIVERSAL NEWLINES. */
|
|
@@ -84,7 +84,7 @@ export const DEFAULT_ORIGIN = { url: 'https://github.com/Arylmera/Geneseed',
|
|
|
84
84
|
* which-guarded, no-window, stripped + credential-redacted, NEVER raises. Returns
|
|
85
85
|
* `[rc, out, err]`; `rc` is null when git is absent or the spawn failed.
|
|
86
86
|
*
|
|
87
|
-
* `
|
|
87
|
+
* `which` and not a bare `'git'`: the reference answers `(None, "", "")` — which
|
|
88
88
|
* `preflight` reports as "git is not installed or not on PATH" — when the lookup misses,
|
|
89
89
|
* and a port that let the OS resolve the name would get an ENOENT instead and have to guess
|
|
90
90
|
* which of the two it was.
|
|
@@ -94,7 +94,7 @@ export const DEFAULT_ORIGIN = { url: 'https://github.com/Arylmera/Geneseed',
|
|
|
94
94
|
* "clean enough".
|
|
95
95
|
*/
|
|
96
96
|
export function gitRun(args, { timeout = 10, network = false } = {}) {
|
|
97
|
-
const exe =
|
|
97
|
+
const exe = which('git');
|
|
98
98
|
if (!exe) return [null, '', ''];
|
|
99
99
|
const cmd = ['-C', String(ROOT)];
|
|
100
100
|
let env;
|
|
@@ -112,8 +112,8 @@ export function gitRun(args, { timeout = 10, network = false } = {}) {
|
|
|
112
112
|
// the arm the reference reaches for every other failure.
|
|
113
113
|
if (r.error || r.status === null) return [null, '', ''];
|
|
114
114
|
return [r.status,
|
|
115
|
-
redactUrlCreds(
|
|
116
|
-
redactUrlCreds(
|
|
115
|
+
redactUrlCreds(stripWhitespace(universal(r.stdout))),
|
|
116
|
+
redactUrlCreds(stripWhitespace(universal(r.stderr)))];
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
/**
|
|
@@ -130,7 +130,7 @@ function urlsplitHostPath(o) {
|
|
|
130
130
|
// A scheme is `[A-Za-z][A-Za-z0-9+.-]*` before the first `:` — and only then. `urlsplit`
|
|
131
131
|
// leaves `foo:bar` alone when `foo` is not a legal scheme, which is what keeps an scp-form
|
|
132
132
|
// remote out of this branch (the caller checks for that shape first anyway).
|
|
133
|
-
const m = /^([A-Za-z][A-Za-z0-9
|
|
133
|
+
const m = /^([A-Za-z][A-Za-z0-9+.-]*):(.*)$/s.exec(rest);
|
|
134
134
|
if (m) rest = m[2];
|
|
135
135
|
let netloc = '';
|
|
136
136
|
if (rest.startsWith('//')) {
|
|
@@ -153,7 +153,7 @@ function urlsplitHostPath(o) {
|
|
|
153
153
|
* Userinfo and port dropped from the url; slug set only for a two-segment github.com path.
|
|
154
154
|
*/
|
|
155
155
|
export function parseOrigin(origin) {
|
|
156
|
-
const o =
|
|
156
|
+
const o = stripWhitespace(origin || '');
|
|
157
157
|
let host = '';
|
|
158
158
|
let p = '';
|
|
159
159
|
if (!o.includes('://') && o.includes('@') && o.split('@').slice(1).join('@').includes(':')) {
|
|
@@ -220,9 +220,9 @@ export function preflight() {
|
|
|
220
220
|
return pre('ready');
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
/** `_fetch_timeout()`. `
|
|
223
|
+
/** `_fetch_timeout()`. `parseIntStrict` is null where `int()` raises, which is the `except` arm. */
|
|
224
224
|
export function fetchTimeout() {
|
|
225
|
-
const n =
|
|
225
|
+
const n = parseIntStrict(process.env.GENESEED_NET_TIMEOUT ?? '120');
|
|
226
226
|
if (n === null) return 120;
|
|
227
227
|
return Math.max(30, n);
|
|
228
228
|
}
|
|
@@ -235,8 +235,8 @@ export function fetchTimeout() {
|
|
|
235
235
|
* (`rev-list --count` prints ASCII). Reproducing the crash would be reproducing a bug; the
|
|
236
236
|
* divergence is confined to input git cannot emit and is recorded rather than matched.
|
|
237
237
|
*/
|
|
238
|
-
export function
|
|
239
|
-
const t =
|
|
238
|
+
export function countOccurrences(s) {
|
|
239
|
+
const t = stripWhitespace(s || '');
|
|
240
240
|
return /^[0-9]+$/.test(t) ? Number(t) : 0;
|
|
241
241
|
}
|
|
242
242
|
|
|
@@ -269,7 +269,7 @@ export function killTree(child) {
|
|
|
269
269
|
export function fetchPhases(rawLines, last = '') {
|
|
270
270
|
const out = [];
|
|
271
271
|
for (const raw of rawLines) {
|
|
272
|
-
const line = redactUrlCreds(
|
|
272
|
+
const line = redactUrlCreds(stripWhitespace(raw));
|
|
273
273
|
if (!line) continue;
|
|
274
274
|
const phase = line.split(':')[0];
|
|
275
275
|
if (phase !== last) {
|
|
@@ -292,7 +292,7 @@ export function fetchPhases(rawLines, last = '') {
|
|
|
292
292
|
* since P6h.
|
|
293
293
|
*/
|
|
294
294
|
export async function fetchStreaming(log = null) {
|
|
295
|
-
const exe =
|
|
295
|
+
const exe = which('git');
|
|
296
296
|
if (!exe) return [null, 'git is not installed or not on PATH'];
|
|
297
297
|
const timeout = fetchTimeout();
|
|
298
298
|
const args = ['-C', String(ROOT),
|
|
@@ -323,7 +323,7 @@ export async function fetchStreaming(log = null) {
|
|
|
323
323
|
const [emit, next] = fetchPhases(parts, last);
|
|
324
324
|
last = next;
|
|
325
325
|
for (const p of parts) {
|
|
326
|
-
const s = redactUrlCreds(
|
|
326
|
+
const s = redactUrlCreds(stripWhitespace(p));
|
|
327
327
|
if (s) lines.push(s);
|
|
328
328
|
}
|
|
329
329
|
if (log) for (const l of emit) log(`[geneseed] ${l}`);
|
|
@@ -373,8 +373,8 @@ export async function measureUpstream(log = null) {
|
|
|
373
373
|
if (rc !== 0) return ['fetch_failed', 0, err];
|
|
374
374
|
const [, aheadRaw] = gitRun(['rev-list', '--count', '@{u}..HEAD']);
|
|
375
375
|
const [, behindRaw] = gitRun(['rev-list', '--count', 'HEAD..@{u}']);
|
|
376
|
-
const ahead =
|
|
377
|
-
const behind =
|
|
376
|
+
const ahead = countOccurrences(aheadRaw);
|
|
377
|
+
const behind = countOccurrences(behindRaw);
|
|
378
378
|
if (ahead > 0) {
|
|
379
379
|
const [mrc] = gitRun(['merge-base', 'HEAD', '@{u}']);
|
|
380
380
|
return [mrc === 0 ? 'diverged' : 'unrelated', 0, ''];
|
|
@@ -412,8 +412,8 @@ export function runDoctor(cand) {
|
|
|
412
412
|
// `stderr=subprocess.STDOUT` — the reference gives the child ONE handle, so its two
|
|
413
413
|
// streams are interleaved in write order and arrive as `proc.stdout`. `spawnSync` has no
|
|
414
414
|
// way to hand two descriptors the same pipe, so the two are concatenated instead: equal
|
|
415
|
-
// whenever the child writes to only one of them, which `doctor` does (`js/doctor.mjs`
|
|
416
|
-
// reports every problem through `
|
|
415
|
+
// whenever the child writes to only one of them, which `doctor` does (`js/inspect/doctor.mjs`
|
|
416
|
+
// reports every problem through `printOut`), and the reason the two doctor cells below are
|
|
417
417
|
// the gate on this rather than a comment. If a future check reports on stderr the
|
|
418
418
|
// ORDERING here is what would differ, not the content.
|
|
419
419
|
return [r.status === 0, universal(r.stdout) + universal(r.stderr)];
|
|
@@ -454,7 +454,7 @@ export function logfile() {
|
|
|
454
454
|
// `Path(env)`, stringified with the platform separator when `upgrade` prints it — the same
|
|
455
455
|
// rule `GENESEED_OUT` needed. No cell can set this (`cell_env` clears every `GENESEED_*`
|
|
456
456
|
// and no upgrade cell re-adds it), so it is spelled from the rule rather than from a gate.
|
|
457
|
-
if (env) return
|
|
457
|
+
if (env) return toPlatformPath(env);
|
|
458
458
|
return path.join(os.homedir(), '.geneseed-install.log');
|
|
459
459
|
}
|
|
460
460
|
|
|
@@ -471,7 +471,7 @@ export class Log {
|
|
|
471
471
|
}
|
|
472
472
|
|
|
473
473
|
call(msg) {
|
|
474
|
-
|
|
474
|
+
printOut(`${msg}\n`);
|
|
475
475
|
if (this.path !== null) {
|
|
476
476
|
try { appendText(this.path, `${msg}\n`); } catch { /* OSError: pass */ }
|
|
477
477
|
}
|
|
@@ -485,7 +485,7 @@ export function resolveEmit(cfg, out) {
|
|
|
485
485
|
for (const marker of [path.join(String(cfg), '.geneseed-emit'),
|
|
486
486
|
path.join(String(out), '.geneseed-emit')]) {
|
|
487
487
|
try {
|
|
488
|
-
const val =
|
|
488
|
+
const val = stripWhitespace(readText(marker));
|
|
489
489
|
if (val) return val;
|
|
490
490
|
} catch { /* OSError: pass */ }
|
|
491
491
|
}
|
|
@@ -497,7 +497,7 @@ export function markerTheme(cfg, out) {
|
|
|
497
497
|
for (const marker of [path.join(String(cfg), '.geneseed-theme'),
|
|
498
498
|
path.join(String(out), '.geneseed-theme')]) {
|
|
499
499
|
try {
|
|
500
|
-
const val =
|
|
500
|
+
const val = stripWhitespace(readText(marker));
|
|
501
501
|
if (val) return val;
|
|
502
502
|
} catch { /* OSError: pass */ }
|
|
503
503
|
}
|
|
@@ -570,11 +570,11 @@ export async function rebuildBundle(here, out, theme, emit, rootDir, log) {
|
|
|
570
570
|
else if (emit === 'opencode-global') buildArgs.push('--emit', 'opencode-global');
|
|
571
571
|
log(`[geneseed] rebuilding bundle -> ${out} (theme: ${theme || 'config default'}, emit: ${emit}) ...`);
|
|
572
572
|
await drainStdout();
|
|
573
|
-
// THE GENERATOR, RE-EXECUTED. `js/generate.mjs`'s `cmdBuild` calls `driverMain` in-process
|
|
573
|
+
// THE GENERATOR, RE-EXECUTED. `js/build/generate.mjs`'s `cmdBuild` calls `driverMain` in-process
|
|
574
574
|
// and is right to — nothing has changed under it. Here the source on disk was replaced
|
|
575
575
|
// three statements ago, so this must be a new process reading the new files.
|
|
576
576
|
const proc = spawnSync(process.execPath,
|
|
577
|
-
[path.join(String(here), 'bin', '
|
|
577
|
+
[path.join(String(here), 'bin', 'build-driver.mjs'), ...buildArgs],
|
|
578
578
|
{ cwd: String(here), stdio: 'inherit' });
|
|
579
579
|
if (proc.status !== 0) return proc.status === null ? 1 : proc.status;
|
|
580
580
|
// If a web daemon is running, bounce it so the new source and the freshly rebuilt web/dist
|
|
@@ -623,13 +623,13 @@ export async function upgrade(ref = null, themeArg = null) {
|
|
|
623
623
|
+ 'current branch (tag/branch pinning was removed with the zip path).');
|
|
624
624
|
}
|
|
625
625
|
const here = String(ROOT);
|
|
626
|
-
// `Path(os.environ.get(...) or ...)`, and `
|
|
626
|
+
// `Path(os.environ.get(...) or ...)`, and `toPlatformPath` is the `Path(str)` half of it: the
|
|
627
627
|
// reference stringifies with the PLATFORM separator, so a `GENESEED_OUT` spelled
|
|
628
628
|
// `C:/x/bundle` — which is how a JSON config, a shell script and this harness's own
|
|
629
629
|
// `{sb/}` placeholder all spell it — reaches the log as `C:\x\bundle`. Six cells differed
|
|
630
630
|
// on exactly that and nothing else.
|
|
631
|
-
const out =
|
|
632
|
-
const rootDir =
|
|
631
|
+
const out = toPlatformPath(process.env.GENESEED_OUT || path.join(path.dirname(here), 'Harness'));
|
|
632
|
+
const rootDir = toPlatformPath(process.env.GENESEED_ROOT || path.dirname(out));
|
|
633
633
|
const cfg = opencodeConfigDir();
|
|
634
634
|
const emit = resolveEmit(cfg, out);
|
|
635
635
|
|
|
@@ -647,7 +647,7 @@ export async function upgrade(ref = null, themeArg = null) {
|
|
|
647
647
|
return p.kind === 'info' ? 3 : 1;
|
|
648
648
|
}
|
|
649
649
|
|
|
650
|
-
log(`[geneseed] fetching from origin (git: ${
|
|
650
|
+
log(`[geneseed] fetching from origin (git: ${which('git') || 'git'}, `
|
|
651
651
|
+ `timeout: ${fetchTimeout()}s) ...`);
|
|
652
652
|
const [code, behind, err] = await measureUpstream(log);
|
|
653
653
|
if (code === 'fetch_failed') {
|
|
@@ -715,11 +715,11 @@ export async function cmdUpgrade(args) {
|
|
|
715
715
|
try {
|
|
716
716
|
const [ipath] = exportImprovements();
|
|
717
717
|
if (ipath) {
|
|
718
|
-
|
|
719
|
-
|
|
718
|
+
printOut(`[upgrade] deployed harness carries local edits — saved to ${ipath}\n`);
|
|
719
|
+
printOut('[upgrade] hand that file to your agent to back-port them into src/.\n');
|
|
720
720
|
}
|
|
721
721
|
} catch (e) {
|
|
722
|
-
|
|
722
|
+
printErr(`[upgrade] ⚠️ could not export local edits (${e && e.message ? e.message : e}) — `
|
|
723
723
|
+ 'run `geneseed diff --out FILE` before upgrading to keep them.\n');
|
|
724
724
|
}
|
|
725
725
|
let { ref } = args;
|
|
@@ -862,17 +862,17 @@ async function bootstrapPlain() {
|
|
|
862
862
|
let failed = false;
|
|
863
863
|
for (let i = 0; i < steps.length; i += 1) {
|
|
864
864
|
const [title, cmd, step] = steps[i];
|
|
865
|
-
|
|
866
|
-
|
|
865
|
+
printOut(`[geneseed] step ${i + 1}/${steps.length}: ${title} ...\n`);
|
|
866
|
+
|
|
867
867
|
const rc = await step();
|
|
868
868
|
if (rc !== 0 && rc !== 3) {
|
|
869
869
|
failed = true;
|
|
870
870
|
for (const ln of diagnoseFailedStep(i + 1, steps.length, title, cmd, rc, '')) {
|
|
871
|
-
|
|
871
|
+
printOut(`${ln}\n`);
|
|
872
872
|
}
|
|
873
873
|
}
|
|
874
874
|
}
|
|
875
|
-
if (!failed)
|
|
875
|
+
if (!failed) printOut('[geneseed] ✓ update complete.\n');
|
|
876
876
|
return failed;
|
|
877
877
|
}
|
|
878
878
|
|