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.
- package/README.md +6 -0
- package/bin/uds.js +14 -0
- package/bundled/ai/standards/agent-communication-protocol.ai.yaml +34 -0
- package/bundled/ai/standards/anti-sycophancy-prompting.ai.yaml +111 -0
- package/bundled/ai/standards/capability-declaration.ai.yaml +113 -0
- package/bundled/ai/standards/circuit-breaker.ai.yaml +93 -0
- package/bundled/ai/standards/developer-memory.ai.yaml +13 -0
- package/bundled/ai/standards/dual-phase-output.ai.yaml +108 -0
- package/bundled/ai/standards/failure-source-taxonomy.ai.yaml +115 -0
- package/bundled/ai/standards/frontend-design-standards.ai.yaml +305 -0
- package/bundled/ai/standards/health-check-standards.ai.yaml +140 -0
- package/bundled/ai/standards/immutability-first.ai.yaml +112 -0
- package/bundled/ai/standards/model-selection.ai.yaml +111 -3
- package/bundled/ai/standards/packaging-standards.ai.yaml +142 -0
- package/bundled/ai/standards/recovery-recipe-registry.ai.yaml +200 -0
- package/bundled/ai/standards/retry-standards.ai.yaml +134 -0
- package/bundled/ai/standards/security-decision.ai.yaml +87 -0
- package/bundled/ai/standards/skill-standard-alignment-check.ai.yaml +119 -0
- package/bundled/ai/standards/standard-admission-criteria.ai.yaml +107 -0
- package/bundled/ai/standards/standard-lifecycle-management.ai.yaml +144 -0
- package/bundled/ai/standards/timeout-standards.ai.yaml +104 -0
- package/bundled/ai/standards/token-budget.ai.yaml +108 -0
- package/bundled/ai/standards/translation-lifecycle-standards.ai.yaml +145 -0
- package/bundled/core/anti-sycophancy-prompting.md +184 -0
- package/bundled/core/capability-declaration.md +59 -0
- package/bundled/core/circuit-breaker.md +58 -0
- package/bundled/core/developer-memory.md +29 -1
- package/bundled/core/dual-phase-output.md +56 -0
- package/bundled/core/failure-source-taxonomy.md +72 -0
- package/bundled/core/frontend-design-standards.md +474 -0
- package/bundled/core/health-check-standards.md +72 -0
- package/bundled/core/immutability-first.md +105 -0
- package/bundled/core/model-selection.md +80 -0
- package/bundled/core/packaging-standards.md +216 -0
- package/bundled/core/recovery-recipe-registry.md +69 -0
- package/bundled/core/retry-standards.md +62 -0
- package/bundled/core/security-decision.md +65 -0
- package/bundled/core/skill-standard-alignment-check.md +79 -0
- package/bundled/core/standard-admission-criteria.md +84 -0
- package/bundled/core/standard-lifecycle-management.md +94 -0
- package/bundled/core/timeout-standards.md +63 -0
- package/bundled/core/token-budget.md +58 -0
- package/bundled/core/translation-lifecycle-standards.md +162 -0
- package/bundled/locales/zh-CN/CHANGELOG.md +51 -3
- package/bundled/locales/zh-CN/README.md +1 -1
- package/bundled/locales/zh-CN/core/anti-hallucination.md +22 -3
- package/bundled/locales/zh-CN/core/anti-sycophancy-prompting.md +192 -0
- package/bundled/locales/zh-CN/core/capability-declaration.md +123 -0
- package/bundled/locales/zh-CN/core/circuit-breaker.md +106 -0
- package/bundled/locales/zh-CN/core/dual-phase-output.md +103 -0
- package/bundled/locales/zh-CN/core/failure-source-taxonomy.md +99 -0
- package/bundled/locales/zh-CN/core/frontend-design-standards.md +289 -0
- package/bundled/locales/zh-CN/core/health-check-standards.md +144 -0
- package/bundled/locales/zh-CN/core/immutability-first.md +96 -0
- package/bundled/locales/zh-CN/core/packaging-standards.md +224 -0
- package/bundled/locales/zh-CN/core/recovery-recipe-registry.md +146 -0
- package/bundled/locales/zh-CN/core/retry-standards.md +131 -0
- package/bundled/locales/zh-CN/core/security-decision.md +104 -0
- package/bundled/locales/zh-CN/core/skill-standard-alignment-check.md +112 -0
- package/bundled/locales/zh-CN/core/standard-admission-criteria.md +104 -0
- package/bundled/locales/zh-CN/core/standard-lifecycle-management.md +116 -0
- package/bundled/locales/zh-CN/core/timeout-standards.md +117 -0
- package/bundled/locales/zh-CN/core/token-budget.md +108 -0
- package/bundled/locales/zh-CN/core/translation-lifecycle-standards.md +159 -0
- package/bundled/locales/zh-TW/CHANGELOG.md +51 -3
- package/bundled/locales/zh-TW/README.md +1 -1
- package/bundled/locales/zh-TW/core/anti-sycophancy-prompting.md +192 -0
- package/bundled/locales/zh-TW/core/capability-declaration.md +111 -0
- package/bundled/locales/zh-TW/core/circuit-breaker.md +111 -0
- package/bundled/locales/zh-TW/core/dual-phase-output.md +132 -0
- package/bundled/locales/zh-TW/core/failure-source-taxonomy.md +146 -0
- package/bundled/locales/zh-TW/core/frontend-design-standards.md +460 -0
- package/bundled/locales/zh-TW/core/health-check-standards.md +144 -0
- package/bundled/locales/zh-TW/core/immutability-first.md +159 -0
- package/bundled/locales/zh-TW/core/packaging-standards.md +224 -0
- package/bundled/locales/zh-TW/core/recovery-recipe-registry.md +146 -0
- package/bundled/locales/zh-TW/core/retry-standards.md +140 -0
- package/bundled/locales/zh-TW/core/security-decision.md +120 -0
- package/bundled/locales/zh-TW/core/skill-standard-alignment-check.md +112 -0
- package/bundled/locales/zh-TW/core/standard-admission-criteria.md +104 -0
- package/bundled/locales/zh-TW/core/standard-lifecycle-management.md +116 -0
- package/bundled/locales/zh-TW/core/timeout-standards.md +117 -0
- package/bundled/locales/zh-TW/core/token-budget.md +143 -0
- package/bundled/locales/zh-TW/core/translation-lifecycle-standards.md +159 -0
- package/bundled/skills/e2e-assistant/SKILL.md +19 -5
- package/bundled/skills/testing-guide/SKILL.md +5 -0
- package/bundled/skills/testing-guide/test-skeleton-templates.md +316 -0
- package/package.json +2 -1
- package/src/commands/check.js +6 -0
- package/src/commands/config.js +9 -0
- package/src/commands/init.js +97 -46
- package/src/commands/mcp.js +26 -0
- package/src/commands/run-intent.js +66 -0
- package/src/commands/update.js +41 -4
- package/src/core/command-router.js +85 -0
- package/src/core/project-config.js +91 -0
- package/src/flows/init-flow.js +6 -1
- package/src/i18n/messages.js +6 -6
- package/src/mcp/__tests__/server.test.js +251 -0
- package/src/mcp/server.js +352 -0
- package/src/prompts/init.js +157 -1
- package/src/reconciler/actual-state-scanner.js +24 -0
- package/src/uninstallers/hook-uninstaller.js +32 -1
- package/src/utils/detect-self-adoption.js +173 -0
- package/src/utils/e2e-analyzer.js +88 -5
- package/src/utils/e2e-detector.js +73 -1
- package/src/utils/integration-generator.js +22 -3
- package/standards-registry.json +203 -4
package/src/prompts/init.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
401
|
+
if (resolvedFramework === 'vitest') {
|
|
319
402
|
output += `\ndescribe('E2E: ${specId}', () => {`;
|
|
320
|
-
} else if (
|
|
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 (
|
|
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 = [
|
|
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
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3128
|
+
`${defaultCmds.install} # Install dependencies (Node.js)`,
|
|
3129
|
+
`${defaultCmds.test} # Run tests`,
|
|
3130
|
+
`${defaultCmds.lint} # Check code style`
|
|
3112
3131
|
];
|
|
3113
3132
|
}
|
|
3114
3133
|
|