bosun 0.29.8 → 0.31.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/cli.mjs +24 -13
- package/config.mjs +12 -3
- package/desktop/launch.mjs +2 -0
- package/monitor.mjs +3 -1
- package/package.json +2 -1
- package/pr-cleanup-daemon.mjs +26 -17
- package/setup-web-server.mjs +606 -0
- package/setup.mjs +164 -14
- package/ui/components/agent-selector.js +54 -89
- package/ui/components/chat-view.js +4 -6
- package/ui/setup.html +1029 -0
- package/ui/styles/components.css +335 -212
- package/ui/styles/sessions.css +79 -44
- package/ui/tabs/chat.js +12 -3
- package/ui/tabs/tasks.js +25 -7
- package/ui-server.mjs +18 -3
package/cli.mjs
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
7
|
* bosun # start with default config
|
|
8
|
-
* bosun --setup #
|
|
8
|
+
* bosun --setup # launch web setup wizard
|
|
9
|
+
* bosun --setup-terminal # terminal setup wizard
|
|
9
10
|
* bosun --args "-MaxParallel 6" # pass orchestrator args
|
|
10
11
|
* bosun --help # show help
|
|
11
12
|
*
|
|
@@ -65,7 +66,8 @@ function showHelp() {
|
|
|
65
66
|
bosun [options]
|
|
66
67
|
|
|
67
68
|
COMMANDS
|
|
68
|
-
--setup
|
|
69
|
+
--setup Launch the web-based setup wizard (default)
|
|
70
|
+
--setup-terminal Run the legacy terminal setup wizard
|
|
69
71
|
--where Show the resolved bosun config directory
|
|
70
72
|
--doctor Validate bosun .env/config setup
|
|
71
73
|
--help Show this help
|
|
@@ -194,7 +196,8 @@ function showHelp() {
|
|
|
194
196
|
|
|
195
197
|
EXAMPLES
|
|
196
198
|
bosun # start with defaults
|
|
197
|
-
bosun --setup #
|
|
199
|
+
bosun --setup # web setup wizard
|
|
200
|
+
bosun --setup-terminal # terminal setup wizard
|
|
198
201
|
bosun --script ./my-orchestrator.sh # custom script
|
|
199
202
|
bosun --args "-MaxParallel 4" --no-telegram-bot # custom args
|
|
200
203
|
bosun --no-codex --no-autofix # minimal mode
|
|
@@ -497,9 +500,10 @@ function startDaemon() {
|
|
|
497
500
|
env: {
|
|
498
501
|
...process.env,
|
|
499
502
|
BOSUN_DAEMON: "1",
|
|
500
|
-
// Propagate the bosun
|
|
503
|
+
// Propagate the bosun config directory so repo-root detection works
|
|
501
504
|
// even when the daemon child's cwd is not inside a git repo.
|
|
502
|
-
|
|
505
|
+
// Use the proper config dir (APPDATA/bosun or ~/bosun), NOT __dirname.
|
|
506
|
+
BOSUN_DIR: process.env.BOSUN_DIR || resolveConfigDirForCli(),
|
|
503
507
|
// Propagate REPO_ROOT if available; otherwise resolve from cwd before detaching
|
|
504
508
|
...(process.env.REPO_ROOT
|
|
505
509
|
? {}
|
|
@@ -983,17 +987,24 @@ async function main() {
|
|
|
983
987
|
process.exit(result.ok ? 0 : 1);
|
|
984
988
|
}
|
|
985
989
|
|
|
986
|
-
// Handle --setup
|
|
987
|
-
if (args.includes("--setup")
|
|
990
|
+
// Handle --setup-terminal (legacy terminal wizard)
|
|
991
|
+
if (args.includes("--setup-terminal")) {
|
|
988
992
|
const configDirArg = getArgValue("--config-dir");
|
|
989
|
-
if (configDirArg)
|
|
990
|
-
process.env.BOSUN_DIR = configDirArg;
|
|
991
|
-
}
|
|
993
|
+
if (configDirArg) process.env.BOSUN_DIR = configDirArg;
|
|
992
994
|
const { runSetup } = await import("./setup.mjs");
|
|
993
995
|
await runSetup();
|
|
994
996
|
process.exit(0);
|
|
995
997
|
}
|
|
996
998
|
|
|
999
|
+
// Handle --setup (web wizard — default)
|
|
1000
|
+
if (args.includes("--setup") || args.includes("setup")) {
|
|
1001
|
+
const configDirArg = getArgValue("--config-dir");
|
|
1002
|
+
if (configDirArg) process.env.BOSUN_DIR = configDirArg;
|
|
1003
|
+
const { startSetupServer } = await import("./setup-web-server.mjs");
|
|
1004
|
+
await startSetupServer();
|
|
1005
|
+
// Server keeps running until setup completes
|
|
1006
|
+
}
|
|
1007
|
+
|
|
997
1008
|
// Handle --whatsapp-auth
|
|
998
1009
|
if (args.includes("--whatsapp-auth") || args.includes("whatsapp-auth")) {
|
|
999
1010
|
const mode = args.includes("--pairing-code") ? "pairing-code" : "qr";
|
|
@@ -1007,13 +1018,13 @@ async function main() {
|
|
|
1007
1018
|
if (!IS_DAEMON_CHILD) {
|
|
1008
1019
|
const { shouldRunSetup } = await import("./setup.mjs");
|
|
1009
1020
|
if (shouldRunSetup()) {
|
|
1010
|
-
console.log("\n 🚀 First run detected — launching setup wizard...\n");
|
|
1011
1021
|
const configDirArg = getArgValue("--config-dir");
|
|
1012
1022
|
if (configDirArg) {
|
|
1013
1023
|
process.env.BOSUN_DIR = configDirArg;
|
|
1014
1024
|
}
|
|
1015
|
-
|
|
1016
|
-
await
|
|
1025
|
+
console.log("\n 🚀 First run detected — launching setup wizard...\n");
|
|
1026
|
+
const { startSetupServer } = await import("./setup-web-server.mjs");
|
|
1027
|
+
await startSetupServer();
|
|
1017
1028
|
console.log("\n Setup complete! Starting bosun...\n");
|
|
1018
1029
|
}
|
|
1019
1030
|
}
|
package/config.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
17
17
|
import { resolve, dirname, basename, relative, isAbsolute } from "node:path";
|
|
18
18
|
import { execSync } from "node:child_process";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
|
+
import os from "node:os";
|
|
20
21
|
import { resolveAgentSdkConfig } from "./agent-sdk.mjs";
|
|
21
22
|
import {
|
|
22
23
|
ensureAgentPromptWorkspace,
|
|
@@ -60,6 +61,10 @@ function isWslInteropRuntime() {
|
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
function resolveConfigDir(repoRoot) {
|
|
64
|
+
// 1. Explicit env override
|
|
65
|
+
if (process.env.BOSUN_DIR) return resolve(process.env.BOSUN_DIR);
|
|
66
|
+
|
|
67
|
+
// 2. Platform-aware user home
|
|
63
68
|
const preferWindowsDirs =
|
|
64
69
|
process.platform === "win32" && !isWslInteropRuntime();
|
|
65
70
|
const baseDir = preferWindowsDirs
|
|
@@ -67,13 +72,13 @@ function resolveConfigDir(repoRoot) {
|
|
|
67
72
|
process.env.LOCALAPPDATA ||
|
|
68
73
|
process.env.USERPROFILE ||
|
|
69
74
|
process.env.HOME ||
|
|
70
|
-
|
|
75
|
+
os.homedir()
|
|
71
76
|
: process.env.HOME ||
|
|
72
77
|
process.env.XDG_CONFIG_HOME ||
|
|
73
78
|
process.env.USERPROFILE ||
|
|
74
79
|
process.env.APPDATA ||
|
|
75
80
|
process.env.LOCALAPPDATA ||
|
|
76
|
-
|
|
81
|
+
os.homedir();
|
|
77
82
|
return resolve(baseDir, "bosun");
|
|
78
83
|
}
|
|
79
84
|
|
|
@@ -353,7 +358,11 @@ function detectRepoRoot() {
|
|
|
353
358
|
}
|
|
354
359
|
}
|
|
355
360
|
|
|
356
|
-
// 5. Final fallback to
|
|
361
|
+
// 5. Final fallback — warn and return cwd. This is unlikely to be a valid
|
|
362
|
+
// git repo (e.g. when the daemon spawns with cwd=homedir), but returning
|
|
363
|
+
// null would crash downstream callers like resolve(repoRoot). The warning
|
|
364
|
+
// helps diagnose "not a git repository" errors from child processes.
|
|
365
|
+
console.warn("[config] detectRepoRoot: no git repository found — falling back to cwd:", process.cwd());
|
|
357
366
|
return process.cwd();
|
|
358
367
|
}
|
|
359
368
|
|
package/desktop/launch.mjs
CHANGED
|
@@ -42,6 +42,7 @@ function ensureElectronInstalled() {
|
|
|
42
42
|
const result = spawnSync("npm", ["install"], {
|
|
43
43
|
cwd: desktopDir,
|
|
44
44
|
stdio: "inherit",
|
|
45
|
+
shell: process.platform === "win32",
|
|
45
46
|
env: process.env,
|
|
46
47
|
});
|
|
47
48
|
return result.status === 0 && existsSync(electronBin);
|
|
@@ -60,6 +61,7 @@ function launch() {
|
|
|
60
61
|
|
|
61
62
|
const child = spawn(electronBin, args, {
|
|
62
63
|
stdio: "inherit",
|
|
64
|
+
shell: process.platform === "win32",
|
|
63
65
|
env: {
|
|
64
66
|
...process.env,
|
|
65
67
|
BOSUN_DESKTOP: "1",
|
package/monitor.mjs
CHANGED
|
@@ -12703,12 +12703,14 @@ if (isContainerEnabled()) {
|
|
|
12703
12703
|
// ── Start PR Cleanup Daemon ──────────────────────────────────────────────────
|
|
12704
12704
|
// Automatically resolves PR conflicts and CI failures every 30 minutes
|
|
12705
12705
|
if (config.prCleanupEnabled !== false) {
|
|
12706
|
-
|
|
12706
|
+
const prRepoRoot = effectiveRepoRoot || repoRoot || process.cwd();
|
|
12707
|
+
console.log(`[monitor] Starting PR cleanup daemon (repoRoot: ${prRepoRoot})...`);
|
|
12707
12708
|
prCleanupDaemon = new PRCleanupDaemon({
|
|
12708
12709
|
intervalMs: 30 * 60 * 1000, // 30 minutes
|
|
12709
12710
|
maxConcurrentCleanups: 3,
|
|
12710
12711
|
dryRun: false,
|
|
12711
12712
|
autoMerge: true,
|
|
12713
|
+
repoRoot: prRepoRoot,
|
|
12712
12714
|
});
|
|
12713
12715
|
prCleanupDaemon.start();
|
|
12714
12716
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosun",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "AI-powered orchestrator supervisor — manages AI agent executors with failover, auto-restarts on failure, analyzes crashes with Codex SDK, creates PRs via Vibe-Kanban API, and sends Telegram notifications. Supports N executors with weighted distribution, multi-repo projects, and auto-setup.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache 2.0",
|
|
@@ -158,6 +158,7 @@
|
|
|
158
158
|
"sdk-conflict-resolver.mjs",
|
|
159
159
|
"session-tracker.mjs",
|
|
160
160
|
"setup.mjs",
|
|
161
|
+
"setup-web-server.mjs",
|
|
161
162
|
"pwsh-runtime.mjs",
|
|
162
163
|
"shared-knowledge.mjs",
|
|
163
164
|
"shared-state-manager.mjs",
|
package/pr-cleanup-daemon.mjs
CHANGED
|
@@ -23,9 +23,9 @@ const exec = promisify(execCallback);
|
|
|
23
23
|
* Check if a branch is already checked out in an existing git worktree.
|
|
24
24
|
* Returns the worktree path if claimed, or null if free.
|
|
25
25
|
*/
|
|
26
|
-
async function getWorktreeForBranch(branch) {
|
|
26
|
+
async function getWorktreeForBranch(branch, repoRoot) {
|
|
27
27
|
try {
|
|
28
|
-
const { stdout } = await exec(`git worktree list --porcelain
|
|
28
|
+
const { stdout } = await exec(`git worktree list --porcelain`, { cwd: repoRoot });
|
|
29
29
|
// Each worktree block is separated by a blank line.
|
|
30
30
|
// Look for a line like: branch refs/heads/<branch>
|
|
31
31
|
const blocks = stdout.split(/\n\n/);
|
|
@@ -64,6 +64,7 @@ class PRCleanupDaemon {
|
|
|
64
64
|
...CONFIG,
|
|
65
65
|
...(config && typeof config === "object" ? config : {}),
|
|
66
66
|
};
|
|
67
|
+
this.repoRoot = this.config.repoRoot || process.cwd();
|
|
67
68
|
this.cleanupQueue = [];
|
|
68
69
|
this.activeCleanups = new Map(); // pr# → cleanup state
|
|
69
70
|
this.lastRunStartedAt = 0;
|
|
@@ -157,6 +158,7 @@ class PRCleanupDaemon {
|
|
|
157
158
|
try {
|
|
158
159
|
const { stdout } = await exec(
|
|
159
160
|
`gh pr list --json number,title,mergeable,labels,statusCheckRollup,headRefName --limit 50`,
|
|
161
|
+
{ cwd: this.repoRoot },
|
|
160
162
|
);
|
|
161
163
|
const allPRs = JSON.parse(stdout);
|
|
162
164
|
|
|
@@ -377,10 +379,10 @@ class PRCleanupDaemon {
|
|
|
377
379
|
tmpDir = await mkdtemp(join(tmpdir(), "pr-merge-"));
|
|
378
380
|
|
|
379
381
|
// Fetch all relevant refs
|
|
380
|
-
await exec(`git fetch origin ${pr.headRefName} main
|
|
382
|
+
await exec(`git fetch origin ${pr.headRefName} main`, { cwd: this.repoRoot });
|
|
381
383
|
|
|
382
384
|
// Guard: skip if the branch is already claimed by another worktree
|
|
383
|
-
const existingWt = await getWorktreeForBranch(pr.headRefName);
|
|
385
|
+
const existingWt = await getWorktreeForBranch(pr.headRefName, this.repoRoot);
|
|
384
386
|
if (existingWt) {
|
|
385
387
|
console.warn(
|
|
386
388
|
`[pr-cleanup-daemon] WARN: Branch "${pr.headRefName}" is in an active worktree at ${existingWt} — skipping conflict resolution`,
|
|
@@ -391,6 +393,7 @@ class PRCleanupDaemon {
|
|
|
391
393
|
// Create worktree on the PR branch
|
|
392
394
|
await exec(
|
|
393
395
|
`git worktree add "${tmpDir}" "origin/${pr.headRefName}" --detach`,
|
|
396
|
+
{ cwd: this.repoRoot },
|
|
394
397
|
);
|
|
395
398
|
await exec(
|
|
396
399
|
`git checkout -B "${pr.headRefName}" "origin/${pr.headRefName}"`,
|
|
@@ -455,13 +458,13 @@ class PRCleanupDaemon {
|
|
|
455
458
|
} finally {
|
|
456
459
|
if (tmpDir) {
|
|
457
460
|
try {
|
|
458
|
-
await exec(`git worktree remove "${tmpDir}" --force
|
|
461
|
+
await exec(`git worktree remove "${tmpDir}" --force`, { cwd: this.repoRoot });
|
|
459
462
|
} catch {
|
|
460
463
|
try {
|
|
461
464
|
await rm(tmpDir, { recursive: true, force: true });
|
|
462
465
|
} catch {}
|
|
463
466
|
try {
|
|
464
|
-
await exec(`git worktree prune
|
|
467
|
+
await exec(`git worktree prune`, { cwd: this.repoRoot });
|
|
465
468
|
} catch {}
|
|
466
469
|
}
|
|
467
470
|
}
|
|
@@ -499,10 +502,10 @@ class PRCleanupDaemon {
|
|
|
499
502
|
tmpDir = await mkdtemp(join(tmpdir(), "pr-cleanup-"));
|
|
500
503
|
|
|
501
504
|
// Fetch latest refs first
|
|
502
|
-
await exec(`git fetch origin ${pr.headRefName}
|
|
505
|
+
await exec(`git fetch origin ${pr.headRefName}`, { cwd: this.repoRoot });
|
|
503
506
|
|
|
504
507
|
// Guard: skip if the branch is already claimed by another worktree
|
|
505
|
-
const existingWt = await getWorktreeForBranch(pr.headRefName);
|
|
508
|
+
const existingWt = await getWorktreeForBranch(pr.headRefName, this.repoRoot);
|
|
506
509
|
if (existingWt) {
|
|
507
510
|
console.warn(
|
|
508
511
|
`[pr-cleanup-daemon] WARN: Branch "${pr.headRefName}" is in an active worktree at ${existingWt} — skipping CI re-trigger`,
|
|
@@ -513,6 +516,7 @@ class PRCleanupDaemon {
|
|
|
513
516
|
// Create a temporary worktree for the PR branch
|
|
514
517
|
await exec(
|
|
515
518
|
`git worktree add "${tmpDir}" "origin/${pr.headRefName}" --detach`,
|
|
519
|
+
{ cwd: this.repoRoot },
|
|
516
520
|
);
|
|
517
521
|
|
|
518
522
|
// Checkout the branch properly inside the worktree
|
|
@@ -538,14 +542,14 @@ class PRCleanupDaemon {
|
|
|
538
542
|
// Clean up the temporary worktree
|
|
539
543
|
if (tmpDir) {
|
|
540
544
|
try {
|
|
541
|
-
await exec(`git worktree remove "${tmpDir}" --force
|
|
545
|
+
await exec(`git worktree remove "${tmpDir}" --force`, { cwd: this.repoRoot });
|
|
542
546
|
} catch {
|
|
543
547
|
// If worktree remove fails, try manual cleanup
|
|
544
548
|
try {
|
|
545
549
|
await rm(tmpDir, { recursive: true, force: true });
|
|
546
550
|
} catch {}
|
|
547
551
|
try {
|
|
548
|
-
await exec(`git worktree prune
|
|
552
|
+
await exec(`git worktree prune`, { cwd: this.repoRoot });
|
|
549
553
|
} catch {}
|
|
550
554
|
}
|
|
551
555
|
}
|
|
@@ -605,7 +609,7 @@ class PRCleanupDaemon {
|
|
|
605
609
|
}
|
|
606
610
|
|
|
607
611
|
try {
|
|
608
|
-
await exec(`gh pr merge ${pr.number} --auto --squash --delete-branch
|
|
612
|
+
await exec(`gh pr merge ${pr.number} --auto --squash --delete-branch`, { cwd: this.repoRoot });
|
|
609
613
|
this.stats.autoMerges++;
|
|
610
614
|
console.log(`[pr-cleanup-daemon] ✓ Auto-merged PR #${pr.number}`);
|
|
611
615
|
} catch (err) {
|
|
@@ -626,7 +630,7 @@ class PRCleanupDaemon {
|
|
|
626
630
|
try {
|
|
627
631
|
// Use GitHub API to get the list of changed files and estimate conflict scope
|
|
628
632
|
// This avoids the need for local checkout entirely
|
|
629
|
-
const { stdout } = await exec(`gh pr diff ${pr.number} --name-only
|
|
633
|
+
const { stdout } = await exec(`gh pr diff ${pr.number} --name-only`, { cwd: this.repoRoot });
|
|
630
634
|
const changedFiles = stdout.trim().split("\n").filter(Boolean);
|
|
631
635
|
|
|
632
636
|
// Estimate: each changed file could have ~10 lines of conflicts on average
|
|
@@ -641,8 +645,8 @@ class PRCleanupDaemon {
|
|
|
641
645
|
let tmpDir;
|
|
642
646
|
try {
|
|
643
647
|
tmpDir = await mkdtemp(join(tmpdir(), "pr-conflict-"));
|
|
644
|
-
await exec(`git fetch origin ${pr.headRefName} main
|
|
645
|
-
await exec(`git worktree add "${tmpDir}" "origin/main" --detach
|
|
648
|
+
await exec(`git fetch origin ${pr.headRefName} main`, { cwd: this.repoRoot });
|
|
649
|
+
await exec(`git worktree add "${tmpDir}" "origin/main" --detach`, { cwd: this.repoRoot });
|
|
646
650
|
|
|
647
651
|
// Attempt merge to count conflicts
|
|
648
652
|
try {
|
|
@@ -675,13 +679,13 @@ class PRCleanupDaemon {
|
|
|
675
679
|
} finally {
|
|
676
680
|
if (tmpDir) {
|
|
677
681
|
try {
|
|
678
|
-
await exec(`git worktree remove "${tmpDir}" --force
|
|
682
|
+
await exec(`git worktree remove "${tmpDir}" --force`, { cwd: this.repoRoot });
|
|
679
683
|
} catch {
|
|
680
684
|
try {
|
|
681
685
|
await rm(tmpDir, { recursive: true, force: true });
|
|
682
686
|
} catch {}
|
|
683
687
|
try {
|
|
684
|
-
await exec(`git worktree prune
|
|
688
|
+
await exec(`git worktree prune`, { cwd: this.repoRoot });
|
|
685
689
|
} catch {}
|
|
686
690
|
}
|
|
687
691
|
}
|
|
@@ -704,6 +708,7 @@ class PRCleanupDaemon {
|
|
|
704
708
|
const child = spawn("node", args, {
|
|
705
709
|
stdio: "inherit",
|
|
706
710
|
env: process.env,
|
|
711
|
+
cwd: this.repoRoot,
|
|
707
712
|
});
|
|
708
713
|
|
|
709
714
|
child.on("exit", (code) => {
|
|
@@ -726,6 +731,7 @@ class PRCleanupDaemon {
|
|
|
726
731
|
try {
|
|
727
732
|
const { stdout } = await exec(
|
|
728
733
|
`gh pr list --json number,title,mergeable,statusCheckRollup,headRefName,autoMergeRequest --limit 30`,
|
|
734
|
+
{ cwd: this.repoRoot },
|
|
729
735
|
);
|
|
730
736
|
const allPRs = JSON.parse(stdout);
|
|
731
737
|
|
|
@@ -767,6 +773,7 @@ class PRCleanupDaemon {
|
|
|
767
773
|
try {
|
|
768
774
|
await exec(
|
|
769
775
|
`gh pr merge ${pr.number} --auto --squash --delete-branch`,
|
|
776
|
+
{ cwd: this.repoRoot },
|
|
770
777
|
);
|
|
771
778
|
console.log(
|
|
772
779
|
`[pr-cleanup-daemon] ⏳ Auto-merge queued for PR #${pr.number} (CI pending)`,
|
|
@@ -787,7 +794,7 @@ class PRCleanupDaemon {
|
|
|
787
794
|
|
|
788
795
|
// All green + mergeable → merge now
|
|
789
796
|
try {
|
|
790
|
-
await exec(`gh pr merge ${pr.number} --squash --delete-branch
|
|
797
|
+
await exec(`gh pr merge ${pr.number} --squash --delete-branch`, { cwd: this.repoRoot });
|
|
791
798
|
this.stats.autoMerges++;
|
|
792
799
|
console.log(
|
|
793
800
|
`[pr-cleanup-daemon] ✅ Auto-merged green PR #${pr.number}: ${pr.title}`,
|
|
@@ -797,6 +804,7 @@ class PRCleanupDaemon {
|
|
|
797
804
|
try {
|
|
798
805
|
await exec(
|
|
799
806
|
`gh pr merge ${pr.number} --auto --squash --delete-branch`,
|
|
807
|
+
{ cwd: this.repoRoot },
|
|
800
808
|
);
|
|
801
809
|
console.log(
|
|
802
810
|
`[pr-cleanup-daemon] ⏳ Auto-merge enabled for PR #${pr.number}`,
|
|
@@ -854,6 +862,7 @@ class PRCleanupDaemon {
|
|
|
854
862
|
try {
|
|
855
863
|
const { stdout } = await exec(
|
|
856
864
|
`gh pr view ${prNumber} --json mergeable,statusCheckRollup`,
|
|
865
|
+
{ cwd: this.repoRoot },
|
|
857
866
|
);
|
|
858
867
|
return JSON.parse(stdout);
|
|
859
868
|
} catch (err) {
|