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,13 +1,13 @@
1
1
  import chalk from 'chalk';
2
2
  import { select } from '@inquirer/prompts';
3
- import ora from 'ora';
3
+ import { createSpinner } from '../utils/spinner.js';
4
4
  import { existsSync, readFileSync } from 'fs';
5
5
  import { join, basename } from 'path';
6
6
  import { execSync } from 'child_process';
7
7
  import { readManifest, writeManifest, isInitialized, copyStandard, copyIntegration } from '../utils/copier.js';
8
8
  import {
9
9
  getAllStandards,
10
- getRepositoryInfo, resolveStandardFilename, resolveStandardSourcePath } from '../utils/registry.js';
10
+ getRepositoryInfo, isShippedFilename, resolveStandardFilename, resolveStandardSourcePath } from '../utils/registry.js';
11
11
  import {
12
12
  computeFileHash,
13
13
  compareFileHash,
@@ -27,6 +27,7 @@ import {
27
27
  } from '../config/ai-agent-paths.js';
28
28
  import {
29
29
  parseReferences,
30
+ findBrokenPathMentions,
30
31
  compareStandardsWithReferences
31
32
  } from '../utils/reference-sync.js';
32
33
  import { extractMarkedContent, getToolFilePath, parseStandardsIndexCount, writeIntegrationFile } from '../utils/integration-generator.js';
@@ -60,6 +61,19 @@ function displayFileIntegritySummary(fileStatus, msg) {
60
61
  }
61
62
  }
62
63
 
64
+ // Reported apart from `missing`, and in grey rather than red, because the
65
+ // adopter who sees this did what the migration guide asked. The remedy named
66
+ // here is the one that works: `--restore` cannot resolve a source for any of
67
+ // them, and used to be the only thing offered.
68
+ if (fileStatus.retired?.length > 0) {
69
+ console.log();
70
+ console.log(chalk.gray(` ${(msg.retiredHeader || '{count} tracked file(s) are no longer shipped by UDS:').replace('{count}', fileStatus.retired.length)}`));
71
+ for (const file of fileStatus.retired) {
72
+ console.log(chalk.gray(` - ${file} (${msg.retired || 'no longer shipped by UDS'})`));
73
+ }
74
+ console.log(chalk.gray(` ${msg.retiredHint || 'Run `uds update --prune` to drop their manifest entries.'}`));
75
+ }
76
+
63
77
  if (fileStatus.noHash.length > 0) {
64
78
  for (const file of fileStatus.noHash) {
65
79
  console.log(chalk.gray(` ? ${file} (${msg.existsNoHash})`));
@@ -71,10 +85,39 @@ function displayFileIntegritySummary(fileStatus, msg) {
71
85
  .replace('{unchanged}', fileStatus.unchanged.length)
72
86
  .replace('{modified}', fileStatus.modified.length)
73
87
  .replace('{missing}', fileStatus.missing.length)}` +
88
+ (fileStatus.retired?.length > 0 ? `, ${fileStatus.retired.length} ${msg.retired || 'no longer shipped'}` : '') +
74
89
  (fileStatus.noHash.length > 0 ? `, ${fileStatus.noHash.length} no hash` : '')));
75
90
  console.log();
76
91
  }
77
92
 
93
+ /**
94
+ * A file `fileHashes` tracks has gone. Is that a problem, or is it the migration
95
+ * guide's instruction carried out?
96
+ *
97
+ * MIGRATION-v6 §2 told adopters to delete the seven descoped `.ai.yaml` copies
98
+ * by hand. Doing so left the hash entries behind, and this check called each one
99
+ * `missing` and offered `uds check --restore` — a restore that then failed on
100
+ * every one with "could not determine source", because there has been no source
101
+ * upstream since 6.0.0. The tool asked the user to undo its own documentation,
102
+ * using a command it already knew could not work.
103
+ *
104
+ * The distinguishing question is whether UDS still ships a file by that name,
105
+ * which is the registry's to answer, not the manifest's. Anything outside
106
+ * `.standards/` is never retired: integration files are generated, not shipped
107
+ * under a standard's name, so a missing CLAUDE.md is exactly what it looks like.
108
+ *
109
+ * @param {string} relativePath - Path as recorded in fileHashes
110
+ * @param {Object} manifest - Project manifest (unused today; kept so callers
111
+ * need not know whether the answer depends on project state)
112
+ * @returns {'missing'|'retired'}
113
+ */
114
+ export function classifyMissingFile(relativePath, manifest) { // eslint-disable-line no-unused-vars
115
+ const normalized = String(relativePath).replace(/\\/g, '/');
116
+ if (!normalized.startsWith('.standards/')) return 'missing';
117
+ const fileName = normalized.split('/').pop();
118
+ return isShippedFilename(fileName) ? 'missing' : 'retired';
119
+ }
120
+
78
121
  /**
79
122
  * Perform integrity check for standards and integration files
80
123
  * @returns {Object} File status object
@@ -84,6 +127,10 @@ function performFileIntegrityCheck(projectPath, manifest, msg) {
84
127
  unchanged: [],
85
128
  modified: [],
86
129
  missing: [],
130
+ // Tracked, gone, and gone on purpose: UDS stopped shipping it. Counted
131
+ // apart from `missing` so the summary line stops calling a completed
132
+ // migration a fault.
133
+ retired: [],
87
134
  noHash: []
88
135
  };
89
136
 
@@ -101,7 +148,7 @@ function performFileIntegrityCheck(projectPath, manifest, msg) {
101
148
  fileStatus.modified.push(relativePath);
102
149
  break;
103
150
  case 'missing':
104
- fileStatus.missing.push(relativePath);
151
+ fileStatus[classifyMissingFile(relativePath, manifest)].push(relativePath);
105
152
  break;
106
153
  }
107
154
  }
@@ -1151,19 +1198,37 @@ function checkIntegrationFiles(manifest, projectPath, msg) {
1151
1198
  // After migrateStandardsPathsToIds(), manifest.standards contains IDs, so a plain
1152
1199
  // content.includes(id) check would fail for these mismatched entries.
1153
1200
  const allRegistryStds = getAllStandards();
1154
- const idToAiFilename = new Map(
1155
- allRegistryStds
1156
- .filter(s => s.source?.ai)
1157
- .map(s => [s.id, basename(s.source.ai)])
1201
+ // 🔴 `source` is a string for some entries and an object for others. Reading
1202
+ // `s.source.ai` on a string yields undefined, so `zh-tw-locale`
1203
+ // (source: 'extensions/locales/zh-tw.md') fell out of this map and the file it
1204
+ // installs — `.standards/zh-tw.md` — was never recognised: "67/68 項標準已參考,
1205
+ // 缺少: zh-tw-locale" on a project whose index lists it (reported 2026-09-16).
1206
+ const idToFilenames = new Map(
1207
+ allRegistryStds.map(s => {
1208
+ const sources = typeof s.source === 'string'
1209
+ ? [s.source]
1210
+ : [s.source?.ai, s.source?.human].filter(Boolean);
1211
+ return [s.id, sources.map(src => basename(src))];
1212
+ })
1158
1213
  );
1159
1214
 
1160
1215
  let hasIssues = false;
1161
1216
  let checkedCount = 0;
1162
1217
 
1218
+ // codex and opencode both write AGENTS.md, so iterating tools printed the same
1219
+ // file twice with identical verdicts (reported 2026-09-16). One file, one
1220
+ // verdict — the tools that share it are named on the line instead.
1221
+ const toolsByFile = new Map();
1163
1222
  for (const tool of manifest.aiTools) {
1164
- const toolFile = getToolFilePath(tool);
1165
- if (!toolFile) continue;
1223
+ const file = getToolFilePath(tool);
1224
+ if (!file) continue;
1225
+ if (!toolsByFile.has(file)) toolsByFile.set(file, []);
1226
+ toolsByFile.get(file).push(tool);
1227
+ }
1166
1228
 
1229
+ for (const [toolFile, sharingTools] of toolsByFile) {
1230
+ const tool = sharingTools[0];
1231
+ const sharedSuffix = sharingTools.length > 1 ? ` (${sharingTools.join(', ')})` : '';
1167
1232
  const fullPath = join(projectPath, toolFile);
1168
1233
 
1169
1234
  // Check if file exists
@@ -1201,9 +1266,9 @@ function checkIntegrationFiles(manifest, projectPath, msg) {
1201
1266
  // migration. For standards where the ID doesn't match the .ai.yaml basename
1202
1267
  // (e.g. ID "error-code-standards" → file "error-codes.ai.yaml"), we must
1203
1268
  // also check the actual filename so those aren't falsely reported as missing.
1204
- const aiFilename = idToAiFilename.get(stdFile);
1269
+ const filenames = idToFilenames.get(stdFile) || [];
1205
1270
  const isReferenced = content.includes(stdFile) ||
1206
- (aiFilename !== undefined && aiFilename !== stdFile && content.includes(aiFilename)) ||
1271
+ filenames.some(name => name !== stdFile && content.includes(name)) ||
1207
1272
  content.includes(`.standards/${stdFile}`) ||
1208
1273
  content.includes(`standards/${stdFile}`);
1209
1274
 
@@ -1224,7 +1289,7 @@ function checkIntegrationFiles(manifest, projectPath, msg) {
1224
1289
  const declaredCount = parseStandardsIndexCount(content);
1225
1290
  if (declaredCount !== null) {
1226
1291
  if (declaredCount === totalTrackable) {
1227
- console.log(chalk.green(` ✓ ${toolFile}:`));
1292
+ console.log(chalk.green(` ✓ ${toolFile}${sharedSuffix}:`));
1228
1293
  console.log(chalk.gray(` ${msg.standardsIndexPresent}`));
1229
1294
  console.log(chalk.gray(` ${msg.standardsIndexCount
1230
1295
  ? msg.standardsIndexCount.replace('{count}', declaredCount)
@@ -1404,9 +1469,12 @@ function checkReferenceSync(manifest, projectPath, msg) {
1404
1469
  }
1405
1470
 
1406
1471
  const references = parseReferences(content);
1407
- if (references.length === 0) continue;
1472
+ // Whole-file scan, separate from reference sync: the paths that matter here
1473
+ // are the ones outside the UDS block, which no generation ever revisits.
1474
+ const brokenMentions = findBrokenPathMentions(content, projectPath);
1475
+ if (references.length === 0 && brokenMentions.length === 0) continue;
1408
1476
 
1409
- results.push({ integrationPath, references });
1477
+ results.push({ integrationPath, references, brokenMentions });
1410
1478
  }
1411
1479
 
1412
1480
  // Skip entire section if no files have references
@@ -1416,14 +1484,42 @@ function checkReferenceSync(manifest, projectPath, msg) {
1416
1484
 
1417
1485
  let hasIssues = false;
1418
1486
 
1419
- for (const { integrationPath, references } of results) {
1487
+ for (const { integrationPath, references, brokenMentions } of results) {
1488
+ // Paths mentioned anywhere in the file that resolve to nothing. Reported
1489
+ // first and separately from reference sync, because these are usually
1490
+ // outside the UDS markers — left by an older `uds init`, never revisited by
1491
+ // any regeneration, and followed by an AI tool that then finds nothing.
1492
+ if (brokenMentions?.length > 0) {
1493
+ hasIssues = true;
1494
+ console.log(chalk.red(` ✗ ${integrationPath}:`));
1495
+ console.log(chalk.red(` ${msg.brokenMentions || 'mentions paths that do not exist in this project:'}`));
1496
+ for (const path of brokenMentions) {
1497
+ console.log(chalk.red(` - ${path}`));
1498
+ }
1499
+ console.log(chalk.gray(` ${msg.brokenMentionsFix || 'These are usually outside the UDS block and must be corrected by hand; UDS does not rewrite text it did not write.'}`));
1500
+ }
1501
+
1420
1502
  // Compare with manifest standards
1421
- const { orphanedRefs, missingRefs, syncedRefs } = compareStandardsWithReferences(
1503
+ const { orphanedRefs, missingRefs, syncedRefs, danglingRefs } = compareStandardsWithReferences(
1422
1504
  manifest.standards,
1423
- references
1505
+ references,
1506
+ { projectPath, options: manifest.options }
1424
1507
  );
1425
1508
 
1426
1509
  // Report results
1510
+ // 🔴 A reference to a file that is not on disk is the one failure an AI tool
1511
+ // cannot work around: it follows the link and finds nothing. Reported before
1512
+ // the manifest-level findings because it needs no interpretation.
1513
+ if (danglingRefs.length > 0) {
1514
+ hasIssues = true;
1515
+ console.log(chalk.red(` ✗ ${integrationPath}:`));
1516
+ console.log(chalk.red(` ${msg.danglingRefs || 'references files that do not exist:'}`));
1517
+ for (const ref of danglingRefs) {
1518
+ console.log(chalk.red(` - .standards/${ref}`));
1519
+ }
1520
+ console.log(chalk.gray(` ${msg.danglingRefsFix || 'Run `uds update --integrations-only` to regenerate them.'}`));
1521
+ }
1522
+
1427
1523
  if (orphanedRefs.length > 0) {
1428
1524
  hasIssues = true;
1429
1525
  console.log(chalk.yellow(` ⚠ ${integrationPath}:`));
@@ -1442,14 +1538,21 @@ function checkReferenceSync(manifest, projectPath, msg) {
1442
1538
  }
1443
1539
  }
1444
1540
 
1445
- if (orphanedRefs.length === 0 && missingRefs.length === 0) {
1541
+ if (orphanedRefs.length === 0 && missingRefs.length === 0 && danglingRefs.length === 0) {
1446
1542
  console.log(chalk.green(` ✓ ${msg.refsInSync.replace('{path}', integrationPath).replace('{count}', syncedRefs.length)}`));
1447
1543
  }
1448
1544
  }
1449
1545
 
1450
1546
  if (hasIssues) {
1451
1547
  console.log();
1452
- console.log(chalk.yellow(` ${msg.runSyncRefs}`));
1548
+ // `uds update --sync-refs` refuses to run without `integrationConfigs`, and a
1549
+ // project initialised non-interactively never had that key — so this hint
1550
+ // named a command that could not work (reproduced on a clean 6.9.0 project,
1551
+ // 2026-09-16). Name the one that regenerates from the manifest instead.
1552
+ const canSyncRefs = manifest.integrationConfigs && Object.keys(manifest.integrationConfigs).length > 0;
1553
+ console.log(chalk.yellow(` ${canSyncRefs
1554
+ ? msg.runSyncRefs
1555
+ : (msg.runIntegrationsOnly || 'Run `uds update --integrations-only` to regenerate integration files.')}`));
1453
1556
  }
1454
1557
 
1455
1558
  console.log();
@@ -1461,7 +1564,7 @@ function checkReferenceSync(manifest, projectPath, msg) {
1461
1564
  */
1462
1565
  async function checkCliVersion(bundledVersion) {
1463
1566
  const msg = t().commands.check;
1464
- const spinner = ora({ text: msg.checkingCliUpdates, spinner: 'dots' }).start();
1567
+ const spinner = createSpinner({ text: msg.checkingCliUpdates, spinner: 'dots' }).start();
1465
1568
 
1466
1569
  try {
1467
1570
  const result = await checkForUpdates(bundledVersion, {
@@ -1582,17 +1685,17 @@ function checkSkillsIntegrity(manifest, projectPath, msg) {
1582
1685
  * @param {Object} msg - Localized messages
1583
1686
  * @returns {Object} Status { unchanged: [], modified: [], missing: [] }
1584
1687
  */
1585
- function checkCommandsIntegrity(manifest, projectPath, msg) {
1688
+ export function checkCommandsIntegrity(manifest, projectPath, msg) {
1586
1689
  const commandHashes = manifest.commandHashes;
1587
1690
 
1588
1691
  // Skip if no command hashes tracked
1589
1692
  if (!commandHashes || Object.keys(commandHashes).length === 0) {
1590
- return { unchanged: [], modified: [], missing: [], tracked: false };
1693
+ return { unchanged: [], modified: [], missing: [], untracked: [], tracked: false };
1591
1694
  }
1592
1695
 
1593
1696
  console.log(chalk.cyan(msg.commandsIntegrityCheck || 'Commands File Integrity'));
1594
1697
 
1595
- const status = { unchanged: [], modified: [], missing: [], tracked: true };
1698
+ const status = { unchanged: [], modified: [], missing: [], untracked: [], tracked: true };
1596
1699
 
1597
1700
  for (const [hashKey, hashInfo] of Object.entries(commandHashes)) {
1598
1701
  // Parse key format: agent/filename.md
@@ -1632,8 +1735,42 @@ function checkCommandsIntegrity(manifest, projectPath, msg) {
1632
1735
  }
1633
1736
  }
1634
1737
 
1738
+ // 🔴 The loop above measures only what the manifest happens to track. When a
1739
+ // partial apply dropped 48 of 51 entries (6.9.0, fixed in plan-executor.js),
1740
+ // this section printed "All command files intact (3 files)" while the adoption
1741
+ // summary printed "Commands: 51 installed" from a directory listing — two
1742
+ // numbers, two sources, never compared, so the gap read as a pass.
1743
+ const trackedKeys = new Set(Object.keys(commandHashes));
1744
+ const agentsToScan = new Set(Object.keys(commandHashes).map(k => k.split('/')[0]));
1745
+ for (const entry of manifest.commands?.installations || []) {
1746
+ const agentName = typeof entry === 'string' ? entry : entry?.agent;
1747
+ if (agentName) agentsToScan.add(agentName);
1748
+ }
1749
+ for (const agentName of agentsToScan) {
1750
+ // Reuse the installer's own listing so this file filter cannot drift from it.
1751
+ const info = getInstalledCommandsForAgent(agentName, 'project', projectPath);
1752
+ if (!info?.installed) continue;
1753
+ const ext = agentName === 'gemini-cli' ? '.toml' : '.md';
1754
+ for (const name of info.commands || []) {
1755
+ const key = `${agentName}/${name}${ext}`;
1756
+ if (!trackedKeys.has(key)) status.untracked.push(key);
1757
+ }
1758
+ }
1759
+ if (status.untracked.length > 0) {
1760
+ console.log(chalk.yellow(` ⚠ ${(msg.commandsUntracked || '{count} installed command file(s) are covered by no hash — integrity is not checked for them')
1761
+ .replace('{count}', status.untracked.length)}`));
1762
+ for (const key of status.untracked.slice(0, 5)) {
1763
+ console.log(chalk.yellow(` - ${key}`));
1764
+ }
1765
+ if (status.untracked.length > 5) {
1766
+ console.log(chalk.yellow(` … +${status.untracked.length - 5}`));
1767
+ }
1768
+ console.log(chalk.gray(` ${msg.commandsUntrackedFix || 'Run `uds update --commands` to record them again.'}`));
1769
+ }
1770
+
1771
+
1635
1772
  // Summary
1636
- if (status.modified.length === 0 && status.missing.length === 0) {
1773
+ if (status.modified.length === 0 && status.missing.length === 0 && status.untracked.length === 0) {
1637
1774
  console.log(chalk.green(` ✓ ${msg.allCommandsIntact || 'All command files intact'} (${status.unchanged.length} files)`));
1638
1775
  } else {
1639
1776
  console.log(chalk.gray(` ${(msg.commandsIntegritySummary || '{unchanged} unchanged, {modified} modified, {missing} missing')
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import { select, confirm as inquirerConfirm } from '@inquirer/prompts';
3
- import ora from 'ora';
3
+ import { createSpinner } from '../utils/spinner.js';
4
4
  import { unlinkSync, existsSync, writeFileSync, mkdirSync } from 'fs';
5
5
  import { join, basename } from 'path';
6
6
  import { config } from '../utils/config-manager.js';
@@ -381,12 +381,12 @@ async function handleDisplayLanguageChange() {
381
381
  if (confirmReinstall) {
382
382
  const cmdLocale = displayLanguageToLocale(newLang);
383
383
  if (hasSkillInstalls) {
384
- const spinner = ora(t('config.reinstallingSkills', 'Reinstalling Skills...')).start();
384
+ const spinner = createSpinner(t('config.reinstallingSkills', 'Reinstalling Skills...')).start();
385
385
  await installSkillsToMultipleAgents(manifest.skills.installations, null, projectPath, cmdLocale);
386
386
  spinner.succeed(t('config.skillsReinstalled', 'Skills reinstalled'));
387
387
  }
388
388
  if (hasCommandInstalls) {
389
- const spinner = ora(t('config.reinstallingCommands', 'Reinstalling Commands...')).start();
389
+ const spinner = createSpinner(t('config.reinstallingCommands', 'Reinstalling Commands...')).start();
390
390
  await installCommandsToMultipleAgents(manifest.commands.installations, null, projectPath, cmdLocale);
391
391
  spinner.succeed(t('config.commandsReinstalled', 'Commands reinstalled'));
392
392
  }
@@ -401,7 +401,7 @@ async function handleDisplayLanguageChange() {
401
401
  });
402
402
 
403
403
  if (confirmRegen) {
404
- const spinner = ora(t('config.applyingPreset', 'Applying...')).start();
404
+ const spinner = createSpinner(t('config.applyingPreset', 'Applying...')).start();
405
405
  regenerateIntegrations(projectPath, manifest);
406
406
  spinner.succeed(t('config.integrationsRegenerated', 'Integrations regenerated'));
407
407
  }
@@ -665,7 +665,7 @@ export async function runProjectConfiguration(options) {
665
665
  newAITools = newAITools.filter(tool => !result.tools.includes(tool));
666
666
 
667
667
  // Remove integration files for removed tools
668
- const spinner = ora(msgObj.removingIntegrations).start();
668
+ const spinner = createSpinner(msgObj.removingIntegrations).start();
669
669
  for (const tool of result.tools) {
670
670
  const filePath = join(projectPath, getToolFilePath(tool));
671
671
  if (existsSync(filePath)) {
@@ -854,7 +854,7 @@ export async function runProjectConfiguration(options) {
854
854
  }
855
855
 
856
856
  // Apply changes
857
- const spinner = ora(msgObj.updatingConfig).start();
857
+ const spinner = createSpinner(msgObj.updatingConfig).start();
858
858
 
859
859
  const results = {
860
860
  copied: [],
@@ -914,7 +914,7 @@ export async function runProjectConfiguration(options) {
914
914
 
915
915
  // Regenerate integration files if needed
916
916
  if (needsIntegrationRegeneration && newAITools.length > 0) {
917
- const intSpinner = ora(msgObj.regeneratingIntegrations).start();
917
+ const intSpinner = createSpinner(msgObj.regeneratingIntegrations).start();
918
918
 
919
919
  // Build installed standards list
920
920
  // Raw, not basename()d. Resolution needs the registry (an ID is not a
@@ -989,7 +989,7 @@ export async function runProjectConfiguration(options) {
989
989
 
990
990
  if (confirmSkills) {
991
991
  const newInstallations = skillCapableTools.map(agent => ({ agent, level }));
992
- const spinner = ora(t('config.installingSkillsForNewTools', 'Installing Skills for new tools...')).start();
992
+ const spinner = createSpinner(t('config.installingSkillsForNewTools', 'Installing Skills for new tools...')).start();
993
993
  const skillResult = await installSkillsToMultipleAgents(newInstallations, null, projectPath, cmdLocale);
994
994
  spinner.succeed(t('config.skillsInstalledForNewTools', 'Skills installed for new tools'));
995
995
 
@@ -1026,7 +1026,7 @@ export async function runProjectConfiguration(options) {
1026
1026
 
1027
1027
  if (confirmCmds) {
1028
1028
  const newCmdInstallations = commandCapableTools.map(agent => ({ agent, level }));
1029
- const spinner = ora(t('config.installingCommandsForNewTools', 'Installing Commands for new tools...')).start();
1029
+ const spinner = createSpinner(t('config.installingCommandsForNewTools', 'Installing Commands for new tools...')).start();
1030
1030
  const cmdResult = await installCommandsToMultipleAgents(newCmdInstallations, null, projectPath, cmdLocale);
1031
1031
  spinner.succeed(t('config.commandsInstalledForNewTools', 'Commands installed for new tools'));
1032
1032
 
@@ -1124,7 +1124,7 @@ export async function runProjectConfiguration(options) {
1124
1124
 
1125
1125
  if (options.yes) {
1126
1126
  // --yes flag: auto-apply without prompting
1127
- const applySpinner = ora(msgObj.applyingChanges).start();
1127
+ const applySpinner = createSpinner(msgObj.applyingChanges).start();
1128
1128
  const applyResults = regenerateIntegrations(projectPath, manifest);
1129
1129
  applySpinner.succeed(msgObj.changesApplied || msgObj.regeneratedIntegrations.replace('{count}', applyResults.updated.length));
1130
1130
 
@@ -1146,7 +1146,7 @@ export async function runProjectConfiguration(options) {
1146
1146
  });
1147
1147
 
1148
1148
  if (apply) {
1149
- const applySpinner = ora(msgObj.applyingChanges).start();
1149
+ const applySpinner = createSpinner(msgObj.applyingChanges).start();
1150
1150
  const applyResults = regenerateIntegrations(projectPath, manifest);
1151
1151
  applySpinner.succeed(msgObj.changesApplied || msgObj.regeneratedIntegrations.replace('{count}', applyResults.updated.length));
1152
1152
 
@@ -1172,12 +1172,12 @@ export async function runProjectConfiguration(options) {
1172
1172
  if (hasSkillInstalls || hasCommandInstalls) {
1173
1173
  const cmdLocale = displayLanguageToLocale(newOptions.display_language);
1174
1174
  if (hasSkillInstalls) {
1175
- const skillSpinner = ora(t('config.reinstallingSkills', 'Reinstalling Skills...')).start();
1175
+ const skillSpinner = createSpinner(t('config.reinstallingSkills', 'Reinstalling Skills...')).start();
1176
1176
  await installSkillsToMultipleAgents(manifest.skills.installations, null, projectPath, cmdLocale);
1177
1177
  skillSpinner.succeed(t('config.skillsReinstalled', 'Skills reinstalled'));
1178
1178
  }
1179
1179
  if (hasCommandInstalls) {
1180
- const cmdSpinner = ora(t('config.reinstallingCommands', 'Reinstalling Commands...')).start();
1180
+ const cmdSpinner = createSpinner(t('config.reinstallingCommands', 'Reinstalling Commands...')).start();
1181
1181
  await installCommandsToMultipleAgents(manifest.commands.installations, null, projectPath, cmdLocale);
1182
1182
  cmdSpinner.succeed(t('config.commandsReinstalled', 'Commands reinstalled'));
1183
1183
  }
@@ -1222,7 +1222,7 @@ async function handleSkillsConfiguration(manifest, projectPath, msgObj, common,
1222
1222
 
1223
1223
  // Install to specified level (defaults to project)
1224
1224
  const installations = [{ agent: specificTool, level }];
1225
- const spinner = ora(`Installing Skills for ${getAgentDisplayName(specificTool)} (${level} level)...`).start();
1225
+ const spinner = createSpinner(`Installing Skills for ${getAgentDisplayName(specificTool)} (${level} level)...`).start();
1226
1226
  const result = await installSkillsToMultipleAgents(installations, null, projectPath);
1227
1227
  spinner.stop();
1228
1228
 
@@ -1359,7 +1359,7 @@ async function handleSkillsConfiguration(manifest, projectPath, msgObj, common,
1359
1359
  }));
1360
1360
 
1361
1361
  // Install Skills
1362
- const spinner = ora(msgObj.installingSkills || 'Installing Skills...').start();
1362
+ const spinner = createSpinner(msgObj.installingSkills || 'Installing Skills...').start();
1363
1363
  const result = await installSkillsToMultipleAgents(installations, null, projectPath);
1364
1364
  spinner.stop();
1365
1365
 
@@ -1394,7 +1394,7 @@ async function handleSkillsConfiguration(manifest, projectPath, msgObj, common,
1394
1394
  }
1395
1395
 
1396
1396
  // Install Skills
1397
- const spinner = ora(msgObj.installingSkills || 'Installing Skills...').start();
1397
+ const spinner = createSpinner(msgObj.installingSkills || 'Installing Skills...').start();
1398
1398
  const result = await installSkillsToMultipleAgents(installations, null, projectPath);
1399
1399
  spinner.stop();
1400
1400
 
@@ -1453,7 +1453,7 @@ async function handleCommandsConfiguration(manifest, projectPath, msgObj, common
1453
1453
 
1454
1454
  // Install to selected level
1455
1455
  const installations = [{ agent: specificTool, level: commandsLevel }];
1456
- const spinner = ora(`Installing Commands for ${getAgentDisplayName(specificTool)} (${commandsLevel} level)...`).start();
1456
+ const spinner = createSpinner(`Installing Commands for ${getAgentDisplayName(specificTool)} (${commandsLevel} level)...`).start();
1457
1457
  const cmdLocale = displayLanguageToLocale(manifest.options?.display_language);
1458
1458
  const result = await installCommandsToMultipleAgents(installations, null, projectPath, cmdLocale);
1459
1459
  spinner.stop();
@@ -1559,7 +1559,7 @@ async function handleCommandsConfiguration(manifest, projectPath, msgObj, common
1559
1559
  }
1560
1560
 
1561
1561
  // Install Commands
1562
- const spinner = ora(msgObj.installingCommands || 'Installing Commands...').start();
1562
+ const spinner = createSpinner(msgObj.installingCommands || 'Installing Commands...').start();
1563
1563
  const cmdLocale = displayLanguageToLocale(manifest.options?.display_language);
1564
1564
  const result = await installCommandsToMultipleAgents(declinedCommandsWithSupport, null, projectPath, cmdLocale);
1565
1565
  spinner.stop();
@@ -1595,7 +1595,7 @@ async function handleCommandsConfiguration(manifest, projectPath, msgObj, common
1595
1595
  }
1596
1596
 
1597
1597
  // Install Commands
1598
- const spinner = ora(msgObj.installingCommands || 'Installing Commands...').start();
1598
+ const spinner = createSpinner(msgObj.installingCommands || 'Installing Commands...').start();
1599
1599
  const cmdLocale = displayLanguageToLocale(manifest.options?.display_language);
1600
1600
  const result = await installCommandsToMultipleAgents(selectedAgents, null, projectPath, cmdLocale);
1601
1601
  spinner.stop();
@@ -1,8 +1,8 @@
1
1
  import chalk from 'chalk';
2
- import ora from 'ora';
2
+ import { createSpinner } from '../utils/spinner.js';
3
3
  import { execSync } from 'child_process';
4
4
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
5
- import { basename, join } from 'path';
5
+ import { join } from 'path';
6
6
  import {
7
7
  manifestExists as isInitialized,
8
8
  normalizeContentMode,
@@ -55,7 +55,7 @@ export async function initCommand(options) {
55
55
  }
56
56
 
57
57
  // STEP 2: Detect project characteristics
58
- const spinner = ora(msg.detectingProject).start();
58
+ const spinner = createSpinner(msg.detectingProject).start();
59
59
  const detected = detectAll(projectPath);
60
60
  spinner.succeed(msg.analysisComplete);
61
61
 
@@ -123,7 +123,16 @@ export async function initCommand(options) {
123
123
  apply: async () => {
124
124
  // 1. Install Standards
125
125
  const standardsResults = await installStandards(config, projectPath);
126
- config.installedStandards = standardsResults.standards.map(s => basename(s));
126
+ // Paths, not basenames. Everything downstream classifies an option
127
+ // by an `/options/` segment, so reducing to basenames here is not a
128
+ // simplification — it deletes the only signal that distinguishes the
129
+ // two, and `contentLayout: flat` installs option files with no
130
+ // directory to recover it from. The index block consequently said
131
+ // "options 0" on every project for two releases, while the manifest
132
+ // right beside it listed seven. `buildToolIntegrationConfig` (the
133
+ // update path's equivalent) has carried a comment warning against
134
+ // exactly this reduction the whole time; only this call site did it.
135
+ config.installedStandards = standardsResults.standards;
127
136
 
128
137
  // 1.5. Generate release-config.yaml if non-default mode selected
129
138
  if (config.releaseMode && config.releaseMode !== 'ci-cd') {
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import chalk from 'chalk';
12
- import ora from 'ora';
12
+ import { createSpinner } from '../utils/spinner.js';
13
13
  import { select, confirm as inquirerConfirm } from '@inquirer/prompts';
14
14
  import { existsSync, readFileSync } from 'node:fs';
15
15
  import { join } from 'node:path';
@@ -48,7 +48,7 @@ export async function specCreateCommand(intent, options = {}) {
48
48
  output: options.output
49
49
  });
50
50
 
51
- const spinner = ora({
51
+ const spinner = createSpinner({
52
52
  text: t('spec.generating', 'Generating micro-spec...'),
53
53
  color: 'cyan'
54
54
  }).start();