@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
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { readMasterDir } from "./agents/io.js";
|
|
5
|
+
import { AGENTS } from "./agents/index.js";
|
|
6
|
+
import { masterDir, readConfig } from "./agentforge-config.js";
|
|
7
|
+
import { LANG_INSTRUCTIONS } 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 OK = `${GREEN}โ${RESET}`;
|
|
16
|
+
const WARN = `${YELLOW}โ ${RESET}`;
|
|
17
|
+
const FAIL = `${RED}โ${RESET}`;
|
|
18
|
+
const AGENT_FILES = {
|
|
19
|
+
claude: { dir: ".claude/skills", guide: "CLAUDE.md" },
|
|
20
|
+
cursor: { dir: ".cursor/rules", guide: ".cursorrules" },
|
|
21
|
+
codex: { dir: ".agents/skills", guide: "AGENTS.md" },
|
|
22
|
+
};
|
|
23
|
+
export async function runDoctor(opts) {
|
|
24
|
+
const root = resolve(opts.pathArg ?? process.cwd());
|
|
25
|
+
let issues = 0;
|
|
26
|
+
let warnings = 0;
|
|
27
|
+
console.log(`${BOLD}๐ฉบ agentforge doctor${RESET} ${DIM}${root}${RESET}\n`);
|
|
28
|
+
// โโ Workspace โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
29
|
+
console.log(`${BOLD}Workspace${RESET}`);
|
|
30
|
+
const cfg = readConfig(root);
|
|
31
|
+
if (!cfg) {
|
|
32
|
+
console.log(` ${FAIL} not initialized โ no agentforge/config.json`);
|
|
33
|
+
console.log(` ${DIM}run ${CYAN}agentforge init${RESET}${DIM} here to set one up${RESET}\n`);
|
|
34
|
+
printToolsSection();
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
console.log(` ${OK} config.json ${DIM}(v${1}, lang=${cfg.lang}, agents=${cfg.agents.join(", ") || "โ"})${RESET}`);
|
|
38
|
+
const md = masterDir(root);
|
|
39
|
+
if (!existsSync(md)) {
|
|
40
|
+
console.log(` ${FAIL} master skills directory missing: ${md}`);
|
|
41
|
+
issues++;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const { skills, skipped, warnings: masterWarnings } = readMasterDir(md);
|
|
45
|
+
console.log(` ${OK} master skills ${DIM}(${skills.length} valid)${RESET}`);
|
|
46
|
+
if (skipped.length > 0) {
|
|
47
|
+
console.log(` ${FAIL} ${skipped.length} invalid master file(s) โ ${DIM}skipped during sync${RESET}`);
|
|
48
|
+
for (const s of skipped) {
|
|
49
|
+
console.log(` ${DIM}- ${s.file}: ${s.reason}${RESET}`);
|
|
50
|
+
}
|
|
51
|
+
issues += skipped.length;
|
|
52
|
+
}
|
|
53
|
+
if (masterWarnings.length > 0) {
|
|
54
|
+
console.log(` ${WARN} ${masterWarnings.length} master file warning(s)`);
|
|
55
|
+
for (const w of masterWarnings) {
|
|
56
|
+
console.log(` ${DIM}- ${w.file}: ${w.warning}${RESET}`);
|
|
57
|
+
}
|
|
58
|
+
warnings += masterWarnings.length;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// โโ Agents โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
62
|
+
console.log(`\n${BOLD}Agents${RESET}`);
|
|
63
|
+
if (cfg.agents.length === 0) {
|
|
64
|
+
console.log(` ${WARN} no agents installed โ run ${CYAN}agentforge add-agent${RESET}`);
|
|
65
|
+
warnings++;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const masterIds = existsSync(md)
|
|
69
|
+
? new Set(readMasterDir(md).skills.map((s) => s.id))
|
|
70
|
+
: new Set();
|
|
71
|
+
for (const id of cfg.agents) {
|
|
72
|
+
const adapter = AGENTS.find((a) => a.id === id);
|
|
73
|
+
const label = adapter?.label ?? id;
|
|
74
|
+
const { dir, guide } = AGENT_FILES[id];
|
|
75
|
+
const dirAbs = join(root, dir);
|
|
76
|
+
const guideAbs = join(root, guide);
|
|
77
|
+
const dirExists = existsSync(dirAbs);
|
|
78
|
+
const guideExists = existsSync(guideAbs);
|
|
79
|
+
const present = dirExists ? countSkills(id, dirAbs) : new Set();
|
|
80
|
+
const missing = [...masterIds].filter((mid) => !present.has(mid));
|
|
81
|
+
const orphans = [...present].filter((pid) => !masterIds.has(pid));
|
|
82
|
+
if (!dirExists || !guideExists) {
|
|
83
|
+
console.log(` ${FAIL} ${label} ${DIM}${!dirExists ? `${dir} missing` : `${guide} missing`}${RESET}`);
|
|
84
|
+
console.log(` ${DIM}run ${CYAN}agentforge sync-skills${RESET}${DIM} to regenerate${RESET}`);
|
|
85
|
+
issues++;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const noteParts = [`${present.size} skill${present.size === 1 ? "" : "s"}`];
|
|
89
|
+
if (missing.length > 0)
|
|
90
|
+
noteParts.push(`${YELLOW}${missing.length} not propagated${DIM}`);
|
|
91
|
+
if (orphans.length > 0)
|
|
92
|
+
noteParts.push(`${YELLOW}${orphans.length} orphan${DIM}`);
|
|
93
|
+
const mark = missing.length === 0 && orphans.length === 0 ? OK : WARN;
|
|
94
|
+
console.log(` ${mark} ${label} ${DIM}(${noteParts.join(", ")})${RESET}`);
|
|
95
|
+
if (missing.length > 0) {
|
|
96
|
+
console.log(` ${DIM}missing:${RESET} ${missing.join(", ")} ${DIM}โ ${CYAN}agentforge sync-skills${RESET}`);
|
|
97
|
+
warnings++;
|
|
98
|
+
}
|
|
99
|
+
if (orphans.length > 0) {
|
|
100
|
+
console.log(` ${DIM}orphan: ${orphans.join(", ")} โ ${CYAN}agentforge remove-skill <id>${RESET}`);
|
|
101
|
+
warnings++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Whole-agent orphans: agent dir/guide present but agent NOT in config.
|
|
105
|
+
// Happens when the user manually deletes from config or copies an
|
|
106
|
+
// existing tree from elsewhere.
|
|
107
|
+
for (const id of Object.keys(AGENT_FILES)) {
|
|
108
|
+
if (cfg.agents.includes(id))
|
|
109
|
+
continue;
|
|
110
|
+
const { dir, guide } = AGENT_FILES[id];
|
|
111
|
+
const hasDir = existsSync(join(root, dir));
|
|
112
|
+
const hasGuide = existsSync(join(root, guide));
|
|
113
|
+
if (hasDir || hasGuide) {
|
|
114
|
+
const label = AGENTS.find((a) => a.id === id)?.label ?? id;
|
|
115
|
+
const which = [hasDir ? dir : null, hasGuide ? guide : null]
|
|
116
|
+
.filter(Boolean)
|
|
117
|
+
.join(", ");
|
|
118
|
+
console.log(` ${WARN} ${label} ${DIM}files present but not in config: ${which}${RESET}`);
|
|
119
|
+
console.log(` ${DIM}โ ${CYAN}agentforge add-agent ${id}${RESET}${DIM} to re-register, or ${CYAN}agentforge remove-agent ${id}${RESET}${DIM} to clean up${RESET}`);
|
|
120
|
+
warnings++;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// โโ Lang drift โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
125
|
+
// config.lang is supposed to reflect what's actually written into master
|
|
126
|
+
// skills (the {{OUTPUT_LANGUAGE_INSTRUCTION}} placeholder is substituted at
|
|
127
|
+
// init time). If someone edited config.lang by hand, the master content
|
|
128
|
+
// now lies โ agents will respond in the wrong language.
|
|
129
|
+
const drift = detectLangDrift(md, cfg.lang);
|
|
130
|
+
if (drift) {
|
|
131
|
+
console.log(`\n${BOLD}Language${RESET}`);
|
|
132
|
+
console.log(` ${WARN} config.lang=${BOLD}${cfg.lang}${RESET}${DIM} but master files were written for ${BOLD}${drift}${RESET}${DIM}.${RESET}`);
|
|
133
|
+
console.log(` ${DIM}โ ${CYAN}agentforge init ${root} --lang ${cfg.lang} --force-skills${RESET}${DIM} to rewrite master files, then ${CYAN}agentforge sync-skills${RESET}`);
|
|
134
|
+
warnings++;
|
|
135
|
+
}
|
|
136
|
+
// โโ External tools โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
137
|
+
printToolsSection();
|
|
138
|
+
// โโ Summary โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
139
|
+
console.log(`${BOLD}Summary${RESET}`);
|
|
140
|
+
if (issues === 0 && warnings === 0) {
|
|
141
|
+
console.log(` ${OK} workspace is healthy.`);
|
|
142
|
+
process.exit(0);
|
|
143
|
+
}
|
|
144
|
+
if (issues > 0)
|
|
145
|
+
console.log(` ${FAIL} ${issues} issue${issues === 1 ? "" : "s"}`);
|
|
146
|
+
if (warnings > 0)
|
|
147
|
+
console.log(` ${WARN} ${warnings} warning${warnings === 1 ? "" : "s"}`);
|
|
148
|
+
process.exit(issues > 0 ? 1 : 0);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Walk master skills and decide which lang their substituted
|
|
152
|
+
* {{OUTPUT_LANGUAGE_INSTRUCTION}} block actually matches. Returns the
|
|
153
|
+
* detected lang if it differs from `expected`, or null if they agree (or if
|
|
154
|
+
* we can't tell โ e.g. no master files, or no language section).
|
|
155
|
+
*/
|
|
156
|
+
function detectLangDrift(md, expected) {
|
|
157
|
+
if (!existsSync(md))
|
|
158
|
+
return null;
|
|
159
|
+
const langs = ["en", "ko", "ja"];
|
|
160
|
+
const entries = readdirSync(md, { withFileTypes: true })
|
|
161
|
+
.filter((e) => e.isFile() && e.name.endsWith(".md"))
|
|
162
|
+
.map((e) => e.name)
|
|
163
|
+
.slice(0, 3); // sample a few; checking all 9 would be wasteful
|
|
164
|
+
if (entries.length === 0)
|
|
165
|
+
return null;
|
|
166
|
+
for (const file of entries) {
|
|
167
|
+
let body;
|
|
168
|
+
try {
|
|
169
|
+
body = readFileSync(join(md, file), "utf8");
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
for (const l of langs) {
|
|
175
|
+
if (body.includes(LANG_INSTRUCTIONS[l])) {
|
|
176
|
+
return l === expected ? null : l;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
function countSkills(agent, dir) {
|
|
183
|
+
const ids = new Set();
|
|
184
|
+
if (!existsSync(dir))
|
|
185
|
+
return ids;
|
|
186
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
187
|
+
for (const e of entries) {
|
|
188
|
+
if (agent === "claude" && e.isDirectory())
|
|
189
|
+
ids.add(e.name);
|
|
190
|
+
else if (agent === "cursor" && e.isFile() && e.name.endsWith(".mdc"))
|
|
191
|
+
ids.add(e.name.slice(0, -4));
|
|
192
|
+
else if (agent === "codex" && e.isFile() && e.name.endsWith(".md"))
|
|
193
|
+
ids.add(e.name.slice(0, -3));
|
|
194
|
+
}
|
|
195
|
+
return ids;
|
|
196
|
+
}
|
|
197
|
+
function printToolsSection() {
|
|
198
|
+
console.log(`\n${BOLD}External tools${RESET}`);
|
|
199
|
+
printTool("git", ["--version"], "required for worktrees + history");
|
|
200
|
+
printTool("gh", ["--version"], "needed by pr-create / pr-review-analyze / incident-context");
|
|
201
|
+
const editor = process.env["VISUAL"] || process.env["EDITOR"];
|
|
202
|
+
if (editor) {
|
|
203
|
+
console.log(` ${OK} $EDITOR ${DIM}${editor}${RESET}`);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
console.log(` ${WARN} $EDITOR not set ${DIM}โ ${CYAN}agentforge add-skill${RESET}${DIM} falls back to vim/nano${RESET}`);
|
|
207
|
+
}
|
|
208
|
+
console.log(` ${OK} node ${DIM}${process.version}${RESET}\n`);
|
|
209
|
+
}
|
|
210
|
+
function printTool(cmd, args, purpose) {
|
|
211
|
+
try {
|
|
212
|
+
const out = execSync(`${cmd} ${args.join(" ")}`, {
|
|
213
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
214
|
+
})
|
|
215
|
+
.toString()
|
|
216
|
+
.trim()
|
|
217
|
+
.split("\n")[0];
|
|
218
|
+
console.log(` ${OK} ${cmd} ${DIM}${out}${RESET}`);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
console.log(` ${FAIL} ${cmd} not found ${DIM}โ ${purpose}${RESET}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
package/dist/enter.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { configPath } from "./agentforge-config.js";
|
|
5
|
+
const DIM = "\x1b[2m";
|
|
6
|
+
const CYAN = "\x1b[36m";
|
|
7
|
+
const YELLOW = "\x1b[33m";
|
|
8
|
+
const RED = "\x1b[31m";
|
|
9
|
+
const RESET = "\x1b[0m";
|
|
10
|
+
export async function runEnter(opts) {
|
|
11
|
+
const root = findWorkspaceRoot(process.cwd());
|
|
12
|
+
if (!root) {
|
|
13
|
+
process.stderr.write(`\n${YELLOW}โ ${RESET} Not inside an agentforge workspace.\n` +
|
|
14
|
+
` ${DIM}cwd: ${process.cwd()}${RESET}\n\n` +
|
|
15
|
+
` Run ${CYAN}agentforge enter${RESET} from a workspace directory (one containing ${CYAN}agentforge/config.json${RESET}).\n\n`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const anvilDir = join(root, "anvil");
|
|
19
|
+
const features = listFeatures(anvilDir);
|
|
20
|
+
if (!opts.slug) {
|
|
21
|
+
if (features.length === 0) {
|
|
22
|
+
process.stderr.write(`\n${YELLOW}โ ${RESET} No active features in ${DIM}${anvilDir}${RESET}.\n\n` +
|
|
23
|
+
` Start one with ${CYAN}claude${RESET} โ "let's start a new feature".\n\n`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
process.stderr.write(`\nusage: ${CYAN}agentforge enter <slug>${RESET}\n\nActive features:\n` +
|
|
27
|
+
features.map((f) => ` ${CYAN}${f}${RESET}`).join("\n") +
|
|
28
|
+
`\n\n`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const target = join(anvilDir, opts.slug);
|
|
32
|
+
if (!existsSync(target) || !statSync(target).isDirectory()) {
|
|
33
|
+
process.stderr.write(`\n${RED}โ${RESET} Feature not found: ${CYAN}${opts.slug}${RESET}\n` +
|
|
34
|
+
` ${DIM}expected: ${target}${RESET}\n\n`);
|
|
35
|
+
if (features.length > 0) {
|
|
36
|
+
process.stderr.write(`Active features:\n` +
|
|
37
|
+
features.map((f) => ` ${CYAN}${f}${RESET}`).join("\n") +
|
|
38
|
+
`\n\n`);
|
|
39
|
+
}
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
process.stdout.write(`${DIM}โ ${target}${RESET}\n${DIM}โ launching claudeโฆ${RESET}\n`);
|
|
43
|
+
const child = spawn("claude", [], { cwd: target, stdio: "inherit" });
|
|
44
|
+
child.on("error", (err) => {
|
|
45
|
+
const e = err;
|
|
46
|
+
if (e.code === "ENOENT") {
|
|
47
|
+
process.stderr.write(`\n${RED}โ${RESET} ${CYAN}claude${RESET} command not found on PATH.\n\n` +
|
|
48
|
+
` Install Claude Code: ${CYAN}https://claude.com/claude-code${RESET}\n\n`);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
process.stderr.write(`\n${RED}โ${RESET} failed to launch claude: ${e.message}\n\n`);
|
|
52
|
+
}
|
|
53
|
+
process.exit(1);
|
|
54
|
+
});
|
|
55
|
+
child.on("exit", (code, signal) => {
|
|
56
|
+
if (signal)
|
|
57
|
+
process.kill(process.pid, signal);
|
|
58
|
+
else
|
|
59
|
+
process.exit(code ?? 0);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function findWorkspaceRoot(start) {
|
|
63
|
+
let dir = resolve(start);
|
|
64
|
+
while (true) {
|
|
65
|
+
if (existsSync(configPath(dir)))
|
|
66
|
+
return dir;
|
|
67
|
+
const parent = dirname(dir);
|
|
68
|
+
if (parent === dir)
|
|
69
|
+
return null;
|
|
70
|
+
dir = parent;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function listFeatures(anvilDir) {
|
|
74
|
+
if (!existsSync(anvilDir))
|
|
75
|
+
return [];
|
|
76
|
+
try {
|
|
77
|
+
return readdirSync(anvilDir, { withFileTypes: true })
|
|
78
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith("."))
|
|
79
|
+
.map((e) => e.name)
|
|
80
|
+
.sort();
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { pickAgents } from "./agent-prompt.js";
|
|
4
|
+
import { ensureDir, readMasterDir, renderTemplate, setVerbose, writeRendered, } from "./agents/io.js";
|
|
5
|
+
import { getAgent } from "./agents/index.js";
|
|
6
|
+
import { configPath, masterDir, upsertConfig } from "./agentforge-config.js";
|
|
7
|
+
import { pickLanguage } from "./lang-prompt.js";
|
|
8
|
+
import { printLogo } from "./logo.js";
|
|
9
|
+
import { promptPath } from "./path-prompt.js";
|
|
10
|
+
import { pickSkills } from "./skill-prompt.js";
|
|
11
|
+
import { LANG_LABEL, SKILLS } from "./skills-data.js";
|
|
12
|
+
const BASE_DIRS = ["repos", "anvil", "artifacts"];
|
|
13
|
+
const DIM = "\x1b[2m";
|
|
14
|
+
const GREEN = "\x1b[32m";
|
|
15
|
+
const YELLOW = "\x1b[33m";
|
|
16
|
+
const CYAN = "\x1b[36m";
|
|
17
|
+
const BOLD = "\x1b[1m";
|
|
18
|
+
const RESET = "\x1b[0m";
|
|
19
|
+
export async function runInit(opts) {
|
|
20
|
+
// Pre-flight validation (before logo / prompts) for the explicit-path case.
|
|
21
|
+
if (opts.pathArg) {
|
|
22
|
+
const targetAbs = resolve(opts.pathArg);
|
|
23
|
+
// (a) refuse filesystem root โ would try to mkdir /repos, /anvil etc.
|
|
24
|
+
if (targetAbs === dirname(targetAbs)) {
|
|
25
|
+
process.stderr.write(`\nerror: ${targetAbs} is the filesystem root โ pick a real directory to host the workspace.\n\n`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
// (b) refuse if target sits inside another agentforge workspace โ
|
|
29
|
+
// workspaces shouldn't nest (repos/ is meant for git checkouts).
|
|
30
|
+
if (!opts.forceSkills && !opts.forceClaude) {
|
|
31
|
+
const outer = findEnclosingWorkspace(targetAbs);
|
|
32
|
+
if (outer && outer !== targetAbs) {
|
|
33
|
+
process.stderr.write(`\n${YELLOW}โ ${RESET} ${targetAbs} sits inside another agentforge workspace:\n` +
|
|
34
|
+
` ${DIM}${outer}${RESET}\n\n` +
|
|
35
|
+
` Workspaces shouldn't nest. ${DIM}repos/${RESET} is meant for ${BOLD}git checkouts${RESET}, not nested workspaces.\n` +
|
|
36
|
+
` Pick a path outside ${DIM}${outer}${RESET}, or pass ${CYAN}--force${RESET} to override.\n\n`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// (c) already-initialized? โ block re-init (drift trap from earlier).
|
|
41
|
+
if (!opts.forceSkills && !opts.forceClaude && existsSync(configPath(targetAbs))) {
|
|
42
|
+
printAlreadyInitialized(targetAbs);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
// (d) dir exists and has unrelated content โ refuse before scrambling
|
|
46
|
+
// workspace files in among the user's project.
|
|
47
|
+
if (!opts.forceSkills && !opts.forceClaude) {
|
|
48
|
+
const intruders = unexpectedEntries(targetAbs);
|
|
49
|
+
if (intruders.length > 0) {
|
|
50
|
+
printNotEmpty(targetAbs, intruders);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
printLogo();
|
|
56
|
+
const targetPath = await resolveTargetPath(opts);
|
|
57
|
+
const targetAbs = resolve(targetPath);
|
|
58
|
+
// Same checks for the interactive path (no --path arg given).
|
|
59
|
+
if (!opts.forceSkills &&
|
|
60
|
+
!opts.forceClaude &&
|
|
61
|
+
existsSync(configPath(targetAbs))) {
|
|
62
|
+
printAlreadyInitialized(targetAbs);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
if (!opts.forceSkills && !opts.forceClaude) {
|
|
66
|
+
const intruders = unexpectedEntries(targetAbs);
|
|
67
|
+
if (intruders.length > 0) {
|
|
68
|
+
printNotEmpty(targetAbs, intruders);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const agentIds = await resolveAgents(opts);
|
|
73
|
+
const lang = await resolveLanguage(opts);
|
|
74
|
+
const selectedSkills = await selectSkills(opts, lang);
|
|
75
|
+
const root = resolve(targetPath);
|
|
76
|
+
if (!existsSync(root)) {
|
|
77
|
+
mkdirSync(root, { recursive: true });
|
|
78
|
+
}
|
|
79
|
+
// Workspace skeleton โ quiet, one summary line.
|
|
80
|
+
setVerbose(false);
|
|
81
|
+
for (const dir of BASE_DIRS) {
|
|
82
|
+
ensureDir(join(root, dir), dir);
|
|
83
|
+
}
|
|
84
|
+
ensureDir(masterDir(root), "agentforge/skills");
|
|
85
|
+
setVerbose(true);
|
|
86
|
+
line(`${GREEN}+${RESET} workspace skeleton ${DIM}(${BASE_DIRS.join("/, ")}/, agentforge/)${RESET}`);
|
|
87
|
+
// Master skills โ quiet writes, one summary line.
|
|
88
|
+
setVerbose(false);
|
|
89
|
+
let written = 0;
|
|
90
|
+
for (const s of selectedSkills) {
|
|
91
|
+
const rendered = renderTemplate(s.template, lang);
|
|
92
|
+
const destAbs = join(masterDir(root), `${s.id}.md`);
|
|
93
|
+
const destRel = `agentforge/skills/${s.id}.md`;
|
|
94
|
+
writeRendered(destAbs, destRel, rendered, opts.forceSkills);
|
|
95
|
+
written++;
|
|
96
|
+
}
|
|
97
|
+
upsertConfig(root, { version: 1, lang, agents: agentIds });
|
|
98
|
+
setVerbose(true);
|
|
99
|
+
line(`${GREEN}+${RESET} master skills ${DIM}(${written} skill${written === 1 ? "" : "s"} โ agentforge/skills/, + agentforge/config.json)${RESET}`);
|
|
100
|
+
// Adapter installs โ quiet, one line per agent.
|
|
101
|
+
const { skills: masterSkills, skipped } = readMasterDir(masterDir(root));
|
|
102
|
+
if (skipped.length > 0) {
|
|
103
|
+
for (const sk of skipped) {
|
|
104
|
+
line(` ${DIM}skipped master file ${sk.file} โ ${sk.reason}${RESET}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
setVerbose(false);
|
|
108
|
+
try {
|
|
109
|
+
for (const id of agentIds) {
|
|
110
|
+
const adapter = getAgent(id);
|
|
111
|
+
adapter.install({
|
|
112
|
+
root,
|
|
113
|
+
masterSkills,
|
|
114
|
+
skillCatalog: SKILLS.slice(),
|
|
115
|
+
lang,
|
|
116
|
+
forceSkills: opts.forceSkills,
|
|
117
|
+
forceClaude: opts.forceClaude,
|
|
118
|
+
});
|
|
119
|
+
line(`${GREEN}+${RESET} ${adapter.label.padEnd(18)} ${DIM}(${adapter.outputSummary})${RESET}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
setVerbose(true);
|
|
124
|
+
}
|
|
125
|
+
printNextSteps(root, agentIds, lang);
|
|
126
|
+
}
|
|
127
|
+
async function resolveTargetPath(opts) {
|
|
128
|
+
if (opts.pathArg)
|
|
129
|
+
return opts.pathArg;
|
|
130
|
+
if (opts.yes)
|
|
131
|
+
return process.cwd();
|
|
132
|
+
return promptPath("Workspace directory", process.cwd());
|
|
133
|
+
}
|
|
134
|
+
async function resolveAgents(opts) {
|
|
135
|
+
if (opts.agents && opts.agents.length > 0)
|
|
136
|
+
return opts.agents;
|
|
137
|
+
if (opts.yes)
|
|
138
|
+
return ["claude"];
|
|
139
|
+
// requireAtLeastOne: interactive picker won't return [] โ it re-prompts.
|
|
140
|
+
return pickAgents(opts.lang ?? "en", { requireAtLeastOne: true });
|
|
141
|
+
}
|
|
142
|
+
async function resolveLanguage(opts) {
|
|
143
|
+
if (opts.lang)
|
|
144
|
+
return opts.lang;
|
|
145
|
+
if (opts.yes)
|
|
146
|
+
return "en";
|
|
147
|
+
return pickLanguage();
|
|
148
|
+
}
|
|
149
|
+
async function selectSkills(opts, lang) {
|
|
150
|
+
if (opts.yes)
|
|
151
|
+
return SKILLS.slice();
|
|
152
|
+
const ids = await pickSkills(SKILLS.map((s) => ({
|
|
153
|
+
id: s.id,
|
|
154
|
+
title: s.title,
|
|
155
|
+
description: s.description[lang],
|
|
156
|
+
details: s.details[lang],
|
|
157
|
+
})));
|
|
158
|
+
return SKILLS.filter((s) => ids.includes(s.id));
|
|
159
|
+
}
|
|
160
|
+
function printNextSteps(root, agentIds, lang) {
|
|
161
|
+
const repos = join(root, "repos");
|
|
162
|
+
const repoCount = existsSync(repos)
|
|
163
|
+
? readdirSync(repos, { withFileTypes: true }).filter((d) => d.isDirectory())
|
|
164
|
+
.length
|
|
165
|
+
: 0;
|
|
166
|
+
console.log("");
|
|
167
|
+
console.log(`${BOLD}${GREEN}โ${RESET} workspace ready at ${CYAN}${root}${RESET} ${DIM}(lang: ${LANG_LABEL[lang]})${RESET}`);
|
|
168
|
+
console.log("");
|
|
169
|
+
console.log(`${BOLD}Next steps:${RESET}`);
|
|
170
|
+
if (repoCount === 0) {
|
|
171
|
+
console.log(` cd ${root}/repos && git clone <repo-url> ${DIM}# add repos to work with${RESET}`);
|
|
172
|
+
console.log(` cd ${root} && ${launchHint(agentIds)} ${DIM}# launch your agent${RESET}`);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
console.log(` ${DIM}(found ${repoCount} repo${repoCount === 1 ? "" : "s"} in repos/)${RESET}`);
|
|
176
|
+
console.log(` cd ${root} && ${launchHint(agentIds)}`);
|
|
177
|
+
}
|
|
178
|
+
console.log("");
|
|
179
|
+
console.log(`${BOLD}Useful commands:${RESET}`);
|
|
180
|
+
console.log(` ${CYAN}agentforge list-skills${RESET} ${DIM}# see what's installed${RESET}`);
|
|
181
|
+
console.log(` ${CYAN}agentforge add-skill${RESET} ${DIM}# create your own skill${RESET}`);
|
|
182
|
+
console.log(` ${CYAN}agentforge sync-skills${RESET} ${DIM}# propagate master edits to all agents${RESET}`);
|
|
183
|
+
console.log(` ${CYAN}agentforge add-agent${RESET} ${DIM}# add another agent later${RESET}`);
|
|
184
|
+
console.log("");
|
|
185
|
+
}
|
|
186
|
+
function launchHint(agentIds) {
|
|
187
|
+
const cmds = agentIds.map((id) => {
|
|
188
|
+
switch (id) {
|
|
189
|
+
case "claude":
|
|
190
|
+
return "claude";
|
|
191
|
+
case "cursor":
|
|
192
|
+
return "cursor .";
|
|
193
|
+
case "codex":
|
|
194
|
+
return "codex";
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
return cmds.join(" / ");
|
|
198
|
+
}
|
|
199
|
+
function line(s) {
|
|
200
|
+
console.log(s);
|
|
201
|
+
}
|
|
202
|
+
/** Files / dirs that agentforge owns at the workspace root. Anything else
|
|
203
|
+
* in the target dir means the user picked a path that's already in use. */
|
|
204
|
+
const WORKSPACE_ENTRIES = new Set([
|
|
205
|
+
...BASE_DIRS,
|
|
206
|
+
"agentforge",
|
|
207
|
+
".claude",
|
|
208
|
+
".cursor",
|
|
209
|
+
".agents",
|
|
210
|
+
"CLAUDE.md",
|
|
211
|
+
"AGENTS.md",
|
|
212
|
+
".cursorrules",
|
|
213
|
+
]);
|
|
214
|
+
/**
|
|
215
|
+
* Return entries in `dir` that don't belong to agentforge. Dotfiles other
|
|
216
|
+
* than agentforge's own (`.claude`, `.cursor`, `.agents`, `.cursorrules`) are
|
|
217
|
+
* surfaced too โ `.git/`, `.env`, IDE folders etc. mean this is already
|
|
218
|
+
* someone's project.
|
|
219
|
+
*/
|
|
220
|
+
function unexpectedEntries(dir) {
|
|
221
|
+
if (!existsSync(dir))
|
|
222
|
+
return [];
|
|
223
|
+
let entries;
|
|
224
|
+
try {
|
|
225
|
+
entries = readdirSync(dir);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
return entries
|
|
231
|
+
.filter((name) => name !== ".DS_Store") // macOS OS noise
|
|
232
|
+
.filter((name) => !WORKSPACE_ENTRIES.has(name))
|
|
233
|
+
.sort();
|
|
234
|
+
}
|
|
235
|
+
function printNotEmpty(targetAbs, intruders) {
|
|
236
|
+
const preview = intruders.slice(0, 6);
|
|
237
|
+
const extra = intruders.length - preview.length;
|
|
238
|
+
const list = preview
|
|
239
|
+
.map((n) => ` ${DIM}-${RESET} ${n}`)
|
|
240
|
+
.join("\n");
|
|
241
|
+
const more = extra > 0 ? `\n ${DIM}โฆ and ${extra} more${RESET}` : "";
|
|
242
|
+
process.stderr.write(`\n${YELLOW}โ ${RESET} ${targetAbs} isn't empty.\n\n` +
|
|
243
|
+
` It contains files that don't look like agentforge:\n${list}${more}\n\n` +
|
|
244
|
+
` agentforge would overlay its workspace structure (${DIM}repos/, anvil/, artifacts/, agentforge/${RESET}) on top.\n` +
|
|
245
|
+
` Pick an empty directory, or pass ${CYAN}--force${RESET} to proceed anyway (existing files are not touched, only the workspace structure is added).\n\n`);
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Walk up from `start` looking for a parent directory that has
|
|
249
|
+
* `agentforge/config.json`. Returns the workspace root if found, else null.
|
|
250
|
+
* Returns `start` itself if `start` is already a workspace.
|
|
251
|
+
*/
|
|
252
|
+
function findEnclosingWorkspace(start) {
|
|
253
|
+
let cur = start;
|
|
254
|
+
while (true) {
|
|
255
|
+
if (existsSync(configPath(cur)))
|
|
256
|
+
return cur;
|
|
257
|
+
const parent = dirname(cur);
|
|
258
|
+
if (parent === cur)
|
|
259
|
+
return null; // reached FS root
|
|
260
|
+
cur = parent;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function printAlreadyInitialized(targetAbs) {
|
|
264
|
+
process.stderr.write(`\n${YELLOW}โ ${RESET} ${targetAbs} is already an agentforge workspace.\n\n` +
|
|
265
|
+
` Use one of these instead:\n` +
|
|
266
|
+
` ${CYAN}agentforge add-agent${RESET} ${DIM}# install another agent (Claude / Cursor / Codex)${RESET}\n` +
|
|
267
|
+
` ${CYAN}agentforge add-skill${RESET} ${DIM}# create a new skill${RESET}\n` +
|
|
268
|
+
` ${CYAN}agentforge sync-skills${RESET} ${DIM}# propagate master skill edits to every agent${RESET}\n` +
|
|
269
|
+
` ${CYAN}agentforge list-skills${RESET} ${DIM}# see what's installed${RESET}\n` +
|
|
270
|
+
` ${CYAN}agentforge doctor${RESET} ${DIM}# diagnose the workspace${RESET}\n\n` +
|
|
271
|
+
` Or pass ${CYAN}--force${RESET} to overwrite everything (existing files are backed up to .bak).\n\n`);
|
|
272
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const CYAN = "\x1b[36m";
|
|
2
|
+
const DIM = "\x1b[2m";
|
|
3
|
+
const BOLD = "\x1b[1m";
|
|
4
|
+
const YELLOW = "\x1b[33m";
|
|
5
|
+
const RESET = "\x1b[0m";
|
|
6
|
+
const OPTIONS = [
|
|
7
|
+
{ value: "en", label: "English", hint: "" },
|
|
8
|
+
{ value: "ko", label: "ํ๊ตญ์ด", hint: "Korean" },
|
|
9
|
+
{ value: "ja", label: "ๆฅๆฌ่ช", hint: "Japanese" },
|
|
10
|
+
];
|
|
11
|
+
async function readKey() {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
process.stdin.once("data", (chunk) => {
|
|
14
|
+
resolve(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export async function pickLanguage() {
|
|
19
|
+
if (!process.stdin.isTTY)
|
|
20
|
+
return "en";
|
|
21
|
+
let cursor = 0;
|
|
22
|
+
let lastLineCount = 0;
|
|
23
|
+
const HEADER = `${CYAN}?${RESET} ${BOLD}Language${RESET} ${DIM}โโ move ยท enter confirm${RESET}`;
|
|
24
|
+
const render = () => {
|
|
25
|
+
if (lastLineCount > 0) {
|
|
26
|
+
process.stdout.write(`\x1b[${lastLineCount}A\x1b[J`);
|
|
27
|
+
}
|
|
28
|
+
const lines = [HEADER, ""];
|
|
29
|
+
for (let i = 0; i < OPTIONS.length; i++) {
|
|
30
|
+
const o = OPTIONS[i];
|
|
31
|
+
const prefix = i === cursor ? `${CYAN}โฏ${RESET}` : " ";
|
|
32
|
+
const label = i === cursor ? `${CYAN}${o.label}${RESET}` : o.label;
|
|
33
|
+
const hint = o.hint ? ` ${DIM}โ ${o.hint}${RESET}` : "";
|
|
34
|
+
lines.push(`${prefix} ${label}${hint}`);
|
|
35
|
+
}
|
|
36
|
+
const text = lines.join("\n") + "\n";
|
|
37
|
+
process.stdout.write(text);
|
|
38
|
+
lastLineCount = text.split("\n").length - 1;
|
|
39
|
+
};
|
|
40
|
+
const stdin = process.stdin;
|
|
41
|
+
stdin.setRawMode(true);
|
|
42
|
+
stdin.resume();
|
|
43
|
+
stdin.setEncoding("utf8");
|
|
44
|
+
process.stdout.write("\x1b[?25l"); // hide cursor
|
|
45
|
+
const cleanup = (clearUi) => {
|
|
46
|
+
process.stdout.write("\x1b[?25h"); // show cursor
|
|
47
|
+
stdin.setRawMode(false);
|
|
48
|
+
stdin.pause();
|
|
49
|
+
if (clearUi && lastLineCount > 0) {
|
|
50
|
+
process.stdout.write(`\x1b[${lastLineCount}A\x1b[J`);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
render();
|
|
54
|
+
try {
|
|
55
|
+
while (true) {
|
|
56
|
+
const chunk = await readKey();
|
|
57
|
+
if (chunk === "\x1b[A") {
|
|
58
|
+
cursor = (cursor - 1 + OPTIONS.length) % OPTIONS.length;
|
|
59
|
+
render();
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (chunk === "\x1b[B") {
|
|
63
|
+
cursor = (cursor + 1) % OPTIONS.length;
|
|
64
|
+
render();
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (chunk.startsWith("\x1b"))
|
|
68
|
+
continue;
|
|
69
|
+
for (const ch of chunk) {
|
|
70
|
+
if (ch === "\x03") {
|
|
71
|
+
cleanup(true);
|
|
72
|
+
process.stdout.write(`${YELLOW}aborted.${RESET}\n`);
|
|
73
|
+
process.exit(130);
|
|
74
|
+
}
|
|
75
|
+
if (ch === "\r" || ch === "\n") {
|
|
76
|
+
const picked = OPTIONS[cursor];
|
|
77
|
+
cleanup(true);
|
|
78
|
+
process.stdout.write(`${CYAN}?${RESET} ${BOLD}Language${RESET} ${DIM}โบ${RESET} ${picked.label}\n`);
|
|
79
|
+
return picked.value;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
cleanup(true);
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
}
|