codex-webstrapper 0.1.5 → 0.2.5
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/bin/codex-webstrap.sh +48 -2
- package/package.json +2 -1
- package/src/bridge-shim.js +12 -0
- package/src/bun-pty-bridge.mjs +161 -0
- package/src/message-router.mjs +1303 -68
- package/src/server.mjs +25 -0
package/src/message-router.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
1
|
+
import { spawn as childSpawn, spawnSync as childSpawnSync } from "node:child_process";
|
|
2
2
|
import { Worker } from "node:worker_threads";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { readFileSync } from "node:fs";
|
|
5
|
+
import { readFileSync, statSync } from "node:fs";
|
|
6
6
|
import fs from "node:fs/promises";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
7
8
|
|
|
8
9
|
import { createLogger, randomId, safeJsonParse, toErrorMessage } from "./util.mjs";
|
|
9
10
|
|
|
@@ -76,6 +77,11 @@ class TerminalRegistry {
|
|
|
76
77
|
this.sendToWs = sendToWs;
|
|
77
78
|
this.logger = logger;
|
|
78
79
|
this.sessions = new Map();
|
|
80
|
+
this.bunPtyAvailable = this._detectBunPtyAvailability();
|
|
81
|
+
this.loggedBunPtyFailure = false;
|
|
82
|
+
this.pythonPtyAvailable = this._detectPythonPtyAvailability();
|
|
83
|
+
this.loggedPythonPtyFailure = false;
|
|
84
|
+
this.hasPtyLikeRuntime = this.bunPtyAvailable || this.pythonPtyAvailable;
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
createOrAttach(ws, message) {
|
|
@@ -83,41 +89,80 @@ class TerminalRegistry {
|
|
|
83
89
|
const existing = this.sessions.get(sessionId);
|
|
84
90
|
if (existing) {
|
|
85
91
|
existing.listeners.add(ws);
|
|
86
|
-
this.sendToWs(ws, {
|
|
92
|
+
this.sendToWs(ws, {
|
|
93
|
+
type: "terminal-attached",
|
|
94
|
+
sessionId,
|
|
95
|
+
cwd: existing.cwd,
|
|
96
|
+
shell: existing.shell
|
|
97
|
+
});
|
|
98
|
+
this.sendToWs(ws, {
|
|
99
|
+
type: "terminal-init-log",
|
|
100
|
+
sessionId,
|
|
101
|
+
log: existing.attachLog
|
|
102
|
+
});
|
|
103
|
+
if (this._hasExplicitDimensions(message)) {
|
|
104
|
+
this.resize(sessionId, message.cols, message.rows);
|
|
105
|
+
}
|
|
87
106
|
return;
|
|
88
107
|
}
|
|
89
108
|
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
109
|
+
const launchConfig = this._resolveLaunchConfig(message);
|
|
110
|
+
let runtime;
|
|
111
|
+
try {
|
|
112
|
+
runtime = this._spawnRuntime(launchConfig, message);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
this.sendToWs(ws, {
|
|
115
|
+
type: "terminal-error",
|
|
116
|
+
sessionId,
|
|
117
|
+
message: toErrorMessage(error)
|
|
118
|
+
});
|
|
119
|
+
this.logger.warn("Terminal spawn failed", {
|
|
120
|
+
sessionId,
|
|
121
|
+
error: toErrorMessage(error)
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
94
125
|
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
});
|
|
126
|
+
const attachLogLines = [
|
|
127
|
+
`Terminal attached via codex-webstrapper (${runtime.mode})`
|
|
128
|
+
];
|
|
129
|
+
if (launchConfig.cwdWasFallback && launchConfig.requestedCwd) {
|
|
130
|
+
attachLogLines.push(`[webstrap] Requested cwd unavailable: ${launchConfig.requestedCwd}`);
|
|
131
|
+
attachLogLines.push(`[webstrap] Using cwd: ${launchConfig.cwd}`);
|
|
132
|
+
}
|
|
133
|
+
const attachLog = `${attachLogLines.join("\r\n")}\r\n`;
|
|
104
134
|
|
|
105
135
|
const session = {
|
|
106
136
|
sessionId,
|
|
107
|
-
|
|
108
|
-
|
|
137
|
+
listeners: new Set([ws]),
|
|
138
|
+
cwd: launchConfig.cwd,
|
|
139
|
+
shell: launchConfig.shell,
|
|
140
|
+
attachLog,
|
|
141
|
+
cols: runtime.cols ?? this._coerceDimension(message?.cols, 120),
|
|
142
|
+
rows: runtime.rows ?? this._coerceDimension(message?.rows, 30),
|
|
143
|
+
...runtime
|
|
109
144
|
};
|
|
110
145
|
|
|
111
146
|
this.sessions.set(sessionId, session);
|
|
112
147
|
|
|
113
|
-
this.sendToWs(ws, {
|
|
148
|
+
this.sendToWs(ws, {
|
|
149
|
+
type: "terminal-attached",
|
|
150
|
+
sessionId,
|
|
151
|
+
cwd: session.cwd,
|
|
152
|
+
shell: session.shell
|
|
153
|
+
});
|
|
114
154
|
this.sendToWs(ws, {
|
|
115
155
|
type: "terminal-init-log",
|
|
116
156
|
sessionId,
|
|
117
|
-
log:
|
|
157
|
+
log: session.attachLog
|
|
118
158
|
});
|
|
119
159
|
|
|
120
|
-
|
|
160
|
+
if (session.mode === "bun-pty") {
|
|
161
|
+
this._attachBunPtyProcess(sessionId, session);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
session.proc.stdout?.on("data", (chunk) => {
|
|
121
166
|
this._broadcast(sessionId, {
|
|
122
167
|
type: "terminal-data",
|
|
123
168
|
sessionId,
|
|
@@ -125,7 +170,7 @@ class TerminalRegistry {
|
|
|
125
170
|
});
|
|
126
171
|
});
|
|
127
172
|
|
|
128
|
-
proc.stderr?.on("data", (chunk) => {
|
|
173
|
+
session.proc.stderr?.on("data", (chunk) => {
|
|
129
174
|
this._broadcast(sessionId, {
|
|
130
175
|
type: "terminal-data",
|
|
131
176
|
sessionId,
|
|
@@ -133,7 +178,7 @@ class TerminalRegistry {
|
|
|
133
178
|
});
|
|
134
179
|
});
|
|
135
180
|
|
|
136
|
-
proc.on("error", (error) => {
|
|
181
|
+
session.proc.on("error", (error) => {
|
|
137
182
|
this._broadcast(sessionId, {
|
|
138
183
|
type: "terminal-error",
|
|
139
184
|
sessionId,
|
|
@@ -141,7 +186,7 @@ class TerminalRegistry {
|
|
|
141
186
|
});
|
|
142
187
|
});
|
|
143
188
|
|
|
144
|
-
proc.on("exit", (code, signal) => {
|
|
189
|
+
session.proc.on("exit", (code, signal) => {
|
|
145
190
|
this._broadcast(sessionId, {
|
|
146
191
|
type: "terminal-exit",
|
|
147
192
|
sessionId,
|
|
@@ -154,17 +199,72 @@ class TerminalRegistry {
|
|
|
154
199
|
|
|
155
200
|
write(sessionId, data) {
|
|
156
201
|
const session = this.sessions.get(sessionId);
|
|
157
|
-
if (!session
|
|
202
|
+
if (!session) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (session.mode === "bun-pty") {
|
|
207
|
+
try {
|
|
208
|
+
this._writeToBunPty(session, {
|
|
209
|
+
type: "write",
|
|
210
|
+
data
|
|
211
|
+
});
|
|
212
|
+
} catch (error) {
|
|
213
|
+
this._broadcast(sessionId, {
|
|
214
|
+
type: "terminal-error",
|
|
215
|
+
sessionId,
|
|
216
|
+
message: toErrorMessage(error)
|
|
217
|
+
});
|
|
218
|
+
}
|
|
158
219
|
return;
|
|
159
220
|
}
|
|
160
|
-
|
|
221
|
+
|
|
222
|
+
if (!session.proc.stdin || session.proc.stdin.destroyed) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
session.proc.stdin.write(data);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
this._broadcast(sessionId, {
|
|
230
|
+
type: "terminal-error",
|
|
231
|
+
sessionId,
|
|
232
|
+
message: toErrorMessage(error)
|
|
233
|
+
});
|
|
234
|
+
}
|
|
161
235
|
}
|
|
162
236
|
|
|
163
|
-
resize(sessionId) {
|
|
164
|
-
|
|
237
|
+
resize(sessionId, cols, rows) {
|
|
238
|
+
const session = this.sessions.get(sessionId);
|
|
239
|
+
if (!session) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (session.mode === "bun-pty") {
|
|
244
|
+
try {
|
|
245
|
+
const nextCols = this._coerceDimension(cols, session.cols || 120);
|
|
246
|
+
const nextRows = this._coerceDimension(rows, session.rows || 30);
|
|
247
|
+
this._writeToBunPty(session, {
|
|
248
|
+
type: "resize",
|
|
249
|
+
cols: nextCols,
|
|
250
|
+
rows: nextRows
|
|
251
|
+
});
|
|
252
|
+
session.cols = nextCols;
|
|
253
|
+
session.rows = nextRows;
|
|
254
|
+
} catch (error) {
|
|
255
|
+
this._broadcast(sessionId, {
|
|
256
|
+
type: "terminal-error",
|
|
257
|
+
sessionId,
|
|
258
|
+
message: toErrorMessage(error)
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (session.mode !== "python-pty") {
|
|
265
|
+
this.logger.debug("Terminal resize ignored (non-PTY mode)", { sessionId });
|
|
165
266
|
return;
|
|
166
267
|
}
|
|
167
|
-
this.logger.debug("Terminal resize ignored (non-PTY mode)", { sessionId });
|
|
168
268
|
}
|
|
169
269
|
|
|
170
270
|
close(sessionId) {
|
|
@@ -173,6 +273,24 @@ class TerminalRegistry {
|
|
|
173
273
|
return;
|
|
174
274
|
}
|
|
175
275
|
|
|
276
|
+
if (session.mode === "bun-pty") {
|
|
277
|
+
try {
|
|
278
|
+
this._writeToBunPty(session, {
|
|
279
|
+
type: "close"
|
|
280
|
+
});
|
|
281
|
+
} catch (error) {
|
|
282
|
+
this.logger.debug("Bun PTY close command failed", {
|
|
283
|
+
sessionId,
|
|
284
|
+
error: toErrorMessage(error)
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
if (!session.proc.killed) {
|
|
288
|
+
session.proc.kill();
|
|
289
|
+
}
|
|
290
|
+
this.sessions.delete(sessionId);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
176
294
|
if (!session.proc.killed) {
|
|
177
295
|
session.proc.kill();
|
|
178
296
|
}
|
|
@@ -204,6 +322,353 @@ class TerminalRegistry {
|
|
|
204
322
|
this.sendToWs(listener, message);
|
|
205
323
|
}
|
|
206
324
|
}
|
|
325
|
+
|
|
326
|
+
_resolveLaunchConfig(message) {
|
|
327
|
+
const requestedCwd = typeof message?.cwd === "string" ? message.cwd.trim() : "";
|
|
328
|
+
const cwdResult = this._resolveCwd(requestedCwd);
|
|
329
|
+
|
|
330
|
+
const commandFromMessage = Array.isArray(message?.command)
|
|
331
|
+
? message.command.filter((entry) => typeof entry === "string" && entry.length > 0)
|
|
332
|
+
: [];
|
|
333
|
+
|
|
334
|
+
let command = commandFromMessage;
|
|
335
|
+
let shell = null;
|
|
336
|
+
|
|
337
|
+
if (command.length === 0) {
|
|
338
|
+
shell = this._resolveShellPath(message?.shell);
|
|
339
|
+
command = this._defaultShellCommand(shell);
|
|
340
|
+
} else {
|
|
341
|
+
shell = command[0];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
command,
|
|
346
|
+
shell,
|
|
347
|
+
cwd: cwdResult.cwd,
|
|
348
|
+
requestedCwd: cwdResult.requestedCwd,
|
|
349
|
+
cwdWasFallback: cwdResult.cwdWasFallback,
|
|
350
|
+
env: this._buildEnv(message?.env)
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
_spawnRuntime(launchConfig, message) {
|
|
355
|
+
const bunPtyRuntime = this._spawnBunPtyRuntime(launchConfig, message);
|
|
356
|
+
if (bunPtyRuntime) {
|
|
357
|
+
return bunPtyRuntime;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const pythonPtyRuntime = this._spawnPythonPtyRuntime(launchConfig);
|
|
361
|
+
if (pythonPtyRuntime) {
|
|
362
|
+
return pythonPtyRuntime;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const [bin, ...args] = launchConfig.command;
|
|
366
|
+
const proc = childSpawn(bin, args, {
|
|
367
|
+
cwd: launchConfig.cwd,
|
|
368
|
+
env: launchConfig.env,
|
|
369
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
mode: "pipe",
|
|
374
|
+
proc,
|
|
375
|
+
cols: this._coerceDimension(message?.cols, 120),
|
|
376
|
+
rows: this._coerceDimension(message?.rows, 30)
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
_spawnBunPtyRuntime(launchConfig, message) {
|
|
381
|
+
if (!this.bunPtyAvailable) {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const [bin, ...args] = launchConfig.command;
|
|
386
|
+
try {
|
|
387
|
+
const bridgePath = fileURLToPath(new URL("./bun-pty-bridge.mjs", import.meta.url));
|
|
388
|
+
const initialCols = this._coerceDimension(message?.cols, 120);
|
|
389
|
+
const initialRows = this._coerceDimension(message?.rows, 30);
|
|
390
|
+
const config = JSON.stringify({
|
|
391
|
+
file: bin,
|
|
392
|
+
args,
|
|
393
|
+
cwd: launchConfig.cwd,
|
|
394
|
+
env: launchConfig.env,
|
|
395
|
+
cols: initialCols,
|
|
396
|
+
rows: initialRows,
|
|
397
|
+
term: launchConfig.env.TERM || "xterm-256color"
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const proc = childSpawn("bun", [bridgePath], {
|
|
401
|
+
cwd: launchConfig.cwd,
|
|
402
|
+
env: {
|
|
403
|
+
...launchConfig.env,
|
|
404
|
+
CODEX_WEBSTRAP_BUN_PTY_CONFIG: config
|
|
405
|
+
},
|
|
406
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
mode: "bun-pty",
|
|
411
|
+
proc,
|
|
412
|
+
bunStdoutBuffer: "",
|
|
413
|
+
cols: initialCols,
|
|
414
|
+
rows: initialRows
|
|
415
|
+
};
|
|
416
|
+
} catch (error) {
|
|
417
|
+
this.bunPtyAvailable = false;
|
|
418
|
+
if (!this.loggedBunPtyFailure) {
|
|
419
|
+
this.loggedBunPtyFailure = true;
|
|
420
|
+
this.logger.warn("Bun PTY unavailable; falling back", {
|
|
421
|
+
error: toErrorMessage(error)
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
_spawnPythonPtyRuntime(launchConfig) {
|
|
429
|
+
if (!this.pythonPtyAvailable) {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const [bin, ...args] = launchConfig.command;
|
|
434
|
+
try {
|
|
435
|
+
const proc = childSpawn(
|
|
436
|
+
"python3",
|
|
437
|
+
[
|
|
438
|
+
"-c",
|
|
439
|
+
"import pty, sys; pty.spawn(sys.argv[1:])",
|
|
440
|
+
bin,
|
|
441
|
+
...args
|
|
442
|
+
],
|
|
443
|
+
{
|
|
444
|
+
cwd: launchConfig.cwd,
|
|
445
|
+
env: launchConfig.env,
|
|
446
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
447
|
+
}
|
|
448
|
+
);
|
|
449
|
+
return {
|
|
450
|
+
mode: "python-pty",
|
|
451
|
+
proc,
|
|
452
|
+
cols: 120,
|
|
453
|
+
rows: 30
|
|
454
|
+
};
|
|
455
|
+
} catch (error) {
|
|
456
|
+
this.pythonPtyAvailable = false;
|
|
457
|
+
if (!this.loggedPythonPtyFailure) {
|
|
458
|
+
this.loggedPythonPtyFailure = true;
|
|
459
|
+
this.logger.warn("python3 PTY fallback unavailable; using pipe terminal", {
|
|
460
|
+
error: toErrorMessage(error)
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
_attachBunPtyProcess(sessionId, session) {
|
|
468
|
+
const handleBridgeLine = (line) => {
|
|
469
|
+
const parsed = safeJsonParse(line);
|
|
470
|
+
if (!parsed || typeof parsed !== "object") {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (parsed.type === "data") {
|
|
475
|
+
this._broadcast(sessionId, {
|
|
476
|
+
type: "terminal-data",
|
|
477
|
+
sessionId,
|
|
478
|
+
data: typeof parsed.data === "string" ? parsed.data : ""
|
|
479
|
+
});
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (parsed.type === "error") {
|
|
484
|
+
this._broadcast(sessionId, {
|
|
485
|
+
type: "terminal-error",
|
|
486
|
+
sessionId,
|
|
487
|
+
message: typeof parsed.message === "string" ? parsed.message : "bun-pty bridge error"
|
|
488
|
+
});
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (parsed.type === "exit") {
|
|
493
|
+
this._broadcast(sessionId, {
|
|
494
|
+
type: "terminal-exit",
|
|
495
|
+
sessionId,
|
|
496
|
+
code: typeof parsed.exitCode === "number" ? parsed.exitCode : null,
|
|
497
|
+
signal: parsed.signal ?? null
|
|
498
|
+
});
|
|
499
|
+
this.sessions.delete(sessionId);
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
session.proc.stdout?.on("data", (chunk) => {
|
|
504
|
+
session.bunStdoutBuffer += chunk.toString("utf8");
|
|
505
|
+
for (;;) {
|
|
506
|
+
const newlineAt = session.bunStdoutBuffer.indexOf("\n");
|
|
507
|
+
if (newlineAt < 0) {
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
const line = session.bunStdoutBuffer.slice(0, newlineAt);
|
|
511
|
+
session.bunStdoutBuffer = session.bunStdoutBuffer.slice(newlineAt + 1);
|
|
512
|
+
if (line.trim().length === 0) {
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
handleBridgeLine(line);
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
session.proc.stderr?.on("data", (chunk) => {
|
|
520
|
+
this._broadcast(sessionId, {
|
|
521
|
+
type: "terminal-error",
|
|
522
|
+
sessionId,
|
|
523
|
+
message: chunk.toString("utf8")
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
session.proc.on("error", (error) => {
|
|
528
|
+
this._broadcast(sessionId, {
|
|
529
|
+
type: "terminal-error",
|
|
530
|
+
sessionId,
|
|
531
|
+
message: toErrorMessage(error)
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
session.proc.on("exit", (code, signal) => {
|
|
536
|
+
if (!this.sessions.has(sessionId)) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
this._broadcast(sessionId, {
|
|
540
|
+
type: "terminal-exit",
|
|
541
|
+
sessionId,
|
|
542
|
+
code,
|
|
543
|
+
signal
|
|
544
|
+
});
|
|
545
|
+
this.sessions.delete(sessionId);
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
_writeToBunPty(session, payload) {
|
|
550
|
+
if (!session?.proc?.stdin || session.proc.stdin.destroyed) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
session.proc.stdin.write(`${JSON.stringify(payload)}\n`);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
_detectBunPtyAvailability() {
|
|
557
|
+
const hasBun = childSpawnSync("bun", ["--version"], { stdio: "ignore" });
|
|
558
|
+
if (hasBun.status !== 0 || hasBun.error) {
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const probe = childSpawnSync(
|
|
563
|
+
"bun",
|
|
564
|
+
["-e", "import 'bun-pty';"],
|
|
565
|
+
{ stdio: "ignore" }
|
|
566
|
+
);
|
|
567
|
+
return probe.status === 0 && !probe.error;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
_detectPythonPtyAvailability() {
|
|
571
|
+
const probe = childSpawnSync(
|
|
572
|
+
"python3",
|
|
573
|
+
["-c", "import pty"],
|
|
574
|
+
{ stdio: "ignore" }
|
|
575
|
+
);
|
|
576
|
+
return probe.status === 0 && !probe.error;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
_buildEnv(messageEnv) {
|
|
580
|
+
const env = {
|
|
581
|
+
...process.env,
|
|
582
|
+
...(messageEnv && typeof messageEnv === "object" ? messageEnv : {})
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
if (!env.TERM || env.TERM === "dumb") {
|
|
586
|
+
env.TERM = "xterm-256color";
|
|
587
|
+
}
|
|
588
|
+
if (!env.COLORTERM) {
|
|
589
|
+
env.COLORTERM = "truecolor";
|
|
590
|
+
}
|
|
591
|
+
if (!env.TERM_PROGRAM) {
|
|
592
|
+
env.TERM_PROGRAM = "codex-webstrapper";
|
|
593
|
+
}
|
|
594
|
+
return env;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
_resolveShellPath(messageShell) {
|
|
598
|
+
if (typeof messageShell === "string" && messageShell.trim().length > 0) {
|
|
599
|
+
return messageShell.trim();
|
|
600
|
+
}
|
|
601
|
+
return process.env.SHELL || "/bin/zsh";
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
_defaultShellCommand(shellPath) {
|
|
605
|
+
const shellName = path.basename(shellPath).toLowerCase();
|
|
606
|
+
const disableProfileLoad = process.env.CODEX_WEBSTRAP_TERMINAL_NO_PROFILE === "1";
|
|
607
|
+
const preferLoginProfile = this.hasPtyLikeRuntime && !disableProfileLoad;
|
|
608
|
+
|
|
609
|
+
if (shellName === "zsh") {
|
|
610
|
+
return preferLoginProfile
|
|
611
|
+
? [shellPath, "-il"]
|
|
612
|
+
: [shellPath, "-fi"];
|
|
613
|
+
}
|
|
614
|
+
if (shellName === "bash") {
|
|
615
|
+
return preferLoginProfile
|
|
616
|
+
? [shellPath, "-il"]
|
|
617
|
+
: [shellPath, "--noprofile", "--norc", "-i"];
|
|
618
|
+
}
|
|
619
|
+
if (shellName === "fish") {
|
|
620
|
+
return preferLoginProfile
|
|
621
|
+
? [shellPath, "-il"]
|
|
622
|
+
: [shellPath, "-i"];
|
|
623
|
+
}
|
|
624
|
+
return [shellPath, "-i"];
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
_resolveCwd(requestedCwd) {
|
|
628
|
+
const requested = requestedCwd && requestedCwd.length > 0
|
|
629
|
+
? path.resolve(requestedCwd)
|
|
630
|
+
: null;
|
|
631
|
+
if (requested && this._isDirectory(requested)) {
|
|
632
|
+
return {
|
|
633
|
+
cwd: requested,
|
|
634
|
+
requestedCwd: requested,
|
|
635
|
+
cwdWasFallback: false
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const fallbackCandidates = [process.cwd(), os.homedir(), "/"];
|
|
640
|
+
const fallback = fallbackCandidates.find((candidate) => this._isDirectory(candidate)) || process.cwd();
|
|
641
|
+
return {
|
|
642
|
+
cwd: fallback,
|
|
643
|
+
requestedCwd: requested,
|
|
644
|
+
cwdWasFallback: Boolean(requested)
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
_isDirectory(candidatePath) {
|
|
649
|
+
if (!candidatePath || typeof candidatePath !== "string") {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
return statSync(candidatePath).isDirectory();
|
|
654
|
+
} catch {
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
_coerceDimension(value, fallback) {
|
|
660
|
+
const parsed = Number(value);
|
|
661
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
662
|
+
return fallback;
|
|
663
|
+
}
|
|
664
|
+
return Math.max(1, Math.floor(parsed));
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
_hasExplicitDimensions(message) {
|
|
668
|
+
const cols = Number(message?.cols);
|
|
669
|
+
const rows = Number(message?.rows);
|
|
670
|
+
return Number.isFinite(cols) && cols > 0 && Number.isFinite(rows) && rows > 0;
|
|
671
|
+
}
|
|
207
672
|
}
|
|
208
673
|
|
|
209
674
|
class GitWorkerBridge {
|
|
@@ -543,7 +1008,7 @@ export class MessageRouter {
|
|
|
543
1008
|
this.terminals.write(message.sessionId, message.data || "");
|
|
544
1009
|
return;
|
|
545
1010
|
case "terminal-resize":
|
|
546
|
-
this.terminals.resize(message.sessionId);
|
|
1011
|
+
this.terminals.resize(message.sessionId, message.cols, message.rows);
|
|
547
1012
|
return;
|
|
548
1013
|
case "terminal-close":
|
|
549
1014
|
this.terminals.close(message.sessionId);
|
|
@@ -753,14 +1218,25 @@ export class MessageRouter {
|
|
|
753
1218
|
this.logger.debug("renderer-fetch", {
|
|
754
1219
|
requestId,
|
|
755
1220
|
method: message.method || "GET",
|
|
756
|
-
url: message.url
|
|
757
|
-
|
|
1221
|
+
url: this._sanitizeUrlForLogs(message.url),
|
|
1222
|
+
hasBody: message.body != null
|
|
758
1223
|
});
|
|
759
1224
|
|
|
760
1225
|
if (await this._handleVirtualFetch(ws, requestId, message)) {
|
|
761
1226
|
return;
|
|
762
1227
|
}
|
|
763
1228
|
|
|
1229
|
+
if (typeof message.url === "string" && message.url === "/transcribe") {
|
|
1230
|
+
const controller = new AbortController();
|
|
1231
|
+
this.fetchControllers.set(requestId, controller);
|
|
1232
|
+
try {
|
|
1233
|
+
await this._handleTranscribeFetch(ws, requestId, message, controller.signal);
|
|
1234
|
+
} finally {
|
|
1235
|
+
this.fetchControllers.delete(requestId);
|
|
1236
|
+
}
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
764
1240
|
const resolvedUrl = this._resolveFetchUrl(message.url);
|
|
765
1241
|
if (!resolvedUrl) {
|
|
766
1242
|
this.sendMainMessage(ws, {
|
|
@@ -772,7 +1248,7 @@ export class MessageRouter {
|
|
|
772
1248
|
});
|
|
773
1249
|
this.logger.warn("renderer-fetch-failed", {
|
|
774
1250
|
requestId,
|
|
775
|
-
url: message.url
|
|
1251
|
+
url: this._sanitizeUrlForLogs(message.url),
|
|
776
1252
|
error: "unsupported_fetch_url"
|
|
777
1253
|
});
|
|
778
1254
|
return;
|
|
@@ -782,10 +1258,11 @@ export class MessageRouter {
|
|
|
782
1258
|
this.fetchControllers.set(requestId, controller);
|
|
783
1259
|
|
|
784
1260
|
try {
|
|
1261
|
+
const outbound = this._prepareOutgoingFetchRequest(message);
|
|
785
1262
|
const response = await fetch(resolvedUrl, {
|
|
786
|
-
method:
|
|
787
|
-
headers:
|
|
788
|
-
body:
|
|
1263
|
+
method: outbound.method,
|
|
1264
|
+
headers: outbound.headers,
|
|
1265
|
+
body: outbound.body,
|
|
789
1266
|
signal: controller.signal
|
|
790
1267
|
});
|
|
791
1268
|
|
|
@@ -814,7 +1291,7 @@ export class MessageRouter {
|
|
|
814
1291
|
requestId,
|
|
815
1292
|
status: response.status,
|
|
816
1293
|
ok: response.ok,
|
|
817
|
-
url: response.url || resolvedUrl
|
|
1294
|
+
url: this._sanitizeUrlForLogs(response.url || resolvedUrl)
|
|
818
1295
|
});
|
|
819
1296
|
} catch (error) {
|
|
820
1297
|
this.sendMainMessage(ws, {
|
|
@@ -826,7 +1303,7 @@ export class MessageRouter {
|
|
|
826
1303
|
});
|
|
827
1304
|
this.logger.warn("renderer-fetch-failed", {
|
|
828
1305
|
requestId,
|
|
829
|
-
url: resolvedUrl,
|
|
1306
|
+
url: this._sanitizeUrlForLogs(resolvedUrl),
|
|
830
1307
|
error: toErrorMessage(error)
|
|
831
1308
|
});
|
|
832
1309
|
} finally {
|
|
@@ -834,31 +1311,296 @@ export class MessageRouter {
|
|
|
834
1311
|
}
|
|
835
1312
|
}
|
|
836
1313
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
}
|
|
847
|
-
return null;
|
|
848
|
-
}
|
|
1314
|
+
async _handleTranscribeFetch(ws, requestId, message, signal) {
|
|
1315
|
+
try {
|
|
1316
|
+
const outbound = this._prepareOutgoingFetchRequest(message);
|
|
1317
|
+
const bodyBuffer = this._asBuffer(outbound.body);
|
|
1318
|
+
const contentType = this._readHeader(outbound.headers, "content-type");
|
|
1319
|
+
const boundary = this._extractMultipartBoundary(contentType);
|
|
1320
|
+
if (!boundary) {
|
|
1321
|
+
throw new Error("Missing multipart boundary for /transcribe request.");
|
|
1322
|
+
}
|
|
849
1323
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1324
|
+
const { fields, files } = this._parseMultipartBody(bodyBuffer, boundary);
|
|
1325
|
+
const file = files.find((part) => part.name === "file") || files[0];
|
|
1326
|
+
if (!file || !file.data || file.data.length === 0) {
|
|
1327
|
+
throw new Error("Missing audio file in /transcribe request.");
|
|
1328
|
+
}
|
|
854
1329
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
1330
|
+
const authToken = await this._resolveTranscriptionAuthToken();
|
|
1331
|
+
if (!authToken) {
|
|
1332
|
+
throw new Error("Dictation requires ChatGPT authentication in Codex.");
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
const model = process.env.CODEX_WEBSTRAP_TRANSCRIBE_MODEL || "gpt-4o-mini-transcribe";
|
|
1336
|
+
const form = new FormData();
|
|
1337
|
+
form.append("model", model);
|
|
1338
|
+
if (typeof fields.language === "string" && fields.language.trim().length > 0) {
|
|
1339
|
+
form.append("language", fields.language.trim());
|
|
1340
|
+
}
|
|
1341
|
+
form.append(
|
|
1342
|
+
"file",
|
|
1343
|
+
new Blob([file.data], { type: file.contentType || "audio/webm" }),
|
|
1344
|
+
file.filename || "codex.webm"
|
|
1345
|
+
);
|
|
1346
|
+
|
|
1347
|
+
const response = await fetch("https://api.openai.com/v1/audio/transcriptions", {
|
|
1348
|
+
method: "POST",
|
|
1349
|
+
headers: {
|
|
1350
|
+
Authorization: `Bearer ${authToken}`
|
|
1351
|
+
},
|
|
1352
|
+
body: form,
|
|
1353
|
+
signal
|
|
1354
|
+
});
|
|
1355
|
+
const responseText = await response.text();
|
|
1356
|
+
|
|
1357
|
+
let bodyJsonString = responseText;
|
|
1358
|
+
if (response.ok) {
|
|
1359
|
+
const parsed = safeJsonParse(responseText);
|
|
1360
|
+
if (parsed && typeof parsed === "object" && typeof parsed.text === "string") {
|
|
1361
|
+
bodyJsonString = JSON.stringify({ text: parsed.text });
|
|
1362
|
+
} else {
|
|
1363
|
+
bodyJsonString = JSON.stringify({ text: "" });
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
this.sendMainMessage(ws, {
|
|
1368
|
+
type: "fetch-response",
|
|
1369
|
+
requestId,
|
|
1370
|
+
responseType: "success",
|
|
1371
|
+
status: response.status,
|
|
1372
|
+
headers: {
|
|
1373
|
+
"content-type": response.headers.get("content-type") || "application/json"
|
|
1374
|
+
},
|
|
1375
|
+
bodyJsonString
|
|
1376
|
+
});
|
|
1377
|
+
} catch (error) {
|
|
1378
|
+
this.sendMainMessage(ws, {
|
|
1379
|
+
type: "fetch-response",
|
|
1380
|
+
requestId,
|
|
1381
|
+
responseType: "error",
|
|
1382
|
+
status: 0,
|
|
1383
|
+
error: toErrorMessage(error)
|
|
1384
|
+
});
|
|
1385
|
+
this.logger.warn("transcribe-fetch-failed", {
|
|
1386
|
+
requestId,
|
|
1387
|
+
error: toErrorMessage(error)
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
_prepareOutgoingFetchRequest(message) {
|
|
1393
|
+
const method = message?.method || "GET";
|
|
1394
|
+
const headers = message?.headers && typeof message.headers === "object"
|
|
1395
|
+
? { ...message.headers }
|
|
1396
|
+
: {};
|
|
1397
|
+
let body = message?.body;
|
|
1398
|
+
|
|
1399
|
+
const base64Marker = this._readHeader(headers, "x-codex-base64");
|
|
1400
|
+
if (base64Marker === "1") {
|
|
1401
|
+
this._deleteHeader(headers, "x-codex-base64");
|
|
1402
|
+
if (typeof body !== "string") {
|
|
1403
|
+
throw new Error("X-Codex-Base64 fetch body must be a base64 string.");
|
|
1404
|
+
}
|
|
1405
|
+
body = Buffer.from(body, "base64");
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
return {
|
|
1409
|
+
method,
|
|
1410
|
+
headers,
|
|
1411
|
+
body
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
_asBuffer(body) {
|
|
1416
|
+
if (Buffer.isBuffer(body)) {
|
|
1417
|
+
return body;
|
|
1418
|
+
}
|
|
1419
|
+
if (body instanceof Uint8Array) {
|
|
1420
|
+
return Buffer.from(body);
|
|
1421
|
+
}
|
|
1422
|
+
if (typeof body === "string") {
|
|
1423
|
+
return Buffer.from(body, "utf8");
|
|
1424
|
+
}
|
|
1425
|
+
if (body == null) {
|
|
1426
|
+
return Buffer.alloc(0);
|
|
1427
|
+
}
|
|
1428
|
+
return Buffer.from(String(body), "utf8");
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
_extractMultipartBoundary(contentType) {
|
|
1432
|
+
if (typeof contentType !== "string") {
|
|
1433
|
+
return null;
|
|
1434
|
+
}
|
|
1435
|
+
const match = contentType.match(/boundary=([^;]+)/i);
|
|
1436
|
+
if (!match || !match[1]) {
|
|
1437
|
+
return null;
|
|
1438
|
+
}
|
|
1439
|
+
return match[1].trim().replace(/^"|"$/g, "");
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
_parseMultipartBody(body, boundary) {
|
|
1443
|
+
const files = [];
|
|
1444
|
+
const fields = {};
|
|
1445
|
+
const delimiter = Buffer.from(`--${boundary}`);
|
|
1446
|
+
const partSeparator = Buffer.from("\r\n\r\n");
|
|
1447
|
+
|
|
1448
|
+
let cursor = 0;
|
|
1449
|
+
for (;;) {
|
|
1450
|
+
const start = body.indexOf(delimiter, cursor);
|
|
1451
|
+
if (start < 0) {
|
|
1452
|
+
break;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
cursor = start + delimiter.length;
|
|
1456
|
+
if (body[cursor] === 45 && body[cursor + 1] === 45) {
|
|
1457
|
+
break;
|
|
1458
|
+
}
|
|
1459
|
+
if (body[cursor] === 13 && body[cursor + 1] === 10) {
|
|
1460
|
+
cursor += 2;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
const headerEnd = body.indexOf(partSeparator, cursor);
|
|
1464
|
+
if (headerEnd < 0) {
|
|
1465
|
+
break;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
const headersText = body.slice(cursor, headerEnd).toString("utf8");
|
|
1469
|
+
const contentStart = headerEnd + partSeparator.length;
|
|
1470
|
+
const nextBoundary = body.indexOf(Buffer.from(`\r\n--${boundary}`), contentStart);
|
|
1471
|
+
const contentEnd = nextBoundary >= 0 ? nextBoundary : body.length;
|
|
1472
|
+
const content = body.slice(contentStart, contentEnd);
|
|
1473
|
+
|
|
1474
|
+
const headers = {};
|
|
1475
|
+
for (const line of headersText.split("\r\n")) {
|
|
1476
|
+
const splitAt = line.indexOf(":");
|
|
1477
|
+
if (splitAt <= 0) {
|
|
1478
|
+
continue;
|
|
1479
|
+
}
|
|
1480
|
+
const key = line.slice(0, splitAt).trim().toLowerCase();
|
|
1481
|
+
const value = line.slice(splitAt + 1).trim();
|
|
1482
|
+
headers[key] = value;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
const disposition = headers["content-disposition"] || "";
|
|
1486
|
+
const nameMatch = disposition.match(/\bname="([^"]+)"/i);
|
|
1487
|
+
const filenameMatch = disposition.match(/\bfilename="([^"]+)"/i);
|
|
1488
|
+
const name = nameMatch ? nameMatch[1] : null;
|
|
1489
|
+
const filename = filenameMatch ? filenameMatch[1] : null;
|
|
1490
|
+
const part = {
|
|
1491
|
+
name,
|
|
1492
|
+
filename,
|
|
1493
|
+
contentType: headers["content-type"] || null,
|
|
1494
|
+
data: content
|
|
1495
|
+
};
|
|
1496
|
+
|
|
1497
|
+
if (filename) {
|
|
1498
|
+
files.push(part);
|
|
1499
|
+
} else if (name) {
|
|
1500
|
+
fields[name] = content.toString("utf8");
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
cursor = contentEnd + 2;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
return { fields, files };
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
async _resolveTranscriptionAuthToken() {
|
|
1510
|
+
if (!this.appServer) {
|
|
1511
|
+
return null;
|
|
1512
|
+
}
|
|
1513
|
+
try {
|
|
1514
|
+
const response = await this.appServer.sendRequest("getAuthStatus", {
|
|
1515
|
+
includeToken: true,
|
|
1516
|
+
refreshToken: true
|
|
1517
|
+
}, {
|
|
1518
|
+
timeoutMs: 10_000
|
|
1519
|
+
});
|
|
1520
|
+
const token = response?.result?.authToken;
|
|
1521
|
+
return typeof token === "string" && token.trim().length > 0 ? token.trim() : null;
|
|
1522
|
+
} catch (error) {
|
|
1523
|
+
this.logger.warn("Failed to resolve transcription auth token", {
|
|
1524
|
+
error: toErrorMessage(error)
|
|
1525
|
+
});
|
|
1526
|
+
return null;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
_readHeader(headers, name) {
|
|
1531
|
+
if (!headers || typeof headers !== "object") {
|
|
1532
|
+
return null;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
const target = String(name).toLowerCase();
|
|
1536
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
1537
|
+
if (String(key).toLowerCase() !== target) {
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
1540
|
+
if (Array.isArray(value)) {
|
|
1541
|
+
return value.length > 0 ? String(value[0]) : "";
|
|
1542
|
+
}
|
|
1543
|
+
return value == null ? null : String(value);
|
|
1544
|
+
}
|
|
1545
|
+
return null;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
_deleteHeader(headers, name) {
|
|
1549
|
+
if (!headers || typeof headers !== "object") {
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
const target = String(name).toLowerCase();
|
|
1554
|
+
for (const key of Object.keys(headers)) {
|
|
1555
|
+
if (String(key).toLowerCase() === target) {
|
|
1556
|
+
delete headers[key];
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
_sanitizeUrlForLogs(url) {
|
|
1562
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
1563
|
+
return null;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// Drop query params and fragments from logs to avoid leaking tokens/user data.
|
|
1567
|
+
const withoutQuery = url.split("?")[0]?.split("#")[0] ?? "";
|
|
1568
|
+
if (withoutQuery.startsWith("http://") || withoutQuery.startsWith("https://")) {
|
|
1569
|
+
try {
|
|
1570
|
+
const parsed = new URL(withoutQuery);
|
|
1571
|
+
return `${parsed.origin}${parsed.pathname}`;
|
|
1572
|
+
} catch {
|
|
1573
|
+
return withoutQuery;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
return withoutQuery;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
_resolveFetchUrl(url) {
|
|
1580
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
1581
|
+
return null;
|
|
1582
|
+
}
|
|
1583
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
1584
|
+
return url;
|
|
1585
|
+
}
|
|
1586
|
+
if (url.startsWith("/")) {
|
|
1587
|
+
return `https://chatgpt.com${url}`;
|
|
1588
|
+
}
|
|
1589
|
+
return null;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
async _handleVirtualFetch(ws, requestId, message) {
|
|
1593
|
+
if (typeof message.url !== "string") {
|
|
1594
|
+
return false;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
if (message.url.startsWith("sentry-ipc://")) {
|
|
1598
|
+
this._sendFetchJson(ws, {
|
|
1599
|
+
requestId,
|
|
1600
|
+
url: message.url,
|
|
1601
|
+
status: 204,
|
|
1602
|
+
payload: ""
|
|
1603
|
+
});
|
|
862
1604
|
this.logger.debug("renderer-fetch-response", {
|
|
863
1605
|
requestId,
|
|
864
1606
|
status: 204,
|
|
@@ -1051,6 +1793,16 @@ export class MessageRouter {
|
|
|
1051
1793
|
};
|
|
1052
1794
|
break;
|
|
1053
1795
|
}
|
|
1796
|
+
case "git-create-branch": {
|
|
1797
|
+
payload = await this._handleGitCreateBranch(params);
|
|
1798
|
+
status = payload.ok ? 200 : 500;
|
|
1799
|
+
break;
|
|
1800
|
+
}
|
|
1801
|
+
case "git-checkout-branch": {
|
|
1802
|
+
payload = await this._handleGitCheckoutBranch(params);
|
|
1803
|
+
status = payload.ok ? 200 : 500;
|
|
1804
|
+
break;
|
|
1805
|
+
}
|
|
1054
1806
|
case "git-push": {
|
|
1055
1807
|
payload = await this._handleGitPush(params);
|
|
1056
1808
|
status = payload.ok ? 200 : 500;
|
|
@@ -1108,14 +1860,29 @@ export class MessageRouter {
|
|
|
1108
1860
|
payload = await this._resolveGhPrStatus({ cwd, headBranch });
|
|
1109
1861
|
break;
|
|
1110
1862
|
}
|
|
1863
|
+
case "generate-pull-request-message":
|
|
1864
|
+
payload = await this._handleGeneratePullRequestMessage(params);
|
|
1865
|
+
break;
|
|
1866
|
+
case "gh-pr-create":
|
|
1867
|
+
payload = await this._handleGhPrCreate(params);
|
|
1868
|
+
break;
|
|
1111
1869
|
case "paths-exist": {
|
|
1112
1870
|
const paths = Array.isArray(params?.paths) ? params.paths.filter((p) => typeof p === "string") : [];
|
|
1113
1871
|
payload = { existingPaths: paths };
|
|
1114
1872
|
break;
|
|
1115
1873
|
}
|
|
1116
1874
|
default:
|
|
1117
|
-
|
|
1118
|
-
|
|
1875
|
+
if (endpoint.startsWith("git-")) {
|
|
1876
|
+
this.logger.warn("Unhandled git vscode fetch endpoint", { endpoint });
|
|
1877
|
+
payload = {
|
|
1878
|
+
ok: false,
|
|
1879
|
+
error: `unhandled git endpoint: ${endpoint}`
|
|
1880
|
+
};
|
|
1881
|
+
status = 500;
|
|
1882
|
+
} else {
|
|
1883
|
+
this.logger.warn("Unhandled vscode fetch endpoint", { endpoint });
|
|
1884
|
+
payload = {};
|
|
1885
|
+
}
|
|
1119
1886
|
}
|
|
1120
1887
|
|
|
1121
1888
|
this._sendFetchJson(ws, {
|
|
@@ -1318,6 +2085,350 @@ export class MessageRouter {
|
|
|
1318
2085
|
};
|
|
1319
2086
|
}
|
|
1320
2087
|
|
|
2088
|
+
async _handleGeneratePullRequestMessage(params) {
|
|
2089
|
+
const cwd = typeof params?.cwd === "string" && params.cwd.length > 0
|
|
2090
|
+
? params.cwd
|
|
2091
|
+
: process.cwd();
|
|
2092
|
+
const prompt = typeof params?.prompt === "string" ? params.prompt : "";
|
|
2093
|
+
const generated = await this._generatePullRequestMessageWithCodex({ cwd, prompt });
|
|
2094
|
+
const fallback = generated || await this._generateFallbackPullRequestMessage({ cwd, prompt });
|
|
2095
|
+
const title = this._normalizePullRequestTitle(fallback.title);
|
|
2096
|
+
const body = this._normalizePullRequestBody(fallback.body);
|
|
2097
|
+
|
|
2098
|
+
return {
|
|
2099
|
+
status: "success",
|
|
2100
|
+
title,
|
|
2101
|
+
body,
|
|
2102
|
+
// Older clients read `bodyInstructions`; keep it in sync with the generated body.
|
|
2103
|
+
bodyInstructions: body
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
_normalizePullRequestTitle(title) {
|
|
2108
|
+
if (typeof title !== "string") {
|
|
2109
|
+
return "Update project files";
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
const normalized = title.replace(/\s+/g, " ").trim();
|
|
2113
|
+
if (normalized.length === 0) {
|
|
2114
|
+
return "Update project files";
|
|
2115
|
+
}
|
|
2116
|
+
if (normalized.length <= 120) {
|
|
2117
|
+
return normalized;
|
|
2118
|
+
}
|
|
2119
|
+
return `${normalized.slice(0, 117).trimEnd()}...`;
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
_normalizePullRequestBody(body) {
|
|
2123
|
+
if (typeof body !== "string") {
|
|
2124
|
+
return "## Summary\n- Update project files.\n\n## Testing\n- Not run (context not provided).";
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
const normalized = body.trim();
|
|
2128
|
+
if (normalized.length > 0) {
|
|
2129
|
+
return normalized;
|
|
2130
|
+
}
|
|
2131
|
+
return "## Summary\n- Update project files.\n\n## Testing\n- Not run (context not provided).";
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
_buildPullRequestCodexPrompt(prompt) {
|
|
2135
|
+
const context = typeof prompt === "string" && prompt.trim().length > 0
|
|
2136
|
+
? prompt.trim().slice(0, 20_000)
|
|
2137
|
+
: "No additional context was provided.";
|
|
2138
|
+
|
|
2139
|
+
return [
|
|
2140
|
+
"Generate a GitHub pull request title and body.",
|
|
2141
|
+
"Return JSON that matches the provided schema.",
|
|
2142
|
+
"Requirements:",
|
|
2143
|
+
"- title: concise imperative sentence, maximum 72 characters.",
|
|
2144
|
+
"- body: markdown with sections exactly '## Summary' and '## Testing'.",
|
|
2145
|
+
"- Summary should include 2 to 6 concrete bullet points.",
|
|
2146
|
+
"- Testing should include bullet points. If unknown, say '- Not run (context not provided).'.",
|
|
2147
|
+
"- Do not wrap output in code fences.",
|
|
2148
|
+
"- Use only the provided context.",
|
|
2149
|
+
"",
|
|
2150
|
+
"Context:",
|
|
2151
|
+
context
|
|
2152
|
+
].join("\n");
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
async _generatePullRequestMessageWithCodex({ cwd, prompt }) {
|
|
2156
|
+
let tempDir = null;
|
|
2157
|
+
try {
|
|
2158
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "codex-webstrap-prmsg-"));
|
|
2159
|
+
const schemaPath = path.join(tempDir, "schema.json");
|
|
2160
|
+
const outputPath = path.join(tempDir, "output.json");
|
|
2161
|
+
const schema = {
|
|
2162
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
2163
|
+
type: "object",
|
|
2164
|
+
required: ["title", "body"],
|
|
2165
|
+
additionalProperties: false,
|
|
2166
|
+
properties: {
|
|
2167
|
+
title: { type: "string" },
|
|
2168
|
+
body: { type: "string" }
|
|
2169
|
+
}
|
|
2170
|
+
};
|
|
2171
|
+
await fs.writeFile(schemaPath, JSON.stringify(schema), "utf8");
|
|
2172
|
+
|
|
2173
|
+
const result = await this._runCommand(
|
|
2174
|
+
"codex",
|
|
2175
|
+
[
|
|
2176
|
+
"exec",
|
|
2177
|
+
"--ephemeral",
|
|
2178
|
+
"--sandbox",
|
|
2179
|
+
"read-only",
|
|
2180
|
+
"--output-schema",
|
|
2181
|
+
schemaPath,
|
|
2182
|
+
"--output-last-message",
|
|
2183
|
+
outputPath,
|
|
2184
|
+
this._buildPullRequestCodexPrompt(prompt)
|
|
2185
|
+
],
|
|
2186
|
+
{
|
|
2187
|
+
timeoutMs: 120_000,
|
|
2188
|
+
allowNonZero: true,
|
|
2189
|
+
cwd
|
|
2190
|
+
}
|
|
2191
|
+
);
|
|
2192
|
+
|
|
2193
|
+
if (!result.ok) {
|
|
2194
|
+
this.logger.warn("PR message generation via codex failed", {
|
|
2195
|
+
cwd,
|
|
2196
|
+
error: result.error || result.stderr || "unknown error"
|
|
2197
|
+
});
|
|
2198
|
+
return null;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
const rawOutput = await fs.readFile(outputPath, "utf8");
|
|
2202
|
+
const parsed = safeJsonParse(rawOutput);
|
|
2203
|
+
if (!parsed || typeof parsed !== "object") {
|
|
2204
|
+
return null;
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
const title = this._normalizePullRequestTitle(parsed.title);
|
|
2208
|
+
const body = this._normalizePullRequestBody(parsed.body);
|
|
2209
|
+
return { title, body };
|
|
2210
|
+
} catch (error) {
|
|
2211
|
+
this.logger.warn("PR message generation via codex errored", {
|
|
2212
|
+
cwd,
|
|
2213
|
+
error: toErrorMessage(error)
|
|
2214
|
+
});
|
|
2215
|
+
return null;
|
|
2216
|
+
} finally {
|
|
2217
|
+
if (tempDir) {
|
|
2218
|
+
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
async _resolvePullRequestBaseRef(cwd) {
|
|
2224
|
+
const originHead = await this._runCommand(
|
|
2225
|
+
"git",
|
|
2226
|
+
["-C", cwd, "symbolic-ref", "--short", "refs/remotes/origin/HEAD"],
|
|
2227
|
+
{ timeoutMs: 5_000, allowNonZero: true, cwd }
|
|
2228
|
+
);
|
|
2229
|
+
if (originHead.ok && originHead.stdout) {
|
|
2230
|
+
return originHead.stdout;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
const candidates = ["origin/main", "origin/master", "main", "master"];
|
|
2234
|
+
for (const candidate of candidates) {
|
|
2235
|
+
const exists = await this._runCommand(
|
|
2236
|
+
"git",
|
|
2237
|
+
["-C", cwd, "rev-parse", "--verify", "--quiet", candidate],
|
|
2238
|
+
{ timeoutMs: 5_000, allowNonZero: true, cwd }
|
|
2239
|
+
);
|
|
2240
|
+
if (exists.code === 0) {
|
|
2241
|
+
return candidate;
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
return null;
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
async _generateFallbackPullRequestMessage({ cwd, prompt }) {
|
|
2249
|
+
const baseRef = await this._resolvePullRequestBaseRef(cwd);
|
|
2250
|
+
const logArgs = baseRef
|
|
2251
|
+
? ["-C", cwd, "log", "--no-merges", "--pretty=format:%s", `${baseRef}..HEAD`, "-n", "6"]
|
|
2252
|
+
: ["-C", cwd, "log", "--no-merges", "--pretty=format:%s", "-n", "6"];
|
|
2253
|
+
const logResult = await this._runCommand("git", logArgs, {
|
|
2254
|
+
timeoutMs: 8_000,
|
|
2255
|
+
allowNonZero: true,
|
|
2256
|
+
cwd
|
|
2257
|
+
});
|
|
2258
|
+
const commitSubjects = (logResult.stdout || "")
|
|
2259
|
+
.split("\n")
|
|
2260
|
+
.map((line) => line.trim())
|
|
2261
|
+
.filter(Boolean);
|
|
2262
|
+
|
|
2263
|
+
const range = baseRef ? `${baseRef}...HEAD` : "HEAD~1..HEAD";
|
|
2264
|
+
const filesResult = await this._runCommand(
|
|
2265
|
+
"git",
|
|
2266
|
+
["-C", cwd, "diff", "--name-only", "--diff-filter=ACMR", range],
|
|
2267
|
+
{ timeoutMs: 8_000, allowNonZero: true, cwd }
|
|
2268
|
+
);
|
|
2269
|
+
const changedFiles = (filesResult.stdout || "")
|
|
2270
|
+
.split("\n")
|
|
2271
|
+
.map((line) => line.trim())
|
|
2272
|
+
.filter(Boolean);
|
|
2273
|
+
|
|
2274
|
+
const statsResult = await this._runCommand(
|
|
2275
|
+
"git",
|
|
2276
|
+
["-C", cwd, "diff", "--numstat", range],
|
|
2277
|
+
{ timeoutMs: 8_000, allowNonZero: true, cwd }
|
|
2278
|
+
);
|
|
2279
|
+
let additions = 0;
|
|
2280
|
+
let deletions = 0;
|
|
2281
|
+
for (const line of (statsResult.stdout || "").split("\n")) {
|
|
2282
|
+
const [addedRaw, deletedRaw] = line.split("\t");
|
|
2283
|
+
const added = Number.parseInt(addedRaw, 10);
|
|
2284
|
+
const deleted = Number.parseInt(deletedRaw, 10);
|
|
2285
|
+
additions += Number.isFinite(added) ? added : 0;
|
|
2286
|
+
deletions += Number.isFinite(deleted) ? deleted : 0;
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
const branch = await this._resolveGitCurrentBranch(cwd);
|
|
2290
|
+
const title = this._normalizePullRequestTitle(
|
|
2291
|
+
commitSubjects[0] || (branch ? `Update ${branch}` : "Update project files")
|
|
2292
|
+
);
|
|
2293
|
+
|
|
2294
|
+
const summaryBullets = [];
|
|
2295
|
+
for (const subject of commitSubjects.slice(0, 3)) {
|
|
2296
|
+
summaryBullets.push(subject);
|
|
2297
|
+
}
|
|
2298
|
+
if (summaryBullets.length === 0) {
|
|
2299
|
+
summaryBullets.push("Update project files.");
|
|
2300
|
+
}
|
|
2301
|
+
if (changedFiles.length > 0) {
|
|
2302
|
+
summaryBullets.push(`Modify ${changedFiles.length} file${changedFiles.length === 1 ? "" : "s"}.`);
|
|
2303
|
+
}
|
|
2304
|
+
if (additions > 0 || deletions > 0) {
|
|
2305
|
+
summaryBullets.push(`Diff summary: +${additions} / -${deletions} lines.`);
|
|
2306
|
+
}
|
|
2307
|
+
if (baseRef) {
|
|
2308
|
+
summaryBullets.push(`Base branch: ${baseRef}.`);
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
const bodyLines = ["## Summary"];
|
|
2312
|
+
for (const bullet of summaryBullets.slice(0, 6)) {
|
|
2313
|
+
bodyLines.push(`- ${bullet}`);
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
bodyLines.push("", "## Testing", "- Not run (context not provided).");
|
|
2317
|
+
|
|
2318
|
+
if (changedFiles.length > 0) {
|
|
2319
|
+
bodyLines.push("", "## Files Changed");
|
|
2320
|
+
for (const file of changedFiles.slice(0, 12)) {
|
|
2321
|
+
bodyLines.push(`- \`${file}\``);
|
|
2322
|
+
}
|
|
2323
|
+
if (changedFiles.length > 12) {
|
|
2324
|
+
bodyLines.push(`- \`...and ${changedFiles.length - 12} more\``);
|
|
2325
|
+
}
|
|
2326
|
+
} else if (typeof prompt === "string" && prompt.trim().length > 0) {
|
|
2327
|
+
bodyLines.push("", "## Notes", "- Generated from available thread context.");
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
return {
|
|
2331
|
+
title,
|
|
2332
|
+
body: this._normalizePullRequestBody(bodyLines.join("\n"))
|
|
2333
|
+
};
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
_extractGithubPrUrl(text) {
|
|
2337
|
+
if (typeof text !== "string" || text.length === 0) {
|
|
2338
|
+
return null;
|
|
2339
|
+
}
|
|
2340
|
+
const match = text.match(/https:\/\/github\.com\/[^\s/]+\/[^\s/]+\/pull\/\d+/);
|
|
2341
|
+
return match ? match[0] : null;
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
async _handleGhPrCreate(params) {
|
|
2345
|
+
const cwd = typeof params?.cwd === "string" && params.cwd.length > 0
|
|
2346
|
+
? params.cwd
|
|
2347
|
+
: process.cwd();
|
|
2348
|
+
const headBranch = typeof params?.headBranch === "string" ? params.headBranch.trim() : "";
|
|
2349
|
+
const baseBranch = typeof params?.baseBranch === "string" ? params.baseBranch.trim() : "";
|
|
2350
|
+
const bodyInstructions = typeof params?.bodyInstructions === "string" ? params.bodyInstructions : "";
|
|
2351
|
+
const titleOverride = typeof params?.titleOverride === "string" ? params.titleOverride.trim() : "";
|
|
2352
|
+
const bodyOverride = typeof params?.bodyOverride === "string" ? params.bodyOverride.trim() : "";
|
|
2353
|
+
|
|
2354
|
+
if (!headBranch || !baseBranch) {
|
|
2355
|
+
return {
|
|
2356
|
+
status: "error",
|
|
2357
|
+
error: "headBranch and baseBranch are required",
|
|
2358
|
+
url: null,
|
|
2359
|
+
number: null
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
const ghStatus = await this._resolveGhCliStatus();
|
|
2364
|
+
if (!ghStatus.isInstalled || !ghStatus.isAuthenticated) {
|
|
2365
|
+
return {
|
|
2366
|
+
status: "error",
|
|
2367
|
+
error: "gh cli unavailable or unauthenticated",
|
|
2368
|
+
url: null,
|
|
2369
|
+
number: null
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
const args = [
|
|
2374
|
+
"pr",
|
|
2375
|
+
"create",
|
|
2376
|
+
"--head",
|
|
2377
|
+
headBranch,
|
|
2378
|
+
"--base",
|
|
2379
|
+
baseBranch
|
|
2380
|
+
];
|
|
2381
|
+
const shouldFill = titleOverride.length === 0 || bodyOverride.length === 0;
|
|
2382
|
+
if (shouldFill) {
|
|
2383
|
+
args.push("--fill");
|
|
2384
|
+
}
|
|
2385
|
+
if (titleOverride.length > 0) {
|
|
2386
|
+
args.push("--title", titleOverride);
|
|
2387
|
+
}
|
|
2388
|
+
if (bodyOverride.length > 0) {
|
|
2389
|
+
args.push("--body", bodyOverride);
|
|
2390
|
+
} else if (bodyInstructions.trim().length > 0) {
|
|
2391
|
+
args.push("--body", bodyInstructions);
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
const result = await this._runCommand("gh", args, {
|
|
2395
|
+
timeoutMs: 30_000,
|
|
2396
|
+
allowNonZero: true,
|
|
2397
|
+
cwd
|
|
2398
|
+
});
|
|
2399
|
+
|
|
2400
|
+
if (result.ok) {
|
|
2401
|
+
const url = this._extractGithubPrUrl(result.stdout) || this._extractGithubPrUrl(result.stderr);
|
|
2402
|
+
const numberMatch = url ? url.match(/\/pull\/(\d+)/) : null;
|
|
2403
|
+
const number = numberMatch ? Number.parseInt(numberMatch[1], 10) : null;
|
|
2404
|
+
return {
|
|
2405
|
+
status: "success",
|
|
2406
|
+
url: url || null,
|
|
2407
|
+
number: Number.isFinite(number) ? number : null
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
const combinedOutput = `${result.stdout || ""}\n${result.stderr || ""}`;
|
|
2412
|
+
const existingUrl = this._extractGithubPrUrl(combinedOutput);
|
|
2413
|
+
const alreadyExists = /already exists|a pull request for branch/i.test(combinedOutput);
|
|
2414
|
+
if (alreadyExists && existingUrl) {
|
|
2415
|
+
const numberMatch = existingUrl.match(/\/pull\/(\d+)/);
|
|
2416
|
+
const number = numberMatch ? Number.parseInt(numberMatch[1], 10) : null;
|
|
2417
|
+
return {
|
|
2418
|
+
status: "success",
|
|
2419
|
+
url: existingUrl,
|
|
2420
|
+
number: Number.isFinite(number) ? number : null
|
|
2421
|
+
};
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
return {
|
|
2425
|
+
status: "error",
|
|
2426
|
+
error: result.error || result.stderr || "failed to create pull request",
|
|
2427
|
+
url: null,
|
|
2428
|
+
number: null
|
|
2429
|
+
};
|
|
2430
|
+
}
|
|
2431
|
+
|
|
1321
2432
|
async _resolveGitMergeBase({ gitRoot, baseBranch }) {
|
|
1322
2433
|
if (!baseBranch) {
|
|
1323
2434
|
return {
|
|
@@ -1339,16 +2450,138 @@ export class MessageRouter {
|
|
|
1339
2450
|
};
|
|
1340
2451
|
}
|
|
1341
2452
|
|
|
2453
|
+
async _resolveGitCurrentBranch(cwd) {
|
|
2454
|
+
const result = await this._runCommand("git", ["-C", cwd, "rev-parse", "--abbrev-ref", "HEAD"], {
|
|
2455
|
+
timeoutMs: 5_000,
|
|
2456
|
+
allowNonZero: true,
|
|
2457
|
+
cwd
|
|
2458
|
+
});
|
|
2459
|
+
if (!result.ok || !result.stdout || result.stdout === "HEAD") {
|
|
2460
|
+
return null;
|
|
2461
|
+
}
|
|
2462
|
+
return result.stdout;
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
async _handleGitCreateBranch(params) {
|
|
2466
|
+
const cwd = typeof params?.cwd === "string" && params.cwd.length > 0
|
|
2467
|
+
? params.cwd
|
|
2468
|
+
: process.cwd();
|
|
2469
|
+
const branch = typeof params?.branch === "string" && params.branch.trim().length > 0
|
|
2470
|
+
? params.branch.trim()
|
|
2471
|
+
: null;
|
|
2472
|
+
|
|
2473
|
+
if (!branch) {
|
|
2474
|
+
return {
|
|
2475
|
+
ok: false,
|
|
2476
|
+
code: null,
|
|
2477
|
+
error: "branch is required",
|
|
2478
|
+
stdout: "",
|
|
2479
|
+
stderr: ""
|
|
2480
|
+
};
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
const existingResult = await this._runCommand("git", ["-C", cwd, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`], {
|
|
2484
|
+
cwd,
|
|
2485
|
+
timeoutMs: 10_000,
|
|
2486
|
+
allowNonZero: true
|
|
2487
|
+
});
|
|
2488
|
+
if (existingResult.code === 0) {
|
|
2489
|
+
return {
|
|
2490
|
+
ok: true,
|
|
2491
|
+
code: 0,
|
|
2492
|
+
branch,
|
|
2493
|
+
created: false,
|
|
2494
|
+
alreadyExists: true,
|
|
2495
|
+
stdout: existingResult.stdout || "",
|
|
2496
|
+
stderr: existingResult.stderr || ""
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
const createResult = await this._runCommand("git", ["-C", cwd, "branch", branch], {
|
|
2501
|
+
cwd,
|
|
2502
|
+
timeoutMs: 10_000,
|
|
2503
|
+
allowNonZero: true
|
|
2504
|
+
});
|
|
2505
|
+
if (createResult.ok) {
|
|
2506
|
+
return {
|
|
2507
|
+
ok: true,
|
|
2508
|
+
code: createResult.code,
|
|
2509
|
+
branch,
|
|
2510
|
+
created: true,
|
|
2511
|
+
alreadyExists: false,
|
|
2512
|
+
stdout: createResult.stdout || "",
|
|
2513
|
+
stderr: createResult.stderr || ""
|
|
2514
|
+
};
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
return {
|
|
2518
|
+
ok: false,
|
|
2519
|
+
code: createResult.code,
|
|
2520
|
+
error: createResult.error || createResult.stderr || "git branch failed",
|
|
2521
|
+
stdout: createResult.stdout || "",
|
|
2522
|
+
stderr: createResult.stderr || ""
|
|
2523
|
+
};
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
async _handleGitCheckoutBranch(params) {
|
|
2527
|
+
const cwd = typeof params?.cwd === "string" && params.cwd.length > 0
|
|
2528
|
+
? params.cwd
|
|
2529
|
+
: process.cwd();
|
|
2530
|
+
const branch = typeof params?.branch === "string" && params.branch.trim().length > 0
|
|
2531
|
+
? params.branch.trim()
|
|
2532
|
+
: null;
|
|
2533
|
+
|
|
2534
|
+
if (!branch) {
|
|
2535
|
+
return {
|
|
2536
|
+
ok: false,
|
|
2537
|
+
code: null,
|
|
2538
|
+
error: "branch is required",
|
|
2539
|
+
stdout: "",
|
|
2540
|
+
stderr: ""
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
const checkoutResult = await this._runCommand("git", ["-C", cwd, "checkout", branch], {
|
|
2545
|
+
cwd,
|
|
2546
|
+
timeoutMs: 20_000,
|
|
2547
|
+
allowNonZero: true
|
|
2548
|
+
});
|
|
2549
|
+
if (!checkoutResult.ok) {
|
|
2550
|
+
return {
|
|
2551
|
+
ok: false,
|
|
2552
|
+
code: checkoutResult.code,
|
|
2553
|
+
error: checkoutResult.error || checkoutResult.stderr || "git checkout failed",
|
|
2554
|
+
stdout: checkoutResult.stdout || "",
|
|
2555
|
+
stderr: checkoutResult.stderr || ""
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
const currentBranch = await this._resolveGitCurrentBranch(cwd);
|
|
2560
|
+
return {
|
|
2561
|
+
ok: true,
|
|
2562
|
+
code: checkoutResult.code,
|
|
2563
|
+
branch: currentBranch || branch,
|
|
2564
|
+
stdout: checkoutResult.stdout || "",
|
|
2565
|
+
stderr: checkoutResult.stderr || ""
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
|
|
1342
2569
|
async _handleGitPush(params) {
|
|
1343
2570
|
const cwd = typeof params?.cwd === "string" && params.cwd.length > 0
|
|
1344
2571
|
? params.cwd
|
|
1345
2572
|
: process.cwd();
|
|
1346
|
-
const
|
|
2573
|
+
const explicitRemote = typeof params?.remote === "string" && params.remote.trim().length > 0
|
|
1347
2574
|
? params.remote.trim()
|
|
1348
2575
|
: null;
|
|
1349
2576
|
const branch = typeof params?.branch === "string" && params.branch.trim().length > 0
|
|
1350
2577
|
? params.branch.trim()
|
|
1351
2578
|
: null;
|
|
2579
|
+
const refspec = typeof params?.refspec === "string" && params.refspec.trim().length > 0
|
|
2580
|
+
? params.refspec.trim()
|
|
2581
|
+
: null;
|
|
2582
|
+
const remote = explicitRemote || (
|
|
2583
|
+
params?.setUpstream === true && (branch || refspec) ? "origin" : null
|
|
2584
|
+
);
|
|
1352
2585
|
|
|
1353
2586
|
const args = ["-C", cwd, "push"];
|
|
1354
2587
|
if (params?.force === true || params?.forceWithLease === true) {
|
|
@@ -1360,7 +2593,9 @@ export class MessageRouter {
|
|
|
1360
2593
|
if (remote) {
|
|
1361
2594
|
args.push(remote);
|
|
1362
2595
|
}
|
|
1363
|
-
if (
|
|
2596
|
+
if (refspec) {
|
|
2597
|
+
args.push(refspec);
|
|
2598
|
+
} else if (branch) {
|
|
1364
2599
|
args.push(branch);
|
|
1365
2600
|
}
|
|
1366
2601
|
|
|
@@ -1390,7 +2625,7 @@ export class MessageRouter {
|
|
|
1390
2625
|
|
|
1391
2626
|
async _runCommand(command, args, { timeoutMs = 5_000, allowNonZero = false, cwd = process.cwd() } = {}) {
|
|
1392
2627
|
return new Promise((resolve) => {
|
|
1393
|
-
const child =
|
|
2628
|
+
const child = childSpawn(command, args, {
|
|
1394
2629
|
cwd,
|
|
1395
2630
|
env: process.env,
|
|
1396
2631
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -1465,7 +2700,7 @@ export class MessageRouter {
|
|
|
1465
2700
|
requestId,
|
|
1466
2701
|
status,
|
|
1467
2702
|
ok: status >= 200 && status < 300,
|
|
1468
|
-
url
|
|
2703
|
+
url: this._sanitizeUrlForLogs(url)
|
|
1469
2704
|
});
|
|
1470
2705
|
}
|
|
1471
2706
|
|
|
@@ -1978,7 +3213,7 @@ export class MessageRouter {
|
|
|
1978
3213
|
return;
|
|
1979
3214
|
}
|
|
1980
3215
|
|
|
1981
|
-
const child =
|
|
3216
|
+
const child = childSpawn("open", [url], {
|
|
1982
3217
|
stdio: ["ignore", "ignore", "ignore"],
|
|
1983
3218
|
detached: true
|
|
1984
3219
|
});
|