vibecodingmachine-core 1.0.0

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.
Files changed (54) hide show
  1. package/.babelrc +13 -0
  2. package/README.md +28 -0
  3. package/__tests__/applescript-manager-claude-fix.test.js +286 -0
  4. package/__tests__/requirement-2-auto-start-looping.test.js +69 -0
  5. package/__tests__/requirement-3-auto-start-looping.test.js +69 -0
  6. package/__tests__/requirement-4-auto-start-looping.test.js +69 -0
  7. package/__tests__/requirement-6-auto-start-looping.test.js +73 -0
  8. package/__tests__/requirement-7-status-tracking.test.js +332 -0
  9. package/jest.config.js +18 -0
  10. package/jest.setup.js +12 -0
  11. package/package.json +46 -0
  12. package/src/auth/access-denied.html +119 -0
  13. package/src/auth/shared-auth-storage.js +230 -0
  14. package/src/autonomous-mode/feature-implementer.cjs +70 -0
  15. package/src/autonomous-mode/feature-implementer.js +425 -0
  16. package/src/chat-management/chat-manager.cjs +71 -0
  17. package/src/chat-management/chat-manager.js +342 -0
  18. package/src/ide-integration/__tests__/applescript-manager-thread-closure.test.js +227 -0
  19. package/src/ide-integration/aider-cli-manager.cjs +850 -0
  20. package/src/ide-integration/applescript-diagnostics.js +0 -0
  21. package/src/ide-integration/applescript-manager.cjs +1088 -0
  22. package/src/ide-integration/applescript-manager.js +2803 -0
  23. package/src/ide-integration/applescript-open-apps.js +0 -0
  24. package/src/ide-integration/applescript-read-response.js +0 -0
  25. package/src/ide-integration/applescript-send-text.js +0 -0
  26. package/src/ide-integration/applescript-thread-closure.js +0 -0
  27. package/src/ide-integration/applescript-utils.js +306 -0
  28. package/src/ide-integration/cdp-manager.cjs +221 -0
  29. package/src/ide-integration/cdp-manager.js +321 -0
  30. package/src/ide-integration/claude-code-cli-manager.cjs +301 -0
  31. package/src/ide-integration/cline-cli-manager.cjs +2252 -0
  32. package/src/ide-integration/continue-cli-manager.js +431 -0
  33. package/src/ide-integration/provider-manager.cjs +354 -0
  34. package/src/ide-integration/quota-detector.cjs +34 -0
  35. package/src/ide-integration/quota-detector.js +349 -0
  36. package/src/ide-integration/windows-automation-manager.js +262 -0
  37. package/src/index.cjs +43 -0
  38. package/src/index.js +17 -0
  39. package/src/llm/direct-llm-manager.cjs +609 -0
  40. package/src/ui/ButtonComponents.js +247 -0
  41. package/src/ui/ChatInterface.js +499 -0
  42. package/src/ui/StateManager.js +259 -0
  43. package/src/ui/StateManager.test.js +0 -0
  44. package/src/utils/audit-logger.cjs +116 -0
  45. package/src/utils/config-helpers.cjs +94 -0
  46. package/src/utils/config-helpers.js +94 -0
  47. package/src/utils/electron-update-checker.js +78 -0
  48. package/src/utils/gcloud-auth.cjs +394 -0
  49. package/src/utils/logger.cjs +193 -0
  50. package/src/utils/logger.js +191 -0
  51. package/src/utils/repo-helpers.cjs +120 -0
  52. package/src/utils/repo-helpers.js +120 -0
  53. package/src/utils/requirement-helpers.js +432 -0
  54. package/src/utils/update-checker.js +167 -0
@@ -0,0 +1,432 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const { getRequirementsPath } = require('./repo-helpers.cjs');
4
+
5
+ /**
6
+ * Get ordinal suffix for numbers (1st, 2nd, 3rd, 4th, etc.)
7
+ * @param {number} num - The number
8
+ * @returns {string} The ordinal suffix
9
+ */
10
+ function getOrdinalSuffix(num) {
11
+ const j = num % 10;
12
+ const k = num % 100;
13
+ if (j === 1 && k !== 11) {
14
+ return 'st';
15
+ }
16
+ if (j === 2 && k !== 12) {
17
+ return 'nd';
18
+ }
19
+ if (j === 3 && k !== 13) {
20
+ return 'rd';
21
+ }
22
+ return 'th';
23
+ }
24
+
25
+ /**
26
+ * Add or increment TRY AGAIN prefix to requirement text
27
+ * @param {string} requirementText - The requirement text
28
+ * @returns {string} Requirement text with TRY AGAIN prefix
29
+ */
30
+ function addTryAgainPrefix(requirementText) {
31
+ const tryAgainMatch = requirementText.match(/^TRY AGAIN \((\d+)(?:st|nd|rd|th) time\): (.+)$/);
32
+
33
+ if (tryAgainMatch) {
34
+ const currentCount = parseInt(tryAgainMatch[1]);
35
+ const baseRequirement = tryAgainMatch[2];
36
+ return `TRY AGAIN (${currentCount + 1}${getOrdinalSuffix(currentCount + 1)} time): ${baseRequirement}`;
37
+ } else {
38
+ return `TRY AGAIN (1st time): ${requirementText}`;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Parse requirement from markdown line
44
+ * @param {string} line - Markdown line (e.g., "- Requirement text")
45
+ * @returns {string} Requirement text without markdown prefix
46
+ */
47
+ function parseRequirementLine(line) {
48
+ if (line.startsWith('- ')) {
49
+ return line.substring(2);
50
+ }
51
+ return line;
52
+ }
53
+
54
+ /**
55
+ * Move requirement from TO VERIFY section to VERIFIED (CHANGELOG)
56
+ * @param {string} reqPath - Path to REQUIREMENTS file
57
+ * @param {string} requirementTitle - Title of requirement to move
58
+ * @returns {Promise<boolean>} Success status
59
+ */
60
+ async function promoteToVerified(reqPath, requirementTitle) {
61
+ try {
62
+ const content = await fs.readFile(reqPath, 'utf-8');
63
+ const lines = content.split('\n');
64
+ const updatedLines = [];
65
+ let inVerifySection = false;
66
+ let verifyCount = 0;
67
+ let requirementToMove = null;
68
+
69
+ for (const line of lines) {
70
+ if (line.includes('## ✅ Verified by AI screenshot')) {
71
+ inVerifySection = true;
72
+ updatedLines.push(line);
73
+ continue;
74
+ }
75
+
76
+ if (inVerifySection && line.startsWith('## ')) {
77
+ inVerifySection = false;
78
+ updatedLines.push(line);
79
+ continue;
80
+ }
81
+
82
+ if (inVerifySection && line.startsWith('- ')) {
83
+ const reqText = parseRequirementLine(line);
84
+ if (reqText === requirementTitle || reqText.includes(requirementTitle)) {
85
+ requirementToMove = reqText;
86
+ verifyCount++;
87
+ continue;
88
+ }
89
+ verifyCount++;
90
+ }
91
+
92
+ updatedLines.push(line);
93
+ }
94
+
95
+ if (requirementToMove) {
96
+ // CHANGELOG.md should be at repository root, not in .vibecodingmachine directory
97
+ const allnightDir = path.dirname(reqPath); // .vibecodingmachine directory
98
+ const repoRoot = path.dirname(allnightDir); // repository root (one level up)
99
+ const changelogPath = path.join(repoRoot, 'CHANGELOG.md');
100
+ const timestamp = new Date().toISOString().split('T')[0];
101
+ const changelogEntry = `- ${requirementToMove} (${timestamp})`;
102
+
103
+ let changelogContent = '';
104
+ if (await fs.pathExists(changelogPath)) {
105
+ changelogContent = await fs.readFile(changelogPath, 'utf-8');
106
+ } else {
107
+ changelogContent = '# Changelog\n\n## Verified Requirements\n\n';
108
+ }
109
+
110
+ if (changelogContent.includes('## Verified Requirements')) {
111
+ changelogContent = changelogContent.replace(
112
+ '## Verified Requirements\n',
113
+ `## Verified Requirements\n${changelogEntry}\n`
114
+ );
115
+ } else {
116
+ changelogContent += `\n## Verified Requirements\n${changelogEntry}\n`;
117
+ }
118
+
119
+ await fs.writeFile(changelogPath, changelogContent);
120
+ await fs.writeFile(reqPath, updatedLines.join('\n'));
121
+ return true;
122
+ }
123
+
124
+ return false;
125
+ } catch (error) {
126
+ throw new Error(`Failed to promote requirement to verified: ${error.message}`);
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Move requirement from VERIFIED (CHANGELOG) back to TODO with TRY AGAIN prefix
132
+ * @param {string} reqPath - Path to REQUIREMENTS file
133
+ * @param {string} requirementTitle - Title of requirement to move
134
+ * @returns {Promise<boolean>} Success status
135
+ */
136
+ async function demoteFromVerifiedToTodo(reqPath, requirementTitle) {
137
+ try {
138
+ // CHANGELOG.md should be at repository root, not in .vibecodingmachine directory
139
+ const allnightDir = path.dirname(reqPath); // .vibecodingmachine directory
140
+ const repoRoot = path.dirname(allnightDir); // repository root (one level up)
141
+ const changelogPath = path.join(repoRoot, 'CHANGELOG.md');
142
+
143
+ if (!(await fs.pathExists(changelogPath))) {
144
+ return false;
145
+ }
146
+
147
+ let changelogContent = await fs.readFile(changelogPath, 'utf-8');
148
+ const changelogLines = changelogContent.split('\n');
149
+ const updatedChangelogLines = [];
150
+ let requirementToMove = null;
151
+
152
+ for (const line of changelogLines) {
153
+ if (line.startsWith('- ')) {
154
+ const lineText = parseRequirementLine(line);
155
+ // Extract title part (before timestamp in parentheses)
156
+ const titleMatch = lineText.match(/^(.+?)\s*\([\d-]+\)$/);
157
+ const lineTitle = titleMatch ? titleMatch[1] : lineText;
158
+
159
+ // Check if this line matches the requirement title
160
+ // Handle both cases: requirementTitle might include timestamp or not
161
+ const reqTitleMatch = requirementTitle.match(/^(.+?)\s*\([\d-]+\)$/);
162
+ const reqTitleOnly = reqTitleMatch ? reqTitleMatch[1] : requirementTitle;
163
+
164
+ if (lineTitle === reqTitleOnly || lineTitle.includes(reqTitleOnly) || reqTitleOnly.includes(lineTitle)) {
165
+ requirementToMove = lineTitle;
166
+ continue;
167
+ }
168
+ }
169
+ updatedChangelogLines.push(line);
170
+ }
171
+
172
+ if (!requirementToMove) {
173
+ return false;
174
+ }
175
+
176
+ await fs.writeFile(changelogPath, updatedChangelogLines.join('\n'));
177
+
178
+ const content = await fs.readFile(reqPath, 'utf-8');
179
+ const lines = content.split('\n');
180
+ const updatedLines = [];
181
+ let foundTodoSection = false;
182
+
183
+ for (let i = 0; i < lines.length; i++) {
184
+ const line = lines[i];
185
+
186
+ if (line.includes('## ⏳ Requirements not yet completed')) {
187
+ foundTodoSection = true;
188
+ updatedLines.push(line);
189
+ const requirementText = addTryAgainPrefix(requirementToMove);
190
+ updatedLines.push(`- ${requirementText}`);
191
+ continue;
192
+ }
193
+
194
+ updatedLines.push(line);
195
+ }
196
+
197
+ if (!foundTodoSection) {
198
+ updatedLines.push('## ⏳ Requirements not yet completed');
199
+ const requirementText = addTryAgainPrefix(requirementToMove);
200
+ updatedLines.push(`- ${requirementText}`);
201
+ }
202
+
203
+ await fs.writeFile(reqPath, updatedLines.join('\n'));
204
+ return true;
205
+ } catch (error) {
206
+ throw new Error(`Failed to demote requirement from verified: ${error.message}`);
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Move requirement from TODO to TO VERIFY section
212
+ * @param {string} reqPath - Path to REQUIREMENTS file
213
+ * @param {string} requirementTitle - Title of requirement to move
214
+ * @returns {Promise<boolean>} Success status
215
+ */
216
+ async function promoteTodoToVerify(reqPath, requirementTitle) {
217
+ try {
218
+ const content = await fs.readFile(reqPath, 'utf-8');
219
+ const lines = content.split('\n');
220
+
221
+ // Find the requirement block (### header format)
222
+ let requirementStartIndex = -1;
223
+ let requirementEndIndex = -1;
224
+ let inTodoSection = false;
225
+
226
+ for (let i = 0; i < lines.length; i++) {
227
+ const line = lines[i].trim();
228
+
229
+ if (line.includes('## ⏳ Requirements not yet completed')) {
230
+ inTodoSection = true;
231
+ continue;
232
+ }
233
+
234
+ if (inTodoSection && line.startsWith('##') && !line.startsWith('###')) {
235
+ break;
236
+ }
237
+
238
+ if (inTodoSection && line.startsWith('###')) {
239
+ const title = line.replace(/^###\s*/, '').trim();
240
+ if (title && (title === requirementTitle || title.includes(requirementTitle) || requirementTitle.includes(title))) {
241
+ requirementStartIndex = i;
242
+ // Find the end of this requirement (next ### or ## header)
243
+ for (let j = i + 1; j < lines.length; j++) {
244
+ const nextLine = lines[j].trim();
245
+ if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
246
+ requirementEndIndex = j;
247
+ break;
248
+ }
249
+ }
250
+ if (requirementEndIndex === -1) {
251
+ requirementEndIndex = lines.length;
252
+ }
253
+ break;
254
+ }
255
+ }
256
+ }
257
+
258
+ if (requirementStartIndex === -1) {
259
+ return false;
260
+ }
261
+
262
+ // Extract the requirement block
263
+ const requirementBlock = lines.slice(requirementStartIndex, requirementEndIndex);
264
+
265
+ // Remove the requirement from its current location
266
+ const updatedLines = [
267
+ ...lines.slice(0, requirementStartIndex),
268
+ ...lines.slice(requirementEndIndex)
269
+ ];
270
+
271
+ // Find or create TO VERIFY section
272
+ const verifySectionVariants = [
273
+ '## 🔍 TO VERIFY BY HUMAN',
274
+ '## 🔍 TO VERIFY',
275
+ '## TO VERIFY',
276
+ '## ✅ TO VERIFY',
277
+ '## ✅ Verified by AI screenshot'
278
+ ];
279
+
280
+ let verifyIndex = -1;
281
+ for (let i = 0; i < updatedLines.length; i++) {
282
+ if (verifySectionVariants.some(variant => updatedLines[i].includes(variant))) {
283
+ verifyIndex = i;
284
+ break;
285
+ }
286
+ }
287
+
288
+ if (verifyIndex === -1) {
289
+ // Create TO VERIFY section before CHANGELOG or at end
290
+ const changelogIndex = updatedLines.findIndex(line => line.includes('## CHANGELOG'));
291
+ const insertIndex = changelogIndex > 0 ? changelogIndex : updatedLines.length;
292
+ updatedLines.splice(insertIndex, 0, '', '## 🔍 TO VERIFY BY HUMAN', '');
293
+ verifyIndex = insertIndex + 1;
294
+ }
295
+
296
+ // Insert requirement block after section header
297
+ let insertIndex = verifyIndex + 1;
298
+ while (insertIndex < updatedLines.length && updatedLines[insertIndex].trim() === '') {
299
+ insertIndex++;
300
+ }
301
+ updatedLines.splice(insertIndex, 0, ...requirementBlock);
302
+ // Add blank line after if needed
303
+ if (insertIndex + requirementBlock.length < updatedLines.length && updatedLines[insertIndex + requirementBlock.length].trim() !== '') {
304
+ updatedLines.splice(insertIndex + requirementBlock.length, 0, '');
305
+ }
306
+
307
+ await fs.writeFile(reqPath, updatedLines.join('\n'));
308
+ return true;
309
+ } catch (error) {
310
+ throw new Error(`Failed to promote requirement from TODO to TO VERIFY: ${error.message}`);
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Move requirement from TO VERIFY back to TODO section
316
+ * @param {string} reqPath - Path to REQUIREMENTS file
317
+ * @param {string} requirementTitle - Title of requirement to move
318
+ * @returns {Promise<boolean>} Success status
319
+ */
320
+ async function demoteVerifyToTodo(reqPath, requirementTitle) {
321
+ try {
322
+ const content = await fs.readFile(reqPath, 'utf-8');
323
+ const lines = content.split('\n');
324
+
325
+ // Find the requirement block in TO VERIFY section (### header format)
326
+ let requirementStartIndex = -1;
327
+ let requirementEndIndex = -1;
328
+ let inVerifySection = false;
329
+
330
+ const verifySectionVariants = [
331
+ '## 🔍 TO VERIFY BY HUMAN',
332
+ '## 🔍 TO VERIFY',
333
+ '## TO VERIFY',
334
+ '## ✅ TO VERIFY',
335
+ '## ✅ Verified by AI screenshot'
336
+ ];
337
+
338
+ for (let i = 0; i < lines.length; i++) {
339
+ const line = lines[i].trim();
340
+
341
+ if (verifySectionVariants.some(variant => line.includes(variant))) {
342
+ inVerifySection = true;
343
+ continue;
344
+ }
345
+
346
+ if (inVerifySection && line.startsWith('##') && !line.startsWith('###')) {
347
+ break;
348
+ }
349
+
350
+ if (inVerifySection && line.startsWith('###')) {
351
+ const title = line.replace(/^###\s*/, '').trim();
352
+ if (title && (title === requirementTitle || title.includes(requirementTitle) || requirementTitle.includes(title))) {
353
+ requirementStartIndex = i;
354
+ // Find the end of this requirement (next ### or ## header)
355
+ for (let j = i + 1; j < lines.length; j++) {
356
+ const nextLine = lines[j].trim();
357
+ if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
358
+ requirementEndIndex = j;
359
+ break;
360
+ }
361
+ }
362
+ if (requirementEndIndex === -1) {
363
+ requirementEndIndex = lines.length;
364
+ }
365
+ break;
366
+ }
367
+ }
368
+ }
369
+
370
+ if (requirementStartIndex === -1) {
371
+ return false;
372
+ }
373
+
374
+ // Extract the requirement block
375
+ const requirementBlock = lines.slice(requirementStartIndex, requirementEndIndex);
376
+
377
+ // Update title with TRY AGAIN prefix
378
+ const originalTitle = requirementBlock[0].replace(/^###\s*/, '').trim();
379
+ const titleWithPrefix = addTryAgainPrefix(originalTitle);
380
+ requirementBlock[0] = `### ${titleWithPrefix}`;
381
+
382
+ // Remove the requirement from its current location
383
+ const updatedLines = [
384
+ ...lines.slice(0, requirementStartIndex),
385
+ ...lines.slice(requirementEndIndex)
386
+ ];
387
+
388
+ // Find or create TODO section
389
+ let todoIndex = -1;
390
+ for (let i = 0; i < updatedLines.length; i++) {
391
+ if (updatedLines[i].includes('## ⏳ Requirements not yet completed')) {
392
+ todoIndex = i;
393
+ break;
394
+ }
395
+ }
396
+
397
+ if (todoIndex === -1) {
398
+ // Create TODO section at the top (after initial headers)
399
+ const firstSectionIndex = updatedLines.findIndex(line => line.startsWith('##') && !line.startsWith('###'));
400
+ const insertIndex = firstSectionIndex > 0 ? firstSectionIndex : updatedLines.length;
401
+ updatedLines.splice(insertIndex, 0, '## ⏳ Requirements not yet completed', '');
402
+ todoIndex = insertIndex;
403
+ }
404
+
405
+ // Insert requirement block after section header
406
+ let insertIndex = todoIndex + 1;
407
+ while (insertIndex < updatedLines.length && updatedLines[insertIndex].trim() === '') {
408
+ insertIndex++;
409
+ }
410
+ updatedLines.splice(insertIndex, 0, ...requirementBlock);
411
+ // Add blank line after if needed
412
+ if (insertIndex + requirementBlock.length < updatedLines.length && updatedLines[insertIndex + requirementBlock.length].trim() !== '') {
413
+ updatedLines.splice(insertIndex + requirementBlock.length, 0, '');
414
+ }
415
+
416
+ await fs.writeFile(reqPath, updatedLines.join('\n'));
417
+ return true;
418
+ } catch (error) {
419
+ throw new Error(`Failed to demote requirement from TO VERIFY to TODO: ${error.message}`);
420
+ }
421
+ }
422
+
423
+ module.exports = {
424
+ getOrdinalSuffix,
425
+ addTryAgainPrefix,
426
+ parseRequirementLine,
427
+ promoteToVerified,
428
+ demoteFromVerifiedToTodo,
429
+ promoteTodoToVerify,
430
+ demoteVerifyToTodo
431
+ };
432
+
@@ -0,0 +1,167 @@
1
+ const https = require('https');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ /**
6
+ * Check for updates from npm registry
7
+ * @param {string} packageName - Name of the package to check (e.g., 'allnightai-cli')
8
+ * @param {string} currentVersion - Current version (e.g., '1.0.0')
9
+ * @returns {Promise<Object>} Update info or null if no update available
10
+ */
11
+ async function checkForUpdates(packageName, currentVersion) {
12
+ return new Promise((resolve, reject) => {
13
+ const registryUrl = `https://registry.npmjs.org/${packageName}`;
14
+
15
+ https.get(registryUrl, (res) => {
16
+ let data = '';
17
+
18
+ res.on('data', (chunk) => {
19
+ data += chunk;
20
+ });
21
+
22
+ res.on('end', () => {
23
+ try {
24
+ const packageInfo = JSON.parse(data);
25
+ const latestVersion = packageInfo['dist-tags'].latest;
26
+
27
+ if (latestVersion !== currentVersion) {
28
+ const versionInfo = packageInfo.versions[latestVersion];
29
+ const publishedDate = new Date(packageInfo.time[latestVersion]);
30
+
31
+ resolve({
32
+ hasUpdate: true,
33
+ currentVersion,
34
+ latestVersion,
35
+ publishedDate: publishedDate.toLocaleString('en-US', {
36
+ year: 'numeric',
37
+ month: '2-digit',
38
+ day: '2-digit',
39
+ hour: '2-digit',
40
+ minute: '2-digit',
41
+ timeZoneName: 'short'
42
+ }),
43
+ packageName,
44
+ description: versionInfo.description || ''
45
+ });
46
+ } else {
47
+ resolve({
48
+ hasUpdate: false,
49
+ currentVersion,
50
+ latestVersion,
51
+ packageName
52
+ });
53
+ }
54
+ } catch (error) {
55
+ console.error('Error parsing registry response:', error);
56
+ resolve({ hasUpdate: false, currentVersion, packageName, error: error.message });
57
+ }
58
+ });
59
+ }).on('error', (error) => {
60
+ console.error('Error checking for updates:', error);
61
+ resolve({ hasUpdate: false, currentVersion, packageName, error: error.message });
62
+ });
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Get update info cache file path
68
+ * @param {string} packageName - Package name
69
+ * @returns {string} Path to cache file
70
+ */
71
+ function getUpdateCachePath(packageName) {
72
+ const homeDir = require('os').homedir();
73
+ const cacheDir = path.join(homeDir, '.config', 'allnightai');
74
+
75
+ // Ensure cache directory exists
76
+ if (!fs.existsSync(cacheDir)) {
77
+ fs.mkdirSync(cacheDir, { recursive: true });
78
+ }
79
+
80
+ return path.join(cacheDir, `${packageName}-update-cache.json`);
81
+ }
82
+
83
+ /**
84
+ * Load cached update info
85
+ * @param {string} packageName - Package name
86
+ * @returns {Object|null} Cached update info or null
87
+ */
88
+ function loadUpdateCache(packageName) {
89
+ try {
90
+ const cachePath = getUpdateCachePath(packageName);
91
+ if (fs.existsSync(cachePath)) {
92
+ const data = fs.readFileSync(cachePath, 'utf8');
93
+ const cache = JSON.parse(data);
94
+
95
+ // Cache is valid for 1 hour
96
+ const cacheAge = Date.now() - cache.checkedAt;
97
+ if (cacheAge < 3600000) { // 1 hour
98
+ return cache.updateInfo;
99
+ }
100
+ }
101
+ } catch (error) {
102
+ console.error('Error loading update cache:', error);
103
+ }
104
+ return null;
105
+ }
106
+
107
+ /**
108
+ * Save update info to cache
109
+ * @param {string} packageName - Package name
110
+ * @param {Object} updateInfo - Update info to cache
111
+ */
112
+ function saveUpdateCache(packageName, updateInfo) {
113
+ try {
114
+ const cachePath = getUpdateCachePath(packageName);
115
+ const cache = {
116
+ checkedAt: Date.now(),
117
+ updateInfo
118
+ };
119
+ fs.writeFileSync(cachePath, JSON.stringify(cache, null, 2));
120
+ } catch (error) {
121
+ console.error('Error saving update cache:', error);
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Check for updates with caching
127
+ * @param {string} packageName - Package name
128
+ * @param {string} currentVersion - Current version
129
+ * @param {boolean} forceCheck - Force check even if cache is valid
130
+ * @returns {Promise<Object>} Update info
131
+ */
132
+ async function checkForUpdatesWithCache(packageName, currentVersion, forceCheck = false) {
133
+ if (!forceCheck) {
134
+ const cached = loadUpdateCache(packageName);
135
+ if (cached) {
136
+ return cached;
137
+ }
138
+ }
139
+
140
+ const updateInfo = await checkForUpdates(packageName, currentVersion);
141
+ saveUpdateCache(packageName, updateInfo);
142
+ return updateInfo;
143
+ }
144
+
145
+ /**
146
+ * Clear update notification (mark as dismissed)
147
+ * @param {string} packageName - Package name
148
+ */
149
+ function clearUpdateNotification(packageName) {
150
+ try {
151
+ const cachePath = getUpdateCachePath(packageName);
152
+ if (fs.existsSync(cachePath)) {
153
+ fs.unlinkSync(cachePath);
154
+ }
155
+ } catch (error) {
156
+ console.error('Error clearing update notification:', error);
157
+ }
158
+ }
159
+
160
+ module.exports = {
161
+ checkForUpdates,
162
+ checkForUpdatesWithCache,
163
+ loadUpdateCache,
164
+ saveUpdateCache,
165
+ clearUpdateNotification,
166
+ getUpdateCachePath
167
+ };