@junctionpanel/server 0.1.20 → 0.1.22

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.
@@ -379,6 +379,19 @@ async function getWorktreeRoot(cwd) {
379
379
  return null;
380
380
  }
381
381
  }
382
+ async function getUpstreamBranch(cwd) {
383
+ try {
384
+ const { stdout } = await execAsync("git rev-parse --abbrev-ref --symbolic-full-name @{upstream}", {
385
+ cwd,
386
+ env: READ_ONLY_GIT_ENV,
387
+ });
388
+ const upstream = stdout.trim();
389
+ return upstream.length > 0 ? upstream : null;
390
+ }
391
+ catch {
392
+ return null;
393
+ }
394
+ }
382
395
  async function getMainRepoRoot(cwd) {
383
396
  const { stdout: commonDirOut } = await execAsync("git rev-parse --path-format=absolute --git-common-dir", { cwd, env: READ_ONLY_GIT_ENV });
384
397
  const commonDir = commonDirOut.trim();
@@ -738,11 +751,13 @@ export async function getCheckoutStatus(cwd, context) {
738
751
  const isDirty = await isWorkingTreeDirty(cwd);
739
752
  const hasRemote = remoteUrl !== null;
740
753
  const baseRef = configured.baseRef ?? (await resolveBaseRef(cwd));
754
+ const upstreamBranch = currentBranch ? await getUpstreamBranch(cwd) : null;
741
755
  const [aheadBehind, aheadOfOrigin, behindOfOrigin] = await Promise.all([
742
756
  baseRef && currentBranch ? getAheadBehind(cwd, baseRef, currentBranch) : Promise.resolve(null),
743
757
  hasRemote && currentBranch ? getAheadOfOrigin(cwd, currentBranch) : Promise.resolve(null),
744
758
  hasRemote && currentBranch ? getBehindOfOrigin(cwd, currentBranch) : Promise.resolve(null),
745
759
  ]);
760
+ const hasUpstream = upstreamBranch !== null;
746
761
  if (configured.isJunctionOwnedWorktree) {
747
762
  const mainRepoRoot = await getMainRepoRoot(cwd);
748
763
  return {
@@ -755,6 +770,8 @@ export async function getCheckoutStatus(cwd, context) {
755
770
  aheadBehind,
756
771
  aheadOfOrigin,
757
772
  behindOfOrigin,
773
+ hasUpstream,
774
+ upstreamBranch,
758
775
  hasRemote,
759
776
  remoteUrl,
760
777
  isJunctionOwnedWorktree: true,
@@ -769,6 +786,8 @@ export async function getCheckoutStatus(cwd, context) {
769
786
  aheadBehind,
770
787
  aheadOfOrigin,
771
788
  behindOfOrigin,
789
+ hasUpstream,
790
+ upstreamBranch,
772
791
  hasRemote,
773
792
  remoteUrl,
774
793
  isJunctionOwnedWorktree: false,
@@ -1200,6 +1219,9 @@ export async function pushCurrentBranch(cwd) {
1200
1219
  }
1201
1220
  await execAsync(`git push -u origin ${currentBranch}`, { cwd });
1202
1221
  }
1222
+ const GH_JSON_MAX_BYTES = 512 * 1024;
1223
+ const GH_FAILED_LOG_MAX_BYTES = 200 * 1024;
1224
+ const GH_FAILED_LOG_TOTAL_MAX_BYTES = 300 * 1024;
1203
1225
  async function ensureGhAvailable(cwd) {
1204
1226
  try {
1205
1227
  await execAsync("gh --version", { cwd });
@@ -1282,13 +1304,18 @@ async function listPullRequestsForHead(options) {
1282
1304
  const parsed = JSON.parse(stdout.trim());
1283
1305
  return Array.isArray(parsed) ? parsed : [];
1284
1306
  }
1285
- function buildPullRequestStatus(current, fallbackHead) {
1307
+ function buildPullRequestBaseStatus(current, fallbackHead) {
1286
1308
  if (!current || typeof current !== "object") {
1287
1309
  return null;
1288
1310
  }
1311
+ const number = typeof current.number === "number" && Number.isFinite(current.number) ? current.number : null;
1289
1312
  const url = current.html_url ?? current.url;
1290
1313
  const title = current.title;
1291
- if (typeof url !== "string" || typeof title !== "string" || !url || !title) {
1314
+ if (number === null ||
1315
+ typeof url !== "string" ||
1316
+ typeof title !== "string" ||
1317
+ !url ||
1318
+ !title) {
1292
1319
  return null;
1293
1320
  }
1294
1321
  const mergedAt = typeof current.merged_at === "string" && current.merged_at.trim().length > 0
@@ -1300,49 +1327,346 @@ function buildPullRequestStatus(current, fallbackHead) {
1300
1327
  ? current.state
1301
1328
  : "";
1302
1329
  return {
1330
+ number,
1303
1331
  url,
1304
1332
  title,
1305
1333
  state,
1306
1334
  baseRefName: current.base?.ref ?? "",
1307
1335
  headRefName: current.head?.ref ?? fallbackHead,
1308
1336
  isMerged: mergedAt !== null,
1337
+ draft: Boolean(current.draft),
1338
+ headSha: typeof current.head?.sha === "string" ? current.head.sha : null,
1309
1339
  };
1310
1340
  }
1311
- export async function createPullRequest(cwd, options) {
1312
- await requireGitRepo(cwd);
1313
- await ensureGhAvailable(cwd);
1314
- const repo = await resolveGitHubRepo(cwd);
1315
- if (!repo) {
1316
- throw new Error("Unable to determine GitHub repo from git remote");
1341
+ function normalizeCheckBucket(value, state) {
1342
+ if (value === "pass" || value === "fail" || value === "pending" || value === "skipping" || value === "cancel") {
1343
+ return value;
1344
+ }
1345
+ const normalizedState = state.toLowerCase();
1346
+ if (normalizedState === "success" ||
1347
+ normalizedState === "neutral" ||
1348
+ normalizedState === "skipped") {
1349
+ return "pass";
1350
+ }
1351
+ if (normalizedState === "failure" ||
1352
+ normalizedState === "error" ||
1353
+ normalizedState === "timed_out" ||
1354
+ normalizedState === "startup_failure" ||
1355
+ normalizedState === "action_required") {
1356
+ return "fail";
1357
+ }
1358
+ if (normalizedState === "cancelled") {
1359
+ return "cancel";
1360
+ }
1361
+ if (normalizedState === "queued" ||
1362
+ normalizedState === "waiting" ||
1363
+ normalizedState === "requested" ||
1364
+ normalizedState === "pending" ||
1365
+ normalizedState === "in_progress") {
1366
+ return "pending";
1317
1367
  }
1318
- const head = options.head ?? (await getCurrentBranch(cwd));
1319
- const configured = await getConfiguredBaseRefForCwd(cwd);
1320
- const base = configured.baseRef ?? options.base ?? (await resolveBaseRef(cwd));
1321
- if (!head) {
1322
- throw new Error("Unable to determine head branch for PR");
1368
+ return null;
1369
+ }
1370
+ function parsePullRequestChecks(input) {
1371
+ if (!Array.isArray(input)) {
1372
+ return [];
1323
1373
  }
1324
- if (!base) {
1325
- throw new Error("Unable to determine base branch for PR");
1374
+ return input
1375
+ .map((entry) => {
1376
+ if (!entry || typeof entry !== "object") {
1377
+ return null;
1378
+ }
1379
+ const state = typeof entry.state === "string" ? entry.state : "";
1380
+ const bucket = normalizeCheckBucket(entry.bucket, state);
1381
+ const name = typeof entry.name === "string" ? entry.name.trim() : "";
1382
+ if (!bucket || !name) {
1383
+ return null;
1384
+ }
1385
+ return {
1386
+ name,
1387
+ workflow: typeof entry.workflow === "string" && entry.workflow.trim().length > 0
1388
+ ? entry.workflow.trim()
1389
+ : null,
1390
+ link: typeof entry.link === "string" && entry.link.trim().length > 0 ? entry.link : null,
1391
+ description: typeof entry.description === "string" && entry.description.trim().length > 0
1392
+ ? entry.description.trim()
1393
+ : null,
1394
+ state,
1395
+ bucket,
1396
+ };
1397
+ })
1398
+ .filter((entry) => entry !== null);
1399
+ }
1400
+ async function getRequiredPullRequestChecks(cwd, prNumber) {
1401
+ let result;
1402
+ try {
1403
+ result = await spawnLimitedText({
1404
+ cmd: "gh",
1405
+ args: [
1406
+ "pr",
1407
+ "checks",
1408
+ String(prNumber),
1409
+ "--required",
1410
+ "--json",
1411
+ "bucket,description,link,name,state,workflow",
1412
+ ],
1413
+ cwd,
1414
+ maxBytes: GH_JSON_MAX_BYTES,
1415
+ acceptExitCodes: [0, 1, 8],
1416
+ });
1326
1417
  }
1327
- const normalizedBase = normalizeLocalBranchRefName(base);
1328
- if (configured.isJunctionOwnedWorktree && options.base && options.base !== base) {
1329
- throw new Error(`Base ref mismatch: expected ${base}, got ${options.base}`);
1418
+ catch (error) {
1419
+ if (getCommandErrorText(error).includes("no checks reported")) {
1420
+ return [];
1421
+ }
1422
+ throw error;
1330
1423
  }
1331
- await execAsync(`git push -u origin ${head}`, { cwd });
1332
- const args = ["api", "-X", "POST", `repos/${repo}/pulls`, "-f", `title=${options.title}`];
1333
- args.push("-f", `head=${head}`);
1334
- args.push("-f", `base=${normalizedBase}`);
1335
- if (options.body) {
1336
- args.push("-f", `body=${options.body}`);
1424
+ const text = result.text.trim();
1425
+ if (!text) {
1426
+ return [];
1337
1427
  }
1338
- const { stdout } = await execFileAsync("gh", args, { cwd });
1339
- const parsed = JSON.parse(stdout.trim());
1340
- if (!parsed?.url || !parsed?.number) {
1341
- throw new Error("GitHub CLI did not return PR url/number");
1428
+ return parsePullRequestChecks(JSON.parse(text));
1429
+ }
1430
+ function parseStatusCheckRollup(input) {
1431
+ if (!Array.isArray(input)) {
1432
+ return [];
1342
1433
  }
1343
- return { url: parsed.url, number: parsed.number };
1434
+ return input
1435
+ .map((entry) => {
1436
+ if (!entry || typeof entry !== "object") {
1437
+ return null;
1438
+ }
1439
+ const statusValue = typeof entry.status === "string" && entry.status.trim().length > 0
1440
+ ? entry.status.trim()
1441
+ : typeof entry.state === "string" && entry.state.trim().length > 0
1442
+ ? entry.state.trim()
1443
+ : "";
1444
+ const conclusionValue = typeof entry.conclusion === "string" && entry.conclusion.trim().length > 0
1445
+ ? entry.conclusion.trim()
1446
+ : "";
1447
+ const bucket = normalizeCheckBucket(undefined, conclusionValue || statusValue) ??
1448
+ (statusValue.toLowerCase() === "completed" && conclusionValue
1449
+ ? normalizeCheckBucket(undefined, conclusionValue)
1450
+ : null);
1451
+ const name = typeof entry.name === "string" && entry.name.trim().length > 0
1452
+ ? entry.name.trim()
1453
+ : typeof entry.context === "string" && entry.context.trim().length > 0
1454
+ ? entry.context.trim()
1455
+ : "";
1456
+ if (!bucket || !name) {
1457
+ return null;
1458
+ }
1459
+ return {
1460
+ name,
1461
+ workflow: typeof entry.workflowName === "string" && entry.workflowName.trim().length > 0
1462
+ ? entry.workflowName.trim()
1463
+ : null,
1464
+ link: typeof entry.detailsUrl === "string" && entry.detailsUrl.trim().length > 0
1465
+ ? entry.detailsUrl.trim()
1466
+ : typeof entry.targetUrl === "string" && entry.targetUrl.trim().length > 0
1467
+ ? entry.targetUrl.trim()
1468
+ : null,
1469
+ description: typeof entry.description === "string" && entry.description.trim().length > 0
1470
+ ? entry.description.trim()
1471
+ : null,
1472
+ state: conclusionValue || statusValue,
1473
+ bucket,
1474
+ };
1475
+ })
1476
+ .filter((entry) => entry !== null);
1344
1477
  }
1345
- export async function getPullRequestStatus(cwd) {
1478
+ function mergeCheckStatuses(requiredChecks, rollupChecks) {
1479
+ const merged = new Map();
1480
+ for (const check of rollupChecks) {
1481
+ const key = `${check.workflow ?? ""}::${check.name}`;
1482
+ merged.set(key, check);
1483
+ }
1484
+ for (const check of requiredChecks) {
1485
+ const key = `${check.workflow ?? ""}::${check.name}`;
1486
+ merged.set(key, check);
1487
+ }
1488
+ return Array.from(merged.values());
1489
+ }
1490
+ function deriveChecksState(checks) {
1491
+ if (checks.some((check) => check.bucket === "fail" || check.bucket === "cancel")) {
1492
+ return "failing";
1493
+ }
1494
+ if (checks.some((check) => check.bucket === "pending")) {
1495
+ return "pending";
1496
+ }
1497
+ return "passing";
1498
+ }
1499
+ async function getPullRequestMergeability(cwd, prNumber) {
1500
+ const result = await spawnLimitedText({
1501
+ cmd: "gh",
1502
+ args: [
1503
+ "pr",
1504
+ "view",
1505
+ String(prNumber),
1506
+ "--json",
1507
+ "mergeable,mergeStateStatus,statusCheckRollup",
1508
+ ],
1509
+ cwd,
1510
+ maxBytes: GH_JSON_MAX_BYTES,
1511
+ });
1512
+ const text = result.text.trim();
1513
+ if (!text) {
1514
+ return { hasConflicts: false, rollupChecks: [] };
1515
+ }
1516
+ const parsed = JSON.parse(text);
1517
+ const mergeable = typeof parsed.mergeable === "string" ? parsed.mergeable.trim().toUpperCase() : "";
1518
+ const mergeStateStatus = typeof parsed.mergeStateStatus === "string"
1519
+ ? parsed.mergeStateStatus.trim().toUpperCase()
1520
+ : "";
1521
+ return {
1522
+ hasConflicts: mergeable === "CONFLICTING" || mergeStateStatus === "DIRTY",
1523
+ rollupChecks: parseStatusCheckRollup(parsed.statusCheckRollup),
1524
+ };
1525
+ }
1526
+ function toPullRequestStatus(baseStatus, checks, mergeability) {
1527
+ const checksState = baseStatus.isMerged ? "passing" : deriveChecksState(checks);
1528
+ const requiredChecksPassed = baseStatus.isMerged
1529
+ ? true
1530
+ : checks.every((check) => check.bucket === "pass" || check.bucket === "skipping");
1531
+ const hasConflicts = baseStatus.isMerged ? false : (mergeability?.hasConflicts ?? false);
1532
+ return {
1533
+ number: baseStatus.number,
1534
+ url: baseStatus.url,
1535
+ title: baseStatus.title,
1536
+ state: baseStatus.state,
1537
+ baseRefName: baseStatus.baseRefName,
1538
+ headRefName: baseStatus.headRefName,
1539
+ isMerged: baseStatus.isMerged,
1540
+ checksState,
1541
+ requiredChecksPassed,
1542
+ canMerge: !baseStatus.isMerged && !baseStatus.draft && requiredChecksPassed && !hasConflicts,
1543
+ hasConflicts,
1544
+ };
1545
+ }
1546
+ async function listWorkflowRunsForHead(options) {
1547
+ const args = [
1548
+ "run",
1549
+ "list",
1550
+ "--json",
1551
+ "conclusion,createdAt,databaseId,displayTitle,name,status,url,workflowName",
1552
+ "-L",
1553
+ "50",
1554
+ ];
1555
+ if (options.headSha) {
1556
+ args.push("--commit", options.headSha);
1557
+ }
1558
+ else {
1559
+ args.push("--branch", options.headBranch);
1560
+ }
1561
+ const result = await spawnLimitedText({
1562
+ cmd: "gh",
1563
+ args,
1564
+ cwd: options.cwd,
1565
+ maxBytes: GH_JSON_MAX_BYTES,
1566
+ });
1567
+ const text = result.text.trim();
1568
+ if (!text) {
1569
+ return [];
1570
+ }
1571
+ const parsed = JSON.parse(text);
1572
+ if (!Array.isArray(parsed)) {
1573
+ return [];
1574
+ }
1575
+ return parsed
1576
+ .map((entry) => {
1577
+ if (!entry || typeof entry !== "object") {
1578
+ return null;
1579
+ }
1580
+ const databaseId = typeof entry.databaseId === "number" && Number.isFinite(entry.databaseId)
1581
+ ? entry.databaseId
1582
+ : null;
1583
+ if (databaseId === null) {
1584
+ return null;
1585
+ }
1586
+ return {
1587
+ databaseId,
1588
+ workflowName: typeof entry.workflowName === "string" && entry.workflowName.trim().length > 0
1589
+ ? entry.workflowName.trim()
1590
+ : null,
1591
+ name: typeof entry.name === "string" && entry.name.trim().length > 0 ? entry.name.trim() : null,
1592
+ displayTitle: typeof entry.displayTitle === "string" && entry.displayTitle.trim().length > 0
1593
+ ? entry.displayTitle.trim()
1594
+ : null,
1595
+ url: typeof entry.url === "string" && entry.url.trim().length > 0 ? entry.url.trim() : null,
1596
+ status: typeof entry.status === "string" && entry.status.trim().length > 0
1597
+ ? entry.status.trim()
1598
+ : null,
1599
+ conclusion: typeof entry.conclusion === "string" && entry.conclusion.trim().length > 0
1600
+ ? entry.conclusion.trim()
1601
+ : null,
1602
+ createdAt: typeof entry.createdAt === "string" && entry.createdAt.trim().length > 0
1603
+ ? entry.createdAt.trim()
1604
+ : null,
1605
+ };
1606
+ })
1607
+ .filter((entry) => entry !== null);
1608
+ }
1609
+ function isWorkflowRunFailed(run) {
1610
+ const conclusion = run.conclusion?.toLowerCase() ?? "";
1611
+ return (conclusion === "failure" ||
1612
+ conclusion === "cancelled" ||
1613
+ conclusion === "timed_out" ||
1614
+ conclusion === "startup_failure" ||
1615
+ conclusion === "action_required");
1616
+ }
1617
+ function findRunsForFailingChecks(checks, runs) {
1618
+ const failedRuns = runs.filter(isWorkflowRunFailed);
1619
+ const selected = [];
1620
+ const seen = new Set();
1621
+ for (const check of checks) {
1622
+ if (check.bucket !== "fail" && check.bucket !== "cancel") {
1623
+ continue;
1624
+ }
1625
+ const matchingRun = failedRuns.find((run) => check.workflow !== null &&
1626
+ (run.workflowName === check.workflow || run.name === check.workflow)) ??
1627
+ failedRuns.find((run) => run.name === check.name || run.displayTitle === check.name) ??
1628
+ null;
1629
+ if (matchingRun && !seen.has(matchingRun.databaseId)) {
1630
+ seen.add(matchingRun.databaseId);
1631
+ selected.push(matchingRun);
1632
+ }
1633
+ }
1634
+ if (selected.length > 0) {
1635
+ return selected;
1636
+ }
1637
+ return failedRuns.slice(0, 3);
1638
+ }
1639
+ async function getFailedWorkflowRunLog(cwd, runId) {
1640
+ const result = await spawnLimitedText({
1641
+ cmd: "gh",
1642
+ args: ["run", "view", String(runId), "--log-failed"],
1643
+ cwd,
1644
+ maxBytes: GH_FAILED_LOG_MAX_BYTES,
1645
+ });
1646
+ const text = result.text.trim();
1647
+ if (!text) {
1648
+ return "No failed-step logs were returned by GitHub CLI.";
1649
+ }
1650
+ return result.truncated ? `${text}\n\n[log truncated]` : text;
1651
+ }
1652
+ function appendBoundedText(parts, next, currentBytes) {
1653
+ if (currentBytes >= GH_FAILED_LOG_TOTAL_MAX_BYTES) {
1654
+ return currentBytes;
1655
+ }
1656
+ const nextBytes = Buffer.byteLength(next, "utf8");
1657
+ if (currentBytes + nextBytes <= GH_FAILED_LOG_TOTAL_MAX_BYTES) {
1658
+ parts.push(next);
1659
+ return currentBytes + nextBytes;
1660
+ }
1661
+ const remaining = GH_FAILED_LOG_TOTAL_MAX_BYTES - currentBytes;
1662
+ if (remaining <= 0) {
1663
+ return currentBytes;
1664
+ }
1665
+ const truncated = Buffer.from(next, "utf8").subarray(0, remaining).toString("utf8");
1666
+ parts.push(`${truncated}\n\n[combined log output truncated]`);
1667
+ return GH_FAILED_LOG_TOTAL_MAX_BYTES;
1668
+ }
1669
+ async function loadCurrentPullRequest(cwd) {
1346
1670
  await requireGitRepo(cwd);
1347
1671
  const repo = await resolveGitHubRepo(cwd);
1348
1672
  const head = await getCurrentBranch(cwd);
@@ -1350,6 +1674,8 @@ export async function getPullRequestStatus(cwd) {
1350
1674
  return {
1351
1675
  status: null,
1352
1676
  githubFeaturesEnabled: false,
1677
+ checks: [],
1678
+ headSha: null,
1353
1679
  };
1354
1680
  }
1355
1681
  try {
@@ -1359,6 +1685,8 @@ export async function getPullRequestStatus(cwd) {
1359
1685
  return {
1360
1686
  status: null,
1361
1687
  githubFeaturesEnabled: false,
1688
+ checks: [],
1689
+ headSha: null,
1362
1690
  };
1363
1691
  }
1364
1692
  const owner = repo.split("/")[0];
@@ -1377,16 +1705,49 @@ export async function getPullRequestStatus(cwd) {
1377
1705
  return {
1378
1706
  status: null,
1379
1707
  githubFeaturesEnabled: false,
1708
+ checks: [],
1709
+ headSha: null,
1380
1710
  };
1381
1711
  }
1382
1712
  throw error;
1383
1713
  }
1384
1714
  const openPull = openPulls[0] ?? null;
1385
- const openStatus = buildPullRequestStatus(openPull, head);
1386
- if (openStatus) {
1715
+ const openBaseStatus = buildPullRequestBaseStatus(openPull, head);
1716
+ if (openBaseStatus) {
1717
+ let checks;
1718
+ let mergeability = { hasConflicts: false, rollupChecks: [] };
1719
+ try {
1720
+ checks = await getRequiredPullRequestChecks(cwd, openBaseStatus.number);
1721
+ }
1722
+ catch (error) {
1723
+ if (isGhAuthError(error)) {
1724
+ return {
1725
+ status: null,
1726
+ githubFeaturesEnabled: false,
1727
+ checks: [],
1728
+ headSha: null,
1729
+ };
1730
+ }
1731
+ throw error;
1732
+ }
1733
+ try {
1734
+ mergeability = await getPullRequestMergeability(cwd, openBaseStatus.number);
1735
+ }
1736
+ catch (error) {
1737
+ if (isGhAuthError(error)) {
1738
+ return {
1739
+ status: null,
1740
+ githubFeaturesEnabled: false,
1741
+ checks: [],
1742
+ headSha: null,
1743
+ };
1744
+ }
1745
+ }
1387
1746
  return {
1388
- status: openStatus,
1747
+ status: toPullRequestStatus(openBaseStatus, mergeCheckStatuses(checks, mergeability.rollupChecks), mergeability),
1389
1748
  githubFeaturesEnabled: true,
1749
+ checks: mergeCheckStatuses(checks, mergeability.rollupChecks),
1750
+ headSha: openBaseStatus.headSha,
1390
1751
  };
1391
1752
  }
1392
1753
  let closedPulls;
@@ -1404,6 +1765,8 @@ export async function getPullRequestStatus(cwd) {
1404
1765
  return {
1405
1766
  status: null,
1406
1767
  githubFeaturesEnabled: false,
1768
+ checks: [],
1769
+ headSha: null,
1407
1770
  };
1408
1771
  }
1409
1772
  throw error;
@@ -1412,16 +1775,150 @@ export async function getPullRequestStatus(cwd) {
1412
1775
  typeof entry === "object" &&
1413
1776
  typeof entry.merged_at === "string" &&
1414
1777
  entry.merged_at.trim().length > 0) ?? null;
1415
- const mergedStatus = buildPullRequestStatus(mergedClosedPull, head);
1416
- if (!mergedStatus) {
1778
+ const mergedBaseStatus = buildPullRequestBaseStatus(mergedClosedPull, head);
1779
+ if (!mergedBaseStatus) {
1417
1780
  return {
1418
1781
  status: null,
1419
1782
  githubFeaturesEnabled: true,
1783
+ checks: [],
1784
+ headSha: null,
1420
1785
  };
1421
1786
  }
1422
1787
  return {
1423
- status: mergedStatus,
1788
+ status: toPullRequestStatus(mergedBaseStatus, []),
1424
1789
  githubFeaturesEnabled: true,
1790
+ checks: [],
1791
+ headSha: mergedBaseStatus.headSha,
1425
1792
  };
1426
1793
  }
1794
+ export async function createPullRequest(cwd, options) {
1795
+ await requireGitRepo(cwd);
1796
+ await ensureGhAvailable(cwd);
1797
+ const repo = await resolveGitHubRepo(cwd);
1798
+ if (!repo) {
1799
+ throw new Error("Unable to determine GitHub repo from git remote");
1800
+ }
1801
+ const head = options.head ?? (await getCurrentBranch(cwd));
1802
+ const configured = await getConfiguredBaseRefForCwd(cwd);
1803
+ const base = configured.baseRef ?? options.base ?? (await resolveBaseRef(cwd));
1804
+ if (!head) {
1805
+ throw new Error("Unable to determine head branch for PR");
1806
+ }
1807
+ if (!base) {
1808
+ throw new Error("Unable to determine base branch for PR");
1809
+ }
1810
+ const normalizedBase = normalizeLocalBranchRefName(base);
1811
+ if (configured.isJunctionOwnedWorktree && options.base && options.base !== base) {
1812
+ throw new Error(`Base ref mismatch: expected ${base}, got ${options.base}`);
1813
+ }
1814
+ await execAsync(`git push -u origin ${head}`, { cwd });
1815
+ const args = ["api", "-X", "POST", `repos/${repo}/pulls`, "-f", `title=${options.title}`];
1816
+ args.push("-f", `head=${head}`);
1817
+ args.push("-f", `base=${normalizedBase}`);
1818
+ if (options.body) {
1819
+ args.push("-f", `body=${options.body}`);
1820
+ }
1821
+ const { stdout } = await execFileAsync("gh", args, { cwd });
1822
+ const parsed = JSON.parse(stdout.trim());
1823
+ if (!parsed?.url || !parsed?.number) {
1824
+ throw new Error("GitHub CLI did not return PR url/number");
1825
+ }
1826
+ return { url: parsed.url, number: parsed.number };
1827
+ }
1828
+ export async function getPullRequestStatus(cwd) {
1829
+ const lookup = await loadCurrentPullRequest(cwd);
1830
+ return {
1831
+ status: lookup.status,
1832
+ githubFeaturesEnabled: lookup.githubFeaturesEnabled,
1833
+ };
1834
+ }
1835
+ export async function getPullRequestFailureLogs(cwd) {
1836
+ const lookup = await loadCurrentPullRequest(cwd);
1837
+ if (!lookup.githubFeaturesEnabled) {
1838
+ return {
1839
+ logs: null,
1840
+ githubFeaturesEnabled: false,
1841
+ };
1842
+ }
1843
+ const failedChecks = lookup.checks.filter((check) => check.bucket === "fail" || check.bucket === "cancel");
1844
+ if (!lookup.status || lookup.status.isMerged || failedChecks.length === 0) {
1845
+ return {
1846
+ logs: null,
1847
+ githubFeaturesEnabled: true,
1848
+ };
1849
+ }
1850
+ const runs = await listWorkflowRunsForHead({
1851
+ cwd,
1852
+ headBranch: lookup.status.headRefName,
1853
+ headSha: lookup.headSha,
1854
+ });
1855
+ const matchedRuns = findRunsForFailingChecks(lookup.checks, runs);
1856
+ const parts = [];
1857
+ let bytes = 0;
1858
+ bytes = appendBoundedText(parts, [
1859
+ `PR #${lookup.status.number}: ${lookup.status.title}`,
1860
+ `Branch: ${lookup.status.headRefName} -> ${lookup.status.baseRefName}`,
1861
+ "",
1862
+ "Failed required checks:",
1863
+ ...failedChecks.map((check) => {
1864
+ const workflowSuffix = check.workflow ? ` [${check.workflow}]` : "";
1865
+ const descriptionSuffix = check.description ? ` - ${check.description}` : "";
1866
+ return `- ${check.name}${workflowSuffix}${descriptionSuffix}`;
1867
+ }),
1868
+ "",
1869
+ ].join("\n"), bytes);
1870
+ if (matchedRuns.length === 0) {
1871
+ return {
1872
+ logs: parts.join(""),
1873
+ githubFeaturesEnabled: true,
1874
+ };
1875
+ }
1876
+ for (const run of matchedRuns) {
1877
+ let body = "No failed-step logs were returned by GitHub CLI.";
1878
+ try {
1879
+ body = await getFailedWorkflowRunLog(cwd, run.databaseId);
1880
+ }
1881
+ catch (error) {
1882
+ if (isGhAuthError(error)) {
1883
+ return {
1884
+ logs: null,
1885
+ githubFeaturesEnabled: false,
1886
+ };
1887
+ }
1888
+ body = error instanceof Error ? error.message : String(error);
1889
+ }
1890
+ const section = [
1891
+ `=== ${run.workflowName ?? run.name ?? `Run ${run.databaseId}`} ===`,
1892
+ run.url ? `URL: ${run.url}` : null,
1893
+ body,
1894
+ "",
1895
+ ]
1896
+ .filter((value) => Boolean(value))
1897
+ .join("\n");
1898
+ bytes = appendBoundedText(parts, section, bytes);
1899
+ if (bytes >= GH_FAILED_LOG_TOTAL_MAX_BYTES) {
1900
+ break;
1901
+ }
1902
+ }
1903
+ return {
1904
+ logs: parts.join(""),
1905
+ githubFeaturesEnabled: true,
1906
+ };
1907
+ }
1908
+ export async function mergePullRequest(cwd, options = {}) {
1909
+ const method = options.method ?? "squash";
1910
+ if (method !== "squash") {
1911
+ throw new Error("Only squash merge is supported");
1912
+ }
1913
+ const statusResult = await getPullRequestStatus(cwd);
1914
+ if (!statusResult.githubFeaturesEnabled) {
1915
+ throw new Error("GitHub CLI (gh) is not available or not authenticated");
1916
+ }
1917
+ if (!statusResult.status || statusResult.status.isMerged) {
1918
+ throw new Error("No open pull request found for current branch");
1919
+ }
1920
+ await execFileAsync("gh", ["pr", "merge", String(statusResult.status.number), "--squash"], {
1921
+ cwd,
1922
+ });
1923
+ }
1427
1924
  //# sourceMappingURL=checkout-git.js.map