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.
@@ -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
- * Format version timestamp to YYYY.MM.DD.HHMM (TIMEZONE)
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})\.(\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
- return `${versionString} (${timeZoneAbbr})`;
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.0519')
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://vibecodingmachine-website.s3.amazonaws.com/downloads/version.json?t=${Date.now()}`;
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://vibecodingmachine-website.s3.amazonaws.com/downloads';
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 (latestVersion !== currentVersion) {
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({