mergestorm 0.3.18 → 0.3.20
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/README.md +4 -3
- package/dist/api.d.ts +2 -2
- package/dist/api.js +42 -2
- package/dist/automation-catalog.d.ts +44 -0
- package/dist/automation-catalog.js +10 -0
- package/dist/client/index.d.ts +4 -1
- package/dist/client/index.js +4 -1
- package/dist/commands/browse.d.ts +1 -1
- package/dist/commands/browse.js +3 -3
- package/dist/commands/help.js +1 -1
- package/dist/commands/queue.d.ts +1 -1
- package/dist/commands/queue.js +21 -6
- package/dist/commands/registry.js +13 -6
- package/dist/commands/review-client.js +2 -10
- package/dist/commands/settings.d.ts +12 -4
- package/dist/commands/settings.js +78 -16
- package/dist/commands/skill.d.ts +4 -3
- package/dist/commands/skill.js +16 -6
- package/dist/commands/stack.d.ts +18 -4
- package/dist/commands/stack.js +145 -40
- package/dist/errors.d.ts +1 -1
- package/dist/gh.d.ts +69 -7
- package/dist/gh.js +236 -35
- package/dist/stack-blockers.d.ts +25 -0
- package/dist/stack-blockers.js +120 -0
- package/dist/stack-dto.d.ts +62 -1
- package/dist/stack-dto.js +55 -1
- package/dist/stack-watch.d.ts +51 -0
- package/dist/stack-watch.js +163 -0
- package/dist/ui/banner.js +6 -0
- package/package.json +1 -1
- package/skill/SKILL.md +6 -4
- package/skill/mergestorm-bounce-watch/SKILL.md +51 -0
- package/skill/mergestorm-pr-loop/SKILL.md +9 -4
package/dist/commands/skill.js
CHANGED
|
@@ -7,14 +7,16 @@ import { gitTopLevel } from "../repo-identity.js";
|
|
|
7
7
|
import { present } from "../ui/present.js";
|
|
8
8
|
export const SKILL_NAME = "mergestorm-review";
|
|
9
9
|
export const PR_LOOP_SKILL_NAME = "mergestorm-pr-loop";
|
|
10
|
-
export const
|
|
10
|
+
export const BOUNCE_WATCH_SKILL_NAME = "mergestorm-bounce-watch";
|
|
11
|
+
export const SKILL_NAMES = [SKILL_NAME, PR_LOOP_SKILL_NAME, BOUNCE_WATCH_SKILL_NAME];
|
|
11
12
|
export const SKILL_USAGE = `usage:
|
|
12
|
-
mergestorm skill install --claude|--cursor [--json]
|
|
13
|
+
mergestorm skill install --claude|--cursor|--agents [--json]
|
|
13
14
|
|
|
14
|
-
Copies
|
|
15
|
+
Copies ${SKILL_NAMES.join(", ")} into this repo (git toplevel, else cwd):
|
|
15
16
|
--claude .claude/skills/<name>/SKILL.md
|
|
16
17
|
--cursor .cursor/skills/<name>/SKILL.md
|
|
17
|
-
|
|
18
|
+
--agents .agents/skills/<name>/SKILL.md
|
|
19
|
+
Pass multiple flags to write multiple trees. Overwrites existing copies.`;
|
|
18
20
|
export function parseSkillArgs(args) {
|
|
19
21
|
const [sub, ...rest] = args;
|
|
20
22
|
if (!sub || sub === "-h" || sub === "--help" || sub === "help") {
|
|
@@ -40,13 +42,18 @@ export function parseSkillArgs(args) {
|
|
|
40
42
|
targets.push("cursor");
|
|
41
43
|
continue;
|
|
42
44
|
}
|
|
45
|
+
if (arg === "--agents") {
|
|
46
|
+
if (!targets.includes("agents"))
|
|
47
|
+
targets.push("agents");
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
43
50
|
if (arg === "-h" || arg === "--help" || arg === "help") {
|
|
44
51
|
return { help: true, json: false, targets: [] };
|
|
45
52
|
}
|
|
46
53
|
throw new CommandError(`${SKILL_USAGE}\nunknown flag: ${arg}`, 1, "usage");
|
|
47
54
|
}
|
|
48
55
|
if (targets.length === 0) {
|
|
49
|
-
throw new CommandError(`${SKILL_USAGE}\npass --claude and/or --
|
|
56
|
+
throw new CommandError(`${SKILL_USAGE}\npass --claude, --cursor, and/or --agents`, 1, "usage");
|
|
50
57
|
}
|
|
51
58
|
return { help: false, json, targets };
|
|
52
59
|
}
|
|
@@ -65,6 +72,9 @@ export function skillDestination(root, target, name = SKILL_NAME) {
|
|
|
65
72
|
if (target === "claude") {
|
|
66
73
|
return path.join(root, ".claude", "skills", name, "SKILL.md");
|
|
67
74
|
}
|
|
75
|
+
if (target === "agents") {
|
|
76
|
+
return path.join(root, ".agents", "skills", name, "SKILL.md");
|
|
77
|
+
}
|
|
68
78
|
return path.join(root, ".cursor", "skills", name, "SKILL.md");
|
|
69
79
|
}
|
|
70
80
|
export function resolveBundledSkillPath(fromFile = import.meta.url, name = SKILL_NAME) {
|
|
@@ -90,7 +100,7 @@ export function resolveBundledSkillPath(fromFile = import.meta.url, name = SKILL
|
|
|
90
100
|
}
|
|
91
101
|
async function assertNoSymlink(dest) {
|
|
92
102
|
const parts = dest.split(path.sep);
|
|
93
|
-
const marker = parts.findIndex((p) => p === ".claude" || p === ".cursor");
|
|
103
|
+
const marker = parts.findIndex((p) => p === ".claude" || p === ".cursor" || p === ".agents");
|
|
94
104
|
const first = marker === -1 ? parts.length - 1 : marker;
|
|
95
105
|
for (let i = first; i < parts.length; i++) {
|
|
96
106
|
const candidate = parts.slice(0, i + 1).join(path.sep);
|
package/dist/commands/stack.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { adoptStack, ensureUpperPark, listStacks, setStackPolicy, type StackDto, type StackPolicyPatch } from "../api.js";
|
|
1
|
+
import { adoptStack, ensureUpperPark, getEnrichedStack, listStacks, setStackPolicy, type StackDto, type StackPolicyPatch } from "../api.js";
|
|
2
2
|
import { loadConfig } from "../config.js";
|
|
3
|
-
import {
|
|
3
|
+
import { createPrs, findOpenPrNumber } from "../gh.js";
|
|
4
4
|
import { parseGithubOriginRepo } from "../git.js";
|
|
5
5
|
import { branchExists, commitsAheadOf, createBranchFromHead, currentBranch, defaultLayerBranchName, deleteBranch, discoverTrunk, fetchRemoteBranch, pushBranch, tipCommitMessage, tipCommitSubject, worktreeDirty } from "../git-stack.js";
|
|
6
6
|
import { buildPrBodyFromCommit } from "../pr-body.js";
|
|
7
7
|
import { gitTopLevel } from "../repo-identity.js";
|
|
8
8
|
import { loadStackMeta, saveStackMeta } from "../stack-meta.js";
|
|
9
|
+
import { pollStackWatch } from "../stack-watch.js";
|
|
9
10
|
/** A registered stack layer that would become the parent of a new layer. */
|
|
10
11
|
export type RegisteredParentHit = {
|
|
11
12
|
stackId: string;
|
|
@@ -58,6 +59,7 @@ export declare function parseStackSubmitArgs(args: string[]): {
|
|
|
58
59
|
autoLand?: boolean;
|
|
59
60
|
autoReview?: boolean;
|
|
60
61
|
autoPatch?: boolean;
|
|
62
|
+
rest: boolean;
|
|
61
63
|
asJson: boolean;
|
|
62
64
|
};
|
|
63
65
|
export declare function parseStackAdoptArgs(args: string[]): {
|
|
@@ -113,13 +115,23 @@ export type StackSubmitDeps = {
|
|
|
113
115
|
tipCommitSubject?: typeof tipCommitSubject;
|
|
114
116
|
tipCommitMessage?: typeof tipCommitMessage;
|
|
115
117
|
buildPrBodyFromCommit?: typeof buildPrBodyFromCommit;
|
|
116
|
-
|
|
118
|
+
createPrs?: typeof createPrs;
|
|
117
119
|
loadConfig?: typeof loadConfig;
|
|
118
120
|
listStacks?: typeof listStacks;
|
|
119
121
|
adoptStack?: typeof adoptStack;
|
|
120
122
|
ensureUpperPark?: typeof ensureUpperPark;
|
|
123
|
+
env?: NodeJS.ProcessEnv;
|
|
121
124
|
};
|
|
122
125
|
export declare function cmdStackSubmit(args: string[], deps?: StackSubmitDeps): Promise<void>;
|
|
126
|
+
export declare function cmdStackWait(args: string[], deps?: {
|
|
127
|
+
loadConfig?: typeof loadConfig;
|
|
128
|
+
pollStackWatch?: typeof pollStackWatch;
|
|
129
|
+
signal?: AbortSignal;
|
|
130
|
+
}): Promise<void>;
|
|
131
|
+
export declare function cmdStackStatus(args: string[], deps?: {
|
|
132
|
+
loadConfig?: typeof loadConfig;
|
|
133
|
+
getEnrichedStack?: typeof getEnrichedStack;
|
|
134
|
+
}): Promise<void>;
|
|
123
135
|
export type StackSetDeps = {
|
|
124
136
|
loadConfig?: typeof loadConfig;
|
|
125
137
|
setStackPolicy?: typeof setStackPolicy;
|
|
@@ -127,4 +139,6 @@ export type StackSetDeps = {
|
|
|
127
139
|
/** Human summary lines for a `stack set` write, one per flag. */
|
|
128
140
|
export declare function stackSetSummary(parsed: StackSetArgs): string[];
|
|
129
141
|
export declare function cmdStackSet(args: string[], deps?: StackSetDeps): Promise<void>;
|
|
130
|
-
export declare function cmdStack(args: string[]
|
|
142
|
+
export declare function cmdStack(args: string[], options?: {
|
|
143
|
+
signal?: AbortSignal;
|
|
144
|
+
}): Promise<void>;
|
package/dist/commands/stack.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { stackBlockersSummary } from "../stack-blockers.js";
|
|
2
|
+
import { adoptStack, ensureUpperPark, getEnrichedStack, landNextStack, listStacks, restackStack, setStackPolicy, } from "../api.js";
|
|
2
3
|
import { loadConfig } from "../config.js";
|
|
3
|
-
import { CommandError } from "../errors.js";
|
|
4
|
-
import {
|
|
4
|
+
import { rateLimitedMessage, REVIEW_EXIT, CommandError } from "../errors.js";
|
|
5
|
+
import { createPrs, findOpenPrNumber, githubApiFromEnv, requireGh, } from "../gh.js";
|
|
5
6
|
import { parseGithubOriginRepo } from "../git.js";
|
|
6
7
|
import { branchExists, commitsAheadOf, createBranchFromHead, currentBranch, defaultLayerBranchName, deleteBranch, discoverTrunk, fetchRemoteBranch, pushBranch, tipCommitMessage, tipCommitSubject, worktreeDirty, } from "../git-stack.js";
|
|
7
8
|
import { buildPrBodyFromCommit } from "../pr-body.js";
|
|
8
9
|
import { gitTopLevel } from "../repo-identity.js";
|
|
9
10
|
import { activeLayers, appendLayer, emptyStackMeta, loadStackMeta, removeActiveStack, resetStackMeta, saveStackMeta, selectStackForCreate, setActivePolicy, } from "../stack-meta.js";
|
|
11
|
+
import { pollStackWatch, StackWatchError, StackWatchTimeoutError, } from "../stack-watch.js";
|
|
10
12
|
import { isMgParkBranch, planSubmitLayerBases } from "../submit-pr-base.js";
|
|
11
13
|
import { ansi } from "../ui/ansi.js";
|
|
12
14
|
import { runLineTabsBrowser } from "../ui/line-tabs.js";
|
|
@@ -15,9 +17,11 @@ import { canBrowse } from "./browse.js";
|
|
|
15
17
|
import { openHelpBrowser } from "./help.js";
|
|
16
18
|
const STACK_USAGE = `usage:
|
|
17
19
|
mergestorm stack create [name] [--onto <branch>] [--trunk <branch>] [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--json]
|
|
18
|
-
mergestorm stack submit [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--json]
|
|
20
|
+
mergestorm stack submit [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--rest] [--json]
|
|
19
21
|
mergestorm stack reset --force
|
|
20
22
|
mergestorm stack list [--json]
|
|
23
|
+
mergestorm stack status <stack-id> [--json]
|
|
24
|
+
mergestorm stack wait <stack-id> [--json] [--timeout <s>]
|
|
21
25
|
mergestorm stack set <stack-id> [--auto-land on|off] [--auto-review on|off|default] [--auto-patch on|off|default] [--json]
|
|
22
26
|
mergestorm stack adopt <owner/repo>#<pr> [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--json]
|
|
23
27
|
mergestorm stack restack <stack-id> [--json]
|
|
@@ -29,6 +33,8 @@ const STACK_USAGE = `usage:
|
|
|
29
33
|
stack submit opens PRs with a body generated from each layer tip commit.
|
|
30
34
|
Layers 1–2 use the git parent as GitHub base. Layer 3+ opens onto the
|
|
31
35
|
review-unit park freeze (mg-park-*) so adopt does not retarget after open.
|
|
36
|
+
stack submit talks to GitHub GraphQL and falls back to the REST API when
|
|
37
|
+
GraphQL is blocked; --rest (or MERGESTORM_GITHUB_API=rest) forces REST.
|
|
32
38
|
Parenting onto a branch that already belongs to a registered (submitted)
|
|
33
39
|
stack requires --extend on create and submit — otherwise mg starts a new
|
|
34
40
|
stack from trunk. stack land promotes into the review unit when one exists;
|
|
@@ -143,7 +149,7 @@ export function stackPolicyLabels(s) {
|
|
|
143
149
|
}
|
|
144
150
|
return labels;
|
|
145
151
|
}
|
|
146
|
-
function formatStackHuman(s) {
|
|
152
|
+
function formatStackHuman(s, includeLayers = true) {
|
|
147
153
|
const lines = [];
|
|
148
154
|
lines.push(` ${ansi.bold(`${s.owner}/${s.repo}`)} trunk=${s.trunkBranch}` +
|
|
149
155
|
stackPolicyLabels(s)
|
|
@@ -154,6 +160,8 @@ function formatStackHuman(s) {
|
|
|
154
160
|
lines.push(ansi.dim(" (no layers)"));
|
|
155
161
|
return lines;
|
|
156
162
|
}
|
|
163
|
+
if (!includeLayers)
|
|
164
|
+
return lines;
|
|
157
165
|
for (const layer of s.layers) {
|
|
158
166
|
const title = layer.title ? ` ${layer.title}` : "";
|
|
159
167
|
const pr = layer.prNumber > 0 ? `#${layer.prNumber}` : "—";
|
|
@@ -262,6 +270,7 @@ export function parseStackCreateArgs(args) {
|
|
|
262
270
|
/** Parse `stack submit` argv. Exported for tests. */
|
|
263
271
|
export function parseStackSubmitArgs(args) {
|
|
264
272
|
let extend = false;
|
|
273
|
+
let rest = false;
|
|
265
274
|
const policy = {};
|
|
266
275
|
let asJson = false;
|
|
267
276
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -274,18 +283,23 @@ export function parseStackSubmitArgs(args) {
|
|
|
274
283
|
extend = true;
|
|
275
284
|
continue;
|
|
276
285
|
}
|
|
286
|
+
if (a === "--rest") {
|
|
287
|
+
rest = true;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
277
290
|
const next = takeOpenPolicyFlag(args, i, policy, "stack submit");
|
|
278
291
|
if (next !== null) {
|
|
279
292
|
i = next - 1;
|
|
280
293
|
continue;
|
|
281
294
|
}
|
|
282
|
-
throw new CommandError("usage: mergestorm stack submit [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--json]");
|
|
295
|
+
throw new CommandError("usage: mergestorm stack submit [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--rest] [--json]");
|
|
283
296
|
}
|
|
284
297
|
return {
|
|
285
298
|
extend,
|
|
286
299
|
autoLand: policy.autoLand,
|
|
287
300
|
autoReview: policy.autoReview,
|
|
288
301
|
autoPatch: policy.autoPatch,
|
|
302
|
+
rest,
|
|
289
303
|
asJson,
|
|
290
304
|
};
|
|
291
305
|
}
|
|
@@ -518,7 +532,8 @@ export async function cmdStackCreate(args, deps = {}) {
|
|
|
518
532
|
await present("Stack", created);
|
|
519
533
|
}
|
|
520
534
|
export async function cmdStackSubmit(args, deps = {}) {
|
|
521
|
-
const { extend, autoLand, autoReview, autoPatch, asJson } = parseStackSubmitArgs(args);
|
|
535
|
+
const { extend, autoLand, autoReview, autoPatch, rest, asJson } = parseStackSubmitArgs(args);
|
|
536
|
+
const api = rest ? "rest" : githubApiFromEnv(deps.env ?? process.env);
|
|
522
537
|
const progress = (message) => {
|
|
523
538
|
if (asJson)
|
|
524
539
|
console.error(message);
|
|
@@ -539,7 +554,7 @@ export async function cmdStackSubmit(args, deps = {}) {
|
|
|
539
554
|
const tipCommitSubjectFn = deps.tipCommitSubject ?? tipCommitSubject;
|
|
540
555
|
const tipCommitMessageFn = deps.tipCommitMessage ?? tipCommitMessage;
|
|
541
556
|
const buildPrBodyFn = deps.buildPrBodyFromCommit ?? buildPrBodyFromCommit;
|
|
542
|
-
const
|
|
557
|
+
const createPrsFn = deps.createPrs ?? createPrs;
|
|
543
558
|
const loadConfigFn = deps.loadConfig ?? loadConfig;
|
|
544
559
|
const listStacksFn = deps.listStacks ?? listStacks;
|
|
545
560
|
const adoptStackFn = deps.adoptStack ?? adoptStack;
|
|
@@ -624,16 +639,11 @@ export async function cmdStackSubmit(args, deps = {}) {
|
|
|
624
639
|
}
|
|
625
640
|
const opened = [];
|
|
626
641
|
const pendingPark = plans.filter((plan) => {
|
|
627
|
-
if (findOpenPrNumberFn(owner, repo, plan.branch, cwd) != null)
|
|
642
|
+
if (findOpenPrNumberFn(owner, repo, plan.branch, cwd, { api }) != null)
|
|
628
643
|
return false;
|
|
629
644
|
return plan.githubBase == null;
|
|
630
645
|
});
|
|
631
|
-
const
|
|
632
|
-
const existing = findOpenPrNumberFn(owner, repo, branch, cwd);
|
|
633
|
-
if (existing != null) {
|
|
634
|
-
progress(ansi.dim(` PR #${existing} already open for ${branch}`));
|
|
635
|
-
return { prNumber: existing, created: false };
|
|
636
|
-
}
|
|
646
|
+
const prepareLayerPr = (branch, base) => {
|
|
637
647
|
const aheadRef = isMgParkBranch(base) ? `origin/${base}` : base;
|
|
638
648
|
if (isMgParkBranch(base)) {
|
|
639
649
|
try {
|
|
@@ -657,30 +667,44 @@ export async function cmdStackSubmit(args, deps = {}) {
|
|
|
657
667
|
throw new CommandError(`No commits on \`${branch}\` ahead of \`${base}\`. ` +
|
|
658
668
|
`Commit your changes, then retry \`mg stack submit\`.`);
|
|
659
669
|
}
|
|
660
|
-
const title = tipCommitSubjectFn(branch, cwd);
|
|
661
|
-
const body = buildPrBodyFn(tipCommitMessageFn(branch, cwd));
|
|
662
|
-
progress(ansi.dim(` Opening PR ${branch} → ${base} …`));
|
|
663
670
|
return {
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
head: branch,
|
|
669
|
-
title,
|
|
670
|
-
body,
|
|
671
|
-
cwd,
|
|
672
|
-
}),
|
|
673
|
-
created: true,
|
|
671
|
+
base,
|
|
672
|
+
head: branch,
|
|
673
|
+
title: tipCommitSubjectFn(branch, cwd),
|
|
674
|
+
body: buildPrBodyFn(tipCommitMessageFn(branch, cwd)),
|
|
674
675
|
};
|
|
675
676
|
};
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
677
|
+
const openLayerWave = (wave) => {
|
|
678
|
+
const rows = [];
|
|
679
|
+
const pending = [];
|
|
680
|
+
for (const { branch, base } of wave) {
|
|
681
|
+
const existing = findOpenPrNumberFn(owner, repo, branch, cwd, { api });
|
|
682
|
+
if (existing != null) {
|
|
683
|
+
progress(ansi.dim(` PR #${existing} already open for ${branch}`));
|
|
684
|
+
rows.push({ branch, base, prNumber: existing, created: false });
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
pending.push({ index: rows.length, pr: prepareLayerPr(branch, base) });
|
|
688
|
+
rows.push({ branch, base, prNumber: 0, created: true });
|
|
679
689
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
690
|
+
if (pending.length === 0)
|
|
691
|
+
return rows;
|
|
692
|
+
for (const { pr } of pending) {
|
|
693
|
+
progress(ansi.dim(` Opening PR ${pr.head} → ${pr.base} …`));
|
|
694
|
+
}
|
|
695
|
+
const numbers = createPrsFn({ owner, repo, prs: pending.map((p) => p.pr), cwd, api });
|
|
696
|
+
if (numbers.length !== pending.length) {
|
|
697
|
+
throw new CommandError(`GitHub returned ${numbers.length} PR numbers for ${pending.length} new pull requests.`);
|
|
698
|
+
}
|
|
699
|
+
pending.forEach(({ index }, i) => {
|
|
700
|
+
rows[index].prNumber = numbers[i];
|
|
701
|
+
});
|
|
702
|
+
return rows;
|
|
703
|
+
};
|
|
704
|
+
opened.push(...openLayerWave(plans
|
|
705
|
+
.filter((plan) => plan.githubBase != null ||
|
|
706
|
+
findOpenPrNumberFn(owner, repo, plan.branch, cwd, { api }) != null)
|
|
707
|
+
.map((plan) => ({ branch: plan.branch, base: plan.githubBase ?? plan.gitParent }))));
|
|
684
708
|
const cfg = await loadConfigFn();
|
|
685
709
|
let adoptData = null;
|
|
686
710
|
let stackId = plans.find((plan) => plan.stackId)?.stackId ?? null;
|
|
@@ -711,10 +735,7 @@ export async function cmdStackSubmit(args, deps = {}) {
|
|
|
711
735
|
}
|
|
712
736
|
progress(ansi.dim(` Ensuring upper-park freeze for stack ${stackId} …`));
|
|
713
737
|
const park = await ensureUpperParkFn(stackId, cfg);
|
|
714
|
-
|
|
715
|
-
const { prNumber, created } = openLayerPr(plan.branch, park.freezeBranch);
|
|
716
|
-
opened.push({ branch: plan.branch, base: park.freezeBranch, prNumber, created });
|
|
717
|
-
}
|
|
738
|
+
opened.push(...openLayerWave(pendingPark.map((plan) => ({ branch: plan.branch, base: park.freezeBranch }))));
|
|
718
739
|
await registerPr(opened[opened.length - 1].prNumber);
|
|
719
740
|
}
|
|
720
741
|
else {
|
|
@@ -787,6 +808,86 @@ async function cmdStackList(args) {
|
|
|
787
808
|
listed.push(ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"));
|
|
788
809
|
await present("Stacks", listed);
|
|
789
810
|
}
|
|
811
|
+
export async function cmdStackWait(args, deps = {}) {
|
|
812
|
+
const usage = "usage: mergestorm stack wait <stack-id> [--json] [--timeout <s>]";
|
|
813
|
+
const positional = [];
|
|
814
|
+
let timeoutS = 45;
|
|
815
|
+
for (let i = 0; i < args.length; i++) {
|
|
816
|
+
const arg = args[i];
|
|
817
|
+
if (arg === "--json")
|
|
818
|
+
continue;
|
|
819
|
+
if (arg === "--timeout" || arg.startsWith("--timeout=")) {
|
|
820
|
+
const value = arg === "--timeout" ? args[++i] : arg.slice("--timeout=".length);
|
|
821
|
+
timeoutS = value?.trim() ? Number(value) : NaN;
|
|
822
|
+
if (!Number.isFinite(timeoutS) || timeoutS < 0 || timeoutS > 300) {
|
|
823
|
+
throw new CommandError(`${usage}\n--timeout must be between 0 and 300 seconds`, REVIEW_EXIT.usage, "usage");
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
else if (arg.startsWith("-")) {
|
|
827
|
+
throw new CommandError(usage, REVIEW_EXIT.usage, "usage");
|
|
828
|
+
}
|
|
829
|
+
else {
|
|
830
|
+
positional.push(arg);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
if (positional.length !== 1)
|
|
834
|
+
throw new CommandError(usage, REVIEW_EXIT.usage, "usage");
|
|
835
|
+
const stackId = requireStackId(positional[0], usage);
|
|
836
|
+
const cfg = await (deps.loadConfig ?? loadConfig)();
|
|
837
|
+
const print = (envelope, retryAfterSeconds) => console.log(args.includes("--json")
|
|
838
|
+
? JSON.stringify({
|
|
839
|
+
...envelope,
|
|
840
|
+
...(retryAfterSeconds !== undefined ? { retry_after_seconds: retryAfterSeconds } : {}),
|
|
841
|
+
}, null, 2)
|
|
842
|
+
: `Stack ${envelope.stackId} · ${envelope.status}${stackBlockersSummary(envelope.blocker && envelope.prNumber != null ? { prNumber: envelope.prNumber, blocker: envelope.blocker } : null, envelope.issues)}${envelope.assessment === "unavailable" ? " · assessment unavailable" : ""}`);
|
|
843
|
+
try {
|
|
844
|
+
print(await (deps.pollStackWatch ?? pollStackWatch)(cfg, stackId, {
|
|
845
|
+
timeoutMs: timeoutS * 1000,
|
|
846
|
+
signal: deps.signal,
|
|
847
|
+
}));
|
|
848
|
+
}
|
|
849
|
+
catch (err) {
|
|
850
|
+
if (err instanceof StackWatchError && err.lastEnvelope.status === "rate_limited") {
|
|
851
|
+
print(err.lastEnvelope, err.retryAfterSeconds);
|
|
852
|
+
throw new CommandError(rateLimitedMessage(err.retryAfterSeconds), REVIEW_EXIT.rate_limited, "rate_limited", { retryAfterSeconds: err.retryAfterSeconds });
|
|
853
|
+
}
|
|
854
|
+
if (err instanceof StackWatchError) {
|
|
855
|
+
print(err.lastEnvelope);
|
|
856
|
+
throw err;
|
|
857
|
+
}
|
|
858
|
+
if (!(err instanceof StackWatchTimeoutError))
|
|
859
|
+
throw err;
|
|
860
|
+
print(err.lastEnvelope);
|
|
861
|
+
throw new CommandError(err.message, REVIEW_EXIT.timeout, "review_timeout");
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
export async function cmdStackStatus(args, deps = {}) {
|
|
865
|
+
const usage = "usage: mergestorm stack status <stack-id> [--json]";
|
|
866
|
+
const positional = args.filter((arg) => arg !== "--json");
|
|
867
|
+
if (positional.length !== 1)
|
|
868
|
+
throw new CommandError(usage, 2, "usage");
|
|
869
|
+
const stackId = requireStackId(positional[0], usage);
|
|
870
|
+
const cfg = await (deps.loadConfig ?? loadConfig)();
|
|
871
|
+
const stack = await (deps.getEnrichedStack ?? getEnrichedStack)(stackId, cfg);
|
|
872
|
+
if (!stack) {
|
|
873
|
+
throw new CommandError(`Stack not found or not owned by the current user: ${stackId}`, 1, "not_found");
|
|
874
|
+
}
|
|
875
|
+
if (args.includes("--json")) {
|
|
876
|
+
console.log(JSON.stringify(stack, null, 2));
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
const lines = formatStackHuman(stack, false);
|
|
880
|
+
for (const layer of stack.layers) {
|
|
881
|
+
const head = layer.headSha ? `@${layer.headSha.slice(0, 7)}` : "";
|
|
882
|
+
const pr = layer.prNumber > 0 ? `#${layer.prNumber}` : "—";
|
|
883
|
+
lines.push(` ${String(layer.position).padStart(2)} ${pr}${head} ${layer.state.padEnd(14)} ${layer.branch}` +
|
|
884
|
+
` CI: ${layer.ciStatus ?? "unknown"} review: ${layer.reviewStatus ?? "unknown"}` +
|
|
885
|
+
(layer.vortexStatus ? ` Vortex: ${layer.vortexStatus}` : "") +
|
|
886
|
+
(layer.cycloneStatus ? ` Cyclone: ${layer.cycloneStatus}` : "") +
|
|
887
|
+
(layer.tempestStatus ? ` Tempest: ${layer.tempestStatus}` : ""));
|
|
888
|
+
}
|
|
889
|
+
await present("Stack status", lines);
|
|
890
|
+
}
|
|
790
891
|
/** Human summary lines for a `stack set` write, one per flag. */
|
|
791
892
|
export function stackSetSummary(parsed) {
|
|
792
893
|
const tri = (value) => value === null ? "default (account setting)" : value ? "on" : "off";
|
|
@@ -885,7 +986,7 @@ async function cmdStackLand(args) {
|
|
|
885
986
|
` on ${id}`),
|
|
886
987
|
]);
|
|
887
988
|
}
|
|
888
|
-
export async function cmdStack(args) {
|
|
989
|
+
export async function cmdStack(args, options = {}) {
|
|
889
990
|
const sub = args[0]?.toLowerCase();
|
|
890
991
|
const rest = args.slice(1);
|
|
891
992
|
// Help must exit 0 — print usage, do not throw CommandError.
|
|
@@ -905,6 +1006,10 @@ export async function cmdStack(args) {
|
|
|
905
1006
|
return cmdStackReset(rest);
|
|
906
1007
|
if (sub === "list" || sub === "ls")
|
|
907
1008
|
return cmdStackList(rest);
|
|
1009
|
+
if (sub === "status")
|
|
1010
|
+
return cmdStackStatus(rest);
|
|
1011
|
+
if (sub === "wait")
|
|
1012
|
+
return cmdStackWait(rest, { signal: options.signal });
|
|
908
1013
|
if (sub === "set")
|
|
909
1014
|
return cmdStackSet(rest);
|
|
910
1015
|
if (sub === "adopt")
|
package/dist/errors.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Stable machine codes for CommandError control flow.
|
|
3
3
|
* UX copy in `message` may change freely; match on `code`, never on message text.
|
|
4
4
|
*/
|
|
5
|
-
export type CommandErrorCode = "auth_invalid" | "api_timeout" | "not_a_repo" | "not_found" | "missing_api_key" | "registered_parent" | "usage" | "review_quota" | "review_failed" | "review_timeout" | "rate_limited";
|
|
5
|
+
export type CommandErrorCode = "auth_invalid" | "api_timeout" | "not_a_repo" | "not_found" | "missing_api_key" | "registered_parent" | "usage" | "review_quota" | "review_failed" | "review_timeout" | "rate_limited" | "cyclone_not_installed" | "pr_merged" | "pr_closed" | "not_stacked" | "draft" | "reserved_branch" | "supabase_unconfigured" | "cyclone_not_connected" | "busy" | "adopt_failed";
|
|
6
6
|
/** Stable one-shot exit codes for machine callers of `mergestorm review`. */
|
|
7
7
|
export declare const REVIEW_EXIT: {
|
|
8
8
|
readonly success: 0;
|
package/dist/gh.d.ts
CHANGED
|
@@ -1,20 +1,82 @@
|
|
|
1
|
-
|
|
1
|
+
import { CommandError } from "./errors.js";
|
|
2
|
+
export type GhResult = {
|
|
2
3
|
ok: true;
|
|
3
4
|
stdout: string;
|
|
4
5
|
} | {
|
|
5
6
|
ok: false;
|
|
6
7
|
stderr: string;
|
|
7
8
|
status: number | null;
|
|
9
|
+
stdout?: string;
|
|
8
10
|
};
|
|
11
|
+
export type GhRunner = (args: string[], input?: string) => GhResult;
|
|
12
|
+
export type GithubApi = "graphql" | "rest";
|
|
13
|
+
export declare const GITHUB_API_ENV = "MERGESTORM_GITHUB_API";
|
|
14
|
+
export declare function githubApiFromEnv(env?: NodeJS.ProcessEnv): GithubApi;
|
|
15
|
+
export declare function isGraphqlEndpointBlocked(text: string): boolean;
|
|
16
|
+
export declare class GraphqlBlockedError extends CommandError {
|
|
17
|
+
readonly opened: readonly number[];
|
|
18
|
+
constructor(message: string, opened?: readonly number[]);
|
|
19
|
+
}
|
|
20
|
+
type GhSpawn = (command: string, args: string[], options: {
|
|
21
|
+
cwd: string;
|
|
22
|
+
encoding: "utf8";
|
|
23
|
+
input?: string;
|
|
24
|
+
}) => {
|
|
25
|
+
error?: Error;
|
|
26
|
+
stdout?: string | null;
|
|
27
|
+
stderr?: string | null;
|
|
28
|
+
status: number | null;
|
|
29
|
+
};
|
|
30
|
+
export declare function runGh(args: string[], cwd?: string, input?: string, spawn?: GhSpawn): GhResult;
|
|
9
31
|
export declare function requireGh(cwd?: string): void;
|
|
10
32
|
/** Open PR number for `head` branch in owner/repo, or null. */
|
|
11
|
-
export declare function findOpenPrNumber(owner: string, repo: string, headBranch: string, cwd?: string
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
33
|
+
export declare function findOpenPrNumber(owner: string, repo: string, headBranch: string, cwd?: string, options?: {
|
|
34
|
+
api?: GithubApi;
|
|
35
|
+
gh?: GhRunner;
|
|
36
|
+
}): number | null;
|
|
37
|
+
export declare function findOpenPrNumberViaRest(owner: string, repo: string, headBranch: string, gh: GhRunner): number | null;
|
|
38
|
+
export type NewPr = {
|
|
15
39
|
base: string;
|
|
16
40
|
head: string;
|
|
17
41
|
title: string;
|
|
18
|
-
body
|
|
42
|
+
body: string;
|
|
43
|
+
};
|
|
44
|
+
export type GraphqlRequest = {
|
|
45
|
+
query: string;
|
|
46
|
+
variables: Record<string, unknown>;
|
|
47
|
+
};
|
|
48
|
+
export type GraphqlResponse = {
|
|
49
|
+
data?: Record<string, unknown> | null;
|
|
50
|
+
errors?: {
|
|
51
|
+
message?: string;
|
|
52
|
+
}[];
|
|
53
|
+
};
|
|
54
|
+
export type GraphqlRunner = (request: GraphqlRequest) => GraphqlResponse;
|
|
55
|
+
type GraphqlSpawnResult = {
|
|
56
|
+
error?: Error;
|
|
57
|
+
stdout?: string | null;
|
|
58
|
+
stderr?: string | null;
|
|
59
|
+
status: number | null;
|
|
60
|
+
};
|
|
61
|
+
type GraphqlSpawn = (command: string, args: string[], options: {
|
|
62
|
+
cwd: string;
|
|
63
|
+
encoding: "utf8";
|
|
64
|
+
input: string;
|
|
65
|
+
}) => GraphqlSpawnResult;
|
|
66
|
+
export declare const CREATE_PR_BATCH_SIZE = 5;
|
|
67
|
+
export declare function ghGraphql(request: GraphqlRequest, cwd?: string, spawn?: GraphqlSpawn): GraphqlResponse;
|
|
68
|
+
export declare function chunkCreatePrs<T>(items: readonly T[], size?: number): T[][];
|
|
69
|
+
export declare function buildCreatePrsMutation(repositoryId: string, prs: readonly NewPr[]): GraphqlRequest;
|
|
70
|
+
export declare function fetchRepositoryId(owner: string, repo: string, graphql: GraphqlRunner): string;
|
|
71
|
+
export declare function createPrsOnRepository(repositoryId: string, prs: readonly NewPr[], graphql: GraphqlRunner): number[];
|
|
72
|
+
export declare function createPrsViaRest(owner: string, repo: string, prs: readonly NewPr[], gh: GhRunner, opened?: readonly number[]): number[];
|
|
73
|
+
export declare function createPrs(input: {
|
|
74
|
+
owner: string;
|
|
75
|
+
repo: string;
|
|
76
|
+
prs: readonly NewPr[];
|
|
19
77
|
cwd?: string;
|
|
20
|
-
|
|
78
|
+
api?: GithubApi;
|
|
79
|
+
graphql?: GraphqlRunner;
|
|
80
|
+
gh?: GhRunner;
|
|
81
|
+
}): number[];
|
|
82
|
+
export {};
|