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

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 (108) hide show
  1. package/README.md +6 -0
  2. package/bin/uds.js +14 -0
  3. package/bundled/ai/standards/agent-communication-protocol.ai.yaml +34 -0
  4. package/bundled/ai/standards/anti-sycophancy-prompting.ai.yaml +111 -0
  5. package/bundled/ai/standards/capability-declaration.ai.yaml +113 -0
  6. package/bundled/ai/standards/circuit-breaker.ai.yaml +93 -0
  7. package/bundled/ai/standards/developer-memory.ai.yaml +13 -0
  8. package/bundled/ai/standards/dual-phase-output.ai.yaml +108 -0
  9. package/bundled/ai/standards/failure-source-taxonomy.ai.yaml +115 -0
  10. package/bundled/ai/standards/frontend-design-standards.ai.yaml +305 -0
  11. package/bundled/ai/standards/health-check-standards.ai.yaml +140 -0
  12. package/bundled/ai/standards/immutability-first.ai.yaml +112 -0
  13. package/bundled/ai/standards/model-selection.ai.yaml +111 -3
  14. package/bundled/ai/standards/packaging-standards.ai.yaml +142 -0
  15. package/bundled/ai/standards/recovery-recipe-registry.ai.yaml +200 -0
  16. package/bundled/ai/standards/retry-standards.ai.yaml +134 -0
  17. package/bundled/ai/standards/security-decision.ai.yaml +87 -0
  18. package/bundled/ai/standards/skill-standard-alignment-check.ai.yaml +119 -0
  19. package/bundled/ai/standards/standard-admission-criteria.ai.yaml +107 -0
  20. package/bundled/ai/standards/standard-lifecycle-management.ai.yaml +144 -0
  21. package/bundled/ai/standards/timeout-standards.ai.yaml +104 -0
  22. package/bundled/ai/standards/token-budget.ai.yaml +108 -0
  23. package/bundled/ai/standards/translation-lifecycle-standards.ai.yaml +145 -0
  24. package/bundled/core/anti-sycophancy-prompting.md +184 -0
  25. package/bundled/core/capability-declaration.md +59 -0
  26. package/bundled/core/circuit-breaker.md +58 -0
  27. package/bundled/core/developer-memory.md +29 -1
  28. package/bundled/core/dual-phase-output.md +56 -0
  29. package/bundled/core/failure-source-taxonomy.md +72 -0
  30. package/bundled/core/frontend-design-standards.md +474 -0
  31. package/bundled/core/health-check-standards.md +72 -0
  32. package/bundled/core/immutability-first.md +105 -0
  33. package/bundled/core/model-selection.md +80 -0
  34. package/bundled/core/packaging-standards.md +216 -0
  35. package/bundled/core/recovery-recipe-registry.md +69 -0
  36. package/bundled/core/retry-standards.md +62 -0
  37. package/bundled/core/security-decision.md +65 -0
  38. package/bundled/core/skill-standard-alignment-check.md +79 -0
  39. package/bundled/core/standard-admission-criteria.md +84 -0
  40. package/bundled/core/standard-lifecycle-management.md +94 -0
  41. package/bundled/core/timeout-standards.md +63 -0
  42. package/bundled/core/token-budget.md +58 -0
  43. package/bundled/core/translation-lifecycle-standards.md +162 -0
  44. package/bundled/locales/zh-CN/CHANGELOG.md +51 -3
  45. package/bundled/locales/zh-CN/README.md +1 -1
  46. package/bundled/locales/zh-CN/core/anti-hallucination.md +22 -3
  47. package/bundled/locales/zh-CN/core/anti-sycophancy-prompting.md +192 -0
  48. package/bundled/locales/zh-CN/core/capability-declaration.md +123 -0
  49. package/bundled/locales/zh-CN/core/circuit-breaker.md +106 -0
  50. package/bundled/locales/zh-CN/core/dual-phase-output.md +103 -0
  51. package/bundled/locales/zh-CN/core/failure-source-taxonomy.md +99 -0
  52. package/bundled/locales/zh-CN/core/frontend-design-standards.md +289 -0
  53. package/bundled/locales/zh-CN/core/health-check-standards.md +144 -0
  54. package/bundled/locales/zh-CN/core/immutability-first.md +96 -0
  55. package/bundled/locales/zh-CN/core/packaging-standards.md +224 -0
  56. package/bundled/locales/zh-CN/core/recovery-recipe-registry.md +146 -0
  57. package/bundled/locales/zh-CN/core/retry-standards.md +131 -0
  58. package/bundled/locales/zh-CN/core/security-decision.md +104 -0
  59. package/bundled/locales/zh-CN/core/skill-standard-alignment-check.md +112 -0
  60. package/bundled/locales/zh-CN/core/standard-admission-criteria.md +104 -0
  61. package/bundled/locales/zh-CN/core/standard-lifecycle-management.md +116 -0
  62. package/bundled/locales/zh-CN/core/timeout-standards.md +117 -0
  63. package/bundled/locales/zh-CN/core/token-budget.md +108 -0
  64. package/bundled/locales/zh-CN/core/translation-lifecycle-standards.md +159 -0
  65. package/bundled/locales/zh-TW/CHANGELOG.md +51 -3
  66. package/bundled/locales/zh-TW/README.md +1 -1
  67. package/bundled/locales/zh-TW/core/anti-sycophancy-prompting.md +192 -0
  68. package/bundled/locales/zh-TW/core/capability-declaration.md +111 -0
  69. package/bundled/locales/zh-TW/core/circuit-breaker.md +111 -0
  70. package/bundled/locales/zh-TW/core/dual-phase-output.md +132 -0
  71. package/bundled/locales/zh-TW/core/failure-source-taxonomy.md +146 -0
  72. package/bundled/locales/zh-TW/core/frontend-design-standards.md +460 -0
  73. package/bundled/locales/zh-TW/core/health-check-standards.md +144 -0
  74. package/bundled/locales/zh-TW/core/immutability-first.md +159 -0
  75. package/bundled/locales/zh-TW/core/packaging-standards.md +224 -0
  76. package/bundled/locales/zh-TW/core/recovery-recipe-registry.md +146 -0
  77. package/bundled/locales/zh-TW/core/retry-standards.md +140 -0
  78. package/bundled/locales/zh-TW/core/security-decision.md +120 -0
  79. package/bundled/locales/zh-TW/core/skill-standard-alignment-check.md +112 -0
  80. package/bundled/locales/zh-TW/core/standard-admission-criteria.md +104 -0
  81. package/bundled/locales/zh-TW/core/standard-lifecycle-management.md +116 -0
  82. package/bundled/locales/zh-TW/core/timeout-standards.md +117 -0
  83. package/bundled/locales/zh-TW/core/token-budget.md +143 -0
  84. package/bundled/locales/zh-TW/core/translation-lifecycle-standards.md +159 -0
  85. package/bundled/skills/e2e-assistant/SKILL.md +19 -5
  86. package/bundled/skills/testing-guide/SKILL.md +5 -0
  87. package/bundled/skills/testing-guide/test-skeleton-templates.md +316 -0
  88. package/package.json +2 -1
  89. package/src/commands/check.js +6 -0
  90. package/src/commands/config.js +9 -0
  91. package/src/commands/init.js +97 -46
  92. package/src/commands/mcp.js +26 -0
  93. package/src/commands/run-intent.js +66 -0
  94. package/src/commands/update.js +41 -4
  95. package/src/core/command-router.js +85 -0
  96. package/src/core/project-config.js +91 -0
  97. package/src/flows/init-flow.js +6 -1
  98. package/src/i18n/messages.js +6 -6
  99. package/src/mcp/__tests__/server.test.js +251 -0
  100. package/src/mcp/server.js +352 -0
  101. package/src/prompts/init.js +157 -1
  102. package/src/reconciler/actual-state-scanner.js +24 -0
  103. package/src/uninstallers/hook-uninstaller.js +32 -1
  104. package/src/utils/detect-self-adoption.js +173 -0
  105. package/src/utils/e2e-analyzer.js +88 -5
  106. package/src/utils/e2e-detector.js +73 -1
  107. package/src/utils/integration-generator.js +22 -3
  108. package/standards-registry.json +203 -4
@@ -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
  }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * UDS Self-Adoption Detection
3
+ *
4
+ * Detect whether the current working directory is the UDS source repo itself.
5
+ * When detected, adoption-oriented CLI commands (update / check / init) MUST
6
+ * refuse to run — otherwise the CLI would overwrite source-of-truth
7
+ * `.standards/` with its own npm-bundled copy (DEC-044 / XSPEC-071).
8
+ *
9
+ * Detection uses three independent heuristics. ANY one being true is
10
+ * sufficient, but each is validated to reduce false positives:
11
+ *
12
+ * 1. `uds-manifest.json` in cwd whose `project === "Universal Development
13
+ * Standards"` (strong signal — bundled manifests are adopter-named).
14
+ * 2. `cli/package.json` in cwd whose `name === "universal-dev-standards"`
15
+ * (strong signal — only the UDS source repo ships this package).
16
+ * 3. `.uds-source-repo` marker file in cwd (explicit opt-in marker).
17
+ *
18
+ * The three signals are OR-combined: any single hit is enough. A consumer
19
+ * (adopter project) will hit none of them even if they installed UDS.
20
+ *
21
+ * Related:
22
+ * - DEC-044: UDS Self-Adoption Bug
23
+ * - XSPEC-071: UDS Self-Adoption Protection
24
+ */
25
+
26
+ import { existsSync, readFileSync } from 'fs';
27
+ import { join } from 'path';
28
+
29
+ /**
30
+ * Indicator returned when detection matches, useful for diagnostics.
31
+ *
32
+ * @typedef {Object} SelfAdoptionResult
33
+ * @property {boolean} isSelfAdoption - True when cwd is UDS source repo
34
+ * @property {string[]} signals - Names of heuristics that matched
35
+ */
36
+
37
+ /**
38
+ * Check whether `uds-manifest.json` at `cwd` identifies UDS source repo.
39
+ */
40
+ function hasSourceManifest(cwd) {
41
+ const manifestPath = join(cwd, 'uds-manifest.json');
42
+ if (!existsSync(manifestPath)) return false;
43
+ try {
44
+ const content = readFileSync(manifestPath, 'utf8');
45
+ const manifest = JSON.parse(content);
46
+ return manifest?.project === 'Universal Development Standards';
47
+ } catch {
48
+ // Malformed JSON: fall back to "not matched" rather than throwing;
49
+ // the file alone is not strong enough proof without verified content.
50
+ return false;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Check whether `cli/package.json` at `cwd` identifies UDS source repo.
56
+ */
57
+ function hasSourceCliPackage(cwd) {
58
+ const pkgPath = join(cwd, 'cli', 'package.json');
59
+ if (!existsSync(pkgPath)) return false;
60
+ try {
61
+ const content = readFileSync(pkgPath, 'utf8');
62
+ const pkg = JSON.parse(content);
63
+ return pkg?.name === 'universal-dev-standards';
64
+ } catch {
65
+ return false;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Check whether the explicit marker file exists.
71
+ */
72
+ function hasSourceMarker(cwd) {
73
+ return existsSync(join(cwd, '.uds-source-repo'));
74
+ }
75
+
76
+ /**
77
+ * Detect UDS self-adoption situation.
78
+ *
79
+ * @param {string} [cwd=process.cwd()] - Directory to inspect
80
+ * @returns {boolean} True when cwd is (likely) the UDS source repo
81
+ */
82
+ export function detectSelfAdoption(cwd = process.cwd()) {
83
+ return detectSelfAdoptionDetailed(cwd).isSelfAdoption;
84
+ }
85
+
86
+ /**
87
+ * Detect UDS self-adoption and return detailed signal breakdown.
88
+ *
89
+ * @param {string} [cwd=process.cwd()] - Directory to inspect
90
+ * @returns {SelfAdoptionResult} Detection outcome with matched signals
91
+ */
92
+ export function detectSelfAdoptionDetailed(cwd = process.cwd()) {
93
+ const signals = [];
94
+ if (hasSourceManifest(cwd)) signals.push('uds-manifest.json');
95
+ if (hasSourceCliPackage(cwd)) signals.push('cli/package.json');
96
+ if (hasSourceMarker(cwd)) signals.push('.uds-source-repo');
97
+ return {
98
+ isSelfAdoption: signals.length > 0,
99
+ signals
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Bilingual refuse message shown when self-adoption is detected and
105
+ * `--force` is not passed. Kept inline (not in i18n bundle) to avoid
106
+ * taking a translation dependency for a fail-safe error path.
107
+ *
108
+ * @param {string} commandName - e.g. 'update' / 'check' / 'init'
109
+ * @returns {string[]} Lines to print
110
+ */
111
+ export function formatSelfAdoptionRefuseMessage(commandName) {
112
+ return [
113
+ `偵測到 UDS source repo。此指令(uds ${commandName})僅供採用專案使用。`,
114
+ 'Source repo 維護請使用 scripts/bump-version.sh 或 npm run docs:sync。',
115
+ '詳見 DEC-044。',
116
+ '',
117
+ `Detected UDS source repo. This command (uds ${commandName}) is for`,
118
+ 'adopter projects only. For source-repo maintenance use',
119
+ 'scripts/bump-version.sh or npm run docs:sync. See DEC-044.',
120
+ '',
121
+ 'Override with --force if you know what you are doing',
122
+ '(若確定要執行可加上 --force 旗標繞過).'
123
+ ];
124
+ }
125
+
126
+ /**
127
+ * Warning printed when `--force` bypasses the self-adoption guard.
128
+ *
129
+ * @param {string} commandName
130
+ * @returns {string[]} Lines to print
131
+ */
132
+ export function formatSelfAdoptionForceWarning(commandName) {
133
+ return [
134
+ `警告:偵測到 UDS source repo,但 --force 已指定,繼續執行 uds ${commandName}。`,
135
+ `Warning: UDS source repo detected; --force was passed, continuing uds ${commandName}.`,
136
+ '若非預期操作,請立即 Ctrl+C 並改用 scripts/bump-version.sh。',
137
+ 'If unintended, abort now (Ctrl+C) and use scripts/bump-version.sh.'
138
+ ];
139
+ }
140
+
141
+ /**
142
+ * Guard helper — prints refuse message and exits with non-zero code when
143
+ * cwd is the UDS source repo and `--force` was not supplied.
144
+ *
145
+ * Does nothing when not self-adoption, or when `--force` is set (in which
146
+ * case a warning is printed but execution proceeds).
147
+ *
148
+ * @param {string} commandName - CLI command being invoked (for message)
149
+ * @param {Object} [options] - Caller options; inspected for `force`
150
+ * @param {string} [cwd=process.cwd()]
151
+ * @returns {boolean} True if command should continue (caller keeps running)
152
+ */
153
+ export function guardAgainstSelfAdoption(commandName, options = {}, cwd = process.cwd()) {
154
+ const { isSelfAdoption, signals } = detectSelfAdoptionDetailed(cwd);
155
+ if (!isSelfAdoption) return true;
156
+
157
+ const force = Boolean(options && options.force);
158
+ if (force) {
159
+ for (const line of formatSelfAdoptionForceWarning(commandName)) {
160
+ console.warn(line);
161
+ }
162
+ console.warn(`Matched signals: ${signals.join(', ')}`);
163
+ return true;
164
+ }
165
+
166
+ for (const line of formatSelfAdoptionRefuseMessage(commandName)) {
167
+ console.error(line);
168
+ }
169
+ console.error(`Matched signals: ${signals.join(', ')}`);
170
+ process.exit(1);
171
+ // Unreachable, but for tests that stub process.exit:
172
+ return false;
173
+ }
@@ -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