@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
package/dist/rename.js ADDED
@@ -0,0 +1,222 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmdirSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { configPath, requireWorkspace } from "./agentforge-config.js";
5
+ const BOLD = "\x1b[1m";
6
+ const DIM = "\x1b[2m";
7
+ const CYAN = "\x1b[36m";
8
+ const GREEN = "\x1b[32m";
9
+ const YELLOW = "\x1b[33m";
10
+ const RED = "\x1b[31m";
11
+ const RESET = "\x1b[0m";
12
+ const SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
13
+ export async function runRename(opts) {
14
+ if (!opts.oldSlug || !opts.newSlug) {
15
+ fail(`usage: ${CYAN}agentforge rename <old-slug> <new-slug>${RESET}\n`);
16
+ }
17
+ if (opts.oldSlug === opts.newSlug) {
18
+ fail(`old and new slug are the same: ${opts.oldSlug}\n`);
19
+ }
20
+ if (!SLUG_RE.test(opts.newSlug)) {
21
+ fail(`invalid new slug: ${opts.newSlug}\n must match /^[a-z0-9][a-z0-9-]*$/ (lowercase, digits, hyphens; start with letter/digit)\n`);
22
+ }
23
+ const root = findWorkspaceRoot(opts.pathArg ?? process.cwd());
24
+ if (!root) {
25
+ fail(`Not inside an agentforge workspace (looked for ${CYAN}agentforge/config.json${RESET} upward from cwd).\n`);
26
+ }
27
+ // Make sure config is valid — also gives a friendly error if not initialized.
28
+ requireWorkspace(root);
29
+ const oldDir = join(root, "anvil", opts.oldSlug);
30
+ const newDir = join(root, "anvil", opts.newSlug);
31
+ if (!existsSync(oldDir) || !statSync(oldDir).isDirectory()) {
32
+ fail(`feature not found: ${CYAN}${opts.oldSlug}${RESET}\n expected: ${oldDir}\n`);
33
+ }
34
+ if (existsSync(newDir)) {
35
+ fail(`target already exists: ${CYAN}${opts.newSlug}${RESET}\n ${newDir}\n Pick a different new slug.\n`);
36
+ }
37
+ // Discover worktrees inside the feature
38
+ const repos = readdirSync(oldDir, { withFileTypes: true })
39
+ .filter((e) => e.isDirectory() && !e.name.startsWith("."))
40
+ .map((e) => e.name)
41
+ .sort();
42
+ console.log(`${BOLD}${CYAN}▸${RESET} renaming feature ${CYAN}${opts.oldSlug}${RESET} → ${CYAN}${opts.newSlug}${RESET}`);
43
+ console.log(` ${DIM}root: ${root}${RESET}`);
44
+ console.log(` ${DIM}worktrees: ${repos.length}${RESET}`);
45
+ console.log("");
46
+ const plan = [];
47
+ for (const repo of repos) {
48
+ const wt = join(oldDir, repo);
49
+ let currentBranch = "";
50
+ let dirty = false;
51
+ try {
52
+ currentBranch = execGit(wt, ["rev-parse", "--abbrev-ref", "HEAD"]).trim();
53
+ }
54
+ catch {
55
+ console.log(` ${YELLOW}skip:${RESET} ${repo} — not a git worktree (or git unavailable)`);
56
+ continue;
57
+ }
58
+ try {
59
+ const status = execGit(wt, ["status", "--porcelain"]);
60
+ dirty = status.trim().length > 0;
61
+ }
62
+ catch {
63
+ // treat unreadable as dirty for safety
64
+ dirty = true;
65
+ }
66
+ const canonical = resolveCanonical(root, repo, wt);
67
+ plan.push({
68
+ repo,
69
+ canonical,
70
+ currentBranch,
71
+ dirty,
72
+ renameBranch: currentBranch === opts.oldSlug,
73
+ });
74
+ console.log(` ${repo} ${DIM}branch=${currentBranch}${RESET}` +
75
+ (dirty ? ` ${YELLOW}(dirty)${RESET}` : "") +
76
+ (currentBranch === opts.oldSlug
77
+ ? ` ${DIM}→ branch will be renamed to ${opts.newSlug}${RESET}`
78
+ : ` ${DIM}→ branch stays (per-repo convention)${RESET}`));
79
+ }
80
+ console.log("");
81
+ // Refuse on dirty unless --force
82
+ const dirtyRepos = plan.filter((p) => p.dirty);
83
+ if (dirtyRepos.length > 0 && !opts.force) {
84
+ fail(`${dirtyRepos.length} worktree(s) have uncommitted changes:\n` +
85
+ dirtyRepos.map((p) => ` - ${p.repo}`).join("\n") +
86
+ `\n\nCommit/stash first, or pass ${CYAN}--force${RESET} to proceed anyway.\n`);
87
+ }
88
+ // Confirm before destructive action unless --yes
89
+ if (!opts.yes) {
90
+ console.log(`${YELLOW}!${RESET} This will:\n` +
91
+ ` • move ${plan.length} worktree dir(s) anvil/${opts.oldSlug}/… → anvil/${opts.newSlug}/…\n` +
92
+ ` • rename ${plan.filter((p) => p.renameBranch).length} branch(es) named "${opts.oldSlug}" to "${opts.newSlug}"\n` +
93
+ ` • move anvil/${opts.oldSlug}/CLAUDE.md → anvil/${opts.newSlug}/CLAUDE.md (and rewrite slug references inside)\n` +
94
+ ` • delete the now-empty anvil/${opts.oldSlug}/\n` +
95
+ `\nRe-run with ${CYAN}--yes${RESET} to proceed.\n`);
96
+ process.exit(0);
97
+ }
98
+ // Create the new feature dir
99
+ mkdirSync(newDir, { recursive: true });
100
+ // Move each worktree
101
+ for (const p of plan) {
102
+ const fromWt = join(oldDir, p.repo);
103
+ const toWt = join(newDir, p.repo);
104
+ try {
105
+ execGit(p.canonical, ["worktree", "move", fromWt, toWt]);
106
+ console.log(`${GREEN}+${RESET} moved worktree: anvil/${opts.oldSlug}/${p.repo} → anvil/${opts.newSlug}/${p.repo}`);
107
+ }
108
+ catch (err) {
109
+ fail(`failed to move worktree for ${p.repo}: ${err.message}\n` +
110
+ ` Some worktrees may have already moved — inspect anvil/ and re-run after cleanup.\n`);
111
+ }
112
+ if (p.renameBranch) {
113
+ try {
114
+ execGit(toWt, ["branch", "-m", opts.oldSlug, opts.newSlug]);
115
+ console.log(`${GREEN}+${RESET} renamed branch: ${p.repo} ${opts.oldSlug} → ${opts.newSlug}`);
116
+ }
117
+ catch (err) {
118
+ console.log(`${YELLOW}!${RESET} branch rename failed for ${p.repo}: ${err.message} (continuing)`);
119
+ }
120
+ }
121
+ }
122
+ // Move CLAUDE.md + rewrite slug references inside
123
+ const oldClaudeMd = join(oldDir, "CLAUDE.md");
124
+ const newClaudeMd = join(newDir, "CLAUDE.md");
125
+ if (existsSync(oldClaudeMd)) {
126
+ let body = readFileSync(oldClaudeMd, "utf8");
127
+ body = body.split(opts.oldSlug).join(opts.newSlug);
128
+ writeFileSync(newClaudeMd, body);
129
+ unlinkSync(oldClaudeMd);
130
+ console.log(`${GREEN}+${RESET} moved CLAUDE.md (and rewrote ${opts.oldSlug} → ${opts.newSlug} inside)`);
131
+ }
132
+ // Move any other top-level files (HANDOFF.md, PLAN.md, etc.) and remove the
133
+ // lock file
134
+ for (const entry of readdirSync(oldDir, { withFileTypes: true })) {
135
+ const src = join(oldDir, entry.name);
136
+ if (entry.name === ".agentforge.lock") {
137
+ try {
138
+ unlinkSync(src);
139
+ }
140
+ catch {
141
+ /* ignore */
142
+ }
143
+ continue;
144
+ }
145
+ if (entry.isFile()) {
146
+ const dst = join(newDir, entry.name);
147
+ let content = readFileSync(src, "utf8");
148
+ content = content.split(opts.oldSlug).join(opts.newSlug);
149
+ writeFileSync(dst, content);
150
+ unlinkSync(src);
151
+ console.log(`${GREEN}+${RESET} moved ${entry.name}`);
152
+ }
153
+ }
154
+ // Remove old dir if now empty
155
+ try {
156
+ rmdirSync(oldDir);
157
+ console.log(`${GREEN}+${RESET} removed empty anvil/${opts.oldSlug}/`);
158
+ }
159
+ catch (err) {
160
+ console.log(`${YELLOW}!${RESET} could not remove anvil/${opts.oldSlug}/ — inspect manually: ${err.message}`);
161
+ }
162
+ // Append to activity log
163
+ appendLog(root, {
164
+ skill: "agentforge-rename",
165
+ action: "renamed",
166
+ oldSlug: opts.oldSlug,
167
+ newSlug: opts.newSlug,
168
+ repos: plan.map((p) => p.repo).join(","),
169
+ });
170
+ console.log("");
171
+ console.log(`${BOLD}${GREEN}✓${RESET} rename complete`);
172
+ console.log(` ${DIM}next: cd anvil/${opts.newSlug}/${RESET}`);
173
+ }
174
+ function execGit(cwd, args) {
175
+ return execFileSync("git", args, { cwd, encoding: "utf8" });
176
+ }
177
+ function resolveCanonical(root, repo, worktree) {
178
+ // Prefer repos/<repo> if it exists; otherwise ask git for the main worktree
179
+ const candidate = join(root, "repos", repo);
180
+ if (existsSync(candidate))
181
+ return candidate;
182
+ try {
183
+ const out = execGit(worktree, ["worktree", "list", "--porcelain"]);
184
+ // The first "worktree <path>" line is the main worktree
185
+ const m = out.match(/^worktree (.+)$/m);
186
+ if (m)
187
+ return m[1];
188
+ }
189
+ catch {
190
+ /* ignore */
191
+ }
192
+ return candidate; // best-effort; git worktree move will error if wrong
193
+ }
194
+ function findWorkspaceRoot(start) {
195
+ let dir = resolve(start);
196
+ while (true) {
197
+ if (existsSync(configPath(dir)))
198
+ return dir;
199
+ const parent = dirname(dir);
200
+ if (parent === dir)
201
+ return null;
202
+ dir = parent;
203
+ }
204
+ }
205
+ function appendLog(root, fields) {
206
+ const logPath = join(root, "agentforge", "log.jsonl");
207
+ mkdirSync(dirname(logPath), { recursive: true });
208
+ const entry = {
209
+ ts: new Date().toISOString(),
210
+ ...fields,
211
+ };
212
+ try {
213
+ writeFileSync(logPath, `${JSON.stringify(entry)}\n`, { flag: "a" });
214
+ }
215
+ catch {
216
+ /* non-fatal */
217
+ }
218
+ }
219
+ function fail(msg) {
220
+ process.stderr.write(`\n${RED}✗${RESET} ${msg}\n`);
221
+ process.exit(1);
222
+ }
@@ -0,0 +1,199 @@
1
+ const CYAN = "\x1b[36m";
2
+ const GREEN = "\x1b[32m";
3
+ const DIM = "\x1b[2m";
4
+ const BOLD = "\x1b[1m";
5
+ const YELLOW = "\x1b[33m";
6
+ const RESET = "\x1b[0m";
7
+ function wrap(text, width) {
8
+ const out = [];
9
+ for (const paragraph of text.split("\n")) {
10
+ if (paragraph === "") {
11
+ out.push("");
12
+ continue;
13
+ }
14
+ let line = "";
15
+ for (const word of paragraph.split(/\s+/)) {
16
+ if (line.length === 0) {
17
+ line = word;
18
+ }
19
+ else if (line.length + 1 + word.length <= width) {
20
+ line += " " + word;
21
+ }
22
+ else {
23
+ out.push(line);
24
+ line = word;
25
+ }
26
+ }
27
+ if (line)
28
+ out.push(line);
29
+ }
30
+ return out;
31
+ }
32
+ async function readKey() {
33
+ return new Promise((resolve) => {
34
+ process.stdin.once("data", (chunk) => {
35
+ resolve(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
36
+ });
37
+ });
38
+ }
39
+ export async function pickSkills(items) {
40
+ if (!process.stdin.isTTY) {
41
+ return items.map((i) => i.id);
42
+ }
43
+ let cursor = 0;
44
+ const selected = new Set(items.map((i) => i.id));
45
+ let view = "list";
46
+ let lastLineCount = 0;
47
+ const HEADER_LIST = `${CYAN}?${RESET} ${BOLD}Skills to install${RESET} ${DIM}↑↓ move · space toggle · → details · enter confirm${RESET}`;
48
+ const headerDetails = (title, idx, total) => `${CYAN}?${RESET} ${BOLD}${title}${RESET} ${DIM}(${idx}/${total}) ↑↓ next · ← back${RESET}`;
49
+ const renderList = () => {
50
+ const lines = [HEADER_LIST, ""];
51
+ for (let i = 0; i < items.length; i++) {
52
+ const item = items[i];
53
+ const isSel = selected.has(item.id);
54
+ const isCur = i === cursor;
55
+ const marker = isSel ? `${GREEN}◉${RESET}` : `${DIM}◯${RESET}`;
56
+ const prefix = isCur ? `${CYAN}❯${RESET}` : " ";
57
+ const title = isCur ? `${CYAN}${item.title}${RESET}` : item.title;
58
+ lines.push(`${prefix} ${marker} ${title} ${DIM}— ${item.description}${RESET}`);
59
+ }
60
+ return lines.join("\n") + "\n";
61
+ };
62
+ const renderDetails = () => {
63
+ const item = items[cursor];
64
+ const isSel = selected.has(item.id);
65
+ const status = isSel
66
+ ? `${GREEN}◉ selected${RESET}`
67
+ : `${DIM}◯ not selected${RESET}`;
68
+ const width = Math.min((process.stdout.columns || 80) - 4, 78);
69
+ const bar = `${DIM}${"─".repeat(width)}${RESET}`;
70
+ const lines = [
71
+ headerDetails(item.title, cursor + 1, items.length),
72
+ ` ${bar}`,
73
+ "",
74
+ ];
75
+ for (const w of wrap(item.details, width - 2)) {
76
+ lines.push(` ${w}`);
77
+ }
78
+ lines.push("");
79
+ lines.push(` ${status} ${DIM}space toggle · enter confirm${RESET}`);
80
+ return lines.join("\n") + "\n";
81
+ };
82
+ const clear = () => {
83
+ if (lastLineCount > 0) {
84
+ process.stdout.write(`\x1b[${lastLineCount}A\x1b[J`);
85
+ }
86
+ };
87
+ const render = () => {
88
+ clear();
89
+ const text = view === "list" ? renderList() : renderDetails();
90
+ process.stdout.write(text);
91
+ lastLineCount = text.split("\n").length - 1;
92
+ };
93
+ // Setup raw mode
94
+ const stdin = process.stdin;
95
+ stdin.setRawMode(true);
96
+ stdin.resume();
97
+ stdin.setEncoding("utf8");
98
+ process.stdout.write("\x1b[?25l"); // hide cursor
99
+ const cleanup = (clearUi) => {
100
+ process.stdout.write("\x1b[?25h"); // show cursor
101
+ stdin.setRawMode(false);
102
+ stdin.pause();
103
+ if (clearUi)
104
+ clear();
105
+ };
106
+ render();
107
+ try {
108
+ while (true) {
109
+ const chunk = await readKey();
110
+ // Whole-chunk escape sequences (arrow keys)
111
+ if (chunk === "\x1b[A") {
112
+ cursor = (cursor - 1 + items.length) % items.length;
113
+ render();
114
+ continue;
115
+ }
116
+ if (chunk === "\x1b[B") {
117
+ cursor = (cursor + 1) % items.length;
118
+ render();
119
+ continue;
120
+ }
121
+ if (chunk === "\x1b[C") {
122
+ if (view === "list") {
123
+ view = "details";
124
+ render();
125
+ }
126
+ continue;
127
+ }
128
+ if (chunk === "\x1b[D") {
129
+ if (view === "details") {
130
+ view = "list";
131
+ render();
132
+ }
133
+ continue;
134
+ }
135
+ if (chunk === "\x1b") {
136
+ // bare ESC
137
+ if (view === "details") {
138
+ view = "list";
139
+ render();
140
+ }
141
+ continue;
142
+ }
143
+ if (chunk.startsWith("\x1b")) {
144
+ continue; // ignore other escapes
145
+ }
146
+ // Per-char
147
+ let handled = false;
148
+ for (const ch of chunk) {
149
+ if (ch === "\x03") {
150
+ // Ctrl+C
151
+ cleanup(true);
152
+ process.stdout.write(`${YELLOW}aborted.${RESET}\n`);
153
+ process.exit(130);
154
+ }
155
+ if (ch === "\r" || ch === "\n") {
156
+ cleanup(true);
157
+ const picked = items.filter((i) => selected.has(i.id));
158
+ const summary = picked.length > 0
159
+ ? picked.map((p) => p.title).join(", ")
160
+ : `${DIM}none${RESET}`;
161
+ process.stdout.write(`${CYAN}?${RESET} ${BOLD}Skills to install${RESET} ${DIM}›${RESET} ${summary}\n`);
162
+ return picked.map((p) => p.id);
163
+ }
164
+ if (ch === " ") {
165
+ const item = items[cursor];
166
+ if (selected.has(item.id))
167
+ selected.delete(item.id);
168
+ else
169
+ selected.add(item.id);
170
+ render();
171
+ handled = true;
172
+ continue;
173
+ }
174
+ // letters could shortcut a/n: select all / select none
175
+ if (ch === "a") {
176
+ for (const it of items)
177
+ selected.add(it.id);
178
+ render();
179
+ handled = true;
180
+ continue;
181
+ }
182
+ if (ch === "n") {
183
+ selected.clear();
184
+ render();
185
+ handled = true;
186
+ continue;
187
+ }
188
+ }
189
+ if (!handled) {
190
+ // unknown — re-render in case of partial state
191
+ render();
192
+ }
193
+ }
194
+ }
195
+ catch (err) {
196
+ cleanup(true);
197
+ throw err;
198
+ }
199
+ }