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