forgedev 1.3.0 → 1.4.1

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 (46) hide show
  1. package/bin/chainproof.js +1 -1
  2. package/bin/devforge.js +1 -1
  3. package/package.json +1 -1
  4. package/src/chainproof-bridge.js +9 -2
  5. package/src/ci-mode.js +3 -4
  6. package/src/claude-configurator.js +114 -58
  7. package/src/composer.js +17 -128
  8. package/src/doctor-checks-chainproof.js +14 -11
  9. package/src/doctor-checks.js +3 -19
  10. package/src/index.js +6 -34
  11. package/src/init-mode.js +14 -3
  12. package/src/recommender.js +319 -310
  13. package/src/uat-generator.js +105 -93
  14. package/src/update.js +56 -12
  15. package/src/utils.js +245 -116
  16. package/templates/auth/jwt-custom/backend/app/api/auth.py.template +39 -45
  17. package/templates/auth/jwt-custom/backend/app/core/security.py.template +44 -37
  18. package/templates/backend/express/package.json.template +35 -33
  19. package/templates/backend/express/src/lib/prisma.ts.template +32 -0
  20. package/templates/backend/express/src/routes/health.ts.template +35 -27
  21. package/templates/backend/fastapi/backend/app/main.py.template +67 -60
  22. package/templates/backend/fastapi/backend/requirements.txt.template +18 -16
  23. package/templates/backend/hono/package.json.template +33 -31
  24. package/templates/backend/hono/src/lib/prisma.ts.template +32 -0
  25. package/templates/backend/hono/src/routes/health.ts.template +38 -27
  26. package/templates/base/.gitignore.template +32 -29
  27. package/templates/claude-code/commands/workflows.md +52 -52
  28. package/templates/database/prisma-postgres/prisma/schema.prisma.template +19 -18
  29. package/templates/database/sqlalchemy-postgres/backend/alembic/env.py.template +39 -40
  30. package/templates/database/sqlalchemy-postgres/backend/alembic.ini.template +38 -36
  31. package/templates/database/sqlalchemy-postgres/backend/app/db/session.py.template +50 -48
  32. package/templates/frontend/nextjs/package.json.template +45 -43
  33. package/templates/frontend/remix/package.json.template +41 -39
  34. package/templates/infra/docker/.dockerignore.template +16 -0
  35. package/templates/infra/docker-compose/docker-compose.yml.template +22 -19
  36. package/templates/infra/github-actions/.github/workflows/ci.yml.template +61 -52
  37. package/templates/infra/k8s/k8s/deployment.yml.template +70 -0
  38. package/templates/infra/k8s/k8s/hpa.yml.template +24 -0
  39. package/templates/infra/k8s/k8s/ingress.yml.template +26 -0
  40. package/templates/infra/k8s/k8s/kustomization.yml.template +13 -0
  41. package/templates/infra/k8s/k8s/namespace.yml.template +4 -0
  42. package/templates/infra/k8s/k8s/networkpolicy.yml.template +41 -0
  43. package/templates/infra/k8s/k8s/secrets.yml.template +10 -0
  44. package/templates/infra/k8s/k8s/service.yml.template +15 -0
  45. package/templates/testing/load/k6/README.md.template +48 -0
  46. package/templates/testing/load/k6/load-test.js.template +57 -0
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.1",
4
4
  "description": "Universal, AI-first project scaffolding CLI with Claude Code infrastructure",
5
5
  "type": "module",
6
6
  "bin": {
@@ -43,8 +43,15 @@ export function verifySignature(payload, signatureB64, publicKeyPem) {
43
43
  publicKeyPem,
44
44
  Buffer.from(signatureB64, 'base64')
45
45
  );
46
- } catch {
47
- // Invalid key or malformed signature
46
+ } catch (err) {
47
+ // Verification should never throw — any failure means "not verified"
48
+ if (err?.code?.startsWith?.('ERR_OSSL') ||
49
+ err?.code?.startsWith?.('ERR_CRYPTO') ||
50
+ err instanceof TypeError) {
51
+ return false;
52
+ }
53
+ // Log unexpected errors for debugging but still return false
54
+ console.warn('Unexpected crypto error during signature verification:', err?.code || err?.message);
48
55
  return false;
49
56
  }
50
57
  }
package/src/ci-mode.js CHANGED
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import chalk from 'chalk';
4
4
  import { log } from './utils.js';
5
- import { scanProject, detectStack } from './scanner.js';
5
+ import { scanProject } from './scanner.js';
6
6
  import { runAllChecks } from './doctor-checks.js';
7
7
  import { generateReport } from './doctor-prompts.js';
8
8
 
@@ -18,10 +18,9 @@ export async function runCI(projectDir) {
18
18
  log.info(' DevForge CI Automated project health check');
19
19
  console.log('');
20
20
 
21
- // Scan project
21
+ // Scan project (scanProject already calls detectStack internally)
22
22
  const scan = scanProject(resolvedDir);
23
- const stack = detectStack(resolvedDir, scan);
24
- log.dim(` Stack: ${stack || 'unknown'}`);
23
+ log.dim(` Stack: ${scan.stackId || 'unknown'}`);
25
24
 
26
25
  // Run all checks
27
26
  const issues = runAllChecks(resolvedDir, scan);
@@ -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) {
package/src/composer.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { ROOT_DIR, ensureDir, readTemplate, replaceVars, toPascalCase, toSnakeCase, getStackCommands, copyEnvCmd, log } from './utils.js';
3
+ import { ROOT_DIR, ensureDir, readTemplate, replaceVars, toPascalCase, toSnakeCase, getStackMetadata, walkDir, log } from './utils.js';
4
4
 
5
5
  const TEMPLATES_DIR = path.join(ROOT_DIR, 'templates');
6
6
 
@@ -53,127 +53,28 @@ export function buildVariables(stackConfig) {
53
53
  vars.AUTH_TYPE = stackConfig.auth || 'none';
54
54
  vars.DEPLOYMENT = stackConfig.deployment || 'docker';
55
55
 
56
- // Commands vary by stack (shared with claude-configurator)
57
- Object.assign(vars, getStackCommands(stackConfig.stackId));
58
-
59
- if (stackConfig.stackId === 'nextjs-fullstack') {
60
- vars.STACK_DESCRIPTION = 'Next.js full-stack application with TypeScript, Tailwind CSS, Prisma, and PostgreSQL';
56
+ // All per-stack values come from a single metadata source
57
+ const meta = getStackMetadata(stackConfig.stackId);
58
+ if (meta) {
59
+ Object.assign(vars, meta.commands);
60
+ vars.STACK_DESCRIPTION = meta.description;
61
+ vars.EXTRA_IGNORES = meta.extraIgnores;
62
+ vars.APP_PORT = meta.port;
63
+ vars.SETUP_COMMANDS = meta.setupCommands();
64
+ vars.AVAILABLE_SCRIPTS = meta.availableScripts;
65
+ } else {
66
+ vars.STACK_DESCRIPTION = '';
61
67
  vars.EXTRA_IGNORES = '';
62
- } else if (stackConfig.stackId === 'fastapi-backend') {
63
- vars.STACK_DESCRIPTION = 'FastAPI backend service with SQLAlchemy, PostgreSQL, and Alembic';
64
- vars.EXTRA_IGNORES = '\n# Python\n__pycache__/\n*.pyc\nvenv/\n.venv/\n*.egg-info/';
65
- } else if (stackConfig.stackId === 'polyglot-fullstack') {
66
- vars.STACK_DESCRIPTION = 'Full-stack application with Next.js frontend and FastAPI backend';
67
- vars.EXTRA_IGNORES = '\n# Python\n__pycache__/\n*.pyc\nvenv/\n.venv/\n*.egg-info/';
68
- } else if (stackConfig.stackId === 'react-express') {
69
- vars.STACK_DESCRIPTION = 'Full-stack application with React (Vite) frontend and Express backend';
70
- vars.EXTRA_IGNORES = '\ndist/';
71
- } else if (stackConfig.stackId === 'remix-fullstack') {
72
- vars.STACK_DESCRIPTION = 'Full-stack Remix application with Vite, Tailwind CSS, and PostgreSQL';
73
- vars.EXTRA_IGNORES = '\nbuild/';
74
- } else if (stackConfig.stackId === 'hono-api') {
75
- vars.STACK_DESCRIPTION = 'Hono API service with TypeScript, Prisma, and PostgreSQL';
76
- vars.EXTRA_IGNORES = '\ndist/';
68
+ vars.APP_PORT = '3000';
69
+ vars.SETUP_COMMANDS = '';
70
+ vars.AVAILABLE_SCRIPTS = '';
77
71
  }
78
72
 
79
- // Setup commands for README
80
- vars.SETUP_COMMANDS = buildSetupCommands(stackConfig);
81
- vars.AVAILABLE_SCRIPTS = buildAvailableScripts(stackConfig);
73
+ vars.IMAGE_TAG = '0.1.0';
82
74
 
83
75
  return vars;
84
76
  }
85
77
 
86
- function buildSetupCommands(config) {
87
- if (config.stackId === 'nextjs-fullstack') {
88
- return `npm install
89
- ${copyEnvCmd()}
90
- npx prisma db push
91
- npm run dev`;
92
- }
93
- if (config.stackId === 'fastapi-backend') {
94
- return `cd backend
95
- python -m venv venv
96
- source venv/bin/activate
97
- pip install -r requirements.txt
98
- ${copyEnvCmd()}
99
- uvicorn app.main:app --reload`;
100
- }
101
- if (config.stackId === 'polyglot-fullstack') {
102
- return `docker compose up -d postgres
103
- # Frontend
104
- cd frontend && npm install && npm run dev
105
- # Backend
106
- cd backend && pip install -r requirements.txt && uvicorn app.main:app --reload`;
107
- }
108
- if (config.stackId === 'react-express') {
109
- return `# Frontend
110
- cd frontend && npm install && npm run dev
111
- # Backend (in a separate terminal)
112
- cd backend && npm install && ${copyEnvCmd()} && npx prisma db push && npm run dev`;
113
- }
114
- if (config.stackId === 'remix-fullstack') {
115
- return `npm install
116
- ${copyEnvCmd()}
117
- npx prisma db push
118
- npm run dev`;
119
- }
120
- if (config.stackId === 'hono-api') {
121
- return `npm install
122
- ${copyEnvCmd()}
123
- npx prisma db push
124
- npm run dev`;
125
- }
126
- return '';
127
- }
128
-
129
- function buildAvailableScripts(config) {
130
- if (config.stackId === 'nextjs-fullstack') {
131
- return `- \`npm run dev\`: Start development server
132
- - \`npm run build\`: Production build
133
- - \`npm run lint\`: Run ESLint
134
- - \`npx prisma studio\`: Database GUI
135
- - \`npx vitest\`: Run unit tests
136
- - \`npx playwright test\`: Run E2E tests`;
137
- }
138
- if (config.stackId === 'fastapi-backend') {
139
- return `- \`uvicorn app.main:app --reload\`: Start dev server
140
- - \`pytest\`: Run tests
141
- - \`ruff check .\`: Run linter
142
- - \`alembic upgrade head\`: Run migrations`;
143
- }
144
- if (config.stackId === 'polyglot-fullstack') {
145
- return `- \`docker compose up\`: Start all services
146
- - \`docker compose up -d postgres\`: Start database only
147
- - Frontend: \`cd frontend && npm run dev\`
148
- - Backend: \`cd backend && uvicorn app.main:app --reload\``;
149
- }
150
- if (config.stackId === 'react-express') {
151
- return `- Frontend: \`cd frontend && npm run dev\` (Vite dev server)
152
- - Backend: \`cd backend && npm run dev\` (Express with tsx watch)
153
- - \`cd backend && npm run build\`: Build backend for production
154
- - \`cd frontend && npm run build\`: Build frontend for production
155
- - \`cd backend && npx prisma studio\`: Database GUI
156
- - \`cd frontend && npx vitest\`: Run frontend tests`;
157
- }
158
- if (config.stackId === 'remix-fullstack') {
159
- return `- \`npm run dev\`: Start Remix dev server
160
- - \`npm run build\`: Production build
161
- - \`npm run start\`: Start production server
162
- - \`npm run lint\`: Run ESLint
163
- - \`npx prisma studio\`: Database GUI
164
- - \`npx vitest\`: Run unit tests`;
165
- }
166
- if (config.stackId === 'hono-api') {
167
- return `- \`npm run dev\`: Start Hono dev server (tsx watch)
168
- - \`npm run build\`: Compile TypeScript
169
- - \`npm run start\`: Start production server
170
- - \`npm run lint\`: Run ESLint
171
- - \`npx prisma studio\`: Database GUI
172
- - \`npx vitest\`: Run unit tests`;
173
- }
174
- return '';
175
- }
176
-
177
78
  export function processTemplateDir(templateDir, outputDir, variables, prefix) {
178
79
  const entries = walkDir(templateDir);
179
80
 
@@ -207,19 +108,7 @@ export function processTemplateDir(templateDir, outputDir, variables, prefix) {
207
108
  }
208
109
  }
209
110
 
210
- function walkDir(dir) {
211
- const results = [];
212
- const entries = fs.readdirSync(dir, { withFileTypes: true });
213
- for (const entry of entries) {
214
- const fullPath = path.join(dir, entry.name);
215
- if (entry.isDirectory()) {
216
- results.push(...walkDir(fullPath));
217
- } else {
218
- results.push(fullPath);
219
- }
220
- }
221
- return results;
222
- }
111
+ // walkDir is imported from utils.js (shared with doctor-checks.js)
223
112
 
224
113
  function injectAuthDependencies(outputDir, stackConfig) {
225
114
  if (!stackConfig.auth || stackConfig.auth === 'none') return;
@@ -66,17 +66,20 @@ export function checkChainproofIntegrity(projectDir) {
66
66
  effort: 'medium',
67
67
  }];
68
68
  }
69
- } catch {
70
- // Malformed chain.json
71
- return [{
72
- severity: 'critical',
73
- title: 'ChainProof chain.json is malformed',
74
- impact: 'Cannot verify trust chain integrity',
75
- files: ['.chainproof/chain.json'],
76
- autoFixable: false,
77
- promptId: 'CHAINPROOF_MALFORMED',
78
- effort: 'medium',
79
- }];
69
+ } catch (err) {
70
+ // JSON parse errors → malformed file; anything else should propagate
71
+ if (err instanceof SyntaxError) {
72
+ return [{
73
+ severity: 'critical',
74
+ title: 'ChainProof chain.json is malformed',
75
+ impact: 'Cannot verify trust chain integrity',
76
+ files: ['.chainproof/chain.json'],
77
+ autoFixable: false,
78
+ promptId: 'CHAINPROOF_MALFORMED',
79
+ effort: 'medium',
80
+ }];
81
+ }
82
+ throw err;
80
83
  }
81
84
 
82
85
  return [];
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { walkDir, DEFAULT_SKIP_DIRS } from './utils.js';
3
4
  import {
4
5
  checkChainproofExists,
5
6
  checkChainproofIntegrity,
@@ -739,24 +740,7 @@ function checkAIPrompts(projectDir) {
739
740
  // Helpers
740
741
 
741
742
  function findFiles(dir, ext) {
742
- const results = [];
743
- if (!fs.existsSync(dir)) return results;
744
-
745
- try {
746
- const entries = fs.readdirSync(dir, { withFileTypes: true });
747
- for (const entry of entries) {
748
- const fullPath = path.join(dir, entry.name);
749
- if (entry.isDirectory()) {
750
- if (['node_modules', '.next', '__pycache__', '.git', 'venv', '.venv', 'dist', 'build'].includes(entry.name)) continue;
751
- results.push(...findFiles(fullPath, ext));
752
- } else if (entry.name.endsWith(ext)) {
753
- results.push(fullPath);
754
- }
755
- }
756
- } catch {
757
- // Permission errors, etc.
758
- }
759
-
760
- return results;
743
+ if (!fs.existsSync(dir)) return [];
744
+ return walkDir(dir, { ext, skipDirs: DEFAULT_SKIP_DIRS });
761
745
  }
762
746
 
package/src/index.js CHANGED
@@ -2,7 +2,7 @@ import path from 'node:path';
2
2
  import fs from 'node:fs';
3
3
  import { execSync } from 'node:child_process';
4
4
  import chalk from 'chalk';
5
- import { log, toKebabCase, copyEnvCmd } from './utils.js';
5
+ import { log, toKebabCase, getStackMetadata } from './utils.js';
6
6
  import { askServiceType, askRefinements, askNewMode, confirmStack } from './prompts.js';
7
7
  import { recommend } from './recommender.js';
8
8
  import { compose } from './composer.js';
@@ -123,39 +123,11 @@ export function printNextSteps(projectName, config, isGuided = false) {
123
123
  console.log(chalk.bold(' Next steps:'));
124
124
  console.log(` cd ${projectName}`);
125
125
 
126
- if (config.stackId === 'nextjs-fullstack') {
127
- console.log(' npm install');
128
- console.log(` ${copyEnvCmd()}`);
129
- console.log(' npx prisma db push');
130
- console.log(' npm run dev');
131
- } else if (config.stackId === 'fastapi-backend') {
132
- console.log(' cd backend');
133
- console.log(' python -m venv venv');
134
- console.log(' source venv/bin/activate # or venv\\Scripts\\activate on Windows');
135
- console.log(' pip install -r requirements.txt');
136
- console.log(` ${copyEnvCmd()}`);
137
- console.log(' uvicorn app.main:app --reload');
138
- } else if (config.stackId === 'polyglot-fullstack') {
139
- console.log(' docker compose up -d postgres');
140
- console.log(' # Frontend:');
141
- console.log(' cd frontend && npm install && npm run dev');
142
- console.log(' # Backend:');
143
- console.log(' cd backend && pip install -r requirements.txt && uvicorn app.main:app --reload');
144
- } else if (config.stackId === 'react-express') {
145
- console.log(' # Frontend:');
146
- console.log(' cd frontend && npm install && npm run dev');
147
- console.log(' # Backend (in a separate terminal):');
148
- console.log(` cd backend && npm install && ${copyEnvCmd()} && npx prisma db push && npm run dev`);
149
- } else if (config.stackId === 'remix-fullstack') {
150
- console.log(' npm install');
151
- console.log(` ${copyEnvCmd()}`);
152
- console.log(' npx prisma db push');
153
- console.log(' npm run dev');
154
- } else if (config.stackId === 'hono-api') {
155
- console.log(' npm install');
156
- console.log(` ${copyEnvCmd()}`);
157
- console.log(' npx prisma db push');
158
- console.log(' npm run dev');
126
+ const meta = getStackMetadata(config.stackId);
127
+ if (meta) {
128
+ for (const line of meta.setupCommands().split('\n')) {
129
+ console.log(` ${line}`);
130
+ }
159
131
  }
160
132
 
161
133
  if (config.claudeCode) {