agentfold 0.1.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/LICENSE +21 -0
- package/README.md +197 -0
- package/agentfold.mjs +1115 -0
- package/lib/compose-layers.mjs +23 -0
- package/lib/diff.mjs +47 -0
- package/lib/load-profile.mjs +112 -0
- package/lib/manifest.mjs +66 -0
- package/lib/pipeline-steps.mjs +853 -0
- package/lib/scope.mjs +158 -0
- package/lib/util.mjs +348 -0
- package/lib/validate.mjs +209 -0
- package/package.json +42 -0
package/agentfold.mjs
ADDED
|
@@ -0,0 +1,1115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { resolveLayerPaths } from './lib/compose-layers.mjs';
|
|
7
|
+
import { diffRenderMapAgainstProject } from './lib/diff.mjs';
|
|
8
|
+
import { loadProfile } from './lib/load-profile.mjs';
|
|
9
|
+
import { loadManifest, pruneManagedFiles, writeManifest } from './lib/manifest.mjs';
|
|
10
|
+
import { buildRenderPlan } from './lib/pipeline-steps.mjs';
|
|
11
|
+
import {
|
|
12
|
+
createLogger,
|
|
13
|
+
ensureDir,
|
|
14
|
+
listFiles,
|
|
15
|
+
parseCliArgs,
|
|
16
|
+
pathExists,
|
|
17
|
+
readJson,
|
|
18
|
+
removeDir,
|
|
19
|
+
toPosixPath,
|
|
20
|
+
writeJson,
|
|
21
|
+
} from './lib/util.mjs';
|
|
22
|
+
import { validatePlan } from './lib/validate.mjs';
|
|
23
|
+
|
|
24
|
+
const selectableTypes = ['skills', 'prompts', 'rules', 'commands', 'mcp', 'subagents'];
|
|
25
|
+
|
|
26
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
27
|
+
const __dirname = path.dirname(__filename);
|
|
28
|
+
const kitRoot = __dirname;
|
|
29
|
+
|
|
30
|
+
function resolveConfigRoot(options) {
|
|
31
|
+
if (options.config) return path.resolve(String(options.config));
|
|
32
|
+
return path.join(os.homedir(), '.agentfold');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function usage() {
|
|
36
|
+
console.log(`agentfold — layered AI agent configuration generator
|
|
37
|
+
|
|
38
|
+
usage:
|
|
39
|
+
agentfold <command> [options]
|
|
40
|
+
|
|
41
|
+
commands:
|
|
42
|
+
init Bootstrap config hub (default: ~/.agentfold)
|
|
43
|
+
lint Validate selected layers/profile
|
|
44
|
+
build Render outputs into <config>/out
|
|
45
|
+
diff Compare rendered outputs with project files
|
|
46
|
+
apply Apply rendered outputs to project files
|
|
47
|
+
status Show current configuration summary
|
|
48
|
+
doctor Validate config hub and project health
|
|
49
|
+
explain Show why a content file is included/excluded
|
|
50
|
+
|
|
51
|
+
create-layer Scaffold a new layer directory
|
|
52
|
+
create-profile Scaffold a new profile JSON
|
|
53
|
+
list-layers List available layers
|
|
54
|
+
list-profiles List available profiles
|
|
55
|
+
list-tags List allowed scope tags
|
|
56
|
+
add-tag Add a scope tag
|
|
57
|
+
delete-tag Delete a scope tag
|
|
58
|
+
|
|
59
|
+
add-rule Add a rule file to a layer
|
|
60
|
+
add-skill Add a skill directory to a layer
|
|
61
|
+
add-subagent Add a subagent file to a layer
|
|
62
|
+
add-command Add a command file to a layer
|
|
63
|
+
add-reference Add a reference file to a layer
|
|
64
|
+
|
|
65
|
+
options:
|
|
66
|
+
--config <path> Config hub root (default: ~/.agentfold)
|
|
67
|
+
--project <path> Target project root (default: cwd)
|
|
68
|
+
--profile <name> Profile name (default: "default")
|
|
69
|
+
--output <path> Build output folder (default: <config>/out)
|
|
70
|
+
--dry-run For apply: show changes only
|
|
71
|
+
--prune For apply: remove stale previously generated files
|
|
72
|
+
--name <value> For create-layer/create-profile/add-*: resource name
|
|
73
|
+
--layer <name> For add-*: target layer (default: "global")
|
|
74
|
+
--layers <list> For create-profile: comma-separated layer names
|
|
75
|
+
--targets <list> For init/create-profile: comma-separated targets (copilot,codex,cursor,claude)
|
|
76
|
+
--force Overwrite existing resources
|
|
77
|
+
--tag <value> For add-tag/delete-tag: tag value
|
|
78
|
+
--file <path> For explain: content file to trace
|
|
79
|
+
--quiet Reduce informational logs
|
|
80
|
+
--version Print version
|
|
81
|
+
`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function normalizeTagValue(input) {
|
|
85
|
+
return String(input || '')
|
|
86
|
+
.trim()
|
|
87
|
+
.toLowerCase()
|
|
88
|
+
.replace(/\s+/g, '-')
|
|
89
|
+
.replace(/_/g, '-')
|
|
90
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
91
|
+
.replace(/-+/g, '-')
|
|
92
|
+
.replace(/^-|-$/g, '');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getTagsPath({ configRoot }) {
|
|
96
|
+
return path.join(configRoot, 'config', 'definitions', 'scope-tags.json');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function readTagList(tagsPath) {
|
|
100
|
+
if (!(await pathExists(tagsPath))) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
const tags = await readJson(tagsPath);
|
|
104
|
+
if (!Array.isArray(tags)) {
|
|
105
|
+
throw new Error(`Expected tag array at ${tagsPath}`);
|
|
106
|
+
}
|
|
107
|
+
return tags.map((tag) => normalizeTagValue(tag)).filter(Boolean);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function runListTags({ options, logger }) {
|
|
111
|
+
const configRoot = resolveConfigRoot(options);
|
|
112
|
+
const tagsPath = getTagsPath({ configRoot });
|
|
113
|
+
const tags = await readTagList(tagsPath);
|
|
114
|
+
const sorted = [...new Set(tags)].sort((a, b) => a.localeCompare(b));
|
|
115
|
+
|
|
116
|
+
logger.info(`Tags source: ${tagsPath}`);
|
|
117
|
+
for (const tag of sorted) {
|
|
118
|
+
console.log(tag);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function runAddTag({ options, logger }) {
|
|
123
|
+
const rawTag = typeof options.tag === 'string' ? options.tag : options.name;
|
|
124
|
+
const normalizedTag = normalizeTagValue(rawTag);
|
|
125
|
+
if (!normalizedTag) {
|
|
126
|
+
throw new Error('add-tag requires --tag <value> (or --name <value>).');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
validateKebabName(normalizedTag, 'Tag');
|
|
130
|
+
|
|
131
|
+
const configRoot = resolveConfigRoot(options);
|
|
132
|
+
const tagsPath = getTagsPath({ configRoot });
|
|
133
|
+
const existing = await readTagList(tagsPath);
|
|
134
|
+
|
|
135
|
+
const next = [...new Set([...existing, normalizedTag])].sort((a, b) => a.localeCompare(b));
|
|
136
|
+
if (next.length === existing.length) {
|
|
137
|
+
logger.info(`Tag already exists: ${normalizedTag}`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await ensureDir(path.dirname(tagsPath));
|
|
142
|
+
await writeJson(tagsPath, next);
|
|
143
|
+
logger.info(`Tag added: ${normalizedTag}`);
|
|
144
|
+
logger.info(`Updated tags file: ${tagsPath}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function runDeleteTag({ options, logger }) {
|
|
148
|
+
const rawTag = typeof options.tag === 'string' ? options.tag : options.name;
|
|
149
|
+
const normalizedTag = normalizeTagValue(rawTag);
|
|
150
|
+
if (!normalizedTag) {
|
|
151
|
+
throw new Error('delete-tag requires --tag <value> (or --name <value>).');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
validateKebabName(normalizedTag, 'Tag');
|
|
155
|
+
|
|
156
|
+
const configRoot = resolveConfigRoot(options);
|
|
157
|
+
const tagsPath = getTagsPath({ configRoot });
|
|
158
|
+
const existing = await readTagList(tagsPath);
|
|
159
|
+
const next = existing.filter((tag) => tag !== normalizedTag);
|
|
160
|
+
|
|
161
|
+
if (next.length === existing.length) {
|
|
162
|
+
logger.info(`Tag not found: ${normalizedTag}`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
await writeJson(tagsPath, [...new Set(next)].sort((a, b) => a.localeCompare(b)));
|
|
167
|
+
logger.info(`Tag deleted: ${normalizedTag}`);
|
|
168
|
+
logger.info(`Updated tags file: ${tagsPath}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function parseCsvOption(raw) {
|
|
172
|
+
if (!raw || typeof raw !== 'string') {
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return raw
|
|
177
|
+
.split(',')
|
|
178
|
+
.map((part) => part.trim())
|
|
179
|
+
.filter(Boolean);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function validateKebabName(name, label) {
|
|
183
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) {
|
|
184
|
+
throw new Error(`${label} must be kebab-case. Received: "${name}"`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function createDefaultLayerSelect() {
|
|
189
|
+
const select = {};
|
|
190
|
+
for (const type of selectableTypes) {
|
|
191
|
+
select[type] = { tags: { mode: 'any', values: [] } };
|
|
192
|
+
}
|
|
193
|
+
return select;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function scaffoldLayer({ layerRoot }) {
|
|
197
|
+
const layerDirs = [
|
|
198
|
+
'agents',
|
|
199
|
+
'commands',
|
|
200
|
+
'mcp',
|
|
201
|
+
'references',
|
|
202
|
+
'rules',
|
|
203
|
+
'skills',
|
|
204
|
+
'subagents',
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
for (const folder of layerDirs) {
|
|
208
|
+
await ensureDir(path.join(layerRoot, folder));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function runCreateLayer({ options, logger }) {
|
|
213
|
+
const layerName = typeof options.name === 'string' ? options.name.trim() : '';
|
|
214
|
+
if (!layerName) {
|
|
215
|
+
throw new Error('create-layer requires --name <layer-name>.');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (layerName === 'global') {
|
|
219
|
+
throw new Error('Layer "global" is permanent and already reserved.');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
validateKebabName(layerName, 'Layer name');
|
|
223
|
+
|
|
224
|
+
const configRoot = resolveConfigRoot(options);
|
|
225
|
+
const layerRoot = path.join(configRoot, 'layers', layerName);
|
|
226
|
+
|
|
227
|
+
const exists = await pathExists(layerRoot);
|
|
228
|
+
if (exists && !options.force) {
|
|
229
|
+
throw new Error(`Layer already exists: ${layerRoot}. Use --force to reuse it.`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
await scaffoldLayer({ layerRoot });
|
|
233
|
+
logger.info(`Layer ready: ${layerRoot}`);
|
|
234
|
+
logger.info('Next: add this layer to a profile with create-profile --layers global,<layer-name>.');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function runCreateProfile({ options, logger }) {
|
|
238
|
+
const profileName = typeof options.name === 'string' ? options.name.trim() : '';
|
|
239
|
+
if (!profileName) {
|
|
240
|
+
throw new Error('create-profile requires --name <profile-name>.');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
validateKebabName(profileName, 'Profile name');
|
|
244
|
+
|
|
245
|
+
const configRoot = resolveConfigRoot(options);
|
|
246
|
+
const requestedLayers = parseCsvOption(options.layers);
|
|
247
|
+
const requestedTargets = parseCsvOption(options.targets);
|
|
248
|
+
const allowedTargets = new Set(['copilot', 'codex', 'cursor', 'claude']);
|
|
249
|
+
const targets = (requestedTargets.length > 0 ? requestedTargets : ['codex']).map((target) => target.toLowerCase());
|
|
250
|
+
|
|
251
|
+
for (const target of targets) {
|
|
252
|
+
if (!allowedTargets.has(target)) {
|
|
253
|
+
throw new Error(`Unsupported target "${target}". Allowed: copilot,codex,cursor,claude.`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const orderedLayers = [];
|
|
258
|
+
const seen = new Set();
|
|
259
|
+
for (const name of ['global', ...(requestedLayers.length > 0 ? requestedLayers : [])]) {
|
|
260
|
+
const normalized = String(name).trim();
|
|
261
|
+
if (!normalized || seen.has(normalized)) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
validateKebabName(normalized, 'Layer name');
|
|
265
|
+
orderedLayers.push(normalized);
|
|
266
|
+
seen.add(normalized);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
for (const layerName of orderedLayers) {
|
|
270
|
+
const layerPath = path.join(configRoot, 'layers', layerName);
|
|
271
|
+
if (!(await pathExists(layerPath))) {
|
|
272
|
+
await scaffoldLayer({ layerRoot: layerPath });
|
|
273
|
+
logger.info(`Created missing layer: ${layerPath}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const profileBody = {
|
|
278
|
+
name: profileName,
|
|
279
|
+
targets,
|
|
280
|
+
layers: orderedLayers.map((layerName) => ({
|
|
281
|
+
name: layerName,
|
|
282
|
+
select: createDefaultLayerSelect(),
|
|
283
|
+
})),
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const profileDir = path.join(configRoot, 'config', 'profiles');
|
|
287
|
+
await ensureDir(profileDir);
|
|
288
|
+
const profilePath = path.join(profileDir, `${profileName}.json`);
|
|
289
|
+
|
|
290
|
+
const exists = await pathExists(profilePath);
|
|
291
|
+
if (exists && !options.force) {
|
|
292
|
+
throw new Error(`Profile already exists: ${profilePath}. Use --force to overwrite.`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
await writeJson(profilePath, profileBody);
|
|
296
|
+
logger.info(`Profile created: ${profilePath}`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// add-rule / add-skill / add-subagent / add-command / add-reference
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
function resolveLayerForAdd(options) {
|
|
304
|
+
const configRoot = resolveConfigRoot(options);
|
|
305
|
+
const layerName = typeof options.layer === 'string' ? options.layer.trim() : 'global';
|
|
306
|
+
validateKebabName(layerName, 'Layer name');
|
|
307
|
+
const layerRoot = path.join(configRoot, 'layers', layerName);
|
|
308
|
+
return { configRoot, layerName, layerRoot };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function runAddRule({ options, logger }) {
|
|
312
|
+
const name = typeof options.name === 'string' ? options.name.trim() : '';
|
|
313
|
+
if (!name) throw new Error('add-rule requires --name <rule-name>.');
|
|
314
|
+
validateKebabName(name, 'Rule name');
|
|
315
|
+
|
|
316
|
+
const { layerRoot } = resolveLayerForAdd(options);
|
|
317
|
+
const filePath = path.join(layerRoot, 'rules', `${name}.md`);
|
|
318
|
+
|
|
319
|
+
if ((await pathExists(filePath)) && !options.force) {
|
|
320
|
+
throw new Error(`Rule already exists: ${filePath}. Use --force to overwrite.`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const content = `---
|
|
324
|
+
metadata:
|
|
325
|
+
scope:
|
|
326
|
+
tags: []
|
|
327
|
+
---
|
|
328
|
+
# ${name.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())}
|
|
329
|
+
|
|
330
|
+
<!-- Add your rule instructions here -->
|
|
331
|
+
`;
|
|
332
|
+
await ensureDir(path.dirname(filePath));
|
|
333
|
+
await fs.writeFile(filePath, content, 'utf8');
|
|
334
|
+
logger.info(`Rule created: ${filePath}`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function runAddSkill({ options, logger }) {
|
|
338
|
+
const name = typeof options.name === 'string' ? options.name.trim() : '';
|
|
339
|
+
if (!name) throw new Error('add-skill requires --name <skill-name>.');
|
|
340
|
+
validateKebabName(name, 'Skill name');
|
|
341
|
+
|
|
342
|
+
const { layerRoot } = resolveLayerForAdd(options);
|
|
343
|
+
const skillDir = path.join(layerRoot, 'skills', name);
|
|
344
|
+
const filePath = path.join(skillDir, 'SKILL.md');
|
|
345
|
+
|
|
346
|
+
if ((await pathExists(filePath)) && !options.force) {
|
|
347
|
+
throw new Error(`Skill already exists: ${filePath}. Use --force to overwrite.`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const title = name.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
351
|
+
const content = `---
|
|
352
|
+
name: ${name}
|
|
353
|
+
description: "TODO: describe when this skill should be invoked"
|
|
354
|
+
metadata:
|
|
355
|
+
scope:
|
|
356
|
+
tags: []
|
|
357
|
+
---
|
|
358
|
+
# ${title}
|
|
359
|
+
|
|
360
|
+
## When to Use
|
|
361
|
+
|
|
362
|
+
<!-- Describe the trigger conditions for this skill -->
|
|
363
|
+
|
|
364
|
+
## Steps
|
|
365
|
+
|
|
366
|
+
1. <!-- Step 1 -->
|
|
367
|
+
2. <!-- Step 2 -->
|
|
368
|
+
3. <!-- Step 3 -->
|
|
369
|
+
`;
|
|
370
|
+
await ensureDir(skillDir);
|
|
371
|
+
await fs.writeFile(filePath, content, 'utf8');
|
|
372
|
+
logger.info(`Skill created: ${filePath}`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function runAddSubagent({ options, logger }) {
|
|
376
|
+
const name = typeof options.name === 'string' ? options.name.trim() : '';
|
|
377
|
+
if (!name) throw new Error('add-subagent requires --name <subagent-name>.');
|
|
378
|
+
validateKebabName(name, 'Subagent name');
|
|
379
|
+
|
|
380
|
+
const { layerRoot } = resolveLayerForAdd(options);
|
|
381
|
+
const filePath = path.join(layerRoot, 'subagents', `${name}.agent.md`);
|
|
382
|
+
|
|
383
|
+
if ((await pathExists(filePath)) && !options.force) {
|
|
384
|
+
throw new Error(`Subagent already exists: ${filePath}. Use --force to overwrite.`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const title = name.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
388
|
+
const content = `---
|
|
389
|
+
name: ${name}
|
|
390
|
+
description: "TODO: describe what this agent specialises in"
|
|
391
|
+
tools: [read, search]
|
|
392
|
+
model: Auto (copilot)
|
|
393
|
+
metadata:
|
|
394
|
+
scope:
|
|
395
|
+
tags: []
|
|
396
|
+
---
|
|
397
|
+
# ${title}
|
|
398
|
+
|
|
399
|
+
<!-- System prompt for this sub-agent -->
|
|
400
|
+
|
|
401
|
+
## Expertise
|
|
402
|
+
|
|
403
|
+
<!-- Describe the agent's domain knowledge -->
|
|
404
|
+
|
|
405
|
+
## Instructions
|
|
406
|
+
|
|
407
|
+
<!-- Step-by-step workflow the agent should follow -->
|
|
408
|
+
`;
|
|
409
|
+
await ensureDir(path.dirname(filePath));
|
|
410
|
+
await fs.writeFile(filePath, content, 'utf8');
|
|
411
|
+
logger.info(`Subagent created: ${filePath}`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function runAddCommand({ options, logger }) {
|
|
415
|
+
const name = typeof options.name === 'string' ? options.name.trim() : '';
|
|
416
|
+
if (!name) throw new Error('add-command requires --name <command-name>.');
|
|
417
|
+
validateKebabName(name, 'Command name');
|
|
418
|
+
|
|
419
|
+
const { layerRoot } = resolveLayerForAdd(options);
|
|
420
|
+
const filePath = path.join(layerRoot, 'commands', `${name}.md`);
|
|
421
|
+
|
|
422
|
+
if ((await pathExists(filePath)) && !options.force) {
|
|
423
|
+
throw new Error(`Command already exists: ${filePath}. Use --force to overwrite.`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const title = name.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
427
|
+
const content = `---
|
|
428
|
+
metadata:
|
|
429
|
+
scope:
|
|
430
|
+
tags: []
|
|
431
|
+
---
|
|
432
|
+
# ${title}
|
|
433
|
+
|
|
434
|
+
## Usage
|
|
435
|
+
|
|
436
|
+
<!-- Describe when and how to invoke this command -->
|
|
437
|
+
|
|
438
|
+
## Steps
|
|
439
|
+
|
|
440
|
+
1. <!-- Step 1 -->
|
|
441
|
+
2. <!-- Step 2 -->
|
|
442
|
+
`;
|
|
443
|
+
await ensureDir(path.dirname(filePath));
|
|
444
|
+
await fs.writeFile(filePath, content, 'utf8');
|
|
445
|
+
logger.info(`Command created: ${filePath}`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function runAddReference({ options, logger }) {
|
|
449
|
+
const name = typeof options.name === 'string' ? options.name.trim() : '';
|
|
450
|
+
if (!name) throw new Error('add-reference requires --name <reference-name>.');
|
|
451
|
+
validateKebabName(name, 'Reference name');
|
|
452
|
+
|
|
453
|
+
const { layerRoot } = resolveLayerForAdd(options);
|
|
454
|
+
const filePath = path.join(layerRoot, 'references', `${name}.md`);
|
|
455
|
+
|
|
456
|
+
if ((await pathExists(filePath)) && !options.force) {
|
|
457
|
+
throw new Error(`Reference already exists: ${filePath}. Use --force to overwrite.`);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const title = name.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
461
|
+
const content = `# ${title}
|
|
462
|
+
|
|
463
|
+
<!-- Reference content that other rules/skills can link to via .agents/references/${name}.md -->
|
|
464
|
+
`;
|
|
465
|
+
await ensureDir(path.dirname(filePath));
|
|
466
|
+
await fs.writeFile(filePath, content, 'utf8');
|
|
467
|
+
logger.info(`Reference created: ${filePath}`);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
// init
|
|
472
|
+
// ---------------------------------------------------------------------------
|
|
473
|
+
|
|
474
|
+
const STARTER_TAGS = [
|
|
475
|
+
'clean-code', 'testing', 'docs', 'frontend', 'backend', 'fullstack',
|
|
476
|
+
];
|
|
477
|
+
|
|
478
|
+
async function runInit({ options, logger }) {
|
|
479
|
+
const configRoot = resolveConfigRoot(options);
|
|
480
|
+
|
|
481
|
+
if ((await pathExists(path.join(configRoot, 'config'))) && !options.force) {
|
|
482
|
+
throw new Error(
|
|
483
|
+
`Config hub already exists at ${configRoot}. Use --force to re‑initialise.`
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const requestedTargets = parseCsvOption(options.targets);
|
|
488
|
+
const allowedTargets = new Set(['copilot', 'codex', 'cursor', 'claude']);
|
|
489
|
+
const targets = (requestedTargets.length > 0 ? requestedTargets : ['codex'])
|
|
490
|
+
.map((t) => t.toLowerCase());
|
|
491
|
+
for (const t of targets) {
|
|
492
|
+
if (!allowedTargets.has(t)) {
|
|
493
|
+
throw new Error(`Unsupported target "${t}". Allowed: copilot,codex,cursor,claude.`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Scaffold global layer
|
|
498
|
+
const globalLayerRoot = path.join(configRoot, 'layers', 'global');
|
|
499
|
+
await scaffoldLayer({ layerRoot: globalLayerRoot });
|
|
500
|
+
|
|
501
|
+
// Default profile
|
|
502
|
+
const profileBody = {
|
|
503
|
+
name: 'default',
|
|
504
|
+
targets,
|
|
505
|
+
layers: [{ name: 'global', select: createDefaultLayerSelect() }],
|
|
506
|
+
};
|
|
507
|
+
await ensureDir(path.join(configRoot, 'config', 'profiles'));
|
|
508
|
+
await writeJson(path.join(configRoot, 'config', 'profiles', 'default.json'), profileBody);
|
|
509
|
+
|
|
510
|
+
// Starter tags
|
|
511
|
+
await ensureDir(path.join(configRoot, 'config', 'definitions'));
|
|
512
|
+
await writeJson(path.join(configRoot, 'config', 'definitions', 'scope-tags.json'), STARTER_TAGS);
|
|
513
|
+
|
|
514
|
+
logger.info(`Initialised config hub at ${configRoot}`);
|
|
515
|
+
logger.info(`Default profile: targets=[${targets.join(',')}], layers=[global]`);
|
|
516
|
+
logger.info('Next: agentfold create-layer --name <team> && agentfold create-profile --name <team> --layers <team>');
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ---------------------------------------------------------------------------
|
|
520
|
+
// status
|
|
521
|
+
// ---------------------------------------------------------------------------
|
|
522
|
+
|
|
523
|
+
async function runStatus({ options, logger }) {
|
|
524
|
+
const configRoot = resolveConfigRoot(options);
|
|
525
|
+
const projectRoot = path.resolve(String(options.project || process.cwd()));
|
|
526
|
+
|
|
527
|
+
// Load profile
|
|
528
|
+
let preset;
|
|
529
|
+
try {
|
|
530
|
+
preset = await loadProfile({
|
|
531
|
+
configRoot,
|
|
532
|
+
profileNameOverride: options.profile ? String(options.profile) : undefined,
|
|
533
|
+
});
|
|
534
|
+
} catch (err) {
|
|
535
|
+
logger.error(`Could not load profile: ${err.message}`);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
console.log(`Config: ${configRoot}`);
|
|
540
|
+
console.log(`Profile: ${preset.profileName} (${preset.profilePath})`);
|
|
541
|
+
|
|
542
|
+
const enabledTargets = Object.entries(preset.targets)
|
|
543
|
+
.filter(([, enabled]) => enabled)
|
|
544
|
+
.map(([name]) => name);
|
|
545
|
+
console.log(`Targets: ${enabledTargets.join(', ') || '(none)'}`);
|
|
546
|
+
|
|
547
|
+
// Layers
|
|
548
|
+
let layers;
|
|
549
|
+
try {
|
|
550
|
+
layers = await resolveLayerPaths({ configRoot, preset });
|
|
551
|
+
} catch {
|
|
552
|
+
layers = [];
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
console.log(`Layers: ${layers.length}`);
|
|
556
|
+
for (const layer of layers) {
|
|
557
|
+
const files = await listFiles(layer.path);
|
|
558
|
+
console.log(` ${layer.name} — ${layer.path} (${files.length} file(s))`);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Tags
|
|
562
|
+
console.log(`Tags: ${preset.scopeTags.length}`);
|
|
563
|
+
|
|
564
|
+
// Manifest
|
|
565
|
+
const manifest = await loadManifest(projectRoot);
|
|
566
|
+
if (manifest) {
|
|
567
|
+
console.log(`Manifest: ${manifest.files.length} managed file(s), generated ${manifest.generatedAt}`);
|
|
568
|
+
} else {
|
|
569
|
+
console.log('Manifest: (none — run agentfold apply first)');
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// ---------------------------------------------------------------------------
|
|
574
|
+
// doctor
|
|
575
|
+
// ---------------------------------------------------------------------------
|
|
576
|
+
|
|
577
|
+
async function runDoctor({ options, logger }) {
|
|
578
|
+
const configRoot = resolveConfigRoot(options);
|
|
579
|
+
const projectRoot = path.resolve(String(options.project || process.cwd()));
|
|
580
|
+
const checks = [];
|
|
581
|
+
|
|
582
|
+
function pass(label) { checks.push({ label, result: 'pass' }); }
|
|
583
|
+
function fail(label, detail) { checks.push({ label, result: 'FAIL', detail }); }
|
|
584
|
+
function warn(label, detail) { checks.push({ label, result: 'warn', detail }); }
|
|
585
|
+
|
|
586
|
+
// 1. Config root exists
|
|
587
|
+
if (await pathExists(configRoot)) {
|
|
588
|
+
pass(`Config root exists: ${configRoot}`);
|
|
589
|
+
} else {
|
|
590
|
+
fail('Config root exists', `Missing at ${configRoot} — run agentfold init`);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// 2. Load profile
|
|
594
|
+
let preset;
|
|
595
|
+
try {
|
|
596
|
+
preset = await loadProfile({
|
|
597
|
+
configRoot,
|
|
598
|
+
profileNameOverride: options.profile ? String(options.profile) : undefined,
|
|
599
|
+
});
|
|
600
|
+
pass(`Profile "${preset.profileName}" loads successfully`);
|
|
601
|
+
} catch (err) {
|
|
602
|
+
fail('Profile loads successfully', err.message);
|
|
603
|
+
printDoctorResults(checks);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// 3. At least one target enabled
|
|
608
|
+
const enabledTargets = Object.entries(preset.targets)
|
|
609
|
+
.filter(([, enabled]) => enabled)
|
|
610
|
+
.map(([name]) => name);
|
|
611
|
+
if (enabledTargets.length > 0) {
|
|
612
|
+
pass(`Targets enabled: ${enabledTargets.join(', ')}`);
|
|
613
|
+
} else {
|
|
614
|
+
fail('At least one target enabled', 'No targets enabled');
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// 4. Layer resolution
|
|
618
|
+
let layers = [];
|
|
619
|
+
try {
|
|
620
|
+
layers = await resolveLayerPaths({ configRoot, preset });
|
|
621
|
+
pass(`All ${layers.length} layer(s) resolve`);
|
|
622
|
+
} catch (err) {
|
|
623
|
+
fail('All layers resolve', err.message);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// 5. Canonical subdirectories in each layer
|
|
627
|
+
const canonicalDirs = ['agents', 'commands', 'mcp', 'references', 'rules', 'skills', 'subagents'];
|
|
628
|
+
for (const layer of layers) {
|
|
629
|
+
const missing = [];
|
|
630
|
+
for (const dir of canonicalDirs) {
|
|
631
|
+
if (!(await pathExists(path.join(layer.path, dir)))) {
|
|
632
|
+
missing.push(dir);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (missing.length === 0) {
|
|
636
|
+
pass(`Layer "${layer.name}" has all canonical folders`);
|
|
637
|
+
} else {
|
|
638
|
+
warn(`Layer "${layer.name}" canonical folders`, `Missing: ${missing.join(', ')}`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// 6. Tags validation
|
|
643
|
+
const scopeTagSet = new Set(preset.scopeTags);
|
|
644
|
+
let tagErrors = 0;
|
|
645
|
+
for (const layer of preset.layers) {
|
|
646
|
+
for (const type of selectableTypes) {
|
|
647
|
+
for (const tag of layer.select[type].tags.values) {
|
|
648
|
+
if (!scopeTagSet.has(tag)) {
|
|
649
|
+
fail(`Tag "${tag}" in ${layer.name}.${type}`, 'Not in scope-tags.json');
|
|
650
|
+
tagErrors++;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (tagErrors === 0) {
|
|
656
|
+
pass('All profile tags are defined');
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// 7. Orphaned layers — layers on disk not referenced by the active profile
|
|
660
|
+
const referencedNames = new Set(preset.layers.map((l) => l.name));
|
|
661
|
+
const layersDir = path.join(configRoot, 'layers');
|
|
662
|
+
if (await pathExists(layersDir)) {
|
|
663
|
+
const entries = await fs.readdir(layersDir, { withFileTypes: true });
|
|
664
|
+
for (const entry of entries) {
|
|
665
|
+
if (entry.isDirectory() && !referencedNames.has(entry.name)) {
|
|
666
|
+
warn(`Orphan layer: ${entry.name}`, 'Exists on disk but not in active profile');
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// 8. Manifest staleness
|
|
672
|
+
const manifest = await loadManifest(projectRoot);
|
|
673
|
+
if (manifest) {
|
|
674
|
+
if (manifest.profile === preset.profileName) {
|
|
675
|
+
pass('Manifest matches active profile');
|
|
676
|
+
} else {
|
|
677
|
+
warn('Manifest profile mismatch', `Manifest: ${manifest.profile}, Active: ${preset.profileName}`);
|
|
678
|
+
}
|
|
679
|
+
} else {
|
|
680
|
+
warn('Manifest', 'No manifest found — run agentfold apply');
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
printDoctorResults(checks);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function printDoctorResults(checks) {
|
|
687
|
+
const icons = { pass: '✓', FAIL: '✗', warn: '!' };
|
|
688
|
+
let failures = 0;
|
|
689
|
+
for (const c of checks) {
|
|
690
|
+
const icon = icons[c.result] || '?';
|
|
691
|
+
const detail = c.detail ? ` — ${c.detail}` : '';
|
|
692
|
+
console.log(` [${icon}] ${c.label}${detail}`);
|
|
693
|
+
if (c.result === 'FAIL') failures++;
|
|
694
|
+
}
|
|
695
|
+
if (failures > 0) {
|
|
696
|
+
process.exitCode = 1;
|
|
697
|
+
console.log(`\n${failures} check(s) failed.`);
|
|
698
|
+
} else {
|
|
699
|
+
console.log('\nAll checks passed.');
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// ---------------------------------------------------------------------------
|
|
704
|
+
// list-layers
|
|
705
|
+
// ---------------------------------------------------------------------------
|
|
706
|
+
|
|
707
|
+
async function runListLayers({ options, logger }) {
|
|
708
|
+
const configRoot = resolveConfigRoot(options);
|
|
709
|
+
const found = [];
|
|
710
|
+
|
|
711
|
+
const layersDir = path.join(configRoot, 'layers');
|
|
712
|
+
if (await pathExists(layersDir)) {
|
|
713
|
+
const entries = await fs.readdir(layersDir, { withFileTypes: true });
|
|
714
|
+
for (const entry of entries) {
|
|
715
|
+
if (entry.isDirectory()) {
|
|
716
|
+
const layerPath = path.join(layersDir, entry.name);
|
|
717
|
+
const files = await listFiles(layerPath);
|
|
718
|
+
found.push({ name: entry.name, path: layerPath, files: files.length });
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
found.sort((a, b) => a.name.localeCompare(b.name));
|
|
724
|
+
if (found.length === 0) {
|
|
725
|
+
logger.info('No layers found.');
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
for (const layer of found) {
|
|
730
|
+
console.log(`${layer.name} ${layer.files} file(s) ${layer.path}`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// ---------------------------------------------------------------------------
|
|
735
|
+
// list-profiles
|
|
736
|
+
// ---------------------------------------------------------------------------
|
|
737
|
+
|
|
738
|
+
async function runListProfiles({ options, logger }) {
|
|
739
|
+
const configRoot = resolveConfigRoot(options);
|
|
740
|
+
const found = [];
|
|
741
|
+
|
|
742
|
+
const profilesDir = path.join(configRoot, 'config', 'profiles');
|
|
743
|
+
if (await pathExists(profilesDir)) {
|
|
744
|
+
const entries = await fs.readdir(profilesDir, { withFileTypes: true });
|
|
745
|
+
for (const entry of entries) {
|
|
746
|
+
if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
747
|
+
const profilePath = path.join(profilesDir, entry.name);
|
|
748
|
+
try {
|
|
749
|
+
const raw = await readJson(profilePath);
|
|
750
|
+
found.push({
|
|
751
|
+
name: entry.name.replace(/\.json$/, ''),
|
|
752
|
+
path: profilePath,
|
|
753
|
+
layers: Array.isArray(raw.layers) ? raw.layers.length : 0,
|
|
754
|
+
targets: Array.isArray(raw.targets) ? raw.targets.join(',') : '',
|
|
755
|
+
});
|
|
756
|
+
} catch {
|
|
757
|
+
found.push({ name: entry.name.replace(/\.json$/, ''), path: profilePath, layers: '?', targets: '?' });
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
found.sort((a, b) => a.name.localeCompare(b.name));
|
|
764
|
+
if (found.length === 0) {
|
|
765
|
+
logger.info('No profiles found.');
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
for (const p of found) {
|
|
770
|
+
console.log(`${p.name} ${p.layers} layer(s) targets=${p.targets} ${p.path}`);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// ---------------------------------------------------------------------------
|
|
775
|
+
// explain
|
|
776
|
+
// ---------------------------------------------------------------------------
|
|
777
|
+
|
|
778
|
+
async function runExplain({ options, logger }) {
|
|
779
|
+
const filePath = typeof options.file === 'string' ? options.file.trim() : '';
|
|
780
|
+
if (!filePath) {
|
|
781
|
+
throw new Error('explain requires --file <path> pointing to a source content file.');
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const configRoot = resolveConfigRoot(options);
|
|
785
|
+
const resolvedFile = path.resolve(filePath);
|
|
786
|
+
|
|
787
|
+
if (!(await pathExists(resolvedFile))) {
|
|
788
|
+
throw new Error(`File not found: ${resolvedFile}`);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Load profile
|
|
792
|
+
const preset = await loadProfile({
|
|
793
|
+
configRoot,
|
|
794
|
+
profileNameOverride: options.profile ? String(options.profile) : undefined,
|
|
795
|
+
});
|
|
796
|
+
const layers = await resolveLayerPaths({ configRoot, preset });
|
|
797
|
+
|
|
798
|
+
// Determine which layer this file belongs to
|
|
799
|
+
let matchedLayer = null;
|
|
800
|
+
let relativeInLayer = null;
|
|
801
|
+
for (const layer of layers) {
|
|
802
|
+
const layerAbs = path.resolve(layer.path);
|
|
803
|
+
if (resolvedFile.startsWith(layerAbs + path.sep)) {
|
|
804
|
+
matchedLayer = layer;
|
|
805
|
+
relativeInLayer = path.relative(layerAbs, resolvedFile);
|
|
806
|
+
break;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (!matchedLayer) {
|
|
811
|
+
console.log(`File is NOT inside any resolved layer for profile "${preset.profileName}".`);
|
|
812
|
+
console.log(`Resolved layers:`);
|
|
813
|
+
for (const l of layers) console.log(` ${l.name} → ${l.path}`);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
console.log(`File: ${resolvedFile}`);
|
|
818
|
+
console.log(`Layer: ${matchedLayer.name} (${matchedLayer.path})`);
|
|
819
|
+
console.log(`Rel: ${relativeInLayer}`);
|
|
820
|
+
|
|
821
|
+
// Determine content type from first path segment
|
|
822
|
+
const firstSegment = relativeInLayer.split(path.sep)[0];
|
|
823
|
+
const typeMap = {
|
|
824
|
+
skills: 'skills', rules: 'rules', commands: 'commands',
|
|
825
|
+
mcp: 'mcp', subagents: 'subagents', agents: 'agents',
|
|
826
|
+
references: 'references',
|
|
827
|
+
};
|
|
828
|
+
const contentType = typeMap[firstSegment] || null;
|
|
829
|
+
console.log(`Type: ${contentType || '(unknown)'}`);
|
|
830
|
+
|
|
831
|
+
if (!contentType || !selectableTypes.includes(contentType)) {
|
|
832
|
+
console.log('Result: INCLUDED (no scope filtering applies to this type)');
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Check scope selector
|
|
837
|
+
const selector = matchedLayer.select[contentType];
|
|
838
|
+
const hasActiveSelector = selector.namespaces.length > 0 || selector.tags.values.length > 0;
|
|
839
|
+
if (!hasActiveSelector) {
|
|
840
|
+
console.log('Select: (empty — all content passes)');
|
|
841
|
+
console.log('Result: INCLUDED');
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
console.log(`Select: namespaces=${JSON.stringify(selector.namespaces)} tags=${JSON.stringify(selector.tags)}`);
|
|
846
|
+
|
|
847
|
+
// Parse frontmatter
|
|
848
|
+
const { normalizeScopeMetadata, matchesScope } = await import('./lib/scope.mjs');
|
|
849
|
+
const { parseFrontMatter } = await import('./lib/util.mjs');
|
|
850
|
+
const text = await fs.readFile(resolvedFile, 'utf8');
|
|
851
|
+
const fm = parseFrontMatter(text);
|
|
852
|
+
if (!fm) {
|
|
853
|
+
console.log('Front: (none)');
|
|
854
|
+
console.log('Result: EXCLUDED (no front matter to match against non-empty selector)');
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const metadata = normalizeScopeMetadata(fm);
|
|
859
|
+
console.log(`Front: namespace="${metadata.namespace}" tags=${JSON.stringify(metadata.tags)}`);
|
|
860
|
+
|
|
861
|
+
const matched = matchesScope({ selector, metadata });
|
|
862
|
+
console.log(`Result: ${matched ? 'INCLUDED' : 'EXCLUDED'}`);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// ---------------------------------------------------------------------------
|
|
866
|
+
// render pipeline helpers
|
|
867
|
+
// ---------------------------------------------------------------------------
|
|
868
|
+
|
|
869
|
+
async function writeRenderMapToDirectory(renderMap, rootDir) {
|
|
870
|
+
await removeDir(rootDir);
|
|
871
|
+
await ensureDir(rootDir);
|
|
872
|
+
|
|
873
|
+
const entries = [...renderMap.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
874
|
+
for (const [relativePath, record] of entries) {
|
|
875
|
+
const destination = path.join(rootDir, relativePath);
|
|
876
|
+
await ensureDir(path.dirname(destination));
|
|
877
|
+
await fs.writeFile(destination, record.content, 'utf8');
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
async function gatherContext({ options, logger }) {
|
|
882
|
+
const configRoot = resolveConfigRoot(options);
|
|
883
|
+
const projectRoot = path.resolve(String(options.project || process.cwd()));
|
|
884
|
+
const outputRoot = path.resolve(String(options.output || path.join(configRoot, 'out')));
|
|
885
|
+
|
|
886
|
+
const preset = await loadProfile({
|
|
887
|
+
configRoot,
|
|
888
|
+
profileNameOverride: options.profile ? String(options.profile) : undefined,
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
const layers = await resolveLayerPaths({ configRoot, preset });
|
|
892
|
+
const build = await buildRenderPlan({
|
|
893
|
+
preset,
|
|
894
|
+
layers,
|
|
895
|
+
logger,
|
|
896
|
+
projectRoot,
|
|
897
|
+
configRoot,
|
|
898
|
+
});
|
|
899
|
+
const validation = await validatePlan({
|
|
900
|
+
preset,
|
|
901
|
+
layers,
|
|
902
|
+
skillDefinitions: build.skillDefinitions,
|
|
903
|
+
promptDefinitions: build.promptDefinitions,
|
|
904
|
+
subagentDefinitions: build.subagentDefinitions,
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
return { configRoot, projectRoot, outputRoot, preset, layers, build, validation };
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function printValidation(validation, logger) {
|
|
911
|
+
for (const warning of validation.warnings) {
|
|
912
|
+
logger.warn(`Warning: ${warning}`);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
if (validation.errors.length > 0) {
|
|
916
|
+
for (const error of validation.errors) {
|
|
917
|
+
logger.error(`Error: ${error}`);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
async function runLint({ options, logger }) {
|
|
923
|
+
const context = await gatherContext({ options, logger });
|
|
924
|
+
printValidation(context.validation, logger);
|
|
925
|
+
|
|
926
|
+
if (context.validation.errors.length > 0) {
|
|
927
|
+
throw new Error(`Lint failed with ${context.validation.errors.length} error(s).`);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
logger.info('Lint passed.');
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
async function runBuild({ options, logger }) {
|
|
934
|
+
const context = await gatherContext({ options, logger });
|
|
935
|
+
printValidation(context.validation, logger);
|
|
936
|
+
|
|
937
|
+
if (context.validation.errors.length > 0) {
|
|
938
|
+
throw new Error(`Build halted due to ${context.validation.errors.length} lint error(s).`);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
await writeRenderMapToDirectory(context.build.renderMap, context.outputRoot);
|
|
942
|
+
logger.info(`Build complete: ${context.outputRoot}`);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
async function ensureBuilt(context, logger) {
|
|
946
|
+
if (!(await pathExists(context.outputRoot))) {
|
|
947
|
+
logger.info('Build output missing; generating it now...');
|
|
948
|
+
await writeRenderMapToDirectory(context.build.renderMap, context.outputRoot);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function printDiff(diff) {
|
|
953
|
+
console.log(`Added: ${diff.added.length}`);
|
|
954
|
+
for (const relativePath of diff.added) {
|
|
955
|
+
console.log(` + ${relativePath}`);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
console.log(`Changed: ${diff.changed.length}`);
|
|
959
|
+
for (const relativePath of diff.changed) {
|
|
960
|
+
console.log(` ~ ${relativePath}`);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
console.log(`Removed: ${diff.removed.length}`);
|
|
964
|
+
for (const relativePath of diff.removed) {
|
|
965
|
+
console.log(` - ${relativePath}`);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
async function runDiff({ options, logger }) {
|
|
970
|
+
const context = await gatherContext({ options, logger });
|
|
971
|
+
printValidation(context.validation, logger);
|
|
972
|
+
|
|
973
|
+
if (context.validation.errors.length > 0) {
|
|
974
|
+
throw new Error(`Diff halted due to ${context.validation.errors.length} lint error(s).`);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
await ensureBuilt(context, logger);
|
|
978
|
+
const previousManifest = await loadManifest(context.projectRoot);
|
|
979
|
+
const diff = await diffRenderMapAgainstProject({
|
|
980
|
+
renderMap: context.build.renderMap,
|
|
981
|
+
projectRoot: context.projectRoot,
|
|
982
|
+
previousManifest,
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
printDiff(diff);
|
|
986
|
+
if (diff.added.length + diff.changed.length + diff.removed.length > 0) {
|
|
987
|
+
process.exitCode = 1;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
async function runApply({ options, logger }) {
|
|
992
|
+
const context = await gatherContext({ options, logger });
|
|
993
|
+
printValidation(context.validation, logger);
|
|
994
|
+
|
|
995
|
+
if (context.validation.errors.length > 0) {
|
|
996
|
+
throw new Error(`Apply halted due to ${context.validation.errors.length} lint error(s).`);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
await ensureBuilt(context, logger);
|
|
1000
|
+
const previousManifest = await loadManifest(context.projectRoot);
|
|
1001
|
+
const diff = await diffRenderMapAgainstProject({
|
|
1002
|
+
renderMap: context.build.renderMap,
|
|
1003
|
+
projectRoot: context.projectRoot,
|
|
1004
|
+
previousManifest,
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
if (options['dry-run']) {
|
|
1008
|
+
logger.info('Apply dry-run mode. No files written.');
|
|
1009
|
+
printDiff(diff);
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const sortedEntries = [...context.build.renderMap.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
1014
|
+
for (const [relativePath, record] of sortedEntries) {
|
|
1015
|
+
const destination = path.join(context.projectRoot, relativePath);
|
|
1016
|
+
await ensureDir(path.dirname(destination));
|
|
1017
|
+
await fs.writeFile(destination, record.content, 'utf8');
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
let deleted = [];
|
|
1021
|
+
const isConfigRootProject =
|
|
1022
|
+
path.resolve(context.projectRoot) === path.resolve(context.configRoot);
|
|
1023
|
+
if (options.prune) {
|
|
1024
|
+
deleted = await pruneManagedFiles({
|
|
1025
|
+
previousManifest,
|
|
1026
|
+
renderMap: context.build.renderMap,
|
|
1027
|
+
projectRoot: context.projectRoot,
|
|
1028
|
+
preservePaths: isConfigRootProject ? ['AGENTS.md'] : [],
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const manifest = await writeManifest({
|
|
1033
|
+
projectRoot: context.projectRoot,
|
|
1034
|
+
preset: context.preset,
|
|
1035
|
+
renderMap: context.build.renderMap,
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
logger.info(`Apply complete. Wrote ${manifest.files.length} managed file(s).`);
|
|
1039
|
+
if (deleted.length > 0) {
|
|
1040
|
+
logger.info(`Pruned ${deleted.length} stale generated file(s).`);
|
|
1041
|
+
for (const relativePath of deleted) {
|
|
1042
|
+
logger.info(` - ${toPosixPath(relativePath)}`);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
async function main() {
|
|
1048
|
+
const { positional, options } = parseCliArgs(process.argv.slice(2));
|
|
1049
|
+
const command = positional[0];
|
|
1050
|
+
const logger = createLogger({ quiet: Boolean(options.quiet) });
|
|
1051
|
+
|
|
1052
|
+
if (options.version || command === '--version') {
|
|
1053
|
+
const pkg = await readJson(path.join(kitRoot, 'package.json'));
|
|
1054
|
+
console.log(`agentfold v${pkg.version}`);
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
if (!command || command === 'help' || command === '--help') {
|
|
1059
|
+
usage();
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const commands = [
|
|
1064
|
+
'lint', 'build', 'diff', 'apply',
|
|
1065
|
+
'init', 'status', 'doctor',
|
|
1066
|
+
'create-layer', 'create-profile',
|
|
1067
|
+
'list-layers', 'list-profiles',
|
|
1068
|
+
'list-tags', 'add-tag', 'delete-tag',
|
|
1069
|
+
'add-rule', 'add-skill', 'add-subagent', 'add-command', 'add-reference',
|
|
1070
|
+
'explain',
|
|
1071
|
+
];
|
|
1072
|
+
|
|
1073
|
+
if (!commands.includes(command)) {
|
|
1074
|
+
usage();
|
|
1075
|
+
throw new Error(`Unknown command: ${command}`);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
if (options.preset) {
|
|
1079
|
+
throw new Error('The --preset option is no longer supported. Use --profile.');
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
if (options.bundled) {
|
|
1083
|
+
throw new Error('The --bundled option is no longer supported. All content lives in the config hub (--config or ~/.agentfold).');
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
const dispatch = {
|
|
1087
|
+
lint: runLint,
|
|
1088
|
+
build: runBuild,
|
|
1089
|
+
diff: runDiff,
|
|
1090
|
+
apply: runApply,
|
|
1091
|
+
init: runInit,
|
|
1092
|
+
status: runStatus,
|
|
1093
|
+
doctor: runDoctor,
|
|
1094
|
+
'create-layer': runCreateLayer,
|
|
1095
|
+
'create-profile': runCreateProfile,
|
|
1096
|
+
'list-layers': runListLayers,
|
|
1097
|
+
'list-profiles': runListProfiles,
|
|
1098
|
+
'list-tags': runListTags,
|
|
1099
|
+
'add-tag': runAddTag,
|
|
1100
|
+
'delete-tag': runDeleteTag,
|
|
1101
|
+
'add-rule': runAddRule,
|
|
1102
|
+
'add-skill': runAddSkill,
|
|
1103
|
+
'add-subagent': runAddSubagent,
|
|
1104
|
+
'add-command': runAddCommand,
|
|
1105
|
+
'add-reference': runAddReference,
|
|
1106
|
+
explain: runExplain,
|
|
1107
|
+
};
|
|
1108
|
+
|
|
1109
|
+
await dispatch[command]({ options, logger });
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
main().catch((error) => {
|
|
1113
|
+
console.error(error.message);
|
|
1114
|
+
process.exit(1);
|
|
1115
|
+
});
|