@dujavi/ai-md 0.4.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 +54 -42
- package/bin/ai-md.js +497 -249
- package/docs/architecture.html +316 -0
- package/docs/architecture.png +0 -0
- 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/scripts.js +104 -0
- package/lib/skeleton.js +117 -0
- package/lib/status.js +110 -35
- package/package.json +11 -7
- package/scripts/sync-config.sh +2 -2
- 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/scripts/ensure-agent-tools.sh +0 -114
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>@dujavi/ai-md architecture</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
|
|
10
|
+
body {
|
|
11
|
+
background: #ffffff;
|
|
12
|
+
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
13
|
+
color: #111827;
|
|
14
|
+
-webkit-font-smoothing: antialiased;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.canvas {
|
|
18
|
+
width: 1100px;
|
|
19
|
+
padding: 36px 40px 44px;
|
|
20
|
+
background: #ffffff;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
h1 {
|
|
24
|
+
font-size: 22px;
|
|
25
|
+
font-weight: 700;
|
|
26
|
+
letter-spacing: -0.02em;
|
|
27
|
+
text-align: center;
|
|
28
|
+
margin-bottom: 28px;
|
|
29
|
+
color: #0f172a;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.columns {
|
|
33
|
+
display: grid;
|
|
34
|
+
grid-template-columns: 1fr 1fr 1fr;
|
|
35
|
+
gap: 28px;
|
|
36
|
+
align-items: start;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.column-label {
|
|
40
|
+
font-size: 12px;
|
|
41
|
+
font-weight: 600;
|
|
42
|
+
text-transform: none;
|
|
43
|
+
letter-spacing: 0.01em;
|
|
44
|
+
color: #475569;
|
|
45
|
+
margin-bottom: 10px;
|
|
46
|
+
text-align: center;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.panel {
|
|
50
|
+
border: 2px solid #1e293b;
|
|
51
|
+
border-radius: 10px;
|
|
52
|
+
background: #f8fafc;
|
|
53
|
+
padding: 14px 14px 16px;
|
|
54
|
+
min-height: 280px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.panel-title {
|
|
58
|
+
font-size: 14px;
|
|
59
|
+
font-weight: 700;
|
|
60
|
+
color: #0f172a;
|
|
61
|
+
text-align: center;
|
|
62
|
+
margin-bottom: 12px;
|
|
63
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.stack {
|
|
67
|
+
display: flex;
|
|
68
|
+
flex-direction: column;
|
|
69
|
+
gap: 10px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.box {
|
|
73
|
+
border: 1.5px solid #334155;
|
|
74
|
+
border-radius: 8px;
|
|
75
|
+
background: #ffffff;
|
|
76
|
+
padding: 12px 14px;
|
|
77
|
+
text-align: center;
|
|
78
|
+
font-size: 13px;
|
|
79
|
+
font-weight: 600;
|
|
80
|
+
color: #0f172a;
|
|
81
|
+
line-height: 1.35;
|
|
82
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.box.primary {
|
|
86
|
+
background: #eff6ff;
|
|
87
|
+
border-color: #1d4ed8;
|
|
88
|
+
color: #1e3a8a;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.box.system {
|
|
92
|
+
background: #ecfdf5;
|
|
93
|
+
border-color: #047857;
|
|
94
|
+
color: #064e3b;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.box.template {
|
|
98
|
+
background: #fffbeb;
|
|
99
|
+
border-color: #b45309;
|
|
100
|
+
color: #78350f;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.box.project {
|
|
104
|
+
background: #fdf2f8;
|
|
105
|
+
border-color: #be185d;
|
|
106
|
+
color: #831843;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.box.script {
|
|
110
|
+
background: #f1f5f9;
|
|
111
|
+
border-color: #475569;
|
|
112
|
+
color: #1e293b;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.box.harness {
|
|
116
|
+
background: #f5f3ff;
|
|
117
|
+
border-color: #5b21b6;
|
|
118
|
+
color: #4c1d95;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.flow {
|
|
122
|
+
position: relative;
|
|
123
|
+
margin-top: 8px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* SVG overlay for arrows spanning columns */
|
|
127
|
+
.arrows {
|
|
128
|
+
position: absolute;
|
|
129
|
+
inset: 0;
|
|
130
|
+
width: 100%;
|
|
131
|
+
height: 100%;
|
|
132
|
+
pointer-events: none;
|
|
133
|
+
overflow: visible;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.diagram {
|
|
137
|
+
position: relative;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.legend {
|
|
141
|
+
margin-top: 22px;
|
|
142
|
+
display: flex;
|
|
143
|
+
justify-content: center;
|
|
144
|
+
gap: 28px;
|
|
145
|
+
flex-wrap: wrap;
|
|
146
|
+
font-size: 12px;
|
|
147
|
+
color: #475569;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.legend-item {
|
|
151
|
+
display: flex;
|
|
152
|
+
align-items: center;
|
|
153
|
+
gap: 8px;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.legend-line {
|
|
157
|
+
width: 28px;
|
|
158
|
+
height: 0;
|
|
159
|
+
border-top: 2px solid #1e293b;
|
|
160
|
+
position: relative;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.legend-line::after {
|
|
164
|
+
content: "";
|
|
165
|
+
position: absolute;
|
|
166
|
+
right: -1px;
|
|
167
|
+
top: -4px;
|
|
168
|
+
border: 4px solid transparent;
|
|
169
|
+
border-left: 7px solid #1e293b;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.footer-note {
|
|
173
|
+
margin-top: 14px;
|
|
174
|
+
text-align: center;
|
|
175
|
+
font-size: 11px;
|
|
176
|
+
color: #64748b;
|
|
177
|
+
}
|
|
178
|
+
</style>
|
|
179
|
+
</head>
|
|
180
|
+
<body>
|
|
181
|
+
<div class="canvas" id="capture">
|
|
182
|
+
<h1>@dujavi/ai-md</h1>
|
|
183
|
+
|
|
184
|
+
<div class="diagram" id="diagram">
|
|
185
|
+
<div class="columns" id="columns">
|
|
186
|
+
<!-- Left: package -->
|
|
187
|
+
<div class="col" id="col-pkg">
|
|
188
|
+
<div class="column-label">npm package</div>
|
|
189
|
+
<div class="panel">
|
|
190
|
+
<div class="panel-title">@dujavi/ai-md</div>
|
|
191
|
+
<div class="stack">
|
|
192
|
+
<div class="box primary" id="box-cli">ai-md CLI</div>
|
|
193
|
+
<div class="box script" id="box-runner">lib/scripts.js</div>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<!-- Center: private dir -->
|
|
199
|
+
<div class="col" id="col-private">
|
|
200
|
+
<div class="column-label">private config</div>
|
|
201
|
+
<div class="panel">
|
|
202
|
+
<div class="panel-title">~/.ai-md</div>
|
|
203
|
+
<div class="stack">
|
|
204
|
+
<div class="box system" id="box-system">skills + rules</div>
|
|
205
|
+
<div class="box template" id="box-templates">templates/</div>
|
|
206
|
+
<div class="box project" id="box-projects">projects/</div>
|
|
207
|
+
<div class="box script" id="box-scripts">scripts/</div>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<!-- Right: harnesses -->
|
|
213
|
+
<div class="col" id="col-harness">
|
|
214
|
+
<div class="column-label">Supported harnesses</div>
|
|
215
|
+
<div class="panel">
|
|
216
|
+
<div class="panel-title">Supported harnesses</div>
|
|
217
|
+
<div class="stack">
|
|
218
|
+
<div class="box harness" id="box-cursor">cursor ~/.cursor</div>
|
|
219
|
+
<div class="box harness" id="box-claude">claude ~/.claude</div>
|
|
220
|
+
<div class="box harness" id="box-agents">agents ~/.agents</div>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<svg class="arrows" id="arrows" xmlns="http://www.w3.org/2000/svg"></svg>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
<div class="legend">
|
|
230
|
+
<div class="legend-item"><span class="legend-line"></span> manages / links</div>
|
|
231
|
+
</div>
|
|
232
|
+
<p class="footer-note">Public package tooling · private content in ~/.ai-md · linked into agent harnesses</p>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<script>
|
|
236
|
+
function centerOf(el, root) {
|
|
237
|
+
const r = el.getBoundingClientRect();
|
|
238
|
+
const b = root.getBoundingClientRect();
|
|
239
|
+
return {
|
|
240
|
+
x: r.left - b.left + r.width / 2,
|
|
241
|
+
y: r.top - b.top + r.height / 2,
|
|
242
|
+
left: r.left - b.left,
|
|
243
|
+
right: r.left - b.left + r.width,
|
|
244
|
+
top: r.top - b.top,
|
|
245
|
+
bottom: r.top - b.top + r.height,
|
|
246
|
+
width: r.width,
|
|
247
|
+
height: r.height,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function draw() {
|
|
252
|
+
const root = document.getElementById("diagram");
|
|
253
|
+
const svg = document.getElementById("arrows");
|
|
254
|
+
const rect = root.getBoundingClientRect();
|
|
255
|
+
svg.setAttribute("width", rect.width);
|
|
256
|
+
svg.setAttribute("height", rect.height);
|
|
257
|
+
svg.style.width = rect.width + "px";
|
|
258
|
+
svg.style.height = rect.height + "px";
|
|
259
|
+
svg.innerHTML = "";
|
|
260
|
+
|
|
261
|
+
const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
|
|
262
|
+
defs.innerHTML = `
|
|
263
|
+
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5"
|
|
264
|
+
markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
265
|
+
<path d="M 0 0 L 10 5 L 0 10 z" fill="#1e293b"/>
|
|
266
|
+
</marker>
|
|
267
|
+
`;
|
|
268
|
+
svg.appendChild(defs);
|
|
269
|
+
|
|
270
|
+
const cli = centerOf(document.getElementById("box-cli"), root);
|
|
271
|
+
const runner = centerOf(document.getElementById("box-runner"), root);
|
|
272
|
+
const system = centerOf(document.getElementById("box-system"), root);
|
|
273
|
+
const templates = centerOf(document.getElementById("box-templates"), root);
|
|
274
|
+
const projects = centerOf(document.getElementById("box-projects"), root);
|
|
275
|
+
const scripts = centerOf(document.getElementById("box-scripts"), root);
|
|
276
|
+
const cursor = centerOf(document.getElementById("box-cursor"), root);
|
|
277
|
+
const claude = centerOf(document.getElementById("box-claude"), root);
|
|
278
|
+
const agents = centerOf(document.getElementById("box-agents"), root);
|
|
279
|
+
|
|
280
|
+
function line(x1, y1, x2, y2, opts = {}) {
|
|
281
|
+
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
282
|
+
const midX = (x1 + x2) / 2;
|
|
283
|
+
const d = opts.curve
|
|
284
|
+
? `M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`
|
|
285
|
+
: `M ${x1} ${y1} L ${x2} ${y2}`;
|
|
286
|
+
path.setAttribute("d", d);
|
|
287
|
+
path.setAttribute("fill", "none");
|
|
288
|
+
path.setAttribute("stroke", opts.color || "#1e293b");
|
|
289
|
+
path.setAttribute("stroke-width", opts.width || "2");
|
|
290
|
+
path.setAttribute("marker-end", "url(#arrow)");
|
|
291
|
+
if (opts.dash) path.setAttribute("stroke-dasharray", "5 4");
|
|
292
|
+
svg.appendChild(path);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// CLI → skills+rules, templates, projects
|
|
296
|
+
line(cli.right, cli.y, system.left, system.y, { curve: true });
|
|
297
|
+
line(cli.right, cli.y + 2, templates.left, templates.y, { curve: true });
|
|
298
|
+
line(cli.right, cli.y + 4, projects.left, projects.y, { curve: true });
|
|
299
|
+
|
|
300
|
+
// CLI → lib/scripts.js (vertical within left panel)
|
|
301
|
+
line(cli.x, cli.bottom, runner.x, runner.top, { curve: false });
|
|
302
|
+
|
|
303
|
+
// lib/scripts.js → scripts/
|
|
304
|
+
line(runner.right, runner.y, scripts.left, scripts.y, { curve: true });
|
|
305
|
+
|
|
306
|
+
// skills+rules → each harness
|
|
307
|
+
line(system.right, system.y, cursor.left, cursor.y, { curve: true });
|
|
308
|
+
line(system.right, system.y + 2, claude.left, claude.y, { curve: true });
|
|
309
|
+
line(system.right, system.y + 4, agents.left, agents.y, { curve: true });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
requestAnimationFrame(() => requestAnimationFrame(draw));
|
|
313
|
+
window.addEventListener("resize", draw);
|
|
314
|
+
</script>
|
|
315
|
+
</body>
|
|
316
|
+
</html>
|
|
Binary file
|
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
|
+
};
|