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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/bin/uds.js +12 -0
  2. package/bundled/ai/standards/agent-communication-protocol.ai.yaml +34 -0
  3. package/bundled/ai/standards/anti-sycophancy-prompting.ai.yaml +111 -0
  4. package/bundled/ai/standards/capability-declaration.ai.yaml +113 -0
  5. package/bundled/ai/standards/circuit-breaker.ai.yaml +93 -0
  6. package/bundled/ai/standards/developer-memory.ai.yaml +13 -0
  7. package/bundled/ai/standards/dual-phase-output.ai.yaml +108 -0
  8. package/bundled/ai/standards/failure-source-taxonomy.ai.yaml +115 -0
  9. package/bundled/ai/standards/frontend-design-standards.ai.yaml +305 -0
  10. package/bundled/ai/standards/health-check-standards.ai.yaml +140 -0
  11. package/bundled/ai/standards/immutability-first.ai.yaml +112 -0
  12. package/bundled/ai/standards/model-selection.ai.yaml +111 -3
  13. package/bundled/ai/standards/packaging-standards.ai.yaml +142 -0
  14. package/bundled/ai/standards/recovery-recipe-registry.ai.yaml +200 -0
  15. package/bundled/ai/standards/retry-standards.ai.yaml +134 -0
  16. package/bundled/ai/standards/security-decision.ai.yaml +87 -0
  17. package/bundled/ai/standards/skill-standard-alignment-check.ai.yaml +119 -0
  18. package/bundled/ai/standards/standard-admission-criteria.ai.yaml +107 -0
  19. package/bundled/ai/standards/standard-lifecycle-management.ai.yaml +144 -0
  20. package/bundled/ai/standards/timeout-standards.ai.yaml +104 -0
  21. package/bundled/ai/standards/token-budget.ai.yaml +108 -0
  22. package/bundled/core/anti-sycophancy-prompting.md +184 -0
  23. package/bundled/core/capability-declaration.md +59 -0
  24. package/bundled/core/circuit-breaker.md +58 -0
  25. package/bundled/core/developer-memory.md +29 -1
  26. package/bundled/core/dual-phase-output.md +56 -0
  27. package/bundled/core/failure-source-taxonomy.md +72 -0
  28. package/bundled/core/frontend-design-standards.md +474 -0
  29. package/bundled/core/health-check-standards.md +72 -0
  30. package/bundled/core/immutability-first.md +105 -0
  31. package/bundled/core/model-selection.md +80 -0
  32. package/bundled/core/packaging-standards.md +216 -0
  33. package/bundled/core/recovery-recipe-registry.md +69 -0
  34. package/bundled/core/retry-standards.md +62 -0
  35. package/bundled/core/security-decision.md +65 -0
  36. package/bundled/core/skill-standard-alignment-check.md +79 -0
  37. package/bundled/core/standard-admission-criteria.md +84 -0
  38. package/bundled/core/standard-lifecycle-management.md +94 -0
  39. package/bundled/core/timeout-standards.md +63 -0
  40. package/bundled/core/token-budget.md +58 -0
  41. package/bundled/locales/zh-CN/CHANGELOG.md +22 -3
  42. package/bundled/locales/zh-CN/README.md +1 -1
  43. package/bundled/locales/zh-TW/CHANGELOG.md +22 -3
  44. package/bundled/locales/zh-TW/README.md +1 -1
  45. package/bundled/locales/zh-TW/core/anti-sycophancy-prompting.md +184 -0
  46. package/bundled/locales/zh-TW/core/packaging-standards.md +224 -0
  47. package/bundled/skills/e2e-assistant/SKILL.md +19 -5
  48. package/bundled/skills/testing-guide/SKILL.md +5 -0
  49. package/bundled/skills/testing-guide/test-skeleton-templates.md +316 -0
  50. package/package.json +1 -1
  51. package/src/commands/config.js +9 -0
  52. package/src/commands/init.js +91 -46
  53. package/src/commands/mcp.js +26 -0
  54. package/src/commands/run-intent.js +66 -0
  55. package/src/commands/update.js +35 -4
  56. package/src/core/command-router.js +85 -0
  57. package/src/core/project-config.js +91 -0
  58. package/src/flows/init-flow.js +6 -1
  59. package/src/i18n/messages.js +6 -6
  60. package/src/mcp/__tests__/server.test.js +251 -0
  61. package/src/mcp/server.js +352 -0
  62. package/src/prompts/init.js +157 -1
  63. package/src/reconciler/actual-state-scanner.js +24 -0
  64. package/src/uninstallers/hook-uninstaller.js +32 -1
  65. package/src/utils/e2e-analyzer.js +88 -5
  66. package/src/utils/e2e-detector.js +73 -1
  67. package/src/utils/integration-generator.js +22 -3
  68. package/standards-registry.json +193 -5
@@ -177,18 +177,22 @@ export async function initCommand(options) {
177
177
  }
178
178
 
179
179
  /**
180
- * Configure Husky pre-commit hook
180
+ * Configure pre-commit hook (language-aware)
181
+ * - Node.js projects: use husky
182
+ * - Non-Node.js projects: write native .git/hooks/pre-commit
181
183
  */
182
184
  async function setupHuskyHook(projectPath) {
183
185
  const hasGit = existsSync(join(projectPath, '.git'));
184
186
  if (!hasGit) return;
185
187
 
186
- console.log(chalk.cyan('Configuring Pre-commit Hook (Husky)...'));
188
+ const isNodeProject = existsSync(join(projectPath, 'package.json'));
187
189
 
188
- // 1. Install husky if needed
189
- try {
190
- const pkgPath = join(projectPath, 'package.json');
191
- if (existsSync(pkgPath)) {
190
+ if (isNodeProject) {
191
+ console.log(chalk.cyan('Configuring Pre-commit Hook (Husky)...'));
192
+
193
+ // 1. Install husky if needed
194
+ try {
195
+ const pkgPath = join(projectPath, 'package.json');
192
196
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
193
197
  const hasHusky = pkg.devDependencies?.husky || pkg.dependencies?.husky;
194
198
 
@@ -196,59 +200,100 @@ async function setupHuskyHook(projectPath) {
196
200
  console.log(chalk.gray(' Installing husky...'));
197
201
  execSync('npm install --save-dev husky', { stdio: 'ignore', cwd: projectPath });
198
202
  }
203
+ } catch (e) {
204
+ console.log(chalk.yellow(` ⚠ Failed to check/install husky: ${e.message}`));
205
+ return;
199
206
  }
200
- } catch (e) {
201
- console.log(chalk.yellow(` ⚠ Failed to check/install husky: ${e.message}`));
202
- return;
203
- }
204
207
 
205
- // 2. Initialize husky
206
- const huskyDir = join(projectPath, '.husky');
207
- try {
208
+ // 2. Initialize husky
209
+ const huskyDir = join(projectPath, '.husky');
210
+ try {
211
+ if (!existsSync(huskyDir)) {
212
+ console.log(chalk.gray(' Initializing husky...'));
213
+ execSync('npx husky init', { stdio: 'ignore', cwd: projectPath });
214
+ }
215
+ } catch (e) {
216
+ // Ignore, might already be init
217
+ }
218
+
219
+ // 3. Ensure .husky directory exists (fallback if husky init failed)
208
220
  if (!existsSync(huskyDir)) {
209
- console.log(chalk.gray(' Initializing husky...'));
210
- execSync('npx husky init', { stdio: 'ignore', cwd: projectPath });
221
+ try {
222
+ mkdirSync(huskyDir, { recursive: true });
223
+ } catch (e) {
224
+ console.log(chalk.red(` ✗ Failed to create .husky directory: ${e.message}`));
225
+ return;
226
+ }
211
227
  }
212
- } catch (e) {
213
- // Ignore, might already be init
214
- }
215
228
 
216
- // 3. Ensure .husky directory exists (fallback if husky init failed)
217
- if (!existsSync(huskyDir)) {
229
+ // 4. Add pre-commit hook
230
+ const preCommitPath = join(huskyDir, 'pre-commit');
231
+ const udsCmd = 'npx uds check';
232
+
218
233
  try {
219
- mkdirSync(huskyDir, { recursive: true });
234
+ let content = '';
235
+ if (existsSync(preCommitPath)) {
236
+ content = readFileSync(preCommitPath, 'utf-8');
237
+ } else {
238
+ // Create if not exists (husky init usually creates it, but just in case)
239
+ content = '#!/usr/bin/env sh\n. "$(dirname -- "$0")/_/husky.sh"\n';
240
+ }
241
+
242
+ if (!content.includes('uds check')) {
243
+ writeFileSync(preCommitPath, content + `\n# UDS Standard Check\n${udsCmd}\n`, 'utf-8');
244
+ try {
245
+ execSync(`chmod +x ${preCommitPath}`);
246
+ } catch (e) {
247
+ // Ignore chmod failures on systems that don't support it
248
+ }
249
+ console.log(chalk.green(' ✓ Adding uds check to pre-commit hook'));
250
+ } else {
251
+ console.log(chalk.gray(' ✓ Pre-commit hook already configured'));
252
+ }
220
253
  } catch (e) {
221
- console.log(chalk.red(` ✗ Failed to create .husky directory: ${e.message}`));
222
- return;
254
+ console.log(chalk.red(` ✗ Failed to configure pre-commit hook: ${e.message}`));
223
255
  }
224
- }
256
+ } else {
257
+ // Non-Node.js: write native .git/hooks/pre-commit
258
+ console.log(chalk.cyan('Configuring Pre-commit Hook (native git hook)...'));
225
259
 
226
- // 4. Add pre-commit hook
227
- const preCommitPath = join(huskyDir, 'pre-commit');
228
- const udsCmd = 'npx uds check';
260
+ const hookDir = join(projectPath, '.git', 'hooks');
261
+ const hookPath = join(hookDir, 'pre-commit');
229
262
 
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
- }
263
+ try {
264
+ if (!existsSync(hookDir)) {
265
+ mkdirSync(hookDir, { recursive: true });
266
+ }
238
267
 
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
268
+ if (existsSync(hookPath) && readFileSync(hookPath, 'utf-8').includes('uds check')) {
269
+ console.log(chalk.gray(' ✓ Pre-commit hook already configured'));
270
+ } else {
271
+ const hookContent = `#!/bin/sh
272
+ # UDS pre-commit hook
273
+ # Auto-generated by uds init
274
+
275
+ echo "Running UDS pre-commit checks..."
276
+
277
+ # Run lint if available
278
+ if [ -f pyproject.toml ] || [ -f requirements.txt ]; then
279
+ python -m ruff check . 2>/dev/null || true
280
+ elif [ -f go.mod ]; then
281
+ go vet ./... 2>/dev/null || true
282
+ elif [ -f Cargo.toml ]; then
283
+ cargo clippy 2>/dev/null || true
284
+ fi
285
+
286
+ # UDS Standard Check
287
+ uds check 2>/dev/null || true
288
+
289
+ echo "Pre-commit checks passed"
290
+ `;
291
+ writeFileSync(hookPath, hookContent, { mode: 0o755 });
292
+ console.log(chalk.green(' ✓ Installed .git/hooks/pre-commit (native git hook)'));
245
293
  }
246
- console.log(chalk.green(' ✓ Adding uds check to pre-commit hook'));
247
- } else {
248
- console.log(chalk.gray(' ✓ Pre-commit hook already configured'));
294
+ } catch (e) {
295
+ console.log(chalk.red(` ✗ Failed to configure pre-commit hook: ${e.message}`));
249
296
  }
250
- } catch (e) {
251
- console.log(chalk.red(` ✗ Failed to configure pre-commit hook: ${e.message}`));
252
297
  }
253
298
  console.log();
254
299
  }
@@ -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
+ }
@@ -128,6 +128,34 @@ function compareVersions(v1, v2) {
128
128
  return 0;
129
129
  }
130
130
 
131
+ /**
132
+ * Detect the global package manager used to install UDS.
133
+ * Checks npm_execpath env var first, then falls back to lock-file detection.
134
+ */
135
+ function detectGlobalPackageManager() {
136
+ // npm_execpath is set by npm/yarn/pnpm when running scripts
137
+ const execPath = process.env.npm_execpath || '';
138
+ if (execPath.includes('yarn')) return 'yarn';
139
+ if (execPath.includes('pnpm')) return 'pnpm';
140
+
141
+ // Check if running under bun
142
+ if (process.versions?.bun) return 'bun';
143
+
144
+ return 'npm'; // default
145
+ }
146
+
147
+ /**
148
+ * Build the global install command for universal-dev-standards.
149
+ */
150
+ function buildInstallCommand(pm, tag) {
151
+ switch (pm) {
152
+ case 'yarn': return `yarn global add universal-dev-standards${tag}`;
153
+ case 'pnpm': return `pnpm add -g universal-dev-standards${tag}`;
154
+ case 'bun': return `bun install -g universal-dev-standards${tag}`;
155
+ default: return `npm install -g universal-dev-standards${tag}`;
156
+ }
157
+ }
158
+
131
159
  /**
132
160
  * Update CLI to latest version and prompt user to re-run
133
161
  * @param {boolean} useBeta - Whether to install beta version
@@ -139,9 +167,9 @@ async function updateCliAndExit(useBeta = false) {
139
167
  try {
140
168
  // Command is hardcoded - no user input, safe from injection
141
169
  const tag = useBeta ? '@beta' : '@latest';
142
- execSync(`npm install -g universal-dev-standards${tag}`, {
143
- stdio: 'pipe'
144
- });
170
+ const pm = detectGlobalPackageManager();
171
+ const installCmd = buildInstallCommand(pm, tag);
172
+ execSync(installCmd, { stdio: 'pipe' });
145
173
 
146
174
  spinner.succeed(msg.cliUpdated);
147
175
  console.log();
@@ -152,7 +180,10 @@ async function updateCliAndExit(useBeta = false) {
152
180
  spinner.fail(msg.cliUpdateFailed);
153
181
  console.log(chalk.yellow(` ${msg.permissionIssue}`));
154
182
  console.log(chalk.gray(` ${msg.tryManually}`));
155
- console.log(chalk.white(` sudo npm install -g universal-dev-standards${useBeta ? '@beta' : ''}`));
183
+ const pm = detectGlobalPackageManager();
184
+ const manualTag = useBeta ? '@beta' : '';
185
+ console.log(chalk.white(` ${buildInstallCommand(pm, manualTag)}`));
186
+ console.log(chalk.gray(` (or: npm install -g universal-dev-standards${manualTag})`));
156
187
  console.log();
157
188
  process.exit(1);
158
189
  }
@@ -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: '⚠ 检测到一些问题。详情请查看上文。',