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/commands/check.js
CHANGED
|
@@ -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);
|
package/src/commands/config.js
CHANGED
|
@@ -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();
|
package/src/commands/init.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
217
|
-
|
|
235
|
+
// 4. Add pre-commit hook
|
|
236
|
+
const preCommitPath = join(huskyDir, 'pre-commit');
|
|
237
|
+
const udsCmd = 'npx uds check';
|
|
238
|
+
|
|
218
239
|
try {
|
|
219
|
-
|
|
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
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
const udsCmd = 'npx uds check';
|
|
266
|
+
const hookDir = join(projectPath, '.git', 'hooks');
|
|
267
|
+
const hookPath = join(hookDir, 'pre-commit');
|
|
229
268
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
247
|
-
|
|
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
|
+
}
|
package/src/commands/update.js
CHANGED
|
@@ -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
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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 };
|
package/src/flows/init-flow.js
CHANGED
|
@@ -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,
|
package/src/i18n/messages.js
CHANGED
|
@@ -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: '⚠ 检测到一些问题。详情请查看上文。',
|