universal-dev-standards 5.1.0-beta.6 → 5.1.0-beta.7

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 (68) hide show
  1. package/bin/uds.js +12 -0
  2. package/bundled/ai/standards/agent-communication-protocol.ai.yaml +34 -0
  3. package/bundled/ai/standards/anti-sycophancy-prompting.ai.yaml +111 -0
  4. package/bundled/ai/standards/capability-declaration.ai.yaml +113 -0
  5. package/bundled/ai/standards/circuit-breaker.ai.yaml +93 -0
  6. package/bundled/ai/standards/developer-memory.ai.yaml +13 -0
  7. package/bundled/ai/standards/dual-phase-output.ai.yaml +108 -0
  8. package/bundled/ai/standards/failure-source-taxonomy.ai.yaml +115 -0
  9. package/bundled/ai/standards/frontend-design-standards.ai.yaml +305 -0
  10. package/bundled/ai/standards/health-check-standards.ai.yaml +140 -0
  11. package/bundled/ai/standards/immutability-first.ai.yaml +112 -0
  12. package/bundled/ai/standards/model-selection.ai.yaml +111 -3
  13. package/bundled/ai/standards/packaging-standards.ai.yaml +142 -0
  14. package/bundled/ai/standards/recovery-recipe-registry.ai.yaml +200 -0
  15. package/bundled/ai/standards/retry-standards.ai.yaml +134 -0
  16. package/bundled/ai/standards/security-decision.ai.yaml +87 -0
  17. package/bundled/ai/standards/skill-standard-alignment-check.ai.yaml +119 -0
  18. package/bundled/ai/standards/standard-admission-criteria.ai.yaml +107 -0
  19. package/bundled/ai/standards/standard-lifecycle-management.ai.yaml +144 -0
  20. package/bundled/ai/standards/timeout-standards.ai.yaml +104 -0
  21. package/bundled/ai/standards/token-budget.ai.yaml +108 -0
  22. package/bundled/core/anti-sycophancy-prompting.md +184 -0
  23. package/bundled/core/capability-declaration.md +59 -0
  24. package/bundled/core/circuit-breaker.md +58 -0
  25. package/bundled/core/developer-memory.md +29 -1
  26. package/bundled/core/dual-phase-output.md +56 -0
  27. package/bundled/core/failure-source-taxonomy.md +72 -0
  28. package/bundled/core/frontend-design-standards.md +474 -0
  29. package/bundled/core/health-check-standards.md +72 -0
  30. package/bundled/core/immutability-first.md +105 -0
  31. package/bundled/core/model-selection.md +80 -0
  32. package/bundled/core/packaging-standards.md +216 -0
  33. package/bundled/core/recovery-recipe-registry.md +69 -0
  34. package/bundled/core/retry-standards.md +62 -0
  35. package/bundled/core/security-decision.md +65 -0
  36. package/bundled/core/skill-standard-alignment-check.md +79 -0
  37. package/bundled/core/standard-admission-criteria.md +84 -0
  38. package/bundled/core/standard-lifecycle-management.md +94 -0
  39. package/bundled/core/timeout-standards.md +63 -0
  40. package/bundled/core/token-budget.md +58 -0
  41. package/bundled/locales/zh-CN/CHANGELOG.md +22 -3
  42. package/bundled/locales/zh-CN/README.md +1 -1
  43. package/bundled/locales/zh-TW/CHANGELOG.md +22 -3
  44. package/bundled/locales/zh-TW/README.md +1 -1
  45. package/bundled/locales/zh-TW/core/anti-sycophancy-prompting.md +184 -0
  46. package/bundled/locales/zh-TW/core/packaging-standards.md +224 -0
  47. package/bundled/skills/e2e-assistant/SKILL.md +19 -5
  48. package/bundled/skills/testing-guide/SKILL.md +5 -0
  49. package/bundled/skills/testing-guide/test-skeleton-templates.md +316 -0
  50. package/package.json +1 -1
  51. package/src/commands/config.js +9 -0
  52. package/src/commands/init.js +91 -46
  53. package/src/commands/mcp.js +26 -0
  54. package/src/commands/run-intent.js +66 -0
  55. package/src/commands/update.js +35 -4
  56. package/src/core/command-router.js +85 -0
  57. package/src/core/project-config.js +91 -0
  58. package/src/flows/init-flow.js +6 -1
  59. package/src/i18n/messages.js +6 -6
  60. package/src/mcp/__tests__/server.test.js +251 -0
  61. package/src/mcp/server.js +352 -0
  62. package/src/prompts/init.js +157 -1
  63. package/src/reconciler/actual-state-scanner.js +24 -0
  64. package/src/uninstallers/hook-uninstaller.js +32 -1
  65. package/src/utils/e2e-analyzer.js +88 -5
  66. package/src/utils/e2e-detector.js +73 -1
  67. package/src/utils/integration-generator.js +22 -3
  68. package/standards-registry.json +193 -5
@@ -1,4 +1,6 @@
1
- import { select, checkbox, confirm as inquirerConfirm, Separator } from '@inquirer/prompts';
1
+ import { select, checkbox, confirm as inquirerConfirm, Separator, input } from '@inquirer/prompts';
2
+ import { existsSync, writeFileSync, readdirSync } from 'fs';
3
+ import { join } from 'path';
2
4
  import chalk from 'chalk';
3
5
  import os from 'os';
4
6
  import { t, setLanguage, detectLanguage } from '../i18n/messages.js';
@@ -1349,3 +1351,157 @@ export async function promptMethodology() {
1349
1351
 
1350
1352
  return methodology;
1351
1353
  }
1354
+
1355
+ // ─────────────────────────────────────────────────────────────────────────────
1356
+ // Project Command Contract (uds.project.yaml) — XSPEC-029 Phase 3
1357
+ // ─────────────────────────────────────────────────────────────────────────────
1358
+
1359
+ /**
1360
+ * Detect project ecosystem and return suggested commands.
1361
+ * @param {string} projectPath
1362
+ * @returns {{ test: string, lint: string, build: string, security: string }}
1363
+ */
1364
+ export function suggestCommands(projectPath) {
1365
+ const p = (f) => join(projectPath, f);
1366
+
1367
+ if (existsSync(p('package.json')))
1368
+ return { test: 'npm test', lint: 'npm run lint', build: 'npm run build', security: 'npm audit' };
1369
+
1370
+ if (existsSync(p('requirements.txt')) || existsSync(p('pyproject.toml')) || existsSync(p('setup.py')))
1371
+ return { test: 'python -m pytest', lint: 'python -m ruff check .', build: 'python -m build', security: 'pip-audit' };
1372
+
1373
+ if (existsSync(p('go.mod')))
1374
+ return { test: 'go test ./...', lint: 'go vet ./...', build: 'go build ./...', security: 'govulncheck ./...' };
1375
+
1376
+ if (existsSync(p('pom.xml')))
1377
+ return { test: 'mvn test', lint: 'mvn checkstyle:check', build: 'mvn package', security: 'mvn dependency-check:check' };
1378
+
1379
+ if (existsSync(p('build.gradle')) || existsSync(p('build.gradle.kts')))
1380
+ return { test: './gradlew test', lint: './gradlew checkstyleMain', build: './gradlew build', security: './gradlew dependencyCheckAnalyze' };
1381
+
1382
+ if (existsSync(p('Cargo.toml')))
1383
+ return { test: 'cargo test', lint: 'cargo clippy', build: 'cargo build', security: 'cargo audit' };
1384
+
1385
+ if (existsSync(p('Gemfile')))
1386
+ return { test: 'bundle exec rspec', lint: 'bundle exec rubocop', build: 'gem build *.gemspec', security: 'bundle audit' };
1387
+
1388
+ // C# — glob for .csproj / .sln
1389
+ try {
1390
+ const files = readdirSync(projectPath);
1391
+ if (files.some(f => f.endsWith('.csproj') || f.endsWith('.sln')))
1392
+ return { test: 'dotnet test', lint: 'dotnet format --verify-no-changes', build: 'dotnet build', security: 'dotnet list package --vulnerable' };
1393
+ } catch {
1394
+ // ignore read errors
1395
+ }
1396
+
1397
+ // Unknown ecosystem — return empty strings so user fills them in
1398
+ return { test: '', lint: '', build: '', security: '' };
1399
+ }
1400
+
1401
+ /**
1402
+ * Generate uds.project.yaml content from a commands map.
1403
+ * @param {{ test?: string, lint?: string, build?: string, security?: string }} commands
1404
+ * @returns {string}
1405
+ */
1406
+ export function generateProjectConfigYaml(commands) {
1407
+ const lines = ['version: "1"', '', 'commands:'];
1408
+ for (const [intent, cmd] of Object.entries(commands)) {
1409
+ if (cmd && cmd.trim()) {
1410
+ lines.push(` ${intent}: ${cmd.trim()}`);
1411
+ }
1412
+ }
1413
+ return lines.join('\n') + '\n';
1414
+ }
1415
+
1416
+ /**
1417
+ * Wraps promptProjectCommandContract with an initial confirm prompt.
1418
+ * Used as the optional Step 13 in the init flow so the whole step can be
1419
+ * mocked in tests as a single unit.
1420
+ *
1421
+ * @param {string} projectPath - Absolute project root path
1422
+ * @returns {Promise<void>}
1423
+ */
1424
+ export async function promptProjectContractStep(projectPath) {
1425
+ try {
1426
+ const wantContract = await inquirerConfirm({
1427
+ message: 'Create uds.project.yaml (project command contract)?',
1428
+ default: true
1429
+ });
1430
+ if (wantContract) {
1431
+ await promptProjectCommandContract(projectPath);
1432
+ }
1433
+ // If false — silently skip (caller can show a hint message)
1434
+ } catch {
1435
+ // Ctrl+C or any error — treat as skip
1436
+ }
1437
+ }
1438
+
1439
+ /**
1440
+ * Interactive wizard that guides the user through creating uds.project.yaml.
1441
+ * Detects the project ecosystem and pre-fills defaults.
1442
+ *
1443
+ * @param {string} projectPath - Absolute project root path
1444
+ * @returns {Promise<boolean>} true if the file was written, false if skipped
1445
+ */
1446
+ export async function promptProjectCommandContract(projectPath) {
1447
+ const contractPath = join(projectPath, 'uds.project.yaml');
1448
+
1449
+ // If already exists, ask whether to overwrite
1450
+ if (existsSync(contractPath)) {
1451
+ console.log();
1452
+ console.log(chalk.yellow(' uds.project.yaml already exists.'));
1453
+
1454
+ const overwrite = await inquirerConfirm({
1455
+ message: 'Overwrite existing uds.project.yaml?',
1456
+ default: false
1457
+ });
1458
+
1459
+ if (!overwrite) {
1460
+ console.log(chalk.gray(' Skipped — keeping existing uds.project.yaml'));
1461
+ return false;
1462
+ }
1463
+ }
1464
+
1465
+ const suggestions = suggestCommands(projectPath);
1466
+ const hasAnySuggestion = Object.values(suggestions).some(v => v);
1467
+
1468
+ console.log();
1469
+ console.log(chalk.bold('📋 Project Command Contract (uds.project.yaml)'));
1470
+ console.log(chalk.gray(' Tell UDS how to run standard commands in this project (language-agnostic).'));
1471
+ console.log(chalk.gray(' Press Enter to accept a suggestion, or type your own. Leave blank to skip.'));
1472
+ console.log();
1473
+
1474
+ if (hasAnySuggestion) {
1475
+ console.log(chalk.gray(' Detected suggestions:'));
1476
+ for (const [intent, cmd] of Object.entries(suggestions)) {
1477
+ if (cmd) console.log(chalk.gray(` ${intent}: ${cmd}`));
1478
+ }
1479
+ console.log();
1480
+ }
1481
+
1482
+ const intents = ['test', 'lint', 'build', 'security'];
1483
+ const commands = {};
1484
+
1485
+ for (const intent of intents) {
1486
+ const defaultVal = suggestions[intent] || '';
1487
+ const answer = await input({
1488
+ message: ` ${intent} command${defaultVal ? ` (default: ${defaultVal})` : ' (optional)'}:`,
1489
+ default: defaultVal
1490
+ });
1491
+ commands[intent] = answer.trim();
1492
+ }
1493
+
1494
+ const hasAnyCommand = Object.values(commands).some(v => v);
1495
+ if (!hasAnyCommand) {
1496
+ console.log(chalk.gray(' No commands entered — uds.project.yaml not created.'));
1497
+ return false;
1498
+ }
1499
+
1500
+ const yaml = generateProjectConfigYaml(commands);
1501
+ writeFileSync(contractPath, yaml, 'utf-8');
1502
+
1503
+ console.log();
1504
+ console.log(chalk.green(' ✔ uds.project.yaml created'));
1505
+ console.log(chalk.gray(` Path: ${contractPath}`));
1506
+ return true;
1507
+ }
@@ -340,6 +340,30 @@ function scanHook(state, projectPath) {
340
340
  } catch {
341
341
  // Ignore read errors
342
342
  }
343
+
344
+ // Also scan native .git/hooks/pre-commit (installed by uds init for non-Node projects)
345
+ const nativeHookPath = join(projectPath, '.git', 'hooks', 'pre-commit');
346
+ if (existsSync(nativeHookPath)) {
347
+ try {
348
+ const content = readFileSync(nativeHookPath, 'utf-8');
349
+ const udsLines = content.split('\n').filter(line =>
350
+ line.includes('uds') || line.includes('UDS') || line.includes('.standards')
351
+ );
352
+
353
+ if (udsLines.length > 0) {
354
+ state.hook.set('.git/hooks/pre-commit', {
355
+ relativePath: '.git/hooks/pre-commit',
356
+ hash: computeFileHash(join(projectPath, '.git', 'hooks', 'pre-commit'))?.hash || null,
357
+ size: null,
358
+ category: 'hook',
359
+ sourcePath: null,
360
+ metadata: { udsLines, scanned: true, type: 'native' }
361
+ });
362
+ }
363
+ } catch {
364
+ // Ignore read errors
365
+ }
366
+ }
343
367
  }
344
368
 
345
369
  /**
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, writeFileSync } from 'fs';
1
+ import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
2
2
  import { join } from 'path';
3
3
 
4
4
  /**
@@ -39,5 +39,36 @@ export function uninstallHook(projectPath, options = {}) {
39
39
  result.errors.push(`.husky/pre-commit — ${error.message}`);
40
40
  }
41
41
 
42
+ // Also handle native .git/hooks/pre-commit (installed by uds init for non-Node projects)
43
+ const nativeHookPath = join(projectPath, '.git', 'hooks', 'pre-commit');
44
+ if (existsSync(nativeHookPath)) {
45
+ try {
46
+ const content = readFileSync(nativeHookPath, 'utf-8');
47
+ const udsPattern = /uds\s+check|checkin-standards|UDS pre-commit hook/;
48
+
49
+ if (!udsPattern.test(content)) {
50
+ result.skipped.push('.git/hooks/pre-commit (no UDS lines found)');
51
+ } else if (dryRun) {
52
+ result.removed.push('.git/hooks/pre-commit (UDS native hook)');
53
+ } else {
54
+ // Remove only UDS-related lines, keep other hook content
55
+ const lines = content.split('\n');
56
+ const filtered = lines.filter(line => !udsPattern.test(line));
57
+
58
+ // If only shebang remains, remove the file entirely; otherwise rewrite
59
+ const nonEmpty = filtered.filter(l => l.trim() && l.trim() !== '#!/bin/sh');
60
+ if (nonEmpty.length === 0) {
61
+ unlinkSync(nativeHookPath);
62
+ result.removed.push('.git/hooks/pre-commit (UDS native hook, file removed)');
63
+ } else {
64
+ writeFileSync(nativeHookPath, filtered.join('\n'), 'utf-8');
65
+ result.removed.push('.git/hooks/pre-commit (UDS lines removed)');
66
+ }
67
+ }
68
+ } catch (error) {
69
+ result.errors.push(`.git/hooks/pre-commit — ${error.message}`);
70
+ }
71
+ }
72
+
42
73
  return result;
43
74
  }
@@ -192,6 +192,22 @@ export function analyzeExistingPatterns(e2eDir) {
192
192
  // REQ-4: E2E 測試骨架生成
193
193
  // ============================================================
194
194
 
195
+ /**
196
+ * Detect the project ecosystem based on manifest files
197
+ * @param {string} [projectPath='.'] - Path to the project root
198
+ * @returns {'node'|'python'|'go'|'java'|'rust'|'ruby'}
199
+ */
200
+ function detectEcosystem(projectPath = '.') {
201
+ const p = (f) => join(projectPath, f);
202
+ if (existsSync(p('package.json'))) return 'node';
203
+ if (existsSync(p('requirements.txt')) || existsSync(p('pyproject.toml'))) return 'python';
204
+ if (existsSync(p('go.mod'))) return 'go';
205
+ if (existsSync(p('pom.xml')) || existsSync(p('build.gradle'))) return 'java';
206
+ if (existsSync(p('Cargo.toml'))) return 'rust';
207
+ if (existsSync(p('Gemfile'))) return 'ruby';
208
+ return 'node'; // fallback
209
+ }
210
+
195
211
  const SKELETON_TEMPLATES = {
196
212
  vitest: {
197
213
  header: (specId) => `/**
@@ -298,6 +314,65 @@ beforeEach(() => {
298
314
  // [TODO] Verify expected outcomes
299
315
  // cy.contains('expected text').should('be.visible');
300
316
  });
317
+ `
318
+ },
319
+
320
+ // Python (pytest-playwright)
321
+ 'pytest-playwright': {
322
+ header: (specId) => `# [Generated] E2E tests for ${specId}
323
+ # [TODO] Fill in test implementations
324
+
325
+ import pytest
326
+ from playwright.sync_api import Page
327
+ `,
328
+ setup: () => `
329
+ # [TODO] Setup fixtures and test data
330
+ `,
331
+ test: (scenario) => `
332
+ # @${scenario.specId} @${scenario.ac}
333
+ def test_${scenario.name.toLowerCase().replace(/\s+/g, '_')}(page: Page):
334
+ # Arrange
335
+ # [TODO] Setup test preconditions
336
+
337
+ # Act
338
+ # [TODO] Execute the user flow
339
+
340
+ # Assert
341
+ # [TODO] Verify expected outcomes
342
+ `
343
+ },
344
+
345
+ // Go (chromedp)
346
+ chromedp: {
347
+ header: (specId) => `// [Generated] E2E tests for ${specId}
348
+ // [TODO] Fill in test implementations
349
+
350
+ package e2e_test
351
+
352
+ import (
353
+ \t"context"
354
+ \t"testing"
355
+ \t"github.com/chromedp/chromedp"
356
+ )
357
+ `,
358
+ setup: () => `
359
+ // [TODO] Setup shared test context if needed
360
+ `,
361
+ test: (scenario) => `
362
+ // @${scenario.specId} @${scenario.ac}
363
+ func Test${scenario.name.replace(/\s+/g, '')}(t *testing.T) {
364
+ \tctx, cancel := chromedp.NewContext(context.Background())
365
+ \tdefer cancel()
366
+
367
+ \t// Arrange
368
+ \t// [TODO] Setup test preconditions
369
+
370
+ \t// Act
371
+ \t// [TODO] Add chromedp tasks
372
+
373
+ \t// Assert
374
+ \t// [TODO] Verify expected outcomes
375
+ }
301
376
  `
302
377
  }
303
378
  };
@@ -308,16 +383,24 @@ beforeEach(() => {
308
383
  * @param {{ framework: string }} options
309
384
  * @returns {string}
310
385
  */
311
- export function generateE2eSkeleton(scenarios, { framework = 'vitest' } = {}) {
312
- const template = SKELETON_TEMPLATES[framework] || SKELETON_TEMPLATES.vitest;
386
+ export function generateE2eSkeleton(scenarios, { framework = 'vitest', projectPath } = {}) {
387
+ // If no framework is explicitly specified (using default), detect ecosystem and adjust
388
+ const resolvedFramework = (() => {
389
+ if (framework !== 'vitest') return framework; // caller specified explicitly
390
+ const ecosystem = detectEcosystem(projectPath);
391
+ if (ecosystem === 'python') return 'pytest-playwright';
392
+ if (ecosystem === 'go') return 'chromedp';
393
+ return framework;
394
+ })();
395
+ const template = SKELETON_TEMPLATES[resolvedFramework] || SKELETON_TEMPLATES.vitest;
313
396
  const specId = scenarios[0]?.specId || 'SPEC-XXX';
314
397
 
315
398
  let output = template.header(specId);
316
399
  output += template.setup();
317
400
 
318
- if (framework === 'vitest') {
401
+ if (resolvedFramework === 'vitest') {
319
402
  output += `\ndescribe('E2E: ${specId}', () => {`;
320
- } else if (framework === 'cypress') {
403
+ } else if (resolvedFramework === 'cypress') {
321
404
  output += `\ndescribe('E2E: ${specId}', () => {`;
322
405
  }
323
406
 
@@ -325,7 +408,7 @@ export function generateE2eSkeleton(scenarios, { framework = 'vitest' } = {}) {
325
408
  output += template.test(s);
326
409
  }
327
410
 
328
- if (framework === 'vitest' || framework === 'cypress') {
411
+ if (resolvedFramework === 'vitest' || resolvedFramework === 'cypress') {
329
412
  output += '});\n';
330
413
  }
331
414
 
@@ -1,7 +1,20 @@
1
1
  import { existsSync, readFileSync, readdirSync } from 'fs';
2
2
  import { join } from 'path';
3
3
 
4
- const SUPPORTED_FRAMEWORKS = ['playwright', 'cypress', 'vitest'];
4
+ const SUPPORTED_FRAMEWORKS = [
5
+ // JavaScript/TypeScript
6
+ 'playwright', 'cypress', 'vitest', 'webdriverio',
7
+ // Python
8
+ 'pytest-playwright', 'selenium-python', 'robot-framework', 'behave',
9
+ // Java
10
+ 'selenium-java', 'playwright-java', 'gauge',
11
+ // Go
12
+ 'chromedp', 'rod',
13
+ // C#
14
+ 'playwright-dotnet', 'selenium-dotnet',
15
+ // Ruby
16
+ 'capybara', 'watir',
17
+ ];
5
18
 
6
19
  /**
7
20
  * Detect E2E testing frameworks in the project
@@ -11,6 +24,65 @@ const SUPPORTED_FRAMEWORKS = ['playwright', 'cypress', 'vitest'];
11
24
  export function detectE2eFramework(projectPath) {
12
25
  const detected = [];
13
26
 
27
+ // Python ecosystem detection
28
+ const pythonFiles = ['requirements.txt', 'pyproject.toml'];
29
+ for (const pyFile of pythonFiles) {
30
+ const pyPath = join(projectPath, pyFile);
31
+ if (existsSync(pyPath)) {
32
+ try {
33
+ const content = readFileSync(pyPath, 'utf-8');
34
+ if (content.includes('pytest-playwright')) detected.push('pytest-playwright');
35
+ if (content.includes('selenium')) detected.push('selenium-python');
36
+ if (content.includes('robotframework')) detected.push('robot-framework');
37
+ if (content.includes('behave')) detected.push('behave');
38
+ } catch {
39
+ // Ignore read errors
40
+ }
41
+ break;
42
+ }
43
+ }
44
+
45
+ // Go ecosystem detection
46
+ const goModPath = join(projectPath, 'go.mod');
47
+ if (existsSync(goModPath)) {
48
+ try {
49
+ const content = readFileSync(goModPath, 'utf-8');
50
+ if (content.includes('chromedp')) detected.push('chromedp');
51
+ if (content.includes('/rod')) detected.push('rod');
52
+ } catch {
53
+ // Ignore read errors
54
+ }
55
+ }
56
+
57
+ // Java ecosystem detection
58
+ for (const javaFile of ['pom.xml', 'build.gradle']) {
59
+ const javaPath = join(projectPath, javaFile);
60
+ if (existsSync(javaPath)) {
61
+ try {
62
+ const content = readFileSync(javaPath, 'utf-8');
63
+ if (content.includes('selenium')) detected.push('selenium-java');
64
+ if (content.includes('playwright')) detected.push('playwright-java');
65
+ if (content.includes('gauge')) detected.push('gauge');
66
+ } catch {
67
+ // Ignore read errors
68
+ }
69
+ break;
70
+ }
71
+ }
72
+
73
+ // Ruby ecosystem detection
74
+ const gemfilePath = join(projectPath, 'Gemfile');
75
+ if (existsSync(gemfilePath)) {
76
+ try {
77
+ const content = readFileSync(gemfilePath, 'utf-8');
78
+ if (content.includes('capybara')) detected.push('capybara');
79
+ if (content.includes('watir')) detected.push('watir');
80
+ } catch {
81
+ // Ignore read errors
82
+ }
83
+ }
84
+
85
+ // JavaScript/TypeScript ecosystem detection
14
86
  const packagePath = join(projectPath, 'package.json');
15
87
  if (existsSync(packagePath)) {
16
88
  try {
@@ -3097,6 +3097,24 @@ export function getToolFilePath(tool) {
3097
3097
 
3098
3098
 
3099
3099
 
3100
+ /**
3101
+ * Get default commands based on ecosystem
3102
+ * @param {'node'|'python'|'go'|'java_maven'|'java_gradle'|'rust'|'ruby'} ecosystem
3103
+ * @returns {{ test: string, lint: string, build: string, install: string }}
3104
+ */
3105
+ function getDefaultCommands(ecosystem) {
3106
+ const commands = {
3107
+ node: { test: 'npm test', lint: 'npm run lint', build: 'npm run build', install: 'npm install' },
3108
+ python: { test: 'python -m pytest', lint: 'python -m ruff check .', build: 'python -m build', install: 'pip install -r requirements.txt' },
3109
+ go: { test: 'go test ./...', lint: 'go vet ./...', build: 'go build ./...', install: 'go mod download' },
3110
+ java_maven: { test: 'mvn test', lint: 'mvn checkstyle:check', build: 'mvn package', install: 'mvn dependency:resolve' },
3111
+ java_gradle: { test: './gradlew test', lint: './gradlew checkstyleMain', build: './gradlew build', install: './gradlew dependencies' },
3112
+ rust: { test: 'cargo test', lint: 'cargo clippy', build: 'cargo build', install: 'cargo fetch' },
3113
+ ruby: { test: 'bundle exec rspec', lint: 'bundle exec rubocop', build: 'gem build *.gemspec', install: 'bundle install' },
3114
+ };
3115
+ return commands[ecosystem] || commands.node;
3116
+ }
3117
+
3100
3118
  /**
3101
3119
  * Detect build/test commands based on project files
3102
3120
  * @param {string} [projectPath] - Project root path
@@ -3104,11 +3122,12 @@ export function getToolFilePath(tool) {
3104
3122
  */
3105
3123
  function detectBuildCommands(projectPath) {
3106
3124
  if (!projectPath) {
3125
+ const defaultCmds = getDefaultCommands('node');
3107
3126
  return [
3108
3127
  '# Check project configuration for build commands',
3109
- 'npm install # Install dependencies (Node.js)',
3110
- 'npm test # Run tests',
3111
- 'npm run lint # Check code style'
3128
+ `${defaultCmds.install} # Install dependencies (Node.js)`,
3129
+ `${defaultCmds.test} # Run tests`,
3130
+ `${defaultCmds.lint} # Check code style`
3112
3131
  ];
3113
3132
  }
3114
3133