episoda 0.2.5 → 0.2.7

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 (73) hide show
  1. package/dist/index.d.ts +0 -7
  2. package/dist/index.js +3736 -82
  3. package/dist/index.js.map +1 -1
  4. package/package.json +5 -4
  5. package/dist/commands/auth.d.ts +0 -22
  6. package/dist/commands/auth.d.ts.map +0 -1
  7. package/dist/commands/auth.js +0 -384
  8. package/dist/commands/auth.js.map +0 -1
  9. package/dist/commands/dev.d.ts +0 -20
  10. package/dist/commands/dev.d.ts.map +0 -1
  11. package/dist/commands/dev.js +0 -305
  12. package/dist/commands/dev.js.map +0 -1
  13. package/dist/commands/status.d.ts +0 -13
  14. package/dist/commands/status.d.ts.map +0 -1
  15. package/dist/commands/status.js +0 -102
  16. package/dist/commands/status.js.map +0 -1
  17. package/dist/commands/stop.d.ts +0 -17
  18. package/dist/commands/stop.d.ts.map +0 -1
  19. package/dist/commands/stop.js +0 -81
  20. package/dist/commands/stop.js.map +0 -1
  21. package/dist/daemon/daemon-manager.d.ts +0 -71
  22. package/dist/daemon/daemon-manager.d.ts.map +0 -1
  23. package/dist/daemon/daemon-manager.js +0 -289
  24. package/dist/daemon/daemon-manager.js.map +0 -1
  25. package/dist/daemon/daemon-process.d.ts +0 -13
  26. package/dist/daemon/daemon-process.d.ts.map +0 -1
  27. package/dist/daemon/daemon-process.js +0 -662
  28. package/dist/daemon/daemon-process.js.map +0 -1
  29. package/dist/daemon/identity-server.d.ts +0 -51
  30. package/dist/daemon/identity-server.d.ts.map +0 -1
  31. package/dist/daemon/identity-server.js +0 -162
  32. package/dist/daemon/identity-server.js.map +0 -1
  33. package/dist/daemon/machine-id.d.ts +0 -36
  34. package/dist/daemon/machine-id.d.ts.map +0 -1
  35. package/dist/daemon/machine-id.js +0 -195
  36. package/dist/daemon/machine-id.js.map +0 -1
  37. package/dist/daemon/project-tracker.d.ts +0 -92
  38. package/dist/daemon/project-tracker.d.ts.map +0 -1
  39. package/dist/daemon/project-tracker.js +0 -259
  40. package/dist/daemon/project-tracker.js.map +0 -1
  41. package/dist/dev-wrapper.d.ts +0 -88
  42. package/dist/dev-wrapper.d.ts.map +0 -1
  43. package/dist/dev-wrapper.js +0 -288
  44. package/dist/dev-wrapper.js.map +0 -1
  45. package/dist/framework-detector.d.ts +0 -29
  46. package/dist/framework-detector.d.ts.map +0 -1
  47. package/dist/framework-detector.js +0 -276
  48. package/dist/framework-detector.js.map +0 -1
  49. package/dist/git-helpers/git-credential-helper.d.ts +0 -29
  50. package/dist/git-helpers/git-credential-helper.d.ts.map +0 -1
  51. package/dist/git-helpers/git-credential-helper.js +0 -349
  52. package/dist/git-helpers/git-credential-helper.js.map +0 -1
  53. package/dist/index.d.ts.map +0 -1
  54. package/dist/ipc/ipc-client.d.ts +0 -116
  55. package/dist/ipc/ipc-client.d.ts.map +0 -1
  56. package/dist/ipc/ipc-client.js +0 -216
  57. package/dist/ipc/ipc-client.js.map +0 -1
  58. package/dist/ipc/ipc-server.d.ts +0 -55
  59. package/dist/ipc/ipc-server.d.ts.map +0 -1
  60. package/dist/ipc/ipc-server.js +0 -177
  61. package/dist/ipc/ipc-server.js.map +0 -1
  62. package/dist/output.d.ts +0 -48
  63. package/dist/output.d.ts.map +0 -1
  64. package/dist/output.js +0 -129
  65. package/dist/output.js.map +0 -1
  66. package/dist/utils/port-check.d.ts +0 -15
  67. package/dist/utils/port-check.d.ts.map +0 -1
  68. package/dist/utils/port-check.js +0 -79
  69. package/dist/utils/port-check.js.map +0 -1
  70. package/dist/utils/update-checker.d.ts +0 -23
  71. package/dist/utils/update-checker.d.ts.map +0 -1
  72. package/dist/utils/update-checker.js +0 -95
  73. package/dist/utils/update-checker.js.map +0 -1
@@ -1,662 +0,0 @@
1
- "use strict";
2
- /**
3
- * Episoda Daemon Process
4
- *
5
- * Main entry point for the persistent daemon that:
6
- * - Maintains WebSocket connections to multiple projects
7
- * - Listens for IPC commands from CLI
8
- * - Handles graceful shutdown
9
- * - Survives terminal close
10
- *
11
- * This file is spawned by daemon-manager.ts in detached mode.
12
- */
13
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
- if (k2 === undefined) k2 = k;
15
- var desc = Object.getOwnPropertyDescriptor(m, k);
16
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
17
- desc = { enumerable: true, get: function() { return m[k]; } };
18
- }
19
- Object.defineProperty(o, k2, desc);
20
- }) : (function(o, m, k, k2) {
21
- if (k2 === undefined) k2 = k;
22
- o[k2] = m[k];
23
- }));
24
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
25
- Object.defineProperty(o, "default", { enumerable: true, value: v });
26
- }) : function(o, v) {
27
- o["default"] = v;
28
- });
29
- var __importStar = (this && this.__importStar) || (function () {
30
- var ownKeys = function(o) {
31
- ownKeys = Object.getOwnPropertyNames || function (o) {
32
- var ar = [];
33
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
34
- return ar;
35
- };
36
- return ownKeys(o);
37
- };
38
- return function (mod) {
39
- if (mod && mod.__esModule) return mod;
40
- var result = {};
41
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
42
- __setModuleDefault(result, mod);
43
- return result;
44
- };
45
- })();
46
- Object.defineProperty(exports, "__esModule", { value: true });
47
- const machine_id_1 = require("./machine-id");
48
- const project_tracker_1 = require("./project-tracker");
49
- const daemon_manager_1 = require("./daemon-manager");
50
- const ipc_server_1 = require("../ipc/ipc-server");
51
- const core_1 = require("@episoda/core");
52
- const update_checker_1 = require("../utils/update-checker");
53
- const identity_server_1 = require("./identity-server");
54
- const fs = __importStar(require("fs"));
55
- const os = __importStar(require("os"));
56
- const path = __importStar(require("path"));
57
- // EP783: Get current version for update checking
58
- const packageJson = require('../../package.json');
59
- /**
60
- * Daemon state
61
- */
62
- class Daemon {
63
- constructor() {
64
- this.machineId = '';
65
- this.deviceId = null; // EP726: Cached device UUID from server
66
- this.deviceName = null; // EP661: Cached device name from server
67
- this.flyMachineId = null; // EP735: Fly.io machine ID for sticky session routing
68
- this.identityServer = null; // EP803: Local identity server for browser detection
69
- this.connections = new Map(); // projectPath -> connection
70
- // EP701: Track which connections are currently live (WebSocket open)
71
- // Updated by 'auth_success' (add) and 'disconnected' (remove) events
72
- this.liveConnections = new Set(); // projectPath
73
- this.shuttingDown = false;
74
- this.ipcServer = new ipc_server_1.IPCServer();
75
- }
76
- /**
77
- * Start the daemon
78
- */
79
- async start() {
80
- console.log('[Daemon] Starting Episoda daemon...');
81
- // Get machine ID
82
- this.machineId = await (0, machine_id_1.getMachineId)();
83
- console.log(`[Daemon] Machine ID: ${this.machineId}`);
84
- // EP726: Load cached device ID from config if available
85
- const config = await (0, core_1.loadConfig)();
86
- if (config?.device_id) {
87
- this.deviceId = config.device_id;
88
- console.log(`[Daemon] Loaded cached Device ID (UUID): ${this.deviceId}`);
89
- }
90
- // Start IPC server
91
- await this.ipcServer.start();
92
- console.log('[Daemon] IPC server started');
93
- // EP803: Start identity server for browser machine detection
94
- this.identityServer = new identity_server_1.IdentityServer(this.machineId);
95
- await this.identityServer.start();
96
- // Register IPC command handlers
97
- this.registerIPCHandlers();
98
- // Restore connections for tracked projects
99
- await this.restoreConnections();
100
- // Setup graceful shutdown
101
- this.setupShutdownHandlers();
102
- console.log('[Daemon] Daemon started successfully');
103
- // EP783: Check for updates in background (non-blocking)
104
- this.checkAndNotifyUpdates();
105
- }
106
- /**
107
- * EP783: Check for CLI updates and auto-update in background
108
- * Non-blocking - runs after daemon starts, fails silently on errors
109
- */
110
- async checkAndNotifyUpdates() {
111
- try {
112
- const result = await (0, update_checker_1.checkForUpdates)(packageJson.version);
113
- if (result.updateAvailable) {
114
- console.log(`\n⬆️ Update available: ${result.currentVersion} → ${result.latestVersion}`);
115
- console.log(' Updating in background...\n');
116
- (0, update_checker_1.performBackgroundUpdate)();
117
- }
118
- }
119
- catch (error) {
120
- // Silently ignore - update check is non-critical
121
- }
122
- }
123
- // EP738: Removed startHttpServer - device info now flows through WebSocket broadcast + database
124
- /**
125
- * Register IPC command handlers
126
- */
127
- registerIPCHandlers() {
128
- // Ping - health check
129
- this.ipcServer.on('ping', async () => {
130
- return { status: 'ok' };
131
- });
132
- // Status - get daemon status
133
- // EP726: Now includes deviceId (UUID) for unified device identification
134
- // EP738: Added hostname, platform, arch for status command (HTTP server removed)
135
- // EP776: Use liveConnections (actual WebSocket state) instead of connections Map
136
- this.ipcServer.on('status', async () => {
137
- const projects = (0, project_tracker_1.getAllProjects)().map(p => ({
138
- id: p.id,
139
- path: p.path,
140
- name: p.name,
141
- connected: this.liveConnections.has(p.path),
142
- }));
143
- return {
144
- running: true,
145
- machineId: this.machineId,
146
- deviceId: this.deviceId, // EP726: UUID for unified device identification
147
- hostname: os.hostname(),
148
- platform: os.platform(),
149
- arch: os.arch(),
150
- projects,
151
- };
152
- });
153
- // EP734: Add project - now blocking, waits for connection to complete
154
- // This eliminates the need for polling from the client side
155
- this.ipcServer.on('add-project', async (params) => {
156
- const { projectId, projectPath } = params;
157
- (0, project_tracker_1.addProject)(projectId, projectPath);
158
- try {
159
- // Await connection - blocks until connected or fails
160
- await this.connectProject(projectId, projectPath);
161
- // EP805: Verify connection is actually healthy before returning success
162
- const isHealthy = this.isConnectionHealthy(projectPath);
163
- if (!isHealthy) {
164
- console.warn(`[Daemon] Connection completed but not healthy for ${projectPath}`);
165
- return { success: false, connected: false, error: 'Connection established but not healthy' };
166
- }
167
- return { success: true, connected: true };
168
- }
169
- catch (error) {
170
- const errorMessage = error instanceof Error ? error.message : String(error);
171
- return { success: false, connected: false, error: errorMessage };
172
- }
173
- });
174
- // EP734: Removed connection-status handler - no longer needed with blocking add-project
175
- // Remove project
176
- this.ipcServer.on('remove-project', async (params) => {
177
- const { projectPath } = params;
178
- await this.disconnectProject(projectPath);
179
- (0, project_tracker_1.removeProject)(projectPath);
180
- return { success: true };
181
- });
182
- // Connect project
183
- this.ipcServer.on('connect-project', async (params) => {
184
- const { projectPath } = params;
185
- const project = (0, project_tracker_1.getAllProjects)().find(p => p.path === projectPath);
186
- if (!project) {
187
- throw new Error('Project not tracked');
188
- }
189
- await this.connectProject(project.id, projectPath);
190
- return { success: true };
191
- });
192
- // Disconnect project
193
- this.ipcServer.on('disconnect-project', async (params) => {
194
- const { projectPath } = params;
195
- await this.disconnectProject(projectPath);
196
- return { success: true };
197
- });
198
- // Shutdown
199
- this.ipcServer.on('shutdown', async () => {
200
- console.log('[Daemon] Shutdown requested via IPC');
201
- await this.shutdown();
202
- return { success: true };
203
- });
204
- // EP805: Verify connection health - checks both Map and liveConnections Set
205
- this.ipcServer.on('verify-health', async () => {
206
- const projects = (0, project_tracker_1.getAllProjects)().map(p => ({
207
- id: p.id,
208
- path: p.path,
209
- name: p.name,
210
- inConnectionsMap: this.connections.has(p.path),
211
- inLiveConnections: this.liveConnections.has(p.path),
212
- isHealthy: this.isConnectionHealthy(p.path),
213
- }));
214
- const healthyCount = projects.filter(p => p.isHealthy).length;
215
- const staleCount = projects.filter(p => p.inConnectionsMap && !p.inLiveConnections).length;
216
- return {
217
- totalProjects: projects.length,
218
- healthyConnections: healthyCount,
219
- staleConnections: staleCount,
220
- projects,
221
- };
222
- });
223
- }
224
- /**
225
- * Restore WebSocket connections for tracked projects
226
- */
227
- async restoreConnections() {
228
- const projects = (0, project_tracker_1.getAllProjects)();
229
- for (const project of projects) {
230
- try {
231
- await this.connectProject(project.id, project.path);
232
- }
233
- catch (error) {
234
- console.error(`[Daemon] Failed to restore connection for ${project.name}:`, error);
235
- }
236
- }
237
- }
238
- /**
239
- * EP805: Check if a connection is healthy (exists AND is live)
240
- * A connection can exist in the Map but be dead if WebSocket disconnected
241
- */
242
- isConnectionHealthy(projectPath) {
243
- return this.connections.has(projectPath) && this.liveConnections.has(projectPath);
244
- }
245
- /**
246
- * Connect to a project's WebSocket
247
- */
248
- async connectProject(projectId, projectPath) {
249
- // EP805: Check BOTH connections Map AND liveConnections Set
250
- // A stale connection (in Map but not in Set) means WebSocket died
251
- if (this.connections.has(projectPath)) {
252
- if (this.liveConnections.has(projectPath)) {
253
- console.log(`[Daemon] Already connected to ${projectPath}`);
254
- return;
255
- }
256
- // EP805: Stale connection detected - clean up and reconnect
257
- console.warn(`[Daemon] Stale connection detected for ${projectPath}, forcing reconnection`);
258
- await this.disconnectProject(projectPath);
259
- }
260
- // Load auth token from config
261
- const config = await (0, core_1.loadConfig)();
262
- if (!config || !config.access_token) {
263
- throw new Error('No access token found. Please run: episoda auth');
264
- }
265
- // EP734: Removed pre-flight health check - WebSocket will fail naturally if server is down
266
- // This saves ~200-500ms on every connection attempt
267
- // EP734: Determine server URL using cached settings (avoids network call)
268
- // Priority: 1. Cached local_server_url, 2. Global config api_url, 3. Default production
269
- let serverUrl = config.api_url || process.env.EPISODA_API_URL || 'https://episoda.dev';
270
- // Use cached local_server_url if available
271
- if (config.project_settings?.local_server_url) {
272
- serverUrl = config.project_settings.local_server_url;
273
- console.log(`[Daemon] Using cached server URL: ${serverUrl}`);
274
- }
275
- // EP593: Connect to WebSocket server on port 3001 (standalone WebSocket server)
276
- const serverUrlObj = new URL(serverUrl);
277
- const wsProtocol = serverUrlObj.protocol === 'https:' ? 'wss:' : 'ws:';
278
- const wsPort = process.env.EPISODA_WS_PORT || '3001';
279
- const wsUrl = `${wsProtocol}//${serverUrlObj.hostname}:${wsPort}`;
280
- console.log(`[Daemon] Connecting to ${wsUrl} for project ${projectId}...`);
281
- // Create EpisodaClient (handles auth, reconnection, heartbeat)
282
- const client = new core_1.EpisodaClient();
283
- // Create GitExecutor for this project
284
- const gitExecutor = new core_1.GitExecutor();
285
- // Store connection
286
- const connection = {
287
- projectId,
288
- projectPath,
289
- client,
290
- gitExecutor,
291
- };
292
- this.connections.set(projectPath, connection);
293
- // Register command handler
294
- client.on('command', async (message) => {
295
- if (message.type === 'command' && message.command) {
296
- console.log(`[Daemon] Received command for ${projectId}:`, message.command);
297
- // EP605: Update activity timestamp to reset idle detection
298
- client.updateActivity();
299
- try {
300
- // Execute git command with working directory
301
- const result = await gitExecutor.execute(message.command, {
302
- cwd: projectPath
303
- });
304
- // Send result back
305
- await client.send({
306
- type: 'result',
307
- commandId: message.id,
308
- result
309
- });
310
- console.log(`[Daemon] Command completed for ${projectId}:`, result.success ? 'success' : 'failed');
311
- }
312
- catch (error) {
313
- // Send error result
314
- await client.send({
315
- type: 'result',
316
- commandId: message.id,
317
- result: {
318
- success: false,
319
- error: 'UNKNOWN_ERROR',
320
- output: error instanceof Error ? error.message : String(error)
321
- }
322
- });
323
- console.error(`[Daemon] Command execution error for ${projectId}:`, error);
324
- }
325
- }
326
- });
327
- // Register shutdown handler - allows server to request graceful shutdown
328
- // EP613: Only exit on user-requested shutdowns, reconnect for server restarts
329
- client.on('shutdown', async (message) => {
330
- const shutdownMessage = message;
331
- const reason = shutdownMessage.reason || 'unknown';
332
- console.log(`[Daemon] Received shutdown request from server for ${projectId}`);
333
- console.log(`[Daemon] Reason: ${reason} - ${shutdownMessage.message || 'No message'}`);
334
- if (reason === 'user_requested') {
335
- // User explicitly requested disconnect - exit cleanly
336
- console.log(`[Daemon] User requested disconnect, shutting down...`);
337
- await this.cleanupAndExit();
338
- }
339
- else {
340
- // Server restart or deployment - don't exit, let WebSocket reconnection handle it
341
- console.log(`[Daemon] Server shutdown (${reason}), will reconnect automatically...`);
342
- // The WebSocket close event will trigger reconnection via EpisodaClient
343
- // We don't call process.exit() here - just let the connection close naturally
344
- }
345
- });
346
- // Register auth success handler
347
- client.on('auth_success', async (message) => {
348
- console.log(`[Daemon] Authenticated for project ${projectId}`);
349
- (0, project_tracker_1.touchProject)(projectPath);
350
- // EP701: Mark connection as live (for /device-info endpoint)
351
- this.liveConnections.add(projectPath);
352
- // EP803: Update identity server connection status
353
- if (this.identityServer) {
354
- this.identityServer.setConnected(true);
355
- }
356
- // EP595: Configure git with userId and workspaceId for post-checkout hook
357
- // EP655: Also configure machineId for device isolation
358
- // EP661: Cache deviceName for browser identification
359
- // EP726: Cache deviceId (UUID) for unified device identification
360
- // EP735: Cache flyMachineId for sticky session routing
361
- const authMessage = message;
362
- if (authMessage.userId && authMessage.workspaceId) {
363
- // EP726: Pass deviceId to configureGitUser so it's stored in git config
364
- await this.configureGitUser(projectPath, authMessage.userId, authMessage.workspaceId, this.machineId, projectId, authMessage.deviceId);
365
- // EP610: Install git hooks after configuring git user
366
- await this.installGitHooks(projectPath);
367
- }
368
- // EP661: Store device name for logging and config
369
- if (authMessage.deviceName) {
370
- this.deviceName = authMessage.deviceName;
371
- console.log(`[Daemon] Device name: ${this.deviceName}`);
372
- }
373
- // EP726: Cache device UUID for unified device identification
374
- // Persist to config for future use
375
- if (authMessage.deviceId) {
376
- this.deviceId = authMessage.deviceId;
377
- console.log(`[Daemon] Device ID (UUID): ${this.deviceId}`);
378
- // Persist deviceId to config file so it's available on daemon restart
379
- await this.cacheDeviceId(authMessage.deviceId);
380
- }
381
- // EP735: Cache Fly.io machine ID for sticky session routing
382
- // Only set when server is running on Fly.io, null otherwise
383
- if (authMessage.flyMachineId) {
384
- this.flyMachineId = authMessage.flyMachineId;
385
- console.log(`[Daemon] Fly Machine ID: ${this.flyMachineId}`);
386
- }
387
- });
388
- // Register error handler
389
- client.on('error', (message) => {
390
- console.error(`[Daemon] Server error for ${projectId}:`, message);
391
- });
392
- // EP701: Register disconnected handler to track connection state
393
- // Removes from liveConnections immediately so /device-info returns accurate state
394
- // Keeps entry in connections map for potential reconnection (client handles reconnect internally)
395
- client.on('disconnected', (event) => {
396
- const disconnectEvent = event;
397
- console.log(`[Daemon] Connection closed for ${projectId}: code=${disconnectEvent.code}, willReconnect=${disconnectEvent.willReconnect}`);
398
- // Always remove from liveConnections - connection is dead until reconnected
399
- this.liveConnections.delete(projectPath);
400
- // EP803: Update identity server connection status
401
- // Only mark as disconnected if no other connections are live
402
- if (this.identityServer && this.liveConnections.size === 0) {
403
- this.identityServer.setConnected(false);
404
- }
405
- // Only remove from connections map if we won't reconnect (intentional disconnect)
406
- if (!disconnectEvent.willReconnect) {
407
- this.connections.delete(projectPath);
408
- console.log(`[Daemon] Removed connection for ${projectPath} from map`);
409
- }
410
- });
411
- try {
412
- // EP601: Read daemon PID from file
413
- let daemonPid;
414
- try {
415
- const pidPath = (0, daemon_manager_1.getPidFilePath)();
416
- if (fs.existsSync(pidPath)) {
417
- const pidStr = fs.readFileSync(pidPath, 'utf-8').trim();
418
- daemonPid = parseInt(pidStr, 10);
419
- }
420
- }
421
- catch (pidError) {
422
- console.warn(`[Daemon] Could not read daemon PID:`, pidError instanceof Error ? pidError.message : pidError);
423
- }
424
- // Connect with OAuth token, machine ID, and device info (EP596, EP601: includes daemonPid)
425
- await client.connect(wsUrl, config.access_token, this.machineId, {
426
- hostname: os.hostname(),
427
- osPlatform: os.platform(),
428
- osArch: os.arch(),
429
- daemonPid
430
- });
431
- console.log(`[Daemon] Successfully connected to project ${projectId}`);
432
- }
433
- catch (error) {
434
- console.error(`[Daemon] Failed to connect to ${projectId}:`, error);
435
- this.connections.delete(projectPath);
436
- throw error;
437
- }
438
- }
439
- /**
440
- * Disconnect from a project's WebSocket
441
- */
442
- async disconnectProject(projectPath) {
443
- const connection = this.connections.get(projectPath);
444
- if (!connection) {
445
- return;
446
- }
447
- // Clear reconnect timer
448
- if (connection.reconnectTimer) {
449
- clearTimeout(connection.reconnectTimer);
450
- }
451
- // Disconnect client (handles WebSocket close gracefully)
452
- await connection.client.disconnect();
453
- this.connections.delete(projectPath);
454
- this.liveConnections.delete(projectPath); // EP701: Also clean up liveConnections
455
- console.log(`[Daemon] Disconnected from ${projectPath}`);
456
- }
457
- /**
458
- * Setup graceful shutdown handlers
459
- * EP613: Now uses cleanupAndExit to ensure PID file is removed
460
- */
461
- setupShutdownHandlers() {
462
- const shutdownHandler = async (signal) => {
463
- console.log(`[Daemon] Received ${signal}, shutting down...`);
464
- await this.cleanupAndExit();
465
- };
466
- process.on('SIGTERM', () => shutdownHandler('SIGTERM'));
467
- process.on('SIGINT', () => shutdownHandler('SIGINT'));
468
- }
469
- /**
470
- * EP595: Configure git with user and workspace ID for post-checkout hook
471
- * EP655: Added machineId for device isolation in multi-device environments
472
- * EP725: Added projectId for main branch badge tracking
473
- * EP726: Added deviceId (UUID) for unified device identification
474
- *
475
- * This stores the IDs in .git/config so the post-checkout hook can
476
- * update module.checkout_* fields when git operations happen from terminal.
477
- */
478
- async configureGitUser(projectPath, userId, workspaceId, machineId, projectId, deviceId) {
479
- try {
480
- const { execSync } = await Promise.resolve().then(() => __importStar(require('child_process')));
481
- // Set git config values in the project's .git/config
482
- execSync(`git config episoda.userId ${userId}`, {
483
- cwd: projectPath,
484
- encoding: 'utf8',
485
- stdio: 'pipe'
486
- });
487
- execSync(`git config episoda.workspaceId ${workspaceId}`, {
488
- cwd: projectPath,
489
- encoding: 'utf8',
490
- stdio: 'pipe'
491
- });
492
- // EP655: Set machineId for device isolation in post-checkout hook
493
- execSync(`git config episoda.machineId ${machineId}`, {
494
- cwd: projectPath,
495
- encoding: 'utf8',
496
- stdio: 'pipe'
497
- });
498
- // EP725: Set projectId for main branch badge tracking
499
- // This ensures main branch checkouts are recorded with project_id
500
- execSync(`git config episoda.projectId ${projectId}`, {
501
- cwd: projectPath,
502
- encoding: 'utf8',
503
- stdio: 'pipe'
504
- });
505
- // EP726: Set deviceId (UUID) for unified device identification
506
- // This allows the post-checkout hook to pass the UUID to the database
507
- if (deviceId) {
508
- execSync(`git config episoda.deviceId ${deviceId}`, {
509
- cwd: projectPath,
510
- encoding: 'utf8',
511
- stdio: 'pipe'
512
- });
513
- }
514
- console.log(`[Daemon] Configured git for project: episoda.userId=${userId}, machineId=${machineId}, projectId=${projectId}${deviceId ? `, deviceId=${deviceId}` : ''}`);
515
- }
516
- catch (error) {
517
- // Non-fatal error - git hook just won't work
518
- console.warn(`[Daemon] Failed to configure git user for ${projectPath}:`, error instanceof Error ? error.message : error);
519
- }
520
- }
521
- /**
522
- * EP610: Install git hooks from bundled files
523
- *
524
- * Installs post-checkout and pre-commit hooks to enable:
525
- * - Branch tracking (post-checkout updates module.checkout_* fields)
526
- * - Main branch protection (pre-commit blocks direct commits to main)
527
- */
528
- async installGitHooks(projectPath) {
529
- const hooks = ['post-checkout', 'pre-commit'];
530
- const hooksDir = path.join(projectPath, '.git', 'hooks');
531
- // Ensure hooks directory exists
532
- if (!fs.existsSync(hooksDir)) {
533
- console.warn(`[Daemon] Hooks directory not found: ${hooksDir}`);
534
- return;
535
- }
536
- for (const hookName of hooks) {
537
- try {
538
- const hookPath = path.join(hooksDir, hookName);
539
- // Read bundled hook content
540
- // __dirname in compiled code points to dist/daemon/, hooks are in dist/hooks/
541
- const bundledHookPath = path.join(__dirname, '..', 'hooks', hookName);
542
- if (!fs.existsSync(bundledHookPath)) {
543
- console.warn(`[Daemon] Bundled hook not found: ${bundledHookPath}`);
544
- continue;
545
- }
546
- const hookContent = fs.readFileSync(bundledHookPath, 'utf-8');
547
- // Check if hook already exists with same content
548
- if (fs.existsSync(hookPath)) {
549
- const existingContent = fs.readFileSync(hookPath, 'utf-8');
550
- if (existingContent === hookContent) {
551
- // Hook is up to date
552
- continue;
553
- }
554
- }
555
- // Write hook file
556
- fs.writeFileSync(hookPath, hookContent, { mode: 0o755 });
557
- console.log(`[Daemon] Installed git hook: ${hookName}`);
558
- }
559
- catch (error) {
560
- // Non-fatal - just log warning
561
- console.warn(`[Daemon] Failed to install ${hookName} hook:`, error instanceof Error ? error.message : error);
562
- }
563
- }
564
- }
565
- /**
566
- * EP726: Cache device UUID to config file
567
- *
568
- * Persists the device_id (UUID) received from the server so it's available
569
- * on daemon restart without needing to re-register the device.
570
- */
571
- async cacheDeviceId(deviceId) {
572
- try {
573
- const config = await (0, core_1.loadConfig)();
574
- if (!config) {
575
- console.warn('[Daemon] Cannot cache device ID - no config found');
576
- return;
577
- }
578
- // Only update if device_id has changed
579
- if (config.device_id === deviceId) {
580
- return;
581
- }
582
- // Update config with device_id and machine_id
583
- const updatedConfig = {
584
- ...config,
585
- device_id: deviceId,
586
- machine_id: this.machineId
587
- };
588
- await (0, core_1.saveConfig)(updatedConfig);
589
- console.log(`[Daemon] Cached device ID to config: ${deviceId}`);
590
- }
591
- catch (error) {
592
- console.warn('[Daemon] Failed to cache device ID:', error instanceof Error ? error.message : error);
593
- }
594
- }
595
- /**
596
- * Gracefully shutdown daemon
597
- */
598
- async shutdown() {
599
- if (this.shuttingDown)
600
- return;
601
- this.shuttingDown = true;
602
- console.log('[Daemon] Shutting down...');
603
- // Close all WebSocket connections
604
- for (const [projectPath, connection] of this.connections) {
605
- if (connection.reconnectTimer) {
606
- clearTimeout(connection.reconnectTimer);
607
- }
608
- await connection.client.disconnect();
609
- }
610
- this.connections.clear();
611
- // EP803: Stop identity server
612
- if (this.identityServer) {
613
- await this.identityServer.stop();
614
- this.identityServer = null;
615
- }
616
- // Stop IPC server
617
- await this.ipcServer.stop();
618
- console.log('[Daemon] Shutdown complete');
619
- }
620
- /**
621
- * EP613: Clean up PID file and exit gracefully
622
- * Called when user explicitly requests disconnect
623
- */
624
- async cleanupAndExit() {
625
- await this.shutdown();
626
- // Clean up PID file
627
- try {
628
- const pidPath = (0, daemon_manager_1.getPidFilePath)();
629
- if (fs.existsSync(pidPath)) {
630
- fs.unlinkSync(pidPath);
631
- console.log('[Daemon] PID file cleaned up');
632
- }
633
- }
634
- catch (error) {
635
- console.error('[Daemon] Failed to clean up PID file:', error);
636
- }
637
- console.log('[Daemon] Exiting...');
638
- process.exit(0);
639
- }
640
- }
641
- /**
642
- * Main entry point
643
- */
644
- async function main() {
645
- // Only run if explicitly in daemon mode
646
- if (!process.env.EPISODA_DAEMON_MODE) {
647
- console.error('This script should only be run by daemon-manager');
648
- process.exit(1);
649
- }
650
- const daemon = new Daemon();
651
- await daemon.start();
652
- // Keep process alive
653
- await new Promise(() => {
654
- // Never resolves - daemon runs until killed
655
- });
656
- }
657
- // Run daemon
658
- main().catch(error => {
659
- console.error('[Daemon] Fatal error:', error);
660
- process.exit(1);
661
- });
662
- //# sourceMappingURL=daemon-process.js.map