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
@@ -36,6 +36,7 @@ import { writeUpdateCache } from '../utils/update-checker.js';
36
36
  import { StandardValidator } from '../utils/standard-validator.js';
37
37
  import { WorkflowGate } from '../utils/workflow-gate.js';
38
38
  import { t, getLanguage, setLanguage, isLanguageExplicitlySet } from '../i18n/messages.js';
39
+ import { guardAgainstSelfAdoption } from '../utils/detect-self-adoption.js';
39
40
 
40
41
  /**
41
42
  * Display the summary of file integrity status
@@ -212,6 +213,11 @@ function displayAdoptionStatus(manifest, msg, common, repoInfo) {
212
213
  export async function checkCommand(options = {}) {
213
214
  const projectPath = process.cwd();
214
215
 
216
+ // Refuse to run inside the UDS source repo itself.
217
+ // See DEC-044 / XSPEC-071 — adoption-drift check makes no sense for the
218
+ // source repo. `--force` bypasses (e.g. private forks of UDS).
219
+ guardAgainstSelfAdoption('check', options, projectPath);
220
+
215
221
  // Handle --standard option (validate specific standard physical spec)
216
222
  if (options.standard) {
217
223
  const validator = new StandardValidator(projectPath);
@@ -562,6 +562,8 @@ export async function runProjectConfiguration(options) {
562
562
  }
563
563
 
564
564
  baseChoices.push(
565
+ new DynSeparator(),
566
+ { name: t('config.projectContractOption', 'Project Command Contract (uds.project.yaml)'), value: 'project_contract' },
565
567
  new DynSeparator(),
566
568
  { name: t('config.vibeMode', 'Vibe Coding Mode'), value: 'vibe_coding' },
567
569
  new DynSeparator(),
@@ -588,6 +590,13 @@ export async function runProjectConfiguration(options) {
588
590
  process.exit(0);
589
591
  }
590
592
 
593
+ // Handle project_contract — guided uds.project.yaml creation (XSPEC-029 Phase 3)
594
+ if (configType === 'project_contract') {
595
+ const { promptProjectCommandContract } = await import('../prompts/init.js');
596
+ await promptProjectCommandContract(projectPath);
597
+ process.exit(0);
598
+ }
599
+
591
600
  // Handle show (flat menu item)
592
601
  if (configType === 'show') {
593
602
  const currentConfig = config.init();
@@ -22,6 +22,7 @@ import {
22
22
  } from '../utils/github.js';
23
23
  import { displayLanguageToLocale } from '../utils/locale.js';
24
24
  import { generateReleaseConfig, RELEASE_MODE_LABELS } from '../utils/release-config.js';
25
+ import { guardAgainstSelfAdoption } from '../utils/detect-self-adoption.js';
25
26
 
26
27
  /**
27
28
  * Init command - initialize standards in current project
@@ -32,6 +33,11 @@ export async function initCommand(options) {
32
33
  let msg = t().commands.init;
33
34
  let common = t().commands.common;
34
35
 
36
+ // Refuse to run inside the UDS source repo itself.
37
+ // See DEC-044 / XSPEC-071 — UDS source repo already ships its standards;
38
+ // running `uds init` here is nonsensical. `--force` bypasses.
39
+ guardAgainstSelfAdoption('init', options, projectPath);
40
+
35
41
  console.log();
36
42
  console.log(chalk.bold(msg.title));
37
43
  console.log(chalk.gray('─'.repeat(50)));
@@ -177,18 +183,22 @@ export async function initCommand(options) {
177
183
  }
178
184
 
179
185
  /**
180
- * Configure Husky pre-commit hook
186
+ * Configure pre-commit hook (language-aware)
187
+ * - Node.js projects: use husky
188
+ * - Non-Node.js projects: write native .git/hooks/pre-commit
181
189
  */
182
190
  async function setupHuskyHook(projectPath) {
183
191
  const hasGit = existsSync(join(projectPath, '.git'));
184
192
  if (!hasGit) return;
185
193
 
186
- console.log(chalk.cyan('Configuring Pre-commit Hook (Husky)...'));
194
+ const isNodeProject = existsSync(join(projectPath, 'package.json'));
195
+
196
+ if (isNodeProject) {
197
+ console.log(chalk.cyan('Configuring Pre-commit Hook (Husky)...'));
187
198
 
188
- // 1. Install husky if needed
189
- try {
190
- const pkgPath = join(projectPath, 'package.json');
191
- if (existsSync(pkgPath)) {
199
+ // 1. Install husky if needed
200
+ try {
201
+ const pkgPath = join(projectPath, 'package.json');
192
202
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
193
203
  const hasHusky = pkg.devDependencies?.husky || pkg.dependencies?.husky;
194
204
 
@@ -196,59 +206,100 @@ async function setupHuskyHook(projectPath) {
196
206
  console.log(chalk.gray(' Installing husky...'));
197
207
  execSync('npm install --save-dev husky', { stdio: 'ignore', cwd: projectPath });
198
208
  }
209
+ } catch (e) {
210
+ console.log(chalk.yellow(` ⚠ Failed to check/install husky: ${e.message}`));
211
+ return;
212
+ }
213
+
214
+ // 2. Initialize husky
215
+ const huskyDir = join(projectPath, '.husky');
216
+ try {
217
+ if (!existsSync(huskyDir)) {
218
+ console.log(chalk.gray(' Initializing husky...'));
219
+ execSync('npx husky init', { stdio: 'ignore', cwd: projectPath });
220
+ }
221
+ } catch (e) {
222
+ // Ignore, might already be init
199
223
  }
200
- } catch (e) {
201
- console.log(chalk.yellow(` ⚠ Failed to check/install husky: ${e.message}`));
202
- return;
203
- }
204
224
 
205
- // 2. Initialize husky
206
- const huskyDir = join(projectPath, '.husky');
207
- try {
225
+ // 3. Ensure .husky directory exists (fallback if husky init failed)
208
226
  if (!existsSync(huskyDir)) {
209
- console.log(chalk.gray(' Initializing husky...'));
210
- execSync('npx husky init', { stdio: 'ignore', cwd: projectPath });
227
+ try {
228
+ mkdirSync(huskyDir, { recursive: true });
229
+ } catch (e) {
230
+ console.log(chalk.red(` ✗ Failed to create .husky directory: ${e.message}`));
231
+ return;
232
+ }
211
233
  }
212
- } catch (e) {
213
- // Ignore, might already be init
214
- }
215
234
 
216
- // 3. Ensure .husky directory exists (fallback if husky init failed)
217
- if (!existsSync(huskyDir)) {
235
+ // 4. Add pre-commit hook
236
+ const preCommitPath = join(huskyDir, 'pre-commit');
237
+ const udsCmd = 'npx uds check';
238
+
218
239
  try {
219
- mkdirSync(huskyDir, { recursive: true });
240
+ let content = '';
241
+ if (existsSync(preCommitPath)) {
242
+ content = readFileSync(preCommitPath, 'utf-8');
243
+ } else {
244
+ // Create if not exists (husky init usually creates it, but just in case)
245
+ content = '#!/usr/bin/env sh\n. "$(dirname -- "$0")/_/husky.sh"\n';
246
+ }
247
+
248
+ if (!content.includes('uds check')) {
249
+ writeFileSync(preCommitPath, content + `\n# UDS Standard Check\n${udsCmd}\n`, 'utf-8');
250
+ try {
251
+ execSync(`chmod +x ${preCommitPath}`);
252
+ } catch (e) {
253
+ // Ignore chmod failures on systems that don't support it
254
+ }
255
+ console.log(chalk.green(' ✓ Adding uds check to pre-commit hook'));
256
+ } else {
257
+ console.log(chalk.gray(' ✓ Pre-commit hook already configured'));
258
+ }
220
259
  } catch (e) {
221
- console.log(chalk.red(` ✗ Failed to create .husky directory: ${e.message}`));
222
- return;
260
+ console.log(chalk.red(` ✗ Failed to configure pre-commit hook: ${e.message}`));
223
261
  }
224
- }
262
+ } else {
263
+ // Non-Node.js: write native .git/hooks/pre-commit
264
+ console.log(chalk.cyan('Configuring Pre-commit Hook (native git hook)...'));
225
265
 
226
- // 4. Add pre-commit hook
227
- const preCommitPath = join(huskyDir, 'pre-commit');
228
- const udsCmd = 'npx uds check';
266
+ const hookDir = join(projectPath, '.git', 'hooks');
267
+ const hookPath = join(hookDir, 'pre-commit');
229
268
 
230
- try {
231
- let content = '';
232
- if (existsSync(preCommitPath)) {
233
- content = readFileSync(preCommitPath, 'utf-8');
234
- } else {
235
- // Create if not exists (husky init usually creates it, but just in case)
236
- content = '#!/usr/bin/env sh\n. "$(dirname -- "$0")/_/husky.sh"\n';
237
- }
269
+ try {
270
+ if (!existsSync(hookDir)) {
271
+ mkdirSync(hookDir, { recursive: true });
272
+ }
238
273
 
239
- if (!content.includes('uds check')) {
240
- writeFileSync(preCommitPath, content + `\n# UDS Standard Check\n${udsCmd}\n`, 'utf-8');
241
- try {
242
- execSync(`chmod +x ${preCommitPath}`);
243
- } catch (e) {
244
- // Ignore chmod failures on systems that don't support it
274
+ if (existsSync(hookPath) && readFileSync(hookPath, 'utf-8').includes('uds check')) {
275
+ console.log(chalk.gray(' ✓ Pre-commit hook already configured'));
276
+ } else {
277
+ const hookContent = `#!/bin/sh
278
+ # UDS pre-commit hook
279
+ # Auto-generated by uds init
280
+
281
+ echo "Running UDS pre-commit checks..."
282
+
283
+ # Run lint if available
284
+ if [ -f pyproject.toml ] || [ -f requirements.txt ]; then
285
+ python -m ruff check . 2>/dev/null || true
286
+ elif [ -f go.mod ]; then
287
+ go vet ./... 2>/dev/null || true
288
+ elif [ -f Cargo.toml ]; then
289
+ cargo clippy 2>/dev/null || true
290
+ fi
291
+
292
+ # UDS Standard Check
293
+ uds check 2>/dev/null || true
294
+
295
+ echo "Pre-commit checks passed"
296
+ `;
297
+ writeFileSync(hookPath, hookContent, { mode: 0o755 });
298
+ console.log(chalk.green(' ✓ Installed .git/hooks/pre-commit (native git hook)'));
245
299
  }
246
- console.log(chalk.green(' ✓ Adding uds check to pre-commit hook'));
247
- } else {
248
- console.log(chalk.gray(' ✓ Pre-commit hook already configured'));
300
+ } catch (e) {
301
+ console.log(chalk.red(` ✗ Failed to configure pre-commit hook: ${e.message}`));
249
302
  }
250
- } catch (e) {
251
- console.log(chalk.red(` ✗ Failed to configure pre-commit hook: ${e.message}`));
252
303
  }
253
304
  console.log();
254
305
  }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * uds mcp - MCP server subcommands for AI tool integration
3
+ */
4
+
5
+ import { McpServer } from '../mcp/server.js';
6
+
7
+ /**
8
+ * Register the `mcp` command group on the given Commander program
9
+ * @param {import('commander').Command} program
10
+ */
11
+ export function mcpCommand(program) {
12
+ const mcp = program
13
+ .command('mcp')
14
+ .description('MCP server commands for AI tool integration');
15
+
16
+ mcp
17
+ .command('serve')
18
+ .description('Start MCP Design Standards Server (stdio transport)')
19
+ .option('--root <path>', 'UDS standards root path', process.cwd())
20
+ .action((options) => {
21
+ const server = new McpServer({ udsRoot: options.root });
22
+ server.start();
23
+ // Log to stderr only — stdout is reserved for MCP JSON-RPC messages
24
+ process.stderr.write('UDS MCP Design Standards Server started (stdio)\n');
25
+ });
26
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * run-intent command — uds run <intent>
3
+ * Unified proxy: resolves intent to actual command via command-router
4
+ * XSPEC-029
5
+ */
6
+ import chalk from 'chalk';
7
+ import { execSync } from 'child_process';
8
+ import { resolveCommand } from '../core/command-router.js';
9
+
10
+ const KNOWN_INTENTS = ['test', 'lint', 'build', 'security'];
11
+
12
+ export async function runIntentCommand(intent, options = {}) {
13
+ const projectPath = process.cwd();
14
+
15
+ if (!intent) {
16
+ console.log(chalk.yellow('使用方式: uds run <intent>'));
17
+ console.log(chalk.gray(`可用 intent: ${KNOWN_INTENTS.join(', ')}`));
18
+ console.log(chalk.gray('自訂 intent 可在 uds.project.yaml 的 custom: 區塊定義'));
19
+ return;
20
+ }
21
+
22
+ let resolved;
23
+ try {
24
+ resolved = resolveCommand(intent, projectPath);
25
+ } catch (err) {
26
+ console.error(chalk.red(`✗ 讀取 uds.project.yaml 失敗:${err.message}`));
27
+ process.exit(1);
28
+ }
29
+
30
+ if (!resolved) {
31
+ console.log();
32
+ console.log(chalk.yellow(`⚠️ 無法解析 intent "${intent}" 的執行命令`));
33
+ console.log();
34
+ console.log(chalk.gray('解決方式:'));
35
+ console.log(chalk.gray(' 1. 執行 uds configure 建立 uds.project.yaml'));
36
+ console.log(chalk.gray(' 2. 或在專案根目錄建立含 test:/lint: target 的 Makefile'));
37
+ console.log(chalk.gray(' 3. 或手動建立 uds.project.yaml:'));
38
+ console.log(chalk.gray(''));
39
+ console.log(chalk.gray(' version: "1"'));
40
+ console.log(chalk.gray(' commands:'));
41
+ console.log(chalk.gray(` ${intent}: <your command here>`));
42
+ console.log();
43
+ process.exit(1);
44
+ }
45
+
46
+ const sourceLabel = {
47
+ 'uds.project.yaml': chalk.green('uds.project.yaml'),
48
+ 'convention-runner': chalk.cyan('Makefile/justfile'),
49
+ }[resolved.source] ?? chalk.gray(`fallback:${resolved.source.split(':')[1] ?? resolved.source}`);
50
+
51
+ console.log();
52
+ console.log(`${chalk.bold('uds run')} ${chalk.cyan(intent)} ${chalk.gray('←')} ${sourceLabel}`);
53
+ console.log(chalk.gray(`$ ${resolved.command}`));
54
+ console.log();
55
+
56
+ if (options.dryRun) {
57
+ console.log(chalk.yellow('(dry-run mode — 未實際執行)'));
58
+ return;
59
+ }
60
+
61
+ try {
62
+ execSync(resolved.command, { stdio: 'inherit', cwd: projectPath });
63
+ } catch (err) {
64
+ process.exit(err.status ?? 1);
65
+ }
66
+ }
@@ -49,6 +49,7 @@ import {
49
49
  listBackups
50
50
  } from '../reconciler/index.js';
51
51
  import { restoreSingleFile } from './check.js';
52
+ import { guardAgainstSelfAdoption } from '../utils/detect-self-adoption.js';
52
53
 
53
54
  /**
54
55
  * Determine the correct target directory for a standard file.
@@ -128,6 +129,34 @@ function compareVersions(v1, v2) {
128
129
  return 0;
129
130
  }
130
131
 
132
+ /**
133
+ * Detect the global package manager used to install UDS.
134
+ * Checks npm_execpath env var first, then falls back to lock-file detection.
135
+ */
136
+ function detectGlobalPackageManager() {
137
+ // npm_execpath is set by npm/yarn/pnpm when running scripts
138
+ const execPath = process.env.npm_execpath || '';
139
+ if (execPath.includes('yarn')) return 'yarn';
140
+ if (execPath.includes('pnpm')) return 'pnpm';
141
+
142
+ // Check if running under bun
143
+ if (process.versions?.bun) return 'bun';
144
+
145
+ return 'npm'; // default
146
+ }
147
+
148
+ /**
149
+ * Build the global install command for universal-dev-standards.
150
+ */
151
+ function buildInstallCommand(pm, tag) {
152
+ switch (pm) {
153
+ case 'yarn': return `yarn global add universal-dev-standards${tag}`;
154
+ case 'pnpm': return `pnpm add -g universal-dev-standards${tag}`;
155
+ case 'bun': return `bun install -g universal-dev-standards${tag}`;
156
+ default: return `npm install -g universal-dev-standards${tag}`;
157
+ }
158
+ }
159
+
131
160
  /**
132
161
  * Update CLI to latest version and prompt user to re-run
133
162
  * @param {boolean} useBeta - Whether to install beta version
@@ -139,9 +168,9 @@ async function updateCliAndExit(useBeta = false) {
139
168
  try {
140
169
  // Command is hardcoded - no user input, safe from injection
141
170
  const tag = useBeta ? '@beta' : '@latest';
142
- execSync(`npm install -g universal-dev-standards${tag}`, {
143
- stdio: 'pipe'
144
- });
171
+ const pm = detectGlobalPackageManager();
172
+ const installCmd = buildInstallCommand(pm, tag);
173
+ execSync(installCmd, { stdio: 'pipe' });
145
174
 
146
175
  spinner.succeed(msg.cliUpdated);
147
176
  console.log();
@@ -152,7 +181,10 @@ async function updateCliAndExit(useBeta = false) {
152
181
  spinner.fail(msg.cliUpdateFailed);
153
182
  console.log(chalk.yellow(` ${msg.permissionIssue}`));
154
183
  console.log(chalk.gray(` ${msg.tryManually}`));
155
- console.log(chalk.white(` sudo npm install -g universal-dev-standards${useBeta ? '@beta' : ''}`));
184
+ const pm = detectGlobalPackageManager();
185
+ const manualTag = useBeta ? '@beta' : '';
186
+ console.log(chalk.white(` ${buildInstallCommand(pm, manualTag)}`));
187
+ console.log(chalk.gray(` (or: npm install -g universal-dev-standards${manualTag})`));
156
188
  console.log();
157
189
  process.exit(1);
158
190
  }
@@ -213,6 +245,11 @@ function resolveLocale(manifest, projectPath) {
213
245
  export async function updateCommand(options) {
214
246
  const projectPath = process.cwd();
215
247
 
248
+ // Refuse to run inside the UDS source repo itself.
249
+ // See DEC-044 / XSPEC-071 — without this guard `uds update` would overwrite
250
+ // source-of-truth `.standards/` with bundled copies. `--force` bypasses.
251
+ guardAgainstSelfAdoption('update', options, projectPath);
252
+
216
253
  // Check if initialized first (use default language)
217
254
  if (!isInitialized(projectPath)) {
218
255
  const common = t().commands.common;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Command Router — resolves an intent to an executable command
3
+ * Priority: uds.project.yaml → Makefile/justfile/taskfile → ecosystem detection → guide
4
+ * XSPEC-029
5
+ */
6
+ import { existsSync, readFileSync } from 'fs';
7
+ import { join } from 'path';
8
+ import { loadProjectConfig, getCommand } from './project-config.js';
9
+
10
+ // Ecosystem defaults (from XSPEC-028)
11
+ const ECOSYSTEM_DEFAULTS = {
12
+ node: { test: 'npm test', lint: 'npm run lint', build: 'npm run build', security: 'npm audit' },
13
+ python: { test: 'python -m pytest', lint: 'python -m ruff check .', build: 'python -m build', security: 'pip-audit' },
14
+ go: { test: 'go test ./...', lint: 'go vet ./...', build: 'go build ./...', security: 'govulncheck ./...' },
15
+ java_maven: { test: 'mvn test', lint: 'mvn checkstyle:check', build: 'mvn package', security: 'mvn dependency-check:check' },
16
+ java_gradle: { test: './gradlew test', lint: './gradlew checkstyleMain', build: './gradlew build', security: './gradlew dependencyCheckAnalyze' },
17
+ rust: { test: 'cargo test', lint: 'cargo clippy', build: 'cargo build', security: 'cargo audit' },
18
+ ruby: { test: 'bundle exec rspec', lint: 'bundle exec rubocop', build: 'gem build *.gemspec', security: 'bundle audit' },
19
+ };
20
+
21
+ /**
22
+ * Detect the project ecosystem from manifest files.
23
+ * Returns an ecosystem key or 'unknown'.
24
+ */
25
+ function detectEcosystem(projectPath) {
26
+ const has = (f) => existsSync(join(projectPath, f));
27
+ if (has('package.json')) return 'node';
28
+ if (has('requirements.txt') || has('pyproject.toml') || has('setup.py')) return 'python';
29
+ if (has('go.mod')) return 'go';
30
+ if (has('pom.xml')) return 'java_maven';
31
+ if (has('build.gradle') || has('build.gradle.kts')) return 'java_gradle';
32
+ if (has('Cargo.toml')) return 'rust';
33
+ if (has('Gemfile')) return 'ruby';
34
+ return 'unknown';
35
+ }
36
+
37
+ /**
38
+ * Check if a Makefile (or justfile/taskfile) has a target for the given intent.
39
+ */
40
+ function findConventionRunner(projectPath, intent) {
41
+ const has = (f) => existsSync(join(projectPath, f));
42
+
43
+ if (has('Makefile')) {
44
+ const content = readFileSync(join(projectPath, 'Makefile'), 'utf8');
45
+ const pattern = new RegExp(`^${intent}\\s*:`, 'm');
46
+ if (pattern.test(content)) return `make ${intent}`;
47
+ }
48
+ if (has('justfile') || has('.justfile')) {
49
+ const file = has('justfile') ? 'justfile' : '.justfile';
50
+ const content = readFileSync(join(projectPath, file), 'utf8');
51
+ const pattern = new RegExp(`^${intent}\\s*:`, 'm');
52
+ if (pattern.test(content)) return `just ${intent}`;
53
+ }
54
+ if (has('Taskfile.yml') || has('taskfile.yml')) {
55
+ return `task ${intent}`;
56
+ }
57
+ return null;
58
+ }
59
+
60
+ /**
61
+ * Resolve a command intent to an executable command string.
62
+ * Returns { command, source } where source describes which layer resolved it.
63
+ * Returns null if unresolvable (caller should trigger configure guidance).
64
+ */
65
+ export function resolveCommand(intent, projectPath = '.') {
66
+ // Layer 1: uds.project.yaml
67
+ // loadProjectConfig throws on invalid config — let it propagate to caller
68
+ const config = loadProjectConfig(projectPath);
69
+ const cmd = getCommand(config, intent);
70
+ if (cmd) return { command: cmd, source: 'uds.project.yaml' };
71
+
72
+ // Layer 2: Convention task runners (Makefile / justfile / taskfile)
73
+ const conventionCmd = findConventionRunner(projectPath, intent);
74
+ if (conventionCmd) return { command: conventionCmd, source: 'convention-runner' };
75
+
76
+ // Layer 3: Ecosystem detection fallback
77
+ const ecosystem = detectEcosystem(projectPath);
78
+ if (ecosystem !== 'unknown') {
79
+ const cmd = ECOSYSTEM_DEFAULTS[ecosystem]?.[intent];
80
+ if (cmd) return { command: cmd, source: `ecosystem-fallback:${ecosystem}` };
81
+ }
82
+
83
+ // Layer 4: Unresolvable
84
+ return null;
85
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Project Command Contract — reads and validates uds.project.yaml
3
+ * XSPEC-029
4
+ */
5
+ import { existsSync, readFileSync } from 'fs';
6
+ import { join } from 'path';
7
+
8
+ const CONFIG_FILENAME = 'uds.project.yaml';
9
+
10
+ /**
11
+ * Parse uds.project.yaml from the given project path.
12
+ * Returns null if the file does not exist.
13
+ * Throws if the file exists but is invalid.
14
+ */
15
+ export function loadProjectConfig(projectPath = '.') {
16
+ const configPath = join(projectPath, CONFIG_FILENAME);
17
+ if (!existsSync(configPath)) return null;
18
+
19
+ const raw = readFileSync(configPath, 'utf8');
20
+
21
+ // Minimal YAML parser for the simple key: value structure we need.
22
+ // We deliberately avoid a heavy dependency — uds.project.yaml is intentionally simple.
23
+ const config = parseMinimalYaml(raw, configPath);
24
+ validateConfig(config, configPath);
25
+ return config;
26
+ }
27
+
28
+ /**
29
+ * Parse a minimal YAML subset sufficient for uds.project.yaml.
30
+ * Supports: top-level scalars, one-level nested mappings (commands:, custom:).
31
+ */
32
+ function parseMinimalYaml(raw, _filePath) {
33
+ const lines = raw.split('\n');
34
+ const result = {};
35
+ let currentSection = null;
36
+
37
+ for (let i = 0; i < lines.length; i++) {
38
+ const line = lines[i];
39
+ const trimmed = line.trimEnd();
40
+
41
+ // Skip blank lines and comments
42
+ if (!trimmed || trimmed.trimStart().startsWith('#')) continue;
43
+
44
+ const indent = line.length - line.trimStart().length;
45
+
46
+ if (indent === 0) {
47
+ // Top-level key
48
+ const match = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(.*)$/);
49
+ if (!match) continue;
50
+ const [, key, value] = match;
51
+ if (value.trim()) {
52
+ result[key] = value.trim().replace(/^["']|["']$/g, '');
53
+ currentSection = null;
54
+ } else {
55
+ result[key] = {};
56
+ currentSection = key;
57
+ }
58
+ } else if (currentSection && indent > 0) {
59
+ // Nested key under current section
60
+ const match = trimmed.trim().match(/^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(.*)$/);
61
+ if (!match) continue;
62
+ const [, key, value] = match;
63
+ result[currentSection][key] = value.trim().replace(/^["']|["']$/g, '');
64
+ }
65
+ }
66
+
67
+ return result;
68
+ }
69
+
70
+ function validateConfig(config, filePath) {
71
+ if (!config.version) {
72
+ throw new Error(
73
+ `${filePath}: missing required field "version". ` +
74
+ 'Add "version: \\"1\\"" at the top of the file.'
75
+ );
76
+ }
77
+ if (config.commands && typeof config.commands !== 'object') {
78
+ throw new Error(`${filePath}: "commands" must be a mapping of intent: command pairs.`);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Get a specific command intent from config.
84
+ * Returns undefined if not configured.
85
+ */
86
+ export function getCommand(config, intent) {
87
+ if (!config) return undefined;
88
+ return config.commands?.[intent] ?? config.custom?.[intent];
89
+ }
90
+
91
+ export { CONFIG_FILENAME };
@@ -13,7 +13,8 @@ import {
13
13
  promptCommandsInstallation,
14
14
  handleAgentsMdSharing,
15
15
  promptAgentsMd,
16
- promptReleaseMode
16
+ promptReleaseMode,
17
+ promptProjectContractStep
17
18
  } from '../prompts/init.js';
18
19
  import {
19
20
  promptIntegrationConfig
@@ -237,6 +238,10 @@ export async function runInitFlow(options, detected, projectPath) {
237
238
  skillsConfig.methodology = methodology;
238
239
  skillsConfig.locale = displayLanguageToLocale(displayLanguage);
239
240
 
241
+ // === STEP 13: Project Command Contract (uds.project.yaml) — XSPEC-029 Phase 3 ===
242
+ // Optional step — skippable by answering "n" or pressing Ctrl+C.
243
+ await promptProjectContractStep(projectPath);
244
+
240
245
  return {
241
246
  languages,
242
247
  frameworks,
@@ -18,7 +18,7 @@ export const messages = {
18
18
  // Update Notice
19
19
  updateNotice: {
20
20
  header: 'Update available',
21
- command: 'npm update -g universal-dev-standards',
21
+ command: 'npm update -g universal-dev-standards (or: yarn global upgrade / pnpm update -g)',
22
22
  disableHint: 'Set UDS_NO_UPDATE_CHECK=1 to disable'
23
23
  },
24
24
 
@@ -887,7 +887,7 @@ export const messages = {
887
887
  currentCli: 'Current CLI',
888
888
  latestOnNpm: 'Latest on npm',
889
889
  latestStable: 'Latest stable',
890
- runNpmUpdate: 'Run: npm update -g universal-dev-standards',
890
+ runNpmUpdate: 'Run: npm update -g universal-dev-standards (or: yarn global upgrade / pnpm update -g)',
891
891
  // Final status
892
892
  projectCompliant: '✓ Project is compliant with standards',
893
893
  issuesDetected: '⚠ Some issues detected. Review above for details.',
@@ -1278,7 +1278,7 @@ export const messages = {
1278
1278
  // 更新通知
1279
1279
  updateNotice: {
1280
1280
  header: '有新版本可用',
1281
- command: 'npm update -g universal-dev-standards',
1281
+ command: 'npm update -g universal-dev-standards(或 yarn global upgrade / pnpm update -g)',
1282
1282
  disableHint: '設定 UDS_NO_UPDATE_CHECK=1 可關閉'
1283
1283
  },
1284
1284
 
@@ -2147,7 +2147,7 @@ export const messages = {
2147
2147
  currentCli: '目前 CLI',
2148
2148
  latestOnNpm: 'npm 最新版本',
2149
2149
  latestStable: '最新穩定版',
2150
- runNpmUpdate: '執行:npm update -g universal-dev-standards',
2150
+ runNpmUpdate: '執行:npm update -g universal-dev-standards(或 yarn global upgrade / pnpm update -g)',
2151
2151
  // Final status
2152
2152
  projectCompliant: '✓ 專案符合標準',
2153
2153
  issuesDetected: '⚠ 偵測到一些問題。請檢視上方詳情。',
@@ -2538,7 +2538,7 @@ export const messages = {
2538
2538
  // 更新通知
2539
2539
  updateNotice: {
2540
2540
  header: '有新版本可用',
2541
- command: 'npm update -g universal-dev-standards',
2541
+ command: 'npm update -g universal-dev-standards(或 yarn global upgrade / pnpm update -g)',
2542
2542
  disableHint: '设置 UDS_NO_UPDATE_CHECK=1 可关闭'
2543
2543
  },
2544
2544
 
@@ -3420,7 +3420,7 @@ export const messages = {
3420
3420
  currentCli: '当前 CLI',
3421
3421
  latestOnNpm: 'npm 最新版本',
3422
3422
  latestStable: '最新稳定版',
3423
- runNpmUpdate: '执行:npm update -g universal-dev-standards',
3423
+ runNpmUpdate: '执行:npm update -g universal-dev-standards(或 yarn global upgrade / pnpm update -g)',
3424
3424
  // Final status
3425
3425
  projectCompliant: '✓ 项目符合标准',
3426
3426
  issuesDetected: '⚠ 检测到一些问题。详情请查看上文。',