geneseed 3.13.0 → 3.14.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 +39 -0
- package/SHIPPED.md +1 -1
- package/adapters/opencode/plugins/geneseed-learn.js +10 -1
- package/bin/geneseed-cli.mjs +10 -14
- package/bin/geneseed-hook.mjs +22 -15
- package/docs/extending.md +5 -5
- package/harness.config.json +1 -1
- package/js/README.md +19 -10
- package/js/build/args.mjs +374 -0
- package/js/build/driver.mjs +137 -553
- package/js/build/emit-claude.mjs +2 -4
- package/js/build/generate.mjs +6 -3
- package/js/build/render.mjs +5 -9
- package/js/build/source.mjs +3 -11
- package/js/hosts/hooks-context.mjs +431 -0
- package/js/hosts/hooks-learn.mjs +275 -0
- package/js/hosts/hooks-prims.mjs +108 -0
- package/js/hosts/hooks.mjs +50 -977
- package/js/hosts/hosts.mjs +12 -4
- package/js/hosts/installs.mjs +5 -6
- package/js/hosts/mcp.mjs +12 -48
- package/js/hosts/memory-files.mjs +181 -0
- package/js/hosts/settings.mjs +135 -414
- package/js/hosts/shim.mjs +373 -0
- package/js/inspect/checks-authoring.mjs +21 -828
- package/js/inspect/checks-build.mjs +4 -4
- package/js/inspect/checks-constitution.mjs +493 -0
- package/js/inspect/checks-counts.mjs +242 -0
- package/js/inspect/checks-repo.mjs +17 -8
- package/js/inspect/diff.mjs +4 -1
- package/js/inspect/doctor.mjs +2 -2
- package/js/inspect/inventory.mjs +4 -4
- package/js/inspect/scan.mjs +1 -1
- package/js/inspect/status.mjs +1 -1
- package/js/inspect/validate.mjs +48 -35
- package/js/loop/catalog.mjs +5 -3
- package/js/loop/cli.mjs +26 -12
- package/js/loop/graph.mjs +1 -1
- package/js/loop/registry.mjs +10 -15
- package/js/loop/score.mjs +1 -1
- package/js/loop/state.mjs +35 -11
- package/js/maintain/memory.mjs +1 -1
- package/js/maintain/migrate.mjs +18 -30
- package/js/maintain/uninstall.mjs +47 -88
- package/js/ui/cli.mjs +12 -12
- package/js/web/actions.mjs +29 -19
- package/js/web/api.mjs +21 -21
- package/js/web/docs.mjs +29 -72
- package/package.json +1 -1
- package/web/dist/assets/Activity-CjRqZnr-.js +1 -0
- package/web/dist/assets/ActivityDetail-OQTxKBj2.js +1 -0
- package/web/dist/assets/Dashboard-J-xCe_Gk.js +2 -0
- package/web/dist/assets/Docs-CvOZ8akL.js +5 -0
- package/web/dist/assets/FilterInput-CVyqmYWM.js +1 -0
- package/web/dist/assets/GreenhouseView-6KDjX8da.js +2 -0
- package/web/dist/assets/Installs-MWuhNjSm.js +2 -0
- package/web/dist/assets/JournalView-C8MrOz2c.js +1 -0
- package/web/dist/assets/Laws-Du0-Zyp2.js +3 -0
- package/web/dist/assets/Library-DOkO0TtB.js +1 -0
- package/web/dist/assets/Loops-Dv_aLByJ.js +2 -0
- package/web/dist/assets/{Markdown-CdTX0kLe.js → Markdown-X2a3qvOh.js} +11 -11
- package/web/dist/assets/OperatorHudView-BPP1dtLz.js +2 -0
- package/web/dist/assets/Personal-26VPYkK0.js +1 -0
- package/web/dist/assets/Tabs-BzKuFskD.js +1 -0
- package/web/dist/assets/activity-Dr7ZMBI3.js +1 -0
- package/web/dist/assets/{drawable-C_x5xncq.js → drawable-zVVDbsyw.js} +1 -1
- package/web/dist/assets/index-Ctp-4poP.css +1 -0
- package/web/dist/assets/index-KHt3wE_K.js +2 -0
- package/web/dist/assets/{react-Bbzhmq4v.js → react-B9Z_tXRV.js} +1 -1
- package/web/dist/assets/sections-GalMS0Zv.js +1 -0
- package/web/dist/assets/usePoll-DD8VjtSg.js +1 -0
- package/web/dist/index.html +4 -5
- package/web/src/pages/Laws.jsx +0 -1
- package/web/dist/assets/Activity-Ca2Aruu7.js +0 -1
- package/web/dist/assets/ActivityDetail-Bo7v4Z_r.js +0 -1
- package/web/dist/assets/Dashboard-BuMq46aT.js +0 -2
- package/web/dist/assets/Docs-BzccCllG.js +0 -5
- package/web/dist/assets/FilterInput-CBcJBQPI.js +0 -1
- package/web/dist/assets/GreenhouseView-BCIKcnGG.js +0 -2
- package/web/dist/assets/Installs-DMFJvq7s.js +0 -2
- package/web/dist/assets/JournalView-o1rmd1_1.js +0 -1
- package/web/dist/assets/Laws-BANxq_I_.js +0 -3
- package/web/dist/assets/Library-DW4x0fbh.js +0 -1
- package/web/dist/assets/Loops-DEBibbBP.js +0 -2
- package/web/dist/assets/OperatorHudView-CLD0fYaA.js +0 -2
- package/web/dist/assets/Personal-6uMJvyBG.js +0 -1
- package/web/dist/assets/Tabs-BubnyZhF.js +0 -1
- package/web/dist/assets/activity-CObqyxkL.js +0 -1
- package/web/dist/assets/index-CMPgezFm.js +0 -2
- package/web/dist/assets/index-DMsT4Aa6.css +0 -1
- package/web/dist/assets/rolldown-runtime-hePW80VL.js +0 -1
- package/web/dist/assets/sections-Dg23Hel5.js +0 -1
- package/web/dist/assets/usePoll-BOqeunzF.js +0 -1
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The generator's flag surface — the defaults `harness.config.json` supplies, the parse tables,
|
|
3
|
+
* `-h`, and the argparse-shaped refusal (`die`). Split out of `driver.mjs`, which consumes it
|
|
4
|
+
* through `parseArgs`/`configDefaults` and re-exports `parseDriverArgs` for `geneseed validate`.
|
|
5
|
+
*
|
|
6
|
+
* Under the same transitive `child_process` ban as the driver: everything here reads the
|
|
7
|
+
* checkout and prints, nothing more.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
10
|
+
import { parseArgs as nodeParseArgs } from 'node:util';
|
|
11
|
+
import { withPlatformNewlines } from '../lib/fs.mjs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { CONFIG, discoverNames, PACK_ORDER, knownRuleIds } from './source.mjs';
|
|
14
|
+
// The loop engine's trust presets — the `--trust` choices. `score.mjs` imports nothing, which is
|
|
15
|
+
// what lets it sit inside this module's `child_process` ban.
|
|
16
|
+
import { DEFAULT_PRESET, PRESETS } from '../loop/score.mjs';
|
|
17
|
+
|
|
18
|
+
/** The nine `--emit` choices: a plain bundle, then a project and a global emit per host. */
|
|
19
|
+
const EMITS = ['files', 'opencode', 'opencode-global', 'claude', 'claude-global',
|
|
20
|
+
'bob', 'bob-global', 'openclaude', 'openclaude-global'];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* build.py:307-321 — the three defaults that come from `harness.config.json`.
|
|
24
|
+
*
|
|
25
|
+
* The corrupt-file branch is reproduced including its imprecision: the warning says only
|
|
26
|
+
* "using theme 'neutral'", but the branch also resets posture and mode. Matching the
|
|
27
|
+
* Python text matters more than fixing it here, because the two CLIs' stderr is compared
|
|
28
|
+
* byte-for-byte by `tests/golden.py` (:316).
|
|
29
|
+
*/
|
|
30
|
+
export function configDefaults() {
|
|
31
|
+
const d = {
|
|
32
|
+
theme: 'neutral', posture: 'peer', mode: 'direct', trust: DEFAULT_PRESET,
|
|
33
|
+
doctrines: [...PACK_ORDER], excludeRules: [],
|
|
34
|
+
};
|
|
35
|
+
if (!existsSync(CONFIG)) return d;
|
|
36
|
+
try {
|
|
37
|
+
const data = JSON.parse(readFileSync(CONFIG, 'utf8'));
|
|
38
|
+
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
39
|
+
if (data.theme !== undefined) d.theme = data.theme;
|
|
40
|
+
if (data.posture !== undefined) d.posture = data.posture;
|
|
41
|
+
if (data.mode !== undefined) d.mode = data.mode;
|
|
42
|
+
// `doctrines` is the only config key that is a LIST, so it is the only one that can be
|
|
43
|
+
// the right type and still nonsense. A bad value warns and falls back to all packs
|
|
44
|
+
// rather than dying: this file is hand-editable, and a typo here must not brick every
|
|
45
|
+
// rebuild of an install whose AGENT.md is otherwise fine. `[]` is a legitimate value
|
|
46
|
+
// (no packs) and must survive the check — hence an explicit Array test, not truthiness.
|
|
47
|
+
if (data.doctrines !== undefined) {
|
|
48
|
+
const known = discoverNames('doctrines', PACK_ORDER[0]);
|
|
49
|
+
if (Array.isArray(data.doctrines) && data.doctrines.every((x) => known.includes(x))) {
|
|
50
|
+
d.doctrines = PACK_ORDER.filter((p) => data.doctrines.includes(p));
|
|
51
|
+
} else {
|
|
52
|
+
process.stderr.write(`[geneseed] WARN: ${path.basename(CONFIG)} "doctrines" is not a `
|
|
53
|
+
+ 'list of known pack names — using all packs.\n');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// The second axis, read the same forgiving way and defaulting the OTHER direction: a
|
|
57
|
+
// bad value excludes NOTHING, because the safe answer for a list of things to take
|
|
58
|
+
// away is to take nothing away. Same reasoning as `doctrines` defaulting to all.
|
|
59
|
+
if (data.excludeRules !== undefined) {
|
|
60
|
+
if (Array.isArray(data.excludeRules)
|
|
61
|
+
&& data.excludeRules.every((x) => typeof x === 'string')) {
|
|
62
|
+
// ⚠ AND EVERY ID IS CHECKED, as `--exclude-rules` checks its own: an unknown id
|
|
63
|
+
// passed through lands in the `Excluded rules:` marker, `rebuild-all` hands it back
|
|
64
|
+
// as a flag, and `parseExcludeRules` dies on it — every rebuild of that install,
|
|
65
|
+
// forever. Unknown ids are dropped with a WARN; the known ones still bind.
|
|
66
|
+
const known = knownRuleIds();
|
|
67
|
+
const ids = data.excludeRules.map((x) => x.trim().replace(/[ \t]+/, '.'));
|
|
68
|
+
const unknown = ids.filter((id) => !known.includes(id));
|
|
69
|
+
if (unknown.length) {
|
|
70
|
+
process.stderr.write(`[geneseed] WARN: ${path.basename(CONFIG)} "excludeRules" names `
|
|
71
|
+
+ `unknown rule(s) ${unknown.map((id) => `'${id.replace('.', ' ')}'`).join(', ')}`
|
|
72
|
+
+ ' — ignoring them.\n');
|
|
73
|
+
}
|
|
74
|
+
d.excludeRules = [...new Set(ids.filter((id) => known.includes(id)))].sort();
|
|
75
|
+
} else {
|
|
76
|
+
process.stderr.write(`[geneseed] WARN: ${path.basename(CONFIG)} "excludeRules" is `
|
|
77
|
+
+ 'not a list of rule addresses — excluding nothing.\n');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
process.stderr.write(`[geneseed] WARN: ${path.basename(CONFIG)} is unreadable — `
|
|
83
|
+
+ "using theme 'neutral'.\n");
|
|
84
|
+
}
|
|
85
|
+
return d;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The parser's tables, at MODULE scope so `usage` reads the same two objects the loop below
|
|
90
|
+
* dispatches on. They were local to `parseArgs` until `-h` arrived; a help text listing its
|
|
91
|
+
* own private copy of the flags is a second source that drifts silently the first time a
|
|
92
|
+
* flag is added to one and not the other, and nothing downstream would say so.
|
|
93
|
+
*/
|
|
94
|
+
const VALUED = {
|
|
95
|
+
'--theme': 'theme', '--posture': 'posture', '--mode': 'mode', '--trust': 'trust',
|
|
96
|
+
'--doctrines': 'doctrines', '--exclude-rules': 'excludeRules',
|
|
97
|
+
'--out': 'out', '--target': 'out', '--emit': 'emit',
|
|
98
|
+
'--footprint': 'footprint', '--root': 'root', '--config-dir': 'cfgDir',
|
|
99
|
+
};
|
|
100
|
+
const FLAGS = {
|
|
101
|
+
'--sync-themes': 'syncThemes', '--validate-only': 'validateOnly',
|
|
102
|
+
'-v': 'verbose', '--verbose': 'verbose',
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* `util.parseArgs`'s own option table, DERIVED from `VALUED`/`FLAGS` rather than typed out a
|
|
107
|
+
* second time — a hand-written twin is exactly the anti-drift failure this module's docblock
|
|
108
|
+
* already names for `usage()`. Every `VALUED` entry is `type: 'string'` (so a bare `--flag
|
|
109
|
+
* value` form is captured as one token instead of leaking `value` as an orphan positional);
|
|
110
|
+
* every long `FLAGS` entry is `type: 'boolean'`. `-v` gets `short: 'v'` for the same reason
|
|
111
|
+
* `-h` deliberately does NOT get one below `PARSE_OPTIONS` — see `parseArgs`'s docblock.
|
|
112
|
+
*/
|
|
113
|
+
const PARSE_OPTIONS = Object.fromEntries([
|
|
114
|
+
...Object.keys(VALUED).map((f) => [f.slice(2), { type: 'string' }]),
|
|
115
|
+
...Object.keys(FLAGS).filter((f) => f.startsWith('--')).map((f) => [f.slice(2), { type: 'boolean' }]),
|
|
116
|
+
]);
|
|
117
|
+
PARSE_OPTIONS.verbose = { type: 'boolean', short: 'v' };
|
|
118
|
+
|
|
119
|
+
/** `token.name` (dashes stripped, short aliases folded in) -> this file's `args` key. */
|
|
120
|
+
const VALUED_BY_NAME = Object.fromEntries(Object.keys(VALUED).map((f) => [f.slice(2), VALUED[f]]));
|
|
121
|
+
const FLAG_BY_NAME = Object.fromEntries(
|
|
122
|
+
Object.keys(FLAGS).filter((f) => f.startsWith('--')).map((f) => [f.slice(2), FLAGS[f]]),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The five flags `choice` validates, as ONE table for the same anti-drift reason.
|
|
127
|
+
*
|
|
128
|
+
* A function and not a constant because two of the five read the checkout: `discoverNames`
|
|
129
|
+
* scans `postures/` and `modes/`, so the answer depends on the source tree this driver is
|
|
130
|
+
* standing in and cannot be frozen at import.
|
|
131
|
+
*
|
|
132
|
+
* `--doctrines` is DELIBERATELY not a sixth row. Every consumer of this table treats an
|
|
133
|
+
* entry as one value drawn from a closed list — `choice` refuses anything else, and
|
|
134
|
+
* `usage`'s metavar prints the list as the whole legal surface. `--doctrines` takes a comma
|
|
135
|
+
* LIST plus the sentinel `none`, so a row here would have refused `craft,rigor` outright and
|
|
136
|
+
* advertised a metavar that lies. It is validated by `parseDoctrines` instead and prints the
|
|
137
|
+
* plain `DOCTRINES` metavar, exactly as `--theme` (also open-ended) already does.
|
|
138
|
+
*/
|
|
139
|
+
function choicesFor() {
|
|
140
|
+
return {
|
|
141
|
+
'--emit': EMITS,
|
|
142
|
+
'--footprint': ['lean', 'full'],
|
|
143
|
+
'--posture': discoverNames('postures', 'peer'),
|
|
144
|
+
'--mode': discoverNames('modes', 'direct'),
|
|
145
|
+
'--trust': Object.keys(PRESETS),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* `--exclude-rules "process 7,craft 3"` (or `process.7`) -> the cfg's exclusion array.
|
|
151
|
+
*
|
|
152
|
+
* BOTH SPELLINGS ACCEPTED ON THE WAY IN, ONE ON THE WAY OUT. The carrier's marker line and
|
|
153
|
+
* every citation in the corpus spell a rule `process 7`; the console's deep link and the
|
|
154
|
+
* catalogue spell it `process.7`. A flag that took only one of them would be wrong for
|
|
155
|
+
* whichever half of the documentation the reader had open, so it takes either and normalises
|
|
156
|
+
* to the dotted form the code compares.
|
|
157
|
+
*
|
|
158
|
+
* ⚠ VALIDATED AGAINST THE PACK FILES, NOT A RANGE. `process 9` and `craft 0` are both
|
|
159
|
+
* refusals, and so is a pack this checkout does not ship — an exclusion that names nothing is
|
|
160
|
+
* a typo that would otherwise take away nothing in silence, which is the failure this flag
|
|
161
|
+
* can least afford: the user asked for a rule to STOP binding and it would keep binding.
|
|
162
|
+
* Sorted, deduped, for the same marker-stability reason `--doctrines` is.
|
|
163
|
+
*/
|
|
164
|
+
function parseExcludeRules(value) {
|
|
165
|
+
const s = value.trim();
|
|
166
|
+
if (s === 'none' || s === '') return [];
|
|
167
|
+
const known = knownRuleIds();
|
|
168
|
+
const out = [];
|
|
169
|
+
for (const raw of s.split(',').map((x) => x.trim()).filter(Boolean)) {
|
|
170
|
+
const id = raw.replace(/[ \t]+/, '.');
|
|
171
|
+
if (!known.includes(id)) {
|
|
172
|
+
die(2, `argument --exclude-rules: invalid choice: '${raw}' (choose from `
|
|
173
|
+
+ `${known.map((c) => `'${c.replace('.', ' ')}'`).join(', ')}, or 'none')`);
|
|
174
|
+
}
|
|
175
|
+
out.push(id);
|
|
176
|
+
}
|
|
177
|
+
return [...new Set(out)].sort();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* `--doctrines craft,rigor` / `--doctrines none` -> the cfg's pack array.
|
|
182
|
+
*
|
|
183
|
+
* Normalised into `PACK_ORDER` rather than kept in the order typed, which also dedupes: the
|
|
184
|
+
* rendered `Active packs:` line is a MARKER a later reader parses back out of a deployed
|
|
185
|
+
* carrier, and a marker whose contents depend on argument order is one that compares unequal
|
|
186
|
+
* to itself. Validation is against DISCOVERY, not `PACK_ORDER`, so the refusal names the
|
|
187
|
+
* packs the checkout actually has; a discovered pack missing from `PACK_ORDER` is a
|
|
188
|
+
* different fault and `js/build/render.mjs` refuses the whole build for it.
|
|
189
|
+
*/
|
|
190
|
+
function parseDoctrines(value) {
|
|
191
|
+
const s = value.trim();
|
|
192
|
+
const known = discoverNames('doctrines', PACK_ORDER[0]);
|
|
193
|
+
if (s === 'none') return [];
|
|
194
|
+
const names = s.split(',').map((x) => x.trim()).filter(Boolean);
|
|
195
|
+
if (!names.length) {
|
|
196
|
+
die(2, "argument --doctrines: expected a comma-separated list of packs, or 'none'");
|
|
197
|
+
}
|
|
198
|
+
for (const n of names) {
|
|
199
|
+
if (!known.includes(n)) {
|
|
200
|
+
die(2, `argument --doctrines: invalid choice: '${n}' (choose from `
|
|
201
|
+
+ `${known.map((c) => `'${c}'`).join(', ')}, or 'none')`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return PACK_ORDER.filter((p) => names.includes(p));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* `-h/--help` — the flag argparse handed the reference for free and this hand-rolled parser
|
|
209
|
+
* did not, so the two entry points disagreed: the reference printed usage and exited 0 while
|
|
210
|
+
* `geneseed-build --help` died with `unrecognized arguments: --help` and exit 2.
|
|
211
|
+
*
|
|
212
|
+
* NO GOLDEN CELL CAN SEE IT: every cell is built out of the render flags and never passes
|
|
213
|
+
* `--help`, and a flag missing from one side is invisible to a gate that never passes it.
|
|
214
|
+
* `tests/unit/node_driver.test.mjs` is the gate, and it checks a hand-written flag table rather
|
|
215
|
+
* than one scraped from this file, so it fails on drift instead of agreeing by construction.
|
|
216
|
+
*
|
|
217
|
+
* The text is deliberately NOT argparse's byte-for-byte. Reproducing its wrapping would put
|
|
218
|
+
* a hand-written copy of the reference's output in a file no gate compares, which is the
|
|
219
|
+
* drift this whole docblock is about; the surface here is the npx one, in the same class as
|
|
220
|
+
* the `--sync-themes` and `--validate-only` refusals that already answer differently.
|
|
221
|
+
*/
|
|
222
|
+
function usage() {
|
|
223
|
+
const choices = choicesFor();
|
|
224
|
+
const metavar = (flag) => (choices[flag]
|
|
225
|
+
? `{${choices[flag].join(',')}}`
|
|
226
|
+
: VALUED[flag].toUpperCase());
|
|
227
|
+
const valued = Object.keys(VALUED);
|
|
228
|
+
const bare = Object.keys(FLAGS);
|
|
229
|
+
return [
|
|
230
|
+
`usage: geneseed-build [-h] ${[...valued.map((f) => `[${f} ${metavar(f)}]`),
|
|
231
|
+
...bare.map((f) => `[${f}]`)].join(' ')}`,
|
|
232
|
+
'',
|
|
233
|
+
'Render the Geneseed harness for a theme.',
|
|
234
|
+
'',
|
|
235
|
+
'options:',
|
|
236
|
+
' -h, --help',
|
|
237
|
+
...valued.map((f) => ` ${f} ${metavar(f)}`),
|
|
238
|
+
...bare.map((f) => ` ${f}`),
|
|
239
|
+
'',
|
|
240
|
+
].join('\n');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* argparse's `--flag value` and `--flag=value`, plus the `--target` alias for `--out` — the
|
|
245
|
+
* SPLITTING now done by `util.parseArgs({tokens: true})`, with every choice/mutex/wording
|
|
246
|
+
* decision this function made before P6 (Task 4) still made HERE, unmoved.
|
|
247
|
+
*
|
|
248
|
+
* `strict: false` is load-bearing twice: it lets an unrecognized flag surface as a token this
|
|
249
|
+
* walk can refuse in its OWN words rather than node's, and it is what makes a value-taking
|
|
250
|
+
* flag (`type: 'string'` in `PARSE_OPTIONS`) swallow the very next raw argument even when that
|
|
251
|
+
* argument itself looks like a flag — `--theme --posture peer` sets `theme` to the literal
|
|
252
|
+
* string `"--posture"` and leaves `peer` a stray token, exactly as the hand-rolled version
|
|
253
|
+
* being replaced did with its own `argv[i += 1]`. That is not a bug this port inherited by
|
|
254
|
+
* accident: it is the one shape `parseArgs`'s tokenizer reproduces for free, and reproducing
|
|
255
|
+
* anything smarter (a lookahead that refuses to swallow a flag-shaped value) would be new
|
|
256
|
+
* behaviour, not a preserved one.
|
|
257
|
+
*
|
|
258
|
+
* `-h` DELIBERATELY IS NOT `PARSE_OPTIONS.help`'s short alias. Declaring one would make
|
|
259
|
+
* `parseArgs` fold a bundled `-vh` into two tokens — `verbose` then `help` — and print the
|
|
260
|
+
* help text for a cluster the original parser refused outright as one unrecognized argument.
|
|
261
|
+
* Detecting help by re-reading `argv[tok.index]` for the literal text `-h`/`--help` keeps the
|
|
262
|
+
* ORIGINAL equality check (`tok === '-h' || tok === '--help'`) intact no matter how `parseArgs`
|
|
263
|
+
* privately tokenizes the cluster, and `argv[tok.index]` is what recovers that literal text
|
|
264
|
+
* for every refusal below — `tok.name`/`tok.rawName` are `parseArgs`'s own canonicalisation and
|
|
265
|
+
* would print `-b`/`-o`/`-g`/`-u`/`-s` for a bundle like `-bogus` where the original parser (and
|
|
266
|
+
* this one) refuses the whole typed token, `-bogus`, once.
|
|
267
|
+
*/
|
|
268
|
+
export function parseArgs(argv, defaults) {
|
|
269
|
+
const args = {
|
|
270
|
+
theme: defaults.theme, posture: defaults.posture, mode: defaults.mode,
|
|
271
|
+
trust: defaults.trust,
|
|
272
|
+
doctrines: defaults.doctrines,
|
|
273
|
+
excludeRules: defaults.excludeRules,
|
|
274
|
+
out: 'Harness', emit: 'files', footprint: 'lean', root: null,
|
|
275
|
+
syncThemes: false, validateOnly: false, verbose: false,
|
|
276
|
+
};
|
|
277
|
+
const { tokens } = nodeParseArgs({
|
|
278
|
+
args: argv, options: PARSE_OPTIONS, allowPositionals: true, strict: false, tokens: true,
|
|
279
|
+
});
|
|
280
|
+
for (const tok of tokens) {
|
|
281
|
+
if (tok.kind === 'option' && tok.value === undefined
|
|
282
|
+
&& (argv[tok.index] === '-h' || argv[tok.index] === '--help')) {
|
|
283
|
+
process.stdout.write(usage());
|
|
284
|
+
// The same marker `die` throws, with a SUCCESS code: help is not a refusal, and
|
|
285
|
+
// `main`'s catch is what turns either into this process's exit status. A bare
|
|
286
|
+
// `process.exit(0)` here would be the P5f bug in reverse — it takes `rebuild-all`'s
|
|
287
|
+
// loop with it, and skips the stdout flush the text was just written to.
|
|
288
|
+
const e = new Error('help');
|
|
289
|
+
e.exitCode = 0;
|
|
290
|
+
throw e;
|
|
291
|
+
}
|
|
292
|
+
if (tok.kind === 'option' && VALUED_BY_NAME[tok.name] !== undefined) {
|
|
293
|
+
if (tok.value === undefined) die(2, `argument ${tok.rawName}: expected one argument`);
|
|
294
|
+
args[VALUED_BY_NAME[tok.name]] = tok.value;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
// `tok.value === undefined` excludes `--sync-themes=x` from matching here — a store_true
|
|
298
|
+
// flag never took `=` in the hand-rolled version either, because its `FLAGS[tok]` lookup
|
|
299
|
+
// keyed on the WHOLE typed token (equals sign and all), never on the split-off flag name.
|
|
300
|
+
// `parseArgs` always sets a `value` KEY on every option token (`undefined` when none was
|
|
301
|
+
// given), so this must compare the VALUE, not `'value' in tok` — the key is always there.
|
|
302
|
+
if (tok.kind === 'option' && tok.value === undefined && FLAG_BY_NAME[tok.name] !== undefined) {
|
|
303
|
+
args[FLAG_BY_NAME[tok.name]] = true;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
// Every other shape — an unrecognized option, a bare positional, or `--` (which `parseArgs`
|
|
307
|
+
// always treats as a terminator but this parser never did) — refuses with the literal text
|
|
308
|
+
// typed at that argv position, matching the original's `unrecognized arguments: ${tok}`.
|
|
309
|
+
die(2, `unrecognized arguments: ${argv[tok.index]}`);
|
|
310
|
+
}
|
|
311
|
+
const choices = choicesFor();
|
|
312
|
+
for (const flag of Object.keys(choices)) choice(flag, args[VALUED[flag]], choices[flag]);
|
|
313
|
+
// Only when the flag was actually PASSED is this a string; left at its default it is
|
|
314
|
+
// already the array `configDefaults` built, and re-parsing an array would stringify it.
|
|
315
|
+
if (typeof args.doctrines === 'string') args.doctrines = parseDoctrines(args.doctrines);
|
|
316
|
+
if (typeof args.excludeRules === 'string') args.excludeRules = parseExcludeRules(args.excludeRules);
|
|
317
|
+
return args;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* The generator's OWN flag surface, for the one consumer that is not this driver.
|
|
322
|
+
*
|
|
323
|
+
* `geneseed validate` takes every render flag but `--sync-themes`/`--config-dir` (it refuses
|
|
324
|
+
* those two, and prints its own `-h` before this parser can print the generator's) — the reference's
|
|
325
|
+
* `--validate-only` reads exactly the flags `build.py`'s parser already produced, so the port
|
|
326
|
+
* hands the CLI this parser rather than a second one beside it. That is not tidiness: a
|
|
327
|
+
* hand-rolled copy would have its own `--target` alias, its own `choices` lists and its own
|
|
328
|
+
* `-h`, and `test_the_help_text_names_every_flag_the_reference_takes` gates only this one.
|
|
329
|
+
*
|
|
330
|
+
* `withPlatformNewlines` because the refusal and `-h` paths WRITE: called from `bin/build-driver.mjs`
|
|
331
|
+
* they sit inside `main`'s funnel, and called from the CLI binary they would not.
|
|
332
|
+
*/
|
|
333
|
+
export function parseDriverArgs(argv) {
|
|
334
|
+
return withPlatformNewlines(() => parseArgs(argv, configDefaults()));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function choice(flag, value, allowed) {
|
|
338
|
+
if (!allowed.includes(value)) {
|
|
339
|
+
die(2, `argument ${flag}: invalid choice: '${value}' `
|
|
340
|
+
+ `(choose from ${allowed.map((c) => `'${c}'`).join(', ')})`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* argparse's error exit — a THROW since P5f, where it used to be `process.exit(code)`.
|
|
346
|
+
*
|
|
347
|
+
* `harness rebuild-all` re-emits every active install and its whole contract is "continue
|
|
348
|
+
* past a failure so one broken install never blocks the rest". The Python gets that for free:
|
|
349
|
+
* each rebuild is a SUBPROCESS, and a child that exits 2 hands back a return code. Here the
|
|
350
|
+
* driver is a module in the same process, so `process.exit` would take the loop, the CLI and
|
|
351
|
+
* every remaining install with it — the one place where importing rather than spawning is not
|
|
352
|
+
* transparent, and it turns a per-install failure into a total one.
|
|
353
|
+
*
|
|
354
|
+
* The marker is `exitCode`, the SAME one `assertSourceComplete` and `effectiveTheme` already
|
|
355
|
+
* use, and the first draft of this used a second name so that only `die` would be converted
|
|
356
|
+
* — on the argument that an incomplete source should keep its stack. That draft was wrong,
|
|
357
|
+
* and `rebuild-all/one-broken-install-does-not-stop-the-rest` is what said so: a
|
|
358
|
+
* `.geneseed-theme` naming a theme that does not exist makes `effectiveTheme` refuse, and
|
|
359
|
+
* with only `die` converted the refusal propagated out of the loop and the remaining
|
|
360
|
+
* installs were never rebuilt. `main` is standing in for a PROCESS, and a process boundary
|
|
361
|
+
* turns every deliberate refusal into an exit code — the narrower rule was a distinction
|
|
362
|
+
* with no principle behind it.
|
|
363
|
+
*
|
|
364
|
+
* One behaviour improves as a side effect, and it is worth naming rather than discovering
|
|
365
|
+
* later: `process.exit` terminates without flushing a pending stdout write, so a refusal on
|
|
366
|
+
* a slow pipe could lose output it had already produced. Returning through `main` lets Node
|
|
367
|
+
* drain normally.
|
|
368
|
+
*/
|
|
369
|
+
export function die(code, msg) {
|
|
370
|
+
process.stderr.write(`geneseed: error: ${msg}\n`);
|
|
371
|
+
const e = new Error(msg);
|
|
372
|
+
e.exitCode = code;
|
|
373
|
+
throw e;
|
|
374
|
+
}
|