geneseed 4.9.1 → 4.9.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 +19 -0
- package/SHIPPED.md +1 -1
- package/adapters/opencode/plugins/geneseed-context.js +7 -1
- package/adapters/opencode/plugins/geneseed-guard.js +18 -10
- package/adapters/opencode/plugins/geneseed-learn.js +7 -1
- package/docs/concepts/hooks.md +1 -1
- package/harness.config.json +1 -1
- package/js/README.md +30 -9
- package/js/build/args.mjs +9 -8
- package/js/build/driver.mjs +21 -21
- package/js/build/emit-claude.mjs +3 -3
- package/js/build/generate.mjs +4 -5
- package/js/build/source.mjs +18 -22
- package/js/hosts/gitref.mjs +5 -3
- package/js/hosts/hooks-done.mjs +21 -9
- package/js/hosts/hooks-learn.mjs +23 -10
- package/js/hosts/hooks-prims.mjs +25 -0
- package/js/hosts/hooks.mjs +6 -1
- package/js/hosts/installs.mjs +5 -4
- package/js/hosts/native.mjs +3 -1
- package/js/hosts/owned.mjs +34 -6
- package/js/hosts/settings.mjs +5 -4
- package/js/inspect/cost.mjs +37 -5
- package/js/inspect/diff.mjs +132 -61
- package/js/inspect/validate.mjs +2 -2
- package/js/lib/fs.mjs +73 -2
- package/js/maintain/setup.mjs +31 -7
- package/js/maintain/uninstall.mjs +1 -1
- package/js/maintain/update.mjs +1 -1
- package/js/web/actions.mjs +36 -3
- package/js/web/api.mjs +12 -5
- package/js/web/handler.mjs +4 -16
- package/js/web/jobs.mjs +5 -3
- package/js/web/state.mjs +3 -0
- package/package.json +1 -1
- package/web/dist/assets/{Activity-GQGAZHIE.js → Activity-D7Igwuoo.js} +1 -1
- package/web/dist/assets/{ActivityDetail-DktWyxim.js → ActivityDetail-53rp05hx.js} +1 -1
- package/web/dist/assets/Dashboard-BoEH5qyY.js +2 -0
- package/web/dist/assets/{Docs-CsWYLnN1.js → Docs-BM56RCT8.js} +3 -3
- package/web/dist/assets/GreenhouseView-BTMvGVco.js +2 -0
- package/web/dist/assets/{Installs-CWuUU4I3.js → Installs-Dh1tB1NK.js} +2 -2
- package/web/dist/assets/JournalView-F4hlWTVB.js +1 -0
- package/web/dist/assets/Laws-iEheocNI.js +3 -0
- package/web/dist/assets/Library-d4_uOGmC.js +1 -0
- package/web/dist/assets/{Loops-BUBOeM0C.js → Loops-BbYeWl70.js} +2 -2
- package/web/dist/assets/{OperatorHudView-BJuCYsjf.js → OperatorHudView-Dki9Q6W3.js} +1 -1
- package/web/dist/assets/Personal-B_YQJjA_.js +1 -0
- package/web/dist/assets/{Tabs-GPQlocTw.js → Tabs-CAl6oFwG.js} +1 -1
- package/web/dist/assets/drawable-BdTDG5oI.js +1 -0
- package/web/dist/assets/index-BZgEGRbL.js +2 -0
- package/web/dist/index.html +1 -1
- package/web/src/pages/Laws.jsx +60 -353
- package/web/dist/assets/Dashboard-CmSn6JfK.js +0 -2
- package/web/dist/assets/GreenhouseView-Dt_cIxAY.js +0 -2
- package/web/dist/assets/JournalView-ClbZj_YF.js +0 -1
- package/web/dist/assets/Laws-R5zZCmya.js +0 -3
- package/web/dist/assets/Library-u36V7oSD.js +0 -1
- package/web/dist/assets/Personal-DWQuSAUz.js +0 -1
- package/web/dist/assets/drawable-CE-arUnF.js +0 -1
- package/web/dist/assets/index-3lnPT_1J.js +0 -2
package/js/inspect/diff.mjs
CHANGED
|
@@ -35,11 +35,15 @@
|
|
|
35
35
|
* directly rather than by comparison, because a check made tolerant of a value owes one
|
|
36
36
|
* somewhere else.
|
|
37
37
|
*/
|
|
38
|
+
import { createHash } from 'node:crypto';
|
|
38
39
|
import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
39
40
|
import os from 'node:os';
|
|
40
41
|
import path from 'node:path';
|
|
41
42
|
|
|
42
|
-
import { emitGlobalInto } from '../build/driver.mjs';
|
|
43
|
+
import { PRIMARY_AGENT_SRC, emitGlobalInto } from '../build/driver.mjs';
|
|
44
|
+
import { rglobFiles } from '../build/emit-common.mjs';
|
|
45
|
+
import { CONFIG, makeCfg } from '../build/source.mjs';
|
|
46
|
+
import { sourceFingerprint } from '../build/version.mjs';
|
|
43
47
|
import {
|
|
44
48
|
GLOBAL_MANIFEST, VERSION_MARKER, expanduser, opencodeConfigDir, resolvePath,
|
|
45
49
|
} from '../hosts/hosts.mjs';
|
|
@@ -53,6 +57,7 @@ import {
|
|
|
53
57
|
import { unifiedDiff, splitLines } from '../lib/udiff.mjs';
|
|
54
58
|
import { printOut, printErr, readText, writeText, isFile } from '../lib/fs.mjs';
|
|
55
59
|
import { toPlatformPath } from '../lib/paths.mjs';
|
|
60
|
+
import { relPosix } from '../lib/text.mjs';
|
|
56
61
|
|
|
57
62
|
/**
|
|
58
63
|
* `_harness_core._T0` — this process's start, in epoch seconds.
|
|
@@ -121,6 +126,81 @@ export function withStdoutSwallowed(fn) {
|
|
|
121
126
|
}
|
|
122
127
|
}
|
|
123
128
|
|
|
129
|
+
/** A manifest's `rel`, spelled the way the expected snapshot keys it. */
|
|
130
|
+
const posixRel = (rel) => rel.split('\\').join('/');
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Render the `expected` tree for `host` at `axes` into a temp dir and SNAPSHOT it — every file,
|
|
134
|
+
* not only the owned ones, because a rel the DEPLOYED manifest claims is looked up here too and
|
|
135
|
+
* its presence decides "edited" against "added". The temp dir is gone before this returns.
|
|
136
|
+
*
|
|
137
|
+
* The one place a render depends on WHERE it is installed: a global Bob emit writes each
|
|
138
|
+
* folder skill's absolute directory into its SKILL.md (see `writeNativeLayer`), so the
|
|
139
|
+
* reference render names the temp tree. Mapped back to the deployed dir here, or every such
|
|
140
|
+
* skill reads as edited. A deployment that has since MOVED still differs, and that is real
|
|
141
|
+
* drift — its skills point at a directory that is no longer theirs. A side-file pointer (and a
|
|
142
|
+
* global OpenCode command's) spells the same directory `/`-separated, so that spelling is
|
|
143
|
+
* mapped too.
|
|
144
|
+
*/
|
|
145
|
+
function renderExpected(host, axes, deployedDir) {
|
|
146
|
+
const tmp = mkdtempSync(path.join(os.tmpdir(), 'geneseed-diff-'));
|
|
147
|
+
try {
|
|
148
|
+
const expected = path.join(tmp, 'expected');
|
|
149
|
+
withStdoutSwallowed(() => emitGlobalInto(host, {
|
|
150
|
+
...axes, out: path.join(tmp, 'bundle'), cfgDir: expected,
|
|
151
|
+
}));
|
|
152
|
+
const posix = (p) => p.split(path.sep).join('/');
|
|
153
|
+
const texts = new Map();
|
|
154
|
+
for (const p of rglobFiles(expected)) {
|
|
155
|
+
texts.set(relPosix(expected, p), readText(p).split(expected).join(deployedDir)
|
|
156
|
+
.split(posix(expected)).join(posix(deployedDir)));
|
|
157
|
+
}
|
|
158
|
+
return { owned: ownedSet(expected), texts };
|
|
159
|
+
} finally {
|
|
160
|
+
// `tempfile.TemporaryDirectory()`'s cleanup, which runs on the exception path too.
|
|
161
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* A memo for `diffCollect`'s `expected` render — opt-in, so `geneseed diff` and every other
|
|
167
|
+
* caller still renders afresh. The web console passes one: `/api/overview` and `/api/fleet`
|
|
168
|
+
* both diff the selected install on every refresh, and the render is ~0.6 s of each.
|
|
169
|
+
*
|
|
170
|
+
* ONLY THE RENDER IS MEMOISED, NEVER THE RESULT: the compare against the deployed tree runs on
|
|
171
|
+
* every call, because catching a hand edit there is the whole point of the drift figure.
|
|
172
|
+
* One entry per deployed dir, at most `cap` of them (least recently used goes first); `renders`
|
|
173
|
+
* counts real renders, for the tests.
|
|
174
|
+
*/
|
|
175
|
+
export function renderMemo(cap = 4) {
|
|
176
|
+
return { cap, renders: 0, entries: new Map() };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The memoised `renderExpected`. The key is everything the render reads: the source
|
|
181
|
+
* fingerprint, the two render inputs it does not hash (`harness.config.json` and the
|
|
182
|
+
* orchestrator agent), the host and every axis. A deployed dir whose key changed is
|
|
183
|
+
* re-rendered in place, and a fingerprint change drops every entry from the old source.
|
|
184
|
+
*/
|
|
185
|
+
function memoExpected(memo, host, axes, deployedDir, sourceFp) {
|
|
186
|
+
const fp = sourceFp ?? sourceFingerprint(makeCfg());
|
|
187
|
+
const extra = createHash('sha256');
|
|
188
|
+
for (const p of [CONFIG, PRIMARY_AGENT_SRC]) extra.update(`${readMaybe(p) ?? ''}\0`);
|
|
189
|
+
const key = JSON.stringify([fp, extra.digest('hex'), host, axes]);
|
|
190
|
+
const hit = memo.entries.get(deployedDir);
|
|
191
|
+
memo.entries.delete(deployedDir);
|
|
192
|
+
if (hit && hit.key === key) {
|
|
193
|
+
memo.entries.set(deployedDir, hit);
|
|
194
|
+
return hit.exp;
|
|
195
|
+
}
|
|
196
|
+
for (const [d, e] of memo.entries) if (e.fp !== fp) memo.entries.delete(d);
|
|
197
|
+
const exp = renderExpected(host, axes, deployedDir);
|
|
198
|
+
memo.renders += 1;
|
|
199
|
+
memo.entries.set(deployedDir, { key, fp, exp });
|
|
200
|
+
while (memo.entries.size > memo.cap) memo.entries.delete(memo.entries.keys().next().value);
|
|
201
|
+
return exp;
|
|
202
|
+
}
|
|
203
|
+
|
|
124
204
|
/**
|
|
125
205
|
* `_harness_diff._diff_collect` — returns `{target, theme, files}`, with `files === null` for
|
|
126
206
|
* "nothing to compare here".
|
|
@@ -140,7 +220,9 @@ export function withStdoutSwallowed(fn) {
|
|
|
140
220
|
* choice, which is the same defect with teeth. `rebuild-all` never had it: it reads
|
|
141
221
|
* `postureOfDir`/`modeOfDir` per install root, and this now asks the same two questions.
|
|
142
222
|
*/
|
|
143
|
-
export function diffCollect({
|
|
223
|
+
export function diffCollect({
|
|
224
|
+
target = null, theme = null, emit = null, footprint = null, memo = null, sourceFp = null,
|
|
225
|
+
} = {}) {
|
|
144
226
|
// `Path(target).expanduser()` — expanded and NOT resolved, which is the opposite of
|
|
145
227
|
// `cmd_version`'s `.expanduser().resolve()` two files over. It is observable: `target` is
|
|
146
228
|
// printed on the summary line, so `--target .` reports `.` here and an absolute path
|
|
@@ -165,69 +247,58 @@ export function diffCollect({ target = null, theme = null, emit = null, footprin
|
|
|
165
247
|
|
|
166
248
|
const fp = footprint || footprintOfDir(dir);
|
|
167
249
|
|
|
250
|
+
// Every input the render reads off the deployment, in ONE object: it is what the render is
|
|
251
|
+
// called with AND what the memo keys on, so the two cannot drift apart.
|
|
252
|
+
const axes = {
|
|
253
|
+
theme: themeName, footprint: fp,
|
|
254
|
+
// The third register read off the deployment, for the same reason as the other two: an
|
|
255
|
+
// install built with `--doctrines craft` compared against an `expected` rendered at all
|
|
256
|
+
// four reports its entire doctrines section — and its `Active packs:` line — as drift.
|
|
257
|
+
// `null` (no marker) means unknown and renders at all four, which `doctrinesForBuild`
|
|
258
|
+
// makes explicit rather than leaving to `makeCfg`'s parameter default two layers down.
|
|
259
|
+
posture: postureOfDir(dir), mode: modeOfDir(dir), doctrines: doctrinesForBuild(dir),
|
|
260
|
+
// The loop skill's preset, or its `Default trust preset:` line reads as edited.
|
|
261
|
+
trust: trustOfDir(dir, host),
|
|
262
|
+
// The excluded rules, or an install built `--exclude-rules "process 5"` reads its
|
|
263
|
+
// carrier as drift: `expected` would re-state the rule the deployment took away.
|
|
264
|
+
excludeRules: excludedRulesOfDir(dir),
|
|
265
|
+
// The excluded skills, or every skill the install left out reads as deleted.
|
|
266
|
+
excludeSkills: excludedSkillsOfDir(dir),
|
|
267
|
+
// The gate switches, or a disarmed install reads its `<gate>: off` line as drift.
|
|
268
|
+
...gatesOfDir(dir),
|
|
269
|
+
};
|
|
270
|
+
const deployedDir = resolvePath(dir);
|
|
271
|
+
const exp = memo
|
|
272
|
+
? memoExpected(memo, host, axes, deployedDir, sourceFp)
|
|
273
|
+
: renderExpected(host, axes, deployedDir);
|
|
274
|
+
|
|
168
275
|
const files = [];
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
// The loop skill's preset, or its `Default trust preset:` line reads as edited.
|
|
181
|
-
trust: trustOfDir(dir, host),
|
|
182
|
-
// The excluded rules, or an install built `--exclude-rules "process 5"` reads its
|
|
183
|
-
// carrier as drift: `expected` would re-state the rule the deployment took away.
|
|
184
|
-
excludeRules: excludedRulesOfDir(dir),
|
|
185
|
-
// The excluded skills, or every skill the install left out reads as deleted.
|
|
186
|
-
excludeSkills: excludedSkillsOfDir(dir),
|
|
187
|
-
// The gate switches, or a disarmed install reads its `<gate>: off` line as drift.
|
|
188
|
-
...gatesOfDir(dir),
|
|
189
|
-
}));
|
|
190
|
-
// The one place a render depends on WHERE it is installed: a global Bob emit writes each
|
|
191
|
-
// folder skill's absolute directory into its SKILL.md (see `writeNativeLayer`), so the
|
|
192
|
-
// reference render names the temp tree. Mapped back to the deployed dir before comparing,
|
|
193
|
-
// or every such skill reads as edited. A deployment that has since MOVED still differs,
|
|
194
|
-
// and that is real drift — its skills point at a directory that is no longer theirs.
|
|
195
|
-
// A side-file pointer (and a global OpenCode command's) spells the same directory
|
|
196
|
-
// `/`-separated, so that spelling is mapped too.
|
|
197
|
-
const deployedDir = resolvePath(dir);
|
|
198
|
-
const posix = (p) => p.split(path.sep).join('/');
|
|
199
|
-
const rels = [...new Set([...ownedSet(dir), ...ownedSet(expected)])].sort();
|
|
200
|
-
for (const rel of rels) {
|
|
201
|
-
const a = path.join(dir, rel);
|
|
202
|
-
const b = path.join(expected, rel);
|
|
203
|
-
if (isFile(a) && isFile(b)) {
|
|
204
|
-
// `read_text(errors="replace")` — Node's utf8 decoder substitutes U+FFFD for the
|
|
205
|
-
// same bytes, and `readText` folds the line endings the way `Path.read_text` does.
|
|
206
|
-
const ta = readText(a);
|
|
207
|
-
const tb = readText(b).split(expected).join(deployedDir)
|
|
208
|
-
.split(posix(expected)).join(posix(deployedDir));
|
|
209
|
-
if (cmpKey(rel, ta) !== cmpKey(rel, tb)) {
|
|
210
|
-
const diff = unifiedDiff(splitLines(tb), splitLines(ta), {
|
|
211
|
-
fromfile: `source/${rel}`, tofile: `deployed/${rel}`, lineterm: '',
|
|
212
|
-
});
|
|
213
|
-
files.push({ rel, status: 'edited', diff });
|
|
214
|
-
}
|
|
215
|
-
} else if (isFile(a)) {
|
|
216
|
-
const body = splitLines(readText(a));
|
|
217
|
-
files.push({
|
|
218
|
-
rel,
|
|
219
|
-
status: 'added',
|
|
220
|
-
diff: ['(only in deployed — your addition)', '', ...body.map((ln) => `+${ln}`)],
|
|
221
|
-
});
|
|
222
|
-
} else {
|
|
223
|
-
files.push({
|
|
224
|
-
rel, status: 'missing', diff: ['(in source, not deployed — re-emit to add)'],
|
|
276
|
+
const rels = [...new Set([...ownedSet(dir), ...exp.owned])].sort();
|
|
277
|
+
for (const rel of rels) {
|
|
278
|
+
const a = path.join(dir, rel);
|
|
279
|
+
const tb = exp.texts.get(posixRel(rel));
|
|
280
|
+
if (isFile(a) && tb !== undefined) {
|
|
281
|
+
// `read_text(errors="replace")` — Node's utf8 decoder substitutes U+FFFD for the
|
|
282
|
+
// same bytes, and `readText` folds the line endings the way `Path.read_text` does.
|
|
283
|
+
const ta = readText(a);
|
|
284
|
+
if (cmpKey(rel, ta) !== cmpKey(rel, tb)) {
|
|
285
|
+
const diff = unifiedDiff(splitLines(tb), splitLines(ta), {
|
|
286
|
+
fromfile: `source/${rel}`, tofile: `deployed/${rel}`, lineterm: '',
|
|
225
287
|
});
|
|
288
|
+
files.push({ rel, status: 'edited', diff });
|
|
226
289
|
}
|
|
290
|
+
} else if (isFile(a)) {
|
|
291
|
+
const body = splitLines(readText(a));
|
|
292
|
+
files.push({
|
|
293
|
+
rel,
|
|
294
|
+
status: 'added',
|
|
295
|
+
diff: ['(only in deployed — your addition)', '', ...body.map((ln) => `+${ln}`)],
|
|
296
|
+
});
|
|
297
|
+
} else {
|
|
298
|
+
files.push({
|
|
299
|
+
rel, status: 'missing', diff: ['(in source, not deployed — re-emit to add)'],
|
|
300
|
+
});
|
|
227
301
|
}
|
|
228
|
-
} finally {
|
|
229
|
-
// `tempfile.TemporaryDirectory()`'s cleanup, which runs on the exception path too.
|
|
230
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
231
302
|
}
|
|
232
303
|
// Already sorted by `rel` above; the Python sorts again after the loop and so does this,
|
|
233
304
|
// because the two sorts are over different things — that one is over the RECORDS, and a
|
package/js/inspect/validate.mjs
CHANGED
|
@@ -130,8 +130,8 @@ export function cmdValidate(args) {
|
|
|
130
130
|
const opts = {
|
|
131
131
|
theme: args.theme, footprint: args.footprint, posture: args.posture, mode: args.mode,
|
|
132
132
|
trust: args.trust, doctrines: args.doctrines, excludeRules: args.excludeRules,
|
|
133
|
-
excludeSkills: args.excludeSkills,
|
|
134
|
-
|
|
133
|
+
excludeSkills: args.excludeSkills,
|
|
134
|
+
...Object.fromEntries(GATE_SWITCHES.map((g) => [g.key, args[g.arg] ? false : null])),
|
|
135
135
|
};
|
|
136
136
|
let scanDirs;
|
|
137
137
|
try {
|
package/js/lib/fs.mjs
CHANGED
|
@@ -29,8 +29,32 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import {
|
|
31
31
|
appendFileSync, writeFileSync, readFileSync, copyFileSync, statSync, utimesSync,
|
|
32
|
+
openSync, fstatSync, readSync, closeSync,
|
|
32
33
|
} from 'node:fs';
|
|
33
34
|
import { EOL } from 'node:os';
|
|
35
|
+
import { resolve } from 'node:path';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* What `writeText` wrote while an emit has armed this (`withWriteRecord` below, around the
|
|
39
|
+
* three manifest-writing emits in `js/build/driver.mjs`): resolved path ->
|
|
40
|
+
* `{ text, size, mtimeMs }`, the translated text and a stat taken right after the write.
|
|
41
|
+
* `ownedHashes` (`js/hosts/owned.mjs`) hashes that text instead of reading the file back —
|
|
42
|
+
* the read-back was ~25% of `doctor --all` — and only while the file's size and mtime still
|
|
43
|
+
* match, so a later rewrite by any writer that does not record (a raw `writeFileSync`, a
|
|
44
|
+
* rename) falls back to the read. `copyFile` and `appendText` drop the entry outright.
|
|
45
|
+
* `null`, the default, records nothing: no other caller pays the stat or retains the text.
|
|
46
|
+
*/
|
|
47
|
+
export const textWrites = { map: null };
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Run one emit with `textWrites` armed, and disarm it on EVERY exit: an emit that throws inside
|
|
51
|
+
* a long-lived process (the web daemon emits in-process) would otherwise leave every later
|
|
52
|
+
* `writeText` paying a stat and keeping its text in memory.
|
|
53
|
+
*/
|
|
54
|
+
export function withWriteRecord(fn) {
|
|
55
|
+
textWrites.map = new Map();
|
|
56
|
+
try { return fn(); } finally { textWrites.map = null; }
|
|
57
|
+
}
|
|
34
58
|
/**
|
|
35
59
|
* `Path.write_text(text, encoding="utf-8")`.
|
|
36
60
|
*
|
|
@@ -49,7 +73,12 @@ import { EOL } from 'node:os';
|
|
|
49
73
|
* The same vendored skill folder can contain files from both categories.
|
|
50
74
|
*/
|
|
51
75
|
export function writeText(path, text) {
|
|
52
|
-
|
|
76
|
+
const out = EOL === '\n' ? text : text.replaceAll('\n', EOL);
|
|
77
|
+
writeFileSync(path, out, 'utf8');
|
|
78
|
+
if (textWrites.map) {
|
|
79
|
+
const st = statSync(path);
|
|
80
|
+
textWrites.map.set(resolve(path), { text: out, size: st.size, mtimeMs: st.mtimeMs });
|
|
81
|
+
}
|
|
53
82
|
}
|
|
54
83
|
|
|
55
84
|
/**
|
|
@@ -62,6 +91,7 @@ export function writeText(path, text) {
|
|
|
62
91
|
* invisible in an editor and a whole-file difference to the gate.
|
|
63
92
|
*/
|
|
64
93
|
export function appendText(path, text) {
|
|
94
|
+
textWrites.map?.delete(resolve(path));
|
|
65
95
|
appendFileSync(path, EOL === '\n' ? text : text.replaceAll('\n', EOL), 'utf8');
|
|
66
96
|
}
|
|
67
97
|
|
|
@@ -79,6 +109,46 @@ export function readText(path) {
|
|
|
79
109
|
return readFileSync(path, 'utf8').replaceAll('\r\n', '\n').replaceAll('\r', '\n');
|
|
80
110
|
}
|
|
81
111
|
|
|
112
|
+
/**
|
|
113
|
+
* The last `bytes` of a file, decoded as `readText` decodes it, and the byte offset the window
|
|
114
|
+
* began at (0: the whole file). A window that starts mid-line drops that partial line, cut at a
|
|
115
|
+
* newline BYTE before decoding — so a multibyte character the window split falls in the dropped
|
|
116
|
+
* part, and a `\r\n` split between its two bytes leaves only an empty line. One byte before the
|
|
117
|
+
* window is read too, so a window that starts exactly on a line keeps it. Throws as
|
|
118
|
+
* `readFileSync` does (missing, unreadable, a directory).
|
|
119
|
+
*/
|
|
120
|
+
export function readTail(path, bytes) {
|
|
121
|
+
const fd = openSync(path, 'r');
|
|
122
|
+
try {
|
|
123
|
+
const st = fstatSync(fd);
|
|
124
|
+
// Windows opens a directory without complaint and reports size 0: refuse it as readFileSync does.
|
|
125
|
+
if (st.isDirectory()) {
|
|
126
|
+
throw Object.assign(new Error(`EISDIR: illegal operation on a directory, read '${path}'`),
|
|
127
|
+
{ code: 'EISDIR' });
|
|
128
|
+
}
|
|
129
|
+
const size = st.size;
|
|
130
|
+
const start = Math.max(0, size - bytes);
|
|
131
|
+
const from = start > 0 ? start - 1 : 0;
|
|
132
|
+
const buf = Buffer.alloc(size - from);
|
|
133
|
+
let got = 0;
|
|
134
|
+
while (got < buf.length) {
|
|
135
|
+
const n = readSync(fd, buf, got, buf.length - got, from + got);
|
|
136
|
+
if (!n) break;
|
|
137
|
+
got += n;
|
|
138
|
+
}
|
|
139
|
+
let cut = 0;
|
|
140
|
+
if (start > 0) {
|
|
141
|
+
const read = buf.subarray(0, got);
|
|
142
|
+
const ends = [read.indexOf(0x0a), read.indexOf(0x0d)].filter((i) => i !== -1);
|
|
143
|
+
cut = ends.length ? Math.min(...ends) + 1 : got; // no line end: nothing whole to decode
|
|
144
|
+
}
|
|
145
|
+
const text = buf.toString('utf8', cut, got).replaceAll('\r\n', '\n').replaceAll('\r', '\n');
|
|
146
|
+
return { text, start };
|
|
147
|
+
} finally {
|
|
148
|
+
closeSync(fd);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
82
152
|
/**
|
|
83
153
|
* `shutil.copy2(src, dest)`.
|
|
84
154
|
*
|
|
@@ -94,6 +164,7 @@ export function readText(path) {
|
|
|
94
164
|
* mode the Python side has and nothing depends on. Mode is not copied here.
|
|
95
165
|
*/
|
|
96
166
|
export function copyFile(src, dest) {
|
|
167
|
+
textWrites.map?.delete(resolve(dest));
|
|
97
168
|
copyFileSync(src, dest);
|
|
98
169
|
const st = statSync(src);
|
|
99
170
|
utimesSync(dest, st.atime, st.mtime);
|
|
@@ -216,7 +287,7 @@ export function withPlatformNewlines(fn) {
|
|
|
216
287
|
*/
|
|
217
288
|
const NOT_THERE = new Set(['ENOENT', 'ENOTDIR', 'ELOOP', 'EBADF']);
|
|
218
289
|
|
|
219
|
-
function statOrNull(p) {
|
|
290
|
+
export function statOrNull(p) {
|
|
220
291
|
try {
|
|
221
292
|
return statSync(p);
|
|
222
293
|
} catch (e) {
|
package/js/maintain/setup.mjs
CHANGED
|
@@ -46,13 +46,13 @@ import path from 'node:path';
|
|
|
46
46
|
|
|
47
47
|
import { main as driverMain, resolveOut } from '../build/driver.mjs';
|
|
48
48
|
import { playLine } from '../ui/anim.mjs';
|
|
49
|
-
import { discoverNames, PACK_ORDER } from '../build/source.mjs';
|
|
49
|
+
import { discoverNames, GATE_SWITCHES, PACK_ORDER } from '../build/source.mjs';
|
|
50
50
|
import { exportImprovements } from '../inspect/diff.mjs';
|
|
51
51
|
import { cmdDoctor } from '../inspect/doctor.mjs';
|
|
52
52
|
import { setupBuildArgs } from '../build/generate.mjs';
|
|
53
53
|
import { GLOBAL_MANIFEST, HOSTS, opencodeConfigDir } from '../hosts/hosts.mjs';
|
|
54
54
|
import {
|
|
55
|
-
claudeCfg, defaultMode, defaultPosture, defaultTheme, EMIT_HOST_SCOPE, installedDefaults,
|
|
55
|
+
claudeCfg, defaultMode, defaultPosture, defaultTheme, EMIT_HOST_SCOPE, gatesOfDir, installedDefaults,
|
|
56
56
|
readJsonMaybe, themeFiles,
|
|
57
57
|
} from '../hosts/installs.mjs';
|
|
58
58
|
import { printOut, printErr } from '../lib/fs.mjs';
|
|
@@ -242,6 +242,24 @@ function askDoctrines(deployed = null) {
|
|
|
242
242
|
* whole, `null` included, because `askDoctrines` distinguishes "no carrier said" from "a
|
|
243
243
|
* carrier said none" and the two open the question differently.
|
|
244
244
|
*/
|
|
245
|
+
/**
|
|
246
|
+
* The gate switches of the one install a setup answer re-runs: the chosen emit's global config
|
|
247
|
+
* dir, or the project/bundle root it names. Never another host's install — carrying its `off`
|
|
248
|
+
* across would disarm a gate nobody switched off here. FAILS CLOSED: no install there, or a dir
|
|
249
|
+
* that cannot be resolved, reads as all armed (`gatesOfDir` already fails closed per line).
|
|
250
|
+
*/
|
|
251
|
+
function targetGates(emit, out) {
|
|
252
|
+
const [host, scope] = EMIT_HOST_SCOPE.get(emit) ?? [];
|
|
253
|
+
try {
|
|
254
|
+
if (!host) return gatesOfDir(resolveOut(out || 'Harness'));
|
|
255
|
+
const dir = scope === 'global'
|
|
256
|
+
? HOSTS.find((h) => h.host === host).configDir() : resolveOut(out || '.');
|
|
257
|
+
return gatesOfDir(dir, host);
|
|
258
|
+
} catch {
|
|
259
|
+
return Object.fromEntries(GATE_SWITCHES.map((g) => [g.key, true]));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
245
263
|
export function collectSetupLines() {
|
|
246
264
|
printOut('Geneseed setup — answer a few questions; nothing is written until you confirm.\n');
|
|
247
265
|
const inst = installedDefaults();
|
|
@@ -279,14 +297,20 @@ export function collectSetupLines() {
|
|
|
279
297
|
const excludeRules = inst.excludeRules?.length ? inst.excludeRules : null;
|
|
280
298
|
// Skills left out are preserved the same way, and elide when there are none.
|
|
281
299
|
const excludeSkills = inst.excludeSkills?.length ? inst.excludeSkills : null;
|
|
300
|
+
// The gate switches too: a re-run that dropped them would re-arm every gate its owner switched
|
|
301
|
+
// off. Read off the install THIS answer targets, never the machine walk above (`targetGates`).
|
|
302
|
+
// `setupBuildArgs` elides an armed gate, so an all-armed install prints what it always did.
|
|
303
|
+
const gates = targetGates(emit, out);
|
|
304
|
+
const gated = Object.values(gates).some((on) => on === false);
|
|
282
305
|
printOut(`\nAbout to run: geneseed build ${
|
|
283
306
|
setupBuildArgs(theme, emit, out, root, footprint, posture, mode, doctrines, allPacks,
|
|
284
|
-
excludeRules, undefined, excludeSkills).join(' ')}\n`);
|
|
307
|
+
excludeRules, undefined, excludeSkills, gates).join(' ')}\n`);
|
|
285
308
|
if (!confirm('Proceed?', true)) return null;
|
|
286
|
-
// The key only when there is something to carry, so a selection that excludes no skill
|
|
287
|
-
// the same object it was before
|
|
309
|
+
// The key only when there is something to carry, so a selection that excludes no skill and
|
|
310
|
+
// switches off no gate is the same object it was before either existed.
|
|
288
311
|
return {
|
|
289
312
|
theme, posture, mode, doctrines, excludeRules, ...(excludeSkills ? { excludeSkills } : {}),
|
|
313
|
+
...(gated ? { gates } : {}),
|
|
290
314
|
emit, out, root, footprint,
|
|
291
315
|
};
|
|
292
316
|
}
|
|
@@ -470,7 +494,7 @@ export function setupLines() {
|
|
|
470
494
|
}
|
|
471
495
|
const {
|
|
472
496
|
theme, emit, out, root, footprint = 'lean', posture = 'peer', mode = 'direct',
|
|
473
|
-
doctrines = null, excludeRules = null, excludeSkills = null,
|
|
497
|
+
doctrines = null, excludeRules = null, excludeSkills = null, gates = {},
|
|
474
498
|
} = sel;
|
|
475
499
|
if (emit === 'opencode-global') {
|
|
476
500
|
// The build below overwrites the deployed global harness, and the self-improvement loops
|
|
@@ -489,7 +513,7 @@ export function setupLines() {
|
|
|
489
513
|
// runs must carry the pack selection the plan line just previewed, or the wizard would print
|
|
490
514
|
// one command and run another.
|
|
491
515
|
const argv = setupBuildArgs(theme, emit, out, root, footprint, posture, mode, doctrines,
|
|
492
|
-
doctrineOptions().map(([n]) => n), excludeRules, undefined, excludeSkills);
|
|
516
|
+
doctrineOptions().map(([n]) => n), excludeRules, undefined, excludeSkills, gates);
|
|
493
517
|
printOut(`Running: geneseed build ${argv.join(' ')}\n`);
|
|
494
518
|
const rc = driverMain(argv);
|
|
495
519
|
if (rc !== 0) {
|
|
@@ -1179,7 +1179,7 @@ function remergeClaudeHooks(cfg, root = cfg, host = 'claude') {
|
|
|
1179
1179
|
const gates = Object.fromEntries(Object.keys(atRoot).map((k) => [k, atRoot[k] && atCfg[k]]));
|
|
1180
1180
|
const [, claims] = mergeClaudeSettings(settingsFile(cfg, managed),
|
|
1181
1181
|
managed.settings_hooks ?? null, hookRunnerEntry(), doctrines, excluded, host, cfg,
|
|
1182
|
-
gates
|
|
1182
|
+
gates);
|
|
1183
1183
|
// `and data` — a manifest that did not parse is not one to write back.
|
|
1184
1184
|
if (!deepEquals(claims, managed.settings_hooks ?? null) && Object.keys(data).length > 0) {
|
|
1185
1185
|
managed.settings_hooks = claims;
|
package/js/maintain/update.mjs
CHANGED
|
@@ -632,7 +632,7 @@ export async function installAxes(dir, { installProfile, setupBuildArgs }, notif
|
|
|
632
632
|
if (legacyProcessCarrierOfDir(String(dir), 'opencode')) return null;
|
|
633
633
|
const p = installProfile('opencode', 'global', String(dir), notify);
|
|
634
634
|
return setupBuildArgs('', 'files', null, null, p.footprint, p.posture, p.mode, p.doctrines,
|
|
635
|
-
undefined, p.excludeRules, p.trust, p.excludeSkills, p
|
|
635
|
+
undefined, p.excludeRules, p.trust, p.excludeSkills, p).slice(4);
|
|
636
636
|
}
|
|
637
637
|
|
|
638
638
|
/**
|
package/js/web/actions.mjs
CHANGED
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
} from '../build/source.mjs';
|
|
39
39
|
import { withStdoutSwallowed } from '../inspect/diff.mjs';
|
|
40
40
|
import { excludeAdd, excludeRemove } from '../inspect/excludes.mjs';
|
|
41
|
-
import { setupBuildArgs } from '../build/generate.mjs';
|
|
41
|
+
import { installProfile, setupBuildArgs } from '../build/generate.mjs';
|
|
42
42
|
import { DEFAULT_PRESET, PRESETS } from '../loop/score.mjs';
|
|
43
43
|
import { activeLoops, setLoopPreset } from '../loop/registry.mjs';
|
|
44
44
|
import {
|
|
@@ -61,6 +61,7 @@ import { stripWhitespace } from '../lib/text.mjs';
|
|
|
61
61
|
import { memoryDelete } from '../maintain/memory.mjs';
|
|
62
62
|
import { installDeactivate, installReactivate, installUninstall } from '../maintain/uninstall.mjs';
|
|
63
63
|
import { emitChoices, themeChoices, viewCfg } from './api.mjs';
|
|
64
|
+
import { actionCommands } from './jobs.mjs';
|
|
64
65
|
import { NotFound, deployed, memoryDir } from './catalog.mjs';
|
|
65
66
|
|
|
66
67
|
/** One body field, `dflt` when the body is not an object or does not carry the key. */
|
|
@@ -439,6 +440,38 @@ export function buildOverride(state, body) {
|
|
|
439
440
|
return [themes.has(t) ? t : state.theme, emits.has(e) ? e : state.emit];
|
|
440
441
|
}
|
|
441
442
|
|
|
443
|
+
/**
|
|
444
|
+
* The job steps for a Build POST.
|
|
445
|
+
*
|
|
446
|
+
* A DEPLOYED INSTALL REBUILT IN ITS OWN EMIT TAKES THE ARGV ITS PROFILE OWNS (`installProfile`,
|
|
447
|
+
* the one reader `rebuild-all`, `status` and `upgrade` share), with only the theme the picker
|
|
448
|
+
* chose swapped in. This path used to re-assemble the axes it happened to carry — no gate
|
|
449
|
+
* switches, `--exclude-rules none`, no excluded skills — so a console Build silently re-armed
|
|
450
|
+
* every switched-off gate and handed back every rule and skill its owner had left out.
|
|
451
|
+
*
|
|
452
|
+
* Nothing deployed (the first-deploy Onboarding form) or a re-target to another emit has no
|
|
453
|
+
* install of that emit to honour — a re-target is a FRESH target, by decision (2026-10-18), so it
|
|
454
|
+
* deliberately carries none of this install's switches or exclusions — and keeps the defaults it
|
|
455
|
+
* always had: footprint, posture
|
|
456
|
+
* and mode off the viewed dir, and ALL packs when unknown — never `harness.config.json`, which
|
|
457
|
+
* would drop the consent gate.
|
|
458
|
+
*/
|
|
459
|
+
export function buildActionCmds(state, body) {
|
|
460
|
+
const [theme, emit] = buildOverride(state, body);
|
|
461
|
+
const [host, scope] = EMIT_HOST_SCOPE.get(state.emit || '') ?? [];
|
|
462
|
+
if (host && deployed(state) && emit === state.emit) {
|
|
463
|
+
// `setupBuildArgs` always leads with `--theme <theme>`, so the swap is the head alone.
|
|
464
|
+
// `state.root`, not `target`: a project install's markers live at the repo root.
|
|
465
|
+
const [, , ...rest] = installProfile(host, scope, state.root).argv;
|
|
466
|
+
return actionCommands('build', { buildArgv: ['--theme', theme, ...rest] });
|
|
467
|
+
}
|
|
468
|
+
return actionCommands('build', {
|
|
469
|
+
theme, emit, footprint: state.footprint, posture: state.posture, mode: state.mode,
|
|
470
|
+
trust: state.trust,
|
|
471
|
+
doctrines: doctrinesForBuild(state.target),
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
442
475
|
/**
|
|
443
476
|
* The global emit matching a deployed install's `.geneseed-emit`, so the EXPECTED render
|
|
444
477
|
* uses the install's own host dialect.
|
|
@@ -595,7 +628,7 @@ export function apiInstallCmd(state, body) {
|
|
|
595
628
|
const gates = bodyGates(body, root, host);
|
|
596
629
|
const out = scope === 'global' ? null : String(root);
|
|
597
630
|
const argv = setupBuildArgs(theme || 'neutral', emit, out, out, fp, pos, mode, doctrines,
|
|
598
|
-
PACK_ORDER, excludeRules, trust, excludedSkillsOfDir(root, host), gates
|
|
631
|
+
PACK_ORDER, excludeRules, trust, excludedSkillsOfDir(root, host), gates);
|
|
599
632
|
return { cmd: [process.execPath, path.join(ROOT, 'bin', 'build-driver.mjs'), ...argv] };
|
|
600
633
|
}
|
|
601
634
|
|
|
@@ -697,6 +730,6 @@ export function apiDeployCmd(state, body) {
|
|
|
697
730
|
const gates = bodyGates(body, root, host);
|
|
698
731
|
// project-scope emit name == host name (opencode / claude / bob / openclaude)
|
|
699
732
|
const argv = setupBuildArgs(theme || 'neutral', host, root, root, fp, pos, mode, doctrines,
|
|
700
|
-
PACK_ORDER, excludeRules, trust, excludedSkillsOfDir(root, host), gates
|
|
733
|
+
PACK_ORDER, excludeRules, trust, excludedSkillsOfDir(root, host), gates);
|
|
701
734
|
return { cmd: [process.execPath, path.join(ROOT, 'bin', 'build-driver.mjs'), ...argv] };
|
|
702
735
|
}
|
package/js/web/api.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { CONFIG, ROOT, THEMES, discoverNames, makeCfg } from '../build/source.mj
|
|
|
15
15
|
import { readVersion, sourceFingerprint } from '../build/version.mjs';
|
|
16
16
|
import { sourceReleaseVersion } from '../hosts/opencode.mjs';
|
|
17
17
|
import { alwaysOnCost } from '../inspect/cost.mjs';
|
|
18
|
-
import { diffCollect } from '../inspect/diff.mjs';
|
|
18
|
+
import { diffCollect, renderMemo } from '../inspect/diff.mjs';
|
|
19
19
|
import { excludesSnapshot } from '../inspect/excludes.mjs';
|
|
20
20
|
import { CLAUDE_STYLE, HOSTS, VERSION_MARKER, resolvePath } from '../hosts/hosts.mjs';
|
|
21
21
|
import {
|
|
@@ -130,6 +130,12 @@ export function* activeInstalls(targets) {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/**
|
|
134
|
+
* The state's memo of `expected` renders, made on first use: overview and fleet diff the same
|
|
135
|
+
* install on every refresh, and only the compare has to run again (see `renderMemo`).
|
|
136
|
+
*/
|
|
137
|
+
const memoOf = (state) => (state.renderMemo ??= renderMemo());
|
|
138
|
+
|
|
133
139
|
/** The edited/added/missing tally of a `diffCollect` file list. */
|
|
134
140
|
const driftCounts = (files) => ({
|
|
135
141
|
edited: files.filter((f) => f.status === 'edited').length,
|
|
@@ -179,8 +185,9 @@ export function apiInstalls(state) {
|
|
|
179
185
|
*
|
|
180
186
|
* Its own route, not more fields on `/api/installs`: the sidebar refetches that list on every
|
|
181
187
|
* data revision, and the drift check below re-renders a whole emit per global install.
|
|
182
|
-
*
|
|
183
|
-
*
|
|
188
|
+
* Each global install's `expected` render is memoised on the state (`renderMemo`, keyed on the
|
|
189
|
+
* source fingerprint computed once per request and every axis), so a refresh re-runs only the
|
|
190
|
+
* compare; the memo holds a handful of installs, enough for any machine seen so far.
|
|
184
191
|
*
|
|
185
192
|
* Each row reads ITS OWN `.geneseed-version`. `statusData().installed_fp` is the first carrier
|
|
186
193
|
* a fixed walk finds, which describes the wrong install as soon as two hosts are deployed.
|
|
@@ -197,7 +204,7 @@ export function apiFleet(state) {
|
|
|
197
204
|
let drift = null;
|
|
198
205
|
if (st === 'active') {
|
|
199
206
|
try {
|
|
200
|
-
const { files } = diffCollect({ target: dir });
|
|
207
|
+
const { files } = diffCollect({ target: dir, memo: memoOf(state), sourceFp });
|
|
201
208
|
if (files !== null) drift = driftCounts(files);
|
|
202
209
|
} catch { /* an install that cannot be re-rendered reports no drift figure, not zero */ }
|
|
203
210
|
}
|
|
@@ -226,7 +233,7 @@ export function apiOverview(state) {
|
|
|
226
233
|
let diff = null;
|
|
227
234
|
if (deployed(state)) {
|
|
228
235
|
const { files } = diffCollect({
|
|
229
|
-
target: state.target, theme: state.theme, emit: state.emit });
|
|
236
|
+
target: state.target, theme: state.theme, emit: state.emit, memo: memoOf(state) });
|
|
230
237
|
if (files !== null) diff = driftCounts(files);
|
|
231
238
|
}
|
|
232
239
|
let buildTime = null;
|
package/js/web/handler.mjs
CHANGED
|
@@ -11,11 +11,10 @@
|
|
|
11
11
|
* declared next door in `routes.mjs`; the daemon that starts it is in `daemon.mjs`.
|
|
12
12
|
*/
|
|
13
13
|
import { resolvePath } from '../hosts/hosts.mjs';
|
|
14
|
-
import { doctrinesForBuild } from '../hosts/installs.mjs';
|
|
15
14
|
import { isTruthy, jsonDumpsCompact } from '../lib/json.mjs';
|
|
16
15
|
import { parseIntStrict, percentDecode } from '../lib/text.mjs';
|
|
17
16
|
import { preflight } from '../maintain/update.mjs';
|
|
18
|
-
import { apiDeployCmd, apiInstallCmd, apiPickFolder, apiRestore,
|
|
17
|
+
import { apiDeployCmd, apiInstallCmd, apiPickFolder, apiRestore, buildActionCmds, mcpTargetPaths } from './actions.mjs';
|
|
19
18
|
import { NotFound } from './catalog.mjs';
|
|
20
19
|
import { openUrl, requestRestart } from './daemon.mjs';
|
|
21
20
|
import { apiDocs, apiDocsPage } from './docs.mjs';
|
|
@@ -301,20 +300,9 @@ export function makeHandler(state, jm, token, dist, holder = null) {
|
|
|
301
300
|
if (jid === null) return sendJson(res, { error: 'busy' }, 409, ae);
|
|
302
301
|
return sendJson(res, { job_id: jid }, 202, ae);
|
|
303
302
|
}
|
|
304
|
-
// Build
|
|
305
|
-
// the deployed theme downstream.
|
|
306
|
-
|
|
307
|
-
const [theme, emit] = action === 'build'
|
|
308
|
-
? buildOverride(state, body) : [state.theme, state.emit];
|
|
309
|
-
// The pack selection follows the install like footprint/posture/mode, and is read here
|
|
310
|
-
// rather than left to the table's default: a console Build must re-emit the constitution
|
|
311
|
-
// the deployment already carries. Unknown (no `Active packs:` marker) resolves to ALL
|
|
312
|
-
// packs — never to `harness.config.json`, which would drop the consent gate.
|
|
313
|
-
const cmds = actionCommands(action, {
|
|
314
|
-
theme, emit, footprint: state.footprint, posture: state.posture, mode: state.mode,
|
|
315
|
-
trust: state.trust,
|
|
316
|
-
doctrines: doctrinesForBuild(state.target),
|
|
317
|
-
});
|
|
303
|
+
// Build resolves its argv off the install (`buildActionCmds`); the other actions take no
|
|
304
|
+
// axes and self-resolve the deployed theme downstream.
|
|
305
|
+
const cmds = action === 'build' ? buildActionCmds(state, body) : actionCommands(action);
|
|
318
306
|
if (!isTruthy(cmds)) {
|
|
319
307
|
return sendJson(res, { error: `unknown action ${action}` }, 404, ae);
|
|
320
308
|
}
|
package/js/web/jobs.mjs
CHANGED
|
@@ -421,14 +421,16 @@ export const INLINE_ACTIONS = ['restore', 'install', 'deploy'];
|
|
|
421
421
|
* `harness.config.json` exactly as the pack list does, so `{"excludeRules":["process.5"]}`
|
|
422
422
|
* there would take the consent gate out of a console Build that never asked for it. `[]` is
|
|
423
423
|
* spelled `--exclude-rules none` and says "exclude nothing" out loud.
|
|
424
|
+
*
|
|
425
|
+
* `buildArgv`, when given, is used whole and the axes above are ignored: it is the argv a
|
|
426
|
+
* deployed install's profile owns (`buildActionCmds`), switches and exclusions included.
|
|
424
427
|
*/
|
|
425
428
|
function actionTable({
|
|
426
429
|
theme = 'neutral', emit = 'opencode-global', footprint = 'full',
|
|
427
430
|
posture = 'peer', mode = 'direct', doctrines = [...PACK_ORDER], excludeRules = [],
|
|
428
|
-
trust = DEFAULT_PRESET,
|
|
431
|
+
trust = DEFAULT_PRESET, buildArgv = setupBuildArgs(theme, emit, null, null, footprint, posture,
|
|
432
|
+
mode, doctrines, PACK_ORDER, excludeRules, trust),
|
|
429
433
|
} = {}) {
|
|
430
|
-
const buildArgv = setupBuildArgs(theme, emit, null, null, footprint, posture, mode,
|
|
431
|
-
doctrines, PACK_ORDER, excludeRules, trust);
|
|
432
434
|
return {
|
|
433
435
|
doctor: [[NODE(), CLI(), 'doctor']],
|
|
434
436
|
build: [[NODE(), GEN(), ...buildArgv]],
|