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 +24 -13
- package/package.json +1 -1
- package/src/init.js +45 -28
- package/src/merge.js +552 -0
- package/templates/.ocs-version +1 -0
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 (
|
|
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 })
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
71
|
+
else {
|
|
72
|
+
init({ isGlobal }).catch((err) => {
|
|
73
|
+
console.error('Error:', err.message);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
});
|
|
76
|
+
}
|
package/package.json
CHANGED
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|