universal-dev-standards 3.5.1-beta.15 → 3.5.1-beta.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/uds.js +2 -0
- package/bundled/core/checkin-standards.md +39 -2
- package/bundled/locales/zh-CN/core/checkin-standards.md +42 -5
- package/bundled/locales/zh-TW/core/checkin-standards.md +42 -5
- package/bundled/skills/claude-code/commands/check.md +52 -1
- package/bundled/skills/claude-code/commands/config.md +14 -0
- package/bundled/skills/claude-code/commands/update.md +23 -7
- package/package.json +2 -1
- package/src/commands/check.js +455 -28
- package/src/commands/configure.js +64 -5
- package/src/commands/init.js +29 -3
- package/src/commands/update.js +227 -49
- package/src/i18n/messages.js +54 -6
- package/src/utils/hasher.js +193 -0
- package/src/utils/integration-generator.js +11 -1
- package/src/utils/skills-installer.js +39 -4
- package/standards-registry.json +3 -3
package/src/utils/hasher.js
CHANGED
|
@@ -2,6 +2,21 @@ import { createHash } from 'crypto';
|
|
|
2
2
|
import { readFileSync, statSync, existsSync, readdirSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Marker block constants for different file formats
|
|
7
|
+
* (Duplicated from integration-generator.js to avoid circular dependency)
|
|
8
|
+
*/
|
|
9
|
+
const MARKERS = {
|
|
10
|
+
markdown: {
|
|
11
|
+
start: '<!-- UDS:STANDARDS:START -->',
|
|
12
|
+
end: '<!-- UDS:STANDARDS:END -->'
|
|
13
|
+
},
|
|
14
|
+
plaintext: {
|
|
15
|
+
start: '# === UDS:STANDARDS:START ===',
|
|
16
|
+
end: '# === UDS:STANDARDS:END ==='
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
5
20
|
/**
|
|
6
21
|
* Compute SHA-256 hash for a file
|
|
7
22
|
* @param {string} filePath - Absolute file path
|
|
@@ -218,3 +233,181 @@ export function scanForUntrackedFiles(projectPath, manifest) {
|
|
|
218
233
|
|
|
219
234
|
return untracked;
|
|
220
235
|
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Detect file format based on file path
|
|
239
|
+
* @param {string} filePath - File path
|
|
240
|
+
* @returns {'markdown'|'plaintext'} Format type
|
|
241
|
+
*/
|
|
242
|
+
function detectFormat(filePath) {
|
|
243
|
+
// Plaintext rules files
|
|
244
|
+
if (filePath.endsWith('.cursorrules') ||
|
|
245
|
+
filePath.endsWith('.windsurfrules') ||
|
|
246
|
+
filePath.endsWith('.clinerules')) {
|
|
247
|
+
return 'plaintext';
|
|
248
|
+
}
|
|
249
|
+
return 'markdown';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Extract content between UDS markers from file content
|
|
254
|
+
* @param {string} content - File content
|
|
255
|
+
* @param {'markdown'|'plaintext'} format - Format type
|
|
256
|
+
* @returns {{before: string, blockContent: string, after: string}} Extracted parts
|
|
257
|
+
*/
|
|
258
|
+
function extractBlockContent(content, format) {
|
|
259
|
+
const markers = MARKERS[format] || MARKERS.markdown;
|
|
260
|
+
const startIdx = content.indexOf(markers.start);
|
|
261
|
+
const endIdx = content.indexOf(markers.end);
|
|
262
|
+
|
|
263
|
+
if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) {
|
|
264
|
+
return { before: content, blockContent: '', after: '' };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
before: content.substring(0, startIdx),
|
|
269
|
+
blockContent: content.substring(startIdx + markers.start.length, endIdx).trim(),
|
|
270
|
+
after: content.substring(endIdx + markers.end.length)
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Compute hash for UDS marker block content in an integration file
|
|
276
|
+
* This only hashes the content between UDS markers, not user customizations outside the block
|
|
277
|
+
* @param {string} filePath - Absolute file path
|
|
278
|
+
* @returns {Object|null} { blockHash, blockSize, fullHash, fullSize } or null if file doesn't exist or no markers found
|
|
279
|
+
*/
|
|
280
|
+
export function computeIntegrationBlockHash(filePath) {
|
|
281
|
+
try {
|
|
282
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
283
|
+
const format = detectFormat(filePath);
|
|
284
|
+
const { blockContent } = extractBlockContent(content, format);
|
|
285
|
+
|
|
286
|
+
// If no markers found, return null (this file may not have UDS markers)
|
|
287
|
+
if (!blockContent) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const blockHash = createHash('sha256').update(blockContent).digest('hex');
|
|
292
|
+
const fullHash = createHash('sha256').update(content).digest('hex');
|
|
293
|
+
const stats = statSync(filePath);
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
blockHash: `sha256:${blockHash}`,
|
|
297
|
+
blockSize: Buffer.byteLength(blockContent, 'utf-8'),
|
|
298
|
+
fullHash: `sha256:${fullHash}`,
|
|
299
|
+
fullSize: stats.size
|
|
300
|
+
};
|
|
301
|
+
} catch {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Compare integration block hash with stored hash info
|
|
308
|
+
* @param {string} filePath - Absolute file path
|
|
309
|
+
* @param {Object} storedInfo - Stored hash info from manifest
|
|
310
|
+
* @param {string} storedInfo.blockHash - Stored block hash
|
|
311
|
+
* @param {number} storedInfo.blockSize - Stored block size in bytes
|
|
312
|
+
* @returns {'unchanged'|'modified'|'missing'|'no_markers'} Block status
|
|
313
|
+
*/
|
|
314
|
+
export function compareIntegrationBlockHash(filePath, storedInfo) {
|
|
315
|
+
if (!existsSync(filePath)) {
|
|
316
|
+
return 'missing';
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const current = computeIntegrationBlockHash(filePath);
|
|
320
|
+
if (!current) {
|
|
321
|
+
return 'no_markers';
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Compare block hash and size
|
|
325
|
+
if (current.blockSize !== storedInfo.blockSize) {
|
|
326
|
+
return 'modified';
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (current.blockHash !== storedInfo.blockHash) {
|
|
330
|
+
return 'modified';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return 'unchanged';
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Compute hashes for all files in a directory recursively
|
|
338
|
+
* @param {string} dirPath - Directory to scan
|
|
339
|
+
* @param {string} baseKey - Base key prefix for hash map entries
|
|
340
|
+
* @returns {Object} Map of key to { hash, size, installedAt }
|
|
341
|
+
*/
|
|
342
|
+
export function computeDirectoryHashes(dirPath, baseKey = '') {
|
|
343
|
+
const hashes = {};
|
|
344
|
+
const now = new Date().toISOString();
|
|
345
|
+
|
|
346
|
+
if (!existsSync(dirPath)) {
|
|
347
|
+
return hashes;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const files = scanDirectory(dirPath, dirPath);
|
|
351
|
+
|
|
352
|
+
for (const relativePath of files) {
|
|
353
|
+
const fullPath = join(dirPath, relativePath);
|
|
354
|
+
const hashInfo = computeFileHash(fullPath);
|
|
355
|
+
|
|
356
|
+
if (hashInfo) {
|
|
357
|
+
// Build key: baseKey/relativePath (using forward slashes for consistency)
|
|
358
|
+
const key = baseKey ? `${baseKey}/${relativePath}` : relativePath;
|
|
359
|
+
hashes[key] = {
|
|
360
|
+
...hashInfo,
|
|
361
|
+
installedAt: now
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return hashes;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Compare directory contents against stored hashes
|
|
371
|
+
* @param {string} dirPath - Directory to check
|
|
372
|
+
* @param {Object} storedHashes - Map of key to { hash, size }
|
|
373
|
+
* @param {string} baseKey - Base key prefix used when computing hashes
|
|
374
|
+
* @returns {Object} { unchanged: [], modified: [], missing: [], added: [] }
|
|
375
|
+
*/
|
|
376
|
+
export function compareDirectoryHashes(dirPath, storedHashes, baseKey = '') {
|
|
377
|
+
const result = {
|
|
378
|
+
unchanged: [],
|
|
379
|
+
modified: [],
|
|
380
|
+
missing: [],
|
|
381
|
+
added: []
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
// Get current files
|
|
385
|
+
const currentHashes = computeDirectoryHashes(dirPath, baseKey);
|
|
386
|
+
const currentKeys = new Set(Object.keys(currentHashes));
|
|
387
|
+
const storedKeys = new Set(Object.keys(storedHashes));
|
|
388
|
+
|
|
389
|
+
// Check stored files
|
|
390
|
+
for (const key of storedKeys) {
|
|
391
|
+
if (!currentKeys.has(key)) {
|
|
392
|
+
result.missing.push(key);
|
|
393
|
+
} else {
|
|
394
|
+
const stored = storedHashes[key];
|
|
395
|
+
const current = currentHashes[key];
|
|
396
|
+
|
|
397
|
+
if (stored.hash === current.hash && stored.size === current.size) {
|
|
398
|
+
result.unchanged.push(key);
|
|
399
|
+
} else {
|
|
400
|
+
result.modified.push(key);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Check for added files
|
|
406
|
+
for (const key of currentKeys) {
|
|
407
|
+
if (!storedKeys.has(key)) {
|
|
408
|
+
result.added.push(key);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return result;
|
|
413
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
2
2
|
import { dirname, join, basename } from 'path';
|
|
3
3
|
import { getLanguageRules } from '../prompts/integrations.js';
|
|
4
|
+
import { computeIntegrationBlockHash } from './hasher.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Marker block constants for different file formats
|
|
@@ -2026,7 +2027,16 @@ export function writeIntegrationFile(tool, config, projectPath) {
|
|
|
2026
2027
|
}
|
|
2027
2028
|
|
|
2028
2029
|
writeFileSync(filePath, content);
|
|
2029
|
-
|
|
2030
|
+
|
|
2031
|
+
// Compute block hash for tracking UDS content separately from user content
|
|
2032
|
+
const blockHashInfo = computeIntegrationBlockHash(filePath);
|
|
2033
|
+
|
|
2034
|
+
return {
|
|
2035
|
+
success: true,
|
|
2036
|
+
path: fileName, // Return relative path for consistency
|
|
2037
|
+
absolutePath: filePath,
|
|
2038
|
+
blockHashInfo // Contains: blockHash, blockSize, fullHash, fullSize
|
|
2039
|
+
};
|
|
2030
2040
|
} catch (error) {
|
|
2031
2041
|
return { success: false, error: error.message };
|
|
2032
2042
|
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
getSkillsDirForAgent,
|
|
16
16
|
getCommandsDirForAgent
|
|
17
17
|
} from '../config/ai-agent-paths.js';
|
|
18
|
+
import { computeDirectoryHashes, computeFileHash } from './hasher.js';
|
|
18
19
|
|
|
19
20
|
// Get the CLI package root directory
|
|
20
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -132,7 +133,8 @@ export async function installSkillsForAgent(agent, level, skillNames = null, pro
|
|
|
132
133
|
level,
|
|
133
134
|
targetDir,
|
|
134
135
|
installed: [],
|
|
135
|
-
errors: []
|
|
136
|
+
errors: [],
|
|
137
|
+
fileHashes: {} // New: file hashes for installed skills
|
|
136
138
|
};
|
|
137
139
|
|
|
138
140
|
for (const skillName of toInstall) {
|
|
@@ -148,6 +150,11 @@ export async function installSkillsForAgent(agent, level, skillNames = null, pro
|
|
|
148
150
|
// Write manifest
|
|
149
151
|
if (results.installed.length > 0) {
|
|
150
152
|
writeSkillsManifestForAgent(agent, level, targetDir);
|
|
153
|
+
|
|
154
|
+
// Compute file hashes for tracking
|
|
155
|
+
// Key format: agent/level/skillName/filename (e.g., "opencode/project/commit-standards/SKILL.md")
|
|
156
|
+
const baseKey = `${agent}/${level}`;
|
|
157
|
+
results.fileHashes = computeDirectoryHashes(targetDir, baseKey);
|
|
151
158
|
}
|
|
152
159
|
|
|
153
160
|
return results;
|
|
@@ -266,7 +273,8 @@ export async function installCommandsForAgent(agent, commandNames = null, projec
|
|
|
266
273
|
agent,
|
|
267
274
|
targetDir,
|
|
268
275
|
installed: [],
|
|
269
|
-
errors: []
|
|
276
|
+
errors: [],
|
|
277
|
+
fileHashes: {} // New: file hashes for installed commands
|
|
270
278
|
};
|
|
271
279
|
|
|
272
280
|
for (const cmdName of toInstall) {
|
|
@@ -282,6 +290,21 @@ export async function installCommandsForAgent(agent, commandNames = null, projec
|
|
|
282
290
|
// Write manifest
|
|
283
291
|
if (results.installed.length > 0) {
|
|
284
292
|
writeCommandsManifest(agent, targetDir, results.installed);
|
|
293
|
+
|
|
294
|
+
// Compute file hashes for tracking
|
|
295
|
+
// Key format: agent/filename (e.g., "opencode/commit.md")
|
|
296
|
+
const now = new Date().toISOString();
|
|
297
|
+
for (const cmdName of results.installed) {
|
|
298
|
+
const ext = getCommandFileExtension(agent);
|
|
299
|
+
const filePath = join(targetDir, `${cmdName}${ext}`);
|
|
300
|
+
const hashInfo = computeFileHash(filePath);
|
|
301
|
+
if (hashInfo) {
|
|
302
|
+
results.fileHashes[`${agent}/${cmdName}${ext}`] = {
|
|
303
|
+
...hashInfo,
|
|
304
|
+
installedAt: now
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
285
308
|
}
|
|
286
309
|
|
|
287
310
|
return results;
|
|
@@ -615,7 +638,8 @@ export async function installSkillsToMultipleAgents(installations, skillNames =
|
|
|
615
638
|
success: true,
|
|
616
639
|
installations: [],
|
|
617
640
|
totalInstalled: 0,
|
|
618
|
-
totalErrors: 0
|
|
641
|
+
totalErrors: 0,
|
|
642
|
+
allFileHashes: {} // New: combined file hashes from all installations
|
|
619
643
|
};
|
|
620
644
|
|
|
621
645
|
for (const { agent, level } of installations) {
|
|
@@ -627,6 +651,11 @@ export async function installSkillsToMultipleAgents(installations, skillNames =
|
|
|
627
651
|
}
|
|
628
652
|
results.totalInstalled += result.installed.length;
|
|
629
653
|
results.totalErrors += result.errors.length;
|
|
654
|
+
|
|
655
|
+
// Merge file hashes from this installation
|
|
656
|
+
if (result.fileHashes) {
|
|
657
|
+
Object.assign(results.allFileHashes, result.fileHashes);
|
|
658
|
+
}
|
|
630
659
|
}
|
|
631
660
|
|
|
632
661
|
return results;
|
|
@@ -644,7 +673,8 @@ export async function installCommandsToMultipleAgents(agents, commandNames = nul
|
|
|
644
673
|
success: true,
|
|
645
674
|
installations: [],
|
|
646
675
|
totalInstalled: 0,
|
|
647
|
-
totalErrors: 0
|
|
676
|
+
totalErrors: 0,
|
|
677
|
+
allFileHashes: {} // New: combined file hashes from all installations
|
|
648
678
|
};
|
|
649
679
|
|
|
650
680
|
for (const agent of agents) {
|
|
@@ -659,6 +689,11 @@ export async function installCommandsToMultipleAgents(agents, commandNames = nul
|
|
|
659
689
|
}
|
|
660
690
|
results.totalInstalled += result.installed.length;
|
|
661
691
|
results.totalErrors += result.errors.length;
|
|
692
|
+
|
|
693
|
+
// Merge file hashes from this installation
|
|
694
|
+
if (result.fileHashes) {
|
|
695
|
+
Object.assign(results.allFileHashes, result.fileHashes);
|
|
696
|
+
}
|
|
662
697
|
}
|
|
663
698
|
|
|
664
699
|
return results;
|
package/standards-registry.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"version": "3.5.1-beta.
|
|
3
|
+
"version": "3.5.1-beta.17",
|
|
4
4
|
"lastUpdated": "2026-01-15",
|
|
5
5
|
"description": "Standards registry for universal-dev-standards with integrated skills and AI-optimized formats",
|
|
6
6
|
"formats": {
|
|
@@ -48,14 +48,14 @@
|
|
|
48
48
|
"standards": {
|
|
49
49
|
"name": "universal-dev-standards",
|
|
50
50
|
"url": "https://github.com/AsiaOstrich/universal-dev-standards",
|
|
51
|
-
"version": "3.5.1-beta.
|
|
51
|
+
"version": "3.5.1-beta.17"
|
|
52
52
|
},
|
|
53
53
|
"skills": {
|
|
54
54
|
"name": "universal-dev-standards",
|
|
55
55
|
"url": "https://github.com/AsiaOstrich/universal-dev-standards",
|
|
56
56
|
"localPath": "skills/claude-code",
|
|
57
57
|
"rawUrl": "https://raw.githubusercontent.com/AsiaOstrich/universal-dev-standards/main/skills/claude-code",
|
|
58
|
-
"version": "3.5.1-beta.
|
|
58
|
+
"version": "3.5.1-beta.17",
|
|
59
59
|
"note": "Skills are now included in the main repository under skills/"
|
|
60
60
|
}
|
|
61
61
|
},
|