universal-dev-standards 6.9.0 → 6.11.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/bin/uds.js +2 -0
- package/bundled/core/agent-communication-protocol.md +8 -0
- package/bundled/core/branch-completion.md +8 -0
- package/bundled/core/change-batching-standards.md +8 -0
- package/bundled/core/execution-history.md +8 -0
- package/bundled/core/pipeline-integration-standards.md +8 -0
- package/bundled/core/workflow-enforcement.md +8 -0
- package/bundled/core/workflow-state-protocol.md +8 -0
- package/bundled/locales/zh-CN/CHANGELOG.md +49 -3
- package/bundled/locales/zh-CN/README.md +1 -1
- package/bundled/locales/zh-CN/SECURITY.md +1 -1
- package/bundled/locales/zh-CN/core/agent-communication-protocol.md +7 -0
- package/bundled/locales/zh-CN/core/branch-completion.md +7 -0
- package/bundled/locales/zh-CN/core/change-batching-standards.md +7 -0
- package/bundled/locales/zh-CN/core/execution-history.md +7 -0
- package/bundled/locales/zh-CN/core/pipeline-integration-standards.md +7 -0
- package/bundled/locales/zh-CN/core/workflow-enforcement.md +7 -0
- package/bundled/locales/zh-CN/core/workflow-state-protocol.md +7 -0
- package/bundled/locales/zh-CN/docs/CHEATSHEET.md +1 -1
- package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +52 -5
- package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +3 -1
- package/bundled/locales/zh-CN/docs/MIGRATION-v6.md +8 -4
- package/bundled/locales/zh-TW/CHANGELOG.md +50 -3
- package/bundled/locales/zh-TW/README.md +1 -1
- package/bundled/locales/zh-TW/SECURITY.md +1 -1
- package/bundled/locales/zh-TW/core/agent-communication-protocol.md +7 -0
- package/bundled/locales/zh-TW/core/branch-completion.md +7 -0
- package/bundled/locales/zh-TW/core/change-batching-standards.md +7 -0
- package/bundled/locales/zh-TW/core/execution-history.md +7 -0
- package/bundled/locales/zh-TW/core/pipeline-integration-standards.md +7 -0
- package/bundled/locales/zh-TW/core/workflow-enforcement.md +7 -0
- package/bundled/locales/zh-TW/core/workflow-state-protocol.md +7 -0
- package/bundled/locales/zh-TW/docs/CHEATSHEET.md +1 -1
- package/bundled/locales/zh-TW/docs/CLI-INIT-OPTIONS.md +52 -5
- package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +3 -1
- package/bundled/locales/zh-TW/docs/MIGRATION-v6.md +8 -4
- package/bundled/locales/zh-TW/integrations/claude-code/README.md +14 -5
- package/package.json +7 -6
- package/src/commands/check.js +413 -44
- package/src/commands/config.js +34 -28
- package/src/commands/init.js +37 -7
- package/src/commands/spec.js +2 -2
- package/src/commands/update.js +560 -74
- package/src/core/manifest.js +39 -1
- package/src/flows/init-flow.js +9 -1
- package/src/generators/layered-claudemd.js +13 -4
- package/src/i18n/messages.js +39 -3
- package/src/installers/integration-installer.js +17 -10
- package/src/installers/manifest-installer.js +4 -0
- package/src/installers/skills-installer.js +4 -4
- package/src/installers/standards-installer.js +3 -3
- package/src/prompts/init.js +33 -4
- package/src/reconciler/actual-state-scanner.js +29 -2
- package/src/reconciler/desired-state-calculator.js +51 -2
- package/src/reconciler/diff-engine.js +76 -5
- package/src/reconciler/plan-executor.js +48 -27
- package/src/utils/hasher.js +61 -5
- package/src/utils/integration-generator.js +431 -92
- package/src/utils/marker-locator.js +140 -0
- package/src/utils/reference-sync.js +156 -8
- package/src/utils/registry.js +57 -0
- package/src/utils/spinner.js +31 -0
- package/standards-registry.json +8 -8
package/src/core/manifest.js
CHANGED
|
@@ -773,10 +773,48 @@ export function needsMigration(manifest) {
|
|
|
773
773
|
if (!manifest) {
|
|
774
774
|
return true;
|
|
775
775
|
}
|
|
776
|
-
|
|
776
|
+
|
|
777
777
|
return manifest.version !== CURRENT_SCHEMA_VERSION;
|
|
778
778
|
}
|
|
779
779
|
|
|
780
|
+
/**
|
|
781
|
+
* Compare two `x.y.z` version strings numerically (not lexically — '3.10.0'
|
|
782
|
+
* must sort after '3.9.0'). Missing components count as 0.
|
|
783
|
+
* @returns {number} negative if a<b, 0 if equal, positive if a>b
|
|
784
|
+
*/
|
|
785
|
+
function compareSemverStrings(a, b) {
|
|
786
|
+
const pa = String(a).split('.').map(Number);
|
|
787
|
+
const pb = String(b).split('.').map(Number);
|
|
788
|
+
for (let i = 0; i < 3; i++) {
|
|
789
|
+
const diff = (pa[i] || 0) - (pb[i] || 0);
|
|
790
|
+
if (diff !== 0) return diff;
|
|
791
|
+
}
|
|
792
|
+
return 0;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Set `manifest.version` to the CLI's current schema version — never lower.
|
|
797
|
+
*
|
|
798
|
+
* XSPEC adopter-report Q1: five write sites across update.js/check.js/config.js
|
|
799
|
+
* each hardcoded a schema version literal ('3.1.0'/'3.2.0'/'3.3.0') instead of
|
|
800
|
+
* CURRENT_SCHEMA_VERSION. Every one of those literals predates a later schema
|
|
801
|
+
* bump, so running the operation on an already-current manifest (3.4.0)
|
|
802
|
+
* downgraded it back to whatever the literal said. This is the single place
|
|
803
|
+
* that decision is made: always move toward CURRENT_SCHEMA_VERSION, never
|
|
804
|
+
* away from it, even if a caller is ever fixed incompletely again.
|
|
805
|
+
*
|
|
806
|
+
* @param {Object} manifest - Manifest object (mutated in place)
|
|
807
|
+
*/
|
|
808
|
+
export function bumpManifestVersion(manifest) {
|
|
809
|
+
const current = manifest.version;
|
|
810
|
+
const isWellFormed = typeof current === 'string' && /^\d+\.\d+\.\d+$/.test(current);
|
|
811
|
+
if (!isWellFormed || compareSemverStrings(current, CURRENT_SCHEMA_VERSION) < 0) {
|
|
812
|
+
manifest.version = CURRENT_SCHEMA_VERSION;
|
|
813
|
+
}
|
|
814
|
+
// else: manifest.version is already at or ahead of CURRENT_SCHEMA_VERSION
|
|
815
|
+
// (e.g. a newer version written by a future CLI) — leave it alone.
|
|
816
|
+
}
|
|
817
|
+
|
|
780
818
|
/**
|
|
781
819
|
* Get all AI tools from manifest
|
|
782
820
|
* @param {Object} manifest - Manifest
|
package/src/flows/init-flow.js
CHANGED
|
@@ -242,6 +242,13 @@ export async function runInitFlow(options, detected, projectPath) {
|
|
|
242
242
|
// Optional step — skippable by answering "n" or pressing Ctrl+C.
|
|
243
243
|
await promptProjectContractStep(projectPath);
|
|
244
244
|
|
|
245
|
+
// XSPEC-418 R2: --claude-target is a flag, not an interactive question, but a
|
|
246
|
+
// user running `uds init --claude-target local` without `-y` still expects it
|
|
247
|
+
// honored while answering the rest of the prompts.
|
|
248
|
+
const integrationTargets = options.claudeTarget === 'local'
|
|
249
|
+
? { 'claude-code': 'CLAUDE.local.md' }
|
|
250
|
+
: undefined;
|
|
251
|
+
|
|
245
252
|
return {
|
|
246
253
|
languages,
|
|
247
254
|
frameworks,
|
|
@@ -253,6 +260,7 @@ export async function runInitFlow(options, detected, projectPath) {
|
|
|
253
260
|
integrations,
|
|
254
261
|
contentMode,
|
|
255
262
|
generateAgentsMd,
|
|
256
|
-
releaseMode
|
|
263
|
+
releaseMode,
|
|
264
|
+
integrationTargets
|
|
257
265
|
};
|
|
258
266
|
}
|
|
@@ -11,26 +11,35 @@
|
|
|
11
11
|
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
12
12
|
import { join } from 'path';
|
|
13
13
|
import { mapStandardsToDirectories } from '../utils/directory-mapper.js';
|
|
14
|
+
import { locateMarkerBlock } from '../utils/marker-locator.js';
|
|
14
15
|
|
|
15
16
|
const UDS_BEGIN = '<!-- UDS:STANDARDS:BEGIN -->';
|
|
16
17
|
const UDS_END = '<!-- UDS:STANDARDS:END -->';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Replace content between UDS markers, preserving content outside markers.
|
|
21
|
+
*
|
|
22
|
+
* XSPEC adopter-report Q5: same defect class as integration-generator.js —
|
|
23
|
+
* raw indexOf matched the marker text anywhere in the file, not only on a
|
|
24
|
+
* standalone line. Routed through the shared locateMarkerBlock so a sentence
|
|
25
|
+
* mentioning the marker is never mistaken for the real boundary. Ambiguity
|
|
26
|
+
* (two real marker pairs) throws AmbiguousMarkerError, which this function
|
|
27
|
+
* intentionally does not catch — a write must refuse rather than guess.
|
|
28
|
+
*
|
|
20
29
|
* @param {string} existing - Existing file content
|
|
21
30
|
* @param {string} newUdsContent - New UDS content to insert between markers
|
|
22
31
|
* @returns {string} Updated content
|
|
23
32
|
*/
|
|
24
33
|
function replaceUdsBlock(existing, newUdsContent) {
|
|
25
|
-
const
|
|
26
|
-
const endIdx = existing.indexOf(UDS_END);
|
|
34
|
+
const block = locateMarkerBlock(existing, { start: UDS_BEGIN, end: UDS_END });
|
|
27
35
|
|
|
28
|
-
if (
|
|
36
|
+
if (!block) {
|
|
29
37
|
// No markers found — append UDS block
|
|
30
38
|
return existing + '\n\n' + wrapWithMarkers(newUdsContent);
|
|
31
39
|
}
|
|
32
40
|
|
|
33
|
-
const
|
|
41
|
+
const { startIdx, endIdx } = block;
|
|
42
|
+
const before = existing.substring(0, startIdx);
|
|
34
43
|
const after = existing.substring(endIdx + UDS_END.length);
|
|
35
44
|
return before + wrapWithMarkers(newUdsContent) + after;
|
|
36
45
|
}
|
package/src/i18n/messages.js
CHANGED
|
@@ -743,6 +743,9 @@ export const messages = {
|
|
|
743
743
|
unchanged: 'unchanged',
|
|
744
744
|
modified: 'modified',
|
|
745
745
|
missing: 'missing',
|
|
746
|
+
retired: 'no longer shipped by UDS',
|
|
747
|
+
retiredHeader: '{count} tracked file(s) are gone because UDS stopped shipping them. That is the migration completed, not a fault:',
|
|
748
|
+
retiredHint: 'Run `uds update` to drop their manifest records. `--restore` cannot bring them back — there is no source upstream.',
|
|
746
749
|
existsNoHash: 'exists, no hash',
|
|
747
750
|
summary: 'Summary: {unchanged} unchanged, {modified} modified, {missing} missing',
|
|
748
751
|
// Actions available
|
|
@@ -837,10 +840,17 @@ export const messages = {
|
|
|
837
840
|
refSyncStatus: 'Reference Sync Status:',
|
|
838
841
|
noRefsFound: 'No standard references found',
|
|
839
842
|
refsNotInManifest: 'References not in manifest:',
|
|
840
|
-
standardsNotReferenced: 'Standards
|
|
843
|
+
standardsNotReferenced: 'Standards with no "Reference:"/"參考:" line in this file (optional):',
|
|
841
844
|
refsInSync: '{path}: references in sync ({count} refs)',
|
|
842
845
|
noIntegrationRefs: 'No integration files with standard references found.',
|
|
843
846
|
runSyncRefs: 'Run `uds update --sync-refs` to fix reference issues.',
|
|
847
|
+
runIntegrationsOnly: 'Run `uds update --integrations-only` to regenerate integration files.',
|
|
848
|
+
danglingRefs: 'references files that are not on disk:',
|
|
849
|
+
brokenMentions: 'mentions paths that do not exist in this project:',
|
|
850
|
+
brokenMentionsFix: 'These are usually outside the UDS block, left by an older install. UDS does not rewrite text it did not write — correct them by hand or delete the line.',
|
|
851
|
+
danglingRefsFix: 'Run `uds update --integrations-only` to regenerate them.',
|
|
852
|
+
commandsUntracked: '{count} installed command file(s) are covered by no hash — integrity is not checked for them',
|
|
853
|
+
commandsUntrackedFix: 'Run `uds update --commands` to record them again.',
|
|
844
854
|
// CLI update
|
|
845
855
|
checkingCliUpdates: 'Checking for CLI updates...',
|
|
846
856
|
couldNotCheckUpdates: 'ℹ Could not check for CLI updates (offline mode)',
|
|
@@ -1020,6 +1030,7 @@ export const messages = {
|
|
|
1020
1030
|
syncingIntegrations: 'Syncing integration files...',
|
|
1021
1031
|
syncedIntegrations: 'Synced {count} integration files',
|
|
1022
1032
|
prunedOrphanedBlockHashes: 'Pruned {count} orphaned integration hash(es): {files}',
|
|
1033
|
+
droppedRetiredHashes: 'Dropped {count} manifest record(s) for files UDS no longer ships and that are already gone:',
|
|
1023
1034
|
// Success
|
|
1024
1035
|
updateSuccess: '✓ Standards updated successfully!',
|
|
1025
1036
|
versionUpdated: 'Version: {current} → {latest}',
|
|
@@ -1047,6 +1058,7 @@ export const messages = {
|
|
|
1047
1058
|
// Sync refs
|
|
1048
1059
|
syncingRefs: 'Syncing integration references...',
|
|
1049
1060
|
noIntegrationConfigs: '⚠ No integration configurations found in manifest.',
|
|
1061
|
+
integrationConfigsBackfilled: 'Rebuilt integration config for {count} file(s) from the manifest.',
|
|
1050
1062
|
integrationConfigsRequired: 'Integration configs are required for reference sync.',
|
|
1051
1063
|
thisHappensWhen: 'This happens when:',
|
|
1052
1064
|
oldVersion: '- The project was initialized with an older version of UDS',
|
|
@@ -1971,6 +1983,9 @@ export const messages = {
|
|
|
1971
1983
|
unchanged: '未變更',
|
|
1972
1984
|
modified: '已修改',
|
|
1973
1985
|
missing: '遺失',
|
|
1986
|
+
retired: 'UDS 已不再出貨',
|
|
1987
|
+
retiredHeader: '有 {count} 個被追蹤的檔案不見了,原因是 UDS 已不再出貨。那是遷移完成的樣子,不是故障:',
|
|
1988
|
+
retiredHint: '執行 `uds update` 清掉它們在 manifest 的紀錄。`--restore` 救不回來——上游已經沒有來源。',
|
|
1974
1989
|
existsNoHash: '存在,無 hash',
|
|
1975
1990
|
summary: '摘要:{unchanged} 未變更,{modified} 已修改,{missing} 遺失',
|
|
1976
1991
|
// Actions available
|
|
@@ -2065,10 +2080,17 @@ export const messages = {
|
|
|
2065
2080
|
refSyncStatus: '參考同步狀態:',
|
|
2066
2081
|
noRefsFound: '找不到標準參考',
|
|
2067
2082
|
refsNotInManifest: '未在 manifest 中的參考:',
|
|
2068
|
-
standardsNotReferenced: '
|
|
2083
|
+
standardsNotReferenced: '本檔案中沒有「Reference:」或「參考:」行提及的標準(選用):',
|
|
2069
2084
|
refsInSync: '{path}:參考已同步({count} 個參考)',
|
|
2070
2085
|
noIntegrationRefs: '找不到有標準參考的整合檔案。',
|
|
2071
2086
|
runSyncRefs: '執行 `uds update --sync-refs` 修復參考問題。',
|
|
2087
|
+
runIntegrationsOnly: '執行 `uds update --integrations-only` 重新產生整合檔案。',
|
|
2088
|
+
danglingRefs: '引用了磁碟上不存在的檔案:',
|
|
2089
|
+
brokenMentions: '提到了本專案裡不存在的路徑:',
|
|
2090
|
+
brokenMentionsFix: '這些通常在 UDS 區塊之外,是舊版安裝留下的。UDS 不會改寫不是它寫的文字——請手動更正或刪掉那一行。',
|
|
2091
|
+
danglingRefsFix: '執行 `uds update --integrations-only` 重新產生。',
|
|
2092
|
+
commandsUntracked: '{count} 個已安裝的斜線命令沒有任何雜湊涵蓋——這些檔案的完整性沒有被檢查',
|
|
2093
|
+
commandsUntrackedFix: '執行 `uds update --commands` 重新記錄。',
|
|
2072
2094
|
// CLI update
|
|
2073
2095
|
checkingCliUpdates: '正在檢查 CLI 更新...',
|
|
2074
2096
|
couldNotCheckUpdates: 'ℹ 無法檢查 CLI 更新(離線模式)',
|
|
@@ -2245,6 +2267,7 @@ export const messages = {
|
|
|
2245
2267
|
syncingIntegrations: '同步整合檔案中...',
|
|
2246
2268
|
syncedIntegrations: '已同步 {count} 個整合檔案',
|
|
2247
2269
|
prunedOrphanedBlockHashes: '已清除 {count} 個孤兒整合雜湊:{files}',
|
|
2270
|
+
droppedRetiredHashes: '已清掉 {count} 筆 manifest 紀錄——這些檔案 UDS 已不再出貨,而且本來就已經不在了:',
|
|
2248
2271
|
// Success
|
|
2249
2272
|
updateSuccess: '✓ 標準更新成功!',
|
|
2250
2273
|
versionUpdated: '版本:{current} → {latest}',
|
|
@@ -2272,6 +2295,7 @@ export const messages = {
|
|
|
2272
2295
|
// Sync refs
|
|
2273
2296
|
syncingRefs: '同步整合參考中...',
|
|
2274
2297
|
noIntegrationConfigs: '⚠ manifest 中找不到整合設定。',
|
|
2298
|
+
integrationConfigsBackfilled: '已從 manifest 重建 {count} 個整合檔的設定。',
|
|
2275
2299
|
integrationConfigsRequired: '參考同步需要整合設定。',
|
|
2276
2300
|
thisHappensWhen: '這會發生在:',
|
|
2277
2301
|
oldVersion: '- 專案使用較舊版本的 UDS 初始化',
|
|
@@ -3211,6 +3235,9 @@ export const messages = {
|
|
|
3211
3235
|
unchanged: '未更改',
|
|
3212
3236
|
modified: '已修改',
|
|
3213
3237
|
missing: '缺失',
|
|
3238
|
+
retired: 'UDS 已不再发布',
|
|
3239
|
+
retiredHeader: '有 {count} 个被追踪的文件不见了,原因是 UDS 已不再发布。那是迁移完成的样子,不是故障:',
|
|
3240
|
+
retiredHint: '执行 `uds update` 清掉它们在 manifest 的记录。`--restore` 救不回来——上游已经没有来源。',
|
|
3214
3241
|
existsNoHash: '存在,无哈希',
|
|
3215
3242
|
summary: '摘要:{unchanged} 未更改,{modified} 已修改,{missing} 缺失',
|
|
3216
3243
|
// Actions available
|
|
@@ -3305,10 +3332,17 @@ export const messages = {
|
|
|
3305
3332
|
refSyncStatus: '参考同步状态:',
|
|
3306
3333
|
noRefsFound: '未找到标准引用',
|
|
3307
3334
|
refsNotInManifest: 'manifest 中无引用:',
|
|
3308
|
-
standardsNotReferenced: '
|
|
3335
|
+
standardsNotReferenced: '本文件中没有"Reference:"或"參考:"行提及的标准(可选):',
|
|
3309
3336
|
refsInSync: '{path}:引用已同步({count} 个引用)',
|
|
3310
3337
|
noIntegrationRefs: '未找到包含标准引用的集成文件。',
|
|
3311
3338
|
runSyncRefs: '执行 `uds update --sync-refs` 修复引用问题。',
|
|
3339
|
+
runIntegrationsOnly: '执行 `uds update --integrations-only` 重新生成集成文件。',
|
|
3340
|
+
danglingRefs: '引用了磁盘上不存在的文件:',
|
|
3341
|
+
brokenMentions: '提到了本项目里不存在的路径:',
|
|
3342
|
+
brokenMentionsFix: '这些通常在 UDS 区块之外,是旧版安装留下的。UDS 不会改写不是它写的文字——请手动更正或删掉那一行。',
|
|
3343
|
+
danglingRefsFix: '执行 `uds update --integrations-only` 重新生成。',
|
|
3344
|
+
commandsUntracked: '{count} 个已安装的斜杠命令没有任何哈希覆盖——这些文件的完整性没有被检查',
|
|
3345
|
+
commandsUntrackedFix: '执行 `uds update --commands` 重新记录。',
|
|
3312
3346
|
// CLI update
|
|
3313
3347
|
checkingCliUpdates: '检查 CLI 更新...',
|
|
3314
3348
|
couldNotCheckUpdates: 'ℹ 无法检查 CLI 更新(离线模式)',
|
|
@@ -3398,6 +3432,7 @@ export const messages = {
|
|
|
3398
3432
|
syncingIntegrations: '正在同步集成文件...',
|
|
3399
3433
|
syncedIntegrations: '已同步 {count} 个集成文件',
|
|
3400
3434
|
prunedOrphanedBlockHashes: '已清除 {count} 个孤儿集成哈希:{files}',
|
|
3435
|
+
droppedRetiredHashes: '已清掉 {count} 笔 manifest 记录——这些文件 UDS 已不再发布,而且本来就已经不在了:',
|
|
3401
3436
|
// Success
|
|
3402
3437
|
updateSuccess: '✓ 标准更新成功!',
|
|
3403
3438
|
versionUpdated: '版本:{current} → {latest}',
|
|
@@ -3425,6 +3460,7 @@ export const messages = {
|
|
|
3425
3460
|
// Sync refs
|
|
3426
3461
|
syncingRefs: '正在同步集成引用...',
|
|
3427
3462
|
noIntegrationConfigs: '⚠ manifest 中未找到集成配置。',
|
|
3463
|
+
integrationConfigsBackfilled: '已从 manifest 重建 {count} 个集成文件的配置。',
|
|
3428
3464
|
integrationConfigsRequired: '引用同步需要集成配置。',
|
|
3429
3465
|
thisHappensWhen: '发生这种情况时:',
|
|
3430
3466
|
oldVersion: '- 项目使用旧版 UDS 初始化',
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { createSpinner } from '../utils/spinner.js';
|
|
2
2
|
import { copyIntegration } from '../utils/copier.js';
|
|
3
3
|
import {
|
|
4
4
|
writeIntegrationFile,
|
|
5
5
|
integrationFileExists,
|
|
6
|
-
|
|
6
|
+
resolveIntegrationTargetFile,
|
|
7
7
|
writeAgentsMdSummary,
|
|
8
8
|
resolveContentModeForTool
|
|
9
9
|
} from '../utils/integration-generator.js';
|
|
@@ -99,14 +99,16 @@ export async function installIntegrations(config, projectPath) {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
// Start installation spinner
|
|
102
|
-
const intSpinner =
|
|
102
|
+
const intSpinner = createSpinner(msg.generatingIntegrations).start();
|
|
103
103
|
|
|
104
104
|
// Track generated files to handle AGENTS.md sharing (codex + opencode)
|
|
105
105
|
const generatedFiles = new Set();
|
|
106
106
|
|
|
107
107
|
for (const tool of integrations) {
|
|
108
108
|
// Check if this file was already generated (for AGENTS.md sharing)
|
|
109
|
-
|
|
109
|
+
// XSPEC-418 R2: honors config.integrationTargets (set from --claude-target at
|
|
110
|
+
// init time) rather than always assuming each tool's default file.
|
|
111
|
+
const targetFile = resolveIntegrationTargetFile(tool, { integrationTargets: config.integrationTargets });
|
|
110
112
|
if (generatedFiles.has(targetFile)) {
|
|
111
113
|
// Skip - file already generated by another tool (e.g., codex generated AGENTS.md, skip opencode)
|
|
112
114
|
continue;
|
|
@@ -129,7 +131,9 @@ export async function installIntegrations(config, projectPath) {
|
|
|
129
131
|
contentMode: resolved.contentMode,
|
|
130
132
|
level: resolved.level ?? level,
|
|
131
133
|
// Pass output_language for dynamic commit standards generation
|
|
132
|
-
outputLanguage
|
|
134
|
+
outputLanguage,
|
|
135
|
+
// XSPEC-418 R2: forwarded so writeIntegrationFile resolves the actual target.
|
|
136
|
+
integrationTargets: config.integrationTargets
|
|
133
137
|
};
|
|
134
138
|
|
|
135
139
|
// Use dynamic generator
|
|
@@ -218,13 +222,14 @@ export async function generateClaudeMd(config, projectPath) {
|
|
|
218
222
|
|
|
219
223
|
const msg = t().commands.init;
|
|
220
224
|
|
|
221
|
-
// Check if Claude Code is selected and
|
|
225
|
+
// Check if Claude Code is selected and its target file doesn't already exist
|
|
226
|
+
// XSPEC-418 R2: the target may be CLAUDE.local.md, not CLAUDE.md.
|
|
222
227
|
const claudeCodeSelected = aiTools.includes('claude-code');
|
|
223
|
-
if (!claudeCodeSelected || integrationFileExists('claude-code', projectPath)) {
|
|
228
|
+
if (!claudeCodeSelected || integrationFileExists('claude-code', projectPath, { integrationTargets: config.integrationTargets })) {
|
|
224
229
|
return { path: null, error: null };
|
|
225
230
|
}
|
|
226
231
|
|
|
227
|
-
const claudeSpinner =
|
|
232
|
+
const claudeSpinner = createSpinner(msg.generatingClaudeMd).start();
|
|
228
233
|
|
|
229
234
|
// Derive categories from installedStandards to avoid referencing non-installed standards
|
|
230
235
|
const categories = installedStandards.length > 0
|
|
@@ -245,7 +250,9 @@ export async function generateClaudeMd(config, projectPath) {
|
|
|
245
250
|
contentMode: claudeResolved.contentMode,
|
|
246
251
|
level: claudeResolved.level ?? level,
|
|
247
252
|
// Pass output_language for dynamic commit standards generation
|
|
248
|
-
outputLanguage
|
|
253
|
+
outputLanguage,
|
|
254
|
+
// XSPEC-418 R2
|
|
255
|
+
integrationTargets: config.integrationTargets
|
|
249
256
|
};
|
|
250
257
|
|
|
251
258
|
const result = writeIntegrationFile('claude-code', claudeConfig, projectPath);
|
|
@@ -285,7 +292,7 @@ export async function generateUniversalAgentsMd(config, integrationResults, proj
|
|
|
285
292
|
}
|
|
286
293
|
|
|
287
294
|
const msg = t().commands.init;
|
|
288
|
-
const spinner =
|
|
295
|
+
const spinner = createSpinner(msg.generatingAgentsMd || 'Generating AGENTS.md...').start();
|
|
289
296
|
|
|
290
297
|
const outputLanguage = config.outputLanguage || config.commitLanguage ||
|
|
291
298
|
config.standardOptions?.output_language || config.standardOptions?.commit_language ||
|
|
@@ -31,6 +31,10 @@ export function writeFinalManifest(config, results, projectPath) {
|
|
|
31
31
|
extensions: results.extensions || [],
|
|
32
32
|
integrations: results.integrations || [],
|
|
33
33
|
integrationConfigs: results.manifestIntegrationConfigs || {},
|
|
34
|
+
// XSPEC-418 R2: only ever present when --claude-target local was chosen.
|
|
35
|
+
// Omitted entirely otherwise so a manifest with no override is byte-for-byte
|
|
36
|
+
// what it was before this feature existed (AC-5).
|
|
37
|
+
...(config.integrationTargets ? { integrationTargets: config.integrationTargets } : {}),
|
|
34
38
|
options: {
|
|
35
39
|
display_language: config.displayLanguage || 'en',
|
|
36
40
|
workflow: config.standardOptions?.workflow || null,
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* @version 1.0.0
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import
|
|
16
|
+
import { createSpinner } from '../utils/spinner.js';
|
|
17
17
|
import chalk from 'chalk';
|
|
18
18
|
import {
|
|
19
19
|
installSkillsToMultipleAgents,
|
|
@@ -115,7 +115,7 @@ function getSkillFiles() {
|
|
|
115
115
|
export async function installSkills(skillsConfig, projectPath, messages, results) {
|
|
116
116
|
// Modern unified multi-agent installation
|
|
117
117
|
if (skillsConfig.needsInstall && skillsConfig.skillsInstallations?.length > 0) {
|
|
118
|
-
const skillSpinner =
|
|
118
|
+
const skillSpinner = createSpinner(messages.installingSkills).start();
|
|
119
119
|
|
|
120
120
|
// Use new unified installer for multi-agent support
|
|
121
121
|
const installResult = await installSkillsToMultipleAgents(
|
|
@@ -186,7 +186,7 @@ export async function installSkills(skillsConfig, projectPath, messages, results
|
|
|
186
186
|
* @returns {Promise<void>}
|
|
187
187
|
*/
|
|
188
188
|
async function installSkillsLegacy(skillsConfig, projectPath, messages, results) {
|
|
189
|
-
const skillSpinner =
|
|
189
|
+
const skillSpinner = createSpinner(messages.installingSkills).start();
|
|
190
190
|
|
|
191
191
|
const skillFiles = getSkillFiles();
|
|
192
192
|
let successCount = 0;
|
|
@@ -255,7 +255,7 @@ export async function installCommands(skillsConfig, projectPath, messages, resul
|
|
|
255
255
|
return;
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
const cmdSpinner =
|
|
258
|
+
const cmdSpinner = createSpinner(messages.installingCommands || 'Installing slash commands...').start();
|
|
259
259
|
|
|
260
260
|
const cmdResult = await installCommandsToMultipleAgents(
|
|
261
261
|
skillsConfig.commandsInstallations,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import
|
|
2
|
+
import { createSpinner } from '../utils/spinner.js';
|
|
3
3
|
import { basename, join } from 'path';
|
|
4
4
|
import {
|
|
5
5
|
getAllStandards,
|
|
@@ -75,7 +75,7 @@ export async function installStandards(config, projectPath) {
|
|
|
75
75
|
};
|
|
76
76
|
|
|
77
77
|
// Start copying standards
|
|
78
|
-
const copySpinner =
|
|
78
|
+
const copySpinner = createSpinner(msg.copyingStandards).start();
|
|
79
79
|
|
|
80
80
|
// Copy standards based on format
|
|
81
81
|
const formatsToUse = config.format === 'both' ? ['ai', 'human'] : [config.format];
|
|
@@ -113,7 +113,7 @@ export async function installStandards(config, projectPath) {
|
|
|
113
113
|
const localeExtension = (config.displayLanguage === 'zh-tw' || config.displayLanguage === 'zh-cn') ? config.displayLanguage : null;
|
|
114
114
|
|
|
115
115
|
if (config.languages.length > 0 || config.frameworks.length > 0 || localeExtension) {
|
|
116
|
-
const extSpinner =
|
|
116
|
+
const extSpinner = createSpinner(msg.copyingExtensions).start();
|
|
117
117
|
|
|
118
118
|
for (const lang of config.languages) {
|
|
119
119
|
if (EXTENSION_MAPPINGS[lang]) {
|
package/src/prompts/init.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { select, checkbox, confirm as inquirerConfirm, Separator, input } from '@inquirer/prompts';
|
|
2
|
-
import { existsSync, writeFileSync, readdirSync } from 'fs';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import os from 'os';
|
|
@@ -1385,11 +1385,40 @@ export function suggestCommands(projectPath) {
|
|
|
1385
1385
|
if (existsSync(p('Gemfile')))
|
|
1386
1386
|
return { test: 'bundle exec rspec', lint: 'bundle exec rubocop', build: 'gem build *.gemspec', security: 'bundle audit' };
|
|
1387
1387
|
|
|
1388
|
-
// C# — glob for .csproj / .sln
|
|
1388
|
+
// C# — glob for .csproj / .sln.
|
|
1389
|
+
//
|
|
1390
|
+
// `.csproj` has named two incompatible build systems since 2017. An SDK-style
|
|
1391
|
+
// project (`<Project Sdk="…">`, `<TargetFramework>`) builds with the `dotnet`
|
|
1392
|
+
// CLI. An old-style one (`<TargetFrameworkVersion>`, no Sdk attribute) needs
|
|
1393
|
+
// MSBuild and Visual Studio's targets, and handing it `dotnet build` produces
|
|
1394
|
+
// `MSB4019: could not find Import project …Microsoft.WebApplication.targets`
|
|
1395
|
+
// — reported 2026-09-16 from a .NET Framework 4.8 Web App, which also got
|
|
1396
|
+
// `dotnet list package --vulnerable`, a command that silently reports nothing
|
|
1397
|
+
// for a `packages.config` project because it only reads `PackageReference`.
|
|
1398
|
+
//
|
|
1399
|
+
// For the old style only `build` gets an answer. `dotnet format` requires an
|
|
1400
|
+
// SDK-style project and there is no drop-in equivalent; test discovery
|
|
1401
|
+
// depends on which runner the solution uses. The unknown-ecosystem branch
|
|
1402
|
+
// below already returns blanks "so user fills them in", and a blank a human
|
|
1403
|
+
// completes beats a command that looks authoritative and fails on first use.
|
|
1389
1404
|
try {
|
|
1390
1405
|
const files = readdirSync(projectPath);
|
|
1391
|
-
|
|
1392
|
-
|
|
1406
|
+
const projectFiles = files.filter(f => f.endsWith('.csproj') || f.endsWith('.vbproj'));
|
|
1407
|
+
if (projectFiles.length > 0 || files.some(f => f.endsWith('.sln'))) {
|
|
1408
|
+
const oldStyle = projectFiles.some(f => {
|
|
1409
|
+
try {
|
|
1410
|
+
const xml = readFileSync(join(projectPath, f), 'utf-8');
|
|
1411
|
+
if (/<Project[^>]*\sSdk\s*=/i.test(xml)) return false;
|
|
1412
|
+
return /<TargetFrameworkVersion\s*>/i.test(xml);
|
|
1413
|
+
} catch {
|
|
1414
|
+
return false;
|
|
1415
|
+
}
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
return oldStyle
|
|
1419
|
+
? { test: '', lint: '', build: 'msbuild /t:Build /p:Configuration=Release', security: '' }
|
|
1420
|
+
: { test: 'dotnet test', lint: 'dotnet format --verify-no-changes', build: 'dotnet build', security: 'dotnet list package --vulnerable' };
|
|
1421
|
+
}
|
|
1393
1422
|
} catch {
|
|
1394
1423
|
// ignore read errors
|
|
1395
1424
|
}
|
|
@@ -13,6 +13,7 @@ import { join, relative } from 'path';
|
|
|
13
13
|
import { readManifest } from '../core/manifest.js';
|
|
14
14
|
import { computeFileHash, computeIntegrationBlockHash, normalizeLineEndings } from '../utils/hasher.js';
|
|
15
15
|
import { SUPPORTED_AI_TOOLS, UDS_MARKERS } from '../core/constants.js';
|
|
16
|
+
import { locateMarkerBlock, AmbiguousMarkerError } from '../utils/marker-locator.js';
|
|
16
17
|
import { getSkillsDirForAgent, getCommandsDirForAgent, getCommandFileExtension } from '../config/ai-agent-paths.js';
|
|
17
18
|
import { getSkillsSourceEntryNames, getAvailableCommandNames } from '../utils/skills-installer.js';
|
|
18
19
|
|
|
@@ -230,7 +231,21 @@ function scanIntegrations(state, projectPath) {
|
|
|
230
231
|
const filePath = join(projectPath, toolConfig.file);
|
|
231
232
|
if (!existsSync(filePath)) continue;
|
|
232
233
|
|
|
233
|
-
|
|
234
|
+
// XSPEC adopter-report Q5: this scan walks every configured tool's file
|
|
235
|
+
// in one pass to build a full picture of the project — one file with an
|
|
236
|
+
// ambiguous marker pair must not abort the whole scan. `uds check`'s
|
|
237
|
+
// dedicated block-integrity pass is what surfaces the ambiguity to the
|
|
238
|
+
// user explicitly, with line numbers; this scan just records "unknown".
|
|
239
|
+
let blockHash;
|
|
240
|
+
try {
|
|
241
|
+
blockHash = computeIntegrationBlockHash(filePath);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (error instanceof AmbiguousMarkerError) {
|
|
244
|
+
blockHash = null;
|
|
245
|
+
} else {
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
234
249
|
const fileHash = computeFileHash(filePath);
|
|
235
250
|
|
|
236
251
|
state.integrations.set(toolConfig.file, {
|
|
@@ -543,7 +558,19 @@ function hasUDSMarkers(filePath, format) {
|
|
|
543
558
|
try {
|
|
544
559
|
const content = readFileSync(filePath, 'utf-8');
|
|
545
560
|
const markers = UDS_MARKERS[format] || UDS_MARKERS.markdown;
|
|
546
|
-
|
|
561
|
+
// XSPEC adopter-report Q5: `.includes()` matched the marker text
|
|
562
|
+
// anywhere in the file, including inside a sentence that merely mentions
|
|
563
|
+
// it — locateMarkerBlock requires a standalone, non-fenced line. An
|
|
564
|
+
// ambiguous file (two real marker lines) still "has UDS markers" for
|
|
565
|
+
// this boolean's purpose, so that is also true, not an error here.
|
|
566
|
+
try {
|
|
567
|
+
return locateMarkerBlock(content, markers) !== null;
|
|
568
|
+
} catch (error) {
|
|
569
|
+
if (error instanceof AmbiguousMarkerError) {
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
throw error;
|
|
573
|
+
}
|
|
547
574
|
} catch {
|
|
548
575
|
return false;
|
|
549
576
|
}
|
|
@@ -15,8 +15,15 @@ import {
|
|
|
15
15
|
MANIFEST_OPTION_BINDINGS,
|
|
16
16
|
OPTIONS_INSTALL_DIR
|
|
17
17
|
} from '../core/constants.js';
|
|
18
|
+
import {
|
|
19
|
+
resolveIntegrationTargetFile,
|
|
20
|
+
buildToolIntegrationConfig,
|
|
21
|
+
generateIntegrationContent,
|
|
22
|
+
extractMarkedContent
|
|
23
|
+
} from '../utils/integration-generator.js';
|
|
18
24
|
import { PathResolver } from '../core/paths.js';
|
|
19
25
|
import { computeFileHash } from '../utils/hasher.js';
|
|
26
|
+
import { createHash } from 'crypto';
|
|
20
27
|
import {
|
|
21
28
|
getSkillsDirForAgent,
|
|
22
29
|
getCommandsDirForAgent,
|
|
@@ -261,6 +268,37 @@ function calculateExtensions(state, manifest) {
|
|
|
261
268
|
}
|
|
262
269
|
}
|
|
263
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Compute the block hash generation would produce for this tool right now,
|
|
273
|
+
* using the same builder (`buildToolIntegrationConfig`) and generator
|
|
274
|
+
* (`generateIntegrationContent`) `--apply`/`uds update` themselves use.
|
|
275
|
+
*
|
|
276
|
+
* Hashed the same way `computeIntegrationBlockHash` (hasher.js) hashes an
|
|
277
|
+
* actual on-disk file — extract the marker block, trim, sha256 — so the two
|
|
278
|
+
* are directly comparable. Never throws: generation can fail for reasons
|
|
279
|
+
* outside this module's control (an incomplete/mocked registry, a tool this
|
|
280
|
+
* process cannot resolve, etc.), and the correct response to "cannot compute
|
|
281
|
+
* an expected hash" is `null` — diffIntegrations already knows how to treat
|
|
282
|
+
* that the same as before this fix (always migrate_block), not to crash the
|
|
283
|
+
* whole state calculation over one tool.
|
|
284
|
+
*
|
|
285
|
+
* @param {Object} manifest
|
|
286
|
+
* @param {string} toolName
|
|
287
|
+
* @param {string} format
|
|
288
|
+
* @returns {string|null}
|
|
289
|
+
*/
|
|
290
|
+
function computeExpectedIntegrationBlockHash(manifest, toolName, format) {
|
|
291
|
+
try {
|
|
292
|
+
const config = { ...buildToolIntegrationConfig(manifest, toolName), format: manifest.format || 'ai' };
|
|
293
|
+
const generated = generateIntegrationContent({ ...config, tool: toolName });
|
|
294
|
+
const { content: blockContent } = extractMarkedContent(generated, format);
|
|
295
|
+
if (!blockContent) return null;
|
|
296
|
+
return `sha256:${createHash('sha256').update(blockContent).digest('hex')}`;
|
|
297
|
+
} catch {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
264
302
|
/**
|
|
265
303
|
* Calculate expected integration files.
|
|
266
304
|
* For integrations we track the UDS marker block, not the entire file.
|
|
@@ -281,11 +319,22 @@ function calculateIntegrations(state, manifest) {
|
|
|
281
319
|
const toolConfig = toolName ? SUPPORTED_AI_TOOLS[toolName] : null;
|
|
282
320
|
if (!toolConfig) continue;
|
|
283
321
|
|
|
284
|
-
|
|
322
|
+
// XSPEC-418 R2/R3: the desired path is the tool's ACTUAL target, not always
|
|
323
|
+
// its default file — a local-target claude-code install's desired state
|
|
324
|
+
// must say CLAUDE.local.md, or `--apply`/`--plan` (and their orphan
|
|
325
|
+
// detection) would treat that file as unmanaged and CLAUDE.md as desired.
|
|
326
|
+
const relativePath = resolveIntegrationTargetFile(toolName, manifest) || toolConfig.file;
|
|
285
327
|
|
|
286
328
|
state.integrations.set(relativePath, {
|
|
287
329
|
relativePath,
|
|
288
|
-
|
|
330
|
+
// XSPEC adopter-report Q6: this used to be a hardcoded `null` ("hashes
|
|
331
|
+
// are computed after generation"), which is why diffIntegrations could
|
|
332
|
+
// never tell "content already matches" from "content differs" and
|
|
333
|
+
// unconditionally produced migrate_block — `--plan` never converged,
|
|
334
|
+
// even immediately after `--apply`. Actually generating the content
|
|
335
|
+
// ahead of time (the same generation `--apply` itself runs) gives a
|
|
336
|
+
// real hash to compare against the actual file's block hash.
|
|
337
|
+
hash: computeExpectedIntegrationBlockHash(manifest, toolName, toolConfig.format),
|
|
289
338
|
size: null,
|
|
290
339
|
category: 'integration',
|
|
291
340
|
sourcePath: null, // Generated, not copied from source
|