claudeforge-cli 1.0.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 (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +431 -0
  3. package/bin/cli.js +155 -0
  4. package/package.json +43 -0
  5. package/src/commands/add.js +205 -0
  6. package/src/commands/create.js +218 -0
  7. package/src/commands/github.js +479 -0
  8. package/src/commands/init.js +107 -0
  9. package/src/commands/project.js +123 -0
  10. package/src/commands/status.js +183 -0
  11. package/src/commands/upgrade.js +114 -0
  12. package/src/index.js +6 -0
  13. package/src/logger.js +90 -0
  14. package/src/scaffolder.js +45 -0
  15. package/src/stack-detector.js +62 -0
  16. package/templates/.env.example.tpl +21 -0
  17. package/templates/.gitignore.tpl +40 -0
  18. package/templates/CLAUDE.local.md.tpl +32 -0
  19. package/templates/CLAUDE.md.tpl +112 -0
  20. package/templates/claude/README.md.tpl +94 -0
  21. package/templates/claude/agents/code-reviewer.md.tpl +142 -0
  22. package/templates/claude/commands/commit.md.tpl +34 -0
  23. package/templates/claude/commands/explain-codebase.md.tpl +37 -0
  24. package/templates/claude/commands/fix-issue.md.tpl +43 -0
  25. package/templates/claude/commands/memory-sync.md.tpl +49 -0
  26. package/templates/claude/commands/project-health.md.tpl +70 -0
  27. package/templates/claude/commands/review-pr.md.tpl +43 -0
  28. package/templates/claude/commands/scaffold-structure.md.tpl +308 -0
  29. package/templates/claude/commands/setup-project.md.tpl +253 -0
  30. package/templates/claude/commands/standup.md.tpl +34 -0
  31. package/templates/claude/hooks/post-tool-use.sh.tpl +44 -0
  32. package/templates/claude/hooks/pre-tool-use.sh.tpl +64 -0
  33. package/templates/claude/rules/no-sensitive-files.md.tpl +29 -0
  34. package/templates/claude/settings.json.tpl +50 -0
  35. package/templates/claude/settings.local.json.tpl +4 -0
  36. package/templates/claude/skills/project-conventions/SKILL.md.tpl +39 -0
  37. package/templates/mcp.json.tpl +9 -0
  38. package/templates/memory/MEMORY.md.tpl +37 -0
  39. package/templates/memory/feedback_communication.md.tpl +29 -0
  40. package/templates/memory/project_ai_workflow.md.tpl +43 -0
  41. package/templates/memory/user_profile.md.tpl +30 -0
@@ -0,0 +1,205 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs-extra');
5
+ const chalk = require('chalk');
6
+ const logger = require('../logger');
7
+
8
+ const TEMPLATES_DIR = path.join(__dirname, '../../templates');
9
+
10
+ async function addAgent(name, options) {
11
+ const targetDir = path.resolve(options.dir || process.cwd());
12
+
13
+ if (!name || !name.trim()) {
14
+ logger.error('Please provide an agent name.');
15
+ console.log(chalk.dim(' Example: claudeforge add agent api-reviewer'));
16
+ process.exit(1);
17
+ }
18
+
19
+ const slug = toKebab(name);
20
+ const destPath = path.join(targetDir, '.claude/agents', `${slug}.md`);
21
+
22
+ await assertNotExists(destPath, options.force);
23
+
24
+ const description = options.description || `Use this agent to [describe when Claude should invoke ${slug}].`;
25
+ const model = options.model || 'sonnet';
26
+ const color = options.color || 'blue';
27
+
28
+ const content = `---
29
+ name: ${slug}
30
+ description: ${description}
31
+ model: ${model}
32
+ color: ${color}
33
+ ---
34
+
35
+ # ${toTitle(name)} Agent
36
+
37
+ <!-- Replace this with the agent's system prompt. Be specific about:
38
+ - What the agent specializes in
39
+ - What inputs it expects from the caller
40
+ - What output format it produces
41
+ - Any rules or constraints it follows
42
+ -->
43
+
44
+ You are a specialized agent for [task].
45
+
46
+ ## Instructions
47
+
48
+ 1. [Step 1]
49
+ 2. [Step 2]
50
+
51
+ ## Output Format
52
+
53
+ [Describe the expected output]
54
+ `;
55
+
56
+ await fs.ensureFile(destPath);
57
+ await fs.writeFile(destPath, content, 'utf8');
58
+
59
+ console.log('');
60
+ console.log(` ${chalk.green('✓')} Created ${chalk.cyan(`.claude/agents/${slug}.md`)}`);
61
+ logger.hints('What to do next:', [
62
+ { cmd: `open .claude/agents/${slug}.md`, note: 'edit the system prompt — be specific about what it reviews' },
63
+ { cmd: `/review-pr`, note: 'test the agent by having it review your current branch' },
64
+ { cmd: `claudeforge status`, note: 'confirm the agent appears in your project' },
65
+ ]);
66
+ }
67
+
68
+ async function addCommand(name, options) {
69
+ const targetDir = path.resolve(options.dir || process.cwd());
70
+
71
+ if (!name || !name.trim()) {
72
+ logger.error('Please provide a command name.');
73
+ console.log(chalk.dim(' Example: claudeforge add command deploy'));
74
+ process.exit(1);
75
+ }
76
+
77
+ const slug = toKebab(name);
78
+ const destPath = path.join(targetDir, '.claude/commands', `${slug}.md`);
79
+
80
+ await assertNotExists(destPath, options.force);
81
+
82
+ const description = options.description || `[Describe what /${slug} does]`;
83
+
84
+ const content = `---
85
+ description: ${description}
86
+ allowed-tools: Bash, Read, Edit
87
+ ---
88
+
89
+ ## Context
90
+
91
+ <!-- Add dynamic context using !command syntax if needed -->
92
+ <!-- Example: - Git status: !\`git status\` -->
93
+
94
+ ## Task
95
+
96
+ <!-- Describe what Claude should do when this command is invoked. -->
97
+ <!-- Be specific: what to read first, what to check, what to produce. -->
98
+
99
+ [Instructions for Claude when /${slug} is invoked]
100
+
101
+ ## Rules
102
+
103
+ - [Rule 1]
104
+ - [Rule 2]
105
+ `;
106
+
107
+ await fs.ensureFile(destPath);
108
+ await fs.writeFile(destPath, content, 'utf8');
109
+
110
+ console.log('');
111
+ console.log(` ${chalk.green('✓')} Created ${chalk.cyan(`.claude/commands/${slug}.md`)}`);
112
+ logger.hints('What to do next:', [
113
+ { cmd: `open .claude/commands/${slug}.md`, note: 'edit the ## Task section — add real instructions' },
114
+ { cmd: `/${slug}`, note: 'run in Claude Code chat to test it' },
115
+ { cmd: `claudeforge add command <name>`, note: 'add another command for your workflow' },
116
+ ]);
117
+ }
118
+
119
+ async function addSkill(name, options) {
120
+ const targetDir = path.resolve(options.dir || process.cwd());
121
+
122
+ if (!name || !name.trim()) {
123
+ logger.error('Please provide a skill name.');
124
+ console.log(chalk.dim(' Example: claudeforge add skill database-patterns'));
125
+ process.exit(1);
126
+ }
127
+
128
+ const slug = toKebab(name);
129
+ const skillDir = path.join(targetDir, '.claude/skills', slug);
130
+ const destPath = path.join(skillDir, 'SKILL.md');
131
+
132
+ await assertNotExists(destPath, options.force);
133
+
134
+ const description = options.description || `Apply ${toTitle(name)} patterns and conventions. Invoke when [specific trigger condition].`;
135
+ const userInvocable = options.userInvocable !== false;
136
+
137
+ const content = `---
138
+ name: ${slug}
139
+ description: ${description}
140
+ user-invocable: ${userInvocable}
141
+ ---
142
+
143
+ # ${toTitle(name)} Skill
144
+
145
+ <!-- Define what Claude should know and do when this skill is active. -->
146
+ <!-- Be specific: which files to read, patterns to follow, rules to apply. -->
147
+
148
+ ## When to Apply
149
+
150
+ [Describe the exact conditions under which this skill should be active]
151
+
152
+ ## Instructions
153
+
154
+ 1. Read [relevant files or context first]
155
+ 2. Apply [specific patterns or conventions]
156
+ 3. [Additional steps]
157
+
158
+ ## Rules
159
+
160
+ - [Rule 1 — specific to this skill]
161
+ - [Rule 2]
162
+
163
+ ## Do Not
164
+
165
+ - [Common mistake to avoid]
166
+ `;
167
+
168
+ await fs.ensureDir(skillDir);
169
+ await fs.writeFile(destPath, content, 'utf8');
170
+
171
+ console.log('');
172
+ console.log(` ${chalk.green('✓')} Created ${chalk.cyan(`.claude/skills/${slug}/SKILL.md`)}`);
173
+ logger.hints('What to do next:', [
174
+ { cmd: `open .claude/skills/${slug}/SKILL.md`, note: 'fill in the Instructions section with specific conventions' },
175
+ { cmd: `claudeforge status`, note: 'verify the skill appears in your project summary' },
176
+ { cmd: `/project-health`, note: 'run in Claude Code chat to audit your full setup' },
177
+ ]);
178
+ }
179
+
180
+ // ── Shared helpers ────────────────────────────────────────────────────────────
181
+
182
+ async function assertNotExists(filePath, force) {
183
+ if (!force && await fs.pathExists(filePath)) {
184
+ logger.error(`File already exists: ${path.relative(process.cwd(), filePath)}`);
185
+ console.log(chalk.dim(' Use --force to overwrite.'));
186
+ process.exit(1);
187
+ }
188
+ }
189
+
190
+ function toKebab(str) {
191
+ return str
192
+ .trim()
193
+ .toLowerCase()
194
+ .replace(/[\s_]+/g, '-')
195
+ .replace(/[^a-z0-9-]/g, '');
196
+ }
197
+
198
+ function toTitle(str) {
199
+ return str
200
+ .split(/[-_\s]+/)
201
+ .map(w => w.charAt(0).toUpperCase() + w.slice(1))
202
+ .join(' ');
203
+ }
204
+
205
+ module.exports = { addAgent, addCommand, addSkill };
@@ -0,0 +1,218 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs-extra');
5
+ const chalk = require('chalk');
6
+ const readline = require('readline');
7
+ const initCommand = require('./init');
8
+ const projectCommand = require('./project');
9
+ const logger = require('../logger');
10
+
11
+ // ── Readline helpers ──────────────────────────────────────────────────────────
12
+
13
+ function ask(rl, question) {
14
+ return new Promise(resolve => rl.question(question, answer => resolve(answer.trim())));
15
+ }
16
+
17
+ async function askDefault(rl, question, defaultVal) {
18
+ const answer = await ask(rl, question);
19
+ return answer || defaultVal;
20
+ }
21
+
22
+ async function askChoice(rl, question, choices, defaultIdx = 0) {
23
+ const labels = choices.map((c, i) => `${chalk.dim(`${i + 1}.`)} ${c.label}`).join('\n ');
24
+ const answer = await ask(rl, `${question}\n\n ${labels}\n\n ${chalk.dim('Enter number')} [${defaultIdx + 1}]: `);
25
+ const idx = parseInt(answer, 10) - 1;
26
+ if (idx >= 0 && idx < choices.length) return choices[idx].value;
27
+ return choices[defaultIdx].value;
28
+ }
29
+
30
+ async function askMultiChoice(rl, question, choices) {
31
+ const labels = choices.map((c, i) => `${chalk.dim(`${i + 1}.`)} ${c.label}`).join('\n ');
32
+ const answer = await ask(rl, `${question}\n\n ${labels}\n\n ${chalk.dim('Enter numbers separated by commas, or press Enter to skip')}: `);
33
+ if (!answer) return [];
34
+ const indices = answer.split(',').map(s => parseInt(s.trim(), 10) - 1).filter(i => i >= 0 && i < choices.length);
35
+ return indices.map(i => choices[i].value);
36
+ }
37
+
38
+ // ── Main ─────────────────────────────────────────────────────────────────────
39
+
40
+ async function create(name, options) {
41
+ const parentDir = path.resolve(options.dir || process.cwd());
42
+
43
+ console.log('');
44
+ console.log(chalk.bold.cyan(' claudeforge') + chalk.dim(' — Create New Project'));
45
+ console.log(chalk.dim(' ─────────────────────────────────────────'));
46
+ console.log('');
47
+ console.log(chalk.dim(' Answer a few questions and claudeforge will scaffold a fully configured'));
48
+ console.log(chalk.dim(' Claude Code project ready for AI-assisted development.'));
49
+ console.log('');
50
+
51
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
52
+
53
+ try {
54
+ // 1. Project name
55
+ const defaultName = name || 'my-project';
56
+ const projectName = await askDefault(rl, ' ' + chalk.bold('Project name') + ' ' + chalk.dim('[' + defaultName + ']') + ': ', defaultName);
57
+
58
+ // 2. Description
59
+ const description = await askDefault(
60
+ rl,
61
+ `\n ${chalk.bold('Describe your project')} ${chalk.dim('(used to AI-configure everything)')}:\n `,
62
+ `${projectName} — a new project`
63
+ );
64
+
65
+ // 3. Stack
66
+ const stack = await askChoice(rl, `\n ${chalk.bold('Primary tech stack')}:`, [
67
+ { label: 'Node.js / TypeScript', value: 'node' },
68
+ { label: 'Python (FastAPI / Django)', value: 'python' },
69
+ { label: 'Go', value: 'go' },
70
+ { label: 'Rust', value: 'rust' },
71
+ { label: 'Other / Mixed', value: 'generic' },
72
+ ], 0);
73
+
74
+ // 4. Optional features
75
+ const features = await askMultiChoice(rl, `\n ${chalk.bold('Optional features')} ${chalk.dim('(select all that apply)')}:`, [
76
+ { label: 'GitHub Actions CI/CD + PR templates', value: 'github' },
77
+ { label: 'VS Code devcontainer', value: 'devcontainer' },
78
+ { label: 'Docker + docker-compose', value: 'docker' },
79
+ ]);
80
+
81
+ rl.close();
82
+
83
+ // ── Scaffold ────────────────────────────────────────────────────────────
84
+ const targetDir = path.join(parentDir, projectName);
85
+ await fs.ensureDir(targetDir);
86
+
87
+ console.log('');
88
+ console.log(chalk.dim(' ─────────────────────────────────────────'));
89
+ console.log('');
90
+ console.log(` ${chalk.green('✓')} Creating project at ${chalk.cyan(targetDir)}`);
91
+ console.log('');
92
+
93
+ // Run init
94
+ await initCommand({ dir: targetDir, force: false, dryRun: false });
95
+
96
+ // Run project
97
+ await projectCommand(description, { dir: targetDir });
98
+
99
+ // Optional: GitHub
100
+ if (features.includes('github')) {
101
+ const githubCommand = require('./github');
102
+ await githubCommand({ dir: targetDir, dryRun: false, stack, devcontainer: features.includes('devcontainer'), prTemplate: true, issueTemplates: true });
103
+ } else if (features.includes('devcontainer')) {
104
+ const githubCommand = require('./github');
105
+ await githubCommand({ dir: targetDir, dryRun: false, stack, devcontainer: true, prTemplate: false, issueTemplates: false });
106
+ }
107
+
108
+ // Optional: Docker stubs
109
+ if (features.includes('docker')) {
110
+ await writeDockerFiles(targetDir, stack, projectName);
111
+ }
112
+
113
+ // Final summary
114
+ console.log('');
115
+ console.log(chalk.dim(' ─────────────────────────────────────────'));
116
+ console.log('');
117
+ console.log(chalk.green.bold(` ✓ Project "${projectName}" is ready!`));
118
+ console.log('');
119
+ console.log(chalk.bold(' What to do next:'));
120
+ console.log('');
121
+ console.log(` ${chalk.dim('1.')} ${chalk.cyan(`cd ${projectName}`)}`);
122
+ console.log(` ${chalk.dim('2.')} Open in VS Code / your IDE with Claude Code`);
123
+ console.log(` ${chalk.dim('3.')} In the Claude Code chat, run:`);
124
+ console.log(` ${chalk.cyan.bold(`/setup-project "${truncate(description, 50)}"`)}`);
125
+ console.log(` ${chalk.dim('— Claude will fill in CLAUDE.md, agents, commands, and more')}`);
126
+ console.log(` ${chalk.dim('4.')} Then run ${chalk.cyan('/scaffold-structure')} to create src/, tests/, etc.`);
127
+ console.log('');
128
+
129
+ } catch (err) {
130
+ if (!rl.closed) rl.close();
131
+ logger.error(err.message || String(err));
132
+ process.exit(1);
133
+ }
134
+ }
135
+
136
+ async function writeDockerFiles(targetDir, stack, projectName) {
137
+ const dockerfile = buildDockerfile(stack, projectName);
138
+ const compose = buildDockerCompose(stack, projectName);
139
+ await fs.writeFile(path.join(targetDir, 'Dockerfile'), dockerfile, 'utf8');
140
+ await fs.writeFile(path.join(targetDir, 'docker-compose.yml'), compose, 'utf8');
141
+ console.log(` ${chalk.green('✓')} Written ${chalk.cyan('Dockerfile')}`);
142
+ console.log(` ${chalk.green('✓')} Written ${chalk.cyan('docker-compose.yml')}`);
143
+ }
144
+
145
+ function buildDockerfile(stack, name) {
146
+ if (stack === 'node') {
147
+ return `FROM node:20-alpine
148
+ WORKDIR /app
149
+ COPY package*.json ./
150
+ RUN npm ci --production
151
+ COPY . .
152
+ EXPOSE 3000
153
+ CMD ["node", "src/index.js"]
154
+ `;
155
+ }
156
+ if (stack === 'python') {
157
+ return `FROM python:3.12-slim
158
+ WORKDIR /app
159
+ COPY requirements*.txt ./
160
+ RUN pip install --no-cache-dir -r requirements.txt
161
+ COPY . .
162
+ EXPOSE 8000
163
+ CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
164
+ `;
165
+ }
166
+ if (stack === 'go') {
167
+ return `FROM golang:1.22-alpine AS builder
168
+ WORKDIR /app
169
+ COPY go.* ./
170
+ RUN go mod download
171
+ COPY . .
172
+ RUN go build -o /app/server ./cmd/server
173
+
174
+ FROM alpine:3.19
175
+ COPY --from=builder /app/server /server
176
+ EXPOSE 8080
177
+ CMD ["/server"]
178
+ `;
179
+ }
180
+ if (stack === 'rust') {
181
+ return `FROM rust:1.77-slim AS builder
182
+ WORKDIR /app
183
+ COPY Cargo.* ./
184
+ COPY src ./src
185
+ RUN cargo build --release
186
+
187
+ FROM debian:bookworm-slim
188
+ COPY --from=builder /app/target/release/${name} /${name}
189
+ EXPOSE 8080
190
+ CMD ["/${name}"]
191
+ `;
192
+ }
193
+ return `FROM ubuntu:22.04
194
+ WORKDIR /app
195
+ COPY . .
196
+ `;
197
+ }
198
+
199
+ function buildDockerCompose(stack, name) {
200
+ const port = stack === 'node' ? '3000' : stack === 'python' ? '8000' : '8080';
201
+ return `services:
202
+ app:
203
+ build: .
204
+ ports:
205
+ - "${port}:${port}"
206
+ environment:
207
+ - NODE_ENV=development
208
+ volumes:
209
+ - .:/app
210
+ - /app/node_modules
211
+ `;
212
+ }
213
+
214
+ function truncate(str, max) {
215
+ return str.length > max ? str.slice(0, max - 1) + '…' : str;
216
+ }
217
+
218
+ module.exports = create;