codex-webstrapper 0.1.5 → 0.2.0
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 +1 -1
- package/src/bridge-shim.js +12 -0
- package/src/message-router.mjs +497 -4
- package/src/server.mjs +25 -0
package/bin/codex-webstrap.sh
CHANGED
|
@@ -17,9 +17,20 @@ OPEN_FLAG="0"
|
|
|
17
17
|
TOKEN_FILE="${CODEX_WEBSTRAP_TOKEN_FILE:-}"
|
|
18
18
|
CODEX_APP="${CODEX_WEBSTRAP_CODEX_APP:-}"
|
|
19
19
|
INTERNAL_WS_PORT="${CODEX_WEBSTRAP_INTERNAL_WS_PORT:-38080}"
|
|
20
|
+
PORT_SET="0"
|
|
21
|
+
BIND_SET="0"
|
|
20
22
|
COPY_FLAG="0"
|
|
21
23
|
COMMAND="serve"
|
|
22
24
|
|
|
25
|
+
# Treat non-empty env overrides as explicit selections so open-mode runtime
|
|
26
|
+
# autodetection cannot replace them.
|
|
27
|
+
if [[ -n "${CODEX_WEBSTRAP_PORT:-}" ]]; then
|
|
28
|
+
PORT_SET="1"
|
|
29
|
+
fi
|
|
30
|
+
if [[ -n "${CODEX_WEBSTRAP_BIND:-}" ]]; then
|
|
31
|
+
BIND_SET="1"
|
|
32
|
+
fi
|
|
33
|
+
|
|
23
34
|
DEFAULT_TOKEN_FILE="${HOME}/.codex-webstrap/token"
|
|
24
35
|
if [[ -z "$TOKEN_FILE" ]]; then
|
|
25
36
|
TOKEN_FILE="$DEFAULT_TOKEN_FILE"
|
|
@@ -70,14 +81,20 @@ fi
|
|
|
70
81
|
|
|
71
82
|
while [[ $# -gt 0 ]]; do
|
|
72
83
|
case "$1" in
|
|
84
|
+
open)
|
|
85
|
+
COMMAND="open"
|
|
86
|
+
shift
|
|
87
|
+
;;
|
|
73
88
|
--port)
|
|
74
89
|
require_value "$1" "${2:-}"
|
|
75
90
|
PORT="$2"
|
|
91
|
+
PORT_SET="1"
|
|
76
92
|
shift 2
|
|
77
93
|
;;
|
|
78
94
|
--bind)
|
|
79
95
|
require_value "$1" "${2:-}"
|
|
80
96
|
BIND="$2"
|
|
97
|
+
BIND_SET="1"
|
|
81
98
|
shift 2
|
|
82
99
|
;;
|
|
83
100
|
--token-file)
|
|
@@ -126,6 +143,36 @@ if [[ "$COMMAND" == "open" ]]; then
|
|
|
126
143
|
exit 1
|
|
127
144
|
fi
|
|
128
145
|
|
|
146
|
+
if [[ "$PORT_SET" == "0" || "$BIND_SET" == "0" ]]; then
|
|
147
|
+
RUNTIME_FILE="${TOKEN_FILE}.runtime"
|
|
148
|
+
if [[ -f "$RUNTIME_FILE" ]]; then
|
|
149
|
+
RUNTIME_VALUES="$(node -e 'const fs = require("node:fs"); const filePath = process.argv[2]; try { const data = JSON.parse(fs.readFileSync(filePath, "utf8")); const bind = data.bind || ""; const port = data.port || ""; if (!bind || !port) { process.exit(1); } process.stdout.write(`${bind}\n${port}\n`); } catch { process.exit(1); }' "$TOKEN_FILE" "$RUNTIME_FILE" 2>/dev/null || true)"
|
|
150
|
+
if [[ -n "$RUNTIME_VALUES" ]]; then
|
|
151
|
+
RUNTIME_BIND="$(printf '%s' "$RUNTIME_VALUES" | sed -n '1p')"
|
|
152
|
+
RUNTIME_PORT="$(printf '%s' "$RUNTIME_VALUES" | sed -n '2p')"
|
|
153
|
+
if [[ -n "$RUNTIME_BIND" && -n "$RUNTIME_PORT" ]]; then
|
|
154
|
+
if command -v curl >/dev/null 2>&1; then
|
|
155
|
+
if curl -fsS --max-time 1 --connect-timeout 1 "http://${RUNTIME_BIND}:${RUNTIME_PORT}/__webstrapper/healthz" >/dev/null 2>&1; then
|
|
156
|
+
if [[ "$BIND_SET" == "0" ]]; then
|
|
157
|
+
BIND="$RUNTIME_BIND"
|
|
158
|
+
fi
|
|
159
|
+
if [[ "$PORT_SET" == "0" ]]; then
|
|
160
|
+
PORT="$RUNTIME_PORT"
|
|
161
|
+
fi
|
|
162
|
+
fi
|
|
163
|
+
else
|
|
164
|
+
if [[ "$BIND_SET" == "0" ]]; then
|
|
165
|
+
BIND="$RUNTIME_BIND"
|
|
166
|
+
fi
|
|
167
|
+
if [[ "$PORT_SET" == "0" ]]; then
|
|
168
|
+
PORT="$RUNTIME_PORT"
|
|
169
|
+
fi
|
|
170
|
+
fi
|
|
171
|
+
fi
|
|
172
|
+
fi
|
|
173
|
+
fi
|
|
174
|
+
fi
|
|
175
|
+
|
|
129
176
|
ENCODED_TOKEN="$(node -e 'process.stdout.write(encodeURIComponent(process.argv[1] || ""))' "$TOKEN")"
|
|
130
177
|
AUTH_URL="http://${BIND}:${PORT}/__webstrapper/auth?token=${ENCODED_TOKEN}"
|
|
131
178
|
|
|
@@ -134,9 +181,8 @@ if [[ "$COMMAND" == "open" ]]; then
|
|
|
134
181
|
echo "pbcopy not found in PATH" >&2
|
|
135
182
|
exit 1
|
|
136
183
|
fi
|
|
137
|
-
printf '%s' "$AUTH_URL" | pbcopy
|
|
184
|
+
printf '%s' "$AUTH_URL" | pbcopy >/dev/null 2>&1
|
|
138
185
|
printf 'Copied auth URL to clipboard.\n'
|
|
139
|
-
printf '%s\n' "$AUTH_URL"
|
|
140
186
|
else
|
|
141
187
|
open "$AUTH_URL"
|
|
142
188
|
printf 'Opened auth URL in browser.\n'
|
package/package.json
CHANGED
package/src/bridge-shim.js
CHANGED
|
@@ -599,6 +599,18 @@
|
|
|
599
599
|
}
|
|
600
600
|
|
|
601
601
|
function sendMessageFromView(payload) {
|
|
602
|
+
if (payload?.type === "open-in-browser") {
|
|
603
|
+
const target = payload?.url || payload?.href || null;
|
|
604
|
+
if (target) {
|
|
605
|
+
const opened = window.open(target, "_blank", "noopener,noreferrer");
|
|
606
|
+
if (!opened) {
|
|
607
|
+
// Fallback when popup is blocked.
|
|
608
|
+
window.location.href = target;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return Promise.resolve();
|
|
612
|
+
}
|
|
613
|
+
|
|
602
614
|
sendEnvelope({
|
|
603
615
|
type: "view-message",
|
|
604
616
|
payload
|
package/src/message-router.mjs
CHANGED
|
@@ -1051,6 +1051,16 @@ export class MessageRouter {
|
|
|
1051
1051
|
};
|
|
1052
1052
|
break;
|
|
1053
1053
|
}
|
|
1054
|
+
case "git-create-branch": {
|
|
1055
|
+
payload = await this._handleGitCreateBranch(params);
|
|
1056
|
+
status = payload.ok ? 200 : 500;
|
|
1057
|
+
break;
|
|
1058
|
+
}
|
|
1059
|
+
case "git-checkout-branch": {
|
|
1060
|
+
payload = await this._handleGitCheckoutBranch(params);
|
|
1061
|
+
status = payload.ok ? 200 : 500;
|
|
1062
|
+
break;
|
|
1063
|
+
}
|
|
1054
1064
|
case "git-push": {
|
|
1055
1065
|
payload = await this._handleGitPush(params);
|
|
1056
1066
|
status = payload.ok ? 200 : 500;
|
|
@@ -1108,14 +1118,29 @@ export class MessageRouter {
|
|
|
1108
1118
|
payload = await this._resolveGhPrStatus({ cwd, headBranch });
|
|
1109
1119
|
break;
|
|
1110
1120
|
}
|
|
1121
|
+
case "generate-pull-request-message":
|
|
1122
|
+
payload = await this._handleGeneratePullRequestMessage(params);
|
|
1123
|
+
break;
|
|
1124
|
+
case "gh-pr-create":
|
|
1125
|
+
payload = await this._handleGhPrCreate(params);
|
|
1126
|
+
break;
|
|
1111
1127
|
case "paths-exist": {
|
|
1112
1128
|
const paths = Array.isArray(params?.paths) ? params.paths.filter((p) => typeof p === "string") : [];
|
|
1113
1129
|
payload = { existingPaths: paths };
|
|
1114
1130
|
break;
|
|
1115
1131
|
}
|
|
1116
1132
|
default:
|
|
1117
|
-
|
|
1118
|
-
|
|
1133
|
+
if (endpoint.startsWith("git-")) {
|
|
1134
|
+
this.logger.warn("Unhandled git vscode fetch endpoint", { endpoint });
|
|
1135
|
+
payload = {
|
|
1136
|
+
ok: false,
|
|
1137
|
+
error: `unhandled git endpoint: ${endpoint}`
|
|
1138
|
+
};
|
|
1139
|
+
status = 500;
|
|
1140
|
+
} else {
|
|
1141
|
+
this.logger.warn("Unhandled vscode fetch endpoint", { endpoint });
|
|
1142
|
+
payload = {};
|
|
1143
|
+
}
|
|
1119
1144
|
}
|
|
1120
1145
|
|
|
1121
1146
|
this._sendFetchJson(ws, {
|
|
@@ -1318,6 +1343,350 @@ export class MessageRouter {
|
|
|
1318
1343
|
};
|
|
1319
1344
|
}
|
|
1320
1345
|
|
|
1346
|
+
async _handleGeneratePullRequestMessage(params) {
|
|
1347
|
+
const cwd = typeof params?.cwd === "string" && params.cwd.length > 0
|
|
1348
|
+
? params.cwd
|
|
1349
|
+
: process.cwd();
|
|
1350
|
+
const prompt = typeof params?.prompt === "string" ? params.prompt : "";
|
|
1351
|
+
const generated = await this._generatePullRequestMessageWithCodex({ cwd, prompt });
|
|
1352
|
+
const fallback = generated || await this._generateFallbackPullRequestMessage({ cwd, prompt });
|
|
1353
|
+
const title = this._normalizePullRequestTitle(fallback.title);
|
|
1354
|
+
const body = this._normalizePullRequestBody(fallback.body);
|
|
1355
|
+
|
|
1356
|
+
return {
|
|
1357
|
+
status: "success",
|
|
1358
|
+
title,
|
|
1359
|
+
body,
|
|
1360
|
+
// Older clients read `bodyInstructions`; keep it in sync with the generated body.
|
|
1361
|
+
bodyInstructions: body
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
_normalizePullRequestTitle(title) {
|
|
1366
|
+
if (typeof title !== "string") {
|
|
1367
|
+
return "Update project files";
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
const normalized = title.replace(/\s+/g, " ").trim();
|
|
1371
|
+
if (normalized.length === 0) {
|
|
1372
|
+
return "Update project files";
|
|
1373
|
+
}
|
|
1374
|
+
if (normalized.length <= 120) {
|
|
1375
|
+
return normalized;
|
|
1376
|
+
}
|
|
1377
|
+
return `${normalized.slice(0, 117).trimEnd()}...`;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
_normalizePullRequestBody(body) {
|
|
1381
|
+
if (typeof body !== "string") {
|
|
1382
|
+
return "## Summary\n- Update project files.\n\n## Testing\n- Not run (context not provided).";
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
const normalized = body.trim();
|
|
1386
|
+
if (normalized.length > 0) {
|
|
1387
|
+
return normalized;
|
|
1388
|
+
}
|
|
1389
|
+
return "## Summary\n- Update project files.\n\n## Testing\n- Not run (context not provided).";
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
_buildPullRequestCodexPrompt(prompt) {
|
|
1393
|
+
const context = typeof prompt === "string" && prompt.trim().length > 0
|
|
1394
|
+
? prompt.trim().slice(0, 20_000)
|
|
1395
|
+
: "No additional context was provided.";
|
|
1396
|
+
|
|
1397
|
+
return [
|
|
1398
|
+
"Generate a GitHub pull request title and body.",
|
|
1399
|
+
"Return JSON that matches the provided schema.",
|
|
1400
|
+
"Requirements:",
|
|
1401
|
+
"- title: concise imperative sentence, maximum 72 characters.",
|
|
1402
|
+
"- body: markdown with sections exactly '## Summary' and '## Testing'.",
|
|
1403
|
+
"- Summary should include 2 to 6 concrete bullet points.",
|
|
1404
|
+
"- Testing should include bullet points. If unknown, say '- Not run (context not provided).'.",
|
|
1405
|
+
"- Do not wrap output in code fences.",
|
|
1406
|
+
"- Use only the provided context.",
|
|
1407
|
+
"",
|
|
1408
|
+
"Context:",
|
|
1409
|
+
context
|
|
1410
|
+
].join("\n");
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
async _generatePullRequestMessageWithCodex({ cwd, prompt }) {
|
|
1414
|
+
let tempDir = null;
|
|
1415
|
+
try {
|
|
1416
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "codex-webstrap-prmsg-"));
|
|
1417
|
+
const schemaPath = path.join(tempDir, "schema.json");
|
|
1418
|
+
const outputPath = path.join(tempDir, "output.json");
|
|
1419
|
+
const schema = {
|
|
1420
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1421
|
+
type: "object",
|
|
1422
|
+
required: ["title", "body"],
|
|
1423
|
+
additionalProperties: false,
|
|
1424
|
+
properties: {
|
|
1425
|
+
title: { type: "string" },
|
|
1426
|
+
body: { type: "string" }
|
|
1427
|
+
}
|
|
1428
|
+
};
|
|
1429
|
+
await fs.writeFile(schemaPath, JSON.stringify(schema), "utf8");
|
|
1430
|
+
|
|
1431
|
+
const result = await this._runCommand(
|
|
1432
|
+
"codex",
|
|
1433
|
+
[
|
|
1434
|
+
"exec",
|
|
1435
|
+
"--ephemeral",
|
|
1436
|
+
"--sandbox",
|
|
1437
|
+
"read-only",
|
|
1438
|
+
"--output-schema",
|
|
1439
|
+
schemaPath,
|
|
1440
|
+
"--output-last-message",
|
|
1441
|
+
outputPath,
|
|
1442
|
+
this._buildPullRequestCodexPrompt(prompt)
|
|
1443
|
+
],
|
|
1444
|
+
{
|
|
1445
|
+
timeoutMs: 120_000,
|
|
1446
|
+
allowNonZero: true,
|
|
1447
|
+
cwd
|
|
1448
|
+
}
|
|
1449
|
+
);
|
|
1450
|
+
|
|
1451
|
+
if (!result.ok) {
|
|
1452
|
+
this.logger.warn("PR message generation via codex failed", {
|
|
1453
|
+
cwd,
|
|
1454
|
+
error: result.error || result.stderr || "unknown error"
|
|
1455
|
+
});
|
|
1456
|
+
return null;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
const rawOutput = await fs.readFile(outputPath, "utf8");
|
|
1460
|
+
const parsed = safeJsonParse(rawOutput);
|
|
1461
|
+
if (!parsed || typeof parsed !== "object") {
|
|
1462
|
+
return null;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
const title = this._normalizePullRequestTitle(parsed.title);
|
|
1466
|
+
const body = this._normalizePullRequestBody(parsed.body);
|
|
1467
|
+
return { title, body };
|
|
1468
|
+
} catch (error) {
|
|
1469
|
+
this.logger.warn("PR message generation via codex errored", {
|
|
1470
|
+
cwd,
|
|
1471
|
+
error: toErrorMessage(error)
|
|
1472
|
+
});
|
|
1473
|
+
return null;
|
|
1474
|
+
} finally {
|
|
1475
|
+
if (tempDir) {
|
|
1476
|
+
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
async _resolvePullRequestBaseRef(cwd) {
|
|
1482
|
+
const originHead = await this._runCommand(
|
|
1483
|
+
"git",
|
|
1484
|
+
["-C", cwd, "symbolic-ref", "--short", "refs/remotes/origin/HEAD"],
|
|
1485
|
+
{ timeoutMs: 5_000, allowNonZero: true, cwd }
|
|
1486
|
+
);
|
|
1487
|
+
if (originHead.ok && originHead.stdout) {
|
|
1488
|
+
return originHead.stdout;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
const candidates = ["origin/main", "origin/master", "main", "master"];
|
|
1492
|
+
for (const candidate of candidates) {
|
|
1493
|
+
const exists = await this._runCommand(
|
|
1494
|
+
"git",
|
|
1495
|
+
["-C", cwd, "rev-parse", "--verify", "--quiet", candidate],
|
|
1496
|
+
{ timeoutMs: 5_000, allowNonZero: true, cwd }
|
|
1497
|
+
);
|
|
1498
|
+
if (exists.code === 0) {
|
|
1499
|
+
return candidate;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
return null;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
async _generateFallbackPullRequestMessage({ cwd, prompt }) {
|
|
1507
|
+
const baseRef = await this._resolvePullRequestBaseRef(cwd);
|
|
1508
|
+
const logArgs = baseRef
|
|
1509
|
+
? ["-C", cwd, "log", "--no-merges", "--pretty=format:%s", `${baseRef}..HEAD`, "-n", "6"]
|
|
1510
|
+
: ["-C", cwd, "log", "--no-merges", "--pretty=format:%s", "-n", "6"];
|
|
1511
|
+
const logResult = await this._runCommand("git", logArgs, {
|
|
1512
|
+
timeoutMs: 8_000,
|
|
1513
|
+
allowNonZero: true,
|
|
1514
|
+
cwd
|
|
1515
|
+
});
|
|
1516
|
+
const commitSubjects = (logResult.stdout || "")
|
|
1517
|
+
.split("\n")
|
|
1518
|
+
.map((line) => line.trim())
|
|
1519
|
+
.filter(Boolean);
|
|
1520
|
+
|
|
1521
|
+
const range = baseRef ? `${baseRef}...HEAD` : "HEAD~1..HEAD";
|
|
1522
|
+
const filesResult = await this._runCommand(
|
|
1523
|
+
"git",
|
|
1524
|
+
["-C", cwd, "diff", "--name-only", "--diff-filter=ACMR", range],
|
|
1525
|
+
{ timeoutMs: 8_000, allowNonZero: true, cwd }
|
|
1526
|
+
);
|
|
1527
|
+
const changedFiles = (filesResult.stdout || "")
|
|
1528
|
+
.split("\n")
|
|
1529
|
+
.map((line) => line.trim())
|
|
1530
|
+
.filter(Boolean);
|
|
1531
|
+
|
|
1532
|
+
const statsResult = await this._runCommand(
|
|
1533
|
+
"git",
|
|
1534
|
+
["-C", cwd, "diff", "--numstat", range],
|
|
1535
|
+
{ timeoutMs: 8_000, allowNonZero: true, cwd }
|
|
1536
|
+
);
|
|
1537
|
+
let additions = 0;
|
|
1538
|
+
let deletions = 0;
|
|
1539
|
+
for (const line of (statsResult.stdout || "").split("\n")) {
|
|
1540
|
+
const [addedRaw, deletedRaw] = line.split("\t");
|
|
1541
|
+
const added = Number.parseInt(addedRaw, 10);
|
|
1542
|
+
const deleted = Number.parseInt(deletedRaw, 10);
|
|
1543
|
+
additions += Number.isFinite(added) ? added : 0;
|
|
1544
|
+
deletions += Number.isFinite(deleted) ? deleted : 0;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
const branch = await this._resolveGitCurrentBranch(cwd);
|
|
1548
|
+
const title = this._normalizePullRequestTitle(
|
|
1549
|
+
commitSubjects[0] || (branch ? `Update ${branch}` : "Update project files")
|
|
1550
|
+
);
|
|
1551
|
+
|
|
1552
|
+
const summaryBullets = [];
|
|
1553
|
+
for (const subject of commitSubjects.slice(0, 3)) {
|
|
1554
|
+
summaryBullets.push(subject);
|
|
1555
|
+
}
|
|
1556
|
+
if (summaryBullets.length === 0) {
|
|
1557
|
+
summaryBullets.push("Update project files.");
|
|
1558
|
+
}
|
|
1559
|
+
if (changedFiles.length > 0) {
|
|
1560
|
+
summaryBullets.push(`Modify ${changedFiles.length} file${changedFiles.length === 1 ? "" : "s"}.`);
|
|
1561
|
+
}
|
|
1562
|
+
if (additions > 0 || deletions > 0) {
|
|
1563
|
+
summaryBullets.push(`Diff summary: +${additions} / -${deletions} lines.`);
|
|
1564
|
+
}
|
|
1565
|
+
if (baseRef) {
|
|
1566
|
+
summaryBullets.push(`Base branch: ${baseRef}.`);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
const bodyLines = ["## Summary"];
|
|
1570
|
+
for (const bullet of summaryBullets.slice(0, 6)) {
|
|
1571
|
+
bodyLines.push(`- ${bullet}`);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
bodyLines.push("", "## Testing", "- Not run (context not provided).");
|
|
1575
|
+
|
|
1576
|
+
if (changedFiles.length > 0) {
|
|
1577
|
+
bodyLines.push("", "## Files Changed");
|
|
1578
|
+
for (const file of changedFiles.slice(0, 12)) {
|
|
1579
|
+
bodyLines.push(`- \`${file}\``);
|
|
1580
|
+
}
|
|
1581
|
+
if (changedFiles.length > 12) {
|
|
1582
|
+
bodyLines.push(`- \`...and ${changedFiles.length - 12} more\``);
|
|
1583
|
+
}
|
|
1584
|
+
} else if (typeof prompt === "string" && prompt.trim().length > 0) {
|
|
1585
|
+
bodyLines.push("", "## Notes", "- Generated from available thread context.");
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
return {
|
|
1589
|
+
title,
|
|
1590
|
+
body: this._normalizePullRequestBody(bodyLines.join("\n"))
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
_extractGithubPrUrl(text) {
|
|
1595
|
+
if (typeof text !== "string" || text.length === 0) {
|
|
1596
|
+
return null;
|
|
1597
|
+
}
|
|
1598
|
+
const match = text.match(/https:\/\/github\.com\/[^\s/]+\/[^\s/]+\/pull\/\d+/);
|
|
1599
|
+
return match ? match[0] : null;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
async _handleGhPrCreate(params) {
|
|
1603
|
+
const cwd = typeof params?.cwd === "string" && params.cwd.length > 0
|
|
1604
|
+
? params.cwd
|
|
1605
|
+
: process.cwd();
|
|
1606
|
+
const headBranch = typeof params?.headBranch === "string" ? params.headBranch.trim() : "";
|
|
1607
|
+
const baseBranch = typeof params?.baseBranch === "string" ? params.baseBranch.trim() : "";
|
|
1608
|
+
const bodyInstructions = typeof params?.bodyInstructions === "string" ? params.bodyInstructions : "";
|
|
1609
|
+
const titleOverride = typeof params?.titleOverride === "string" ? params.titleOverride.trim() : "";
|
|
1610
|
+
const bodyOverride = typeof params?.bodyOverride === "string" ? params.bodyOverride.trim() : "";
|
|
1611
|
+
|
|
1612
|
+
if (!headBranch || !baseBranch) {
|
|
1613
|
+
return {
|
|
1614
|
+
status: "error",
|
|
1615
|
+
error: "headBranch and baseBranch are required",
|
|
1616
|
+
url: null,
|
|
1617
|
+
number: null
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
const ghStatus = await this._resolveGhCliStatus();
|
|
1622
|
+
if (!ghStatus.isInstalled || !ghStatus.isAuthenticated) {
|
|
1623
|
+
return {
|
|
1624
|
+
status: "error",
|
|
1625
|
+
error: "gh cli unavailable or unauthenticated",
|
|
1626
|
+
url: null,
|
|
1627
|
+
number: null
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
const args = [
|
|
1632
|
+
"pr",
|
|
1633
|
+
"create",
|
|
1634
|
+
"--head",
|
|
1635
|
+
headBranch,
|
|
1636
|
+
"--base",
|
|
1637
|
+
baseBranch
|
|
1638
|
+
];
|
|
1639
|
+
const shouldFill = titleOverride.length === 0 || bodyOverride.length === 0;
|
|
1640
|
+
if (shouldFill) {
|
|
1641
|
+
args.push("--fill");
|
|
1642
|
+
}
|
|
1643
|
+
if (titleOverride.length > 0) {
|
|
1644
|
+
args.push("--title", titleOverride);
|
|
1645
|
+
}
|
|
1646
|
+
if (bodyOverride.length > 0) {
|
|
1647
|
+
args.push("--body", bodyOverride);
|
|
1648
|
+
} else if (bodyInstructions.trim().length > 0) {
|
|
1649
|
+
args.push("--body", bodyInstructions);
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
const result = await this._runCommand("gh", args, {
|
|
1653
|
+
timeoutMs: 30_000,
|
|
1654
|
+
allowNonZero: true,
|
|
1655
|
+
cwd
|
|
1656
|
+
});
|
|
1657
|
+
|
|
1658
|
+
if (result.ok) {
|
|
1659
|
+
const url = this._extractGithubPrUrl(result.stdout) || this._extractGithubPrUrl(result.stderr);
|
|
1660
|
+
const numberMatch = url ? url.match(/\/pull\/(\d+)/) : null;
|
|
1661
|
+
const number = numberMatch ? Number.parseInt(numberMatch[1], 10) : null;
|
|
1662
|
+
return {
|
|
1663
|
+
status: "success",
|
|
1664
|
+
url: url || null,
|
|
1665
|
+
number: Number.isFinite(number) ? number : null
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
const combinedOutput = `${result.stdout || ""}\n${result.stderr || ""}`;
|
|
1670
|
+
const existingUrl = this._extractGithubPrUrl(combinedOutput);
|
|
1671
|
+
const alreadyExists = /already exists|a pull request for branch/i.test(combinedOutput);
|
|
1672
|
+
if (alreadyExists && existingUrl) {
|
|
1673
|
+
const numberMatch = existingUrl.match(/\/pull\/(\d+)/);
|
|
1674
|
+
const number = numberMatch ? Number.parseInt(numberMatch[1], 10) : null;
|
|
1675
|
+
return {
|
|
1676
|
+
status: "success",
|
|
1677
|
+
url: existingUrl,
|
|
1678
|
+
number: Number.isFinite(number) ? number : null
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
return {
|
|
1683
|
+
status: "error",
|
|
1684
|
+
error: result.error || result.stderr || "failed to create pull request",
|
|
1685
|
+
url: null,
|
|
1686
|
+
number: null
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1321
1690
|
async _resolveGitMergeBase({ gitRoot, baseBranch }) {
|
|
1322
1691
|
if (!baseBranch) {
|
|
1323
1692
|
return {
|
|
@@ -1339,16 +1708,138 @@ export class MessageRouter {
|
|
|
1339
1708
|
};
|
|
1340
1709
|
}
|
|
1341
1710
|
|
|
1711
|
+
async _resolveGitCurrentBranch(cwd) {
|
|
1712
|
+
const result = await this._runCommand("git", ["-C", cwd, "rev-parse", "--abbrev-ref", "HEAD"], {
|
|
1713
|
+
timeoutMs: 5_000,
|
|
1714
|
+
allowNonZero: true,
|
|
1715
|
+
cwd
|
|
1716
|
+
});
|
|
1717
|
+
if (!result.ok || !result.stdout || result.stdout === "HEAD") {
|
|
1718
|
+
return null;
|
|
1719
|
+
}
|
|
1720
|
+
return result.stdout;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
async _handleGitCreateBranch(params) {
|
|
1724
|
+
const cwd = typeof params?.cwd === "string" && params.cwd.length > 0
|
|
1725
|
+
? params.cwd
|
|
1726
|
+
: process.cwd();
|
|
1727
|
+
const branch = typeof params?.branch === "string" && params.branch.trim().length > 0
|
|
1728
|
+
? params.branch.trim()
|
|
1729
|
+
: null;
|
|
1730
|
+
|
|
1731
|
+
if (!branch) {
|
|
1732
|
+
return {
|
|
1733
|
+
ok: false,
|
|
1734
|
+
code: null,
|
|
1735
|
+
error: "branch is required",
|
|
1736
|
+
stdout: "",
|
|
1737
|
+
stderr: ""
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
const existingResult = await this._runCommand("git", ["-C", cwd, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`], {
|
|
1742
|
+
cwd,
|
|
1743
|
+
timeoutMs: 10_000,
|
|
1744
|
+
allowNonZero: true
|
|
1745
|
+
});
|
|
1746
|
+
if (existingResult.code === 0) {
|
|
1747
|
+
return {
|
|
1748
|
+
ok: true,
|
|
1749
|
+
code: 0,
|
|
1750
|
+
branch,
|
|
1751
|
+
created: false,
|
|
1752
|
+
alreadyExists: true,
|
|
1753
|
+
stdout: existingResult.stdout || "",
|
|
1754
|
+
stderr: existingResult.stderr || ""
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
const createResult = await this._runCommand("git", ["-C", cwd, "branch", branch], {
|
|
1759
|
+
cwd,
|
|
1760
|
+
timeoutMs: 10_000,
|
|
1761
|
+
allowNonZero: true
|
|
1762
|
+
});
|
|
1763
|
+
if (createResult.ok) {
|
|
1764
|
+
return {
|
|
1765
|
+
ok: true,
|
|
1766
|
+
code: createResult.code,
|
|
1767
|
+
branch,
|
|
1768
|
+
created: true,
|
|
1769
|
+
alreadyExists: false,
|
|
1770
|
+
stdout: createResult.stdout || "",
|
|
1771
|
+
stderr: createResult.stderr || ""
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
return {
|
|
1776
|
+
ok: false,
|
|
1777
|
+
code: createResult.code,
|
|
1778
|
+
error: createResult.error || createResult.stderr || "git branch failed",
|
|
1779
|
+
stdout: createResult.stdout || "",
|
|
1780
|
+
stderr: createResult.stderr || ""
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
async _handleGitCheckoutBranch(params) {
|
|
1785
|
+
const cwd = typeof params?.cwd === "string" && params.cwd.length > 0
|
|
1786
|
+
? params.cwd
|
|
1787
|
+
: process.cwd();
|
|
1788
|
+
const branch = typeof params?.branch === "string" && params.branch.trim().length > 0
|
|
1789
|
+
? params.branch.trim()
|
|
1790
|
+
: null;
|
|
1791
|
+
|
|
1792
|
+
if (!branch) {
|
|
1793
|
+
return {
|
|
1794
|
+
ok: false,
|
|
1795
|
+
code: null,
|
|
1796
|
+
error: "branch is required",
|
|
1797
|
+
stdout: "",
|
|
1798
|
+
stderr: ""
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
const checkoutResult = await this._runCommand("git", ["-C", cwd, "checkout", branch], {
|
|
1803
|
+
cwd,
|
|
1804
|
+
timeoutMs: 20_000,
|
|
1805
|
+
allowNonZero: true
|
|
1806
|
+
});
|
|
1807
|
+
if (!checkoutResult.ok) {
|
|
1808
|
+
return {
|
|
1809
|
+
ok: false,
|
|
1810
|
+
code: checkoutResult.code,
|
|
1811
|
+
error: checkoutResult.error || checkoutResult.stderr || "git checkout failed",
|
|
1812
|
+
stdout: checkoutResult.stdout || "",
|
|
1813
|
+
stderr: checkoutResult.stderr || ""
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
const currentBranch = await this._resolveGitCurrentBranch(cwd);
|
|
1818
|
+
return {
|
|
1819
|
+
ok: true,
|
|
1820
|
+
code: checkoutResult.code,
|
|
1821
|
+
branch: currentBranch || branch,
|
|
1822
|
+
stdout: checkoutResult.stdout || "",
|
|
1823
|
+
stderr: checkoutResult.stderr || ""
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1342
1827
|
async _handleGitPush(params) {
|
|
1343
1828
|
const cwd = typeof params?.cwd === "string" && params.cwd.length > 0
|
|
1344
1829
|
? params.cwd
|
|
1345
1830
|
: process.cwd();
|
|
1346
|
-
const
|
|
1831
|
+
const explicitRemote = typeof params?.remote === "string" && params.remote.trim().length > 0
|
|
1347
1832
|
? params.remote.trim()
|
|
1348
1833
|
: null;
|
|
1349
1834
|
const branch = typeof params?.branch === "string" && params.branch.trim().length > 0
|
|
1350
1835
|
? params.branch.trim()
|
|
1351
1836
|
: null;
|
|
1837
|
+
const refspec = typeof params?.refspec === "string" && params.refspec.trim().length > 0
|
|
1838
|
+
? params.refspec.trim()
|
|
1839
|
+
: null;
|
|
1840
|
+
const remote = explicitRemote || (
|
|
1841
|
+
params?.setUpstream === true && (branch || refspec) ? "origin" : null
|
|
1842
|
+
);
|
|
1352
1843
|
|
|
1353
1844
|
const args = ["-C", cwd, "push"];
|
|
1354
1845
|
if (params?.force === true || params?.forceWithLease === true) {
|
|
@@ -1360,7 +1851,9 @@ export class MessageRouter {
|
|
|
1360
1851
|
if (remote) {
|
|
1361
1852
|
args.push(remote);
|
|
1362
1853
|
}
|
|
1363
|
-
if (
|
|
1854
|
+
if (refspec) {
|
|
1855
|
+
args.push(refspec);
|
|
1856
|
+
} else if (branch) {
|
|
1364
1857
|
args.push(branch);
|
|
1365
1858
|
}
|
|
1366
1859
|
|
package/src/server.mjs
CHANGED
|
@@ -114,6 +114,7 @@ async function main() {
|
|
|
114
114
|
const config = parseConfig();
|
|
115
115
|
|
|
116
116
|
const tokenResult = await ensurePersistentToken(config.tokenFile);
|
|
117
|
+
const runtimeMetadataPath = `${tokenResult.tokenFilePath}.runtime`;
|
|
117
118
|
const sessionStore = new SessionStore({ ttlMs: 1000 * 60 * 60 * 12 });
|
|
118
119
|
const auth = createAuthController({ token: tokenResult.token, sessionStore });
|
|
119
120
|
|
|
@@ -299,6 +300,25 @@ async function main() {
|
|
|
299
300
|
server.listen(config.port, config.bind, resolve);
|
|
300
301
|
});
|
|
301
302
|
|
|
303
|
+
try {
|
|
304
|
+
await fs.writeFile(
|
|
305
|
+
runtimeMetadataPath,
|
|
306
|
+
JSON.stringify({
|
|
307
|
+
bind: config.bind,
|
|
308
|
+
port: config.port,
|
|
309
|
+
tokenFile: tokenResult.tokenFilePath,
|
|
310
|
+
pid: process.pid,
|
|
311
|
+
startedAt: Date.now()
|
|
312
|
+
}) + "\n",
|
|
313
|
+
{ mode: 0o600 }
|
|
314
|
+
);
|
|
315
|
+
} catch (error) {
|
|
316
|
+
logger.warn("Failed to write runtime metadata file", {
|
|
317
|
+
path: runtimeMetadataPath,
|
|
318
|
+
error: toErrorMessage(error)
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
302
322
|
const authHint = `http://${config.bind}:${config.port}/__webstrapper/auth?token=<redacted>`;
|
|
303
323
|
const loginCommand = `open \"http://${config.bind}:${config.port}/__webstrapper/auth?token=$(cat ${tokenResult.tokenFilePath})\"`;
|
|
304
324
|
|
|
@@ -356,6 +376,11 @@ async function main() {
|
|
|
356
376
|
await new Promise((resolve) => {
|
|
357
377
|
server.close(() => resolve());
|
|
358
378
|
});
|
|
379
|
+
try {
|
|
380
|
+
await fs.unlink(runtimeMetadataPath);
|
|
381
|
+
} catch {
|
|
382
|
+
// ignore
|
|
383
|
+
}
|
|
359
384
|
|
|
360
385
|
process.exit(0);
|
|
361
386
|
}
|