universal-dev-standards 6.1.1 → 6.2.1
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/bundled/locales/zh-CN/CHANGELOG.md +40 -3
- package/bundled/locales/zh-CN/README.md +75 -33
- package/bundled/locales/zh-CN/SECURITY.md +1 -1
- package/bundled/locales/zh-CN/core/behavior-snapshot.md +2 -2
- package/bundled/locales/zh-CN/core/data-migration-testing.md +2 -2
- package/bundled/locales/zh-CN/docs/CHEATSHEET.md +4 -4
- package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +9 -3
- package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +10 -11
- package/bundled/locales/zh-CN/docs/USER-MANUAL.md +40 -21
- package/bundled/locales/zh-CN/integrations/gemini-cli/README.md +12 -0
- package/bundled/locales/zh-CN/skills/ac-coverage/SKILL.md +11 -5
- package/bundled/locales/zh-CN/skills/adr-assistant/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/commands/brainstorm.md +2 -2
- package/bundled/locales/zh-CN/skills/commit-standards/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/contract-test-assistant/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/deploy-assistant/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/dev-methodology/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/dev-workflow-guide/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/journey-test-assistant/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/knowledge-graph/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/knowledge-graph/guide.md +2 -2
- package/bundled/locales/zh-CN/skills/migration-assistant/SKILL.md +188 -2
- package/bundled/locales/zh-CN/skills/observability-assistant/guide.md +2 -2
- package/bundled/locales/zh-CN/skills/orchestrate/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/plan/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/push/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/retrospective-assistant/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/reverse-engineer/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/runbook-assistant/guide.md +2 -2
- package/bundled/locales/zh-CN/skills/skill-builder/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/slo-assistant/guide.md +2 -2
- package/bundled/locales/zh-CN/skills/spec-derivation/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/spec-driven-dev/SKILL.md +40 -2
- package/bundled/locales/zh-CN/skills/sweep/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/testing-guide/SKILL.md +2 -2
- package/bundled/locales/zh-TW/CHANGELOG.md +41 -3
- package/bundled/locales/zh-TW/README.md +75 -33
- package/bundled/locales/zh-TW/SECURITY.md +1 -1
- package/bundled/locales/zh-TW/core/audit-trail.md +2 -2
- package/bundled/locales/zh-TW/core/behavior-snapshot.md +2 -2
- package/bundled/locales/zh-TW/core/browser-compatibility-standards.md +18 -7
- package/bundled/locales/zh-TW/docs/CHEATSHEET.md +4 -4
- package/bundled/locales/zh-TW/docs/CLI-INIT-OPTIONS.md +9 -3
- package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +10 -11
- package/bundled/locales/zh-TW/docs/USER-MANUAL.md +40 -21
- package/bundled/locales/zh-TW/integrations/gemini-cli/README.md +12 -0
- package/bundled/skills/commands/journey-test.md +80 -6
- package/bundled/skills/commands/skill-builder.md +75 -6
- package/package.json +3 -3
- package/src/commands/check.js +54 -7
- package/src/commands/config.js +9 -0
- package/src/commands/init.js +15 -1
- package/src/commands/release.js +1 -1
- package/src/commands/update.js +87 -31
- package/src/config/ai-agent-paths.js +47 -7
- package/src/core/constants.js +61 -1
- package/src/core/manifest.js +56 -0
- package/src/flow/flow-parser.js +1 -1
- package/src/flow/gate-loader.js +1 -1
- package/src/i18n/messages.js +6 -0
- package/src/installers/standards-installer.js +10 -20
- package/src/reconciler/actual-state-scanner.js +118 -43
- package/src/reconciler/desired-state-calculator.js +157 -43
- package/src/reconciler/diff-engine.js +7 -2
- package/src/reconciler/manifest-migrator.js +5 -2
- package/src/reconciler/plan-executor.js +53 -14
- package/src/uninstallers/integration-uninstaller.js +7 -1
- package/src/utils/config-loader.js +1 -1
- package/src/utils/config-manager.js +1 -1
- package/src/utils/github.js +5 -1
- package/src/utils/hasher.js +7 -4
- package/src/utils/integration-generator.js +121 -78
- package/src/utils/registry.js +39 -0
- package/src/utils/skills-installer.js +49 -34
- package/src/utils/skills-source.js +51 -0
- package/src/utils/standard-fixer.js +1 -1
- package/src/utils/standard-validator.js +1 -1
- package/standards-registry.json +21 -11
|
@@ -33,10 +33,79 @@ It is backed by the `skill-builder` skill (`skills/skill-builder/SKILL.md`).
|
|
|
33
33
|
| Complex | any of the above is exceeded | Create an XSPEC first (`/sdd`), then build |
|
|
34
34
|
| Delta | modifying an existing Skill | Append `## MODIFIED` / `## ADDED`, bump version |
|
|
35
35
|
|
|
36
|
-
##
|
|
36
|
+
## AI Agent Behavior | AI 代理行為
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
> Follows [AI Command Behavior Standards](../../core/ai-command-behavior.md)
|
|
39
|
+
|
|
40
|
+
### Entry Router | 進入路由
|
|
41
|
+
|
|
42
|
+
| Input | AI Action |
|
|
43
|
+
|-------|-----------|
|
|
44
|
+
| `/skill-builder` | 讀取 `SKILL-CANDIDATES.md`:有已達 3 次的候選 → 列出供選擇;無 → 詢問重複流程的描述 |
|
|
45
|
+
| `/skill-builder <process description>` | 以該描述進入 Step 1,直接開始擷取流程 |
|
|
46
|
+
|
|
47
|
+
### Interaction Script | 互動腳本
|
|
48
|
+
|
|
49
|
+
先讀取 `skills/skill-builder/SKILL.md`,取得完整決策樹、放置規則、工作流程與輸出檢查表。
|
|
50
|
+
|
|
51
|
+
#### Step 1–2: 擷取流程並登記候選
|
|
52
|
+
|
|
53
|
+
1. 擷取重複序列:步驟與順序、目前手動執行過幾次、觸及哪些工具或檔案
|
|
54
|
+
2. 開啟專案的 `SKILL-CANDIDATES.md`(首次則從 `templates/SKILL-CANDIDATES.md` 複製)
|
|
55
|
+
|
|
56
|
+
**Decision: 觸發次數**
|
|
57
|
+
- IF 尚未登記 → 新增列,填入目前次數
|
|
58
|
+
- IF 已登記 → 次數 +1
|
|
59
|
+
- IF 次數 < 3 → **告知尚未達觸發門檻,登記後停止,不建立 SKILL.md**
|
|
60
|
+
- IF 次數 ≥ 3 → 標記觸發 ✅,繼續 Step 3
|
|
61
|
+
|
|
62
|
+
🛑 **STOP**: 次數未達 3 時停止於此,等待使用者明確要求才破例
|
|
63
|
+
|
|
64
|
+
#### Step 3: 路徑選擇
|
|
65
|
+
|
|
66
|
+
回答 4 個判斷問題,判定路徑:
|
|
67
|
+
|
|
68
|
+
- IF ≤7 步、無分支、<3 個標準、無原始碼產出 → **Simple**(Step 4a)
|
|
69
|
+
- IF 上述任一超出 → **Complex**(Step 4b)
|
|
70
|
+
- IF 修改既有 Skill → **Delta**(Step 4c)
|
|
71
|
+
|
|
72
|
+
🛑 **STOP**: 展示判定結果與理由後等待使用者確認路徑
|
|
73
|
+
|
|
74
|
+
#### Step 4: 依路徑分流
|
|
75
|
+
|
|
76
|
+
- **4a Simple** — 以 `templates/SKILL-BRIEF-TEMPLATE.md` 填寫 Brief:觸發情境、3–7 個核心步驟、2–3 條 AC、明確的 out of scope
|
|
77
|
+
- **4b Complex** — 執行 `/sdd` 建立 XSPEC;**XSPEC 未 Approved 前不進入 Step 5**
|
|
78
|
+
- **4c Delta** — 標定既有 SKILL.md 受影響章節,於檔末加 `## MODIFIED Requirements` 或 `## ADDED Requirements`
|
|
79
|
+
|
|
80
|
+
#### Step 5–7: 產出與登記
|
|
81
|
+
|
|
82
|
+
1. **放置決策**:步驟引用專案特有路徑(如 `TECH-RADAR.md`、`DEC-*.md`)→ `{project}/.claude/skills/`;步驟為通用 → UDS `skills/{name}/` **並建立 zh-TW locale**
|
|
83
|
+
2. 由 Brief 或 XSPEC 生成 SKILL.md,核對 frontmatter:`name`、`scope`、`description`、`allowed-tools`
|
|
84
|
+
3. 回填 `SKILL-CANDIDATES.md`:觸發 ✅、Skill 欄填入
|
|
85
|
+
4. 依 skill 的 commit 格式提交
|
|
86
|
+
|
|
87
|
+
🛑 **STOP**: commit 前展示輸出檢查表結果,等待使用者確認
|
|
88
|
+
|
|
89
|
+
### Stop Points | 停止點
|
|
90
|
+
|
|
91
|
+
| Stop Point | 等待內容 |
|
|
92
|
+
|-----------|---------|
|
|
93
|
+
| 觸發次數 < 3 | 使用者是否明確要求破例建立 |
|
|
94
|
+
| 路徑判定後 | 使用者確認 Simple / Complex / Delta |
|
|
95
|
+
| Complex 路徑 | XSPEC 經 `/sdd` 建立並 Approved |
|
|
96
|
+
| commit 前 | 使用者確認輸出檢查表 |
|
|
97
|
+
|
|
98
|
+
### Error Handling | 錯誤處理
|
|
99
|
+
|
|
100
|
+
| Error Condition | AI Action |
|
|
101
|
+
|-----------------|-----------|
|
|
102
|
+
| 未提供流程描述且無候選 | 詢問重複流程內容,**不得臆造流程** |
|
|
103
|
+
| `SKILL-CANDIDATES.md` 不存在 | 從 `templates/SKILL-CANDIDATES.md` 複製後繼續 |
|
|
104
|
+
| 觸發次數不足 3 次 | 登記候選後停止,說明門檻理由 |
|
|
105
|
+
| 同名 Skill 已存在 | 改走 Delta 路徑,不覆蓋既有 SKILL.md |
|
|
106
|
+
| 放置位置無法判定 | 列出「專案特有 vs 通用」判準請使用者裁定 |
|
|
107
|
+
|
|
108
|
+
## References | 參考
|
|
109
|
+
|
|
110
|
+
- [Skill Builder Skill](../skill-builder/SKILL.md)
|
|
111
|
+
- Related: [/sdd](./sdd.md) (Complex path creates an XSPEC first)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-dev-standards",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.2.1",
|
|
4
4
|
"description": "CLI tool for adopting Universal Development Standards",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"documentation",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"@inquirer/prompts": "^8.4.2",
|
|
63
63
|
"chalk": "^5.3.0",
|
|
64
64
|
"commander": "^15.0.0",
|
|
65
|
-
"js-yaml": "^
|
|
65
|
+
"js-yaml": "^5.2.1",
|
|
66
66
|
"ora": "^9.4.0"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"@vitest/coverage-v8": "^4.1.5",
|
|
71
71
|
"ajv": "^8.20.0",
|
|
72
72
|
"ajv-formats": "^3.0.1",
|
|
73
|
-
"eslint": "10.
|
|
73
|
+
"eslint": "10.7.0",
|
|
74
74
|
"glob": "^13.0.1",
|
|
75
75
|
"globals": "17.7.0",
|
|
76
76
|
"husky": "^9.1.7",
|
package/src/commands/check.js
CHANGED
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
parseReferences,
|
|
31
31
|
compareStandardsWithReferences
|
|
32
32
|
} from '../utils/reference-sync.js';
|
|
33
|
-
import { extractMarkedContent, getToolFilePath } from '../utils/integration-generator.js';
|
|
33
|
+
import { extractMarkedContent, getToolFilePath, parseStandardsIndexCount } from '../utils/integration-generator.js';
|
|
34
34
|
import { getToolFormat } from '../core/constants.js';
|
|
35
35
|
import { checkForUpdates } from '../utils/npm-registry.js';
|
|
36
36
|
import { writeUpdateCache } from '../utils/update-checker.js';
|
|
@@ -39,6 +39,7 @@ import { WorkflowGate } from '../utils/workflow-gate.js';
|
|
|
39
39
|
import { t, setLanguage, isLanguageExplicitlySet } from '../i18n/messages.js';
|
|
40
40
|
import { guardAgainstSelfAdoption } from '../utils/detect-self-adoption.js';
|
|
41
41
|
import { lintAll as lintI18nAll, partitionFindings as partitionI18nFindings } from '../lint/i18n.js';
|
|
42
|
+
import { resolveIntegrationFile } from '../core/constants.js';
|
|
42
43
|
|
|
43
44
|
/**
|
|
44
45
|
* Display the summary of file integrity status
|
|
@@ -161,7 +162,9 @@ function performFileIntegrityCheck(projectPath, manifest, msg) {
|
|
|
161
162
|
}
|
|
162
163
|
|
|
163
164
|
// Check integrations
|
|
164
|
-
for (const
|
|
165
|
+
for (const intEntry of manifest.integrations) {
|
|
166
|
+
// 兩種形狀都要能解出路徑(XSPEC-343 R1)
|
|
167
|
+
const int = resolveIntegrationFile(intEntry) || intEntry;
|
|
165
168
|
const filePath = join(projectPath, int);
|
|
166
169
|
if (existsSync(filePath)) {
|
|
167
170
|
fileStatus.noHash.push(int);
|
|
@@ -727,7 +730,9 @@ function removeFromManifest(manifest, relativePath) {
|
|
|
727
730
|
const fileName = basename(relativePath);
|
|
728
731
|
manifest.standards = manifest.standards.filter(s => !s.endsWith(fileName));
|
|
729
732
|
manifest.extensions = manifest.extensions.filter(e => typeof e !== 'string' || !e.endsWith(fileName));
|
|
730
|
-
manifest.integrations = manifest.integrations.filter(
|
|
733
|
+
manifest.integrations = manifest.integrations.filter(
|
|
734
|
+
i => (resolveIntegrationFile(i) || i) !== relativePath
|
|
735
|
+
);
|
|
731
736
|
|
|
732
737
|
// Remove from integrationBlockHashes
|
|
733
738
|
if (manifest.integrationBlockHashes && manifest.integrationBlockHashes[relativePath]) {
|
|
@@ -761,7 +766,7 @@ export function getSourcePathFromRelative(manifest, relativePath) {
|
|
|
761
766
|
}
|
|
762
767
|
|
|
763
768
|
// Check integrations - these might need special handling
|
|
764
|
-
if (manifest.integrations.
|
|
769
|
+
if (manifest.integrations.some(i => (resolveIntegrationFile(i) || i) === relativePath)) {
|
|
765
770
|
// Integration files have different source paths
|
|
766
771
|
const integrationMappings = {
|
|
767
772
|
'.cursorrules': 'integrations/cursor/.cursorrules',
|
|
@@ -831,7 +836,8 @@ async function migrateToHashBasedTracking(projectPath, manifest) {
|
|
|
831
836
|
}
|
|
832
837
|
|
|
833
838
|
// Process integrations
|
|
834
|
-
for (const
|
|
839
|
+
for (const intEntry of manifest.integrations) {
|
|
840
|
+
const int = resolveIntegrationFile(intEntry) || intEntry;
|
|
835
841
|
const fullPath = join(projectPath, int);
|
|
836
842
|
|
|
837
843
|
const hashInfo = computeFileHash(fullPath);
|
|
@@ -1148,6 +1154,27 @@ function checkIntegrationFiles(manifest, projectPath, msg) {
|
|
|
1148
1154
|
// Report status - use all installed standards as the total
|
|
1149
1155
|
const totalTrackable = standardsFiles.length;
|
|
1150
1156
|
|
|
1157
|
+
// Index-mode blocks deliberately do NOT enumerate standards (XSPEC-358 R1);
|
|
1158
|
+
// grepping for each name reports a false failure and points at `uds update`,
|
|
1159
|
+
// which regenerates the identical block. Assert the declared count instead.
|
|
1160
|
+
const declaredCount = parseStandardsIndexCount(content);
|
|
1161
|
+
if (declaredCount !== null) {
|
|
1162
|
+
if (declaredCount === totalTrackable) {
|
|
1163
|
+
console.log(chalk.green(` ✓ ${toolFile}:`));
|
|
1164
|
+
console.log(chalk.gray(` ${msg.standardsIndexPresent}`));
|
|
1165
|
+
console.log(chalk.gray(` ${msg.standardsIndexCount
|
|
1166
|
+
? msg.standardsIndexCount.replace('{count}', declaredCount)
|
|
1167
|
+
: `Index declares ${declaredCount} standards (matches manifest)`}`));
|
|
1168
|
+
} else {
|
|
1169
|
+
console.log(chalk.yellow(` ⚠ ${toolFile}:`));
|
|
1170
|
+
console.log(chalk.yellow(` ${msg.standardsIndexCountMismatch
|
|
1171
|
+
? msg.standardsIndexCountMismatch.replace('{declared}', declaredCount).replace('{actual}', totalTrackable)
|
|
1172
|
+
: `Index declares ${declaredCount} standards but the manifest has ${totalTrackable}`}`));
|
|
1173
|
+
hasIssues = true;
|
|
1174
|
+
}
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1151
1178
|
if (hasStandardsIndex && missingStandards.length === 0) {
|
|
1152
1179
|
console.log(chalk.green(` ✓ ${toolFile}:`));
|
|
1153
1180
|
console.log(chalk.gray(` ${msg.standardsIndexPresent}`));
|
|
@@ -1219,6 +1246,25 @@ function checkAgentsMdSync(manifest, projectPath, msg) {
|
|
|
1219
1246
|
const content = readFileSync(agentsMdPath, 'utf-8');
|
|
1220
1247
|
const installedStandards = (manifest.standards || []).map(s => basename(s));
|
|
1221
1248
|
|
|
1249
|
+
// Same as the AI-tool integration check above: an index-mode block declares a
|
|
1250
|
+
// count instead of listing names, so the name grep below cannot apply to it.
|
|
1251
|
+
const declaredCount = parseStandardsIndexCount(content);
|
|
1252
|
+
if (declaredCount !== null) {
|
|
1253
|
+
const actual = installedStandards.length;
|
|
1254
|
+
if (declaredCount === actual) {
|
|
1255
|
+
console.log(chalk.green(` ✓ AGENTS.md ${msg.standardsSynced || 'standards synced'} (${declaredCount})`));
|
|
1256
|
+
} else {
|
|
1257
|
+
console.log(chalk.yellow(` ⚠ AGENTS.md ${msg.standardsOutOfSync || 'standards out of sync'} (${declaredCount} != ${actual})`));
|
|
1258
|
+
console.log(chalk.gray(` ${msg.runUpdateToSync || 'Run "uds update" to sync'}`));
|
|
1259
|
+
}
|
|
1260
|
+
const idxLineCount = content.split('\n').length;
|
|
1261
|
+
if (idxLineCount > 150) {
|
|
1262
|
+
console.log(chalk.yellow(` ⚠ AGENTS.md ${msg.exceedsLineLimit || 'exceeds 150 line limit'} (${idxLineCount} lines)`));
|
|
1263
|
+
}
|
|
1264
|
+
console.log();
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1222
1268
|
// Check standards listed in AGENTS.md vs manifest
|
|
1223
1269
|
const aiYamlStandards = installedStandards.filter(s => s.endsWith('.ai.yaml'));
|
|
1224
1270
|
let listedCount = 0;
|
|
@@ -1264,7 +1310,8 @@ function checkReferenceSync(manifest, projectPath, msg) {
|
|
|
1264
1310
|
|
|
1265
1311
|
// Pre-scan: collect results for files that have references
|
|
1266
1312
|
const results = [];
|
|
1267
|
-
for (const
|
|
1313
|
+
for (const integrationEntry of manifest.integrations) {
|
|
1314
|
+
const integrationPath = resolveIntegrationFile(integrationEntry) || integrationEntry;
|
|
1268
1315
|
const fullPath = join(projectPath, integrationPath);
|
|
1269
1316
|
|
|
1270
1317
|
if (!existsSync(fullPath)) continue;
|
|
@@ -1700,7 +1747,7 @@ function getFileStatusCounts(manifest, projectPath) {
|
|
|
1700
1747
|
const allFiles = [
|
|
1701
1748
|
...manifest.standards.map(s => `.standards/${basename(s)}`),
|
|
1702
1749
|
...manifest.extensions.filter(e => typeof e === 'string').map(e => `.standards/${basename(e)}`),
|
|
1703
|
-
...manifest.integrations
|
|
1750
|
+
...manifest.integrations.map(i => resolveIntegrationFile(i) || i)
|
|
1704
1751
|
];
|
|
1705
1752
|
for (const relativePath of allFiles) {
|
|
1706
1753
|
const fullPath = join(projectPath, relativePath);
|
package/src/commands/config.js
CHANGED
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
} from '../utils/integration-generator.js';
|
|
50
50
|
import { getMarketplaceSkillsInfo } from '../utils/github.js';
|
|
51
51
|
import { regenerateIntegrations } from './update.js';
|
|
52
|
+
import { mergeInstalledNames } from '../core/manifest.js';
|
|
52
53
|
|
|
53
54
|
/**
|
|
54
55
|
* Get localized message with fallback (for config-specific keys)
|
|
@@ -995,6 +996,7 @@ export async function runProjectConfiguration(options) {
|
|
|
995
996
|
manifest.skills.installations.push(inst);
|
|
996
997
|
}
|
|
997
998
|
}
|
|
999
|
+
manifest.skills.names = mergeInstalledNames(manifest.skills.names, skillResult);
|
|
998
1000
|
if (skillResult.allFileHashes) {
|
|
999
1001
|
manifest.skillHashes = { ...(manifest.skillHashes || {}), ...skillResult.allFileHashes };
|
|
1000
1002
|
}
|
|
@@ -1031,6 +1033,7 @@ export async function runProjectConfiguration(options) {
|
|
|
1031
1033
|
manifest.commands.installations.push(inst);
|
|
1032
1034
|
}
|
|
1033
1035
|
}
|
|
1036
|
+
manifest.commands.names = mergeInstalledNames(manifest.commands.names, cmdResult);
|
|
1034
1037
|
if (cmdResult.allFileHashes) {
|
|
1035
1038
|
manifest.commandHashes = { ...(manifest.commandHashes || {}), ...cmdResult.allFileHashes };
|
|
1036
1039
|
}
|
|
@@ -1234,6 +1237,7 @@ async function handleSkillsConfiguration(manifest, projectPath, msgObj, common,
|
|
|
1234
1237
|
} else {
|
|
1235
1238
|
manifest.skills.installations.push(installations[0]);
|
|
1236
1239
|
}
|
|
1240
|
+
manifest.skills.names = mergeInstalledNames(manifest.skills.names, result);
|
|
1237
1241
|
writeManifest(manifest, projectPath);
|
|
1238
1242
|
return;
|
|
1239
1243
|
}
|
|
@@ -1368,6 +1372,7 @@ async function handleSkillsConfiguration(manifest, projectPath, msgObj, common,
|
|
|
1368
1372
|
// Update manifest - clear declined status for installed tools
|
|
1369
1373
|
manifest.skills = manifest.skills || {};
|
|
1370
1374
|
manifest.skills.installations = installations;
|
|
1375
|
+
manifest.skills.names = mergeInstalledNames(manifest.skills.names, result);
|
|
1371
1376
|
if (manifest.declinedFeatures?.skills) {
|
|
1372
1377
|
manifest.declinedFeatures.skills = manifest.declinedFeatures.skills.filter(
|
|
1373
1378
|
tool => !declinedToolsWithSupport.includes(tool)
|
|
@@ -1402,6 +1407,7 @@ async function handleSkillsConfiguration(manifest, projectPath, msgObj, common,
|
|
|
1402
1407
|
// Update manifest
|
|
1403
1408
|
manifest.skills = manifest.skills || {};
|
|
1404
1409
|
manifest.skills.installations = installations;
|
|
1410
|
+
manifest.skills.names = mergeInstalledNames(manifest.skills.names, result);
|
|
1405
1411
|
writeManifest(manifest, projectPath);
|
|
1406
1412
|
}
|
|
1407
1413
|
|
|
@@ -1465,6 +1471,7 @@ async function handleCommandsConfiguration(manifest, projectPath, msgObj, common
|
|
|
1465
1471
|
} else {
|
|
1466
1472
|
manifest.commands.installations.push(installations[0]);
|
|
1467
1473
|
}
|
|
1474
|
+
manifest.commands.names = mergeInstalledNames(manifest.commands.names, result);
|
|
1468
1475
|
writeManifest(manifest, projectPath);
|
|
1469
1476
|
return;
|
|
1470
1477
|
}
|
|
@@ -1566,6 +1573,7 @@ async function handleCommandsConfiguration(manifest, projectPath, msgObj, common
|
|
|
1566
1573
|
// Update manifest - clear declined status for installed tools
|
|
1567
1574
|
manifest.commands = manifest.commands || {};
|
|
1568
1575
|
manifest.commands.installations = declinedCommandsWithSupport;
|
|
1576
|
+
manifest.commands.names = mergeInstalledNames(manifest.commands.names, result);
|
|
1569
1577
|
if (manifest.declinedFeatures?.commands) {
|
|
1570
1578
|
manifest.declinedFeatures.commands = manifest.declinedFeatures.commands.filter(
|
|
1571
1579
|
tool => !declinedCommandsWithSupport.includes(tool)
|
|
@@ -1601,5 +1609,6 @@ async function handleCommandsConfiguration(manifest, projectPath, msgObj, common
|
|
|
1601
1609
|
// Update manifest
|
|
1602
1610
|
manifest.commands = manifest.commands || {};
|
|
1603
1611
|
manifest.commands.installations = selectedAgents;
|
|
1612
|
+
manifest.commands.names = mergeInstalledNames(manifest.commands.names, result);
|
|
1604
1613
|
writeManifest(manifest, projectPath);
|
|
1605
1614
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -240,7 +240,21 @@ export async function initCommand(options) {
|
|
|
240
240
|
* - Node.js projects: use husky
|
|
241
241
|
* - Non-Node.js projects: write native .git/hooks/pre-commit
|
|
242
242
|
*/
|
|
243
|
-
export async function setupHuskyHook(projectPath) {
|
|
243
|
+
export async function setupHuskyHook(projectPath, { allowInTest = false } = {}) {
|
|
244
|
+
// 2026-07-30:本函式在跑測試時改寫了 universal-dev-standards 自己
|
|
245
|
+
// ——`npm install --save-dev husky`、改 package.json、建 .husky/pre-commit。
|
|
246
|
+
//
|
|
247
|
+
// 路徑是:測試呼叫 `initCommand({...})`(三個 init 測試檔都沒帶路徑、
|
|
248
|
+
// 也沒 mock child_process)→ `init.js` 取 `process.cwd()` → 走到這裡。
|
|
249
|
+
// 而下面那個 `.git` 是唯一的守衛,它只問「cwd 是不是一個 repo」:
|
|
250
|
+
// 從 `cli/` 跑 → cwd 沒有 .git → 提早 return,無害;
|
|
251
|
+
// 從 repo root 跑 → cwd **有** .git → 照跑,動到開發者自己的 repo。
|
|
252
|
+
// 同一批測試、不同 cwd,一個無害一個會寫檔。
|
|
253
|
+
//
|
|
254
|
+
// 守衛用顯式 opt-in 而非「偵測到測試就一律跳過」:`init.husky.test.js` 有十幾條
|
|
255
|
+
// 測試是**刻意**在暫存目錄裡驗這個函式的行為,一律跳過會把它們變成永遠通過的空測試。
|
|
256
|
+
if (process.env.VITEST && !allowInTest) return;
|
|
257
|
+
|
|
244
258
|
const hasGit = existsSync(join(projectPath, '.git'));
|
|
245
259
|
if (!hasGit) return;
|
|
246
260
|
|
package/src/commands/release.js
CHANGED
package/src/commands/update.js
CHANGED
|
@@ -2,7 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import { select, confirm as inquirerConfirm, checkbox, Separator } from '@inquirer/prompts';
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
|
-
import { existsSync, unlinkSync } from 'fs';
|
|
5
|
+
import { existsSync, readFileSync, unlinkSync } from 'fs';
|
|
6
6
|
import { join, basename } from 'path';
|
|
7
7
|
import { readManifest, writeManifest, copyStandard, isInitialized } from '../utils/copier.js';
|
|
8
8
|
import { getRepositoryInfo, getAllStandards, getStandardSource } from '../utils/registry.js';
|
|
@@ -11,7 +11,10 @@ import {
|
|
|
11
11
|
writeIntegrationFile,
|
|
12
12
|
getToolFilePath,
|
|
13
13
|
writeAgentsMdSummary,
|
|
14
|
-
resolveContentModeForTool
|
|
14
|
+
resolveContentModeForTool,
|
|
15
|
+
generateIntegrationContent,
|
|
16
|
+
extractMarkedContent,
|
|
17
|
+
buildToolIntegrationConfig
|
|
15
18
|
} from '../utils/integration-generator.js';
|
|
16
19
|
import {
|
|
17
20
|
calculateCategoriesFromStandards,
|
|
@@ -51,6 +54,8 @@ import {
|
|
|
51
54
|
} from '../reconciler/index.js';
|
|
52
55
|
import { restoreSingleFile } from './check.js';
|
|
53
56
|
import { guardAgainstSelfAdoption } from '../utils/detect-self-adoption.js';
|
|
57
|
+
import { resolveIntegrationFile } from '../core/constants.js';
|
|
58
|
+
import { mergeInstalledNames } from '../core/manifest.js';
|
|
54
59
|
|
|
55
60
|
/**
|
|
56
61
|
* Determine the correct target directory for a standard file.
|
|
@@ -335,8 +340,12 @@ export async function updateCommand(options) {
|
|
|
335
340
|
}
|
|
336
341
|
|
|
337
342
|
// Handle --integrations-only option
|
|
343
|
+
// ⚠️ 這個分支在 --plan 檢查(見下)**之前**,所以必須自己處理 --plan——
|
|
344
|
+
// 否則 `--plan --integrations-only` 會靜默寫檔。2026-07-30 實測踩到:
|
|
345
|
+
// 以該組合驗證 XSPEC-358 的區塊變更時,它改了 machine-setup 的
|
|
346
|
+
// CLAUDE.md 與 .standards/manifest.json,而旗標文件寫的是 "without executing"。
|
|
338
347
|
if (options.integrationsOnly) {
|
|
339
|
-
await updateIntegrationsOnly(projectPath, manifest);
|
|
348
|
+
await updateIntegrationsOnly(projectPath, manifest, options);
|
|
340
349
|
return;
|
|
341
350
|
}
|
|
342
351
|
|
|
@@ -847,8 +856,9 @@ export async function updateCommand(options) {
|
|
|
847
856
|
const fileName = basename(ext);
|
|
848
857
|
allTrackedFiles.push(join('.standards', fileName));
|
|
849
858
|
}
|
|
850
|
-
for (const
|
|
851
|
-
|
|
859
|
+
for (const intEntry of (manifest.integrations || [])) {
|
|
860
|
+
// 兩種形狀都要能解出路徑(XSPEC-343 R1)
|
|
861
|
+
const filePath = resolveIntegrationFile(intEntry) || getToolFilePath(intEntry);
|
|
852
862
|
if (filePath) {
|
|
853
863
|
allTrackedFiles.push(filePath);
|
|
854
864
|
}
|
|
@@ -947,10 +957,12 @@ export async function updateCommand(options) {
|
|
|
947
957
|
if (!manifest.skills) manifest.skills = {};
|
|
948
958
|
manifest.skills.installed = true;
|
|
949
959
|
manifest.skills.version = repoInfo.skills.version;
|
|
960
|
+
manifest.skills.locale = skillsLocale;
|
|
950
961
|
manifest.skills.installations = [
|
|
951
962
|
...(manifest.skills.installations || []),
|
|
952
963
|
...installSkills
|
|
953
964
|
];
|
|
965
|
+
manifest.skills.names = mergeInstalledNames(manifest.skills.names, skillResult);
|
|
954
966
|
|
|
955
967
|
// Derive location from installations if not set
|
|
956
968
|
if (!manifest.skills.location) {
|
|
@@ -987,6 +999,8 @@ export async function updateCommand(options) {
|
|
|
987
999
|
// Update manifest version
|
|
988
1000
|
if (!manifest.skills) manifest.skills = {};
|
|
989
1001
|
manifest.skills.version = repoInfo.skills.version;
|
|
1002
|
+
manifest.skills.locale = updateLocale;
|
|
1003
|
+
manifest.skills.names = mergeInstalledNames(manifest.skills.names, updateResult);
|
|
990
1004
|
|
|
991
1005
|
// Derive location from installations if not set
|
|
992
1006
|
if (!manifest.skills.location && manifest.skills.installations?.length > 0) {
|
|
@@ -1028,6 +1042,7 @@ export async function updateCommand(options) {
|
|
|
1028
1042
|
...(manifest.commands.installations || []),
|
|
1029
1043
|
...installCommands
|
|
1030
1044
|
];
|
|
1045
|
+
manifest.commands.names = mergeInstalledNames(manifest.commands.names, cmdResult);
|
|
1031
1046
|
|
|
1032
1047
|
// Update command hashes for integrity tracking
|
|
1033
1048
|
if (cmdResult.allFileHashes) {
|
|
@@ -1053,6 +1068,7 @@ export async function updateCommand(options) {
|
|
|
1053
1068
|
// Update manifest version
|
|
1054
1069
|
if (!manifest.commands) manifest.commands = {};
|
|
1055
1070
|
manifest.commands.version = repoInfo.skills.version;
|
|
1071
|
+
manifest.commands.names = mergeInstalledNames(manifest.commands.names, updateCmdResult);
|
|
1056
1072
|
|
|
1057
1073
|
// Update command hashes for integrity tracking
|
|
1058
1074
|
if (updateCmdResult.allFileHashes) {
|
|
@@ -1126,7 +1142,9 @@ export async function updateCommand(options) {
|
|
|
1126
1142
|
if (!manifest.skills) manifest.skills = {};
|
|
1127
1143
|
manifest.skills.installed = true;
|
|
1128
1144
|
manifest.skills.version = repoInfo.skills.version;
|
|
1145
|
+
manifest.skills.locale = skillsLocale;
|
|
1129
1146
|
manifest.skills.installations = [...(manifest.skills.installations || []), ...missingSkills];
|
|
1147
|
+
manifest.skills.names = mergeInstalledNames(manifest.skills.names, result);
|
|
1130
1148
|
if (result.allFileHashes) {
|
|
1131
1149
|
if (!manifest.skillHashes) manifest.skillHashes = {};
|
|
1132
1150
|
Object.assign(manifest.skillHashes, result.allFileHashes);
|
|
@@ -1144,6 +1162,8 @@ export async function updateCommand(options) {
|
|
|
1144
1162
|
const result = await installSkillsToMultipleAgents(outdatedSkills, null, projectPath, skillsLocale);
|
|
1145
1163
|
if (!manifest.skills) manifest.skills = {};
|
|
1146
1164
|
manifest.skills.version = repoInfo.skills.version;
|
|
1165
|
+
manifest.skills.locale = skillsLocale;
|
|
1166
|
+
manifest.skills.names = mergeInstalledNames(manifest.skills.names, result);
|
|
1147
1167
|
if (!manifest.skills.location && manifest.skills.installations?.length > 0) {
|
|
1148
1168
|
const levels = manifest.skills.installations.map(s => s.level).filter(Boolean);
|
|
1149
1169
|
const uniqueLevels = [...new Set(levels)];
|
|
@@ -1168,6 +1188,7 @@ export async function updateCommand(options) {
|
|
|
1168
1188
|
manifest.commands.installed = true;
|
|
1169
1189
|
manifest.commands.version = repoInfo.skills.version;
|
|
1170
1190
|
manifest.commands.installations = [...(manifest.commands.installations || []), ...missingCommands];
|
|
1191
|
+
manifest.commands.names = mergeInstalledNames(manifest.commands.names, result);
|
|
1171
1192
|
if (result.allFileHashes) {
|
|
1172
1193
|
if (!manifest.commandHashes) manifest.commandHashes = {};
|
|
1173
1194
|
replaceCommandHashesForUpdatedAgents(manifest.commandHashes, result.allFileHashes);
|
|
@@ -1185,6 +1206,7 @@ export async function updateCommand(options) {
|
|
|
1185
1206
|
const result = await installCommandsToMultipleAgents(outdatedCommands, null, projectPath, skillsLocale);
|
|
1186
1207
|
if (!manifest.commands) manifest.commands = {};
|
|
1187
1208
|
manifest.commands.version = repoInfo.skills.version;
|
|
1209
|
+
manifest.commands.names = mergeInstalledNames(manifest.commands.names, result);
|
|
1188
1210
|
if (result.allFileHashes) {
|
|
1189
1211
|
if (!manifest.commandHashes) manifest.commandHashes = {};
|
|
1190
1212
|
replaceCommandHashesForUpdatedAgents(manifest.commandHashes, result.allFileHashes);
|
|
@@ -1319,17 +1341,6 @@ export function regenerateIntegrations(projectPath, manifest) {
|
|
|
1319
1341
|
return { success: true, updated: [], errors: [] };
|
|
1320
1342
|
}
|
|
1321
1343
|
|
|
1322
|
-
// Build installed standards list
|
|
1323
|
-
const installedStandardsList = manifest.standards?.map(s => basename(s)) || [];
|
|
1324
|
-
|
|
1325
|
-
// Determine language setting
|
|
1326
|
-
let commonLanguage = 'en';
|
|
1327
|
-
if ((manifest.options?.output_language || manifest.options?.commit_language) === 'bilingual') {
|
|
1328
|
-
commonLanguage = 'bilingual';
|
|
1329
|
-
} else if ((manifest.options?.output_language || manifest.options?.commit_language) === 'traditional-chinese') {
|
|
1330
|
-
commonLanguage = 'zh-tw';
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
1344
|
const results = {
|
|
1334
1345
|
updated: [],
|
|
1335
1346
|
errors: []
|
|
@@ -1345,19 +1356,8 @@ export function regenerateIntegrations(projectPath, manifest) {
|
|
|
1345
1356
|
continue; // Skip if already generated (AGENTS.md sharing)
|
|
1346
1357
|
}
|
|
1347
1358
|
|
|
1348
|
-
//
|
|
1349
|
-
const
|
|
1350
|
-
const resolvedMode = resolveContentModeForTool(tool, savedMode);
|
|
1351
|
-
const toolConfig = {
|
|
1352
|
-
tool,
|
|
1353
|
-
categories: ['anti-hallucination', 'commit-standards', 'code-review'],
|
|
1354
|
-
language: commonLanguage,
|
|
1355
|
-
installedStandards: installedStandardsList,
|
|
1356
|
-
contentMode: resolvedMode.contentMode,
|
|
1357
|
-
level: resolvedMode.level,
|
|
1358
|
-
// Pass output_language for dynamic commit standards generation
|
|
1359
|
-
outputLanguage: manifest.options?.output_language || manifest.options?.commit_language || 'english'
|
|
1360
|
-
};
|
|
1359
|
+
// Shared with the reconciler so both paths emit the identical block.
|
|
1360
|
+
const toolConfig = buildToolIntegrationConfig(manifest, tool);
|
|
1361
1361
|
|
|
1362
1362
|
const result = writeIntegrationFile(tool, toolConfig, projectPath);
|
|
1363
1363
|
if (result.success) {
|
|
@@ -1392,7 +1392,7 @@ export function regenerateIntegrations(projectPath, manifest) {
|
|
|
1392
1392
|
// Regenerate universal AGENTS.md if enabled and not already covered
|
|
1393
1393
|
if (manifest.generateAgentsMd && !generatedFiles.has('AGENTS.md')) {
|
|
1394
1394
|
const summaryConfig = {
|
|
1395
|
-
installedStandards:
|
|
1395
|
+
installedStandards: (manifest.standards || []).map(s => basename(s)),
|
|
1396
1396
|
language: manifest.options?.display_language || 'en',
|
|
1397
1397
|
outputLanguage: manifest.options?.output_language || manifest.options?.commit_language || 'english',
|
|
1398
1398
|
standardOptions: manifest.options || {}
|
|
@@ -1422,7 +1422,7 @@ export function regenerateIntegrations(projectPath, manifest) {
|
|
|
1422
1422
|
* @param {string} projectPath - Project path
|
|
1423
1423
|
* @param {Object} manifest - Manifest object
|
|
1424
1424
|
*/
|
|
1425
|
-
async function updateIntegrationsOnly(projectPath, manifest) {
|
|
1425
|
+
async function updateIntegrationsOnly(projectPath, manifest, options = {}) {
|
|
1426
1426
|
const msg = t().commands.update;
|
|
1427
1427
|
|
|
1428
1428
|
console.log(chalk.cyan(msg.updatingIntegrationsOnly));
|
|
@@ -1436,6 +1436,59 @@ async function updateIntegrationsOnly(projectPath, manifest) {
|
|
|
1436
1436
|
return;
|
|
1437
1437
|
}
|
|
1438
1438
|
|
|
1439
|
+
// --plan:只比對、不寫任何檔(連 manifest 也不寫)。
|
|
1440
|
+
// 這一段刻意不呼叫 regenerateIntegrations——那個函式會寫檔,
|
|
1441
|
+
// 「先寫再還原」的作法在中途掛掉時會留下壞狀態,比沒有 dry-run 更糟。
|
|
1442
|
+
if (options.plan) {
|
|
1443
|
+
console.log(chalk.bold('=== Integration Plan (dry run — nothing is written) ==='));
|
|
1444
|
+
const wouldChange = [];
|
|
1445
|
+
const unchanged = [];
|
|
1446
|
+
const seen = new Set();
|
|
1447
|
+
for (const tool of aiTools) {
|
|
1448
|
+
const targetFile = getToolFilePath(tool);
|
|
1449
|
+
if (seen.has(targetFile)) continue;
|
|
1450
|
+
seen.add(targetFile);
|
|
1451
|
+
const savedMode = manifest.contentMode || 'auto';
|
|
1452
|
+
const resolvedMode = resolveContentModeForTool(tool, savedMode);
|
|
1453
|
+
const next = generateIntegrationContent({
|
|
1454
|
+
tool,
|
|
1455
|
+
categories: ['anti-hallucination', 'commit-standards', 'code-review'],
|
|
1456
|
+
language: (manifest.options?.output_language || manifest.options?.commit_language) === 'bilingual'
|
|
1457
|
+
? 'bilingual'
|
|
1458
|
+
: (manifest.options?.output_language || manifest.options?.commit_language) === 'traditional-chinese'
|
|
1459
|
+
? 'zh-tw' : 'en',
|
|
1460
|
+
installedStandards: manifest.standards?.map((x) => basename(x)) || [],
|
|
1461
|
+
contentMode: resolvedMode.contentMode,
|
|
1462
|
+
level: resolvedMode.level,
|
|
1463
|
+
outputLanguage: manifest.options?.output_language || manifest.options?.commit_language || 'english'
|
|
1464
|
+
});
|
|
1465
|
+
const full = join(projectPath, targetFile);
|
|
1466
|
+
const current = existsSync(full) ? readFileSync(full, 'utf8') : '';
|
|
1467
|
+
const format = targetFile.endsWith('.md') ? 'markdown' : 'plaintext';
|
|
1468
|
+
const cur = extractMarkedContent(current, format).content || '';
|
|
1469
|
+
// 比對正規化過的內容:只關心「受管區塊會不會變」,不關心尾端空白。
|
|
1470
|
+
if (cur.trim() === String(next).trim()) {
|
|
1471
|
+
unchanged.push(targetFile);
|
|
1472
|
+
} else {
|
|
1473
|
+
const delta = String(next).trim().length - cur.trim().length;
|
|
1474
|
+
wouldChange.push(`${targetFile} (${delta >= 0 ? '+' : ''}${delta} bytes in the UDS block)`);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
if (wouldChange.length > 0) {
|
|
1478
|
+
console.log(chalk.yellow(` ~ Would update (${wouldChange.length}):`));
|
|
1479
|
+
for (const f of wouldChange) console.log(chalk.gray(` ${f}`));
|
|
1480
|
+
}
|
|
1481
|
+
if (unchanged.length > 0) {
|
|
1482
|
+
console.log(chalk.gray(` = Unchanged (${unchanged.length}): ${unchanged.join(', ')}`));
|
|
1483
|
+
}
|
|
1484
|
+
console.log();
|
|
1485
|
+
console.log(chalk.gray(' Also NOT written in plan mode: .standards/manifest.json'));
|
|
1486
|
+
console.log(chalk.gray(' (a real run rewrites it — version, fileHashes, integrationBlockHashes,'));
|
|
1487
|
+
console.log(chalk.gray(' and it normalises any standards entries stored as full paths)'));
|
|
1488
|
+
console.log();
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1439
1492
|
const spinner = ora(msg.regeneratingIntegrations).start();
|
|
1440
1493
|
|
|
1441
1494
|
// Use reusable regeneration function
|
|
@@ -1697,7 +1750,9 @@ async function updateSkillsOnly(projectPath, manifest, options) {
|
|
|
1697
1750
|
|
|
1698
1751
|
// Update manifest
|
|
1699
1752
|
manifest.skills.version = latestVersion;
|
|
1753
|
+
manifest.skills.locale = skillsLocaleForUpdate;
|
|
1700
1754
|
manifest.skills.installations = skillsInstallations;
|
|
1755
|
+
manifest.skills.names = mergeInstalledNames(manifest.skills.names, result);
|
|
1701
1756
|
|
|
1702
1757
|
// Update skill hashes for integrity tracking
|
|
1703
1758
|
if (result.allFileHashes) {
|
|
@@ -1804,6 +1859,7 @@ async function updateCommandsOnly(projectPath, manifest, options) {
|
|
|
1804
1859
|
}
|
|
1805
1860
|
return inst;
|
|
1806
1861
|
});
|
|
1862
|
+
manifest.commands.names = mergeInstalledNames(manifest.commands.names, result);
|
|
1807
1863
|
|
|
1808
1864
|
// Update command hashes for integrity tracking
|
|
1809
1865
|
if (result.allFileHashes) {
|