create-sdd-project 0.2.0 → 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.
@@ -177,12 +177,15 @@ function generateInit(config) {
177
177
  // 8. Append to .gitignore
178
178
  appendGitignore(dest, skipped);
179
179
 
180
- // 9. Copy .env.example if not present
181
- copyFileIfNotExists(
182
- path.join(templateDir, '.env.example'),
183
- path.join(dest, '.env.example'),
184
- skipped
185
- );
180
+ // 9. Copy and adapt .env.example if not present
181
+ const envExamplePath = path.join(dest, '.env.example');
182
+ if (!fs.existsSync(envExamplePath)) {
183
+ let envContent = fs.readFileSync(path.join(templateDir, '.env.example'), 'utf8');
184
+ envContent = adaptEnvExample(envContent, config, scan);
185
+ fs.writeFileSync(envExamplePath, envContent, 'utf8');
186
+ } else {
187
+ skipped.push('.env.example');
188
+ }
186
189
 
187
190
  // Show skipped files
188
191
  if (skipped.length > 0) {
@@ -290,6 +293,12 @@ function safeDelete(filePath) {
290
293
  function adaptBackendStandards(template, scan) {
291
294
  let content = template;
292
295
 
296
+ // Update frontmatter description to be generic (not "Covers DDD architecture")
297
+ content = content.replace(
298
+ /description: Backend development standards.*?\n/,
299
+ 'description: Backend development standards, best practices, and conventions.\n'
300
+ );
301
+
293
302
  // Add TODO marker after frontmatter
294
303
  content = content.replace(
295
304
  '<!-- CONFIG: This file defaults to Node.js/Express/Prisma/PostgreSQL. Adjust for your stack. -->',
@@ -298,25 +307,43 @@ function adaptBackendStandards(template, scan) {
298
307
 
299
308
  // Update globs in frontmatter
300
309
  const srcRoot = findSrcRootName(scan);
310
+ const globPattern = srcRoot ? `${srcRoot}/**/*.{ts,js,tsx,jsx}` : '**/*.{ts,js,tsx,jsx}';
301
311
  content = content.replace(
302
312
  /globs: \[.*?\]/,
303
- `globs: ["${srcRoot}/**/*.{ts,js,tsx,jsx}"]`
313
+ `globs: ["${globPattern}"]`
304
314
  );
305
315
 
306
316
  // Update Technology Stack
307
- const framework = scan.backend.framework || 'Unknown';
308
- const orm = scan.backend.orm || 'Unknown';
309
- const db = scan.backend.db || 'Unknown';
317
+ const orm = scan.backend.orm;
318
+ const db = scan.backend.db;
310
319
  const lang = scan.language === 'typescript' ? 'TypeScript' : 'JavaScript';
311
320
 
321
+ const testFramework = scan.tests.framework !== 'none'
322
+ ? capitalizeFramework(scan.tests.framework)
323
+ : 'Not configured';
324
+
325
+ let stackLines = [
326
+ `- **Runtime**: Node.js with ${lang}`,
327
+ ];
328
+ if (scan.backend.framework) {
329
+ stackLines.push(`- **Framework**: ${scan.backend.framework}`);
330
+ }
331
+ if (orm) {
332
+ stackLines.push(`- **ORM**: ${orm}${db ? ` (${db})` : ''}`);
333
+ } else if (db) {
334
+ stackLines.push(`- **Database**: ${db}`);
335
+ }
336
+ stackLines.push(`- **Testing**: ${testFramework}`);
337
+
312
338
  content = content.replace(
313
339
  /## Technology Stack\n\n[\s\S]*?(?=\n## Architecture)/,
314
- `## Technology Stack\n\n- **Runtime**: Node.js with ${lang}\n- **Framework**: ${framework}\n- **ORM**: ${orm} (${db})\n- **Testing**: ${scan.tests.framework !== 'none' ? scan.tests.framework : 'Not configured'}\n\n`
340
+ `## Technology Stack\n\n${stackLines.join('\n')}\n\n`
315
341
  );
316
342
 
317
343
  // Update Architecture section
318
344
  const patternLabels = {
319
345
  mvc: 'MVC',
346
+ layered: 'Layered',
320
347
  ddd: 'DDD Layered',
321
348
  'feature-based': 'Feature-Based',
322
349
  'handler-based': 'Handler-Based',
@@ -326,16 +353,35 @@ function adaptBackendStandards(template, scan) {
326
353
  const patternLabel = patternLabels[scan.srcStructure.pattern] || 'Custom';
327
354
 
328
355
  const archStructure = buildArchitectureTree(scan);
356
+ // Robust: match any "## Architecture — <anything>" heading up to "## Naming"
329
357
  content = content.replace(
330
- /## 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)/,
331
359
  `## Architecture — ${patternLabel}\n\n\`\`\`\n${archStructure}\`\`\`\n\n<!-- TODO: Add layer rules that match your project's architecture. -->\n\n`
332
360
  );
333
361
 
334
362
  // Update Database Patterns section if not Prisma
335
- if (scan.backend.orm && scan.backend.orm !== 'Prisma') {
363
+ // Robust: match from "## Database Patterns" to next "## " heading
364
+ if (!scan.backend.orm) {
365
+ content = content.replace(
366
+ /## Database Patterns\n\n[\s\S]*?(?=\n## )/,
367
+ `## Database Patterns\n\n<!-- TODO: Add database access patterns for your project. -->\n`
368
+ );
369
+ } else if (scan.backend.orm !== 'Prisma') {
336
370
  content = content.replace(
337
- /## Database Patterns\n\n### Prisma Best Practices[\s\S]*?### Repository Pattern[\s\S]*?```\n/,
338
- `## 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`
373
+ );
374
+ }
375
+
376
+ // Replace Prisma-specific references in Security and Performance sections
377
+ if (scan.backend.orm !== 'Prisma') {
378
+ content = content.replace(
379
+ '- Use parameterized queries (Prisma handles this)',
380
+ '- Use parameterized queries to prevent injection attacks'
381
+ );
382
+ content = content.replace(
383
+ '- Use Prisma `include` instead of N+1 queries',
384
+ '- Avoid N+1 queries — use eager loading or batch fetching'
339
385
  );
340
386
  }
341
387
 
@@ -366,7 +412,7 @@ function adaptFrontendStandards(template, scan) {
366
412
 
367
413
  content = content.replace(
368
414
  /## Technology Stack\n\n[\s\S]*?(?=\n## Project Structure)/,
369
- `## Technology Stack\n\n- **Framework**: ${framework}\n- **Language**: ${lang}\n- **Styling**: ${styling}${components ? `\n- **Components**: ${components.slice(2)}` : ''}${state ? `\n- **State Management**: ${state.slice(2)}` : ''}\n- **Testing**: ${scan.tests.framework !== 'none' ? scan.tests.framework : 'Not configured'}\n\n`
415
+ `## Technology Stack\n\n- **Framework**: ${framework}\n- **Language**: ${lang}\n- **Styling**: ${styling}${components ? `\n- **Components**: ${components.slice(2)}` : ''}${state ? `\n- **State Management**: ${state.slice(2)}` : ''}\n- **Testing**: ${scan.tests.framework !== 'none' ? capitalizeFramework(scan.tests.framework) : 'Not configured'}\n\n`
370
416
  );
371
417
 
372
418
  // Update Project Structure
@@ -383,14 +429,15 @@ function adaptFrontendStandards(template, scan) {
383
429
  function buildArchitectureTree(scan) {
384
430
  const srcRoot = findSrcRootName(scan);
385
431
  const dirs = scan.srcStructure.dirs;
432
+ const rootLabel = srcRoot || 'project';
386
433
 
387
434
  if (dirs.length === 0) {
388
- return `${srcRoot}/\n└── <!-- TODO: Map your project structure here -->\n`;
435
+ return `${rootLabel}/\n└── <!-- TODO: Map your project structure here -->\n`;
389
436
  }
390
437
 
391
438
  // Build a simple tree from detected directories (depth 1 only)
392
439
  const topLevel = dirs.filter((d) => !d.includes('/'));
393
- const lines = [`${srcRoot}/\n`];
440
+ const lines = [`${rootLabel}/\n`];
394
441
  topLevel.forEach((dir, i) => {
395
442
  const prefix = i === topLevel.length - 1 ? '└── ' : '├── ';
396
443
  lines.push(`${prefix}${dir}/\n`);
@@ -399,13 +446,18 @@ function buildArchitectureTree(scan) {
399
446
  return lines.join('');
400
447
  }
401
448
 
449
+ function capitalizeFramework(name) {
450
+ const map = { jest: 'Jest', vitest: 'Vitest', mocha: 'Mocha', playwright: 'Playwright', cypress: 'Cypress' };
451
+ return map[name] || name;
452
+ }
453
+
402
454
  function findSrcRootName(scan) {
403
455
  // Determine the source root directory name from scan
404
456
  if (scan.rootDirs.includes('src/')) return 'src';
405
457
  if (scan.rootDirs.includes('app/')) return 'app';
406
458
  if (scan.rootDirs.includes('server/')) return 'server';
407
459
  if (scan.rootDirs.includes('lib/')) return 'lib';
408
- return 'src';
460
+ return null; // No conventional source root — code is at project root
409
461
  }
410
462
 
411
463
  // --- AGENTS.md Adaptation ---
@@ -418,22 +470,45 @@ function adaptAgentsMd(template, config, scan) {
418
470
  const tree = rootDirs.map((d) => `├── ${d.replace(/\/$/, '/')} `).join('\n');
419
471
  const treeBlock = `\`\`\`\nproject/\n${tree}\n└── docs/ ← Documentation\n\`\`\``;
420
472
 
473
+ // Robust: flexible whitespace between CONFIG comment and code block
421
474
  content = content.replace(
422
- /<!-- CONFIG: Adjust directories.*?-->\n\n```\nproject\/\n[\s\S]*?```/,
475
+ /<!-- CONFIG: Adjust directories[^>]*-->\n+```\nproject\/\n[\s\S]*?```/,
423
476
  treeBlock
424
477
  );
425
478
 
426
479
  // If not monorepo, simplify the install instructions
480
+ // Robust: match any number of table rows (not hardcoded count)
427
481
  if (!scan.isMonorepo) {
428
482
  content = content.replace(
429
- /\*\*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)+/,
430
484
  ''
431
485
  );
432
486
  }
433
487
 
488
+ // Adapt Standards References descriptions
489
+ if (scan.backend.detected) {
490
+ const parts = [scan.srcStructure.pattern ? patternLabelFor(scan.srcStructure.pattern) : null, scan.backend.framework, scan.backend.orm].filter(Boolean);
491
+ content = content.replace(
492
+ 'Backend patterns (DDD, Express, Prisma)',
493
+ `Backend patterns (${parts.join(', ')})`
494
+ );
495
+ }
496
+ if (scan.frontend.detected) {
497
+ const parts = [scan.frontend.framework, scan.frontend.styling, scan.frontend.components].filter(Boolean);
498
+ content = content.replace(
499
+ 'Frontend patterns (Next.js, Tailwind, Radix)',
500
+ `Frontend patterns (${parts.join(', ')})`
501
+ );
502
+ }
503
+
434
504
  return content;
435
505
  }
436
506
 
507
+ function patternLabelFor(pattern) {
508
+ const map = { mvc: 'MVC', layered: 'Layered', ddd: 'DDD', 'feature-based': 'Feature-Based', 'handler-based': 'Handler-Based', flat: 'Flat', unknown: null };
509
+ return map[pattern] || null;
510
+ }
511
+
437
512
  // --- key_facts.md Configuration ---
438
513
 
439
514
  function configureKeyFacts(template, config, scan) {
@@ -458,16 +533,23 @@ function configureKeyFacts(template, config, scan) {
458
533
 
459
534
  // Technology Stack from scan
460
535
  if (scan.backend.detected) {
461
- const fw = scan.backend.framework || 'Unknown';
462
536
  const runtime = scan.language === 'typescript' ? 'Node.js (TypeScript)' : 'Node.js';
463
- 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);
464
541
 
465
- const db = scan.backend.db || 'Unknown';
466
- const port = scan.backend.port || config.backendPort;
467
- content = content.replace('[Type, host, port]', `${db}, localhost, ${port}`);
542
+ if (scan.backend.db) {
543
+ content = content.replace('[Type, host, port]', scan.backend.db);
544
+ } else {
545
+ content = content.replace('- **Database**: [Type, host, port]\n', '');
546
+ }
468
547
 
469
- const orm = scan.backend.orm || 'Unknown';
470
- content = content.replace('[Name, version]', orm);
548
+ if (scan.backend.orm) {
549
+ content = content.replace('[Name, version]', scan.backend.orm);
550
+ } else {
551
+ content = content.replace('- **ORM**: [Name, version]\n', '');
552
+ }
471
553
  }
472
554
 
473
555
  if (scan.frontend.detected) {
@@ -509,6 +591,28 @@ function configureKeyFacts(template, config, scan) {
509
591
  content = content.replace('## Technology Stack', `${contextSection}\n## Technology Stack`);
510
592
  }
511
593
 
594
+ // Remove irrelevant Infrastructure lines based on project type
595
+ if (!scan.frontend.detected) {
596
+ content = content.replace('- **Frontend Hosting**: [e.g., Vercel]\n', '');
597
+ }
598
+ if (!scan.backend.detected) {
599
+ content = content.replace('- **Backend Hosting**: [e.g., Render]\n', '');
600
+ content = content.replace('- **Database Hosting**: [e.g., Neon, Supabase, RDS]\n', '');
601
+ }
602
+
603
+ // Adapt DB hosting examples based on detected database
604
+ if (scan.backend.db === 'MongoDB') {
605
+ content = content.replace('[e.g., Neon, Supabase, RDS]', '[e.g., MongoDB Atlas, Cosmos DB]');
606
+ }
607
+
608
+ // Remove irrelevant Reusable Components subsections
609
+ if (!scan.frontend.detected) {
610
+ content = content.replace('\n### Frontend\n- [List key components, hooks, stores as you build them]\n', '');
611
+ }
612
+ if (!scan.backend.detected) {
613
+ content = content.replace('\n### Backend\n- [List key services, middleware, validators as you build them]\n', '');
614
+ }
615
+
512
616
  // Prisma schema reference
513
617
  if (scan.existingDocs.hasPrismaSchema) {
514
618
  content = content.replace(
@@ -532,6 +636,20 @@ function configureSprintTracker(template, scan) {
532
636
  const end = endDate.toISOString().split('T')[0];
533
637
  content = content.replace(/\[YYYY-MM-DD\] to \[YYYY-MM-DD\]/, `${today} to ${end}`);
534
638
 
639
+ // Remove irrelevant task tables based on detected stack
640
+ if (!scan.frontend.detected) {
641
+ content = content.replace(
642
+ /\n### Frontend\n\n\| # \| Task \| Status \| Notes \|\n\|---\|------\|--------\|-------\|\n\| F0\.1 \| \[Task description\] \| ⬚ \| \|\n/,
643
+ ''
644
+ );
645
+ }
646
+ if (!scan.backend.detected) {
647
+ content = content.replace(
648
+ /\n### Backend\n\n\| # \| Task \| Status \| Notes \|\n\|---\|------\|--------\|-------\|\n\| B0\.1 \| \[Task description\] \| ⬚ \| \|\n/,
649
+ ''
650
+ );
651
+ }
652
+
535
653
  // Add retrofit testing tasks if coverage is low
536
654
  if (scan.tests.estimatedCoverage === 'none' || scan.tests.estimatedCoverage === 'low') {
537
655
  const retrofitSection = `\n### Retrofit Testing (recommended)\n\n| # | Task | Status | Notes |\n|---|------|--------|-------|\n| R0.1 | Audit existing test coverage | ⬚ | |\n| R0.2 | Add missing unit tests for critical paths | ⬚ | |\n| R0.3 | Set up CI test pipeline | ⬚ | |\n`;
@@ -566,6 +684,38 @@ function updateAutonomy(dest, config) {
566
684
  }
567
685
  }
568
686
 
687
+ // --- .env.example Adaptation ---
688
+
689
+ function adaptEnvExample(template, config, scan) {
690
+ let content = template;
691
+ const port = scan.backend.port || config.backendPort || 3010;
692
+
693
+ // Adapt port
694
+ content = content.replace('PORT=3010', `PORT=${port}`);
695
+ content = content.replace('localhost:3010', `localhost:${port}`);
696
+
697
+ // Adapt DATABASE_URL based on detected DB
698
+ if (scan.backend.db === 'MongoDB') {
699
+ content = content.replace(
700
+ 'DATABASE_URL=postgresql://user:password@localhost:5432/dbname',
701
+ 'MONGODB_URI=mongodb://localhost:27017/dbname'
702
+ );
703
+ }
704
+
705
+ // Remove frontend section if backend-only
706
+ if (!scan.frontend.detected) {
707
+ content = content.replace(/\n# Frontend\nNEXT_PUBLIC_API_URL=.*\n/, '\n');
708
+ }
709
+
710
+ // Remove backend section if frontend-only
711
+ if (!scan.backend.detected) {
712
+ content = content.replace(/# Backend\nNODE_ENV=.*\nPORT=.*\nDATABASE_URL=.*\n/, '');
713
+ content = content.replace(/# Backend\nNODE_ENV=.*\nPORT=.*\nMONGODB_URI=.*\n/, '');
714
+ }
715
+
716
+ return content;
717
+ }
718
+
569
719
  // --- .gitignore ---
570
720
 
571
721
  function appendGitignore(dest, skipped) {
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';
@@ -231,8 +243,11 @@ function detectArchitecture(dir, pkg) {
231
243
  result.hasHandlers = dirNames.has('handlers') || dirNames.has('handler');
232
244
 
233
245
  // Determine pattern
246
+ const hasManagers = dirNames.has('managers') || dirNames.has('manager');
234
247
  if (result.hasDomain && (dirNames.has('application') || dirNames.has('infrastructure'))) {
235
248
  result.pattern = 'ddd';
249
+ } else if (result.hasHandlers && result.hasControllers && hasManagers) {
250
+ result.pattern = 'layered';
236
251
  } else if (result.hasControllers && result.hasModels) {
237
252
  result.pattern = 'mvc';
238
253
  } else if (result.hasFeatures) {
@@ -277,14 +292,15 @@ function detectTests(dir, pkg) {
277
292
  const deps = getAllDeps(pkg);
278
293
  const result = {
279
294
  framework: 'none',
295
+ e2eFramework: null,
280
296
  hasConfig: false,
281
297
  testFiles: 0,
282
298
  testDirs: [],
283
299
  estimatedCoverage: 'none',
284
300
  };
285
301
 
286
- // Framework detection
287
- if (deps['jest'] || deps['@jest/core']) {
302
+ // Unit test framework detection
303
+ if (deps['jest'] || deps['@jest/core'] || deps['ts-jest'] || deps['@types/jest']) {
288
304
  result.framework = 'jest';
289
305
  } else if (deps['vitest']) {
290
306
  result.framework = 'vitest';
@@ -292,11 +308,21 @@ function detectTests(dir, pkg) {
292
308
  result.framework = 'mocha';
293
309
  }
294
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
+
295
319
  // Config files
296
320
  const configFiles = [
297
321
  'jest.config.js', 'jest.config.ts', 'jest.config.mjs', 'jest.config.cjs',
298
322
  'vitest.config.js', 'vitest.config.ts', 'vitest.config.mjs',
299
323
  '.mocharc.yml', '.mocharc.json', '.mocharc.js',
324
+ 'playwright.config.ts', 'playwright.config.js',
325
+ 'cypress.config.ts', 'cypress.config.js',
300
326
  ];
301
327
  result.hasConfig = configFiles.some((f) => fs.existsSync(path.join(dir, f)));
302
328
 
@@ -414,7 +440,10 @@ function detectDatabaseFromPrisma(dir) {
414
440
  if (fs.existsSync(schemaPath)) {
415
441
  try {
416
442
  const content = fs.readFileSync(schemaPath, 'utf8');
417
- 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+)"/);
418
447
  if (match) {
419
448
  const provider = match[1];
420
449
  const dbMap = {
@@ -441,6 +470,7 @@ function detectDatabaseFromEnv(dir) {
441
470
  if (fs.existsSync(envPath)) {
442
471
  try {
443
472
  const content = fs.readFileSync(envPath, 'utf8');
473
+ // Check DATABASE_URL
444
474
  const match = content.match(/DATABASE_URL\s*=\s*(\S+)/);
445
475
  if (match) {
446
476
  const url = match[1].replace(/["']/g, '');
@@ -449,6 +479,10 @@ function detectDatabaseFromEnv(dir) {
449
479
  if (url.startsWith('mysql://')) return 'MySQL';
450
480
  if (url.includes('sqlite')) return 'SQLite';
451
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';
452
486
  } catch { /* ignore */ }
453
487
  }
454
488
  }
@@ -463,7 +497,7 @@ function detectPort(dir, pkg) {
463
497
  if (fs.existsSync(envPath)) {
464
498
  try {
465
499
  const content = fs.readFileSync(envPath, 'utf8');
466
- const match = content.match(/^PORT\s*=\s*(\d+)/m);
500
+ const match = content.match(/^PORT\s*=\s*["']?(\d+)/m);
467
501
  if (match) return parseInt(match[1], 10);
468
502
  } catch { /* ignore */ }
469
503
  }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "create-sdd-project",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Create a new SDD DevFlow project with AI-assisted development workflow",
5
5
  "bin": {
6
- "create-sdd-project": "./bin/cli.js"
6
+ "create-sdd-project": "bin/cli.js"
7
7
  },
8
8
  "scripts": {
9
9
  "test": "node test/smoke.js",
@@ -36,6 +36,10 @@
36
36
  "license": "MIT",
37
37
  "repository": {
38
38
  "type": "git",
39
- "url": "https://github.com/pbojeda/sdd-devflow.git"
39
+ "url": "git+https://github.com/pbojeda/sdd-devflow.git"
40
+ },
41
+ "homepage": "https://github.com/pbojeda/sdd-devflow#readme",
42
+ "bugs": {
43
+ "url": "https://github.com/pbojeda/sdd-devflow/issues"
40
44
  }
41
45
  }