create-sdd-project 0.2.1 → 0.2.2

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.
@@ -314,7 +314,6 @@ function adaptBackendStandards(template, scan) {
314
314
  );
315
315
 
316
316
  // Update Technology Stack
317
- const framework = scan.backend.framework || 'Unknown';
318
317
  const orm = scan.backend.orm;
319
318
  const db = scan.backend.db;
320
319
  const lang = scan.language === 'typescript' ? 'TypeScript' : 'JavaScript';
@@ -325,8 +324,10 @@ function adaptBackendStandards(template, scan) {
325
324
 
326
325
  let stackLines = [
327
326
  `- **Runtime**: Node.js with ${lang}`,
328
- `- **Framework**: ${framework}`,
329
327
  ];
328
+ if (scan.backend.framework) {
329
+ stackLines.push(`- **Framework**: ${scan.backend.framework}`);
330
+ }
330
331
  if (orm) {
331
332
  stackLines.push(`- **ORM**: ${orm}${db ? ` (${db})` : ''}`);
332
333
  } else if (db) {
@@ -352,21 +353,23 @@ function adaptBackendStandards(template, scan) {
352
353
  const patternLabel = patternLabels[scan.srcStructure.pattern] || 'Custom';
353
354
 
354
355
  const archStructure = buildArchitectureTree(scan);
356
+ // Robust: match any "## Architecture — <anything>" heading up to "## Naming"
355
357
  content = content.replace(
356
- /## Architecture — DDD Layered\n\n```\n[\s\S]*?```\n\n### Layer Rules\n\n[\s\S]*?(?=\n## Naming)/,
358
+ /## Architecture — [^\n]+\n\n```\n[\s\S]*?```\n\n(?:### Layer Rules\n\n[\s\S]*?)?(?=\n## Naming)/,
357
359
  `## Architecture — ${patternLabel}\n\n\`\`\`\n${archStructure}\`\`\`\n\n<!-- TODO: Add layer rules that match your project's architecture. -->\n\n`
358
360
  );
359
361
 
360
362
  // Update Database Patterns section if not Prisma
363
+ // Robust: match from "## Database Patterns" to next "## " heading
361
364
  if (!scan.backend.orm) {
362
365
  content = content.replace(
363
- /## Database Patterns\n\n### Prisma Best Practices[\s\S]*?### Repository Pattern[\s\S]*?```\n/,
364
- `## Database Patterns\n\n<!-- TODO: Add database access patterns for your project. -->\n\n`
366
+ /## Database Patterns\n\n[\s\S]*?(?=\n## )/,
367
+ `## Database Patterns\n\n<!-- TODO: Add database access patterns for your project. -->\n`
365
368
  );
366
369
  } else if (scan.backend.orm !== 'Prisma') {
367
370
  content = content.replace(
368
- /## Database Patterns\n\n### Prisma Best Practices[\s\S]*?### Repository Pattern[\s\S]*?```\n/,
369
- `## Database Patterns\n\n<!-- TODO: Add ${scan.backend.orm} best practices and patterns for your project. -->\n\n`
371
+ /## Database Patterns\n\n[\s\S]*?(?=\n## )/,
372
+ `## Database Patterns\n\n<!-- TODO: Add ${scan.backend.orm} best practices and patterns for your project. -->\n`
370
373
  );
371
374
  }
372
375
 
@@ -444,7 +447,7 @@ function buildArchitectureTree(scan) {
444
447
  }
445
448
 
446
449
  function capitalizeFramework(name) {
447
- const map = { jest: 'Jest', vitest: 'Vitest', mocha: 'Mocha' };
450
+ const map = { jest: 'Jest', vitest: 'Vitest', mocha: 'Mocha', playwright: 'Playwright', cypress: 'Cypress' };
448
451
  return map[name] || name;
449
452
  }
450
453
 
@@ -467,15 +470,17 @@ function adaptAgentsMd(template, config, scan) {
467
470
  const tree = rootDirs.map((d) => `├── ${d.replace(/\/$/, '/')} `).join('\n');
468
471
  const treeBlock = `\`\`\`\nproject/\n${tree}\n└── docs/ ← Documentation\n\`\`\``;
469
472
 
473
+ // Robust: flexible whitespace between CONFIG comment and code block
470
474
  content = content.replace(
471
- /<!-- CONFIG: Adjust directories.*?-->\n\n```\nproject\/\n[\s\S]*?```/,
475
+ /<!-- CONFIG: Adjust directories[^>]*-->\n+```\nproject\/\n[\s\S]*?```/,
472
476
  treeBlock
473
477
  );
474
478
 
475
479
  // If not monorepo, simplify the install instructions
480
+ // Robust: match any number of table rows (not hardcoded count)
476
481
  if (!scan.isMonorepo) {
477
482
  content = content.replace(
478
- /\*\*Critical\*\*: NEVER install dependencies in the root directory\.\n\n\| Action.*\n\|.*\n\|.*\n\|.*\n\|.*\n/,
483
+ /\*\*Critical\*\*: NEVER install dependencies in the root directory\.\n\n(\|.*\n)+/,
479
484
  ''
480
485
  );
481
486
  }
@@ -528,9 +533,11 @@ function configureKeyFacts(template, config, scan) {
528
533
 
529
534
  // Technology Stack from scan
530
535
  if (scan.backend.detected) {
531
- const fw = scan.backend.framework || 'Unknown';
532
536
  const runtime = scan.language === 'typescript' ? 'Node.js (TypeScript)' : 'Node.js';
533
- content = content.replace('[Framework, runtime, version]', `${fw}, ${runtime}`);
537
+ const backendLabel = scan.backend.framework
538
+ ? `${scan.backend.framework}, ${runtime}`
539
+ : runtime;
540
+ content = content.replace('[Framework, runtime, version]', backendLabel);
534
541
 
535
542
  if (scan.backend.db) {
536
543
  content = content.replace('[Type, host, port]', scan.backend.db);
package/lib/prompts.js CHANGED
@@ -47,29 +47,29 @@ function askMultiline(rl, question) {
47
47
  console.log(' (Enter text below. Empty line to finish, or press Enter to skip)');
48
48
  const lines = [];
49
49
  let firstLine = true;
50
+ let done = false;
50
51
 
51
- const onLine = (line) => {
52
+ const handler = (line) => {
53
+ if (done) return;
52
54
  if (firstLine && line.trim() === '') {
53
- rl.removeListener('line', onLine);
55
+ done = true;
56
+ rl.removeListener('line', handler);
54
57
  resolve('');
55
58
  return;
56
59
  }
57
60
  firstLine = false;
58
61
  if (line.trim() === '') {
59
- rl.removeListener('line', onLine);
62
+ done = true;
63
+ rl.removeListener('line', handler);
60
64
  resolve(lines.join('\n'));
61
65
  } else {
62
66
  lines.push(line);
67
+ process.stdout.write(' > ');
63
68
  }
64
69
  };
65
70
 
66
71
  process.stdout.write(' > ');
67
- rl.on('line', (line) => {
68
- if (lines.length > 0 || line.trim() !== '') {
69
- process.stdout.write(' > ');
70
- }
71
- onLine(line);
72
- });
72
+ rl.on('line', handler);
73
73
  });
74
74
  }
75
75
 
package/lib/scanner.js CHANGED
@@ -67,11 +67,12 @@ function detectBackend(dir, pkg) {
67
67
 
68
68
  // Framework detection
69
69
  const frameworks = [
70
- { key: 'express', dep: 'express', label: 'Express' },
71
- { key: 'fastify', dep: 'fastify', label: 'Fastify' },
72
- { key: 'koa', dep: 'koa', label: 'Koa' },
73
- { key: 'nestjs', dep: '@nestjs/core', label: 'NestJS' },
74
- { key: 'hapi', dep: '@hapi/hapi', label: 'Hapi' },
70
+ { dep: 'express', label: 'Express' },
71
+ { dep: 'fastify', label: 'Fastify' },
72
+ { dep: 'koa', label: 'Koa' },
73
+ { dep: '@nestjs/core', label: 'NestJS' },
74
+ { dep: '@hapi/hapi', label: 'Hapi' },
75
+ { dep: '@adonisjs/core', label: 'AdonisJS' },
75
76
  ];
76
77
 
77
78
  for (const fw of frameworks) {
@@ -84,11 +85,14 @@ function detectBackend(dir, pkg) {
84
85
 
85
86
  // ORM detection
86
87
  const orms = [
87
- { key: 'prisma', dep: '@prisma/client', label: 'Prisma' },
88
- { key: 'mongoose', dep: 'mongoose', label: 'Mongoose' },
89
- { key: 'typeorm', dep: 'typeorm', label: 'TypeORM' },
90
- { key: 'sequelize', dep: 'sequelize', label: 'Sequelize' },
91
- { key: 'drizzle', dep: 'drizzle-orm', label: 'Drizzle' },
88
+ { dep: '@prisma/client', label: 'Prisma' },
89
+ { dep: 'mongoose', label: 'Mongoose' },
90
+ { dep: 'typeorm', label: 'TypeORM' },
91
+ { dep: 'sequelize', label: 'Sequelize' },
92
+ { dep: 'drizzle-orm', label: 'Drizzle' },
93
+ { dep: 'knex', label: 'Knex' },
94
+ { dep: '@mikro-orm/core', label: 'MikroORM' },
95
+ { dep: 'objection', label: 'Objection.js' },
92
96
  ];
93
97
 
94
98
  for (const orm of orms) {
@@ -129,9 +133,13 @@ function detectFrontend(dir, pkg) {
129
133
  const deps = getAllDeps(pkg);
130
134
  const result = { detected: false, framework: null, styling: null, components: null, state: null };
131
135
 
132
- // Framework detection
136
+ // Framework detection (order matters — more specific first)
133
137
  const frameworks = [
134
138
  { dep: 'next', label: 'Next.js' },
139
+ { dep: 'nuxt', label: 'Nuxt' },
140
+ { dep: '@remix-run/react', label: 'Remix' },
141
+ { dep: 'astro', label: 'Astro' },
142
+ { dep: 'solid-js', label: 'SolidJS' },
135
143
  { dep: 'react', label: 'React' },
136
144
  { dep: 'vue', label: 'Vue' },
137
145
  { dep: '@angular/core', label: 'Angular' },
@@ -155,6 +163,8 @@ function detectFrontend(dir, pkg) {
155
163
  // Component libraries
156
164
  if (deps['@radix-ui/react-dialog'] || deps['@radix-ui/react-select'] || hasRadixDep(deps)) {
157
165
  result.components = 'Radix UI';
166
+ } else if (deps['@headlessui/react']) {
167
+ result.components = 'Headless UI';
158
168
  } else if (deps['@mui/material']) {
159
169
  result.components = 'Material UI';
160
170
  } else if (deps['@chakra-ui/react']) {
@@ -166,6 +176,8 @@ function detectFrontend(dir, pkg) {
166
176
  // State management
167
177
  if (deps['zustand']) result.state = 'Zustand';
168
178
  else if (deps['@reduxjs/toolkit'] || deps['redux']) result.state = 'Redux';
179
+ else if (deps['jotai']) result.state = 'Jotai';
180
+ else if (deps['@tanstack/react-query']) result.state = 'TanStack Query';
169
181
  else if (deps['recoil']) result.state = 'Recoil';
170
182
  else if (deps['pinia']) result.state = 'Pinia';
171
183
  else if (deps['mobx']) result.state = 'MobX';
@@ -280,13 +292,14 @@ function detectTests(dir, pkg) {
280
292
  const deps = getAllDeps(pkg);
281
293
  const result = {
282
294
  framework: 'none',
295
+ e2eFramework: null,
283
296
  hasConfig: false,
284
297
  testFiles: 0,
285
298
  testDirs: [],
286
299
  estimatedCoverage: 'none',
287
300
  };
288
301
 
289
- // Framework detection
302
+ // Unit test framework detection
290
303
  if (deps['jest'] || deps['@jest/core'] || deps['ts-jest'] || deps['@types/jest']) {
291
304
  result.framework = 'jest';
292
305
  } else if (deps['vitest']) {
@@ -295,11 +308,21 @@ function detectTests(dir, pkg) {
295
308
  result.framework = 'mocha';
296
309
  }
297
310
 
311
+ // E2E test framework detection (supplement, doesn't override unit framework)
312
+ result.e2eFramework = null;
313
+ if (deps['@playwright/test'] || deps['playwright']) {
314
+ result.e2eFramework = 'playwright';
315
+ } else if (deps['cypress']) {
316
+ result.e2eFramework = 'cypress';
317
+ }
318
+
298
319
  // Config files
299
320
  const configFiles = [
300
321
  'jest.config.js', 'jest.config.ts', 'jest.config.mjs', 'jest.config.cjs',
301
322
  'vitest.config.js', 'vitest.config.ts', 'vitest.config.mjs',
302
323
  '.mocharc.yml', '.mocharc.json', '.mocharc.js',
324
+ 'playwright.config.ts', 'playwright.config.js',
325
+ 'cypress.config.ts', 'cypress.config.js',
303
326
  ];
304
327
  result.hasConfig = configFiles.some((f) => fs.existsSync(path.join(dir, f)));
305
328
 
@@ -417,7 +440,10 @@ function detectDatabaseFromPrisma(dir) {
417
440
  if (fs.existsSync(schemaPath)) {
418
441
  try {
419
442
  const content = fs.readFileSync(schemaPath, 'utf8');
420
- const match = content.match(/provider\s*=\s*"(\w+)"/);
443
+ // Match provider only within a datasource block (skip generator blocks)
444
+ const dsBlock = content.match(/datasource\s+\w+\s*\{[^}]*\}/);
445
+ const providerSource = dsBlock ? dsBlock[0] : content;
446
+ const match = providerSource.match(/provider\s*=\s*"(\w+)"/);
421
447
  if (match) {
422
448
  const provider = match[1];
423
449
  const dbMap = {
@@ -444,6 +470,7 @@ function detectDatabaseFromEnv(dir) {
444
470
  if (fs.existsSync(envPath)) {
445
471
  try {
446
472
  const content = fs.readFileSync(envPath, 'utf8');
473
+ // Check DATABASE_URL
447
474
  const match = content.match(/DATABASE_URL\s*=\s*(\S+)/);
448
475
  if (match) {
449
476
  const url = match[1].replace(/["']/g, '');
@@ -452,6 +479,10 @@ function detectDatabaseFromEnv(dir) {
452
479
  if (url.startsWith('mysql://')) return 'MySQL';
453
480
  if (url.includes('sqlite')) return 'SQLite';
454
481
  }
482
+ // Check MONGODB_URI / MONGO_URI
483
+ if (/^MONGO(?:DB)?_URI\s*=/m.test(content)) return 'MongoDB';
484
+ // Check REDIS_URL
485
+ if (/^REDIS_URL\s*=/m.test(content)) return 'Redis';
455
486
  } catch { /* ignore */ }
456
487
  }
457
488
  }
@@ -466,7 +497,7 @@ function detectPort(dir, pkg) {
466
497
  if (fs.existsSync(envPath)) {
467
498
  try {
468
499
  const content = fs.readFileSync(envPath, 'utf8');
469
- const match = content.match(/^PORT\s*=\s*(\d+)/m);
500
+ const match = content.match(/^PORT\s*=\s*["']?(\d+)/m);
470
501
  if (match) return parseInt(match[1], 10);
471
502
  } catch { /* ignore */ }
472
503
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-sdd-project",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Create a new SDD DevFlow project with AI-assisted development workflow",
5
5
  "bin": {
6
6
  "create-sdd-project": "bin/cli.js"