agent-mgr 0.2.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 +113 -0
- package/dist/index.js +1477 -0
- package/package.json +47 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1477 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { program } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import { join as join7 } from "path";
|
|
8
|
+
import { existsSync as existsSync4 } from "fs";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
import { checkbox, confirm } from "@inquirer/prompts";
|
|
11
|
+
|
|
12
|
+
// src/adapters/claude-code.ts
|
|
13
|
+
import { homedir } from "os";
|
|
14
|
+
import { join as join2 } from "path";
|
|
15
|
+
|
|
16
|
+
// src/lib/fs-utils.ts
|
|
17
|
+
import { mkdirSync, existsSync, symlinkSync, copyFileSync, readFileSync, writeFileSync, unlinkSync, readdirSync, appendFileSync } from "fs";
|
|
18
|
+
import { dirname, join, relative, extname } from "path";
|
|
19
|
+
function ensureDir(dir) {
|
|
20
|
+
if (!existsSync(dir)) {
|
|
21
|
+
mkdirSync(dir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function syncFile(source, dest, method = "symlink") {
|
|
25
|
+
ensureDir(dirname(dest));
|
|
26
|
+
if (existsSync(dest)) unlinkSync(dest);
|
|
27
|
+
if (method === "symlink") {
|
|
28
|
+
const rel = relative(dirname(dest), source);
|
|
29
|
+
symlinkSync(rel, dest);
|
|
30
|
+
} else {
|
|
31
|
+
copyFileSync(source, dest);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function removeFile(filePath) {
|
|
35
|
+
if (existsSync(filePath)) {
|
|
36
|
+
unlinkSync(filePath);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
function listMarkdownFiles(dir) {
|
|
42
|
+
if (!existsSync(dir)) return [];
|
|
43
|
+
return readdirSync(dir, { recursive: true }).map((f) => f.toString()).filter((f) => extname(f) === ".md");
|
|
44
|
+
}
|
|
45
|
+
function readJson(path) {
|
|
46
|
+
if (!existsSync(path)) return {};
|
|
47
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
48
|
+
}
|
|
49
|
+
function writeJson(path, data) {
|
|
50
|
+
ensureDir(dirname(path));
|
|
51
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
|
|
52
|
+
}
|
|
53
|
+
function parseSimpleYaml(text) {
|
|
54
|
+
const result = {};
|
|
55
|
+
let currentKey = null;
|
|
56
|
+
let currentList = null;
|
|
57
|
+
for (const line of text.split("\n")) {
|
|
58
|
+
const trimmed = line.trim();
|
|
59
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
60
|
+
if (trimmed.startsWith("- ") && currentKey) {
|
|
61
|
+
if (!currentList) currentList = [];
|
|
62
|
+
currentList.push(trimmed.slice(2).trim());
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (currentKey && currentList) {
|
|
66
|
+
result[currentKey] = currentList;
|
|
67
|
+
currentList = null;
|
|
68
|
+
}
|
|
69
|
+
const match = trimmed.match(/^(\w+):\s*(.*)$/);
|
|
70
|
+
if (match) {
|
|
71
|
+
currentKey = match[1];
|
|
72
|
+
const value = match[2].trim();
|
|
73
|
+
if (value) {
|
|
74
|
+
result[currentKey] = value;
|
|
75
|
+
currentKey = null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (currentKey && currentList) {
|
|
80
|
+
result[currentKey] = currentList;
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
function serializeSimpleYaml(data) {
|
|
85
|
+
let out = "";
|
|
86
|
+
for (const [key, value] of Object.entries(data)) {
|
|
87
|
+
if (Array.isArray(value)) {
|
|
88
|
+
out += `${key}:
|
|
89
|
+
`;
|
|
90
|
+
for (const item of value) {
|
|
91
|
+
out += ` - ${item}
|
|
92
|
+
`;
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
out += `${key}: ${value}
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
function readYaml(path) {
|
|
102
|
+
if (!existsSync(path)) return {};
|
|
103
|
+
const text = readFileSync(path, "utf-8");
|
|
104
|
+
return parseSimpleYaml(text);
|
|
105
|
+
}
|
|
106
|
+
function writeYaml(path, data) {
|
|
107
|
+
ensureDir(dirname(path));
|
|
108
|
+
writeFileSync(path, serializeSimpleYaml(data));
|
|
109
|
+
}
|
|
110
|
+
function writeToml(path, data) {
|
|
111
|
+
ensureDir(dirname(path));
|
|
112
|
+
writeFileSync(path, serializeToml(data));
|
|
113
|
+
}
|
|
114
|
+
function readToml(path) {
|
|
115
|
+
if (!existsSync(path)) return {};
|
|
116
|
+
const text = readFileSync(path, "utf-8");
|
|
117
|
+
return parseSimpleToml(text);
|
|
118
|
+
}
|
|
119
|
+
function serializeToml(data, prefix = "") {
|
|
120
|
+
let out = "";
|
|
121
|
+
for (const [key, value] of Object.entries(data)) {
|
|
122
|
+
if (value === null || value === void 0) continue;
|
|
123
|
+
if (typeof value === "object" && !Array.isArray(value)) {
|
|
124
|
+
const section = prefix ? `${prefix}.${key}` : key;
|
|
125
|
+
out += `[${section}]
|
|
126
|
+
`;
|
|
127
|
+
for (const [k, v] of Object.entries(value)) {
|
|
128
|
+
if (typeof v === "object" && !Array.isArray(v) && v !== null) {
|
|
129
|
+
out += serializeToml({ [k]: v }, section);
|
|
130
|
+
} else {
|
|
131
|
+
out += `${k} = ${tomlValue(v)}
|
|
132
|
+
`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
out += "\n";
|
|
136
|
+
} else {
|
|
137
|
+
out += `${key} = ${tomlValue(value)}
|
|
138
|
+
`;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
function tomlValue(v) {
|
|
144
|
+
if (typeof v === "string") return `"${v.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
145
|
+
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
146
|
+
if (Array.isArray(v)) return `[${v.map(tomlValue).join(", ")}]`;
|
|
147
|
+
if (typeof v === "object" && v !== null) {
|
|
148
|
+
const entries = Object.entries(v);
|
|
149
|
+
return `{ ${entries.map(([k, val]) => `${k} = ${tomlValue(val)}`).join(", ")} }`;
|
|
150
|
+
}
|
|
151
|
+
return `"${v}"`;
|
|
152
|
+
}
|
|
153
|
+
function parseSimpleToml(text) {
|
|
154
|
+
const result = {};
|
|
155
|
+
let currentSection = "";
|
|
156
|
+
for (const line of text.split("\n")) {
|
|
157
|
+
const trimmed = line.trim();
|
|
158
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
159
|
+
const sectionMatch = trimmed.match(/^\[(.+)\]$/);
|
|
160
|
+
if (sectionMatch) {
|
|
161
|
+
currentSection = sectionMatch[1];
|
|
162
|
+
const parts = currentSection.split(".");
|
|
163
|
+
let obj = result;
|
|
164
|
+
for (const part of parts) {
|
|
165
|
+
if (!(part in obj)) obj[part] = {};
|
|
166
|
+
obj = obj[part];
|
|
167
|
+
}
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const kvMatch = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
|
|
171
|
+
if (kvMatch) {
|
|
172
|
+
const key = kvMatch[1];
|
|
173
|
+
const rawVal = kvMatch[2].trim();
|
|
174
|
+
const val = parseTomlValue(rawVal);
|
|
175
|
+
if (currentSection) {
|
|
176
|
+
const parts = currentSection.split(".");
|
|
177
|
+
let obj = result;
|
|
178
|
+
for (const part of parts) {
|
|
179
|
+
obj = obj[part];
|
|
180
|
+
}
|
|
181
|
+
obj[key] = val;
|
|
182
|
+
} else {
|
|
183
|
+
result[key] = val;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
function parseTomlValue(raw) {
|
|
190
|
+
if (raw.startsWith('"') && raw.endsWith('"')) return raw.slice(1, -1);
|
|
191
|
+
if (raw === "true") return true;
|
|
192
|
+
if (raw === "false") return false;
|
|
193
|
+
if (/^\d+$/.test(raw)) return parseInt(raw, 10);
|
|
194
|
+
if (raw.startsWith("[") && raw.endsWith("]")) {
|
|
195
|
+
const inner = raw.slice(1, -1).trim();
|
|
196
|
+
if (!inner) return [];
|
|
197
|
+
return inner.split(",").map((s) => parseTomlValue(s.trim()));
|
|
198
|
+
}
|
|
199
|
+
if (raw.startsWith("{") && raw.endsWith("}")) {
|
|
200
|
+
const inner = raw.slice(1, -1).trim();
|
|
201
|
+
if (!inner) return {};
|
|
202
|
+
const result = {};
|
|
203
|
+
for (const pair of inner.split(",")) {
|
|
204
|
+
const [k, ...v] = pair.split("=");
|
|
205
|
+
if (k && v.length) result[k.trim()] = parseTomlValue(v.join("=").trim());
|
|
206
|
+
}
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
return raw;
|
|
210
|
+
}
|
|
211
|
+
var GIT_EXCLUDE_PATTERNS = [
|
|
212
|
+
".agent-mgr.yml",
|
|
213
|
+
"commands/",
|
|
214
|
+
".claude/commands/",
|
|
215
|
+
".cursor/commands/",
|
|
216
|
+
".codex/",
|
|
217
|
+
".opencode/commands/",
|
|
218
|
+
".mcp.json",
|
|
219
|
+
"opencode.json"
|
|
220
|
+
];
|
|
221
|
+
function excludeHasPattern(content, pattern) {
|
|
222
|
+
return content.split("\n").some((line) => line.trim() === pattern);
|
|
223
|
+
}
|
|
224
|
+
function addGitExclude(projectRoot) {
|
|
225
|
+
const excludePath = join(projectRoot, ".git", "info", "exclude");
|
|
226
|
+
if (!existsSync(join(projectRoot, ".git"))) return;
|
|
227
|
+
const existing = existsSync(excludePath) ? readFileSync(excludePath, "utf-8") : "";
|
|
228
|
+
const missing = GIT_EXCLUDE_PATTERNS.filter((p) => !excludeHasPattern(existing, p));
|
|
229
|
+
if (missing.length === 0) return;
|
|
230
|
+
const addition = "\n# agent-mgr\n" + missing.join("\n") + "\n";
|
|
231
|
+
ensureDir(dirname(excludePath));
|
|
232
|
+
appendFileSync(excludePath, addition);
|
|
233
|
+
}
|
|
234
|
+
function updateGitExclude(projectRoot) {
|
|
235
|
+
const excludePath = join(projectRoot, ".git", "info", "exclude");
|
|
236
|
+
if (!existsSync(excludePath)) return;
|
|
237
|
+
const existing = readFileSync(excludePath, "utf-8");
|
|
238
|
+
if (!existing.includes("# agent-mgr")) return;
|
|
239
|
+
const missing = GIT_EXCLUDE_PATTERNS.filter((p) => !excludeHasPattern(existing, p));
|
|
240
|
+
if (missing.length === 0) return;
|
|
241
|
+
appendFileSync(excludePath, missing.join("\n") + "\n");
|
|
242
|
+
}
|
|
243
|
+
function parseFrontmatter(content) {
|
|
244
|
+
const trimmed = content.trimStart();
|
|
245
|
+
if (!trimmed.startsWith("---")) {
|
|
246
|
+
return { description: "", body: content.trim() };
|
|
247
|
+
}
|
|
248
|
+
const endIndex = trimmed.indexOf("---", 3);
|
|
249
|
+
if (endIndex === -1) {
|
|
250
|
+
return { description: "", body: content.trim() };
|
|
251
|
+
}
|
|
252
|
+
const frontmatter = trimmed.slice(3, endIndex).trim();
|
|
253
|
+
const body = trimmed.slice(endIndex + 3).trim();
|
|
254
|
+
let description = "";
|
|
255
|
+
for (const line of frontmatter.split("\n")) {
|
|
256
|
+
const match = line.match(/^description:\s*(.+)$/);
|
|
257
|
+
if (match) {
|
|
258
|
+
description = match[1].trim();
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return { description, body };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/adapters/claude-code.ts
|
|
266
|
+
var claudeCodeAdapter = {
|
|
267
|
+
name: "Claude Code",
|
|
268
|
+
id: "claude-code",
|
|
269
|
+
supportsCommands: true,
|
|
270
|
+
getCommandsDir(scope, projectRoot) {
|
|
271
|
+
if (scope === "global") return join2(homedir(), ".claude", "commands");
|
|
272
|
+
return join2(projectRoot, ".claude", "commands");
|
|
273
|
+
},
|
|
274
|
+
getMcpConfigPath(scope, projectRoot) {
|
|
275
|
+
if (scope === "global") return join2(homedir(), ".claude.json");
|
|
276
|
+
return join2(projectRoot, ".mcp.json");
|
|
277
|
+
},
|
|
278
|
+
async readMcpConfig(scope, projectRoot) {
|
|
279
|
+
const path = this.getMcpConfigPath(scope, projectRoot);
|
|
280
|
+
const data = readJson(path);
|
|
281
|
+
return data.mcpServers ?? {};
|
|
282
|
+
},
|
|
283
|
+
async writeMcpConfig(servers, scope, projectRoot) {
|
|
284
|
+
const path = this.getMcpConfigPath(scope, projectRoot);
|
|
285
|
+
const existing = readJson(path);
|
|
286
|
+
existing.mcpServers = {
|
|
287
|
+
...existing.mcpServers ?? {},
|
|
288
|
+
...servers
|
|
289
|
+
};
|
|
290
|
+
writeJson(path, existing);
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// src/adapters/cursor.ts
|
|
295
|
+
import { homedir as homedir2 } from "os";
|
|
296
|
+
import { join as join3 } from "path";
|
|
297
|
+
var cursorAdapter = {
|
|
298
|
+
name: "Cursor",
|
|
299
|
+
id: "cursor",
|
|
300
|
+
supportsCommands: true,
|
|
301
|
+
getCommandsDir(scope, projectRoot) {
|
|
302
|
+
if (scope === "global") return join3(homedir2(), ".cursor", "commands");
|
|
303
|
+
return join3(projectRoot, ".cursor", "commands");
|
|
304
|
+
},
|
|
305
|
+
getMcpConfigPath(scope, projectRoot) {
|
|
306
|
+
if (scope === "global") return join3(homedir2(), ".cursor", "mcp.json");
|
|
307
|
+
return join3(projectRoot, ".cursor", "mcp.json");
|
|
308
|
+
},
|
|
309
|
+
async readMcpConfig(scope, projectRoot) {
|
|
310
|
+
const path = this.getMcpConfigPath(scope, projectRoot);
|
|
311
|
+
const data = readJson(path);
|
|
312
|
+
return data.mcpServers ?? {};
|
|
313
|
+
},
|
|
314
|
+
async writeMcpConfig(servers, scope, projectRoot) {
|
|
315
|
+
const path = this.getMcpConfigPath(scope, projectRoot);
|
|
316
|
+
const existing = readJson(path);
|
|
317
|
+
existing.mcpServers = {
|
|
318
|
+
...existing.mcpServers ?? {},
|
|
319
|
+
...servers
|
|
320
|
+
};
|
|
321
|
+
writeJson(path, existing);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// src/adapters/codex.ts
|
|
326
|
+
import { homedir as homedir3 } from "os";
|
|
327
|
+
import { join as join4 } from "path";
|
|
328
|
+
var codexAdapter = {
|
|
329
|
+
name: "Codex",
|
|
330
|
+
id: "codex",
|
|
331
|
+
supportsCommands: false,
|
|
332
|
+
getCommandsDir() {
|
|
333
|
+
return null;
|
|
334
|
+
},
|
|
335
|
+
getMcpConfigPath(scope, projectRoot) {
|
|
336
|
+
if (scope === "global") return join4(homedir3(), ".codex", "config.toml");
|
|
337
|
+
return join4(projectRoot, ".codex", "config.toml");
|
|
338
|
+
},
|
|
339
|
+
async readMcpConfig(scope, projectRoot) {
|
|
340
|
+
const path = this.getMcpConfigPath(scope, projectRoot);
|
|
341
|
+
const data = readToml(path);
|
|
342
|
+
const servers = data.mcp_servers ?? {};
|
|
343
|
+
const result = {};
|
|
344
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
345
|
+
result[name] = {
|
|
346
|
+
name,
|
|
347
|
+
command: cfg.command ?? "",
|
|
348
|
+
args: cfg.args ?? [],
|
|
349
|
+
env: cfg.env ?? {}
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
return result;
|
|
353
|
+
},
|
|
354
|
+
async writeMcpConfig(servers, scope, projectRoot) {
|
|
355
|
+
const path = this.getMcpConfigPath(scope, projectRoot);
|
|
356
|
+
const existing = readToml(path);
|
|
357
|
+
const mcpServers = existing.mcp_servers ?? {};
|
|
358
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
359
|
+
mcpServers[name] = {
|
|
360
|
+
command: server.command,
|
|
361
|
+
args: server.args ?? [],
|
|
362
|
+
env: server.env ?? {}
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
existing.mcp_servers = mcpServers;
|
|
366
|
+
writeToml(path, existing);
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
// src/adapters/opencode.ts
|
|
371
|
+
import { homedir as homedir4 } from "os";
|
|
372
|
+
import { join as join5 } from "path";
|
|
373
|
+
var opencodeAdapter = {
|
|
374
|
+
name: "OpenCode",
|
|
375
|
+
id: "opencode",
|
|
376
|
+
supportsCommands: true,
|
|
377
|
+
getCommandsDir(scope, projectRoot) {
|
|
378
|
+
if (scope === "global") return join5(homedir4(), ".config", "opencode", "commands");
|
|
379
|
+
return join5(projectRoot, ".opencode", "commands");
|
|
380
|
+
},
|
|
381
|
+
getMcpConfigPath(scope, projectRoot) {
|
|
382
|
+
if (scope === "global") return join5(homedir4(), ".config", "opencode", "opencode.json");
|
|
383
|
+
return join5(projectRoot, "opencode.json");
|
|
384
|
+
},
|
|
385
|
+
async readMcpConfig(scope, projectRoot) {
|
|
386
|
+
const path = this.getMcpConfigPath(scope, projectRoot);
|
|
387
|
+
const data = readJson(path);
|
|
388
|
+
const mcp2 = data.mcp ?? {};
|
|
389
|
+
const result = {};
|
|
390
|
+
for (const [name, cfg] of Object.entries(mcp2)) {
|
|
391
|
+
const cmd = cfg.command;
|
|
392
|
+
if (Array.isArray(cmd) && cmd.length > 0) {
|
|
393
|
+
result[name] = {
|
|
394
|
+
name,
|
|
395
|
+
command: cmd[0],
|
|
396
|
+
args: cmd.slice(1),
|
|
397
|
+
env: cfg.environment ?? {}
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return result;
|
|
402
|
+
},
|
|
403
|
+
async writeMcpConfig(servers, scope, projectRoot) {
|
|
404
|
+
const path = this.getMcpConfigPath(scope, projectRoot);
|
|
405
|
+
const existing = readJson(path);
|
|
406
|
+
const mcp2 = existing.mcp ?? {};
|
|
407
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
408
|
+
mcp2[name] = {
|
|
409
|
+
type: "local",
|
|
410
|
+
command: [server.command, ...server.args ?? []],
|
|
411
|
+
environment: server.env ?? {},
|
|
412
|
+
enabled: true
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
existing.mcp = mcp2;
|
|
416
|
+
writeJson(path, existing);
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// src/adapters/index.ts
|
|
421
|
+
var adapters = {
|
|
422
|
+
"claude-code": claudeCodeAdapter,
|
|
423
|
+
cursor: cursorAdapter,
|
|
424
|
+
codex: codexAdapter,
|
|
425
|
+
opencode: opencodeAdapter
|
|
426
|
+
};
|
|
427
|
+
function getAdapters(ids) {
|
|
428
|
+
return ids.map((id) => adapters[id]).filter((a) => a !== void 0);
|
|
429
|
+
}
|
|
430
|
+
var ALL_TARGET_IDS = Object.keys(adapters);
|
|
431
|
+
|
|
432
|
+
// src/lib/config.ts
|
|
433
|
+
import { existsSync as existsSync3 } from "fs";
|
|
434
|
+
|
|
435
|
+
// src/lib/paths.ts
|
|
436
|
+
import { homedir as homedir5 } from "os";
|
|
437
|
+
import { join as join6, resolve } from "path";
|
|
438
|
+
import { existsSync as existsSync2 } from "fs";
|
|
439
|
+
var GLOBAL_DIR = join6(homedir5(), ".agent-mgr");
|
|
440
|
+
var GLOBAL_CONFIG_PATH = join6(GLOBAL_DIR, "config.yml");
|
|
441
|
+
var GLOBAL_COMMANDS_DIR = join6(GLOBAL_DIR, "commands");
|
|
442
|
+
var GLOBAL_MCP_PATH = join6(GLOBAL_DIR, "mcp.json");
|
|
443
|
+
var PROJECT_CONFIG_FILE = ".agent-mgr.yml";
|
|
444
|
+
var PROJECT_COMMANDS_DIR = "commands";
|
|
445
|
+
function findProjectRoot(from = process.cwd()) {
|
|
446
|
+
let dir = resolve(from);
|
|
447
|
+
while (true) {
|
|
448
|
+
if (existsSync2(join6(dir, PROJECT_CONFIG_FILE))) return dir;
|
|
449
|
+
const parent = resolve(dir, "..");
|
|
450
|
+
if (parent === dir) return null;
|
|
451
|
+
dir = parent;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function getCommandsDir(scope, projectRoot) {
|
|
455
|
+
if (scope === "global") return GLOBAL_COMMANDS_DIR;
|
|
456
|
+
return join6(projectRoot ?? process.cwd(), PROJECT_COMMANDS_DIR);
|
|
457
|
+
}
|
|
458
|
+
function getConfigPath(scope, projectRoot) {
|
|
459
|
+
if (scope === "global") return GLOBAL_CONFIG_PATH;
|
|
460
|
+
return join6(projectRoot ?? process.cwd(), PROJECT_CONFIG_FILE);
|
|
461
|
+
}
|
|
462
|
+
var GLOBAL_PROFILES_DIR = join6(GLOBAL_DIR, "profiles");
|
|
463
|
+
var GLOBAL_REPOS_DIR = join6(GLOBAL_DIR, "repos");
|
|
464
|
+
function getProfileDir(profileName) {
|
|
465
|
+
return join6(GLOBAL_PROFILES_DIR, profileName);
|
|
466
|
+
}
|
|
467
|
+
function getProfileCommandsDir(profileName) {
|
|
468
|
+
return join6(GLOBAL_PROFILES_DIR, profileName, "commands");
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// src/lib/config.ts
|
|
472
|
+
function loadConfig(scope, projectRoot) {
|
|
473
|
+
const configPath = getConfigPath(scope, projectRoot);
|
|
474
|
+
if (!existsSync3(configPath)) return { targets: [] };
|
|
475
|
+
const raw = readYaml(configPath);
|
|
476
|
+
return {
|
|
477
|
+
targets: Array.isArray(raw.targets) ? raw.targets : [],
|
|
478
|
+
activeProfile: typeof raw.activeProfile === "string" ? raw.activeProfile : void 0,
|
|
479
|
+
repos: Array.isArray(raw.repos) ? raw.repos : void 0
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
function saveConfig(config, scope, projectRoot) {
|
|
483
|
+
const configPath = getConfigPath(scope, projectRoot);
|
|
484
|
+
const data = { targets: config.targets };
|
|
485
|
+
if (config.activeProfile) data.activeProfile = config.activeProfile;
|
|
486
|
+
if (config.repos && config.repos.length > 0) data.repos = config.repos;
|
|
487
|
+
writeYaml(configPath, data);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// src/commands/init.ts
|
|
491
|
+
async function initCommand(options) {
|
|
492
|
+
const scope = options.global ? "global" : "project";
|
|
493
|
+
const cwd = process.cwd();
|
|
494
|
+
if (scope === "project" && existsSync4(join7(cwd, PROJECT_CONFIG_FILE))) {
|
|
495
|
+
console.log(chalk.yellow("Already initialized in this directory."));
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (scope === "global" && existsSync4(join7(GLOBAL_DIR, "config.yml"))) {
|
|
499
|
+
console.log(chalk.yellow("Global config already exists at ~/.agent-mgr/"));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
let targets;
|
|
503
|
+
if (options.all) {
|
|
504
|
+
targets = ALL_TARGET_IDS;
|
|
505
|
+
} else if (options.targets) {
|
|
506
|
+
targets = options.targets.split(",").map((t) => t.trim());
|
|
507
|
+
const invalid = targets.filter((t) => !ALL_TARGET_IDS.includes(t));
|
|
508
|
+
if (invalid.length > 0) {
|
|
509
|
+
console.log(chalk.red(`Unknown targets: ${invalid.join(", ")}`));
|
|
510
|
+
console.log(chalk.dim(`Available: ${ALL_TARGET_IDS.join(", ")}`));
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
} else {
|
|
514
|
+
targets = await checkbox({
|
|
515
|
+
message: "What platforms do you want to sync?",
|
|
516
|
+
choices: [
|
|
517
|
+
{ name: "Claude Code", value: "claude-code" },
|
|
518
|
+
{ name: "Cursor", value: "cursor" },
|
|
519
|
+
{ name: "Codex", value: "codex" },
|
|
520
|
+
{ name: "OpenCode", value: "opencode" }
|
|
521
|
+
]
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
if (targets.length === 0) {
|
|
525
|
+
console.log(chalk.red("No targets selected. Aborting."));
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (scope === "global") {
|
|
529
|
+
ensureDir(GLOBAL_DIR);
|
|
530
|
+
ensureDir(GLOBAL_COMMANDS_DIR);
|
|
531
|
+
saveConfig({ targets }, "global");
|
|
532
|
+
console.log(chalk.green("\u2713 Created ~/.agent-mgr/config.yml"));
|
|
533
|
+
console.log(chalk.green("\u2713 Created ~/.agent-mgr/commands/"));
|
|
534
|
+
} else {
|
|
535
|
+
const commandsDir = join7(cwd, PROJECT_COMMANDS_DIR);
|
|
536
|
+
ensureDir(commandsDir);
|
|
537
|
+
saveConfig({ targets }, "project", cwd);
|
|
538
|
+
console.log(chalk.green(`\u2713 Created ${PROJECT_CONFIG_FILE}`));
|
|
539
|
+
console.log(chalk.green(`\u2713 Created ${PROJECT_COMMANDS_DIR}/`));
|
|
540
|
+
if (existsSync4(join7(cwd, ".git"))) {
|
|
541
|
+
const shouldGitignore = options.gitignore !== void 0 ? options.gitignore : await confirm({
|
|
542
|
+
message: "Gitignore agent-mgr config and generated files?",
|
|
543
|
+
default: true
|
|
544
|
+
});
|
|
545
|
+
if (shouldGitignore) {
|
|
546
|
+
addGitExclude(cwd);
|
|
547
|
+
console.log(chalk.green("\u2713 Added agent-mgr files to .git/info/exclude"));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
console.log(chalk.dim("\nAdd commands with: amgr add <name>"));
|
|
552
|
+
console.log(chalk.dim("Sync with: amgr sync"));
|
|
553
|
+
console.log("");
|
|
554
|
+
console.log(chalk.bold("Tip:") + " This tool works best when your AI agent helps you configure it.");
|
|
555
|
+
console.log(chalk.dim("Ask your agent to run `amgr help-agent` to learn what it can do, or run:"));
|
|
556
|
+
console.log(chalk.cyan(" amgr help ") + chalk.dim("\u2014 see all commands"));
|
|
557
|
+
console.log(chalk.cyan(" amgr help-agent ") + chalk.dim("\u2014 give your AI agent the full reference"));
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// src/commands/add.ts
|
|
561
|
+
import { join as join8 } from "path";
|
|
562
|
+
import { existsSync as existsSync5, writeFileSync as writeFileSync2, readFileSync as readFileSync2 } from "fs";
|
|
563
|
+
import chalk2 from "chalk";
|
|
564
|
+
function addCommand(name, options) {
|
|
565
|
+
const scope = options.global ? "global" : "project";
|
|
566
|
+
let commandsDir;
|
|
567
|
+
if (scope === "global") {
|
|
568
|
+
commandsDir = GLOBAL_COMMANDS_DIR;
|
|
569
|
+
} else {
|
|
570
|
+
const root = findProjectRoot();
|
|
571
|
+
if (!root) {
|
|
572
|
+
console.log(chalk2.red("Not in an agent-mgr project. Run `agent-mgr init` first."));
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
commandsDir = getCommandsDir("project", root);
|
|
576
|
+
}
|
|
577
|
+
const cleanName = name.replace(/\.md$/, "");
|
|
578
|
+
const filePath = join8(commandsDir, `${cleanName}.md`);
|
|
579
|
+
if (existsSync5(filePath)) {
|
|
580
|
+
console.log(chalk2.yellow(`Command "${cleanName}" already exists at ${filePath}`));
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
ensureDir(commandsDir);
|
|
584
|
+
let fileContent;
|
|
585
|
+
if (options.from) {
|
|
586
|
+
const sourcePath = options.from;
|
|
587
|
+
if (!existsSync5(sourcePath)) {
|
|
588
|
+
console.log(chalk2.red(`File not found: ${sourcePath}`));
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
const sourceContent = readFileSync2(sourcePath, "utf-8");
|
|
592
|
+
if (sourceContent.trimStart().startsWith("---")) {
|
|
593
|
+
fileContent = sourceContent;
|
|
594
|
+
} else {
|
|
595
|
+
fileContent = `---
|
|
596
|
+
description: ${cleanName} command
|
|
597
|
+
---
|
|
598
|
+
|
|
599
|
+
${sourceContent}`;
|
|
600
|
+
}
|
|
601
|
+
} else if (options.content) {
|
|
602
|
+
fileContent = `---
|
|
603
|
+
description: ${cleanName} command
|
|
604
|
+
---
|
|
605
|
+
|
|
606
|
+
${options.content}
|
|
607
|
+
|
|
608
|
+
$ARGUMENTS
|
|
609
|
+
`;
|
|
610
|
+
} else {
|
|
611
|
+
fileContent = `---
|
|
612
|
+
description: ${cleanName} command
|
|
613
|
+
---
|
|
614
|
+
|
|
615
|
+
$ARGUMENTS
|
|
616
|
+
`;
|
|
617
|
+
}
|
|
618
|
+
writeFileSync2(filePath, fileContent);
|
|
619
|
+
console.log(chalk2.green(`\u2713 Created ${filePath}`));
|
|
620
|
+
console.log(chalk2.dim("Sync with: amgr sync"));
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// src/commands/remove.ts
|
|
624
|
+
import { join as join9 } from "path";
|
|
625
|
+
import chalk3 from "chalk";
|
|
626
|
+
async function removeCommand(name, options) {
|
|
627
|
+
const scope = options.global ? "global" : "project";
|
|
628
|
+
const cleanName = name.replace(/\.md$/, "");
|
|
629
|
+
let commandsDir;
|
|
630
|
+
let projectRoot;
|
|
631
|
+
if (scope === "global") {
|
|
632
|
+
commandsDir = GLOBAL_COMMANDS_DIR;
|
|
633
|
+
projectRoot = "";
|
|
634
|
+
} else {
|
|
635
|
+
const root = findProjectRoot();
|
|
636
|
+
if (!root) {
|
|
637
|
+
console.log(chalk3.red("Not in an agent-mgr project. Run `agent-mgr init` first."));
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
projectRoot = root;
|
|
641
|
+
commandsDir = getCommandsDir("project", root);
|
|
642
|
+
}
|
|
643
|
+
const sourcePath = join9(commandsDir, `${cleanName}.md`);
|
|
644
|
+
const removed = removeFile(sourcePath);
|
|
645
|
+
if (!removed) {
|
|
646
|
+
console.log(chalk3.red(`Command "${cleanName}" not found.`));
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
console.log(chalk3.green(`\u2713 Removed ${sourcePath}`));
|
|
650
|
+
const config = loadConfig(scope === "global" ? "global" : "project", projectRoot || void 0);
|
|
651
|
+
const targetAdapters = getAdapters(config.targets);
|
|
652
|
+
for (const adapter of targetAdapters) {
|
|
653
|
+
if (!adapter.supportsCommands) continue;
|
|
654
|
+
const dir = adapter.getCommandsDir(scope, projectRoot);
|
|
655
|
+
if (dir) {
|
|
656
|
+
const targetPath = join9(dir, `${cleanName}.md`);
|
|
657
|
+
if (removeFile(targetPath)) {
|
|
658
|
+
console.log(chalk3.green(`\u2713 Removed from ${adapter.name}`));
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if (adapter.removeCommand) {
|
|
662
|
+
const didRemove = await adapter.removeCommand(cleanName, scope, projectRoot);
|
|
663
|
+
if (didRemove) {
|
|
664
|
+
console.log(chalk3.green(`\u2713 Removed from ${adapter.name} config`));
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// src/commands/sync.ts
|
|
671
|
+
import { join as join10 } from "path";
|
|
672
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
673
|
+
import chalk4 from "chalk";
|
|
674
|
+
function collectCommands(projectRoot) {
|
|
675
|
+
const seen = /* @__PURE__ */ new Map();
|
|
676
|
+
const globalConfig = loadConfig("global");
|
|
677
|
+
const globalFiles = listMarkdownFiles(GLOBAL_COMMANDS_DIR);
|
|
678
|
+
for (const file of globalFiles) {
|
|
679
|
+
seen.set(file, { file, sourcePath: join10(GLOBAL_COMMANDS_DIR, file), scope: "global" });
|
|
680
|
+
}
|
|
681
|
+
if (globalConfig.activeProfile) {
|
|
682
|
+
const profileDir = getProfileCommandsDir(globalConfig.activeProfile);
|
|
683
|
+
const profileFiles = listMarkdownFiles(profileDir);
|
|
684
|
+
for (const file of profileFiles) {
|
|
685
|
+
seen.set(file, { file, sourcePath: join10(profileDir, file), scope: "profile" });
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (projectRoot) {
|
|
689
|
+
const projectDir = getCommandsDir("project", projectRoot);
|
|
690
|
+
const projectFiles = listMarkdownFiles(projectDir);
|
|
691
|
+
for (const file of projectFiles) {
|
|
692
|
+
seen.set(file, { file, sourcePath: join10(projectDir, file), scope: "project" });
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return Array.from(seen.values());
|
|
696
|
+
}
|
|
697
|
+
async function syncCommand(options) {
|
|
698
|
+
if (options.global) {
|
|
699
|
+
syncSingleScope("global", "", GLOBAL_COMMANDS_DIR);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
const root = findProjectRoot();
|
|
703
|
+
if (!root) {
|
|
704
|
+
console.log(chalk4.red("Not in an agent-mgr project. Run `agent-mgr init` first."));
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const config = loadConfig("project", root);
|
|
708
|
+
if (config.targets.length === 0) {
|
|
709
|
+
console.log(chalk4.red("No targets configured. Run `agent-mgr init` first."));
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
const commands = collectCommands(root);
|
|
713
|
+
if (commands.length === 0) {
|
|
714
|
+
console.log(chalk4.yellow("No commands found. Add some with: amgr add <name>"));
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const targetAdapters = getAdapters(config.targets);
|
|
718
|
+
const results = [];
|
|
719
|
+
for (const cmd of commands) {
|
|
720
|
+
for (const adapter of targetAdapters) {
|
|
721
|
+
if (!adapter.supportsCommands) {
|
|
722
|
+
results.push({ target: adapter.name, command: cmd.file, status: "skipped", reason: "no command support", scope: cmd.scope });
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
try {
|
|
726
|
+
if (adapter.syncCommand) {
|
|
727
|
+
const raw = readFileSync3(cmd.sourcePath, "utf-8");
|
|
728
|
+
const { description, body } = parseFrontmatter(raw);
|
|
729
|
+
const cmdName = cmd.file.replace(/\.md$/, "");
|
|
730
|
+
await adapter.syncCommand(cmdName, description, body, "project", root);
|
|
731
|
+
results.push({ target: adapter.name, command: cmd.file, status: "synced", scope: cmd.scope });
|
|
732
|
+
} else {
|
|
733
|
+
const targetDir = adapter.getCommandsDir("project", root);
|
|
734
|
+
if (!targetDir) {
|
|
735
|
+
results.push({ target: adapter.name, command: cmd.file, status: "skipped", reason: "no directory for scope", scope: cmd.scope });
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
const destPath = join10(targetDir, cmd.file);
|
|
739
|
+
syncFile(cmd.sourcePath, destPath);
|
|
740
|
+
results.push({ target: adapter.name, command: cmd.file, status: "synced", scope: cmd.scope });
|
|
741
|
+
}
|
|
742
|
+
} catch (err) {
|
|
743
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
744
|
+
results.push({ target: adapter.name, command: cmd.file, status: "failed", reason: msg, scope: cmd.scope });
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
let synced = 0;
|
|
749
|
+
let skipped = 0;
|
|
750
|
+
let failed = 0;
|
|
751
|
+
for (const r of results) {
|
|
752
|
+
if (r.status === "synced") {
|
|
753
|
+
const scopeTag = chalk4.dim(`[${r.scope}]`);
|
|
754
|
+
console.log(chalk4.green(`\u2713 ${r.command} \u2192 ${r.target} ${scopeTag}`));
|
|
755
|
+
synced++;
|
|
756
|
+
} else if (r.status === "skipped") {
|
|
757
|
+
skipped++;
|
|
758
|
+
} else {
|
|
759
|
+
console.log(chalk4.red(`\u2717 ${r.command} \u2192 ${r.target}: ${r.reason}`));
|
|
760
|
+
failed++;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
const sourceFileNames = new Set(commands.map((c) => c.file));
|
|
764
|
+
for (const adapter of targetAdapters) {
|
|
765
|
+
if (!adapter.supportsCommands) continue;
|
|
766
|
+
const targetDir = adapter.getCommandsDir("project", root);
|
|
767
|
+
if (!targetDir) continue;
|
|
768
|
+
const existingFiles = listMarkdownFiles(targetDir);
|
|
769
|
+
for (const existing of existingFiles) {
|
|
770
|
+
if (!sourceFileNames.has(existing)) {
|
|
771
|
+
const stalePath = join10(targetDir, existing);
|
|
772
|
+
if (removeFile(stalePath)) {
|
|
773
|
+
console.log(chalk4.dim(` Pruned ${existing} from ${adapter.name}`));
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
updateGitExclude(root);
|
|
779
|
+
console.log("");
|
|
780
|
+
console.log(`Synced ${chalk4.green(String(synced))} command(s) to ${targetAdapters.filter((a) => a.supportsCommands).length} target(s)`);
|
|
781
|
+
if (skipped > 0) console.log(chalk4.dim(`Skipped ${skipped} (no command support)`));
|
|
782
|
+
if (failed > 0) console.log(chalk4.red(`Failed: ${failed}`));
|
|
783
|
+
}
|
|
784
|
+
async function syncSingleScope(scope, projectRoot, commandsDir) {
|
|
785
|
+
const config = loadConfig(scope, projectRoot || void 0);
|
|
786
|
+
if (config.targets.length === 0) {
|
|
787
|
+
console.log(chalk4.red("No targets configured."));
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
const files = listMarkdownFiles(commandsDir);
|
|
791
|
+
if (files.length === 0) {
|
|
792
|
+
console.log(chalk4.yellow("No commands found."));
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
const targetAdapters = getAdapters(config.targets);
|
|
796
|
+
let synced = 0;
|
|
797
|
+
for (const file of files) {
|
|
798
|
+
const sourcePath = join10(commandsDir, file);
|
|
799
|
+
for (const adapter of targetAdapters) {
|
|
800
|
+
if (!adapter.supportsCommands) continue;
|
|
801
|
+
try {
|
|
802
|
+
if (adapter.syncCommand) {
|
|
803
|
+
const raw = readFileSync3(sourcePath, "utf-8");
|
|
804
|
+
const { description, body } = parseFrontmatter(raw);
|
|
805
|
+
const cmdName = file.replace(/\.md$/, "");
|
|
806
|
+
await adapter.syncCommand(cmdName, description, body, scope, projectRoot);
|
|
807
|
+
} else {
|
|
808
|
+
const targetDir = adapter.getCommandsDir(scope, projectRoot);
|
|
809
|
+
if (!targetDir) continue;
|
|
810
|
+
syncFile(sourcePath, join10(targetDir, file));
|
|
811
|
+
}
|
|
812
|
+
console.log(chalk4.green(`\u2713 ${file} \u2192 ${adapter.name}`));
|
|
813
|
+
synced++;
|
|
814
|
+
} catch (err) {
|
|
815
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
816
|
+
console.log(chalk4.red(`\u2717 ${file} \u2192 ${adapter.name}: ${msg}`));
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
console.log(`
|
|
821
|
+
Synced ${chalk4.green(String(synced))} command(s)`);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// src/commands/list.ts
|
|
825
|
+
import { join as join11 } from "path";
|
|
826
|
+
import { existsSync as existsSync6, lstatSync, readlinkSync } from "fs";
|
|
827
|
+
import { resolve as resolve2, dirname as dirname2 } from "path";
|
|
828
|
+
import chalk5 from "chalk";
|
|
829
|
+
function collectAllCommands(projectRoot) {
|
|
830
|
+
const seen = /* @__PURE__ */ new Map();
|
|
831
|
+
const globalFiles = listMarkdownFiles(GLOBAL_COMMANDS_DIR);
|
|
832
|
+
for (const file of globalFiles) {
|
|
833
|
+
seen.set(file, { file, sourcePath: join11(GLOBAL_COMMANDS_DIR, file), scope: "global" });
|
|
834
|
+
}
|
|
835
|
+
const globalConfig = loadConfig("global");
|
|
836
|
+
if (globalConfig.activeProfile) {
|
|
837
|
+
const profileDir = getProfileCommandsDir(globalConfig.activeProfile);
|
|
838
|
+
const profileFiles = listMarkdownFiles(profileDir);
|
|
839
|
+
for (const file of profileFiles) {
|
|
840
|
+
seen.set(file, { file, sourcePath: join11(profileDir, file), scope: "profile" });
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
if (projectRoot) {
|
|
844
|
+
const projectDir = getCommandsDir("project", projectRoot);
|
|
845
|
+
const projectFiles = listMarkdownFiles(projectDir);
|
|
846
|
+
for (const file of projectFiles) {
|
|
847
|
+
seen.set(file, { file, sourcePath: join11(projectDir, file), scope: "project" });
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return Array.from(seen.values());
|
|
851
|
+
}
|
|
852
|
+
function listCommand(options) {
|
|
853
|
+
let projectRoot = null;
|
|
854
|
+
if (!options.global) {
|
|
855
|
+
projectRoot = findProjectRoot();
|
|
856
|
+
if (!projectRoot) {
|
|
857
|
+
console.log(chalk5.red("Not in an agent-mgr project. Run `agent-mgr init` first."));
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
const config = options.global ? loadConfig("global") : loadConfig("project", projectRoot);
|
|
862
|
+
const targetAdapters = getAdapters(config.targets).filter((a) => a.supportsCommands);
|
|
863
|
+
const commands = options.global ? listMarkdownFiles(GLOBAL_COMMANDS_DIR).map((f) => ({ file: f, sourcePath: join11(GLOBAL_COMMANDS_DIR, f), scope: "global" })) : collectAllCommands(projectRoot);
|
|
864
|
+
if (commands.length === 0) {
|
|
865
|
+
console.log(chalk5.yellow("No commands found."));
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
console.log(chalk5.bold("\nCommands:\n"));
|
|
869
|
+
for (const cmd of commands) {
|
|
870
|
+
const statuses = [];
|
|
871
|
+
for (const adapter of targetAdapters) {
|
|
872
|
+
const dir = adapter.getCommandsDir(options.global ? "global" : "project", projectRoot ?? "");
|
|
873
|
+
if (!dir) {
|
|
874
|
+
statuses.push(chalk5.dim(`${adapter.id} \u2014`));
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
const targetPath = join11(dir, cmd.file);
|
|
878
|
+
if (existsSync6(targetPath)) {
|
|
879
|
+
const isSymlink = lstatSync(targetPath).isSymbolicLink();
|
|
880
|
+
if (isSymlink) {
|
|
881
|
+
const linkTarget = resolve2(dirname2(targetPath), readlinkSync(targetPath));
|
|
882
|
+
const inSync = linkTarget === resolve2(cmd.sourcePath);
|
|
883
|
+
statuses.push(inSync ? chalk5.green(`${adapter.id} \u2713`) : chalk5.yellow(`${adapter.id} \u26A0`));
|
|
884
|
+
} else {
|
|
885
|
+
statuses.push(chalk5.yellow(`${adapter.id} \u2713 (copy)`));
|
|
886
|
+
}
|
|
887
|
+
} else {
|
|
888
|
+
statuses.push(chalk5.red(`${adapter.id} \u2717`));
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
const scopeTag = chalk5.dim(`[${cmd.scope}]`);
|
|
892
|
+
console.log(` ${cmd.file.padEnd(25)} ${scopeTag.padEnd(20)} ${statuses.join(" ")}`);
|
|
893
|
+
}
|
|
894
|
+
console.log("");
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// src/commands/mcp-add.ts
|
|
898
|
+
import chalk6 from "chalk";
|
|
899
|
+
import { input, checkbox as checkbox2 } from "@inquirer/prompts";
|
|
900
|
+
async function mcpAddCommand(options) {
|
|
901
|
+
const scope = options.global ? "global" : "project";
|
|
902
|
+
let projectRoot;
|
|
903
|
+
if (scope === "project") {
|
|
904
|
+
const root = findProjectRoot();
|
|
905
|
+
if (!root) {
|
|
906
|
+
console.log(chalk6.red("Not in an agent-mgr project. Run `agent-mgr init` first."));
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
projectRoot = root;
|
|
910
|
+
} else {
|
|
911
|
+
projectRoot = "";
|
|
912
|
+
}
|
|
913
|
+
const config = loadConfig(scope, projectRoot || void 0);
|
|
914
|
+
const name = await input({ message: "MCP server name:" });
|
|
915
|
+
const command = await input({ message: "Command:" });
|
|
916
|
+
const argsRaw = await input({ message: "Arguments (space-separated, or empty):" });
|
|
917
|
+
const envRaw = await input({ message: "Environment variables (KEY=VAL KEY=VAL, or empty):" });
|
|
918
|
+
const args = argsRaw.trim() ? argsRaw.trim().split(/\s+/) : void 0;
|
|
919
|
+
const env = envRaw.trim() ? Object.fromEntries(envRaw.trim().split(/\s+/).map((pair) => {
|
|
920
|
+
const [k, ...v] = pair.split("=");
|
|
921
|
+
return [k, v.join("=")];
|
|
922
|
+
})) : void 0;
|
|
923
|
+
const availableTargets = config.targets.length > 0 ? config.targets : ALL_TARGET_IDS;
|
|
924
|
+
const targets = await checkbox2({
|
|
925
|
+
message: "Which tools?",
|
|
926
|
+
choices: availableTargets.map((id) => ({ name: id, value: id }))
|
|
927
|
+
});
|
|
928
|
+
const targetAdapters = getAdapters(targets);
|
|
929
|
+
for (const adapter of targetAdapters) {
|
|
930
|
+
try {
|
|
931
|
+
await adapter.writeMcpConfig(
|
|
932
|
+
{ [name]: { name, command, args: args ?? [], env: env ?? {} } },
|
|
933
|
+
scope,
|
|
934
|
+
projectRoot
|
|
935
|
+
);
|
|
936
|
+
console.log(chalk6.green(`\u2713 ${name} \u2192 ${adapter.name}`));
|
|
937
|
+
} catch (err) {
|
|
938
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
939
|
+
console.log(chalk6.red(`\u2717 ${adapter.name}: ${msg}`));
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// src/commands/mcp-remove.ts
|
|
945
|
+
import chalk7 from "chalk";
|
|
946
|
+
async function mcpRemoveCommand(name, options) {
|
|
947
|
+
const scope = options.global ? "global" : "project";
|
|
948
|
+
let projectRoot;
|
|
949
|
+
if (scope === "project") {
|
|
950
|
+
const root = findProjectRoot();
|
|
951
|
+
if (!root) {
|
|
952
|
+
console.log(chalk7.red("Not in an agent-mgr project. Run `agent-mgr init` first."));
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
projectRoot = root;
|
|
956
|
+
} else {
|
|
957
|
+
projectRoot = "";
|
|
958
|
+
}
|
|
959
|
+
const config = loadConfig(scope, projectRoot || void 0);
|
|
960
|
+
const targetAdapters = getAdapters(config.targets);
|
|
961
|
+
for (const adapter of targetAdapters) {
|
|
962
|
+
try {
|
|
963
|
+
const mcpPath = adapter.getMcpConfigPath(scope, projectRoot);
|
|
964
|
+
const data = readJson(mcpPath);
|
|
965
|
+
if (data.mcpServers && name in data.mcpServers) {
|
|
966
|
+
delete data.mcpServers[name];
|
|
967
|
+
writeJson(mcpPath, data);
|
|
968
|
+
console.log(chalk7.green(`\u2713 Removed ${name} from ${adapter.name}`));
|
|
969
|
+
} else {
|
|
970
|
+
console.log(chalk7.dim(` ${adapter.name}: ${name} not found`));
|
|
971
|
+
}
|
|
972
|
+
} catch (err) {
|
|
973
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
974
|
+
console.log(chalk7.red(`\u2717 ${adapter.name}: ${msg}`));
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// src/commands/mcp-list.ts
|
|
980
|
+
import chalk8 from "chalk";
|
|
981
|
+
async function mcpListCommand(options) {
|
|
982
|
+
const scope = options.global ? "global" : "project";
|
|
983
|
+
let projectRoot;
|
|
984
|
+
if (scope === "project") {
|
|
985
|
+
const root = findProjectRoot();
|
|
986
|
+
if (!root) {
|
|
987
|
+
console.log(chalk8.red("Not in an agent-mgr project. Run `agent-mgr init` first."));
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
projectRoot = root;
|
|
991
|
+
} else {
|
|
992
|
+
projectRoot = "";
|
|
993
|
+
}
|
|
994
|
+
const config = loadConfig(scope, projectRoot || void 0);
|
|
995
|
+
const targetAdapters = getAdapters(config.targets);
|
|
996
|
+
const serversByTarget = /* @__PURE__ */ new Map();
|
|
997
|
+
const allServers = /* @__PURE__ */ new Set();
|
|
998
|
+
for (const adapter of targetAdapters) {
|
|
999
|
+
try {
|
|
1000
|
+
const servers = await adapter.readMcpConfig(scope, projectRoot);
|
|
1001
|
+
const names = new Set(Object.keys(servers));
|
|
1002
|
+
serversByTarget.set(adapter.id, names);
|
|
1003
|
+
for (const n of names) allServers.add(n);
|
|
1004
|
+
} catch {
|
|
1005
|
+
serversByTarget.set(adapter.id, /* @__PURE__ */ new Set());
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
if (allServers.size === 0) {
|
|
1009
|
+
console.log(chalk8.yellow("No MCP servers configured."));
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
console.log(chalk8.bold("\nMCP Servers:\n"));
|
|
1013
|
+
for (const server of allServers) {
|
|
1014
|
+
const statuses = [];
|
|
1015
|
+
for (const adapter of targetAdapters) {
|
|
1016
|
+
const has = serversByTarget.get(adapter.id)?.has(server);
|
|
1017
|
+
statuses.push(has ? chalk8.green(`${adapter.id} \u2713`) : chalk8.red(`${adapter.id} \u2717`));
|
|
1018
|
+
}
|
|
1019
|
+
console.log(` ${server.padEnd(25)} ${statuses.join(" ")}`);
|
|
1020
|
+
}
|
|
1021
|
+
console.log("");
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// src/commands/hook.ts
|
|
1025
|
+
import { join as join12 } from "path";
|
|
1026
|
+
import { existsSync as existsSync7, writeFileSync as writeFileSync3, readFileSync as readFileSync4, chmodSync } from "fs";
|
|
1027
|
+
import chalk9 from "chalk";
|
|
1028
|
+
var HOOK_MARKER = "# agent-mgr auto-sync";
|
|
1029
|
+
var HOOK_CONTENT = `${HOOK_MARKER}
|
|
1030
|
+
npx agent-mgr sync 2>/dev/null || true
|
|
1031
|
+
`;
|
|
1032
|
+
function hookInstallCommand() {
|
|
1033
|
+
const gitDir = join12(process.cwd(), ".git");
|
|
1034
|
+
if (!existsSync7(gitDir)) {
|
|
1035
|
+
console.log(chalk9.red("Not a git repository."));
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
const hooksDir = join12(gitDir, "hooks");
|
|
1039
|
+
ensureDir(hooksDir);
|
|
1040
|
+
const hookNames = ["post-checkout", "post-merge"];
|
|
1041
|
+
for (const hookName of hookNames) {
|
|
1042
|
+
const hookPath = join12(hooksDir, hookName);
|
|
1043
|
+
if (existsSync7(hookPath)) {
|
|
1044
|
+
const content = readFileSync4(hookPath, "utf-8");
|
|
1045
|
+
if (content.includes(HOOK_MARKER)) {
|
|
1046
|
+
console.log(chalk9.dim(` ${hookName}: already installed`));
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
writeFileSync3(hookPath, content.trimEnd() + "\n\n" + HOOK_CONTENT);
|
|
1050
|
+
} else {
|
|
1051
|
+
writeFileSync3(hookPath, "#!/bin/sh\n\n" + HOOK_CONTENT);
|
|
1052
|
+
}
|
|
1053
|
+
chmodSync(hookPath, 493);
|
|
1054
|
+
console.log(chalk9.green(`\u2713 Installed ${hookName} hook`));
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
function hookRemoveCommand() {
|
|
1058
|
+
const gitDir = join12(process.cwd(), ".git");
|
|
1059
|
+
if (!existsSync7(gitDir)) {
|
|
1060
|
+
console.log(chalk9.red("Not a git repository."));
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
const hookNames = ["post-checkout", "post-merge"];
|
|
1064
|
+
for (const hookName of hookNames) {
|
|
1065
|
+
const hookPath = join12(join12(gitDir, "hooks"), hookName);
|
|
1066
|
+
if (!existsSync7(hookPath)) continue;
|
|
1067
|
+
const content = readFileSync4(hookPath, "utf-8");
|
|
1068
|
+
if (!content.includes(HOOK_MARKER)) continue;
|
|
1069
|
+
const lines = content.split("\n");
|
|
1070
|
+
const filtered = lines.filter(
|
|
1071
|
+
(line) => !line.includes(HOOK_MARKER) && !line.includes("npx agent-mgr sync")
|
|
1072
|
+
);
|
|
1073
|
+
const cleaned = filtered.join("\n").trim();
|
|
1074
|
+
if (cleaned === "#!/bin/sh" || cleaned === "") {
|
|
1075
|
+
writeFileSync3(hookPath, "#!/bin/sh\n");
|
|
1076
|
+
} else {
|
|
1077
|
+
writeFileSync3(hookPath, cleaned + "\n");
|
|
1078
|
+
}
|
|
1079
|
+
console.log(chalk9.green(`\u2713 Removed ${hookName} hook`));
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// src/commands/help-agent.ts
|
|
1084
|
+
import chalk10 from "chalk";
|
|
1085
|
+
var AGENT_PROMPT = `# agent-mgr \u2014 CLI Tool Reference
|
|
1086
|
+
|
|
1087
|
+
You have access to \`agent-mgr\` (alias: \`amgr\`), a CLI tool that manages AI agent commands, prompts, and MCP server configs across multiple platforms (Claude Code, Cursor, Codex, OpenCode).
|
|
1088
|
+
|
|
1089
|
+
## How It Works
|
|
1090
|
+
|
|
1091
|
+
Commands are markdown files stored in source directories. When synced, they get symlinked to each platform's expected directory. Commands are merged from three scopes with priority: project > profile > global.
|
|
1092
|
+
|
|
1093
|
+
## Available Commands
|
|
1094
|
+
|
|
1095
|
+
### Setup
|
|
1096
|
+
- \`amgr init\` \u2014 Initialize in current repo (interactive)
|
|
1097
|
+
- \`amgr init --all --gitignore\` \u2014 Init with all platforms, gitignore generated dirs
|
|
1098
|
+
- \`amgr init --targets claude-code,cursor\` \u2014 Init with specific platforms
|
|
1099
|
+
- \`amgr init --global\` \u2014 Initialize global config at ~/.agent-mgr/
|
|
1100
|
+
|
|
1101
|
+
### Managing Commands
|
|
1102
|
+
- \`amgr add <name>\` \u2014 Create a new command template
|
|
1103
|
+
- \`amgr add <name> --from <path>\` \u2014 Import command from an existing .md file
|
|
1104
|
+
- \`amgr add <name> --content "prompt text"\` \u2014 Create command with inline content
|
|
1105
|
+
- \`amgr add <name> --global\` \u2014 Add to global commands
|
|
1106
|
+
- \`amgr remove <name>\` \u2014 Remove command from source and all synced targets
|
|
1107
|
+
- \`amgr list\` \u2014 Show all commands with scope and sync status per platform
|
|
1108
|
+
|
|
1109
|
+
### Syncing
|
|
1110
|
+
- \`amgr sync\` \u2014 Merge and distribute project + profile + global commands
|
|
1111
|
+
- \`amgr sync --global\` \u2014 Sync global commands only
|
|
1112
|
+
|
|
1113
|
+
### Profiles
|
|
1114
|
+
- \`amgr profile create <name>\` \u2014 Create a named command set
|
|
1115
|
+
- \`amgr profile switch <name>\` \u2014 Activate a profile
|
|
1116
|
+
- \`amgr profile list\` \u2014 Show all profiles and which is active
|
|
1117
|
+
- \`amgr profile delete <name>\` \u2014 Delete a profile
|
|
1118
|
+
|
|
1119
|
+
### Import from GitHub
|
|
1120
|
+
- \`amgr sync-repo add <url>\` \u2014 Clone a repo and import its commands/ directory
|
|
1121
|
+
- \`amgr sync-repo add <url> --profile <name>\` \u2014 Import into a specific profile
|
|
1122
|
+
- \`amgr sync-repo update\` \u2014 Pull latest from all tracked repos
|
|
1123
|
+
- \`amgr sync-repo list\` \u2014 Show tracked repos
|
|
1124
|
+
- \`amgr sync-repo remove <url>\` \u2014 Stop tracking a repo
|
|
1125
|
+
|
|
1126
|
+
### MCP Server Management
|
|
1127
|
+
- \`amgr mcp add\` \u2014 Interactive: add an MCP server config to selected platforms
|
|
1128
|
+
- \`amgr mcp remove <name>\` \u2014 Remove MCP server from all platforms
|
|
1129
|
+
- \`amgr mcp list\` \u2014 Show MCP servers and which platforms have them
|
|
1130
|
+
|
|
1131
|
+
### Git Hooks
|
|
1132
|
+
- \`amgr hook install\` \u2014 Install post-checkout/post-merge hooks for auto-sync
|
|
1133
|
+
- \`amgr hook remove\` \u2014 Remove the git hooks
|
|
1134
|
+
|
|
1135
|
+
### Help
|
|
1136
|
+
- \`amgr help\` \u2014 Detailed help with workflow and examples
|
|
1137
|
+
- \`amgr help-agent\` \u2014 Output this reference (for AI agents)
|
|
1138
|
+
|
|
1139
|
+
## Command File Format
|
|
1140
|
+
|
|
1141
|
+
Commands are markdown files with optional YAML frontmatter:
|
|
1142
|
+
|
|
1143
|
+
\`\`\`markdown
|
|
1144
|
+
---
|
|
1145
|
+
description: What this command does
|
|
1146
|
+
---
|
|
1147
|
+
|
|
1148
|
+
Your prompt content here.
|
|
1149
|
+
|
|
1150
|
+
$ARGUMENTS
|
|
1151
|
+
\`\`\`
|
|
1152
|
+
|
|
1153
|
+
\`$ARGUMENTS\` gets replaced with whatever the user types after the slash command.
|
|
1154
|
+
|
|
1155
|
+
## Command Priority (when syncing)
|
|
1156
|
+
|
|
1157
|
+
1. Project commands (\`commands/\`) \u2014 highest priority
|
|
1158
|
+
2. Active profile commands (\`~/.agent-mgr/profiles/<name>/commands/\`)
|
|
1159
|
+
3. Global commands (\`~/.agent-mgr/commands/\`) \u2014 lowest priority
|
|
1160
|
+
|
|
1161
|
+
Same filename = higher scope wins.
|
|
1162
|
+
|
|
1163
|
+
## Config Files
|
|
1164
|
+
|
|
1165
|
+
- Project config: \`.agent-mgr.yml\` (targets, overrides)
|
|
1166
|
+
- Global config: \`~/.agent-mgr/config.yml\` (targets, active profile, tracked repos)
|
|
1167
|
+
- Source commands: \`commands/\` (project) or \`~/.agent-mgr/commands/\` (global)
|
|
1168
|
+
- Profiles: \`~/.agent-mgr/profiles/<name>/commands/\`
|
|
1169
|
+
- Repo cache: \`~/.agent-mgr/repos/\`
|
|
1170
|
+
|
|
1171
|
+
## Supported Platforms
|
|
1172
|
+
|
|
1173
|
+
| Platform | Commands | MCP |
|
|
1174
|
+
|----------|----------|-----|
|
|
1175
|
+
| Claude Code | \u2713 (.claude/commands/) | \u2713 (.claude/mcp.json) |
|
|
1176
|
+
| Cursor | \u2713 (.cursor/prompts/) | \u2713 (.cursor/mcp.json) |
|
|
1177
|
+
| Codex | \u2717 | \u2713 (.codex/mcp.json) |
|
|
1178
|
+
| OpenCode | \u2717 | \u2713 (opencode.json) |
|
|
1179
|
+
`;
|
|
1180
|
+
function helpAgentCommand() {
|
|
1181
|
+
console.log(AGENT_PROMPT);
|
|
1182
|
+
console.log(chalk10.dim("Copy the above and paste it to your AI agent, or pipe it:"));
|
|
1183
|
+
console.log(chalk10.dim(" amgr help-agent | pbcopy"));
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// src/commands/help-rich.ts
|
|
1187
|
+
import chalk11 from "chalk";
|
|
1188
|
+
function helpCommand() {
|
|
1189
|
+
console.log(`
|
|
1190
|
+
${chalk11.bold("agent-manager")} \u2014 Write commands once, sync everywhere.
|
|
1191
|
+
|
|
1192
|
+
${chalk11.bold.underline("WORKFLOW")}
|
|
1193
|
+
|
|
1194
|
+
${chalk11.cyan("1.")} ${chalk11.bold("amgr init")} Set up in your repo or globally
|
|
1195
|
+
${chalk11.cyan("2.")} ${chalk11.bold("amgr add <name>")} Create a command (or --from/--content)
|
|
1196
|
+
${chalk11.cyan("3.")} ${chalk11.bold("amgr sync")} Distribute to all platforms
|
|
1197
|
+
${chalk11.cyan("4.")} Use ${chalk11.bold("/command-name")} in your AI tool
|
|
1198
|
+
|
|
1199
|
+
${chalk11.bold.underline("COMMANDS")}
|
|
1200
|
+
|
|
1201
|
+
${chalk11.bold("Setup")}
|
|
1202
|
+
amgr init Interactive setup
|
|
1203
|
+
amgr init --all --gitignore All platforms, auto-gitignore
|
|
1204
|
+
amgr init --global Global config at ~/.agent-mgr/
|
|
1205
|
+
|
|
1206
|
+
${chalk11.bold("Commands")}
|
|
1207
|
+
amgr add <name> Create template
|
|
1208
|
+
amgr add <name> --from <file> Import from existing .md
|
|
1209
|
+
amgr add <name> --content "..." Inline content
|
|
1210
|
+
amgr remove <name> Delete command + synced copies
|
|
1211
|
+
amgr sync Sync all (project + profile + global)
|
|
1212
|
+
amgr list Show commands with sync status
|
|
1213
|
+
|
|
1214
|
+
${chalk11.bold("Profiles")}
|
|
1215
|
+
amgr profile create <name> Create a named command set
|
|
1216
|
+
amgr profile switch <name> Activate a profile
|
|
1217
|
+
amgr profile list Show all profiles
|
|
1218
|
+
amgr profile delete <name> Delete a profile
|
|
1219
|
+
|
|
1220
|
+
${chalk11.bold("Sync from GitHub")}
|
|
1221
|
+
amgr sync-repo add <url> Import commands from a repo
|
|
1222
|
+
amgr sync-repo update Pull latest from tracked repos
|
|
1223
|
+
amgr sync-repo list Show tracked repos
|
|
1224
|
+
amgr sync-repo remove <url> Stop tracking a repo
|
|
1225
|
+
|
|
1226
|
+
${chalk11.bold("MCP Servers")}
|
|
1227
|
+
amgr mcp add Add MCP server (interactive)
|
|
1228
|
+
amgr mcp remove <name> Remove from all platforms
|
|
1229
|
+
amgr mcp list Show MCP servers per platform
|
|
1230
|
+
|
|
1231
|
+
${chalk11.bold("Git Hooks")}
|
|
1232
|
+
amgr hook install Auto-sync on checkout/merge
|
|
1233
|
+
amgr hook remove Remove hooks
|
|
1234
|
+
|
|
1235
|
+
${chalk11.bold("Help")}
|
|
1236
|
+
amgr help This help page
|
|
1237
|
+
amgr help-agent Output reference for AI agents
|
|
1238
|
+
|
|
1239
|
+
${chalk11.bold.underline("COMMAND PRIORITY")}
|
|
1240
|
+
|
|
1241
|
+
When syncing, commands are merged from three sources:
|
|
1242
|
+
${chalk11.green("project")} > ${chalk11.blue("profile")} > ${chalk11.dim("global")}
|
|
1243
|
+
If the same filename exists in multiple scopes, the higher priority wins.
|
|
1244
|
+
|
|
1245
|
+
${chalk11.bold.underline("CONFIG FILES")}
|
|
1246
|
+
|
|
1247
|
+
Project: .agent-mgr.yml + commands/
|
|
1248
|
+
Global: ~/.agent-mgr/config.yml + commands/
|
|
1249
|
+
Profiles: ~/.agent-mgr/profiles/<name>/commands/
|
|
1250
|
+
Repos: ~/.agent-mgr/repos/ (cached clones)
|
|
1251
|
+
`);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// src/commands/profile.ts
|
|
1255
|
+
import { existsSync as existsSync8, readdirSync as readdirSync2, rmSync } from "fs";
|
|
1256
|
+
import chalk12 from "chalk";
|
|
1257
|
+
function profileCreateCommand(name) {
|
|
1258
|
+
const profileDir = getProfileDir(name);
|
|
1259
|
+
if (existsSync8(profileDir)) {
|
|
1260
|
+
console.log(chalk12.yellow(`Profile "${name}" already exists.`));
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
ensureDir(getProfileCommandsDir(name));
|
|
1264
|
+
console.log(chalk12.green(`\u2713 Created profile "${name}"`));
|
|
1265
|
+
console.log(chalk12.dim(` Commands dir: ${getProfileCommandsDir(name)}`));
|
|
1266
|
+
console.log(chalk12.dim(` Switch to it: amgr profile switch ${name}`));
|
|
1267
|
+
}
|
|
1268
|
+
function profileSwitchCommand(name) {
|
|
1269
|
+
const profileDir = getProfileDir(name);
|
|
1270
|
+
if (!existsSync8(profileDir)) {
|
|
1271
|
+
console.log(chalk12.red(`Profile "${name}" does not exist. Create it first: amgr profile create ${name}`));
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
const config = loadConfig("global");
|
|
1275
|
+
config.activeProfile = name;
|
|
1276
|
+
saveConfig(config, "global");
|
|
1277
|
+
console.log(chalk12.green(`\u2713 Switched to profile "${name}"`));
|
|
1278
|
+
console.log(chalk12.dim("Run `amgr sync` to apply."));
|
|
1279
|
+
}
|
|
1280
|
+
function profileListCommand() {
|
|
1281
|
+
if (!existsSync8(GLOBAL_PROFILES_DIR)) {
|
|
1282
|
+
console.log(chalk12.yellow("No profiles found."));
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
const entries = readdirSync2(GLOBAL_PROFILES_DIR, { withFileTypes: true });
|
|
1286
|
+
const profiles = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1287
|
+
if (profiles.length === 0) {
|
|
1288
|
+
console.log(chalk12.yellow("No profiles found."));
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
const config = loadConfig("global");
|
|
1292
|
+
const active = config.activeProfile;
|
|
1293
|
+
console.log(chalk12.bold("\nProfiles:\n"));
|
|
1294
|
+
for (const p of profiles) {
|
|
1295
|
+
const commands = listMarkdownFiles(getProfileCommandsDir(p));
|
|
1296
|
+
const isActive = p === active;
|
|
1297
|
+
const marker = isActive ? chalk12.green(" (active)") : "";
|
|
1298
|
+
console.log(` ${p}${marker} \u2014 ${commands.length} command(s)`);
|
|
1299
|
+
}
|
|
1300
|
+
console.log("");
|
|
1301
|
+
}
|
|
1302
|
+
function profileDeleteCommand(name) {
|
|
1303
|
+
const profileDir = getProfileDir(name);
|
|
1304
|
+
if (!existsSync8(profileDir)) {
|
|
1305
|
+
console.log(chalk12.red(`Profile "${name}" does not exist.`));
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
const config = loadConfig("global");
|
|
1309
|
+
if (config.activeProfile === name) {
|
|
1310
|
+
config.activeProfile = void 0;
|
|
1311
|
+
saveConfig(config, "global");
|
|
1312
|
+
}
|
|
1313
|
+
rmSync(profileDir, { recursive: true, force: true });
|
|
1314
|
+
console.log(chalk12.green(`\u2713 Deleted profile "${name}"`));
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// src/commands/sync-repo.ts
|
|
1318
|
+
import { join as join13, dirname as dirname3 } from "path";
|
|
1319
|
+
import { existsSync as existsSync9, copyFileSync as copyFileSync2, rmSync as rmSync2 } from "fs";
|
|
1320
|
+
import { execSync } from "child_process";
|
|
1321
|
+
import chalk13 from "chalk";
|
|
1322
|
+
function parseRepoUrl(url) {
|
|
1323
|
+
const match = url.match(/^(?:https?:\/\/)?(?:github\.com\/)?([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+?)(?:\.git)?$/);
|
|
1324
|
+
if (!match) return null;
|
|
1325
|
+
const owner = match[1];
|
|
1326
|
+
const name = match[2];
|
|
1327
|
+
const cloneUrl = `https://github.com/${encodeURIComponent(owner)}/${encodeURIComponent(name)}.git`;
|
|
1328
|
+
return { owner, name, cloneUrl };
|
|
1329
|
+
}
|
|
1330
|
+
function getRepoCacheDir(owner, name) {
|
|
1331
|
+
return join13(GLOBAL_REPOS_DIR, `${owner}-${name}`);
|
|
1332
|
+
}
|
|
1333
|
+
function syncRepoCommand(url, options) {
|
|
1334
|
+
const parsed = parseRepoUrl(url);
|
|
1335
|
+
if (!parsed) {
|
|
1336
|
+
console.log(chalk13.red("Invalid repo URL. Use: github.com/owner/repo or owner/repo"));
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
const { owner, name, cloneUrl } = parsed;
|
|
1340
|
+
const cacheDir = getRepoCacheDir(owner, name);
|
|
1341
|
+
if (existsSync9(cacheDir)) {
|
|
1342
|
+
console.log(chalk13.dim(`Updating ${owner}/${name}...`));
|
|
1343
|
+
try {
|
|
1344
|
+
execSync("git pull --ff-only", { cwd: cacheDir, stdio: "pipe" });
|
|
1345
|
+
} catch {
|
|
1346
|
+
console.log(chalk13.yellow("Pull failed, re-cloning..."));
|
|
1347
|
+
rmSync2(cacheDir, { recursive: true, force: true });
|
|
1348
|
+
execSync(`git clone --depth 1 ${cloneUrl} ${cacheDir}`, { stdio: "pipe" });
|
|
1349
|
+
}
|
|
1350
|
+
} else {
|
|
1351
|
+
console.log(chalk13.dim(`Cloning ${owner}/${name}...`));
|
|
1352
|
+
ensureDir(GLOBAL_REPOS_DIR);
|
|
1353
|
+
try {
|
|
1354
|
+
execSync(`git clone --depth 1 ${cloneUrl} ${cacheDir}`, { stdio: "pipe" });
|
|
1355
|
+
} catch {
|
|
1356
|
+
console.log(chalk13.red(`Failed to clone ${cloneUrl}. Check the URL and your git authentication.`));
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
const repoCommandsDir = join13(cacheDir, "commands");
|
|
1361
|
+
if (!existsSync9(repoCommandsDir)) {
|
|
1362
|
+
console.log(chalk13.red(`No commands/ directory found in ${owner}/${name}.`));
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
const files = listMarkdownFiles(repoCommandsDir);
|
|
1366
|
+
if (files.length === 0) {
|
|
1367
|
+
console.log(chalk13.yellow(`No .md files found in ${owner}/${name}/commands/`));
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
let destDir;
|
|
1371
|
+
const config = loadConfig("global");
|
|
1372
|
+
if (options.profile) {
|
|
1373
|
+
destDir = getProfileCommandsDir(options.profile);
|
|
1374
|
+
ensureDir(destDir);
|
|
1375
|
+
} else if (config.activeProfile) {
|
|
1376
|
+
destDir = getProfileCommandsDir(config.activeProfile);
|
|
1377
|
+
ensureDir(destDir);
|
|
1378
|
+
} else {
|
|
1379
|
+
destDir = GLOBAL_COMMANDS_DIR;
|
|
1380
|
+
ensureDir(destDir);
|
|
1381
|
+
}
|
|
1382
|
+
let copied = 0;
|
|
1383
|
+
for (const file of files) {
|
|
1384
|
+
const src = join13(repoCommandsDir, file);
|
|
1385
|
+
const dest = join13(destDir, file);
|
|
1386
|
+
ensureDir(dirname3(dest));
|
|
1387
|
+
copyFileSync2(src, dest);
|
|
1388
|
+
console.log(chalk13.green(`\u2713 ${file}`));
|
|
1389
|
+
copied++;
|
|
1390
|
+
}
|
|
1391
|
+
if (!config.repos) config.repos = [];
|
|
1392
|
+
if (!config.repos.includes(url)) {
|
|
1393
|
+
config.repos.push(url);
|
|
1394
|
+
saveConfig(config, "global");
|
|
1395
|
+
}
|
|
1396
|
+
console.log("");
|
|
1397
|
+
console.log(`Imported ${chalk13.green(String(copied))} command(s) from ${owner}/${name}`);
|
|
1398
|
+
console.log(chalk13.dim("Run `amgr sync` to distribute to your platforms."));
|
|
1399
|
+
}
|
|
1400
|
+
function syncRepoUpdateCommand() {
|
|
1401
|
+
const config = loadConfig("global");
|
|
1402
|
+
if (!config.repos || config.repos.length === 0) {
|
|
1403
|
+
console.log(chalk13.yellow("No tracked repos. Add one with: amgr sync-repo add <url>"));
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
for (const url of config.repos) {
|
|
1407
|
+
console.log(chalk13.bold(`
|
|
1408
|
+
${url}`));
|
|
1409
|
+
syncRepoCommand(url, {});
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
function syncRepoListCommand() {
|
|
1413
|
+
const config = loadConfig("global");
|
|
1414
|
+
if (!config.repos || config.repos.length === 0) {
|
|
1415
|
+
console.log(chalk13.yellow("No tracked repos."));
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
console.log(chalk13.bold("\nTracked repos:\n"));
|
|
1419
|
+
for (const url of config.repos) {
|
|
1420
|
+
const parsed = parseRepoUrl(url);
|
|
1421
|
+
if (parsed) {
|
|
1422
|
+
const cacheDir = getRepoCacheDir(parsed.owner, parsed.name);
|
|
1423
|
+
const cached = existsSync9(cacheDir) ? chalk13.green("cached") : chalk13.dim("not cached");
|
|
1424
|
+
console.log(` ${url} (${cached})`);
|
|
1425
|
+
} else {
|
|
1426
|
+
console.log(` ${url}`);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
console.log("");
|
|
1430
|
+
}
|
|
1431
|
+
function syncRepoRemoveCommand(url) {
|
|
1432
|
+
const config = loadConfig("global");
|
|
1433
|
+
if (!config.repos || !config.repos.includes(url)) {
|
|
1434
|
+
console.log(chalk13.red(`Repo "${url}" is not tracked.`));
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
config.repos = config.repos.filter((r) => r !== url);
|
|
1438
|
+
saveConfig(config, "global");
|
|
1439
|
+
const parsed = parseRepoUrl(url);
|
|
1440
|
+
if (parsed) {
|
|
1441
|
+
const cacheDir = getRepoCacheDir(parsed.owner, parsed.name);
|
|
1442
|
+
if (existsSync9(cacheDir)) {
|
|
1443
|
+
rmSync2(cacheDir, { recursive: true, force: true });
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
console.log(chalk13.green(`\u2713 Removed ${url}`));
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// src/index.ts
|
|
1450
|
+
program.name("agent-mgr").description(
|
|
1451
|
+
"Write commands once, sync everywhere. Manage AI agent commands, prompts, and MCP configs."
|
|
1452
|
+
).version("0.2.0");
|
|
1453
|
+
program.command("init").description("Initialize agent-mgr in this repo or globally").option("-g, --global", "Initialize global config at ~/.agent-mgr/").option("-t, --targets <targets>", "Comma-separated list of targets (claude-code,cursor,codex,opencode)").option("-a, --all", "Select all available targets").option("--gitignore", "Add generated dirs to .git/info/exclude").option("--no-gitignore", "Skip gitignoring generated dirs").action(initCommand);
|
|
1454
|
+
program.command("add <name>").description("Create a new command template").option("-g, --global", "Add to global commands").option("-f, --from <path>", "Import command from an existing .md file").option("-c, --content <text>", "Set the command content inline").action(addCommand);
|
|
1455
|
+
program.command("remove <name>").description("Remove a command from source and all targets").option("-g, --global", "Remove from global commands").action(removeCommand);
|
|
1456
|
+
program.command("sync").description("Sync all commands to configured targets").option("-g, --global", "Sync global commands").action(syncCommand);
|
|
1457
|
+
program.command("list").description("Show commands and their sync status").option("-g, --global", "List global commands").action(listCommand);
|
|
1458
|
+
var mcp = program.command("mcp").description("Manage MCP server configs across tools");
|
|
1459
|
+
mcp.command("add").description("Add an MCP server to configured targets").option("-g, --global", "Add to global MCP config").action(mcpAddCommand);
|
|
1460
|
+
mcp.command("remove <name>").description("Remove an MCP server from all targets").option("-g, --global", "Remove from global MCP config").action(mcpRemoveCommand);
|
|
1461
|
+
mcp.command("list").description("List MCP servers across targets").option("-g, --global", "List global MCP servers").action(mcpListCommand);
|
|
1462
|
+
var hook = program.command("hook").description("Manage git hooks for auto-sync");
|
|
1463
|
+
hook.command("install").description("Install post-checkout and post-merge hooks").action(hookInstallCommand);
|
|
1464
|
+
hook.command("remove").description("Remove agent-mgr git hooks").action(hookRemoveCommand);
|
|
1465
|
+
program.command("help-agent").description("Output a full reference for AI agents to understand this tool").action(helpAgentCommand);
|
|
1466
|
+
var profile = program.command("profile").description("Manage command profiles");
|
|
1467
|
+
profile.command("create <name>").description("Create a new profile").action(profileCreateCommand);
|
|
1468
|
+
profile.command("switch <name>").description("Switch to a profile").action(profileSwitchCommand);
|
|
1469
|
+
profile.command("list").description("List all profiles").action(profileListCommand);
|
|
1470
|
+
profile.command("delete <name>").description("Delete a profile").action(profileDeleteCommand);
|
|
1471
|
+
var syncRepo = program.command("sync-repo").description("Import commands from a GitHub repo");
|
|
1472
|
+
syncRepo.command("add <url>").description("Clone a repo and import its commands").option("-p, --profile <name>", "Import into a specific profile").action(syncRepoCommand);
|
|
1473
|
+
syncRepo.command("update").description("Pull latest from all tracked repos").action(syncRepoUpdateCommand);
|
|
1474
|
+
syncRepo.command("list").description("List tracked repos").action(syncRepoListCommand);
|
|
1475
|
+
syncRepo.command("remove <url>").description("Stop tracking a repo and remove cached clone").action(syncRepoRemoveCommand);
|
|
1476
|
+
program.command("help").description("Show detailed help with examples").action(helpCommand);
|
|
1477
|
+
program.parse();
|