@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
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 } = 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,248 @@ 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
|
+
if (!fs.existsSync(dir)) {
|
|
263
|
+
gitClone(cfg.remote, dir, { dryRun: opts.dryRun });
|
|
264
|
+
} else if (!fs.existsSync(path.join(dir, ".git"))) {
|
|
265
|
+
const err = new Error(
|
|
266
|
+
`${dir} exists but is not a git repo. Use ai-md init or remove it.`
|
|
267
|
+
);
|
|
268
|
+
err.code = "EEXIST";
|
|
269
|
+
throw err;
|
|
270
|
+
}
|
|
271
|
+
// migrate hint: if legacy flat rules/ exist, status will note
|
|
272
|
+
return buildAndLink(cfg, {
|
|
273
|
+
agents: opts.agents,
|
|
274
|
+
force: opts.force,
|
|
275
|
+
dryRun: opts.dryRun,
|
|
276
|
+
forceLink: opts.forceLink,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function runPull(cfg, opts = {}) {
|
|
281
|
+
if (!fs.existsSync(path.join(cfg.dir, ".git"))) {
|
|
282
|
+
const err = new Error(`${cfg.dir} is not a git repo; run ai-md install`);
|
|
283
|
+
err.code = "ENOENT";
|
|
284
|
+
throw err;
|
|
285
|
+
}
|
|
286
|
+
gitPull(cfg.dir, { dryRun: opts.dryRun });
|
|
287
|
+
return buildAndLink(cfg, {
|
|
288
|
+
agents: opts.agents,
|
|
289
|
+
force: opts.force,
|
|
290
|
+
dryRun: opts.dryRun,
|
|
291
|
+
forceLink: opts.forceLink,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function runPush(cfg, opts = {}) {
|
|
296
|
+
return gitPush(cfg.dir, {
|
|
297
|
+
message: opts.message,
|
|
298
|
+
dryRun: opts.dryRun,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function runDoctor({
|
|
303
|
+
fix = false,
|
|
304
|
+
force = false,
|
|
305
|
+
agents = ["cursor"],
|
|
306
|
+
dryRun = false,
|
|
307
|
+
forceLink = false,
|
|
308
|
+
} = {}) {
|
|
228
309
|
const cfg = resolveConfig();
|
|
229
310
|
const status = collectStatus({ full: true, agents });
|
|
230
311
|
const repairs = [];
|
|
231
312
|
if (fix || force) {
|
|
232
|
-
|
|
233
|
-
|
|
313
|
+
try {
|
|
314
|
+
const bl = buildAndLink(cfg, {
|
|
315
|
+
agents,
|
|
316
|
+
force: true,
|
|
317
|
+
dryRun,
|
|
318
|
+
forceLink,
|
|
319
|
+
});
|
|
320
|
+
repairs.push(bl);
|
|
321
|
+
} catch (e) {
|
|
322
|
+
repairs.push({ error: e.message, code: e.code, dirty: e.dirty });
|
|
323
|
+
}
|
|
234
324
|
}
|
|
235
325
|
const after = fix || force ? collectStatus({ full: true, agents }) : status;
|
|
236
326
|
return {
|
|
237
|
-
before: {
|
|
238
|
-
problems: status.problems,
|
|
239
|
-
counts: status.counts,
|
|
240
|
-
},
|
|
327
|
+
before: { problems: status.problems, counts: status.counts },
|
|
241
328
|
repairs,
|
|
242
329
|
after: {
|
|
243
330
|
problems: after.problems,
|
|
244
331
|
counts: after.counts,
|
|
245
332
|
links: after.links,
|
|
333
|
+
harnesses: after.harnesses,
|
|
246
334
|
projects: after.projects,
|
|
247
335
|
},
|
|
248
336
|
help: statusHelp(after),
|
|
337
|
+
wsl: wslWarnings(cfg, []),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function harnessList(cfg) {
|
|
342
|
+
const ids = new Set([
|
|
343
|
+
...listBuiltinIds(),
|
|
344
|
+
...Object.keys((cfg.machineConfig && cfg.machineConfig.harnesses) || {}),
|
|
345
|
+
]);
|
|
346
|
+
return [...ids].sort().map((id) => {
|
|
347
|
+
const def = resolveHarness(id, cfg);
|
|
348
|
+
return {
|
|
349
|
+
id: def.id,
|
|
350
|
+
format: def.format,
|
|
351
|
+
emitter: def.emitter,
|
|
352
|
+
aliasOf: def.aliasOf,
|
|
353
|
+
skills: def.skills,
|
|
354
|
+
rules: def.rules,
|
|
355
|
+
enabled: def.enabled,
|
|
356
|
+
installed: isHarnessInstalled(def),
|
|
357
|
+
distId: def.distId,
|
|
358
|
+
tier: def.tier,
|
|
359
|
+
};
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function harnessShow(cfg, id) {
|
|
364
|
+
const def = resolveHarness(id, cfg);
|
|
365
|
+
return {
|
|
366
|
+
...def,
|
|
367
|
+
installed: isHarnessInstalled(def),
|
|
368
|
+
builtin: Boolean(getBuiltin(id)),
|
|
249
369
|
};
|
|
250
370
|
}
|
|
251
371
|
|
|
372
|
+
function harnessSet(cfg, id, { skills, rules, format } = {}) {
|
|
373
|
+
const key = String(id).toLowerCase();
|
|
374
|
+
const existing = { ...(cfg.machineConfig || {}) };
|
|
375
|
+
const harnesses = { ...(existing.harnesses || {}) };
|
|
376
|
+
const cur = { ...(harnesses[key] || {}) };
|
|
377
|
+
if (skills != null) cur.skills = skills;
|
|
378
|
+
if (rules != null) cur.rules = rules;
|
|
379
|
+
if (format != null) cur.format = format;
|
|
380
|
+
cur.enabled = cur.enabled !== false;
|
|
381
|
+
harnesses[key] = cur;
|
|
382
|
+
const agents = existing.agents || ["cursor"];
|
|
383
|
+
if (!agents.includes(key) && key !== "codex") {
|
|
384
|
+
agents.push(key);
|
|
385
|
+
}
|
|
386
|
+
const saved = writeMachineConfig({ ...existing, harnesses, agents });
|
|
387
|
+
ensureAgentSourceDirs(
|
|
388
|
+
{ ...cfg, dir: saved.config.dir || cfg.dir },
|
|
389
|
+
key === "codex" ? "agents" : key
|
|
390
|
+
);
|
|
391
|
+
return { id: key, harness: cur, config: saved };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function harnessUnset(cfg, id, { pathsOnly = false } = {}) {
|
|
395
|
+
const key = String(id).toLowerCase();
|
|
396
|
+
const existing = { ...(cfg.machineConfig || {}) };
|
|
397
|
+
const harnesses = { ...(existing.harnesses || {}) };
|
|
398
|
+
if (pathsOnly && harnesses[key]) {
|
|
399
|
+
delete harnesses[key].skills;
|
|
400
|
+
delete harnesses[key].rules;
|
|
401
|
+
delete harnesses[key].format;
|
|
402
|
+
} else {
|
|
403
|
+
delete harnesses[key];
|
|
404
|
+
}
|
|
405
|
+
return writeMachineConfig({ ...existing, harnesses });
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function harnessEnable(cfg, id, enabled) {
|
|
409
|
+
const key = String(id).toLowerCase();
|
|
410
|
+
const existing = { ...(cfg.machineConfig || {}) };
|
|
411
|
+
const harnesses = { ...(existing.harnesses || {}) };
|
|
412
|
+
harnesses[key] = { ...(harnesses[key] || {}), enabled };
|
|
413
|
+
let agents = [...(existing.agents || ["cursor"])];
|
|
414
|
+
if (enabled && !agents.includes(key) && key !== "codex") agents.push(key);
|
|
415
|
+
if (!enabled) agents = agents.filter((a) => a !== key);
|
|
416
|
+
return writeMachineConfig({ ...existing, harnesses, agents });
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// legacy exports expected by older callers
|
|
420
|
+
function ensureCursorLinks(cfg, opts = {}) {
|
|
421
|
+
return linkHarnesses(cfg, ["cursor"], opts).links;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function ensureAgentSkillLinks(cfg, agents, opts = {}) {
|
|
425
|
+
return linkHarnesses(cfg, agents, opts).links;
|
|
426
|
+
}
|
|
427
|
+
|
|
252
428
|
module.exports = {
|
|
253
429
|
ensureCursorLinks,
|
|
254
430
|
ensureAgentSkillLinks,
|
|
@@ -258,5 +434,22 @@ module.exports = {
|
|
|
258
434
|
linkProject,
|
|
259
435
|
runDoctor,
|
|
260
436
|
linkPath,
|
|
437
|
+
linkHarnesses,
|
|
438
|
+
buildAndLink,
|
|
439
|
+
runInstall,
|
|
440
|
+
runPull,
|
|
441
|
+
runPush,
|
|
442
|
+
runBuild,
|
|
443
|
+
runRescue,
|
|
444
|
+
seedSkeleton,
|
|
445
|
+
initRepo,
|
|
446
|
+
harnessList,
|
|
447
|
+
harnessShow,
|
|
448
|
+
harnessSet,
|
|
449
|
+
harnessUnset,
|
|
450
|
+
harnessEnable,
|
|
261
451
|
collectProjects,
|
|
452
|
+
SKELETON_VERSION,
|
|
453
|
+
distDirty,
|
|
454
|
+
defaultLinkMode,
|
|
262
455
|
};
|
|
@@ -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
|
+
};
|