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/commands/update.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import
|
|
2
|
+
import { createSpinner } from '../utils/spinner.js';
|
|
3
3
|
import { select, confirm as inquirerConfirm, checkbox, Separator } from '@inquirer/prompts';
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
|
|
6
6
|
import { join, basename, dirname, relative } from 'path';
|
|
7
7
|
import { readManifest, writeManifest, copyStandard, isInitialized, getRepoRoot } from '../utils/copier.js';
|
|
8
|
-
import { getRepositoryInfo, getAllStandards, getStandardSource } from '../utils/registry.js';
|
|
9
|
-
import { computeFileHash, planStandardsRemovals, refreshIntegrationBlockHashes } from '../utils/hasher.js';
|
|
8
|
+
import { getRepositoryInfo, getAllStandards, getShippableFilenames, getStandardSource } from '../utils/registry.js';
|
|
9
|
+
import { computeFileHash, planStandardsRemovals, refreshIntegrationBlockHashes, pruneIntegrationFileHashes } from '../utils/hasher.js';
|
|
10
|
+
import { AmbiguousMarkerError } from '../utils/marker-locator.js';
|
|
10
11
|
import {
|
|
11
12
|
writeIntegrationFile,
|
|
12
|
-
|
|
13
|
+
resolveIntegrationTargetFile,
|
|
13
14
|
writeAgentsMdSummary,
|
|
14
15
|
resolveContentModeForTool,
|
|
15
16
|
generateIntegrationContent,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
} from '../utils/integration-generator.js';
|
|
19
20
|
import {
|
|
20
21
|
calculateCategoriesFromStandards,
|
|
22
|
+
repairIntegrationConfigCategories,
|
|
21
23
|
arraysEqual,
|
|
22
24
|
getToolFromPath
|
|
23
25
|
} from '../utils/reference-sync.js';
|
|
@@ -56,13 +58,14 @@ import {
|
|
|
56
58
|
} from '../reconciler/index.js';
|
|
57
59
|
import { restoreSingleFile } from './check.js';
|
|
58
60
|
import { guardAgainstSelfAdoption } from '../utils/detect-self-adoption.js';
|
|
59
|
-
import { resolveIntegrationFile } from '../core/constants.js';
|
|
61
|
+
import { resolveIntegrationFile, SUPPORTED_AI_TOOLS, getToolFormat } from '../core/constants.js';
|
|
60
62
|
import {
|
|
61
63
|
mergeInstalledNames,
|
|
62
64
|
recordFileProvenance,
|
|
63
65
|
forgetFileProvenance,
|
|
64
66
|
establishProvenance,
|
|
65
|
-
isProvenanceEstablished
|
|
67
|
+
isProvenanceEstablished,
|
|
68
|
+
bumpManifestVersion
|
|
66
69
|
} from '../core/manifest.js';
|
|
67
70
|
|
|
68
71
|
/**
|
|
@@ -274,7 +277,7 @@ function buildInstallCommand(pm, tag) {
|
|
|
274
277
|
*/
|
|
275
278
|
async function updateCliAndExit(useBeta = false) {
|
|
276
279
|
const msg = t().commands.update;
|
|
277
|
-
const spinner =
|
|
280
|
+
const spinner = createSpinner(msg.updatingCli).start();
|
|
278
281
|
|
|
279
282
|
try {
|
|
280
283
|
// Command is hardcoded - no user input, safe from injection
|
|
@@ -438,6 +441,15 @@ export async function updateCommand(options) {
|
|
|
438
441
|
console.log(chalk.bold(msg.title));
|
|
439
442
|
console.log(chalk.gray('─'.repeat(50)));
|
|
440
443
|
|
|
444
|
+
// Handle --claude-target option (XSPEC-418 R4): switch an EXISTING
|
|
445
|
+
// installation's claude-code integration target between CLAUDE.md and
|
|
446
|
+
// CLAUDE.local.md, without a full reinstall. Standalone, like --sync-refs
|
|
447
|
+
// below — it does not compose with other update modes/scopes.
|
|
448
|
+
if (options.claudeTarget) {
|
|
449
|
+
await switchClaudeTarget(projectPath, manifest, options.claudeTarget, options);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
441
453
|
// Handle --sync-refs option.
|
|
442
454
|
// `--plan` is honoured here too. This branch is above the mode dispatch
|
|
443
455
|
// because sync-refs is its own operation rather than a scope of the
|
|
@@ -489,7 +501,7 @@ export async function updateCommand(options) {
|
|
|
489
501
|
// Handle --plan option (DSR dry-run). Nothing below this line writes.
|
|
490
502
|
if (options.plan) {
|
|
491
503
|
if (!scopedToSkills && !scopedToCommands) {
|
|
492
|
-
await handlePlan(projectPath, options);
|
|
504
|
+
await handlePlan(projectPath, options, manifest);
|
|
493
505
|
}
|
|
494
506
|
if (scopedToSkills) await planSkills(projectPath, manifest, options);
|
|
495
507
|
if (scopedToCommands) await planCommands(projectPath, manifest, options);
|
|
@@ -581,6 +593,23 @@ export async function updateCommand(options) {
|
|
|
581
593
|
if (versionComparison > 0) {
|
|
582
594
|
console.log(chalk.gray(` ${msg.newerVersion.replace('{version}', currentVersion)}`));
|
|
583
595
|
}
|
|
596
|
+
|
|
597
|
+
// Still forget records of files that are gone and no longer shipped. An
|
|
598
|
+
// adopter who followed MIGRATION-v6 §2 is on the current version by
|
|
599
|
+
// definition — telling them to run `uds update --prune` and then returning
|
|
600
|
+
// before anything is examined is how the complaint became unclearable.
|
|
601
|
+
const retiredOnLatest = pruneRetiredHashes(manifest, projectPath);
|
|
602
|
+
if (retiredOnLatest.length > 0) {
|
|
603
|
+
console.log();
|
|
604
|
+
console.log(chalk.gray(
|
|
605
|
+
` ${(msg.droppedRetiredHashes || 'Dropped {count} manifest record(s) for files UDS no longer ships and that are already gone:').replace('{count}', retiredOnLatest.length)}`
|
|
606
|
+
));
|
|
607
|
+
for (const path of retiredOnLatest) {
|
|
608
|
+
console.log(chalk.gray(` - ${path}`));
|
|
609
|
+
}
|
|
610
|
+
writeManifest(manifest, projectPath);
|
|
611
|
+
}
|
|
612
|
+
|
|
584
613
|
console.log();
|
|
585
614
|
return;
|
|
586
615
|
}
|
|
@@ -642,7 +671,7 @@ export async function updateCommand(options) {
|
|
|
642
671
|
|
|
643
672
|
// Perform update
|
|
644
673
|
console.log();
|
|
645
|
-
const spinner =
|
|
674
|
+
const spinner = createSpinner(msg.updatingStandards).start();
|
|
646
675
|
|
|
647
676
|
const results = {
|
|
648
677
|
updated: [],
|
|
@@ -714,7 +743,7 @@ export async function updateCommand(options) {
|
|
|
714
743
|
}
|
|
715
744
|
|
|
716
745
|
if (shouldInstallNew) {
|
|
717
|
-
const newSpinner =
|
|
746
|
+
const newSpinner = createSpinner(msg.installingNewStandards).start();
|
|
718
747
|
let newCount = 0;
|
|
719
748
|
|
|
720
749
|
for (const ns of newStandards) {
|
|
@@ -735,7 +764,17 @@ export async function updateCommand(options) {
|
|
|
735
764
|
|
|
736
765
|
// Update integrations (unless --standards-only)
|
|
737
766
|
if (!options.standardsOnly && manifest.integrations && manifest.integrations.length > 0) {
|
|
738
|
-
|
|
767
|
+
// XSPEC adopter-report Q1 follow-up: this block writes
|
|
768
|
+
// integrationBlockHashes but never touched manifest.integrationConfigs at
|
|
769
|
+
// all, so a manifest that picked up a broken (empty/unrecognized)
|
|
770
|
+
// categories array from an older buggy `--sync-refs` run stayed broken
|
|
771
|
+
// through every subsequent plain `uds update` — the only path that
|
|
772
|
+
// repaired it was `--sync-refs` itself. Self-heal corruption here too, not
|
|
773
|
+
// just there; an already-valid list is left alone (see
|
|
774
|
+
// repairIntegrationConfigCategories's docblock for why).
|
|
775
|
+
repairIntegrationConfigCategories(manifest);
|
|
776
|
+
|
|
777
|
+
const intSpinner = createSpinner(msg.syncingIntegrations).start();
|
|
739
778
|
|
|
740
779
|
// Build installed standards list
|
|
741
780
|
// Raw, not basename()d. Resolution needs the registry (an ID is not a
|
|
@@ -756,7 +795,9 @@ export async function updateCommand(options) {
|
|
|
756
795
|
const aiTools = manifest.aiTools || [];
|
|
757
796
|
|
|
758
797
|
for (const tool of aiTools) {
|
|
759
|
-
|
|
798
|
+
// XSPEC-418 R3: honors manifest.integrationTargets — the tool's file may
|
|
799
|
+
// be CLAUDE.local.md, not the hardcoded default.
|
|
800
|
+
const targetFile = resolveIntegrationTargetFile(tool, manifest);
|
|
760
801
|
if (generatedFiles.has(targetFile)) {
|
|
761
802
|
continue; // Skip if already generated (AGENTS.md sharing)
|
|
762
803
|
}
|
|
@@ -773,7 +814,9 @@ export async function updateCommand(options) {
|
|
|
773
814
|
contentMode: resolved.contentMode,
|
|
774
815
|
level: resolved.level,
|
|
775
816
|
// Pass output_language for dynamic commit standards generation
|
|
776
|
-
outputLanguage: manifest.options?.output_language || manifest.options?.commit_language || 'english'
|
|
817
|
+
outputLanguage: manifest.options?.output_language || manifest.options?.commit_language || 'english',
|
|
818
|
+
// XSPEC-418 R2/R3: so writeIntegrationFile resolves the actual target.
|
|
819
|
+
integrationTargets: manifest.integrationTargets
|
|
777
820
|
};
|
|
778
821
|
|
|
779
822
|
const result = writeIntegrationFile(tool, toolConfig, projectPath);
|
|
@@ -813,6 +856,7 @@ export async function updateCommand(options) {
|
|
|
813
856
|
installedAt: new Date().toISOString()
|
|
814
857
|
};
|
|
815
858
|
}
|
|
859
|
+
refreshTrackedFileHash(manifest, projectPath, agentsMdResult.path);
|
|
816
860
|
}
|
|
817
861
|
}
|
|
818
862
|
|
|
@@ -831,7 +875,11 @@ export async function updateCommand(options) {
|
|
|
831
875
|
if (manifest.integrationBlockHashes) {
|
|
832
876
|
const expectedFiles = new Set();
|
|
833
877
|
for (const tool of (manifest.aiTools || [])) {
|
|
834
|
-
|
|
878
|
+
// XSPEC-418 R3: an expected set built from the hardcoded default file
|
|
879
|
+
// pruned CLAUDE.local.md's own hash the moment a local-target install
|
|
880
|
+
// ran this cleanup — it looked orphaned because nothing here knew the
|
|
881
|
+
// real target had moved.
|
|
882
|
+
const targetFile = resolveIntegrationTargetFile(tool, manifest);
|
|
835
883
|
if (targetFile) expectedFiles.add(targetFile);
|
|
836
884
|
}
|
|
837
885
|
// Universal AGENTS.md is tracked when generateAgentsMd is enabled.
|
|
@@ -862,8 +910,14 @@ export async function updateCommand(options) {
|
|
|
862
910
|
if (!layeredResult.fallback) {
|
|
863
911
|
console.log(chalk.green(` ✓ Layered CLAUDE.md updated (${layeredResult.generatedFiles.length} files)`));
|
|
864
912
|
}
|
|
865
|
-
} catch {
|
|
866
|
-
//
|
|
913
|
+
} catch (error) {
|
|
914
|
+
// XSPEC adopter-report Q5: an ambiguous marker pair is a real problem
|
|
915
|
+
// the adopter needs to see — refuse that one write and say why,
|
|
916
|
+
// rather than folding it into "generator not available".
|
|
917
|
+
if (error instanceof AmbiguousMarkerError) {
|
|
918
|
+
console.log(chalk.red(` ✗ Layered CLAUDE.md not updated: ${error.message}`));
|
|
919
|
+
}
|
|
920
|
+
// Otherwise: silently skip if generator not available
|
|
867
921
|
}
|
|
868
922
|
}
|
|
869
923
|
|
|
@@ -933,14 +987,14 @@ export async function updateCommand(options) {
|
|
|
933
987
|
}
|
|
934
988
|
}
|
|
935
989
|
|
|
936
|
-
//
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
990
|
+
// XSPEC-418 R6: integration files (results.integrations — CLAUDE.md,
|
|
991
|
+
// CLAUDE.local.md, AGENTS.md, etc.) are tracked by their UDS block in
|
|
992
|
+
// integrationBlockHashes, not by whole-file hash here. A whole-file entry
|
|
993
|
+
// for one of these disagrees the moment the adopter edits anything outside
|
|
994
|
+
// the block — exactly the customization the marker-based update preserves —
|
|
995
|
+
// and made `uds check` report "modified" while its own block-integrity
|
|
996
|
+
// check said the block was intact. See pruneIntegrationFileHashes below for
|
|
997
|
+
// cleanup of any such entry an older CLI already wrote.
|
|
944
998
|
|
|
945
999
|
// Record what UDS wrote this run, then decide what (if anything) may go.
|
|
946
1000
|
// (XSPEC-384 R1/R2/R3)
|
|
@@ -989,6 +1043,22 @@ export async function updateCommand(options) {
|
|
|
989
1043
|
}
|
|
990
1044
|
}
|
|
991
1045
|
|
|
1046
|
+
// Records of files that are already gone and that UDS no longer ships. Not
|
|
1047
|
+
// part of the removal plan above: that plan walks what is on disk, and these
|
|
1048
|
+
// are precisely the ones that are not. Reported rather than done quietly,
|
|
1049
|
+
// because a manifest shrinking without explanation is the shape this whole
|
|
1050
|
+
// release is about.
|
|
1051
|
+
const retiredHashes = pruneRetiredHashes(manifest, projectPath);
|
|
1052
|
+
if (retiredHashes.length > 0) {
|
|
1053
|
+
console.log();
|
|
1054
|
+
console.log(chalk.gray(
|
|
1055
|
+
` ${(msg.droppedRetiredHashes || 'Dropped {count} manifest record(s) for files UDS no longer ships and that are already gone:').replace('{count}', retiredHashes.length)}`
|
|
1056
|
+
));
|
|
1057
|
+
for (const path of retiredHashes) {
|
|
1058
|
+
console.log(chalk.gray(` - ${path}`));
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
992
1062
|
// Provenance is only a complete account of what we own once a run has
|
|
993
1063
|
// written the whole installed set. Until then every unrecorded file is
|
|
994
1064
|
// `unknown` rather than `foreign`, and nothing gets deleted on the strength
|
|
@@ -1052,7 +1122,7 @@ export async function updateCommand(options) {
|
|
|
1052
1122
|
// date. The manifest is still written so that hash/migration bookkeeping for
|
|
1053
1123
|
// the files that DID succeed is persisted.
|
|
1054
1124
|
const updateIncomplete = results.errors.length > 0;
|
|
1055
|
-
manifest
|
|
1125
|
+
bumpManifestVersion(manifest);
|
|
1056
1126
|
if (!updateIncomplete) {
|
|
1057
1127
|
manifest.upstream.version = latestVersion;
|
|
1058
1128
|
manifest.upstream.installed = new Date().toISOString().split('T')[0];
|
|
@@ -1085,8 +1155,9 @@ export async function updateCommand(options) {
|
|
|
1085
1155
|
allTrackedFiles.push(join('.standards', fileName));
|
|
1086
1156
|
}
|
|
1087
1157
|
for (const intEntry of (manifest.integrations || [])) {
|
|
1088
|
-
// 兩種形狀都要能解出路徑(XSPEC-343 R1
|
|
1089
|
-
|
|
1158
|
+
// 兩種形狀都要能解出路徑(XSPEC-343 R1),且要尊重 manifest.integrationTargets
|
|
1159
|
+
// 的目標覆寫(XSPEC-418 R2/R3)——resolveIntegrationTargetFile 已同時處理兩者。
|
|
1160
|
+
const filePath = resolveIntegrationTargetFile(intEntry, manifest);
|
|
1090
1161
|
if (filePath) {
|
|
1091
1162
|
allTrackedFiles.push(filePath);
|
|
1092
1163
|
}
|
|
@@ -1110,7 +1181,7 @@ export async function updateCommand(options) {
|
|
|
1110
1181
|
}
|
|
1111
1182
|
|
|
1112
1183
|
if (shouldRestore) {
|
|
1113
|
-
const restoreSpinner =
|
|
1184
|
+
const restoreSpinner = createSpinner((msg.restoringMissing || 'Restoring missing files...')).start();
|
|
1114
1185
|
const checkMsg = t().commands.check;
|
|
1115
1186
|
let restoredCount = 0;
|
|
1116
1187
|
|
|
@@ -1177,7 +1248,7 @@ export async function updateCommand(options) {
|
|
|
1177
1248
|
|
|
1178
1249
|
// Install Skills if user agreed
|
|
1179
1250
|
if (installSkills.length > 0) {
|
|
1180
|
-
const skillSpinner =
|
|
1251
|
+
const skillSpinner = createSpinner(msg.installingNewSkills || 'Installing Skills...').start();
|
|
1181
1252
|
const skillsLocale = resolveLocale(manifest, projectPath, options);
|
|
1182
1253
|
const skillResult = await installSkillsToMultipleAgents(installSkills, null, projectPath, skillsLocale);
|
|
1183
1254
|
|
|
@@ -1225,7 +1296,7 @@ export async function updateCommand(options) {
|
|
|
1225
1296
|
|
|
1226
1297
|
// Update outdated Skills if user agreed
|
|
1227
1298
|
if (updateSkills.length > 0) {
|
|
1228
|
-
const updateSpinner =
|
|
1299
|
+
const updateSpinner = createSpinner(msg.updatingSkills || 'Updating Skills...').start();
|
|
1229
1300
|
const updateLocale = resolveLocale(manifest, projectPath, options);
|
|
1230
1301
|
const updateResult = await installSkillsToMultipleAgents(updateSkills, null, projectPath, updateLocale);
|
|
1231
1302
|
|
|
@@ -1263,7 +1334,7 @@ export async function updateCommand(options) {
|
|
|
1263
1334
|
|
|
1264
1335
|
// Install Commands if user agreed
|
|
1265
1336
|
if (installCommands.length > 0) {
|
|
1266
|
-
const cmdSpinner =
|
|
1337
|
+
const cmdSpinner = createSpinner(msg.installingNewCommands || 'Installing commands...').start();
|
|
1267
1338
|
const cmdLocale = resolveLocale(manifest, projectPath, options);
|
|
1268
1339
|
const cmdResult = await installCommandsToMultipleAgents(installCommands, null, projectPath, cmdLocale);
|
|
1269
1340
|
|
|
@@ -1294,7 +1365,7 @@ export async function updateCommand(options) {
|
|
|
1294
1365
|
|
|
1295
1366
|
// Update outdated Commands if user agreed
|
|
1296
1367
|
if (updateCommands.length > 0) {
|
|
1297
|
-
const updateCmdSpinner =
|
|
1368
|
+
const updateCmdSpinner = createSpinner(msg.updatingCommands || 'Updating Commands...').start();
|
|
1298
1369
|
const updateCmdLocale = resolveLocale(manifest, projectPath, options);
|
|
1299
1370
|
const updateCmdResult = await installCommandsToMultipleAgents(updateCommands, null, projectPath, updateCmdLocale);
|
|
1300
1371
|
|
|
@@ -1370,7 +1441,7 @@ export async function updateCommand(options) {
|
|
|
1370
1441
|
let hasChanges = false;
|
|
1371
1442
|
|
|
1372
1443
|
if (missingSkills.length > 0) {
|
|
1373
|
-
const spinner =
|
|
1444
|
+
const spinner = createSpinner(msg.installingNewSkills || 'Installing Skills...').start();
|
|
1374
1445
|
const result = await installSkillsToMultipleAgents(missingSkills, null, projectPath, skillsLocale);
|
|
1375
1446
|
if (!manifest.skills) manifest.skills = {};
|
|
1376
1447
|
manifest.skills.installed = true;
|
|
@@ -1391,7 +1462,7 @@ export async function updateCommand(options) {
|
|
|
1391
1462
|
}
|
|
1392
1463
|
|
|
1393
1464
|
if (outdatedSkills.length > 0) {
|
|
1394
|
-
const spinner =
|
|
1465
|
+
const spinner = createSpinner(msg.updatingSkills || 'Updating Skills...').start();
|
|
1395
1466
|
const result = await installSkillsToMultipleAgents(outdatedSkills, null, projectPath, skillsLocale);
|
|
1396
1467
|
if (!manifest.skills) manifest.skills = {};
|
|
1397
1468
|
manifest.skills.version = repoInfo.skills.version;
|
|
@@ -1415,7 +1486,7 @@ export async function updateCommand(options) {
|
|
|
1415
1486
|
}
|
|
1416
1487
|
|
|
1417
1488
|
if (missingCommands.length > 0) {
|
|
1418
|
-
const spinner =
|
|
1489
|
+
const spinner = createSpinner(msg.installingNewCommands || 'Installing commands...').start();
|
|
1419
1490
|
const result = await installCommandsToMultipleAgents(missingCommands, null, projectPath, skillsLocale);
|
|
1420
1491
|
if (!manifest.commands) manifest.commands = {};
|
|
1421
1492
|
manifest.commands.installed = true;
|
|
@@ -1435,7 +1506,7 @@ export async function updateCommand(options) {
|
|
|
1435
1506
|
}
|
|
1436
1507
|
|
|
1437
1508
|
if (outdatedCommands.length > 0) {
|
|
1438
|
-
const spinner =
|
|
1509
|
+
const spinner = createSpinner(msg.updatingCommands || 'Updating Commands...').start();
|
|
1439
1510
|
const result = await installCommandsToMultipleAgents(outdatedCommands, null, projectPath, skillsLocale);
|
|
1440
1511
|
if (!manifest.commands) manifest.commands = {};
|
|
1441
1512
|
manifest.commands.version = repoInfo.skills.version;
|
|
@@ -1607,6 +1678,140 @@ async function offerErrorExitGate(projectPath, options) {
|
|
|
1607
1678
|
console.log();
|
|
1608
1679
|
}
|
|
1609
1680
|
|
|
1681
|
+
/**
|
|
1682
|
+
* Switch the claude-code integration's target file between CLAUDE.md and
|
|
1683
|
+
* CLAUDE.local.md for an EXISTING installation, without a full reinstall.
|
|
1684
|
+
* // implements XSPEC-418 R4
|
|
1685
|
+
*
|
|
1686
|
+
* The scenario this exists for: an adopter installed with the default target
|
|
1687
|
+
* (CLAUDE.md), then either hand-moved the UDS block to CLAUDE.local.md (it now
|
|
1688
|
+
* exists there with markers already) or wants to move it there for the first
|
|
1689
|
+
* time. Either way `uds update --claude-target local` should leave exactly one
|
|
1690
|
+
* up-to-date block in the new target and none in the old one.
|
|
1691
|
+
*
|
|
1692
|
+
* @param {string} projectPath
|
|
1693
|
+
* @param {Object} manifest - Mutated and written to disk on success
|
|
1694
|
+
* @param {string} target - 'project' | 'local'
|
|
1695
|
+
* @param {Object} options - CLI options (unused today; kept for symmetry with
|
|
1696
|
+
* the other option handlers and so a future confirmation prompt has somewhere
|
|
1697
|
+
* to read --yes from)
|
|
1698
|
+
*/
|
|
1699
|
+
async function switchClaudeTarget(projectPath, manifest, target, options) { // eslint-disable-line no-unused-vars
|
|
1700
|
+
if (target !== 'project' && target !== 'local') {
|
|
1701
|
+
console.log(chalk.red(` ✗ --claude-target must be "project" or "local", got "${target}"`));
|
|
1702
|
+
console.log();
|
|
1703
|
+
process.exitCode = 1;
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
if (!(manifest.aiTools || []).includes('claude-code')) {
|
|
1708
|
+
console.log(chalk.yellow(' claude-code is not among the configured AI tools — nothing to switch.'));
|
|
1709
|
+
console.log();
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
const oldFile = resolveIntegrationTargetFile('claude-code', manifest);
|
|
1714
|
+
const newFile = target === 'local' ? 'CLAUDE.local.md' : 'CLAUDE.md';
|
|
1715
|
+
|
|
1716
|
+
if (oldFile === newFile) {
|
|
1717
|
+
console.log(chalk.gray(` claude-code integration already targets ${newFile}. Nothing to do.`));
|
|
1718
|
+
console.log();
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
console.log(chalk.cyan(` Switching claude-code integration target: ${oldFile} → ${newFile}`));
|
|
1723
|
+
|
|
1724
|
+
// 1. Remove the UDS block from the OLD file, preserving any user content —
|
|
1725
|
+
// the same rule uninstallIntegrations already applies. A file left 100%
|
|
1726
|
+
// UDS-generated once the block is gone is deleted outright.
|
|
1727
|
+
const oldPath = join(projectPath, oldFile);
|
|
1728
|
+
if (existsSync(oldPath)) {
|
|
1729
|
+
const format = getToolFormat('claude-code');
|
|
1730
|
+
const content = readFileSync(oldPath, 'utf-8');
|
|
1731
|
+
// XSPEC adopter-report Q5: an ambiguous marker pair means this write
|
|
1732
|
+
// must refuse rather than guess which block to move — report and leave
|
|
1733
|
+
// the old file untouched instead of switching targets on a bad guess.
|
|
1734
|
+
let parts;
|
|
1735
|
+
try {
|
|
1736
|
+
parts = extractMarkedContent(content, format);
|
|
1737
|
+
} catch (error) {
|
|
1738
|
+
if (error instanceof AmbiguousMarkerError) {
|
|
1739
|
+
console.log(chalk.red(` ✗ ${oldFile}: ${error.message}`));
|
|
1740
|
+
console.log(chalk.gray(` Not switching — fix the marker pair in ${oldFile} first.`));
|
|
1741
|
+
process.exitCode = 1;
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
throw error;
|
|
1745
|
+
}
|
|
1746
|
+
if (parts.content) {
|
|
1747
|
+
const userContent = (parts.before.trim() + parts.after.trim()).trim();
|
|
1748
|
+
if (userContent.length > 0) {
|
|
1749
|
+
const cleaned = (parts.before + parts.after).trim() + '\n';
|
|
1750
|
+
writeFileSync(oldPath, cleaned, 'utf-8');
|
|
1751
|
+
console.log(chalk.gray(` ${oldFile}: UDS block removed, your content kept`));
|
|
1752
|
+
} else {
|
|
1753
|
+
unlinkSync(oldPath);
|
|
1754
|
+
console.log(chalk.gray(` ${oldFile}: deleted (was 100% UDS-generated)`));
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
// else: no UDS markers found in the old file (already moved by hand) —
|
|
1758
|
+
// nothing UDS-owned to remove.
|
|
1759
|
+
}
|
|
1760
|
+
if (manifest.integrationBlockHashes) delete manifest.integrationBlockHashes[oldFile];
|
|
1761
|
+
if (manifest.fileHashes) delete manifest.fileHashes[oldFile];
|
|
1762
|
+
|
|
1763
|
+
// 2. Point the manifest at the new target. Switching back to 'project'
|
|
1764
|
+
// removes the override entirely rather than writing it as 'CLAUDE.md', so a
|
|
1765
|
+
// round-tripped manifest is shaped exactly as if local had never been chosen
|
|
1766
|
+
// (XSPEC-418 AC-5).
|
|
1767
|
+
if (target === 'local') {
|
|
1768
|
+
manifest.integrationTargets = { ...(manifest.integrationTargets || {}), 'claude-code': 'CLAUDE.local.md' };
|
|
1769
|
+
} else if (manifest.integrationTargets) {
|
|
1770
|
+
delete manifest.integrationTargets['claude-code'];
|
|
1771
|
+
if (Object.keys(manifest.integrationTargets).length === 0) delete manifest.integrationTargets;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// 3. Write (or update in place) the new target. writeIntegrationFile already
|
|
1775
|
+
// does a marker-based UPDATE when the file exists and has UDS markers — the
|
|
1776
|
+
// "hand-moved to CLAUDE.local.md already" arm of R4 — and only appends when
|
|
1777
|
+
// it does not, so this one call covers both AC-4 arms.
|
|
1778
|
+
const toolConfig = buildToolIntegrationConfig(manifest, 'claude-code');
|
|
1779
|
+
const result = writeIntegrationFile('claude-code', toolConfig, projectPath);
|
|
1780
|
+
if (!result.success) {
|
|
1781
|
+
console.log(chalk.red(` ✗ Failed to write ${newFile}: ${result.error}`));
|
|
1782
|
+
console.log();
|
|
1783
|
+
process.exitCode = 1;
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
if (result.blockHashInfo) {
|
|
1788
|
+
if (!manifest.integrationBlockHashes) manifest.integrationBlockHashes = {};
|
|
1789
|
+
manifest.integrationBlockHashes[result.path] = {
|
|
1790
|
+
...result.blockHashInfo,
|
|
1791
|
+
installedAt: new Date().toISOString()
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
// XSPEC-418 R6: no whole-file fileHashes entry for the new target either —
|
|
1795
|
+
// only its UDS block is tracked, above.
|
|
1796
|
+
|
|
1797
|
+
// 4. manifest.integrations may record the old file path (XSPEC-343 shapes) —
|
|
1798
|
+
// point it at the new one instead of leaving a stale entry check would then
|
|
1799
|
+
// report as missing.
|
|
1800
|
+
manifest.integrations = (manifest.integrations || []).map(entry => (entry === oldFile ? newFile : entry));
|
|
1801
|
+
if (!manifest.integrations.includes(newFile) && !manifest.integrations.includes('claude-code')) {
|
|
1802
|
+
manifest.integrations.push(newFile);
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
// XSPEC-418 R6: also catches a stale whole-file entry for either file left
|
|
1806
|
+
// by an older CLI, not just the one this run might otherwise have added.
|
|
1807
|
+
pruneIntegrationFileHashes(manifest);
|
|
1808
|
+
|
|
1809
|
+
writeManifest(manifest, projectPath);
|
|
1810
|
+
|
|
1811
|
+
console.log(chalk.green(` ✓ claude-code now targets ${newFile}`));
|
|
1812
|
+
console.log();
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1610
1815
|
/**
|
|
1611
1816
|
* Regenerate integration files for all configured AI tools
|
|
1612
1817
|
* Reusable core logic that can be called from both updateIntegrationsOnly and configureCommand
|
|
@@ -1631,12 +1836,15 @@ export function regenerateIntegrations(projectPath, manifest) {
|
|
|
1631
1836
|
const now = new Date().toISOString();
|
|
1632
1837
|
|
|
1633
1838
|
for (const tool of aiTools) {
|
|
1634
|
-
|
|
1839
|
+
// XSPEC-418 R3
|
|
1840
|
+
const targetFile = resolveIntegrationTargetFile(tool, manifest);
|
|
1635
1841
|
if (generatedFiles.has(targetFile)) {
|
|
1636
1842
|
continue; // Skip if already generated (AGENTS.md sharing)
|
|
1637
1843
|
}
|
|
1638
1844
|
|
|
1639
1845
|
// Shared with the reconciler so both paths emit the identical block.
|
|
1846
|
+
// buildToolIntegrationConfig already carries manifest.integrationTargets
|
|
1847
|
+
// through to writeIntegrationFile (XSPEC-418 R2).
|
|
1640
1848
|
const toolConfig = buildToolIntegrationConfig(manifest, tool);
|
|
1641
1849
|
|
|
1642
1850
|
const result = writeIntegrationFile(tool, toolConfig, projectPath);
|
|
@@ -1644,16 +1852,11 @@ export function regenerateIntegrations(projectPath, manifest) {
|
|
|
1644
1852
|
results.updated.push(result.path);
|
|
1645
1853
|
generatedFiles.add(targetFile);
|
|
1646
1854
|
|
|
1647
|
-
//
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
manifest.fileHashes = {};
|
|
1653
|
-
}
|
|
1654
|
-
const normalizedPath = result.path.replace(/\\/g, '/');
|
|
1655
|
-
manifest.fileHashes[normalizedPath] = { ...hashInfo, installedAt: now };
|
|
1656
|
-
}
|
|
1855
|
+
// XSPEC-418 R6: no whole-file `fileHashes` entry for an integration
|
|
1856
|
+
// file — only the UDS block is tracked, below. A whole-file entry here
|
|
1857
|
+
// is exactly what made `uds check` report "CLAUDE.md (modified)" for
|
|
1858
|
+
// content outside the block that UDS's own marker-based update leaves
|
|
1859
|
+
// alone on purpose.
|
|
1657
1860
|
|
|
1658
1861
|
// Track integration block hash for UDS content integrity
|
|
1659
1862
|
if (result.blockHashInfo) {
|
|
@@ -1688,9 +1891,18 @@ export function regenerateIntegrations(projectPath, manifest) {
|
|
|
1688
1891
|
installedAt: now
|
|
1689
1892
|
};
|
|
1690
1893
|
}
|
|
1894
|
+
refreshTrackedFileHash(manifest, projectPath, agentsMdResult.path);
|
|
1691
1895
|
}
|
|
1692
1896
|
}
|
|
1693
1897
|
|
|
1898
|
+
// XSPEC-418 R6: drop any fileHashes entry left over from an older CLI (or
|
|
1899
|
+
// from refreshTrackedFileHash above, which predates this rule and still
|
|
1900
|
+
// refreshes AGENTS.md's whole-file hash if one is already tracked). Called
|
|
1901
|
+
// here rather than relying only on refreshIntegrationBlockHashes downstream
|
|
1902
|
+
// because not every caller of this function calls that afterward (`uds
|
|
1903
|
+
// config`'s AI-tools flow does not).
|
|
1904
|
+
pruneIntegrationFileHashes(manifest);
|
|
1905
|
+
|
|
1694
1906
|
return {
|
|
1695
1907
|
success: results.errors.length === 0,
|
|
1696
1908
|
updated: results.updated,
|
|
@@ -1758,9 +1970,11 @@ async function updateIntegrationsOnly(projectPath, manifest, options = {}) {
|
|
|
1758
1970
|
console.log(chalk.bold('=== Integration Plan (dry run — nothing is written) ==='));
|
|
1759
1971
|
const wouldChange = [];
|
|
1760
1972
|
const unchanged = [];
|
|
1973
|
+
const ambiguous = [];
|
|
1761
1974
|
const seen = new Set();
|
|
1762
1975
|
for (const tool of aiTools) {
|
|
1763
|
-
|
|
1976
|
+
// XSPEC-418 R3: the plan must show the actual target, not the default.
|
|
1977
|
+
const targetFile = resolveIntegrationTargetFile(tool, manifest);
|
|
1764
1978
|
if (seen.has(targetFile)) continue;
|
|
1765
1979
|
seen.add(targetFile);
|
|
1766
1980
|
const savedMode = manifest.contentMode || 'auto';
|
|
@@ -1787,7 +2001,18 @@ async function updateIntegrationsOnly(projectPath, manifest, options = {}) {
|
|
|
1787
2001
|
const full = join(projectPath, targetFile);
|
|
1788
2002
|
const current = existsSync(full) ? readFileSync(full, 'utf8') : '';
|
|
1789
2003
|
const format = targetFile.endsWith('.md') ? 'markdown' : 'plaintext';
|
|
1790
|
-
|
|
2004
|
+
// XSPEC adopter-report Q5: an ambiguous marker pair can't be diffed
|
|
2005
|
+
// against — report it explicitly instead of crashing the whole plan.
|
|
2006
|
+
let cur;
|
|
2007
|
+
try {
|
|
2008
|
+
cur = extractMarkedContent(current, format).content || '';
|
|
2009
|
+
} catch (error) {
|
|
2010
|
+
if (error instanceof AmbiguousMarkerError) {
|
|
2011
|
+
ambiguous.push(`${targetFile}: ${error.message}`);
|
|
2012
|
+
continue;
|
|
2013
|
+
}
|
|
2014
|
+
throw error;
|
|
2015
|
+
}
|
|
1791
2016
|
// 比對正規化過的內容:只關心「受管區塊會不會變」,不關心尾端空白。
|
|
1792
2017
|
if (cur.trim() === String(next).trim()) {
|
|
1793
2018
|
unchanged.push(targetFile);
|
|
@@ -1796,6 +2021,10 @@ async function updateIntegrationsOnly(projectPath, manifest, options = {}) {
|
|
|
1796
2021
|
wouldChange.push(`${targetFile} (${delta >= 0 ? '+' : ''}${delta} bytes in the UDS block)`);
|
|
1797
2022
|
}
|
|
1798
2023
|
}
|
|
2024
|
+
if (ambiguous.length > 0) {
|
|
2025
|
+
console.log(chalk.red(` ✗ Ambiguous UDS markers (${ambiguous.length}) — will not be touched by --apply:`));
|
|
2026
|
+
for (const a of ambiguous) console.log(chalk.gray(` ${a}`));
|
|
2027
|
+
}
|
|
1799
2028
|
if (wouldChange.length > 0) {
|
|
1800
2029
|
console.log(chalk.yellow(` ~ Would update (${wouldChange.length}):`));
|
|
1801
2030
|
for (const f of wouldChange) console.log(chalk.gray(` ${f}`));
|
|
@@ -1811,7 +2040,7 @@ async function updateIntegrationsOnly(projectPath, manifest, options = {}) {
|
|
|
1811
2040
|
return;
|
|
1812
2041
|
}
|
|
1813
2042
|
|
|
1814
|
-
const spinner =
|
|
2043
|
+
const spinner = createSpinner(msg.regeneratingIntegrations).start();
|
|
1815
2044
|
|
|
1816
2045
|
// Use reusable regeneration function
|
|
1817
2046
|
const results = regenerateIntegrations(projectPath, manifest);
|
|
@@ -1819,7 +2048,7 @@ async function updateIntegrationsOnly(projectPath, manifest, options = {}) {
|
|
|
1819
2048
|
spinner.succeed(msg.regeneratedIntegrations.replace('{count}', results.updated.length));
|
|
1820
2049
|
|
|
1821
2050
|
// Update manifest
|
|
1822
|
-
manifest
|
|
2051
|
+
bumpManifestVersion(manifest);
|
|
1823
2052
|
refreshIntegrationBlockHashes(manifest, projectPath);
|
|
1824
2053
|
writeManifest(manifest, projectPath);
|
|
1825
2054
|
|
|
@@ -1840,6 +2069,199 @@ async function updateIntegrationsOnly(projectPath, manifest, options = {}) {
|
|
|
1840
2069
|
process.exit(0);
|
|
1841
2070
|
}
|
|
1842
2071
|
|
|
2072
|
+
/**
|
|
2073
|
+
* Rebuild `manifest.integrationConfigs` from what the manifest already records.
|
|
2074
|
+
*
|
|
2075
|
+
* 🔴 Only `uds init`'s interactive flow and `--sync-refs` itself ever wrote this
|
|
2076
|
+
* key; no update or reconcile path backfills it, so a project created with
|
|
2077
|
+
* `uds init -y` carries `{}` from the first day (reproduced on a clean 6.9.0
|
|
2078
|
+
* project, 2026-09-16). `--sync-refs` then refuses to run — and `uds check` was
|
|
2079
|
+
* recommending exactly that command. Everything the config holds is derivable
|
|
2080
|
+
* from the manifest, so derive it instead of demanding the user re-init.
|
|
2081
|
+
*
|
|
2082
|
+
* Existing entries are never overwritten: they may hold choices (categories,
|
|
2083
|
+
* content mode) that this derivation cannot know.
|
|
2084
|
+
*
|
|
2085
|
+
* @param {Object} manifest - Manifest object (mutated)
|
|
2086
|
+
* @returns {number} How many entries were added
|
|
2087
|
+
*/
|
|
2088
|
+
export function backfillIntegrationConfigs(manifest) {
|
|
2089
|
+
if (!manifest.integrationConfigs) manifest.integrationConfigs = {};
|
|
2090
|
+
|
|
2091
|
+
const tools = manifest.aiTools?.length ? manifest.aiTools : [];
|
|
2092
|
+
const byFile = new Map();
|
|
2093
|
+
for (const tool of tools) {
|
|
2094
|
+
// XSPEC-418 R3: keyed by the actual target file, or integrationConfigs
|
|
2095
|
+
// would key claude-code's entry as 'CLAUDE.md' while every other record
|
|
2096
|
+
// (integrations, integrationBlockHashes) already uses CLAUDE.local.md.
|
|
2097
|
+
const file = resolveIntegrationTargetFile(tool, manifest);
|
|
2098
|
+
if (file && !byFile.has(file)) byFile.set(file, tool);
|
|
2099
|
+
}
|
|
2100
|
+
// Integrations can be recorded as file names (`CLAUDE.md`) or tool keys.
|
|
2101
|
+
for (const entry of manifest.integrations || []) {
|
|
2102
|
+
const file = resolveIntegrationFile(entry) || (String(entry).includes('.') ? String(entry) : null);
|
|
2103
|
+
if (!file) continue;
|
|
2104
|
+
if (!byFile.has(file)) {
|
|
2105
|
+
const tool = getToolFromPath(file) || resolveToolKeyFromEntry(entry);
|
|
2106
|
+
if (tool) byFile.set(file, tool);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
let added = 0;
|
|
2111
|
+
for (const [file, tool] of byFile) {
|
|
2112
|
+
if (manifest.integrationConfigs[file]) continue;
|
|
2113
|
+
manifest.integrationConfigs[file] = {
|
|
2114
|
+
...buildToolIntegrationConfig(manifest, tool),
|
|
2115
|
+
format: manifest.format || 'ai',
|
|
2116
|
+
backfilledAt: new Date().toISOString()
|
|
2117
|
+
};
|
|
2118
|
+
added++;
|
|
2119
|
+
}
|
|
2120
|
+
return added;
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
function resolveToolKeyFromEntry(entry) {
|
|
2124
|
+
const key = String(entry).replace(/\.[^.]+$/, '').toLowerCase();
|
|
2125
|
+
return SUPPORTED_AI_TOOLS?.[key] ? key : null;
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
/**
|
|
2129
|
+
* If the manifest already tracks this file, make its record describe what is on
|
|
2130
|
+
* disk now.
|
|
2131
|
+
*
|
|
2132
|
+
* Every path that writes AGENTS.md recorded `integrationBlockHashes` and none
|
|
2133
|
+
* recorded `fileHashes`, so a project whose manifest tracked the file (an older
|
|
2134
|
+
* install, or one where AGENTS.md was written as a tool's integration file) got
|
|
2135
|
+
* `⚠ AGENTS.md (modified)` from `uds check` the moment UDS itself regenerated
|
|
2136
|
+
* it. UDS wrote the file and did not record what it wrote.
|
|
2137
|
+
*
|
|
2138
|
+
* Conditional on purpose. `uds init` does not enrol AGENTS.md in `fileHashes`,
|
|
2139
|
+
* and adopters are expected to extend that summary — a path that started
|
|
2140
|
+
* tracking it would convert every legitimate edit into a reported fault. This
|
|
2141
|
+
* only ever refreshes a record that already exists.
|
|
2142
|
+
*
|
|
2143
|
+
* XSPEC-418 R6 narrowed that further: AGENTS.md is also tracked by its UDS
|
|
2144
|
+
* block (`integrationBlockHashes`) once one exists, and every call site of
|
|
2145
|
+
* this function now also calls `pruneIntegrationFileHashes` afterward — so a
|
|
2146
|
+
* `fileHashes` entry this refreshes is removed again in the same run. Kept
|
|
2147
|
+
* (rather than deleted outright) because it still matters for the moment
|
|
2148
|
+
* between this call and the prune, and for any caller added later that
|
|
2149
|
+
* forgets to prune; the prune is what actually enforces R6.
|
|
2150
|
+
*
|
|
2151
|
+
* @param {Object} manifest - Project manifest (mutated)
|
|
2152
|
+
* @param {string} projectPath - Project root
|
|
2153
|
+
* @param {string} relativePath - Path as UDS reports it
|
|
2154
|
+
* @returns {boolean} Whether a record was refreshed
|
|
2155
|
+
*/
|
|
2156
|
+
export function refreshTrackedFileHash(manifest, projectPath, relativePath) {
|
|
2157
|
+
if (!manifest?.fileHashes || !relativePath) return false;
|
|
2158
|
+
const normalized = String(relativePath).replace(/\\/g, '/');
|
|
2159
|
+
if (!(normalized in manifest.fileHashes)) return false;
|
|
2160
|
+
|
|
2161
|
+
const hashInfo = computeFileHash(join(projectPath, normalized));
|
|
2162
|
+
if (!hashInfo) return false;
|
|
2163
|
+
|
|
2164
|
+
manifest.fileHashes[normalized] = { ...hashInfo, installedAt: new Date().toISOString() };
|
|
2165
|
+
return true;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
/**
|
|
2169
|
+
* Forget `fileHashes` records for files that are gone AND that UDS no longer
|
|
2170
|
+
* ships.
|
|
2171
|
+
*
|
|
2172
|
+
* `--prune` deletes files it finds while walking `.standards/`. A file the
|
|
2173
|
+
* adopter already removed by hand is not in that walk, so its record survived
|
|
2174
|
+
* every prune — and the record is what made `uds check` report the completed
|
|
2175
|
+
* migration as seven faults. Following MIGRATION-v6 §2 therefore produced a
|
|
2176
|
+
* complaint that no command could clear.
|
|
2177
|
+
*
|
|
2178
|
+
* This is not a deletion. The file is already absent and no registry entry can
|
|
2179
|
+
* ever resolve to it again, so the record cannot refer to anything. Both facts
|
|
2180
|
+
* are required: a missing file UDS still ships is a real fault and stays; a
|
|
2181
|
+
* descoped standard still on disk is `--prune`'s business, gated on ownership,
|
|
2182
|
+
* and is left alone here.
|
|
2183
|
+
*
|
|
2184
|
+
* @param {Object} manifest - Project manifest (mutated)
|
|
2185
|
+
* @param {string} projectPath - Project root
|
|
2186
|
+
* @param {{shippable?: Set<string>}} [deps] - Injection point for tests
|
|
2187
|
+
* @returns {string[]} Paths whose records were dropped, sorted
|
|
2188
|
+
*/
|
|
2189
|
+
export function pruneRetiredHashes(manifest, projectPath, deps = {}) {
|
|
2190
|
+
if (!manifest?.fileHashes) return [];
|
|
2191
|
+
|
|
2192
|
+
let shippable;
|
|
2193
|
+
try {
|
|
2194
|
+
shippable = deps.shippable || getShippableFilenames();
|
|
2195
|
+
} catch {
|
|
2196
|
+
return [];
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
// A shippable set this small means the registry did not load, not that UDS
|
|
2200
|
+
// stopped shipping everything. Without this arm a broken install would empty
|
|
2201
|
+
// the manifest and every downstream check would go quiet at the same time.
|
|
2202
|
+
if (!shippable || shippable.size < 50) return [];
|
|
2203
|
+
|
|
2204
|
+
const dropped = [];
|
|
2205
|
+
for (const recorded of Object.keys(manifest.fileHashes)) {
|
|
2206
|
+
const normalized = recorded.replace(/\\/g, '/');
|
|
2207
|
+
if (!normalized.startsWith('.standards/')) continue;
|
|
2208
|
+
const fileName = normalized.split('/').pop();
|
|
2209
|
+
if (shippable.has(fileName)) continue;
|
|
2210
|
+
if (existsSync(join(projectPath, recorded))) continue;
|
|
2211
|
+
delete manifest.fileHashes[recorded];
|
|
2212
|
+
dropped.push(recorded);
|
|
2213
|
+
}
|
|
2214
|
+
return dropped.sort();
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
/**
|
|
2218
|
+
* Build the config `--sync-refs` regenerates an integration file from.
|
|
2219
|
+
*
|
|
2220
|
+
* The stored `integrationConfigs[file]` is a snapshot taken when the file was
|
|
2221
|
+
* installed. Its `categories` are the thing sync-refs exists to update, and its
|
|
2222
|
+
* `installedStandards` is the thing that must never be trusted: it is not
|
|
2223
|
+
* refreshed by any path, so on a project upgraded across majors it still lists
|
|
2224
|
+
* standards that stopped shipping, and whatever it lists becomes the file's
|
|
2225
|
+
* content. That is how a manifest holding 73 standards produced a CLAUDE.md
|
|
2226
|
+
* announcing 76, which `uds check` then flagged against the same manifest.
|
|
2227
|
+
*
|
|
2228
|
+
* So: manifest-derived fields win, the caller's freshly computed categories win
|
|
2229
|
+
* over both, and anything the manifest knows nothing about (timestamps, fields
|
|
2230
|
+
* a future version adds) is carried through untouched.
|
|
2231
|
+
*
|
|
2232
|
+
* `outputLanguage` is the one place the stored value still gets a say — a
|
|
2233
|
+
* manifest with no `output_language` should not silently reset a project that
|
|
2234
|
+
* chose one before the option was recorded there.
|
|
2235
|
+
*
|
|
2236
|
+
* @param {Object} manifest - Project manifest
|
|
2237
|
+
* @param {string} toolName - Tool key (e.g. 'claude-code')
|
|
2238
|
+
* @param {Object} storedConfig - The existing integrationConfigs entry
|
|
2239
|
+
* @param {string[]} expectedCategories - Categories computed from current standards
|
|
2240
|
+
* @returns {Object} Config for writeIntegrationFile
|
|
2241
|
+
*/
|
|
2242
|
+
export function refreshIntegrationConfig(manifest, toolName, storedConfig = {}, expectedCategories = []) {
|
|
2243
|
+
const outputLanguage =
|
|
2244
|
+
manifest.options?.output_language
|
|
2245
|
+
|| manifest.options?.commit_language
|
|
2246
|
+
|| storedConfig.outputLanguage
|
|
2247
|
+
|| storedConfig.commitLanguage
|
|
2248
|
+
|| 'english';
|
|
2249
|
+
|
|
2250
|
+
const fresh = buildToolIntegrationConfig(
|
|
2251
|
+
{ ...manifest, options: { ...(manifest.options || {}), output_language: outputLanguage } },
|
|
2252
|
+
toolName
|
|
2253
|
+
);
|
|
2254
|
+
|
|
2255
|
+
return {
|
|
2256
|
+
...storedConfig,
|
|
2257
|
+
...fresh,
|
|
2258
|
+
tool: toolName,
|
|
2259
|
+
categories: expectedCategories,
|
|
2260
|
+
outputLanguage,
|
|
2261
|
+
format: manifest.format || storedConfig.format || 'ai'
|
|
2262
|
+
};
|
|
2263
|
+
}
|
|
2264
|
+
|
|
1843
2265
|
/**
|
|
1844
2266
|
* Sync integration file references based on manifest standards
|
|
1845
2267
|
* @param {string} projectPath - Project path
|
|
@@ -1851,6 +2273,16 @@ async function syncIntegrationReferences(projectPath, manifest, { plan = false }
|
|
|
1851
2273
|
console.log(chalk.cyan(msg.syncingRefs));
|
|
1852
2274
|
console.log();
|
|
1853
2275
|
|
|
2276
|
+
// Rebuild the config when it is absent rather than sending the user away —
|
|
2277
|
+
// the manifest holds everything it is derived from.
|
|
2278
|
+
if (!manifest.integrationConfigs || Object.keys(manifest.integrationConfigs).length === 0) {
|
|
2279
|
+
const filled = backfillIntegrationConfigs(manifest);
|
|
2280
|
+
if (filled > 0) {
|
|
2281
|
+
console.log(chalk.gray(` ${(msg.integrationConfigsBackfilled || 'Rebuilt integration config for {count} file(s) from the manifest.').replace('{count}', filled)}`));
|
|
2282
|
+
console.log();
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
|
|
1854
2286
|
// Check if integrationConfigs exists
|
|
1855
2287
|
if (!manifest.integrationConfigs || Object.keys(manifest.integrationConfigs).length === 0) {
|
|
1856
2288
|
console.log(chalk.yellow(msg.noIntegrationConfigs));
|
|
@@ -1903,14 +2335,9 @@ async function syncIntegrationReferences(projectPath, manifest, { plan = false }
|
|
|
1903
2335
|
continue;
|
|
1904
2336
|
}
|
|
1905
2337
|
|
|
1906
|
-
// Regenerate the integration file with updated categories
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
tool: toolName,
|
|
1910
|
-
categories: expectedCategories,
|
|
1911
|
-
// Pass output_language for dynamic commit standards generation
|
|
1912
|
-
outputLanguage: manifest.options?.output_language || manifest.options?.commit_language || config.outputLanguage || config.commitLanguage || 'english'
|
|
1913
|
-
};
|
|
2338
|
+
// Regenerate the integration file with updated categories, from the
|
|
2339
|
+
// manifest rather than from the snapshot stored beside it.
|
|
2340
|
+
const newConfig = refreshIntegrationConfig(manifest, toolName, config, expectedCategories);
|
|
1914
2341
|
|
|
1915
2342
|
if (plan) {
|
|
1916
2343
|
console.log(chalk.yellow(` ~ ${integrationPath}: categories would change to ${expectedCategories.join(', ') || '(none)'} (dry run — nothing is written)`));
|
|
@@ -1930,15 +2357,8 @@ async function syncIntegrationReferences(projectPath, manifest, { plan = false }
|
|
|
1930
2357
|
generatedAt: now
|
|
1931
2358
|
};
|
|
1932
2359
|
|
|
1933
|
-
//
|
|
1934
|
-
|
|
1935
|
-
if (hashInfo) {
|
|
1936
|
-
if (!manifest.fileHashes) {
|
|
1937
|
-
manifest.fileHashes = {};
|
|
1938
|
-
}
|
|
1939
|
-
const normalizedIntPath = integrationPath.replace(/\\/g, '/');
|
|
1940
|
-
manifest.fileHashes[normalizedIntPath] = { ...hashInfo, installedAt: now };
|
|
1941
|
-
}
|
|
2360
|
+
// XSPEC-418 R6: no whole-file fileHashes entry — only the UDS block is
|
|
2361
|
+
// tracked, below.
|
|
1942
2362
|
|
|
1943
2363
|
// Track integration block hash for UDS content integrity
|
|
1944
2364
|
if (result.blockHashInfo) {
|
|
@@ -1958,7 +2378,7 @@ async function syncIntegrationReferences(projectPath, manifest, { plan = false }
|
|
|
1958
2378
|
|
|
1959
2379
|
// Update manifest version and save
|
|
1960
2380
|
if (updatedCount > 0) {
|
|
1961
|
-
manifest
|
|
2381
|
+
bumpManifestVersion(manifest);
|
|
1962
2382
|
refreshIntegrationBlockHashes(manifest, projectPath);
|
|
1963
2383
|
if (plan) {
|
|
1964
2384
|
console.log(chalk.gray(' (dry run — the manifest was not written)'));
|
|
@@ -1995,6 +2415,66 @@ async function syncIntegrationReferences(projectPath, manifest, { plan = false }
|
|
|
1995
2415
|
* "Write then restore" leaves a broken tree if it dies halfway, which is worse
|
|
1996
2416
|
* than having no dry run at all — the same reasoning as the integrations plan.
|
|
1997
2417
|
*/
|
|
2418
|
+
/**
|
|
2419
|
+
* A general `uds update --plan` (no --skills/--commands scope) never called
|
|
2420
|
+
* planSkills/planCommands — those are the only two places version staleness
|
|
2421
|
+
* is computed, and they only run under `--plan --skills`/`--plan --commands`.
|
|
2422
|
+
* An adopter running plain `--plan` saw a clean reconciliation plan and
|
|
2423
|
+
* nothing else, with Skills or Commands a version behind and no hint that a
|
|
2424
|
+
* scoped plan would have said so. This prints a short, best-effort note —
|
|
2425
|
+
* not a full plan — so the general path is never silent about it.
|
|
2426
|
+
*
|
|
2427
|
+
* Skills staleness is read the same way planSkills does: per-installation,
|
|
2428
|
+
* from what is actually on disk (`getInstalledSkillsInfoForAgent`), because
|
|
2429
|
+
* different agents/levels can be out of sync independently. Commands have no
|
|
2430
|
+
* per-installation version on disk (only a file count), so Commands
|
|
2431
|
+
* staleness is read from the one version the manifest itself records
|
|
2432
|
+
* (`manifest.commands.version`, written by every path that installs
|
|
2433
|
+
* commands) against the latest version UDS ships. // implements XSPEC adopter-report Q3
|
|
2434
|
+
*
|
|
2435
|
+
* @param {string} projectPath
|
|
2436
|
+
* @param {Object} manifest
|
|
2437
|
+
*/
|
|
2438
|
+
function reportStaleSkillsCommandsHint(projectPath, manifest) {
|
|
2439
|
+
const repoInfo = getRepositoryInfo();
|
|
2440
|
+
const latestVersion = repoInfo.skills.version;
|
|
2441
|
+
const stale = [];
|
|
2442
|
+
|
|
2443
|
+
const skillsInstallations = (manifest.skills?.installations || []).filter((i) => i.level !== 'marketplace');
|
|
2444
|
+
for (const inst of skillsInstallations) {
|
|
2445
|
+
const info = getInstalledSkillsInfoForAgent(inst.agent, inst.level, projectPath);
|
|
2446
|
+
const current = info?.version;
|
|
2447
|
+
if (current && current !== latestVersion) {
|
|
2448
|
+
stale.push({
|
|
2449
|
+
label: `${getAgentDisplayName(inst.agent)} (${inst.level}): Skills v${current} → v${latestVersion}`,
|
|
2450
|
+
flag: '--skills'
|
|
2451
|
+
});
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
if (manifest.commands?.installed && (manifest.commands?.installations || []).length > 0) {
|
|
2456
|
+
const current = manifest.commands.version;
|
|
2457
|
+
if (current && current !== latestVersion) {
|
|
2458
|
+
stale.push({
|
|
2459
|
+
label: `Commands: v${current} → v${latestVersion}`,
|
|
2460
|
+
flag: '--commands'
|
|
2461
|
+
});
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
if (stale.length === 0) return;
|
|
2466
|
+
|
|
2467
|
+
console.log(chalk.yellow(' Skills/Commands installed but out of date:'));
|
|
2468
|
+
for (const s of stale) {
|
|
2469
|
+
console.log(chalk.gray(` ~ ${s.label}`));
|
|
2470
|
+
}
|
|
2471
|
+
const flags = [...new Set(stale.map((s) => s.flag))];
|
|
2472
|
+
for (const flag of flags) {
|
|
2473
|
+
console.log(chalk.gray(` Run \`uds update --apply --yes ${flag}\` to update.`));
|
|
2474
|
+
}
|
|
2475
|
+
console.log();
|
|
2476
|
+
}
|
|
2477
|
+
|
|
1998
2478
|
async function planSkills(projectPath, manifest, options) {
|
|
1999
2479
|
const repoInfo = getRepositoryInfo();
|
|
2000
2480
|
const latestVersion = repoInfo.skills.version;
|
|
@@ -2159,7 +2639,7 @@ async function updateSkillsOnly(projectPath, manifest, options) {
|
|
|
2159
2639
|
return;
|
|
2160
2640
|
}
|
|
2161
2641
|
|
|
2162
|
-
const spinner =
|
|
2642
|
+
const spinner = createSpinner(msg.installingSkills || 'Installing Skills...').start();
|
|
2163
2643
|
|
|
2164
2644
|
const skillsLocaleForUpdate = resolveLocale(manifest, projectPath, options);
|
|
2165
2645
|
const result = await installSkillsToMultipleAgents(
|
|
@@ -2283,7 +2763,7 @@ async function updateCommandsOnly(projectPath, manifest, options) {
|
|
|
2283
2763
|
}
|
|
2284
2764
|
console.log();
|
|
2285
2765
|
|
|
2286
|
-
const spinner =
|
|
2766
|
+
const spinner = createSpinner(msg.installingCommands || 'Installing commands...').start();
|
|
2287
2767
|
|
|
2288
2768
|
const commandsLocale = resolveLocale(manifest, projectPath, options);
|
|
2289
2769
|
const result = await installCommandsToMultipleAgents(
|
|
@@ -2779,8 +3259,8 @@ async function handleRollback(projectPath) {
|
|
|
2779
3259
|
/**
|
|
2780
3260
|
* Handle --plan: show what the reconciler would do without executing.
|
|
2781
3261
|
*/
|
|
2782
|
-
async function handlePlan(projectPath, options) {
|
|
2783
|
-
const spinner =
|
|
3262
|
+
async function handlePlan(projectPath, options, manifest) {
|
|
3263
|
+
const spinner = createSpinner('Calculating reconciliation plan...').start();
|
|
2784
3264
|
|
|
2785
3265
|
const result = await reconcilerPlan(projectPath, { force: false });
|
|
2786
3266
|
|
|
@@ -2796,6 +3276,12 @@ async function handlePlan(projectPath, options) {
|
|
|
2796
3276
|
console.log(formatPlan(result.plan));
|
|
2797
3277
|
console.log();
|
|
2798
3278
|
|
|
3279
|
+
// XSPEC adopter-report Q3: the reconciliation plan above never covers
|
|
3280
|
+
// Skills/Commands version staleness — only --plan --skills/--commands did.
|
|
3281
|
+
if (manifest) {
|
|
3282
|
+
reportStaleSkillsCommandsHint(projectPath, manifest);
|
|
3283
|
+
}
|
|
3284
|
+
|
|
2799
3285
|
if (result.plan.actions.length > 0) {
|
|
2800
3286
|
// NOT `uds update`. That runs the legacy path, which never executes this
|
|
2801
3287
|
// plan — it refreshes existing standards and reports success for having done
|
|
@@ -2855,7 +3341,7 @@ async function handleReconcile(projectPath, options, { force }) {
|
|
|
2855
3341
|
}
|
|
2856
3342
|
|
|
2857
3343
|
// Execute
|
|
2858
|
-
const spinner =
|
|
3344
|
+
const spinner = createSpinner('Applying reconciliation plan...').start();
|
|
2859
3345
|
|
|
2860
3346
|
const result = await reconcile(projectPath, {
|
|
2861
3347
|
force,
|