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.
- 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 +29 -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/MIGRATION-v6.md +8 -4
- package/bundled/locales/zh-TW/CHANGELOG.md +30 -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/MIGRATION-v6.md +8 -4
- package/package.json +7 -6
- package/src/commands/check.js +161 -24
- package/src/commands/config.js +19 -19
- package/src/commands/init.js +13 -4
- package/src/commands/spec.js +2 -2
- package/src/commands/update.js +251 -29
- package/src/i18n/messages.js +36 -0
- package/src/installers/integration-installer.js +4 -4
- 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/diff-engine.js +57 -2
- package/src/reconciler/plan-executor.js +31 -11
- package/src/utils/integration-generator.js +209 -81
- package/src/utils/reference-sync.js +103 -7
- package/src/utils/registry.js +57 -0
- package/src/utils/spinner.js +31 -0
- package/standards-registry.json +8 -8
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import
|
|
2
|
+
import { createSpinner } from '../utils/spinner.js';
|
|
3
3
|
import { basename, join } from 'path';
|
|
4
4
|
import {
|
|
5
5
|
getAllStandards,
|
|
@@ -75,7 +75,7 @@ export async function installStandards(config, projectPath) {
|
|
|
75
75
|
};
|
|
76
76
|
|
|
77
77
|
// Start copying standards
|
|
78
|
-
const copySpinner =
|
|
78
|
+
const copySpinner = createSpinner(msg.copyingStandards).start();
|
|
79
79
|
|
|
80
80
|
// Copy standards based on format
|
|
81
81
|
const formatsToUse = config.format === 'both' ? ['ai', 'human'] : [config.format];
|
|
@@ -113,7 +113,7 @@ export async function installStandards(config, projectPath) {
|
|
|
113
113
|
const localeExtension = (config.displayLanguage === 'zh-tw' || config.displayLanguage === 'zh-cn') ? config.displayLanguage : null;
|
|
114
114
|
|
|
115
115
|
if (config.languages.length > 0 || config.frameworks.length > 0 || localeExtension) {
|
|
116
|
-
const extSpinner =
|
|
116
|
+
const extSpinner = createSpinner(msg.copyingExtensions).start();
|
|
117
117
|
|
|
118
118
|
for (const lang of config.languages) {
|
|
119
119
|
if (EXTENSION_MAPPINGS[lang]) {
|
package/src/prompts/init.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { select, checkbox, confirm as inquirerConfirm, Separator, input } from '@inquirer/prompts';
|
|
2
|
-
import { existsSync, writeFileSync, readdirSync } from 'fs';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import os from 'os';
|
|
@@ -1385,11 +1385,40 @@ export function suggestCommands(projectPath) {
|
|
|
1385
1385
|
if (existsSync(p('Gemfile')))
|
|
1386
1386
|
return { test: 'bundle exec rspec', lint: 'bundle exec rubocop', build: 'gem build *.gemspec', security: 'bundle audit' };
|
|
1387
1387
|
|
|
1388
|
-
// C# — glob for .csproj / .sln
|
|
1388
|
+
// C# — glob for .csproj / .sln.
|
|
1389
|
+
//
|
|
1390
|
+
// `.csproj` has named two incompatible build systems since 2017. An SDK-style
|
|
1391
|
+
// project (`<Project Sdk="…">`, `<TargetFramework>`) builds with the `dotnet`
|
|
1392
|
+
// CLI. An old-style one (`<TargetFrameworkVersion>`, no Sdk attribute) needs
|
|
1393
|
+
// MSBuild and Visual Studio's targets, and handing it `dotnet build` produces
|
|
1394
|
+
// `MSB4019: could not find Import project …Microsoft.WebApplication.targets`
|
|
1395
|
+
// — reported 2026-09-16 from a .NET Framework 4.8 Web App, which also got
|
|
1396
|
+
// `dotnet list package --vulnerable`, a command that silently reports nothing
|
|
1397
|
+
// for a `packages.config` project because it only reads `PackageReference`.
|
|
1398
|
+
//
|
|
1399
|
+
// For the old style only `build` gets an answer. `dotnet format` requires an
|
|
1400
|
+
// SDK-style project and there is no drop-in equivalent; test discovery
|
|
1401
|
+
// depends on which runner the solution uses. The unknown-ecosystem branch
|
|
1402
|
+
// below already returns blanks "so user fills them in", and a blank a human
|
|
1403
|
+
// completes beats a command that looks authoritative and fails on first use.
|
|
1389
1404
|
try {
|
|
1390
1405
|
const files = readdirSync(projectPath);
|
|
1391
|
-
|
|
1392
|
-
|
|
1406
|
+
const projectFiles = files.filter(f => f.endsWith('.csproj') || f.endsWith('.vbproj'));
|
|
1407
|
+
if (projectFiles.length > 0 || files.some(f => f.endsWith('.sln'))) {
|
|
1408
|
+
const oldStyle = projectFiles.some(f => {
|
|
1409
|
+
try {
|
|
1410
|
+
const xml = readFileSync(join(projectPath, f), 'utf-8');
|
|
1411
|
+
if (/<Project[^>]*\sSdk\s*=/i.test(xml)) return false;
|
|
1412
|
+
return /<TargetFrameworkVersion\s*>/i.test(xml);
|
|
1413
|
+
} catch {
|
|
1414
|
+
return false;
|
|
1415
|
+
}
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
return oldStyle
|
|
1419
|
+
? { test: '', lint: '', build: 'msbuild /t:Build /p:Configuration=Release', security: '' }
|
|
1420
|
+
: { test: 'dotnet test', lint: 'dotnet format --verify-no-changes', build: 'dotnet build', security: 'dotnet list package --vulnerable' };
|
|
1421
|
+
}
|
|
1393
1422
|
} catch {
|
|
1394
1423
|
// ignore read errors
|
|
1395
1424
|
}
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* Integration files use migrate_block: only the UDS marker block is replaced.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { isShippedFilename } from '../utils/registry.js';
|
|
18
|
+
|
|
17
19
|
/**
|
|
18
20
|
* @typedef {Object} PlanAction
|
|
19
21
|
* @property {'create'|'update'|'delete'|'migrate_block'|'patch_hook'} type
|
|
@@ -205,12 +207,18 @@ function diffCategory(desiredMap, actualMap, category, actions, warnings, summar
|
|
|
205
207
|
// Check actual entries not in desired → delete (only if UDS-managed)
|
|
206
208
|
for (const [key, actualEntry] of actualMap) {
|
|
207
209
|
if (!desiredMap.has(key)) {
|
|
208
|
-
if (
|
|
210
|
+
if (isUserConfigPath(actualEntry.relativePath)) {
|
|
211
|
+
// Said out loud rather than skipped quietly: a plan that silently omits
|
|
212
|
+
// things is how the previous round of deletion bugs stayed invisible.
|
|
213
|
+
warnings.push(
|
|
214
|
+
`Keeping ${actualEntry.relativePath}: it holds a setting you chose (uds config writes it), not content UDS ships`
|
|
215
|
+
);
|
|
216
|
+
} else if (isUDSManaged(actualEntry)) {
|
|
209
217
|
actions.push({
|
|
210
218
|
type: 'delete',
|
|
211
219
|
category,
|
|
212
220
|
path: actualEntry.relativePath,
|
|
213
|
-
reason:
|
|
221
|
+
reason: describeDeletion(actualEntry.relativePath, category),
|
|
214
222
|
details: {
|
|
215
223
|
currentHash: actualEntry.hash,
|
|
216
224
|
metadata: actualEntry.metadata
|
|
@@ -330,6 +338,53 @@ function diffIntegrations(desiredMap, actualMap, actions, warnings, summary, for
|
|
|
330
338
|
* Files in .standards/ are always UDS-managed.
|
|
331
339
|
* Skill/command entries from manifest installations are UDS-managed.
|
|
332
340
|
*/
|
|
341
|
+
/**
|
|
342
|
+
* Files under `.standards/` that hold a choice the user made, not content UDS
|
|
343
|
+
* ships.
|
|
344
|
+
*
|
|
345
|
+
* `release-config.yaml` is written by `uds init` from the release mode the user
|
|
346
|
+
* picked and rewritten by `uds config`. Nothing in the desired-state calculator
|
|
347
|
+
* models it, so it fell out of every diff as "no longer in desired state" and
|
|
348
|
+
* into the delete plan — a plan that discards a user setting and calls that
|
|
349
|
+
* reconciliation. Reported 2026-09-16 by an adopter who could not tell from the
|
|
350
|
+
* plan whether the file held anything of theirs. It did.
|
|
351
|
+
*
|
|
352
|
+
* @param {string} relativePath
|
|
353
|
+
* @returns {boolean}
|
|
354
|
+
*/
|
|
355
|
+
export function isUserConfigPath(relativePath) {
|
|
356
|
+
const normalized = String(relativePath || '').replace(/\\/g, '/');
|
|
357
|
+
return normalized === '.standards/release-config.yaml';
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Why this file is being deleted, in terms the adopter can act on.
|
|
362
|
+
*
|
|
363
|
+
* "no longer in desired state" was the reason given for a standard UDS retired
|
|
364
|
+
* upstream, an option the project deselected, and a path the desired state does
|
|
365
|
+
* not model — three different situations, three different right responses, one
|
|
366
|
+
* sentence. The distinction that matters most is the first one: if UDS still
|
|
367
|
+
* ships the file, the change came from this project and is reversible by
|
|
368
|
+
* re-selecting it; if UDS does not, it is gone upstream and re-selecting it will
|
|
369
|
+
* not bring it back.
|
|
370
|
+
*
|
|
371
|
+
* @param {string} relativePath
|
|
372
|
+
* @param {string} category - 'standard' | 'option' | 'skill' | 'command' | …
|
|
373
|
+
* @returns {string}
|
|
374
|
+
*/
|
|
375
|
+
export function describeDeletion(relativePath, category) {
|
|
376
|
+
const normalized = String(relativePath || '').replace(/\\/g, '/');
|
|
377
|
+
|
|
378
|
+
if (category === 'standard' || category === 'option') {
|
|
379
|
+
const fileName = normalized.split('/').pop();
|
|
380
|
+
return isShippedFilename(fileName)
|
|
381
|
+
? 'this project no longer selects it (UDS still ships it, so re-selecting restores it)'
|
|
382
|
+
: 'UDS no longer ships this file (re-selecting will not bring it back)';
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return `not part of what this project's manifest asks for (category: ${category})`;
|
|
386
|
+
}
|
|
387
|
+
|
|
333
388
|
function isUDSManaged(entry) {
|
|
334
389
|
// Files in .standards/ are always UDS-managed
|
|
335
390
|
if (entry.relativePath.startsWith('.standards/')) return true;
|
|
@@ -436,6 +436,22 @@ async function executeSkillBatch(projectPath, skillActions, manifest) {
|
|
|
436
436
|
return results;
|
|
437
437
|
}
|
|
438
438
|
|
|
439
|
+
/**
|
|
440
|
+
* Hash-map key for a command action: `<agent>/<file>`.
|
|
441
|
+
*
|
|
442
|
+
* `commandHashes` is keyed by agent and filename; a plan action carries a
|
|
443
|
+
* project-relative path (`.opencode/command/atdd.md`). Deleting by the wrong key
|
|
444
|
+
* leaves the entry behind and the next `check` reports the file missing.
|
|
445
|
+
*/
|
|
446
|
+
function commandHashKey(action) {
|
|
447
|
+
const meta = action.details?.metadata;
|
|
448
|
+
if (meta?.agent && meta?.commandName) return `${meta.agent}/${meta.commandName}.md`;
|
|
449
|
+
const parts = action.path.split('/').filter(Boolean);
|
|
450
|
+
const agent = parts[0] ? parts[0].replace(/^\./, '') : null;
|
|
451
|
+
const file = parts[parts.length - 1];
|
|
452
|
+
return agent && file ? `${agent}/${file}` : action.path;
|
|
453
|
+
}
|
|
454
|
+
|
|
439
455
|
/**
|
|
440
456
|
* Batch execute command installations.
|
|
441
457
|
*/
|
|
@@ -452,6 +468,9 @@ async function executeCommandBatch(projectPath, commandActions, manifest) {
|
|
|
452
468
|
if (existsSync(targetPath)) {
|
|
453
469
|
rmSync(targetPath, { recursive: true, force: true });
|
|
454
470
|
}
|
|
471
|
+
// A deleted file must also lose its hash entry, or the next check calls
|
|
472
|
+
// it missing for good.
|
|
473
|
+
delete manifest.commandHashes[commandHashKey(action)];
|
|
455
474
|
results.push({ action, success: true });
|
|
456
475
|
} catch (err) {
|
|
457
476
|
results.push({ action, success: false, error: err.message });
|
|
@@ -475,17 +494,18 @@ async function executeCommandBatch(projectPath, commandActions, manifest) {
|
|
|
475
494
|
);
|
|
476
495
|
|
|
477
496
|
if (installResult.allFileHashes) {
|
|
478
|
-
//
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
497
|
+
// Merge, exactly as the skill path above does.
|
|
498
|
+
//
|
|
499
|
+
// 🔴 This used to delete every key whose prefix matched an agent in the
|
|
500
|
+
// result before merging. That is correct only when the installer
|
|
501
|
+
// reinstalled ALL of that agent's commands — which `uds update`'s own
|
|
502
|
+
// call sites do (they pass `commandNames = null`). Here the plan names a
|
|
503
|
+
// subset, so the wipe threw away the entry for every command the plan did
|
|
504
|
+
// not mention. Measured on 6.9.0 (2026-09-16): a three-file drift turned
|
|
505
|
+
// 51 tracked commands into 3, the other 48 stayed on disk untracked, and
|
|
506
|
+
// `uds check` printed "All command files intact (3 files)" in the same run
|
|
507
|
+
// as "Commands: 51 installed" — tampering with one of the 48 changed
|
|
508
|
+
// neither line.
|
|
489
509
|
Object.assign(manifest.commandHashes, installResult.allFileHashes);
|
|
490
510
|
}
|
|
491
511
|
|