episoda 0.2.3 → 0.2.7

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