universal-dev-standards 6.9.0 → 6.10.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.
Files changed (47) hide show
  1. package/bundled/core/agent-communication-protocol.md +8 -0
  2. package/bundled/core/branch-completion.md +8 -0
  3. package/bundled/core/change-batching-standards.md +8 -0
  4. package/bundled/core/execution-history.md +8 -0
  5. package/bundled/core/pipeline-integration-standards.md +8 -0
  6. package/bundled/core/workflow-enforcement.md +8 -0
  7. package/bundled/core/workflow-state-protocol.md +8 -0
  8. package/bundled/locales/zh-CN/CHANGELOG.md +29 -3
  9. package/bundled/locales/zh-CN/README.md +1 -1
  10. package/bundled/locales/zh-CN/SECURITY.md +1 -1
  11. package/bundled/locales/zh-CN/core/agent-communication-protocol.md +7 -0
  12. package/bundled/locales/zh-CN/core/branch-completion.md +7 -0
  13. package/bundled/locales/zh-CN/core/change-batching-standards.md +7 -0
  14. package/bundled/locales/zh-CN/core/execution-history.md +7 -0
  15. package/bundled/locales/zh-CN/core/pipeline-integration-standards.md +7 -0
  16. package/bundled/locales/zh-CN/core/workflow-enforcement.md +7 -0
  17. package/bundled/locales/zh-CN/core/workflow-state-protocol.md +7 -0
  18. package/bundled/locales/zh-CN/docs/MIGRATION-v6.md +8 -4
  19. package/bundled/locales/zh-TW/CHANGELOG.md +30 -3
  20. package/bundled/locales/zh-TW/README.md +1 -1
  21. package/bundled/locales/zh-TW/SECURITY.md +1 -1
  22. package/bundled/locales/zh-TW/core/agent-communication-protocol.md +7 -0
  23. package/bundled/locales/zh-TW/core/branch-completion.md +7 -0
  24. package/bundled/locales/zh-TW/core/change-batching-standards.md +7 -0
  25. package/bundled/locales/zh-TW/core/execution-history.md +7 -0
  26. package/bundled/locales/zh-TW/core/pipeline-integration-standards.md +7 -0
  27. package/bundled/locales/zh-TW/core/workflow-enforcement.md +7 -0
  28. package/bundled/locales/zh-TW/core/workflow-state-protocol.md +7 -0
  29. package/bundled/locales/zh-TW/docs/MIGRATION-v6.md +8 -4
  30. package/package.json +7 -6
  31. package/src/commands/check.js +161 -24
  32. package/src/commands/config.js +19 -19
  33. package/src/commands/init.js +13 -4
  34. package/src/commands/spec.js +2 -2
  35. package/src/commands/update.js +251 -29
  36. package/src/i18n/messages.js +36 -0
  37. package/src/installers/integration-installer.js +4 -4
  38. package/src/installers/skills-installer.js +4 -4
  39. package/src/installers/standards-installer.js +3 -3
  40. package/src/prompts/init.js +33 -4
  41. package/src/reconciler/diff-engine.js +57 -2
  42. package/src/reconciler/plan-executor.js +31 -11
  43. package/src/utils/integration-generator.js +209 -81
  44. package/src/utils/reference-sync.js +103 -7
  45. package/src/utils/registry.js +57 -0
  46. package/src/utils/spinner.js +31 -0
  47. package/standards-registry.json +8 -8
@@ -1,11 +1,11 @@
1
1
  import chalk from 'chalk';
2
- import ora from 'ora';
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';
8
+ import { getRepositoryInfo, getAllStandards, getShippableFilenames, getStandardSource } from '../utils/registry.js';
9
9
  import { computeFileHash, planStandardsRemovals, refreshIntegrationBlockHashes } from '../utils/hasher.js';
10
10
  import {
11
11
  writeIntegrationFile,
@@ -56,7 +56,7 @@ import {
56
56
  } from '../reconciler/index.js';
57
57
  import { restoreSingleFile } from './check.js';
58
58
  import { guardAgainstSelfAdoption } from '../utils/detect-self-adoption.js';
59
- import { resolveIntegrationFile } from '../core/constants.js';
59
+ import { resolveIntegrationFile, SUPPORTED_AI_TOOLS } from '../core/constants.js';
60
60
  import {
61
61
  mergeInstalledNames,
62
62
  recordFileProvenance,
@@ -274,7 +274,7 @@ function buildInstallCommand(pm, tag) {
274
274
  */
275
275
  async function updateCliAndExit(useBeta = false) {
276
276
  const msg = t().commands.update;
277
- const spinner = ora(msg.updatingCli).start();
277
+ const spinner = createSpinner(msg.updatingCli).start();
278
278
 
279
279
  try {
280
280
  // Command is hardcoded - no user input, safe from injection
@@ -581,6 +581,23 @@ export async function updateCommand(options) {
581
581
  if (versionComparison > 0) {
582
582
  console.log(chalk.gray(` ${msg.newerVersion.replace('{version}', currentVersion)}`));
583
583
  }
584
+
585
+ // Still forget records of files that are gone and no longer shipped. An
586
+ // adopter who followed MIGRATION-v6 §2 is on the current version by
587
+ // definition — telling them to run `uds update --prune` and then returning
588
+ // before anything is examined is how the complaint became unclearable.
589
+ const retiredOnLatest = pruneRetiredHashes(manifest, projectPath);
590
+ if (retiredOnLatest.length > 0) {
591
+ console.log();
592
+ console.log(chalk.gray(
593
+ ` ${(msg.droppedRetiredHashes || 'Dropped {count} manifest record(s) for files UDS no longer ships and that are already gone:').replace('{count}', retiredOnLatest.length)}`
594
+ ));
595
+ for (const path of retiredOnLatest) {
596
+ console.log(chalk.gray(` - ${path}`));
597
+ }
598
+ writeManifest(manifest, projectPath);
599
+ }
600
+
584
601
  console.log();
585
602
  return;
586
603
  }
@@ -642,7 +659,7 @@ export async function updateCommand(options) {
642
659
 
643
660
  // Perform update
644
661
  console.log();
645
- const spinner = ora(msg.updatingStandards).start();
662
+ const spinner = createSpinner(msg.updatingStandards).start();
646
663
 
647
664
  const results = {
648
665
  updated: [],
@@ -714,7 +731,7 @@ export async function updateCommand(options) {
714
731
  }
715
732
 
716
733
  if (shouldInstallNew) {
717
- const newSpinner = ora(msg.installingNewStandards).start();
734
+ const newSpinner = createSpinner(msg.installingNewStandards).start();
718
735
  let newCount = 0;
719
736
 
720
737
  for (const ns of newStandards) {
@@ -735,7 +752,7 @@ export async function updateCommand(options) {
735
752
 
736
753
  // Update integrations (unless --standards-only)
737
754
  if (!options.standardsOnly && manifest.integrations && manifest.integrations.length > 0) {
738
- const intSpinner = ora(msg.syncingIntegrations).start();
755
+ const intSpinner = createSpinner(msg.syncingIntegrations).start();
739
756
 
740
757
  // Build installed standards list
741
758
  // Raw, not basename()d. Resolution needs the registry (an ID is not a
@@ -813,6 +830,7 @@ export async function updateCommand(options) {
813
830
  installedAt: new Date().toISOString()
814
831
  };
815
832
  }
833
+ refreshTrackedFileHash(manifest, projectPath, agentsMdResult.path);
816
834
  }
817
835
  }
818
836
 
@@ -989,6 +1007,22 @@ export async function updateCommand(options) {
989
1007
  }
990
1008
  }
991
1009
 
1010
+ // Records of files that are already gone and that UDS no longer ships. Not
1011
+ // part of the removal plan above: that plan walks what is on disk, and these
1012
+ // are precisely the ones that are not. Reported rather than done quietly,
1013
+ // because a manifest shrinking without explanation is the shape this whole
1014
+ // release is about.
1015
+ const retiredHashes = pruneRetiredHashes(manifest, projectPath);
1016
+ if (retiredHashes.length > 0) {
1017
+ console.log();
1018
+ console.log(chalk.gray(
1019
+ ` ${(msg.droppedRetiredHashes || 'Dropped {count} manifest record(s) for files UDS no longer ships and that are already gone:').replace('{count}', retiredHashes.length)}`
1020
+ ));
1021
+ for (const path of retiredHashes) {
1022
+ console.log(chalk.gray(` - ${path}`));
1023
+ }
1024
+ }
1025
+
992
1026
  // Provenance is only a complete account of what we own once a run has
993
1027
  // written the whole installed set. Until then every unrecorded file is
994
1028
  // `unknown` rather than `foreign`, and nothing gets deleted on the strength
@@ -1110,7 +1144,7 @@ export async function updateCommand(options) {
1110
1144
  }
1111
1145
 
1112
1146
  if (shouldRestore) {
1113
- const restoreSpinner = ora((msg.restoringMissing || 'Restoring missing files...')).start();
1147
+ const restoreSpinner = createSpinner((msg.restoringMissing || 'Restoring missing files...')).start();
1114
1148
  const checkMsg = t().commands.check;
1115
1149
  let restoredCount = 0;
1116
1150
 
@@ -1177,7 +1211,7 @@ export async function updateCommand(options) {
1177
1211
 
1178
1212
  // Install Skills if user agreed
1179
1213
  if (installSkills.length > 0) {
1180
- const skillSpinner = ora(msg.installingNewSkills || 'Installing Skills...').start();
1214
+ const skillSpinner = createSpinner(msg.installingNewSkills || 'Installing Skills...').start();
1181
1215
  const skillsLocale = resolveLocale(manifest, projectPath, options);
1182
1216
  const skillResult = await installSkillsToMultipleAgents(installSkills, null, projectPath, skillsLocale);
1183
1217
 
@@ -1225,7 +1259,7 @@ export async function updateCommand(options) {
1225
1259
 
1226
1260
  // Update outdated Skills if user agreed
1227
1261
  if (updateSkills.length > 0) {
1228
- const updateSpinner = ora(msg.updatingSkills || 'Updating Skills...').start();
1262
+ const updateSpinner = createSpinner(msg.updatingSkills || 'Updating Skills...').start();
1229
1263
  const updateLocale = resolveLocale(manifest, projectPath, options);
1230
1264
  const updateResult = await installSkillsToMultipleAgents(updateSkills, null, projectPath, updateLocale);
1231
1265
 
@@ -1263,7 +1297,7 @@ export async function updateCommand(options) {
1263
1297
 
1264
1298
  // Install Commands if user agreed
1265
1299
  if (installCommands.length > 0) {
1266
- const cmdSpinner = ora(msg.installingNewCommands || 'Installing commands...').start();
1300
+ const cmdSpinner = createSpinner(msg.installingNewCommands || 'Installing commands...').start();
1267
1301
  const cmdLocale = resolveLocale(manifest, projectPath, options);
1268
1302
  const cmdResult = await installCommandsToMultipleAgents(installCommands, null, projectPath, cmdLocale);
1269
1303
 
@@ -1294,7 +1328,7 @@ export async function updateCommand(options) {
1294
1328
 
1295
1329
  // Update outdated Commands if user agreed
1296
1330
  if (updateCommands.length > 0) {
1297
- const updateCmdSpinner = ora(msg.updatingCommands || 'Updating Commands...').start();
1331
+ const updateCmdSpinner = createSpinner(msg.updatingCommands || 'Updating Commands...').start();
1298
1332
  const updateCmdLocale = resolveLocale(manifest, projectPath, options);
1299
1333
  const updateCmdResult = await installCommandsToMultipleAgents(updateCommands, null, projectPath, updateCmdLocale);
1300
1334
 
@@ -1370,7 +1404,7 @@ export async function updateCommand(options) {
1370
1404
  let hasChanges = false;
1371
1405
 
1372
1406
  if (missingSkills.length > 0) {
1373
- const spinner = ora(msg.installingNewSkills || 'Installing Skills...').start();
1407
+ const spinner = createSpinner(msg.installingNewSkills || 'Installing Skills...').start();
1374
1408
  const result = await installSkillsToMultipleAgents(missingSkills, null, projectPath, skillsLocale);
1375
1409
  if (!manifest.skills) manifest.skills = {};
1376
1410
  manifest.skills.installed = true;
@@ -1391,7 +1425,7 @@ export async function updateCommand(options) {
1391
1425
  }
1392
1426
 
1393
1427
  if (outdatedSkills.length > 0) {
1394
- const spinner = ora(msg.updatingSkills || 'Updating Skills...').start();
1428
+ const spinner = createSpinner(msg.updatingSkills || 'Updating Skills...').start();
1395
1429
  const result = await installSkillsToMultipleAgents(outdatedSkills, null, projectPath, skillsLocale);
1396
1430
  if (!manifest.skills) manifest.skills = {};
1397
1431
  manifest.skills.version = repoInfo.skills.version;
@@ -1415,7 +1449,7 @@ export async function updateCommand(options) {
1415
1449
  }
1416
1450
 
1417
1451
  if (missingCommands.length > 0) {
1418
- const spinner = ora(msg.installingNewCommands || 'Installing commands...').start();
1452
+ const spinner = createSpinner(msg.installingNewCommands || 'Installing commands...').start();
1419
1453
  const result = await installCommandsToMultipleAgents(missingCommands, null, projectPath, skillsLocale);
1420
1454
  if (!manifest.commands) manifest.commands = {};
1421
1455
  manifest.commands.installed = true;
@@ -1435,7 +1469,7 @@ export async function updateCommand(options) {
1435
1469
  }
1436
1470
 
1437
1471
  if (outdatedCommands.length > 0) {
1438
- const spinner = ora(msg.updatingCommands || 'Updating Commands...').start();
1472
+ const spinner = createSpinner(msg.updatingCommands || 'Updating Commands...').start();
1439
1473
  const result = await installCommandsToMultipleAgents(outdatedCommands, null, projectPath, skillsLocale);
1440
1474
  if (!manifest.commands) manifest.commands = {};
1441
1475
  manifest.commands.version = repoInfo.skills.version;
@@ -1688,6 +1722,7 @@ export function regenerateIntegrations(projectPath, manifest) {
1688
1722
  installedAt: now
1689
1723
  };
1690
1724
  }
1725
+ refreshTrackedFileHash(manifest, projectPath, agentsMdResult.path);
1691
1726
  }
1692
1727
  }
1693
1728
 
@@ -1811,7 +1846,7 @@ async function updateIntegrationsOnly(projectPath, manifest, options = {}) {
1811
1846
  return;
1812
1847
  }
1813
1848
 
1814
- const spinner = ora(msg.regeneratingIntegrations).start();
1849
+ const spinner = createSpinner(msg.regeneratingIntegrations).start();
1815
1850
 
1816
1851
  // Use reusable regeneration function
1817
1852
  const results = regenerateIntegrations(projectPath, manifest);
@@ -1840,6 +1875,188 @@ async function updateIntegrationsOnly(projectPath, manifest, options = {}) {
1840
1875
  process.exit(0);
1841
1876
  }
1842
1877
 
1878
+ /**
1879
+ * Rebuild `manifest.integrationConfigs` from what the manifest already records.
1880
+ *
1881
+ * 🔴 Only `uds init`'s interactive flow and `--sync-refs` itself ever wrote this
1882
+ * key; no update or reconcile path backfills it, so a project created with
1883
+ * `uds init -y` carries `{}` from the first day (reproduced on a clean 6.9.0
1884
+ * project, 2026-09-16). `--sync-refs` then refuses to run — and `uds check` was
1885
+ * recommending exactly that command. Everything the config holds is derivable
1886
+ * from the manifest, so derive it instead of demanding the user re-init.
1887
+ *
1888
+ * Existing entries are never overwritten: they may hold choices (categories,
1889
+ * content mode) that this derivation cannot know.
1890
+ *
1891
+ * @param {Object} manifest - Manifest object (mutated)
1892
+ * @returns {number} How many entries were added
1893
+ */
1894
+ export function backfillIntegrationConfigs(manifest) {
1895
+ if (!manifest.integrationConfigs) manifest.integrationConfigs = {};
1896
+
1897
+ const tools = manifest.aiTools?.length ? manifest.aiTools : [];
1898
+ const byFile = new Map();
1899
+ for (const tool of tools) {
1900
+ const file = resolveIntegrationFile(tool) || getToolFilePath(tool);
1901
+ if (file && !byFile.has(file)) byFile.set(file, tool);
1902
+ }
1903
+ // Integrations can be recorded as file names (`CLAUDE.md`) or tool keys.
1904
+ for (const entry of manifest.integrations || []) {
1905
+ const file = resolveIntegrationFile(entry) || (String(entry).includes('.') ? String(entry) : null);
1906
+ if (!file) continue;
1907
+ if (!byFile.has(file)) {
1908
+ const tool = getToolFromPath(file) || resolveToolKeyFromEntry(entry);
1909
+ if (tool) byFile.set(file, tool);
1910
+ }
1911
+ }
1912
+
1913
+ let added = 0;
1914
+ for (const [file, tool] of byFile) {
1915
+ if (manifest.integrationConfigs[file]) continue;
1916
+ manifest.integrationConfigs[file] = {
1917
+ ...buildToolIntegrationConfig(manifest, tool),
1918
+ format: manifest.format || 'ai',
1919
+ backfilledAt: new Date().toISOString()
1920
+ };
1921
+ added++;
1922
+ }
1923
+ return added;
1924
+ }
1925
+
1926
+ function resolveToolKeyFromEntry(entry) {
1927
+ const key = String(entry).replace(/\.[^.]+$/, '').toLowerCase();
1928
+ return SUPPORTED_AI_TOOLS?.[key] ? key : null;
1929
+ }
1930
+
1931
+ /**
1932
+ * If the manifest already tracks this file, make its record describe what is on
1933
+ * disk now.
1934
+ *
1935
+ * Every path that writes AGENTS.md recorded `integrationBlockHashes` and none
1936
+ * recorded `fileHashes`, so a project whose manifest tracked the file (an older
1937
+ * install, or one where AGENTS.md was written as a tool's integration file) got
1938
+ * `⚠ AGENTS.md (modified)` from `uds check` the moment UDS itself regenerated
1939
+ * it. UDS wrote the file and did not record what it wrote.
1940
+ *
1941
+ * Conditional on purpose. `uds init` does not enrol AGENTS.md in `fileHashes`,
1942
+ * and adopters are expected to extend that summary — a path that started
1943
+ * tracking it would convert every legitimate edit into a reported fault. This
1944
+ * only ever refreshes a record that already exists.
1945
+ *
1946
+ * @param {Object} manifest - Project manifest (mutated)
1947
+ * @param {string} projectPath - Project root
1948
+ * @param {string} relativePath - Path as UDS reports it
1949
+ * @returns {boolean} Whether a record was refreshed
1950
+ */
1951
+ export function refreshTrackedFileHash(manifest, projectPath, relativePath) {
1952
+ if (!manifest?.fileHashes || !relativePath) return false;
1953
+ const normalized = String(relativePath).replace(/\\/g, '/');
1954
+ if (!(normalized in manifest.fileHashes)) return false;
1955
+
1956
+ const hashInfo = computeFileHash(join(projectPath, normalized));
1957
+ if (!hashInfo) return false;
1958
+
1959
+ manifest.fileHashes[normalized] = { ...hashInfo, installedAt: new Date().toISOString() };
1960
+ return true;
1961
+ }
1962
+
1963
+ /**
1964
+ * Forget `fileHashes` records for files that are gone AND that UDS no longer
1965
+ * ships.
1966
+ *
1967
+ * `--prune` deletes files it finds while walking `.standards/`. A file the
1968
+ * adopter already removed by hand is not in that walk, so its record survived
1969
+ * every prune — and the record is what made `uds check` report the completed
1970
+ * migration as seven faults. Following MIGRATION-v6 §2 therefore produced a
1971
+ * complaint that no command could clear.
1972
+ *
1973
+ * This is not a deletion. The file is already absent and no registry entry can
1974
+ * ever resolve to it again, so the record cannot refer to anything. Both facts
1975
+ * are required: a missing file UDS still ships is a real fault and stays; a
1976
+ * descoped standard still on disk is `--prune`'s business, gated on ownership,
1977
+ * and is left alone here.
1978
+ *
1979
+ * @param {Object} manifest - Project manifest (mutated)
1980
+ * @param {string} projectPath - Project root
1981
+ * @param {{shippable?: Set<string>}} [deps] - Injection point for tests
1982
+ * @returns {string[]} Paths whose records were dropped, sorted
1983
+ */
1984
+ export function pruneRetiredHashes(manifest, projectPath, deps = {}) {
1985
+ if (!manifest?.fileHashes) return [];
1986
+
1987
+ let shippable;
1988
+ try {
1989
+ shippable = deps.shippable || getShippableFilenames();
1990
+ } catch {
1991
+ return [];
1992
+ }
1993
+
1994
+ // A shippable set this small means the registry did not load, not that UDS
1995
+ // stopped shipping everything. Without this arm a broken install would empty
1996
+ // the manifest and every downstream check would go quiet at the same time.
1997
+ if (!shippable || shippable.size < 50) return [];
1998
+
1999
+ const dropped = [];
2000
+ for (const recorded of Object.keys(manifest.fileHashes)) {
2001
+ const normalized = recorded.replace(/\\/g, '/');
2002
+ if (!normalized.startsWith('.standards/')) continue;
2003
+ const fileName = normalized.split('/').pop();
2004
+ if (shippable.has(fileName)) continue;
2005
+ if (existsSync(join(projectPath, recorded))) continue;
2006
+ delete manifest.fileHashes[recorded];
2007
+ dropped.push(recorded);
2008
+ }
2009
+ return dropped.sort();
2010
+ }
2011
+
2012
+ /**
2013
+ * Build the config `--sync-refs` regenerates an integration file from.
2014
+ *
2015
+ * The stored `integrationConfigs[file]` is a snapshot taken when the file was
2016
+ * installed. Its `categories` are the thing sync-refs exists to update, and its
2017
+ * `installedStandards` is the thing that must never be trusted: it is not
2018
+ * refreshed by any path, so on a project upgraded across majors it still lists
2019
+ * standards that stopped shipping, and whatever it lists becomes the file's
2020
+ * content. That is how a manifest holding 73 standards produced a CLAUDE.md
2021
+ * announcing 76, which `uds check` then flagged against the same manifest.
2022
+ *
2023
+ * So: manifest-derived fields win, the caller's freshly computed categories win
2024
+ * over both, and anything the manifest knows nothing about (timestamps, fields
2025
+ * a future version adds) is carried through untouched.
2026
+ *
2027
+ * `outputLanguage` is the one place the stored value still gets a say — a
2028
+ * manifest with no `output_language` should not silently reset a project that
2029
+ * chose one before the option was recorded there.
2030
+ *
2031
+ * @param {Object} manifest - Project manifest
2032
+ * @param {string} toolName - Tool key (e.g. 'claude-code')
2033
+ * @param {Object} storedConfig - The existing integrationConfigs entry
2034
+ * @param {string[]} expectedCategories - Categories computed from current standards
2035
+ * @returns {Object} Config for writeIntegrationFile
2036
+ */
2037
+ export function refreshIntegrationConfig(manifest, toolName, storedConfig = {}, expectedCategories = []) {
2038
+ const outputLanguage =
2039
+ manifest.options?.output_language
2040
+ || manifest.options?.commit_language
2041
+ || storedConfig.outputLanguage
2042
+ || storedConfig.commitLanguage
2043
+ || 'english';
2044
+
2045
+ const fresh = buildToolIntegrationConfig(
2046
+ { ...manifest, options: { ...(manifest.options || {}), output_language: outputLanguage } },
2047
+ toolName
2048
+ );
2049
+
2050
+ return {
2051
+ ...storedConfig,
2052
+ ...fresh,
2053
+ tool: toolName,
2054
+ categories: expectedCategories,
2055
+ outputLanguage,
2056
+ format: manifest.format || storedConfig.format || 'ai'
2057
+ };
2058
+ }
2059
+
1843
2060
  /**
1844
2061
  * Sync integration file references based on manifest standards
1845
2062
  * @param {string} projectPath - Project path
@@ -1851,6 +2068,16 @@ async function syncIntegrationReferences(projectPath, manifest, { plan = false }
1851
2068
  console.log(chalk.cyan(msg.syncingRefs));
1852
2069
  console.log();
1853
2070
 
2071
+ // Rebuild the config when it is absent rather than sending the user away —
2072
+ // the manifest holds everything it is derived from.
2073
+ if (!manifest.integrationConfigs || Object.keys(manifest.integrationConfigs).length === 0) {
2074
+ const filled = backfillIntegrationConfigs(manifest);
2075
+ if (filled > 0) {
2076
+ console.log(chalk.gray(` ${(msg.integrationConfigsBackfilled || 'Rebuilt integration config for {count} file(s) from the manifest.').replace('{count}', filled)}`));
2077
+ console.log();
2078
+ }
2079
+ }
2080
+
1854
2081
  // Check if integrationConfigs exists
1855
2082
  if (!manifest.integrationConfigs || Object.keys(manifest.integrationConfigs).length === 0) {
1856
2083
  console.log(chalk.yellow(msg.noIntegrationConfigs));
@@ -1903,14 +2130,9 @@ async function syncIntegrationReferences(projectPath, manifest, { plan = false }
1903
2130
  continue;
1904
2131
  }
1905
2132
 
1906
- // Regenerate the integration file with updated categories
1907
- const newConfig = {
1908
- ...config,
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
- };
2133
+ // Regenerate the integration file with updated categories, from the
2134
+ // manifest rather than from the snapshot stored beside it.
2135
+ const newConfig = refreshIntegrationConfig(manifest, toolName, config, expectedCategories);
1914
2136
 
1915
2137
  if (plan) {
1916
2138
  console.log(chalk.yellow(` ~ ${integrationPath}: categories would change to ${expectedCategories.join(', ') || '(none)'} (dry run — nothing is written)`));
@@ -2159,7 +2381,7 @@ async function updateSkillsOnly(projectPath, manifest, options) {
2159
2381
  return;
2160
2382
  }
2161
2383
 
2162
- const spinner = ora(msg.installingSkills || 'Installing Skills...').start();
2384
+ const spinner = createSpinner(msg.installingSkills || 'Installing Skills...').start();
2163
2385
 
2164
2386
  const skillsLocaleForUpdate = resolveLocale(manifest, projectPath, options);
2165
2387
  const result = await installSkillsToMultipleAgents(
@@ -2283,7 +2505,7 @@ async function updateCommandsOnly(projectPath, manifest, options) {
2283
2505
  }
2284
2506
  console.log();
2285
2507
 
2286
- const spinner = ora(msg.installingCommands || 'Installing commands...').start();
2508
+ const spinner = createSpinner(msg.installingCommands || 'Installing commands...').start();
2287
2509
 
2288
2510
  const commandsLocale = resolveLocale(manifest, projectPath, options);
2289
2511
  const result = await installCommandsToMultipleAgents(
@@ -2780,7 +3002,7 @@ async function handleRollback(projectPath) {
2780
3002
  * Handle --plan: show what the reconciler would do without executing.
2781
3003
  */
2782
3004
  async function handlePlan(projectPath, options) {
2783
- const spinner = ora('Calculating reconciliation plan...').start();
3005
+ const spinner = createSpinner('Calculating reconciliation plan...').start();
2784
3006
 
2785
3007
  const result = await reconcilerPlan(projectPath, { force: false });
2786
3008
 
@@ -2855,7 +3077,7 @@ async function handleReconcile(projectPath, options, { force }) {
2855
3077
  }
2856
3078
 
2857
3079
  // Execute
2858
- const spinner = ora('Applying reconciliation plan...').start();
3080
+ const spinner = createSpinner('Applying reconciliation plan...').start();
2859
3081
 
2860
3082
  const result = await reconcile(projectPath, {
2861
3083
  force,
@@ -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
@@ -841,6 +844,13 @@ export const messages = {
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
@@ -2069,6 +2084,13 @@ export const messages = {
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
@@ -3309,6 +3336,13 @@ export const messages = {
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,4 +1,4 @@
1
- import ora from 'ora';
1
+ import { createSpinner } from '../utils/spinner.js';
2
2
  import { copyIntegration } from '../utils/copier.js';
3
3
  import {
4
4
  writeIntegrationFile,
@@ -99,7 +99,7 @@ export async function installIntegrations(config, projectPath) {
99
99
  }
100
100
 
101
101
  // Start installation spinner
102
- const intSpinner = ora(msg.generatingIntegrations).start();
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();
@@ -224,7 +224,7 @@ export async function generateClaudeMd(config, projectPath) {
224
224
  return { path: null, error: null };
225
225
  }
226
226
 
227
- const claudeSpinner = ora(msg.generatingClaudeMd).start();
227
+ const claudeSpinner = createSpinner(msg.generatingClaudeMd).start();
228
228
 
229
229
  // Derive categories from installedStandards to avoid referencing non-installed standards
230
230
  const categories = installedStandards.length > 0
@@ -285,7 +285,7 @@ export async function generateUniversalAgentsMd(config, integrationResults, proj
285
285
  }
286
286
 
287
287
  const msg = t().commands.init;
288
- const spinner = ora(msg.generatingAgentsMd || 'Generating AGENTS.md...').start();
288
+ const spinner = createSpinner(msg.generatingAgentsMd || 'Generating AGENTS.md...').start();
289
289
 
290
290
  const outputLanguage = config.outputLanguage || config.commitLanguage ||
291
291
  config.standardOptions?.output_language || config.standardOptions?.commit_language ||
@@ -13,7 +13,7 @@
13
13
  * @version 1.0.0
14
14
  */
15
15
 
16
- import ora from 'ora';
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 = ora(messages.installingSkills).start();
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 = ora(messages.installingSkills).start();
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 = ora(messages.installingCommands || 'Installing slash commands...').start();
258
+ const cmdSpinner = createSpinner(messages.installingCommands || 'Installing slash commands...').start();
259
259
 
260
260
  const cmdResult = await installCommandsToMultipleAgents(
261
261
  skillsConfig.commandsInstallations,