mergestorm 0.3.15 → 0.3.17
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 -4
- package/dist/api.d.ts +44 -4
- package/dist/api.js +55 -12
- package/dist/automation-catalog.d.ts +37 -0
- package/dist/automation-catalog.js +13 -0
- package/dist/client/index.d.ts +4 -2
- package/dist/client/index.js +4 -2
- package/dist/commands/browse.d.ts +2 -2
- package/dist/commands/browse.js +7 -11
- package/dist/commands/credits.js +1 -0
- package/dist/commands/pr.d.ts +4 -0
- package/dist/commands/pr.js +32 -5
- package/dist/commands/registry.js +7 -4
- package/dist/commands/review-client.d.ts +10 -3
- package/dist/commands/review-client.js +75 -24
- package/dist/commands/review.js +17 -14
- package/dist/commands/settings.d.ts +2 -2
- package/dist/commands/settings.js +2 -9
- package/dist/commands/stack.d.ts +55 -2
- package/dist/commands/stack.js +274 -37
- package/dist/commands/status.js +4 -2
- package/dist/commands/whoami.js +9 -9
- package/dist/git.js +1 -1
- package/dist/repo-identity.d.ts +1 -1
- package/dist/repo-identity.js +13 -2
- package/dist/stack-dto.d.ts +55 -2
- package/dist/stack-dto.js +0 -6
- package/dist/stack-meta.d.ts +12 -0
- package/dist/stack-meta.js +32 -3
- package/dist/ui/banner.js +6 -0
- package/dist/ui/human-summary.d.ts +2 -0
- package/dist/ui/human-summary.js +153 -0
- package/dist/ui/usage.d.ts +1 -2
- package/dist/ui/usage.js +3 -4
- package/package.json +1 -1
- package/skill/SKILL.md +3 -3
- package/skill/mergestorm-pr-loop/SKILL.md +36 -21
package/dist/commands/stack.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { adoptStack, ensureUpperPark, landNextStack, listStacks, restackStack, } from "../api.js";
|
|
1
|
+
import { adoptStack, ensureUpperPark, landNextStack, listStacks, restackStack, setStackPolicy, } 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";
|
|
@@ -6,7 +6,7 @@ import { parseGithubOriginRepo } from "../git.js";
|
|
|
6
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
|
-
import { activeLayers, appendLayer, emptyStackMeta, loadStackMeta, removeActiveStack, resetStackMeta, saveStackMeta, selectStackForCreate, } from "../stack-meta.js";
|
|
9
|
+
import { activeLayers, appendLayer, emptyStackMeta, loadStackMeta, removeActiveStack, resetStackMeta, saveStackMeta, selectStackForCreate, setActivePolicy, } from "../stack-meta.js";
|
|
10
10
|
import { isMgParkBranch, planSubmitLayerBases } from "../submit-pr-base.js";
|
|
11
11
|
import { ansi } from "../ui/ansi.js";
|
|
12
12
|
import { runLineTabsBrowser } from "../ui/line-tabs.js";
|
|
@@ -14,14 +14,18 @@ import { present } from "../ui/present.js";
|
|
|
14
14
|
import { canBrowse } from "./browse.js";
|
|
15
15
|
import { openHelpBrowser } from "./help.js";
|
|
16
16
|
const STACK_USAGE = `usage:
|
|
17
|
-
mergestorm stack create [name] [--onto <branch>] [--trunk <branch>] [--extend] [--json]
|
|
18
|
-
mergestorm stack submit [--extend] [--json]
|
|
17
|
+
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]
|
|
19
19
|
mergestorm stack reset --force
|
|
20
20
|
mergestorm stack list [--json]
|
|
21
|
-
mergestorm stack
|
|
21
|
+
mergestorm stack set <stack-id> [--auto-land on|off] [--auto-review on|off|default] [--auto-patch on|off|default] [--json]
|
|
22
|
+
mergestorm stack adopt <owner/repo>#<pr> [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--json]
|
|
22
23
|
mergestorm stack restack <stack-id> [--json]
|
|
23
24
|
mergestorm stack land <stack-id> [--json]
|
|
24
25
|
|
|
26
|
+
--auto-review / --auto-patch pin Vortex auto-review / Cyclone auto-patch for
|
|
27
|
+
one stack in either direction. Absent, or \`default\` on stack set, follows
|
|
28
|
+
the account setting.
|
|
25
29
|
stack submit opens PRs with a body generated from each layer tip commit.
|
|
26
30
|
Layers 1–2 use the git parent as GitHub base. Layer 3+ opens onto the
|
|
27
31
|
review-unit park freeze (mg-park-*) so adopt does not retarget after open.
|
|
@@ -126,9 +130,25 @@ export function buildStackListLines(stacks) {
|
|
|
126
130
|
lines.push(ansi.dim(" create · submit · land <id> · q to close"));
|
|
127
131
|
return lines;
|
|
128
132
|
}
|
|
133
|
+
/** Short per-stack policy suffixes: Auto land when on, overrides when pinned. */
|
|
134
|
+
export function stackPolicyLabels(s) {
|
|
135
|
+
const labels = [];
|
|
136
|
+
if (s.autoEnqueueWhenReady)
|
|
137
|
+
labels.push("auto-land on");
|
|
138
|
+
if (typeof s.autoReviewOverride === "boolean") {
|
|
139
|
+
labels.push(`auto-review ${s.autoReviewOverride ? "on" : "off"}`);
|
|
140
|
+
}
|
|
141
|
+
if (typeof s.autoPatchOverride === "boolean") {
|
|
142
|
+
labels.push(`auto-patch ${s.autoPatchOverride ? "on" : "off"}`);
|
|
143
|
+
}
|
|
144
|
+
return labels;
|
|
145
|
+
}
|
|
129
146
|
function formatStackHuman(s) {
|
|
130
147
|
const lines = [];
|
|
131
|
-
lines.push(` ${ansi.bold(`${s.owner}/${s.repo}`)} trunk=${s.trunkBranch}`
|
|
148
|
+
lines.push(` ${ansi.bold(`${s.owner}/${s.repo}`)} trunk=${s.trunkBranch}` +
|
|
149
|
+
stackPolicyLabels(s)
|
|
150
|
+
.map((label) => ` ${label}`)
|
|
151
|
+
.join(""));
|
|
132
152
|
lines.push(` ${ansi.dim(s.id)}`);
|
|
133
153
|
if (s.layers.length === 0) {
|
|
134
154
|
lines.push(ansi.dim(" (no layers)"));
|
|
@@ -141,12 +161,45 @@ function formatStackHuman(s) {
|
|
|
141
161
|
}
|
|
142
162
|
return lines;
|
|
143
163
|
}
|
|
164
|
+
const OPEN_POLICY_FLAGS = [
|
|
165
|
+
{ flag: "--auto-land", key: "autoLand" },
|
|
166
|
+
{ flag: "--auto-review", key: "autoReview" },
|
|
167
|
+
{ flag: "--auto-patch", key: "autoPatch" },
|
|
168
|
+
];
|
|
169
|
+
/**
|
|
170
|
+
* Consume one `--auto-* on|off` flag at `args[i]` (space or `=` form). Returns
|
|
171
|
+
* the next index to scan, or null when `args[i]` is not a policy flag.
|
|
172
|
+
*/
|
|
173
|
+
function takeOpenPolicyFlag(args, i, into, verb) {
|
|
174
|
+
const a = args[i];
|
|
175
|
+
for (const { flag, key } of OPEN_POLICY_FLAGS) {
|
|
176
|
+
if (a === flag) {
|
|
177
|
+
into[key] = parseOnOff(args[i + 1], flag, verb);
|
|
178
|
+
return i + 2;
|
|
179
|
+
}
|
|
180
|
+
if (a.startsWith(`${flag}=`)) {
|
|
181
|
+
into[key] = parseOnOff(a.slice(flag.length + 1), flag, verb);
|
|
182
|
+
return i + 1;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
/** Wire policy for adopt from the parsed `on|off` flags; absent keys stay off the body. */
|
|
188
|
+
export function openPolicyPatch(flags) {
|
|
189
|
+
const patch = {
|
|
190
|
+
...(typeof flags.autoLand === "boolean" ? { autoEnqueueWhenReady: flags.autoLand } : {}),
|
|
191
|
+
...(typeof flags.autoReview === "boolean" ? { autoReviewOverride: flags.autoReview } : {}),
|
|
192
|
+
...(typeof flags.autoPatch === "boolean" ? { autoPatchOverride: flags.autoPatch } : {}),
|
|
193
|
+
};
|
|
194
|
+
return Object.keys(patch).length > 0 ? patch : undefined;
|
|
195
|
+
}
|
|
144
196
|
/** Parse `stack create` argv into options. Exported for tests. */
|
|
145
197
|
export function parseStackCreateArgs(args) {
|
|
146
198
|
let name;
|
|
147
199
|
let onto;
|
|
148
200
|
let trunk;
|
|
149
201
|
let extend = false;
|
|
202
|
+
const policy = {};
|
|
150
203
|
let asJson = false;
|
|
151
204
|
for (let i = 0; i < args.length; i++) {
|
|
152
205
|
const a = args[i];
|
|
@@ -158,6 +211,11 @@ export function parseStackCreateArgs(args) {
|
|
|
158
211
|
extend = true;
|
|
159
212
|
continue;
|
|
160
213
|
}
|
|
214
|
+
const next = takeOpenPolicyFlag(args, i, policy, "stack create");
|
|
215
|
+
if (next !== null) {
|
|
216
|
+
i = next - 1;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
161
219
|
if (a === "--onto") {
|
|
162
220
|
const v = args[++i];
|
|
163
221
|
if (!v) {
|
|
@@ -190,13 +248,24 @@ export function parseStackCreateArgs(args) {
|
|
|
190
248
|
}
|
|
191
249
|
name = a;
|
|
192
250
|
}
|
|
193
|
-
return {
|
|
251
|
+
return {
|
|
252
|
+
name,
|
|
253
|
+
onto,
|
|
254
|
+
trunk,
|
|
255
|
+
extend,
|
|
256
|
+
autoLand: policy.autoLand,
|
|
257
|
+
autoReview: policy.autoReview,
|
|
258
|
+
autoPatch: policy.autoPatch,
|
|
259
|
+
asJson,
|
|
260
|
+
};
|
|
194
261
|
}
|
|
195
262
|
/** Parse `stack submit` argv. Exported for tests. */
|
|
196
263
|
export function parseStackSubmitArgs(args) {
|
|
197
264
|
let extend = false;
|
|
265
|
+
const policy = {};
|
|
198
266
|
let asJson = false;
|
|
199
|
-
for (
|
|
267
|
+
for (let i = 0; i < args.length; i++) {
|
|
268
|
+
const a = args[i];
|
|
200
269
|
if (a === "--json") {
|
|
201
270
|
asJson = true;
|
|
202
271
|
continue;
|
|
@@ -205,43 +274,159 @@ export function parseStackSubmitArgs(args) {
|
|
|
205
274
|
extend = true;
|
|
206
275
|
continue;
|
|
207
276
|
}
|
|
208
|
-
|
|
277
|
+
const next = takeOpenPolicyFlag(args, i, policy, "stack submit");
|
|
278
|
+
if (next !== null) {
|
|
279
|
+
i = next - 1;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
throw new CommandError("usage: mergestorm stack submit [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--json]");
|
|
209
283
|
}
|
|
210
|
-
return {
|
|
284
|
+
return {
|
|
285
|
+
extend,
|
|
286
|
+
autoLand: policy.autoLand,
|
|
287
|
+
autoReview: policy.autoReview,
|
|
288
|
+
autoPatch: policy.autoPatch,
|
|
289
|
+
asJson,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function parseOnOff(raw, flag, verb) {
|
|
293
|
+
if (raw === "on")
|
|
294
|
+
return true;
|
|
295
|
+
if (raw === "off")
|
|
296
|
+
return false;
|
|
297
|
+
throw new CommandError(`usage: mergestorm ${verb} [${flag} on|off]`);
|
|
298
|
+
}
|
|
299
|
+
/** `stack set` overrides are tri-state: `default` writes null (follow the account flag). */
|
|
300
|
+
function parseOnOffDefault(raw, flag, verb) {
|
|
301
|
+
if (raw === "on")
|
|
302
|
+
return true;
|
|
303
|
+
if (raw === "off")
|
|
304
|
+
return false;
|
|
305
|
+
if (raw === "default")
|
|
306
|
+
return null;
|
|
307
|
+
throw new CommandError(`usage: mergestorm ${verb} [${flag} on|off|default]`);
|
|
308
|
+
}
|
|
309
|
+
export function parseStackAdoptArgs(args) {
|
|
310
|
+
const targetArgs = [];
|
|
311
|
+
const policy = {};
|
|
312
|
+
let asJson = false;
|
|
313
|
+
for (let i = 0; i < args.length; i++) {
|
|
314
|
+
const a = args[i];
|
|
315
|
+
if (a === "--json") {
|
|
316
|
+
asJson = true;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
const next = takeOpenPolicyFlag(args, i, policy, "stack adopt <owner/repo>#<pr>");
|
|
320
|
+
if (next !== null) {
|
|
321
|
+
i = next - 1;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (a.startsWith("-")) {
|
|
325
|
+
throw new CommandError(`${STACK_USAGE}\nunknown flag: ${a}`);
|
|
326
|
+
}
|
|
327
|
+
targetArgs.push(a);
|
|
328
|
+
}
|
|
329
|
+
const target = parseAdoptTarget(targetArgs);
|
|
330
|
+
return {
|
|
331
|
+
...target,
|
|
332
|
+
autoLand: policy.autoLand,
|
|
333
|
+
autoReview: policy.autoReview,
|
|
334
|
+
autoPatch: policy.autoPatch,
|
|
335
|
+
asJson,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
export function parseStackSetArgs(args) {
|
|
339
|
+
const usage = "usage: mergestorm stack set <stack-id> [--auto-land on|off] [--auto-review on|off|default] [--auto-patch on|off|default] [--json]";
|
|
340
|
+
const verb = "stack set <stack-id>";
|
|
341
|
+
let stackId;
|
|
342
|
+
let autoLand;
|
|
343
|
+
let autoReview;
|
|
344
|
+
let autoPatch;
|
|
345
|
+
let asJson = false;
|
|
346
|
+
for (let i = 0; i < args.length; i++) {
|
|
347
|
+
const arg = args[i];
|
|
348
|
+
if (arg === "--json") {
|
|
349
|
+
asJson = true;
|
|
350
|
+
}
|
|
351
|
+
else if (arg === "--auto-land") {
|
|
352
|
+
autoLand = parseOnOff(args[++i], "--auto-land", verb);
|
|
353
|
+
}
|
|
354
|
+
else if (arg.startsWith("--auto-land=")) {
|
|
355
|
+
autoLand = parseOnOff(arg.slice("--auto-land=".length), "--auto-land", verb);
|
|
356
|
+
}
|
|
357
|
+
else if (arg === "--auto-review") {
|
|
358
|
+
autoReview = parseOnOffDefault(args[++i], "--auto-review", verb);
|
|
359
|
+
}
|
|
360
|
+
else if (arg.startsWith("--auto-review=")) {
|
|
361
|
+
autoReview = parseOnOffDefault(arg.slice("--auto-review=".length), "--auto-review", verb);
|
|
362
|
+
}
|
|
363
|
+
else if (arg === "--auto-patch") {
|
|
364
|
+
autoPatch = parseOnOffDefault(args[++i], "--auto-patch", verb);
|
|
365
|
+
}
|
|
366
|
+
else if (arg.startsWith("--auto-patch=")) {
|
|
367
|
+
autoPatch = parseOnOffDefault(arg.slice("--auto-patch=".length), "--auto-patch", verb);
|
|
368
|
+
}
|
|
369
|
+
else if (arg.startsWith("-") || stackId !== undefined) {
|
|
370
|
+
throw new CommandError(usage);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
stackId = arg;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (autoLand === undefined && autoReview === undefined && autoPatch === undefined) {
|
|
377
|
+
throw new CommandError(usage);
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
stackId: requireStackId(stackId, usage),
|
|
381
|
+
...(autoLand !== undefined ? { autoLand } : {}),
|
|
382
|
+
...(autoReview !== undefined ? { autoReview } : {}),
|
|
383
|
+
...(autoPatch !== undefined ? { autoPatch } : {}),
|
|
384
|
+
asJson,
|
|
385
|
+
};
|
|
211
386
|
}
|
|
212
387
|
export function parseStackResetArgs(args) {
|
|
213
388
|
if (args.length !== 1 || args[0] !== "--force") {
|
|
214
389
|
throw new CommandError("usage: mergestorm stack reset --force\nClears only CLI-managed pre-submit state; branches, PRs, and registered stacks are unchanged.");
|
|
215
390
|
}
|
|
216
391
|
}
|
|
217
|
-
async function cmdStackCreate(args, deps = {}) {
|
|
218
|
-
const { name, onto, trunk: trunkFlag, extend, asJson } = parseStackCreateArgs(args);
|
|
392
|
+
export async function cmdStackCreate(args, deps = {}) {
|
|
393
|
+
const { name, onto, trunk: trunkFlag, extend, autoLand, autoReview, autoPatch, asJson, } = parseStackCreateArgs(args);
|
|
219
394
|
const cwd = deps.cwd ?? process.cwd();
|
|
395
|
+
const gitTopLevelFn = deps.gitTopLevel ?? gitTopLevel;
|
|
396
|
+
const worktreeDirtyFn = deps.worktreeDirty ?? worktreeDirty;
|
|
397
|
+
const currentBranchFn = deps.currentBranch ?? currentBranch;
|
|
398
|
+
const branchExistsFn = deps.branchExists ?? branchExists;
|
|
399
|
+
const createBranchFromHeadFn = deps.createBranchFromHead ?? createBranchFromHead;
|
|
400
|
+
const deleteBranchFn = deps.deleteBranch ?? deleteBranch;
|
|
401
|
+
const discoverTrunkFn = deps.discoverTrunk ?? discoverTrunk;
|
|
402
|
+
const defaultLayerBranchNameFn = deps.defaultLayerBranchName ?? defaultLayerBranchName;
|
|
403
|
+
const loadStackMetaFn = deps.loadStackMeta ?? loadStackMeta;
|
|
404
|
+
const saveStackMetaFn = deps.saveStackMeta ?? saveStackMeta;
|
|
220
405
|
const listStacksFn = deps.listStacks ?? listStacks;
|
|
221
406
|
const loadConfigFn = deps.loadConfig ?? loadConfig;
|
|
222
407
|
const parseOriginFn = deps.parseGithubOriginRepo ?? parseGithubOriginRepo;
|
|
223
408
|
try {
|
|
224
|
-
|
|
409
|
+
gitTopLevelFn(cwd);
|
|
225
410
|
}
|
|
226
411
|
catch {
|
|
227
412
|
throw new CommandError("Not in a git repository. Run `mg stack create` from inside the repository whose stack you want to author.");
|
|
228
413
|
}
|
|
229
|
-
if (
|
|
414
|
+
if (worktreeDirtyFn(cwd)) {
|
|
230
415
|
throw new CommandError("Working tree is dirty. Commit or stash changes before `stack create`.");
|
|
231
416
|
}
|
|
232
|
-
const parentBranch = onto?.trim() ||
|
|
417
|
+
const parentBranch = onto?.trim() || currentBranchFn(cwd);
|
|
233
418
|
if (!parentBranch) {
|
|
234
419
|
throw new CommandError("Detached HEAD. Check out a branch (or pass --onto <branch>) before `stack create`.");
|
|
235
420
|
}
|
|
236
|
-
if (!
|
|
421
|
+
if (!branchExistsFn(parentBranch, cwd)) {
|
|
237
422
|
throw new CommandError(`Parent branch not found: ${parentBranch}`);
|
|
238
423
|
}
|
|
239
|
-
const branch =
|
|
240
|
-
if (
|
|
424
|
+
const branch = defaultLayerBranchNameFn(name);
|
|
425
|
+
if (branchExistsFn(branch, cwd)) {
|
|
241
426
|
throw new CommandError(`Branch already exists: ${branch}`);
|
|
242
427
|
}
|
|
243
|
-
let meta = await
|
|
244
|
-
const trunk = trunkFlag?.trim() || meta?.trunk ||
|
|
428
|
+
let meta = await loadStackMetaFn(cwd);
|
|
429
|
+
const trunk = trunkFlag?.trim() || meta?.trunk || discoverTrunkFn(cwd);
|
|
245
430
|
if (!meta) {
|
|
246
431
|
meta = emptyStackMeta(trunk);
|
|
247
432
|
}
|
|
@@ -281,23 +466,28 @@ async function cmdStackCreate(args, deps = {}) {
|
|
|
281
466
|
}
|
|
282
467
|
try {
|
|
283
468
|
meta = selectStackForCreate(meta, parentBranch);
|
|
469
|
+
meta = setActivePolicy(meta, {
|
|
470
|
+
autoEnqueueWhenReady: autoLand,
|
|
471
|
+
autoReviewOverride: autoReview,
|
|
472
|
+
autoPatchOverride: autoPatch,
|
|
473
|
+
});
|
|
284
474
|
}
|
|
285
475
|
catch (err) {
|
|
286
476
|
throw new CommandError(err instanceof Error ? err.message : String(err));
|
|
287
477
|
}
|
|
288
478
|
try {
|
|
289
|
-
|
|
479
|
+
createBranchFromHeadFn(branch, cwd);
|
|
290
480
|
}
|
|
291
481
|
catch (err) {
|
|
292
482
|
throw new CommandError(err instanceof Error ? err.message : String(err));
|
|
293
483
|
}
|
|
294
484
|
try {
|
|
295
485
|
meta = appendLayer(meta, { branch, parentBranch });
|
|
296
|
-
await
|
|
486
|
+
await saveStackMetaFn(meta, cwd);
|
|
297
487
|
}
|
|
298
488
|
catch (err) {
|
|
299
489
|
try {
|
|
300
|
-
|
|
490
|
+
deleteBranchFn(branch, cwd);
|
|
301
491
|
}
|
|
302
492
|
catch { /* rollback best-effort */ }
|
|
303
493
|
throw new CommandError(err instanceof Error ? err.message : String(err));
|
|
@@ -328,7 +518,13 @@ async function cmdStackCreate(args, deps = {}) {
|
|
|
328
518
|
await present("Stack", created);
|
|
329
519
|
}
|
|
330
520
|
export async function cmdStackSubmit(args, deps = {}) {
|
|
331
|
-
const { extend, asJson } = parseStackSubmitArgs(args);
|
|
521
|
+
const { extend, autoLand, autoReview, autoPatch, asJson } = parseStackSubmitArgs(args);
|
|
522
|
+
const progress = (message) => {
|
|
523
|
+
if (asJson)
|
|
524
|
+
console.error(message);
|
|
525
|
+
else
|
|
526
|
+
console.log(message);
|
|
527
|
+
};
|
|
332
528
|
const cwd = deps.cwd ?? process.cwd();
|
|
333
529
|
const gitTopLevelFn = deps.gitTopLevel ?? gitTopLevel;
|
|
334
530
|
const loadStackMetaFn = deps.loadStackMeta ?? loadStackMeta;
|
|
@@ -399,11 +595,26 @@ export async function cmdStackSubmit(args, deps = {}) {
|
|
|
399
595
|
owner,
|
|
400
596
|
repo,
|
|
401
597
|
});
|
|
598
|
+
// Explicit submit flags win; otherwise a fresh (unregistered) stack carries
|
|
599
|
+
// the choices remembered at `stack create`. Joining a registered stack
|
|
600
|
+
// never replays stored create-time flags onto it.
|
|
601
|
+
const stored = meta.stacks[meta.active];
|
|
602
|
+
const freshStack = plans.every((plan) => plan.stackId === null);
|
|
603
|
+
const pick = (flag, remembered) => typeof flag === "boolean"
|
|
604
|
+
? flag
|
|
605
|
+
: freshStack && typeof remembered === "boolean"
|
|
606
|
+
? remembered
|
|
607
|
+
: undefined;
|
|
608
|
+
const policy = openPolicyPatch({
|
|
609
|
+
autoLand: pick(autoLand, stored?.autoEnqueueWhenReady),
|
|
610
|
+
autoReview: pick(autoReview, stored?.autoReviewOverride),
|
|
611
|
+
autoPatch: pick(autoPatch, stored?.autoPatchOverride),
|
|
612
|
+
});
|
|
402
613
|
for (const layer of layers) {
|
|
403
614
|
if (!branchExistsFn(layer.branch, cwd)) {
|
|
404
615
|
throw new CommandError(`Stack layer branch missing locally: ${layer.branch}`);
|
|
405
616
|
}
|
|
406
|
-
|
|
617
|
+
progress(ansi.dim(` Pushing ${layer.branch} …`));
|
|
407
618
|
try {
|
|
408
619
|
pushBranchFn(layer.branch, cwd);
|
|
409
620
|
}
|
|
@@ -420,7 +631,7 @@ export async function cmdStackSubmit(args, deps = {}) {
|
|
|
420
631
|
const openLayerPr = (branch, base) => {
|
|
421
632
|
const existing = findOpenPrNumberFn(owner, repo, branch, cwd);
|
|
422
633
|
if (existing != null) {
|
|
423
|
-
|
|
634
|
+
progress(ansi.dim(` PR #${existing} already open for ${branch}`));
|
|
424
635
|
return { prNumber: existing, created: false };
|
|
425
636
|
}
|
|
426
637
|
const aheadRef = isMgParkBranch(base) ? `origin/${base}` : base;
|
|
@@ -448,7 +659,7 @@ export async function cmdStackSubmit(args, deps = {}) {
|
|
|
448
659
|
}
|
|
449
660
|
const title = tipCommitSubjectFn(branch, cwd);
|
|
450
661
|
const body = buildPrBodyFn(tipCommitMessageFn(branch, cwd));
|
|
451
|
-
|
|
662
|
+
progress(ansi.dim(` Opening PR ${branch} → ${base} …`));
|
|
452
663
|
return {
|
|
453
664
|
prNumber: createPrFn({
|
|
454
665
|
owner,
|
|
@@ -474,8 +685,8 @@ export async function cmdStackSubmit(args, deps = {}) {
|
|
|
474
685
|
let adoptData = null;
|
|
475
686
|
let stackId = plans.find((plan) => plan.stackId)?.stackId ?? null;
|
|
476
687
|
const registerPr = async (prNumber) => {
|
|
477
|
-
|
|
478
|
-
const body = await adoptStackFn(owner, repo, prNumber, cfg);
|
|
688
|
+
progress(ansi.dim(` Registering stack via import (${owner}/${repo}#${prNumber}) …`));
|
|
689
|
+
const body = await adoptStackFn(owner, repo, prNumber, cfg, policy);
|
|
479
690
|
if (typeof body !== "object" || body === null) {
|
|
480
691
|
throw new CommandError("Unexpected response shape from adopt API");
|
|
481
692
|
}
|
|
@@ -498,7 +709,7 @@ export async function cmdStackSubmit(args, deps = {}) {
|
|
|
498
709
|
if (!stackId) {
|
|
499
710
|
throw new CommandError("Cannot mint a park freeze before the stack is registered. Open layers 1–2 first, then retry.");
|
|
500
711
|
}
|
|
501
|
-
|
|
712
|
+
progress(ansi.dim(` Ensuring upper-park freeze for stack ${stackId} …`));
|
|
502
713
|
const park = await ensureUpperParkFn(stackId, cfg);
|
|
503
714
|
for (const plan of pendingPark) {
|
|
504
715
|
const { prNumber, created } = openLayerPr(plan.branch, park.freezeBranch);
|
|
@@ -576,15 +787,39 @@ async function cmdStackList(args) {
|
|
|
576
787
|
listed.push(ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"));
|
|
577
788
|
await present("Stacks", listed);
|
|
578
789
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
790
|
+
/** Human summary lines for a `stack set` write, one per flag. */
|
|
791
|
+
export function stackSetSummary(parsed) {
|
|
792
|
+
const tri = (value) => value === null ? "default (account setting)" : value ? "on" : "off";
|
|
793
|
+
const lines = [];
|
|
794
|
+
if (parsed.autoLand !== undefined) {
|
|
795
|
+
lines.push(`Auto land ${parsed.autoLand ? "on" : "off"}`);
|
|
584
796
|
}
|
|
585
|
-
|
|
797
|
+
if (parsed.autoReview !== undefined) {
|
|
798
|
+
lines.push(`Auto-review ${tri(parsed.autoReview)}`);
|
|
799
|
+
}
|
|
800
|
+
if (parsed.autoPatch !== undefined) {
|
|
801
|
+
lines.push(`Auto-patch ${tri(parsed.autoPatch)}`);
|
|
802
|
+
}
|
|
803
|
+
return lines;
|
|
804
|
+
}
|
|
805
|
+
export async function cmdStackSet(args, deps = {}) {
|
|
806
|
+
const parsed = parseStackSetArgs(args);
|
|
807
|
+
const cfg = await (deps.loadConfig ?? loadConfig)();
|
|
808
|
+
const body = await (deps.setStackPolicy ?? setStackPolicy)(parsed.stackId, {
|
|
809
|
+
...(parsed.autoLand !== undefined ? { autoEnqueueWhenReady: parsed.autoLand } : {}),
|
|
810
|
+
...(parsed.autoReview !== undefined ? { autoReviewOverride: parsed.autoReview } : {}),
|
|
811
|
+
...(parsed.autoPatch !== undefined ? { autoPatchOverride: parsed.autoPatch } : {}),
|
|
812
|
+
}, cfg);
|
|
813
|
+
if (parsed.asJson) {
|
|
814
|
+
console.log(JSON.stringify(body, null, 2));
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
await present("Stack", stackSetSummary(parsed).map((line) => ansi.brightGreen(` ${line} for ${parsed.stackId}`)));
|
|
818
|
+
}
|
|
819
|
+
async function cmdStackAdopt(args) {
|
|
820
|
+
const { owner, repo, prNumber, autoLand, autoReview, autoPatch, asJson } = parseStackAdoptArgs(args);
|
|
586
821
|
const cfg = await loadConfig();
|
|
587
|
-
const body = await adoptStack(owner, repo, prNumber, cfg);
|
|
822
|
+
const body = await adoptStack(owner, repo, prNumber, cfg, openPolicyPatch({ autoLand, autoReview, autoPatch }));
|
|
588
823
|
if (asJson) {
|
|
589
824
|
console.log(JSON.stringify(body, null, 2));
|
|
590
825
|
return;
|
|
@@ -670,6 +905,8 @@ export async function cmdStack(args) {
|
|
|
670
905
|
return cmdStackReset(rest);
|
|
671
906
|
if (sub === "list" || sub === "ls")
|
|
672
907
|
return cmdStackList(rest);
|
|
908
|
+
if (sub === "set")
|
|
909
|
+
return cmdStackSet(rest);
|
|
673
910
|
if (sub === "adopt")
|
|
674
911
|
return cmdStackAdopt(rest);
|
|
675
912
|
if (sub === "restack")
|
package/dist/commands/status.js
CHANGED
|
@@ -5,6 +5,7 @@ import { canBrowse, formatJobDetailLines } from "./browse.js";
|
|
|
5
5
|
import { CommandError, DetachedError, REVIEW_EXIT, isCommandErrorCode, } from "../errors.js";
|
|
6
6
|
import { ansi } from "../ui/ansi.js";
|
|
7
7
|
import { renderFindings } from "../ui/findings.js";
|
|
8
|
+
import { humanSummary } from "../ui/human-summary.js";
|
|
8
9
|
import { toReviewJobEnvelope, } from "../ui/job-envelope.js";
|
|
9
10
|
import { REVIEW_POLL_DEFAULT_TIMEOUT_MS, ReviewPollTimeoutError, pollReview, } from "./review-client.js";
|
|
10
11
|
const STATUS_USAGE = "usage: mergestorm status <job_id> [--json] [--wait] [--timeout seconds]";
|
|
@@ -71,8 +72,9 @@ function printPretty(row) {
|
|
|
71
72
|
if (envelope.credits) {
|
|
72
73
|
console.log(`Credits: ${envelope.credits.standard} standard`);
|
|
73
74
|
}
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
const summary = envelope.summary ? humanSummary(envelope.summary) : null;
|
|
76
|
+
if (summary)
|
|
77
|
+
console.log(summary);
|
|
76
78
|
if (envelope.error)
|
|
77
79
|
console.log(ansi.red(`Error: ${envelope.error}`));
|
|
78
80
|
renderFindings(envelope.findings, {
|
package/dist/commands/whoami.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getMe } from "../api.js";
|
|
2
|
-
import { apiBase, configPath, loadConfig, resolveApiKey
|
|
2
|
+
import { apiBase, configPath, loadConfig, resolveApiKey } from "../config.js";
|
|
3
3
|
import { CommandError } from "../errors.js";
|
|
4
4
|
import { ansi } from "../ui/ansi.js";
|
|
5
5
|
import { present } from "../ui/present.js";
|
|
@@ -16,14 +16,17 @@ export async function cmdWhoami(args, opts = {}) {
|
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
18
18
|
const me = await getMe(cfg);
|
|
19
|
+
if (!me) {
|
|
20
|
+
throw new CommandError("Live account details unavailable. Could not verify account with /me.");
|
|
21
|
+
}
|
|
19
22
|
const payload = {
|
|
20
|
-
key_prefix: me
|
|
21
|
-
key_name: me
|
|
22
|
-
plan_key: me
|
|
23
|
-
plan_label_key: me
|
|
23
|
+
key_prefix: me.key.prefix,
|
|
24
|
+
key_name: me.key.name ?? null,
|
|
25
|
+
plan_key: me.plan_key ?? null,
|
|
26
|
+
plan_label_key: me.plan_label_key ?? null,
|
|
24
27
|
api_base: apiBase(cfg),
|
|
25
28
|
config_path: configPath(),
|
|
26
|
-
usage: me
|
|
29
|
+
usage: me.usage ?? null,
|
|
27
30
|
};
|
|
28
31
|
if (asJson) {
|
|
29
32
|
const text = JSON.stringify(payload, null, 2);
|
|
@@ -42,8 +45,5 @@ export async function cmdWhoami(args, opts = {}) {
|
|
|
42
45
|
}
|
|
43
46
|
lines.push(` API ${payload.api_base}`);
|
|
44
47
|
lines.push(` Config ${payload.config_path}`);
|
|
45
|
-
if (!me) {
|
|
46
|
-
lines.push(ansi.dim(" (live account details unavailable — older API or offline)"));
|
|
47
|
-
}
|
|
48
48
|
await present("Account", lines);
|
|
49
49
|
}
|
package/dist/git.js
CHANGED
|
@@ -48,7 +48,7 @@ export async function changedNamesInSandbox(base, head, cwd = process.cwd(), san
|
|
|
48
48
|
// even though their contents cannot be read from the working tree for upload.
|
|
49
49
|
// -z disables core.quotePath quoting and NUL-terminates names, so paths with
|
|
50
50
|
// spaces or non-ASCII characters survive the split verbatim.
|
|
51
|
-
const names = git(["diff", "--name-only", "-z", "--diff-filter=ACMRDT", `${base}...${head}`], cwd)
|
|
51
|
+
const names = git(["diff", "--no-ext-diff", "--no-textconv", "--name-only", "-z", "--diff-filter=ACMRDT", `${base}...${head}`], cwd)
|
|
52
52
|
.split("\0")
|
|
53
53
|
.filter(Boolean);
|
|
54
54
|
const kept = [];
|
package/dist/repo-identity.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export declare const LEGACY_STACK_META_REL: string;
|
|
|
2
2
|
export declare function gitTopLevel(cwd?: string): string;
|
|
3
3
|
export declare function gitCommonDir(cwd?: string): string;
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Each worktree has its own authoring state.
|
|
6
6
|
* Independent clones remain isolated even when they point at the same remote.
|
|
7
7
|
*/
|
|
8
8
|
export declare function stableRepoId(cwd?: string): string;
|
package/dist/repo-identity.js
CHANGED
|
@@ -41,11 +41,22 @@ export function gitCommonDir(cwd = process.cwd()) {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
44
|
+
* Each worktree has its own authoring state.
|
|
45
45
|
* Independent clones remain isolated even when they point at the same remote.
|
|
46
46
|
*/
|
|
47
47
|
export function stableRepoId(cwd = process.cwd()) {
|
|
48
|
-
|
|
48
|
+
let raw;
|
|
49
|
+
try {
|
|
50
|
+
raw = git(["rev-parse", "--git-dir"], cwd).trim();
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
rethrowIfNotARepo(err);
|
|
54
|
+
}
|
|
55
|
+
const absolute = path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
|
|
56
|
+
return createHash("sha256")
|
|
57
|
+
.update(realpathSync(absolute))
|
|
58
|
+
.digest("hex")
|
|
59
|
+
.slice(0, 20);
|
|
49
60
|
}
|
|
50
61
|
export function cliStacksRoot() {
|
|
51
62
|
return path.join(mergestormHome(), "stacks");
|