vibecodingmachine-core 2025.12.1-534 → 2025.12.22-2230

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,410 @@
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
+ // If AWS SDK is not installed in this environment (common in local dev),
69
+ // fall back to offline mode instead of throwing so the CLI remains usable.
70
+ if (error && error.code === 'MODULE_NOT_FOUND' && /@aws-sdk\//.test(error.message)) {
71
+ this.options.offlineMode = true;
72
+ this.isOnline = false;
73
+ this.emit('warning', { type: 'offline-fallback', message: 'AWS SDK not available, running in offline mode', error });
74
+ this.emit('initialized');
75
+ return false;
76
+ }
77
+
78
+ this.emit('error', { type: 'initialization', error });
79
+ throw error;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Initialize WebSocket connection for real-time updates
85
+ */
86
+ async _initializeWebSocket() {
87
+ // TODO: Implement WebSocket connection to AWS IoT Core or API Gateway WebSocket
88
+ // For now, we'll use polling
89
+ this.emit('websocket-status', { connected: false, reason: 'not-implemented' });
90
+ }
91
+
92
+ /**
93
+ * Start sync engine
94
+ */
95
+ async start() {
96
+ if (!this.dynamoClient) {
97
+ await this.initialize();
98
+ }
99
+
100
+ // Start periodic sync
101
+ this.syncInterval = setInterval(() => {
102
+ this.sync().catch(error => {
103
+ this.emit('error', { type: 'sync', error });
104
+ });
105
+ }, this.options.syncInterval);
106
+
107
+ // Do initial sync
108
+ await this.sync();
109
+
110
+ this.emit('started');
111
+ }
112
+
113
+ /**
114
+ * Stop sync engine
115
+ */
116
+ stop() {
117
+ if (this.syncInterval) {
118
+ clearInterval(this.syncInterval);
119
+ this.syncInterval = null;
120
+ }
121
+
122
+ this.emit('stopped');
123
+ }
124
+
125
+ /**
126
+ * Perform sync operation
127
+ */
128
+ async sync() {
129
+ if (this.isSyncing) {
130
+ return; // Already syncing
131
+ }
132
+
133
+ this.isSyncing = true;
134
+ this.emit('sync-start');
135
+
136
+ try {
137
+ // Check if online
138
+ if (!this.isOnline) {
139
+ this.emit('sync-complete', { status: 'offline', queued: this.offlineQueue.length });
140
+ return;
141
+ }
142
+
143
+ // Process offline queue first
144
+ if (this.offlineQueue.length > 0) {
145
+ await this._processOfflineQueue();
146
+ }
147
+
148
+ // Fetch remote changes
149
+ const remoteChanges = await this._fetchRemoteChanges();
150
+
151
+ // Detect local changes
152
+ const localChanges = await this._detectLocalChanges();
153
+
154
+ // Resolve conflicts
155
+ const conflicts = this._detectConflicts(localChanges, remoteChanges);
156
+ if (conflicts.length > 0) {
157
+ await this._resolveConflicts(conflicts);
158
+ }
159
+
160
+ // Apply remote changes locally
161
+ if (remoteChanges.length > 0) {
162
+ await this._applyRemoteChanges(remoteChanges);
163
+ }
164
+
165
+ // Push local changes to remote
166
+ if (localChanges.length > 0) {
167
+ await this._pushLocalChanges(localChanges);
168
+ }
169
+
170
+ this.lastSyncTime = Date.now();
171
+ this.emit('sync-complete', {
172
+ status: 'success',
173
+ remoteChanges: remoteChanges.length,
174
+ localChanges: localChanges.length,
175
+ conflicts: conflicts.length
176
+ });
177
+ } catch (error) {
178
+ this.emit('sync-complete', { status: 'error', error: error.message });
179
+ // Don't throw - just emit error event
180
+ this.emit('error', { type: 'sync', error: error.message });
181
+ } finally {
182
+ this.isSyncing = false;
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Fetch remote changes from DynamoDB
188
+ */
189
+ async _fetchRemoteChanges() {
190
+ if (!this.dynamoClient) {
191
+ this.emit('warning', { type: 'no-dynamo', message: 'DynamoDB client not initialized, skipping remote fetch' });
192
+ return [];
193
+ }
194
+
195
+ const { ScanCommand } = require('@aws-sdk/lib-dynamodb');
196
+
197
+ const tableName = process.env.DYNAMODB_TABLE_NAME || 'vibecodingmachine-requirements';
198
+ const lastSync = this.lastSyncTime || 0;
199
+
200
+ try {
201
+ // Use Scan with filter instead of Query since we need to check all items
202
+ // In production, consider using DynamoDB Streams for real-time updates
203
+ const command = new ScanCommand({
204
+ TableName: tableName,
205
+ FilterExpression: '#ts > :lastSync',
206
+ ExpressionAttributeNames: {
207
+ '#ts': 'timestamp'
208
+ },
209
+ ExpressionAttributeValues: {
210
+ ':lastSync': lastSync
211
+ }
212
+ });
213
+
214
+ const response = await this.dynamoClient.send(command);
215
+ return response.Items || [];
216
+ } catch (error) {
217
+ this.emit('error', { type: 'fetch-remote', error });
218
+ return [];
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Detect local changes
224
+ */
225
+ async _detectLocalChanges() {
226
+ // TODO: Implement file watching and change detection
227
+ // For now, return empty array
228
+ return [];
229
+ }
230
+
231
+ /**
232
+ * Detect conflicts between local and remote changes
233
+ */
234
+ _detectConflicts(localChanges, remoteChanges) {
235
+ const conflicts = [];
236
+
237
+ for (const local of localChanges) {
238
+ for (const remote of remoteChanges) {
239
+ if (local.requirementId === remote.requirementId) {
240
+ // Same requirement modified both locally and remotely
241
+ if (local.timestamp !== remote.timestamp) {
242
+ conflicts.push({ local, remote });
243
+ }
244
+ }
245
+ }
246
+ }
247
+
248
+ return conflicts;
249
+ }
250
+
251
+ /**
252
+ * Resolve conflicts based on strategy
253
+ */
254
+ async _resolveConflicts(conflicts) {
255
+ for (const conflict of conflicts) {
256
+ this.emit('conflict', conflict);
257
+
258
+ let resolution;
259
+ switch (this.options.conflictStrategy) {
260
+ case 'last-write-wins':
261
+ resolution = conflict.local.timestamp > conflict.remote.timestamp
262
+ ? conflict.local
263
+ : conflict.remote;
264
+ break;
265
+
266
+ case 'manual':
267
+ // Emit event for manual resolution
268
+ resolution = await new Promise((resolve) => {
269
+ this.once('conflict-resolved', resolve);
270
+ });
271
+ break;
272
+
273
+ case 'auto-merge':
274
+ resolution = this._autoMerge(conflict.local, conflict.remote);
275
+ break;
276
+
277
+ default:
278
+ resolution = conflict.remote; // Default to remote
279
+ }
280
+
281
+ // Log conflict resolution
282
+ this.changeHistory.push({
283
+ type: 'conflict-resolution',
284
+ timestamp: Date.now(),
285
+ conflict,
286
+ resolution
287
+ });
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Auto-merge non-overlapping changes
293
+ */
294
+ _autoMerge(local, remote) {
295
+ // Simple merge: combine changes from both
296
+ return {
297
+ ...remote,
298
+ ...local,
299
+ timestamp: Math.max(local.timestamp, remote.timestamp)
300
+ };
301
+ }
302
+
303
+ /**
304
+ * Apply remote changes locally
305
+ */
306
+ async _applyRemoteChanges(changes) {
307
+ for (const change of changes) {
308
+ try {
309
+ // TODO: Apply change to local requirements file
310
+ this.emit('remote-change-applied', change);
311
+ } catch (error) {
312
+ this.emit('error', { type: 'apply-remote', change, error });
313
+ }
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Push local changes to remote
319
+ */
320
+ async _pushLocalChanges(changes) {
321
+ if (!this.dynamoClient) {
322
+ this.emit('warning', { type: 'no-dynamo', message: 'DynamoDB client not initialized, queuing local changes' });
323
+ // Queue all changes for later push
324
+ this.offlineQueue.push(...changes);
325
+ return;
326
+ }
327
+
328
+ const { PutCommand } = require('@aws-sdk/lib-dynamodb');
329
+
330
+ const tableName = process.env.DYNAMODB_TABLE_NAME || 'vibecodingmachine-requirements';
331
+
332
+ for (const change of changes) {
333
+ try {
334
+ const command = new PutCommand({
335
+ TableName: tableName,
336
+ Item: {
337
+ computerId: this.computerId,
338
+ timestamp: Date.now(),
339
+ ...change
340
+ }
341
+ });
342
+
343
+ await this.dynamoClient.send(command);
344
+ this.emit('local-change-pushed', change);
345
+ } catch (error) {
346
+ this.emit('error', { type: 'push-local', change, error });
347
+
348
+ // Add to offline queue if push fails
349
+ if (!this.isOnline) {
350
+ this.offlineQueue.push(change);
351
+ }
352
+ }
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Process offline queue
358
+ */
359
+ async _processOfflineQueue() {
360
+ const queue = [...this.offlineQueue];
361
+ this.offlineQueue = [];
362
+
363
+ for (const change of queue) {
364
+ try {
365
+ await this._pushLocalChanges([change]);
366
+ } catch (error) {
367
+ // Re-add to queue if still failing
368
+ this.offlineQueue.push(change);
369
+ }
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Get sync status
375
+ */
376
+ getStatus() {
377
+ return {
378
+ computerId: this.computerId,
379
+ isOnline: this.isOnline,
380
+ isSyncing: this.isSyncing,
381
+ lastSyncTime: this.lastSyncTime,
382
+ queuedChanges: this.offlineQueue.length,
383
+ conflictStrategy: this.options.conflictStrategy
384
+ };
385
+ }
386
+
387
+ /**
388
+ * Get change history
389
+ */
390
+ getHistory(limit = 100) {
391
+ return this.changeHistory.slice(-limit);
392
+ }
393
+
394
+ /**
395
+ * Set online/offline mode
396
+ */
397
+ setOnlineMode(isOnline) {
398
+ this.isOnline = isOnline;
399
+ this.emit('online-status-changed', { isOnline });
400
+
401
+ if (isOnline && this.offlineQueue.length > 0) {
402
+ // Trigger sync to process offline queue
403
+ this.sync().catch(error => {
404
+ this.emit('error', { type: 'sync', error });
405
+ });
406
+ }
407
+ }
408
+ }
409
+
410
+ module.exports = SyncEngine;
@@ -0,0 +1,92 @@
1
+ const fs = require('fs');
2
+ const { pipeline } = require('stream');
3
+ const { promisify } = require('util');
4
+ const streamPipeline = promisify(pipeline);
5
+ const ora = require('ora');
6
+
7
+ function progressBar(percent, width) {
8
+ const fill = Math.round((percent / 100) * width);
9
+ return '█'.repeat(fill) + '-'.repeat(Math.max(0, width - fill));
10
+ }
11
+
12
+ function formatEta(sec) {
13
+ if (!isFinite(sec) || sec === null) return '--:--';
14
+ const s = Math.max(0, Math.round(sec));
15
+ const m = Math.floor(s / 60);
16
+ const ss = s % 60;
17
+ return `${m}:${ss.toString().padStart(2, '0')}`;
18
+ }
19
+
20
+ async function downloadWithProgress(url, dest, opts = {}) {
21
+ const fetch = require('node-fetch');
22
+ const spinner = opts.spinner || ora();
23
+ const label = opts.label || 'Downloading...';
24
+ const onProgress = typeof opts.onProgress === 'function' ? opts.onProgress : null;
25
+
26
+ spinner.start(label);
27
+
28
+ const res = await fetch(url);
29
+ if (!res.ok) {
30
+ spinner.fail(`Download failed: ${res.status} ${res.statusText}`);
31
+ throw new Error(`Failed to download ${url}: ${res.status}`);
32
+ }
33
+ // Stop the spinner so progress prints are not overwritten
34
+ try { spinner.stop(); } catch (e) {}
35
+ try { process.stdout.write('\r\x1b[2KDownloading: 0.0 MB'); } catch (e) {}
36
+
37
+ const total = Number(res.headers.get('content-length')) || 0;
38
+ const fileStream = fs.createWriteStream(dest);
39
+
40
+ return await new Promise((resolve, reject) => {
41
+ let downloaded = 0;
42
+ const start = Date.now();
43
+ let lastPercent = -1;
44
+
45
+ res.body.on('data', (chunk) => {
46
+ downloaded += chunk.length;
47
+ if (total) {
48
+ const percent = Math.round((downloaded / total) * 100);
49
+ if (percent !== lastPercent) {
50
+ lastPercent = percent;
51
+ const mbDownloaded = (downloaded / (1024 * 1024)).toFixed(1);
52
+ const mbTotal = (total / (1024 * 1024)).toFixed(1);
53
+ const elapsed = Math.max(0.001, (Date.now() - start) / 1000);
54
+ const speed = downloaded / elapsed; // bytes/sec
55
+ const etaSec = (total - downloaded) / (speed || 1);
56
+ const eta = formatEta(etaSec);
57
+ const bar = progressBar(percent, 30);
58
+ // CLI/UI progress
59
+ if (onProgress) {
60
+ onProgress({ percent, downloaded, total, mbDownloaded, mbTotal, eta, bar });
61
+ } else {
62
+ process.stdout.write(`\r\x1b[2K[${bar}] ${percent}% ${mbDownloaded}MB / ${mbTotal}MB ETA: ${eta}`);
63
+ }
64
+ }
65
+ } else {
66
+ const mbDownloaded = (downloaded / (1024 * 1024)).toFixed(1);
67
+ if (onProgress) onProgress({ percent: null, downloaded, total, mbDownloaded });
68
+ else process.stdout.write(`\r\x1b[2K${label} ${mbDownloaded} MB`);
69
+ }
70
+ });
71
+
72
+ res.body.on('error', (err) => {
73
+ spinner.fail('Download error');
74
+ reject(err);
75
+ });
76
+
77
+ fileStream.on('error', (err) => {
78
+ spinner.fail('File write error');
79
+ reject(err);
80
+ });
81
+
82
+ fileStream.on('finish', () => {
83
+ process.stdout.write('\n');
84
+ spinner.succeed('Download complete');
85
+ resolve();
86
+ });
87
+
88
+ res.body.pipe(fileStream);
89
+ });
90
+ }
91
+
92
+ module.exports = { downloadWithProgress };
@@ -39,12 +39,19 @@ function formatVersionTimestamp(versionString, date) {
39
39
  }
40
40
  }
41
41
 
42
+ const { shouldCheckUpdates } = require('./env-helpers');
43
+
42
44
  /**
43
45
  * Check for Electron app updates from S3 version manifest
44
46
  * @param {string} currentVersion - Current version (e.g., '2025.11.26-0519')
45
47
  * @returns {Promise<Object>} Update info or null if no update available
46
48
  */
47
49
  async function checkForElectronUpdates(currentVersion) {
50
+ if (!shouldCheckUpdates()) {
51
+ console.log('ℹ️ [UPDATE CHECK] Skipped in development environment');
52
+ return { hasUpdate: false, currentVersion };
53
+ }
54
+
48
55
  return new Promise((resolve, reject) => {
49
56
  const manifestUrl = `https://d3fh7zgi8horze.cloudfront.net/downloads/version.json?t=${Date.now()}`;
50
57
  console.log(`🔍 [UPDATE CHECK] Current version: ${currentVersion}`);
@@ -0,0 +1,54 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ /**
5
+ * Check if running in development environment
6
+ * @returns {boolean} True if in development
7
+ */
8
+ function isDevelopment() {
9
+ // Check standard environment variables
10
+ if (process.env.NODE_ENV === 'development' ||
11
+ process.env.VIBECODINGMACHINE_ENV === 'development' ||
12
+ process.env.ELECTRON_IS_DEV === '1') {
13
+ return true;
14
+ }
15
+
16
+ // Check for --dev flag in arguments
17
+ if (process.argv.includes('--dev')) {
18
+ return true;
19
+ }
20
+
21
+ // Heuristics for local development
22
+ // 1. Check if we are in a git repository that looks like the source code
23
+ // Root of the monorepo usually has 'packages' directory and 'lerna.json' or 'pnpm-workspace.yaml'
24
+ // But this might be too aggressive if user installs via git clone.
25
+
26
+ // 2. Check if electron is running from default app (not packaged)
27
+ if (process.versions && process.versions.electron) {
28
+ const electron = require('electron');
29
+ if (electron.app && !electron.app.isPackaged) {
30
+ return true;
31
+ }
32
+ }
33
+
34
+ return false;
35
+ }
36
+
37
+ /**
38
+ * Check if we should perform update checks
39
+ * @returns {boolean} True if update checks are allowed
40
+ */
41
+ function shouldCheckUpdates() {
42
+ // Don't check updates in development unless explicitly forced (not implemented yet)
43
+ if (isDevelopment()) {
44
+ return false;
45
+ }
46
+
47
+ // Can add more logic here (e.g. disable updates via config)
48
+ return true;
49
+ }
50
+
51
+ module.exports = {
52
+ isDevelopment,
53
+ shouldCheckUpdates
54
+ };