geneseed 3.1.1 → 3.1.3
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 +123 -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/docs/web/exclusions.md +1 -1
- package/docs/web/footprint.md +1 -1
- package/docs/web/foreman-mode.md +2 -2
- package/docs/web/themes-author.md +1 -1
- package/docs/web-ui.md +41 -29
- 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 +154 -51
- 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 +245 -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/{Activity-D_sYBlW-.js → Activity-B49fE_ug.js} +1 -1
- package/web/dist/assets/{ActivityDetail-BomKcMhy.js → ActivityDetail-abt7hzD2.js} +1 -1
- package/web/dist/assets/{Diff-BSvi1WUg.js → Diff-D3_bVkih.js} +1 -1
- package/web/dist/assets/{Doctor-Cz1s-y1n.js → Doctor-hXp7srsx.js} +1 -1
- package/web/dist/assets/{Graph-CuntzFJ0.js → Graph-DvYMtlvl.js} +1 -1
- package/web/dist/assets/Harness-BIWei7N5.js +1 -0
- package/web/dist/assets/Laws-CxmnRdwt.js +3 -0
- package/web/dist/assets/{Library-DXhTN9ju.js → Library-MjbOkHIW.js} +1 -1
- package/web/dist/assets/{Markdown-BoiZcdUl.js → Markdown-Dx_w0faR.js} +1 -1
- package/web/dist/assets/{Profile-DfCHlZ03.js → Profile-BRFvv2gr.js} +1 -1
- package/web/dist/assets/{Rules-eND7hJv9.js → Rules-CfsV1EVy.js} +1 -1
- package/web/dist/assets/{Skills-CDl9opZO.js → Skills-DArA6IHS.js} +1 -1
- package/web/dist/assets/{StatusBadge-DSDtcArX.js → StatusBadge-C9bZOEZk.js} +1 -1
- package/web/dist/assets/{activity-Dmb0FTAx.js → activity-Cc0n2jwx.js} +1 -1
- package/web/dist/assets/index-Bx3oxZ1o.css +1 -0
- package/web/dist/assets/index-DC3-GlCY.js +1 -0
- package/web/dist/assets/index-DP035wPa.js +12 -0
- package/web/dist/assets/{index-Dg58ScBe.js → index-dHi-7M-F.js} +1 -1
- package/web/dist/index.html +2 -2
- package/web/src/pages/Laws.jsx +5 -19
- 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/About-DyK7yRy7.js +0 -1
- package/web/dist/assets/Harnesses-DgRyOviS.js +0 -1
- package/web/dist/assets/Laws-CWk3Mpri.js +0 -3
- package/web/dist/assets/Themes-CoAWuIeM.js +0 -2
- package/web/dist/assets/index-B-mXquBZ.css +0 -1
- package/web/dist/assets/index-CH_Z725C.js +0 -12
- package/web/dist/assets/index-jrY4m9jP.js +0 -1
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The OpenCode emits — a project's `.opencode/` and the global config directory.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `emit.mjs` beside `emit-claude.mjs`, over the shared writers in
|
|
5
|
+
* `emit-common.mjs`. The two are siblings on purpose: an emit that is right for one host and
|
|
6
|
+
* wrong for the other is the defect these have historically carried, and it reads far more
|
|
7
|
+
* plainly as two files with the same shape than as two sections of one.
|
|
8
|
+
*/
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { VERSION_MARKER } from '../hosts/hosts.mjs';
|
|
11
|
+
import { loadAgentOverrides, writeNativeLayer } from '../hosts/native.mjs';
|
|
12
|
+
import {
|
|
13
|
+
copyPlugins, copyWorkflows, ensureAgentOverridesStub, writeColorThemes, writeCommandLayer,
|
|
14
|
+
writePonytailCommand, writePrimaryAgent, writeTheme,
|
|
15
|
+
} from '../hosts/opencode.mjs';
|
|
16
|
+
import { mergeOpencodeJson } from '../hosts/settings.mjs';
|
|
17
|
+
import { readText, writeText } from '../lib/fs.mjs';
|
|
18
|
+
import { assertSourceComplete, build, phaseLog } from './bundle.mjs';
|
|
19
|
+
import {
|
|
20
|
+
globalMemory, globalNotebook, isFile, relPosix, shipLeanLaws, stripCapabilityLinks,
|
|
21
|
+
} from './emit-common.mjs';
|
|
22
|
+
import { renderAll } from './render.mjs';
|
|
23
|
+
import {
|
|
24
|
+
ensureExcludesStub, ensureMemoryIndex, ensureNotebookIndex, ensureProfileStub, ensureRulesStub,
|
|
25
|
+
ensureWikiStub,
|
|
26
|
+
} from './stubs.mjs';
|
|
27
|
+
import { writeVersion } from './version.mjs';
|
|
28
|
+
import { mkdirSync } from 'node:fs';
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// emit_opencode — the RENDER stage only
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* RENDER and WIRE for `_build_emit.emit_opencode` — everything through the
|
|
36
|
+
* `mergeOpencodeJson` call at the bottom. PRUNE and MANIFEST are the driver's, in
|
|
37
|
+
* `bin/build-driver.mjs`.
|
|
38
|
+
*
|
|
39
|
+
* `oldOwned` and `manifestExisted` arrive in the job rather than being read here. The
|
|
40
|
+
* manifest is the DRIVER's stage: it reads the file, hands the prior claim in, and prunes
|
|
41
|
+
* and rewrites it after this returns. One owner of the file, and this function is not it.
|
|
42
|
+
*
|
|
43
|
+
* `nativeCatalog` likewise arrives decided. It is `HOSTS['opencode']['native_catalog']` from
|
|
44
|
+
* `js/hosts/hosts.mjs`, resolved by the driver before the call; passing the decision rather
|
|
45
|
+
* than re-reading the registry keeps one owner of it.
|
|
46
|
+
*/
|
|
47
|
+
export function emitOpencodeRender(cfg, job) {
|
|
48
|
+
phaseLog('RENDER');
|
|
49
|
+
const {
|
|
50
|
+
theme: _theme, out, root, footprint, nativeCatalog, oldOwned, manifestExisted, agentPath,
|
|
51
|
+
} = job;
|
|
52
|
+
const oc = path.join(root, '.opencode');
|
|
53
|
+
|
|
54
|
+
build(cfg, _theme, out, { footprint, nativeCatalog });
|
|
55
|
+
|
|
56
|
+
// OpenCode loads agents/skills natively, so strip AGENT.md's per-row spec links to
|
|
57
|
+
// plain names (the portable build keeps them). A deliberate de-link, not a fix.
|
|
58
|
+
const agentMd = path.join(out, 'AGENT.md');
|
|
59
|
+
if (isFile(agentMd)) writeText(agentMd, stripCapabilityLinks(cfg, readText(agentMd)));
|
|
60
|
+
|
|
61
|
+
const owned = [];
|
|
62
|
+
const { theme, items } = renderAll(cfg, _theme);
|
|
63
|
+
|
|
64
|
+
ensureAgentOverridesStub(cfg, out);
|
|
65
|
+
const overrides = loadAgentOverrides(out);
|
|
66
|
+
|
|
67
|
+
const { nAgents, nSkills, written } = writeNativeLayer(
|
|
68
|
+
items, path.join(oc, 'agents'), path.join(oc, 'skills'), overrides,
|
|
69
|
+
{ host: 'opencode', oldOwned, cfg: oc, manifestExisted, theme, src: cfg.src });
|
|
70
|
+
for (const p of written) owned.push(relPosix(oc, p));
|
|
71
|
+
|
|
72
|
+
const primary = writePrimaryAgent(cfg, path.join(oc, 'agents'), overrides);
|
|
73
|
+
if (primary) owned.push(relPosix(oc, primary));
|
|
74
|
+
|
|
75
|
+
const commands = writeCommandLayer(cfg, items, path.join(oc, 'command'));
|
|
76
|
+
commands.push(writePonytailCommand(path.join(oc, 'command'))); // always-on /ponytail
|
|
77
|
+
for (const p of commands) owned.push(relPosix(oc, p));
|
|
78
|
+
|
|
79
|
+
owned.push(relPosix(oc, writeTheme(path.join(oc, 'themes'), _theme, theme)));
|
|
80
|
+
for (const p of writeColorThemes(cfg, path.join(oc, 'themes'))) owned.push(relPosix(oc, p));
|
|
81
|
+
|
|
82
|
+
const nPlugins = copyPlugins(cfg, path.join(oc, 'plugins'), owned);
|
|
83
|
+
const nWorkflows = copyWorkflows(cfg, path.join(oc, 'workflows'), owned);
|
|
84
|
+
|
|
85
|
+
// WIRE — the one file of this emit the user co-owns. `agentPath` arrives decided
|
|
86
|
+
// (`_rel_under` is the Python side by design), and only the BASENAME is consumed
|
|
87
|
+
// downstream: `opencode.json`, or the `.jsonc` sibling when that is what is on disk.
|
|
88
|
+
phaseLog('WIRE');
|
|
89
|
+
const cfgName = path.basename(mergeOpencodeJson(path.join(root, 'opencode.json'), agentPath,
|
|
90
|
+
cfg.doctrines, cfg.excludeRules));
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
owned,
|
|
94
|
+
stats: {
|
|
95
|
+
nAgents, nSkills, nPlugins, nWorkflows, nCommands: commands.length, primary: !!primary,
|
|
96
|
+
},
|
|
97
|
+
cfgName,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// emit_opencode_global — the ninth, and the last emit to cross
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* BOTH halves of `_build_global.emit_opencode_global`, the "everything global, zero
|
|
107
|
+
* per-repo" OpenCode deployment. PRUNE and MANIFEST are the driver's; there is no VERIFY,
|
|
108
|
+
* because this emit writes no settings file.
|
|
109
|
+
*
|
|
110
|
+
* ASSEMBLY, NOT TRANSLATION. Every unit it needs was already here — 58 of the 65 functions
|
|
111
|
+
* in its Python closure, counted with `ast` rather than by name-mapping, which is how the
|
|
112
|
+
* scout that produced this list first reported `_posture_body` and `_mode_body` as missing
|
|
113
|
+
* when both are `registerBody` in `js/build/render.mjs`. What is new is the assembly and the
|
|
114
|
+
* three values that had to be decided on the Python side.
|
|
115
|
+
*
|
|
116
|
+
* `cfgDir` IS THE FIRST OF THEM AND THE REASON THE OTHERS WERE ASKED ABOUT.
|
|
117
|
+
* `_build_core._opencode_config_dir` is an `_OWNED` name — the suite and `harness diff`
|
|
118
|
+
* both redirect it at a sandbox — so resolving it here would render 135 files into the
|
|
119
|
+
* developer's real `~/.config/opencode`. Python's entry point resolves it once, before
|
|
120
|
+
* anything else, and sends the answer. Fourth phase running (`STRUCTURE` at P2d,
|
|
121
|
+
* `capabilityLinkRe` at P2e, `_PREAMBLE_CONFIG_DIR` at P3b): send the decision, never the
|
|
122
|
+
* resolver.
|
|
123
|
+
*
|
|
124
|
+
* `out` IS NOT THE TARGET, and with the default nobody can tell. `emit_opencode_global`
|
|
125
|
+
* writes into `<cfg>` and takes `out` ONLY as the legacy bundle to migrate a memory or
|
|
126
|
+
* notebook store from; the CLI's `--out` defaults to a directory this emit never writes a
|
|
127
|
+
* byte into. A port that derived the legacy source from `<cfg>` would pass every cell that
|
|
128
|
+
* leaves them coincident — the `claude/bundle-in-subfolder` finding at a third call site.
|
|
129
|
+
*
|
|
130
|
+
* `agentPath` travels for the same reason `emitOpencodeRender`'s does: it is WIRE's one
|
|
131
|
+
* input, and `Path.as_posix()` is the Python side's spelling of it.
|
|
132
|
+
*/
|
|
133
|
+
export function emitOpencodeGlobalRender(cfg, job) {
|
|
134
|
+
phaseLog('RENDER');
|
|
135
|
+
const {
|
|
136
|
+
theme: themeName, cfgDir, out, footprint, nativeCatalog, oldOwned, agentPath,
|
|
137
|
+
} = job;
|
|
138
|
+
|
|
139
|
+
// No `lawsPrefix`: the standalone laws dir sits beside AGENT.md in <cfg>, so the lean
|
|
140
|
+
// pointer's relative `laws/universal.md` resolves with no prefix.
|
|
141
|
+
const { theme, items } = renderAll(cfg, themeName, { footprint, nativeCatalog });
|
|
142
|
+
assertSourceComplete(cfg, 'opencode-global');
|
|
143
|
+
mkdirSync(cfgDir, { recursive: true });
|
|
144
|
+
|
|
145
|
+
const owned = [];
|
|
146
|
+
const agentText = items.find((i) => i.rel === 'AGENT.md' && i.text !== null)?.text ?? null;
|
|
147
|
+
if (agentText !== null) {
|
|
148
|
+
// OpenCode loads agents/skills natively, so drop AGENT.md's per-row spec links to
|
|
149
|
+
// plain names. Memory links stay RELATIVE: in the global layout AGENT.md and the
|
|
150
|
+
// store are siblings, so `memory/` resolves from AGENT.md's own location and stays
|
|
151
|
+
// hermetic — no absolute path a doctor check would flag as an escape.
|
|
152
|
+
writeText(path.join(cfgDir, 'AGENT.md'), stripCapabilityLinks(cfg, agentText));
|
|
153
|
+
owned.push('AGENT.md');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
ensureAgentOverridesStub(cfg, cfgDir);
|
|
157
|
+
const overrides = loadAgentOverrides(cfgDir);
|
|
158
|
+
|
|
159
|
+
// `manifestExisted` is deliberately not passed — the Python does not pass it either, so
|
|
160
|
+
// the pre-manifest header line is unreachable from this emit on both sides.
|
|
161
|
+
const { nAgents, nSkills, written } = writeNativeLayer(
|
|
162
|
+
items, path.join(cfgDir, 'agents'), path.join(cfgDir, 'skills'), overrides,
|
|
163
|
+
{ host: 'opencode', oldOwned, cfg: cfgDir, theme, src: cfg.src });
|
|
164
|
+
for (const p of written) owned.push(relPosix(cfgDir, p));
|
|
165
|
+
|
|
166
|
+
const primary = writePrimaryAgent(cfg, path.join(cfgDir, 'agents'), overrides);
|
|
167
|
+
if (primary) owned.push(relPosix(cfgDir, primary));
|
|
168
|
+
|
|
169
|
+
const commands = writeCommandLayer(cfg, items, path.join(cfgDir, 'command'));
|
|
170
|
+
commands.push(writePonytailCommand(path.join(cfgDir, 'command'))); // always-on /ponytail
|
|
171
|
+
for (const p of commands) owned.push(relPosix(cfgDir, p));
|
|
172
|
+
|
|
173
|
+
owned.push(relPosix(cfgDir, writeTheme(path.join(cfgDir, 'themes'), themeName, theme)));
|
|
174
|
+
for (const p of writeColorThemes(cfg, path.join(cfgDir, 'themes'))) {
|
|
175
|
+
owned.push(relPosix(cfgDir, p));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const nPlugins = copyPlugins(cfg, path.join(cfgDir, 'plugins'), owned);
|
|
179
|
+
const nWorkflows = copyWorkflows(cfg, path.join(cfgDir, 'workflows'), owned);
|
|
180
|
+
|
|
181
|
+
const memStatus = globalMemory(cfgDir, items, out, cfg.src);
|
|
182
|
+
ensureMemoryIndex(path.join(cfgDir, 'memory'));
|
|
183
|
+
const nbStatus = globalNotebook(cfgDir, items, out, cfg.src);
|
|
184
|
+
ensureNotebookIndex(path.join(cfgDir, 'notebook'));
|
|
185
|
+
ensureWikiStub(cfgDir);
|
|
186
|
+
ensureRulesStub(cfgDir);
|
|
187
|
+
ensureProfileStub(cfgDir);
|
|
188
|
+
ensureExcludesStub(cfgDir);
|
|
189
|
+
|
|
190
|
+
writeVersion(cfg, cfgDir);
|
|
191
|
+
owned.push(VERSION_MARKER);
|
|
192
|
+
|
|
193
|
+
shipLeanLaws(items, theme, cfgDir, owned, footprint);
|
|
194
|
+
|
|
195
|
+
// WIRE — the one file of this emit the user co-owns, and the last render is now behind
|
|
196
|
+
// us. Inlined the way `emitOpencodeRender`'s is (one call, the same merge, the same file
|
|
197
|
+
// name). Nothing walks the SOURCE for that split now: `tests/unit/emit_phase_order.test.mjs`
|
|
198
|
+
// reads the runtime `GENESEED_PHASE_LOG` trace instead, so the `phaseLog('WIRE')` below is
|
|
199
|
+
// what the gate sees — and deleting it is mutation M30.
|
|
200
|
+
phaseLog('WIRE');
|
|
201
|
+
const cfgName = path.basename(mergeOpencodeJson(path.join(cfgDir, 'opencode.json'),
|
|
202
|
+
agentPath, cfg.doctrines, cfg.excludeRules));
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
owned,
|
|
206
|
+
stats: {
|
|
207
|
+
nAgents, nSkills, nPlugins, nWorkflows, nCommands: commands.length, primary: !!primary,
|
|
208
|
+
},
|
|
209
|
+
memStatus,
|
|
210
|
+
nbStatus,
|
|
211
|
+
cfgName,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The three cheap generator verbs — `build`, `prompt` and `theme`.
|
|
3
3
|
*
|
|
4
4
|
* These are the harness's GENERATOR FACE: the subcommands whose whole job is to drive the
|
|
5
|
-
* thing `bin/
|
|
5
|
+
* thing `bin/build-driver.mjs` already is. That is why they are the cheapest slice left and why
|
|
6
6
|
* they are the last one that can be measured by a closure walk alone — `cmd_build`'s body is
|
|
7
7
|
* three lines because the work is a `subprocess`, and a name walk cannot see through one.
|
|
8
8
|
*
|
|
9
9
|
* WHY `build` IMPORTS THE DRIVER RATHER THAN SPAWNING IT. The Python is
|
|
10
10
|
* `run([sys.executable, BUILD, *extra]).returncode` — a passthrough to a second process.
|
|
11
|
-
* Reproducing that shape here would mean spawning `node bin/
|
|
11
|
+
* Reproducing that shape here would mean spawning `node bin/build-driver.mjs`, and
|
|
12
12
|
* `test_the_cli_reaches_no_child_process_module` forbids it: the ban is transitive over this
|
|
13
13
|
* entry's imports, and P5c's argument for keeping it is that `web`, `upgrade` and `setup`
|
|
14
14
|
* will genuinely need to spawn, so the ban should outlive them rather than be dismantled by
|
|
15
15
|
* a verb that does not. It is also the wrong shape on its own terms. Python needs a second
|
|
16
|
-
* process because `build.py` is a different PROGRAM; `bin/
|
|
16
|
+
* process because `build.py` is a different PROGRAM; `bin/build-driver.mjs` is a module in this
|
|
17
17
|
* one, and a Node CLI spawning a Node driver to do work it could do in-process is exactly
|
|
18
18
|
* the passthrough the whole port has been removing. So the driver's `main` is exported and
|
|
19
19
|
* called directly, and the observable contract — the tree, the streams, the exit code — is
|
|
@@ -32,19 +32,18 @@
|
|
|
32
32
|
import { existsSync, mkdirSync, statSync } from 'node:fs';
|
|
33
33
|
import path from 'node:path';
|
|
34
34
|
|
|
35
|
-
import { main as driverMain } from '
|
|
36
|
-
import { makeCfg, PACK_ORDER } from './
|
|
37
|
-
import { opencodeConfigDir,
|
|
35
|
+
import { main as driverMain } from '../../bin/build-driver.mjs';
|
|
36
|
+
import { makeCfg, PACK_ORDER } from './source.mjs';
|
|
37
|
+
import { opencodeConfigDir, resolvePath } from '../hosts/hosts.mjs';
|
|
38
38
|
import {
|
|
39
39
|
EMIT_HOST_SCOPE, defaultMode, defaultPosture, defaultTheme, doctrinesForBuild,
|
|
40
40
|
excludedRulesOfDir, footprintOfDir, installState, installTargets, modeOfDir, postureOfDir,
|
|
41
41
|
readMaybe, themeOfDir,
|
|
42
|
-
} from '
|
|
43
|
-
import { colorThemeFiles, colorThemeJson, PALETTE_ROLES } from '
|
|
42
|
+
} from '../hosts/installs.mjs';
|
|
43
|
+
import { colorThemeFiles, colorThemeJson, PALETTE_ROLES } from '../hosts/opencode.mjs';
|
|
44
44
|
import { renderAll } from './render.mjs';
|
|
45
|
-
import {
|
|
46
|
-
|
|
47
|
-
} from './lib/fs.mjs';
|
|
45
|
+
import { printOut, printErr, readText, writeText } from '../lib/fs.mjs';
|
|
46
|
+
import { jsonDumpsIndent, parseJson, formatRepr } from '../lib/json.mjs';
|
|
48
47
|
|
|
49
48
|
/** `_harness_build._HEX_RE`. Anchored at BOTH ends — `#123` and `#1122334` both fail. */
|
|
50
49
|
const HEX_RE = /^#[0-9a-fA-F]{6}$/;
|
|
@@ -52,18 +51,18 @@ const HEX_RE = /^#[0-9a-fA-F]{6}$/;
|
|
|
52
51
|
/**
|
|
53
52
|
* `raise SystemExit(msg)` — the message on stderr, exit 1.
|
|
54
53
|
*
|
|
55
|
-
* The convention `js/
|
|
54
|
+
* The convention `js/build/bundle.mjs`'s `assertSourceComplete` set and `bin/geneseed-cli.mjs`
|
|
56
55
|
* honours: write at the raise site, mark the throw as a DELIBERATE refusal with `exitCode`,
|
|
57
56
|
* and let anything unmarked keep its stack. Python prints a traceback for an unhandled
|
|
58
57
|
* exception, so dressing a crash as a one-liner would hide a bug.
|
|
59
58
|
*/
|
|
60
59
|
function sysExit(msg) {
|
|
61
|
-
// `
|
|
60
|
+
// `printErr`, NOT a local newline translation. The first draft inlined the `\n` ->
|
|
62
61
|
// `\r\n` rule here and every cell passed, because `harness_golden` reads stderr through
|
|
63
62
|
// `subprocess`'s universal-newline decode and cannot see the difference — the same
|
|
64
63
|
// transport hole P5b measured. A second copy of a translation is exactly what P5d's
|
|
65
|
-
// `pyStrPath`/`
|
|
66
|
-
|
|
64
|
+
// `pyStrPath`/`toPlatformPath` split was, and it is ungated in precisely the same way.
|
|
65
|
+
printErr(`${msg}\n`);
|
|
67
66
|
const e = new Error(msg);
|
|
68
67
|
e.exitCode = 1;
|
|
69
68
|
throw e;
|
|
@@ -188,7 +187,7 @@ export const DEFAULT_EMIT = new Map([
|
|
|
188
187
|
export function cmdRebuildAll() {
|
|
189
188
|
const targets = installTargets().filter(([h, s, r]) => installState(r, h, s) === 'active');
|
|
190
189
|
if (!targets.length) {
|
|
191
|
-
|
|
190
|
+
printOut('[rebuild-all] no active installs detected.\n');
|
|
192
191
|
return 0;
|
|
193
192
|
}
|
|
194
193
|
const failures = [];
|
|
@@ -218,20 +217,20 @@ export function cmdRebuildAll() {
|
|
|
218
217
|
const argv = setupBuildArgs(theme, emit, out, out, footprint, posture, mode, doctrines,
|
|
219
218
|
PACK_ORDER, excludeRules);
|
|
220
219
|
const label = `${host}:${scope} (${root})`;
|
|
221
|
-
|
|
220
|
+
printOut(`[rebuild-all] ${label}: theme=${theme} emit=${emit} footprint=${footprint} `
|
|
222
221
|
+ `posture=${posture} mode=${mode}\n`);
|
|
223
222
|
const rc = driverMain(argv);
|
|
224
223
|
if (rc !== 0) {
|
|
225
224
|
failures.push(label);
|
|
226
|
-
|
|
225
|
+
printErr(`[rebuild-all] FAILED ${label} (exit ${rc})\n`);
|
|
227
226
|
}
|
|
228
227
|
}
|
|
229
228
|
if (failures.length) {
|
|
230
|
-
|
|
229
|
+
printErr(`[rebuild-all] ${failures.length}/${targets.length} install(s) failed: `
|
|
231
230
|
+ `${failures.join(', ')}\n`);
|
|
232
231
|
return 1;
|
|
233
232
|
}
|
|
234
|
-
|
|
233
|
+
printOut(`[rebuild-all] rebuilt ${targets.length} install(s).\n`);
|
|
235
234
|
return 0;
|
|
236
235
|
}
|
|
237
236
|
|
|
@@ -311,9 +310,9 @@ export function cmdPrompt(args) {
|
|
|
311
310
|
if (dest) mkdirSync(dest, { recursive: true });
|
|
312
311
|
writeText(args.out, text);
|
|
313
312
|
// The RAW argument, not the resolved path — `f"[prompt] wrote {args.out}"`.
|
|
314
|
-
|
|
313
|
+
printOut(`[prompt] wrote ${args.out} (${themeName})\n`);
|
|
315
314
|
} else {
|
|
316
|
-
|
|
315
|
+
printOut(text);
|
|
317
316
|
}
|
|
318
317
|
return 0;
|
|
319
318
|
}
|
|
@@ -324,8 +323,8 @@ export function cmdPrompt(args) {
|
|
|
324
323
|
|
|
325
324
|
/** `_harness_build._resolve_themes_dir` — explicit `--dir`, else a repo `.opencode/`, else global. */
|
|
326
325
|
export function resolveThemesDir(args) {
|
|
327
|
-
// `Path(args.dir).expanduser().resolve()` — `
|
|
328
|
-
if (args.dir) return
|
|
326
|
+
// `Path(args.dir).expanduser().resolve()` — `resolvePath` already expands a leading `~`.
|
|
327
|
+
if (args.dir) return resolvePath(args.dir);
|
|
329
328
|
const repo = path.join(process.cwd(), '.opencode');
|
|
330
329
|
if (!args.globalDir && isDir(repo)) return path.join(repo, 'themes');
|
|
331
330
|
return path.join(opencodeConfigDir(), 'themes');
|
|
@@ -383,7 +382,7 @@ export function loadUserPalette(args, cfg) {
|
|
|
383
382
|
}
|
|
384
383
|
// `f"{r}={pal[r]!r}"` — Python's repr, so a string is single-quoted and an int is bare.
|
|
385
384
|
const bad = [...pal].filter(([, v]) => !(typeof v === 'string' && HEX_RE.test(v)))
|
|
386
|
-
.map(([r, v]) => `${r}=${
|
|
385
|
+
.map(([r, v]) => `${r}=${formatRepr(v)}`);
|
|
387
386
|
if (bad.length) sysExit(`[theme] non-#rrggbb value(s): ${bad.join(', ')}`);
|
|
388
387
|
return pal;
|
|
389
388
|
}
|
|
@@ -421,8 +420,8 @@ export function cmdTheme(args) {
|
|
|
421
420
|
writeText(dest, `${jsonDumpsIndent(colorThemeJson(Object.fromEntries(pal), transparent))}\n`);
|
|
422
421
|
written.push(dest);
|
|
423
422
|
}
|
|
424
|
-
|
|
425
|
-
|
|
423
|
+
printOut(`[theme] wrote ${written.map((p) => path.basename(p)).join(', ')} to ${destDir}\n`);
|
|
424
|
+
printOut(`[theme] select in OpenCode with: /theme ${full}`
|
|
426
425
|
+ (args.solidOnly ? '' : ` (or /theme ${full}-transparent)`) + '\n');
|
|
427
426
|
return 0;
|
|
428
427
|
}
|
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
* RENDER* -> WIRE* -> PRUNE -> MANIFEST -> VERIFY (see `_build_global._emit_claude_core`
|
|
11
11
|
* and tests/test_emit_phase_order.py). RENDER produces files Geneseed owns wholesale;
|
|
12
12
|
* WIRE reconciles files the user co-owns (settings.json, opencode.json, the CLAUDE.md
|
|
13
|
-
* managed block) and
|
|
13
|
+
* managed block) and lives in `js/hosts/settings.mjs`, driven from the two emit modules.
|
|
14
|
+
* THIS module is only RENDER
|
|
14
15
|
* because it is the only stage whose output is a pure function of the source tree.
|
|
15
16
|
*
|
|
16
17
|
* PARITY IS THE CONTRACT, NOT THE INTENT. Every function here has a named Python
|
|
17
|
-
* counterpart and
|
|
18
|
+
* counterpart and had to produce byte-identical output for it. The reference is gone, so
|
|
19
|
+
* what holds the line is `tests/golden.mjs --against`, replaying recorded emit cells;
|
|
18
20
|
* drives both over every theme x footprint x posture x mode and byte-compares the
|
|
19
21
|
* written trees. Where Python and JS semantics differ silently — path sort order,
|
|
20
22
|
* `.suffix`, `splitlines`, universal newlines — the difference is spelled out at the
|
|
@@ -28,9 +30,11 @@
|
|
|
28
30
|
*/
|
|
29
31
|
import { readdirSync, statSync, existsSync } from 'node:fs';
|
|
30
32
|
import path from 'node:path';
|
|
31
|
-
import { readText
|
|
32
|
-
|
|
33
|
-
import {
|
|
33
|
+
import { readText } from '../lib/fs.mjs';
|
|
34
|
+
import { parseJson } from '../lib/json.mjs';
|
|
35
|
+
import { normcase, comparePaths } from '../lib/paths.mjs';
|
|
36
|
+
// `js/build/source.mjs` imports nothing but node builtins, so this direction cannot cycle.
|
|
37
|
+
import { PACK_ORDER } from './source.mjs';
|
|
34
38
|
|
|
35
39
|
/** Document STRUCTURE is theme-INDEPENDENT — mirrors `_build_render.STRUCTURE`. */
|
|
36
40
|
export const STRUCTURE = {
|
|
@@ -47,7 +51,7 @@ export const STRUCTURE = {
|
|
|
47
51
|
|
|
48
52
|
/**
|
|
49
53
|
* Source top-level dirs whose OUTPUT name is themed. Mirrors `SRC_DIR_TOKENS`.
|
|
50
|
-
* Exported because `js/
|
|
54
|
+
* Exported because `js/build/bundle.mjs`'s `build` resolves the same tokens to decide which
|
|
51
55
|
* dirs it owns and wipes — one owner, so the two cannot disagree about a folder name.
|
|
52
56
|
*/
|
|
53
57
|
export const SRC_DIR_TOKENS = {
|
|
@@ -101,7 +105,7 @@ export function loadTheme(cfg, name) {
|
|
|
101
105
|
// `sys.exit(<str>)` — the message on stderr, exit 1 — spelled the way
|
|
102
106
|
// `assertSourceComplete` spells it: written at the raise site, and the throw carries
|
|
103
107
|
// only the marker. ⚠ It was once described as an unreachable nicety under
|
|
104
|
-
// `bin/
|
|
108
|
+
// `bin/build-driver.mjs`, on the grounds that `--theme` is validated against `choices` before
|
|
105
109
|
// anything renders — it is not: `--theme` takes a value and has no `choices` entry, so THIS
|
|
106
110
|
// is the refusal, on every path. It is also reachable from `geneseed status`, which takes
|
|
107
111
|
// its theme from a `.geneseed-theme`
|
|
@@ -253,7 +257,7 @@ function registerBody(cfg, theme, dir, selected, fallback) {
|
|
|
253
257
|
* entry point and an emit that quietly dropped a pack the user asked for is exactly the
|
|
254
258
|
* failure the `Active packs:` marker would then attest to.
|
|
255
259
|
*
|
|
256
|
-
* Both throw with `exitCode`, the marker `bin/
|
|
260
|
+
* Both throw with `exitCode`, the marker `bin/build-driver.mjs`'s `main` turns into a status.
|
|
257
261
|
*/
|
|
258
262
|
function activeDoctrines(cfg) {
|
|
259
263
|
const dir = path.join(cfg.src, 'doctrines');
|
|
@@ -265,7 +269,7 @@ function activeDoctrines(cfg) {
|
|
|
265
269
|
const stray = onDisk.filter((n) => !PACK_ORDER.includes(n));
|
|
266
270
|
if (stray.length) {
|
|
267
271
|
const e = new Error(`doctrine pack file(s) ${stray.join(', ')} exist under src/doctrines/ `
|
|
268
|
-
+ 'but are missing from PACK_ORDER (js/
|
|
272
|
+
+ 'but are missing from PACK_ORDER (js/build/source.mjs) — add them there, or they render '
|
|
269
273
|
+ 'into no install at all');
|
|
270
274
|
e.exitCode = 1;
|
|
271
275
|
throw e;
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* The checkout this code is running FROM — `_build_core`'s ROOT and the paths under it,
|
|
3
3
|
* plus the `cfg` every render needs.
|
|
4
4
|
*
|
|
5
|
-
* The inverse of `js/hosts.mjs`. That module answers "where does the output go"; this one
|
|
6
|
-
* answers "where is the source". Both were `bin/
|
|
5
|
+
* The inverse of `js/hosts/hosts.mjs`. That module answers "where does the output go"; this one
|
|
6
|
+
* answers "where is the source". Both were `bin/build-driver.mjs`'s, and both leave it for the
|
|
7
7
|
* same arithmetic reason: `bin/geneseed-cli.mjs` needs them too, and the value that decides
|
|
8
8
|
* which tree gets rendered is the last thing that should exist twice.
|
|
9
9
|
*
|
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
* `build.render_all(theme)`. So the CLI has to build the same `cfg` the driver builds, and
|
|
13
13
|
* until this move only the driver could.
|
|
14
14
|
*
|
|
15
|
-
* THE TWO ABSENT KEYS ARE STILL ABSENT, and the reason is bin/
|
|
15
|
+
* THE TWO ABSENT KEYS ARE STILL ABSENT, and the reason is bin/build-driver.mjs's, unchanged:
|
|
16
16
|
* `_build_core.js_cfg()` always sends `structure` and `capabilityLinkRe` because the Python
|
|
17
17
|
* originals are module-level names TESTS MUTATE (`_OWNED` membership asked one level out).
|
|
18
18
|
* A Node process has no Python module to mutate, so it sends neither and
|
|
19
|
-
* `js/render.mjs`'s `cfg.structure ?? STRUCTURE` takes its right-hand branch.
|
|
19
|
+
* `js/build/render.mjs`'s `cfg.structure ?? STRUCTURE` takes its right-hand branch.
|
|
20
20
|
*
|
|
21
|
-
* The move is safe for the reason P5c's was: `tests/golden.py` drives `bin/
|
|
21
|
+
* The move is safe for the reason P5c's was: `tests/golden.py` drives `bin/build-driver.mjs`
|
|
22
22
|
* over 259 cells and compares the tree byte-for-byte, and every one of them builds a cfg
|
|
23
23
|
* from these paths. A depth error or a renamed key fails 259 cells, not zero.
|
|
24
24
|
*/
|
|
@@ -37,7 +37,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
37
37
|
* which is what makes them comparable and what makes them unfenceable. See the
|
|
38
38
|
* status/version section of `tests/harness_golden.py`.
|
|
39
39
|
*/
|
|
40
|
-
export const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
40
|
+
export const ROOT = path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))));
|
|
41
41
|
export const SRC = path.join(ROOT, 'src');
|
|
42
42
|
export const CONFIG = path.join(ROOT, 'harness.config.json');
|
|
43
43
|
export const THEMES = path.join(ROOT, 'themes');
|
|
@@ -48,8 +48,8 @@ export const WORKFLOW_SRC = path.join(ROOT, 'adapters', 'opencode', 'workflows')
|
|
|
48
48
|
* `_build_render.posture_names()` / `mode_names()` — discovered, never hardcoded, so a new
|
|
49
49
|
* posture file appears in both CLIs' choices with no code change.
|
|
50
50
|
*
|
|
51
|
-
* P5f moved this here from `bin/
|
|
52
|
-
* `js/installs.mjs` needed the same two lists: `_posture_of_dir` scans a deployed carrier for
|
|
51
|
+
* P5f moved this here from `bin/build-driver.mjs`, which had the only caller until
|
|
52
|
+
* `js/hosts/installs.mjs` needed the same two lists: `_posture_of_dir` scans a deployed carrier for
|
|
53
53
|
* `**<Name>**` and has to be scanning for the SAME names the driver's `--posture` accepts.
|
|
54
54
|
* Two discoveries of one directory is precisely the shape that lets a new posture file be
|
|
55
55
|
* buildable and undetectable.
|
|
@@ -72,7 +72,7 @@ export function discoverNames(dir, first) {
|
|
|
72
72
|
* DELIBERATELY NOT `discoverNames('doctrines', 'craft')`. That helper `.sort()`s, so it
|
|
73
73
|
* answers `craft, ops, process, rigor`; the constitution's order is a reading order —
|
|
74
74
|
* write it (craft), prove it (rigor), run it (ops), ship it (process) — and no sort
|
|
75
|
-
* produces it. Discovery still runs, but as a GATE rather than as the order: `js/render.mjs`
|
|
75
|
+
* produces it. Discovery still runs, but as a GATE rather than as the order: `js/build/render.mjs`
|
|
76
76
|
* refuses to build when a pack file exists under `src/doctrines/` that is missing from this
|
|
77
77
|
* array, so a fifth pack cannot be silently dropped from every install.
|
|
78
78
|
*
|