bmad-plus 0.9.2 → 0.12.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/README.md +1 -1
- package/osint-agent-package/skills/bmad-osint-investigate/osint/SKILL.md +30 -0
- package/osint-agent-package/skills/bmad-osint-investigate/osint/assets/dossier-template.md +10 -0
- package/osint-agent-package/skills/bmad-osint-investigate/osint/assets/lawful-basis-record.md +48 -0
- package/osint-agent-package/skills/bmad-osint-investigate/osint/references/gdpr-osint.md +48 -0
- package/package.json +3 -1
- package/tools/build/README.md +78 -0
- package/tools/build/adapters.config.js +117 -0
- package/tools/build/generate-adapters.js +485 -0
- package/tools/build/generate.js +284 -0
- package/tools/build/generated-adapters/.codex/AGENTS.md +121 -0
- package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +126 -0
- package/tools/build/generated-adapters/.opencode/AGENTS.md +121 -0
- package/tools/build/generated-adapters/AGENTS.md +119 -0
- package/tools/build/generated-adapters/CLAUDE.md +122 -0
- package/tools/build/generated-adapters/CONVENTIONS.md +121 -0
- package/tools/build/generated-adapters/GEMINI.md +126 -0
- package/tools/build/generated-adapters/README.md +79 -0
- package/tools/cli/bmad-plus-cli.js +11 -0
- package/tools/cli/commands/install.js +46 -0
- package/tools/cli/commands/memory-journal-cmd.js +311 -0
- package/tools/cli/lib/README-memory-journal.md +125 -0
- package/tools/cli/lib/memory-journal.js +0 -0
- package/tools/cli/lib/packs.js +195 -108
- package/tools/cli/lib/python-provision.js +508 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* BMAD+ Build — registry.yaml → multi-CLI adapter generator (Pillar 5)
|
|
4
|
+
*
|
|
5
|
+
* Reads the root registry.yaml (the single source of truth) and, for each
|
|
6
|
+
* targets.adapters[] entry, emits a THIN adapter file that:
|
|
7
|
+
* - points every tool at the AGENTS.md spine (targets.spine),
|
|
8
|
+
* - adds tool-specific invocation notes,
|
|
9
|
+
* - lists agent/pack facts COMPUTED from the registry (counts are derived,
|
|
10
|
+
* never typed by a human → adapters cannot drift or lie).
|
|
11
|
+
*
|
|
12
|
+
* Adapters sharing one on-disk file (e.g. gemini-cli + antigravity → GEMINI.md)
|
|
13
|
+
* are grouped into a single generated file with one notes block per tool.
|
|
14
|
+
*
|
|
15
|
+
* ADOPTED (load-bearing since v0.11.0): the repo-root adapter files
|
|
16
|
+
* (CLAUDE.md, GEMINI.md, .cursor/rules/bmad-plus.mdc, .codex/AGENTS.md,
|
|
17
|
+
* .opencode/AGENTS.md, CONVENTIONS.md) AND the spine itself (AGENTS.md) are
|
|
18
|
+
* GENERATED by this script — never hand-edited. Hand-authored project
|
|
19
|
+
* instructions live in tools/build/adapters.config.js and are injected into
|
|
20
|
+
* every generated file, so no tool loses instructions it used to have.
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* node tools/build/generate-adapters.js # write previews to
|
|
24
|
+
* # tools/build/generated-adapters/
|
|
25
|
+
* node tools/build/generate-adapters.js --adopt # write the REAL repo-root
|
|
26
|
+
* # adapters + spine
|
|
27
|
+
* node tools/build/generate-adapters.js --adopt --force # overwrite root files that
|
|
28
|
+
* # lack the AUTO-GENERATED marker
|
|
29
|
+
* node tools/build/generate-adapters.js --out-dir <dir> # write to <dir>
|
|
30
|
+
* node tools/build/generate-adapters.js --print # print all adapters to stdout
|
|
31
|
+
* node tools/build/generate-adapters.js --check # verify preview adapters match
|
|
32
|
+
* node tools/build/generate-adapters.js --check --adopt # verify ROOT adapters + spine
|
|
33
|
+
* # match (exit 1 on drift)
|
|
34
|
+
*
|
|
35
|
+
* Author: Laurent Rochetta
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
'use strict';
|
|
39
|
+
|
|
40
|
+
const fs = require('node:fs');
|
|
41
|
+
const path = require('node:path');
|
|
42
|
+
const { loadRegistry, DEFAULT_REGISTRY_PATH } = require('./generate');
|
|
43
|
+
const { PROJECT_INSTRUCTIONS } = require('./adapters.config');
|
|
44
|
+
|
|
45
|
+
const DEFAULT_OUT_DIR = path.join(__dirname, 'generated-adapters');
|
|
46
|
+
const REPO_ROOT = path.join(__dirname, '..', '..');
|
|
47
|
+
const GENERATED_MARKER = 'AUTO-GENERATED by tools/build/generate-adapters.js';
|
|
48
|
+
|
|
49
|
+
/** Per-tool invocation notes. Unknown tools fall back to buildDefaultNotes(). */
|
|
50
|
+
const TOOL_NOTES = {
|
|
51
|
+
'claude-code': [
|
|
52
|
+
'Claude Code loads `CLAUDE.md` from the project root automatically.',
|
|
53
|
+
'Activate an agent by saying its name (see the spine for the full roster).',
|
|
54
|
+
'Skills are loaded from `.agents/skills/`; auto-activation triggers live in `.agents/data/role-triggers.yaml`.',
|
|
55
|
+
],
|
|
56
|
+
'gemini-cli': [
|
|
57
|
+
'Gemini CLI loads `GEMINI.md` from the project root automatically.',
|
|
58
|
+
'Activate an agent by saying its name (see the spine for the full roster).',
|
|
59
|
+
],
|
|
60
|
+
antigravity: [
|
|
61
|
+
'Antigravity reads the same `GEMINI.md` file (detected via `.gemini/antigravity`).',
|
|
62
|
+
'Concurrency rule: coordinate with other agentic CLIs before writing — never edit the same files simultaneously.',
|
|
63
|
+
],
|
|
64
|
+
cursor: [
|
|
65
|
+
'Cursor loads this rule from `.cursor/rules/` (`alwaysApply: true` in the frontmatter).',
|
|
66
|
+
'Treat the spine as the single source of truth; this rule only adds Cursor-specific notes.',
|
|
67
|
+
],
|
|
68
|
+
'codex-cli': [
|
|
69
|
+
'Codex CLI follows the AGENTS.md open standard (https://agents.md/).',
|
|
70
|
+
'This file relays to the repo-root spine; prefer reading the spine directly when both exist.',
|
|
71
|
+
],
|
|
72
|
+
opencode: [
|
|
73
|
+
'OpenCode follows the AGENTS.md open standard (https://agents.md/).',
|
|
74
|
+
'This file relays to the repo-root spine; prefer reading the spine directly when both exist.',
|
|
75
|
+
],
|
|
76
|
+
aider: [
|
|
77
|
+
'Aider does not auto-load convention files: add `read: CONVENTIONS.md` to `.aider.conf.yml` or run `/read CONVENTIONS.md`.',
|
|
78
|
+
'Treat the spine as the single source of truth; this file only adds Aider-specific notes.',
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Fallback notes for adapter tools added to the registry later. */
|
|
83
|
+
function buildDefaultNotes(tool, file) {
|
|
84
|
+
return [
|
|
85
|
+
`${tool} reads \`${file}\`.`,
|
|
86
|
+
'Treat the spine as the single source of truth; this file only adds tool-specific notes.',
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Validate the targets contract this generator relies on.
|
|
92
|
+
* (loadRegistry validates packs; targets are validated here.)
|
|
93
|
+
*/
|
|
94
|
+
function validateTargets(registry) {
|
|
95
|
+
const t = registry.targets;
|
|
96
|
+
if (!t || typeof t !== 'object') {
|
|
97
|
+
throw new Error('registry.yaml: missing top-level "targets" mapping');
|
|
98
|
+
}
|
|
99
|
+
if (typeof t.spine !== 'string' || t.spine === '') {
|
|
100
|
+
throw new Error('registry.yaml: targets.spine must be a non-empty string');
|
|
101
|
+
}
|
|
102
|
+
if (!Array.isArray(t.adapters) || t.adapters.length === 0) {
|
|
103
|
+
throw new Error('registry.yaml: targets.adapters must be a non-empty list');
|
|
104
|
+
}
|
|
105
|
+
t.adapters.forEach((a, i) => {
|
|
106
|
+
if (!a || typeof a.tool !== 'string' || a.tool === '' || typeof a.file !== 'string' || a.file === '') {
|
|
107
|
+
throw new Error(`registry.yaml: targets.adapters[${i}] must have string "tool" and "file"`);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
return registry;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Sum of list lengths for a given key across a pack's categories. */
|
|
114
|
+
function sumCategories(pack, key) {
|
|
115
|
+
if (!Array.isArray(pack.categories)) return 0;
|
|
116
|
+
return pack.categories.reduce((n, c) => n + (Array.isArray(c[key]) ? c[key].length : 0), 0);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Facts DERIVED from the registry — every number here is computed from the
|
|
121
|
+
* declared lists, so adapters can never contradict registry.yaml.
|
|
122
|
+
*/
|
|
123
|
+
function buildRegistryFacts(registry) {
|
|
124
|
+
const packEntries = Object.entries(registry.packs).sort((a, b) => a[1].order - b[1].order);
|
|
125
|
+
const packs = packEntries.map(([id, p]) => {
|
|
126
|
+
const fact = {
|
|
127
|
+
id,
|
|
128
|
+
name: p.cli.name,
|
|
129
|
+
displayName: p.display_name || p.cli.name,
|
|
130
|
+
desc: p.cli.desc,
|
|
131
|
+
agentCount: p.agents.length,
|
|
132
|
+
required: p.required === true,
|
|
133
|
+
};
|
|
134
|
+
const categoryAgents = sumCategories(p, 'agents');
|
|
135
|
+
const categoryWorkflows = sumCategories(p, 'workflows');
|
|
136
|
+
if (categoryAgents > 0) fact.categoryAgentCount = categoryAgents;
|
|
137
|
+
if (Array.isArray(p.sub_agents)) fact.subAgentCount = p.sub_agents.length;
|
|
138
|
+
if (Array.isArray(p.workflows)) fact.workflowCount = p.workflows.length;
|
|
139
|
+
if (categoryWorkflows > 0) fact.workflowCount = (fact.workflowCount || 0) + categoryWorkflows;
|
|
140
|
+
if (Array.isArray(p.compliance_tags) && p.compliance_tags.length > 0) {
|
|
141
|
+
fact.frameworkCount = p.compliance_tags.length;
|
|
142
|
+
}
|
|
143
|
+
return fact;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
product: {
|
|
148
|
+
code: registry.product.code,
|
|
149
|
+
displayName: registry.product.display_name,
|
|
150
|
+
version: registry.product.version,
|
|
151
|
+
derivedFrom: registry.product.derived_from,
|
|
152
|
+
},
|
|
153
|
+
spine: registry.targets.spine,
|
|
154
|
+
modelsSupported: [...(registry.targets.models_supported || [])],
|
|
155
|
+
packCount: packs.length,
|
|
156
|
+
installerAgentCount: packs.reduce((n, p) => n + p.agentCount, 0),
|
|
157
|
+
packs,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Group targets.adapters[] by output file → [{ file, tools: [tool, ...] }]. */
|
|
162
|
+
function groupAdaptersByFile(registry) {
|
|
163
|
+
const byFile = new Map();
|
|
164
|
+
for (const { tool, file } of registry.targets.adapters) {
|
|
165
|
+
if (!byFile.has(file)) byFile.set(file, []);
|
|
166
|
+
const tools = byFile.get(file);
|
|
167
|
+
if (!tools.includes(tool)) tools.push(tool);
|
|
168
|
+
}
|
|
169
|
+
return [...byFile.entries()].map(([file, tools]) => ({ file, tools }));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Render the computed-facts section shared by every adapter. */
|
|
173
|
+
function renderFactsSection(facts) {
|
|
174
|
+
const lines = [
|
|
175
|
+
'## Registry facts (computed from registry.yaml — never hand-typed)',
|
|
176
|
+
'',
|
|
177
|
+
`- Product: ${facts.product.displayName} v${facts.product.version} (derived from ${facts.product.derivedFrom})`,
|
|
178
|
+
`- Models supported: ${facts.modelsSupported.join(', ')} (model-agnostic by contract)`,
|
|
179
|
+
`- Packs (${facts.packCount}): ${facts.packs.map((p) => p.name).join(', ')}`,
|
|
180
|
+
`- Installer agents (${facts.installerAgentCount} across all packs):`,
|
|
181
|
+
];
|
|
182
|
+
for (const p of facts.packs) {
|
|
183
|
+
const extras = [];
|
|
184
|
+
if (p.subAgentCount) extras.push(`${p.subAgentCount} sub-agents`);
|
|
185
|
+
if (p.categoryAgentCount) extras.push(`${p.categoryAgentCount} specialized agents`);
|
|
186
|
+
if (p.workflowCount) extras.push(`${p.workflowCount} workflows`);
|
|
187
|
+
if (p.frameworkCount) extras.push(`${p.frameworkCount} compliance frameworks`);
|
|
188
|
+
const suffix = extras.length > 0 ? ` — ${extras.join(', ')}` : '';
|
|
189
|
+
lines.push(
|
|
190
|
+
` - ${p.name}${p.required ? ' (required)' : ''}: ${p.agentCount} installer agent${p.agentCount === 1 ? '' : 's'} — ${p.desc}${suffix}`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
return lines;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Render the project-context + hand-authored instruction block shared by the
|
|
198
|
+
* spine and every adapter. The context sentence is DERIVED from registry
|
|
199
|
+
* facts (product name / upstream), the instruction sections come verbatim
|
|
200
|
+
* from tools/build/adapters.config.js (the one hand-authored source).
|
|
201
|
+
*/
|
|
202
|
+
function renderProjectInstructions(facts) {
|
|
203
|
+
return [
|
|
204
|
+
'## Project Context',
|
|
205
|
+
'',
|
|
206
|
+
`This project uses ${facts.product.displayName}, an augmented AI-driven development framework.`,
|
|
207
|
+
`Based on ${facts.product.derivedFrom} with multi-role agents, autopilot mode, and parallel execution.`,
|
|
208
|
+
'',
|
|
209
|
+
'<!-- Hand-authored project instructions — single source: tools/build/adapters.config.js. -->',
|
|
210
|
+
'<!-- Edit the config, then regenerate; never edit this file directly. -->',
|
|
211
|
+
'',
|
|
212
|
+
...PROJECT_INSTRUCTIONS,
|
|
213
|
+
];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Render the shared key-commands footer. */
|
|
217
|
+
function renderKeyCommands() {
|
|
218
|
+
return [
|
|
219
|
+
'## Key commands',
|
|
220
|
+
'',
|
|
221
|
+
'- `bmad-help` — Show all available agents and skills',
|
|
222
|
+
'- `autopilot` — Launch the orchestrator (Nexus) in full pipeline mode',
|
|
223
|
+
'- `parallel` — Enable parallel multi-agent execution',
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Render ONE adapter file body (pure — no clock, no randomness, no I/O).
|
|
229
|
+
*
|
|
230
|
+
* @param {object} facts - buildRegistryFacts() output
|
|
231
|
+
* @param {string} file - adapter file path as declared in the registry
|
|
232
|
+
* @param {string[]} tools - tools consuming this file
|
|
233
|
+
* @returns {string} full file content
|
|
234
|
+
*/
|
|
235
|
+
function renderAdapterContent(facts, file, tools) {
|
|
236
|
+
const spine = facts.spine;
|
|
237
|
+
const toolLabel = tools.join(' + ');
|
|
238
|
+
const lines = [];
|
|
239
|
+
|
|
240
|
+
if (file.endsWith('.mdc')) {
|
|
241
|
+
lines.push(
|
|
242
|
+
'---',
|
|
243
|
+
`description: "${facts.product.displayName} agents adapter (generated from registry.yaml)"`,
|
|
244
|
+
'alwaysApply: true',
|
|
245
|
+
'---',
|
|
246
|
+
''
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
lines.push(
|
|
251
|
+
`<!-- AUTO-GENERATED by tools/build/generate-adapters.js — DO NOT EDIT. -->`,
|
|
252
|
+
`<!-- Source of truth: registry.yaml → spine ${spine}. Regenerate: node tools/build/generate-adapters.js -->`,
|
|
253
|
+
'',
|
|
254
|
+
`# ${facts.product.displayName} — Adapter for ${toolLabel}`,
|
|
255
|
+
'',
|
|
256
|
+
`This file is a THIN adapter. The single source of truth for agents, packs,`,
|
|
257
|
+
`skills, and workflows is the spine file: **${spine}** (project root).`,
|
|
258
|
+
`Read ${spine} first; this adapter only adds tool-specific invocation notes.`,
|
|
259
|
+
''
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
for (const tool of tools) {
|
|
263
|
+
lines.push(`## Tool notes — ${tool}`, '');
|
|
264
|
+
for (const note of TOOL_NOTES[tool] || buildDefaultNotes(tool, file)) {
|
|
265
|
+
lines.push(`- ${note}`);
|
|
266
|
+
}
|
|
267
|
+
lines.push('');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Full project instructions are inlined in EVERY adapter (not only the
|
|
271
|
+
// spine) because most tools auto-load only their own file — a thin pointer
|
|
272
|
+
// would silently lose behavior for tools that never open the spine.
|
|
273
|
+
lines.push(...renderProjectInstructions(facts), '');
|
|
274
|
+
|
|
275
|
+
lines.push(...renderFactsSection(facts));
|
|
276
|
+
|
|
277
|
+
lines.push('', ...renderKeyCommands(), '');
|
|
278
|
+
|
|
279
|
+
return lines.join('\n');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Render the SPINE file (targets.spine, i.e. AGENTS.md) — the single source
|
|
284
|
+
* of truth document following the AGENTS.md open standard (https://agents.md/).
|
|
285
|
+
* Pure: derived facts from the registry + hand-authored config sections.
|
|
286
|
+
*/
|
|
287
|
+
function renderSpineContent(facts) {
|
|
288
|
+
const lines = [
|
|
289
|
+
`<!-- AUTO-GENERATED by tools/build/generate-adapters.js — DO NOT EDIT. -->`,
|
|
290
|
+
`<!-- Sources of truth: registry.yaml (facts) + tools/build/adapters.config.js (hand-authored sections). -->`,
|
|
291
|
+
`<!-- Regenerate: node tools/build/generate-adapters.js --adopt -->`,
|
|
292
|
+
'',
|
|
293
|
+
`# ${facts.product.displayName} — Agent Spine`,
|
|
294
|
+
'',
|
|
295
|
+
`This file is the SPINE: the single source of truth for agents, packs, skills,`,
|
|
296
|
+
`and workflows, following the AGENTS.md open standard (https://agents.md/).`,
|
|
297
|
+
`Tool-specific adapter files (CLAUDE.md, GEMINI.md, .cursor/rules/, .codex/,`,
|
|
298
|
+
`.opencode/, CONVENTIONS.md) are generated from the same registry and only add`,
|
|
299
|
+
`tool-specific invocation notes.`,
|
|
300
|
+
'',
|
|
301
|
+
...renderProjectInstructions(facts),
|
|
302
|
+
'',
|
|
303
|
+
...renderFactsSection(facts),
|
|
304
|
+
'',
|
|
305
|
+
...renderKeyCommands(),
|
|
306
|
+
'',
|
|
307
|
+
];
|
|
308
|
+
return lines.join('\n');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Generate every adapter from a loaded registry.
|
|
313
|
+
* @returns {Array<{ file: string, tools: string[], content: string }>}
|
|
314
|
+
*/
|
|
315
|
+
function generateAllAdapters(registry) {
|
|
316
|
+
validateTargets(registry);
|
|
317
|
+
const facts = buildRegistryFacts(registry);
|
|
318
|
+
return groupAdaptersByFile(registry).map(({ file, tools }) => ({
|
|
319
|
+
file,
|
|
320
|
+
tools,
|
|
321
|
+
content: renderAdapterContent(facts, file, tools),
|
|
322
|
+
}));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Generate the FULL file set: the spine (targets.spine) + every adapter.
|
|
327
|
+
* This is what adoption/preview/check operate on.
|
|
328
|
+
* @returns {Array<{ file: string, tools: string[], spine?: true, content: string }>}
|
|
329
|
+
*/
|
|
330
|
+
function generateAllFiles(registry) {
|
|
331
|
+
validateTargets(registry);
|
|
332
|
+
const facts = buildRegistryFacts(registry);
|
|
333
|
+
const spine = {
|
|
334
|
+
file: registry.targets.spine,
|
|
335
|
+
tools: ['spine'],
|
|
336
|
+
spine: true,
|
|
337
|
+
content: renderSpineContent(facts),
|
|
338
|
+
};
|
|
339
|
+
return [spine, ...generateAllAdapters(registry)];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Adoption guard: list on-disk targets under outDir that exist but do NOT
|
|
344
|
+
* carry the AUTO-GENERATED marker (i.e. hand-authored files that would be
|
|
345
|
+
* clobbered). Overwriting them requires an explicit --force.
|
|
346
|
+
*/
|
|
347
|
+
function listAdoptionBlockers(outDir, files) {
|
|
348
|
+
const blockers = [];
|
|
349
|
+
for (const { file } of files) {
|
|
350
|
+
const target = path.join(outDir, file);
|
|
351
|
+
if (!fs.existsSync(target)) continue;
|
|
352
|
+
const current = fs.readFileSync(target, 'utf8');
|
|
353
|
+
if (!current.includes(GENERATED_MARKER)) blockers.push(file);
|
|
354
|
+
}
|
|
355
|
+
return blockers;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Write generated spine + adapters under outDir, preserving relative paths. */
|
|
359
|
+
function writeAdapters({ registryPath = DEFAULT_REGISTRY_PATH, outDir = DEFAULT_OUT_DIR } = {}) {
|
|
360
|
+
const files = generateAllFiles(loadRegistry(registryPath));
|
|
361
|
+
const written = [];
|
|
362
|
+
for (const { file, content } of files) {
|
|
363
|
+
const target = path.join(outDir, file);
|
|
364
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
365
|
+
fs.writeFileSync(target, content, 'utf8');
|
|
366
|
+
written.push(target);
|
|
367
|
+
}
|
|
368
|
+
return written;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* `--check` mode: prove on-disk adapter files under outDir match what the
|
|
373
|
+
* registry generates. Returns { ok, mismatches } — never throws on drift.
|
|
374
|
+
*/
|
|
375
|
+
// Compare content ignoring line-ending style: git may rewrite LF→CRLF on
|
|
376
|
+
// checkout (Windows), so a byte comparison would false-positive "drift" on a
|
|
377
|
+
// fresh clone even when the content is identical.
|
|
378
|
+
const normalizeEol = (s) => s.replace(/\r\n/g, '\n');
|
|
379
|
+
|
|
380
|
+
function check({ registryPath = DEFAULT_REGISTRY_PATH, outDir = DEFAULT_OUT_DIR } = {}) {
|
|
381
|
+
const files = generateAllFiles(loadRegistry(registryPath));
|
|
382
|
+
const mismatches = [];
|
|
383
|
+
for (const { file, content } of files) {
|
|
384
|
+
const target = path.join(outDir, file);
|
|
385
|
+
if (!fs.existsSync(target)) {
|
|
386
|
+
mismatches.push(`${file}: missing on disk (expected at ${target})`);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const current = fs.readFileSync(target, 'utf8');
|
|
390
|
+
if (normalizeEol(current) !== normalizeEol(content)) {
|
|
391
|
+
mismatches.push(`${file}: content differs from what registry.yaml generates`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return { ok: mismatches.length === 0, mismatches };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/* ── CLI ────────────────────────────────────────────────────────────────── */
|
|
398
|
+
|
|
399
|
+
function parseOutDir(args) {
|
|
400
|
+
const idx = args.indexOf('--out-dir');
|
|
401
|
+
if (args.includes('--adopt')) {
|
|
402
|
+
if (idx !== -1) throw new Error('--adopt and --out-dir are mutually exclusive (--adopt targets the repo root)');
|
|
403
|
+
return REPO_ROOT;
|
|
404
|
+
}
|
|
405
|
+
if (idx === -1) return DEFAULT_OUT_DIR;
|
|
406
|
+
const value = args[idx + 1];
|
|
407
|
+
if (!value) throw new Error('--out-dir requires a directory path');
|
|
408
|
+
return path.resolve(value);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function main(argv) {
|
|
412
|
+
const args = argv.slice(2);
|
|
413
|
+
let outDir;
|
|
414
|
+
try {
|
|
415
|
+
outDir = parseOutDir(args);
|
|
416
|
+
} catch (err) {
|
|
417
|
+
console.error(err.message);
|
|
418
|
+
return 1;
|
|
419
|
+
}
|
|
420
|
+
const adopting = args.includes('--adopt');
|
|
421
|
+
const label = adopting ? 'root adapters + spine' : 'adapters';
|
|
422
|
+
|
|
423
|
+
if (args.includes('--check')) {
|
|
424
|
+
const result = check({ outDir });
|
|
425
|
+
if (result.ok) {
|
|
426
|
+
console.log(`OK — ${label} under ${outDir} match registry.yaml exactly (no drift).`);
|
|
427
|
+
return 0;
|
|
428
|
+
}
|
|
429
|
+
console.error(`DRIFT DETECTED between registry.yaml and ${label} under ${outDir}:`);
|
|
430
|
+
for (const m of result.mismatches) console.error(` - ${m}`);
|
|
431
|
+
console.error(
|
|
432
|
+
'Fix: node tools/build/generate-adapters.js' +
|
|
433
|
+
(adopting ? ' --adopt' : outDir === DEFAULT_OUT_DIR ? '' : ` --out-dir ${outDir}`)
|
|
434
|
+
);
|
|
435
|
+
return 1;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (args.includes('--print')) {
|
|
439
|
+
for (const { file, content } of generateAllFiles(loadRegistry())) {
|
|
440
|
+
console.log(`──── ${file} ────`);
|
|
441
|
+
console.log(content);
|
|
442
|
+
}
|
|
443
|
+
return 0;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (adopting && !args.includes('--force')) {
|
|
447
|
+
const blockers = listAdoptionBlockers(outDir, generateAllFiles(loadRegistry()));
|
|
448
|
+
if (blockers.length > 0) {
|
|
449
|
+
console.error('REFUSING to overwrite hand-authored root files (no AUTO-GENERATED marker):');
|
|
450
|
+
for (const b of blockers) console.error(` - ${b}`);
|
|
451
|
+
console.error('Fold their content into tools/build/adapters.config.js (or registry.yaml),');
|
|
452
|
+
console.error('then re-run with: node tools/build/generate-adapters.js --adopt --force');
|
|
453
|
+
return 1;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const written = writeAdapters({ outDir });
|
|
458
|
+
for (const target of written) console.log(`Generated ${target}`);
|
|
459
|
+
return 0;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (require.main === module) {
|
|
463
|
+
process.exitCode = main(process.argv);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
module.exports = {
|
|
467
|
+
DEFAULT_OUT_DIR,
|
|
468
|
+
REPO_ROOT,
|
|
469
|
+
GENERATED_MARKER,
|
|
470
|
+
TOOL_NOTES,
|
|
471
|
+
validateTargets,
|
|
472
|
+
buildRegistryFacts,
|
|
473
|
+
groupAdaptersByFile,
|
|
474
|
+
renderFactsSection,
|
|
475
|
+
renderProjectInstructions,
|
|
476
|
+
renderKeyCommands,
|
|
477
|
+
renderAdapterContent,
|
|
478
|
+
renderSpineContent,
|
|
479
|
+
generateAllAdapters,
|
|
480
|
+
generateAllFiles,
|
|
481
|
+
listAdoptionBlockers,
|
|
482
|
+
writeAdapters,
|
|
483
|
+
check,
|
|
484
|
+
main,
|
|
485
|
+
};
|