mergestorm 0.3.9 → 0.3.13

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.
Files changed (43) hide show
  1. package/README.md +57 -13
  2. package/dist/api.d.ts +27 -38
  3. package/dist/api.js +81 -4
  4. package/dist/client/index.d.ts +5 -0
  5. package/dist/client/index.js +5 -0
  6. package/dist/commands/credits.js +33 -12
  7. package/dist/commands/login.js +28 -7
  8. package/dist/commands/registry.d.ts +1 -1
  9. package/dist/commands/registry.js +26 -19
  10. package/dist/commands/review-client.d.ts +93 -0
  11. package/dist/commands/review-client.js +289 -0
  12. package/dist/commands/review.d.ts +30 -6
  13. package/dist/commands/review.js +385 -146
  14. package/dist/commands/stack.d.ts +34 -2
  15. package/dist/commands/stack.js +252 -58
  16. package/dist/commands/status.d.ts +16 -2
  17. package/dist/commands/status.js +221 -21
  18. package/dist/errors.d.ts +18 -2
  19. package/dist/errors.js +22 -1
  20. package/dist/git-stack.d.ts +2 -0
  21. package/dist/git-stack.js +4 -0
  22. package/dist/git.d.ts +12 -1
  23. package/dist/git.js +52 -7
  24. package/dist/shell.d.ts +1 -1
  25. package/dist/shell.js +6 -3
  26. package/dist/stack-dto.d.ts +185 -0
  27. package/dist/stack-dto.js +7 -0
  28. package/dist/submit-pr-base.d.ts +31 -0
  29. package/dist/submit-pr-base.js +69 -0
  30. package/dist/ui/banner.js +7 -0
  31. package/dist/ui/box.d.ts +13 -9
  32. package/dist/ui/box.js +28 -44
  33. package/dist/ui/findings.d.ts +2 -0
  34. package/dist/ui/findings.js +2 -1
  35. package/dist/ui/job-envelope.d.ts +68 -0
  36. package/dist/ui/job-envelope.js +35 -0
  37. package/dist/ui/prompt.d.ts +91 -0
  38. package/dist/ui/prompt.js +241 -62
  39. package/dist/ui/usage.d.ts +25 -1
  40. package/dist/ui/usage.js +50 -1
  41. package/dist/ui/width.d.ts +26 -0
  42. package/dist/ui/width.js +262 -0
  43. package/package.json +5 -1
@@ -1,16 +1,17 @@
1
- import { adoptStack, landNextStack, listStacks, restackStack, setStackAutoPromote, } from "../api.js";
1
+ import { adoptStack, ensureUpperPark, landNextStack, listStacks, restackStack, setStackAutoPromote, } 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";
11
12
  const STACK_USAGE = `usage:
12
- mergestorm stack create [name] [--onto <branch>] [--trunk <branch>] [--json]
13
- mergestorm stack submit [--json]
13
+ mergestorm stack create [name] [--onto <branch>] [--trunk <branch>] [--extend] [--json]
14
+ mergestorm stack submit [--extend] [--json]
14
15
  mergestorm stack reset --force
15
16
  mergestorm stack list [--json]
16
17
  mergestorm stack adopt <owner/repo>#<pr>
@@ -19,8 +20,55 @@ const STACK_USAGE = `usage:
19
20
  mergestorm stack auto-promote on|off <stack-id> [--json]
20
21
 
21
22
  stack submit opens PRs with a body generated from each layer tip commit.
22
- stack land promotes into the review unit when one exists; otherwise lands
23
- the bottom open PR (unit-less stacks).`;
23
+ Layers 1–2 use the git parent as GitHub base. Layer 3+ opens onto the
24
+ review-unit park freeze (mg-park-*) so adopt does not retarget after open.
25
+ Parenting onto a branch that already belongs to a registered (submitted)
26
+ stack requires --extend on create and submit — otherwise mg starts a new
27
+ stack from trunk. stack land promotes into the review unit when one exists;
28
+ otherwise lands the bottom open PR (unit-less stacks).`;
29
+ /** Cap the registered-stack guard's API probe so it cannot stall local authoring. */
30
+ const STACK_GUARD_TIMEOUT_MS = 8_000;
31
+ /** Find a registered stack layer whose branch matches `parentBranch`. */
32
+ export function findRegisteredParent(stacks, parentBranch, owner, repo) {
33
+ const want = parentBranch.trim();
34
+ if (!want)
35
+ return null;
36
+ for (const s of stacks) {
37
+ if (owner !== undefined && repo !== undefined) {
38
+ if (s.owner !== owner || s.repo !== repo)
39
+ continue;
40
+ }
41
+ for (const layer of s.layers) {
42
+ if (layer.branch === want) {
43
+ return {
44
+ stackId: s.id,
45
+ owner: s.owner,
46
+ repo: s.repo,
47
+ branch: layer.branch,
48
+ prNumber: layer.prNumber,
49
+ position: layer.position,
50
+ };
51
+ }
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+ /**
57
+ * Refuse silently growing an already-registered stack.
58
+ * Local unsubmitted parents are fine; only API-listed layers trip this.
59
+ */
60
+ export function assertMayParentOntoRegistered(parentBranch, stacks, extend, verb, owner, repo) {
61
+ const hit = findRegisteredParent(stacks, parentBranch, owner, repo);
62
+ if (!hit)
63
+ return null;
64
+ if (extend)
65
+ return hit;
66
+ const pr = hit.prNumber > 0 ? `PR #${hit.prNumber}` : "an open PR";
67
+ throw new CommandError(`Parent \`${hit.branch}\` is already layer ${hit.position} of registered stack ${hit.stackId} (${hit.owner}/${hit.repo}, ${pr}).\n` +
68
+ `Refusing to ${verb} a new layer onto that stack without an explicit opt-in.\n` +
69
+ `• New independent stack: check out trunk and pass \`--onto <trunk>\` (e.g. \`mg stack create --onto main\`).\n` +
70
+ `• Intentionally grow that stack: pass \`--extend\` on \`stack ${verb}\`.`, 1, "registered_parent");
71
+ }
24
72
  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
73
  export function requireStackId(raw, usage) {
26
74
  const id = raw?.trim() ?? "";
@@ -70,6 +118,7 @@ export function parseStackCreateArgs(args) {
70
118
  let name;
71
119
  let onto;
72
120
  let trunk;
121
+ let extend = false;
73
122
  let asJson = false;
74
123
  for (let i = 0; i < args.length; i++) {
75
124
  const a = args[i];
@@ -77,10 +126,15 @@ export function parseStackCreateArgs(args) {
77
126
  asJson = true;
78
127
  continue;
79
128
  }
129
+ if (a === "--extend") {
130
+ extend = true;
131
+ continue;
132
+ }
80
133
  if (a === "--onto") {
81
134
  const v = args[++i];
82
- if (!v)
83
- throw new CommandError("usage: mergestorm stack create [name] [--onto <branch>]");
135
+ if (!v) {
136
+ throw new CommandError("usage: mergestorm stack create [name] [--onto <branch>] [--trunk <branch>] [--extend]");
137
+ }
84
138
  onto = v;
85
139
  continue;
86
140
  }
@@ -90,8 +144,9 @@ export function parseStackCreateArgs(args) {
90
144
  }
91
145
  if (a === "--trunk") {
92
146
  const v = args[++i];
93
- if (!v)
94
- throw new CommandError("usage: mergestorm stack create [name] [--trunk <branch>]");
147
+ if (!v) {
148
+ throw new CommandError("usage: mergestorm stack create [name] [--onto <branch>] [--trunk <branch>] [--extend]");
149
+ }
95
150
  trunk = v;
96
151
  continue;
97
152
  }
@@ -103,20 +158,40 @@ export function parseStackCreateArgs(args) {
103
158
  throw new CommandError(`${STACK_USAGE}\nunknown flag: ${a}`);
104
159
  }
105
160
  if (name != null) {
106
- throw new CommandError("usage: mergestorm stack create [name] [--onto <branch>] [--trunk <branch>]");
161
+ throw new CommandError("usage: mergestorm stack create [name] [--onto <branch>] [--trunk <branch>] [--extend]");
107
162
  }
108
163
  name = a;
109
164
  }
110
- return { name, onto, trunk, asJson };
165
+ return { name, onto, trunk, extend, asJson };
166
+ }
167
+ /** Parse `stack submit` argv. Exported for tests. */
168
+ export function parseStackSubmitArgs(args) {
169
+ let extend = false;
170
+ let asJson = false;
171
+ for (const a of args) {
172
+ if (a === "--json") {
173
+ asJson = true;
174
+ continue;
175
+ }
176
+ if (a === "--extend") {
177
+ extend = true;
178
+ continue;
179
+ }
180
+ throw new CommandError("usage: mergestorm stack submit [--extend] [--json]");
181
+ }
182
+ return { extend, asJson };
111
183
  }
112
184
  export function parseStackResetArgs(args) {
113
185
  if (args.length !== 1 || args[0] !== "--force") {
114
186
  throw new CommandError("usage: mergestorm stack reset --force\nClears only CLI-managed pre-submit state; branches, PRs, and registered stacks are unchanged.");
115
187
  }
116
188
  }
117
- async function cmdStackCreate(args) {
118
- const { name, onto, trunk: trunkFlag, asJson } = parseStackCreateArgs(args);
119
- const cwd = process.cwd();
189
+ async function cmdStackCreate(args, deps = {}) {
190
+ const { name, onto, trunk: trunkFlag, extend, asJson } = parseStackCreateArgs(args);
191
+ const cwd = deps.cwd ?? process.cwd();
192
+ const listStacksFn = deps.listStacks ?? listStacks;
193
+ const loadConfigFn = deps.loadConfig ?? loadConfig;
194
+ const parseOriginFn = deps.parseGithubOriginRepo ?? parseGithubOriginRepo;
120
195
  try {
121
196
  gitTopLevel(cwd);
122
197
  }
@@ -145,6 +220,37 @@ async function cmdStackCreate(args) {
145
220
  else if (trunkFlag?.trim() && meta.trunk !== trunk) {
146
221
  throw new CommandError(`Stack trunk is ${meta.trunk}; refusing --trunk ${trunk}. Run \`mg stack reset --force\` to discard local pre-submit state.`);
147
222
  }
223
+ // --trunk only sets metadata; parent defaults to current branch. Call that out
224
+ // when someone passes --trunk while sitting on a different branch without --onto.
225
+ if (trunkFlag?.trim() && !onto?.trim() && parentBranch !== trunk) {
226
+ console.error(ansi.dim(` note: --trunk ${trunk} does not change the parent; new layer parents onto \`${parentBranch}\`. ` +
227
+ `Pass \`--onto ${trunk}\` for a fresh stack from trunk.`));
228
+ }
229
+ // Block silent attach onto an already-submitted stack tip (requires --extend).
230
+ // --trunk is user metadata, so a registered tip must not be able to masquerade
231
+ // as trunk — always consult the stacks API rather than trusting parent === trunk.
232
+ // Best-effort: API/key failures degrade to a warning so create stays local.
233
+ const origin = parseOriginFn(cwd);
234
+ let registeredHit = null;
235
+ if (origin) {
236
+ try {
237
+ const cfg = await loadConfigFn();
238
+ const registered = await listStacksFn(cfg, { timeoutMs: STACK_GUARD_TIMEOUT_MS });
239
+ registeredHit = assertMayParentOntoRegistered(parentBranch, registered, extend, "create", origin.owner, origin.repo);
240
+ }
241
+ catch (err) {
242
+ if (err instanceof CommandError && err.code === "registered_parent")
243
+ throw err;
244
+ console.error(ansi.dim(` warning: could not check registered stacks (${err instanceof Error ? err.message : String(err)}); ` +
245
+ `skipping --extend guard. Pass --extend if you intend to grow an open stack.`));
246
+ }
247
+ }
248
+ else {
249
+ 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."));
250
+ }
251
+ if (parentBranch === trunk && extend && !registeredHit) {
252
+ throw new CommandError("`--extend` is only valid when parenting onto a registered stack layer, not trunk. Omit --extend (or pass --onto <tip> --extend).");
253
+ }
148
254
  try {
149
255
  meta = selectStackForCreate(meta, parentBranch);
150
256
  }
@@ -174,6 +280,7 @@ async function cmdStackCreate(args) {
174
280
  branch,
175
281
  parentBranch,
176
282
  trunk: meta.trunk,
283
+ extend: Boolean(registeredHit),
177
284
  active: meta.active,
178
285
  stacks: meta.stacks.length,
179
286
  layers,
@@ -182,14 +289,15 @@ async function cmdStackCreate(args) {
182
289
  return;
183
290
  }
184
291
  console.log(ansi.green(` Created ${branch} (onto ${parentBranch})`));
292
+ if (registeredHit) {
293
+ console.log(ansi.dim(` extending registered stack ${registeredHit.stackId}` +
294
+ (registeredHit.prNumber > 0 ? ` (from #${registeredHit.prNumber})` : "")));
295
+ }
185
296
  console.log(ansi.dim(` trunk: ${meta.trunk} · local stacks: ${meta.stacks.length} · active layers: ${layers.length}`));
186
297
  console.log(ansi.dim(" authoring state: managed automatically outside the repository"));
187
298
  }
188
299
  export async function cmdStackSubmit(args, deps = {}) {
189
- const asJson = args.includes("--json");
190
- if (args.some((a) => a !== "--json")) {
191
- throw new CommandError("usage: mergestorm stack submit [--json]");
192
- }
300
+ const { extend, asJson } = parseStackSubmitArgs(args);
193
301
  const cwd = deps.cwd ?? process.cwd();
194
302
  const gitTopLevelFn = deps.gitTopLevel ?? gitTopLevel;
195
303
  const loadStackMetaFn = deps.loadStackMeta ?? loadStackMeta;
@@ -197,6 +305,7 @@ export async function cmdStackSubmit(args, deps = {}) {
197
305
  const parseOriginFn = deps.parseGithubOriginRepo ?? parseGithubOriginRepo;
198
306
  const requireGhFn = deps.requireGh ?? requireGh;
199
307
  const branchExistsFn = deps.branchExists ?? branchExists;
308
+ const fetchRemoteBranchFn = deps.fetchRemoteBranch ?? fetchRemoteBranch;
200
309
  const pushBranchFn = deps.pushBranch ?? pushBranch;
201
310
  const findOpenPrNumberFn = deps.findOpenPrNumber ?? findOpenPrNumber;
202
311
  const commitsAheadOfFn = deps.commitsAheadOf ?? commitsAheadOf;
@@ -205,7 +314,9 @@ export async function cmdStackSubmit(args, deps = {}) {
205
314
  const buildPrBodyFn = deps.buildPrBodyFromCommit ?? buildPrBodyFromCommit;
206
315
  const createPrFn = deps.createPr ?? createPr;
207
316
  const loadConfigFn = deps.loadConfig ?? loadConfig;
317
+ const listStacksFn = deps.listStacks ?? listStacks;
208
318
  const adoptStackFn = deps.adoptStack ?? adoptStack;
319
+ const ensureUpperParkFn = deps.ensureUpperPark ?? ensureUpperPark;
209
320
  try {
210
321
  gitTopLevelFn(cwd);
211
322
  }
@@ -223,9 +334,41 @@ export async function cmdStackSubmit(args, deps = {}) {
223
334
  }
224
335
  const { owner, repo } = origin;
225
336
  requireGhFn(cwd);
226
- const opened = [];
337
+ // Safety net: refuse submit that would open/register onto a registered tip
338
+ // without --extend. Check every layer's parent (mid-stack layers from older
339
+ // local state must not sail through), scoped to this repo, and fail closed
340
+ // when the API cannot be consulted — otherwise the attach happens anyway.
341
+ // meta.trunk is user metadata, so it must not exempt a registered tip from
342
+ // the check (a tip can masquerade as trunk via --trunk at create).
343
+ const parents = [...new Set(layers.map((l) => l.parentBranch || meta.trunk))];
344
+ let registeredHits = 0;
345
+ let registered = [];
346
+ try {
347
+ const cfg = await loadConfigFn();
348
+ registered = await listStacksFn(cfg, { timeoutMs: STACK_GUARD_TIMEOUT_MS });
349
+ for (const parent of parents) {
350
+ if (assertMayParentOntoRegistered(parent, registered, extend, "submit", owner, repo)) {
351
+ registeredHits += 1;
352
+ }
353
+ }
354
+ }
355
+ catch (err) {
356
+ if (err instanceof CommandError && err.code === "registered_parent")
357
+ throw err;
358
+ throw new CommandError(`Could not verify that the stack parents onto registered stacks (${err instanceof Error ? err.message : String(err)}); ` +
359
+ `refusing to submit. If a layer grows a registered stack, pass --extend (retry when the API is reachable).`);
360
+ }
361
+ if (extend && registeredHits === 0 && parents.every((p) => p === meta.trunk)) {
362
+ throw new CommandError("`--extend` is only valid when a layer parents onto a registered stack layer, not trunk.");
363
+ }
364
+ const plans = planSubmitLayerBases({
365
+ layers,
366
+ trunk: meta.trunk,
367
+ registered,
368
+ owner,
369
+ repo,
370
+ });
227
371
  for (const layer of layers) {
228
- const base = layer.parentBranch || meta.trunk;
229
372
  if (!branchExistsFn(layer.branch, cwd)) {
230
373
  throw new CommandError(`Stack layer branch missing locally: ${layer.branch}`);
231
374
  }
@@ -236,58 +379,109 @@ export async function cmdStackSubmit(args, deps = {}) {
236
379
  catch (err) {
237
380
  throw new CommandError(err instanceof Error ? err.message : String(err));
238
381
  }
239
- let prNumber = findOpenPrNumberFn(owner, repo, layer.branch, cwd);
240
- let created = false;
241
- if (prNumber == null) {
242
- // Refuse empty layers before `gh pr create` dumps opaque stderr.
243
- if (!branchExistsFn(base, cwd)) {
244
- throw new CommandError(`Stack base branch missing locally: ${base}`);
245
- }
246
- let ahead;
382
+ }
383
+ const opened = [];
384
+ const pendingPark = plans.filter((plan) => {
385
+ if (findOpenPrNumberFn(owner, repo, plan.branch, cwd) != null)
386
+ return false;
387
+ return plan.githubBase == null;
388
+ });
389
+ const openLayerPr = (branch, base) => {
390
+ const existing = findOpenPrNumberFn(owner, repo, branch, cwd);
391
+ if (existing != null) {
392
+ console.log(ansi.dim(` PR #${existing} already open for ${branch}`));
393
+ return { prNumber: existing, created: false };
394
+ }
395
+ const aheadRef = isMgParkBranch(base) ? `origin/${base}` : base;
396
+ if (isMgParkBranch(base)) {
247
397
  try {
248
- ahead = commitsAheadOfFn(base, layer.branch, cwd);
398
+ fetchRemoteBranchFn(base, cwd);
249
399
  }
250
400
  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\`.`);
401
+ throw new CommandError(`Could not fetch park base \`${base}\` (${err instanceof Error ? err.message : String(err)}).`);
256
402
  }
257
- const title = tipCommitSubjectFn(layer.branch, cwd);
258
- const body = buildPrBodyFn(tipCommitMessageFn(layer.branch, cwd));
259
- console.log(ansi.dim(` Opening PR ${layer.branch} → ${base} …`));
260
- prNumber = createPrFn({
403
+ }
404
+ else if (!branchExistsFn(base, cwd)) {
405
+ throw new CommandError(`Stack base branch missing locally: ${base}`);
406
+ }
407
+ let ahead;
408
+ try {
409
+ ahead = commitsAheadOfFn(aheadRef, branch, cwd);
410
+ }
411
+ catch (err) {
412
+ throw new CommandError(err instanceof Error ? err.message : String(err));
413
+ }
414
+ if (ahead < 1) {
415
+ throw new CommandError(`No commits on \`${branch}\` ahead of \`${base}\`. ` +
416
+ `Commit your changes, then retry \`mg stack submit\`.`);
417
+ }
418
+ const title = tipCommitSubjectFn(branch, cwd);
419
+ const body = buildPrBodyFn(tipCommitMessageFn(branch, cwd));
420
+ console.log(ansi.dim(` Opening PR ${branch} → ${base} …`));
421
+ return {
422
+ prNumber: createPrFn({
261
423
  owner,
262
424
  repo,
263
425
  base,
264
- head: layer.branch,
426
+ head: branch,
265
427
  title,
266
428
  body,
267
429
  cwd,
268
- });
269
- created = true;
270
- }
271
- else {
272
- console.log(ansi.dim(` PR #${prNumber} already open for ${layer.branch}`));
430
+ }),
431
+ created: true,
432
+ };
433
+ };
434
+ for (const plan of plans) {
435
+ if (plan.githubBase == null && findOpenPrNumberFn(owner, repo, plan.branch, cwd) == null) {
436
+ continue;
273
437
  }
274
- opened.push({ branch: layer.branch, base, prNumber, created });
438
+ const base = plan.githubBase ?? plan.gitParent;
439
+ const { prNumber, created } = openLayerPr(plan.branch, base);
440
+ opened.push({ branch: plan.branch, base, prNumber, created });
275
441
  }
276
- const registerPr = opened[0].prNumber;
277
442
  const cfg = await loadConfigFn();
278
- console.log(ansi.dim(` Registering stack via import (${owner}/${repo}#${registerPr}) …`));
279
- const body = await adoptStackFn(owner, repo, registerPr, cfg);
280
- if (typeof body !== "object" || body === null) {
281
- throw new CommandError("Unexpected response shape from adopt API");
282
- }
283
- const data = body;
284
- if (data.error) {
285
- throw new CommandError(data.error);
443
+ let adoptData = null;
444
+ let stackId = plans.find((plan) => plan.stackId)?.stackId ?? null;
445
+ const registerPr = async (prNumber) => {
446
+ console.log(ansi.dim(` Registering stack via import (${owner}/${repo}#${prNumber}) …`));
447
+ const body = await adoptStackFn(owner, repo, prNumber, cfg);
448
+ if (typeof body !== "object" || body === null) {
449
+ throw new CommandError("Unexpected response shape from adopt API");
450
+ }
451
+ const data = body;
452
+ if (data.error) {
453
+ throw new CommandError(data.error);
454
+ }
455
+ if (!data.stack) {
456
+ throw new CommandError("Stack was not registered");
457
+ }
458
+ adoptData = data;
459
+ if (typeof data.stack.id === "string" && data.stack.id) {
460
+ stackId = data.stack.id;
461
+ }
462
+ };
463
+ if (pendingPark.length > 0) {
464
+ if (opened.length > 0) {
465
+ await registerPr(opened[0].prNumber);
466
+ }
467
+ if (!stackId) {
468
+ throw new CommandError("Cannot mint a park freeze before the stack is registered. Open layers 1–2 first, then retry.");
469
+ }
470
+ console.log(ansi.dim(` Ensuring upper-park freeze for stack ${stackId} …`));
471
+ const park = await ensureUpperParkFn(stackId, cfg);
472
+ for (const plan of pendingPark) {
473
+ const { prNumber, created } = openLayerPr(plan.branch, park.freezeBranch);
474
+ opened.push({ branch: plan.branch, base: park.freezeBranch, prNumber, created });
475
+ }
476
+ await registerPr(opened[opened.length - 1].prNumber);
286
477
  }
287
- if (!data.stack) {
288
- throw new CommandError("Stack was not registered");
478
+ else {
479
+ if (opened.length === 0) {
480
+ throw new CommandError("No pull requests to register after submit.");
481
+ }
482
+ await registerPr(opened[0].prNumber);
289
483
  }
290
- const stackId = data.stack.id;
484
+ const data = adoptData;
291
485
  // Drop the submitted local stack so the next create onto trunk starts fresh.
292
486
  await saveStackMetaFn(removeActiveStack(meta), cwd);
293
487
  if (asJson) {
@@ -1,5 +1,19 @@
1
+ export type ParsedStatusArgs = {
2
+ jobId: string;
3
+ format: "json" | "pretty";
4
+ wait: boolean;
5
+ timeoutMs: number;
6
+ };
1
7
  export type StatusOptions = {
2
- format?: "json" | "pretty";
8
+ /** Oneshot defaults to json; the shell defaults to pretty. */
9
+ defaultFormat?: "json" | "pretty";
3
10
  signal?: AbortSignal;
11
+ /** When true, Ctrl+C / per-request timeout detaches instead of exiting 5. */
12
+ interactive?: boolean;
13
+ /** Test seam; production uses the two-second poll interval. */
14
+ pollIntervalMs?: number;
4
15
  };
5
- export declare function cmdStatus(jobId: string, opts?: StatusOptions): Promise<void>;
16
+ export declare function parseStatusArgs(args: string[], defaults?: {
17
+ format: "json" | "pretty";
18
+ }): ParsedStatusArgs;
19
+ export declare function cmdStatus(args: string[], opts?: StatusOptions): Promise<void>;