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,123 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs-extra');
5
+ const chalk = require('chalk');
6
+ const { detect } = require('../stack-detector');
7
+ const logger = require('../logger');
8
+
9
+ async function project(description, options) {
10
+ const targetDir = path.resolve(options.dir || process.cwd());
11
+
12
+ if (!description || !description.trim()) {
13
+ logger.error('Please provide a project description.');
14
+ console.log(chalk.dim(' Example: claudeforge project "FastAPI REST API with PostgreSQL and Redis"'));
15
+ process.exit(1);
16
+ }
17
+
18
+ if (!await fs.pathExists(path.join(targetDir, '.claude'))) {
19
+ logger.error('No .claude/ directory found. Run `claudeforge init` first.');
20
+ process.exit(1);
21
+ }
22
+
23
+ // Detect tech stack from existing files
24
+ const { labels, context } = await detect(targetDir);
25
+
26
+ // Write SETUP_CONTEXT.md so the /setup-project slash command can read it
27
+ const contextContent = buildContextFile(description.trim(), labels, context, targetDir);
28
+ const contextPath = path.join(targetDir, 'SETUP_CONTEXT.md');
29
+ await fs.writeFile(contextPath, contextContent, 'utf8');
30
+
31
+ // Print onboarding guide
32
+ printGuide(description.trim(), labels, targetDir);
33
+ }
34
+
35
+ function buildContextFile(description, labels, context, targetDir) {
36
+ const lines = [
37
+ '# Project Setup Context',
38
+ '',
39
+ '> This file was generated by `claudeforge project`. It is read by the `/setup-project`',
40
+ '> slash command in Claude Code. Delete it once setup is complete.',
41
+ '',
42
+ '## Project Description',
43
+ '',
44
+ description,
45
+ '',
46
+ ];
47
+
48
+ if (labels.length > 0) {
49
+ lines.push('## Detected Tech Stack', '');
50
+ labels.forEach(l => lines.push(`- ${l}`));
51
+ lines.push('');
52
+ }
53
+
54
+ if (context) {
55
+ lines.push('## Key Project Files', '', '```', context, '```', '');
56
+ }
57
+
58
+ lines.push(
59
+ '## What `/setup-project` Should Do',
60
+ '',
61
+ 'Fill in the following files with content tailored to the project description above:',
62
+ '',
63
+ '1. **`CLAUDE.md`** — project name, real commands for the stack, architecture, code style, env vars, gotchas',
64
+ '2. **`.claude/settings.json`** — add stack-appropriate permissions (test runner, build, linter)',
65
+ '3. **`.env.example`** — real environment variables for this stack with descriptions',
66
+ '4. **`.mcp.json`** — context7 plus any relevant MCP servers (database, browser, etc.)',
67
+ '5. **`memory/project_ai_workflow.md`** — AI conventions specific to this project',
68
+ '6. **New agents** in `.claude/agents/` — 2–4 specialized agents for this project type',
69
+ '7. **New commands** in `.claude/commands/` — 2–4 slash commands for the workflow',
70
+ '8. **`.gitignore`** — append tech-stack specific patterns',
71
+ '',
72
+ 'Then delete this file (`SETUP_CONTEXT.md`) when done.',
73
+ );
74
+
75
+ return lines.join('\n') + '\n';
76
+ }
77
+
78
+ function printGuide(description, labels, targetDir) {
79
+ console.log('');
80
+ console.log(chalk.bold.cyan(' claudeforge') + chalk.dim(' — Project Setup'));
81
+ console.log(chalk.dim(' ─────────────────────────────────────────'));
82
+ console.log('');
83
+ console.log(` ${chalk.green('✓')} Written ${chalk.cyan('SETUP_CONTEXT.md')} with your project description`);
84
+
85
+ if (labels.length > 0) {
86
+ console.log(` ${chalk.green('✓')} Detected stack: ${chalk.white(labels.join(', '))}`);
87
+ }
88
+
89
+ console.log('');
90
+ console.log(chalk.bold(' Next: open Claude Code and run the setup command'));
91
+ console.log('');
92
+ console.log(chalk.dim(' ┌─────────────────────────────────────────────────────────┐'));
93
+ console.log(chalk.dim(' │') + chalk.bold.white(' In the Claude Code chat window, type: ') + chalk.dim('│'));
94
+ console.log(chalk.dim(' │') + ' ' + chalk.dim('│'));
95
+ console.log(chalk.dim(' │') + ' ' + chalk.cyan.bold('/setup-project') + chalk.white(` "${truncate(description, 38)}"`) + ' ' + chalk.dim('│'));
96
+ console.log(chalk.dim(' │') + ' ' + chalk.dim('│'));
97
+ console.log(chalk.dim(' └─────────────────────────────────────────────────────────┘'));
98
+ console.log('');
99
+ console.log(chalk.dim(' Claude will read SETUP_CONTEXT.md and fill in:'));
100
+ console.log(chalk.dim(' • CLAUDE.md — project commands, architecture, style'));
101
+ console.log(chalk.dim(' • settings.json — stack-appropriate permissions'));
102
+ console.log(chalk.dim(' • .env.example — real environment variables'));
103
+ console.log(chalk.dim(' • .mcp.json — MCP servers for your stack'));
104
+ console.log(chalk.dim(' • agents/ — specialized sub-agents for your domain'));
105
+ console.log(chalk.dim(' • commands/ — slash commands for your workflow'));
106
+ console.log(chalk.dim(' • memory/ — project AI workflow conventions'));
107
+ console.log('');
108
+ console.log(chalk.dim(' Works with any Claude model — GitHub Copilot, Claude.ai, or'));
109
+ console.log(chalk.dim(' any IDE with Claude Code extension. No API key required.'));
110
+ console.log('');
111
+ console.log(chalk.dim(' ─────────────────────────────────────────'));
112
+ console.log(chalk.dim(' Other useful commands once in the IDE:'));
113
+ console.log(chalk.dim(' /project-health — audit your setup and get improvement tips'));
114
+ console.log(chalk.dim(' /memory-sync — update memory files after a work session'));
115
+ console.log(chalk.dim(' /standup — generate a standup from recent commits'));
116
+ console.log('');
117
+ }
118
+
119
+ function truncate(str, max) {
120
+ return str.length > max ? str.slice(0, max - 1) + '…' : str;
121
+ }
122
+
123
+ module.exports = project;
@@ -0,0 +1,183 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs-extra');
5
+ const chalk = require('chalk');
6
+
7
+ async function status(options) {
8
+ const targetDir = path.resolve(options.dir || process.cwd());
9
+
10
+ console.log('');
11
+ console.log(chalk.bold.cyan(' claudeforge') + chalk.dim(' — Project Status'));
12
+ console.log(chalk.dim(' ─────────────────────────────────────────'));
13
+ console.log('');
14
+
15
+ // ── Core scaffold check ───────────────────────────────────────────────────
16
+ const coreFiles = [
17
+ { path: 'CLAUDE.md', label: 'CLAUDE.md (project context)' },
18
+ { path: 'CLAUDE.local.md', label: 'CLAUDE.local.md (personal context)' },
19
+ { path: '.claude/settings.json', label: '.claude/settings.json' },
20
+ { path: '.claude/settings.local.json', label: '.claude/settings.local.json' },
21
+ { path: '.mcp.json', label: '.mcp.json (MCP servers)' },
22
+ { path: '.env.example', label: '.env.example' },
23
+ { path: '.gitignore', label: '.gitignore' },
24
+ { path: 'memory/MEMORY.md', label: 'memory/MEMORY.md (index)' },
25
+ ];
26
+
27
+ console.log(chalk.bold(' Core Files'));
28
+ for (const f of coreFiles) {
29
+ const exists = await fs.pathExists(path.join(targetDir, f.path));
30
+ const icon = exists ? chalk.green('✓') : chalk.red('✗');
31
+ const label = exists ? chalk.dim(f.label) : chalk.red(f.label + ' — missing');
32
+ console.log(` ${icon} ${label}`);
33
+ }
34
+
35
+ // ── Settings summary ──────────────────────────────────────────────────────
36
+ const settingsPath = path.join(targetDir, '.claude/settings.json');
37
+ if (await fs.pathExists(settingsPath)) {
38
+ try {
39
+ const settings = await fs.readJson(settingsPath);
40
+ console.log('');
41
+ console.log(chalk.bold(' Settings'));
42
+ console.log(chalk.dim(' Model: ') + chalk.white(settings.model || chalk.yellow('(not set, using global default)')));
43
+
44
+ const allowCount = (settings.permissions?.allow || []).length;
45
+ const denyCount = (settings.permissions?.deny || []).length;
46
+ console.log(chalk.dim(' Perms: ') + chalk.white(`${allowCount} allowed, ${denyCount} denied`));
47
+
48
+ const hookCount = Object.values(settings.hooks || {}).reduce((n, arr) => n + arr.length, 0);
49
+ console.log(chalk.dim(' Hooks: ') + chalk.white(`${hookCount} hook rule(s) configured`));
50
+ } catch (_) {
51
+ console.log(chalk.yellow(' ⚠ Could not parse .claude/settings.json'));
52
+ }
53
+ }
54
+
55
+ // ── MCP servers ───────────────────────────────────────────────────────────
56
+ const mcpPath = path.join(targetDir, '.mcp.json');
57
+ if (await fs.pathExists(mcpPath)) {
58
+ try {
59
+ const mcp = await fs.readJson(mcpPath);
60
+ const servers = Object.keys(mcp.mcpServers || {});
61
+ console.log('');
62
+ console.log(chalk.bold(' MCP Servers'));
63
+ if (servers.length === 0) {
64
+ console.log(chalk.dim(' (none configured)'));
65
+ } else {
66
+ servers.forEach(s => console.log(chalk.dim(' •') + ' ' + chalk.white(s)));
67
+ }
68
+ } catch (_) {}
69
+ }
70
+
71
+ // ── Agents ────────────────────────────────────────────────────────────────
72
+ const agentsDir = path.join(targetDir, '.claude/agents');
73
+ if (await fs.pathExists(agentsDir)) {
74
+ const agents = (await fs.readdir(agentsDir)).filter(f => f.endsWith('.md'));
75
+ console.log('');
76
+ console.log(chalk.bold(' Agents') + chalk.dim(` (${agents.length})`));
77
+ if (agents.length === 0) {
78
+ console.log(chalk.dim(' (none — run `claudeforge add agent <name>`)'));
79
+ } else {
80
+ for (const agent of agents) {
81
+ const name = agent.replace('.md', '');
82
+ const desc = await readFrontmatterField(path.join(agentsDir, agent), 'description');
83
+ console.log(chalk.dim(' •') + ' ' + chalk.cyan(name) + (desc ? chalk.dim(` — ${truncate(desc, 60)}`) : ''));
84
+ }
85
+ }
86
+ }
87
+
88
+ // ── Slash Commands ────────────────────────────────────────────────────────
89
+ const commandsDir = path.join(targetDir, '.claude/commands');
90
+ if (await fs.pathExists(commandsDir)) {
91
+ const commands = (await fs.readdir(commandsDir)).filter(f => f.endsWith('.md'));
92
+ console.log('');
93
+ console.log(chalk.bold(' Slash Commands') + chalk.dim(` (${commands.length})`));
94
+ if (commands.length === 0) {
95
+ console.log(chalk.dim(' (none — run `claudeforge add command <name>`)'));
96
+ } else {
97
+ for (const cmd of commands) {
98
+ const name = cmd.replace('.md', '');
99
+ const desc = await readFrontmatterField(path.join(commandsDir, cmd), 'description');
100
+ console.log(chalk.dim(' •') + ' /' + chalk.cyan(name) + (desc ? chalk.dim(` — ${truncate(desc, 55)}`) : ''));
101
+ }
102
+ }
103
+ }
104
+
105
+ // ── Skills ────────────────────────────────────────────────────────────────
106
+ const skillsDir = path.join(targetDir, '.claude/skills');
107
+ if (await fs.pathExists(skillsDir)) {
108
+ const skills = await fs.readdir(skillsDir);
109
+ const skillDirs = [];
110
+ for (const s of skills) {
111
+ const sp = path.join(skillsDir, s, 'SKILL.md');
112
+ if (await fs.pathExists(sp)) skillDirs.push(s);
113
+ }
114
+ console.log('');
115
+ console.log(chalk.bold(' Skills') + chalk.dim(` (${skillDirs.length})`));
116
+ if (skillDirs.length === 0) {
117
+ console.log(chalk.dim(' (none — run `claudeforge add skill <name>`)'));
118
+ } else {
119
+ for (const skill of skillDirs) {
120
+ const desc = await readFrontmatterField(path.join(skillsDir, skill, 'SKILL.md'), 'description');
121
+ console.log(chalk.dim(' •') + ' ' + chalk.cyan(skill) + (desc ? chalk.dim(` — ${truncate(desc, 55)}`) : ''));
122
+ }
123
+ }
124
+ }
125
+
126
+ // ── Memory files ──────────────────────────────────────────────────────────
127
+ const memoryDir = path.join(targetDir, 'memory');
128
+ if (await fs.pathExists(memoryDir)) {
129
+ const memFiles = (await fs.readdir(memoryDir)).filter(f => f.endsWith('.md'));
130
+ console.log('');
131
+ console.log(chalk.bold(' Memory') + chalk.dim(` (${memFiles.length} files)`));
132
+ for (const f of memFiles) {
133
+ const size = (await fs.stat(path.join(memoryDir, f))).size;
134
+ const filled = size > 200 ? chalk.green('●') : chalk.yellow('○');
135
+ console.log(` ${filled} ${chalk.dim(f)}${size <= 200 ? chalk.yellow(' — empty template') : ''}`);
136
+ }
137
+ }
138
+
139
+ console.log('');
140
+ console.log(chalk.dim(' ─────────────────────────────────────────'));
141
+
142
+ // Contextual hints based on what's missing
143
+ const missingCore = [];
144
+ for (const f of coreFiles) {
145
+ if (!await fs.pathExists(path.join(targetDir, f.path))) missingCore.push(f.path);
146
+ }
147
+
148
+ if (missingCore.length > 0) {
149
+ console.log(chalk.yellow(` ⚠ ${missingCore.length} core file(s) missing. Run: `) + chalk.cyan('claudeforge init'));
150
+ console.log('');
151
+ } else {
152
+ console.log('');
153
+ console.log(chalk.bold(' Productivity tips:'));
154
+ console.log('');
155
+ console.log(` ${chalk.dim('›')} ${chalk.cyan('/setup-project "description"')} ${chalk.dim('AI-fills CLAUDE.md, agents, commands in the IDE chat')}`);
156
+ console.log(` ${chalk.dim('›')} ${chalk.cyan('/scaffold-structure')} ${chalk.dim('creates src/, tests/, and starter files in IDE chat')}`);
157
+ console.log(` ${chalk.dim('›')} ${chalk.cyan('/project-health')} ${chalk.dim('audits your setup and suggests improvements in IDE chat')}`);
158
+ console.log(` ${chalk.dim('›')} ${chalk.cyan('claudeforge upgrade')} ${chalk.dim('update hook scripts and built-in commands to latest')}`);
159
+ console.log('');
160
+ }
161
+ }
162
+
163
+ // ── Helpers ──────────────────────────────────────────────────────────────────
164
+
165
+ async function readFrontmatterField(filePath, field) {
166
+ try {
167
+ const content = await fs.readFile(filePath, 'utf8');
168
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
169
+ if (!match) return null;
170
+ const fm = match[1];
171
+ const fieldMatch = fm.match(new RegExp(`^${field}:\\s*(.+)$`, 'm'));
172
+ return fieldMatch ? fieldMatch[1].trim() : null;
173
+ } catch (_) {
174
+ return null;
175
+ }
176
+ }
177
+
178
+ function truncate(str, maxLen) {
179
+ if (!str) return '';
180
+ return str.length > maxLen ? str.slice(0, maxLen - 1) + '…' : str;
181
+ }
182
+
183
+ module.exports = status;
@@ -0,0 +1,114 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const chalk = require('chalk');
5
+ const scaffolder = require('../scaffolder');
6
+ const logger = require('../logger');
7
+ const fs = require('fs-extra');
8
+
9
+ /**
10
+ * Files that are infrastructure templates (always safe to upgrade).
11
+ * User-edited files like CLAUDE.md are excluded by default.
12
+ */
13
+ const UPGRADEABLE = [
14
+ // Infrastructure — always safe to re-apply
15
+ { src: 'claude/settings.local.json.tpl', dest: '.claude/settings.local.json' },
16
+ { src: 'claude/README.md.tpl', dest: '.claude/README.md' },
17
+ { src: 'claude/hooks/pre-tool-use.sh.tpl', dest: '.claude/hooks/pre-tool-use.sh' },
18
+ { src: 'claude/hooks/post-tool-use.sh.tpl', dest: '.claude/hooks/post-tool-use.sh' },
19
+ { src: 'claude/rules/no-sensitive-files.md.tpl', dest: '.claude/rules/no-sensitive-files.md' },
20
+ { src: 'claude/skills/project-conventions/SKILL.md.tpl', dest: '.claude/skills/project-conventions/SKILL.md' },
21
+ { src: 'memory/MEMORY.md.tpl', dest: 'memory/MEMORY.md' },
22
+ // Built-in AI slash commands — safe to upgrade (project-specific ones in USER_OWNED)
23
+ { src: 'claude/commands/setup-project.md.tpl', dest: '.claude/commands/setup-project.md' },
24
+ { src: 'claude/commands/memory-sync.md.tpl', dest: '.claude/commands/memory-sync.md' },
25
+ { src: 'claude/commands/project-health.md.tpl', dest: '.claude/commands/project-health.md' },
26
+ { src: 'claude/commands/standup.md.tpl', dest: '.claude/commands/standup.md' },
27
+ { src: 'claude/commands/explain-codebase.md.tpl', dest: '.claude/commands/explain-codebase.md' },
28
+ { src: 'claude/commands/fix-issue.md.tpl', dest: '.claude/commands/fix-issue.md' },
29
+ { src: 'claude/commands/scaffold-structure.md.tpl', dest: '.claude/commands/scaffold-structure.md' },
30
+ ];
31
+
32
+ /**
33
+ * User-owned files — only upgraded with --all flag.
34
+ */
35
+ const USER_OWNED = [
36
+ { src: 'CLAUDE.md.tpl', dest: 'CLAUDE.md' },
37
+ { src: 'CLAUDE.local.md.tpl', dest: 'CLAUDE.local.md' },
38
+ { src: '.env.example.tpl', dest: '.env.example' },
39
+ { src: 'mcp.json.tpl', dest: '.mcp.json' },
40
+ { src: '.gitignore.tpl', dest: '.gitignore' },
41
+ { src: 'claude/settings.json.tpl', dest: '.claude/settings.json' },
42
+ { src: 'claude/agents/code-reviewer.md.tpl', dest: '.claude/agents/code-reviewer.md' },
43
+ { src: 'claude/commands/commit.md.tpl', dest: '.claude/commands/commit.md' },
44
+ { src: 'claude/commands/review-pr.md.tpl', dest: '.claude/commands/review-pr.md' },
45
+ { src: 'memory/feedback_communication.md.tpl', dest: 'memory/feedback_communication.md' },
46
+ { src: 'memory/project_ai_workflow.md.tpl', dest: 'memory/project_ai_workflow.md' },
47
+ { src: 'memory/user_profile.md.tpl', dest: 'memory/user_profile.md' },
48
+ ];
49
+
50
+ async function upgrade(options) {
51
+ const targetDir = path.resolve(options.dir || process.cwd());
52
+ const includeAll = options.all || false;
53
+ const dryRun = options.dryRun || false;
54
+
55
+ if (!await fs.pathExists(path.join(targetDir, '.claude'))) {
56
+ logger.error('No .claude/ directory found. Run `claudeforge init` first.');
57
+ process.exit(1);
58
+ }
59
+
60
+ console.log('');
61
+ console.log(chalk.bold.cyan(' claudeforge') + chalk.dim(' — Upgrade Templates'));
62
+ console.log(chalk.dim(' ─────────────────────────────────────────'));
63
+ if (dryRun) console.log(chalk.bold.yellow(' [DRY RUN] No files will be written'));
64
+ if (includeAll) console.log(chalk.yellow(' [--all] Including user-owned files (CLAUDE.md, settings.json, etc.)'));
65
+ console.log('');
66
+
67
+ const templatesDir = path.join(__dirname, '../../templates');
68
+ const manifest = includeAll ? [...UPGRADEABLE, ...USER_OWNED] : UPGRADEABLE;
69
+
70
+ const stats = { updated: 0, skipped: 0 };
71
+
72
+ for (const entry of manifest) {
73
+ const srcAbs = path.join(templatesDir, entry.src);
74
+ const destAbs = path.join(targetDir, entry.dest);
75
+
76
+ const result = await scaffolder.writeFile(srcAbs, destAbs, { force: true, dryRun });
77
+
78
+ if (dryRun) {
79
+ console.log(` ${chalk.cyan('~')} ${chalk.dim(entry.dest)} ${chalk.cyan('would update')}`);
80
+ } else {
81
+ console.log(` ${chalk.yellow('↺')} ${chalk.dim(entry.dest)}`);
82
+ }
83
+ stats.updated++;
84
+ }
85
+
86
+ // Re-chmod hooks
87
+ if (!dryRun) {
88
+ await scaffolder.chmod(path.join(targetDir, '.claude/hooks/pre-tool-use.sh'), 0o755);
89
+ await scaffolder.chmod(path.join(targetDir, '.claude/hooks/post-tool-use.sh'), 0o755);
90
+ }
91
+
92
+ console.log('');
93
+ console.log(chalk.dim(' ─────────────────────────────────────────'));
94
+
95
+ if (dryRun) {
96
+ console.log(chalk.cyan(` Dry run: ${stats.updated} file(s) would be updated.`));
97
+ console.log('');
98
+ } else {
99
+ console.log(chalk.green(` ✓ ${stats.updated} infrastructure file(s) updated to latest templates.`));
100
+ if (!includeAll) {
101
+ console.log('');
102
+ console.log(chalk.dim(' User-owned files (CLAUDE.md, settings.json, agents, commands) were NOT touched.'));
103
+ console.log(chalk.dim(' Use --all to force-update everything (will overwrite your edits).'));
104
+ }
105
+ console.log('');
106
+ console.log(chalk.bold(' What to do next:'));
107
+ console.log('');
108
+ console.log(` ${chalk.dim('›')} ${chalk.cyan('/project-health')} ${chalk.dim('audit your setup to see if anything else needs attention')}`);
109
+ console.log(` ${chalk.dim('›')} ${chalk.cyan('claudeforge status')} ${chalk.dim('verify the updated files look correct')}`);
110
+ console.log('');
111
+ }
112
+ }
113
+
114
+ module.exports = upgrade;
package/src/index.js ADDED
@@ -0,0 +1,6 @@
1
+ 'use strict';
2
+
3
+ // Public API — useful if someone requires('claudeforge') programmatically
4
+ const init = require('./commands/init');
5
+
6
+ module.exports = { init };
package/src/logger.js ADDED
@@ -0,0 +1,90 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+
5
+ const ICONS = { created: '✓', skipped: '○', overwritten: '↺', dir: '⌂' };
6
+ const COLORS = {
7
+ created: chalk.green,
8
+ skipped: chalk.gray,
9
+ overwritten: chalk.yellow,
10
+ dir: chalk.blue,
11
+ };
12
+
13
+ function banner(dryRun) {
14
+ console.log('');
15
+ console.log(chalk.bold.cyan(' claudeforge') + chalk.dim(' — Claude Code Project Scaffolder'));
16
+ console.log(chalk.dim(' ─────────────────────────────────────────'));
17
+ if (dryRun) console.log(chalk.bold.yellow(' [DRY RUN] No files will be written'));
18
+ console.log('');
19
+ }
20
+
21
+ function info(msg) {
22
+ console.log(chalk.dim(' →') + ' ' + chalk.white(msg));
23
+ }
24
+
25
+ function fileResult(result, relPath, dryRun) {
26
+ if (dryRun && result !== 'skipped') {
27
+ console.log(` ${chalk.cyan('~')} ${chalk.dim(relPath)} ${chalk.cyan('would create')}`);
28
+ return;
29
+ }
30
+ const icon = COLORS[result](ICONS[result]);
31
+ console.log(` ${icon} ${chalk.dim(relPath)} ${chalk.dim(result)}`);
32
+ }
33
+
34
+ function dirResult(relPath, dryRun) {
35
+ if (dryRun) {
36
+ console.log(` ${chalk.cyan('~')} ${chalk.dim(relPath + '/')} ${chalk.cyan('dir')}`);
37
+ return;
38
+ }
39
+ console.log(` ${COLORS.dir(ICONS.dir)} ${chalk.dim(relPath + '/')} ${chalk.blue('dir')}`);
40
+ }
41
+
42
+ function summary(stats, dryRun) {
43
+ console.log('');
44
+ console.log(chalk.dim(' ─────────────────────────────────────────'));
45
+
46
+ if (dryRun) {
47
+ console.log(chalk.cyan(` Dry run complete. ${stats.created} file(s) would be created, ${stats.skipped} would be skipped.`));
48
+ console.log('');
49
+ return;
50
+ }
51
+
52
+ console.log(
53
+ chalk.green(' Done!') +
54
+ chalk.dim(` ${stats.created} created, ${stats.overwritten} overwritten, ${stats.skipped} skipped.`)
55
+ );
56
+ console.log('');
57
+ _hints('What to do next:', [
58
+ { cmd: 'claudeforge project "describe your project"', note: 'prepares AI context for the IDE' },
59
+ { cmd: '/setup-project "describe your project"', note: 'run in Claude Code chat — fills in everything' },
60
+ { cmd: '/scaffold-structure', note: 'run in Claude Code chat — creates src/, tests/, etc.' },
61
+ { cmd: 'claudeforge status', note: 'see everything configured so far' },
62
+ ]);
63
+ }
64
+
65
+ function error(msg) {
66
+ console.error(chalk.red(' ✗ Error: ') + msg);
67
+ }
68
+
69
+ // ── Shared hint box ───────────────────────────────────────────────────────────
70
+
71
+ /**
72
+ * Print a contextual "What's next?" hint box.
73
+ * @param {string} title
74
+ * @param {Array<{cmd: string, note: string}>} hints
75
+ */
76
+ function hints(title, hintList) {
77
+ _hints(title, hintList);
78
+ }
79
+
80
+ function _hints(title, hintList) {
81
+ console.log(chalk.bold(` ${title}`));
82
+ console.log('');
83
+ for (const h of hintList) {
84
+ console.log(` ${chalk.dim('›')} ${chalk.cyan(h.cmd)}`);
85
+ if (h.note) console.log(` ${chalk.dim(h.note)}`);
86
+ }
87
+ console.log('');
88
+ }
89
+
90
+ module.exports = { banner, info, fileResult, dirResult, summary, error, hints };
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs-extra');
4
+
5
+ /**
6
+ * Ensures a directory exists (no-op in dry-run).
7
+ */
8
+ async function ensureDir(destAbs, dryRun) {
9
+ if (!dryRun) {
10
+ await fs.ensureDir(destAbs);
11
+ }
12
+ }
13
+
14
+ /**
15
+ * Writes a template file to its destination.
16
+ * Returns 'created' | 'skipped' | 'overwritten'
17
+ */
18
+ async function writeFile(srcAbs, destAbs, { force, dryRun }) {
19
+ const exists = await fs.pathExists(destAbs);
20
+
21
+ if (exists && !force) {
22
+ return 'skipped';
23
+ }
24
+
25
+ if (!dryRun) {
26
+ const content = await fs.readFile(srcAbs, 'utf8');
27
+ await fs.ensureFile(destAbs);
28
+ await fs.writeFile(destAbs, content, 'utf8');
29
+ }
30
+
31
+ return exists ? 'overwritten' : 'created';
32
+ }
33
+
34
+ /**
35
+ * Makes a file executable (silently ignores errors, e.g. on Windows).
36
+ */
37
+ async function chmod(filePath, mode) {
38
+ try {
39
+ await fs.chmod(filePath, mode);
40
+ } catch (_) {
41
+ // Non-fatal — Windows doesn't support Unix permissions
42
+ }
43
+ }
44
+
45
+ module.exports = { ensureDir, writeFile, chmod };
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs-extra');
5
+
6
+ /**
7
+ * Maps indicator files → tech stack labels and relevant metadata.
8
+ */
9
+ const INDICATORS = [
10
+ { file: 'package.json', label: 'Node.js', readFile: true },
11
+ { file: 'requirements.txt', label: 'Python', readFile: true },
12
+ { file: 'pyproject.toml', label: 'Python', readFile: true },
13
+ { file: 'go.mod', label: 'Go', readFile: true },
14
+ { file: 'Cargo.toml', label: 'Rust', readFile: true },
15
+ { file: 'pom.xml', label: 'Java/Maven', readFile: false },
16
+ { file: 'build.gradle', label: 'Java/Gradle', readFile: false },
17
+ { file: 'Gemfile', label: 'Ruby', readFile: false },
18
+ { file: 'composer.json', label: 'PHP', readFile: false },
19
+ { file: 'tsconfig.json', label: 'TypeScript', readFile: false },
20
+ { file: 'next.config.js', label: 'Next.js', readFile: false },
21
+ { file: 'next.config.ts', label: 'Next.js', readFile: false },
22
+ { file: 'vite.config.ts', label: 'Vite', readFile: false },
23
+ { file: 'vite.config.js', label: 'Vite', readFile: false },
24
+ { file: 'docker-compose.yml', label: 'Docker', readFile: false },
25
+ { file: 'docker-compose.yaml','label': 'Docker', readFile: false },
26
+ { file: 'Dockerfile', label: 'Docker', readFile: false },
27
+ { file: '.terraform', label: 'Terraform', readFile: false },
28
+ ];
29
+
30
+ /**
31
+ * Detects the tech stack and returns labels + key file contents for context.
32
+ * @param {string} targetDir
33
+ * @returns {{ labels: string[], context: string }}
34
+ */
35
+ async function detect(targetDir) {
36
+ const labels = new Set();
37
+ const contextParts = [];
38
+
39
+ for (const indicator of INDICATORS) {
40
+ const filePath = path.join(targetDir, indicator.file);
41
+ const exists = await fs.pathExists(filePath);
42
+ if (!exists) continue;
43
+
44
+ labels.add(indicator.label);
45
+
46
+ if (indicator.readFile) {
47
+ try {
48
+ const content = await fs.readFile(filePath, 'utf8');
49
+ // Truncate to avoid bloating the prompt
50
+ const trimmed = content.slice(0, 1500);
51
+ contextParts.push(`--- ${indicator.file} ---\n${trimmed}`);
52
+ } catch (_) {}
53
+ }
54
+ }
55
+
56
+ return {
57
+ labels: [...labels],
58
+ context: contextParts.join('\n\n'),
59
+ };
60
+ }
61
+
62
+ module.exports = { detect };
@@ -0,0 +1,21 @@
1
+ # ──────────────────────────────────────────────────────────────────────────────
2
+ # Environment Variables
3
+ #
4
+ # Copy this file to .env and fill in the values.
5
+ # NEVER commit .env to git — it is listed in .gitignore.
6
+ # ──────────────────────────────────────────────────────────────────────────────
7
+
8
+ # ── Application ──────────────────────────────────────────────────────────────
9
+ NODE_ENV=development
10
+ PORT=3000
11
+ LOG_LEVEL=info
12
+
13
+ # ── Database ──────────────────────────────────────────────────────────────────
14
+ # DATABASE_URL=postgresql://user:password@localhost:5432/dbname
15
+
16
+ # ── External APIs ─────────────────────────────────────────────────────────────
17
+ # API_KEY=your-key-here
18
+
19
+ # ── Claude / Anthropic API (if your app calls the Claude API) ─────────────────
20
+ # ANTHROPIC_API_KEY=sk-ant-...
21
+ # ANTHROPIC_MODEL=claude-sonnet-4-6