ocs-stats 1.1.2 → 1.1.5

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/cli.js CHANGED
@@ -7,6 +7,8 @@ const args = process.argv.slice(2);
7
7
  const command = args[0];
8
8
  const isGlobal = args.includes('--global') || args.includes('-g');
9
9
  const isHelp = args.includes('--help') || args.includes('-h');
10
+ const isForce = args.includes('--force') || args.includes('-f');
11
+ const isCheck = args.includes('--check') || args.includes('-c');
10
12
 
11
13
  if (isHelp) {
12
14
  console.log(`
@@ -15,7 +17,9 @@ ocs-stats - Install OpenCode skills and agents
15
17
  Usage:
16
18
  npx ocs-stats Install to current project
17
19
  npx ocs-stats --global Install globally (~/.opencode)
18
- npx ocs-stats update Update skills (removes existing)
20
+ npx ocs-stats update Update skills (smart merge)
21
+ npx ocs-stats update --check Check for updates
22
+ npx ocs-stats update --force Force fresh install
19
23
  npx ocs-stats stats Show security agent progress
20
24
  npx ocs-stats stats testing Show testing agent progress
21
25
  npx ocs-stats display-xp <amount> "<reason>"
@@ -23,11 +27,15 @@ Usage:
23
27
 
24
28
  Options:
25
29
  -g, --global Install to user home directory
30
+ -f, --force Force fresh install (delete + copy)
31
+ -c, --check Check for updates without applying
26
32
  -h, --help Show this help message
27
33
 
28
34
  Examples:
29
35
  npx ocs-stats
30
36
  npx ocs-stats update
37
+ npx ocs-stats update --check
38
+ npx ocs-stats update --force
31
39
  npx ocs-stats stats
32
40
  npx ocs-stats stats testing
33
41
  npx ocs-stats display-xp 35 "Fixed high issue"
@@ -36,21 +44,22 @@ Examples:
36
44
  process.exit(0);
37
45
  }
38
46
 
39
- if (command === 'update') {
40
- update({ isGlobal }).catch((err) => {
41
- console.error('Error:', err.message);
42
- process.exit(1);
43
- });
44
- process.exit(0);
47
+ else if (command === 'update') {
48
+ update({ isGlobal, force: isForce, checkOnly: isCheck })
49
+ .then(() => process.exit(0))
50
+ .catch((err) => {
51
+ console.error('Error:', err.message);
52
+ process.exit(1);
53
+ });
45
54
  }
46
55
 
47
- if (command === 'stats') {
56
+ else if (command === 'stats') {
48
57
  const category = args[1] || 'security';
49
58
  stats(category);
50
59
  process.exit(0);
51
60
  }
52
61
 
53
- if (command === 'display-xp') {
62
+ else if (command === 'display-xp') {
54
63
  const amount = args[1];
55
64
  const reason = args.slice(2).join(' ') || 'XP earned';
56
65
  const category = reason.includes('[testing]') ? 'testing' : 'security';
@@ -59,7 +68,9 @@ if (command === 'display-xp') {
59
68
  process.exit(0);
60
69
  }
61
70
 
62
- init({ isGlobal }).catch((err) => {
63
- console.error('Error:', err.message);
64
- process.exit(1);
65
- });
71
+ else {
72
+ init({ isGlobal }).catch((err) => {
73
+ console.error('Error:', err.message);
74
+ process.exit(1);
75
+ });
76
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ocs-stats",
3
- "version": "1.1.2",
3
+ "version": "1.1.5",
4
4
  "description": "OpenCode Skills - One-click installer with gamified XP stats",
5
5
  "type": "module",
6
6
  "bin": {
package/src/init.js CHANGED
@@ -1,12 +1,32 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { fileURLToPath } from 'url';
4
+ import { performUpdate, checkForUpdates, formatUpdateSummary } from './merge.js';
4
5
 
5
6
  const __filename = fileURLToPath(import.meta.url);
6
7
  const __dirname = path.dirname(__filename);
7
8
 
8
9
  const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
9
10
 
11
+ function copyDir(src, dest) {
12
+ fs.mkdirSync(dest, { recursive: true });
13
+
14
+ const entries = fs.readdirSync(src, { withFileTypes: true });
15
+
16
+ for (const entry of entries) {
17
+ if (entry.name === '.git') continue;
18
+
19
+ const srcPath = path.join(src, entry.name);
20
+ const destPath = path.join(dest, entry.name);
21
+
22
+ if (entry.isDirectory()) {
23
+ copyDir(srcPath, destPath);
24
+ } else {
25
+ fs.copyFileSync(srcPath, destPath);
26
+ }
27
+ }
28
+ }
29
+
10
30
  export async function init({ isGlobal = false } = {}) {
11
31
  const targetDir = isGlobal
12
32
  ? path.join(process.env.HOME || process.env.USERPROFILE, '.opencode')
@@ -43,7 +63,7 @@ export async function init({ isGlobal = false } = {}) {
43
63
  }
44
64
  }
45
65
 
46
- export async function update({ isGlobal = false } = {}) {
66
+ export async function update({ isGlobal = false, force = false, checkOnly = false } = {}) {
47
67
  const targetDir = isGlobal
48
68
  ? path.join(process.env.HOME || process.env.USERPROFILE, '.opencode')
49
69
  : path.join(process.cwd(), '.opencode');
@@ -55,35 +75,32 @@ export async function update({ isGlobal = false } = {}) {
55
75
  process.exit(1);
56
76
  }
57
77
 
58
- // Remove existing
59
- fs.rmSync(targetDir, { recursive: true, force: true });
60
- console.log(' Removed existing .opencode folder');
61
-
62
- // Reinstall
63
- copyDir(TEMPLATES_DIR, targetDir);
64
- console.log(' Installed fresh copy\n');
78
+ if (force) {
79
+ console.log(' Force mode: Removing existing .opencode folder');
80
+ fs.rmSync(targetDir, { recursive: true, force: true });
81
+ copyDir(TEMPLATES_DIR, targetDir);
82
+ console.log(' Fresh install complete\n');
83
+ return;
84
+ }
65
85
 
66
- console.log('Updated successfully!\n');
67
- console.log('What was installed:');
68
- console.log(' * Agents: security, testing');
69
- console.log(' * Skills: commit, memories, mobile, security, testing, webapp');
70
- console.log(' * Security: XP tracking, knowledge base');
71
- console.log(' * Testing: XP tracking, knowledge base\n');
72
- }
86
+ if (checkOnly) {
87
+ console.log(' Checking for updates...\n');
88
+ const updateInfo = await checkForUpdates(targetDir);
89
+ if (!updateInfo.hasUpdates) {
90
+ console.log(` Already up to date (v${updateInfo.currentVersion})\n`);
91
+ return;
92
+ }
93
+ formatUpdateSummary({ ...updateInfo, results: updateInfo.changes }, true);
94
+ return;
95
+ }
73
96
 
74
- function copyDir(src, dest) {
75
- fs.mkdirSync(dest, { recursive: true });
76
-
77
- const entries = fs.readdirSync(src, { withFileTypes: true });
97
+ console.log(' Updating...\n');
98
+ const updateResult = await performUpdate(targetDir, { force, checkOnly });
78
99
 
79
- for (const entry of entries) {
80
- const srcPath = path.join(src, entry.name);
81
- const destPath = path.join(dest, entry.name);
82
-
83
- if (entry.isDirectory()) {
84
- copyDir(srcPath, destPath);
85
- } else {
86
- fs.copyFileSync(srcPath, destPath);
87
- }
100
+ if (!updateResult.updated) {
101
+ console.log(` Already up to date (v${updateResult.currentVersion})\n`);
102
+ return;
88
103
  }
104
+
105
+ formatUpdateSummary(updateResult, false);
89
106
  }
package/src/merge.js ADDED
@@ -0,0 +1,552 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import crypto from 'crypto';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
10
+
11
+ export const FILE_STATE = {
12
+ NEW: 'new',
13
+ UNMODIFIED: 'unmodified',
14
+ MODIFIED: 'modified',
15
+ DELETED: 'deleted',
16
+ };
17
+
18
+ export const FILE_CATEGORY = {
19
+ XP_DATA: 'xp_data',
20
+ MEMORIES: 'memories',
21
+ KNOWLEDGE: 'knowledge',
22
+ SKILL: 'skill',
23
+ AGENT: 'agent',
24
+ };
25
+
26
+ const PROTECTED_CATEGORIES = [FILE_CATEGORY.XP_DATA, FILE_CATEGORY.MEMORIES];
27
+ const SMART_MERGE_CATEGORIES = [FILE_CATEGORY.XP_DATA, FILE_CATEGORY.MEMORIES, FILE_CATEGORY.KNOWLEDGE];
28
+
29
+ function hashFile(filePath) {
30
+ if (!fs.existsSync(filePath)) return null;
31
+ const content = fs.readFileSync(filePath, 'utf-8');
32
+ return crypto.createHash('md5').update(content).digest('hex');
33
+ }
34
+
35
+ function getCategory(relativePath) {
36
+ const pathLower = relativePath.toLowerCase();
37
+
38
+ if (pathLower.includes('/xp.json') || pathLower.endsWith('xp.json')) {
39
+ return FILE_CATEGORY.XP_DATA;
40
+ }
41
+ if (pathLower.includes('/memories/') || pathLower.includes('memories/')) {
42
+ return FILE_CATEGORY.MEMORIES;
43
+ }
44
+ if (pathLower.includes('/knowledge.md')) {
45
+ return FILE_CATEGORY.KNOWLEDGE;
46
+ }
47
+ if (pathLower.includes('/skills/')) {
48
+ return FILE_CATEGORY.SKILL;
49
+ }
50
+ if (pathLower.includes('/agents/')) {
51
+ return FILE_CATEGORY.AGENT;
52
+ }
53
+ return FILE_CATEGORY.SKILL;
54
+ }
55
+
56
+ function compareVersion(v1, v2) {
57
+ const parts1 = v1.split('.').map(Number);
58
+ const parts2 = v2.split('.').map(Number);
59
+ for (let i = 0; i < 3; i++) {
60
+ if (parts1[i] > parts2[i]) return 1;
61
+ if (parts1[i] < parts2[i]) return -1;
62
+ }
63
+ return 0;
64
+ }
65
+
66
+ function smartMergeXp(userXp, templateXp) {
67
+ const preserveFields = [
68
+ 'xp', 'level', 'title', 'totalTests', 'totalAudits',
69
+ 'testsWritten', 'issuesFixed', 'testsFixed', 'patternsAdded',
70
+ 'completedSuites', 'completedAudits', 'seenPatterns', 'seenIssues',
71
+ 'mistakes', 'mistakeHistory', 'levelHistory'
72
+ ];
73
+
74
+ let merged = { ...templateXp };
75
+
76
+ for (const field of preserveFields) {
77
+ if (userXp[field] !== undefined) {
78
+ merged[field] = userXp[field];
79
+ }
80
+ }
81
+
82
+ return merged;
83
+ }
84
+
85
+ function getSmartMergePreserveInfo(userXp, templateXp) {
86
+ const preserveFields = [
87
+ 'xp', 'level', 'title', 'totalTests', 'totalAudits',
88
+ 'testsWritten', 'issuesFixed', 'testsFixed', 'patternsAdded'
89
+ ];
90
+
91
+ const preserved = [];
92
+ for (const field of preserveFields) {
93
+ if (userXp[field] !== undefined && userXp[field] !== 0) {
94
+ if (field === 'xp') {
95
+ preserved.push(`XP ${userXp[field]}`);
96
+ } else if (field === 'level') {
97
+ preserved.push(`level ${userXp[field]}`);
98
+ } else if (field === 'title') {
99
+ preserved.push(`title: ${userXp[field]}`);
100
+ } else if (field === 'totalTests') {
101
+ preserved.push(`tests: ${userXp[field]}`);
102
+ } else if (field === 'testsWritten' && userXp.testsWritten) {
103
+ const total = (userXp.testsWritten.unit || 0) + (userXp.testsWritten.integration || 0) + (userXp.testsWritten.e2e || 0);
104
+ if (total > 0) preserved.push(`tests: ${total}`);
105
+ } else if (field === 'issuesFixed' && userXp.issuesFixed) {
106
+ const total = (userXp.issuesFixed.critical || 0) + (userXp.issuesFixed.high || 0) + (userXp.issuesFixed.medium || 0) + (userXp.issuesFixed.low || 0);
107
+ if (total > 0) preserved.push(`fixed: ${total}`);
108
+ } else if (field === 'patternsAdded' && userXp.patternsAdded > 0) {
109
+ preserved.push(`patterns: ${userXp[field]}`);
110
+ }
111
+ }
112
+ }
113
+
114
+ return preserved.length > 0 ? preserved.join(', ') : 'preserved';
115
+ }
116
+
117
+ function smartMergeMemories(userContent, templateContent) {
118
+ const templateSections = {};
119
+ const userSections = {};
120
+
121
+ const parseSections = (content) => {
122
+ const sections = {};
123
+ const lines = content.split('\n');
124
+ let currentSection = null;
125
+ let currentLines = [];
126
+
127
+ for (const line of lines) {
128
+ const headerMatch = line.match(/^(#{1,3})\s+(.+)$/);
129
+ if (headerMatch) {
130
+ if (currentSection) {
131
+ sections[currentSection] = currentLines.join('\n').trim();
132
+ }
133
+ currentSection = headerMatch[2];
134
+ currentLines = [];
135
+ } else {
136
+ currentLines.push(line);
137
+ }
138
+ }
139
+ if (currentSection) {
140
+ sections[currentSection] = currentLines.join('\n').trim();
141
+ }
142
+ return sections;
143
+ };
144
+
145
+ const templateParsed = parseSections(templateContent);
146
+ const userParsed = parseSections(userContent);
147
+
148
+ let merged = '';
149
+ const preservedSections = [];
150
+
151
+ for (const sectionName of Object.keys(templateParsed)) {
152
+ if (userParsed[sectionName] && userParsed[sectionName].length > templateParsed[sectionName].length * 0.5) {
153
+ merged += `${templateParsed[sectionName].split('\n')[0]}\n${userParsed[sectionName]}\n`;
154
+ preservedSections.push(sectionName);
155
+ } else {
156
+ merged += templateParsed[sectionName] + '\n';
157
+ }
158
+ }
159
+
160
+ return {
161
+ content: merged.trim(),
162
+ preservedSections
163
+ };
164
+ }
165
+
166
+ function smartMergeKnowledge(userContent, templateContent) {
167
+ const userLines = userContent.split('\n');
168
+ const templateLines = templateContent.split('\n');
169
+
170
+ const userEntries = new Set();
171
+ const newEntries = [];
172
+
173
+ let currentTable = null;
174
+ let inUserTable = false;
175
+
176
+ for (const line of userLines) {
177
+ if (line.includes('|') && line.includes('---')) {
178
+ if (!inUserTable) inUserTable = true;
179
+ continue;
180
+ }
181
+ if (inUserTable && line.includes('|')) {
182
+ const cells = line.split('|').filter(c => c.trim());
183
+ if (cells.length >= 2 && !line.includes('Date') && !line.includes('---')) {
184
+ const entry = cells[1].trim();
185
+ if (entry && entry !== 'Mistake' && entry !== 'Pattern') {
186
+ userEntries.add(entry);
187
+ }
188
+ }
189
+ }
190
+ }
191
+
192
+ let merged = '';
193
+ let newLessonsCount = 0;
194
+ let addedNewSection = false;
195
+
196
+ for (const line of templateLines) {
197
+ if (line.includes('|') && line.includes('---')) {
198
+ merged += line + '\n';
199
+ continue;
200
+ }
201
+
202
+ if (line.includes('|') && !userEntries.has(line.split('|')[1]?.trim())) {
203
+ merged += line + '\n';
204
+ newLessonsCount++;
205
+ } else if (line.includes('|') && userEntries.has(line.split('|')[1]?.trim())) {
206
+ // Skip - user has this entry
207
+ } else {
208
+ merged += line + '\n';
209
+ }
210
+ }
211
+
212
+ return {
213
+ content: merged.trim(),
214
+ newEntriesCount: newLessonsCount
215
+ };
216
+ }
217
+
218
+ function getAllTemplateFiles(srcDir, baseDir = '') {
219
+ const files = [];
220
+ if (!fs.existsSync(srcDir)) return files;
221
+
222
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
223
+ for (const entry of entries) {
224
+ if (entry.name === '.git') continue;
225
+
226
+ const relativePath = path.join(baseDir, entry.name);
227
+ const srcPath = path.join(srcDir, entry.name);
228
+
229
+ if (entry.isDirectory()) {
230
+ files.push(...getAllTemplateFiles(srcPath, relativePath));
231
+ } else {
232
+ files.push(relativePath);
233
+ }
234
+ }
235
+ return files;
236
+ }
237
+
238
+ export async function checkForUpdates(targetDir) {
239
+ const versionFile = path.join(targetDir, '.ocs-version');
240
+ const currentVersion = fs.existsSync(versionFile)
241
+ ? fs.readFileSync(versionFile, 'utf-8').trim()
242
+ : '0.0.0';
243
+
244
+ const templateVersion = fs.readFileSync(path.join(TEMPLATES_DIR, '.ocs-version'), 'utf-8').trim();
245
+
246
+ const comparison = compareVersion(templateVersion, currentVersion);
247
+
248
+ if (comparison <= 0) {
249
+ return {
250
+ hasUpdates: false,
251
+ currentVersion,
252
+ templateVersion,
253
+ changes: []
254
+ };
255
+ }
256
+
257
+ const templateFiles = getAllTemplateFiles(TEMPLATES_DIR);
258
+ const changes = [];
259
+
260
+ for (const relativePath of templateFiles) {
261
+ const templatePath = path.join(TEMPLATES_DIR, relativePath);
262
+ const targetPath = path.join(targetDir, relativePath);
263
+
264
+ const templateHash = hashFile(templatePath);
265
+ const targetHash = hashFile(targetPath);
266
+ const category = getCategory(relativePath);
267
+
268
+ let state;
269
+ if (!targetHash) {
270
+ state = FILE_STATE.NEW;
271
+ } else if (templateHash === targetHash) {
272
+ state = FILE_STATE.UNMODIFIED;
273
+ } else {
274
+ state = FILE_STATE.MODIFIED;
275
+ }
276
+
277
+ changes.push({
278
+ relativePath,
279
+ category,
280
+ state,
281
+ templateHash,
282
+ targetHash
283
+ });
284
+ }
285
+
286
+ return {
287
+ hasUpdates: comparison > 0,
288
+ currentVersion,
289
+ templateVersion,
290
+ changes
291
+ };
292
+ }
293
+
294
+ export async function performUpdate(targetDir, options = {}) {
295
+ const { force = false, checkOnly = false } = options;
296
+
297
+ const versionFile = path.join(targetDir, '.ocs-version');
298
+ const currentVersion = fs.existsSync(versionFile)
299
+ ? fs.readFileSync(versionFile, 'utf-8').trim()
300
+ : '0.0.0';
301
+
302
+ const templateVersion = fs.readFileSync(path.join(TEMPLATES_DIR, '.ocs-version'), 'utf-8').trim();
303
+
304
+ if (compareVersion(templateVersion, currentVersion) <= 0 && !force) {
305
+ console.log(` Already up to date (v${currentVersion})\n`);
306
+ return { updated: false, changes: [] };
307
+ }
308
+
309
+ const templateFiles = getAllTemplateFiles(TEMPLATES_DIR);
310
+
311
+ const results = {
312
+ updated: [],
313
+ added: [],
314
+ conflicts: [],
315
+ skipped: [],
316
+ merged: []
317
+ };
318
+
319
+ for (const relativePath of templateFiles) {
320
+ const templatePath = path.join(TEMPLATES_DIR, relativePath);
321
+ const targetPath = path.join(targetDir, relativePath);
322
+ const category = getCategory(relativePath);
323
+
324
+ const isVersionFile = relativePath === '.ocs-version';
325
+ const templateHash = hashFile(templatePath);
326
+ const targetHash = hashFile(targetPath);
327
+
328
+ if (!targetHash) {
329
+ if (!checkOnly) {
330
+ const destDir = path.dirname(targetPath);
331
+ if (!fs.existsSync(destDir)) {
332
+ fs.mkdirSync(destDir, { recursive: true });
333
+ }
334
+ fs.copyFileSync(templatePath, targetPath);
335
+ }
336
+ results.added.push(relativePath);
337
+ continue;
338
+ }
339
+
340
+ if (isVersionFile) {
341
+ if (!checkOnly) {
342
+ fs.copyFileSync(templatePath, targetPath);
343
+ }
344
+ results.updated.push(relativePath);
345
+ continue;
346
+ }
347
+
348
+ if (SMART_MERGE_CATEGORIES.includes(category)) {
349
+ if (category === FILE_CATEGORY.XP_DATA) {
350
+ const userXp = JSON.parse(fs.readFileSync(targetPath, 'utf-8'));
351
+ const templateXp = JSON.parse(fs.readFileSync(templatePath, 'utf-8'));
352
+
353
+ const preserveInfo = getSmartMergePreserveInfo(userXp, templateXp);
354
+
355
+ if (!checkOnly) {
356
+ const merged = smartMergeXp(userXp, templateXp);
357
+ fs.writeFileSync(targetPath, JSON.stringify(merged, null, 2));
358
+ }
359
+
360
+ results.merged.push({ path: relativePath, action: 'smart-merged', preserveInfo });
361
+ continue;
362
+ }
363
+
364
+ if (category === FILE_CATEGORY.MEMORIES) {
365
+ const userContent = fs.readFileSync(targetPath, 'utf-8');
366
+ const templateContent = fs.readFileSync(templatePath, 'utf-8');
367
+
368
+ const mergeResult = smartMergeMemories(userContent, templateContent);
369
+
370
+ if (!checkOnly) {
371
+ fs.writeFileSync(targetPath, mergeResult.content, 'utf-8');
372
+ }
373
+
374
+ results.merged.push({
375
+ path: relativePath,
376
+ action: 'smart-merged',
377
+ preserveInfo: mergeResult.preservedSections.join(', ')
378
+ });
379
+ continue;
380
+ }
381
+
382
+ if (category === FILE_CATEGORY.KNOWLEDGE) {
383
+ const userContent = fs.readFileSync(targetPath, 'utf-8');
384
+ const templateContent = fs.readFileSync(templatePath, 'utf-8');
385
+
386
+ const mergeResult = smartMergeKnowledge(userContent, templateContent);
387
+
388
+ if (!checkOnly) {
389
+ fs.writeFileSync(targetPath, mergeResult.content, 'utf-8');
390
+ }
391
+
392
+ results.merged.push({
393
+ path: relativePath,
394
+ action: 'smart-merged',
395
+ preserveInfo: `${mergeResult.newEntriesCount} new entries`
396
+ });
397
+ continue;
398
+ }
399
+ }
400
+
401
+ if (PROTECTED_CATEGORIES.includes(category)) {
402
+ results.skipped.push({ path: relativePath, reason: 'protected (user data)' });
403
+ continue;
404
+ }
405
+
406
+ if (templateHash === targetHash) {
407
+ if (!checkOnly) {
408
+ fs.copyFileSync(templatePath, targetPath);
409
+ }
410
+ results.updated.push(relativePath);
411
+ continue;
412
+ }
413
+
414
+ if (SMART_MERGE_CATEGORIES.includes(category)) {
415
+ if (category === FILE_CATEGORY.XP_DATA) {
416
+ const userXp = JSON.parse(fs.readFileSync(targetPath, 'utf-8'));
417
+ const templateXp = JSON.parse(fs.readFileSync(templatePath, 'utf-8'));
418
+
419
+ const preserveInfo = getSmartMergePreserveInfo(userXp, templateXp);
420
+
421
+ if (!checkOnly) {
422
+ const merged = smartMergeXp(userXp, templateXp);
423
+ fs.writeFileSync(targetPath, JSON.stringify(merged, null, 2));
424
+ }
425
+
426
+ results.merged.push({ path: relativePath, action: 'smart-merged', preserveInfo });
427
+ continue;
428
+ }
429
+
430
+ if (category === FILE_CATEGORY.MEMORIES) {
431
+ const userContent = fs.readFileSync(targetPath, 'utf-8');
432
+ const templateContent = fs.readFileSync(templatePath, 'utf-8');
433
+
434
+ const mergeResult = smartMergeMemories(userContent, templateContent);
435
+
436
+ if (!checkOnly) {
437
+ fs.writeFileSync(targetPath, mergeResult.content, 'utf-8');
438
+ }
439
+
440
+ results.merged.push({
441
+ path: relativePath,
442
+ action: 'smart-merged',
443
+ preserveInfo: mergeResult.preservedSections.join(', ')
444
+ });
445
+ continue;
446
+ }
447
+
448
+ if (category === FILE_CATEGORY.KNOWLEDGE) {
449
+ const userContent = fs.readFileSync(targetPath, 'utf-8');
450
+ const templateContent = fs.readFileSync(templatePath, 'utf-8');
451
+
452
+ const mergeResult = smartMergeKnowledge(userContent, templateContent);
453
+
454
+ if (!checkOnly) {
455
+ fs.writeFileSync(targetPath, mergeResult.content, 'utf-8');
456
+ }
457
+
458
+ results.merged.push({
459
+ path: relativePath,
460
+ action: 'smart-merged',
461
+ preserveInfo: `${mergeResult.newEntriesCount} new entries`
462
+ });
463
+ continue;
464
+ }
465
+ }
466
+
467
+ if (checkOnly) {
468
+ results.conflicts.push({ path: relativePath, category });
469
+ continue;
470
+ }
471
+
472
+ results.conflicts.push({ path: relativePath, category, needsPrompt: true });
473
+ }
474
+
475
+ if (!checkOnly) {
476
+ fs.writeFileSync(versionFile, templateVersion, 'utf-8');
477
+ }
478
+
479
+ return {
480
+ updated: true,
481
+ currentVersion,
482
+ templateVersion,
483
+ results
484
+ };
485
+ }
486
+
487
+ export function formatUpdateSummary(updateResult, checkOnly = false) {
488
+ const { results, templateVersion } = updateResult;
489
+
490
+ console.log('');
491
+
492
+ if (checkOnly) {
493
+ console.log(` Template version: v${templateVersion}`);
494
+ console.log(` Status: ${updateResult.hasUpdates ? 'Update available' : 'Up to date'}`);
495
+ } else {
496
+ console.log(` Updated to v${templateVersion}`);
497
+ }
498
+
499
+ if (results.added.length > 0) {
500
+ console.log(`\n Added (${results.added.length}):`);
501
+ for (const file of results.added.slice(0, 5)) {
502
+ console.log(` + ${file}`);
503
+ }
504
+ if (results.added.length > 5) {
505
+ console.log(` ... and ${results.added.length - 5} more`);
506
+ }
507
+ }
508
+
509
+ if (results.updated.length > 0) {
510
+ console.log(`\n Updated (${results.updated.length}):`);
511
+ for (const file of results.updated.slice(0, 5)) {
512
+ console.log(` ~ ${file}`);
513
+ }
514
+ if (results.updated.length > 5) {
515
+ console.log(` ... and ${results.updated.length - 5} more`);
516
+ }
517
+ }
518
+
519
+ if (results.merged.length > 0) {
520
+ console.log(`\n Smart Merged (${results.merged.length}):`);
521
+ for (const file of results.merged.slice(0, 4)) {
522
+ const info = file.preserveInfo ? ` (${file.preserveInfo})` : '';
523
+ console.log(` ◊ ${file.path}${info}`);
524
+ }
525
+ if (results.merged.length > 4) {
526
+ console.log(` ... and ${results.merged.length - 4} more`);
527
+ }
528
+ }
529
+
530
+ if (results.skipped.length > 0) {
531
+ console.log(`\n Skipped - protected (${results.skipped.length}):`);
532
+ for (const file of results.skipped.slice(0, 3)) {
533
+ console.log(` ⊘ ${file.path} (${file.reason})`);
534
+ }
535
+ if (results.skipped.length > 3) {
536
+ console.log(` ... and ${results.skipped.length - 3} more`);
537
+ }
538
+ }
539
+
540
+ if (results.conflicts.length > 0) {
541
+ console.log(`\n Conflicts (${results.conflicts.length}):`);
542
+ for (const file of results.conflicts.slice(0, 5)) {
543
+ console.log(` ⚠ ${file.path}`);
544
+ }
545
+ if (results.conflicts.length > 5) {
546
+ console.log(` ... and ${results.conflicts.length - 5} more`);
547
+ }
548
+ console.log(`\n Run without --check to resolve conflicts`);
549
+ }
550
+
551
+ console.log('');
552
+ }
@@ -0,0 +1 @@
1
+ 1.1.4