@dujavi/ai-md 0.5.0 → 0.6.1
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 +50 -106
- package/bin/ai-md.js +358 -191
- package/lib/build.js +268 -0
- package/lib/commands.js +421 -99
- package/lib/config-paths.js +73 -0
- package/lib/config.js +124 -67
- package/lib/harnesses/index.js +309 -0
- package/lib/link.js +224 -0
- package/lib/remote.js +116 -0
- package/lib/rescue.js +95 -0
- package/lib/skeleton.js +117 -0
- package/lib/status.js +114 -36
- package/package.json +4 -3
- package/scripts/sync-config.sh +10 -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/commands.js
CHANGED
|
@@ -5,60 +5,24 @@ const path = require("path");
|
|
|
5
5
|
const {
|
|
6
6
|
resolveConfig,
|
|
7
7
|
templatePath,
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
writeMachineConfig,
|
|
9
|
+
gitClone,
|
|
10
|
+
gitPull,
|
|
11
|
+
gitPush,
|
|
10
12
|
} = require("./config");
|
|
11
13
|
const { collectStatus, statusHelp, collectProjects } = require("./status");
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (state.state === "not_symlink" && !force) {
|
|
25
|
-
const err = new Error(
|
|
26
|
-
`${link} exists and is not a symlink (use --force to replace)`
|
|
27
|
-
);
|
|
28
|
-
err.code = "EEXIST";
|
|
29
|
-
throw err;
|
|
30
|
-
}
|
|
31
|
-
if (dryRun) {
|
|
32
|
-
return { path: link, action: state.state === "missing" ? "would_link" : "would_repair", target };
|
|
33
|
-
}
|
|
34
|
-
if (fs.existsSync(link) || state.state !== "missing") {
|
|
35
|
-
try {
|
|
36
|
-
fs.lstatSync(link);
|
|
37
|
-
fs.rmSync(link, { recursive: true, force: true });
|
|
38
|
-
} catch {
|
|
39
|
-
/* missing */
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
fs.symlinkSync(target, link);
|
|
43
|
-
return {
|
|
44
|
-
path: link,
|
|
45
|
-
action: state.state === "missing" ? "linked" : "repaired",
|
|
46
|
-
target,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function ensureCursorLinks(cfg, opts = {}) {
|
|
51
|
-
return [
|
|
52
|
-
linkPath(cfg.skillsDir, cfg.cursorSkills, opts),
|
|
53
|
-
linkPath(cfg.rulesDir, cfg.cursorRules, opts),
|
|
54
|
-
];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function ensureAgentSkillLinks(cfg, agents, opts = {}) {
|
|
58
|
-
return agentSkillTargets(cfg.home, agents)
|
|
59
|
-
.filter((t) => t.name !== "cursor") // cursor handled via ensureCursorLinks
|
|
60
|
-
.map((t) => linkPath(cfg.skillsDir, t.path, opts));
|
|
61
|
-
}
|
|
14
|
+
const { linkPath, ensureDir, wslWarnings, defaultLinkMode } = require("./link");
|
|
15
|
+
const { runBuild, distRoot, distDirty } = require("./build");
|
|
16
|
+
const { runRescue } = require("./rescue");
|
|
17
|
+
const {
|
|
18
|
+
selectHarnesses,
|
|
19
|
+
resolveHarness,
|
|
20
|
+
listBuiltinIds,
|
|
21
|
+
getBuiltin,
|
|
22
|
+
ensureAgentSourceDirs,
|
|
23
|
+
isHarnessInstalled,
|
|
24
|
+
} = require("./harnesses");
|
|
25
|
+
const { seedSkeleton, initRepo, SKELETON_VERSION, isEmptyDir } = require("./skeleton");
|
|
62
26
|
|
|
63
27
|
function ensureGitignore(repoPath, { dryRun = false } = {}) {
|
|
64
28
|
const gitignore = path.join(repoPath, ".gitignore");
|
|
@@ -84,6 +48,37 @@ function copyDir(src, dest, { dryRun = false } = {}) {
|
|
|
84
48
|
return { action: "copied", from: src, to: dest };
|
|
85
49
|
}
|
|
86
50
|
|
|
51
|
+
function countEntries(dir) {
|
|
52
|
+
if (!fs.existsSync(dir)) return 0;
|
|
53
|
+
return fs.readdirSync(dir).filter((n) => !n.startsWith(".")).length;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function mergeTemplate(templateDir, projectDir, { dryRun = false } = {}) {
|
|
57
|
+
const actions = [];
|
|
58
|
+
for (const kind of ["rules", "skills"]) {
|
|
59
|
+
const srcRoot = path.join(templateDir, kind);
|
|
60
|
+
const destRoot = path.join(projectDir, kind);
|
|
61
|
+
if (!fs.existsSync(srcRoot)) continue;
|
|
62
|
+
ensureDir(destRoot);
|
|
63
|
+
for (const name of fs.readdirSync(srcRoot)) {
|
|
64
|
+
if (name.startsWith(".")) continue;
|
|
65
|
+
const src = path.join(srcRoot, name);
|
|
66
|
+
const dest = path.join(destRoot, name);
|
|
67
|
+
if (fs.existsSync(dest)) {
|
|
68
|
+
actions.push({ action: "keep", path: dest });
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (dryRun) {
|
|
72
|
+
actions.push({ action: "would_add", path: dest, from: src });
|
|
73
|
+
} else {
|
|
74
|
+
fs.cpSync(src, dest, { recursive: true });
|
|
75
|
+
actions.push({ action: "added", path: dest, from: src });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return actions;
|
|
80
|
+
}
|
|
81
|
+
|
|
87
82
|
function initProject({
|
|
88
83
|
repo,
|
|
89
84
|
name,
|
|
@@ -92,8 +87,8 @@ function initProject({
|
|
|
92
87
|
dryRun = false,
|
|
93
88
|
} = {}) {
|
|
94
89
|
const cfg = resolveConfig();
|
|
95
|
-
if (!fs.existsSync(path.join(cfg.dir, ".git"))) {
|
|
96
|
-
const err = new Error(`${cfg.dir}
|
|
90
|
+
if (!fs.existsSync(path.join(cfg.dir, ".git")) && !fs.existsSync(cfg.dir)) {
|
|
91
|
+
const err = new Error(`${cfg.dir} missing; run ai-md init or ai-md install first`);
|
|
97
92
|
err.code = "ENOENT";
|
|
98
93
|
throw err;
|
|
99
94
|
}
|
|
@@ -131,7 +126,13 @@ function initProject({
|
|
|
131
126
|
}
|
|
132
127
|
|
|
133
128
|
const link = path.join(absRepo, ".cursor");
|
|
134
|
-
actions.push(
|
|
129
|
+
actions.push(
|
|
130
|
+
linkPath(projectDir, link, {
|
|
131
|
+
force,
|
|
132
|
+
dryRun,
|
|
133
|
+
linkMode: cfg.linkMode,
|
|
134
|
+
})
|
|
135
|
+
);
|
|
135
136
|
actions.push(ensureGitignore(absRepo, { dryRun }));
|
|
136
137
|
|
|
137
138
|
return {
|
|
@@ -144,37 +145,6 @@ function initProject({
|
|
|
144
145
|
};
|
|
145
146
|
}
|
|
146
147
|
|
|
147
|
-
function countEntries(dir) {
|
|
148
|
-
if (!fs.existsSync(dir)) return 0;
|
|
149
|
-
return fs.readdirSync(dir).filter((n) => !n.startsWith(".")).length;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function mergeTemplate(templateDir, projectDir, { dryRun = false } = {}) {
|
|
153
|
-
const actions = [];
|
|
154
|
-
for (const kind of ["rules", "skills"]) {
|
|
155
|
-
const srcRoot = path.join(templateDir, kind);
|
|
156
|
-
const destRoot = path.join(projectDir, kind);
|
|
157
|
-
if (!fs.existsSync(srcRoot)) continue;
|
|
158
|
-
ensureDir(destRoot);
|
|
159
|
-
for (const name of fs.readdirSync(srcRoot)) {
|
|
160
|
-
if (name.startsWith(".")) continue;
|
|
161
|
-
const src = path.join(srcRoot, name);
|
|
162
|
-
const dest = path.join(destRoot, name);
|
|
163
|
-
if (fs.existsSync(dest)) {
|
|
164
|
-
actions.push({ action: "keep", path: dest });
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
if (dryRun) {
|
|
168
|
-
actions.push({ action: "would_add", path: dest, from: src });
|
|
169
|
-
} else {
|
|
170
|
-
fs.cpSync(src, dest, { recursive: true });
|
|
171
|
-
actions.push({ action: "added", path: dest, from: src });
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
return actions;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
148
|
function applyTemplate({ project, from = "base", dryRun = false } = {}) {
|
|
179
149
|
const cfg = resolveConfig();
|
|
180
150
|
if (!project) {
|
|
@@ -200,11 +170,6 @@ function applyTemplate({ project, from = "base", dryRun = false } = {}) {
|
|
|
200
170
|
|
|
201
171
|
function linkProject({ repo, name, force = false, dryRun = false } = {}) {
|
|
202
172
|
const cfg = resolveConfig();
|
|
203
|
-
if (!fs.existsSync(path.join(cfg.dir, ".git"))) {
|
|
204
|
-
const err = new Error(`${cfg.dir} is not a git repo; run ai-md install first`);
|
|
205
|
-
err.code = "ENOENT";
|
|
206
|
-
throw err;
|
|
207
|
-
}
|
|
208
173
|
if (!repo || !fs.existsSync(repo)) {
|
|
209
174
|
const err = new Error("missing or invalid --repo <path>");
|
|
210
175
|
err.code = "EINVAL";
|
|
@@ -218,37 +183,376 @@ function linkProject({ repo, name, force = false, dryRun = false } = {}) {
|
|
|
218
183
|
ensureDir(path.join(projectDir, "skills"));
|
|
219
184
|
}
|
|
220
185
|
const actions = [
|
|
221
|
-
linkPath(projectDir, path.join(absRepo, ".cursor"), {
|
|
186
|
+
linkPath(projectDir, path.join(absRepo, ".cursor"), {
|
|
187
|
+
force,
|
|
188
|
+
dryRun,
|
|
189
|
+
linkMode: cfg.linkMode,
|
|
190
|
+
}),
|
|
222
191
|
ensureGitignore(absRepo, { dryRun }),
|
|
223
192
|
];
|
|
224
193
|
return { project: projectName, projectDir, repo: absRepo, actions };
|
|
225
194
|
}
|
|
226
195
|
|
|
227
|
-
|
|
196
|
+
/** Link dist/<id> → live harness paths. Only for installed harnesses. */
|
|
197
|
+
function linkHarnesses(cfg, agents, opts = {}) {
|
|
198
|
+
const {
|
|
199
|
+
force = false,
|
|
200
|
+
dryRun = false,
|
|
201
|
+
forceLink = false,
|
|
202
|
+
} = opts;
|
|
203
|
+
const { selected, skipped, warnings } = selectHarnesses(cfg, agents, {
|
|
204
|
+
forceLink,
|
|
205
|
+
});
|
|
206
|
+
const links = [];
|
|
207
|
+
const platformWarn = wslWarnings(cfg, selected);
|
|
208
|
+
|
|
209
|
+
const linkedDist = new Set();
|
|
210
|
+
for (const h of selected) {
|
|
211
|
+
if (linkedDist.has(h.distId)) continue;
|
|
212
|
+
linkedDist.add(h.distId);
|
|
213
|
+
const dist = distRoot(cfg, h.distId);
|
|
214
|
+
if (!fs.existsSync(dist)) {
|
|
215
|
+
links.push({
|
|
216
|
+
harness: h.id,
|
|
217
|
+
action: "skip",
|
|
218
|
+
reason: "dist_missing",
|
|
219
|
+
help: "Run ai-md build first",
|
|
220
|
+
});
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (h.skills) {
|
|
224
|
+
links.push({
|
|
225
|
+
harness: h.id,
|
|
226
|
+
kind: "skills",
|
|
227
|
+
...linkPath(path.join(dist, "skills"), h.skills, {
|
|
228
|
+
force,
|
|
229
|
+
dryRun,
|
|
230
|
+
linkMode: cfg.linkMode,
|
|
231
|
+
}),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
if (h.rules && h.format !== "skills-only") {
|
|
235
|
+
links.push({
|
|
236
|
+
harness: h.id,
|
|
237
|
+
kind: "rules",
|
|
238
|
+
...linkPath(path.join(dist, "rules"), h.rules, {
|
|
239
|
+
force,
|
|
240
|
+
dryRun,
|
|
241
|
+
linkMode: cfg.linkMode,
|
|
242
|
+
}),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
links,
|
|
249
|
+
skipped,
|
|
250
|
+
warnings: [...warnings, ...platformWarn],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function buildAndLink(cfg, opts = {}) {
|
|
255
|
+
const build = runBuild(cfg, opts);
|
|
256
|
+
const link = linkHarnesses(cfg, opts.agents, opts);
|
|
257
|
+
return { build, link };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function runInstall(cfg, opts = {}) {
|
|
261
|
+
const dir = cfg.dir;
|
|
262
|
+
const empty = !fs.existsSync(dir) || isEmptyDir(dir);
|
|
263
|
+
|
|
264
|
+
if (empty) {
|
|
265
|
+
if (!cfg.remote) {
|
|
266
|
+
const err = new Error(
|
|
267
|
+
"No remote configured and content dir is empty.\n" +
|
|
268
|
+
" ai-md init # skeleton only (no remote)\n" +
|
|
269
|
+
" ai-md setup --remote <url> # clone existing github.com/<user>/.ai-md"
|
|
270
|
+
);
|
|
271
|
+
err.code = "EINVAL";
|
|
272
|
+
throw err;
|
|
273
|
+
}
|
|
274
|
+
if (fs.existsSync(dir) && isEmptyDir(dir)) {
|
|
275
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
276
|
+
}
|
|
277
|
+
gitClone(cfg.remote, dir, { dryRun: opts.dryRun });
|
|
278
|
+
if (!opts.dryRun) {
|
|
279
|
+
seedSkeleton(cfg, { force: false });
|
|
280
|
+
}
|
|
281
|
+
} else if (!fs.existsSync(path.join(dir, ".git"))) {
|
|
282
|
+
const err = new Error(
|
|
283
|
+
`${dir} exists but is not a git repo. Move it aside, then: ai-md setup --remote <url>`
|
|
284
|
+
);
|
|
285
|
+
err.code = "EEXIST";
|
|
286
|
+
throw err;
|
|
287
|
+
} else if (cfg.remote && opts.pull !== false) {
|
|
288
|
+
try {
|
|
289
|
+
gitPull(cfg.dir, { dryRun: opts.dryRun });
|
|
290
|
+
} catch {
|
|
291
|
+
/* offline / no upstream — continue with local tree */
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return buildAndLink(cfg, {
|
|
296
|
+
agents: opts.agents,
|
|
297
|
+
force: opts.force,
|
|
298
|
+
dryRun: opts.dryRun,
|
|
299
|
+
forceLink: opts.forceLink,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Bootstrap: remote set → clone/sync first (never skeleton-first).
|
|
305
|
+
* No remote → seed skeleton only.
|
|
306
|
+
*/
|
|
307
|
+
function bootstrapContent(cfg, opts = {}) {
|
|
308
|
+
const {
|
|
309
|
+
noGit = false,
|
|
310
|
+
force = false,
|
|
311
|
+
dryRun = false,
|
|
312
|
+
forceLink = false,
|
|
313
|
+
agents = ["cursor"],
|
|
314
|
+
} = opts;
|
|
315
|
+
|
|
316
|
+
if (cfg.remote) {
|
|
317
|
+
const dir = cfg.dir;
|
|
318
|
+
const exists = fs.existsSync(dir);
|
|
319
|
+
const empty = !exists || isEmptyDir(dir);
|
|
320
|
+
const isGit = exists && fs.existsSync(path.join(dir, ".git"));
|
|
321
|
+
|
|
322
|
+
if (!empty && isGit) {
|
|
323
|
+
if (!dryRun) {
|
|
324
|
+
writeMachineConfig({
|
|
325
|
+
dir: cfg.dir,
|
|
326
|
+
remote: cfg.remote,
|
|
327
|
+
agents,
|
|
328
|
+
linkMode: cfg.linkMode,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
action: "synced",
|
|
333
|
+
remote: cfg.remote,
|
|
334
|
+
dir,
|
|
335
|
+
...runInstall(cfg, {
|
|
336
|
+
agents,
|
|
337
|
+
force,
|
|
338
|
+
dryRun,
|
|
339
|
+
forceLink,
|
|
340
|
+
pull: true,
|
|
341
|
+
}),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (!empty && !isGit) {
|
|
346
|
+
if (!force) {
|
|
347
|
+
const err = new Error(
|
|
348
|
+
`${dir} is not empty and has no .git.\n` +
|
|
349
|
+
` mv ${dir} ${dir}.bak && ai-md setup --remote ${cfg.remote}\n` +
|
|
350
|
+
` (refusing to seed skeleton before sync when a remote is set)`
|
|
351
|
+
);
|
|
352
|
+
err.code = "EEXIST";
|
|
353
|
+
throw err;
|
|
354
|
+
}
|
|
355
|
+
if (dryRun) {
|
|
356
|
+
return { action: "would_replace_and_clone", remote: cfg.remote, dir };
|
|
357
|
+
}
|
|
358
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (dryRun) {
|
|
362
|
+
return { action: "would_clone", remote: cfg.remote, dir };
|
|
363
|
+
}
|
|
364
|
+
if (fs.existsSync(dir) && isEmptyDir(dir)) {
|
|
365
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
366
|
+
}
|
|
367
|
+
gitClone(cfg.remote, dir, { dryRun: false });
|
|
368
|
+
// After clone only — fill gaps; never invent a tree instead of syncing
|
|
369
|
+
seedSkeleton(cfg, { force: false });
|
|
370
|
+
writeMachineConfig({
|
|
371
|
+
dir: cfg.dir,
|
|
372
|
+
remote: cfg.remote,
|
|
373
|
+
agents,
|
|
374
|
+
linkMode: cfg.linkMode,
|
|
375
|
+
});
|
|
376
|
+
const fresh = resolveConfig(process.env, {
|
|
377
|
+
dir: cfg.dir,
|
|
378
|
+
remote: cfg.remote,
|
|
379
|
+
skipRemoteDetect: true,
|
|
380
|
+
});
|
|
381
|
+
return {
|
|
382
|
+
action: "cloned",
|
|
383
|
+
remote: cfg.remote,
|
|
384
|
+
dir,
|
|
385
|
+
...buildAndLink(fresh, { agents, force: true, forceLink }),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const data = initRepo(cfg, { noGit, force, dryRun });
|
|
390
|
+
if (!dryRun) {
|
|
391
|
+
writeMachineConfig({
|
|
392
|
+
dir: cfg.dir,
|
|
393
|
+
agents,
|
|
394
|
+
linkMode: cfg.linkMode,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
const fresh = resolveConfig(process.env, {
|
|
398
|
+
dir: cfg.dir,
|
|
399
|
+
skipRemoteDetect: true,
|
|
400
|
+
});
|
|
401
|
+
let bl = null;
|
|
402
|
+
if (!dryRun) {
|
|
403
|
+
bl = buildAndLink(fresh, { agents, force: true, forceLink });
|
|
404
|
+
}
|
|
405
|
+
return { action: "initialized", init: data, buildLink: bl };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function runPull(cfg, opts = {}) {
|
|
409
|
+
if (!fs.existsSync(path.join(cfg.dir, ".git"))) {
|
|
410
|
+
const err = new Error(`${cfg.dir} is not a git repo; run ai-md install`);
|
|
411
|
+
err.code = "ENOENT";
|
|
412
|
+
throw err;
|
|
413
|
+
}
|
|
414
|
+
gitPull(cfg.dir, { dryRun: opts.dryRun });
|
|
415
|
+
return buildAndLink(cfg, {
|
|
416
|
+
agents: opts.agents,
|
|
417
|
+
force: opts.force,
|
|
418
|
+
dryRun: opts.dryRun,
|
|
419
|
+
forceLink: opts.forceLink,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function runPush(cfg, opts = {}) {
|
|
424
|
+
return gitPush(cfg.dir, {
|
|
425
|
+
message: opts.message,
|
|
426
|
+
dryRun: opts.dryRun,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function runDoctor({
|
|
431
|
+
fix = false,
|
|
432
|
+
force = false,
|
|
433
|
+
agents = ["cursor"],
|
|
434
|
+
dryRun = false,
|
|
435
|
+
forceLink = false,
|
|
436
|
+
} = {}) {
|
|
228
437
|
const cfg = resolveConfig();
|
|
229
438
|
const status = collectStatus({ full: true, agents });
|
|
230
439
|
const repairs = [];
|
|
231
440
|
if (fix || force) {
|
|
232
|
-
|
|
233
|
-
|
|
441
|
+
try {
|
|
442
|
+
const bl = buildAndLink(cfg, {
|
|
443
|
+
agents,
|
|
444
|
+
force: true,
|
|
445
|
+
dryRun,
|
|
446
|
+
forceLink,
|
|
447
|
+
});
|
|
448
|
+
repairs.push(bl);
|
|
449
|
+
} catch (e) {
|
|
450
|
+
repairs.push({ error: e.message, code: e.code, dirty: e.dirty });
|
|
451
|
+
}
|
|
234
452
|
}
|
|
235
453
|
const after = fix || force ? collectStatus({ full: true, agents }) : status;
|
|
236
454
|
return {
|
|
237
|
-
before: {
|
|
238
|
-
problems: status.problems,
|
|
239
|
-
counts: status.counts,
|
|
240
|
-
},
|
|
455
|
+
before: { problems: status.problems, counts: status.counts },
|
|
241
456
|
repairs,
|
|
242
457
|
after: {
|
|
243
458
|
problems: after.problems,
|
|
244
459
|
counts: after.counts,
|
|
245
460
|
links: after.links,
|
|
461
|
+
harnesses: after.harnesses,
|
|
246
462
|
projects: after.projects,
|
|
247
463
|
},
|
|
248
464
|
help: statusHelp(after),
|
|
465
|
+
wsl: wslWarnings(cfg, []),
|
|
249
466
|
};
|
|
250
467
|
}
|
|
251
468
|
|
|
469
|
+
function harnessList(cfg) {
|
|
470
|
+
const ids = new Set([
|
|
471
|
+
...listBuiltinIds(),
|
|
472
|
+
...Object.keys((cfg.machineConfig && cfg.machineConfig.harnesses) || {}),
|
|
473
|
+
]);
|
|
474
|
+
return [...ids].sort().map((id) => {
|
|
475
|
+
const def = resolveHarness(id, cfg);
|
|
476
|
+
return {
|
|
477
|
+
id: def.id,
|
|
478
|
+
format: def.format,
|
|
479
|
+
emitter: def.emitter,
|
|
480
|
+
aliasOf: def.aliasOf,
|
|
481
|
+
skills: def.skills,
|
|
482
|
+
rules: def.rules,
|
|
483
|
+
enabled: def.enabled,
|
|
484
|
+
installed: isHarnessInstalled(def),
|
|
485
|
+
distId: def.distId,
|
|
486
|
+
tier: def.tier,
|
|
487
|
+
};
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function harnessShow(cfg, id) {
|
|
492
|
+
const def = resolveHarness(id, cfg);
|
|
493
|
+
return {
|
|
494
|
+
...def,
|
|
495
|
+
installed: isHarnessInstalled(def),
|
|
496
|
+
builtin: Boolean(getBuiltin(id)),
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function harnessSet(cfg, id, { skills, rules, format } = {}) {
|
|
501
|
+
const key = String(id).toLowerCase();
|
|
502
|
+
const existing = { ...(cfg.machineConfig || {}) };
|
|
503
|
+
const harnesses = { ...(existing.harnesses || {}) };
|
|
504
|
+
const cur = { ...(harnesses[key] || {}) };
|
|
505
|
+
if (skills != null) cur.skills = skills;
|
|
506
|
+
if (rules != null) cur.rules = rules;
|
|
507
|
+
if (format != null) cur.format = format;
|
|
508
|
+
cur.enabled = cur.enabled !== false;
|
|
509
|
+
harnesses[key] = cur;
|
|
510
|
+
const agents = existing.agents || ["cursor"];
|
|
511
|
+
if (!agents.includes(key) && key !== "codex") {
|
|
512
|
+
agents.push(key);
|
|
513
|
+
}
|
|
514
|
+
const saved = writeMachineConfig({ ...existing, harnesses, agents });
|
|
515
|
+
ensureAgentSourceDirs(
|
|
516
|
+
{ ...cfg, dir: saved.config.dir || cfg.dir },
|
|
517
|
+
key === "codex" ? "agents" : key
|
|
518
|
+
);
|
|
519
|
+
return { id: key, harness: cur, config: saved };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function harnessUnset(cfg, id, { pathsOnly = false } = {}) {
|
|
523
|
+
const key = String(id).toLowerCase();
|
|
524
|
+
const existing = { ...(cfg.machineConfig || {}) };
|
|
525
|
+
const harnesses = { ...(existing.harnesses || {}) };
|
|
526
|
+
if (pathsOnly && harnesses[key]) {
|
|
527
|
+
delete harnesses[key].skills;
|
|
528
|
+
delete harnesses[key].rules;
|
|
529
|
+
delete harnesses[key].format;
|
|
530
|
+
} else {
|
|
531
|
+
delete harnesses[key];
|
|
532
|
+
}
|
|
533
|
+
return writeMachineConfig({ ...existing, harnesses });
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function harnessEnable(cfg, id, enabled) {
|
|
537
|
+
const key = String(id).toLowerCase();
|
|
538
|
+
const existing = { ...(cfg.machineConfig || {}) };
|
|
539
|
+
const harnesses = { ...(existing.harnesses || {}) };
|
|
540
|
+
harnesses[key] = { ...(harnesses[key] || {}), enabled };
|
|
541
|
+
let agents = [...(existing.agents || ["cursor"])];
|
|
542
|
+
if (enabled && !agents.includes(key) && key !== "codex") agents.push(key);
|
|
543
|
+
if (!enabled) agents = agents.filter((a) => a !== key);
|
|
544
|
+
return writeMachineConfig({ ...existing, harnesses, agents });
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// legacy exports expected by older callers
|
|
548
|
+
function ensureCursorLinks(cfg, opts = {}) {
|
|
549
|
+
return linkHarnesses(cfg, ["cursor"], opts).links;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function ensureAgentSkillLinks(cfg, agents, opts = {}) {
|
|
553
|
+
return linkHarnesses(cfg, agents, opts).links;
|
|
554
|
+
}
|
|
555
|
+
|
|
252
556
|
module.exports = {
|
|
253
557
|
ensureCursorLinks,
|
|
254
558
|
ensureAgentSkillLinks,
|
|
@@ -258,5 +562,23 @@ module.exports = {
|
|
|
258
562
|
linkProject,
|
|
259
563
|
runDoctor,
|
|
260
564
|
linkPath,
|
|
565
|
+
linkHarnesses,
|
|
566
|
+
buildAndLink,
|
|
567
|
+
runInstall,
|
|
568
|
+
runPull,
|
|
569
|
+
runPush,
|
|
570
|
+
runBuild,
|
|
571
|
+
runRescue,
|
|
572
|
+
seedSkeleton,
|
|
573
|
+
initRepo,
|
|
574
|
+
bootstrapContent,
|
|
575
|
+
harnessList,
|
|
576
|
+
harnessShow,
|
|
577
|
+
harnessSet,
|
|
578
|
+
harnessUnset,
|
|
579
|
+
harnessEnable,
|
|
261
580
|
collectProjects,
|
|
581
|
+
SKELETON_VERSION,
|
|
582
|
+
distDirty,
|
|
583
|
+
defaultLinkMode,
|
|
262
584
|
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
function expandHome(p) {
|
|
8
|
+
if (!p) return p;
|
|
9
|
+
let s = String(p);
|
|
10
|
+
if (process.platform === "win32") {
|
|
11
|
+
s = s.replace(/%([^%]+)%/g, (_, name) => process.env[name] || `%${name}%`);
|
|
12
|
+
}
|
|
13
|
+
if (s === "~") return os.homedir();
|
|
14
|
+
if (s.startsWith("~/") || s.startsWith("~\\")) {
|
|
15
|
+
return path.join(os.homedir(), s.slice(2));
|
|
16
|
+
}
|
|
17
|
+
return s;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isWsl() {
|
|
21
|
+
if (process.platform !== "linux") return false;
|
|
22
|
+
try {
|
|
23
|
+
const rel = os.release().toLowerCase();
|
|
24
|
+
if (rel.includes("microsoft") || rel.includes("wsl")) return true;
|
|
25
|
+
} catch {
|
|
26
|
+
/* ignore */
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const v = fs.readFileSync("/proc/version", "utf8").toLowerCase();
|
|
30
|
+
return v.includes("microsoft") || v.includes("wsl");
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function defaultLinkMode() {
|
|
37
|
+
if (process.platform === "win32") return "junction";
|
|
38
|
+
return "symlink";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function looksLikeWindowsPath(p) {
|
|
42
|
+
return /^[a-zA-Z]:[\\/]/.test(p) || p.startsWith("\\\\");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizePath(p) {
|
|
46
|
+
if (!p) return p;
|
|
47
|
+
try {
|
|
48
|
+
return fs.realpathSync.native
|
|
49
|
+
? fs.realpathSync.native(p)
|
|
50
|
+
: fs.realpathSync(p);
|
|
51
|
+
} catch {
|
|
52
|
+
return path.resolve(p);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function pathsEqual(a, b) {
|
|
57
|
+
if (!a || !b) return false;
|
|
58
|
+
const na = normalizePath(a);
|
|
59
|
+
const nb = normalizePath(b);
|
|
60
|
+
if (process.platform === "win32") {
|
|
61
|
+
return na.toLowerCase() === nb.toLowerCase();
|
|
62
|
+
}
|
|
63
|
+
return na === nb;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = {
|
|
67
|
+
expandHome,
|
|
68
|
+
isWsl,
|
|
69
|
+
defaultLinkMode,
|
|
70
|
+
looksLikeWindowsPath,
|
|
71
|
+
normalizePath,
|
|
72
|
+
pathsEqual,
|
|
73
|
+
};
|