geneseed 3.1.2 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +125 -0
- package/README.md +2 -2
- package/SHIPPED.md +3 -3
- package/bin/build-driver.mjs +127 -79
- package/bin/geneseed-cli.mjs +42 -84
- package/bin/geneseed-hook.mjs +5 -2
- 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 +1 -10
- package/js/build/emit-claude.mjs +4 -5
- package/js/build/emit-common.mjs +63 -114
- package/js/build/emit-opencode.mjs +74 -66
- package/js/build/generate.mjs +4 -6
- package/js/build/render.mjs +13 -13
- package/js/build/source.mjs +7 -6
- package/js/build/stubs.mjs +25 -33
- package/js/build/themes.mjs +1 -1
- package/js/build/version.mjs +1 -3
- package/js/hosts/hooks.mjs +3 -3
- package/js/hosts/hosts.mjs +2 -4
- package/js/hosts/installs.mjs +35 -28
- package/js/hosts/link.mjs +1 -1
- package/js/hosts/native.mjs +37 -19
- package/js/hosts/opencode.mjs +39 -28
- package/js/hosts/settings.mjs +33 -110
- package/js/inspect/checks-authoring.mjs +5 -4
- package/js/inspect/checks-build.mjs +1 -1
- package/js/inspect/diff.mjs +1 -3
- package/js/inspect/doctor.mjs +59 -44
- package/js/inspect/excludes.mjs +4 -10
- package/js/inspect/inventory.mjs +37 -55
- package/js/inspect/registry.mjs +4 -7
- package/js/inspect/scan.mjs +8 -15
- package/js/inspect/status.mjs +1 -1
- package/js/lib/fs.mjs +34 -0
- package/js/lib/json.mjs +28 -1
- package/js/lib/paths.mjs +0 -10
- package/js/lib/udiff.mjs +11 -16
- package/js/maintain/memory.mjs +2 -4
- package/js/maintain/migrate.mjs +3 -3
- package/js/maintain/uninstall.mjs +90 -78
- package/js/maintain/update.mjs +2 -2
- package/js/ui/anim.mjs +1 -1
- package/js/ui/menu.mjs +1 -1
- package/js/ui/tui.mjs +3 -20
- package/js/web/actions.mjs +230 -217
- package/js/web/activity.mjs +71 -89
- package/js/web/api.mjs +210 -220
- package/js/web/daemon.mjs +92 -110
- package/js/web/docs.mjs +66 -111
- package/js/web/graph.mjs +0 -0
- package/js/web/handler.mjs +78 -97
- package/js/web/jobs.mjs +124 -165
- package/js/web/routes.mjs +79 -136
- package/js/web/server.mjs +47 -82
- package/package.json +1 -1
- package/src/agents/explorer.md +2 -1
- package/src/doctrines/process.md +4 -2
- package/src/laws/universal.md +55 -43
- package/src/skills/clarify.md +1 -1
- package/src/skills/forge-mcp.md +2 -1
- package/src/skills/fresh-eyes.md +1 -1
- package/src/skills/ingest.md +2 -1
- package/src/skills/parallel-agents.md +1 -1
- package/src/skills/ponytail.md +2 -2
- package/src/skills/research.md +4 -2
- package/themes/_TEMPLATE.json +2 -0
- package/themes/biker.json +2 -0
- package/themes/commentator.json +2 -0
- package/themes/cyberpunk.json +2 -0
- package/themes/gamer.json +2 -0
- package/themes/imperial.json +2 -0
- package/themes/joker.json +2 -0
- package/themes/marvin.json +2 -0
- package/themes/mean.json +2 -0
- package/themes/military.json +2 -0
- package/themes/neutral.json +2 -0
- package/themes/pirate.json +2 -0
- package/themes/sports.json +2 -0
- package/themes/verstappen.json +2 -0
- package/themes/wizard.json +2 -0
- package/web/dist/assets/{Activity-z0uXdS7J.js → Activity-BTCG2gMl.js} +1 -1
- package/web/dist/assets/{ActivityDetail-CEj5IJpF.js → ActivityDetail-julsmPDB.js} +1 -1
- package/web/dist/assets/CatalogRow-DqS4e5XM.js +1 -0
- package/web/dist/assets/Diff-CIrGbiqm.js +5 -0
- package/web/dist/assets/Doctor-CpI53oHY.js +1 -0
- package/web/dist/assets/{Graph-BV6a6t2l.js → Graph-CS4tA7YS.js} +1 -1
- package/web/dist/assets/Harness-vRYs11kX.js +1 -0
- package/web/dist/assets/Laws-BjbHJrJt.js +3 -0
- package/web/dist/assets/Library-CjC7BAWN.js +1 -0
- package/web/dist/assets/{marked.esm-JKvyS0Pp.js → Markdown-kgpg9EQM.js} +15 -15
- package/web/dist/assets/Profile-CHr-PPf6.js +1 -0
- package/web/dist/assets/{Rules-DobVPd80.js → Rules-CsC6hfqk.js} +1 -1
- package/web/dist/assets/Skills-BvatJGfe.js +1 -0
- package/web/dist/assets/{StatusBadge-BhJ3HhdI.js → StatusBadge-gRrak0nO.js} +1 -1
- package/web/dist/assets/{activity-D9mfv7pY.js → activity-B0-FXZCj.js} +1 -1
- package/web/dist/assets/index-BnRWQbZb.js +12 -0
- package/web/dist/assets/index-Bx3I7cRW.js +2 -0
- package/web/dist/assets/index-D0f9tJlA.css +1 -0
- package/web/dist/assets/index-DJOhOwwR.js +1 -0
- package/web/dist/index.html +2 -2
- package/web/src/pages/Laws.jsx +46 -89
- package/web/dist/assets/About-CPvIFZGh.js +0 -1
- package/web/dist/assets/Diff-BMR8fPuG.js +0 -5
- package/web/dist/assets/Doctor-Bz5qE4r9.js +0 -1
- package/web/dist/assets/Harnesses-C5Ag5PaQ.js +0 -1
- package/web/dist/assets/Laws-BK07NKw9.js +0 -3
- package/web/dist/assets/Library-CiXN9sC3.js +0 -1
- package/web/dist/assets/Markdown-DG6FjUz-.js +0 -1
- package/web/dist/assets/Profile-B2oSvdxH.js +0 -1
- package/web/dist/assets/Skills-DQPsvsn9.js +0 -1
- package/web/dist/assets/Themes-BP38FBU9.js +0 -2
- package/web/dist/assets/index-BE226Gv6.css +0 -1
- package/web/dist/assets/index-BZ7cPLMI.js +0 -1
- package/web/dist/assets/index-BgIs5eIa.js +0 -12
- package/web/dist/assets/index-DGM5j2hB.js +0 -2
package/js/build/emit-common.mjs
CHANGED
|
@@ -10,9 +10,17 @@
|
|
|
10
10
|
* `emit-claude.mjs` do that; `emit.mjs` renders the bundle they read from.
|
|
11
11
|
*/
|
|
12
12
|
import path from 'node:path';
|
|
13
|
-
import { copyFile, writeText } from '../lib/fs.mjs';
|
|
13
|
+
import { copyFile, writeText, isFile, isDir } from '../lib/fs.mjs';
|
|
14
|
+
import { get, isDict } from '../lib/json.mjs';
|
|
14
15
|
import { SRC_DIR_TOKENS, destRel } from './render.mjs';
|
|
15
|
-
import { mkdirSync, readdirSync,
|
|
16
|
+
import { mkdirSync, readdirSync, cpSync } from 'node:fs';
|
|
17
|
+
|
|
18
|
+
// `isFile`/`isDir`/`get`/`isDict` are owned by `js/lib` now (single owner across `hosts/`,
|
|
19
|
+
// `build/`, `inspect/` and `web/` — layering forbids `build/` reaching into `inspect/scan.mjs`
|
|
20
|
+
// for them, so `js/lib` is the one home both sides can reach). Re-exported here so this
|
|
21
|
+
// module's own existing callers (`emit-claude.mjs`, `emit-opencode.mjs`, `version.mjs`,
|
|
22
|
+
// `bundle.mjs`) keep importing them from `./emit-common.mjs` unchanged.
|
|
23
|
+
export { isFile, isDir, get, isDict };
|
|
16
24
|
|
|
17
25
|
/** `_build_render.SRC_DIRS_MARKER`. */
|
|
18
26
|
export const SRC_DIRS_MARKER = '.geneseed-srcdirs.json';
|
|
@@ -41,13 +49,15 @@ export const OWNED_SRC_DIRS = ['laws', 'agents', 'skills', 'ontology', 'doctrine
|
|
|
41
49
|
const LEAN_FULL_TEXT_DIRS = ['laws', 'ontology', 'doctrines'];
|
|
42
50
|
|
|
43
51
|
/**
|
|
44
|
-
* `_build_core.CAPABILITY_LINK_RE
|
|
52
|
+
* `_build_core.CAPABILITY_LINK_RE`.
|
|
45
53
|
*
|
|
46
|
-
* `cfg.capabilityLinkRe`
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
54
|
+
* NOT OVERRIDABLE. Python's `cfg.capabilityLinkRe` was the driver's seam for tests that
|
|
55
|
+
* monkeypatch `_build_core.CAPABILITY_LINK_RE`; `bin/build-driver.mjs` deliberately never
|
|
56
|
+
* sends one (see its docblock), so the override branch that used to sit in
|
|
57
|
+
* `stripCapabilityLinks` never took its left-hand side and was deleted as dead code in the
|
|
58
|
+
* over-engineering cleanup. `tests/unit/harness.test.mjs`'s planted-fault test drives this
|
|
59
|
+
* exact constant by rewriting the module's source text instead, which is what "no injection
|
|
60
|
+
* point left" means there.
|
|
51
61
|
*/
|
|
52
62
|
const CAPABILITY_LINK_RE =
|
|
53
63
|
/\[([^\]]+)\]\((?:(?!https?:\/\/|\/)[A-Za-z0-9_.-]+\/)*(?:agents|skills)\/[A-Za-z0-9_-]+\.md\)/g;
|
|
@@ -55,59 +65,18 @@ const CAPABILITY_LINK_RE =
|
|
|
55
65
|
/** `_build_render._TMPL_SPEC_RE`. */
|
|
56
66
|
export const TMPL_SPEC_RE = /\{\{DIR_(AGENTS|SKILLS)\}\}\/([A-Za-z0-9_-]+)\.md/g;
|
|
57
67
|
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
// Small filesystem predicates. Python spells these `Path.is_file()` / `is_dir()`, both
|
|
60
|
-
// of which answer False for a missing path rather than raising; `statSync` throws.
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
|
|
63
|
-
export function isFile(p) {
|
|
64
|
-
try { return statSync(p).isFile(); } catch { return false; }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function isDir(p) {
|
|
68
|
-
try { return statSync(p).isDirectory(); } catch { return false; }
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** `dict.get(key)` with Python's semantics — OWN properties only.
|
|
72
|
-
*
|
|
73
|
-
* The same hazard `js/hosts/settings.mjs` and `js/hosts/native.mjs` both spell out: the keys here come
|
|
74
|
-
* out of a manifest the user can edit, and `old['constructor']` hands back
|
|
75
|
-
* `Object.prototype`'s member where Python's `.get` returns None. */
|
|
76
|
-
export function get(obj, key) {
|
|
77
|
-
return Object.prototype.hasOwnProperty.call(obj, key) ? obj[key] : undefined;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function isDict(v) {
|
|
81
|
-
return Boolean(v) && typeof v === 'object' && !Array.isArray(v);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** `Path.rglob("*")` restricted to files, skipping `__pycache__` as Python's callers do. */
|
|
85
|
-
export function rglobFiles(root, out = []) {
|
|
86
|
-
for (const name of readdirSync(root)) {
|
|
87
|
-
if (name === '__pycache__') continue;
|
|
88
|
-
const full = path.join(root, name);
|
|
89
|
-
if (isDir(full)) rglobFiles(full, out);
|
|
90
|
-
else out.push(full);
|
|
91
|
-
}
|
|
92
|
-
return out;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
68
|
/**
|
|
96
|
-
* `Path.rglob("*")` restricted to files,
|
|
69
|
+
* `Path.rglob("*")` restricted to files, skipping `__pycache__` as Python's callers do.
|
|
97
70
|
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* the two runtimes would disagree on silently.
|
|
71
|
+
* `recursive: true, withFileTypes: true` (Node >= 20.1, `parentPath` >= 21.4 — this repo
|
|
72
|
+
* requires >= 22.3) replaces a hand-rolled walk; the order it yields is NOT the walk's
|
|
73
|
+
* order, but `sourceFingerprint`, this function's one caller, re-sorts every result by a
|
|
74
|
+
* plain string key before it is ever observed, so no caller depends on it.
|
|
103
75
|
*/
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
else out.push(full);
|
|
109
|
-
}
|
|
110
|
-
return out;
|
|
76
|
+
export function rglobFiles(root) {
|
|
77
|
+
return readdirSync(root, { recursive: true, withFileTypes: true })
|
|
78
|
+
.filter((d) => d.isFile() && !d.parentPath.split(path.sep).includes('__pycache__'))
|
|
79
|
+
.map((d) => path.join(d.parentPath, d.name));
|
|
111
80
|
}
|
|
112
81
|
|
|
113
82
|
/** `p.relative_to(base).as_posix()`. */
|
|
@@ -123,11 +92,8 @@ export function relPosix(base, p) {
|
|
|
123
92
|
* that difference back by design and a check that did not would report the file stale on
|
|
124
93
|
* every run forever.
|
|
125
94
|
*/
|
|
126
|
-
export function stripCapabilityLinks(
|
|
127
|
-
|
|
128
|
-
? new RegExp(cfg.capabilityLinkRe, 'g')
|
|
129
|
-
: CAPABILITY_LINK_RE;
|
|
130
|
-
return text.replace(re, '$1');
|
|
95
|
+
export function stripCapabilityLinks(text) {
|
|
96
|
+
return text.replace(CAPABILITY_LINK_RE, '$1');
|
|
131
97
|
}
|
|
132
98
|
|
|
133
99
|
// ---------------------------------------------------------------------------
|
|
@@ -135,84 +101,67 @@ export function stripCapabilityLinks(cfg, text) {
|
|
|
135
101
|
// ---------------------------------------------------------------------------
|
|
136
102
|
|
|
137
103
|
/**
|
|
138
|
-
* `_build_global._global_memory` — ensure `<cfg
|
|
139
|
-
* store that already holds something.
|
|
104
|
+
* `_build_global._global_memory`/`_global_notebook` — ensure `<cfg>/<name>` exists, without
|
|
105
|
+
* ever touching a store that already holds something.
|
|
140
106
|
*
|
|
141
107
|
* The three outcomes are the returned status string, which the emit prints, so they are
|
|
142
108
|
* compared through stdout as well as through the tree. Python's `theme` parameter is
|
|
143
|
-
* dropped: the store dir is ALWAYS the classic English
|
|
109
|
+
* dropped for both: the store dir is ALWAYS the classic English name, never themed (the
|
|
144
110
|
* OpenCode config dir uses fixed names), so the argument was never read.
|
|
145
111
|
*
|
|
112
|
+
* THE MIGRATED MESSAGE'S SHAPE TRACKS THE ALIAS COUNT, not a caller-supplied format flag.
|
|
113
|
+
* `memory` is the only store with more than one legacy name — `anamnesis`, an older themed
|
|
114
|
+
* install's — so its status names which one matched: `migrated anamnesis/ -> memory/`.
|
|
115
|
+
* `notebook` has exactly one candidate and its status has never named an arrow:
|
|
116
|
+
* `migrated notebook/`. One alias is nothing a rename could be reporting, so the arrow is
|
|
117
|
+
* dropped rather than printed as a no-op `notebook/ -> notebook/`.
|
|
118
|
+
*
|
|
146
119
|
* The migration branch copies arbitrary USER files out of a legacy bundle, which is the
|
|
147
120
|
* reason this is not a plain translation: everything it touches is the user's, and the
|
|
148
121
|
* only thing keeping it safe is that it runs at all only when the destination is empty.
|
|
149
122
|
*/
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
mkdirSync(memDir, { recursive: true });
|
|
123
|
+
function globalStore(cfgDir, items, legacy, srcRoot, name, aliases = [name]) {
|
|
124
|
+
const dir = path.join(cfgDir, name);
|
|
125
|
+
if (isDir(dir) && readdirSync(dir).length) return `kept ${name}/`;
|
|
126
|
+
mkdirSync(dir, { recursive: true });
|
|
155
127
|
if (legacy) {
|
|
156
|
-
// `dict.fromkeys(
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
// is what keeps the duplicate harmless rather than copying the same dir twice.
|
|
161
|
-
for (const nm of [...new Set([memName, 'memory', 'anamnesis'])]) {
|
|
128
|
+
// `dict.fromkeys(aliases)` — de-duplicated, order kept. The dedupe is what keeps a
|
|
129
|
+
// caller that lists `name` itself among its aliases (as `memory` does) from copying
|
|
130
|
+
// the same dir twice.
|
|
131
|
+
for (const nm of [...new Set(aliases)]) {
|
|
162
132
|
const src = path.join(legacy, nm);
|
|
163
133
|
if (isDir(src) && readdirSync(src).length) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
return `migrated ${nm}/ -> ${
|
|
134
|
+
// `cpSync(..., {preserveTimestamps: true})` — the recursive-copy equivalent of
|
|
135
|
+
// `copyFile` above (byte copy + carried mtime, no `shutil.copy2` mode bits; see
|
|
136
|
+
// that function's docblock in `js/lib/fs.mjs`), replacing a walk-then-copyFile
|
|
137
|
+
// loop that visited every legacy file one at a time.
|
|
138
|
+
cpSync(src, dir, { recursive: true, preserveTimestamps: true });
|
|
139
|
+
return aliases.length > 1 ? `migrated ${nm}/ -> ${name}/` : `migrated ${name}/`;
|
|
170
140
|
}
|
|
171
141
|
}
|
|
172
142
|
}
|
|
173
143
|
for (const { text, src } of items) {
|
|
174
144
|
const sp = relPosix(srcRoot, src).split('/');
|
|
175
|
-
if (sp[0] ===
|
|
145
|
+
if (sp[0] === name && sp.length > 1) {
|
|
176
146
|
// `destRel`, because this seeds from the SOURCE path and not from the item's `rel`
|
|
177
|
-
// (which is themed, and
|
|
178
|
-
// with the on-disk name `gitignore` — see `js/build/render.mjs`'s `destRel`.
|
|
179
|
-
const dest = path.join(
|
|
147
|
+
// (which is themed, and the store dir never is). Without it the store would be
|
|
148
|
+
// seeded with the on-disk name `gitignore` — see `js/build/render.mjs`'s `destRel`.
|
|
149
|
+
const dest = path.join(dir, destRel(path.join(...sp.slice(1))));
|
|
180
150
|
mkdirSync(path.dirname(dest), { recursive: true });
|
|
181
151
|
if (text !== null) writeText(dest, text);
|
|
182
152
|
else copyFile(src, dest);
|
|
183
153
|
}
|
|
184
154
|
}
|
|
185
|
-
return `seeded ${
|
|
155
|
+
return `seeded ${name}/`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function globalMemory(cfgDir, items, legacy, srcRoot) {
|
|
159
|
+
return globalStore(cfgDir, items, legacy, srcRoot, 'memory', ['memory', 'anamnesis']);
|
|
186
160
|
}
|
|
187
161
|
|
|
188
162
|
/** `_build_global._global_notebook` — the same shape, with no `anamnesis` alias. */
|
|
189
163
|
export function globalNotebook(cfgDir, items, legacy, srcRoot) {
|
|
190
|
-
|
|
191
|
-
const nbDir = path.join(cfgDir, nbName);
|
|
192
|
-
if (isDir(nbDir) && readdirSync(nbDir).length) return `kept ${nbName}/`;
|
|
193
|
-
mkdirSync(nbDir, { recursive: true });
|
|
194
|
-
if (legacy) {
|
|
195
|
-
const src = path.join(legacy, nbName);
|
|
196
|
-
if (isDir(src) && readdirSync(src).length) {
|
|
197
|
-
for (const f of rglobAllFiles(src)) {
|
|
198
|
-
const dest = path.join(nbDir, path.relative(src, f));
|
|
199
|
-
mkdirSync(path.dirname(dest), { recursive: true });
|
|
200
|
-
copyFile(f, dest);
|
|
201
|
-
}
|
|
202
|
-
return `migrated ${nbName}/`;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
for (const { text, src } of items) {
|
|
206
|
-
const sp = relPosix(srcRoot, src).split('/');
|
|
207
|
-
if (sp[0] === nbName && sp.length > 1) {
|
|
208
|
-
// `destRel` — same reason as `globalMemory` above.
|
|
209
|
-
const dest = path.join(nbDir, destRel(path.join(...sp.slice(1))));
|
|
210
|
-
mkdirSync(path.dirname(dest), { recursive: true });
|
|
211
|
-
if (text !== null) writeText(dest, text);
|
|
212
|
-
else copyFile(src, dest);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
return `seeded ${nbName}/`;
|
|
164
|
+
return globalStore(cfgDir, items, legacy, srcRoot, 'notebook');
|
|
216
165
|
}
|
|
217
166
|
|
|
218
167
|
/**
|
|
@@ -27,14 +27,71 @@ import {
|
|
|
27
27
|
import { writeVersion } from './version.mjs';
|
|
28
28
|
import { mkdirSync } from 'node:fs';
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* The layer both OpenCode emits share once RENDER has `items`/`theme` in hand: overrides, the
|
|
32
|
+
* native agents/skills layer, the primary agent, commands, themes, plugins/workflows, and the
|
|
33
|
+
* final `opencode.json` WIRE. `dir` is the emit's own directory — `.opencode/` for the
|
|
34
|
+
* per-repo emit, `cfgDir` for the global one — which is why `wireBase` (where `opencode.json`
|
|
35
|
+
* itself lives) travels separately: the per-repo config sits at the project ROOT, one level
|
|
36
|
+
* above `dir`, while the global config sits IN `cfgDir`, i.e. `dir` itself.
|
|
37
|
+
*
|
|
38
|
+
* Folding WIRE in here moves it earlier in the global emit's own write order (it used to run
|
|
39
|
+
* after the memory/notebook/laws steps, now before them) — harmless, because none of those
|
|
40
|
+
* later steps read `opencode.json` back, and the phase-order gate only requires WIRE to log no
|
|
41
|
+
* earlier than RENDER, which still holds since nothing after this logs a phase at all.
|
|
42
|
+
*
|
|
43
|
+
* `manifestExisted` only ever arrives from the per-repo caller; the global emit does not pass
|
|
44
|
+
* it, matching the Python side, so the pre-manifest header line stays unreachable from there on
|
|
45
|
+
* both sides.
|
|
46
|
+
*
|
|
47
|
+
* `overridesDir` is a THIRD directory, and it is not `dir` for the per-repo emit: the
|
|
48
|
+
* per-repo `agent-overrides.json` lives beside the portable bundle in `out`, while every other
|
|
49
|
+
* write in this layer goes under `.opencode/`. The global emit has no such split — `out` there
|
|
50
|
+
* is only the legacy memory/notebook source, never a write target — so its `overridesDir`
|
|
51
|
+
* equals `dir`.
|
|
52
|
+
*/
|
|
53
|
+
function opencodeLayer(cfg, items, themeName, theme, dir, owned, opts) {
|
|
54
|
+
const {
|
|
55
|
+
oldOwned, manifestExisted, agentPath, wireBase, overridesDir,
|
|
56
|
+
} = opts;
|
|
57
|
+
ensureAgentOverridesStub(cfg, overridesDir);
|
|
58
|
+
const overrides = loadAgentOverrides(overridesDir);
|
|
59
|
+
|
|
60
|
+
const { nAgents, nSkills, written } = writeNativeLayer(
|
|
61
|
+
items, path.join(dir, 'agents'), path.join(dir, 'skills'), overrides,
|
|
62
|
+
{ host: 'opencode', oldOwned, cfg: dir, manifestExisted, theme, src: cfg.src });
|
|
63
|
+
for (const p of written) owned.push(relPosix(dir, p));
|
|
64
|
+
|
|
65
|
+
const primary = writePrimaryAgent(cfg, path.join(dir, 'agents'), overrides);
|
|
66
|
+
if (primary) owned.push(relPosix(dir, primary));
|
|
67
|
+
|
|
68
|
+
const commands = writeCommandLayer(cfg, items, path.join(dir, 'command'));
|
|
69
|
+
commands.push(writePonytailCommand(path.join(dir, 'command'))); // always-on /ponytail
|
|
70
|
+
for (const p of commands) owned.push(relPosix(dir, p));
|
|
71
|
+
|
|
72
|
+
owned.push(relPosix(dir, writeTheme(path.join(dir, 'themes'), themeName, theme)));
|
|
73
|
+
for (const p of writeColorThemes(cfg, path.join(dir, 'themes'))) owned.push(relPosix(dir, p));
|
|
74
|
+
|
|
75
|
+
const nPlugins = copyPlugins(cfg, path.join(dir, 'plugins'), owned);
|
|
76
|
+
const nWorkflows = copyWorkflows(cfg, path.join(dir, 'workflows'), owned);
|
|
77
|
+
|
|
78
|
+
// WIRE — the one file of this layer the user co-owns.
|
|
79
|
+
phaseLog('WIRE');
|
|
80
|
+
const cfgName = path.basename(mergeOpencodeJson(path.join(wireBase, 'opencode.json'),
|
|
81
|
+
agentPath, cfg.doctrines, cfg.excludeRules));
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
nAgents, nSkills, nPlugins, nWorkflows, primary, nCommands: commands.length, cfgName,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
30
88
|
// ---------------------------------------------------------------------------
|
|
31
89
|
// emit_opencode — the RENDER stage only
|
|
32
90
|
// ---------------------------------------------------------------------------
|
|
33
91
|
|
|
34
92
|
/**
|
|
35
|
-
* RENDER and WIRE for `_build_emit.emit_opencode` — everything through
|
|
36
|
-
* `mergeOpencodeJson` call
|
|
37
|
-
* `bin/build-driver.mjs`.
|
|
93
|
+
* RENDER and WIRE for `_build_emit.emit_opencode` — everything through `opencodeLayer`'s
|
|
94
|
+
* `mergeOpencodeJson` call. PRUNE and MANIFEST are the driver's, in `bin/build-driver.mjs`.
|
|
38
95
|
*
|
|
39
96
|
* `oldOwned` and `manifestExisted` arrive in the job rather than being read here. The
|
|
40
97
|
* manifest is the DRIVER's stage: it reads the file, hands the prior claim in, and prunes
|
|
@@ -56,43 +113,21 @@ export function emitOpencodeRender(cfg, job) {
|
|
|
56
113
|
// OpenCode loads agents/skills natively, so strip AGENT.md's per-row spec links to
|
|
57
114
|
// plain names (the portable build keeps them). A deliberate de-link, not a fix.
|
|
58
115
|
const agentMd = path.join(out, 'AGENT.md');
|
|
59
|
-
if (isFile(agentMd)) writeText(agentMd, stripCapabilityLinks(
|
|
116
|
+
if (isFile(agentMd)) writeText(agentMd, stripCapabilityLinks(readText(agentMd)));
|
|
60
117
|
|
|
61
118
|
const owned = [];
|
|
62
119
|
const { theme, items } = renderAll(cfg, _theme);
|
|
63
120
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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));
|
|
121
|
+
const {
|
|
122
|
+
nAgents, nSkills, nPlugins, nWorkflows, primary, nCommands, cfgName,
|
|
123
|
+
} = opencodeLayer(cfg, items, _theme, theme, oc, owned, {
|
|
124
|
+
oldOwned, manifestExisted, agentPath, wireBase: root, overridesDir: out,
|
|
125
|
+
});
|
|
91
126
|
|
|
92
127
|
return {
|
|
93
128
|
owned,
|
|
94
129
|
stats: {
|
|
95
|
-
nAgents, nSkills, nPlugins, nWorkflows, nCommands
|
|
130
|
+
nAgents, nSkills, nPlugins, nWorkflows, nCommands, primary: !!primary,
|
|
96
131
|
},
|
|
97
132
|
cfgName,
|
|
98
133
|
};
|
|
@@ -149,34 +184,17 @@ export function emitOpencodeGlobalRender(cfg, job) {
|
|
|
149
184
|
// plain names. Memory links stay RELATIVE: in the global layout AGENT.md and the
|
|
150
185
|
// store are siblings, so `memory/` resolves from AGENT.md's own location and stays
|
|
151
186
|
// hermetic — no absolute path a doctor check would flag as an escape.
|
|
152
|
-
writeText(path.join(cfgDir, 'AGENT.md'), stripCapabilityLinks(
|
|
187
|
+
writeText(path.join(cfgDir, 'AGENT.md'), stripCapabilityLinks(agentText));
|
|
153
188
|
owned.push('AGENT.md');
|
|
154
189
|
}
|
|
155
190
|
|
|
156
|
-
ensureAgentOverridesStub(cfg, cfgDir);
|
|
157
|
-
const overrides = loadAgentOverrides(cfgDir);
|
|
158
|
-
|
|
159
191
|
// `manifestExisted` is deliberately not passed — the Python does not pass it either, so
|
|
160
192
|
// the pre-manifest header line is unreachable from this emit on both sides.
|
|
161
|
-
const {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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);
|
|
193
|
+
const {
|
|
194
|
+
nAgents, nSkills, nPlugins, nWorkflows, primary, nCommands, cfgName,
|
|
195
|
+
} = opencodeLayer(cfg, items, themeName, theme, cfgDir, owned, {
|
|
196
|
+
oldOwned, agentPath, wireBase: cfgDir, overridesDir: cfgDir,
|
|
197
|
+
});
|
|
180
198
|
|
|
181
199
|
const memStatus = globalMemory(cfgDir, items, out, cfg.src);
|
|
182
200
|
ensureMemoryIndex(path.join(cfgDir, 'memory'));
|
|
@@ -192,23 +210,13 @@ export function emitOpencodeGlobalRender(cfg, job) {
|
|
|
192
210
|
|
|
193
211
|
shipLeanLaws(items, theme, cfgDir, owned, footprint);
|
|
194
212
|
|
|
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
213
|
return {
|
|
205
214
|
owned,
|
|
206
215
|
stats: {
|
|
207
|
-
nAgents, nSkills, nPlugins, nWorkflows, nCommands
|
|
216
|
+
nAgents, nSkills, nPlugins, nWorkflows, nCommands, primary: !!primary,
|
|
208
217
|
},
|
|
209
218
|
memStatus,
|
|
210
219
|
nbStatus,
|
|
211
220
|
cfgName,
|
|
212
221
|
};
|
|
213
222
|
}
|
|
214
|
-
|
package/js/build/generate.mjs
CHANGED
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
} from '../hosts/installs.mjs';
|
|
43
43
|
import { colorThemeFiles, colorThemeJson, PALETTE_ROLES } from '../hosts/opencode.mjs';
|
|
44
44
|
import { renderAll } from './render.mjs';
|
|
45
|
-
import { printOut, printErr, readText, writeText } from '../lib/fs.mjs';
|
|
45
|
+
import { printOut, printErr, readText, writeText, isDir } from '../lib/fs.mjs';
|
|
46
46
|
import { jsonDumpsIndent, parseJson, formatRepr } from '../lib/json.mjs';
|
|
47
47
|
|
|
48
48
|
/** `_harness_build._HEX_RE`. Anchored at BOTH ends — `#123` and `#1122334` both fail. */
|
|
@@ -68,8 +68,6 @@ function sysExit(msg) {
|
|
|
68
68
|
throw e;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
const isDir = (p) => { try { return statSync(p).isDirectory(); } catch { return false; } };
|
|
72
|
-
|
|
73
71
|
// --------------------------------------------------------------------------------------
|
|
74
72
|
// build
|
|
75
73
|
// --------------------------------------------------------------------------------------
|
|
@@ -322,7 +320,7 @@ export function cmdPrompt(args) {
|
|
|
322
320
|
// --------------------------------------------------------------------------------------
|
|
323
321
|
|
|
324
322
|
/** `_harness_build._resolve_themes_dir` — explicit `--dir`, else a repo `.opencode/`, else global. */
|
|
325
|
-
|
|
323
|
+
function resolveThemesDir(args) {
|
|
326
324
|
// `Path(args.dir).expanduser().resolve()` — `resolvePath` already expands a leading `~`.
|
|
327
325
|
if (args.dir) return resolvePath(args.dir);
|
|
328
326
|
const repo = path.join(process.cwd(), '.opencode');
|
|
@@ -352,7 +350,7 @@ export function resolveThemesDir(args) {
|
|
|
352
350
|
* escaping rule) traded for the ORDER OF TWO NAMES in an error message that both
|
|
353
351
|
* implementations still emit, for the same values, with the same exit code.
|
|
354
352
|
*/
|
|
355
|
-
|
|
353
|
+
function loadUserPalette(args, cfg) {
|
|
356
354
|
const pal = new Map();
|
|
357
355
|
if (args.fromTheme) {
|
|
358
356
|
const src = path.join(cfg.colorThemes, `${args.fromTheme}.json`);
|
|
@@ -369,7 +367,7 @@ export function loadUserPalette(args, cfg) {
|
|
|
369
367
|
// `Path(args.palette)` — relative to cwd, with no expanduser and no resolve.
|
|
370
368
|
const raw = parseJson(readText(args.palette));
|
|
371
369
|
// `raw.get("palette", raw)` — a document with no "palette" key IS the role map.
|
|
372
|
-
const map = Object.
|
|
370
|
+
const map = Object.hasOwn(raw, 'palette') ? raw.palette : raw;
|
|
373
371
|
for (const [k, v] of Object.entries(map)) pal.set(k, v);
|
|
374
372
|
}
|
|
375
373
|
if (pal.size === 0) {
|
package/js/build/render.mjs
CHANGED
|
@@ -81,17 +81,17 @@ function suffixOf(name) {
|
|
|
81
81
|
return 0 < i && i < name.length - 1 ? name.slice(i) : '';
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* `Path.rglob("*")` restricted to files, then `sorted()`.
|
|
86
|
+
*
|
|
87
|
+
* `recursive: true, withFileTypes: true` replaces the hand-rolled walk — the order it
|
|
88
|
+
* yields is not the walk's order, but the explicit `.sort(comparePaths)` below is what
|
|
89
|
+
* every caller actually observes, so a different collection order changes nothing.
|
|
90
|
+
*/
|
|
85
91
|
function sortedSourceFiles(src) {
|
|
86
|
-
const out =
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const full = path.join(dir, name);
|
|
90
|
-
if (name === '__pycache__') continue; // `"__pycache__" in path.parts`
|
|
91
|
-
if (statSync(full).isDirectory()) walk(full); // rglob yields dirs; render_all skips them
|
|
92
|
-
else out.push(full);
|
|
93
|
-
}
|
|
94
|
-
})(src);
|
|
92
|
+
const out = readdirSync(src, { recursive: true, withFileTypes: true })
|
|
93
|
+
.filter((d) => d.isFile() && !d.parentPath.split(path.sep).includes('__pycache__'))
|
|
94
|
+
.map((d) => path.join(d.parentPath, d.name));
|
|
95
95
|
return out.sort(comparePaths); // `sorted()` — case-folded on Windows only
|
|
96
96
|
}
|
|
97
97
|
|
|
@@ -126,7 +126,7 @@ export function loadTheme(cfg, name) {
|
|
|
126
126
|
/** `_build_render.substitute`. */
|
|
127
127
|
export function substitute(text, theme) {
|
|
128
128
|
return text.replace(TOKEN_RE, (whole, key) => {
|
|
129
|
-
if (!Object.
|
|
129
|
+
if (!Object.hasOwn(theme, key)) return whole; // visible for debugging
|
|
130
130
|
const v = theme[key];
|
|
131
131
|
// Python renders `str(theme[key])`, which agrees with JS only for strings —
|
|
132
132
|
// `str(True)`/`String(true)` and `str({...})`/`String({...})` do not. Every one of
|
|
@@ -143,7 +143,7 @@ export function substitute(text, theme) {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
/** `_build_render._resolve_catalogs`. */
|
|
146
|
-
|
|
146
|
+
function resolveCatalogs(text, nativeCatalog) {
|
|
147
147
|
return text.replace(CATALOG_BLOCK_RE, (_m, table, pointer) => (nativeCatalog ? pointer : table));
|
|
148
148
|
}
|
|
149
149
|
|
|
@@ -397,7 +397,7 @@ export function effectiveTheme(cfg, themeName, { footprint = 'full', lawsPrefix
|
|
|
397
397
|
/** `_build_render.themed_rel` — rename the top-level folder of an output path per theme. */
|
|
398
398
|
export function themedRel(rel, theme) {
|
|
399
399
|
const parts = rel.split(path.sep);
|
|
400
|
-
if (parts.length && Object.
|
|
400
|
+
if (parts.length && Object.hasOwn(SRC_DIR_TOKENS, parts[0])) {
|
|
401
401
|
parts[0] = theme[SRC_DIR_TOKENS[parts[0]]] ?? parts[0];
|
|
402
402
|
}
|
|
403
403
|
return parts.join(path.sep);
|
package/js/build/source.mjs
CHANGED
|
@@ -12,11 +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
|
|
16
|
-
* `_build_core.js_cfg()` always
|
|
17
|
-
* originals
|
|
18
|
-
* A Node process has no Python module to mutate, so it sends
|
|
15
|
+
* THE ABSENT KEY IS STILL ABSENT, and the reason is bin/build-driver.mjs's, unchanged:
|
|
16
|
+
* `_build_core.js_cfg()` always sent `structure` and `capabilityLinkRe` because the Python
|
|
17
|
+
* originals were module-level names TESTS MUTATE (`_OWNED` membership asked one level out).
|
|
18
|
+
* A Node process has no Python module to mutate, so it sends no `structure`, and
|
|
19
19
|
* `js/build/render.mjs`'s `cfg.structure ?? STRUCTURE` takes its right-hand branch.
|
|
20
|
+
* `capabilityLinkRe`'s equivalent override in `stripCapabilityLinks` had no producer either
|
|
21
|
+
* and was deleted outright in the over-engineering cleanup, `cfg` param and all.
|
|
20
22
|
*
|
|
21
23
|
* The move is safe for the reason P5c's was: `tests/golden.py` drives `bin/build-driver.mjs`
|
|
22
24
|
* over 259 cells and compares the tree byte-for-byte, and every one of them builds a cfg
|
|
@@ -24,7 +26,6 @@
|
|
|
24
26
|
*/
|
|
25
27
|
import { readdirSync, readFileSync } from 'node:fs';
|
|
26
28
|
import path from 'node:path';
|
|
27
|
-
import { fileURLToPath } from 'node:url';
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* `_build_core.ROOT` — the checkout, from this file's own location (`js/..`).
|
|
@@ -37,7 +38,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
37
38
|
* which is what makes them comparable and what makes them unfenceable. See the
|
|
38
39
|
* status/version section of `tests/harness_golden.py`.
|
|
39
40
|
*/
|
|
40
|
-
export const ROOT = path.
|
|
41
|
+
export const ROOT = path.resolve(import.meta.dirname, '../..');
|
|
41
42
|
export const SRC = path.join(ROOT, 'src');
|
|
42
43
|
export const CONFIG = path.join(ROOT, 'harness.config.json');
|
|
43
44
|
export const THEMES = path.join(ROOT, 'themes');
|