episoda 0.2.9 → 0.2.11

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,3721 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __commonJS = (cb, mod) => function __require() {
10
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+
29
+ // ../core/dist/command-protocol.js
30
+ var require_command_protocol = __commonJS({
31
+ "../core/dist/command-protocol.js"(exports2) {
32
+ "use strict";
33
+ Object.defineProperty(exports2, "__esModule", { value: true });
34
+ }
35
+ });
36
+
37
+ // ../core/dist/git-validator.js
38
+ var require_git_validator = __commonJS({
39
+ "../core/dist/git-validator.js"(exports2) {
40
+ "use strict";
41
+ Object.defineProperty(exports2, "__esModule", { value: true });
42
+ exports2.validateBranchName = validateBranchName;
43
+ exports2.validateCommitMessage = validateCommitMessage;
44
+ exports2.validateFilePaths = validateFilePaths;
45
+ exports2.sanitizeArgs = sanitizeArgs;
46
+ function validateBranchName(branchName) {
47
+ if (!branchName || branchName.trim().length === 0) {
48
+ return { valid: false, error: "UNKNOWN_ERROR" };
49
+ }
50
+ const invalidPatterns = [
51
+ /^\-/,
52
+ // Starts with -
53
+ /^\//,
54
+ // Starts with /
55
+ /\.\./,
56
+ // Contains ..
57
+ /@\{/,
58
+ // Contains @{
59
+ /\\/,
60
+ // Contains backslash
61
+ /\.lock$/,
62
+ // Ends with .lock
63
+ /\/$/,
64
+ // Ends with /
65
+ /\s/,
66
+ // Contains whitespace
67
+ /[\x00-\x1F\x7F]/,
68
+ // Contains control characters
69
+ /[\*\?\[\]~\^:]/
70
+ // Contains special characters
71
+ ];
72
+ for (const pattern of invalidPatterns) {
73
+ if (pattern.test(branchName)) {
74
+ return { valid: false, error: "UNKNOWN_ERROR" };
75
+ }
76
+ }
77
+ if (branchName === "@") {
78
+ return { valid: false, error: "UNKNOWN_ERROR" };
79
+ }
80
+ if (branchName.length > 200) {
81
+ return { valid: false, error: "UNKNOWN_ERROR" };
82
+ }
83
+ return { valid: true };
84
+ }
85
+ function validateCommitMessage(message) {
86
+ if (!message || message.trim().length === 0) {
87
+ return { valid: false, error: "UNKNOWN_ERROR" };
88
+ }
89
+ if (message.length > 1e4) {
90
+ return { valid: false, error: "UNKNOWN_ERROR" };
91
+ }
92
+ return { valid: true };
93
+ }
94
+ function validateFilePaths(files) {
95
+ if (!files || files.length === 0) {
96
+ return { valid: true };
97
+ }
98
+ for (const file of files) {
99
+ if (file.includes("\0")) {
100
+ return { valid: false, error: "UNKNOWN_ERROR" };
101
+ }
102
+ if (/[\x00-\x1F\x7F]/.test(file)) {
103
+ return { valid: false, error: "UNKNOWN_ERROR" };
104
+ }
105
+ if (file.trim().length === 0) {
106
+ return { valid: false, error: "UNKNOWN_ERROR" };
107
+ }
108
+ }
109
+ return { valid: true };
110
+ }
111
+ function sanitizeArgs(args) {
112
+ return args.map((arg) => {
113
+ return arg.replace(/\0/g, "");
114
+ });
115
+ }
116
+ }
117
+ });
118
+
119
+ // ../core/dist/git-parser.js
120
+ var require_git_parser = __commonJS({
121
+ "../core/dist/git-parser.js"(exports2) {
122
+ "use strict";
123
+ Object.defineProperty(exports2, "__esModule", { value: true });
124
+ exports2.parseGitStatus = parseGitStatus;
125
+ exports2.parseMergeConflicts = parseMergeConflicts;
126
+ exports2.parseGitError = parseGitError;
127
+ exports2.extractBranchName = extractBranchName;
128
+ exports2.isDetachedHead = isDetachedHead;
129
+ exports2.parseRemoteTracking = parseRemoteTracking;
130
+ function parseGitStatus(output) {
131
+ const lines = output.split("\n");
132
+ const uncommittedFiles = [];
133
+ let currentBranch;
134
+ for (const line of lines) {
135
+ if (line.startsWith("## ")) {
136
+ const branchInfo = line.substring(3);
137
+ const branchMatch = branchInfo.match(/^([^\s.]+)/);
138
+ if (branchMatch && branchMatch[1]) {
139
+ currentBranch = branchMatch[1] === "HEAD" ? "HEAD (detached)" : branchMatch[1];
140
+ }
141
+ continue;
142
+ }
143
+ if (line.length >= 3) {
144
+ const status = line.substring(0, 2);
145
+ const filePath = line.substring(3).trim();
146
+ if (status.trim().length > 0 && filePath.length > 0) {
147
+ uncommittedFiles.push(filePath);
148
+ }
149
+ }
150
+ }
151
+ const isClean = uncommittedFiles.length === 0;
152
+ return { uncommittedFiles, isClean, currentBranch };
153
+ }
154
+ function parseMergeConflicts(output) {
155
+ const lines = output.split("\n");
156
+ const conflictingFiles = [];
157
+ for (const line of lines) {
158
+ const trimmed = line.trim();
159
+ if (trimmed.startsWith("CONFLICT")) {
160
+ const match = trimmed.match(/CONFLICT.*in (.+)$/);
161
+ if (match && match[1]) {
162
+ conflictingFiles.push(match[1]);
163
+ }
164
+ }
165
+ if (trimmed.startsWith("UU ")) {
166
+ conflictingFiles.push(trimmed.substring(3).trim());
167
+ }
168
+ }
169
+ return conflictingFiles;
170
+ }
171
+ function parseGitError(stderr, stdout, exitCode) {
172
+ const combinedOutput = `${stderr}
173
+ ${stdout}`.toLowerCase();
174
+ if (combinedOutput.includes("git: command not found") || combinedOutput.includes("'git' is not recognized")) {
175
+ return "GIT_NOT_INSTALLED";
176
+ }
177
+ if (combinedOutput.includes("not a git repository") || combinedOutput.includes("not a git repo")) {
178
+ return "NOT_GIT_REPO";
179
+ }
180
+ if (combinedOutput.includes("conflict") || combinedOutput.includes("merge conflict") || exitCode === 1 && combinedOutput.includes("automatic merge failed")) {
181
+ return "MERGE_CONFLICT";
182
+ }
183
+ if (combinedOutput.includes("please commit your changes") || combinedOutput.includes("would be overwritten") || combinedOutput.includes("cannot checkout") && combinedOutput.includes("files would be overwritten")) {
184
+ return "UNCOMMITTED_CHANGES";
185
+ }
186
+ if (combinedOutput.includes("authentication failed") || combinedOutput.includes("could not read username") || combinedOutput.includes("permission denied") || combinedOutput.includes("could not read password") || combinedOutput.includes("fatal: authentication failed")) {
187
+ return "AUTH_FAILURE";
188
+ }
189
+ if (combinedOutput.includes("could not resolve host") || combinedOutput.includes("failed to connect") || combinedOutput.includes("network is unreachable") || combinedOutput.includes("connection timed out") || combinedOutput.includes("could not read from remote")) {
190
+ return "NETWORK_ERROR";
191
+ }
192
+ if (combinedOutput.includes("did not match any file") || combinedOutput.includes("branch") && combinedOutput.includes("not found") || combinedOutput.includes("pathspec") && combinedOutput.includes("did not match")) {
193
+ return "BRANCH_NOT_FOUND";
194
+ }
195
+ if (combinedOutput.includes("already exists") || combinedOutput.includes("a branch named") && combinedOutput.includes("already exists")) {
196
+ return "BRANCH_ALREADY_EXISTS";
197
+ }
198
+ if (combinedOutput.includes("push rejected") || combinedOutput.includes("failed to push") || combinedOutput.includes("rejected") && combinedOutput.includes("non-fast-forward") || combinedOutput.includes("updates were rejected")) {
199
+ return "PUSH_REJECTED";
200
+ }
201
+ if (exitCode === 124 || exitCode === 143) {
202
+ return "COMMAND_TIMEOUT";
203
+ }
204
+ return "UNKNOWN_ERROR";
205
+ }
206
+ function extractBranchName(output) {
207
+ const lines = output.split("\n");
208
+ for (const line of lines) {
209
+ const trimmed = line.trim();
210
+ let match = trimmed.match(/Switched to (?:a new )?branch '(.+)'/);
211
+ if (match && match[1]) {
212
+ return match[1];
213
+ }
214
+ match = trimmed.match(/On branch (.+)/);
215
+ if (match && match[1]) {
216
+ return match[1];
217
+ }
218
+ }
219
+ return void 0;
220
+ }
221
+ function isDetachedHead(output) {
222
+ return output.toLowerCase().includes("head detached") || output.toLowerCase().includes("you are in 'detached head' state");
223
+ }
224
+ function parseRemoteTracking(output) {
225
+ const lines = output.split("\n");
226
+ for (const line of lines) {
227
+ const trimmed = line.trim();
228
+ const match = trimmed.match(/set up to track remote branch '(.+?)'(?: from '(.+?)')?/);
229
+ if (match) {
230
+ return {
231
+ hasUpstream: true,
232
+ remoteBranch: match[1],
233
+ remote: match[2] || "origin"
234
+ };
235
+ }
236
+ const upstreamMatch = trimmed.match(/(?:up to date with|ahead of|behind) '(.+?)\/(.+?)'/);
237
+ if (upstreamMatch) {
238
+ return {
239
+ hasUpstream: true,
240
+ remote: upstreamMatch[1],
241
+ remoteBranch: upstreamMatch[2]
242
+ };
243
+ }
244
+ }
245
+ return { hasUpstream: false };
246
+ }
247
+ }
248
+ });
249
+
250
+ // ../core/dist/git-executor.js
251
+ var require_git_executor = __commonJS({
252
+ "../core/dist/git-executor.js"(exports2) {
253
+ "use strict";
254
+ var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
255
+ if (k2 === void 0) k2 = k;
256
+ var desc = Object.getOwnPropertyDescriptor(m, k);
257
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
258
+ desc = { enumerable: true, get: function() {
259
+ return m[k];
260
+ } };
261
+ }
262
+ Object.defineProperty(o, k2, desc);
263
+ }) : (function(o, m, k, k2) {
264
+ if (k2 === void 0) k2 = k;
265
+ o[k2] = m[k];
266
+ }));
267
+ var __setModuleDefault = exports2 && exports2.__setModuleDefault || (Object.create ? (function(o, v) {
268
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
269
+ }) : function(o, v) {
270
+ o["default"] = v;
271
+ });
272
+ var __importStar = exports2 && exports2.__importStar || /* @__PURE__ */ (function() {
273
+ var ownKeys = function(o) {
274
+ ownKeys = Object.getOwnPropertyNames || function(o2) {
275
+ var ar = [];
276
+ for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
277
+ return ar;
278
+ };
279
+ return ownKeys(o);
280
+ };
281
+ return function(mod) {
282
+ if (mod && mod.__esModule) return mod;
283
+ var result = {};
284
+ if (mod != null) {
285
+ for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
286
+ }
287
+ __setModuleDefault(result, mod);
288
+ return result;
289
+ };
290
+ })();
291
+ Object.defineProperty(exports2, "__esModule", { value: true });
292
+ exports2.GitExecutor = void 0;
293
+ var child_process_1 = require("child_process");
294
+ var util_1 = require("util");
295
+ var git_validator_1 = require_git_validator();
296
+ var git_parser_1 = require_git_parser();
297
+ var execAsync = (0, util_1.promisify)(child_process_1.exec);
298
+ var GitExecutor2 = class {
299
+ /**
300
+ * Execute a git command
301
+ * @param command - The git command to execute
302
+ * @param options - Execution options (timeout, cwd, etc.)
303
+ * @returns Structured result with success/error details
304
+ */
305
+ async execute(command, options) {
306
+ try {
307
+ const gitInstalled = await this.validateGitInstalled();
308
+ if (!gitInstalled) {
309
+ return {
310
+ success: false,
311
+ error: "GIT_NOT_INSTALLED"
312
+ };
313
+ }
314
+ const cwd = options?.cwd || process.cwd();
315
+ const isGitRepo = await this.isGitRepository(cwd);
316
+ if (!isGitRepo) {
317
+ return {
318
+ success: false,
319
+ error: "NOT_GIT_REPO"
320
+ };
321
+ }
322
+ switch (command.action) {
323
+ case "checkout":
324
+ return await this.executeCheckout(command, cwd, options);
325
+ case "create_branch":
326
+ return await this.executeCreateBranch(command, cwd, options);
327
+ case "commit":
328
+ return await this.executeCommit(command, cwd, options);
329
+ case "push":
330
+ return await this.executePush(command, cwd, options);
331
+ case "status":
332
+ return await this.executeStatus(cwd, options);
333
+ case "pull":
334
+ return await this.executePull(command, cwd, options);
335
+ case "delete_branch":
336
+ return await this.executeDeleteBranch(command, cwd, options);
337
+ // EP597: Read operations for production local dev mode
338
+ case "branch_exists":
339
+ return await this.executeBranchExists(command, cwd, options);
340
+ case "branch_has_commits":
341
+ return await this.executeBranchHasCommits(command, cwd, options);
342
+ // EP598: Main branch check for production
343
+ case "main_branch_check":
344
+ return await this.executeMainBranchCheck(cwd, options);
345
+ // EP599: Get commits for branch
346
+ case "get_commits":
347
+ return await this.executeGetCommits(command, cwd, options);
348
+ // EP599: Advanced operations for move-to-module and discard-main-changes
349
+ case "stash":
350
+ return await this.executeStash(command, cwd, options);
351
+ case "reset":
352
+ return await this.executeReset(command, cwd, options);
353
+ case "merge":
354
+ return await this.executeMerge(command, cwd, options);
355
+ case "cherry_pick":
356
+ return await this.executeCherryPick(command, cwd, options);
357
+ case "clean":
358
+ return await this.executeClean(command, cwd, options);
359
+ case "add":
360
+ return await this.executeAdd(command, cwd, options);
361
+ case "fetch":
362
+ return await this.executeFetch(command, cwd, options);
363
+ // EP599-3: Composite operations
364
+ case "move_to_module":
365
+ return await this.executeMoveToModule(command, cwd, options);
366
+ case "discard_main_changes":
367
+ return await this.executeDiscardMainChanges(cwd, options);
368
+ // EP523: Branch sync operations
369
+ case "sync_status":
370
+ return await this.executeSyncStatus(command, cwd, options);
371
+ case "sync_main":
372
+ return await this.executeSyncMain(cwd, options);
373
+ case "rebase_branch":
374
+ return await this.executeRebaseBranch(command, cwd, options);
375
+ case "rebase_abort":
376
+ return await this.executeRebaseAbort(cwd, options);
377
+ case "rebase_continue":
378
+ return await this.executeRebaseContinue(cwd, options);
379
+ case "rebase_status":
380
+ return await this.executeRebaseStatus(cwd, options);
381
+ default:
382
+ return {
383
+ success: false,
384
+ error: "UNKNOWN_ERROR",
385
+ output: "Unknown command action"
386
+ };
387
+ }
388
+ } catch (error) {
389
+ return {
390
+ success: false,
391
+ error: "UNKNOWN_ERROR",
392
+ output: error instanceof Error ? error.message : "Unknown error occurred"
393
+ };
394
+ }
395
+ }
396
+ /**
397
+ * Execute checkout command
398
+ */
399
+ async executeCheckout(command, cwd, options) {
400
+ const validation = (0, git_validator_1.validateBranchName)(command.branch);
401
+ if (!validation.valid) {
402
+ return {
403
+ success: false,
404
+ error: validation.error || "UNKNOWN_ERROR"
405
+ };
406
+ }
407
+ const statusResult = await this.executeStatus(cwd, options);
408
+ if (statusResult.success && statusResult.details?.uncommittedFiles?.length) {
409
+ return {
410
+ success: false,
411
+ error: "UNCOMMITTED_CHANGES",
412
+ details: {
413
+ uncommittedFiles: statusResult.details.uncommittedFiles
414
+ }
415
+ };
416
+ }
417
+ const args = ["checkout"];
418
+ if (command.create) {
419
+ args.push("-b");
420
+ }
421
+ args.push(command.branch);
422
+ return await this.runGitCommand(args, cwd, options);
423
+ }
424
+ /**
425
+ * Execute create_branch command
426
+ */
427
+ async executeCreateBranch(command, cwd, options) {
428
+ const validation = (0, git_validator_1.validateBranchName)(command.branch);
429
+ if (!validation.valid) {
430
+ return {
431
+ success: false,
432
+ error: validation.error || "UNKNOWN_ERROR"
433
+ };
434
+ }
435
+ if (command.from) {
436
+ const fromValidation = (0, git_validator_1.validateBranchName)(command.from);
437
+ if (!fromValidation.valid) {
438
+ return {
439
+ success: false,
440
+ error: fromValidation.error || "UNKNOWN_ERROR"
441
+ };
442
+ }
443
+ }
444
+ const args = ["checkout", "-b", command.branch];
445
+ if (command.from) {
446
+ args.push(command.from);
447
+ }
448
+ return await this.runGitCommand(args, cwd, options);
449
+ }
450
+ /**
451
+ * Execute commit command
452
+ */
453
+ async executeCommit(command, cwd, options) {
454
+ const validation = (0, git_validator_1.validateCommitMessage)(command.message);
455
+ if (!validation.valid) {
456
+ return {
457
+ success: false,
458
+ error: validation.error || "UNKNOWN_ERROR"
459
+ };
460
+ }
461
+ if (command.files) {
462
+ const fileValidation = (0, git_validator_1.validateFilePaths)(command.files);
463
+ if (!fileValidation.valid) {
464
+ return {
465
+ success: false,
466
+ error: fileValidation.error || "UNKNOWN_ERROR"
467
+ };
468
+ }
469
+ for (const file of command.files) {
470
+ const addResult = await this.runGitCommand(["add", file], cwd, options);
471
+ if (!addResult.success) {
472
+ return addResult;
473
+ }
474
+ }
475
+ } else {
476
+ const addResult = await this.runGitCommand(["add", "-A"], cwd, options);
477
+ if (!addResult.success) {
478
+ return addResult;
479
+ }
480
+ }
481
+ const args = ["commit", "-m", command.message];
482
+ return await this.runGitCommand(args, cwd, options);
483
+ }
484
+ /**
485
+ * Execute push command
486
+ * EP769: Added force parameter for pushing rebased branches
487
+ */
488
+ async executePush(command, cwd, options) {
489
+ const validation = (0, git_validator_1.validateBranchName)(command.branch);
490
+ if (!validation.valid) {
491
+ return {
492
+ success: false,
493
+ error: validation.error || "UNKNOWN_ERROR"
494
+ };
495
+ }
496
+ const args = ["push"];
497
+ if (command.force) {
498
+ args.push("--force");
499
+ }
500
+ if (command.setUpstream) {
501
+ args.push("-u", "origin", command.branch);
502
+ } else {
503
+ args.push("origin", command.branch);
504
+ }
505
+ const env = { ...process.env };
506
+ if (options?.githubToken) {
507
+ env.GIT_ASKPASS = "echo";
508
+ env.GIT_USERNAME = "x-access-token";
509
+ env.GIT_PASSWORD = options.githubToken;
510
+ }
511
+ return await this.runGitCommand(args, cwd, { ...options, env });
512
+ }
513
+ /**
514
+ * Execute status command
515
+ */
516
+ async executeStatus(cwd, options) {
517
+ const result = await this.runGitCommand(["status", "--porcelain", "-b"], cwd, options);
518
+ if (result.success && result.output) {
519
+ const statusInfo = (0, git_parser_1.parseGitStatus)(result.output);
520
+ return {
521
+ success: true,
522
+ output: result.output,
523
+ details: {
524
+ uncommittedFiles: statusInfo.uncommittedFiles,
525
+ branchName: statusInfo.currentBranch
526
+ }
527
+ };
528
+ }
529
+ return result;
530
+ }
531
+ /**
532
+ * Execute pull command
533
+ */
534
+ async executePull(command, cwd, options) {
535
+ if (command.branch) {
536
+ const validation = (0, git_validator_1.validateBranchName)(command.branch);
537
+ if (!validation.valid) {
538
+ return {
539
+ success: false,
540
+ error: validation.error || "UNKNOWN_ERROR"
541
+ };
542
+ }
543
+ }
544
+ const statusResult = await this.executeStatus(cwd, options);
545
+ if (statusResult.success && statusResult.details?.uncommittedFiles?.length) {
546
+ return {
547
+ success: false,
548
+ error: "UNCOMMITTED_CHANGES",
549
+ details: {
550
+ uncommittedFiles: statusResult.details.uncommittedFiles
551
+ }
552
+ };
553
+ }
554
+ const args = ["pull"];
555
+ if (command.branch) {
556
+ args.push("origin", command.branch);
557
+ }
558
+ const result = await this.runGitCommand(args, cwd, options);
559
+ if (!result.success && result.output) {
560
+ const conflicts = (0, git_parser_1.parseMergeConflicts)(result.output);
561
+ if (conflicts.length > 0) {
562
+ return {
563
+ success: false,
564
+ error: "MERGE_CONFLICT",
565
+ output: result.output,
566
+ details: {
567
+ conflictingFiles: conflicts
568
+ }
569
+ };
570
+ }
571
+ }
572
+ return result;
573
+ }
574
+ /**
575
+ * Execute delete_branch command
576
+ */
577
+ async executeDeleteBranch(command, cwd, options) {
578
+ const validation = (0, git_validator_1.validateBranchName)(command.branch);
579
+ if (!validation.valid) {
580
+ return {
581
+ success: false,
582
+ error: validation.error || "UNKNOWN_ERROR"
583
+ };
584
+ }
585
+ const args = ["branch"];
586
+ args.push(command.force ? "-D" : "-d");
587
+ args.push(command.branch);
588
+ return await this.runGitCommand(args, cwd, options);
589
+ }
590
+ /**
591
+ * EP597: Execute branch_exists command
592
+ * Checks if a branch exists locally and/or remotely
593
+ */
594
+ async executeBranchExists(command, cwd, options) {
595
+ const validation = (0, git_validator_1.validateBranchName)(command.branch);
596
+ if (!validation.valid) {
597
+ return {
598
+ success: false,
599
+ error: validation.error || "UNKNOWN_ERROR"
600
+ };
601
+ }
602
+ try {
603
+ let isLocal = false;
604
+ let isRemote = false;
605
+ try {
606
+ const { stdout: localBranches } = await execAsync("git branch --list", { cwd, timeout: options?.timeout || 1e4 });
607
+ isLocal = localBranches.split("\n").some((line) => {
608
+ const branchName = line.replace(/^\*?\s*/, "").trim();
609
+ return branchName === command.branch;
610
+ });
611
+ } catch {
612
+ }
613
+ try {
614
+ const { stdout: remoteBranches } = await execAsync(`git ls-remote --heads origin ${command.branch}`, { cwd, timeout: options?.timeout || 1e4 });
615
+ isRemote = remoteBranches.trim().length > 0;
616
+ } catch {
617
+ }
618
+ const branchExists = isLocal || isRemote;
619
+ return {
620
+ success: true,
621
+ output: branchExists ? `Branch ${command.branch} exists` : `Branch ${command.branch} does not exist`,
622
+ details: {
623
+ branchName: command.branch,
624
+ branchExists,
625
+ isLocal,
626
+ isRemote
627
+ }
628
+ };
629
+ } catch (error) {
630
+ return {
631
+ success: false,
632
+ error: "UNKNOWN_ERROR",
633
+ output: error.message || "Failed to check branch existence"
634
+ };
635
+ }
636
+ }
637
+ /**
638
+ * EP597: Execute branch_has_commits command
639
+ * Checks if a branch has commits ahead of the base branch (default: main)
640
+ */
641
+ async executeBranchHasCommits(command, cwd, options) {
642
+ const validation = (0, git_validator_1.validateBranchName)(command.branch);
643
+ if (!validation.valid) {
644
+ return {
645
+ success: false,
646
+ error: validation.error || "UNKNOWN_ERROR"
647
+ };
648
+ }
649
+ const baseBranch = command.baseBranch || "main";
650
+ try {
651
+ const { stdout } = await execAsync(`git cherry origin/${baseBranch} ${command.branch}`, { cwd, timeout: options?.timeout || 1e4 });
652
+ const uniqueCommits = stdout.trim().split("\n").filter((line) => line.startsWith("+"));
653
+ const hasCommits = uniqueCommits.length > 0;
654
+ return {
655
+ success: true,
656
+ output: hasCommits ? `Branch ${command.branch} has ${uniqueCommits.length} commits ahead of ${baseBranch}` : `Branch ${command.branch} has no commits ahead of ${baseBranch}`,
657
+ details: {
658
+ branchName: command.branch,
659
+ hasCommits
660
+ }
661
+ };
662
+ } catch (error) {
663
+ try {
664
+ const { stdout } = await execAsync(`git rev-list --count origin/${baseBranch}..${command.branch}`, { cwd, timeout: options?.timeout || 1e4 });
665
+ const commitCount = parseInt(stdout.trim(), 10);
666
+ const hasCommits = commitCount > 0;
667
+ return {
668
+ success: true,
669
+ output: hasCommits ? `Branch ${command.branch} has ${commitCount} commits ahead of ${baseBranch}` : `Branch ${command.branch} has no commits ahead of ${baseBranch}`,
670
+ details: {
671
+ branchName: command.branch,
672
+ hasCommits
673
+ }
674
+ };
675
+ } catch {
676
+ return {
677
+ success: false,
678
+ error: "BRANCH_NOT_FOUND",
679
+ output: error.message || `Failed to check commits for branch ${command.branch}`
680
+ };
681
+ }
682
+ }
683
+ }
684
+ /**
685
+ * EP598: Execute main branch check - returns current branch, uncommitted files, and unpushed commits
686
+ */
687
+ async executeMainBranchCheck(cwd, options) {
688
+ try {
689
+ let currentBranch = "";
690
+ try {
691
+ const { stdout } = await execAsync("git branch --show-current", { cwd, timeout: options?.timeout || 1e4 });
692
+ currentBranch = stdout.trim();
693
+ } catch (error) {
694
+ return {
695
+ success: false,
696
+ error: "UNKNOWN_ERROR",
697
+ output: error.message || "Failed to get current branch"
698
+ };
699
+ }
700
+ let uncommittedFiles = [];
701
+ try {
702
+ const { stdout } = await execAsync("git status --porcelain", { cwd, timeout: options?.timeout || 1e4 });
703
+ if (stdout) {
704
+ uncommittedFiles = stdout.split("\n").filter((line) => line.trim()).map((line) => {
705
+ const parts = line.trim().split(/\s+/);
706
+ return parts.slice(1).join(" ");
707
+ });
708
+ }
709
+ } catch {
710
+ }
711
+ let localCommits = [];
712
+ if (currentBranch === "main") {
713
+ try {
714
+ const { stdout } = await execAsync('git log origin/main..HEAD --format="%H|%s|%an"', { cwd, timeout: options?.timeout || 1e4 });
715
+ if (stdout) {
716
+ localCommits = stdout.split("\n").filter((line) => line.trim()).map((line) => {
717
+ const [sha, message, author] = line.split("|");
718
+ return {
719
+ sha: sha ? sha.substring(0, 8) : "",
720
+ message: message || "",
721
+ author: author || ""
722
+ };
723
+ });
724
+ }
725
+ } catch {
726
+ }
727
+ }
728
+ return {
729
+ success: true,
730
+ output: `Branch: ${currentBranch}, Uncommitted: ${uncommittedFiles.length}, Unpushed: ${localCommits.length}`,
731
+ details: {
732
+ currentBranch,
733
+ uncommittedFiles,
734
+ localCommits
735
+ }
736
+ };
737
+ } catch (error) {
738
+ return {
739
+ success: false,
740
+ error: "UNKNOWN_ERROR",
741
+ output: error.message || "Failed to check main branch"
742
+ };
743
+ }
744
+ }
745
+ /**
746
+ * EP599: Execute get_commits command
747
+ * Returns commits for a branch with pushed/unpushed status
748
+ */
749
+ async executeGetCommits(command, cwd, options) {
750
+ const validation = (0, git_validator_1.validateBranchName)(command.branch);
751
+ if (!validation.valid) {
752
+ return {
753
+ success: false,
754
+ error: validation.error || "UNKNOWN_ERROR"
755
+ };
756
+ }
757
+ const limit = command.limit || 10;
758
+ const baseBranch = command.baseBranch || "main";
759
+ try {
760
+ let stdout;
761
+ try {
762
+ const result = await execAsync(`git log ${baseBranch}.."${command.branch}" --pretty=format:"%H|%an|%ae|%aI|%s" -n ${limit} --`, { cwd, timeout: options?.timeout || 1e4 });
763
+ stdout = result.stdout;
764
+ } catch (error) {
765
+ try {
766
+ const result = await execAsync(`git log "${command.branch}" --pretty=format:"%H|%an|%ae|%aI|%s" -n ${limit} --`, { cwd, timeout: options?.timeout || 1e4 });
767
+ stdout = result.stdout;
768
+ } catch (branchError) {
769
+ return {
770
+ success: false,
771
+ error: "BRANCH_NOT_FOUND",
772
+ output: `Branch ${command.branch} not found locally`
773
+ };
774
+ }
775
+ }
776
+ if (!stdout.trim()) {
777
+ return {
778
+ success: true,
779
+ output: "No commits found",
780
+ details: {
781
+ commits: []
782
+ }
783
+ };
784
+ }
785
+ const commitLines = stdout.trim().split("\n");
786
+ let remoteShas = /* @__PURE__ */ new Set();
787
+ try {
788
+ const { stdout: remoteCommits } = await execAsync(`git log "origin/${command.branch}" --pretty=format:"%H" -n ${limit} --`, { cwd, timeout: options?.timeout || 1e4 });
789
+ remoteShas = new Set(remoteCommits.trim().split("\n").filter(Boolean));
790
+ } catch {
791
+ }
792
+ const commits = commitLines.map((line) => {
793
+ const [sha, authorName, authorEmail, date, ...messageParts] = line.split("|");
794
+ const message = messageParts.join("|");
795
+ const isPushed = remoteShas.has(sha);
796
+ return {
797
+ sha,
798
+ message,
799
+ authorName,
800
+ authorEmail,
801
+ date,
802
+ isPushed
803
+ };
804
+ });
805
+ return {
806
+ success: true,
807
+ output: `Found ${commits.length} commits`,
808
+ details: {
809
+ commits
810
+ }
811
+ };
812
+ } catch (error) {
813
+ return {
814
+ success: false,
815
+ error: "UNKNOWN_ERROR",
816
+ output: error.message || "Failed to get commits"
817
+ };
818
+ }
819
+ }
820
+ // ========================================
821
+ // EP599: Advanced operations for move-to-module and discard-main-changes
822
+ // ========================================
823
+ /**
824
+ * Execute git stash operations
825
+ */
826
+ async executeStash(command, cwd, options) {
827
+ try {
828
+ const args = ["stash"];
829
+ switch (command.operation) {
830
+ case "push":
831
+ args.push("push");
832
+ if (command.includeUntracked) {
833
+ args.push("--include-untracked");
834
+ }
835
+ if (command.message) {
836
+ args.push("-m", command.message);
837
+ }
838
+ break;
839
+ case "pop":
840
+ args.push("pop");
841
+ break;
842
+ case "drop":
843
+ args.push("drop");
844
+ break;
845
+ case "list":
846
+ args.push("list");
847
+ break;
848
+ }
849
+ return await this.runGitCommand(args, cwd, options);
850
+ } catch (error) {
851
+ return {
852
+ success: false,
853
+ error: "UNKNOWN_ERROR",
854
+ output: error.message || "Stash operation failed"
855
+ };
856
+ }
857
+ }
858
+ /**
859
+ * Execute git reset
860
+ */
861
+ async executeReset(command, cwd, options) {
862
+ try {
863
+ const args = ["reset", `--${command.mode}`];
864
+ if (command.target) {
865
+ args.push(command.target);
866
+ }
867
+ return await this.runGitCommand(args, cwd, options);
868
+ } catch (error) {
869
+ return {
870
+ success: false,
871
+ error: "UNKNOWN_ERROR",
872
+ output: error.message || "Reset failed"
873
+ };
874
+ }
875
+ }
876
+ /**
877
+ * Execute git merge
878
+ */
879
+ async executeMerge(command, cwd, options) {
880
+ try {
881
+ if (command.abort) {
882
+ return await this.runGitCommand(["merge", "--abort"], cwd, options);
883
+ }
884
+ const branchValidation = (0, git_validator_1.validateBranchName)(command.branch);
885
+ if (!branchValidation.valid) {
886
+ return {
887
+ success: false,
888
+ error: "BRANCH_NOT_FOUND",
889
+ output: branchValidation.error || "Invalid branch name"
890
+ };
891
+ }
892
+ const args = ["merge", command.branch];
893
+ if (command.strategy === "ours") {
894
+ args.push("--strategy=ours");
895
+ } else if (command.strategy === "theirs") {
896
+ args.push("--strategy-option=theirs");
897
+ }
898
+ if (command.noEdit) {
899
+ args.push("--no-edit");
900
+ }
901
+ const result = await this.runGitCommand(args, cwd, options);
902
+ if (!result.success && result.output?.includes("CONFLICT")) {
903
+ result.details = result.details || {};
904
+ result.details.mergeConflicts = true;
905
+ }
906
+ return result;
907
+ } catch (error) {
908
+ return {
909
+ success: false,
910
+ error: "MERGE_CONFLICT",
911
+ output: error.message || "Merge failed"
912
+ };
913
+ }
914
+ }
915
+ /**
916
+ * Execute git cherry-pick
917
+ */
918
+ async executeCherryPick(command, cwd, options) {
919
+ try {
920
+ if (command.abort) {
921
+ return await this.runGitCommand(["cherry-pick", "--abort"], cwd, options);
922
+ }
923
+ const result = await this.runGitCommand(["cherry-pick", command.sha], cwd, options);
924
+ if (!result.success && result.output?.includes("CONFLICT")) {
925
+ result.details = result.details || {};
926
+ result.details.cherryPickConflicts = true;
927
+ }
928
+ return result;
929
+ } catch (error) {
930
+ return {
931
+ success: false,
932
+ error: "MERGE_CONFLICT",
933
+ output: error.message || "Cherry-pick failed"
934
+ };
935
+ }
936
+ }
937
+ /**
938
+ * Execute git clean
939
+ */
940
+ async executeClean(command, cwd, options) {
941
+ try {
942
+ const args = ["clean"];
943
+ if (command.force) {
944
+ args.push("-f");
945
+ }
946
+ if (command.directories) {
947
+ args.push("-d");
948
+ }
949
+ return await this.runGitCommand(args, cwd, options);
950
+ } catch (error) {
951
+ return {
952
+ success: false,
953
+ error: "UNKNOWN_ERROR",
954
+ output: error.message || "Clean failed"
955
+ };
956
+ }
957
+ }
958
+ /**
959
+ * Execute git add
960
+ */
961
+ async executeAdd(command, cwd, options) {
962
+ try {
963
+ const args = ["add"];
964
+ if (command.all) {
965
+ args.push("-A");
966
+ } else if (command.files && command.files.length > 0) {
967
+ const validation = (0, git_validator_1.validateFilePaths)(command.files);
968
+ if (!validation.valid) {
969
+ return {
970
+ success: false,
971
+ error: "UNKNOWN_ERROR",
972
+ output: validation.error || "Invalid file paths"
973
+ };
974
+ }
975
+ args.push(...command.files);
976
+ } else {
977
+ args.push("-A");
978
+ }
979
+ return await this.runGitCommand(args, cwd, options);
980
+ } catch (error) {
981
+ return {
982
+ success: false,
983
+ error: "UNKNOWN_ERROR",
984
+ output: error.message || "Add failed"
985
+ };
986
+ }
987
+ }
988
+ /**
989
+ * Execute git fetch
990
+ */
991
+ async executeFetch(command, cwd, options) {
992
+ try {
993
+ const args = ["fetch"];
994
+ if (command.remote) {
995
+ args.push(command.remote);
996
+ if (command.branch) {
997
+ args.push(command.branch);
998
+ }
999
+ } else {
1000
+ args.push("origin");
1001
+ }
1002
+ return await this.runGitCommand(args, cwd, options);
1003
+ } catch (error) {
1004
+ return {
1005
+ success: false,
1006
+ error: "NETWORK_ERROR",
1007
+ output: error.message || "Fetch failed"
1008
+ };
1009
+ }
1010
+ }
1011
+ // ========================================
1012
+ // EP599-3: Composite operations
1013
+ // ========================================
1014
+ /**
1015
+ * Execute move_to_module - composite operation
1016
+ * Moves commits/changes to a module branch with conflict handling
1017
+ */
1018
+ async executeMoveToModule(command, cwd, options) {
1019
+ const { targetBranch, commitShas, conflictResolution } = command;
1020
+ let hasStash = false;
1021
+ const cherryPickedCommits = [];
1022
+ try {
1023
+ const { stdout: currentBranchOut } = await execAsync("git rev-parse --abbrev-ref HEAD", { cwd });
1024
+ const currentBranch = currentBranchOut.trim();
1025
+ const { stdout: statusOutput } = await execAsync("git status --porcelain", { cwd });
1026
+ if (statusOutput.trim()) {
1027
+ try {
1028
+ await execAsync("git add -A", { cwd });
1029
+ const { stdout: stashHash } = await execAsync('git stash create -m "episoda-move-to-module"', { cwd });
1030
+ if (stashHash && stashHash.trim()) {
1031
+ await execAsync(`git stash store -m "episoda-move-to-module" ${stashHash.trim()}`, { cwd });
1032
+ await execAsync("git reset --hard HEAD", { cwd });
1033
+ hasStash = true;
1034
+ }
1035
+ } catch (stashError) {
1036
+ }
1037
+ }
1038
+ if (commitShas && commitShas.length > 0 && currentBranch !== "main" && currentBranch !== "master") {
1039
+ await execAsync("git checkout main", { cwd });
1040
+ }
1041
+ let branchExists = false;
1042
+ try {
1043
+ await execAsync(`git rev-parse --verify ${targetBranch}`, { cwd });
1044
+ branchExists = true;
1045
+ } catch {
1046
+ branchExists = false;
1047
+ }
1048
+ if (!branchExists) {
1049
+ await execAsync(`git checkout -b ${targetBranch}`, { cwd });
1050
+ } else {
1051
+ await execAsync(`git checkout ${targetBranch}`, { cwd });
1052
+ if (currentBranch === "main" || currentBranch === "master") {
1053
+ try {
1054
+ const mergeStrategy = conflictResolution === "ours" ? "--strategy=ours" : conflictResolution === "theirs" ? "--strategy-option=theirs" : "";
1055
+ await execAsync(`git merge ${currentBranch} ${mergeStrategy} --no-edit`, { cwd });
1056
+ } catch (mergeError) {
1057
+ const { stdout: conflictStatus } = await execAsync("git status --porcelain", { cwd });
1058
+ if (conflictStatus.includes("UU ") || conflictStatus.includes("AA ") || conflictStatus.includes("DD ")) {
1059
+ const { stdout: conflictFiles } = await execAsync("git diff --name-only --diff-filter=U", { cwd });
1060
+ const conflictedFiles = conflictFiles.trim().split("\n").filter(Boolean);
1061
+ if (conflictResolution) {
1062
+ for (const file of conflictedFiles) {
1063
+ await execAsync(`git checkout --${conflictResolution} "${file}"`, { cwd });
1064
+ await execAsync(`git add "${file}"`, { cwd });
1065
+ }
1066
+ await execAsync("git commit --no-edit", { cwd });
1067
+ } else {
1068
+ await execAsync("git merge --abort", { cwd });
1069
+ return {
1070
+ success: false,
1071
+ error: "MERGE_CONFLICT",
1072
+ output: "Merge conflicts detected",
1073
+ details: {
1074
+ hasConflicts: true,
1075
+ conflictedFiles,
1076
+ movedToBranch: targetBranch
1077
+ }
1078
+ };
1079
+ }
1080
+ }
1081
+ }
1082
+ }
1083
+ }
1084
+ if (commitShas && commitShas.length > 0 && (currentBranch === "main" || currentBranch === "master")) {
1085
+ for (const sha of commitShas) {
1086
+ try {
1087
+ const { stdout: logOutput } = await execAsync(`git log --format=%H ${targetBranch} | grep ${sha}`, { cwd }).catch(() => ({ stdout: "" }));
1088
+ if (!logOutput.trim()) {
1089
+ await execAsync(`git cherry-pick ${sha}`, { cwd });
1090
+ cherryPickedCommits.push(sha);
1091
+ }
1092
+ } catch (err) {
1093
+ await execAsync("git cherry-pick --abort", { cwd }).catch(() => {
1094
+ });
1095
+ }
1096
+ }
1097
+ await execAsync("git checkout main", { cwd });
1098
+ await execAsync("git reset --hard origin/main", { cwd });
1099
+ await execAsync(`git checkout ${targetBranch}`, { cwd });
1100
+ }
1101
+ if (hasStash) {
1102
+ try {
1103
+ await execAsync("git stash pop", { cwd });
1104
+ } catch (stashError) {
1105
+ const { stdout: conflictStatus } = await execAsync("git status --porcelain", { cwd });
1106
+ if (conflictStatus.includes("UU ") || conflictStatus.includes("AA ")) {
1107
+ if (conflictResolution) {
1108
+ const { stdout: conflictFiles } = await execAsync("git diff --name-only --diff-filter=U", { cwd });
1109
+ const conflictedFiles = conflictFiles.trim().split("\n").filter(Boolean);
1110
+ for (const file of conflictedFiles) {
1111
+ await execAsync(`git checkout --${conflictResolution} "${file}"`, { cwd });
1112
+ await execAsync(`git add "${file}"`, { cwd });
1113
+ }
1114
+ }
1115
+ }
1116
+ }
1117
+ }
1118
+ return {
1119
+ success: true,
1120
+ output: `Successfully moved to branch ${targetBranch}`,
1121
+ details: {
1122
+ movedToBranch: targetBranch,
1123
+ cherryPickedCommits,
1124
+ currentBranch: targetBranch
1125
+ }
1126
+ };
1127
+ } catch (error) {
1128
+ if (hasStash) {
1129
+ try {
1130
+ const { stdout: stashList } = await execAsync("git stash list", { cwd });
1131
+ if (stashList.includes("episoda-move-to-module")) {
1132
+ await execAsync("git stash pop", { cwd });
1133
+ }
1134
+ } catch (e) {
1135
+ }
1136
+ }
1137
+ return {
1138
+ success: false,
1139
+ error: "UNKNOWN_ERROR",
1140
+ output: error.message || "Move to module failed"
1141
+ };
1142
+ }
1143
+ }
1144
+ /**
1145
+ * Execute discard_main_changes - composite operation
1146
+ * Discards all uncommitted files and local commits on main branch
1147
+ */
1148
+ async executeDiscardMainChanges(cwd, options) {
1149
+ try {
1150
+ const { stdout: currentBranchOut } = await execAsync("git rev-parse --abbrev-ref HEAD", { cwd });
1151
+ const branch = currentBranchOut.trim();
1152
+ if (branch !== "main" && branch !== "master") {
1153
+ return {
1154
+ success: false,
1155
+ error: "BRANCH_NOT_FOUND",
1156
+ output: `Cannot discard changes - not on main branch. Current branch: ${branch}`
1157
+ };
1158
+ }
1159
+ let discardedFiles = 0;
1160
+ const { stdout: statusOutput } = await execAsync("git status --porcelain", { cwd });
1161
+ if (statusOutput.trim()) {
1162
+ try {
1163
+ await execAsync("git stash --include-untracked", { cwd });
1164
+ discardedFiles = statusOutput.trim().split("\n").length;
1165
+ } catch (stashError) {
1166
+ }
1167
+ }
1168
+ await execAsync("git fetch origin", { cwd });
1169
+ await execAsync(`git reset --hard origin/${branch}`, { cwd });
1170
+ try {
1171
+ await execAsync("git clean -fd", { cwd });
1172
+ } catch (cleanError) {
1173
+ }
1174
+ try {
1175
+ await execAsync("git stash drop", { cwd });
1176
+ } catch (dropError) {
1177
+ }
1178
+ return {
1179
+ success: true,
1180
+ output: `Successfully discarded all changes and reset to origin/${branch}`,
1181
+ details: {
1182
+ currentBranch: branch,
1183
+ discardedFiles
1184
+ }
1185
+ };
1186
+ } catch (error) {
1187
+ return {
1188
+ success: false,
1189
+ error: "UNKNOWN_ERROR",
1190
+ output: error.message || "Discard main changes failed"
1191
+ };
1192
+ }
1193
+ }
1194
+ // ========================================
1195
+ // EP523: Branch sync operations
1196
+ // ========================================
1197
+ /**
1198
+ * EP523: Get sync status of a branch relative to main
1199
+ * Returns how many commits behind/ahead the branch is
1200
+ */
1201
+ async executeSyncStatus(command, cwd, options) {
1202
+ try {
1203
+ const validation = (0, git_validator_1.validateBranchName)(command.branch);
1204
+ if (!validation.valid) {
1205
+ return {
1206
+ success: false,
1207
+ error: validation.error || "UNKNOWN_ERROR"
1208
+ };
1209
+ }
1210
+ try {
1211
+ await execAsync("git fetch origin", { cwd, timeout: options?.timeout || 3e4 });
1212
+ } catch (fetchError) {
1213
+ return {
1214
+ success: false,
1215
+ error: "NETWORK_ERROR",
1216
+ output: "Unable to fetch from remote. Check your network connection."
1217
+ };
1218
+ }
1219
+ let commitsBehind = 0;
1220
+ let commitsAhead = 0;
1221
+ try {
1222
+ const { stdout: behindOutput } = await execAsync(`git rev-list --count ${command.branch}..origin/main`, { cwd, timeout: options?.timeout || 1e4 });
1223
+ commitsBehind = parseInt(behindOutput.trim(), 10) || 0;
1224
+ } catch {
1225
+ commitsBehind = 0;
1226
+ }
1227
+ try {
1228
+ const { stdout: aheadOutput } = await execAsync(`git rev-list --count origin/main..${command.branch}`, { cwd, timeout: options?.timeout || 1e4 });
1229
+ commitsAhead = parseInt(aheadOutput.trim(), 10) || 0;
1230
+ } catch {
1231
+ commitsAhead = 0;
1232
+ }
1233
+ const isBehind = commitsBehind > 0;
1234
+ const isAhead = commitsAhead > 0;
1235
+ const needsSync = isBehind;
1236
+ return {
1237
+ success: true,
1238
+ output: isBehind ? `Branch ${command.branch} is ${commitsBehind} commit(s) behind main` : `Branch ${command.branch} is up to date with main`,
1239
+ details: {
1240
+ branchName: command.branch,
1241
+ commitsBehind,
1242
+ commitsAhead,
1243
+ isBehind,
1244
+ isAhead,
1245
+ needsSync
1246
+ }
1247
+ };
1248
+ } catch (error) {
1249
+ return {
1250
+ success: false,
1251
+ error: "UNKNOWN_ERROR",
1252
+ output: error.message || "Failed to check sync status"
1253
+ };
1254
+ }
1255
+ }
1256
+ /**
1257
+ * EP523: Sync local main branch with remote
1258
+ * Used before creating new branches to ensure we branch from latest main
1259
+ */
1260
+ async executeSyncMain(cwd, options) {
1261
+ try {
1262
+ let currentBranch = "";
1263
+ try {
1264
+ const { stdout } = await execAsync("git branch --show-current", { cwd, timeout: 5e3 });
1265
+ currentBranch = stdout.trim();
1266
+ } catch {
1267
+ }
1268
+ try {
1269
+ await execAsync("git fetch origin main", { cwd, timeout: options?.timeout || 3e4 });
1270
+ } catch (fetchError) {
1271
+ return {
1272
+ success: false,
1273
+ error: "NETWORK_ERROR",
1274
+ output: "Unable to fetch from remote. Check your network connection."
1275
+ };
1276
+ }
1277
+ const needsSwitch = currentBranch !== "main" && currentBranch !== "";
1278
+ if (needsSwitch) {
1279
+ const { stdout: statusOutput } = await execAsync("git status --porcelain", { cwd, timeout: 5e3 });
1280
+ if (statusOutput.trim()) {
1281
+ return {
1282
+ success: false,
1283
+ error: "UNCOMMITTED_CHANGES",
1284
+ output: "Cannot sync main: you have uncommitted changes. Commit or stash them first.",
1285
+ details: {
1286
+ uncommittedFiles: statusOutput.trim().split("\n").map((line) => line.slice(3))
1287
+ }
1288
+ };
1289
+ }
1290
+ await execAsync("git checkout main", { cwd, timeout: options?.timeout || 1e4 });
1291
+ }
1292
+ try {
1293
+ await execAsync("git pull origin main", { cwd, timeout: options?.timeout || 3e4 });
1294
+ } catch (pullError) {
1295
+ if (pullError.message?.includes("CONFLICT") || pullError.stderr?.includes("CONFLICT")) {
1296
+ await execAsync("git merge --abort", { cwd, timeout: 5e3 }).catch(() => {
1297
+ });
1298
+ return {
1299
+ success: false,
1300
+ error: "MERGE_CONFLICT",
1301
+ output: "Conflict while syncing main. This is unexpected - main should not have local commits."
1302
+ };
1303
+ }
1304
+ throw pullError;
1305
+ }
1306
+ if (needsSwitch && currentBranch) {
1307
+ await execAsync(`git checkout "${currentBranch}"`, { cwd, timeout: options?.timeout || 1e4 });
1308
+ }
1309
+ return {
1310
+ success: true,
1311
+ output: "Successfully synced main with remote",
1312
+ details: {
1313
+ currentBranch: needsSwitch ? currentBranch : "main"
1314
+ }
1315
+ };
1316
+ } catch (error) {
1317
+ return {
1318
+ success: false,
1319
+ error: "UNKNOWN_ERROR",
1320
+ output: error.message || "Failed to sync main"
1321
+ };
1322
+ }
1323
+ }
1324
+ /**
1325
+ * EP523: Rebase a branch onto main
1326
+ * Used when resuming work on a branch that's behind main
1327
+ */
1328
+ async executeRebaseBranch(command, cwd, options) {
1329
+ try {
1330
+ const validation = (0, git_validator_1.validateBranchName)(command.branch);
1331
+ if (!validation.valid) {
1332
+ return {
1333
+ success: false,
1334
+ error: validation.error || "UNKNOWN_ERROR"
1335
+ };
1336
+ }
1337
+ const { stdout: statusOutput } = await execAsync("git status --porcelain", { cwd, timeout: 5e3 });
1338
+ if (statusOutput.trim()) {
1339
+ return {
1340
+ success: false,
1341
+ error: "UNCOMMITTED_CHANGES",
1342
+ output: "Cannot rebase: you have uncommitted changes. Commit or stash them first.",
1343
+ details: {
1344
+ uncommittedFiles: statusOutput.trim().split("\n").map((line) => line.slice(3))
1345
+ }
1346
+ };
1347
+ }
1348
+ const { stdout: currentBranchOut } = await execAsync("git branch --show-current", { cwd, timeout: 5e3 });
1349
+ const currentBranch = currentBranchOut.trim();
1350
+ if (currentBranch !== command.branch) {
1351
+ await execAsync(`git checkout "${command.branch}"`, { cwd, timeout: options?.timeout || 1e4 });
1352
+ }
1353
+ await execAsync("git fetch origin main", { cwd, timeout: options?.timeout || 3e4 });
1354
+ try {
1355
+ await execAsync("git rebase origin/main", { cwd, timeout: options?.timeout || 6e4 });
1356
+ } catch (rebaseError) {
1357
+ const errorOutput = (rebaseError.stderr || "") + (rebaseError.stdout || "");
1358
+ if (errorOutput.includes("CONFLICT") || errorOutput.includes("could not apply")) {
1359
+ let conflictFiles = [];
1360
+ try {
1361
+ const { stdout: conflictOutput } = await execAsync("git diff --name-only --diff-filter=U", { cwd, timeout: 5e3 });
1362
+ conflictFiles = conflictOutput.trim().split("\n").filter(Boolean);
1363
+ } catch {
1364
+ try {
1365
+ const { stdout: statusOut } = await execAsync("git status --porcelain", { cwd, timeout: 5e3 });
1366
+ conflictFiles = statusOut.trim().split("\n").filter((line) => line.startsWith("UU ") || line.startsWith("AA ") || line.startsWith("DD ")).map((line) => line.slice(3));
1367
+ } catch {
1368
+ }
1369
+ }
1370
+ return {
1371
+ success: false,
1372
+ error: "REBASE_CONFLICT",
1373
+ output: `Rebase conflict in ${conflictFiles.length} file(s). Resolve conflicts then use rebase_continue, or use rebase_abort to cancel.`,
1374
+ details: {
1375
+ inRebase: true,
1376
+ rebaseConflicts: conflictFiles,
1377
+ hasConflicts: true,
1378
+ conflictedFiles: conflictFiles
1379
+ }
1380
+ };
1381
+ }
1382
+ throw rebaseError;
1383
+ }
1384
+ return {
1385
+ success: true,
1386
+ output: `Successfully rebased ${command.branch} onto main`,
1387
+ details: {
1388
+ branchName: command.branch,
1389
+ inRebase: false
1390
+ }
1391
+ };
1392
+ } catch (error) {
1393
+ return {
1394
+ success: false,
1395
+ error: "UNKNOWN_ERROR",
1396
+ output: error.message || "Rebase failed"
1397
+ };
1398
+ }
1399
+ }
1400
+ /**
1401
+ * EP523: Abort an in-progress rebase
1402
+ * Returns to the state before rebase was started
1403
+ */
1404
+ async executeRebaseAbort(cwd, options) {
1405
+ try {
1406
+ await execAsync("git rebase --abort", { cwd, timeout: options?.timeout || 1e4 });
1407
+ return {
1408
+ success: true,
1409
+ output: "Rebase aborted. Your branch has been restored to its previous state.",
1410
+ details: {
1411
+ inRebase: false
1412
+ }
1413
+ };
1414
+ } catch (error) {
1415
+ if (error.message?.includes("No rebase in progress") || error.stderr?.includes("No rebase in progress")) {
1416
+ return {
1417
+ success: true,
1418
+ output: "No rebase in progress.",
1419
+ details: {
1420
+ inRebase: false
1421
+ }
1422
+ };
1423
+ }
1424
+ return {
1425
+ success: false,
1426
+ error: "UNKNOWN_ERROR",
1427
+ output: error.message || "Failed to abort rebase"
1428
+ };
1429
+ }
1430
+ }
1431
+ /**
1432
+ * EP523: Continue a paused rebase after conflicts are resolved
1433
+ */
1434
+ async executeRebaseContinue(cwd, options) {
1435
+ try {
1436
+ await execAsync("git add -A", { cwd, timeout: 5e3 });
1437
+ await execAsync("git rebase --continue", { cwd, timeout: options?.timeout || 6e4 });
1438
+ return {
1439
+ success: true,
1440
+ output: "Rebase continued successfully.",
1441
+ details: {
1442
+ inRebase: false
1443
+ }
1444
+ };
1445
+ } catch (error) {
1446
+ const errorOutput = (error.stderr || "") + (error.stdout || "");
1447
+ if (errorOutput.includes("CONFLICT") || errorOutput.includes("could not apply")) {
1448
+ let conflictFiles = [];
1449
+ try {
1450
+ const { stdout: conflictOutput } = await execAsync("git diff --name-only --diff-filter=U", { cwd, timeout: 5e3 });
1451
+ conflictFiles = conflictOutput.trim().split("\n").filter(Boolean);
1452
+ } catch {
1453
+ }
1454
+ return {
1455
+ success: false,
1456
+ error: "REBASE_CONFLICT",
1457
+ output: "More conflicts encountered. Resolve them and try again.",
1458
+ details: {
1459
+ inRebase: true,
1460
+ rebaseConflicts: conflictFiles,
1461
+ hasConflicts: true,
1462
+ conflictedFiles: conflictFiles
1463
+ }
1464
+ };
1465
+ }
1466
+ if (errorOutput.includes("No rebase in progress")) {
1467
+ return {
1468
+ success: true,
1469
+ output: "No rebase in progress.",
1470
+ details: {
1471
+ inRebase: false
1472
+ }
1473
+ };
1474
+ }
1475
+ return {
1476
+ success: false,
1477
+ error: "UNKNOWN_ERROR",
1478
+ output: error.message || "Failed to continue rebase"
1479
+ };
1480
+ }
1481
+ }
1482
+ /**
1483
+ * EP523: Check if a rebase is currently in progress
1484
+ */
1485
+ async executeRebaseStatus(cwd, options) {
1486
+ try {
1487
+ let inRebase = false;
1488
+ let rebaseConflicts = [];
1489
+ try {
1490
+ const { stdout: gitDir } = await execAsync("git rev-parse --git-dir", { cwd, timeout: 5e3 });
1491
+ const gitDirPath = gitDir.trim();
1492
+ const fs6 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1493
+ const rebaseMergePath = `${gitDirPath}/rebase-merge`;
1494
+ const rebaseApplyPath = `${gitDirPath}/rebase-apply`;
1495
+ try {
1496
+ await fs6.access(rebaseMergePath);
1497
+ inRebase = true;
1498
+ } catch {
1499
+ try {
1500
+ await fs6.access(rebaseApplyPath);
1501
+ inRebase = true;
1502
+ } catch {
1503
+ inRebase = false;
1504
+ }
1505
+ }
1506
+ } catch {
1507
+ try {
1508
+ const { stdout: statusOutput } = await execAsync("git status", { cwd, timeout: 5e3 });
1509
+ inRebase = statusOutput.includes("rebase in progress") || statusOutput.includes("interactive rebase in progress") || statusOutput.includes("You are currently rebasing");
1510
+ } catch {
1511
+ inRebase = false;
1512
+ }
1513
+ }
1514
+ if (inRebase) {
1515
+ try {
1516
+ const { stdout: conflictOutput } = await execAsync("git diff --name-only --diff-filter=U", { cwd, timeout: 5e3 });
1517
+ rebaseConflicts = conflictOutput.trim().split("\n").filter(Boolean);
1518
+ } catch {
1519
+ }
1520
+ }
1521
+ return {
1522
+ success: true,
1523
+ output: inRebase ? `Rebase in progress with ${rebaseConflicts.length} conflicting file(s)` : "No rebase in progress",
1524
+ details: {
1525
+ inRebase,
1526
+ rebaseConflicts,
1527
+ hasConflicts: rebaseConflicts.length > 0
1528
+ }
1529
+ };
1530
+ } catch (error) {
1531
+ return {
1532
+ success: false,
1533
+ error: "UNKNOWN_ERROR",
1534
+ output: error.message || "Failed to check rebase status"
1535
+ };
1536
+ }
1537
+ }
1538
+ /**
1539
+ * Run a git command and return structured result
1540
+ */
1541
+ async runGitCommand(args, cwd, options) {
1542
+ try {
1543
+ const sanitizedArgs = (0, git_validator_1.sanitizeArgs)(args);
1544
+ const command = ["git", ...sanitizedArgs].join(" ");
1545
+ const timeout = options?.timeout || 3e4;
1546
+ const execOptions = {
1547
+ cwd,
1548
+ timeout,
1549
+ env: options?.env || process.env,
1550
+ maxBuffer: 1024 * 1024 * 10
1551
+ // 10MB buffer
1552
+ };
1553
+ const { stdout, stderr } = await execAsync(command, execOptions);
1554
+ const output = (stdout + stderr).trim();
1555
+ const details = {};
1556
+ const branchName = (0, git_parser_1.extractBranchName)(output);
1557
+ if (branchName) {
1558
+ details.branchName = branchName;
1559
+ }
1560
+ if ((0, git_parser_1.isDetachedHead)(output)) {
1561
+ details.branchName = "HEAD (detached)";
1562
+ }
1563
+ return {
1564
+ success: true,
1565
+ output,
1566
+ details: Object.keys(details).length > 0 ? details : void 0
1567
+ };
1568
+ } catch (error) {
1569
+ const stderr = error.stderr || "";
1570
+ const stdout = error.stdout || "";
1571
+ const exitCode = error.code || 1;
1572
+ const errorCode = (0, git_parser_1.parseGitError)(stderr, stdout, exitCode);
1573
+ const details = {
1574
+ exitCode
1575
+ };
1576
+ if (errorCode === "MERGE_CONFLICT") {
1577
+ const conflicts = (0, git_parser_1.parseMergeConflicts)(stdout + stderr);
1578
+ if (conflicts.length > 0) {
1579
+ details.conflictingFiles = conflicts;
1580
+ }
1581
+ }
1582
+ if (errorCode === "UNCOMMITTED_CHANGES") {
1583
+ try {
1584
+ const statusResult = await this.executeStatus(cwd, options);
1585
+ if (statusResult.details?.uncommittedFiles) {
1586
+ details.uncommittedFiles = statusResult.details.uncommittedFiles;
1587
+ }
1588
+ } catch {
1589
+ }
1590
+ }
1591
+ return {
1592
+ success: false,
1593
+ error: errorCode,
1594
+ output: (stdout + stderr).trim(),
1595
+ details
1596
+ };
1597
+ }
1598
+ }
1599
+ /**
1600
+ * Validate that git is installed
1601
+ */
1602
+ async validateGitInstalled() {
1603
+ try {
1604
+ await execAsync("git --version", { timeout: 5e3 });
1605
+ return true;
1606
+ } catch {
1607
+ return false;
1608
+ }
1609
+ }
1610
+ /**
1611
+ * Check if directory is a git repository
1612
+ */
1613
+ async isGitRepository(cwd) {
1614
+ try {
1615
+ await execAsync("git rev-parse --git-dir", { cwd, timeout: 5e3 });
1616
+ return true;
1617
+ } catch {
1618
+ return false;
1619
+ }
1620
+ }
1621
+ /**
1622
+ * Detect the git working directory (repository root)
1623
+ * @returns Path to the git repository root
1624
+ */
1625
+ async detectWorkingDirectory(startPath) {
1626
+ try {
1627
+ const { stdout } = await execAsync("git rev-parse --show-toplevel", {
1628
+ cwd: startPath || process.cwd(),
1629
+ timeout: 5e3
1630
+ });
1631
+ return stdout.trim();
1632
+ } catch {
1633
+ return null;
1634
+ }
1635
+ }
1636
+ };
1637
+ exports2.GitExecutor = GitExecutor2;
1638
+ }
1639
+ });
1640
+
1641
+ // ../core/dist/version.js
1642
+ var require_version = __commonJS({
1643
+ "../core/dist/version.js"(exports2) {
1644
+ "use strict";
1645
+ Object.defineProperty(exports2, "__esModule", { value: true });
1646
+ exports2.VERSION = void 0;
1647
+ var fs_1 = require("fs");
1648
+ var path_1 = require("path");
1649
+ var packageJsonPath = (0, path_1.join)(__dirname, "..", "package.json");
1650
+ var packageJson2 = JSON.parse((0, fs_1.readFileSync)(packageJsonPath, "utf-8"));
1651
+ exports2.VERSION = packageJson2.version;
1652
+ }
1653
+ });
1654
+
1655
+ // ../core/dist/websocket-client.js
1656
+ var require_websocket_client = __commonJS({
1657
+ "../core/dist/websocket-client.js"(exports2) {
1658
+ "use strict";
1659
+ var __importDefault = exports2 && exports2.__importDefault || function(mod) {
1660
+ return mod && mod.__esModule ? mod : { "default": mod };
1661
+ };
1662
+ Object.defineProperty(exports2, "__esModule", { value: true });
1663
+ exports2.EpisodaClient = void 0;
1664
+ var ws_1 = __importDefault(require("ws"));
1665
+ var https_1 = __importDefault(require("https"));
1666
+ var version_1 = require_version();
1667
+ var ipv4Agent = new https_1.default.Agent({ family: 4 });
1668
+ var INITIAL_RECONNECT_DELAY = 1e3;
1669
+ var MAX_RECONNECT_DELAY = 6e4;
1670
+ var IDLE_RECONNECT_DELAY = 6e5;
1671
+ var MAX_RETRY_DURATION = 6 * 60 * 60 * 1e3;
1672
+ var IDLE_THRESHOLD = 60 * 60 * 1e3;
1673
+ var RAPID_CLOSE_THRESHOLD = 2e3;
1674
+ var RAPID_CLOSE_BACKOFF = 3e4;
1675
+ var CLIENT_HEARTBEAT_INTERVAL = 45e3;
1676
+ var CLIENT_HEARTBEAT_TIMEOUT = 15e3;
1677
+ var CONNECTION_TIMEOUT = 15e3;
1678
+ var EpisodaClient2 = class {
1679
+ constructor() {
1680
+ this.eventHandlers = /* @__PURE__ */ new Map();
1681
+ this.reconnectAttempts = 0;
1682
+ this.url = "";
1683
+ this.token = "";
1684
+ this.isConnected = false;
1685
+ this.isDisconnecting = false;
1686
+ this.isGracefulShutdown = false;
1687
+ this.lastCommandTime = Date.now();
1688
+ this.isIntentionalDisconnect = false;
1689
+ this.lastConnectAttemptTime = 0;
1690
+ }
1691
+ /**
1692
+ * Connect to episoda.dev WebSocket gateway
1693
+ * @param url - WebSocket URL (wss://episoda.dev/cli)
1694
+ * @param token - OAuth access token
1695
+ * @param machineId - Optional machine identifier for multi-machine support
1696
+ * @param deviceInfo - Optional device information (hostname, OS, daemonPid)
1697
+ */
1698
+ async connect(url, token, machineId, deviceInfo) {
1699
+ this.url = url;
1700
+ this.token = token;
1701
+ this.machineId = machineId;
1702
+ this.hostname = deviceInfo?.hostname;
1703
+ this.osPlatform = deviceInfo?.osPlatform;
1704
+ this.osArch = deviceInfo?.osArch;
1705
+ this.daemonPid = deviceInfo?.daemonPid;
1706
+ this.isDisconnecting = false;
1707
+ this.isGracefulShutdown = false;
1708
+ this.isIntentionalDisconnect = false;
1709
+ this.lastConnectAttemptTime = Date.now();
1710
+ this.lastErrorCode = void 0;
1711
+ return new Promise((resolve2, reject) => {
1712
+ const connectionTimeout = setTimeout(() => {
1713
+ if (this.ws) {
1714
+ this.ws.terminate();
1715
+ }
1716
+ reject(new Error(`Connection timeout after ${CONNECTION_TIMEOUT / 1e3}s - server may be unreachable`));
1717
+ }, CONNECTION_TIMEOUT);
1718
+ try {
1719
+ this.ws = new ws_1.default(url, { agent: ipv4Agent });
1720
+ this.ws.on("open", () => {
1721
+ clearTimeout(connectionTimeout);
1722
+ console.log("[EpisodaClient] WebSocket connected");
1723
+ this.isConnected = true;
1724
+ this.reconnectAttempts = 0;
1725
+ this.firstDisconnectTime = void 0;
1726
+ this.lastCommandTime = Date.now();
1727
+ this.send({
1728
+ type: "auth",
1729
+ token,
1730
+ version: version_1.VERSION,
1731
+ machineId,
1732
+ hostname: this.hostname,
1733
+ osPlatform: this.osPlatform,
1734
+ osArch: this.osArch,
1735
+ daemonPid: this.daemonPid
1736
+ });
1737
+ this.startHeartbeat();
1738
+ resolve2();
1739
+ });
1740
+ this.ws.on("pong", () => {
1741
+ if (this.heartbeatTimeoutTimer) {
1742
+ clearTimeout(this.heartbeatTimeoutTimer);
1743
+ this.heartbeatTimeoutTimer = void 0;
1744
+ }
1745
+ });
1746
+ this.ws.on("message", (data) => {
1747
+ try {
1748
+ const message = JSON.parse(data.toString());
1749
+ this.handleMessage(message);
1750
+ } catch (error) {
1751
+ console.error("[EpisodaClient] Failed to parse message:", error);
1752
+ }
1753
+ });
1754
+ this.ws.on("ping", () => {
1755
+ console.log("[EpisodaClient] Received ping from server");
1756
+ });
1757
+ this.ws.on("close", (code, reason) => {
1758
+ console.log(`[EpisodaClient] WebSocket closed: ${code} ${reason.toString()}`);
1759
+ this.isConnected = false;
1760
+ const willReconnect = !this.isDisconnecting;
1761
+ this.emit({
1762
+ type: "disconnected",
1763
+ code,
1764
+ reason: reason.toString(),
1765
+ willReconnect
1766
+ });
1767
+ if (willReconnect) {
1768
+ this.scheduleReconnect();
1769
+ }
1770
+ });
1771
+ this.ws.on("error", (error) => {
1772
+ console.error("[EpisodaClient] WebSocket error:", error);
1773
+ if (!this.isConnected) {
1774
+ clearTimeout(connectionTimeout);
1775
+ reject(error);
1776
+ }
1777
+ });
1778
+ } catch (error) {
1779
+ clearTimeout(connectionTimeout);
1780
+ reject(error);
1781
+ }
1782
+ });
1783
+ }
1784
+ /**
1785
+ * Disconnect from the server
1786
+ * @param intentional - If true, prevents automatic reconnection (user-initiated disconnect)
1787
+ */
1788
+ async disconnect(intentional = true) {
1789
+ this.isDisconnecting = true;
1790
+ this.isIntentionalDisconnect = intentional;
1791
+ if (this.reconnectTimeout) {
1792
+ clearTimeout(this.reconnectTimeout);
1793
+ this.reconnectTimeout = void 0;
1794
+ }
1795
+ if (this.heartbeatTimer) {
1796
+ clearInterval(this.heartbeatTimer);
1797
+ this.heartbeatTimer = void 0;
1798
+ }
1799
+ if (this.heartbeatTimeoutTimer) {
1800
+ clearTimeout(this.heartbeatTimeoutTimer);
1801
+ this.heartbeatTimeoutTimer = void 0;
1802
+ }
1803
+ if (this.ws) {
1804
+ this.ws.close();
1805
+ this.ws = void 0;
1806
+ }
1807
+ this.isConnected = false;
1808
+ }
1809
+ /**
1810
+ * Register an event handler
1811
+ * @param event - Event type ('command', 'ping', 'error', 'auth_success')
1812
+ * @param handler - Handler function
1813
+ */
1814
+ on(event, handler) {
1815
+ if (!this.eventHandlers.has(event)) {
1816
+ this.eventHandlers.set(event, []);
1817
+ }
1818
+ this.eventHandlers.get(event).push(handler);
1819
+ }
1820
+ /**
1821
+ * Send a message to the server
1822
+ * @param message - Client message to send
1823
+ */
1824
+ async send(message) {
1825
+ if (!this.ws || !this.isConnected) {
1826
+ throw new Error("WebSocket not connected");
1827
+ }
1828
+ return new Promise((resolve2, reject) => {
1829
+ this.ws.send(JSON.stringify(message), (error) => {
1830
+ if (error) {
1831
+ console.error("[EpisodaClient] Failed to send message:", error);
1832
+ reject(error);
1833
+ } else {
1834
+ resolve2();
1835
+ }
1836
+ });
1837
+ });
1838
+ }
1839
+ /**
1840
+ * Get current connection status
1841
+ */
1842
+ getStatus() {
1843
+ return {
1844
+ connected: this.isConnected
1845
+ };
1846
+ }
1847
+ /**
1848
+ * EP605: Update last command time to reset idle detection
1849
+ * Call this when a command is received/executed
1850
+ */
1851
+ updateActivity() {
1852
+ this.lastCommandTime = Date.now();
1853
+ }
1854
+ /**
1855
+ * EP701: Emit a client-side event to registered handlers
1856
+ * Used for events like 'disconnected' that originate from the client, not server
1857
+ */
1858
+ emit(event) {
1859
+ const handlers = this.eventHandlers.get(event.type) || [];
1860
+ handlers.forEach((handler) => {
1861
+ try {
1862
+ handler(event);
1863
+ } catch (error) {
1864
+ console.error(`[EpisodaClient] Handler error for ${event.type}:`, error);
1865
+ }
1866
+ });
1867
+ }
1868
+ /**
1869
+ * Handle incoming message from server
1870
+ */
1871
+ handleMessage(message) {
1872
+ if (message.type === "shutdown") {
1873
+ console.log("[EpisodaClient] Received graceful shutdown message from server");
1874
+ this.isGracefulShutdown = true;
1875
+ }
1876
+ if (message.type === "error") {
1877
+ const errorMessage = message;
1878
+ this.lastErrorCode = errorMessage.code;
1879
+ if (errorMessage.code === "RATE_LIMITED" || errorMessage.code === "TOO_SOON") {
1880
+ const defaultRetry = errorMessage.code === "RATE_LIMITED" ? 60 : 5;
1881
+ const retryAfterMs = (errorMessage.retryAfter || defaultRetry) * 1e3;
1882
+ this.rateLimitBackoffUntil = Date.now() + retryAfterMs;
1883
+ console.log(`[EpisodaClient] ${errorMessage.code}: will retry after ${retryAfterMs / 1e3}s`);
1884
+ }
1885
+ }
1886
+ const handlers = this.eventHandlers.get(message.type) || [];
1887
+ handlers.forEach((handler) => {
1888
+ try {
1889
+ handler(message);
1890
+ } catch (error) {
1891
+ console.error(`[EpisodaClient] Handler error for ${message.type}:`, error);
1892
+ }
1893
+ });
1894
+ }
1895
+ /**
1896
+ * Schedule reconnection with exponential backoff and randomization
1897
+ *
1898
+ * EP605: Conservative reconnection with multiple safeguards:
1899
+ * - 6-hour maximum retry duration to prevent indefinite retrying
1900
+ * - Activity-based backoff (10 min delay if idle for 1+ hour)
1901
+ * - No reconnection after intentional disconnect
1902
+ * - Randomization to prevent thundering herd
1903
+ *
1904
+ * EP648: Additional protections against reconnection loops:
1905
+ * - Rate limit awareness: respect server's RATE_LIMITED response
1906
+ * - Rapid close detection: if connection closes within 2s, apply longer backoff
1907
+ */
1908
+ scheduleReconnect() {
1909
+ if (this.isIntentionalDisconnect) {
1910
+ console.log("[EpisodaClient] Intentional disconnect - not reconnecting");
1911
+ return;
1912
+ }
1913
+ if (this.heartbeatTimer) {
1914
+ clearInterval(this.heartbeatTimer);
1915
+ this.heartbeatTimer = void 0;
1916
+ }
1917
+ if (this.heartbeatTimeoutTimer) {
1918
+ clearTimeout(this.heartbeatTimeoutTimer);
1919
+ this.heartbeatTimeoutTimer = void 0;
1920
+ }
1921
+ if (!this.firstDisconnectTime) {
1922
+ this.firstDisconnectTime = Date.now();
1923
+ }
1924
+ const retryDuration = Date.now() - this.firstDisconnectTime;
1925
+ if (retryDuration >= MAX_RETRY_DURATION) {
1926
+ console.error(`[EpisodaClient] Maximum retry duration (6 hours) exceeded, giving up. Please restart the CLI.`);
1927
+ return;
1928
+ }
1929
+ if (this.reconnectAttempts > 0 && this.reconnectAttempts % 10 === 0) {
1930
+ const hoursRemaining = ((MAX_RETRY_DURATION - retryDuration) / (60 * 60 * 1e3)).toFixed(1);
1931
+ console.log(`[EpisodaClient] Still attempting to reconnect (attempt ${this.reconnectAttempts}, ${hoursRemaining}h remaining)...`);
1932
+ }
1933
+ if (this.rateLimitBackoffUntil && Date.now() < this.rateLimitBackoffUntil) {
1934
+ const waitTime = this.rateLimitBackoffUntil - Date.now();
1935
+ console.log(`[EpisodaClient] Rate limited, waiting ${Math.round(waitTime / 1e3)}s before retry`);
1936
+ this.reconnectAttempts++;
1937
+ this.reconnectTimeout = setTimeout(() => {
1938
+ this.rateLimitBackoffUntil = void 0;
1939
+ this.scheduleReconnect();
1940
+ }, waitTime);
1941
+ return;
1942
+ }
1943
+ const timeSinceConnect = Date.now() - this.lastConnectAttemptTime;
1944
+ const wasRapidClose = timeSinceConnect < RAPID_CLOSE_THRESHOLD && this.lastConnectAttemptTime > 0;
1945
+ const timeSinceLastCommand = Date.now() - this.lastCommandTime;
1946
+ const isIdle = timeSinceLastCommand >= IDLE_THRESHOLD;
1947
+ let baseDelay;
1948
+ if (this.isGracefulShutdown && this.reconnectAttempts < 3) {
1949
+ baseDelay = 500 * Math.pow(2, this.reconnectAttempts);
1950
+ } else if (wasRapidClose) {
1951
+ baseDelay = Math.max(RAPID_CLOSE_BACKOFF, INITIAL_RECONNECT_DELAY * Math.pow(2, Math.min(this.reconnectAttempts, 6)));
1952
+ console.log(`[EpisodaClient] Rapid close detected (${timeSinceConnect}ms), applying ${baseDelay / 1e3}s backoff`);
1953
+ } else if (isIdle) {
1954
+ baseDelay = IDLE_RECONNECT_DELAY;
1955
+ } else {
1956
+ const cappedAttempts = Math.min(this.reconnectAttempts, 6);
1957
+ baseDelay = INITIAL_RECONNECT_DELAY * Math.pow(2, cappedAttempts);
1958
+ }
1959
+ const jitter = baseDelay * 0.25 * (Math.random() * 2 - 1);
1960
+ const maxDelay = isIdle ? IDLE_RECONNECT_DELAY : MAX_RECONNECT_DELAY;
1961
+ const delay = Math.min(baseDelay + jitter, maxDelay);
1962
+ this.reconnectAttempts++;
1963
+ const shutdownType = this.isGracefulShutdown ? "graceful" : "ungraceful";
1964
+ const idleStatus = isIdle ? ", idle" : "";
1965
+ const rapidStatus = wasRapidClose ? ", rapid-close" : "";
1966
+ if (this.reconnectAttempts <= 5 || this.reconnectAttempts % 10 === 0) {
1967
+ console.log(`[EpisodaClient] Reconnecting in ${Math.round(delay / 1e3)}s (attempt ${this.reconnectAttempts}, ${shutdownType}${idleStatus}${rapidStatus})`);
1968
+ }
1969
+ this.reconnectTimeout = setTimeout(() => {
1970
+ console.log("[EpisodaClient] Attempting reconnection...");
1971
+ this.connect(this.url, this.token, this.machineId, {
1972
+ hostname: this.hostname,
1973
+ osPlatform: this.osPlatform,
1974
+ osArch: this.osArch,
1975
+ daemonPid: this.daemonPid
1976
+ }).then(() => {
1977
+ console.log("[EpisodaClient] Reconnection successful, resetting retry counter");
1978
+ this.reconnectAttempts = 0;
1979
+ this.isGracefulShutdown = false;
1980
+ this.firstDisconnectTime = void 0;
1981
+ this.rateLimitBackoffUntil = void 0;
1982
+ }).catch((error) => {
1983
+ console.error("[EpisodaClient] Reconnection failed:", error);
1984
+ });
1985
+ }, delay);
1986
+ }
1987
+ /**
1988
+ * EP605: Start client-side heartbeat to detect dead connections
1989
+ *
1990
+ * Sends ping every 45 seconds and expects pong within 15 seconds.
1991
+ * If no pong received, terminates connection to trigger reconnection.
1992
+ */
1993
+ startHeartbeat() {
1994
+ if (this.heartbeatTimer) {
1995
+ clearInterval(this.heartbeatTimer);
1996
+ }
1997
+ this.heartbeatTimer = setInterval(() => {
1998
+ if (!this.ws || this.ws.readyState !== ws_1.default.OPEN) {
1999
+ return;
2000
+ }
2001
+ try {
2002
+ this.ws.ping();
2003
+ if (this.heartbeatTimeoutTimer) {
2004
+ clearTimeout(this.heartbeatTimeoutTimer);
2005
+ }
2006
+ this.heartbeatTimeoutTimer = setTimeout(() => {
2007
+ console.log("[EpisodaClient] Heartbeat timeout - no pong received, terminating connection");
2008
+ if (this.ws) {
2009
+ this.ws.terminate();
2010
+ }
2011
+ }, CLIENT_HEARTBEAT_TIMEOUT);
2012
+ } catch (error) {
2013
+ console.error("[EpisodaClient] Error sending heartbeat ping:", error);
2014
+ }
2015
+ }, CLIENT_HEARTBEAT_INTERVAL);
2016
+ }
2017
+ };
2018
+ exports2.EpisodaClient = EpisodaClient2;
2019
+ }
2020
+ });
2021
+
2022
+ // ../core/dist/auth.js
2023
+ var require_auth = __commonJS({
2024
+ "../core/dist/auth.js"(exports2) {
2025
+ "use strict";
2026
+ var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
2027
+ if (k2 === void 0) k2 = k;
2028
+ var desc = Object.getOwnPropertyDescriptor(m, k);
2029
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
2030
+ desc = { enumerable: true, get: function() {
2031
+ return m[k];
2032
+ } };
2033
+ }
2034
+ Object.defineProperty(o, k2, desc);
2035
+ }) : (function(o, m, k, k2) {
2036
+ if (k2 === void 0) k2 = k;
2037
+ o[k2] = m[k];
2038
+ }));
2039
+ var __setModuleDefault = exports2 && exports2.__setModuleDefault || (Object.create ? (function(o, v) {
2040
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
2041
+ }) : function(o, v) {
2042
+ o["default"] = v;
2043
+ });
2044
+ var __importStar = exports2 && exports2.__importStar || /* @__PURE__ */ (function() {
2045
+ var ownKeys = function(o) {
2046
+ ownKeys = Object.getOwnPropertyNames || function(o2) {
2047
+ var ar = [];
2048
+ for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
2049
+ return ar;
2050
+ };
2051
+ return ownKeys(o);
2052
+ };
2053
+ return function(mod) {
2054
+ if (mod && mod.__esModule) return mod;
2055
+ var result = {};
2056
+ if (mod != null) {
2057
+ for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
2058
+ }
2059
+ __setModuleDefault(result, mod);
2060
+ return result;
2061
+ };
2062
+ })();
2063
+ Object.defineProperty(exports2, "__esModule", { value: true });
2064
+ exports2.getConfigDir = getConfigDir5;
2065
+ exports2.getConfigPath = getConfigPath;
2066
+ exports2.loadConfig = loadConfig2;
2067
+ exports2.saveConfig = saveConfig2;
2068
+ exports2.validateToken = validateToken;
2069
+ var fs6 = __importStar(require("fs"));
2070
+ var path7 = __importStar(require("path"));
2071
+ var os4 = __importStar(require("os"));
2072
+ var child_process_1 = require("child_process");
2073
+ var DEFAULT_CONFIG_FILE = "config.json";
2074
+ function getConfigDir5() {
2075
+ return process.env.EPISODA_CONFIG_DIR || path7.join(os4.homedir(), ".episoda");
2076
+ }
2077
+ function getConfigPath(configPath) {
2078
+ if (configPath) {
2079
+ return configPath;
2080
+ }
2081
+ return path7.join(getConfigDir5(), DEFAULT_CONFIG_FILE);
2082
+ }
2083
+ function ensureConfigDir(configPath) {
2084
+ const dir = path7.dirname(configPath);
2085
+ const isNew = !fs6.existsSync(dir);
2086
+ if (isNew) {
2087
+ fs6.mkdirSync(dir, { recursive: true, mode: 448 });
2088
+ }
2089
+ if (process.platform === "darwin") {
2090
+ const nosyncPath = path7.join(dir, ".nosync");
2091
+ if (isNew || !fs6.existsSync(nosyncPath)) {
2092
+ try {
2093
+ fs6.writeFileSync(nosyncPath, "", { mode: 384 });
2094
+ (0, child_process_1.execSync)(`xattr -w com.apple.fileprovider.ignore 1 "${dir}"`, {
2095
+ stdio: "ignore",
2096
+ timeout: 5e3
2097
+ });
2098
+ } catch {
2099
+ }
2100
+ }
2101
+ }
2102
+ }
2103
+ async function loadConfig2(configPath) {
2104
+ const fullPath = getConfigPath(configPath);
2105
+ if (!fs6.existsSync(fullPath)) {
2106
+ return null;
2107
+ }
2108
+ try {
2109
+ const content = fs6.readFileSync(fullPath, "utf8");
2110
+ const config = JSON.parse(content);
2111
+ return config;
2112
+ } catch (error) {
2113
+ console.error("Error loading config:", error);
2114
+ return null;
2115
+ }
2116
+ }
2117
+ async function saveConfig2(config, configPath) {
2118
+ const fullPath = getConfigPath(configPath);
2119
+ ensureConfigDir(fullPath);
2120
+ try {
2121
+ const content = JSON.stringify(config, null, 2);
2122
+ fs6.writeFileSync(fullPath, content, { mode: 384 });
2123
+ } catch (error) {
2124
+ throw new Error(`Failed to save config: ${error instanceof Error ? error.message : String(error)}`);
2125
+ }
2126
+ }
2127
+ async function validateToken(token) {
2128
+ return Boolean(token && token.length > 0);
2129
+ }
2130
+ }
2131
+ });
2132
+
2133
+ // ../core/dist/errors.js
2134
+ var require_errors = __commonJS({
2135
+ "../core/dist/errors.js"(exports2) {
2136
+ "use strict";
2137
+ Object.defineProperty(exports2, "__esModule", { value: true });
2138
+ exports2.EpisodaError = void 0;
2139
+ exports2.formatErrorMessage = formatErrorMessage;
2140
+ var EpisodaError = class extends Error {
2141
+ constructor(code, message, details) {
2142
+ super(message);
2143
+ this.code = code;
2144
+ this.details = details;
2145
+ this.name = "EpisodaError";
2146
+ }
2147
+ };
2148
+ exports2.EpisodaError = EpisodaError;
2149
+ function formatErrorMessage(code, details) {
2150
+ const messages = {
2151
+ "GIT_NOT_INSTALLED": "Git is not installed or not in PATH",
2152
+ "NOT_GIT_REPO": "Not a git repository",
2153
+ "MERGE_CONFLICT": "Merge conflict detected",
2154
+ "REBASE_CONFLICT": "Rebase conflict detected - resolve conflicts or abort rebase",
2155
+ "UNCOMMITTED_CHANGES": "Uncommitted changes would be overwritten",
2156
+ "NETWORK_ERROR": "Network error occurred",
2157
+ "AUTH_FAILURE": "Authentication failed",
2158
+ "BRANCH_NOT_FOUND": "Branch not found",
2159
+ "BRANCH_ALREADY_EXISTS": "Branch already exists",
2160
+ "PUSH_REJECTED": "Push rejected by remote",
2161
+ "COMMAND_TIMEOUT": "Command timed out",
2162
+ "UNKNOWN_ERROR": "Unknown error occurred"
2163
+ };
2164
+ let message = messages[code] || `Error: ${code}`;
2165
+ if (details) {
2166
+ if (details.uncommittedFiles && details.uncommittedFiles.length > 0) {
2167
+ message += `
2168
+ Uncommitted files: ${details.uncommittedFiles.join(", ")}`;
2169
+ }
2170
+ if (details.conflictingFiles && details.conflictingFiles.length > 0) {
2171
+ message += `
2172
+ Conflicting files: ${details.conflictingFiles.join(", ")}`;
2173
+ }
2174
+ if (details.branchName) {
2175
+ message += `
2176
+ Branch: ${details.branchName}`;
2177
+ }
2178
+ }
2179
+ return message;
2180
+ }
2181
+ }
2182
+ });
2183
+
2184
+ // ../core/dist/index.js
2185
+ var require_dist = __commonJS({
2186
+ "../core/dist/index.js"(exports2) {
2187
+ "use strict";
2188
+ var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
2189
+ if (k2 === void 0) k2 = k;
2190
+ var desc = Object.getOwnPropertyDescriptor(m, k);
2191
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
2192
+ desc = { enumerable: true, get: function() {
2193
+ return m[k];
2194
+ } };
2195
+ }
2196
+ Object.defineProperty(o, k2, desc);
2197
+ }) : (function(o, m, k, k2) {
2198
+ if (k2 === void 0) k2 = k;
2199
+ o[k2] = m[k];
2200
+ }));
2201
+ var __exportStar = exports2 && exports2.__exportStar || function(m, exports3) {
2202
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports3, p)) __createBinding(exports3, m, p);
2203
+ };
2204
+ Object.defineProperty(exports2, "__esModule", { value: true });
2205
+ exports2.VERSION = exports2.EpisodaClient = exports2.GitExecutor = void 0;
2206
+ __exportStar(require_command_protocol(), exports2);
2207
+ var git_executor_1 = require_git_executor();
2208
+ Object.defineProperty(exports2, "GitExecutor", { enumerable: true, get: function() {
2209
+ return git_executor_1.GitExecutor;
2210
+ } });
2211
+ var websocket_client_1 = require_websocket_client();
2212
+ Object.defineProperty(exports2, "EpisodaClient", { enumerable: true, get: function() {
2213
+ return websocket_client_1.EpisodaClient;
2214
+ } });
2215
+ __exportStar(require_auth(), exports2);
2216
+ __exportStar(require_errors(), exports2);
2217
+ __exportStar(require_git_validator(), exports2);
2218
+ __exportStar(require_git_parser(), exports2);
2219
+ var version_1 = require_version();
2220
+ Object.defineProperty(exports2, "VERSION", { enumerable: true, get: function() {
2221
+ return version_1.VERSION;
2222
+ } });
2223
+ }
2224
+ });
2225
+
2226
+ // package.json
2227
+ var require_package = __commonJS({
2228
+ "package.json"(exports2, module2) {
2229
+ module2.exports = {
2230
+ name: "episoda",
2231
+ version: "0.2.10",
2232
+ description: "CLI tool for Episoda local development workflow orchestration",
2233
+ main: "dist/index.js",
2234
+ types: "dist/index.d.ts",
2235
+ bin: {
2236
+ episoda: "dist/index.js"
2237
+ },
2238
+ scripts: {
2239
+ build: "tsup",
2240
+ dev: "tsup --watch",
2241
+ clean: "rm -rf dist",
2242
+ typecheck: "tsc --noEmit"
2243
+ },
2244
+ keywords: [
2245
+ "episoda",
2246
+ "cli",
2247
+ "git",
2248
+ "workflow",
2249
+ "development"
2250
+ ],
2251
+ author: "Episoda",
2252
+ license: "MIT",
2253
+ dependencies: {
2254
+ chalk: "^4.1.2",
2255
+ commander: "^11.1.0",
2256
+ ora: "^5.4.1",
2257
+ semver: "7.7.3",
2258
+ ws: "^8.18.0",
2259
+ zod: "^4.0.10"
2260
+ },
2261
+ devDependencies: {
2262
+ "@episoda/core": "*",
2263
+ "@types/node": "^20.11.24",
2264
+ "@types/semver": "7.7.1",
2265
+ "@types/ws": "^8.5.10",
2266
+ tsup: "8.5.1",
2267
+ typescript: "^5.3.3"
2268
+ },
2269
+ engines: {
2270
+ node: ">=20.0.0"
2271
+ },
2272
+ files: [
2273
+ "dist",
2274
+ "README.md"
2275
+ ],
2276
+ repository: {
2277
+ type: "git",
2278
+ url: "https://github.com/v20x/episoda.git",
2279
+ directory: "packages/episoda"
2280
+ }
2281
+ };
2282
+ }
2283
+ });
2284
+
2285
+ // src/daemon/machine-id.ts
2286
+ var os = __toESM(require("os"));
2287
+ var fs = __toESM(require("fs"));
2288
+ var path = __toESM(require("path"));
2289
+ var crypto = __toESM(require("crypto"));
2290
+ var import_child_process = require("child_process");
2291
+ var import_core = __toESM(require_dist());
2292
+ async function getMachineId() {
2293
+ const machineIdPath = path.join((0, import_core.getConfigDir)(), "machine-id");
2294
+ try {
2295
+ if (fs.existsSync(machineIdPath)) {
2296
+ const machineId2 = fs.readFileSync(machineIdPath, "utf-8").trim();
2297
+ if (machineId2) {
2298
+ return machineId2;
2299
+ }
2300
+ }
2301
+ } catch (error) {
2302
+ }
2303
+ const machineId = generateMachineId();
2304
+ try {
2305
+ const dir = path.dirname(machineIdPath);
2306
+ if (!fs.existsSync(dir)) {
2307
+ fs.mkdirSync(dir, { recursive: true });
2308
+ }
2309
+ fs.writeFileSync(machineIdPath, machineId, "utf-8");
2310
+ } catch (error) {
2311
+ console.error("Warning: Could not save machine ID to disk:", error);
2312
+ }
2313
+ return machineId;
2314
+ }
2315
+ function getHardwareUUID() {
2316
+ try {
2317
+ if (process.platform === "darwin") {
2318
+ const output = (0, import_child_process.execSync)(
2319
+ `ioreg -d2 -c IOPlatformExpertDevice | awk -F\\" '/IOPlatformUUID/{print $(NF-1)}'`,
2320
+ { encoding: "utf-8", timeout: 5e3 }
2321
+ ).trim();
2322
+ if (output && output.length > 0) {
2323
+ return output;
2324
+ }
2325
+ } else if (process.platform === "linux") {
2326
+ if (fs.existsSync("/etc/machine-id")) {
2327
+ const machineId = fs.readFileSync("/etc/machine-id", "utf-8").trim();
2328
+ if (machineId && machineId.length > 0) {
2329
+ return machineId;
2330
+ }
2331
+ }
2332
+ if (fs.existsSync("/var/lib/dbus/machine-id")) {
2333
+ const dbusId = fs.readFileSync("/var/lib/dbus/machine-id", "utf-8").trim();
2334
+ if (dbusId && dbusId.length > 0) {
2335
+ return dbusId;
2336
+ }
2337
+ }
2338
+ } else if (process.platform === "win32") {
2339
+ const output = (0, import_child_process.execSync)("wmic csproduct get uuid", {
2340
+ encoding: "utf-8",
2341
+ timeout: 5e3
2342
+ });
2343
+ const lines = output.trim().split("\n");
2344
+ if (lines.length >= 2) {
2345
+ const uuid = lines[1].trim();
2346
+ if (uuid && uuid.length > 0 && uuid !== "UUID") {
2347
+ return uuid;
2348
+ }
2349
+ }
2350
+ }
2351
+ } catch (error) {
2352
+ console.warn("Could not get hardware UUID, using random fallback:", error);
2353
+ }
2354
+ return crypto.randomUUID();
2355
+ }
2356
+ function generateMachineId() {
2357
+ const hostname4 = os.hostname();
2358
+ const hwUUID = getHardwareUUID();
2359
+ const shortId = hwUUID.replace(/-/g, "").slice(0, 8).toLowerCase();
2360
+ return `${hostname4}-${shortId}`;
2361
+ }
2362
+
2363
+ // src/daemon/project-tracker.ts
2364
+ var fs2 = __toESM(require("fs"));
2365
+ var path2 = __toESM(require("path"));
2366
+ var import_core2 = __toESM(require_dist());
2367
+ function getProjectsFilePath() {
2368
+ return path2.join((0, import_core2.getConfigDir)(), "projects.json");
2369
+ }
2370
+ function readProjects() {
2371
+ const projectsPath = getProjectsFilePath();
2372
+ try {
2373
+ if (!fs2.existsSync(projectsPath)) {
2374
+ return { projects: [] };
2375
+ }
2376
+ const content = fs2.readFileSync(projectsPath, "utf-8");
2377
+ const data = JSON.parse(content);
2378
+ if (!data.projects || !Array.isArray(data.projects)) {
2379
+ console.warn("Invalid projects.json structure, resetting");
2380
+ return { projects: [] };
2381
+ }
2382
+ return data;
2383
+ } catch (error) {
2384
+ console.error("Error reading projects.json:", error);
2385
+ return { projects: [] };
2386
+ }
2387
+ }
2388
+ function writeProjects(data) {
2389
+ const projectsPath = getProjectsFilePath();
2390
+ try {
2391
+ const dir = path2.dirname(projectsPath);
2392
+ if (!fs2.existsSync(dir)) {
2393
+ fs2.mkdirSync(dir, { recursive: true });
2394
+ }
2395
+ fs2.writeFileSync(projectsPath, JSON.stringify(data, null, 2), "utf-8");
2396
+ } catch (error) {
2397
+ throw new Error(`Failed to write projects.json: ${error}`);
2398
+ }
2399
+ }
2400
+ function addProject(projectId, projectPath) {
2401
+ const data = readProjects();
2402
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2403
+ const existingByPath = data.projects.find((p) => p.path === projectPath);
2404
+ if (existingByPath) {
2405
+ existingByPath.id = projectId;
2406
+ existingByPath.last_active = now;
2407
+ writeProjects(data);
2408
+ return existingByPath;
2409
+ }
2410
+ const existingByIdIndex = data.projects.findIndex((p) => p.id === projectId);
2411
+ if (existingByIdIndex !== -1) {
2412
+ const existingById = data.projects[existingByIdIndex];
2413
+ console.log(`[ProjectTracker] Replacing project entry: ${existingById.path} -> ${projectPath}`);
2414
+ data.projects.splice(existingByIdIndex, 1);
2415
+ }
2416
+ const projectName = path2.basename(projectPath);
2417
+ const newProject = {
2418
+ id: projectId,
2419
+ path: projectPath,
2420
+ name: projectName,
2421
+ added_at: now,
2422
+ last_active: now
2423
+ };
2424
+ data.projects.push(newProject);
2425
+ writeProjects(data);
2426
+ return newProject;
2427
+ }
2428
+ function removeProject(projectPath) {
2429
+ const data = readProjects();
2430
+ const initialLength = data.projects.length;
2431
+ data.projects = data.projects.filter((p) => p.path !== projectPath);
2432
+ if (data.projects.length < initialLength) {
2433
+ writeProjects(data);
2434
+ return true;
2435
+ }
2436
+ return false;
2437
+ }
2438
+ function getAllProjects() {
2439
+ const data = readProjects();
2440
+ return data.projects;
2441
+ }
2442
+ function touchProject(projectPath) {
2443
+ const data = readProjects();
2444
+ const project = data.projects.find((p) => p.path === projectPath);
2445
+ if (project) {
2446
+ project.last_active = (/* @__PURE__ */ new Date()).toISOString();
2447
+ writeProjects(data);
2448
+ }
2449
+ }
2450
+
2451
+ // src/daemon/daemon-manager.ts
2452
+ var path3 = __toESM(require("path"));
2453
+ var import_core3 = __toESM(require_dist());
2454
+ function getPidFilePath() {
2455
+ return path3.join((0, import_core3.getConfigDir)(), "daemon.pid");
2456
+ }
2457
+
2458
+ // src/ipc/ipc-server.ts
2459
+ var net = __toESM(require("net"));
2460
+ var fs3 = __toESM(require("fs"));
2461
+ var path4 = __toESM(require("path"));
2462
+ var import_core4 = __toESM(require_dist());
2463
+ var getSocketPath = () => path4.join((0, import_core4.getConfigDir)(), "daemon.sock");
2464
+ var IPCServer = class {
2465
+ constructor() {
2466
+ this.server = null;
2467
+ this.handlers = /* @__PURE__ */ new Map();
2468
+ }
2469
+ /**
2470
+ * Register a command handler
2471
+ *
2472
+ * @param command Command name
2473
+ * @param handler Async function to handle command
2474
+ */
2475
+ on(command, handler) {
2476
+ this.handlers.set(command, handler);
2477
+ }
2478
+ /**
2479
+ * Start the IPC server
2480
+ *
2481
+ * Creates Unix socket and listens for connections.
2482
+ */
2483
+ async start() {
2484
+ const socketPath = getSocketPath();
2485
+ if (fs3.existsSync(socketPath)) {
2486
+ fs3.unlinkSync(socketPath);
2487
+ }
2488
+ const dir = path4.dirname(socketPath);
2489
+ if (!fs3.existsSync(dir)) {
2490
+ fs3.mkdirSync(dir, { recursive: true });
2491
+ }
2492
+ this.server = net.createServer((socket) => {
2493
+ this.handleConnection(socket);
2494
+ });
2495
+ return new Promise((resolve2, reject) => {
2496
+ this.server.listen(socketPath, () => {
2497
+ fs3.chmodSync(socketPath, 384);
2498
+ resolve2();
2499
+ });
2500
+ this.server.on("error", reject);
2501
+ });
2502
+ }
2503
+ /**
2504
+ * Stop the IPC server
2505
+ */
2506
+ async stop() {
2507
+ if (!this.server) return;
2508
+ const socketPath = getSocketPath();
2509
+ return new Promise((resolve2) => {
2510
+ this.server.close(() => {
2511
+ if (fs3.existsSync(socketPath)) {
2512
+ fs3.unlinkSync(socketPath);
2513
+ }
2514
+ resolve2();
2515
+ });
2516
+ });
2517
+ }
2518
+ /**
2519
+ * Handle incoming client connection
2520
+ */
2521
+ handleConnection(socket) {
2522
+ let buffer = "";
2523
+ socket.on("data", async (chunk) => {
2524
+ buffer += chunk.toString();
2525
+ const newlineIndex = buffer.indexOf("\n");
2526
+ if (newlineIndex === -1) return;
2527
+ const message = buffer.slice(0, newlineIndex);
2528
+ buffer = buffer.slice(newlineIndex + 1);
2529
+ try {
2530
+ const request = JSON.parse(message);
2531
+ const response = await this.handleRequest(request);
2532
+ socket.write(JSON.stringify(response) + "\n");
2533
+ } catch (error) {
2534
+ const errorResponse = {
2535
+ id: "unknown",
2536
+ success: false,
2537
+ error: error instanceof Error ? error.message : "Unknown error"
2538
+ };
2539
+ socket.write(JSON.stringify(errorResponse) + "\n");
2540
+ }
2541
+ });
2542
+ socket.on("error", (error) => {
2543
+ console.error("[IPC Server] Socket error:", error);
2544
+ });
2545
+ }
2546
+ /**
2547
+ * Handle IPC request
2548
+ */
2549
+ async handleRequest(request) {
2550
+ const handler = this.handlers.get(request.command);
2551
+ if (!handler) {
2552
+ return {
2553
+ id: request.id,
2554
+ success: false,
2555
+ error: `Unknown command: ${request.command}`
2556
+ };
2557
+ }
2558
+ try {
2559
+ const data = await handler(request.params);
2560
+ return {
2561
+ id: request.id,
2562
+ success: true,
2563
+ data
2564
+ };
2565
+ } catch (error) {
2566
+ return {
2567
+ id: request.id,
2568
+ success: false,
2569
+ error: error instanceof Error ? error.message : "Unknown error"
2570
+ };
2571
+ }
2572
+ }
2573
+ };
2574
+
2575
+ // src/daemon/daemon-process.ts
2576
+ var import_core5 = __toESM(require_dist());
2577
+
2578
+ // src/utils/update-checker.ts
2579
+ var import_child_process2 = require("child_process");
2580
+ var semver = __toESM(require("semver"));
2581
+ var PACKAGE_NAME = "episoda";
2582
+ var NPM_REGISTRY = "https://registry.npmjs.org";
2583
+ async function checkForUpdates(currentVersion) {
2584
+ try {
2585
+ const controller = new AbortController();
2586
+ const timeoutId = setTimeout(() => controller.abort(), 5e3);
2587
+ const response = await fetch(`${NPM_REGISTRY}/${PACKAGE_NAME}/latest`, {
2588
+ signal: controller.signal
2589
+ });
2590
+ clearTimeout(timeoutId);
2591
+ if (!response.ok) {
2592
+ return { currentVersion, latestVersion: currentVersion, updateAvailable: false };
2593
+ }
2594
+ const data = await response.json();
2595
+ const latestVersion = data.version;
2596
+ return {
2597
+ currentVersion,
2598
+ latestVersion,
2599
+ updateAvailable: semver.gt(latestVersion, currentVersion)
2600
+ };
2601
+ } catch (error) {
2602
+ return { currentVersion, latestVersion: currentVersion, updateAvailable: false };
2603
+ }
2604
+ }
2605
+ function performBackgroundUpdate() {
2606
+ try {
2607
+ const child = (0, import_child_process2.spawn)("npm", ["update", "-g", PACKAGE_NAME], {
2608
+ detached: true,
2609
+ stdio: "ignore",
2610
+ // Use shell on Windows for proper npm execution
2611
+ shell: process.platform === "win32"
2612
+ });
2613
+ child.unref();
2614
+ } catch (error) {
2615
+ }
2616
+ }
2617
+
2618
+ // src/daemon/identity-server.ts
2619
+ var http = __toESM(require("http"));
2620
+ var os2 = __toESM(require("os"));
2621
+ var IDENTITY_SERVER_PORT = 3002;
2622
+ var IdentityServer = class {
2623
+ constructor(machineId) {
2624
+ this.server = null;
2625
+ this.isConnected = false;
2626
+ this.machineId = machineId;
2627
+ }
2628
+ /**
2629
+ * Update connection status
2630
+ * Called when WebSocket connection state changes
2631
+ */
2632
+ setConnected(connected) {
2633
+ this.isConnected = connected;
2634
+ }
2635
+ /**
2636
+ * Start the identity server on localhost:3002
2637
+ */
2638
+ async start() {
2639
+ return new Promise((resolve2, reject) => {
2640
+ this.server = http.createServer((req, res) => {
2641
+ res.setHeader("Access-Control-Allow-Origin", "*");
2642
+ res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
2643
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
2644
+ res.setHeader("Access-Control-Allow-Private-Network", "true");
2645
+ if (req.method === "OPTIONS") {
2646
+ res.writeHead(204);
2647
+ res.end();
2648
+ return;
2649
+ }
2650
+ if (req.method !== "GET") {
2651
+ res.writeHead(405, { "Content-Type": "application/json" });
2652
+ res.end(JSON.stringify({ error: "Method not allowed" }));
2653
+ return;
2654
+ }
2655
+ const url = new URL(req.url || "/", `http://${req.headers.host}`);
2656
+ if (url.pathname === "/identity") {
2657
+ const response = {
2658
+ machineId: this.machineId,
2659
+ hostname: os2.hostname(),
2660
+ platform: os2.platform(),
2661
+ arch: os2.arch(),
2662
+ connected: this.isConnected
2663
+ };
2664
+ res.writeHead(200, { "Content-Type": "application/json" });
2665
+ res.end(JSON.stringify(response));
2666
+ return;
2667
+ }
2668
+ if (url.pathname === "/health") {
2669
+ res.writeHead(200, { "Content-Type": "application/json" });
2670
+ res.end(JSON.stringify({ status: "ok", machineId: this.machineId }));
2671
+ return;
2672
+ }
2673
+ res.writeHead(404, { "Content-Type": "application/json" });
2674
+ res.end(JSON.stringify({ error: "Not found" }));
2675
+ });
2676
+ this.server.listen(IDENTITY_SERVER_PORT, "127.0.0.1", () => {
2677
+ console.log(`[IdentityServer] Started on http://127.0.0.1:${IDENTITY_SERVER_PORT}`);
2678
+ resolve2();
2679
+ });
2680
+ this.server.on("error", (err) => {
2681
+ if (err.code === "EADDRINUSE") {
2682
+ console.warn(`[IdentityServer] Port ${IDENTITY_SERVER_PORT} already in use, skipping`);
2683
+ resolve2();
2684
+ } else {
2685
+ console.error("[IdentityServer] Failed to start:", err.message);
2686
+ reject(err);
2687
+ }
2688
+ });
2689
+ });
2690
+ }
2691
+ /**
2692
+ * Stop the identity server
2693
+ */
2694
+ async stop() {
2695
+ return new Promise((resolve2) => {
2696
+ if (this.server) {
2697
+ this.server.close(() => {
2698
+ console.log("[IdentityServer] Stopped");
2699
+ this.server = null;
2700
+ resolve2();
2701
+ });
2702
+ } else {
2703
+ resolve2();
2704
+ }
2705
+ });
2706
+ }
2707
+ };
2708
+
2709
+ // src/daemon/handlers/file-handlers.ts
2710
+ var fs4 = __toESM(require("fs"));
2711
+ var path5 = __toESM(require("path"));
2712
+ var readline = __toESM(require("readline"));
2713
+ var DEFAULT_MAX_FILE_SIZE = 20 * 1024 * 1024;
2714
+ function validatePath(filePath, projectPath) {
2715
+ const normalizedProjectPath = path5.resolve(projectPath);
2716
+ const absolutePath = path5.isAbsolute(filePath) ? path5.resolve(filePath) : path5.resolve(projectPath, filePath);
2717
+ const normalizedPath = path5.normalize(absolutePath);
2718
+ if (!normalizedPath.startsWith(normalizedProjectPath + path5.sep) && normalizedPath !== normalizedProjectPath) {
2719
+ return null;
2720
+ }
2721
+ return normalizedPath;
2722
+ }
2723
+ async function handleFileRead(command, projectPath) {
2724
+ const { path: filePath, encoding = "base64", maxSize = DEFAULT_MAX_FILE_SIZE } = command;
2725
+ const validPath = validatePath(filePath, projectPath);
2726
+ if (!validPath) {
2727
+ return {
2728
+ success: false,
2729
+ error: "Invalid path: directory traversal not allowed"
2730
+ };
2731
+ }
2732
+ try {
2733
+ if (!fs4.existsSync(validPath)) {
2734
+ return {
2735
+ success: false,
2736
+ error: "File not found"
2737
+ };
2738
+ }
2739
+ const stats = fs4.statSync(validPath);
2740
+ if (stats.isDirectory()) {
2741
+ return {
2742
+ success: false,
2743
+ error: "Path is a directory, not a file"
2744
+ };
2745
+ }
2746
+ if (stats.size > maxSize) {
2747
+ return {
2748
+ success: false,
2749
+ error: `File too large: ${stats.size} bytes exceeds limit of ${maxSize} bytes`
2750
+ };
2751
+ }
2752
+ const buffer = fs4.readFileSync(validPath);
2753
+ let content;
2754
+ if (encoding === "base64") {
2755
+ content = buffer.toString("base64");
2756
+ } else {
2757
+ content = buffer.toString("utf8");
2758
+ }
2759
+ return {
2760
+ success: true,
2761
+ content,
2762
+ encoding,
2763
+ size: stats.size
2764
+ };
2765
+ } catch (error) {
2766
+ const errMsg = error instanceof Error ? error.message : String(error);
2767
+ const isPermissionError = errMsg.includes("EACCES") || errMsg.includes("permission");
2768
+ return {
2769
+ success: false,
2770
+ error: isPermissionError ? "Permission denied" : "Failed to read file"
2771
+ };
2772
+ }
2773
+ }
2774
+ async function handleFileWrite(command, projectPath) {
2775
+ const { path: filePath, content, encoding = "utf8", createDirs = true } = command;
2776
+ const validPath = validatePath(filePath, projectPath);
2777
+ if (!validPath) {
2778
+ return {
2779
+ success: false,
2780
+ error: "Invalid path: directory traversal not allowed"
2781
+ };
2782
+ }
2783
+ try {
2784
+ if (createDirs) {
2785
+ const dirPath = path5.dirname(validPath);
2786
+ if (!fs4.existsSync(dirPath)) {
2787
+ fs4.mkdirSync(dirPath, { recursive: true });
2788
+ }
2789
+ }
2790
+ let buffer;
2791
+ if (encoding === "base64") {
2792
+ buffer = Buffer.from(content, "base64");
2793
+ } else {
2794
+ buffer = Buffer.from(content, "utf8");
2795
+ }
2796
+ const tempPath = `${validPath}.tmp.${Date.now()}`;
2797
+ fs4.writeFileSync(tempPath, buffer);
2798
+ fs4.renameSync(tempPath, validPath);
2799
+ return {
2800
+ success: true,
2801
+ bytesWritten: buffer.length
2802
+ };
2803
+ } catch (error) {
2804
+ const errMsg = error instanceof Error ? error.message : String(error);
2805
+ const isPermissionError = errMsg.includes("EACCES") || errMsg.includes("permission");
2806
+ const isDiskError = errMsg.includes("ENOSPC") || errMsg.includes("no space");
2807
+ return {
2808
+ success: false,
2809
+ error: isPermissionError ? "Permission denied" : isDiskError ? "Disk full" : "Failed to write file"
2810
+ };
2811
+ }
2812
+ }
2813
+ async function handleFileList(command, projectPath) {
2814
+ const { path: dirPath, recursive = false, includeHidden = false } = command;
2815
+ const validPath = validatePath(dirPath, projectPath);
2816
+ if (!validPath) {
2817
+ return {
2818
+ success: false,
2819
+ error: "Invalid path: directory traversal not allowed"
2820
+ };
2821
+ }
2822
+ try {
2823
+ if (!fs4.existsSync(validPath)) {
2824
+ return {
2825
+ success: false,
2826
+ error: "Directory not found"
2827
+ };
2828
+ }
2829
+ const stats = fs4.statSync(validPath);
2830
+ if (!stats.isDirectory()) {
2831
+ return {
2832
+ success: false,
2833
+ error: "Path is not a directory"
2834
+ };
2835
+ }
2836
+ const entries = [];
2837
+ if (recursive) {
2838
+ await listDirectoryRecursive(validPath, validPath, entries, includeHidden);
2839
+ } else {
2840
+ const dirEntries = await fs4.promises.readdir(validPath, { withFileTypes: true });
2841
+ for (const entry of dirEntries) {
2842
+ if (!includeHidden && entry.name.startsWith(".")) continue;
2843
+ const entryPath = path5.join(validPath, entry.name);
2844
+ const entryStats = await fs4.promises.stat(entryPath);
2845
+ entries.push({
2846
+ name: entry.name,
2847
+ type: entry.isDirectory() ? "directory" : "file",
2848
+ size: entryStats.size
2849
+ });
2850
+ }
2851
+ }
2852
+ return {
2853
+ success: true,
2854
+ entries
2855
+ };
2856
+ } catch (error) {
2857
+ const errMsg = error instanceof Error ? error.message : String(error);
2858
+ const isPermissionError = errMsg.includes("EACCES") || errMsg.includes("permission");
2859
+ return {
2860
+ success: false,
2861
+ error: isPermissionError ? "Permission denied" : "Failed to list directory"
2862
+ };
2863
+ }
2864
+ }
2865
+ async function listDirectoryRecursive(basePath, currentPath, entries, includeHidden) {
2866
+ const dirEntries = await fs4.promises.readdir(currentPath, { withFileTypes: true });
2867
+ for (const entry of dirEntries) {
2868
+ if (!includeHidden && entry.name.startsWith(".")) continue;
2869
+ const entryPath = path5.join(currentPath, entry.name);
2870
+ const relativePath = path5.relative(basePath, entryPath);
2871
+ try {
2872
+ const entryStats = await fs4.promises.stat(entryPath);
2873
+ entries.push({
2874
+ name: relativePath,
2875
+ type: entry.isDirectory() ? "directory" : "file",
2876
+ size: entryStats.size
2877
+ });
2878
+ if (entry.isDirectory()) {
2879
+ await listDirectoryRecursive(basePath, entryPath, entries, includeHidden);
2880
+ }
2881
+ } catch {
2882
+ }
2883
+ }
2884
+ }
2885
+ async function handleFileSearch(command, projectPath) {
2886
+ const { pattern, basePath, maxResults = 100 } = command;
2887
+ const effectiveMaxResults = Math.min(Math.max(1, maxResults), 1e3);
2888
+ const validPath = validatePath(basePath, projectPath);
2889
+ if (!validPath) {
2890
+ return {
2891
+ success: false,
2892
+ error: "Invalid path: directory traversal not allowed"
2893
+ };
2894
+ }
2895
+ try {
2896
+ if (!fs4.existsSync(validPath)) {
2897
+ return {
2898
+ success: false,
2899
+ error: "Directory not found"
2900
+ };
2901
+ }
2902
+ const files = [];
2903
+ const globRegex = globToRegex(pattern);
2904
+ await searchFilesRecursive(validPath, validPath, globRegex, files, effectiveMaxResults);
2905
+ return {
2906
+ success: true,
2907
+ files
2908
+ };
2909
+ } catch (error) {
2910
+ const errMsg = error instanceof Error ? error.message : String(error);
2911
+ const isPermissionError = errMsg.includes("EACCES") || errMsg.includes("permission");
2912
+ return {
2913
+ success: false,
2914
+ error: isPermissionError ? "Permission denied" : "Failed to search files"
2915
+ };
2916
+ }
2917
+ }
2918
+ function globToRegex(pattern) {
2919
+ let i = 0;
2920
+ let regexStr = "";
2921
+ while (i < pattern.length) {
2922
+ const char = pattern[i];
2923
+ if (char === "*" && pattern[i + 1] === "*") {
2924
+ regexStr += ".*";
2925
+ i += 2;
2926
+ if (pattern[i] === "/") i++;
2927
+ continue;
2928
+ }
2929
+ if (char === "*") {
2930
+ regexStr += "[^/]*";
2931
+ i++;
2932
+ continue;
2933
+ }
2934
+ if (char === "?") {
2935
+ regexStr += "[^/]";
2936
+ i++;
2937
+ continue;
2938
+ }
2939
+ if (char === "{") {
2940
+ const closeIdx = pattern.indexOf("}", i);
2941
+ if (closeIdx !== -1) {
2942
+ const options = pattern.slice(i + 1, closeIdx).split(",");
2943
+ const escaped = options.map((opt) => escapeRegexChars(opt));
2944
+ regexStr += `(?:${escaped.join("|")})`;
2945
+ i = closeIdx + 1;
2946
+ continue;
2947
+ }
2948
+ }
2949
+ if (char === "[") {
2950
+ const closeIdx = findClosingBracket(pattern, i);
2951
+ if (closeIdx !== -1) {
2952
+ let classContent = pattern.slice(i + 1, closeIdx);
2953
+ if (classContent.startsWith("!")) {
2954
+ classContent = "^" + classContent.slice(1);
2955
+ }
2956
+ regexStr += `[${classContent}]`;
2957
+ i = closeIdx + 1;
2958
+ continue;
2959
+ }
2960
+ }
2961
+ if (".+^${}()|[]\\".includes(char)) {
2962
+ regexStr += "\\" + char;
2963
+ } else {
2964
+ regexStr += char;
2965
+ }
2966
+ i++;
2967
+ }
2968
+ return new RegExp(`^${regexStr}$`);
2969
+ }
2970
+ function escapeRegexChars(str) {
2971
+ return str.replace(/[.+^${}()|[\]\\*?]/g, "\\$&");
2972
+ }
2973
+ function findClosingBracket(pattern, start) {
2974
+ let i = start + 1;
2975
+ if (pattern[i] === "!" || pattern[i] === "^") i++;
2976
+ if (pattern[i] === "]") i++;
2977
+ while (i < pattern.length) {
2978
+ if (pattern[i] === "]") return i;
2979
+ if (pattern[i] === "\\" && i + 1 < pattern.length) i++;
2980
+ i++;
2981
+ }
2982
+ return -1;
2983
+ }
2984
+ async function searchFilesRecursive(basePath, currentPath, pattern, files, maxResults) {
2985
+ if (files.length >= maxResults) return;
2986
+ const entries = await fs4.promises.readdir(currentPath, { withFileTypes: true });
2987
+ for (const entry of entries) {
2988
+ if (files.length >= maxResults) return;
2989
+ if (entry.name.startsWith(".")) continue;
2990
+ const entryPath = path5.join(currentPath, entry.name);
2991
+ const relativePath = path5.relative(basePath, entryPath);
2992
+ try {
2993
+ if (entry.isDirectory()) {
2994
+ await searchFilesRecursive(basePath, entryPath, pattern, files, maxResults);
2995
+ } else if (pattern.test(relativePath) || pattern.test(entry.name)) {
2996
+ files.push(relativePath);
2997
+ }
2998
+ } catch {
2999
+ }
3000
+ }
3001
+ }
3002
+ async function handleFileGrep(command, projectPath) {
3003
+ const {
3004
+ pattern,
3005
+ path: searchPath,
3006
+ filePattern = "*",
3007
+ caseSensitive = true,
3008
+ maxResults = 100
3009
+ } = command;
3010
+ const effectiveMaxResults = Math.min(Math.max(1, maxResults), 1e3);
3011
+ const validPath = validatePath(searchPath, projectPath);
3012
+ if (!validPath) {
3013
+ return {
3014
+ success: false,
3015
+ error: "Invalid path: directory traversal not allowed"
3016
+ };
3017
+ }
3018
+ try {
3019
+ if (!fs4.existsSync(validPath)) {
3020
+ return {
3021
+ success: false,
3022
+ error: "Path not found"
3023
+ };
3024
+ }
3025
+ const matches = [];
3026
+ const searchRegex = new RegExp(pattern, caseSensitive ? "" : "i");
3027
+ const fileGlobRegex = globToRegex(filePattern);
3028
+ const stats = fs4.statSync(validPath);
3029
+ if (stats.isFile()) {
3030
+ await grepFile(validPath, validPath, searchRegex, matches, effectiveMaxResults);
3031
+ } else {
3032
+ await grepDirectoryRecursive(validPath, validPath, searchRegex, fileGlobRegex, matches, effectiveMaxResults);
3033
+ }
3034
+ return {
3035
+ success: true,
3036
+ matches
3037
+ };
3038
+ } catch (error) {
3039
+ const errMsg = error instanceof Error ? error.message : String(error);
3040
+ const isPermissionError = errMsg.includes("EACCES") || errMsg.includes("permission");
3041
+ return {
3042
+ success: false,
3043
+ error: isPermissionError ? "Permission denied" : "Failed to search content"
3044
+ };
3045
+ }
3046
+ }
3047
+ async function grepFile(basePath, filePath, pattern, matches, maxResults) {
3048
+ if (matches.length >= maxResults) return;
3049
+ const relativePath = path5.relative(basePath, filePath);
3050
+ const fileStream = fs4.createReadStream(filePath, { encoding: "utf8" });
3051
+ const rl = readline.createInterface({
3052
+ input: fileStream,
3053
+ crlfDelay: Infinity
3054
+ });
3055
+ let lineNumber = 0;
3056
+ for await (const line of rl) {
3057
+ lineNumber++;
3058
+ if (matches.length >= maxResults) {
3059
+ rl.close();
3060
+ break;
3061
+ }
3062
+ if (pattern.test(line)) {
3063
+ matches.push({
3064
+ file: relativePath || path5.basename(filePath),
3065
+ line: lineNumber,
3066
+ content: line.slice(0, 500)
3067
+ // Truncate long lines
3068
+ });
3069
+ }
3070
+ }
3071
+ }
3072
+ async function grepDirectoryRecursive(basePath, currentPath, searchPattern, filePattern, matches, maxResults) {
3073
+ if (matches.length >= maxResults) return;
3074
+ const entries = await fs4.promises.readdir(currentPath, { withFileTypes: true });
3075
+ for (const entry of entries) {
3076
+ if (matches.length >= maxResults) return;
3077
+ if (entry.name.startsWith(".")) continue;
3078
+ const entryPath = path5.join(currentPath, entry.name);
3079
+ try {
3080
+ if (entry.isDirectory()) {
3081
+ await grepDirectoryRecursive(basePath, entryPath, searchPattern, filePattern, matches, maxResults);
3082
+ } else if (filePattern.test(entry.name)) {
3083
+ await grepFile(basePath, entryPath, searchPattern, matches, maxResults);
3084
+ }
3085
+ } catch {
3086
+ }
3087
+ }
3088
+ }
3089
+
3090
+ // src/daemon/handlers/exec-handler.ts
3091
+ var import_child_process3 = require("child_process");
3092
+ var DEFAULT_TIMEOUT = 3e4;
3093
+ var MAX_TIMEOUT = 3e5;
3094
+ async function handleExec(command, projectPath) {
3095
+ const {
3096
+ command: cmd,
3097
+ cwd = projectPath,
3098
+ timeout = DEFAULT_TIMEOUT,
3099
+ env = {}
3100
+ } = command;
3101
+ const effectiveTimeout = Math.min(Math.max(timeout, 1e3), MAX_TIMEOUT);
3102
+ return new Promise((resolve2) => {
3103
+ let stdout = "";
3104
+ let stderr = "";
3105
+ let timedOut = false;
3106
+ let resolved = false;
3107
+ const done = (result) => {
3108
+ if (resolved) return;
3109
+ resolved = true;
3110
+ resolve2(result);
3111
+ };
3112
+ try {
3113
+ const proc = (0, import_child_process3.spawn)(cmd, {
3114
+ shell: true,
3115
+ cwd,
3116
+ env: { ...process.env, ...env },
3117
+ stdio: ["ignore", "pipe", "pipe"]
3118
+ });
3119
+ const timeoutId = setTimeout(() => {
3120
+ timedOut = true;
3121
+ proc.kill("SIGTERM");
3122
+ setTimeout(() => {
3123
+ if (!resolved) {
3124
+ proc.kill("SIGKILL");
3125
+ }
3126
+ }, 5e3);
3127
+ }, effectiveTimeout);
3128
+ proc.stdout.on("data", (data) => {
3129
+ stdout += data.toString();
3130
+ if (stdout.length > 10 * 1024 * 1024) {
3131
+ stdout = stdout.slice(-10 * 1024 * 1024);
3132
+ }
3133
+ });
3134
+ proc.stderr.on("data", (data) => {
3135
+ stderr += data.toString();
3136
+ if (stderr.length > 10 * 1024 * 1024) {
3137
+ stderr = stderr.slice(-10 * 1024 * 1024);
3138
+ }
3139
+ });
3140
+ proc.on("close", (code, signal) => {
3141
+ clearTimeout(timeoutId);
3142
+ done({
3143
+ success: code === 0 && !timedOut,
3144
+ stdout,
3145
+ stderr,
3146
+ exitCode: code ?? -1,
3147
+ timedOut,
3148
+ error: timedOut ? `Command timed out after ${effectiveTimeout}ms` : code !== 0 ? `Command exited with code ${code}` : void 0
3149
+ });
3150
+ });
3151
+ proc.on("error", (error) => {
3152
+ clearTimeout(timeoutId);
3153
+ done({
3154
+ success: false,
3155
+ stdout,
3156
+ stderr,
3157
+ exitCode: -1,
3158
+ error: `Failed to execute command: ${error.message}`
3159
+ });
3160
+ });
3161
+ } catch (error) {
3162
+ done({
3163
+ success: false,
3164
+ stdout: "",
3165
+ stderr: "",
3166
+ exitCode: -1,
3167
+ error: `Failed to spawn process: ${error instanceof Error ? error.message : String(error)}`
3168
+ });
3169
+ }
3170
+ });
3171
+ }
3172
+
3173
+ // src/daemon/daemon-process.ts
3174
+ var fs5 = __toESM(require("fs"));
3175
+ var os3 = __toESM(require("os"));
3176
+ var path6 = __toESM(require("path"));
3177
+ var packageJson = require_package();
3178
+ var Daemon = class {
3179
+ constructor() {
3180
+ this.machineId = "";
3181
+ this.deviceId = null;
3182
+ // EP726: Cached device UUID from server
3183
+ this.deviceName = null;
3184
+ // EP661: Cached device name from server
3185
+ this.flyMachineId = null;
3186
+ this.identityServer = null;
3187
+ // EP803: Local identity server for browser detection
3188
+ this.connections = /* @__PURE__ */ new Map();
3189
+ // projectPath -> connection
3190
+ // EP701: Track which connections are currently live (WebSocket open)
3191
+ // Updated by 'auth_success' (add) and 'disconnected' (remove) events
3192
+ this.liveConnections = /* @__PURE__ */ new Set();
3193
+ // projectPath
3194
+ this.shuttingDown = false;
3195
+ this.ipcServer = new IPCServer();
3196
+ }
3197
+ /**
3198
+ * Start the daemon
3199
+ */
3200
+ async start() {
3201
+ console.log("[Daemon] Starting Episoda daemon...");
3202
+ this.machineId = await getMachineId();
3203
+ console.log(`[Daemon] Machine ID: ${this.machineId}`);
3204
+ const config = await (0, import_core5.loadConfig)();
3205
+ if (config?.device_id) {
3206
+ this.deviceId = config.device_id;
3207
+ console.log(`[Daemon] Loaded cached Device ID (UUID): ${this.deviceId}`);
3208
+ }
3209
+ await this.ipcServer.start();
3210
+ console.log("[Daemon] IPC server started");
3211
+ this.identityServer = new IdentityServer(this.machineId);
3212
+ await this.identityServer.start();
3213
+ this.registerIPCHandlers();
3214
+ await this.restoreConnections();
3215
+ this.setupShutdownHandlers();
3216
+ console.log("[Daemon] Daemon started successfully");
3217
+ this.checkAndNotifyUpdates();
3218
+ }
3219
+ /**
3220
+ * EP783: Check for CLI updates and auto-update in background
3221
+ * Non-blocking - runs after daemon starts, fails silently on errors
3222
+ */
3223
+ async checkAndNotifyUpdates() {
3224
+ try {
3225
+ const result = await checkForUpdates(packageJson.version);
3226
+ if (result.updateAvailable) {
3227
+ console.log(`
3228
+ \u2B06\uFE0F Update available: ${result.currentVersion} \u2192 ${result.latestVersion}`);
3229
+ console.log(" Updating in background...\n");
3230
+ performBackgroundUpdate();
3231
+ }
3232
+ } catch (error) {
3233
+ }
3234
+ }
3235
+ // EP738: Removed startHttpServer - device info now flows through WebSocket broadcast + database
3236
+ /**
3237
+ * Register IPC command handlers
3238
+ */
3239
+ registerIPCHandlers() {
3240
+ this.ipcServer.on("ping", async () => {
3241
+ return { status: "ok" };
3242
+ });
3243
+ this.ipcServer.on("status", async () => {
3244
+ const projects = getAllProjects().map((p) => ({
3245
+ id: p.id,
3246
+ path: p.path,
3247
+ name: p.name,
3248
+ connected: this.liveConnections.has(p.path)
3249
+ }));
3250
+ return {
3251
+ running: true,
3252
+ machineId: this.machineId,
3253
+ deviceId: this.deviceId,
3254
+ // EP726: UUID for unified device identification
3255
+ hostname: os3.hostname(),
3256
+ platform: os3.platform(),
3257
+ arch: os3.arch(),
3258
+ projects
3259
+ };
3260
+ });
3261
+ this.ipcServer.on("add-project", async (params) => {
3262
+ const { projectId, projectPath } = params;
3263
+ addProject(projectId, projectPath);
3264
+ try {
3265
+ await this.connectProject(projectId, projectPath);
3266
+ const isHealthy = this.isConnectionHealthy(projectPath);
3267
+ if (!isHealthy) {
3268
+ console.warn(`[Daemon] Connection completed but not healthy for ${projectPath}`);
3269
+ return { success: false, connected: false, error: "Connection established but not healthy" };
3270
+ }
3271
+ return { success: true, connected: true };
3272
+ } catch (error) {
3273
+ const errorMessage = error instanceof Error ? error.message : String(error);
3274
+ return { success: false, connected: false, error: errorMessage };
3275
+ }
3276
+ });
3277
+ this.ipcServer.on("remove-project", async (params) => {
3278
+ const { projectPath } = params;
3279
+ await this.disconnectProject(projectPath);
3280
+ removeProject(projectPath);
3281
+ return { success: true };
3282
+ });
3283
+ this.ipcServer.on("connect-project", async (params) => {
3284
+ const { projectPath } = params;
3285
+ const project = getAllProjects().find((p) => p.path === projectPath);
3286
+ if (!project) {
3287
+ throw new Error("Project not tracked");
3288
+ }
3289
+ await this.connectProject(project.id, projectPath);
3290
+ return { success: true };
3291
+ });
3292
+ this.ipcServer.on("disconnect-project", async (params) => {
3293
+ const { projectPath } = params;
3294
+ await this.disconnectProject(projectPath);
3295
+ return { success: true };
3296
+ });
3297
+ this.ipcServer.on("shutdown", async () => {
3298
+ console.log("[Daemon] Shutdown requested via IPC");
3299
+ await this.shutdown();
3300
+ return { success: true };
3301
+ });
3302
+ this.ipcServer.on("verify-health", async () => {
3303
+ const projects = getAllProjects().map((p) => ({
3304
+ id: p.id,
3305
+ path: p.path,
3306
+ name: p.name,
3307
+ inConnectionsMap: this.connections.has(p.path),
3308
+ inLiveConnections: this.liveConnections.has(p.path),
3309
+ isHealthy: this.isConnectionHealthy(p.path)
3310
+ }));
3311
+ const healthyCount = projects.filter((p) => p.isHealthy).length;
3312
+ const staleCount = projects.filter((p) => p.inConnectionsMap && !p.inLiveConnections).length;
3313
+ return {
3314
+ totalProjects: projects.length,
3315
+ healthyConnections: healthyCount,
3316
+ staleConnections: staleCount,
3317
+ projects
3318
+ };
3319
+ });
3320
+ }
3321
+ /**
3322
+ * Restore WebSocket connections for tracked projects
3323
+ */
3324
+ async restoreConnections() {
3325
+ const projects = getAllProjects();
3326
+ for (const project of projects) {
3327
+ try {
3328
+ await this.connectProject(project.id, project.path);
3329
+ } catch (error) {
3330
+ console.error(`[Daemon] Failed to restore connection for ${project.name}:`, error);
3331
+ }
3332
+ }
3333
+ }
3334
+ /**
3335
+ * EP805: Check if a connection is healthy (exists AND is live)
3336
+ * A connection can exist in the Map but be dead if WebSocket disconnected
3337
+ */
3338
+ isConnectionHealthy(projectPath) {
3339
+ return this.connections.has(projectPath) && this.liveConnections.has(projectPath);
3340
+ }
3341
+ /**
3342
+ * Connect to a project's WebSocket
3343
+ */
3344
+ async connectProject(projectId, projectPath) {
3345
+ if (this.connections.has(projectPath)) {
3346
+ if (this.liveConnections.has(projectPath)) {
3347
+ console.log(`[Daemon] Already connected to ${projectPath}`);
3348
+ return;
3349
+ }
3350
+ console.warn(`[Daemon] Stale connection detected for ${projectPath}, forcing reconnection`);
3351
+ await this.disconnectProject(projectPath);
3352
+ }
3353
+ const config = await (0, import_core5.loadConfig)();
3354
+ if (!config || !config.access_token) {
3355
+ throw new Error("No access token found. Please run: episoda auth");
3356
+ }
3357
+ let serverUrl = config.api_url || process.env.EPISODA_API_URL || "https://episoda.dev";
3358
+ if (config.project_settings?.local_server_url) {
3359
+ serverUrl = config.project_settings.local_server_url;
3360
+ console.log(`[Daemon] Using cached server URL: ${serverUrl}`);
3361
+ }
3362
+ const serverUrlObj = new URL(serverUrl);
3363
+ const wsProtocol = serverUrlObj.protocol === "https:" ? "wss:" : "ws:";
3364
+ const wsPort = process.env.EPISODA_WS_PORT || "3001";
3365
+ const wsUrl = `${wsProtocol}//${serverUrlObj.hostname}:${wsPort}`;
3366
+ console.log(`[Daemon] Connecting to ${wsUrl} for project ${projectId}...`);
3367
+ const client = new import_core5.EpisodaClient();
3368
+ const gitExecutor = new import_core5.GitExecutor();
3369
+ const connection = {
3370
+ projectId,
3371
+ projectPath,
3372
+ client,
3373
+ gitExecutor
3374
+ };
3375
+ this.connections.set(projectPath, connection);
3376
+ client.on("command", async (message) => {
3377
+ if (message.type === "command" && message.command) {
3378
+ console.log(`[Daemon] Received command for ${projectId}:`, message.command);
3379
+ client.updateActivity();
3380
+ try {
3381
+ const result = await gitExecutor.execute(message.command, {
3382
+ cwd: projectPath
3383
+ });
3384
+ await client.send({
3385
+ type: "result",
3386
+ commandId: message.id,
3387
+ result
3388
+ });
3389
+ console.log(`[Daemon] Command completed for ${projectId}:`, result.success ? "success" : "failed");
3390
+ } catch (error) {
3391
+ await client.send({
3392
+ type: "result",
3393
+ commandId: message.id,
3394
+ result: {
3395
+ success: false,
3396
+ error: "UNKNOWN_ERROR",
3397
+ output: error instanceof Error ? error.message : String(error)
3398
+ }
3399
+ });
3400
+ console.error(`[Daemon] Command execution error for ${projectId}:`, error);
3401
+ }
3402
+ }
3403
+ });
3404
+ client.on("remote_command", async (message) => {
3405
+ if (message.type === "remote_command" && message.command) {
3406
+ const cmd = message.command;
3407
+ console.log(`[Daemon] Received remote command for ${projectId}:`, cmd.action);
3408
+ client.updateActivity();
3409
+ try {
3410
+ let result;
3411
+ switch (cmd.action) {
3412
+ case "file:read":
3413
+ result = await handleFileRead(cmd, projectPath);
3414
+ break;
3415
+ case "file:write":
3416
+ result = await handleFileWrite(cmd, projectPath);
3417
+ break;
3418
+ case "file:list":
3419
+ result = await handleFileList(cmd, projectPath);
3420
+ break;
3421
+ case "file:search":
3422
+ result = await handleFileSearch(cmd, projectPath);
3423
+ break;
3424
+ case "file:grep":
3425
+ result = await handleFileGrep(cmd, projectPath);
3426
+ break;
3427
+ case "exec":
3428
+ result = await handleExec(cmd, projectPath);
3429
+ break;
3430
+ default:
3431
+ result = {
3432
+ success: false,
3433
+ error: `Unknown remote command action: ${cmd.action}`
3434
+ };
3435
+ }
3436
+ await client.send({
3437
+ type: "remote_result",
3438
+ commandId: message.id,
3439
+ result
3440
+ });
3441
+ console.log(`[Daemon] Remote command ${cmd.action} completed for ${projectId}:`, result.success ? "success" : "failed");
3442
+ } catch (error) {
3443
+ await client.send({
3444
+ type: "remote_result",
3445
+ commandId: message.id,
3446
+ result: {
3447
+ success: false,
3448
+ error: error instanceof Error ? error.message : String(error)
3449
+ }
3450
+ });
3451
+ console.error(`[Daemon] Remote command execution error for ${projectId}:`, error);
3452
+ }
3453
+ }
3454
+ });
3455
+ client.on("shutdown", async (message) => {
3456
+ const shutdownMessage = message;
3457
+ const reason = shutdownMessage.reason || "unknown";
3458
+ console.log(`[Daemon] Received shutdown request from server for ${projectId}`);
3459
+ console.log(`[Daemon] Reason: ${reason} - ${shutdownMessage.message || "No message"}`);
3460
+ if (reason === "user_requested") {
3461
+ console.log(`[Daemon] User requested disconnect, shutting down...`);
3462
+ await this.cleanupAndExit();
3463
+ } else {
3464
+ console.log(`[Daemon] Server shutdown (${reason}), will reconnect automatically...`);
3465
+ }
3466
+ });
3467
+ client.on("auth_success", async (message) => {
3468
+ console.log(`[Daemon] Authenticated for project ${projectId}`);
3469
+ touchProject(projectPath);
3470
+ this.liveConnections.add(projectPath);
3471
+ if (this.identityServer) {
3472
+ this.identityServer.setConnected(true);
3473
+ }
3474
+ const authMessage = message;
3475
+ if (authMessage.userId && authMessage.workspaceId) {
3476
+ await this.configureGitUser(projectPath, authMessage.userId, authMessage.workspaceId, this.machineId, projectId, authMessage.deviceId);
3477
+ await this.installGitHooks(projectPath);
3478
+ }
3479
+ if (authMessage.deviceName) {
3480
+ this.deviceName = authMessage.deviceName;
3481
+ console.log(`[Daemon] Device name: ${this.deviceName}`);
3482
+ }
3483
+ if (authMessage.deviceId) {
3484
+ this.deviceId = authMessage.deviceId;
3485
+ console.log(`[Daemon] Device ID (UUID): ${this.deviceId}`);
3486
+ await this.cacheDeviceId(authMessage.deviceId);
3487
+ }
3488
+ if (authMessage.flyMachineId) {
3489
+ this.flyMachineId = authMessage.flyMachineId;
3490
+ console.log(`[Daemon] Fly Machine ID: ${this.flyMachineId}`);
3491
+ }
3492
+ });
3493
+ client.on("error", (message) => {
3494
+ console.error(`[Daemon] Server error for ${projectId}:`, message);
3495
+ });
3496
+ client.on("disconnected", (event) => {
3497
+ const disconnectEvent = event;
3498
+ console.log(`[Daemon] Connection closed for ${projectId}: code=${disconnectEvent.code}, willReconnect=${disconnectEvent.willReconnect}`);
3499
+ this.liveConnections.delete(projectPath);
3500
+ if (this.identityServer && this.liveConnections.size === 0) {
3501
+ this.identityServer.setConnected(false);
3502
+ }
3503
+ if (!disconnectEvent.willReconnect) {
3504
+ this.connections.delete(projectPath);
3505
+ console.log(`[Daemon] Removed connection for ${projectPath} from map`);
3506
+ }
3507
+ });
3508
+ try {
3509
+ let daemonPid;
3510
+ try {
3511
+ const pidPath = getPidFilePath();
3512
+ if (fs5.existsSync(pidPath)) {
3513
+ const pidStr = fs5.readFileSync(pidPath, "utf-8").trim();
3514
+ daemonPid = parseInt(pidStr, 10);
3515
+ }
3516
+ } catch (pidError) {
3517
+ console.warn(`[Daemon] Could not read daemon PID:`, pidError instanceof Error ? pidError.message : pidError);
3518
+ }
3519
+ await client.connect(wsUrl, config.access_token, this.machineId, {
3520
+ hostname: os3.hostname(),
3521
+ osPlatform: os3.platform(),
3522
+ osArch: os3.arch(),
3523
+ daemonPid
3524
+ });
3525
+ console.log(`[Daemon] Successfully connected to project ${projectId}`);
3526
+ } catch (error) {
3527
+ console.error(`[Daemon] Failed to connect to ${projectId}:`, error);
3528
+ this.connections.delete(projectPath);
3529
+ throw error;
3530
+ }
3531
+ }
3532
+ /**
3533
+ * Disconnect from a project's WebSocket
3534
+ */
3535
+ async disconnectProject(projectPath) {
3536
+ const connection = this.connections.get(projectPath);
3537
+ if (!connection) {
3538
+ return;
3539
+ }
3540
+ if (connection.reconnectTimer) {
3541
+ clearTimeout(connection.reconnectTimer);
3542
+ }
3543
+ await connection.client.disconnect();
3544
+ this.connections.delete(projectPath);
3545
+ this.liveConnections.delete(projectPath);
3546
+ console.log(`[Daemon] Disconnected from ${projectPath}`);
3547
+ }
3548
+ /**
3549
+ * Setup graceful shutdown handlers
3550
+ * EP613: Now uses cleanupAndExit to ensure PID file is removed
3551
+ */
3552
+ setupShutdownHandlers() {
3553
+ const shutdownHandler = async (signal) => {
3554
+ console.log(`[Daemon] Received ${signal}, shutting down...`);
3555
+ await this.cleanupAndExit();
3556
+ };
3557
+ process.on("SIGTERM", () => shutdownHandler("SIGTERM"));
3558
+ process.on("SIGINT", () => shutdownHandler("SIGINT"));
3559
+ }
3560
+ /**
3561
+ * EP595: Configure git with user and workspace ID for post-checkout hook
3562
+ * EP655: Added machineId for device isolation in multi-device environments
3563
+ * EP725: Added projectId for main branch badge tracking
3564
+ * EP726: Added deviceId (UUID) for unified device identification
3565
+ *
3566
+ * This stores the IDs in .git/config so the post-checkout hook can
3567
+ * update module.checkout_* fields when git operations happen from terminal.
3568
+ */
3569
+ async configureGitUser(projectPath, userId, workspaceId, machineId, projectId, deviceId) {
3570
+ try {
3571
+ const { execSync: execSync2 } = await import("child_process");
3572
+ execSync2(`git config episoda.userId ${userId}`, {
3573
+ cwd: projectPath,
3574
+ encoding: "utf8",
3575
+ stdio: "pipe"
3576
+ });
3577
+ execSync2(`git config episoda.workspaceId ${workspaceId}`, {
3578
+ cwd: projectPath,
3579
+ encoding: "utf8",
3580
+ stdio: "pipe"
3581
+ });
3582
+ execSync2(`git config episoda.machineId ${machineId}`, {
3583
+ cwd: projectPath,
3584
+ encoding: "utf8",
3585
+ stdio: "pipe"
3586
+ });
3587
+ execSync2(`git config episoda.projectId ${projectId}`, {
3588
+ cwd: projectPath,
3589
+ encoding: "utf8",
3590
+ stdio: "pipe"
3591
+ });
3592
+ if (deviceId) {
3593
+ execSync2(`git config episoda.deviceId ${deviceId}`, {
3594
+ cwd: projectPath,
3595
+ encoding: "utf8",
3596
+ stdio: "pipe"
3597
+ });
3598
+ }
3599
+ console.log(`[Daemon] Configured git for project: episoda.userId=${userId}, machineId=${machineId}, projectId=${projectId}${deviceId ? `, deviceId=${deviceId}` : ""}`);
3600
+ } catch (error) {
3601
+ console.warn(`[Daemon] Failed to configure git user for ${projectPath}:`, error instanceof Error ? error.message : error);
3602
+ }
3603
+ }
3604
+ /**
3605
+ * EP610: Install git hooks from bundled files
3606
+ *
3607
+ * Installs post-checkout and pre-commit hooks to enable:
3608
+ * - Branch tracking (post-checkout updates module.checkout_* fields)
3609
+ * - Main branch protection (pre-commit blocks direct commits to main)
3610
+ */
3611
+ async installGitHooks(projectPath) {
3612
+ const hooks = ["post-checkout", "pre-commit"];
3613
+ const hooksDir = path6.join(projectPath, ".git", "hooks");
3614
+ if (!fs5.existsSync(hooksDir)) {
3615
+ console.warn(`[Daemon] Hooks directory not found: ${hooksDir}`);
3616
+ return;
3617
+ }
3618
+ for (const hookName of hooks) {
3619
+ try {
3620
+ const hookPath = path6.join(hooksDir, hookName);
3621
+ const bundledHookPath = path6.join(__dirname, "..", "hooks", hookName);
3622
+ if (!fs5.existsSync(bundledHookPath)) {
3623
+ console.warn(`[Daemon] Bundled hook not found: ${bundledHookPath}`);
3624
+ continue;
3625
+ }
3626
+ const hookContent = fs5.readFileSync(bundledHookPath, "utf-8");
3627
+ if (fs5.existsSync(hookPath)) {
3628
+ const existingContent = fs5.readFileSync(hookPath, "utf-8");
3629
+ if (existingContent === hookContent) {
3630
+ continue;
3631
+ }
3632
+ }
3633
+ fs5.writeFileSync(hookPath, hookContent, { mode: 493 });
3634
+ console.log(`[Daemon] Installed git hook: ${hookName}`);
3635
+ } catch (error) {
3636
+ console.warn(`[Daemon] Failed to install ${hookName} hook:`, error instanceof Error ? error.message : error);
3637
+ }
3638
+ }
3639
+ }
3640
+ /**
3641
+ * EP726: Cache device UUID to config file
3642
+ *
3643
+ * Persists the device_id (UUID) received from the server so it's available
3644
+ * on daemon restart without needing to re-register the device.
3645
+ */
3646
+ async cacheDeviceId(deviceId) {
3647
+ try {
3648
+ const config = await (0, import_core5.loadConfig)();
3649
+ if (!config) {
3650
+ console.warn("[Daemon] Cannot cache device ID - no config found");
3651
+ return;
3652
+ }
3653
+ if (config.device_id === deviceId) {
3654
+ return;
3655
+ }
3656
+ const updatedConfig = {
3657
+ ...config,
3658
+ device_id: deviceId,
3659
+ machine_id: this.machineId
3660
+ };
3661
+ await (0, import_core5.saveConfig)(updatedConfig);
3662
+ console.log(`[Daemon] Cached device ID to config: ${deviceId}`);
3663
+ } catch (error) {
3664
+ console.warn("[Daemon] Failed to cache device ID:", error instanceof Error ? error.message : error);
3665
+ }
3666
+ }
3667
+ /**
3668
+ * Gracefully shutdown daemon
3669
+ */
3670
+ async shutdown() {
3671
+ if (this.shuttingDown) return;
3672
+ this.shuttingDown = true;
3673
+ console.log("[Daemon] Shutting down...");
3674
+ for (const [projectPath, connection] of this.connections) {
3675
+ if (connection.reconnectTimer) {
3676
+ clearTimeout(connection.reconnectTimer);
3677
+ }
3678
+ await connection.client.disconnect();
3679
+ }
3680
+ this.connections.clear();
3681
+ if (this.identityServer) {
3682
+ await this.identityServer.stop();
3683
+ this.identityServer = null;
3684
+ }
3685
+ await this.ipcServer.stop();
3686
+ console.log("[Daemon] Shutdown complete");
3687
+ }
3688
+ /**
3689
+ * EP613: Clean up PID file and exit gracefully
3690
+ * Called when user explicitly requests disconnect
3691
+ */
3692
+ async cleanupAndExit() {
3693
+ await this.shutdown();
3694
+ try {
3695
+ const pidPath = getPidFilePath();
3696
+ if (fs5.existsSync(pidPath)) {
3697
+ fs5.unlinkSync(pidPath);
3698
+ console.log("[Daemon] PID file cleaned up");
3699
+ }
3700
+ } catch (error) {
3701
+ console.error("[Daemon] Failed to clean up PID file:", error);
3702
+ }
3703
+ console.log("[Daemon] Exiting...");
3704
+ process.exit(0);
3705
+ }
3706
+ };
3707
+ async function main() {
3708
+ if (!process.env.EPISODA_DAEMON_MODE) {
3709
+ console.error("This script should only be run by daemon-manager");
3710
+ process.exit(1);
3711
+ }
3712
+ const daemon = new Daemon();
3713
+ await daemon.start();
3714
+ await new Promise(() => {
3715
+ });
3716
+ }
3717
+ main().catch((error) => {
3718
+ console.error("[Daemon] Fatal error:", error);
3719
+ process.exit(1);
3720
+ });
3721
+ //# sourceMappingURL=daemon-process.js.map