@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.
- package/LICENSE +21 -0
- package/README.md +275 -0
- package/dist/add-agent.js +145 -0
- package/dist/add-skill.js +185 -0
- package/dist/agent-prompt.js +211 -0
- package/dist/agentforge-config.js +106 -0
- package/dist/agents/claude.js +46 -0
- package/dist/agents/codex.js +67 -0
- package/dist/agents/cursor.js +54 -0
- package/dist/agents/index.js +15 -0
- package/dist/agents/io.js +252 -0
- package/dist/agents/types.js +1 -0
- package/dist/cli.js +374 -0
- package/dist/confirm.js +20 -0
- package/dist/doctor.js +223 -0
- package/dist/enter.js +85 -0
- package/dist/init.js +272 -0
- package/dist/lang-prompt.js +88 -0
- package/dist/list-skills.js +120 -0
- package/dist/logo.js +181 -0
- package/dist/path-prompt.js +148 -0
- package/dist/remove-agent.js +63 -0
- package/dist/remove-skill.js +88 -0
- package/dist/rename.js +222 -0
- package/dist/skill-prompt.js +199 -0
- package/dist/skills-data.js +727 -0
- package/dist/sync-skills.js +59 -0
- package/dist/templates/CLAUDE.md.tpl +141 -0
- package/dist/templates/context-handoff.SKILL.md.tpl +222 -0
- package/dist/templates/cross-repo-impact.SKILL.md.tpl +241 -0
- package/dist/templates/feature-retro.SKILL.md.tpl +312 -0
- package/dist/templates/feature-start.SKILL.md.tpl +631 -0
- package/dist/templates/history.SKILL.md.tpl +165 -0
- package/dist/templates/incident-context.SKILL.md.tpl +260 -0
- package/dist/templates/pr-create.SKILL.md.tpl +403 -0
- package/dist/templates/pr-review-analyze.SKILL.md.tpl +303 -0
- package/dist/templates/pre-deploy-check.SKILL.md.tpl +350 -0
- package/dist/templates/project-router.SKILL.md.tpl +55 -0
- package/dist/templates/release-coordinate.SKILL.md.tpl +209 -0
- 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
|
+
}
|