delimit-cli 4.0.4 → 4.0.5

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 (2) hide show
  1. package/bin/delimit-cli.js +228 -11
  2. package/package.json +1 -1
@@ -1318,7 +1318,7 @@ program
1318
1318
  }
1319
1319
  }
1320
1320
 
1321
- // Auto-detect OpenAPI spec files
1321
+ // Auto-detect OpenAPI spec files — flat patterns + recursive scan
1322
1322
  const specPatterns = [
1323
1323
  'openapi.yaml', 'openapi.yml', 'openapi.json',
1324
1324
  'swagger.yaml', 'swagger.yml', 'swagger.json',
@@ -1329,9 +1329,146 @@ program
1329
1329
  'api/openapi.yaml', 'api/openapi.json',
1330
1330
  'contrib/openapi.json',
1331
1331
  ];
1332
- const foundSpecs = specPatterns.filter(p => fs.existsSync(path.join(projectDir, p)));
1332
+ let foundSpecs = specPatterns.filter(p => fs.existsSync(path.join(projectDir, p)));
1333
+
1334
+ // Recursive scan: search common directories for OpenAPI/Swagger files
1335
+ const specDirs = ['swagger', 'api', 'docs', 'spec', 'specs', 'openapi', 'schema', 'schemas', 'config', 'src'];
1336
+ const specExtensions = ['.yaml', '.yml', '.json'];
1337
+ const specKeywords = ['openapi', 'swagger', 'api-spec', 'api_spec'];
1338
+ function scanDirForSpecs(dir, depth = 0) {
1339
+ if (depth > 2) return []; // limit recursion depth
1340
+ const results = [];
1341
+ try {
1342
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
1343
+ for (const entry of entries) {
1344
+ if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === 'vendor') continue;
1345
+ const fullPath = path.join(dir, entry.name);
1346
+ if (entry.isDirectory() && depth < 2) {
1347
+ results.push(...scanDirForSpecs(fullPath, depth + 1));
1348
+ } else if (entry.isFile() && specExtensions.includes(path.extname(entry.name).toLowerCase())) {
1349
+ // Check if file looks like an OpenAPI/Swagger spec
1350
+ const nameLower = entry.name.toLowerCase();
1351
+ const isLikelySpec = specKeywords.some(kw => nameLower.includes(kw)) || nameLower === 'api.yaml' || nameLower === 'api.yml' || nameLower === 'api.json';
1352
+ if (isLikelySpec) {
1353
+ results.push(path.relative(projectDir, fullPath));
1354
+ } else {
1355
+ // Peek inside to check for openapi/swagger key
1356
+ try {
1357
+ const head = fs.readFileSync(fullPath, 'utf-8').slice(0, 512);
1358
+ if (head.includes('"openapi"') || head.includes("openapi:") || head.includes('"swagger"') || head.includes("swagger:")) {
1359
+ results.push(path.relative(projectDir, fullPath));
1360
+ }
1361
+ } catch {}
1362
+ }
1363
+ }
1364
+ }
1365
+ } catch {}
1366
+ return results;
1367
+ }
1368
+ for (const sd of specDirs) {
1369
+ const sdPath = path.join(projectDir, sd);
1370
+ if (fs.existsSync(sdPath)) {
1371
+ const deepSpecs = scanDirForSpecs(sdPath);
1372
+ for (const ds of deepSpecs) {
1373
+ if (!foundSpecs.includes(ds)) foundSpecs.push(ds);
1374
+ }
1375
+ }
1376
+ }
1377
+ // Also scan root-level yaml/json files for OpenAPI markers
1378
+ try {
1379
+ const rootEntries = fs.readdirSync(projectDir, { withFileTypes: true });
1380
+ for (const entry of rootEntries) {
1381
+ if (!entry.isFile()) continue;
1382
+ const ext = path.extname(entry.name).toLowerCase();
1383
+ if (!specExtensions.includes(ext)) continue;
1384
+ const rel = entry.name;
1385
+ if (foundSpecs.includes(rel)) continue;
1386
+ try {
1387
+ const head = fs.readFileSync(path.join(projectDir, rel), 'utf-8').slice(0, 512);
1388
+ if (head.includes('"openapi"') || head.includes("openapi:") || head.includes('"swagger"') || head.includes("swagger:")) {
1389
+ foundSpecs.push(rel);
1390
+ }
1391
+ } catch {}
1392
+ }
1393
+ } catch {}
1333
1394
  const specPath = foundSpecs.length > 0 ? foundSpecs[0] : null;
1334
1395
 
1396
+ // Detect test files and count them
1397
+ let testFileCount = 0;
1398
+ let testFramework = null;
1399
+ function countTestFiles(dir, depth = 0) {
1400
+ if (depth > 3) return;
1401
+ try {
1402
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
1403
+ for (const entry of entries) {
1404
+ if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === 'vendor' || entry.name === '__pycache__') continue;
1405
+ const fullPath = path.join(dir, entry.name);
1406
+ if (entry.isDirectory()) {
1407
+ if (entry.name === '__tests__' || entry.name === 'test' || entry.name === 'tests' || entry.name === 'spec') {
1408
+ countTestFiles(fullPath, depth + 1);
1409
+ } else if (depth < 2) {
1410
+ countTestFiles(fullPath, depth + 1);
1411
+ }
1412
+ } else if (entry.isFile()) {
1413
+ const name = entry.name.toLowerCase();
1414
+ if (name.endsWith('.test.js') || name.endsWith('.test.ts') || name.endsWith('.test.tsx') || name.endsWith('.spec.js') || name.endsWith('.spec.ts')) {
1415
+ testFileCount++;
1416
+ } else if (name.startsWith('test_') && name.endsWith('.py')) {
1417
+ testFileCount++;
1418
+ } else if (name.endsWith('_test.go')) {
1419
+ testFileCount++;
1420
+ }
1421
+ }
1422
+ }
1423
+ } catch {}
1424
+ }
1425
+ countTestFiles(projectDir);
1426
+ // Detect test framework
1427
+ if (fs.existsSync(pkgJsonPath)) {
1428
+ try {
1429
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
1430
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
1431
+ if (allDeps['jest'] || (pkg.scripts && pkg.scripts.test && pkg.scripts.test.includes('jest'))) testFramework = 'jest';
1432
+ else if (allDeps['vitest']) testFramework = 'vitest';
1433
+ else if (allDeps['mocha']) testFramework = 'mocha';
1434
+ } catch {}
1435
+ }
1436
+ if (!testFramework && fs.existsSync(requirementsPath)) {
1437
+ try {
1438
+ const content = fs.readFileSync(requirementsPath, 'utf-8').toLowerCase();
1439
+ if (content.includes('pytest')) testFramework = 'pytest';
1440
+ } catch {}
1441
+ }
1442
+
1443
+ // Quick security scan
1444
+ const securityFindings = [];
1445
+ // Check for common secret patterns in spec files
1446
+ for (const sp of foundSpecs.slice(0, 5)) {
1447
+ try {
1448
+ const content = fs.readFileSync(path.join(projectDir, sp), 'utf-8');
1449
+ if (/(?:api[_-]?key|secret|password|token)\s*[:=]\s*["'][^"']{8,}/i.test(content)) {
1450
+ securityFindings.push({ severity: 'high', file: sp, issue: 'Possible hardcoded secret in spec file' });
1451
+ }
1452
+ if (/http:\/\/(?!localhost|127\.0\.0\.1)/i.test(content)) {
1453
+ securityFindings.push({ severity: 'medium', file: sp, issue: 'Non-localhost HTTP URL in spec (should use HTTPS)' });
1454
+ }
1455
+ } catch {}
1456
+ }
1457
+ // Check for .env files committed (not in .gitignore)
1458
+ const envFiles = ['.env', '.env.local', '.env.production'];
1459
+ const gitignorePath = path.join(projectDir, '.gitignore');
1460
+ let gitignoreContent = '';
1461
+ try { gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8'); } catch {}
1462
+ for (const envFile of envFiles) {
1463
+ if (fs.existsSync(path.join(projectDir, envFile))) {
1464
+ if (!gitignoreContent.includes(envFile)) {
1465
+ securityFindings.push({ severity: 'high', file: envFile, issue: `${envFile} exists and is not in .gitignore` });
1466
+ }
1467
+ }
1468
+ }
1469
+ // Check for package-lock.json / yarn.lock (dependency lockfile)
1470
+ const hasLockfile = fs.existsSync(path.join(projectDir, 'package-lock.json')) || fs.existsSync(path.join(projectDir, 'yarn.lock')) || fs.existsSync(path.join(projectDir, 'pnpm-lock.yaml'));
1471
+
1335
1472
  // Check for CI
1336
1473
  const hasGitHub = fs.existsSync(path.join(projectDir, '.github'));
1337
1474
  const hasGitLabCI = fs.existsSync(path.join(projectDir, '.gitlab-ci.yml'));
@@ -1340,11 +1477,33 @@ program
1340
1477
  // Display detection results
1341
1478
  console.log(` Project: ${chalk.bold(projectName)}`);
1342
1479
  if (frameworkLabel) console.log(` Framework: ${chalk.bold(frameworkLabel)}`);
1343
- if (specPath) console.log(` Spec: ${chalk.bold(specPath)}`);
1344
- else if (['fastapi', 'nestjs', 'express'].includes(framework))
1480
+ if (foundSpecs.length > 1) {
1481
+ console.log(` Specs: ${chalk.bold(foundSpecs.length + ' found')}`);
1482
+ for (const sp of foundSpecs.slice(0, 5)) {
1483
+ console.log(` ${chalk.gray('-')} ${sp}`);
1484
+ }
1485
+ if (foundSpecs.length > 5) console.log(` ${chalk.gray(`... and ${foundSpecs.length - 5} more`)}`);
1486
+ } else if (specPath) {
1487
+ console.log(` Spec: ${chalk.bold(specPath)}`);
1488
+ } else if (['fastapi', 'nestjs', 'express'].includes(framework)) {
1345
1489
  console.log(` Spec: ${chalk.gray('none found')} (Zero-Spec Mode available for ${frameworkLabel})`);
1346
- else console.log(` Spec: ${chalk.gray('none found')}`);
1490
+ } else {
1491
+ console.log(` Spec: ${chalk.gray('none found')}`);
1492
+ }
1493
+ if (testFileCount > 0) {
1494
+ console.log(` Tests: ${chalk.bold(testFileCount + ' file' + (testFileCount !== 1 ? 's' : ''))}${testFramework ? chalk.gray(' (' + testFramework + ')') : ''}`);
1495
+ } else {
1496
+ console.log(` Tests: ${chalk.gray('none detected')}`);
1497
+ }
1347
1498
  if (ciProvider !== 'none') console.log(` CI: ${chalk.bold(ciProvider === 'github' ? 'GitHub Actions' : 'GitLab CI')}`);
1499
+ if (securityFindings.length > 0) {
1500
+ console.log(` Security: ${chalk.yellow(securityFindings.length + ' finding' + (securityFindings.length !== 1 ? 's' : ''))}`);
1501
+ } else {
1502
+ console.log(` Security: ${chalk.green('clean (quick scan)')}`);
1503
+ }
1504
+ if (!hasLockfile && fs.existsSync(pkgJsonPath)) {
1505
+ console.log(` Lockfile: ${chalk.yellow('missing — consider committing a lockfile')}`);
1506
+ }
1348
1507
  console.log('');
1349
1508
 
1350
1509
  // Step 2: Choose preset
@@ -1596,7 +1755,7 @@ jobs:
1596
1755
  }
1597
1756
  }
1598
1757
 
1599
- // Step 6: Save first evidence event (LED-258)
1758
+ // Step 6: Save first evidence event + comprehensive report (LED-258)
1600
1759
  const evidenceDir = path.join(configDir, 'evidence');
1601
1760
  fs.mkdirSync(evidenceDir, { recursive: true });
1602
1761
  const evidenceEvent = {
@@ -1605,23 +1764,67 @@ jobs:
1605
1764
  type: 'governance_init',
1606
1765
  tool: 'delimit_init',
1607
1766
  model: 'cli',
1608
- status: 'pass',
1767
+ status: securityFindings.some(f => f.severity === 'high') ? 'warn' : 'pass',
1609
1768
  summary: `Governance initialized with ${preset} preset`,
1610
1769
  detail: [
1611
1770
  `Project: ${projectName}`,
1612
1771
  frameworkLabel ? `Framework: ${frameworkLabel}` : null,
1613
- specPath ? `Spec: ${specPath}` : 'Mode: Zero-Spec',
1772
+ foundSpecs.length > 0 ? `Specs found: ${foundSpecs.length} (${foundSpecs.join(', ')})` : 'Mode: Zero-Spec',
1614
1773
  `Preset: ${preset}`,
1774
+ `Test files: ${testFileCount}`,
1775
+ testFramework ? `Test framework: ${testFramework}` : null,
1615
1776
  ciProvider !== 'none' ? `CI: ${ciProvider}` : null,
1777
+ securityFindings.length > 0 ? `Security findings: ${securityFindings.length}` : 'Security: clean',
1616
1778
  ].filter(Boolean).join('\n'),
1617
1779
  venture: projectName,
1618
1780
  };
1619
1781
  try {
1620
1782
  const evidenceFile = path.join(evidenceDir, 'events.jsonl');
1621
1783
  fs.appendFileSync(evidenceFile, JSON.stringify(evidenceEvent) + '\n');
1622
- console.log(chalk.green(' Evidence recorded — first governance event saved'));
1623
1784
  } catch {}
1624
1785
 
1786
+ // Generate first evidence report (LED-258: zero-config onboarding)
1787
+ const firstReport = {
1788
+ generated_at: new Date().toISOString(),
1789
+ project: projectName,
1790
+ framework: frameworkLabel || 'unknown',
1791
+ specs: {
1792
+ count: foundSpecs.length,
1793
+ files: foundSpecs,
1794
+ primary: specPath,
1795
+ },
1796
+ tests: {
1797
+ file_count: testFileCount,
1798
+ framework: testFramework,
1799
+ },
1800
+ security: {
1801
+ findings_count: securityFindings.length,
1802
+ findings: securityFindings,
1803
+ lockfile_present: hasLockfile,
1804
+ },
1805
+ governance: {
1806
+ preset: preset,
1807
+ compliance_template: complianceTemplate,
1808
+ ci_provider: ciProvider,
1809
+ gates_active: specPath || ['fastapi', 'nestjs', 'express'].includes(framework) ? ['api_lint'] : [],
1810
+ gates_ready: ['security_audit', 'deploy_plan', 'release_validate'],
1811
+ },
1812
+ };
1813
+ try {
1814
+ const reportFile = path.join(evidenceDir, 'first-report.json');
1815
+ fs.writeFileSync(reportFile, JSON.stringify(firstReport, null, 2));
1816
+ console.log(chalk.green(' Evidence recorded — first governance report saved'));
1817
+ } catch {}
1818
+
1819
+ // Display security findings if any
1820
+ if (securityFindings.length > 0) {
1821
+ console.log(chalk.bold('\n Security Findings:'));
1822
+ for (const finding of securityFindings) {
1823
+ const icon = finding.severity === 'high' ? chalk.red('!') : chalk.yellow('~');
1824
+ console.log(` ${icon} ${chalk.bold(finding.severity.toUpperCase())} ${finding.file} — ${finding.issue}`);
1825
+ }
1826
+ }
1827
+
1625
1828
  // Step 7: Show gate status (LED-258)
1626
1829
  console.log(chalk.bold('\n Governance Gates:'));
1627
1830
  const gates = [
@@ -1639,15 +1842,29 @@ jobs:
1639
1842
  // Summary
1640
1843
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
1641
1844
  console.log(chalk.bold(`\n Setup complete in ${elapsed}s`));
1642
- console.log(chalk.gray(` Evidence saved to .delimit/evidence/events.jsonl\n`));
1845
+ console.log(chalk.gray(` Evidence saved to .delimit/evidence/\n`));
1643
1846
  console.log(' Next steps:');
1644
1847
  if (specPath) {
1645
1848
  console.log(` ${chalk.bold('delimit lint')} ${specPath} ${specPath} — lint on every PR`);
1849
+ } else if (['fastapi', 'nestjs', 'express'].includes(framework)) {
1850
+ console.log(` ${chalk.bold('delimit lint')} — zero-spec mode (${frameworkLabel})`);
1646
1851
  } else {
1647
- console.log(` ${chalk.bold('delimit lint')} — zero-spec mode`);
1852
+ console.log(` ${chalk.bold('delimit lint')} — add an OpenAPI spec first`);
1648
1853
  }
1649
1854
  console.log(` ${chalk.bold('delimit doctor')} — verify setup`);
1650
1855
  console.log(` ${chalk.bold('delimit explain')} — human-readable report`);
1856
+ if (securityFindings.length > 0) {
1857
+ console.log(` ${chalk.yellow('Fix security findings above')} — ${securityFindings.length} issue${securityFindings.length !== 1 ? 's' : ''} found`);
1858
+ }
1859
+ if (testFileCount === 0) {
1860
+ console.log(` ${chalk.gray('Add tests')} — no test files detected`);
1861
+ }
1862
+ if (foundSpecs.length > 1) {
1863
+ console.log(` ${chalk.gray('Review all ' + foundSpecs.length + ' specs')} — multiple specs detected`);
1864
+ }
1865
+ if (ciProvider === 'none') {
1866
+ console.log(` ${chalk.gray('Add CI')} — no CI detected; consider GitHub Actions`);
1867
+ }
1651
1868
 
1652
1869
  // Beta capture after init (LED-263)
1653
1870
  if (!options.yes) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "delimit-cli",
3
3
  "mcpName": "io.github.delimit-ai/delimit-mcp-server",
4
- "version": "4.0.4",
4
+ "version": "4.0.5",
5
5
  "description": "Unify Claude Code, Codex, Cursor, and Gemini CLI with persistent context, governance, and multi-model debate.",
6
6
  "main": "index.js",
7
7
  "files": [