universal-dev-standards 6.10.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/locales/zh-CN/CHANGELOG.md +23 -3
- package/bundled/locales/zh-CN/README.md +1 -1
- package/bundled/locales/zh-CN/SECURITY.md +1 -1
- 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-TW/CHANGELOG.md +23 -3
- package/bundled/locales/zh-TW/README.md +1 -1
- package/bundled/locales/zh-TW/SECURITY.md +1 -1
- 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/integrations/claude-code/README.md +14 -5
- package/package.json +1 -1
- package/src/commands/check.js +253 -21
- package/src/commands/config.js +15 -9
- package/src/commands/init.js +24 -3
- package/src/commands/update.js +311 -47
- 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 +3 -3
- package/src/installers/integration-installer.js +13 -6
- package/src/installers/manifest-installer.js +4 -0
- package/src/reconciler/actual-state-scanner.js +29 -2
- package/src/reconciler/desired-state-calculator.js +51 -2
- package/src/reconciler/diff-engine.js +19 -3
- package/src/reconciler/plan-executor.js +17 -16
- package/src/utils/hasher.js +61 -5
- package/src/utils/integration-generator.js +239 -28
- package/src/utils/marker-locator.js +140 -0
- package/src/utils/reference-sync.js +53 -1
- package/standards-registry.json +7 -7
package/src/commands/check.js
CHANGED
|
@@ -12,7 +12,9 @@ import {
|
|
|
12
12
|
computeFileHash,
|
|
13
13
|
compareFileHash,
|
|
14
14
|
hasFileHashes,
|
|
15
|
-
compareIntegrationBlockHash
|
|
15
|
+
compareIntegrationBlockHash,
|
|
16
|
+
computeIntegrationBlockHash,
|
|
17
|
+
pruneIntegrationFileHashes
|
|
16
18
|
} from '../utils/hasher.js';
|
|
17
19
|
import { downloadFromGitHub, getMarketplaceSkillsInfo } from '../utils/github.js';
|
|
18
20
|
import {
|
|
@@ -30,7 +32,9 @@ import {
|
|
|
30
32
|
findBrokenPathMentions,
|
|
31
33
|
compareStandardsWithReferences
|
|
32
34
|
} from '../utils/reference-sync.js';
|
|
33
|
-
import { extractMarkedContent,
|
|
35
|
+
import { extractMarkedContent, resolveIntegrationTargetFile, parseStandardsIndexCount, writeIntegrationFile } from '../utils/integration-generator.js';
|
|
36
|
+
import { AmbiguousMarkerError } from '../utils/marker-locator.js';
|
|
37
|
+
import { bumpManifestVersion } from '../core/manifest.js';
|
|
34
38
|
import { INTEGRATION_MAPPINGS } from '../installers/integration-installer.js';
|
|
35
39
|
import { getToolFormat } from '../core/constants.js';
|
|
36
40
|
import { checkForUpdates } from '../utils/npm-registry.js';
|
|
@@ -137,6 +141,19 @@ function performFileIntegrityCheck(projectPath, manifest, msg) {
|
|
|
137
141
|
if (hasFileHashes(manifest)) {
|
|
138
142
|
// Hash-based integrity check
|
|
139
143
|
for (const [relativePath, hashInfo] of Object.entries(manifest.fileHashes)) {
|
|
144
|
+
// XSPEC-418 R6 gap 2: a manifest written before this fix can still
|
|
145
|
+
// carry a whole-file entry for a path that is ALSO tracked by its UDS
|
|
146
|
+
// block (integrationBlockHashes) — the pruning added for R6 only runs
|
|
147
|
+
// on a write (uds update / check --restore / --migrate), so a project
|
|
148
|
+
// that has not run one of those yet stayed red on `uds check --ci`
|
|
149
|
+
// forever, for content outside the block that the block check itself
|
|
150
|
+
// says is fine. This is a READ-time skip — it does not touch the
|
|
151
|
+
// manifest, so a plain `uds check` alone cannot fix the underlying
|
|
152
|
+
// stale entry; the write paths still do that (see
|
|
153
|
+
// pruneIntegrationFileHashes in hasher.js).
|
|
154
|
+
if (manifest.integrationBlockHashes && relativePath in manifest.integrationBlockHashes) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
140
157
|
const fullPath = join(projectPath, relativePath);
|
|
141
158
|
const status = compareFileHash(fullPath, hashInfo);
|
|
142
159
|
|
|
@@ -401,12 +418,38 @@ export async function checkCommand(options = {}) {
|
|
|
401
418
|
// Check Commands integrity if commandHashes exist
|
|
402
419
|
checkCommandsIntegrity(manifest, projectPath, msg);
|
|
403
420
|
|
|
421
|
+
// XSPEC adopter-report Q3: neither of the two checks above (content-hash
|
|
422
|
+
// integrity) says anything about an installed Skills/Commands version
|
|
423
|
+
// being behind the latest UDS release — that was only ever computed by
|
|
424
|
+
// `uds update --plan --skills`/`--commands`. A plain `uds check` gave no
|
|
425
|
+
// signal at all that `uds update --skills` had anything to do.
|
|
426
|
+
checkSkillsCommandsVersionStaleness(manifest, projectPath, msg);
|
|
427
|
+
|
|
404
428
|
// Check Integration blocks integrity if integrationBlockHashes exist
|
|
405
|
-
|
|
429
|
+
// XSPEC-418 R1: the return value used to be discarded, so a removed/modified
|
|
430
|
+
// UDS block never affected the final verdict below — `uds check --ci` printed
|
|
431
|
+
// "compliant" and exited 0 with a visible ✗ still on screen.
|
|
432
|
+
const integrationBlockStatus = checkIntegrationBlocksIntegrity(manifest, projectPath, msg);
|
|
406
433
|
|
|
407
434
|
// Handle --restore option
|
|
408
435
|
if (options.restore) {
|
|
409
|
-
|
|
436
|
+
// XSPEC-418 R6 gap 1: integration files no longer live in `fileHashes`
|
|
437
|
+
// at all (that was the point of R6), so `fileStatus` — built entirely
|
|
438
|
+
// from `fileHashes` — never contains them any more. Without this,
|
|
439
|
+
// `--restore` silently did nothing for a damaged UDS block: it
|
|
440
|
+
// regenerated zero files and printed "Restored 0 file(s)" while the
|
|
441
|
+
// block-modified/markers-removed content sat untouched.
|
|
442
|
+
// `restoreSingleFile`'s integration-file branch (`manifest.integrationConfigs`)
|
|
443
|
+
// does not depend on fileHashes, so feeding these paths in is enough —
|
|
444
|
+
// it already regenerates just the block and leaves everything else alone.
|
|
445
|
+
const integrationFilesToRestore = [
|
|
446
|
+
...integrationBlockStatus.modified,
|
|
447
|
+
...integrationBlockStatus.noMarkers,
|
|
448
|
+
...integrationBlockStatus.missing
|
|
449
|
+
];
|
|
450
|
+
await restoreFiles(projectPath, manifest, [
|
|
451
|
+
...new Set([...fileStatus.modified, ...fileStatus.missing, ...integrationFilesToRestore])
|
|
452
|
+
]);
|
|
410
453
|
return;
|
|
411
454
|
}
|
|
412
455
|
|
|
@@ -470,8 +513,15 @@ export async function checkCommand(options = {}) {
|
|
|
470
513
|
displayWorkflowStatus(projectPath);
|
|
471
514
|
|
|
472
515
|
// Final status
|
|
516
|
+
// XSPEC-418 R1: integration block problems (UDS markers removed, block
|
|
517
|
+
// modified, or the tracked file missing) now feed the verdict — they used to
|
|
518
|
+
// be checked and printed above, then silently dropped here.
|
|
473
519
|
const allGood = fileStatus.missing.length === 0 &&
|
|
474
|
-
fileStatus.modified.length === 0
|
|
520
|
+
fileStatus.modified.length === 0 &&
|
|
521
|
+
integrationBlockStatus.modified.length === 0 &&
|
|
522
|
+
integrationBlockStatus.missing.length === 0 &&
|
|
523
|
+
integrationBlockStatus.noMarkers.length === 0 &&
|
|
524
|
+
integrationBlockStatus.ambiguous.length === 0;
|
|
475
525
|
if (allGood) {
|
|
476
526
|
console.log(chalk.green(msg.projectCompliant));
|
|
477
527
|
} else {
|
|
@@ -584,6 +634,11 @@ async function interactiveMode(projectPath, manifest, fileStatus, msg) {
|
|
|
584
634
|
}
|
|
585
635
|
|
|
586
636
|
if (manifestUpdated) {
|
|
637
|
+
// XSPEC-418 R6: interactive mode still has a "keep current content" branch
|
|
638
|
+
// that calls the whole-file updateFileHash(); if issue.file happens to be
|
|
639
|
+
// an integration file left in fileHashes by an older CLI, this is the
|
|
640
|
+
// write point that stops it from surviving another round.
|
|
641
|
+
pruneIntegrationFileHashes(manifest);
|
|
587
642
|
writeManifest(manifest, projectPath);
|
|
588
643
|
console.log(chalk.green(msg.manifestUpdated));
|
|
589
644
|
console.log();
|
|
@@ -701,6 +756,9 @@ async function restoreFiles(projectPath, manifest, files) {
|
|
|
701
756
|
}
|
|
702
757
|
|
|
703
758
|
// Update manifest
|
|
759
|
+
// XSPEC-418 R6: catches any stale whole-file entry for an integration file
|
|
760
|
+
// this particular restore run didn't touch, not just the ones it did.
|
|
761
|
+
pruneIntegrationFileHashes(manifest);
|
|
704
762
|
writeManifest(manifest, projectPath);
|
|
705
763
|
console.log(chalk.gray(` ${msg.manifestUpdatedShort}`));
|
|
706
764
|
console.log();
|
|
@@ -728,7 +786,13 @@ export async function restoreSingleFile(projectPath, manifest, relativePath, msg
|
|
|
728
786
|
installedStandards: genConfig.installedStandards || manifest.standards || []
|
|
729
787
|
}, projectPath);
|
|
730
788
|
if (result.success) {
|
|
731
|
-
|
|
789
|
+
// XSPEC-418 R6: this rewrites the UDS block, so the BLOCK hash is what
|
|
790
|
+
// must be refreshed — not a whole-file hash. Restoring used to call
|
|
791
|
+
// updateFileHash() here, which recorded the whole file (block +
|
|
792
|
+
// whatever the adopter wrote outside it) while leaving the stale block
|
|
793
|
+
// hash untouched; the next `uds check` then reported "UDS block
|
|
794
|
+
// modified" for a block that had just been correctly restored.
|
|
795
|
+
updateIntegrationBlockHash(manifest, relativePath, result.blockHashInfo);
|
|
732
796
|
console.log(chalk.green(` ✓ ${relativePath}: ${msg.restored}`));
|
|
733
797
|
return true;
|
|
734
798
|
}
|
|
@@ -750,7 +814,32 @@ export async function restoreSingleFile(projectPath, manifest, relativePath, msg
|
|
|
750
814
|
// Integration file - copy to root
|
|
751
815
|
const result = await copyIntegration(sourcePath, relativePath, projectPath);
|
|
752
816
|
if (result.success) {
|
|
753
|
-
|
|
817
|
+
// XSPEC-418 R6: same rule as the genConfig branch above — track the
|
|
818
|
+
// block, not the whole file. This legacy static-copy fallback has no
|
|
819
|
+
// blockHashInfo returned to it, so compute one from what was just
|
|
820
|
+
// written; a template with no UDS markers at all (computeIntegration
|
|
821
|
+
// BlockHash returns null) falls back to the old whole-file behaviour,
|
|
822
|
+
// since there is no block to track instead.
|
|
823
|
+
// XSPEC adopter-report Q5: the file was just overwritten wholesale by
|
|
824
|
+
// copyIntegration (a full template copy, not a marker-preserving
|
|
825
|
+
// merge), so an ambiguous marker pair here cannot be this restore's
|
|
826
|
+
// own doing — fall back to whole-file hashing rather than fail a
|
|
827
|
+
// restore that already succeeded on disk.
|
|
828
|
+
let blockHashInfo;
|
|
829
|
+
try {
|
|
830
|
+
blockHashInfo = computeIntegrationBlockHash(join(projectPath, relativePath));
|
|
831
|
+
} catch (error) {
|
|
832
|
+
if (error instanceof AmbiguousMarkerError) {
|
|
833
|
+
blockHashInfo = null;
|
|
834
|
+
} else {
|
|
835
|
+
throw error;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
if (blockHashInfo) {
|
|
839
|
+
updateIntegrationBlockHash(manifest, relativePath, blockHashInfo);
|
|
840
|
+
} else {
|
|
841
|
+
updateFileHash(projectPath, manifest, relativePath);
|
|
842
|
+
}
|
|
754
843
|
console.log(chalk.green(` ✓ ${relativePath}: ${msg.restored}`));
|
|
755
844
|
return true;
|
|
756
845
|
} else {
|
|
@@ -770,6 +859,30 @@ export async function restoreSingleFile(projectPath, manifest, relativePath, msg
|
|
|
770
859
|
}
|
|
771
860
|
}
|
|
772
861
|
|
|
862
|
+
/**
|
|
863
|
+
* Update an integration file's UDS BLOCK hash in the manifest — not its
|
|
864
|
+
* whole-file hash (XSPEC-418 R6). Also drops any whole-file `fileHashes`
|
|
865
|
+
* entry for the same path, so restoring an integration file can never leave
|
|
866
|
+
* both records behind at once.
|
|
867
|
+
*
|
|
868
|
+
* @param {Object} manifest - Manifest object (mutated in place)
|
|
869
|
+
* @param {string} relativePath - Path as UDS reports it
|
|
870
|
+
* @param {Object|null|undefined} blockHashInfo - `{ blockHash, blockSize, fullHash, fullSize }`,
|
|
871
|
+
* e.g. from `writeIntegrationFile`'s result or `computeIntegrationBlockHash`
|
|
872
|
+
* @returns {boolean} Whether a block hash was recorded
|
|
873
|
+
*/
|
|
874
|
+
export function updateIntegrationBlockHash(manifest, relativePath, blockHashInfo) {
|
|
875
|
+
if (!blockHashInfo) return false;
|
|
876
|
+
const normalizedPath = relativePath.replace(/\\/g, '/');
|
|
877
|
+
if (!manifest.integrationBlockHashes) manifest.integrationBlockHashes = {};
|
|
878
|
+
manifest.integrationBlockHashes[normalizedPath] = {
|
|
879
|
+
...blockHashInfo,
|
|
880
|
+
installedAt: new Date().toISOString()
|
|
881
|
+
};
|
|
882
|
+
if (manifest.fileHashes) delete manifest.fileHashes[normalizedPath];
|
|
883
|
+
return true;
|
|
884
|
+
}
|
|
885
|
+
|
|
773
886
|
/**
|
|
774
887
|
* Update file hash in manifest
|
|
775
888
|
*/
|
|
@@ -918,21 +1031,54 @@ async function migrateToHashBasedTracking(projectPath, manifest) {
|
|
|
918
1031
|
}
|
|
919
1032
|
}
|
|
920
1033
|
|
|
921
|
-
// Process integrations
|
|
1034
|
+
// Process integrations — tracked by their UDS BLOCK hash, not a whole-file
|
|
1035
|
+
// hash (XSPEC-418 R6). This loop used to write the whole file into the same
|
|
1036
|
+
// `fileHashes` map as standards/extensions above, which is the same defect
|
|
1037
|
+
// as every other write site R6 fixes, just reached by `--migrate` instead
|
|
1038
|
+
// of a normal update: any content the adopter had outside the block then
|
|
1039
|
+
// read as "modified" by standards-file integrity, contradicting the block
|
|
1040
|
+
// check in the same `uds check` run.
|
|
1041
|
+
const integrationBlockHashes = { ...(manifest.integrationBlockHashes || {}) };
|
|
922
1042
|
for (const intEntry of manifest.integrations) {
|
|
923
1043
|
const int = resolveIntegrationFile(intEntry) || intEntry;
|
|
924
1044
|
const fullPath = join(projectPath, int);
|
|
925
1045
|
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1046
|
+
// XSPEC adopter-report Q5: this is a read-only manifest migration over
|
|
1047
|
+
// every tracked integration file at once — one file with an ambiguous
|
|
1048
|
+
// marker pair must not abort migrating the rest. It is simply left
|
|
1049
|
+
// without a block hash, same as a file with no markers at all; `uds
|
|
1050
|
+
// check`'s own block-integrity pass (below) is what surfaces the
|
|
1051
|
+
// ambiguity to the user, with line numbers.
|
|
1052
|
+
let blockHashInfo;
|
|
1053
|
+
try {
|
|
1054
|
+
blockHashInfo = computeIntegrationBlockHash(fullPath);
|
|
1055
|
+
} catch (error) {
|
|
1056
|
+
if (error instanceof AmbiguousMarkerError) {
|
|
1057
|
+
blockHashInfo = null;
|
|
1058
|
+
} else {
|
|
1059
|
+
throw error;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
if (blockHashInfo) {
|
|
1063
|
+
integrationBlockHashes[int] = { ...blockHashInfo, installedAt: now };
|
|
929
1064
|
count++;
|
|
1065
|
+
} else {
|
|
1066
|
+
// No UDS markers found (plaintext template with none, or a file the
|
|
1067
|
+
// adopter fully rewrote) — fall back to whole-file tracking, same as
|
|
1068
|
+
// before this fix, rather than silently tracking nothing.
|
|
1069
|
+
const hashInfo = computeFileHash(fullPath);
|
|
1070
|
+
if (hashInfo) {
|
|
1071
|
+
fileHashes[int] = { ...hashInfo, installedAt: now };
|
|
1072
|
+
count++;
|
|
1073
|
+
}
|
|
930
1074
|
}
|
|
931
1075
|
}
|
|
932
1076
|
|
|
933
1077
|
// Update manifest
|
|
934
1078
|
manifest.fileHashes = fileHashes;
|
|
935
|
-
manifest.
|
|
1079
|
+
manifest.integrationBlockHashes = integrationBlockHashes;
|
|
1080
|
+
bumpManifestVersion(manifest);
|
|
1081
|
+
pruneIntegrationFileHashes(manifest);
|
|
936
1082
|
writeManifest(manifest, projectPath);
|
|
937
1083
|
|
|
938
1084
|
console.log(chalk.green(msg.migratedCount.replace('{count}', count)));
|
|
@@ -1220,7 +1366,9 @@ function checkIntegrationFiles(manifest, projectPath, msg) {
|
|
|
1220
1366
|
// verdict — the tools that share it are named on the line instead.
|
|
1221
1367
|
const toolsByFile = new Map();
|
|
1222
1368
|
for (const tool of manifest.aiTools) {
|
|
1223
|
-
|
|
1369
|
+
// XSPEC-418 R3: check must inspect the actual target file (e.g. CLAUDE.local.md),
|
|
1370
|
+
// not the tool's hardcoded default.
|
|
1371
|
+
const file = resolveIntegrationTargetFile(tool, manifest);
|
|
1224
1372
|
if (!file) continue;
|
|
1225
1373
|
if (!toolsByFile.has(file)) toolsByFile.set(file, []);
|
|
1226
1374
|
toolsByFile.get(file).push(tool);
|
|
@@ -1251,7 +1399,20 @@ function checkIntegrationFiles(manifest, projectPath, msg) {
|
|
|
1251
1399
|
|
|
1252
1400
|
// Check for standards index marker
|
|
1253
1401
|
const format = getToolFormat(tool);
|
|
1254
|
-
|
|
1402
|
+
let markedContent;
|
|
1403
|
+
try {
|
|
1404
|
+
({ content: markedContent } = extractMarkedContent(content, format));
|
|
1405
|
+
} catch (error) {
|
|
1406
|
+
if (error instanceof AmbiguousMarkerError) {
|
|
1407
|
+
// XSPEC adopter-report Q5: report explicitly with line numbers
|
|
1408
|
+
// instead of guessing which marker pair is real, or silently
|
|
1409
|
+
// treating the file as if it had no UDS block at all.
|
|
1410
|
+
console.log(chalk.red(` ✗ ${toolFile}: ${error.message}`));
|
|
1411
|
+
hasIssues = true;
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
throw error;
|
|
1415
|
+
}
|
|
1255
1416
|
const hasStandardsIndex = markedContent.length > 0 ||
|
|
1256
1417
|
content.includes('Standards Index') ||
|
|
1257
1418
|
content.includes('Standards Compliance');
|
|
@@ -1607,6 +1768,59 @@ async function checkCliVersion(bundledVersion) {
|
|
|
1607
1768
|
// Enhanced Integrity Check Functions (v3.3.0+)
|
|
1608
1769
|
// ============================================================
|
|
1609
1770
|
|
|
1771
|
+
/**
|
|
1772
|
+
* Warn when an installed Skills or Commands version is behind the latest
|
|
1773
|
+
* UDS release. This is deliberately modeled on the existing top-level
|
|
1774
|
+
* "Version: X → Y ⚠" row (see the `hasUpdate` check in the summary
|
|
1775
|
+
* dashboard below) rather than on `checkIntegrationBlocksIntegrity`
|
|
1776
|
+
* (XSPEC-418 R1): a version simply being behind the latest release is
|
|
1777
|
+
* ambient, expected state until the adopter chooses to update — not a
|
|
1778
|
+
* compliance defect the way a modified/missing/ambiguous UDS block is — so
|
|
1779
|
+
* it is reported but, unlike a block problem, does not fail `--ci`.
|
|
1780
|
+
*
|
|
1781
|
+
* Skills staleness is read per-installation from what is actually on disk
|
|
1782
|
+
* (`getInstalledSkillsInfoForAgent`), the same source `uds update --plan
|
|
1783
|
+
* --skills` uses, because different agents/levels can be out of sync
|
|
1784
|
+
* independently. Commands have no per-installation version on disk (only a
|
|
1785
|
+
* file count), so Commands staleness is read from the one version the
|
|
1786
|
+
* manifest itself records (`manifest.commands.version`).
|
|
1787
|
+
* // implements XSPEC adopter-report Q3
|
|
1788
|
+
*
|
|
1789
|
+
* @param {Object} manifest
|
|
1790
|
+
* @param {string} projectPath
|
|
1791
|
+
* @param {Object} msg - Localized messages (unused today; kept for symmetry
|
|
1792
|
+
* with the other Enhanced Integrity Check functions, which all take one)
|
|
1793
|
+
*/
|
|
1794
|
+
function checkSkillsCommandsVersionStaleness(manifest, projectPath, msg) { // eslint-disable-line no-unused-vars
|
|
1795
|
+
const repoInfo = getRepositoryInfo();
|
|
1796
|
+
const latestVersion = repoInfo.skills.version;
|
|
1797
|
+
const stale = [];
|
|
1798
|
+
|
|
1799
|
+
const skillsInstallations = (manifest.skills?.installations || []).filter((i) => i.level !== 'marketplace');
|
|
1800
|
+
for (const inst of skillsInstallations) {
|
|
1801
|
+
const info = getInstalledSkillsInfoForAgent(inst.agent, inst.level, projectPath);
|
|
1802
|
+
const current = info?.version;
|
|
1803
|
+
if (current && current !== latestVersion) {
|
|
1804
|
+
stale.push(`${getAgentDisplayName(inst.agent)} (${inst.level}): Skills v${current} → v${latestVersion}`);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
if (manifest.commands?.installed && (manifest.commands?.installations || []).length > 0) {
|
|
1809
|
+
const current = manifest.commands.version;
|
|
1810
|
+
if (current && current !== latestVersion) {
|
|
1811
|
+
stale.push(`Commands: v${current} → v${latestVersion}`);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
if (stale.length === 0) return;
|
|
1816
|
+
|
|
1817
|
+
for (const line of stale) {
|
|
1818
|
+
console.log(chalk.yellow(` ⚠ ${line}`));
|
|
1819
|
+
}
|
|
1820
|
+
console.log(chalk.gray(' Run `uds update --plan --skills` / `--commands` for details, then `--apply` to update.'));
|
|
1821
|
+
console.log();
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1610
1824
|
/**
|
|
1611
1825
|
* Check Skills files integrity against stored hashes
|
|
1612
1826
|
* @param {Object} manifest - Manifest object
|
|
@@ -1789,19 +2003,19 @@ export function checkCommandsIntegrity(manifest, projectPath, msg) {
|
|
|
1789
2003
|
* @param {Object} manifest - Manifest object
|
|
1790
2004
|
* @param {string} projectPath - Project root path
|
|
1791
2005
|
* @param {Object} msg - Localized messages
|
|
1792
|
-
* @returns {Object} Status { unchanged: [], modified: [], missing: [], noMarkers: [] }
|
|
2006
|
+
* @returns {Object} Status { unchanged: [], modified: [], missing: [], noMarkers: [], ambiguous: [] }
|
|
1793
2007
|
*/
|
|
1794
2008
|
function checkIntegrationBlocksIntegrity(manifest, projectPath, msg) {
|
|
1795
2009
|
const blockHashes = manifest.integrationBlockHashes;
|
|
1796
2010
|
|
|
1797
2011
|
// Skip if no block hashes tracked
|
|
1798
2012
|
if (!blockHashes || Object.keys(blockHashes).length === 0) {
|
|
1799
|
-
return { unchanged: [], modified: [], missing: [], noMarkers: [], tracked: false };
|
|
2013
|
+
return { unchanged: [], modified: [], missing: [], noMarkers: [], ambiguous: [], tracked: false };
|
|
1800
2014
|
}
|
|
1801
2015
|
|
|
1802
2016
|
console.log(chalk.cyan(msg.integrationBlocksCheck || 'Integration UDS Block Integrity'));
|
|
1803
2017
|
|
|
1804
|
-
const status = { unchanged: [], modified: [], missing: [], noMarkers: [], tracked: true };
|
|
2018
|
+
const status = { unchanged: [], modified: [], missing: [], noMarkers: [], ambiguous: [], tracked: true };
|
|
1805
2019
|
|
|
1806
2020
|
for (const [filePath, hashInfo] of Object.entries(blockHashes)) {
|
|
1807
2021
|
const fullPath = join(projectPath, filePath);
|
|
@@ -1813,7 +2027,21 @@ function checkIntegrationBlocksIntegrity(manifest, projectPath, msg) {
|
|
|
1813
2027
|
}
|
|
1814
2028
|
|
|
1815
2029
|
// Compare block hash
|
|
1816
|
-
|
|
2030
|
+
// XSPEC adopter-report Q5: a file with two real START or END marker
|
|
2031
|
+
// lines is not "modified" and not "no markers" — it is ambiguous, and
|
|
2032
|
+
// the whole point of this fix is to report that explicitly (with line
|
|
2033
|
+
// numbers) instead of silently picking one and guessing.
|
|
2034
|
+
let blockStatus;
|
|
2035
|
+
try {
|
|
2036
|
+
blockStatus = compareIntegrationBlockHash(fullPath, hashInfo);
|
|
2037
|
+
} catch (error) {
|
|
2038
|
+
if (error instanceof AmbiguousMarkerError) {
|
|
2039
|
+
status.ambiguous.push(filePath);
|
|
2040
|
+
console.log(chalk.red(` ✗ ${filePath}: ${error.message}`));
|
|
2041
|
+
continue;
|
|
2042
|
+
}
|
|
2043
|
+
throw error;
|
|
2044
|
+
}
|
|
1817
2045
|
|
|
1818
2046
|
switch (blockStatus) {
|
|
1819
2047
|
case 'unchanged':
|
|
@@ -1835,17 +2063,21 @@ function checkIntegrationBlocksIntegrity(manifest, projectPath, msg) {
|
|
|
1835
2063
|
}
|
|
1836
2064
|
|
|
1837
2065
|
// Summary
|
|
1838
|
-
if (status.modified.length === 0 && status.missing.length === 0 && status.noMarkers.length === 0) {
|
|
2066
|
+
if (status.modified.length === 0 && status.missing.length === 0 && status.noMarkers.length === 0 && status.ambiguous.length === 0) {
|
|
1839
2067
|
console.log(chalk.green(` ✓ ${msg.allBlocksIntact || 'All UDS blocks intact'} (${status.unchanged.length} files)`));
|
|
1840
2068
|
console.log(chalk.gray(` ${msg.userContentPreserved || 'User customizations outside UDS blocks are preserved'}`));
|
|
1841
2069
|
} else {
|
|
1842
2070
|
console.log(chalk.gray(` ${(msg.blocksIntegritySummary || '{unchanged} intact, {modified} modified, {missing} missing')
|
|
1843
2071
|
.replace('{unchanged}', status.unchanged.length)
|
|
1844
2072
|
.replace('{modified}', status.modified.length)
|
|
1845
|
-
.replace('{missing}', status.missing.length + status.noMarkers.length)}`));
|
|
2073
|
+
.replace('{missing}', status.missing.length + status.noMarkers.length + status.ambiguous.length)}`));
|
|
1846
2074
|
|
|
1847
2075
|
if (status.modified.length > 0 || status.noMarkers.length > 0) {
|
|
1848
|
-
|
|
2076
|
+
// XSPEC-418 R6 gap 1: `uds check --restore` now regenerates just the
|
|
2077
|
+
// UDS block for these files too (it used to do nothing for them — see
|
|
2078
|
+
// the --restore handler above), so it is named alongside `uds update
|
|
2079
|
+
// --integrations-only` rather than as a second-choice remedy.
|
|
2080
|
+
console.log(chalk.yellow(` ${msg.runUpdateIntegrations || 'Run "uds check --restore" or "uds update --integrations-only" to restore UDS content'}`));
|
|
1849
2081
|
}
|
|
1850
2082
|
}
|
|
1851
2083
|
|
package/src/commands/config.js
CHANGED
|
@@ -45,11 +45,11 @@ import {
|
|
|
45
45
|
import { displayLanguageToLocale } from '../utils/locale.js';
|
|
46
46
|
import {
|
|
47
47
|
writeIntegrationFile,
|
|
48
|
-
|
|
48
|
+
resolveIntegrationTargetFile
|
|
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
|
+
import { mergeInstalledNames, bumpManifestVersion } from '../core/manifest.js';
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
55
|
* Get localized message with fallback (for config-specific keys)
|
|
@@ -667,13 +667,16 @@ export async function runProjectConfiguration(options) {
|
|
|
667
667
|
// Remove integration files for removed tools
|
|
668
668
|
const spinner = createSpinner(msgObj.removingIntegrations).start();
|
|
669
669
|
for (const tool of result.tools) {
|
|
670
|
-
|
|
670
|
+
// XSPEC-418 R3: remove the file this tool actually targets (e.g.
|
|
671
|
+
// CLAUDE.local.md), not always its hardcoded default.
|
|
672
|
+
const removedToolFile = resolveIntegrationTargetFile(tool, manifest);
|
|
673
|
+
const filePath = join(projectPath, removedToolFile);
|
|
671
674
|
if (existsSync(filePath)) {
|
|
672
675
|
try {
|
|
673
676
|
unlinkSync(filePath);
|
|
674
|
-
console.log(chalk.gray(` ${msgObj.removed}: ${
|
|
677
|
+
console.log(chalk.gray(` ${msgObj.removed}: ${removedToolFile}`));
|
|
675
678
|
} catch {
|
|
676
|
-
console.log(chalk.yellow(` ${msgObj.couldNotRemove}: ${
|
|
679
|
+
console.log(chalk.yellow(` ${msgObj.couldNotRemove}: ${removedToolFile}`));
|
|
677
680
|
}
|
|
678
681
|
}
|
|
679
682
|
|
|
@@ -711,7 +714,7 @@ export async function runProjectConfiguration(options) {
|
|
|
711
714
|
}
|
|
712
715
|
|
|
713
716
|
// Remove from integrationBlockHashes (keyed by tool file path)
|
|
714
|
-
const toolFileName =
|
|
717
|
+
const toolFileName = removedToolFile;
|
|
715
718
|
if (manifest.integrationBlockHashes?.[toolFileName]) {
|
|
716
719
|
delete manifest.integrationBlockHashes[toolFileName];
|
|
717
720
|
}
|
|
@@ -934,7 +937,8 @@ export async function runProjectConfiguration(options) {
|
|
|
934
937
|
const generatedFiles = new Set();
|
|
935
938
|
|
|
936
939
|
for (const tool of newAITools) {
|
|
937
|
-
|
|
940
|
+
// XSPEC-418 R3
|
|
941
|
+
const targetFile = resolveIntegrationTargetFile(tool, manifest);
|
|
938
942
|
if (generatedFiles.has(targetFile)) {
|
|
939
943
|
continue; // Skip if already generated (AGENTS.md sharing)
|
|
940
944
|
}
|
|
@@ -947,7 +951,9 @@ export async function runProjectConfiguration(options) {
|
|
|
947
951
|
standardsFormat: manifest.format || 'ai',
|
|
948
952
|
contentMode: newContentMode,
|
|
949
953
|
// Pass output_language for dynamic commit standards generation
|
|
950
|
-
outputLanguage: newOptions.output_language || 'english'
|
|
954
|
+
outputLanguage: newOptions.output_language || 'english',
|
|
955
|
+
// XSPEC-418 R2/R3
|
|
956
|
+
integrationTargets: manifest.integrationTargets
|
|
951
957
|
};
|
|
952
958
|
|
|
953
959
|
const result = writeIntegrationFile(tool, toolConfig, projectPath);
|
|
@@ -1052,7 +1058,7 @@ export async function runProjectConfiguration(options) {
|
|
|
1052
1058
|
manifest.options = newOptions;
|
|
1053
1059
|
manifest.contentMode = newContentMode;
|
|
1054
1060
|
manifest.aiTools = newAITools;
|
|
1055
|
-
manifest
|
|
1061
|
+
bumpManifestVersion(manifest);
|
|
1056
1062
|
|
|
1057
1063
|
// Update methodology
|
|
1058
1064
|
if (newMethodology) {
|
package/src/commands/init.js
CHANGED
|
@@ -26,7 +26,7 @@ import { displayLanguageToLocale } from '../utils/locale.js';
|
|
|
26
26
|
import { generateReleaseConfig, RELEASE_MODE_LABELS } from '../utils/release-config.js';
|
|
27
27
|
import { guardAgainstSelfAdoption } from '../utils/detect-self-adoption.js';
|
|
28
28
|
import { readInstallYaml } from '../utils/config-manager.js';
|
|
29
|
-
import {
|
|
29
|
+
import { resolveIntegrationTargetFile } from '../utils/integration-generator.js';
|
|
30
30
|
import { withFileTransaction } from '../utils/transaction.js';
|
|
31
31
|
|
|
32
32
|
/**
|
|
@@ -107,7 +107,10 @@ export async function initCommand(options) {
|
|
|
107
107
|
// `uds init` re-installs them idempotently.
|
|
108
108
|
const ownedPaths = [join(projectPath, '.standards')];
|
|
109
109
|
for (const tool of (config.integrations || config.aiTools || [])) {
|
|
110
|
-
|
|
110
|
+
// XSPEC-418 R2: honors --claude-target so a local-target install's owned
|
|
111
|
+
// path is CLAUDE.local.md, not CLAUDE.md — rollback must tear down the
|
|
112
|
+
// file actually written, not the tool's default.
|
|
113
|
+
const file = resolveIntegrationTargetFile(tool, { integrationTargets: config.integrationTargets });
|
|
111
114
|
if (file) ownedPaths.push(join(projectPath, file));
|
|
112
115
|
}
|
|
113
116
|
if (config.generateAgentsMd) ownedPaths.push(join(projectPath, 'AGENTS.md'));
|
|
@@ -658,6 +661,23 @@ function buildNonInteractiveConfig(options, detected, projectPath) {
|
|
|
658
661
|
? false // codex/opencode handles AGENTS.md, skip universal output
|
|
659
662
|
: (options.agentsMd !== undefined ? !!options.agentsMd : true);
|
|
660
663
|
|
|
664
|
+
// XSPEC-418 R2: --claude-target <project|local>, default 'project'. An
|
|
665
|
+
// unrecognized value is never substituted quietly (same policy as
|
|
666
|
+
// --content-mode above) — it falls back to 'project' with a warning rather
|
|
667
|
+
// than silently writing into CLAUDE.local.md or vice versa.
|
|
668
|
+
let claudeTargetFlag = options.claudeTarget || 'project';
|
|
669
|
+
if (claudeTargetFlag !== 'project' && claudeTargetFlag !== 'local') {
|
|
670
|
+
console.log(chalk.yellow(
|
|
671
|
+
`⚠ Unknown --claude-target '${claudeTargetFlag}'; using 'project'. Supported: project, local.`
|
|
672
|
+
));
|
|
673
|
+
claudeTargetFlag = 'project';
|
|
674
|
+
}
|
|
675
|
+
// Only ever set when 'local' — 'project' or unspecified leaves the manifest
|
|
676
|
+
// shape completely unchanged (XSPEC-418 AC-5).
|
|
677
|
+
const integrationTargets = claudeTargetFlag === 'local'
|
|
678
|
+
? { 'claude-code': 'CLAUDE.local.md' }
|
|
679
|
+
: undefined;
|
|
680
|
+
|
|
661
681
|
return {
|
|
662
682
|
languages: options.lang ? [options.lang] : Object.keys(detected.languages).filter(k => detected.languages[k]),
|
|
663
683
|
frameworks: options.framework ? [options.framework] : Object.keys(detected.frameworks).filter(k => detected.frameworks[k]),
|
|
@@ -678,7 +698,8 @@ function buildNonInteractiveConfig(options, detected, projectPath) {
|
|
|
678
698
|
generateAgentsMd,
|
|
679
699
|
releaseMode: options.releaseMode || 'ci-cd',
|
|
680
700
|
withHooks: !!options.withHooks,
|
|
681
|
-
contentLayout: options.contentLayout || 'flat'
|
|
701
|
+
contentLayout: options.contentLayout || 'flat',
|
|
702
|
+
integrationTargets
|
|
682
703
|
};
|
|
683
704
|
}
|
|
684
705
|
|