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.
Files changed (3) hide show
  1. package/README.md +113 -0
  2. package/dist/index.js +1477 -0
  3. 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();