mergestorm 0.3.9 → 0.3.15
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 +67 -24
- package/dist/api.d.ts +106 -40
- package/dist/api.js +245 -9
- package/dist/client/index.d.ts +5 -0
- package/dist/client/index.js +5 -0
- package/dist/commands/branches.d.ts +6 -2
- package/dist/commands/branches.js +29 -14
- package/dist/commands/browse.d.ts +39 -0
- package/dist/commands/browse.js +259 -0
- package/dist/commands/credits.js +30 -12
- package/dist/commands/help.d.ts +5 -0
- package/dist/commands/help.js +93 -0
- package/dist/commands/jobs.d.ts +11 -2
- package/dist/commands/jobs.js +64 -15
- package/dist/commands/login.js +32 -10
- package/dist/commands/logout.js +2 -1
- package/dist/commands/pr.d.ts +22 -0
- package/dist/commands/pr.js +127 -0
- package/dist/commands/queue.d.ts +12 -0
- package/dist/commands/queue.js +160 -0
- package/dist/commands/registry.d.ts +1 -1
- package/dist/commands/registry.js +115 -33
- package/dist/commands/review-client.d.ts +106 -0
- package/dist/commands/review-client.js +399 -0
- package/dist/commands/review.d.ts +36 -6
- package/dist/commands/review.js +395 -145
- package/dist/commands/settings.d.ts +17 -0
- package/dist/commands/settings.js +89 -0
- package/dist/commands/skill.d.ts +20 -0
- package/dist/commands/skill.js +145 -0
- package/dist/commands/stack.d.ts +36 -2
- package/dist/commands/stack.js +341 -113
- package/dist/commands/status.d.ts +18 -2
- package/dist/commands/status.js +236 -21
- package/dist/commands/whoami.d.ts +3 -1
- package/dist/commands/whoami.js +21 -7
- package/dist/errors.d.ts +18 -2
- package/dist/errors.js +22 -1
- package/dist/git-stack.d.ts +2 -0
- package/dist/git-stack.js +4 -0
- package/dist/git.d.ts +12 -1
- package/dist/git.js +52 -7
- package/dist/shell.d.ts +1 -1
- package/dist/shell.js +39 -28
- package/dist/stack-dto.d.ts +266 -0
- package/dist/stack-dto.js +7 -0
- package/dist/submit-pr-base.d.ts +31 -0
- package/dist/submit-pr-base.js +69 -0
- package/dist/ui/ansi.d.ts +2 -0
- package/dist/ui/ansi.js +2 -0
- package/dist/ui/banner.d.ts +34 -2
- package/dist/ui/banner.js +170 -79
- package/dist/ui/box.d.ts +13 -9
- package/dist/ui/box.js +28 -44
- package/dist/ui/findings.d.ts +4 -1
- package/dist/ui/findings.js +14 -7
- package/dist/ui/job-envelope.d.ts +68 -0
- package/dist/ui/job-envelope.js +35 -0
- package/dist/ui/line-tabs.d.ts +27 -0
- package/dist/ui/line-tabs.js +170 -0
- package/dist/ui/logo.d.ts +10 -2
- package/dist/ui/logo.js +13 -9
- package/dist/ui/present.d.ts +10 -0
- package/dist/ui/present.js +13 -0
- package/dist/ui/prompt.d.ts +148 -0
- package/dist/ui/prompt.js +433 -71
- package/dist/ui/select.js +1 -1
- package/dist/ui/tabs.d.ts +125 -0
- package/dist/ui/tabs.js +480 -0
- package/dist/ui/usage.d.ts +29 -1
- package/dist/ui/usage.js +71 -2
- package/dist/ui/width.d.ts +26 -0
- package/dist/ui/width.js +262 -0
- package/package.json +6 -1
- package/skill/SKILL.md +98 -0
- package/skill/mergestorm-pr-loop/SKILL.md +76 -0
package/dist/commands/stack.js
CHANGED
|
@@ -1,26 +1,77 @@
|
|
|
1
|
-
import { adoptStack, landNextStack, listStacks, restackStack,
|
|
1
|
+
import { adoptStack, ensureUpperPark, landNextStack, listStacks, restackStack, } from "../api.js";
|
|
2
2
|
import { loadConfig } from "../config.js";
|
|
3
3
|
import { CommandError } from "../errors.js";
|
|
4
4
|
import { createPr, findOpenPrNumber, requireGh } from "../gh.js";
|
|
5
5
|
import { parseGithubOriginRepo } from "../git.js";
|
|
6
|
-
import { branchExists, commitsAheadOf, createBranchFromHead, currentBranch, defaultLayerBranchName, deleteBranch, discoverTrunk, pushBranch, tipCommitMessage, tipCommitSubject, worktreeDirty, } from "../git-stack.js";
|
|
6
|
+
import { branchExists, commitsAheadOf, createBranchFromHead, currentBranch, defaultLayerBranchName, deleteBranch, discoverTrunk, fetchRemoteBranch, pushBranch, tipCommitMessage, tipCommitSubject, worktreeDirty, } from "../git-stack.js";
|
|
7
7
|
import { buildPrBodyFromCommit } from "../pr-body.js";
|
|
8
8
|
import { gitTopLevel } from "../repo-identity.js";
|
|
9
9
|
import { activeLayers, appendLayer, emptyStackMeta, loadStackMeta, removeActiveStack, resetStackMeta, saveStackMeta, selectStackForCreate, } from "../stack-meta.js";
|
|
10
|
+
import { isMgParkBranch, planSubmitLayerBases } from "../submit-pr-base.js";
|
|
10
11
|
import { ansi } from "../ui/ansi.js";
|
|
12
|
+
import { runLineTabsBrowser } from "../ui/line-tabs.js";
|
|
13
|
+
import { present } from "../ui/present.js";
|
|
14
|
+
import { canBrowse } from "./browse.js";
|
|
15
|
+
import { openHelpBrowser } from "./help.js";
|
|
11
16
|
const STACK_USAGE = `usage:
|
|
12
|
-
mergestorm stack create [name] [--onto <branch>] [--trunk <branch>] [--json]
|
|
13
|
-
mergestorm stack submit [--json]
|
|
17
|
+
mergestorm stack create [name] [--onto <branch>] [--trunk <branch>] [--extend] [--json]
|
|
18
|
+
mergestorm stack submit [--extend] [--json]
|
|
14
19
|
mergestorm stack reset --force
|
|
15
20
|
mergestorm stack list [--json]
|
|
16
21
|
mergestorm stack adopt <owner/repo>#<pr>
|
|
17
22
|
mergestorm stack restack <stack-id> [--json]
|
|
18
23
|
mergestorm stack land <stack-id> [--json]
|
|
19
|
-
mergestorm stack auto-promote on|off <stack-id> [--json]
|
|
20
24
|
|
|
21
25
|
stack submit opens PRs with a body generated from each layer tip commit.
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
Layers 1–2 use the git parent as GitHub base. Layer 3+ opens onto the
|
|
27
|
+
review-unit park freeze (mg-park-*) so adopt does not retarget after open.
|
|
28
|
+
Parenting onto a branch that already belongs to a registered (submitted)
|
|
29
|
+
stack requires --extend on create and submit — otherwise mg starts a new
|
|
30
|
+
stack from trunk. stack land promotes into the review unit when one exists;
|
|
31
|
+
otherwise lands the bottom open PR (unit-less stacks).`;
|
|
32
|
+
/** Cap the registered-stack guard's API probe so it cannot stall local authoring. */
|
|
33
|
+
const STACK_GUARD_TIMEOUT_MS = 8_000;
|
|
34
|
+
/** Find a registered stack layer whose branch matches `parentBranch`. */
|
|
35
|
+
export function findRegisteredParent(stacks, parentBranch, owner, repo) {
|
|
36
|
+
const want = parentBranch.trim();
|
|
37
|
+
if (!want)
|
|
38
|
+
return null;
|
|
39
|
+
for (const s of stacks) {
|
|
40
|
+
if (owner !== undefined && repo !== undefined) {
|
|
41
|
+
if (s.owner !== owner || s.repo !== repo)
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
for (const layer of s.layers) {
|
|
45
|
+
if (layer.branch === want) {
|
|
46
|
+
return {
|
|
47
|
+
stackId: s.id,
|
|
48
|
+
owner: s.owner,
|
|
49
|
+
repo: s.repo,
|
|
50
|
+
branch: layer.branch,
|
|
51
|
+
prNumber: layer.prNumber,
|
|
52
|
+
position: layer.position,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Refuse silently growing an already-registered stack.
|
|
61
|
+
* Local unsubmitted parents are fine; only API-listed layers trip this.
|
|
62
|
+
*/
|
|
63
|
+
export function assertMayParentOntoRegistered(parentBranch, stacks, extend, verb, owner, repo) {
|
|
64
|
+
const hit = findRegisteredParent(stacks, parentBranch, owner, repo);
|
|
65
|
+
if (!hit)
|
|
66
|
+
return null;
|
|
67
|
+
if (extend)
|
|
68
|
+
return hit;
|
|
69
|
+
const pr = hit.prNumber > 0 ? `PR #${hit.prNumber}` : "an open PR";
|
|
70
|
+
throw new CommandError(`Parent \`${hit.branch}\` is already layer ${hit.position} of registered stack ${hit.stackId} (${hit.owner}/${hit.repo}, ${pr}).\n` +
|
|
71
|
+
`Refusing to ${verb} a new layer onto that stack without an explicit opt-in.\n` +
|
|
72
|
+
`• New independent stack: check out trunk and pass \`--onto <trunk>\` (e.g. \`mg stack create --onto main\`).\n` +
|
|
73
|
+
`• Intentionally grow that stack: pass \`--extend\` on \`stack ${verb}\`.`, 1, "registered_parent");
|
|
74
|
+
}
|
|
24
75
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
25
76
|
export function requireStackId(raw, usage) {
|
|
26
77
|
const id = raw?.trim() ?? "";
|
|
@@ -49,10 +100,35 @@ export function parseAdoptTarget(args) {
|
|
|
49
100
|
}
|
|
50
101
|
throw new CommandError(`usage: mergestorm stack adopt <owner/repo>#<pr>`);
|
|
51
102
|
}
|
|
103
|
+
const stackCmd = (name, blurb) => ` ${ansi.brightGreen(name.padEnd(26))}${blurb}`;
|
|
104
|
+
/** Empty + populated bodies for the TTY `stack list` panel. */
|
|
105
|
+
export function buildStackListLines(stacks) {
|
|
106
|
+
if (stacks.length === 0) {
|
|
107
|
+
return [
|
|
108
|
+
` ${ansi.bold("None yet")}`,
|
|
109
|
+
" No registered stacks for this key.",
|
|
110
|
+
"",
|
|
111
|
+
` ${ansi.bold("Author one")}`,
|
|
112
|
+
stackCmd("stack create", "new layer on this branch"),
|
|
113
|
+
" commit, then stack submit",
|
|
114
|
+
"",
|
|
115
|
+
` ${ansi.bold("Or import")}`,
|
|
116
|
+
stackCmd("stack adopt org/repo#12", "open GitHub chain"),
|
|
117
|
+
"",
|
|
118
|
+
ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"),
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
const lines = [];
|
|
122
|
+
for (const s of stacks) {
|
|
123
|
+
lines.push(...formatStackHuman(s));
|
|
124
|
+
lines.push("");
|
|
125
|
+
}
|
|
126
|
+
lines.push(ansi.dim(" create · submit · land <id> · q to close"));
|
|
127
|
+
return lines;
|
|
128
|
+
}
|
|
52
129
|
function formatStackHuman(s) {
|
|
53
130
|
const lines = [];
|
|
54
|
-
|
|
55
|
-
lines.push(` ${ansi.bold(`${s.owner}/${s.repo}`)} trunk=${s.trunkBranch}${auto}`);
|
|
131
|
+
lines.push(` ${ansi.bold(`${s.owner}/${s.repo}`)} trunk=${s.trunkBranch}`);
|
|
56
132
|
lines.push(` ${ansi.dim(s.id)}`);
|
|
57
133
|
if (s.layers.length === 0) {
|
|
58
134
|
lines.push(ansi.dim(" (no layers)"));
|
|
@@ -70,6 +146,7 @@ export function parseStackCreateArgs(args) {
|
|
|
70
146
|
let name;
|
|
71
147
|
let onto;
|
|
72
148
|
let trunk;
|
|
149
|
+
let extend = false;
|
|
73
150
|
let asJson = false;
|
|
74
151
|
for (let i = 0; i < args.length; i++) {
|
|
75
152
|
const a = args[i];
|
|
@@ -77,10 +154,15 @@ export function parseStackCreateArgs(args) {
|
|
|
77
154
|
asJson = true;
|
|
78
155
|
continue;
|
|
79
156
|
}
|
|
157
|
+
if (a === "--extend") {
|
|
158
|
+
extend = true;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
80
161
|
if (a === "--onto") {
|
|
81
162
|
const v = args[++i];
|
|
82
|
-
if (!v)
|
|
83
|
-
throw new CommandError("usage: mergestorm stack create [name] [--onto <branch>]");
|
|
163
|
+
if (!v) {
|
|
164
|
+
throw new CommandError("usage: mergestorm stack create [name] [--onto <branch>] [--trunk <branch>] [--extend]");
|
|
165
|
+
}
|
|
84
166
|
onto = v;
|
|
85
167
|
continue;
|
|
86
168
|
}
|
|
@@ -90,8 +172,9 @@ export function parseStackCreateArgs(args) {
|
|
|
90
172
|
}
|
|
91
173
|
if (a === "--trunk") {
|
|
92
174
|
const v = args[++i];
|
|
93
|
-
if (!v)
|
|
94
|
-
throw new CommandError("usage: mergestorm stack create [name] [--trunk <branch>]");
|
|
175
|
+
if (!v) {
|
|
176
|
+
throw new CommandError("usage: mergestorm stack create [name] [--onto <branch>] [--trunk <branch>] [--extend]");
|
|
177
|
+
}
|
|
95
178
|
trunk = v;
|
|
96
179
|
continue;
|
|
97
180
|
}
|
|
@@ -103,20 +186,40 @@ export function parseStackCreateArgs(args) {
|
|
|
103
186
|
throw new CommandError(`${STACK_USAGE}\nunknown flag: ${a}`);
|
|
104
187
|
}
|
|
105
188
|
if (name != null) {
|
|
106
|
-
throw new CommandError("usage: mergestorm stack create [name] [--onto <branch>] [--trunk <branch>]");
|
|
189
|
+
throw new CommandError("usage: mergestorm stack create [name] [--onto <branch>] [--trunk <branch>] [--extend]");
|
|
107
190
|
}
|
|
108
191
|
name = a;
|
|
109
192
|
}
|
|
110
|
-
return { name, onto, trunk, asJson };
|
|
193
|
+
return { name, onto, trunk, extend, asJson };
|
|
194
|
+
}
|
|
195
|
+
/** Parse `stack submit` argv. Exported for tests. */
|
|
196
|
+
export function parseStackSubmitArgs(args) {
|
|
197
|
+
let extend = false;
|
|
198
|
+
let asJson = false;
|
|
199
|
+
for (const a of args) {
|
|
200
|
+
if (a === "--json") {
|
|
201
|
+
asJson = true;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (a === "--extend") {
|
|
205
|
+
extend = true;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
throw new CommandError("usage: mergestorm stack submit [--extend] [--json]");
|
|
209
|
+
}
|
|
210
|
+
return { extend, asJson };
|
|
111
211
|
}
|
|
112
212
|
export function parseStackResetArgs(args) {
|
|
113
213
|
if (args.length !== 1 || args[0] !== "--force") {
|
|
114
214
|
throw new CommandError("usage: mergestorm stack reset --force\nClears only CLI-managed pre-submit state; branches, PRs, and registered stacks are unchanged.");
|
|
115
215
|
}
|
|
116
216
|
}
|
|
117
|
-
async function cmdStackCreate(args) {
|
|
118
|
-
const { name, onto, trunk: trunkFlag, asJson } = parseStackCreateArgs(args);
|
|
119
|
-
const cwd = process.cwd();
|
|
217
|
+
async function cmdStackCreate(args, deps = {}) {
|
|
218
|
+
const { name, onto, trunk: trunkFlag, extend, asJson } = parseStackCreateArgs(args);
|
|
219
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
220
|
+
const listStacksFn = deps.listStacks ?? listStacks;
|
|
221
|
+
const loadConfigFn = deps.loadConfig ?? loadConfig;
|
|
222
|
+
const parseOriginFn = deps.parseGithubOriginRepo ?? parseGithubOriginRepo;
|
|
120
223
|
try {
|
|
121
224
|
gitTopLevel(cwd);
|
|
122
225
|
}
|
|
@@ -145,6 +248,37 @@ async function cmdStackCreate(args) {
|
|
|
145
248
|
else if (trunkFlag?.trim() && meta.trunk !== trunk) {
|
|
146
249
|
throw new CommandError(`Stack trunk is ${meta.trunk}; refusing --trunk ${trunk}. Run \`mg stack reset --force\` to discard local pre-submit state.`);
|
|
147
250
|
}
|
|
251
|
+
// --trunk only sets metadata; parent defaults to current branch. Call that out
|
|
252
|
+
// when someone passes --trunk while sitting on a different branch without --onto.
|
|
253
|
+
if (trunkFlag?.trim() && !onto?.trim() && parentBranch !== trunk) {
|
|
254
|
+
console.error(ansi.dim(` note: --trunk ${trunk} does not change the parent; new layer parents onto \`${parentBranch}\`. ` +
|
|
255
|
+
`Pass \`--onto ${trunk}\` for a fresh stack from trunk.`));
|
|
256
|
+
}
|
|
257
|
+
// Block silent attach onto an already-submitted stack tip (requires --extend).
|
|
258
|
+
// --trunk is user metadata, so a registered tip must not be able to masquerade
|
|
259
|
+
// as trunk — always consult the stacks API rather than trusting parent === trunk.
|
|
260
|
+
// Best-effort: API/key failures degrade to a warning so create stays local.
|
|
261
|
+
const origin = parseOriginFn(cwd);
|
|
262
|
+
let registeredHit = null;
|
|
263
|
+
if (origin) {
|
|
264
|
+
try {
|
|
265
|
+
const cfg = await loadConfigFn();
|
|
266
|
+
const registered = await listStacksFn(cfg, { timeoutMs: STACK_GUARD_TIMEOUT_MS });
|
|
267
|
+
registeredHit = assertMayParentOntoRegistered(parentBranch, registered, extend, "create", origin.owner, origin.repo);
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
if (err instanceof CommandError && err.code === "registered_parent")
|
|
271
|
+
throw err;
|
|
272
|
+
console.error(ansi.dim(` warning: could not check registered stacks (${err instanceof Error ? err.message : String(err)}); ` +
|
|
273
|
+
`skipping --extend guard. Pass --extend if you intend to grow an open stack.`));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
console.error(ansi.dim(" warning: could not determine GitHub owner/repo from `origin`; skipping --extend guard. Pass --extend if you intend to grow an open stack."));
|
|
278
|
+
}
|
|
279
|
+
if (parentBranch === trunk && extend && !registeredHit) {
|
|
280
|
+
throw new CommandError("`--extend` is only valid when parenting onto a registered stack layer, not trunk. Omit --extend (or pass --onto <tip> --extend).");
|
|
281
|
+
}
|
|
148
282
|
try {
|
|
149
283
|
meta = selectStackForCreate(meta, parentBranch);
|
|
150
284
|
}
|
|
@@ -174,6 +308,7 @@ async function cmdStackCreate(args) {
|
|
|
174
308
|
branch,
|
|
175
309
|
parentBranch,
|
|
176
310
|
trunk: meta.trunk,
|
|
311
|
+
extend: Boolean(registeredHit),
|
|
177
312
|
active: meta.active,
|
|
178
313
|
stacks: meta.stacks.length,
|
|
179
314
|
layers,
|
|
@@ -181,15 +316,19 @@ async function cmdStackCreate(args) {
|
|
|
181
316
|
}, null, 2));
|
|
182
317
|
return;
|
|
183
318
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
319
|
+
const created = [
|
|
320
|
+
ansi.brightGreen(` Created ${branch} (onto ${parentBranch})`),
|
|
321
|
+
];
|
|
322
|
+
if (registeredHit) {
|
|
323
|
+
created.push(ansi.dim(` extending registered stack ${registeredHit.stackId}` +
|
|
324
|
+
(registeredHit.prNumber > 0 ? ` (from #${registeredHit.prNumber})` : "")));
|
|
325
|
+
}
|
|
326
|
+
created.push(ansi.dim(` trunk: ${meta.trunk} · local stacks: ${meta.stacks.length} · active layers: ${layers.length}`));
|
|
327
|
+
created.push(ansi.dim(" authoring state: managed automatically outside the repository"));
|
|
328
|
+
await present("Stack", created);
|
|
187
329
|
}
|
|
188
330
|
export async function cmdStackSubmit(args, deps = {}) {
|
|
189
|
-
const asJson = args
|
|
190
|
-
if (args.some((a) => a !== "--json")) {
|
|
191
|
-
throw new CommandError("usage: mergestorm stack submit [--json]");
|
|
192
|
-
}
|
|
331
|
+
const { extend, asJson } = parseStackSubmitArgs(args);
|
|
193
332
|
const cwd = deps.cwd ?? process.cwd();
|
|
194
333
|
const gitTopLevelFn = deps.gitTopLevel ?? gitTopLevel;
|
|
195
334
|
const loadStackMetaFn = deps.loadStackMeta ?? loadStackMeta;
|
|
@@ -197,6 +336,7 @@ export async function cmdStackSubmit(args, deps = {}) {
|
|
|
197
336
|
const parseOriginFn = deps.parseGithubOriginRepo ?? parseGithubOriginRepo;
|
|
198
337
|
const requireGhFn = deps.requireGh ?? requireGh;
|
|
199
338
|
const branchExistsFn = deps.branchExists ?? branchExists;
|
|
339
|
+
const fetchRemoteBranchFn = deps.fetchRemoteBranch ?? fetchRemoteBranch;
|
|
200
340
|
const pushBranchFn = deps.pushBranch ?? pushBranch;
|
|
201
341
|
const findOpenPrNumberFn = deps.findOpenPrNumber ?? findOpenPrNumber;
|
|
202
342
|
const commitsAheadOfFn = deps.commitsAheadOf ?? commitsAheadOf;
|
|
@@ -205,7 +345,9 @@ export async function cmdStackSubmit(args, deps = {}) {
|
|
|
205
345
|
const buildPrBodyFn = deps.buildPrBodyFromCommit ?? buildPrBodyFromCommit;
|
|
206
346
|
const createPrFn = deps.createPr ?? createPr;
|
|
207
347
|
const loadConfigFn = deps.loadConfig ?? loadConfig;
|
|
348
|
+
const listStacksFn = deps.listStacks ?? listStacks;
|
|
208
349
|
const adoptStackFn = deps.adoptStack ?? adoptStack;
|
|
350
|
+
const ensureUpperParkFn = deps.ensureUpperPark ?? ensureUpperPark;
|
|
209
351
|
try {
|
|
210
352
|
gitTopLevelFn(cwd);
|
|
211
353
|
}
|
|
@@ -223,9 +365,41 @@ export async function cmdStackSubmit(args, deps = {}) {
|
|
|
223
365
|
}
|
|
224
366
|
const { owner, repo } = origin;
|
|
225
367
|
requireGhFn(cwd);
|
|
226
|
-
|
|
368
|
+
// Safety net: refuse submit that would open/register onto a registered tip
|
|
369
|
+
// without --extend. Check every layer's parent (mid-stack layers from older
|
|
370
|
+
// local state must not sail through), scoped to this repo, and fail closed
|
|
371
|
+
// when the API cannot be consulted — otherwise the attach happens anyway.
|
|
372
|
+
// meta.trunk is user metadata, so it must not exempt a registered tip from
|
|
373
|
+
// the check (a tip can masquerade as trunk via --trunk at create).
|
|
374
|
+
const parents = [...new Set(layers.map((l) => l.parentBranch || meta.trunk))];
|
|
375
|
+
let registeredHits = 0;
|
|
376
|
+
let registered = [];
|
|
377
|
+
try {
|
|
378
|
+
const cfg = await loadConfigFn();
|
|
379
|
+
registered = await listStacksFn(cfg, { timeoutMs: STACK_GUARD_TIMEOUT_MS });
|
|
380
|
+
for (const parent of parents) {
|
|
381
|
+
if (assertMayParentOntoRegistered(parent, registered, extend, "submit", owner, repo)) {
|
|
382
|
+
registeredHits += 1;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
catch (err) {
|
|
387
|
+
if (err instanceof CommandError && err.code === "registered_parent")
|
|
388
|
+
throw err;
|
|
389
|
+
throw new CommandError(`Could not verify that the stack parents onto registered stacks (${err instanceof Error ? err.message : String(err)}); ` +
|
|
390
|
+
`refusing to submit. If a layer grows a registered stack, pass --extend (retry when the API is reachable).`);
|
|
391
|
+
}
|
|
392
|
+
if (extend && registeredHits === 0 && parents.every((p) => p === meta.trunk)) {
|
|
393
|
+
throw new CommandError("`--extend` is only valid when a layer parents onto a registered stack layer, not trunk.");
|
|
394
|
+
}
|
|
395
|
+
const plans = planSubmitLayerBases({
|
|
396
|
+
layers,
|
|
397
|
+
trunk: meta.trunk,
|
|
398
|
+
registered,
|
|
399
|
+
owner,
|
|
400
|
+
repo,
|
|
401
|
+
});
|
|
227
402
|
for (const layer of layers) {
|
|
228
|
-
const base = layer.parentBranch || meta.trunk;
|
|
229
403
|
if (!branchExistsFn(layer.branch, cwd)) {
|
|
230
404
|
throw new CommandError(`Stack layer branch missing locally: ${layer.branch}`);
|
|
231
405
|
}
|
|
@@ -236,58 +410,109 @@ export async function cmdStackSubmit(args, deps = {}) {
|
|
|
236
410
|
catch (err) {
|
|
237
411
|
throw new CommandError(err instanceof Error ? err.message : String(err));
|
|
238
412
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
413
|
+
}
|
|
414
|
+
const opened = [];
|
|
415
|
+
const pendingPark = plans.filter((plan) => {
|
|
416
|
+
if (findOpenPrNumberFn(owner, repo, plan.branch, cwd) != null)
|
|
417
|
+
return false;
|
|
418
|
+
return plan.githubBase == null;
|
|
419
|
+
});
|
|
420
|
+
const openLayerPr = (branch, base) => {
|
|
421
|
+
const existing = findOpenPrNumberFn(owner, repo, branch, cwd);
|
|
422
|
+
if (existing != null) {
|
|
423
|
+
console.log(ansi.dim(` PR #${existing} already open for ${branch}`));
|
|
424
|
+
return { prNumber: existing, created: false };
|
|
425
|
+
}
|
|
426
|
+
const aheadRef = isMgParkBranch(base) ? `origin/${base}` : base;
|
|
427
|
+
if (isMgParkBranch(base)) {
|
|
247
428
|
try {
|
|
248
|
-
|
|
429
|
+
fetchRemoteBranchFn(base, cwd);
|
|
249
430
|
}
|
|
250
431
|
catch (err) {
|
|
251
|
-
throw new CommandError(err instanceof Error ? err.message : String(err));
|
|
252
|
-
}
|
|
253
|
-
if (ahead < 1) {
|
|
254
|
-
throw new CommandError(`No commits on \`${layer.branch}\` ahead of \`${base}\`. ` +
|
|
255
|
-
`Commit your changes, then retry \`mg stack submit\`.`);
|
|
432
|
+
throw new CommandError(`Could not fetch park base \`${base}\` (${err instanceof Error ? err.message : String(err)}).`);
|
|
256
433
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
434
|
+
}
|
|
435
|
+
else if (!branchExistsFn(base, cwd)) {
|
|
436
|
+
throw new CommandError(`Stack base branch missing locally: ${base}`);
|
|
437
|
+
}
|
|
438
|
+
let ahead;
|
|
439
|
+
try {
|
|
440
|
+
ahead = commitsAheadOfFn(aheadRef, branch, cwd);
|
|
441
|
+
}
|
|
442
|
+
catch (err) {
|
|
443
|
+
throw new CommandError(err instanceof Error ? err.message : String(err));
|
|
444
|
+
}
|
|
445
|
+
if (ahead < 1) {
|
|
446
|
+
throw new CommandError(`No commits on \`${branch}\` ahead of \`${base}\`. ` +
|
|
447
|
+
`Commit your changes, then retry \`mg stack submit\`.`);
|
|
448
|
+
}
|
|
449
|
+
const title = tipCommitSubjectFn(branch, cwd);
|
|
450
|
+
const body = buildPrBodyFn(tipCommitMessageFn(branch, cwd));
|
|
451
|
+
console.log(ansi.dim(` Opening PR ${branch} → ${base} …`));
|
|
452
|
+
return {
|
|
453
|
+
prNumber: createPrFn({
|
|
261
454
|
owner,
|
|
262
455
|
repo,
|
|
263
456
|
base,
|
|
264
|
-
head:
|
|
457
|
+
head: branch,
|
|
265
458
|
title,
|
|
266
459
|
body,
|
|
267
460
|
cwd,
|
|
268
|
-
})
|
|
269
|
-
created
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
|
|
461
|
+
}),
|
|
462
|
+
created: true,
|
|
463
|
+
};
|
|
464
|
+
};
|
|
465
|
+
for (const plan of plans) {
|
|
466
|
+
if (plan.githubBase == null && findOpenPrNumberFn(owner, repo, plan.branch, cwd) == null) {
|
|
467
|
+
continue;
|
|
273
468
|
}
|
|
274
|
-
|
|
469
|
+
const base = plan.githubBase ?? plan.gitParent;
|
|
470
|
+
const { prNumber, created } = openLayerPr(plan.branch, base);
|
|
471
|
+
opened.push({ branch: plan.branch, base, prNumber, created });
|
|
275
472
|
}
|
|
276
|
-
const registerPr = opened[0].prNumber;
|
|
277
473
|
const cfg = await loadConfigFn();
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
474
|
+
let adoptData = null;
|
|
475
|
+
let stackId = plans.find((plan) => plan.stackId)?.stackId ?? null;
|
|
476
|
+
const registerPr = async (prNumber) => {
|
|
477
|
+
console.log(ansi.dim(` Registering stack via import (${owner}/${repo}#${prNumber}) …`));
|
|
478
|
+
const body = await adoptStackFn(owner, repo, prNumber, cfg);
|
|
479
|
+
if (typeof body !== "object" || body === null) {
|
|
480
|
+
throw new CommandError("Unexpected response shape from adopt API");
|
|
481
|
+
}
|
|
482
|
+
const data = body;
|
|
483
|
+
if (data.error) {
|
|
484
|
+
throw new CommandError(data.error);
|
|
485
|
+
}
|
|
486
|
+
if (!data.stack) {
|
|
487
|
+
throw new CommandError("Stack was not registered");
|
|
488
|
+
}
|
|
489
|
+
adoptData = data;
|
|
490
|
+
if (typeof data.stack.id === "string" && data.stack.id) {
|
|
491
|
+
stackId = data.stack.id;
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
if (pendingPark.length > 0) {
|
|
495
|
+
if (opened.length > 0) {
|
|
496
|
+
await registerPr(opened[0].prNumber);
|
|
497
|
+
}
|
|
498
|
+
if (!stackId) {
|
|
499
|
+
throw new CommandError("Cannot mint a park freeze before the stack is registered. Open layers 1–2 first, then retry.");
|
|
500
|
+
}
|
|
501
|
+
console.log(ansi.dim(` Ensuring upper-park freeze for stack ${stackId} …`));
|
|
502
|
+
const park = await ensureUpperParkFn(stackId, cfg);
|
|
503
|
+
for (const plan of pendingPark) {
|
|
504
|
+
const { prNumber, created } = openLayerPr(plan.branch, park.freezeBranch);
|
|
505
|
+
opened.push({ branch: plan.branch, base: park.freezeBranch, prNumber, created });
|
|
506
|
+
}
|
|
507
|
+
await registerPr(opened[opened.length - 1].prNumber);
|
|
286
508
|
}
|
|
287
|
-
|
|
288
|
-
|
|
509
|
+
else {
|
|
510
|
+
if (opened.length === 0) {
|
|
511
|
+
throw new CommandError("No pull requests to register after submit.");
|
|
512
|
+
}
|
|
513
|
+
await registerPr(opened[0].prNumber);
|
|
289
514
|
}
|
|
290
|
-
const
|
|
515
|
+
const data = adoptData;
|
|
291
516
|
// Drop the submitted local stack so the next create onto trunk starts fresh.
|
|
292
517
|
await saveStackMetaFn(removeActiveStack(meta), cwd);
|
|
293
518
|
if (asJson) {
|
|
@@ -301,22 +526,26 @@ export async function cmdStackSubmit(args, deps = {}) {
|
|
|
301
526
|
}, null, 2));
|
|
302
527
|
return;
|
|
303
528
|
}
|
|
529
|
+
const summary = [];
|
|
304
530
|
for (const row of opened) {
|
|
305
531
|
const verb = row.created ? "opened" : "exists";
|
|
306
|
-
|
|
532
|
+
summary.push(ansi.brightGreen(` #${row.prNumber} ${row.branch} → ${row.base} (${verb})`));
|
|
307
533
|
}
|
|
308
534
|
if (stackId) {
|
|
309
|
-
|
|
535
|
+
summary.push(ansi.brightGreen(` Registered stack ${stackId}`));
|
|
310
536
|
}
|
|
311
537
|
else {
|
|
312
|
-
|
|
538
|
+
summary.push(ansi.dim(" Stack registration returned no stack ID"));
|
|
313
539
|
}
|
|
314
|
-
|
|
540
|
+
summary.push(ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"));
|
|
541
|
+
await present("Stack", summary);
|
|
315
542
|
}
|
|
316
543
|
async function cmdStackReset(args) {
|
|
317
544
|
parseStackResetArgs(args);
|
|
318
545
|
await resetStackMeta(process.cwd());
|
|
319
|
-
|
|
546
|
+
await present("Stack", [
|
|
547
|
+
ansi.brightGreen(" Cleared CLI-managed pre-submit stack state. Branches, PRs, and registered stacks were not changed."),
|
|
548
|
+
]);
|
|
320
549
|
}
|
|
321
550
|
async function cmdStackList(args) {
|
|
322
551
|
const asJson = args.includes("--json");
|
|
@@ -326,18 +555,26 @@ async function cmdStackList(args) {
|
|
|
326
555
|
console.log(JSON.stringify({ stacks }, null, 2));
|
|
327
556
|
return;
|
|
328
557
|
}
|
|
558
|
+
if (canBrowse()) {
|
|
559
|
+
await runLineTabsBrowser({
|
|
560
|
+
tabs: [{ id: "yours", label: "Stacks", lines: buildStackListLines(stacks) }],
|
|
561
|
+
});
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
329
564
|
if (stacks.length === 0) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
565
|
+
await present("Stacks", [
|
|
566
|
+
ansi.dim(" No stacks yet. Author with `stack create` → commit → `stack submit`."),
|
|
567
|
+
ansi.dim(" Import an existing chain only: `stack adopt owner/repo#pr`"),
|
|
568
|
+
ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"),
|
|
569
|
+
]);
|
|
333
570
|
return;
|
|
334
571
|
}
|
|
572
|
+
const listed = [];
|
|
335
573
|
for (const s of stacks) {
|
|
336
|
-
|
|
337
|
-
console.log(line);
|
|
338
|
-
console.log("");
|
|
574
|
+
listed.push(...formatStackHuman(s), "");
|
|
339
575
|
}
|
|
340
|
-
|
|
576
|
+
listed.push(ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"));
|
|
577
|
+
await present("Stacks", listed);
|
|
341
578
|
}
|
|
342
579
|
async function cmdStackAdopt(args) {
|
|
343
580
|
const asJson = args.includes("--json");
|
|
@@ -358,13 +595,16 @@ async function cmdStackAdopt(args) {
|
|
|
358
595
|
const data = body;
|
|
359
596
|
const stack = data.stack;
|
|
360
597
|
const chain = data.chain;
|
|
361
|
-
|
|
362
|
-
(
|
|
363
|
-
|
|
598
|
+
const imported = [
|
|
599
|
+
ansi.brightGreen(` Imported ${owner}/${repo}#${prNumber}` +
|
|
600
|
+
(stack?.id ? ` → ${stack.id}` : "") +
|
|
601
|
+
(Array.isArray(chain) ? ` (${chain.length} layer(s))` : "")),
|
|
602
|
+
];
|
|
364
603
|
if (stack?.trunkBranch) {
|
|
365
|
-
|
|
604
|
+
imported.push(ansi.dim(` trunk: ${stack.trunkBranch}`));
|
|
366
605
|
}
|
|
367
|
-
|
|
606
|
+
imported.push(ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"));
|
|
607
|
+
await present("Stack", imported);
|
|
368
608
|
}
|
|
369
609
|
async function cmdStackRestack(args) {
|
|
370
610
|
const asJson = args.includes("--json");
|
|
@@ -376,7 +616,9 @@ async function cmdStackRestack(args) {
|
|
|
376
616
|
return;
|
|
377
617
|
}
|
|
378
618
|
const fromBranch = body.fromBranch;
|
|
379
|
-
|
|
619
|
+
await present("Stack", [
|
|
620
|
+
ansi.brightGreen(` Restack started` + (fromBranch ? ` from ${fromBranch}` : "") + ` (${id})`),
|
|
621
|
+
]);
|
|
380
622
|
}
|
|
381
623
|
async function cmdStackLand(args) {
|
|
382
624
|
const asJson = args.includes("--json");
|
|
@@ -392,39 +634,31 @@ async function cmdStackLand(args) {
|
|
|
392
634
|
const branch = body.mergedBranch;
|
|
393
635
|
const uNumber = body.uNumber;
|
|
394
636
|
if (typeof uNumber === "number" && uNumber > 0) {
|
|
395
|
-
|
|
396
|
-
(
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
637
|
+
await present("Stack", [
|
|
638
|
+
ansi.brightGreen(` Promoted` +
|
|
639
|
+
(pr != null ? ` #${pr}` : "") +
|
|
640
|
+
` into U${uNumber}` +
|
|
641
|
+
(branch ? ` (${branch})` : "") +
|
|
642
|
+
` on ${id}`),
|
|
643
|
+
]);
|
|
400
644
|
return;
|
|
401
645
|
}
|
|
402
|
-
|
|
403
|
-
(
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
const asJson = args.includes("--json");
|
|
409
|
-
const filtered = args.filter((a) => a !== "--json");
|
|
410
|
-
const mode = filtered[0]?.toLowerCase();
|
|
411
|
-
const id = requireStackId(filtered[1], "usage: mergestorm stack auto-promote on|off <stack-id>");
|
|
412
|
-
if (mode !== "on" && mode !== "off") {
|
|
413
|
-
throw new CommandError("usage: mergestorm stack auto-promote on|off <stack-id>");
|
|
414
|
-
}
|
|
415
|
-
const cfg = await loadConfig();
|
|
416
|
-
const body = await setStackAutoPromote(id, mode === "on", cfg);
|
|
417
|
-
if (asJson) {
|
|
418
|
-
console.log(JSON.stringify(body, null, 2));
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
console.log(ansi.green(` Auto-promote ${mode} for ${id}`));
|
|
646
|
+
await present("Stack", [
|
|
647
|
+
ansi.brightGreen(` Landed` +
|
|
648
|
+
(pr != null ? ` #${pr}` : "") +
|
|
649
|
+
(branch ? ` (${branch})` : "") +
|
|
650
|
+
` on ${id}`),
|
|
651
|
+
]);
|
|
422
652
|
}
|
|
423
653
|
export async function cmdStack(args) {
|
|
424
654
|
const sub = args[0]?.toLowerCase();
|
|
425
655
|
const rest = args.slice(1);
|
|
426
656
|
// Help must exit 0 — print usage, do not throw CommandError.
|
|
427
657
|
if (!sub || sub === "-h" || sub === "--help" || sub === "help") {
|
|
658
|
+
if (canBrowse()) {
|
|
659
|
+
await openHelpBrowser("stacks");
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
428
662
|
console.log(STACK_USAGE);
|
|
429
663
|
return;
|
|
430
664
|
}
|
|
@@ -442,11 +676,5 @@ export async function cmdStack(args) {
|
|
|
442
676
|
return cmdStackRestack(rest);
|
|
443
677
|
if (sub === "land" || sub === "land-next")
|
|
444
678
|
return cmdStackLand(rest);
|
|
445
|
-
if (sub === "auto-promote" ||
|
|
446
|
-
sub === "autopromote" ||
|
|
447
|
-
sub === "auto-land" ||
|
|
448
|
-
sub === "autoland") {
|
|
449
|
-
return cmdStackAutoLand(rest);
|
|
450
|
-
}
|
|
451
679
|
throw new CommandError(`${STACK_USAGE}\nunknown stack subcommand: ${sub}`);
|
|
452
680
|
}
|