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.
- package/lib/init-generator.js +178 -28
- package/lib/prompts.js +9 -9
- package/lib/scanner.js +49 -15
- package/package.json +7 -3
package/lib/init-generator.js
CHANGED
|
@@ -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
|
-
|
|
182
|
-
|
|
183
|
-
path.join(
|
|
184
|
-
|
|
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: ["${
|
|
313
|
+
`globs: ["${globPattern}"]`
|
|
304
314
|
);
|
|
305
315
|
|
|
306
316
|
// Update Technology Stack
|
|
307
|
-
const
|
|
308
|
-
const
|
|
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
|
|
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 —
|
|
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
|
-
|
|
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
|
|
338
|
-
`## Database Patterns\n\n<!-- TODO: Add ${scan.backend.orm} best practices and patterns for your project. -->\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 `${
|
|
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 = [`${
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
537
|
+
const backendLabel = scan.backend.framework
|
|
538
|
+
? `${scan.backend.framework}, ${runtime}`
|
|
539
|
+
: runtime;
|
|
540
|
+
content = content.replace('[Framework, runtime, version]', backendLabel);
|
|
464
541
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
470
|
-
|
|
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
|
|
52
|
+
const handler = (line) => {
|
|
53
|
+
if (done) return;
|
|
52
54
|
if (firstLine && line.trim() === '') {
|
|
53
|
-
|
|
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
|
-
|
|
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',
|
|
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
|
-
{
|
|
71
|
-
{
|
|
72
|
-
{
|
|
73
|
-
{
|
|
74
|
-
{
|
|
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
|
-
{
|
|
88
|
-
{
|
|
89
|
-
{
|
|
90
|
-
{
|
|
91
|
-
{
|
|
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
|
-
//
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
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
|
}
|