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