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.
@@ -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
- return { success: true, path: filePath };
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;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "version": "3.5.1-beta.15",
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.15"
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.15",
58
+ "version": "3.5.1-beta.17",
59
59
  "note": "Skills are now included in the main repository under skills/"
60
60
  }
61
61
  },