vibecodingmachine-core 2025.11.2-9.855 → 2025.12.6-1702
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/package.json +4 -1
- package/src/auth/shared-auth-storage.js +43 -6
- package/src/ide-integration/applescript-manager.cjs +126 -18
- package/src/ide-integration/applescript-manager.js +172 -84
- package/src/index.cjs +2 -0
- package/src/index.js +2 -0
- package/src/sync/aws-setup.js +445 -0
- package/src/sync/sync-engine.js +388 -0
- package/src/utils/electron-update-checker.js +23 -8
- package/src/utils/requirement-helpers.js +139 -70
- package/src/utils/requirements-parser.js +310 -0
- package/src/utils/update-checker.js +15 -3
- package/src/utils/version-checker.js +6 -5
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const EventEmitter = require('events');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* SyncEngine - Handles bidirectional sync between local files and AWS DynamoDB
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Real-time change detection using file watchers
|
|
12
|
+
* - Conflict resolution strategies
|
|
13
|
+
* - Offline queue for disconnected state
|
|
14
|
+
* - Change history tracking
|
|
15
|
+
* - Delta sync for efficiency
|
|
16
|
+
*/
|
|
17
|
+
class SyncEngine extends EventEmitter {
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
super();
|
|
20
|
+
|
|
21
|
+
this.options = {
|
|
22
|
+
syncInterval: options.syncInterval || 30000, // 30 seconds
|
|
23
|
+
conflictStrategy: options.conflictStrategy || 'last-write-wins',
|
|
24
|
+
offlineMode: options.offlineMode || false,
|
|
25
|
+
...options
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
this.computerId = this._getComputerId();
|
|
29
|
+
this.isOnline = !this.offlineMode;
|
|
30
|
+
this.isSyncing = false;
|
|
31
|
+
this.offlineQueue = [];
|
|
32
|
+
this.lastSyncTime = null;
|
|
33
|
+
this.changeHistory = [];
|
|
34
|
+
|
|
35
|
+
// AWS clients (will be initialized when needed)
|
|
36
|
+
this.dynamoClient = null;
|
|
37
|
+
this.apiClient = null;
|
|
38
|
+
this.wsClient = null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get unique computer ID
|
|
43
|
+
*/
|
|
44
|
+
_getComputerId() {
|
|
45
|
+
const hostname = os.hostname();
|
|
46
|
+
return hostname;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Initialize AWS clients
|
|
51
|
+
*/
|
|
52
|
+
async initialize() {
|
|
53
|
+
try {
|
|
54
|
+
// Initialize DynamoDB client
|
|
55
|
+
const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
|
|
56
|
+
const { DynamoDBDocumentClient } = require('@aws-sdk/lib-dynamodb');
|
|
57
|
+
|
|
58
|
+
const region = process.env.AWS_REGION || 'us-east-1';
|
|
59
|
+
const client = new DynamoDBClient({ region });
|
|
60
|
+
this.dynamoClient = DynamoDBDocumentClient.from(client);
|
|
61
|
+
|
|
62
|
+
// Initialize WebSocket client for real-time updates
|
|
63
|
+
await this._initializeWebSocket();
|
|
64
|
+
|
|
65
|
+
this.emit('initialized');
|
|
66
|
+
return true;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
this.emit('error', { type: 'initialization', error });
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Initialize WebSocket connection for real-time updates
|
|
75
|
+
*/
|
|
76
|
+
async _initializeWebSocket() {
|
|
77
|
+
// TODO: Implement WebSocket connection to AWS IoT Core or API Gateway WebSocket
|
|
78
|
+
// For now, we'll use polling
|
|
79
|
+
this.emit('websocket-status', { connected: false, reason: 'not-implemented' });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Start sync engine
|
|
84
|
+
*/
|
|
85
|
+
async start() {
|
|
86
|
+
if (!this.dynamoClient) {
|
|
87
|
+
await this.initialize();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Start periodic sync
|
|
91
|
+
this.syncInterval = setInterval(() => {
|
|
92
|
+
this.sync().catch(error => {
|
|
93
|
+
this.emit('error', { type: 'sync', error });
|
|
94
|
+
});
|
|
95
|
+
}, this.options.syncInterval);
|
|
96
|
+
|
|
97
|
+
// Do initial sync
|
|
98
|
+
await this.sync();
|
|
99
|
+
|
|
100
|
+
this.emit('started');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Stop sync engine
|
|
105
|
+
*/
|
|
106
|
+
stop() {
|
|
107
|
+
if (this.syncInterval) {
|
|
108
|
+
clearInterval(this.syncInterval);
|
|
109
|
+
this.syncInterval = null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.emit('stopped');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Perform sync operation
|
|
117
|
+
*/
|
|
118
|
+
async sync() {
|
|
119
|
+
if (this.isSyncing) {
|
|
120
|
+
return; // Already syncing
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.isSyncing = true;
|
|
124
|
+
this.emit('sync-start');
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
// Check if online
|
|
128
|
+
if (!this.isOnline) {
|
|
129
|
+
this.emit('sync-complete', { status: 'offline', queued: this.offlineQueue.length });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Process offline queue first
|
|
134
|
+
if (this.offlineQueue.length > 0) {
|
|
135
|
+
await this._processOfflineQueue();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Fetch remote changes
|
|
139
|
+
const remoteChanges = await this._fetchRemoteChanges();
|
|
140
|
+
|
|
141
|
+
// Detect local changes
|
|
142
|
+
const localChanges = await this._detectLocalChanges();
|
|
143
|
+
|
|
144
|
+
// Resolve conflicts
|
|
145
|
+
const conflicts = this._detectConflicts(localChanges, remoteChanges);
|
|
146
|
+
if (conflicts.length > 0) {
|
|
147
|
+
await this._resolveConflicts(conflicts);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Apply remote changes locally
|
|
151
|
+
if (remoteChanges.length > 0) {
|
|
152
|
+
await this._applyRemoteChanges(remoteChanges);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Push local changes to remote
|
|
156
|
+
if (localChanges.length > 0) {
|
|
157
|
+
await this._pushLocalChanges(localChanges);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this.lastSyncTime = Date.now();
|
|
161
|
+
this.emit('sync-complete', {
|
|
162
|
+
status: 'success',
|
|
163
|
+
remoteChanges: remoteChanges.length,
|
|
164
|
+
localChanges: localChanges.length,
|
|
165
|
+
conflicts: conflicts.length
|
|
166
|
+
});
|
|
167
|
+
} catch (error) {
|
|
168
|
+
this.emit('sync-complete', { status: 'error', error: error.message });
|
|
169
|
+
// Don't throw - just emit error event
|
|
170
|
+
this.emit('error', { type: 'sync', error: error.message });
|
|
171
|
+
} finally {
|
|
172
|
+
this.isSyncing = false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Fetch remote changes from DynamoDB
|
|
178
|
+
*/
|
|
179
|
+
async _fetchRemoteChanges() {
|
|
180
|
+
const { ScanCommand } = require('@aws-sdk/lib-dynamodb');
|
|
181
|
+
|
|
182
|
+
const tableName = process.env.DYNAMODB_TABLE_NAME || 'vibecodingmachine-requirements';
|
|
183
|
+
const lastSync = this.lastSyncTime || 0;
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
// Use Scan with filter instead of Query since we need to check all items
|
|
187
|
+
// In production, consider using DynamoDB Streams for real-time updates
|
|
188
|
+
const command = new ScanCommand({
|
|
189
|
+
TableName: tableName,
|
|
190
|
+
FilterExpression: '#ts > :lastSync',
|
|
191
|
+
ExpressionAttributeNames: {
|
|
192
|
+
'#ts': 'timestamp'
|
|
193
|
+
},
|
|
194
|
+
ExpressionAttributeValues: {
|
|
195
|
+
':lastSync': lastSync
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const response = await this.dynamoClient.send(command);
|
|
200
|
+
return response.Items || [];
|
|
201
|
+
} catch (error) {
|
|
202
|
+
this.emit('error', { type: 'fetch-remote', error });
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Detect local changes
|
|
209
|
+
*/
|
|
210
|
+
async _detectLocalChanges() {
|
|
211
|
+
// TODO: Implement file watching and change detection
|
|
212
|
+
// For now, return empty array
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Detect conflicts between local and remote changes
|
|
218
|
+
*/
|
|
219
|
+
_detectConflicts(localChanges, remoteChanges) {
|
|
220
|
+
const conflicts = [];
|
|
221
|
+
|
|
222
|
+
for (const local of localChanges) {
|
|
223
|
+
for (const remote of remoteChanges) {
|
|
224
|
+
if (local.requirementId === remote.requirementId) {
|
|
225
|
+
// Same requirement modified both locally and remotely
|
|
226
|
+
if (local.timestamp !== remote.timestamp) {
|
|
227
|
+
conflicts.push({ local, remote });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return conflicts;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Resolve conflicts based on strategy
|
|
238
|
+
*/
|
|
239
|
+
async _resolveConflicts(conflicts) {
|
|
240
|
+
for (const conflict of conflicts) {
|
|
241
|
+
this.emit('conflict', conflict);
|
|
242
|
+
|
|
243
|
+
let resolution;
|
|
244
|
+
switch (this.options.conflictStrategy) {
|
|
245
|
+
case 'last-write-wins':
|
|
246
|
+
resolution = conflict.local.timestamp > conflict.remote.timestamp
|
|
247
|
+
? conflict.local
|
|
248
|
+
: conflict.remote;
|
|
249
|
+
break;
|
|
250
|
+
|
|
251
|
+
case 'manual':
|
|
252
|
+
// Emit event for manual resolution
|
|
253
|
+
resolution = await new Promise((resolve) => {
|
|
254
|
+
this.once('conflict-resolved', resolve);
|
|
255
|
+
});
|
|
256
|
+
break;
|
|
257
|
+
|
|
258
|
+
case 'auto-merge':
|
|
259
|
+
resolution = this._autoMerge(conflict.local, conflict.remote);
|
|
260
|
+
break;
|
|
261
|
+
|
|
262
|
+
default:
|
|
263
|
+
resolution = conflict.remote; // Default to remote
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Log conflict resolution
|
|
267
|
+
this.changeHistory.push({
|
|
268
|
+
type: 'conflict-resolution',
|
|
269
|
+
timestamp: Date.now(),
|
|
270
|
+
conflict,
|
|
271
|
+
resolution
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Auto-merge non-overlapping changes
|
|
278
|
+
*/
|
|
279
|
+
_autoMerge(local, remote) {
|
|
280
|
+
// Simple merge: combine changes from both
|
|
281
|
+
return {
|
|
282
|
+
...remote,
|
|
283
|
+
...local,
|
|
284
|
+
timestamp: Math.max(local.timestamp, remote.timestamp)
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Apply remote changes locally
|
|
290
|
+
*/
|
|
291
|
+
async _applyRemoteChanges(changes) {
|
|
292
|
+
for (const change of changes) {
|
|
293
|
+
try {
|
|
294
|
+
// TODO: Apply change to local requirements file
|
|
295
|
+
this.emit('remote-change-applied', change);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
this.emit('error', { type: 'apply-remote', change, error });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Push local changes to remote
|
|
304
|
+
*/
|
|
305
|
+
async _pushLocalChanges(changes) {
|
|
306
|
+
const { PutCommand } = require('@aws-sdk/lib-dynamodb');
|
|
307
|
+
|
|
308
|
+
const tableName = process.env.DYNAMODB_TABLE_NAME || 'vibecodingmachine-requirements';
|
|
309
|
+
|
|
310
|
+
for (const change of changes) {
|
|
311
|
+
try {
|
|
312
|
+
const command = new PutCommand({
|
|
313
|
+
TableName: tableName,
|
|
314
|
+
Item: {
|
|
315
|
+
computerId: this.computerId,
|
|
316
|
+
timestamp: Date.now(),
|
|
317
|
+
...change
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
await this.dynamoClient.send(command);
|
|
322
|
+
this.emit('local-change-pushed', change);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
this.emit('error', { type: 'push-local', change, error });
|
|
325
|
+
|
|
326
|
+
// Add to offline queue if push fails
|
|
327
|
+
if (!this.isOnline) {
|
|
328
|
+
this.offlineQueue.push(change);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Process offline queue
|
|
336
|
+
*/
|
|
337
|
+
async _processOfflineQueue() {
|
|
338
|
+
const queue = [...this.offlineQueue];
|
|
339
|
+
this.offlineQueue = [];
|
|
340
|
+
|
|
341
|
+
for (const change of queue) {
|
|
342
|
+
try {
|
|
343
|
+
await this._pushLocalChanges([change]);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
// Re-add to queue if still failing
|
|
346
|
+
this.offlineQueue.push(change);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Get sync status
|
|
353
|
+
*/
|
|
354
|
+
getStatus() {
|
|
355
|
+
return {
|
|
356
|
+
computerId: this.computerId,
|
|
357
|
+
isOnline: this.isOnline,
|
|
358
|
+
isSyncing: this.isSyncing,
|
|
359
|
+
lastSyncTime: this.lastSyncTime,
|
|
360
|
+
queuedChanges: this.offlineQueue.length,
|
|
361
|
+
conflictStrategy: this.options.conflictStrategy
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Get change history
|
|
367
|
+
*/
|
|
368
|
+
getHistory(limit = 100) {
|
|
369
|
+
return this.changeHistory.slice(-limit);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Set online/offline mode
|
|
374
|
+
*/
|
|
375
|
+
setOnlineMode(isOnline) {
|
|
376
|
+
this.isOnline = isOnline;
|
|
377
|
+
this.emit('online-status-changed', { isOnline });
|
|
378
|
+
|
|
379
|
+
if (isOnline && this.offlineQueue.length > 0) {
|
|
380
|
+
// Trigger sync to process offline queue
|
|
381
|
+
this.sync().catch(error => {
|
|
382
|
+
this.emit('error', { type: 'sync', error });
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
module.exports = SyncEngine;
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
const https = require('https');
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Compare two timestamp-based versions (YYYY.MM.DD-HHMM)
|
|
5
|
+
* Returns true if version1 is newer than version2
|
|
6
|
+
* Handles both new format (YYYY.MM.DD-HHMM) and old format (YYYY.MM.DD.HHMM)
|
|
7
|
+
*/
|
|
8
|
+
function isVersionNewer(version1, version2) {
|
|
9
|
+
// Versions are in format YYYY.MM.DD-HHMM, so string comparison works
|
|
10
|
+
// Normalize both versions (handle old format with dots too)
|
|
11
|
+
const normalize = (v) => v.replace(/\.(\d{2})(\d{2})$/, '-$1$2');
|
|
12
|
+
return normalize(version1) > normalize(version2);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Format version timestamp to YYYY.MM.DD-HHMM (TIMEZONE)
|
|
5
17
|
*/
|
|
6
18
|
function formatVersionTimestamp(versionString, date) {
|
|
7
|
-
// Version format: YYYY.MM.DD.HHMM
|
|
8
|
-
const match = versionString.match(/^(\d{4})\.(\d{2})\.(\d{2})
|
|
19
|
+
// Version format: YYYY.MM.DD-HHMM (or old format YYYY.MM.DD.HHMM for backward compat)
|
|
20
|
+
const match = versionString.match(/^(\d{4})\.(\d{2})\.(\d{2})[-.](\d{2})(\d{2})$/);
|
|
9
21
|
if (!match) return versionString;
|
|
10
22
|
|
|
11
23
|
try {
|
|
@@ -19,7 +31,9 @@ function formatVersionTimestamp(versionString, date) {
|
|
|
19
31
|
timeZoneName: 'short'
|
|
20
32
|
}).format(versionDate).split(' ').pop();
|
|
21
33
|
|
|
22
|
-
|
|
34
|
+
// Normalize to new format for display
|
|
35
|
+
const normalized = `${year}.${month}.${day}-${hour}${minute}`;
|
|
36
|
+
return `${normalized} (${timeZoneAbbr})`;
|
|
23
37
|
} catch (error) {
|
|
24
38
|
return `${versionString} (UTC)`;
|
|
25
39
|
}
|
|
@@ -27,12 +41,12 @@ function formatVersionTimestamp(versionString, date) {
|
|
|
27
41
|
|
|
28
42
|
/**
|
|
29
43
|
* Check for Electron app updates from S3 version manifest
|
|
30
|
-
* @param {string} currentVersion - Current version (e.g., '2025.11.26
|
|
44
|
+
* @param {string} currentVersion - Current version (e.g., '2025.11.26-0519')
|
|
31
45
|
* @returns {Promise<Object>} Update info or null if no update available
|
|
32
46
|
*/
|
|
33
47
|
async function checkForElectronUpdates(currentVersion) {
|
|
34
48
|
return new Promise((resolve, reject) => {
|
|
35
|
-
const manifestUrl = `https://
|
|
49
|
+
const manifestUrl = `https://d3fh7zgi8horze.cloudfront.net/downloads/version.json?t=${Date.now()}`;
|
|
36
50
|
console.log(`🔍 [UPDATE CHECK] Current version: ${currentVersion}`);
|
|
37
51
|
console.log(`📡 [UPDATE CHECK] Fetching: ${manifestUrl}`);
|
|
38
52
|
|
|
@@ -61,7 +75,7 @@ async function checkForElectronUpdates(currentVersion) {
|
|
|
61
75
|
// Detect architecture and choose appropriate download URL
|
|
62
76
|
const arch = process.arch; // 'arm64' or 'x64'
|
|
63
77
|
const platform = process.platform; // 'darwin' or 'win32'
|
|
64
|
-
const baseUrl = 'https://
|
|
78
|
+
const baseUrl = 'https://d3fh7zgi8horze.cloudfront.net/downloads';
|
|
65
79
|
|
|
66
80
|
let downloadUrl;
|
|
67
81
|
if (platform === 'darwin') {
|
|
@@ -79,7 +93,8 @@ async function checkForElectronUpdates(currentVersion) {
|
|
|
79
93
|
downloadUrl = baseUrl;
|
|
80
94
|
}
|
|
81
95
|
|
|
82
|
-
if
|
|
96
|
+
// Only show update if latest version is actually newer than current
|
|
97
|
+
if (latestVersion !== currentVersion && isVersionNewer(latestVersion, currentVersion)) {
|
|
83
98
|
console.log(`✅ [UPDATE CHECK] Update available: ${currentVersion} → ${latestVersion}`);
|
|
84
99
|
console.log(`📥 [UPDATE CHECK] Download URL: ${downloadUrl}`);
|
|
85
100
|
resolve({
|