@eric0117/agentforge 0.1.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 (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +275 -0
  3. package/dist/add-agent.js +145 -0
  4. package/dist/add-skill.js +185 -0
  5. package/dist/agent-prompt.js +211 -0
  6. package/dist/agentforge-config.js +106 -0
  7. package/dist/agents/claude.js +46 -0
  8. package/dist/agents/codex.js +67 -0
  9. package/dist/agents/cursor.js +54 -0
  10. package/dist/agents/index.js +15 -0
  11. package/dist/agents/io.js +252 -0
  12. package/dist/agents/types.js +1 -0
  13. package/dist/cli.js +374 -0
  14. package/dist/confirm.js +20 -0
  15. package/dist/doctor.js +223 -0
  16. package/dist/enter.js +85 -0
  17. package/dist/init.js +272 -0
  18. package/dist/lang-prompt.js +88 -0
  19. package/dist/list-skills.js +120 -0
  20. package/dist/logo.js +181 -0
  21. package/dist/path-prompt.js +148 -0
  22. package/dist/remove-agent.js +63 -0
  23. package/dist/remove-skill.js +88 -0
  24. package/dist/rename.js +222 -0
  25. package/dist/skill-prompt.js +199 -0
  26. package/dist/skills-data.js +727 -0
  27. package/dist/sync-skills.js +59 -0
  28. package/dist/templates/CLAUDE.md.tpl +141 -0
  29. package/dist/templates/context-handoff.SKILL.md.tpl +222 -0
  30. package/dist/templates/cross-repo-impact.SKILL.md.tpl +241 -0
  31. package/dist/templates/feature-retro.SKILL.md.tpl +312 -0
  32. package/dist/templates/feature-start.SKILL.md.tpl +631 -0
  33. package/dist/templates/history.SKILL.md.tpl +165 -0
  34. package/dist/templates/incident-context.SKILL.md.tpl +260 -0
  35. package/dist/templates/pr-create.SKILL.md.tpl +403 -0
  36. package/dist/templates/pr-review-analyze.SKILL.md.tpl +303 -0
  37. package/dist/templates/pre-deploy-check.SKILL.md.tpl +350 -0
  38. package/dist/templates/project-router.SKILL.md.tpl +55 -0
  39. package/dist/templates/release-coordinate.SKILL.md.tpl +209 -0
  40. package/package.json +54 -0
@@ -0,0 +1,120 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { readMasterDir } from "./agents/io.js";
4
+ import { AGENTS } from "./agents/index.js";
5
+ import { masterDir, requireWorkspace } from "./agentforge-config.js";
6
+ import { SKILLS } from "./skills-data.js";
7
+ const DIM = "\x1b[2m";
8
+ const GREEN = "\x1b[32m";
9
+ const YELLOW = "\x1b[33m";
10
+ const CYAN = "\x1b[36m";
11
+ const BOLD = "\x1b[1m";
12
+ const RED = "\x1b[31m";
13
+ const RESET = "\x1b[0m";
14
+ const AGENT_SKILL_PATHS = {
15
+ claude: (root, id) => join(root, ".claude/skills", id, "SKILL.md"),
16
+ cursor: (root, id) => join(root, ".cursor/rules", `${id}.mdc`),
17
+ codex: (root, id) => join(root, ".agents/skills", `${id}.md`),
18
+ };
19
+ const AGENT_SKILL_DIRS = {
20
+ claude: (root) => join(root, ".claude/skills"),
21
+ cursor: (root) => join(root, ".cursor/rules"),
22
+ codex: (root) => join(root, ".agents/skills"),
23
+ };
24
+ export async function runListSkills(opts) {
25
+ const root = resolve(opts.pathArg ?? process.cwd());
26
+ const cfg = requireWorkspace(root);
27
+ if (!existsSync(masterDir(root))) {
28
+ process.stderr.write(`\n${YELLOW}⚠${RESET} No master skills directory at ${DIM}${masterDir(root)}${RESET}\n\n` +
29
+ ` Run ${CYAN}agentforge init${RESET} here first to set up the workspace.\n\n`);
30
+ process.exit(1);
31
+ }
32
+ const { skills, skipped, warnings } = readMasterDir(masterDir(root));
33
+ const stdSet = new Set(SKILLS.map((s) => s.id));
34
+ console.log(`${BOLD}Skills in${RESET} ${CYAN}${root}${RESET} ${DIM}(lang: ${cfg.lang}, agents: ${cfg.agents.join(", ")})${RESET}`);
35
+ console.log("");
36
+ // header
37
+ console.log(` ${DIM}${"id".padEnd(34)} ${"kind".padEnd(8)} ${cfg.agents
38
+ .map((id) => (AGENTS.find((a) => a.id === id)?.label ?? id).padEnd(18))
39
+ .join("")}description${RESET}`);
40
+ console.log(` ${DIM}${"-".repeat(34)} ${"-".repeat(8)} ${cfg.agents
41
+ .map(() => "-".repeat(18))
42
+ .join("")}${"-".repeat(40)}${RESET}`);
43
+ // body — track drift while we go
44
+ let driftCount = 0;
45
+ for (const s of skills) {
46
+ const kindLabel = stdSet.has(s.id)
47
+ ? `${DIM}standard${RESET}`
48
+ : `${GREEN}custom${RESET} `;
49
+ const desc = (s.frontmatter["description"] ?? "")
50
+ .replace(/\s+/g, " ")
51
+ .slice(0, 80);
52
+ const cells = cfg.agents.map((id) => {
53
+ const present = existsSync(AGENT_SKILL_PATHS[id](root, s.id));
54
+ if (!present)
55
+ driftCount++;
56
+ return (present ? `${GREEN}✓${RESET}` : `${YELLOW}·${RESET}`).padEnd(18 + 9);
57
+ });
58
+ console.log(` ${s.id.padEnd(34)} ${kindLabel} ${cells.join("")}${desc}`);
59
+ }
60
+ // drift hint
61
+ if (driftCount > 0) {
62
+ console.log("");
63
+ console.log(`${YELLOW}⚠${RESET} ${driftCount} skill cell(s) not yet propagated — run ${CYAN}agentforge sync-skills${RESET} to bring all agents in sync.`);
64
+ }
65
+ // warnings on master files (e.g. name/filename mismatch, placeholder body)
66
+ if (warnings.length > 0) {
67
+ console.log("");
68
+ console.log(`${YELLOW}⚠ master file warnings:${RESET}`);
69
+ for (const w of warnings) {
70
+ console.log(` ${DIM}-${RESET} ${w.file}: ${w.warning}`);
71
+ }
72
+ }
73
+ // skipped master files (couldn't load)
74
+ if (skipped.length > 0) {
75
+ console.log("");
76
+ console.log(`${RED}✗ invalid master files (skipped — not propagated):${RESET}`);
77
+ for (const sk of skipped) {
78
+ console.log(` ${DIM}-${RESET} ${sk.file}: ${sk.reason}`);
79
+ }
80
+ }
81
+ // orphan detection — adapter files without a backing master.
82
+ const orphans = detectOrphans(root, cfg.agents, new Set(skills.map((s) => s.id)));
83
+ if (orphans.length > 0) {
84
+ console.log("");
85
+ console.log(`${YELLOW}⚠ orphan adapter files (no matching master skill):${RESET}`);
86
+ for (const o of orphans) {
87
+ console.log(` ${DIM}-${RESET} ${o.agent}: ${o.id} ${DIM}(${o.path})${RESET}`);
88
+ }
89
+ console.log(` ${DIM}→ create a master file at agentforge/skills/<id>.md, or remove via \`agentforge remove-skill <id>\`.${RESET}`);
90
+ }
91
+ console.log("");
92
+ }
93
+ function detectOrphans(root, agents, masterIds) {
94
+ const out = [];
95
+ for (const agent of agents) {
96
+ const dir = AGENT_SKILL_DIRS[agent](root);
97
+ if (!existsSync(dir))
98
+ continue;
99
+ const entries = readdirSync(dir, { withFileTypes: true });
100
+ for (const e of entries) {
101
+ let id = null;
102
+ if (agent === "claude" && e.isDirectory())
103
+ id = e.name;
104
+ else if (agent === "cursor" && e.isFile() && e.name.endsWith(".mdc"))
105
+ id = e.name.slice(0, -4);
106
+ else if (agent === "codex" && e.isFile() && e.name.endsWith(".md"))
107
+ id = e.name.slice(0, -3);
108
+ if (!id)
109
+ continue;
110
+ if (!masterIds.has(id)) {
111
+ out.push({
112
+ agent,
113
+ id,
114
+ path: `${dir.slice(root.length + 1)}/${e.name}`,
115
+ });
116
+ }
117
+ }
118
+ }
119
+ return out;
120
+ }
package/dist/logo.js ADDED
@@ -0,0 +1,181 @@
1
+ const RESET = "\x1b[0m";
2
+ const BOLD = "\x1b[1m";
3
+ const DIM = "\x1b[2m";
4
+ const ITALIC = "\x1b[3m";
5
+ // cyan gradient for the letters — center-bright
6
+ const GRADIENT = [
7
+ "\x1b[38;5;30m",
8
+ "\x1b[38;5;37m",
9
+ "\x1b[38;5;44m",
10
+ "\x1b[38;5;50m",
11
+ "\x1b[38;5;44m",
12
+ "\x1b[38;5;37m",
13
+ ];
14
+ // Everything below is in the same cyan / teal family as the letters.
15
+ const SPARK = "\x1b[38;5;87m"; // light cyan sparkles above
16
+ const ICE = "\x1b[38;5;87m"; // top of anvil — brightest
17
+ const BRIGHT_CYAN = "\x1b[38;5;51m"; // upper body
18
+ const MID_CYAN = "\x1b[38;5;44m"; // mid (matches letter mid-tone)
19
+ const TEAL = "\x1b[38;5;37m"; // lower body
20
+ const DEEP_TEAL = "\x1b[38;5;30m"; // base
21
+ const SHADOW = "\x1b[38;5;24m"; // deepest shadow at the bottom
22
+ const ACCENT = "\x1b[38;5;51m"; // hammer accents on the tagline
23
+ const SUB = "\x1b[38;5;245m";
24
+ const LOGO_LINES = [
25
+ " █████╗ ██████╗ ███████╗███╗ ██╗████████╗███████╗ ██████╗ ██████╗ ██████╗ ███████╗",
26
+ " ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝██╔════╝██╔═══██╗██╔══██╗██╔════╝ ██╔════╝",
27
+ " ███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║ █████╗ ██║ ██║██████╔╝██║ ███╗█████╗",
28
+ " ██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║ ██╔══╝ ██║ ██║██╔══██╗██║ ██║██╔══╝",
29
+ " ██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║ ██║ ╚██████╔╝██║ ██║╚██████╔╝███████╗",
30
+ " ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝",
31
+ ];
32
+ // LEFT: gap = spaces *before* the anvil's first visible character.
33
+ // smaller = closer to the anvil.
34
+ const LEFT_SPARKS = [
35
+ [2, "⋆"],
36
+ [-1, ""],
37
+ [3, "·"],
38
+ [2, "✦"],
39
+ [-1, ""],
40
+ [4, "·"],
41
+ [2, "⋆"],
42
+ [-1, ""],
43
+ [3, "✧"],
44
+ [5, "·"],
45
+ [-1, ""],
46
+ [2, "⋆"],
47
+ [4, "✦"],
48
+ ];
49
+ // RIGHT: gap = spaces *after* the anvil row's last character.
50
+ const RIGHT_SPARKS = [
51
+ [3, "·"],
52
+ [-1, ""],
53
+ [5, "⋆"],
54
+ [2, "✦"],
55
+ [-1, ""],
56
+ [4, "·"],
57
+ [-1, ""],
58
+ [3, "⋆"],
59
+ [6, "·"],
60
+ [-1, ""],
61
+ [2, "✧"],
62
+ [5, "·"],
63
+ [-1, ""],
64
+ ];
65
+ // anvil + hammer ASCII art — used verbatim as supplied
66
+ const ANVIL_ART = [
67
+ " =++++++:.::-:-===+=++*=+***##.",
68
+ " .:...:--=+#*=@*---========++++*###%#",
69
+ " =*#%#*=-.. .=@=@*:------=====++=",
70
+ " :=+++++===#@=@*:-----====+=",
71
+ " :+%@@@%+@+::-==--===:",
72
+ " .+#@%*++++=+++",
73
+ " =#%@@%==-====",
74
+ " ++++++====++=",
75
+ " :*#@@@@@---=-===-",
76
+ " -##*#%%%@@@+==*%%@@*+==",
77
+ " =##%%#%%%@%%%%=+++%@@@@@@++++",
78
+ " ####*#**++*+**+=--: :+###*==.",
79
+ " .:===+*+=-",
80
+ ];
81
+ const PAD = " ";
82
+ // cyan gradient matching the AGENTFORGE letters above:
83
+ // brightest at the top (light catches the hammer face) → deeper teal/blue
84
+ // at the bottom (heavy base in shadow).
85
+ function anvilColor(rowIndex, total) {
86
+ const r = rowIndex / total;
87
+ if (r < 0.17)
88
+ return ICE;
89
+ if (r < 0.34)
90
+ return BRIGHT_CYAN;
91
+ if (r < 0.55)
92
+ return MID_CYAN;
93
+ if (r < 0.72)
94
+ return TEAL;
95
+ if (r < 0.90)
96
+ return DEEP_TEAL;
97
+ return SHADOW;
98
+ }
99
+ // build one anvil row with sparkles hugging the silhouette on either side.
100
+ // the anvil rows have variable leading whitespace; we treat ANVIL_INDENT plus
101
+ // each row's own leading spaces as the "left margin" the sparkle lives in.
102
+ function buildAnvilLine(i, total) {
103
+ const ANVIL_INDENT_COLS = 18; // shifts anvil under the AGENTFORGE letters
104
+ const row = ANVIL_ART[i];
105
+ const trimmed = row.trimStart();
106
+ const rowLead = row.length - trimmed.length;
107
+ const totalLead = ANVIL_INDENT_COLS + rowLead; // columns until the anvil draws
108
+ const [lgap, lchar] = LEFT_SPARKS[i] ?? [-1, ""];
109
+ const [rgap, rchar] = RIGHT_SPARKS[i] ?? [-1, ""];
110
+ // left margin: spaces + optional sparkle `lgap` columns before the anvil
111
+ let leftSegment;
112
+ if (lgap >= 0 && lchar && lgap < totalLead) {
113
+ const sparkleCol = totalLead - lgap;
114
+ leftSegment =
115
+ " ".repeat(sparkleCol) +
116
+ `${SPARK}${lchar}${RESET}` +
117
+ " ".repeat(Math.max(0, totalLead - sparkleCol - 1));
118
+ }
119
+ else {
120
+ leftSegment = " ".repeat(totalLead);
121
+ }
122
+ // anvil — only the visible part (we already accounted for leading spaces)
123
+ const anvilSegment = `${anvilColor(i, total)}${shaded(trimmed)}${RESET}`;
124
+ // right margin: `rgap` spaces from anvil's last character, then sparkle
125
+ const rightSegment = rgap >= 0 && rchar
126
+ ? `${" ".repeat(rgap)}${SPARK}${rchar}${RESET}`
127
+ : "";
128
+ return leftSegment + anvilSegment + rightSegment;
129
+ }
130
+ // re-shade the source ASCII (which uses + # @ = * : - .) into block characters
131
+ // so the result reads as a metal silhouette instead of typed text.
132
+ // @ → █ (full block)
133
+ // # % → ▓ (dark shade)
134
+ // + * = → ▒ (medium shade)
135
+ // : - . → ░ (light shade)
136
+ function shaded(line) {
137
+ let out = "";
138
+ for (const ch of line) {
139
+ switch (ch) {
140
+ case "@":
141
+ out += "█";
142
+ break;
143
+ case "#":
144
+ case "%":
145
+ out += "▓";
146
+ break;
147
+ case "+":
148
+ case "*":
149
+ case "=":
150
+ out += "▒";
151
+ break;
152
+ case ":":
153
+ case "-":
154
+ case ".":
155
+ out += "░";
156
+ break;
157
+ default:
158
+ out += ch; // spaces and anything else pass through
159
+ }
160
+ }
161
+ return out;
162
+ }
163
+ export function printLogo() {
164
+ const lines = [""];
165
+ // 1–6. letters with cyan gradient (no sparks above — sparkles live around
166
+ // the anvil now).
167
+ for (let i = 0; i < LOGO_LINES.length; i++) {
168
+ lines.push(`${BOLD}${GRADIENT[i]}${LOGO_LINES[i]}${RESET}`);
169
+ }
170
+ // 7+. anvil ASCII, centered under the letters, with floating sparkles
171
+ // flanking it on the left and right (per-row pattern in LEFT/RIGHT_SPARKS).
172
+ for (let i = 0; i < ANVIL_ART.length; i++) {
173
+ lines.push(buildAnvilLine(i, ANVIL_ART.length));
174
+ }
175
+ lines.push("");
176
+ // tagline + subtitle, hammer-flanked
177
+ lines.push(`${PAD}${ACCENT}${BOLD}⚒${RESET} ${SUB}${ITALIC}forge your Claude Code workspace${RESET} ${ACCENT}${BOLD}⚒${RESET}`);
178
+ lines.push(`${PAD}${DIM}bootstrap multi-repo · multi-worktree · multi-feature${RESET}`);
179
+ lines.push("");
180
+ process.stdout.write(lines.join("\n") + "\n");
181
+ }
@@ -0,0 +1,148 @@
1
+ import { readdirSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, dirname, join } from "node:path";
4
+ import * as readline from "node:readline/promises";
5
+ const CYAN = "\x1b[36m";
6
+ const GREEN = "\x1b[32m";
7
+ const DIM = "\x1b[2m";
8
+ const BOLD = "\x1b[1m";
9
+ const RESET = "\x1b[0m";
10
+ function expandTilde(p) {
11
+ if (p === "~")
12
+ return homedir();
13
+ if (p.startsWith("~/"))
14
+ return join(homedir(), p.slice(2));
15
+ return p;
16
+ }
17
+ /** Suffix that should appear after the current buffer as a ghost-text hint. */
18
+ function findSuggestion(buffer) {
19
+ if (buffer.length === 0)
20
+ return "";
21
+ const expanded = expandTilde(buffer);
22
+ let dir;
23
+ let prefix;
24
+ if (expanded.endsWith("/")) {
25
+ dir = expanded;
26
+ prefix = "";
27
+ }
28
+ else {
29
+ const d = dirname(expanded);
30
+ dir = d === "" ? "." : d;
31
+ prefix = basename(expanded);
32
+ }
33
+ let names = [];
34
+ try {
35
+ names = readdirSync(dir, { withFileTypes: true })
36
+ .filter((e) => e.isDirectory() &&
37
+ e.name.toLowerCase().startsWith(prefix.toLowerCase()))
38
+ .map((e) => e.name)
39
+ .sort();
40
+ }
41
+ catch {
42
+ return "";
43
+ }
44
+ if (names.length === 0)
45
+ return "";
46
+ if (expanded.endsWith("/")) {
47
+ return `${names[0]}/`;
48
+ }
49
+ const first = names[0];
50
+ if (first.toLowerCase() === prefix.toLowerCase()) {
51
+ return "/";
52
+ }
53
+ return `${first.slice(prefix.length)}/`;
54
+ }
55
+ async function promptPathTTY(message, initial) {
56
+ process.stdout.write(`${CYAN}?${RESET} ${BOLD}${message}${RESET} ${DIM}(→ or Tab to accept · Enter to confirm)${RESET}\n`);
57
+ const PROMPT = `${DIM}›${RESET} `;
58
+ let buffer = initial;
59
+ const render = () => {
60
+ const suggestion = findSuggestion(buffer);
61
+ process.stdout.write(`\r\x1b[K${PROMPT}${buffer}${DIM}${suggestion}${RESET}`);
62
+ if (suggestion.length > 0) {
63
+ process.stdout.write(`\x1b[${suggestion.length}D`);
64
+ }
65
+ };
66
+ return new Promise((resolve) => {
67
+ const stdin = process.stdin;
68
+ stdin.setRawMode(true);
69
+ stdin.resume();
70
+ stdin.setEncoding("utf8");
71
+ const cleanup = () => {
72
+ stdin.setRawMode(false);
73
+ stdin.pause();
74
+ stdin.removeListener("data", onData);
75
+ };
76
+ const acceptSuggestion = () => {
77
+ const sug = findSuggestion(buffer);
78
+ if (sug)
79
+ buffer += sug;
80
+ };
81
+ const onData = (chunk) => {
82
+ // Whole-chunk escape sequences (arrow keys, function keys, etc.)
83
+ if (chunk === "\x1b[C") {
84
+ acceptSuggestion();
85
+ render();
86
+ return;
87
+ }
88
+ if (chunk.startsWith("\x1b")) {
89
+ // ignore other escapes (left/up/down arrows, etc.)
90
+ return;
91
+ }
92
+ for (const ch of chunk) {
93
+ if (ch === "\x03") {
94
+ // Ctrl+C
95
+ cleanup();
96
+ process.stdout.write(`\n${DIM}aborted.${RESET}\n`);
97
+ process.exit(130);
98
+ }
99
+ if (ch === "\r" || ch === "\n") {
100
+ cleanup();
101
+ // commit final line with no ghost text
102
+ process.stdout.write(`\r\x1b[K${PROMPT}${buffer}\n`);
103
+ const val = buffer.trim();
104
+ const final = val === "" ? initial : val;
105
+ const resolved = expandTilde(final);
106
+ process.stdout.write(` ${GREEN}✔${RESET} ${DIM}${resolved}${RESET}\n`);
107
+ resolve(resolved);
108
+ return;
109
+ }
110
+ if (ch === "\t") {
111
+ acceptSuggestion();
112
+ render();
113
+ continue;
114
+ }
115
+ if (ch === "\x7f" || ch === "\x08") {
116
+ // Backspace / Delete
117
+ buffer = buffer.slice(0, -1);
118
+ render();
119
+ continue;
120
+ }
121
+ if (ch >= " ") {
122
+ buffer += ch;
123
+ render();
124
+ continue;
125
+ }
126
+ // ignore other control chars
127
+ }
128
+ };
129
+ stdin.on("data", onData);
130
+ render();
131
+ });
132
+ }
133
+ async function promptPathFallback(message, initial) {
134
+ const rl = readline.createInterface({
135
+ input: process.stdin,
136
+ output: process.stdout,
137
+ });
138
+ const ans = await rl.question(`${BOLD}${message}${RESET} `);
139
+ rl.close();
140
+ const v = ans.trim();
141
+ return expandTilde(v === "" ? initial : v);
142
+ }
143
+ export async function promptPath(message, initial) {
144
+ if (!process.stdin.isTTY) {
145
+ return promptPathFallback(message, initial);
146
+ }
147
+ return promptPathTTY(message, initial);
148
+ }
@@ -0,0 +1,63 @@
1
+ import { existsSync, rmSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { AGENTS } from "./agents/index.js";
4
+ import { masterDir, requireWorkspace, setAgents, } from "./agentforge-config.js";
5
+ import { confirm } from "./confirm.js";
6
+ const DIM = "\x1b[2m";
7
+ const GREEN = "\x1b[32m";
8
+ const YELLOW = "\x1b[33m";
9
+ const CYAN = "\x1b[36m";
10
+ const BOLD = "\x1b[1m";
11
+ const RED = "\x1b[31m";
12
+ const RESET = "\x1b[0m";
13
+ /** files / directories owned by each agent at workspace root */
14
+ const AGENT_ARTIFACTS = {
15
+ claude: [".claude/skills", "CLAUDE.md"],
16
+ cursor: [".cursor/rules", ".cursorrules"],
17
+ codex: [".agents/skills", "AGENTS.md"],
18
+ };
19
+ export async function runRemoveAgent(opts) {
20
+ const root = resolve(opts.pathArg ?? process.cwd());
21
+ const cfg = requireWorkspace(root);
22
+ const adapter = AGENTS.find((a) => a.id === opts.agent);
23
+ if (!adapter) {
24
+ const valid = AGENTS.map((a) => a.id).join(", ");
25
+ process.stderr.write(`\n${RED}✗${RESET} Unknown agent: "${opts.agent}"\n\n Valid options: ${CYAN}${valid}${RESET}\n\n`);
26
+ process.exit(1);
27
+ }
28
+ // master is preserved — only this agent's per-agent files are removed.
29
+ const paths = AGENT_ARTIFACTS[opts.agent]
30
+ .map((rel) => ({ rel, abs: join(root, rel) }))
31
+ .filter((p) => existsSync(p.abs));
32
+ if (paths.length === 0) {
33
+ console.log(`${YELLOW}${adapter.label} has no artifacts in ${root} — nothing to remove.${RESET}`);
34
+ // still update config in case it's listed
35
+ if (cfg.agents.includes(opts.agent)) {
36
+ setAgents(root, cfg.agents.filter((id) => id !== opts.agent));
37
+ console.log(`${DIM} config.agents updated.${RESET}`);
38
+ }
39
+ return;
40
+ }
41
+ console.log(`${BOLD}Removing ${adapter.label}${RESET} from ${CYAN}${root}${RESET}`);
42
+ console.log(` ${DIM}files to delete:${RESET}`);
43
+ for (const p of paths)
44
+ console.log(` - ${p.rel}`);
45
+ console.log("");
46
+ console.log(` ${DIM}master skills (agentforge/skills/) are kept.${RESET}`);
47
+ console.log("");
48
+ const ok = opts.yes || (await confirm(`Proceed?`, false));
49
+ if (!ok) {
50
+ console.log(`${YELLOW}aborted.${RESET}`);
51
+ return;
52
+ }
53
+ for (const p of paths) {
54
+ rmSync(p.abs, { recursive: true, force: true });
55
+ console.log(`${GREEN}-${RESET} removed: ${p.rel}`);
56
+ }
57
+ setAgents(root, cfg.agents.filter((id) => id !== opts.agent));
58
+ console.log(`${DIM} config.agents updated.${RESET}`);
59
+ // dummy reference to silence "masterDir unused" if it ever becomes so
60
+ void masterDir;
61
+ console.log("");
62
+ console.log(`${BOLD}${GREEN}✓${RESET} ${adapter.label} removed.`);
63
+ }
@@ -0,0 +1,88 @@
1
+ import { existsSync, rmSync, unlinkSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { readMasterDir } from "./agents/io.js";
4
+ import { getAgent } from "./agents/index.js";
5
+ import { masterDir, requireWorkspace } from "./agentforge-config.js";
6
+ import { confirm } from "./confirm.js";
7
+ import { SKILLS } from "./skills-data.js";
8
+ const DIM = "\x1b[2m";
9
+ const GREEN = "\x1b[32m";
10
+ const YELLOW = "\x1b[33m";
11
+ const CYAN = "\x1b[36m";
12
+ const BOLD = "\x1b[1m";
13
+ const RED = "\x1b[31m";
14
+ const RESET = "\x1b[0m";
15
+ const AGENT_SKILL_TARGETS = {
16
+ claude: (root, id) => ({
17
+ path: join(root, ".claude/skills", id),
18
+ isDir: true,
19
+ }),
20
+ cursor: (root, id) => ({
21
+ path: join(root, ".cursor/rules", `${id}.mdc`),
22
+ isDir: false,
23
+ }),
24
+ codex: (root, id) => ({
25
+ path: join(root, ".agents/skills", `${id}.md`),
26
+ isDir: false,
27
+ }),
28
+ };
29
+ export async function runRemoveSkill(opts) {
30
+ const root = resolve(opts.pathArg ?? process.cwd());
31
+ const cfg = requireWorkspace(root);
32
+ const masterPath = join(masterDir(root), `${opts.name}.md`);
33
+ if (!existsSync(masterPath)) {
34
+ process.stderr.write(`\n${RED}✗${RESET} No master skill named "${opts.name}".\n ${DIM}${masterPath}${RESET}\n\n` +
35
+ ` Run ${CYAN}agentforge list-skills${RESET} to see what's available.\n\n`);
36
+ process.exit(1);
37
+ }
38
+ // collect agent artifacts to remove
39
+ const targets = [];
40
+ for (const id of cfg.agents) {
41
+ const t = AGENT_SKILL_TARGETS[id](root, opts.name);
42
+ if (existsSync(t.path)) {
43
+ targets.push({ rel: t.path.slice(root.length + 1), abs: t.path, isDir: t.isDir });
44
+ }
45
+ }
46
+ const isStandard = SKILLS.some((s) => s.id === opts.name);
47
+ console.log(`${BOLD}Removing skill${RESET} ${CYAN}${opts.name}${RESET} from ${CYAN}${root}${RESET}`);
48
+ if (isStandard) {
49
+ console.log(` ${YELLOW}note:${RESET} this is a standard agentforge skill. Re-init or \`add-skill --from\` will restore it.`);
50
+ }
51
+ console.log(` ${DIM}files to delete:${RESET}`);
52
+ console.log(` - agentforge/skills/${opts.name}.md ${DIM}(master)${RESET}`);
53
+ for (const t of targets)
54
+ console.log(` - ${t.rel}`);
55
+ console.log("");
56
+ const ok = opts.yes || (await confirm("Proceed?", false));
57
+ if (!ok) {
58
+ console.log(`${YELLOW}aborted.${RESET}`);
59
+ return;
60
+ }
61
+ // delete master first; then re-sync per-agent index files (AGENTS.md) by
62
+ // re-running the install for each agent on the remaining master skills.
63
+ unlinkSync(masterPath);
64
+ console.log(`${GREEN}-${RESET} removed: agentforge/skills/${opts.name}.md`);
65
+ for (const t of targets) {
66
+ if (t.isDir)
67
+ rmSync(t.abs, { recursive: true, force: true });
68
+ else
69
+ unlinkSync(t.abs);
70
+ console.log(`${GREEN}-${RESET} removed: ${t.rel}`);
71
+ }
72
+ // Re-run each agent's install with the remaining master skills so that
73
+ // index sections (e.g. AGENTS.md Skills) reflect the deletion.
74
+ const { skills: remaining } = readMasterDir(masterDir(root));
75
+ for (const id of cfg.agents) {
76
+ const adapter = getAgent(id);
77
+ adapter.install({
78
+ root,
79
+ masterSkills: remaining,
80
+ skillCatalog: SKILLS.slice(),
81
+ lang: cfg.lang,
82
+ forceSkills: true,
83
+ forceClaude: true, // re-emit root guides to refresh indexes
84
+ });
85
+ }
86
+ console.log("");
87
+ console.log(`${BOLD}${GREEN}✓${RESET} removed skill: ${opts.name}`);
88
+ }