@dujavi/ai-md 0.3.0 → 0.5.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 +108 -24
- package/bin/ai-md.js +346 -99
- package/docs/architecture.html +316 -0
- package/docs/architecture.png +0 -0
- package/lib/config.js +110 -8
- package/lib/scripts.js +104 -0
- package/lib/status.js +11 -1
- package/package.json +9 -6
- package/scripts/sync-config.sh +2 -2
- 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/config.js
CHANGED
|
@@ -4,30 +4,127 @@ const fs = require("fs");
|
|
|
4
4
|
const os = require("os");
|
|
5
5
|
const path = require("path");
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
const DEFAULT_REMOTE = "https://github.com/dujavi/.ai-md.git";
|
|
8
|
+
|
|
9
|
+
function expandHome(p) {
|
|
10
|
+
if (!p) return p;
|
|
11
|
+
if (p === "~") return os.homedir();
|
|
12
|
+
if (p.startsWith("~/")) return path.join(os.homedir(), p.slice(2));
|
|
13
|
+
return p;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Machine-local config (not inside the private content repo). */
|
|
17
|
+
function machineConfigPath(env = process.env) {
|
|
18
|
+
if (env.AI_MD_CONFIG) return expandHome(env.AI_MD_CONFIG);
|
|
19
|
+
const xdg = env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
20
|
+
return path.join(xdg, "ai-md", "config.json");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readMachineConfig(env = process.env) {
|
|
24
|
+
const configPath = machineConfigPath(env);
|
|
25
|
+
try {
|
|
26
|
+
const raw = fs.readFileSync(configPath, "utf8");
|
|
27
|
+
const data = JSON.parse(raw);
|
|
28
|
+
return {
|
|
29
|
+
path: configPath,
|
|
30
|
+
dir: data.dir ? expandHome(String(data.dir)) : null,
|
|
31
|
+
remote: data.remote ? String(data.remote) : null,
|
|
32
|
+
raw: data,
|
|
33
|
+
};
|
|
34
|
+
} catch {
|
|
35
|
+
return { path: configPath, dir: null, remote: null, raw: null };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function writeMachineConfig({ dir, remote }, env = process.env, { dryRun = false } = {}) {
|
|
40
|
+
const configPath = machineConfigPath(env);
|
|
41
|
+
const existing = readMachineConfig(env).raw || {};
|
|
42
|
+
const next = {
|
|
43
|
+
...existing,
|
|
44
|
+
...(dir != null ? { dir: expandHome(dir) } : {}),
|
|
45
|
+
...(remote != null ? { remote: String(remote) } : {}),
|
|
46
|
+
updatedAt: new Date().toISOString(),
|
|
47
|
+
};
|
|
48
|
+
if (!next.dir && !next.remote) {
|
|
49
|
+
const err = new Error("nothing to write: provide --dir and/or --remote");
|
|
50
|
+
err.code = "EINVAL";
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
if (dryRun) {
|
|
54
|
+
return { action: "would_write", path: configPath, config: next };
|
|
55
|
+
}
|
|
56
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
57
|
+
fs.writeFileSync(configPath, `${JSON.stringify(next, null, 2)}\n`, {
|
|
58
|
+
mode: 0o600,
|
|
59
|
+
});
|
|
60
|
+
try {
|
|
61
|
+
fs.chmodSync(configPath, 0o600);
|
|
62
|
+
} catch {
|
|
63
|
+
/* best effort */
|
|
64
|
+
}
|
|
65
|
+
return { action: "wrote", path: configPath, config: next };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Precedence: explicit opts > env > machine config file > defaults.
|
|
70
|
+
* @param {{ dir?: string, remote?: string }} [opts]
|
|
71
|
+
*/
|
|
72
|
+
function resolveConfig(env = process.env, opts = {}) {
|
|
8
73
|
const home = os.homedir();
|
|
9
|
-
const
|
|
10
|
-
|
|
74
|
+
const stored = readMachineConfig(env);
|
|
75
|
+
const defaultDir = path.join(home, ".ai-md");
|
|
76
|
+
|
|
77
|
+
const envDir = env.AI_MD_DIR || env.CURSOR_MD_DIR || null;
|
|
78
|
+
const envRemote = env.AI_MD_REMOTE || env.CURSOR_MD_REMOTE || null;
|
|
79
|
+
|
|
80
|
+
const dir = expandHome(
|
|
81
|
+
opts.dir || envDir || stored.dir || defaultDir
|
|
82
|
+
);
|
|
11
83
|
const remote =
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
84
|
+
opts.remote || envRemote || stored.remote || DEFAULT_REMOTE;
|
|
85
|
+
|
|
86
|
+
const sources = {
|
|
87
|
+
dir: opts.dir
|
|
88
|
+
? "flag"
|
|
89
|
+
: envDir && !(stored.dir && expandHome(envDir) === stored.dir)
|
|
90
|
+
? "env"
|
|
91
|
+
: stored.dir
|
|
92
|
+
? "config"
|
|
93
|
+
: "default",
|
|
94
|
+
remote: opts.remote
|
|
95
|
+
? "flag"
|
|
96
|
+
: envRemote && !(stored.remote && envRemote === stored.remote)
|
|
97
|
+
? "env"
|
|
98
|
+
: stored.remote
|
|
99
|
+
? "config"
|
|
100
|
+
: "default",
|
|
101
|
+
};
|
|
102
|
+
|
|
15
103
|
const templatesDir = path.join(dir, "templates");
|
|
16
104
|
return {
|
|
17
105
|
home,
|
|
18
106
|
dir,
|
|
19
107
|
remote,
|
|
108
|
+
sources,
|
|
109
|
+
machineConfigPath: stored.path,
|
|
110
|
+
machineConfig: stored.raw,
|
|
20
111
|
cursorSkills: path.join(home, ".cursor", "skills"),
|
|
21
112
|
cursorRules: path.join(home, ".cursor", "rules"),
|
|
22
113
|
projectsDir: path.join(dir, "projects"),
|
|
23
114
|
templatesDir,
|
|
24
|
-
/** @deprecated use templatePath(cfg, name) */
|
|
25
115
|
templateDir: path.join(templatesDir, "base"),
|
|
26
116
|
skillsDir: path.join(dir, "skills"),
|
|
27
117
|
rulesDir: path.join(dir, "rules"),
|
|
28
118
|
};
|
|
29
119
|
}
|
|
30
120
|
|
|
121
|
+
function applyEnvFromConfig(cfg) {
|
|
122
|
+
process.env.AI_MD_DIR = cfg.dir;
|
|
123
|
+
process.env.AI_MD_REMOTE = cfg.remote;
|
|
124
|
+
process.env.CURSOR_MD_DIR = cfg.dir;
|
|
125
|
+
process.env.CURSOR_MD_REMOTE = cfg.remote;
|
|
126
|
+
}
|
|
127
|
+
|
|
31
128
|
function templatePath(cfg, name = "base") {
|
|
32
129
|
return path.join(cfg.templatesDir, name);
|
|
33
130
|
}
|
|
@@ -104,7 +201,6 @@ function agentSkillTargets(home, agents) {
|
|
|
104
201
|
.map((name) => ({ name, path: map[name] }));
|
|
105
202
|
}
|
|
106
203
|
|
|
107
|
-
/** One-time: projects/template → templates/base */
|
|
108
204
|
function migrateLegacyTemplate(cfg, { dryRun = false } = {}) {
|
|
109
205
|
const legacy = path.join(cfg.projectsDir, "template");
|
|
110
206
|
const dest = templatePath(cfg, "base");
|
|
@@ -118,7 +214,13 @@ function migrateLegacyTemplate(cfg, { dryRun = false } = {}) {
|
|
|
118
214
|
}
|
|
119
215
|
|
|
120
216
|
module.exports = {
|
|
217
|
+
DEFAULT_REMOTE,
|
|
218
|
+
machineConfigPath,
|
|
219
|
+
readMachineConfig,
|
|
220
|
+
writeMachineConfig,
|
|
121
221
|
resolveConfig,
|
|
222
|
+
applyEnvFromConfig,
|
|
223
|
+
expandHome,
|
|
122
224
|
templatePath,
|
|
123
225
|
listTemplates,
|
|
124
226
|
migrateLegacyTemplate,
|
package/lib/scripts.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { spawnSync } = require("child_process");
|
|
6
|
+
|
|
7
|
+
const NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
8
|
+
|
|
9
|
+
function scriptsDir(cfg) {
|
|
10
|
+
return path.join(cfg.dir, "scripts");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve a private script by basename only (no path separators).
|
|
15
|
+
* Prefers exact name, then `<name>.sh`.
|
|
16
|
+
*/
|
|
17
|
+
function resolveScript(cfg, name) {
|
|
18
|
+
const raw = String(name || "").trim();
|
|
19
|
+
if (!raw || !NAME_RE.test(raw) || raw.includes("..")) {
|
|
20
|
+
const err = new Error(
|
|
21
|
+
`invalid script name: ${JSON.stringify(name)} (use a single basename)`
|
|
22
|
+
);
|
|
23
|
+
err.code = "EINVAL";
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const dir = scriptsDir(cfg);
|
|
28
|
+
const exact = path.join(dir, raw);
|
|
29
|
+
const withSh = raw.endsWith(".sh") ? null : path.join(dir, `${raw}.sh`);
|
|
30
|
+
|
|
31
|
+
if (fs.existsSync(exact) && fs.statSync(exact).isFile()) {
|
|
32
|
+
return { name: raw, path: exact, dir };
|
|
33
|
+
}
|
|
34
|
+
if (withSh && fs.existsSync(withSh) && fs.statSync(withSh).isFile()) {
|
|
35
|
+
return { name: raw, path: withSh, dir };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const err = new Error(
|
|
39
|
+
`script not found: ${raw} (looked in ${dir}/${raw} and ${dir}/${raw}.sh)`
|
|
40
|
+
);
|
|
41
|
+
err.code = "ENOENT";
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Run a private script with forwarded args. Returns { name, args, path, exitCode }.
|
|
47
|
+
* Uses bash for .sh; otherwise executes the file directly.
|
|
48
|
+
*/
|
|
49
|
+
function runScript(cfg, name, args = [], { dryRun = false } = {}) {
|
|
50
|
+
const resolved = resolveScript(cfg, name);
|
|
51
|
+
const scriptArgs = Array.isArray(args) ? args.map(String) : [];
|
|
52
|
+
const useBash = resolved.path.endsWith(".sh");
|
|
53
|
+
const argv = useBash
|
|
54
|
+
? ["bash", resolved.path, ...scriptArgs]
|
|
55
|
+
: [resolved.path, ...scriptArgs];
|
|
56
|
+
|
|
57
|
+
if (dryRun) {
|
|
58
|
+
return {
|
|
59
|
+
name: resolved.name,
|
|
60
|
+
args: scriptArgs,
|
|
61
|
+
path: resolved.path,
|
|
62
|
+
exitCode: 0,
|
|
63
|
+
dryRun: true,
|
|
64
|
+
command: argv,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const result = spawnSync(argv[0], argv.slice(1), {
|
|
69
|
+
stdio: "inherit",
|
|
70
|
+
env: process.env,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (result.error) {
|
|
74
|
+
const err = new Error(result.error.message);
|
|
75
|
+
err.code = result.error.code || "EEXEC";
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
name: resolved.name,
|
|
81
|
+
args: scriptArgs,
|
|
82
|
+
path: resolved.path,
|
|
83
|
+
exitCode: result.status === null ? 1 : result.status,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function runScripts(cfg, names, args = [], { dryRun = false } = {}) {
|
|
88
|
+
const results = [];
|
|
89
|
+
for (const name of names) {
|
|
90
|
+
const result = runScript(cfg, name, args, { dryRun });
|
|
91
|
+
results.push(result);
|
|
92
|
+
if (!dryRun && result.exitCode !== 0) {
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return results;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
scriptsDir,
|
|
101
|
+
resolveScript,
|
|
102
|
+
runScript,
|
|
103
|
+
runScripts,
|
|
104
|
+
};
|
package/lib/status.js
CHANGED
|
@@ -135,6 +135,9 @@ function collectStatus(opts = {}) {
|
|
|
135
135
|
generatedAt: new Date().toISOString(),
|
|
136
136
|
dir: cfg.dir,
|
|
137
137
|
remote: remote || cfg.remote,
|
|
138
|
+
sources: cfg.sources,
|
|
139
|
+
machineConfigPath: cfg.machineConfigPath,
|
|
140
|
+
machineConfig: cfg.machineConfig,
|
|
138
141
|
branch,
|
|
139
142
|
dirty,
|
|
140
143
|
statusLine: aheadBehind,
|
|
@@ -173,7 +176,14 @@ function collectStatus(opts = {}) {
|
|
|
173
176
|
function statusHelp(data) {
|
|
174
177
|
const help = [];
|
|
175
178
|
if (data.problems.includes("ai_md_missing") || data.problems.includes("ai_md_not_git")) {
|
|
176
|
-
help.push(
|
|
179
|
+
help.push(
|
|
180
|
+
"Run `ai-md setup --remote <git-url>` (or `ai-md install --remote <git-url>`) to clone and link"
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
if (!data.machineConfig) {
|
|
184
|
+
help.push(
|
|
185
|
+
"Persist remote/dir with `ai-md setup --remote <url>` or `ai-md config set --remote <url> --dir ~/.ai-md`"
|
|
186
|
+
);
|
|
177
187
|
}
|
|
178
188
|
if (data.links.some((l) => l.state !== "ok")) {
|
|
179
189
|
help.push("Run `ai-md doctor --fix` to repair ~/.cursor skills/rules symlinks");
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dujavi/ai-md",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "AXI-shaped CLI for ~/.ai-md system skills/rules, templates
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "AXI-shaped CLI for a private ~/.ai-md: system skills/rules, templates, projects, and private script runner for agent harnesses (Cursor, Claude, agents)",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ai-md": "bin/ai-md.js"
|
|
7
7
|
},
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"bin/",
|
|
10
10
|
"lib/",
|
|
11
11
|
"scripts/",
|
|
12
|
+
"docs/",
|
|
12
13
|
"README.md",
|
|
13
14
|
"LICENSE"
|
|
14
15
|
],
|
|
@@ -22,14 +23,16 @@
|
|
|
22
23
|
"@toon-format/toon": "^2.3.1"
|
|
23
24
|
},
|
|
24
25
|
"keywords": [
|
|
26
|
+
"axi",
|
|
25
27
|
"cursor",
|
|
26
|
-
"
|
|
28
|
+
"claude",
|
|
29
|
+
"agents",
|
|
27
30
|
"skills",
|
|
28
31
|
"rules",
|
|
29
|
-
"
|
|
30
|
-
"grok",
|
|
32
|
+
"personal-config",
|
|
31
33
|
"cli",
|
|
32
|
-
"
|
|
34
|
+
"toon",
|
|
35
|
+
"ai-md"
|
|
33
36
|
],
|
|
34
37
|
"author": "dujavi",
|
|
35
38
|
"license": "MIT",
|
package/scripts/sync-config.sh
CHANGED
|
@@ -22,7 +22,7 @@ Commands:
|
|
|
22
22
|
status Show repo state, symlink health, and rule/skill counts
|
|
23
23
|
doctor Diagnose problems; with --fix, repair symlinks
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Private scripts: ai-md script <name> (from ~/.ai-md/scripts/)
|
|
26
26
|
|
|
27
27
|
Options:
|
|
28
28
|
-m, --message <msg> Commit message for push (default: generic update)
|
|
@@ -146,7 +146,7 @@ cmd_install() {
|
|
|
146
146
|
log "Install complete."
|
|
147
147
|
log " rules: $(count_rules)"
|
|
148
148
|
log " skills: $(count_skills)"
|
|
149
|
-
log "Next: ai-md
|
|
149
|
+
log "Next: ai-md script <name> # private ~/.ai-md/scripts/"
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
cmd_pull() {
|