forgedev 1.3.0 → 1.4.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/bin/chainproof.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * ChainProof CLI - local trust chain operations.
package/bin/devforge.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  import { parseCommand } from '../src/cli.js';
3
3
 
4
4
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgedev",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Universal, AI-first project scaffolding CLI with Claude Code infrastructure",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { ROOT_DIR, ensureDir, writeFile, readTemplate, replaceVars, getStackCommands } from './utils.js';
3
+ import { ROOT_DIR, ensureDir, writeFile, readTemplate, replaceVars, getStackCommands, toPascalCase } from './utils.js';
4
4
 
5
5
  const CLAUDE_TEMPLATES_DIR = path.join(ROOT_DIR, 'templates', 'claude-code');
6
6
  const DOCS_DIR = path.join(ROOT_DIR, 'docs');
@@ -13,10 +13,12 @@ export async function generateClaudeConfig(outputDir, stackConfig, options = {})
13
13
  }
14
14
  generateHooks(outputDir, stackConfig, { merge: options.mergeSettings });
15
15
  generateSkills(outputDir, stackConfig);
16
- generateAgents(outputDir, stackConfig, vars);
17
- generateCommands(outputDir, stackConfig, vars);
16
+ const agents = generateAgents(outputDir, stackConfig, vars);
17
+ const commands = generateCommands(outputDir, stackConfig, vars);
18
18
  copyPromptLibrary(outputDir);
19
19
  stampVersion(outputDir);
20
+
21
+ return { agents, commands };
20
22
  }
21
23
 
22
24
  function stampVersion(outputDir) {
@@ -30,59 +32,69 @@ function stampVersion(outputDir) {
30
32
  function buildClaudeVars(config) {
31
33
  const vars = {
32
34
  PROJECT_NAME: config.projectName,
33
- PROJECT_NAME_PASCAL: config.projectName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(''),
35
+ PROJECT_NAME_PASCAL: toPascalCase(config.projectName),
34
36
  };
35
37
 
36
38
  // Commands vary by stack (shared with composer)
37
39
  Object.assign(vars, getStackCommands(config.stackId));
38
40
 
39
- if (config.stackId === 'nextjs-fullstack') {
40
- vars.STACK_SUMMARY = 'Next.js 15 (App Router) + TypeScript + Tailwind CSS + Prisma + PostgreSQL';
41
- vars.DIR_MAP = `- src/app/ - Next.js App Router pages and API routes
41
+ const claudeVars = {
42
+ 'nextjs-fullstack': {
43
+ STACK_SUMMARY: 'Next.js 15 (App Router) + TypeScript + Tailwind CSS + Prisma + PostgreSQL',
44
+ DIR_MAP: `- src/app/ - Next.js App Router pages and API routes
42
45
  - src/lib/ - Shared utilities, database client, error handling
43
46
  - src/components/ - React components
44
47
  - prisma/ - Database schema and migrations
45
- - e2e/ - Playwright E2E tests`;
46
- } else if (config.stackId === 'fastapi-backend') {
47
- vars.STACK_SUMMARY = 'FastAPI + Python + SQLAlchemy 2.0 + PostgreSQL + Alembic';
48
- vars.DIR_MAP = `- backend/app/ - FastAPI application
48
+ - e2e/ - Playwright E2E tests`,
49
+ },
50
+ 'fastapi-backend': {
51
+ STACK_SUMMARY: 'FastAPI + Python + SQLAlchemy 2.0 + PostgreSQL + Alembic',
52
+ DIR_MAP: `- backend/app/ - FastAPI application
49
53
  - backend/app/api/ - API route handlers
50
54
  - backend/app/core/ - Config, security, error handling
51
55
  - backend/app/db/ - Database session and models
52
56
  - backend/app/models/ - SQLAlchemy models
53
57
  - backend/app/schemas/ - Pydantic schemas
54
58
  - backend/tests/ - Pytest tests
55
- - backend/alembic/ - Database migrations`;
56
- } else if (config.stackId === 'polyglot-fullstack') {
57
- vars.STACK_SUMMARY = 'Next.js 15 (frontend) + FastAPI (backend) + PostgreSQL';
58
- vars.DIR_MAP = `- frontend/ - Next.js 15 App Router application
59
+ - backend/alembic/ - Database migrations`,
60
+ },
61
+ 'polyglot-fullstack': {
62
+ STACK_SUMMARY: 'Next.js 15 (frontend) + FastAPI (backend) + PostgreSQL',
63
+ DIR_MAP: `- frontend/ - Next.js 15 App Router application
59
64
  - frontend/src/app/ - Pages and API routes
60
65
  - frontend/src/lib/ - Shared utilities
61
66
  - backend/ - FastAPI application
62
67
  - backend/app/api/ - API route handlers
63
68
  - backend/app/core/ - Config, security, error handling
64
69
  - backend/app/db/ - Database session and models
65
- - e2e/ - Playwright E2E tests`;
66
- } else if (config.stackId === 'react-express') {
67
- vars.STACK_SUMMARY = 'React (Vite) + Express + TypeScript + Prisma + PostgreSQL';
68
- vars.DIR_MAP = `- frontend/ - React (Vite) SPA
70
+ - e2e/ - Playwright E2E tests`,
71
+ },
72
+ 'react-express': {
73
+ STACK_SUMMARY: 'React (Vite) + Express + TypeScript + Prisma + PostgreSQL',
74
+ DIR_MAP: `- frontend/ - React (Vite) SPA
69
75
  - frontend/src/ - React components and pages
70
76
  - backend/ - Express API server
71
77
  - backend/src/ - Express routes and middleware
72
78
  - backend/src/routes/ - API route handlers
73
- - backend/prisma/ - Database schema and migrations`;
74
- } else if (config.stackId === 'remix-fullstack') {
75
- vars.STACK_SUMMARY = 'Remix + Vite + TypeScript + Tailwind CSS + Prisma + PostgreSQL';
76
- vars.DIR_MAP = `- app/ - Remix application
79
+ - backend/prisma/ - Database schema and migrations`,
80
+ },
81
+ 'remix-fullstack': {
82
+ STACK_SUMMARY: 'Remix + Vite + TypeScript + Tailwind CSS + Prisma + PostgreSQL',
83
+ DIR_MAP: `- app/ - Remix application
77
84
  - app/routes/ - File-based routes and API resource routes
78
85
  - app/routes/api.health.ts - Health check endpoint
79
- - prisma/ - Database schema and migrations`;
80
- } else if (config.stackId === 'hono-api') {
81
- vars.STACK_SUMMARY = 'Hono + TypeScript + Prisma + PostgreSQL';
82
- vars.DIR_MAP = `- src/ - Hono application
86
+ - prisma/ - Database schema and migrations`,
87
+ },
88
+ 'hono-api': {
89
+ STACK_SUMMARY: 'Hono + TypeScript + Prisma + PostgreSQL',
90
+ DIR_MAP: `- src/ - Hono application
83
91
  - src/routes/ - API route handlers
84
92
  - src/routes/health.ts - Health check endpoints
85
- - prisma/ - Database schema and migrations`;
93
+ - prisma/ - Database schema and migrations`,
94
+ },
95
+ };
96
+ if (claudeVars[config.stackId]) {
97
+ Object.assign(vars, claudeVars[config.stackId]);
86
98
  }
87
99
 
88
100
  // Build skills list for CLAUDE.md reference
@@ -115,20 +127,18 @@ function generateClaudeMd(outputDir, config, vars) {
115
127
  let content = readTemplate(basePath);
116
128
 
117
129
  // Read stack-specific section
118
- let stackSection = '';
119
- if (config.stackId === 'nextjs-fullstack') {
120
- stackSection = readTemplate(path.join(CLAUDE_TEMPLATES_DIR, 'claude-md', 'nextjs.md'));
121
- } else if (config.stackId === 'fastapi-backend') {
122
- stackSection = readTemplate(path.join(CLAUDE_TEMPLATES_DIR, 'claude-md', 'fastapi.md'));
123
- } else if (config.stackId === 'polyglot-fullstack') {
124
- stackSection = readTemplate(path.join(CLAUDE_TEMPLATES_DIR, 'claude-md', 'fullstack.md'));
125
- } else if (config.stackId === 'react-express') {
126
- stackSection = readTemplate(path.join(CLAUDE_TEMPLATES_DIR, 'claude-md', 'fullstack.md'));
127
- } else if (config.stackId === 'remix-fullstack') {
128
- stackSection = readTemplate(path.join(CLAUDE_TEMPLATES_DIR, 'claude-md', 'remix.md'));
129
- } else if (config.stackId === 'hono-api') {
130
- stackSection = readTemplate(path.join(CLAUDE_TEMPLATES_DIR, 'claude-md', 'hono.md'));
131
- }
130
+ const claudeMdTemplates = {
131
+ 'nextjs-fullstack': 'nextjs.md',
132
+ 'fastapi-backend': 'fastapi.md',
133
+ 'polyglot-fullstack': 'fullstack.md',
134
+ 'react-express': 'fullstack.md',
135
+ 'remix-fullstack': 'remix.md',
136
+ 'hono-api': 'hono.md',
137
+ };
138
+ const mdFile = claudeMdTemplates[config.stackId];
139
+ const stackSection = mdFile
140
+ ? readTemplate(path.join(CLAUDE_TEMPLATES_DIR, 'claude-md', mdFile))
141
+ : '';
132
142
 
133
143
  content = content.replace('{{STACK_SPECIFIC_RULES}}', stackSection);
134
144
  content = replaceVars(content, vars);
@@ -137,21 +147,19 @@ function generateClaudeMd(outputDir, config, vars) {
137
147
  }
138
148
 
139
149
  function generateHooks(outputDir, config, options = {}) {
140
- let hookFile;
150
+ const hookConfig = {
151
+ 'nextjs-fullstack': { file: 'typescript.json', autofix: 'autofix-typescript.mjs' },
152
+ 'fastapi-backend': { file: 'python.json', autofix: 'autofix-python.mjs' },
153
+ 'polyglot-fullstack': { file: 'polyglot.json', autofix: 'autofix-polyglot.mjs' },
154
+ 'react-express': { file: 'typescript.json', autofix: 'autofix-typescript.mjs' },
155
+ 'remix-fullstack': { file: 'typescript.json', autofix: 'autofix-typescript.mjs' },
156
+ 'hono-api': { file: 'typescript.json', autofix: 'autofix-typescript.mjs' },
157
+ };
158
+ const hook = hookConfig[config.stackId];
159
+ const hookFile = hook?.file;
141
160
  const scriptFiles = ['guard-protected-files.mjs', 'code-hygiene.mjs', 'pre-commit-gate.mjs'];
142
-
143
- if (config.stackId === 'nextjs-fullstack' || config.stackId === 'remix-fullstack' || config.stackId === 'hono-api') {
144
- hookFile = 'typescript.json';
145
- scriptFiles.push('autofix-typescript.mjs');
146
- } else if (config.stackId === 'fastapi-backend') {
147
- hookFile = 'python.json';
148
- scriptFiles.push('autofix-python.mjs');
149
- } else if (config.stackId === 'polyglot-fullstack') {
150
- hookFile = 'polyglot.json';
151
- scriptFiles.push('autofix-polyglot.mjs');
152
- } else if (config.stackId === 'react-express') {
153
- hookFile = 'typescript.json';
154
- scriptFiles.push('autofix-typescript.mjs');
161
+ if (hook?.autofix) {
162
+ scriptFiles.push(hook.autofix);
155
163
  }
156
164
 
157
165
  const settingsPath = path.join(outputDir, '.claude', 'settings.json');
@@ -160,7 +168,7 @@ function generateHooks(outputDir, config, options = {}) {
160
168
  // Merge hooks into existing settings.json
161
169
  const existing = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
162
170
  const incoming = JSON.parse(readTemplate(path.join(CLAUDE_TEMPLATES_DIR, 'hooks', hookFile)));
163
- const merged = deepMergeSettings(existing, incoming);
171
+ const merged = mergeHooksIntoSettings(existing, incoming);
164
172
  writeFile(settingsPath, JSON.stringify(merged, null, 2));
165
173
  } else {
166
174
  const hookPath = path.join(CLAUDE_TEMPLATES_DIR, 'hooks', hookFile);
@@ -179,7 +187,7 @@ function generateHooks(outputDir, config, options = {}) {
179
187
  }
180
188
  }
181
189
 
182
- function deepMergeSettings(existing, incoming) {
190
+ function mergeHooksIntoSettings(existing, incoming) {
183
191
  const merged = { ...existing };
184
192
  if (incoming.hooks) {
185
193
  merged.hooks = merged.hooks || {};
@@ -264,9 +272,14 @@ function generateAgents(outputDir, config, vars) {
264
272
  if (fs.existsSync(srcPath)) {
265
273
  let content = readTemplate(srcPath);
266
274
  content = replaceVars(content, vars);
275
+ content = stampManaged(content);
267
276
  writeFile(path.join(outputDir, '.claude', 'agents', agent), content);
268
277
  }
269
278
  }
279
+
280
+ // Clean up orphaned agents from older versions
281
+ const removed = cleanOrphans(path.join(outputDir, '.claude', 'agents'), agents);
282
+ return { count: agents.length, removed };
270
283
  }
271
284
 
272
285
  function generateCommands(outputDir, config, vars) {
@@ -306,9 +319,52 @@ function generateCommands(outputDir, config, vars) {
306
319
  if (fs.existsSync(srcPath)) {
307
320
  let content = readTemplate(srcPath);
308
321
  content = replaceVars(content, vars);
322
+ content = stampManaged(content);
309
323
  writeFile(path.join(outputDir, '.claude', 'commands', cmd), content);
310
324
  }
311
325
  }
326
+
327
+ // Clean up orphaned commands from older versions
328
+ const removed = cleanOrphans(path.join(outputDir, '.claude', 'commands'), commands);
329
+ return { count: commands.length, removed };
330
+ }
331
+
332
+ const MANAGED_MARKER = '<!-- Generated by DevForge -->';
333
+
334
+ /**
335
+ * Append the DevForge managed marker to generated file content.
336
+ */
337
+ function stampManaged(content) {
338
+ return content.trimEnd() + '\n\n' + MANAGED_MARKER + '\n';
339
+ }
340
+
341
+ /**
342
+ * Remove DevForge-managed .md files that are no longer in the current manifest.
343
+ * Only deletes files stamped with the DevForge marker. User-created custom
344
+ * files are left untouched. Returns array of removed filenames.
345
+ */
346
+ function cleanOrphans(dir, expectedFiles) {
347
+ const removed = [];
348
+ if (!fs.existsSync(dir)) return removed;
349
+
350
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
351
+ const existing = entries
352
+ .filter(e => e.isFile() && !e.isSymbolicLink() && e.name.endsWith('.md'))
353
+ .map(e => e.name);
354
+ const expectedSet = new Set(expectedFiles);
355
+ for (const file of existing) {
356
+ if (!expectedSet.has(file)) {
357
+ const filePath = path.join(dir, file);
358
+ const stat = fs.statSync(filePath);
359
+ if (stat.size > 1_000_000) continue; // skip unexpectedly large files
360
+ const content = fs.readFileSync(filePath, 'utf-8');
361
+ if (content.includes(MANAGED_MARKER)) {
362
+ fs.unlinkSync(filePath);
363
+ removed.push(file);
364
+ }
365
+ }
366
+ }
367
+ return removed;
312
368
  }
313
369
 
314
370
  function copyPromptLibrary(outputDir) {