@dujavi/ai-md 0.5.0 → 0.6.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/README.md +47 -107
- package/bin/ai-md.js +344 -186
- package/lib/build.js +268 -0
- package/lib/commands.js +292 -99
- package/lib/config-paths.js +73 -0
- package/lib/config.js +85 -57
- package/lib/harnesses/index.js +309 -0
- package/lib/link.js +224 -0
- package/lib/rescue.js +95 -0
- package/lib/skeleton.js +117 -0
- package/lib/status.js +110 -35
- package/package.json +4 -3
- package/skeleton/README.md +3 -0
- package/skeleton/agents/agents/skills/.gitkeep +0 -0
- package/skeleton/agents/claude/rules/.gitkeep +0 -0
- package/skeleton/agents/claude/skills/.gitkeep +0 -0
- package/skeleton/agents/cursor/skills/.gitkeep +0 -0
- package/skeleton/agents/gemini/skills/.gitkeep +0 -0
- package/skeleton/agents/opencode/skills/.gitkeep +0 -0
- package/skeleton/projects/.gitkeep +0 -0
- package/skeleton/scripts/.gitkeep +0 -0
- package/skeleton/shared/rules/edit-source-not-dist.mdc +23 -0
- package/skeleton/shared/rules/personal-config.mdc +49 -0
- package/skeleton/shared/skills/ai-md-config/SKILL.md +51 -0
- package/skeleton/templates/base/rules/.gitkeep +0 -0
- package/skeleton/templates/base/rules/project-context.mdc +8 -0
- package/skeleton/templates/base/skills/.gitkeep +0 -0
package/lib/build.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const crypto = require("crypto");
|
|
6
|
+
const { ensureDir, fingerprintTree, walkFiles, hashFile } = require("./link");
|
|
7
|
+
const { selectHarnesses, ensureAgentSourceDirs } = require("./harnesses");
|
|
8
|
+
|
|
9
|
+
function sharedRoots(cfg) {
|
|
10
|
+
return {
|
|
11
|
+
rules: path.join(cfg.dir, "shared", "rules"),
|
|
12
|
+
skills: path.join(cfg.dir, "shared", "skills"),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function agentRoots(cfg, id) {
|
|
17
|
+
const srcId = id === "codex" ? "agents" : id;
|
|
18
|
+
return {
|
|
19
|
+
rules: path.join(cfg.dir, "agents", srcId, "rules"),
|
|
20
|
+
skills: path.join(cfg.dir, "agents", srcId, "skills"),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function distRoot(cfg, distId) {
|
|
25
|
+
return path.join(cfg.dir, "dist", distId);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function listRuleFiles(dir) {
|
|
29
|
+
if (!fs.existsSync(dir)) return [];
|
|
30
|
+
return fs
|
|
31
|
+
.readdirSync(dir)
|
|
32
|
+
.filter((f) => f.endsWith(".mdc") || f.endsWith(".md"))
|
|
33
|
+
.sort();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function listSkillDirs(dir) {
|
|
37
|
+
if (!fs.existsSync(dir)) return [];
|
|
38
|
+
return fs
|
|
39
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
40
|
+
.filter((d) => d.isDirectory() && !d.name.startsWith("."))
|
|
41
|
+
.map((d) => d.name)
|
|
42
|
+
.sort();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function copyFile(src, dest) {
|
|
46
|
+
ensureDir(path.dirname(dest));
|
|
47
|
+
fs.copyFileSync(src, dest);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function copyDir(src, dest) {
|
|
51
|
+
fs.cpSync(src, dest, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Merge shared then agent; agent wins on basename. */
|
|
55
|
+
function mergeRules(sharedDir, agentDir, format) {
|
|
56
|
+
const map = new Map(); // basename without forcing ext
|
|
57
|
+
for (const f of listRuleFiles(sharedDir)) {
|
|
58
|
+
map.set(f.replace(/\.(mdc|md)$/i, ""), {
|
|
59
|
+
src: path.join(sharedDir, f),
|
|
60
|
+
name: f,
|
|
61
|
+
layer: "shared",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
for (const f of listRuleFiles(agentDir)) {
|
|
65
|
+
const key = f.replace(/\.(mdc|md)$/i, "");
|
|
66
|
+
map.set(key, {
|
|
67
|
+
src: path.join(agentDir, f),
|
|
68
|
+
name: f,
|
|
69
|
+
layer: "agent",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const out = [];
|
|
73
|
+
for (const [, ent] of map) {
|
|
74
|
+
let destName = ent.name;
|
|
75
|
+
if (format === "md" && destName.endsWith(".mdc")) {
|
|
76
|
+
destName = destName.slice(0, -4) + ".md";
|
|
77
|
+
} else if (format === "mdc" && destName.endsWith(".md") && !destName.endsWith(".mdc")) {
|
|
78
|
+
// keep .md as-is for mdc harness if already md; cursor prefers .mdc
|
|
79
|
+
if (!destName.endsWith(".mdc")) destName = destName.replace(/\.md$/i, ".mdc");
|
|
80
|
+
}
|
|
81
|
+
out.push({ ...ent, destName });
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function mergeSkills(sharedDir, agentDir) {
|
|
87
|
+
const map = new Map();
|
|
88
|
+
for (const name of listSkillDirs(sharedDir)) {
|
|
89
|
+
map.set(name, { src: path.join(sharedDir, name), layer: "shared" });
|
|
90
|
+
}
|
|
91
|
+
for (const name of listSkillDirs(agentDir)) {
|
|
92
|
+
map.set(name, { src: path.join(agentDir, name), layer: "agent" });
|
|
93
|
+
}
|
|
94
|
+
return [...map.entries()].map(([name, v]) => ({ name, ...v }));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function readManifest(distDir) {
|
|
98
|
+
const p = path.join(distDir, ".build-manifest.json");
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function distDirty(distDir) {
|
|
107
|
+
const manifest = readManifest(distDir);
|
|
108
|
+
if (!manifest || !manifest.files) {
|
|
109
|
+
if (!fs.existsSync(distDir)) return { dirty: false, files: [] };
|
|
110
|
+
// no manifest but files exist
|
|
111
|
+
const files = walkFiles(distDir);
|
|
112
|
+
return {
|
|
113
|
+
dirty: files.length > 0,
|
|
114
|
+
files: files.map((f) => f.rel),
|
|
115
|
+
reason: "missing_manifest",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const current = fingerprintTree(distDir);
|
|
119
|
+
const dirtyFiles = [];
|
|
120
|
+
for (const [rel, hash] of Object.entries(manifest.files)) {
|
|
121
|
+
if (current[rel] !== hash) dirtyFiles.push(rel);
|
|
122
|
+
}
|
|
123
|
+
for (const rel of Object.keys(current)) {
|
|
124
|
+
if (!manifest.files[rel]) dirtyFiles.push(rel);
|
|
125
|
+
}
|
|
126
|
+
return { dirty: dirtyFiles.length > 0, files: dirtyFiles, reason: "drift" };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function emitHarness(cfg, harness, { dryRun = false, verbose = false } = {}) {
|
|
130
|
+
const distId = harness.distId;
|
|
131
|
+
const outDir = distRoot(cfg, distId);
|
|
132
|
+
const shared = sharedRoots(cfg);
|
|
133
|
+
const agent = agentRoots(cfg, harness.id);
|
|
134
|
+
ensureAgentSourceDirs(cfg, harness.id);
|
|
135
|
+
|
|
136
|
+
const overrides = [];
|
|
137
|
+
const rules =
|
|
138
|
+
harness.format === "skills-only"
|
|
139
|
+
? []
|
|
140
|
+
: mergeRules(shared.rules, agent.rules, harness.format);
|
|
141
|
+
const skills = mergeSkills(shared.skills, agent.skills);
|
|
142
|
+
|
|
143
|
+
for (const r of rules) {
|
|
144
|
+
if (r.layer === "agent") overrides.push(`rules/${r.destName}`);
|
|
145
|
+
}
|
|
146
|
+
for (const s of skills) {
|
|
147
|
+
if (s.layer === "agent") overrides.push(`skills/${s.name}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (dryRun) {
|
|
151
|
+
return {
|
|
152
|
+
id: harness.id,
|
|
153
|
+
distId,
|
|
154
|
+
action: "would_build",
|
|
155
|
+
rules: rules.length,
|
|
156
|
+
skills: skills.length,
|
|
157
|
+
overrides: verbose ? overrides : overrides.length,
|
|
158
|
+
outDir,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
fs.rmSync(outDir, { recursive: true, force: true });
|
|
163
|
+
ensureDir(path.join(outDir, "skills"));
|
|
164
|
+
if (harness.format !== "skills-only") ensureDir(path.join(outDir, "rules"));
|
|
165
|
+
|
|
166
|
+
for (const r of rules) {
|
|
167
|
+
copyFile(r.src, path.join(outDir, "rules", r.destName));
|
|
168
|
+
}
|
|
169
|
+
for (const s of skills) {
|
|
170
|
+
copyDir(s.src, path.join(outDir, "skills", s.name));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const files = fingerprintTree(outDir);
|
|
174
|
+
const manifest = {
|
|
175
|
+
version: 1,
|
|
176
|
+
harness: harness.id,
|
|
177
|
+
distId,
|
|
178
|
+
format: harness.format,
|
|
179
|
+
builtAt: new Date().toISOString(),
|
|
180
|
+
inputsHash: crypto
|
|
181
|
+
.createHash("sha256")
|
|
182
|
+
.update(JSON.stringify({ rules, skills: skills.map((s) => s.name) }))
|
|
183
|
+
.digest("hex"),
|
|
184
|
+
files,
|
|
185
|
+
overrides,
|
|
186
|
+
};
|
|
187
|
+
fs.writeFileSync(
|
|
188
|
+
path.join(outDir, ".build-manifest.json"),
|
|
189
|
+
JSON.stringify(manifest, null, 2) + "\n"
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
id: harness.id,
|
|
194
|
+
distId,
|
|
195
|
+
action: "built",
|
|
196
|
+
rules: rules.length,
|
|
197
|
+
skills: skills.length,
|
|
198
|
+
overrides: verbose ? overrides : overrides.length,
|
|
199
|
+
outDir,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Build dist for selected harnesses. Refuses if dirty unless force.
|
|
205
|
+
*/
|
|
206
|
+
function runBuild(cfg, opts = {}) {
|
|
207
|
+
const {
|
|
208
|
+
agents,
|
|
209
|
+
force = false,
|
|
210
|
+
dryRun = false,
|
|
211
|
+
verbose = false,
|
|
212
|
+
forceLink = false,
|
|
213
|
+
includeUninstalled = false,
|
|
214
|
+
} = opts;
|
|
215
|
+
|
|
216
|
+
const { selected, skipped, warnings } = selectHarnesses(cfg, agents, {
|
|
217
|
+
forceLink,
|
|
218
|
+
includeUninstalled,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Deduplicate by distId for emit
|
|
222
|
+
const byDist = new Map();
|
|
223
|
+
for (const h of selected) {
|
|
224
|
+
if (!byDist.has(h.distId)) byDist.set(h.distId, h);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const dirtyBlocks = [];
|
|
228
|
+
for (const [distId] of byDist) {
|
|
229
|
+
const d = distDirty(distRoot(cfg, distId));
|
|
230
|
+
if (d.dirty && !force) {
|
|
231
|
+
dirtyBlocks.push({ distId, ...d });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (dirtyBlocks.length && !dryRun) {
|
|
236
|
+
const err = new Error(
|
|
237
|
+
`dist has edits not in source (${dirtyBlocks.map((d) => d.distId).join(", ")}). ` +
|
|
238
|
+
`Run: ai-md rescue --agents <id> or ai-md build --force`
|
|
239
|
+
);
|
|
240
|
+
err.code = "EDIRTY";
|
|
241
|
+
err.dirty = dirtyBlocks;
|
|
242
|
+
throw err;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const results = [];
|
|
246
|
+
for (const h of byDist.values()) {
|
|
247
|
+
results.push(emitHarness(cfg, h, { dryRun, verbose }));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
built: results,
|
|
252
|
+
skipped,
|
|
253
|
+
warnings,
|
|
254
|
+
dirtyForced: force ? dirtyBlocks : [],
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
module.exports = {
|
|
259
|
+
sharedRoots,
|
|
260
|
+
agentRoots,
|
|
261
|
+
distRoot,
|
|
262
|
+
mergeRules,
|
|
263
|
+
mergeSkills,
|
|
264
|
+
distDirty,
|
|
265
|
+
readManifest,
|
|
266
|
+
runBuild,
|
|
267
|
+
emitHarness,
|
|
268
|
+
};
|