baldart 4.82.0 → 4.83.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 +32 -0
- package/README.md +4 -2
- package/VERSION +1 -1
- package/framework/.claude/hooks/agent-discovery-gate.js +4 -2
- package/framework/docs/ROOT-PRIMITIVES.md +98 -0
- package/framework/templates/primitives/AGENTS.CHANGELOG.md +18 -0
- package/framework/templates/primitives/AGENTS.md +111 -0
- package/framework/templates/primitives/CLAUDE.CHANGELOG.md +15 -0
- package/framework/templates/primitives/CLAUDE.md +41 -0
- package/package.json +1 -1
- package/src/commands/add.js +15 -0
- package/src/commands/doctor.js +65 -0
- package/src/commands/migrate.js +30 -0
- package/src/commands/update.js +31 -0
- package/src/utils/root-primitives.js +418 -0
- package/src/utils/symlinks.js +24 -10
- package/framework/AGENTS.md +0 -255
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const yaml = require('js-yaml');
|
|
4
|
+
const {
|
|
5
|
+
mergeOverlay,
|
|
6
|
+
shortSha,
|
|
7
|
+
MARKER_PREFIX,
|
|
8
|
+
isGeneratedFile,
|
|
9
|
+
} = require('./overlay-merger');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Root-file primitives writer (AGENTS.md + CLAUDE.md).
|
|
13
|
+
*
|
|
14
|
+
* BALDART ships versioned skeletons under
|
|
15
|
+
* `.framework/framework/templates/primitives/<NAME>.md`. This module resolves
|
|
16
|
+
* their `{{ slot }}` / `{{> partial }}` / `{{#flag}}` placeholders from
|
|
17
|
+
* `baldart.config.yml` (+ package.json / README probes) at install/update time,
|
|
18
|
+
* merges `.baldart/overlays/<NAME>.md` (OVERRIDE/APPEND/PREPEND via
|
|
19
|
+
* overlay-merger), stamps a `baldart-generated` marker, and writes the concrete
|
|
20
|
+
* file to the consumer repo root.
|
|
21
|
+
*
|
|
22
|
+
* The marker reuses overlay-merger's MARKER_PREFIX and keeps `kind`, `name`,
|
|
23
|
+
* `base_sha`, `overlay_sha` in the positions overlay-merger's `readMarker`
|
|
24
|
+
* expects — so `isGeneratedFile()` and the framework-edit-gate recognise these
|
|
25
|
+
* root files as generated. Two extra trailing fields (`config_sha`,
|
|
26
|
+
* `primitive_version`) are appended AFTER `overlay_sha`, which readMarker
|
|
27
|
+
* tolerates (it matches a prefix). `base_sha` carries the raw-template sha.
|
|
28
|
+
*
|
|
29
|
+
* States handled per file (checked in this order): generated (regenerate) →
|
|
30
|
+
* symlink (legacy AGENTS.md → replace) → handwritten (adopt) → absent (create).
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const FRAMEWORK_PAYLOAD = path.join('.framework', 'framework');
|
|
34
|
+
const PRIMITIVES = ['AGENTS', 'CLAUDE'];
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// small fs helpers (mirror configure.js detect() ergonomics)
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
function readSafe(p) { try { return fs.readFileSync(p, 'utf8'); } catch (_) { return ''; } }
|
|
40
|
+
function readJsonSafe(p) { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return null; } }
|
|
41
|
+
function existsAt(p) { try { fs.accessSync(p); return true; } catch (_) { return false; } }
|
|
42
|
+
|
|
43
|
+
function loadConfig(cwd) {
|
|
44
|
+
const p = path.join(cwd, 'baldart.config.yml');
|
|
45
|
+
try { return yaml.load(fs.readFileSync(p, 'utf8')) || {}; }
|
|
46
|
+
catch (_) { return {}; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readPrimitiveVersion(templateContent) {
|
|
50
|
+
const m = templateContent.match(/baldart-primitive:[^\n]*primitive_version=([0-9]+\.[0-9]+\.[0-9]+)/);
|
|
51
|
+
return m ? m[1] : '0.0.0';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// slot resolution — deterministic, no LLM, no invention
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
const PRETTY = {
|
|
58
|
+
nextjs: 'Next.js', remix: 'Remix', sveltekit: 'SvelteKit', astro: 'Astro',
|
|
59
|
+
nuxt: 'Nuxt', rails: 'Rails', django: 'Django', fastapi: 'FastAPI', express: 'Express',
|
|
60
|
+
supabase: 'Supabase', firestore: 'Firestore', postgres: 'Postgres', mysql: 'MySQL',
|
|
61
|
+
mongodb: 'MongoDB', dynamodb: 'DynamoDB', sqlite: 'SQLite',
|
|
62
|
+
'firebase-auth': 'Firebase Auth', 'supabase-auth': 'Supabase Auth', clerk: 'Clerk',
|
|
63
|
+
auth0: 'Auth0', cognito: 'Cognito', nextauth: 'NextAuth', lucia: 'Lucia',
|
|
64
|
+
playwright: 'Playwright', cypress: 'Cypress', biome: 'Biome', vitest: 'Vitest',
|
|
65
|
+
eslint: 'ESLint', prettier: 'Prettier', tsc: 'TypeScript', jest: 'Jest',
|
|
66
|
+
};
|
|
67
|
+
function pretty(v) { return PRETTY[v] || v; }
|
|
68
|
+
|
|
69
|
+
function detectPackageManager(cwd) {
|
|
70
|
+
if (existsAt(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
71
|
+
if (existsAt(path.join(cwd, 'yarn.lock'))) return 'yarn';
|
|
72
|
+
if (existsAt(path.join(cwd, 'bun.lockb'))) return 'bun';
|
|
73
|
+
return 'npm';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function deriveSourceRoot(cwd) {
|
|
77
|
+
for (const d of ['src', 'app', 'lib']) {
|
|
78
|
+
if (existsAt(path.join(cwd, d))) return `${d}/`;
|
|
79
|
+
}
|
|
80
|
+
return '';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function firstParagraph(readme) {
|
|
84
|
+
if (!readme) return '';
|
|
85
|
+
const lines = readme.split('\n');
|
|
86
|
+
let i = 0;
|
|
87
|
+
// skip leading blanks + a single H1
|
|
88
|
+
while (i < lines.length && lines[i].trim() === '') i++;
|
|
89
|
+
if (i < lines.length && /^#\s+/.test(lines[i])) i++;
|
|
90
|
+
while (i < lines.length && lines[i].trim() === '') i++;
|
|
91
|
+
const para = [];
|
|
92
|
+
while (i < lines.length && lines[i].trim() !== '' && !/^#{1,6}\s/.test(lines[i]) && !/^[-*]\s/.test(lines[i])) {
|
|
93
|
+
para.push(lines[i].trim());
|
|
94
|
+
i++;
|
|
95
|
+
}
|
|
96
|
+
const text = para.join(' ').trim();
|
|
97
|
+
return text.length > 200 ? text.slice(0, 197).trimEnd() + '…' : text;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolve every slot + partial + flag from config and repo probes.
|
|
102
|
+
* Returns { scalars, partials, flags, meta }.
|
|
103
|
+
*/
|
|
104
|
+
function resolveSlots(cwd, cfg) {
|
|
105
|
+
const pkg = readJsonSafe(path.join(cwd, 'package.json')) || {};
|
|
106
|
+
const readme = readSafe(path.join(cwd, 'README.md'));
|
|
107
|
+
const identity = cfg.identity || {};
|
|
108
|
+
const stack = cfg.stack || {};
|
|
109
|
+
const paths = cfg.paths || {};
|
|
110
|
+
const features = cfg.features || {};
|
|
111
|
+
const git = cfg.git || {};
|
|
112
|
+
const toolchain = cfg.toolchain || {};
|
|
113
|
+
const pm = detectPackageManager(cwd);
|
|
114
|
+
|
|
115
|
+
// identity.name
|
|
116
|
+
let name = identity.brand_name || pkg.name || path.basename(cwd);
|
|
117
|
+
if (typeof name === 'string' && name.startsWith('@') && name.includes('/')) name = name.split('/')[1];
|
|
118
|
+
|
|
119
|
+
// identity.description
|
|
120
|
+
const description = firstParagraph(readme)
|
|
121
|
+
|| (pkg.description ? String(pkg.description).trim() : '')
|
|
122
|
+
|| '<!-- TODO: one-line description of what this project is -->';
|
|
123
|
+
|
|
124
|
+
// stack.summary (compose present, pretty)
|
|
125
|
+
const stackParts = [];
|
|
126
|
+
if (stack.framework && stack.framework !== 'none') stackParts.push(pretty(stack.framework));
|
|
127
|
+
if (stack.database && stack.database !== 'none') stackParts.push(pretty(stack.database));
|
|
128
|
+
if (stack.auth_provider && stack.auth_provider !== 'none') stackParts.push(pretty(stack.auth_provider));
|
|
129
|
+
const e2e = stack.testing && stack.testing.e2e;
|
|
130
|
+
if (e2e) stackParts.push(pretty(e2e));
|
|
131
|
+
if (Array.isArray(toolchain.installed_tools)) {
|
|
132
|
+
for (const t of toolchain.installed_tools) { const p = pretty(t); if (!stackParts.includes(p)) stackParts.push(p); }
|
|
133
|
+
}
|
|
134
|
+
const stackSummary = stackParts.length ? stackParts.join(' · ') : 'the project stack';
|
|
135
|
+
|
|
136
|
+
// stack.node
|
|
137
|
+
let node = (pkg.engines && pkg.engines.node) ? String(pkg.engines.node).trim() : '';
|
|
138
|
+
if (!node) { const nvmrc = readSafe(path.join(cwd, '.nvmrc')).trim(); if (nvmrc) node = nvmrc.replace(/^v/, ''); }
|
|
139
|
+
const nodeSuffix = node ? ` Node ${node}.` : '';
|
|
140
|
+
|
|
141
|
+
// output style (Claude-only, personal settings)
|
|
142
|
+
const settingsLocal = readJsonSafe(path.join(cwd, '.claude', 'settings.local.json')) || {};
|
|
143
|
+
const outputStyle = typeof settingsLocal.outputStyle === 'string' ? settingsLocal.outputStyle : '';
|
|
144
|
+
|
|
145
|
+
// flags
|
|
146
|
+
const flags = {
|
|
147
|
+
has_backlog: features.has_backlog === true,
|
|
148
|
+
has_adrs: features.has_adrs === true,
|
|
149
|
+
has_i18n: features.has_i18n === true,
|
|
150
|
+
has_design_system: features.has_design_system === true,
|
|
151
|
+
has_toolchain: features.has_toolchain === true,
|
|
152
|
+
has_project_status: !!(paths.references_dir && existsAt(path.join(cwd, paths.references_dir, 'project-status.md'))),
|
|
153
|
+
output_style: !!outputStyle,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const scalars = {
|
|
157
|
+
'identity.name': name,
|
|
158
|
+
'identity.description': description,
|
|
159
|
+
'stack.summary': stackSummary,
|
|
160
|
+
'stack.node_suffix': nodeSuffix,
|
|
161
|
+
'git.trunk_branch': git.trunk_branch || 'main',
|
|
162
|
+
'git.feature_prefix': 'feat', // STATIC BALDART convention
|
|
163
|
+
'git.commit_format': '[FEAT-XXXX] description', // STATIC default (override via overlay)
|
|
164
|
+
'i18n.registry': paths.i18n_registry || 'the i18n registry',
|
|
165
|
+
'output_style': outputStyle,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const partials = {
|
|
169
|
+
commands_section: buildCommandsSection(pkg, toolchain, pm),
|
|
170
|
+
repo_map: buildRepoMap(cwd, paths, flags),
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
return { scalars, partials, flags };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function buildCommandsSection(pkg, toolchain, pm) {
|
|
177
|
+
const scripts = (pkg && pkg.scripts) || {};
|
|
178
|
+
const tc = (toolchain && toolchain.commands) || {};
|
|
179
|
+
const run = (s) => `${pm} run ${s}`;
|
|
180
|
+
const rows = [];
|
|
181
|
+
const push = (label, cmd) => { if (cmd) rows.push(`| \`${cmd}\` | ${label} |`); };
|
|
182
|
+
push('Run the app (dev)', scripts.dev ? run('dev') : (scripts.start ? run('start') : ''));
|
|
183
|
+
push('Production build', tc.build || (scripts.build ? run('build') : ''));
|
|
184
|
+
push('Lint', tc.lint || (scripts.lint ? run('lint') : ''));
|
|
185
|
+
push('Type check', tc.typecheck || (scripts.typecheck ? run('typecheck') : ''));
|
|
186
|
+
push('Unit tests', tc.test || (scripts.test ? run('test') : ''));
|
|
187
|
+
push('E2E tests', scripts['test:e2e'] ? run('test:e2e') : '');
|
|
188
|
+
push('Full QA gate', scripts.qa ? run('qa') : '');
|
|
189
|
+
if (!rows.length) return '';
|
|
190
|
+
return '## Commands\n\n| Command | Purpose |\n|---|---|\n' + rows.join('\n') + '\n';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function buildRepoMap(cwd, paths, flags) {
|
|
194
|
+
const lines = ['- `AGENTS.md` (this file) + `CLAUDE.md` (Claude Code entry).'];
|
|
195
|
+
const sourceRoot = deriveSourceRoot(cwd);
|
|
196
|
+
if (sourceRoot) lines.push(`- \`${sourceRoot}\` — application code.`);
|
|
197
|
+
if (flags.has_backlog && paths.backlog_dir) lines.push(`- \`${paths.backlog_dir}\` — work cards.`);
|
|
198
|
+
if (paths.references_dir) lines.push(`- \`${paths.references_dir}\` — data model, API, status, history.`);
|
|
199
|
+
if (flags.has_adrs && paths.adrs_dir) lines.push(`- \`${paths.adrs_dir}\` — architectural decision records.`);
|
|
200
|
+
if (flags.has_design_system && paths.design_system) lines.push(`- \`${paths.design_system}\` — design-system SSOT.`);
|
|
201
|
+
return lines.join('\n');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// minimal template engine: {{#flag}}…{{/flag}}, {{> partial}}, {{ scalar }}
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
function fillTemplate(template, { scalars, partials, flags }) {
|
|
208
|
+
// 1. strip leading authoring banner (<!-- baldart-primitive: … -->)
|
|
209
|
+
let out = template.replace(/^?\s*<!--\s*baldart-primitive:[\s\S]*?-->\s*\n/, '');
|
|
210
|
+
|
|
211
|
+
// 2. conditionals (non-nested; each flag block resolved independently)
|
|
212
|
+
out = out.replace(/\{\{#([\w.]+)\}\}\n?([\s\S]*?)\{\{\/\1\}\}\n?/g, (_, key, body) =>
|
|
213
|
+
(flags[key] ? body : ''));
|
|
214
|
+
|
|
215
|
+
// 3. partials (whole-line replacement)
|
|
216
|
+
out = out.replace(/^[ \t]*\{\{>\s*([\w.]+)\s*\}\}[ \t]*$/gm, (_, key) =>
|
|
217
|
+
(partials[key] != null ? partials[key] : ''));
|
|
218
|
+
|
|
219
|
+
// 4. scalars
|
|
220
|
+
out = out.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, key) =>
|
|
221
|
+
(scalars[key] != null ? String(scalars[key]) : ''));
|
|
222
|
+
|
|
223
|
+
// 5. cleanup — drop empty bullets/rows, collapse blank runs, single trailing NL
|
|
224
|
+
out = out
|
|
225
|
+
.split('\n')
|
|
226
|
+
.filter((l) => !/^\s*[-|]\s*$/.test(l))
|
|
227
|
+
.join('\n')
|
|
228
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
229
|
+
.replace(/\s+$/, '') + '\n';
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// marker
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
function buildRootMarker({ name, templateSha, overlaySha, configSha, primitiveVersion }) {
|
|
237
|
+
return `${MARKER_PREFIX} kind=root name=${name} base_sha=${templateSha} overlay_sha=${overlaySha || 'none'} config_sha=${configSha} primitive_version=${primitiveVersion}
|
|
238
|
+
DO NOT EDIT MANUALLY. Edit .baldart/overlays/${name}.md instead;
|
|
239
|
+
this file is regenerated by \`npx baldart update\`. -->`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function readRootMarker(content) {
|
|
243
|
+
const idx = content.indexOf(MARKER_PREFIX);
|
|
244
|
+
if (idx === -1) return null;
|
|
245
|
+
const block = content.slice(idx, idx + 4000);
|
|
246
|
+
const end = block.indexOf('-->');
|
|
247
|
+
if (end === -1) return null;
|
|
248
|
+
const seg = block.slice(0, end);
|
|
249
|
+
const kind = (seg.match(/kind=(\S+)/) || [])[1];
|
|
250
|
+
if (kind !== 'root') return null;
|
|
251
|
+
return {
|
|
252
|
+
name: (seg.match(/name=(\S+)/) || [])[1] || null,
|
|
253
|
+
templateSha: (seg.match(/base_sha=(\S+)/) || [])[1] || null,
|
|
254
|
+
overlaySha: (seg.match(/overlay_sha=(\S+)/) || [])[1] || null,
|
|
255
|
+
configSha: (seg.match(/config_sha=(\S+)/) || [])[1] || null,
|
|
256
|
+
primitiveVersion: (seg.match(/primitive_version=(\S+)/) || [])[1] || null,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
// build the generated content for one primitive (no fs side effects)
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
function renderPrimitive({ cwd, name, cfg, frameworkVersion, overlayContent }) {
|
|
264
|
+
const templatePath = path.join(cwd, FRAMEWORK_PAYLOAD, 'templates', 'primitives', `${name}.md`);
|
|
265
|
+
const template = readSafe(templatePath);
|
|
266
|
+
if (!template) return null;
|
|
267
|
+
const primitiveVersion = readPrimitiveVersion(template);
|
|
268
|
+
const resolved = resolveSlots(cwd, cfg);
|
|
269
|
+
const filled = fillTemplate(template, resolved);
|
|
270
|
+
|
|
271
|
+
// section-merge overlay via the proven engine, then swap in our own marker.
|
|
272
|
+
const { generated } = mergeOverlay({
|
|
273
|
+
kind: 'root', name, baseContent: filled,
|
|
274
|
+
overlayContent: overlayContent || '', frameworkVersion,
|
|
275
|
+
});
|
|
276
|
+
// mergeOverlay puts its marker first (no frontmatter on root files); strip it.
|
|
277
|
+
let body = generated;
|
|
278
|
+
const mstart = body.indexOf(MARKER_PREFIX);
|
|
279
|
+
if (mstart !== -1) {
|
|
280
|
+
const mend = body.indexOf('-->', mstart);
|
|
281
|
+
if (mend !== -1) body = body.slice(mend + 3).replace(/^\n/, '');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const templateSha = shortSha(template);
|
|
285
|
+
const overlaySha = overlayContent ? shortSha(overlayContent) : 'none';
|
|
286
|
+
const configSha = shortSha(JSON.stringify(resolved) + '|' + templateSha);
|
|
287
|
+
const marker = buildRootMarker({ name, templateSha, overlaySha, configSha, primitiveVersion });
|
|
288
|
+
return { content: `${marker}\n${body}`.replace(/\s+$/, '') + '\n', templateSha, overlaySha, configSha, primitiveVersion };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// state detection
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
function detectState(cwd, name) {
|
|
295
|
+
const rootPath = path.join(cwd, `${name}.md`);
|
|
296
|
+
let lstat = null;
|
|
297
|
+
try { lstat = fs.lstatSync(rootPath); } catch (_) { return { state: 'absent', content: '' }; }
|
|
298
|
+
if (lstat.isSymbolicLink()) return { state: 'symlink', content: '' };
|
|
299
|
+
const content = readSafe(rootPath);
|
|
300
|
+
if (isGeneratedFile(content) && readRootMarker(content)) return { state: 'generated', content };
|
|
301
|
+
return { state: 'handwritten', content };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
// public: status (for doctor) — does NOT write
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
function rootPrimitiveStatus({ cwd, name, cfg }) {
|
|
308
|
+
cfg = cfg || loadConfig(cwd);
|
|
309
|
+
const { state, content } = detectState(cwd, name);
|
|
310
|
+
if (state === 'absent') return { state, stale: true, reason: 'missing' };
|
|
311
|
+
if (state === 'symlink') return { state, stale: true, reason: 'legacy-symlink' };
|
|
312
|
+
if (state === 'handwritten') return { state, stale: false, reason: 'handwritten (needs adoption — run interactively)' };
|
|
313
|
+
// generated → compare marker vs freshly rendered
|
|
314
|
+
const rendered = renderPrimitive({ cwd, name, cfg, frameworkVersion: readFrameworkVersion(cwd), overlayContent: readOverlay(cwd, name) });
|
|
315
|
+
if (!rendered) return { state, stale: false, reason: 'no-template' };
|
|
316
|
+
const stale = content.replace(/\s+$/, '') !== rendered.content.replace(/\s+$/, '');
|
|
317
|
+
return { state, stale, reason: stale ? 'template / overlay / config drift' : 'up-to-date' };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function readOverlay(cwd, name) {
|
|
321
|
+
return readSafe(path.join(cwd, '.baldart', 'overlays', `${name}.md`));
|
|
322
|
+
}
|
|
323
|
+
function readFrameworkVersion(cwd) {
|
|
324
|
+
// The subtree pulls the whole repo into `.framework/`, so the VERSION file
|
|
325
|
+
// (repo root) lands at `.framework/VERSION` — NOT under the `framework/`
|
|
326
|
+
// payload subdir. (Informational only: feeds mergeOverlay's baseVersionTargeted.)
|
|
327
|
+
return readSafe(path.join(cwd, '.framework', 'VERSION')).trim() || 'unknown';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function isAutonomous(opts) {
|
|
331
|
+
if (opts && typeof opts.autonomous === 'boolean') return opts.autonomous;
|
|
332
|
+
return !!(process.env.CI || process.env.BALDART_AUTONOMOUS) || !process.stdout.isTTY;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
// public: write all root primitives (add / update / doctor / migrate)
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
/**
|
|
339
|
+
* @param {Object} opts
|
|
340
|
+
* @param {string} opts.cwd
|
|
341
|
+
* @param {string[]} [opts.tools] enabled tools; CLAUDE.md needs 'claude'
|
|
342
|
+
* @param {boolean} [opts.autonomous] CI/unattended → never mutate a handwritten file
|
|
343
|
+
* @returns {{ written:[], adopted:[], skipped:[], blockers:[] }}
|
|
344
|
+
*/
|
|
345
|
+
function writeRootPrimitives(opts = {}) {
|
|
346
|
+
const cwd = opts.cwd || process.cwd();
|
|
347
|
+
const tools = (opts.tools && opts.tools.length) ? opts.tools : ['claude'];
|
|
348
|
+
const UI = safeUI();
|
|
349
|
+
const cfg = loadConfig(cwd);
|
|
350
|
+
const frameworkVersion = readFrameworkVersion(cwd);
|
|
351
|
+
const autonomous = isAutonomous(opts);
|
|
352
|
+
const result = { written: [], adopted: [], skipped: [], blockers: [] };
|
|
353
|
+
|
|
354
|
+
for (const name of PRIMITIVES) {
|
|
355
|
+
// CLAUDE.md is Claude-only.
|
|
356
|
+
if (name === 'CLAUDE' && !tools.includes('claude')) { result.skipped.push({ name, reason: 'claude-disabled' }); continue; }
|
|
357
|
+
|
|
358
|
+
const templatePath = path.join(cwd, FRAMEWORK_PAYLOAD, 'templates', 'primitives', `${name}.md`);
|
|
359
|
+
if (!existsAt(templatePath)) { result.skipped.push({ name, reason: 'no-template' }); continue; }
|
|
360
|
+
|
|
361
|
+
const rootPath = path.join(cwd, `${name}.md`);
|
|
362
|
+
const overlayPath = path.join(cwd, '.baldart', 'overlays', `${name}.md`);
|
|
363
|
+
const { state, content } = detectState(cwd, name);
|
|
364
|
+
|
|
365
|
+
// Adoption: a handwritten file with no overlay yet.
|
|
366
|
+
if (state === 'handwritten' && !existsAt(overlayPath)) {
|
|
367
|
+
if (autonomous) {
|
|
368
|
+
result.blockers.push({ name, reason: `${name}.md è scritto a mano — esegui \`npx baldart\` in modalità interattiva per adottarlo in un overlay (nessuna modifica applicata ora)` });
|
|
369
|
+
result.skipped.push({ name, reason: 'handwritten-autonomous' });
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
// Preserve the handwritten file verbatim into the overlay (zero-loss on disk).
|
|
373
|
+
try {
|
|
374
|
+
fs.mkdirSync(path.dirname(overlayPath), { recursive: true });
|
|
375
|
+
fs.writeFileSync(overlayPath, content, 'utf8');
|
|
376
|
+
} catch (e) {
|
|
377
|
+
result.blockers.push({ name, reason: `impossibile scrivere l'overlay di adozione: ${e.message}` });
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
result.adopted.push({ name, overlay: path.relative(cwd, overlayPath) });
|
|
381
|
+
UI && UI.info(`Adottato ${name}.md → ${path.relative(cwd, overlayPath)} (verbatim, zero perdita). Rivedi e ri-smista: molte sezioni potrebbero appartenere all'overlay di AGENTS.md.`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Render + write (generated / symlink / absent / just-adopted).
|
|
385
|
+
const overlayContent = readOverlay(cwd, name);
|
|
386
|
+
const rendered = renderPrimitive({ cwd, name, cfg, frameworkVersion, overlayContent });
|
|
387
|
+
if (!rendered) { result.skipped.push({ name, reason: 'render-failed' }); continue; }
|
|
388
|
+
|
|
389
|
+
// Byte-stable: skip write if unchanged (avoids spurious diffs on update).
|
|
390
|
+
if (state === 'generated' && content.replace(/\s+$/, '') === rendered.content.replace(/\s+$/, '')) {
|
|
391
|
+
result.skipped.push({ name, reason: 'up-to-date' });
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
if (state === 'symlink') { try { fs.unlinkSync(rootPath); } catch (_) {} }
|
|
396
|
+
fs.writeFileSync(rootPath, rendered.content, 'utf8');
|
|
397
|
+
result.written.push({ name, from: state });
|
|
398
|
+
UI && UI.success(`Root primitive scritto: ${name}.md (${state})`);
|
|
399
|
+
} catch (e) {
|
|
400
|
+
result.blockers.push({ name, reason: `impossibile scrivere ${name}.md: ${e.message}` });
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return result;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function safeUI() { try { return require('./ui'); } catch (_) { return null; } }
|
|
407
|
+
|
|
408
|
+
module.exports = {
|
|
409
|
+
writeRootPrimitives,
|
|
410
|
+
rootPrimitiveStatus,
|
|
411
|
+
renderPrimitive,
|
|
412
|
+
resolveSlots,
|
|
413
|
+
fillTemplate,
|
|
414
|
+
readRootMarker,
|
|
415
|
+
buildRootMarker,
|
|
416
|
+
detectState,
|
|
417
|
+
PRIMITIVES,
|
|
418
|
+
};
|
package/src/utils/symlinks.js
CHANGED
|
@@ -150,15 +150,12 @@ class SymlinkUtils {
|
|
|
150
150
|
async createBulkSymlinks(opts = {}) {
|
|
151
151
|
const mode = opts.mode || 'prompt';
|
|
152
152
|
|
|
153
|
-
// AGENTS.md
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
{ mode }
|
|
160
|
-
);
|
|
161
|
-
|
|
153
|
+
// NB: `AGENTS.md` is NO LONGER a bulk symlink since v4.83.0 — it is a
|
|
154
|
+
// generated real file (skeleton + slots + overlay) written by
|
|
155
|
+
// writeRootPrimitives(), which createAllSymlinks() invokes after the
|
|
156
|
+
// per-item merges. A pre-v4.83.0 install has AGENTS.md as a symlink; the
|
|
157
|
+
// writer detects that (state 'symlink') and replaces it with the generated
|
|
158
|
+
// file. The cross-tool `agents/` reference dir stays a bulk symlink.
|
|
162
159
|
await this.createSymlink(
|
|
163
160
|
path.join(FRAMEWORK_PAYLOAD, 'agents'),
|
|
164
161
|
'agents',
|
|
@@ -358,6 +355,22 @@ class SymlinkUtils {
|
|
|
358
355
|
this.mergeOutputStyles({ tools: opts.tools });
|
|
359
356
|
|
|
360
357
|
UI.newline();
|
|
358
|
+
UI.section('Writing Root Primitives');
|
|
359
|
+
this.writeRootPrimitives({ tools: opts.tools, autonomous: opts.autonomous });
|
|
360
|
+
|
|
361
|
+
UI.newline();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Generate the root-file primitives (AGENTS.md + CLAUDE.md) from the
|
|
366
|
+
* versioned skeletons under `.framework/framework/templates/primitives/`,
|
|
367
|
+
* filling slots from baldart.config.yml and merging `.baldart/overlays/`.
|
|
368
|
+
* Delegates to the standalone writer module (see src/utils/root-primitives.js).
|
|
369
|
+
*/
|
|
370
|
+
writeRootPrimitives(opts = {}) {
|
|
371
|
+
return require('./root-primitives').writeRootPrimitives({
|
|
372
|
+
cwd: this.cwd, tools: opts.tools, autonomous: opts.autonomous,
|
|
373
|
+
});
|
|
361
374
|
}
|
|
362
375
|
|
|
363
376
|
// -----------------------------------------------------------------------
|
|
@@ -695,8 +708,9 @@ class SymlinkUtils {
|
|
|
695
708
|
}
|
|
696
709
|
|
|
697
710
|
verifySymlinks() {
|
|
711
|
+
// NB: `AGENTS.md` is NOT here since v4.83.0 — it is a generated real file
|
|
712
|
+
// (writeRootPrimitives), no longer a bulk symlink. Only `agents/` remains.
|
|
698
713
|
const bulkSymlinks = [
|
|
699
|
-
'AGENTS.md',
|
|
700
714
|
'agents'
|
|
701
715
|
];
|
|
702
716
|
|