universal-dev-standards 6.1.1 → 6.2.1
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/locales/zh-CN/CHANGELOG.md +40 -3
- package/bundled/locales/zh-CN/README.md +75 -33
- package/bundled/locales/zh-CN/SECURITY.md +1 -1
- package/bundled/locales/zh-CN/core/behavior-snapshot.md +2 -2
- package/bundled/locales/zh-CN/core/data-migration-testing.md +2 -2
- package/bundled/locales/zh-CN/docs/CHEATSHEET.md +4 -4
- package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +9 -3
- package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +10 -11
- package/bundled/locales/zh-CN/docs/USER-MANUAL.md +40 -21
- package/bundled/locales/zh-CN/integrations/gemini-cli/README.md +12 -0
- package/bundled/locales/zh-CN/skills/ac-coverage/SKILL.md +11 -5
- package/bundled/locales/zh-CN/skills/adr-assistant/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/commands/brainstorm.md +2 -2
- package/bundled/locales/zh-CN/skills/commit-standards/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/contract-test-assistant/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/deploy-assistant/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/dev-methodology/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/dev-workflow-guide/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/journey-test-assistant/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/knowledge-graph/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/knowledge-graph/guide.md +2 -2
- package/bundled/locales/zh-CN/skills/migration-assistant/SKILL.md +188 -2
- package/bundled/locales/zh-CN/skills/observability-assistant/guide.md +2 -2
- package/bundled/locales/zh-CN/skills/orchestrate/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/plan/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/push/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/retrospective-assistant/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/reverse-engineer/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/runbook-assistant/guide.md +2 -2
- package/bundled/locales/zh-CN/skills/skill-builder/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/slo-assistant/guide.md +2 -2
- package/bundled/locales/zh-CN/skills/spec-derivation/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/spec-driven-dev/SKILL.md +40 -2
- package/bundled/locales/zh-CN/skills/sweep/SKILL.md +2 -2
- package/bundled/locales/zh-CN/skills/testing-guide/SKILL.md +2 -2
- package/bundled/locales/zh-TW/CHANGELOG.md +41 -3
- package/bundled/locales/zh-TW/README.md +75 -33
- package/bundled/locales/zh-TW/SECURITY.md +1 -1
- package/bundled/locales/zh-TW/core/audit-trail.md +2 -2
- package/bundled/locales/zh-TW/core/behavior-snapshot.md +2 -2
- package/bundled/locales/zh-TW/core/browser-compatibility-standards.md +18 -7
- package/bundled/locales/zh-TW/docs/CHEATSHEET.md +4 -4
- package/bundled/locales/zh-TW/docs/CLI-INIT-OPTIONS.md +9 -3
- package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +10 -11
- package/bundled/locales/zh-TW/docs/USER-MANUAL.md +40 -21
- package/bundled/locales/zh-TW/integrations/gemini-cli/README.md +12 -0
- package/bundled/skills/commands/journey-test.md +80 -6
- package/bundled/skills/commands/skill-builder.md +75 -6
- package/package.json +3 -3
- package/src/commands/check.js +54 -7
- package/src/commands/config.js +9 -0
- package/src/commands/init.js +15 -1
- package/src/commands/release.js +1 -1
- package/src/commands/update.js +87 -31
- package/src/config/ai-agent-paths.js +47 -7
- package/src/core/constants.js +61 -1
- package/src/core/manifest.js +56 -0
- package/src/flow/flow-parser.js +1 -1
- package/src/flow/gate-loader.js +1 -1
- package/src/i18n/messages.js +6 -0
- package/src/installers/standards-installer.js +10 -20
- package/src/reconciler/actual-state-scanner.js +118 -43
- package/src/reconciler/desired-state-calculator.js +157 -43
- package/src/reconciler/diff-engine.js +7 -2
- package/src/reconciler/manifest-migrator.js +5 -2
- package/src/reconciler/plan-executor.js +53 -14
- package/src/uninstallers/integration-uninstaller.js +7 -1
- package/src/utils/config-loader.js +1 -1
- package/src/utils/config-manager.js +1 -1
- package/src/utils/github.js +5 -1
- package/src/utils/hasher.js +7 -4
- package/src/utils/integration-generator.js +121 -78
- package/src/utils/registry.js +39 -0
- package/src/utils/skills-installer.js +49 -34
- package/src/utils/skills-source.js +51 -0
- package/src/utils/standard-fixer.js +1 -1
- package/src/utils/standard-validator.js +1 -1
- package/standards-registry.json +21 -11
|
@@ -9,13 +9,24 @@
|
|
|
9
9
|
|
|
10
10
|
import { join, basename } from 'path';
|
|
11
11
|
import { getAllStandards, getStandardSource, findOption, getOptionSource } from '../utils/registry.js';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
resolveToolKey,
|
|
14
|
+
SUPPORTED_AI_TOOLS,
|
|
15
|
+
MANIFEST_OPTION_BINDINGS,
|
|
16
|
+
OPTIONS_INSTALL_DIR
|
|
17
|
+
} from '../core/constants.js';
|
|
13
18
|
import { PathResolver } from '../core/paths.js';
|
|
14
19
|
import { computeFileHash } from '../utils/hasher.js';
|
|
15
20
|
import {
|
|
16
21
|
getSkillsDirForAgent,
|
|
17
|
-
getCommandsDirForAgent
|
|
22
|
+
getCommandsDirForAgent,
|
|
23
|
+
getCommandFileExtension
|
|
18
24
|
} from '../config/ai-agent-paths.js';
|
|
25
|
+
import {
|
|
26
|
+
getAvailableSkillNames,
|
|
27
|
+
getAvailableCommandNames
|
|
28
|
+
} from '../utils/skills-installer.js';
|
|
29
|
+
import { MARKETPLACE_NAMES_SENTINEL } from '../core/manifest.js';
|
|
19
30
|
|
|
20
31
|
/**
|
|
21
32
|
* @typedef {Object} FileEntry
|
|
@@ -62,6 +73,9 @@ export function calculateDesiredState(projectPath, manifest) {
|
|
|
62
73
|
// 2. Option files
|
|
63
74
|
calculateOptions(state, manifest);
|
|
64
75
|
|
|
76
|
+
// 2b. Extension files (locale/language/framework add-ons)
|
|
77
|
+
calculateExtensions(state, manifest);
|
|
78
|
+
|
|
65
79
|
// 3. Integration files (CLAUDE.md, .cursorrules, etc.)
|
|
66
80
|
calculateIntegrations(state, manifest);
|
|
67
81
|
|
|
@@ -137,6 +151,19 @@ function calculateStandards(state, manifest) {
|
|
|
137
151
|
|
|
138
152
|
/**
|
|
139
153
|
* Calculate expected option files.
|
|
154
|
+
*
|
|
155
|
+
* `manifest.options` is flat — `{ workflow: 'github-flow', test_levels: [...] }` —
|
|
156
|
+
* and the registry nests options under a standard id and a category key whose
|
|
157
|
+
* name is not always the same word (`test_levels` vs `test_level`). This function
|
|
158
|
+
* used to iterate the manifest as if its keys were *standard ids*, look up a
|
|
159
|
+
* standard named `workflow`, find none, and `continue`. It therefore produced an
|
|
160
|
+
* **empty** desired option set for every adopter repo, and every installed option
|
|
161
|
+
* file diffed as "no longer in desired state" — including files the manifest had
|
|
162
|
+
* explicitly selected. EngramGraph's plan proposed deleting all seven of the
|
|
163
|
+
* options its own manifest names. (XSPEC-343 R2)
|
|
164
|
+
*
|
|
165
|
+
* The desired path is flat too: the installer copies into `.standards/options`,
|
|
166
|
+
* not into a `<standardId>/<categoryKey>/` tree.
|
|
140
167
|
*/
|
|
141
168
|
function calculateOptions(state, manifest) {
|
|
142
169
|
const format = manifest.format || 'ai';
|
|
@@ -144,47 +171,89 @@ function calculateOptions(state, manifest) {
|
|
|
144
171
|
|
|
145
172
|
if (!manifest.options) return;
|
|
146
173
|
|
|
147
|
-
for (const
|
|
148
|
-
|
|
174
|
+
for (const binding of MANIFEST_OPTION_BINDINGS) {
|
|
175
|
+
const key = binding.manifestKeys.find(k => manifest.options[k] != null);
|
|
176
|
+
if (!key) continue;
|
|
149
177
|
|
|
150
|
-
const registryEntry = allStandards.find(s => s.id === standardId);
|
|
178
|
+
const registryEntry = allStandards.find(s => s.id === binding.standardId);
|
|
151
179
|
if (!registryEntry) continue;
|
|
152
180
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
181
|
+
const selection = manifest.options[key];
|
|
182
|
+
const selections = Array.isArray(selection) ? selection : [selection];
|
|
183
|
+
|
|
184
|
+
for (const optionId of selections) {
|
|
185
|
+
if (typeof optionId !== 'string') continue;
|
|
186
|
+
|
|
187
|
+
const option = findOption(registryEntry, binding.categoryKey, optionId);
|
|
188
|
+
if (!option) continue;
|
|
189
|
+
|
|
190
|
+
const source = getOptionSource(option, format);
|
|
191
|
+
if (!source) continue;
|
|
192
|
+
|
|
193
|
+
const relativePath = `${OPTIONS_INSTALL_DIR}/${basename(source)}`;
|
|
194
|
+
const absSource = PathResolver.getStandardSource(source);
|
|
195
|
+
|
|
196
|
+
let hash = null;
|
|
197
|
+
let size = null;
|
|
198
|
+
if (absSource) {
|
|
199
|
+
const hashInfo = computeFileHash(absSource);
|
|
200
|
+
if (hashInfo) {
|
|
201
|
+
hash = hashInfo.hash;
|
|
202
|
+
size = hashInfo.size;
|
|
176
203
|
}
|
|
204
|
+
}
|
|
177
205
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
206
|
+
state.options.set(relativePath, {
|
|
207
|
+
relativePath,
|
|
208
|
+
hash,
|
|
209
|
+
size,
|
|
210
|
+
category: 'option',
|
|
211
|
+
sourcePath: absSource,
|
|
212
|
+
metadata: { standardId: binding.standardId, categoryKey: binding.categoryKey, optionId, format }
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Calculate expected extension files.
|
|
220
|
+
*
|
|
221
|
+
* `manifest.extensions` records add-ons installed alongside the standards —
|
|
222
|
+
* locale packs (`extensions/locales/zh-tw.md`), language style guides, framework
|
|
223
|
+
* patterns — copied flat into `.standards/` by the installer.
|
|
224
|
+
*
|
|
225
|
+
* The calculator had no branch for them at all: the word "extensions" appeared
|
|
226
|
+
* exactly once in the whole reconciler, in the scanner's empty initialiser. So
|
|
227
|
+
* every installed extension fell outside the desired state and was proposed for
|
|
228
|
+
* deletion, while `manifest.extensions` went on listing it. Three repos lost
|
|
229
|
+
* their 717-line Traditional Chinese locale pack to this before it was noticed —
|
|
230
|
+
* including one where the removal was applied and committed. (XSPEC-343 R2)
|
|
231
|
+
*/
|
|
232
|
+
function calculateExtensions(state, manifest) {
|
|
233
|
+
for (const source of (manifest.extensions || [])) {
|
|
234
|
+
if (typeof source !== 'string' || !source) continue;
|
|
235
|
+
|
|
236
|
+
const relativePath = `.standards/${basename(source)}`;
|
|
237
|
+
const absSource = PathResolver.getStandardSource(source);
|
|
238
|
+
|
|
239
|
+
let hash = null;
|
|
240
|
+
let size = null;
|
|
241
|
+
if (absSource) {
|
|
242
|
+
const hashInfo = computeFileHash(absSource);
|
|
243
|
+
if (hashInfo) {
|
|
244
|
+
hash = hashInfo.hash;
|
|
245
|
+
size = hashInfo.size;
|
|
186
246
|
}
|
|
187
247
|
}
|
|
248
|
+
|
|
249
|
+
state.standards.set(relativePath, {
|
|
250
|
+
relativePath,
|
|
251
|
+
hash,
|
|
252
|
+
size,
|
|
253
|
+
category: 'standard',
|
|
254
|
+
sourcePath: absSource,
|
|
255
|
+
metadata: { extensionSource: source }
|
|
256
|
+
});
|
|
188
257
|
}
|
|
189
258
|
}
|
|
190
259
|
|
|
@@ -193,8 +262,19 @@ function calculateOptions(state, manifest) {
|
|
|
193
262
|
* For integrations we track the UDS marker block, not the entire file.
|
|
194
263
|
*/
|
|
195
264
|
function calculateIntegrations(state, manifest) {
|
|
196
|
-
for (const
|
|
197
|
-
|
|
265
|
+
for (const entry of (manifest.integrations || [])) {
|
|
266
|
+
// Accept both shapes. `manifest.integrations` is normalised to tool keys by
|
|
267
|
+
// the v3.5.0 migration, but a repo whose manifest has not been re-read and
|
|
268
|
+
// rewritten still holds file paths — and treating those as unknown tools
|
|
269
|
+
// silently produced an EMPTY desired state for integrations, which the
|
|
270
|
+
// reconciler then read as "delete CLAUDE.md's UDS block". Twenty of
|
|
271
|
+
// twenty-one adopter repos were in that state on 2026-07-30.
|
|
272
|
+
//
|
|
273
|
+
// Tolerating the legacy shape here rather than relying on migration alone
|
|
274
|
+
// is deliberate: the failure mode is silent and destructive, and the reader
|
|
275
|
+
// is the last place that can still tell the difference.
|
|
276
|
+
const toolName = resolveToolKey(entry);
|
|
277
|
+
const toolConfig = toolName ? SUPPORTED_AI_TOOLS[toolName] : null;
|
|
198
278
|
if (!toolConfig) continue;
|
|
199
279
|
|
|
200
280
|
const relativePath = toolConfig.file;
|
|
@@ -217,13 +297,35 @@ function calculateIntegrations(state, manifest) {
|
|
|
217
297
|
|
|
218
298
|
/**
|
|
219
299
|
* Calculate expected skill files.
|
|
300
|
+
*
|
|
301
|
+
* The desired set is **what this UDS version ships**, not `manifest.skills.names`.
|
|
302
|
+
* That field is written once by `init` and by no other code path — across five UDS
|
|
303
|
+
* upgrades it stayed frozen at its original 32 entries while the shipped set grew
|
|
304
|
+
* to 55, so every skill installed after init counted as "not desired" and was
|
|
305
|
+
* proposed for deletion. `uds update` always installs the full set (every call site
|
|
306
|
+
* passes `skillNames = null`), so the shipped list *is* the desired list.
|
|
307
|
+
* (XSPEC-343 R1/R2 — 40 of machine-setup's 86 proposed deletions.)
|
|
220
308
|
*/
|
|
221
309
|
function calculateSkills(state, projectPath, manifest) {
|
|
222
310
|
const skills = manifest.skills;
|
|
223
311
|
if (!skills || !skills.installed) return;
|
|
224
312
|
|
|
225
|
-
|
|
226
|
-
|
|
313
|
+
// Marketplace installs live inside the plugin, not the project. There is no
|
|
314
|
+
// project-level desired state to compute, and computing one would mark every
|
|
315
|
+
// on-disk skill for deletion.
|
|
316
|
+
if (skills.location === 'marketplace' || (skills.names || []).includes(MARKETPLACE_NAMES_SENTINEL)) return;
|
|
317
|
+
|
|
318
|
+
const skillNames = getAvailableSkillNames();
|
|
319
|
+
if (skillNames.length === 0) {
|
|
320
|
+
// An empty shipped list means the source tree is unreadable, not that the
|
|
321
|
+
// project should hold no skills. Returning here would leave `desired` empty
|
|
322
|
+
// and every installed skill would diff as a deletion — the failure mode this
|
|
323
|
+
// whole function exists to prevent. Fail loudly instead.
|
|
324
|
+
throw new Error(
|
|
325
|
+
'Cannot compute desired skill state: the UDS skills source directory is empty or unreadable. ' +
|
|
326
|
+
'Refusing to plan (an empty desired state would propose deleting every installed skill).'
|
|
327
|
+
);
|
|
328
|
+
}
|
|
227
329
|
|
|
228
330
|
const installations = skills.installations || [];
|
|
229
331
|
|
|
@@ -254,13 +356,22 @@ function calculateSkills(state, projectPath, manifest) {
|
|
|
254
356
|
|
|
255
357
|
/**
|
|
256
358
|
* Calculate expected command files.
|
|
359
|
+
*
|
|
360
|
+
* Same reasoning as calculateSkills: derive from what UDS ships, not from the
|
|
361
|
+
* frozen `manifest.commands.names`. machine-setup's list said 31 while UDS
|
|
362
|
+
* shipped 51. (XSPEC-343 R1/R2)
|
|
257
363
|
*/
|
|
258
364
|
function calculateCommands(state, projectPath, manifest) {
|
|
259
365
|
const commands = manifest.commands;
|
|
260
366
|
if (!commands || !commands.installed) return;
|
|
261
367
|
|
|
262
|
-
const commandNames =
|
|
263
|
-
if (commandNames.length === 0)
|
|
368
|
+
const commandNames = getAvailableCommandNames();
|
|
369
|
+
if (commandNames.length === 0) {
|
|
370
|
+
throw new Error(
|
|
371
|
+
'Cannot compute desired command state: the UDS commands source directory is empty or unreadable. ' +
|
|
372
|
+
'Refusing to plan (an empty desired state would propose deleting every installed command).'
|
|
373
|
+
);
|
|
374
|
+
}
|
|
264
375
|
|
|
265
376
|
const installations = commands.installations || [];
|
|
266
377
|
|
|
@@ -269,9 +380,12 @@ function calculateCommands(state, projectPath, manifest) {
|
|
|
269
380
|
const commandsDir = getCommandsDirForAgent(agent, level, projectPath);
|
|
270
381
|
if (!commandsDir) continue;
|
|
271
382
|
|
|
383
|
+
// The installed file carries the agent's extension; the key does not.
|
|
384
|
+
const ext = getCommandFileExtension(agent);
|
|
385
|
+
|
|
272
386
|
for (const commandName of commandNames) {
|
|
273
387
|
const relativeBase = level === 'project'
|
|
274
|
-
? getRelativePath(projectPath, join(commandsDir, commandName))
|
|
388
|
+
? getRelativePath(projectPath, join(commandsDir, `${commandName}${ext}`))
|
|
275
389
|
: null;
|
|
276
390
|
|
|
277
391
|
if (relativeBase) {
|
|
@@ -286,8 +286,13 @@ function isUDSManaged(entry) {
|
|
|
286
286
|
// Files in .standards/ are always UDS-managed
|
|
287
287
|
if (entry.relativePath.startsWith('.standards/')) return true;
|
|
288
288
|
|
|
289
|
-
// Scanned skill/command entries
|
|
290
|
-
|
|
289
|
+
// Scanned skill/command entries carry a provenance flag from the scanner.
|
|
290
|
+
// This used to return true unconditionally, which meant every directory in an
|
|
291
|
+
// adopter's skills folder was ours to delete — including hand-written ones.
|
|
292
|
+
// (XSPEC-343 R2)
|
|
293
|
+
if (entry.category === 'skill' || entry.category === 'command') {
|
|
294
|
+
return entry.metadata?.udsManaged !== false;
|
|
295
|
+
}
|
|
291
296
|
|
|
292
297
|
// Integration files with markers
|
|
293
298
|
if (entry.metadata?.hasMarkers) return true;
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from '../core/manifest.js';
|
|
18
18
|
import { computeFileHash, computeIntegrationBlockHash } from '../utils/hasher.js';
|
|
19
19
|
import { SUPPORTED_AI_TOOLS } from '../core/constants.js';
|
|
20
|
+
import { resolveToolKey } from '../core/constants.js';
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Migrate manifest to latest schema and backfill missing hashes.
|
|
@@ -144,8 +145,10 @@ export function backfillFileHashes(projectPath, manifest) {
|
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
// Backfill integration block hashes
|
|
147
|
-
for (const
|
|
148
|
-
|
|
148
|
+
for (const entry of (updated.integrations || [])) {
|
|
149
|
+
// 兩種形狀都要能解出工具(XSPEC-343 R1)
|
|
150
|
+
const toolName = resolveToolKey(entry);
|
|
151
|
+
const toolConfig = toolName ? SUPPORTED_AI_TOOLS[toolName] : null;
|
|
149
152
|
if (!toolConfig) continue;
|
|
150
153
|
const filePath = join(projectPath, toolConfig.file);
|
|
151
154
|
if (!updated.integrationBlockHashes[toolConfig.file] && existsSync(filePath)) {
|
|
@@ -14,12 +14,13 @@
|
|
|
14
14
|
import { existsSync, unlinkSync, mkdirSync, rmSync, copyFileSync, statSync } from 'fs';
|
|
15
15
|
import { join, dirname } from 'path';
|
|
16
16
|
import { copyStandard } from '../utils/copier.js';
|
|
17
|
-
import { writeIntegrationFile } from '../utils/integration-generator.js';
|
|
17
|
+
import { writeIntegrationFile, buildToolIntegrationConfig } from '../utils/integration-generator.js';
|
|
18
18
|
import {
|
|
19
19
|
installSkillsToMultipleAgents,
|
|
20
20
|
installCommandsToMultipleAgents
|
|
21
21
|
} from '../utils/skills-installer.js';
|
|
22
22
|
import { writeManifest } from '../core/manifest.js';
|
|
23
|
+
import { getRepositoryInfo } from '../utils/registry.js';
|
|
23
24
|
import { displayLanguageToLocale } from '../utils/locale.js';
|
|
24
25
|
import { computeFileHash } from '../utils/hasher.js';
|
|
25
26
|
import { createBackup, cleanupBackups } from './backup-manager.js';
|
|
@@ -146,6 +147,24 @@ export async function executePlan(projectPath, plan, manifest, options = {}) {
|
|
|
146
147
|
}
|
|
147
148
|
}
|
|
148
149
|
|
|
150
|
+
// Record the version we just reconciled to — but only on a clean run, mirroring
|
|
151
|
+
// `uds update`'s rule (a partial failure must stay retryable).
|
|
152
|
+
//
|
|
153
|
+
// Without this the reconciler applied everything and advanced nothing: the repo
|
|
154
|
+
// still recorded 6.1.0, `uds check` still said "behind the latest release", and
|
|
155
|
+
// the weekly staleness scout — which reads exactly `upstream.version` — kept
|
|
156
|
+
// reporting the repo as stale after a fully successful reconcile. (XSPEC-343 R2)
|
|
157
|
+
if (!dryRun && results.every(r => r.success)) {
|
|
158
|
+
const version = getRepositoryInfo()?.standards?.version;
|
|
159
|
+
if (version) {
|
|
160
|
+
updatedManifest.upstream = {
|
|
161
|
+
...(updatedManifest.upstream || {}),
|
|
162
|
+
version,
|
|
163
|
+
installed: new Date().toISOString().split('T')[0]
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
149
168
|
// Write updated manifest
|
|
150
169
|
if (!dryRun) {
|
|
151
170
|
try {
|
|
@@ -304,6 +323,21 @@ function executeMigrateBlock(projectPath, action, manifest) {
|
|
|
304
323
|
if (result.blockHashInfo) {
|
|
305
324
|
manifest.integrationBlockHashes[result.path] = result.blockHashInfo;
|
|
306
325
|
}
|
|
326
|
+
// `init` also records integration files in the whole-file `fileHashes`, which
|
|
327
|
+
// is what `uds check`'s File Integrity compares. Rewriting the block without
|
|
328
|
+
// refreshing that entry left the file permanently reported as "modified" —
|
|
329
|
+
// a successful reconcile that reads, afterwards, as a damaged install.
|
|
330
|
+
// (XSPEC-343 R2)
|
|
331
|
+
const tracked = manifest.fileHashes?.[result.path];
|
|
332
|
+
if (tracked) {
|
|
333
|
+
const info = computeFileHash(join(projectPath, result.path));
|
|
334
|
+
if (info) {
|
|
335
|
+
manifest.fileHashes[result.path] = {
|
|
336
|
+
...info,
|
|
337
|
+
installedAt: tracked.installedAt || new Date().toISOString()
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
}
|
|
307
341
|
return { action, success: true };
|
|
308
342
|
}
|
|
309
343
|
|
|
@@ -341,10 +375,19 @@ async function executeSkillBatch(projectPath, skillActions, manifest) {
|
|
|
341
375
|
|
|
342
376
|
if (skillNames.length > 0) {
|
|
343
377
|
try {
|
|
378
|
+
// The locale argument was omitted here while the command path below
|
|
379
|
+
// passes it, so a reconcile silently reinstalled every skill in English.
|
|
380
|
+
// telemetry-server lost 59 zh-TW skill files to this before it was caught,
|
|
381
|
+
// and its manifest went on recording `skills.locale: zh-TW` throughout.
|
|
382
|
+
// Prefer the recorded skills locale; fall back to the display language,
|
|
383
|
+
// which is what `uds update` uses. (XSPEC-343 R2)
|
|
384
|
+
const skillLocale = manifest.skills?.locale
|
|
385
|
+
|| displayLanguageToLocale(manifest?.options?.display_language);
|
|
344
386
|
const installResult = await installSkillsToMultipleAgents(
|
|
345
387
|
manifest.skills.installations,
|
|
346
388
|
skillNames,
|
|
347
|
-
projectPath
|
|
389
|
+
projectPath,
|
|
390
|
+
skillLocale
|
|
348
391
|
);
|
|
349
392
|
|
|
350
393
|
if (installResult.allFileHashes) {
|
|
@@ -434,20 +477,16 @@ async function executeCommandBatch(projectPath, commandActions, manifest) {
|
|
|
434
477
|
|
|
435
478
|
/**
|
|
436
479
|
* Build integration generation config from manifest.
|
|
480
|
+
*
|
|
481
|
+
* Delegates to the shared builder so the reconciler and `uds update` generate the
|
|
482
|
+
* same block. The private copy that used to live here omitted `categories`,
|
|
483
|
+
* defaulted the output language to English, and read `integrationConfigs` by tool
|
|
484
|
+
* key when the manifest stores it by file name — so reconciling a repo silently
|
|
485
|
+
* dropped sections the normal update path always wrote. (XSPEC-343 R2)
|
|
437
486
|
*/
|
|
438
487
|
function buildIntegrationConfig(manifest, toolName) {
|
|
439
488
|
return {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
s.startsWith('.standards/') ? s : `.standards/${s}`
|
|
443
|
-
),
|
|
444
|
-
format: manifest.format || 'ai',
|
|
445
|
-
contentMode: manifest.contentMode || 'index',
|
|
446
|
-
language: manifest.integrationConfigs?.[toolName]?.language || 'en',
|
|
447
|
-
outputLanguage: manifest.integrationConfigs?.[toolName]?.outputLanguage || manifest.integrationConfigs?.[toolName]?.commitLanguage || 'english',
|
|
448
|
-
installedSkills: manifest.skills?.names || [],
|
|
449
|
-
installedCommands: manifest.commands?.names || [],
|
|
450
|
-
methodology: manifest.methodology,
|
|
451
|
-
...(manifest.integrationConfigs?.[toolName] || {})
|
|
489
|
+
...buildToolIntegrationConfig(manifest, toolName),
|
|
490
|
+
format: manifest.format || 'ai'
|
|
452
491
|
};
|
|
453
492
|
}
|
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { extractMarkedContent } from '../utils/integration-generator.js';
|
|
4
4
|
import { SUPPORTED_AI_TOOLS } from '../core/constants.js';
|
|
5
|
+
import { resolveIntegrationFile } from '../core/constants.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Get the format for a given integration file name
|
|
@@ -36,7 +37,12 @@ export async function uninstallIntegrations(projectPath, manifest, options = {})
|
|
|
36
37
|
return result;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
for (const
|
|
40
|
+
for (const entry of integrations) {
|
|
41
|
+
// Accept both shapes: this uninstaller was written against file names,
|
|
42
|
+
// while the reconciler reads the same field as tool keys. Normalising the
|
|
43
|
+
// manifest to one shape fixes one consumer and breaks the other, so both
|
|
44
|
+
// resolve through the shared helper instead. (XSPEC-343 R1)
|
|
45
|
+
const fileName = resolveIntegrationFile(entry) || entry;
|
|
40
46
|
const filePath = join(projectPath, fileName);
|
|
41
47
|
|
|
42
48
|
if (!existsSync(filePath)) {
|
|
@@ -2,7 +2,7 @@ import { ConfigLoader } from './config-loader.js';
|
|
|
2
2
|
import { ConfigMerger } from './config-merger.js';
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
|
-
import yaml from 'js-yaml';
|
|
5
|
+
import * as yaml from 'js-yaml';
|
|
6
6
|
|
|
7
7
|
export class ConfigManager {
|
|
8
8
|
constructor(cwd = process.cwd()) {
|
package/src/utils/github.js
CHANGED
|
@@ -4,6 +4,7 @@ import { homedir } from 'os';
|
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import https from 'https';
|
|
6
6
|
import { setTimeout as delay } from 'timers/promises';
|
|
7
|
+
import { getSkillsSourceDir } from './skills-source.js';
|
|
7
8
|
|
|
8
9
|
// Re-export agent-specific functions from ai-agent-paths for unified API
|
|
9
10
|
export {
|
|
@@ -26,7 +27,10 @@ const SKILLS_RAW_BASE = 'https://raw.githubusercontent.com/AsiaOstrich/universal
|
|
|
26
27
|
const __filename = fileURLToPath(import.meta.url);
|
|
27
28
|
const __dirname = dirname(__filename);
|
|
28
29
|
const CLI_ROOT = join(__dirname, '..', '..');
|
|
29
|
-
|
|
30
|
+
|
|
31
|
+
// Resolved centrally — this file used to hardcode `<CLI_ROOT>/../skills/claude-code`, a
|
|
32
|
+
// layout that no longer exists in either a checkout or an npm install. See skills-source.js.
|
|
33
|
+
const SKILLS_LOCAL_DIR = getSkillsSourceDir();
|
|
30
34
|
|
|
31
35
|
/**
|
|
32
36
|
* Status codes that are safe to retry (transient errors)
|
package/src/utils/hasher.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash } from 'crypto';
|
|
|
2
2
|
import { readFileSync, statSync, existsSync, readdirSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { UDS_MARKERS } from '../core/constants.js';
|
|
5
|
+
import { resolveIntegrationFile } from '../core/constants.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Compute SHA-256 hash for a file
|
|
@@ -109,10 +110,12 @@ export function getFileStatusSummary(projectPath, manifest) {
|
|
|
109
110
|
source: e,
|
|
110
111
|
target: join('.standards', e.split('/').pop())
|
|
111
112
|
})),
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
// `integrations` holds file names in most repos and tool keys in some
|
|
114
|
+
// (XSPEC-343 R1) — resolve so both shapes yield a real path.
|
|
115
|
+
...manifest.integrations.map(i => {
|
|
116
|
+
const f = resolveIntegrationFile(i) || i;
|
|
117
|
+
return { source: f, target: f };
|
|
118
|
+
})
|
|
116
119
|
];
|
|
117
120
|
|
|
118
121
|
for (const file of allFiles) {
|