@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/config.js
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const fs = require("fs");
|
|
4
|
-
const os = require("os");
|
|
5
4
|
const path = require("path");
|
|
5
|
+
const { execFileSync } = require("child_process");
|
|
6
|
+
const { expandHome } = require("./config-paths");
|
|
7
|
+
const { detectDefaultRemote } = require("./remote");
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
/** @deprecated No hardcoded personal default — use detectDefaultRemote(). */
|
|
10
|
+
const DEFAULT_REMOTE = null;
|
|
8
11
|
|
|
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
12
|
function machineConfigPath(env = process.env) {
|
|
18
13
|
if (env.AI_MD_CONFIG) return expandHome(env.AI_MD_CONFIG);
|
|
19
|
-
const xdg =
|
|
14
|
+
const xdg =
|
|
15
|
+
env.XDG_CONFIG_HOME ||
|
|
16
|
+
path.join(require("os").homedir(), ".config");
|
|
20
17
|
return path.join(xdg, "ai-md", "config.json");
|
|
21
18
|
}
|
|
22
19
|
|
|
@@ -36,20 +33,16 @@ function readMachineConfig(env = process.env) {
|
|
|
36
33
|
}
|
|
37
34
|
}
|
|
38
35
|
|
|
39
|
-
function writeMachineConfig(
|
|
36
|
+
function writeMachineConfig(patch, env = process.env, { dryRun = false } = {}) {
|
|
40
37
|
const configPath = machineConfigPath(env);
|
|
41
38
|
const existing = readMachineConfig(env).raw || {};
|
|
42
39
|
const next = {
|
|
43
40
|
...existing,
|
|
44
|
-
...
|
|
45
|
-
...(remote != null ? { remote: String(remote) } : {}),
|
|
41
|
+
...patch,
|
|
46
42
|
updatedAt: new Date().toISOString(),
|
|
47
43
|
};
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
err.code = "EINVAL";
|
|
51
|
-
throw err;
|
|
52
|
-
}
|
|
44
|
+
if (patch.dir != null) next.dir = expandHome(patch.dir);
|
|
45
|
+
if (patch.remote != null) next.remote = String(patch.remote);
|
|
53
46
|
if (dryRun) {
|
|
54
47
|
return { action: "would_write", path: configPath, config: next };
|
|
55
48
|
}
|
|
@@ -65,11 +58,9 @@ function writeMachineConfig({ dir, remote }, env = process.env, { dryRun = false
|
|
|
65
58
|
return { action: "wrote", path: configPath, config: next };
|
|
66
59
|
}
|
|
67
60
|
|
|
68
|
-
/**
|
|
69
|
-
* Precedence: explicit opts > env > machine config file > defaults.
|
|
70
|
-
* @param {{ dir?: string, remote?: string }} [opts]
|
|
71
|
-
*/
|
|
72
61
|
function resolveConfig(env = process.env, opts = {}) {
|
|
62
|
+
const os = require("os");
|
|
63
|
+
const { defaultLinkMode } = require("./config-paths");
|
|
73
64
|
const home = os.homedir();
|
|
74
65
|
const stored = readMachineConfig(env);
|
|
75
66
|
const defaultDir = path.join(home, ".ai-md");
|
|
@@ -77,11 +68,25 @@ function resolveConfig(env = process.env, opts = {}) {
|
|
|
77
68
|
const envDir = env.AI_MD_DIR || env.CURSOR_MD_DIR || null;
|
|
78
69
|
const envRemote = env.AI_MD_REMOTE || env.CURSOR_MD_REMOTE || null;
|
|
79
70
|
|
|
80
|
-
const dir = expandHome(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
71
|
+
const dir = expandHome(opts.dir || envDir || stored.dir || defaultDir);
|
|
72
|
+
|
|
73
|
+
let remote = opts.remote || envRemote || stored.remote || null;
|
|
74
|
+
let remoteSource = opts.remote
|
|
75
|
+
? "flag"
|
|
76
|
+
: envRemote
|
|
77
|
+
? "env"
|
|
78
|
+
: stored.remote
|
|
79
|
+
? "config"
|
|
80
|
+
: "none";
|
|
81
|
+
let remoteDetection = null;
|
|
82
|
+
|
|
83
|
+
if (!remote && opts.skipRemoteDetect !== true) {
|
|
84
|
+
remoteDetection = detectDefaultRemote();
|
|
85
|
+
if (remoteDetection.remote) {
|
|
86
|
+
remote = remoteDetection.remote;
|
|
87
|
+
remoteSource = "detected";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
85
90
|
|
|
86
91
|
const sources = {
|
|
87
92
|
dir: opts.dir
|
|
@@ -91,38 +96,53 @@ function resolveConfig(env = process.env, opts = {}) {
|
|
|
91
96
|
: stored.dir
|
|
92
97
|
? "config"
|
|
93
98
|
: "default",
|
|
94
|
-
remote:
|
|
95
|
-
? "flag"
|
|
96
|
-
: envRemote && !(stored.remote && envRemote === stored.remote)
|
|
97
|
-
? "env"
|
|
98
|
-
: stored.remote
|
|
99
|
-
? "config"
|
|
100
|
-
: "default",
|
|
99
|
+
remote: remoteSource,
|
|
101
100
|
};
|
|
102
101
|
|
|
102
|
+
const raw = stored.raw || {};
|
|
103
|
+
const agents = Array.isArray(raw.agents) && raw.agents.length
|
|
104
|
+
? raw.agents.map((a) => String(a).toLowerCase())
|
|
105
|
+
: ["cursor"];
|
|
106
|
+
const linkMode = opts.linkMode || raw.linkMode || defaultLinkMode();
|
|
107
|
+
|
|
103
108
|
const templatesDir = path.join(dir, "templates");
|
|
104
109
|
return {
|
|
105
110
|
home,
|
|
106
111
|
dir,
|
|
107
112
|
remote,
|
|
108
113
|
sources,
|
|
114
|
+
remoteDetection,
|
|
115
|
+
agents,
|
|
116
|
+
linkMode,
|
|
109
117
|
machineConfigPath: stored.path,
|
|
110
|
-
machineConfig:
|
|
118
|
+
machineConfig: raw,
|
|
111
119
|
cursorSkills: path.join(home, ".cursor", "skills"),
|
|
112
120
|
cursorRules: path.join(home, ".cursor", "rules"),
|
|
113
121
|
projectsDir: path.join(dir, "projects"),
|
|
114
122
|
templatesDir,
|
|
115
123
|
templateDir: path.join(templatesDir, "base"),
|
|
116
|
-
|
|
117
|
-
|
|
124
|
+
// legacy aliases → shared (build uses shared/)
|
|
125
|
+
skillsDir: path.join(dir, "shared", "skills"),
|
|
126
|
+
rulesDir: path.join(dir, "shared", "rules"),
|
|
127
|
+
sharedDir: path.join(dir, "shared"),
|
|
128
|
+
agentsDir: path.join(dir, "agents"),
|
|
129
|
+
distDir: path.join(dir, "dist"),
|
|
118
130
|
};
|
|
119
131
|
}
|
|
120
132
|
|
|
121
133
|
function applyEnvFromConfig(cfg) {
|
|
122
134
|
process.env.AI_MD_DIR = cfg.dir;
|
|
123
|
-
process.env.AI_MD_REMOTE = cfg.remote;
|
|
124
135
|
process.env.CURSOR_MD_DIR = cfg.dir;
|
|
125
|
-
|
|
136
|
+
// Never invent remotes into the environment (avoids fake defaults and
|
|
137
|
+
// flipping sources.remote to "env" on re-resolve). Only pass through when
|
|
138
|
+
// the process already had one, or the caller passed --remote (flag).
|
|
139
|
+
if (
|
|
140
|
+
cfg.remote &&
|
|
141
|
+
(cfg.sources.remote === "env" || cfg.sources.remote === "flag")
|
|
142
|
+
) {
|
|
143
|
+
process.env.AI_MD_REMOTE = cfg.remote;
|
|
144
|
+
process.env.CURSOR_MD_REMOTE = cfg.remote;
|
|
145
|
+
}
|
|
126
146
|
}
|
|
127
147
|
|
|
128
148
|
function templatePath(cfg, name = "base") {
|
|
@@ -142,16 +162,14 @@ function countRules(rulesDir) {
|
|
|
142
162
|
if (!fs.existsSync(rulesDir)) return 0;
|
|
143
163
|
return fs
|
|
144
164
|
.readdirSync(rulesDir)
|
|
145
|
-
.filter((f) => f.endsWith(".mdc") || f.endsWith(".md"))
|
|
146
|
-
.length;
|
|
165
|
+
.filter((f) => f.endsWith(".mdc") || f.endsWith(".md")).length;
|
|
147
166
|
}
|
|
148
167
|
|
|
149
168
|
function countSkills(skillsDir) {
|
|
150
169
|
if (!fs.existsSync(skillsDir)) return 0;
|
|
151
170
|
return fs
|
|
152
171
|
.readdirSync(skillsDir, { withFileTypes: true })
|
|
153
|
-
.filter((d) => d.isDirectory() && !d.name.startsWith("."))
|
|
154
|
-
.length;
|
|
172
|
+
.filter((d) => d.isDirectory() && !d.name.startsWith(".")).length;
|
|
155
173
|
}
|
|
156
174
|
|
|
157
175
|
function listSkillNames(skillsDir) {
|
|
@@ -172,33 +190,24 @@ function listRuleNames(rulesDir) {
|
|
|
172
190
|
}
|
|
173
191
|
|
|
174
192
|
function symlinkState(linkPath, expected) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
stat = fs.lstatSync(linkPath);
|
|
178
|
-
} catch {
|
|
179
|
-
return { path: linkPath, state: "missing", target: null, expected };
|
|
180
|
-
}
|
|
181
|
-
if (!stat.isSymbolicLink()) {
|
|
182
|
-
return { path: linkPath, state: "not_symlink", target: null, expected };
|
|
183
|
-
}
|
|
184
|
-
const target = fs.readlinkSync(linkPath);
|
|
185
|
-
if (target === expected) {
|
|
186
|
-
return { path: linkPath, state: "ok", target, expected };
|
|
187
|
-
}
|
|
188
|
-
return { path: linkPath, state: "wrong_target", target, expected };
|
|
193
|
+
const { linkState } = require("./link");
|
|
194
|
+
return linkState(linkPath, expected);
|
|
189
195
|
}
|
|
190
196
|
|
|
197
|
+
/** @deprecated use harnesses.selectHarnesses */
|
|
191
198
|
function agentSkillTargets(home, agents) {
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
claude: path.join(home, ".claude", "skills"),
|
|
195
|
-
agents: path.join(home, ".agents", "skills"),
|
|
196
|
-
};
|
|
199
|
+
const { resolveHarness } = require("./harnesses");
|
|
200
|
+
const cfg = { home, machineConfig: {} };
|
|
197
201
|
return (agents || [])
|
|
198
|
-
.map((a) =>
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
+
.map((a) => {
|
|
203
|
+
try {
|
|
204
|
+
const h = resolveHarness(a, { ...cfg, home });
|
|
205
|
+
return h.skills ? { name: h.id, path: h.skills } : null;
|
|
206
|
+
} catch {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
.filter(Boolean);
|
|
202
211
|
}
|
|
203
212
|
|
|
204
213
|
function migrateLegacyTemplate(cfg, { dryRun = false } = {}) {
|
|
@@ -213,6 +222,51 @@ function migrateLegacyTemplate(cfg, { dryRun = false } = {}) {
|
|
|
213
222
|
return { action: "migrated", from: legacy, to: dest };
|
|
214
223
|
}
|
|
215
224
|
|
|
225
|
+
function gitClone(remote, dir, { dryRun = false } = {}) {
|
|
226
|
+
if (!remote) {
|
|
227
|
+
const err = new Error(
|
|
228
|
+
"git clone requires a remote URL (pass --remote or set AI_MD_REMOTE)"
|
|
229
|
+
);
|
|
230
|
+
err.code = "EINVAL";
|
|
231
|
+
throw err;
|
|
232
|
+
}
|
|
233
|
+
if (dryRun) return { action: "would_clone", remote, dir };
|
|
234
|
+
fs.mkdirSync(path.dirname(dir), { recursive: true });
|
|
235
|
+
execFileSync("git", ["clone", remote, dir], { stdio: "inherit" });
|
|
236
|
+
return { action: "cloned", remote, dir };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function gitPull(dir, { dryRun = false } = {}) {
|
|
240
|
+
if (dryRun) return { action: "would_pull", dir };
|
|
241
|
+
execFileSync("git", ["-C", dir, "pull", "--rebase", "--autostash"], {
|
|
242
|
+
stdio: "inherit",
|
|
243
|
+
});
|
|
244
|
+
return { action: "pulled", dir };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function gitPush(dir, { message, dryRun = false } = {}) {
|
|
248
|
+
if (dryRun) return { action: "would_push", dir, message };
|
|
249
|
+
execFileSync("git", ["-C", dir, "add", "-A"], { stdio: "inherit" });
|
|
250
|
+
let dirty = true;
|
|
251
|
+
try {
|
|
252
|
+
execFileSync("git", ["-C", dir, "diff", "--cached", "--quiet"], {
|
|
253
|
+
stdio: "pipe",
|
|
254
|
+
});
|
|
255
|
+
dirty = false;
|
|
256
|
+
} catch {
|
|
257
|
+
dirty = true;
|
|
258
|
+
}
|
|
259
|
+
if (dirty) {
|
|
260
|
+
execFileSync(
|
|
261
|
+
"git",
|
|
262
|
+
["-C", dir, "commit", "-m", message || "Update personal AI skills/rules"],
|
|
263
|
+
{ stdio: "inherit" }
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
execFileSync("git", ["-C", dir, "push"], { stdio: "inherit" });
|
|
267
|
+
return { action: "pushed", dir, committed: dirty };
|
|
268
|
+
}
|
|
269
|
+
|
|
216
270
|
module.exports = {
|
|
217
271
|
DEFAULT_REMOTE,
|
|
218
272
|
machineConfigPath,
|
|
@@ -230,4 +284,7 @@ module.exports = {
|
|
|
230
284
|
listRuleNames,
|
|
231
285
|
symlinkState,
|
|
232
286
|
agentSkillTargets,
|
|
287
|
+
gitClone,
|
|
288
|
+
gitPull,
|
|
289
|
+
gitPush,
|
|
233
290
|
};
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { expandHome } = require("../config-paths");
|
|
7
|
+
|
|
8
|
+
/** Built-in harness registry. codex aliases agents (shared ~/.agents/skills). */
|
|
9
|
+
const BUILTINS = {
|
|
10
|
+
cursor: {
|
|
11
|
+
id: "cursor",
|
|
12
|
+
tier: "A",
|
|
13
|
+
format: "mdc",
|
|
14
|
+
emitter: "full",
|
|
15
|
+
skills: "~/.cursor/skills",
|
|
16
|
+
rules: "~/.cursor/rules",
|
|
17
|
+
unique: true,
|
|
18
|
+
aliasOf: null,
|
|
19
|
+
detect: { dirs: ["~/.cursor"], bins: ["cursor", "agent"] },
|
|
20
|
+
},
|
|
21
|
+
claude: {
|
|
22
|
+
id: "claude",
|
|
23
|
+
tier: "A",
|
|
24
|
+
format: "md",
|
|
25
|
+
emitter: "full",
|
|
26
|
+
skills: "~/.claude/skills",
|
|
27
|
+
rules: "~/.claude/rules",
|
|
28
|
+
unique: true,
|
|
29
|
+
aliasOf: null,
|
|
30
|
+
detect: { dirs: ["~/.claude"], bins: ["claude"] },
|
|
31
|
+
},
|
|
32
|
+
agents: {
|
|
33
|
+
id: "agents",
|
|
34
|
+
tier: "A",
|
|
35
|
+
format: "skills-only",
|
|
36
|
+
emitter: "full",
|
|
37
|
+
skills: "~/.agents/skills",
|
|
38
|
+
rules: null,
|
|
39
|
+
unique: false,
|
|
40
|
+
aliasOf: null,
|
|
41
|
+
detect: { dirs: ["~/.agents", "~/.codex"], bins: ["codex"] },
|
|
42
|
+
},
|
|
43
|
+
codex: {
|
|
44
|
+
id: "codex",
|
|
45
|
+
tier: "A",
|
|
46
|
+
format: "skills-only",
|
|
47
|
+
emitter: "full",
|
|
48
|
+
skills: "~/.agents/skills",
|
|
49
|
+
rules: null,
|
|
50
|
+
unique: false,
|
|
51
|
+
aliasOf: "agents",
|
|
52
|
+
detect: { dirs: ["~/.codex", "~/.agents"], bins: ["codex"] },
|
|
53
|
+
},
|
|
54
|
+
gemini: {
|
|
55
|
+
id: "gemini",
|
|
56
|
+
tier: "A",
|
|
57
|
+
format: "skills-only",
|
|
58
|
+
emitter: "full",
|
|
59
|
+
skills: "~/.gemini/skills",
|
|
60
|
+
rules: null,
|
|
61
|
+
unique: true,
|
|
62
|
+
aliasOf: null,
|
|
63
|
+
detect: { dirs: ["~/.gemini"], bins: ["gemini"] },
|
|
64
|
+
},
|
|
65
|
+
opencode: {
|
|
66
|
+
id: "opencode",
|
|
67
|
+
tier: "A",
|
|
68
|
+
format: "skills-only",
|
|
69
|
+
emitter: "full",
|
|
70
|
+
skills: "~/.config/opencode/skills",
|
|
71
|
+
rules: null,
|
|
72
|
+
unique: true,
|
|
73
|
+
aliasOf: null,
|
|
74
|
+
detect: { dirs: ["~/.config/opencode"], bins: ["opencode"] },
|
|
75
|
+
},
|
|
76
|
+
copilot: {
|
|
77
|
+
id: "copilot",
|
|
78
|
+
tier: "B",
|
|
79
|
+
format: "skills-only",
|
|
80
|
+
emitter: "stub",
|
|
81
|
+
skills: "~/.copilot/skills",
|
|
82
|
+
rules: null,
|
|
83
|
+
unique: true,
|
|
84
|
+
aliasOf: null,
|
|
85
|
+
detect: { dirs: ["~/.copilot"], bins: ["copilot"] },
|
|
86
|
+
},
|
|
87
|
+
windsurf: {
|
|
88
|
+
id: "windsurf",
|
|
89
|
+
tier: "B",
|
|
90
|
+
format: "skills-only",
|
|
91
|
+
emitter: "stub",
|
|
92
|
+
skills: null,
|
|
93
|
+
rules: null,
|
|
94
|
+
unique: true,
|
|
95
|
+
aliasOf: null,
|
|
96
|
+
detect: { dirs: ["~/.codeium"], bins: ["windsurf"] },
|
|
97
|
+
},
|
|
98
|
+
grok: {
|
|
99
|
+
id: "grok",
|
|
100
|
+
tier: "B",
|
|
101
|
+
format: "skills-only",
|
|
102
|
+
emitter: "stub",
|
|
103
|
+
skills: null,
|
|
104
|
+
rules: null,
|
|
105
|
+
unique: true,
|
|
106
|
+
aliasOf: null,
|
|
107
|
+
detect: { dirs: ["~/.grok"], bins: ["grok"] },
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
function which(bin) {
|
|
112
|
+
const pathEnv = process.env.PATH || "";
|
|
113
|
+
const sep = process.platform === "win32" ? ";" : ":";
|
|
114
|
+
const exts =
|
|
115
|
+
process.platform === "win32"
|
|
116
|
+
? (process.env.PATHEXT || ".EXE;.CMD;.BAT").split(";").filter(Boolean)
|
|
117
|
+
: [""];
|
|
118
|
+
for (const dir of pathEnv.split(sep)) {
|
|
119
|
+
if (!dir) continue;
|
|
120
|
+
for (const ext of exts) {
|
|
121
|
+
const candidate = path.join(dir, bin + ext);
|
|
122
|
+
try {
|
|
123
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
124
|
+
return candidate;
|
|
125
|
+
} catch {
|
|
126
|
+
/* try next */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function dirExists(p) {
|
|
134
|
+
try {
|
|
135
|
+
return fs.statSync(expandHome(p)).isDirectory();
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* True if the harness looks installed on this machine.
|
|
143
|
+
* Custom harnesses: installed if any configured live parent exists, or always if --force-link.
|
|
144
|
+
*/
|
|
145
|
+
function isHarnessInstalled(def, { forceLink = false } = {}) {
|
|
146
|
+
if (forceLink) return true;
|
|
147
|
+
if (!def) return false;
|
|
148
|
+
if (def.detect) {
|
|
149
|
+
for (const d of def.detect.dirs || []) {
|
|
150
|
+
if (dirExists(d)) return true;
|
|
151
|
+
}
|
|
152
|
+
for (const b of def.detect.bins || []) {
|
|
153
|
+
if (which(b)) return true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Custom / override: consider installed if skills or rules parent exists
|
|
157
|
+
if (def.skills && dirExists(path.dirname(expandHome(def.skills)))) return true;
|
|
158
|
+
if (def.rules && dirExists(path.dirname(expandHome(def.rules)))) return true;
|
|
159
|
+
// Built-in with no detect hit
|
|
160
|
+
if (BUILTINS[def.id] && def.emitter === "stub" && !def.skills) return false;
|
|
161
|
+
if (!BUILTINS[def.id] && (def.skills || def.rules)) {
|
|
162
|
+
// user-registered: require parent dir or force
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function getBuiltin(id) {
|
|
169
|
+
return BUILTINS[String(id).toLowerCase()] || null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function listBuiltinIds() {
|
|
173
|
+
return Object.keys(BUILTINS).sort();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Merge built-in + machine config harnesses.<id> overrides.
|
|
178
|
+
* @returns {object} resolved harness definition
|
|
179
|
+
*/
|
|
180
|
+
function resolveHarness(id, cfg, oneShot = {}) {
|
|
181
|
+
const key = String(id).toLowerCase();
|
|
182
|
+
const builtin = getBuiltin(key);
|
|
183
|
+
const overrides = (cfg.machineConfig && cfg.machineConfig.harnesses) || {};
|
|
184
|
+
const over = overrides[key] || {};
|
|
185
|
+
|
|
186
|
+
const base = builtin
|
|
187
|
+
? { ...builtin }
|
|
188
|
+
: {
|
|
189
|
+
id: key,
|
|
190
|
+
tier: "C",
|
|
191
|
+
format: "skills-only",
|
|
192
|
+
emitter: "full",
|
|
193
|
+
skills: null,
|
|
194
|
+
rules: null,
|
|
195
|
+
unique: true,
|
|
196
|
+
aliasOf: null,
|
|
197
|
+
detect: null,
|
|
198
|
+
custom: true,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const skills =
|
|
202
|
+
oneShot.skills ||
|
|
203
|
+
over.skills ||
|
|
204
|
+
base.skills;
|
|
205
|
+
const rules =
|
|
206
|
+
oneShot.rules !== undefined
|
|
207
|
+
? oneShot.rules
|
|
208
|
+
: over.rules !== undefined
|
|
209
|
+
? over.rules
|
|
210
|
+
: base.rules;
|
|
211
|
+
const format =
|
|
212
|
+
oneShot.format ||
|
|
213
|
+
over.format ||
|
|
214
|
+
base.format ||
|
|
215
|
+
(skills && rules ? "md" : "skills-only");
|
|
216
|
+
|
|
217
|
+
const enabled =
|
|
218
|
+
over.enabled !== undefined
|
|
219
|
+
? Boolean(over.enabled)
|
|
220
|
+
: builtin
|
|
221
|
+
? true
|
|
222
|
+
: true;
|
|
223
|
+
|
|
224
|
+
const distId = base.aliasOf || key;
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
...base,
|
|
228
|
+
id: key,
|
|
229
|
+
skills: skills ? expandHome(skills) : null,
|
|
230
|
+
rules: rules ? expandHome(rules) : null,
|
|
231
|
+
skillsRaw: skills || null,
|
|
232
|
+
rulesRaw: rules || null,
|
|
233
|
+
format,
|
|
234
|
+
enabled,
|
|
235
|
+
distId,
|
|
236
|
+
override: Boolean(over.skills || over.rules || over.format || over.enabled !== undefined),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Expand enabled agent list: drop aliases that duplicate canonical, warn.
|
|
242
|
+
* Filters to installed harnesses unless forceLink.
|
|
243
|
+
*/
|
|
244
|
+
function selectHarnesses(cfg, agentIds, { forceLink = false, includeUninstalled = false } = {}) {
|
|
245
|
+
const requested = (agentIds && agentIds.length ? agentIds : cfg.agents || ["cursor"]).map(
|
|
246
|
+
(a) => String(a).trim().toLowerCase()
|
|
247
|
+
);
|
|
248
|
+
const seenLive = new Map(); // live skills path -> id
|
|
249
|
+
const selected = [];
|
|
250
|
+
const skipped = [];
|
|
251
|
+
const warnings = [];
|
|
252
|
+
|
|
253
|
+
for (const id of requested) {
|
|
254
|
+
let def;
|
|
255
|
+
try {
|
|
256
|
+
def = resolveHarness(id, cfg);
|
|
257
|
+
} catch (e) {
|
|
258
|
+
skipped.push({ id, reason: e.message });
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (!def.enabled) {
|
|
262
|
+
skipped.push({ id, reason: "disabled" });
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (def.emitter === "stub" && !def.skills && !def.rules) {
|
|
266
|
+
skipped.push({ id, reason: "stub_no_paths" });
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (!includeUninstalled && !isHarnessInstalled(def, { forceLink })) {
|
|
270
|
+
skipped.push({ id, reason: "not_installed" });
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (def.aliasOf) {
|
|
274
|
+
warnings.push(`${id} is an alias of ${def.aliasOf}; using dist/${def.distId}`);
|
|
275
|
+
}
|
|
276
|
+
const liveKey = def.skills || def.rules || def.id;
|
|
277
|
+
if (seenLive.has(liveKey)) {
|
|
278
|
+
warnings.push(
|
|
279
|
+
`skip ${id}: shares live path with ${seenLive.get(liveKey)} (${liveKey})`
|
|
280
|
+
);
|
|
281
|
+
skipped.push({ id, reason: "shared_live_path", with: seenLive.get(liveKey) });
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
seenLive.set(liveKey, def.distId);
|
|
285
|
+
selected.push(def);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return { selected, skipped, warnings };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function ensureAgentSourceDirs(cfg, harnessId) {
|
|
292
|
+
const id = harnessId === "codex" ? "agents" : harnessId;
|
|
293
|
+
const root = path.join(cfg.dir, "agents", id);
|
|
294
|
+
fs.mkdirSync(path.join(root, "rules"), { recursive: true });
|
|
295
|
+
fs.mkdirSync(path.join(root, "skills"), { recursive: true });
|
|
296
|
+
return root;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
module.exports = {
|
|
300
|
+
BUILTINS,
|
|
301
|
+
getBuiltin,
|
|
302
|
+
listBuiltinIds,
|
|
303
|
+
resolveHarness,
|
|
304
|
+
selectHarnesses,
|
|
305
|
+
isHarnessInstalled,
|
|
306
|
+
ensureAgentSourceDirs,
|
|
307
|
+
which,
|
|
308
|
+
dirExists,
|
|
309
|
+
};
|