geneseed 3.1.0 → 3.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +110 -0
- package/DESIGN.md +31 -20
- package/README.md +2 -2
- package/SETUP.md +30 -3
- package/SHIPPED.md +1 -1
- package/bin/{geneseed.mjs → build-driver.mjs} +88 -42
- package/bin/geneseed-cli.mjs +33 -32
- package/bin/geneseed-hook.mjs +7 -7
- package/docs/design-history.md +40 -40
- package/docs/extending.md +113 -85
- package/docs/web/rules.md +2 -0
- package/docs/web/setup-choices.md +13 -1
- package/harness.config.json +1 -1
- package/js/README.md +203 -0
- package/js/build/bundle.mjs +231 -0
- package/js/{catalog.mjs → build/catalog.mjs} +7 -7
- package/js/build/emit-claude.mjs +287 -0
- package/js/build/emit-common.mjs +255 -0
- package/js/build/emit-opencode.mjs +214 -0
- package/js/{generate.mjs → build/generate.mjs} +45 -31
- package/js/{render.mjs → build/render.mjs} +72 -11
- package/js/{checkout.mjs → build/source.mjs} +52 -11
- package/js/build/stubs.mjs +256 -0
- package/js/{themes.mjs → build/themes.mjs} +24 -24
- package/js/build/version.mjs +155 -0
- package/js/{hooks.mjs → hosts/hooks.mjs} +47 -44
- package/js/{hosts.mjs → hosts/hosts.mjs} +30 -28
- package/js/{installs.mjs → hosts/installs.mjs} +68 -16
- package/js/{link.mjs → hosts/link.mjs} +29 -28
- package/js/{mcp.mjs → hosts/mcp.mjs} +10 -9
- package/js/{native.mjs → hosts/native.mjs} +24 -22
- package/js/{opencode.mjs → hosts/opencode.mjs} +12 -7
- package/js/{settings.mjs → hosts/settings.mjs} +94 -69
- package/js/inspect/checks-authoring.mjs +761 -0
- package/js/inspect/checks-build.mjs +231 -0
- package/js/inspect/checks-repo.mjs +308 -0
- package/js/{diff.mjs → inspect/diff.mjs} +34 -31
- package/js/inspect/doctor.mjs +286 -0
- package/js/{excludes.mjs → inspect/excludes.mjs} +30 -33
- package/js/{inventory.mjs → inspect/inventory.mjs} +34 -20
- package/js/{registry.mjs → inspect/registry.mjs} +12 -10
- package/js/inspect/scan.mjs +125 -0
- package/js/{status.mjs → inspect/status.mjs} +27 -25
- package/js/inspect/validate.mjs +199 -0
- package/js/lib/fs.mjs +37 -759
- package/js/lib/json.mjs +344 -0
- package/js/lib/paths.mjs +242 -0
- package/js/lib/proc.mjs +3 -3
- package/js/lib/text.mjs +181 -0
- package/js/lib/udiff.mjs +1 -1
- package/js/{memory.mjs → maintain/memory.mjs} +10 -10
- package/js/{migrate.mjs → maintain/migrate.mjs} +28 -26
- package/js/{setup.mjs → maintain/setup.mjs} +49 -37
- package/js/{uninstall.mjs → maintain/uninstall.mjs} +62 -58
- package/js/{update.mjs → maintain/update.mjs} +51 -51
- package/js/{anim.mjs → ui/anim.mjs} +30 -28
- package/js/{cli.mjs → ui/cli.mjs} +10 -10
- package/js/{menu.mjs → ui/menu.mjs} +10 -9
- package/js/{tui.mjs → ui/tui.mjs} +36 -34
- package/js/web/actions.mjs +124 -89
- package/js/web/activity.mjs +22 -21
- package/js/web/api.mjs +88 -58
- package/js/web/daemon.mjs +319 -0
- package/js/web/docs.mjs +24 -21
- package/js/web/graph.mjs +0 -0
- package/js/web/handler.mjs +442 -0
- package/js/web/jobs.mjs +29 -21
- package/js/web/routes.mjs +223 -0
- package/js/web/server.mjs +56 -987
- package/package.json +11 -3
- package/src/AGENT.md.tmpl +1 -1
- package/src/doctrines/README.md +1 -1
- package/themes/opencode/README.md +1 -1
- package/web/dist/assets/{About-_MnJcwb_.js → About-CPvIFZGh.js} +1 -1
- package/web/dist/assets/{Activity-DpjqO-FR.js → Activity-z0uXdS7J.js} +1 -1
- package/web/dist/assets/{ActivityDetail-Bvrjv8oW.js → ActivityDetail-CEj5IJpF.js} +1 -1
- package/web/dist/assets/{Diff-DZ0IehBV.js → Diff-BMR8fPuG.js} +1 -1
- package/web/dist/assets/{Doctor-cu4krooO.js → Doctor-Bz5qE4r9.js} +1 -1
- package/web/dist/assets/{Graph-DUOPY65j.js → Graph-BV6a6t2l.js} +1 -1
- package/web/dist/assets/{Harnesses-B5Osg-BF.js → Harnesses-C5Ag5PaQ.js} +1 -1
- package/web/dist/assets/Laws-BK07NKw9.js +3 -0
- package/web/dist/assets/{Library-8y69I3_z.js → Library-CiXN9sC3.js} +1 -1
- package/web/dist/assets/{Markdown-Ds5GS1OD.js → Markdown-DG6FjUz-.js} +1 -1
- package/web/dist/assets/{Profile-7Jxa7ZUX.js → Profile-B2oSvdxH.js} +1 -1
- package/web/dist/assets/{Rules-UZ0jWkjg.js → Rules-DobVPd80.js} +1 -1
- package/web/dist/assets/{Skills-Bgkl0xLd.js → Skills-DQPsvsn9.js} +1 -1
- package/web/dist/assets/{StatusBadge-Cx4caF_2.js → StatusBadge-BhJ3HhdI.js} +1 -1
- package/web/dist/assets/{Themes-De8az7Sv.js → Themes-BP38FBU9.js} +1 -1
- package/web/dist/assets/{activity-VDAdVQ3q.js → activity-D9mfv7pY.js} +1 -1
- package/web/dist/assets/index-BE226Gv6.css +1 -0
- package/web/dist/assets/{index-BKrjBRV4.js → index-BZ7cPLMI.js} +1 -1
- package/web/dist/assets/index-BgIs5eIa.js +12 -0
- package/web/dist/assets/{index-wNiNzFFQ.js → index-DGM5j2hB.js} +1 -1
- package/web/dist/index.html +2 -2
- package/web/src/pages/Laws.jsx +135 -69
- package/docs/declined.md +0 -200
- package/docs/limits.md +0 -329
- package/js/doctor.mjs +0 -1712
- package/js/emit.mjs +0 -1410
- package/web/dist/assets/Laws-D8DJgTBp.js +0 -3
- package/web/dist/assets/index-1pPSz7Kl.css +0 -1
- package/web/dist/assets/index-C-gb-q-r.js +0 -12
|
@@ -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,25 +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
|
-
claudeCfg, claudeReadManifest, doctrinesOfDir, emitHostScopeOf,
|
|
53
|
+
claudeCfg, claudeReadManifest, doctrinesOfDir, emitHostScopeOf, excludedRulesOfDir, installKind,
|
|
54
|
+
installState,
|
|
54
55
|
registeredTargets, readMaybe, DISABLED_STASH,
|
|
55
|
-
} from '
|
|
56
|
+
} from '../hosts/installs.mjs';
|
|
56
57
|
import {
|
|
57
|
-
GLOBAL_MANIFEST, HOSTS, VERSION_MARKER, expanduser, opencodeConfigDir,
|
|
58
|
-
} from '
|
|
59
|
-
import { mcpCommented } from '
|
|
58
|
+
GLOBAL_MANIFEST, HOSTS, VERSION_MARKER, expanduser, opencodeConfigDir, resolvePath,
|
|
59
|
+
} from '../hosts/hosts.mjs';
|
|
60
|
+
import { mcpCommented } from '../hosts/mcp.mjs';
|
|
60
61
|
import {
|
|
61
62
|
managedBlockRead, managedBlockRemove, managedBlockWrite, mergeClaudeSettings,
|
|
62
63
|
opencodeTarget, readJsonc, settingsIntegrityCheck, wireClaudeExcludes,
|
|
63
64
|
unwireClaudeExcludes, unwireClaudeSettings,
|
|
64
|
-
} from '
|
|
65
|
-
import {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
} 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';
|
|
69
69
|
|
|
70
70
|
const isDir = (p) => { try { return statSync(p).isDirectory(); } catch { return false; } };
|
|
71
71
|
const isFile = (p) => { try { return statSync(p).isFile(); } catch { return false; } };
|
|
@@ -172,7 +172,7 @@ function unlinkOwned(base, owned, label = '') {
|
|
|
172
172
|
// `f"{rel} ({e})"`. `str(OSError)` and an `Error.message` word the same fault
|
|
173
173
|
// differently, and no cell can reach this: it needs a file that exists, is a file, and
|
|
174
174
|
// cannot be unlinked. Recorded rather than papered over — the same standing item
|
|
175
|
-
// `js/excludes.mjs` carries for its `could not remove <stub>` branch.
|
|
175
|
+
// `js/inspect/excludes.mjs` carries for its `could not remove <stub>` branch.
|
|
176
176
|
failed.push(`${label}${rel} (${e && e.message ? e.message : e})`);
|
|
177
177
|
}
|
|
178
178
|
}
|
|
@@ -181,12 +181,12 @@ function unlinkOwned(base, owned, label = '') {
|
|
|
181
181
|
|
|
182
182
|
/** The two WARNs every reversal prints when an owned file survives. Identical in all three. */
|
|
183
183
|
function warnSurvivors(failed) {
|
|
184
|
-
|
|
184
|
+
printErr('[uninstall] WARN: could not remove '
|
|
185
185
|
+ `${failed.length} owned file(s): ${failed.join(', ')}\n`);
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
function warnMarkersKept() {
|
|
189
|
-
|
|
189
|
+
printErr('[uninstall] WARN: the manifest and markers were KEPT so '
|
|
190
190
|
+ '`geneseed uninstall` can be retried once the file(s) are '
|
|
191
191
|
+ 'unlocked/removable.\n');
|
|
192
192
|
}
|
|
@@ -215,7 +215,7 @@ export function unmergeOpencodeJson(p, entry) {
|
|
|
215
215
|
const instr = cfg.instructions;
|
|
216
216
|
if (!Array.isArray(instr) || !instr.includes(entry)) return false;
|
|
217
217
|
if (path.extname(target) === '.jsonc' && hadComments) {
|
|
218
|
-
|
|
218
|
+
printOut(`[uninstall] ${path.basename(target)} has comments — not rewriting it. Remove `
|
|
219
219
|
+ `this from its "instructions" by hand: ${jsonDumps(entry)}\n`);
|
|
220
220
|
return false;
|
|
221
221
|
}
|
|
@@ -264,7 +264,7 @@ function settingsFile(cfg, managed) {
|
|
|
264
264
|
/** `_harness_mcp._claude_md_path` — where the manifest says the managed block lives. */
|
|
265
265
|
function claudeMdPath(cfg, managed) {
|
|
266
266
|
const rel = ((managed && managed.claude_md) || {}).rel || 'CLAUDE.md';
|
|
267
|
-
return
|
|
267
|
+
return resolvePath(path.join(cfg, rel));
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
/** A manifest's `managed` map, or `{}` — the Python's `isinstance(..., dict)` guard. */
|
|
@@ -360,7 +360,7 @@ function mcpLoadOpencode(p) {
|
|
|
360
360
|
*
|
|
361
361
|
* A global install wires the ABSOLUTE posix path; a project install wires the relative
|
|
362
362
|
* `…/AGENT.md` the emit recorded, read back off the live config so a bundle sub-dir layout
|
|
363
|
-
* round-trips. `
|
|
363
|
+
* round-trips. `isAbsolutePath` and not `path.isAbsolute`: they disagree on Windows for a
|
|
364
364
|
* ROOTLESS `/repo/AGENT.md`, which Python keeps and `path.isAbsolute` would have skipped.
|
|
365
365
|
*/
|
|
366
366
|
export function installAgentEntry(root, kind) {
|
|
@@ -382,7 +382,7 @@ export function installAgentEntry(root, kind) {
|
|
|
382
382
|
export function installAgentEntryOf(instr) {
|
|
383
383
|
if (Array.isArray(instr)) {
|
|
384
384
|
for (const e of instr) {
|
|
385
|
-
if (typeof e === 'string' && !
|
|
385
|
+
if (typeof e === 'string' && !isAbsolutePath(e) && path.basename(e) === 'AGENT.md') return e;
|
|
386
386
|
}
|
|
387
387
|
}
|
|
388
388
|
return 'AGENT.md';
|
|
@@ -508,7 +508,7 @@ export function installUninstall(root, host = 'opencode', scope = 'global', memo
|
|
|
508
508
|
if (survivors.length || failed.length) {
|
|
509
509
|
if (survivors.length && !failed.length) {
|
|
510
510
|
// With `failed` set the reversal already warned twice — no third overlapping WARN.
|
|
511
|
-
|
|
511
|
+
printErr('[uninstall] WARN: could not fully remove the install — still present: '
|
|
512
512
|
+ `${survivors.join(', ')}. The install marker was KEPT so you can retry `
|
|
513
513
|
+ '`geneseed uninstall` once the file(s) are unlocked/removable.\n');
|
|
514
514
|
}
|
|
@@ -544,7 +544,7 @@ export function projectQualifies(root, host) {
|
|
|
544
544
|
const cfg = path.join(root, spec.projectMarker);
|
|
545
545
|
if (!isDir(cfg)) return false;
|
|
546
546
|
try {
|
|
547
|
-
if (
|
|
547
|
+
if (resolvePath(cfg) === resolvePath(spec.configDir())) return false;
|
|
548
548
|
} catch { /* as the Python's bare `except Exception: pass` */ }
|
|
549
549
|
if (isFile(path.join(cfg, GLOBAL_MANIFEST))) return true;
|
|
550
550
|
const hs = emitHostScopeOf(root);
|
|
@@ -568,7 +568,7 @@ export function uninstallResolve(targetArg) {
|
|
|
568
568
|
const globalHit = (p) => {
|
|
569
569
|
for (const spec of HOSTS) {
|
|
570
570
|
try {
|
|
571
|
-
if (p ===
|
|
571
|
+
if (p === resolvePath(spec.configDir())) return [spec.host, 'global', p];
|
|
572
572
|
} catch { continue; }
|
|
573
573
|
}
|
|
574
574
|
return null;
|
|
@@ -580,7 +580,7 @@ export function uninstallResolve(targetArg) {
|
|
|
580
580
|
return null;
|
|
581
581
|
};
|
|
582
582
|
if (targetArg) {
|
|
583
|
-
const p =
|
|
583
|
+
const p = resolvePath(expanduser(targetArg));
|
|
584
584
|
const hit = globalHit(p);
|
|
585
585
|
if (hit) return hit;
|
|
586
586
|
for (const spec of HOSTS) {
|
|
@@ -594,7 +594,7 @@ export function uninstallResolve(targetArg) {
|
|
|
594
594
|
// `.resolve()` so the cwd fallback matches every other branch and the registry, which
|
|
595
595
|
// stores resolved paths — a short-form (8.3) cwd, as Windows CI hands back for %TEMP%,
|
|
596
596
|
// would otherwise return a root that LOOKS different from the identical directory.
|
|
597
|
-
const hit = projectHit(
|
|
597
|
+
const hit = projectHit(resolvePath(process.cwd()));
|
|
598
598
|
if (hit) return hit;
|
|
599
599
|
return ['opencode', 'global', opencodeConfigDir()];
|
|
600
600
|
}
|
|
@@ -608,11 +608,11 @@ export function uninstallResolve(targetArg) {
|
|
|
608
608
|
* rediscovered from, and the just-removed root is excluded.
|
|
609
609
|
*/
|
|
610
610
|
export function survivingProjectInstalls(removedRoot) {
|
|
611
|
-
const rroot =
|
|
611
|
+
const rroot = resolvePath(removedRoot);
|
|
612
612
|
const out = [];
|
|
613
613
|
for (const [host, scope, root] of registeredTargets()) {
|
|
614
614
|
if (scope !== 'project') continue;
|
|
615
|
-
try { if (
|
|
615
|
+
try { if (resolvePath(root) === rroot) continue; } catch { /* as the Python's except OSError */ }
|
|
616
616
|
out.push([host, scope, root]);
|
|
617
617
|
}
|
|
618
618
|
return out;
|
|
@@ -622,10 +622,10 @@ export function survivingProjectInstalls(removedRoot) {
|
|
|
622
622
|
export function printSurvivingProjectInventory(removedRoot) {
|
|
623
623
|
const survivors = survivingProjectInstalls(removedRoot);
|
|
624
624
|
if (!survivors.length) return;
|
|
625
|
-
|
|
625
|
+
printOut(`[uninstall] ${survivors.length} project install(s) remain — the global removal `
|
|
626
626
|
+ 'does not affect them (each is self-contained):\n');
|
|
627
627
|
for (const [host, scope, root] of survivors) {
|
|
628
|
-
|
|
628
|
+
printOut(` - ${root} (${host}:${scope}) — remove with: `
|
|
629
629
|
+ `geneseed uninstall --target "${root}" --yes\n`);
|
|
630
630
|
}
|
|
631
631
|
}
|
|
@@ -639,8 +639,8 @@ export function printSurvivingProjectInventory(removedRoot) {
|
|
|
639
639
|
export function printOtherHostHits(root, removedHost) {
|
|
640
640
|
for (const spec of HOSTS) {
|
|
641
641
|
if (spec.host !== removedHost && projectQualifies(root, spec.host)) {
|
|
642
|
-
|
|
643
|
-
+ 'uninstall
|
|
642
|
+
printOut(`[uninstall] also found ${spec.host}:project here — run \`harness `
|
|
643
|
+
+ 'uninstall` again to remove it.\n');
|
|
644
644
|
}
|
|
645
645
|
}
|
|
646
646
|
}
|
|
@@ -648,7 +648,7 @@ export function printOtherHostHits(root, removedHost) {
|
|
|
648
648
|
/**
|
|
649
649
|
* `_harness_setup._confirm`, and the only interactive read this verb makes.
|
|
650
650
|
*
|
|
651
|
-
* 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
|
|
652
652
|
* `_harness_setup` beside the `_ask` it is built on, and `setup` is its second caller. The
|
|
653
653
|
* body stayed here for one phase because a helper with one caller is not shared yet; the
|
|
654
654
|
* rule this port uses is that the SECOND owner is what moves it, and a byte gate is what
|
|
@@ -673,8 +673,8 @@ export function cmdUninstall(args) {
|
|
|
673
673
|
if (hit === null) {
|
|
674
674
|
// `uninstallResolve(null)` never returns null — it falls back to the OpenCode global
|
|
675
675
|
// default — so reaching here means `--target` was given and was not recognised.
|
|
676
|
-
const targetDesc =
|
|
677
|
-
|
|
676
|
+
const targetDesc = resolvePath(expanduser(args.target));
|
|
677
|
+
printErr(`[uninstall] no Geneseed install detected at ${targetDesc}.\n`
|
|
678
678
|
+ '[uninstall] pass --target <repo> for a project install (.opencode/.claude/'
|
|
679
679
|
+ '.bob/.github) or --target <config dir> for a global one.\n');
|
|
680
680
|
return 1;
|
|
@@ -683,60 +683,60 @@ export function cmdUninstall(args) {
|
|
|
683
683
|
if (installState(root, host, scope) === 'absent') {
|
|
684
684
|
const where = scope === 'project' && host !== 'opencode'
|
|
685
685
|
? ` under ${hostSpec(host).projectMarker}/` : '';
|
|
686
|
-
|
|
686
|
+
printErr(`[uninstall] no ${host}:${scope} Geneseed install at ${root} `
|
|
687
687
|
+ `(no ${GLOBAL_MANIFEST}${where}).\n`);
|
|
688
688
|
return 1;
|
|
689
689
|
}
|
|
690
690
|
const data = installDataDir(root, host, scope);
|
|
691
691
|
const stores = ['memory', 'notebook'].filter((n) => isDir(path.join(data, n)));
|
|
692
|
-
|
|
692
|
+
printOut(`[uninstall] target: ${root} (${host}:${scope})\n`);
|
|
693
693
|
if (host === 'copilot') {
|
|
694
|
-
|
|
694
|
+
printOut('[uninstall] removes: agents/, skills/, markers, and the '
|
|
695
695
|
+ `${hostSpec(host).agentFile} managed block (Copilot has no `
|
|
696
696
|
+ 'settings.json/hooks to unwire; your own .github files are kept).\n');
|
|
697
697
|
} else if (host === 'claude' || host === 'bob') {
|
|
698
|
-
|
|
698
|
+
printOut('[uninstall] removes: agents/, skills/, markers, the '
|
|
699
699
|
+ `${hostSpec(host).agentFile} managed block, and Geneseed's `
|
|
700
700
|
+ 'settings.json hooks/excludes (your own keys/hooks are kept).\n');
|
|
701
701
|
} else if (scope === 'global') {
|
|
702
|
-
|
|
702
|
+
printOut('[uninstall] removes: AGENT.md, agents/, skills/, plugins/, markers, and the '
|
|
703
703
|
+ 'opencode.json instructions entry.\n');
|
|
704
704
|
} else {
|
|
705
|
-
|
|
705
|
+
printOut('[uninstall] removes: AGENT.md, .opencode/, laws/, agents/, skills/, and the '
|
|
706
706
|
+ 'opencode.json instructions entry.\n');
|
|
707
707
|
}
|
|
708
|
-
|
|
708
|
+
printOut('[uninstall] memory/ and notebook/ are kept in place (never deleted here)'
|
|
709
709
|
+ (stores.length ? ' — --archive-memory sets both aside.' : '.') + '\n');
|
|
710
710
|
if (stores.length && args.archiveMemory) {
|
|
711
|
-
|
|
711
|
+
printOut(`[uninstall] ${stores.join(' + ')}: will be ARCHIVED to a sibling `
|
|
712
712
|
+ 'archived-<name>/<timestamp>/ (never deleted)\n');
|
|
713
713
|
}
|
|
714
714
|
if (!args.yes) {
|
|
715
715
|
if (!process.stdin.isTTY) {
|
|
716
|
-
|
|
716
|
+
printErr('[uninstall] refusing to proceed without --yes (non-interactive).\n');
|
|
717
717
|
return 1;
|
|
718
718
|
}
|
|
719
719
|
if (!confirm('Proceed with uninstall?', false)) {
|
|
720
|
-
|
|
720
|
+
printOut('[uninstall] cancelled — nothing removed.\n');
|
|
721
721
|
return 0;
|
|
722
722
|
}
|
|
723
723
|
}
|
|
724
724
|
const memory = args.archiveMemory ? 'archive' : 'keep';
|
|
725
725
|
const s = installUninstall(root, host, scope, memory);
|
|
726
726
|
if (!(s.ok ?? true)) {
|
|
727
|
-
|
|
727
|
+
printErr(`[uninstall] failed: ${s.error || 'unknown error'}\n`);
|
|
728
728
|
return 1;
|
|
729
729
|
}
|
|
730
730
|
const archived = s.archived || [];
|
|
731
731
|
const mem = archived.length ? `archived -> ${archived.join(', ')}` : 'kept in place';
|
|
732
732
|
const cfgfile = host === 'opencode' ? 'opencode.json' : 'settings.json';
|
|
733
733
|
if (s.incomplete && s.incomplete.length) {
|
|
734
|
-
|
|
734
|
+
printOut(`[uninstall] INCOMPLETE — removed ${s.removed} file(s), but `
|
|
735
735
|
+ `${s.incomplete.length} item(s) survived (see the WARN above); `
|
|
736
736
|
+ 'the install marker was kept — retry `geneseed uninstall` once they\'re '
|
|
737
737
|
+ `removable. ${cfgfile} updated where needed; memory/notebook ${mem}.\n`);
|
|
738
738
|
} else {
|
|
739
|
-
|
|
739
|
+
printOut(`[uninstall] done — removed ${s.removed} file(s); ${cfgfile} updated where `
|
|
740
740
|
+ `needed; memory/notebook ${mem}. Start a new session to apply.\n`);
|
|
741
741
|
}
|
|
742
742
|
if (scope === 'global') printSurvivingProjectInventory(root);
|
|
@@ -835,8 +835,8 @@ function installMoveList(root, kind) {
|
|
|
835
835
|
} else {
|
|
836
836
|
rels = ownedOf(claudeReadManifest(root)).filter((r) => r !== VERSION_MARKER);
|
|
837
837
|
}
|
|
838
|
-
const rroot =
|
|
839
|
-
return rels.filter((r) => r && within(
|
|
838
|
+
const rroot = resolvePath(root);
|
|
839
|
+
return rels.filter((r) => r && within(resolvePath(path.resolve(rroot, r)), rroot));
|
|
840
840
|
}
|
|
841
841
|
|
|
842
842
|
/**
|
|
@@ -862,9 +862,9 @@ export function installReaddEntry(target, entry) {
|
|
|
862
862
|
const [cfg, hadComments] = readJsonc(raw);
|
|
863
863
|
if (typeof cfg !== 'object' || cfg === null || Array.isArray(cfg)) return false;
|
|
864
864
|
const instr = Array.isArray(cfg.instructions) ? cfg.instructions : [];
|
|
865
|
-
if (
|
|
865
|
+
if (indexOfDeepEqual(instr, entry) >= 0) return false;
|
|
866
866
|
if (path.extname(target) === '.jsonc' && hadComments) {
|
|
867
|
-
|
|
867
|
+
printOut(`[activate] ${path.basename(target)} has comments — not rewriting it. Add this `
|
|
868
868
|
+ `to its "instructions" by hand: ${jsonDumps(entry)}\n`);
|
|
869
869
|
return false;
|
|
870
870
|
}
|
|
@@ -1032,10 +1032,14 @@ function remergeClaudeHooks(cfg, root = cfg) {
|
|
|
1032
1032
|
// config dir (`.github/copilot-instructions.md`), and on a global install the two are the
|
|
1033
1033
|
// same directory anyway. Both silent ⇒ `null` ⇒ the gate stays.
|
|
1034
1034
|
const doctrines = doctrinesOfDir(root) ?? doctrinesOfDir(cfg);
|
|
1035
|
+
// Same two-carrier read for the second axis, and its own default: an absent line means
|
|
1036
|
+
// NOTHING excluded, so a reactivate can only ever restore a gate, never remove one.
|
|
1037
|
+
const excluded = excludedRulesOfDir(root).length ? excludedRulesOfDir(root)
|
|
1038
|
+
: excludedRulesOfDir(cfg);
|
|
1035
1039
|
const [, claims] = mergeClaudeSettings(settingsFile(cfg, managed), 'global',
|
|
1036
|
-
managed.settings_hooks ?? null, hookRunnerEntry(), doctrines);
|
|
1040
|
+
managed.settings_hooks ?? null, hookRunnerEntry(), doctrines, excluded);
|
|
1037
1041
|
// `and data` — a manifest that did not parse is not one to write back.
|
|
1038
|
-
if (!
|
|
1042
|
+
if (!deepEquals(claims, managed.settings_hooks ?? null) && Object.keys(data).length > 0) {
|
|
1039
1043
|
managed.settings_hooks = claims;
|
|
1040
1044
|
data.managed = managed;
|
|
1041
1045
|
const tmp = path.join(cfg, `${GLOBAL_MANIFEST}.tmp`);
|
|
@@ -1058,9 +1062,9 @@ function claudeDeactivate(root, scope = 'global', host = 'claude') {
|
|
|
1058
1062
|
const man = claudeReadManifest(cfg);
|
|
1059
1063
|
const managed = managedOf(man);
|
|
1060
1064
|
const stash = path.join(cfg, DISABLED_STASH, host);
|
|
1061
|
-
const rroot =
|
|
1065
|
+
const rroot = resolvePath(cfg);
|
|
1062
1066
|
const rels = ownedOf(man).filter((r) => r && r !== VERSION_MARKER
|
|
1063
|
-
&& within(
|
|
1067
|
+
&& within(resolvePath(path.resolve(rroot, r)), rroot));
|
|
1064
1068
|
const done = [];
|
|
1065
1069
|
for (const rel of rels) {
|
|
1066
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
|
|