mergestorm 0.3.64 → 0.3.67
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 +28 -9
- package/dist/api.d.ts +77 -11
- package/dist/api.js +228 -6
- package/dist/automation-catalog.d.ts +9 -1
- package/dist/automation-catalog.js +2 -0
- package/dist/cli.js +21 -19
- package/dist/client/index.d.ts +3 -2
- package/dist/client/index.js +3 -2
- package/dist/commands/account.d.ts +3 -0
- package/dist/commands/account.js +75 -0
- package/dist/commands/browse.d.ts +1 -9
- package/dist/commands/browse.js +10 -8
- package/dist/commands/dismiss.d.ts +0 -2
- package/dist/commands/dismiss.js +2 -2
- package/dist/commands/help.js +3 -2
- package/dist/commands/issue.d.ts +24 -6
- package/dist/commands/issue.js +157 -31
- package/dist/commands/login.js +46 -14
- package/dist/commands/logout.js +6 -7
- package/dist/commands/memory.d.ts +2 -3
- package/dist/commands/memory.js +8 -6
- package/dist/commands/registry.d.ts +1 -0
- package/dist/commands/registry.js +35 -11
- package/dist/commands/review-client.d.ts +0 -3
- package/dist/commands/review-client.js +3 -3
- package/dist/commands/review.d.ts +1 -1
- package/dist/commands/review.js +1 -1
- package/dist/commands/settings.d.ts +3 -5
- package/dist/commands/settings.js +5 -3
- package/dist/commands/stack.d.ts +19 -5
- package/dist/commands/stack.js +88 -4
- package/dist/commands/whoami.js +14 -10
- package/dist/config.d.ts +55 -2
- package/dist/config.js +264 -13
- package/dist/errors.d.ts +1 -1
- package/dist/gh.d.ts +0 -4
- package/dist/gh.js +4 -4
- package/dist/git.d.ts +1 -1
- package/dist/git.js +2 -2
- package/dist/memory-api.d.ts +1 -0
- package/dist/memory-api.js +2 -2
- package/dist/node-version.d.ts +0 -2
- package/dist/node-version.js +1 -1
- package/dist/pr-body.d.ts +4 -0
- package/dist/pr-body.js +15 -0
- package/dist/repo-identity.d.ts +2 -1
- package/dist/repo-identity.js +8 -4
- package/dist/server-publish.d.ts +1 -24
- package/dist/server-publish.js +9 -9
- package/dist/stack-blockers.d.ts +12 -38
- package/dist/stack-blockers.js +57 -33
- package/dist/stack-dto.d.ts +3 -0
- package/dist/stack-dto.js +15 -2
- package/dist/stack-meta.d.ts +2 -2
- package/dist/stack-meta.js +1 -1
- package/dist/stack-watch-loop.d.ts +2 -13
- package/dist/stack-watch-loop.js +12 -12
- package/dist/submit-pr-base.d.ts +0 -1
- package/dist/submit-pr-base.js +1 -1
- package/dist/ui/ansi.d.ts +0 -10
- package/dist/ui/ansi.js +2 -2
- package/dist/ui/banner.d.ts +3 -1
- package/dist/ui/banner.js +13 -3
- package/dist/ui/box.d.ts +0 -2
- package/dist/ui/box.js +1 -1
- package/dist/ui/prompt.d.ts +0 -11
- package/dist/ui/prompt.js +7 -7
- package/dist/ui/tabs.d.ts +2 -2
- package/dist/ui/tabs.js +1 -1
- package/dist/ui/width.d.ts +0 -9
- package/dist/ui/width.js +3 -3
- package/dist/web-api.d.ts +17 -0
- package/dist/web-api.js +30 -0
- package/package.json +1 -1
- package/skill/SKILL.md +4 -0
- package/skill/mergestorm-bounce-watch/SKILL.md +1 -1
package/dist/commands/login.js
CHANGED
|
@@ -2,8 +2,8 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { platform } from "node:os";
|
|
3
3
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { apiBase,
|
|
6
|
-
import { devicePost, getMe } from "../api.js";
|
|
5
|
+
import { accountNameFromMe, apiBase, configFor, configPath, mergestormHome, provisionalAccountName, readStore, saveAccount, } from "../config.js";
|
|
6
|
+
import { accountLabel, devicePost, getMe, revokeOwnKey } from "../api.js";
|
|
7
7
|
import { CommandError } from "../errors.js";
|
|
8
8
|
import { ansi } from "../ui/ansi.js";
|
|
9
9
|
import { present } from "../ui/present.js";
|
|
@@ -40,6 +40,29 @@ export async function validateApiKey(cfg) {
|
|
|
40
40
|
throw err;
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
async function loginConfig() {
|
|
44
|
+
const store = await readStore();
|
|
45
|
+
return configFor(store, store.activeAccount);
|
|
46
|
+
}
|
|
47
|
+
async function storeLogin(cfg, me, opts) {
|
|
48
|
+
const apiKey = cfg.apiKey ?? "";
|
|
49
|
+
const name = accountNameFromMe(me) ?? provisionalAccountName(apiKey);
|
|
50
|
+
const before = await readStore();
|
|
51
|
+
const replaced = Object.hasOwn(before.accounts, name) ? configFor(before, name) : undefined;
|
|
52
|
+
try {
|
|
53
|
+
await saveAccount(name, { apiKey, apiBase: cfg.apiBase }, { activate: true });
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
if (opts.minted && err instanceof CommandError && err.code === "account_limit") {
|
|
57
|
+
await revokeOwnKey(cfg, { timeoutMs: 8_000 });
|
|
58
|
+
}
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
if (replaced?.apiKey && replaced.apiKey !== apiKey) {
|
|
62
|
+
await revokeOwnKey({ ...replaced, ignoreEnv: true }, { timeoutMs: 8_000 });
|
|
63
|
+
}
|
|
64
|
+
return name;
|
|
65
|
+
}
|
|
43
66
|
async function loginWithKey() {
|
|
44
67
|
// Always use muted secret input on TTY so the key never lands in
|
|
45
68
|
// question()/history echo.
|
|
@@ -47,12 +70,12 @@ async function loginWithKey() {
|
|
|
47
70
|
if (!key.startsWith("msk_live_")) {
|
|
48
71
|
throw new CommandError("Key should start with msk_live_");
|
|
49
72
|
}
|
|
50
|
-
const cfg = await
|
|
51
|
-
|
|
52
|
-
const me = await validateApiKey(
|
|
53
|
-
await
|
|
73
|
+
const cfg = await loginConfig();
|
|
74
|
+
const candidate = { apiKey: key, apiBase: apiBase(cfg), ignoreEnv: true };
|
|
75
|
+
const me = await validateApiKey(candidate);
|
|
76
|
+
const name = await storeLogin(candidate, me, { minted: false });
|
|
54
77
|
const prefix = me.key?.prefix?.trim() || "msk_live_…";
|
|
55
|
-
await present("Login", [` Verified ${prefix}… · saved to ${configPath()}`]);
|
|
78
|
+
await present("Login", [` Logged in as ${name}. Verified ${prefix}… · saved to ${configPath()}`]);
|
|
56
79
|
}
|
|
57
80
|
export function pendingLoginPath() {
|
|
58
81
|
return path.join(mergestormHome(), "login-pending.json");
|
|
@@ -140,12 +163,21 @@ async function pollDeviceLogin(pending, retryCommand, recovery) {
|
|
|
140
163
|
throw new CommandError(`Login failed: network error. Check your connection and run \`${retryCommand}\` again.`);
|
|
141
164
|
}
|
|
142
165
|
if (poll.status === 200 && poll.body?.api_key) {
|
|
143
|
-
const
|
|
144
|
-
next.apiKey = poll.body.api_key;
|
|
145
|
-
next.apiBase = pending.apiBase;
|
|
146
|
-
await saveConfig(next);
|
|
166
|
+
const minted = { apiKey: poll.body.api_key, apiBase: pending.apiBase, ignoreEnv: true };
|
|
147
167
|
await clearPendingLogin(pending.deviceCode);
|
|
148
|
-
|
|
168
|
+
let me = accountLabel(poll.body?.account)
|
|
169
|
+
? { account: poll.body.account }
|
|
170
|
+
: null;
|
|
171
|
+
if (!me) {
|
|
172
|
+
try {
|
|
173
|
+
me = await getMe(minted, { timeoutMs: 8_000 });
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
me = null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const name = await storeLogin(minted, me, { minted: true });
|
|
180
|
+
await present("Login", [` Logged in as ${name}. Key saved to ${configPath()}`]);
|
|
149
181
|
return;
|
|
150
182
|
}
|
|
151
183
|
const err = poll.body?.error;
|
|
@@ -177,7 +209,7 @@ async function pollDeviceLogin(pending, retryCommand, recovery) {
|
|
|
177
209
|
throw new CommandError(`Login timed out. ${recovery}`);
|
|
178
210
|
}
|
|
179
211
|
async function loginStart() {
|
|
180
|
-
const cfg = await
|
|
212
|
+
const cfg = await loginConfig();
|
|
181
213
|
const base = apiBase(cfg);
|
|
182
214
|
const pending = pendingFromStart(base, await startDeviceLogin(base));
|
|
183
215
|
await savePendingLogin(pending);
|
|
@@ -213,7 +245,7 @@ export async function cmdLogin(args, deps = { openBrowser }) {
|
|
|
213
245
|
await loginFinish();
|
|
214
246
|
return;
|
|
215
247
|
}
|
|
216
|
-
const cfg = await
|
|
248
|
+
const cfg = await loginConfig();
|
|
217
249
|
const base = apiBase(cfg);
|
|
218
250
|
const pending = pendingFromStart(base, await startDeviceLogin(base));
|
|
219
251
|
console.log("To sign in, open this URL and confirm the code:\n");
|
package/dist/commands/logout.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { loadConfig } from "../config.js";
|
|
2
2
|
import { CommandError } from "../errors.js";
|
|
3
3
|
import { present } from "../ui/present.js";
|
|
4
|
+
import { logoutAccount } from "./account.js";
|
|
4
5
|
export async function cmdLogout() {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
catch (err) {
|
|
10
|
-
throw new CommandError(`Could not remove config: ${err instanceof Error ? err.message : String(err)}`);
|
|
6
|
+
const cfg = await loadConfig();
|
|
7
|
+
if (!cfg.account) {
|
|
8
|
+
throw new CommandError("Not logged in. Nothing to log out of.");
|
|
11
9
|
}
|
|
10
|
+
await present("Logout", await logoutAccount(cfg.account));
|
|
12
11
|
}
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { type Config } from "../config.js";
|
|
2
2
|
import { deleteMemory, getMemoryMode, listMemories, saveMemory, searchMemories, setMemoryMode, updateMemory, type MemoryItem, type MemoryKind, type MemoryScope } from "../memory-api.js";
|
|
3
|
-
export declare const MEMORY_USAGE: string;
|
|
4
|
-
export declare const MEMORY_SUBCOMMANDS: readonly ["list", "search", "add", "update", "delete", "mode"];
|
|
5
3
|
type Flags = {
|
|
6
4
|
positional: string[];
|
|
7
5
|
json: boolean;
|
|
6
|
+
keepBoth?: boolean;
|
|
8
7
|
repo?: string;
|
|
9
8
|
kind?: MemoryKind;
|
|
10
9
|
scope?: MemoryScope;
|
|
11
10
|
limit?: number;
|
|
12
11
|
};
|
|
13
|
-
export declare function parseMemoryFlags(args: string[], allowed: ReadonlyArray<"repo" | "kind" | "scope" | "limit">): Flags;
|
|
12
|
+
export declare function parseMemoryFlags(args: string[], allowed: ReadonlyArray<"repo" | "kind" | "scope" | "limit" | "keep-both">): Flags;
|
|
14
13
|
export declare function formatMemoryLine(memory: MemoryItem): string;
|
|
15
14
|
export type MemoryDeps = {
|
|
16
15
|
loadConfig?: () => Promise<Config>;
|
package/dist/commands/memory.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { loadConfig } from "../config.js";
|
|
2
2
|
import { CommandError, REVIEW_EXIT } from "../errors.js";
|
|
3
3
|
import { MEMORY_KINDS, MEMORY_SEARCH_LIMIT_MAX, deleteMemory, getMemoryMode, isMemoryKind, isMemoryMode, isMemoryScope, listMemories, memoryChatId, saveMemory, searchMemories, setMemoryMode, updateMemory, } from "../memory-api.js";
|
|
4
|
-
|
|
4
|
+
const MEMORY_USAGE = [
|
|
5
5
|
"usage: mergestorm memory list [--scope user|repo] [--repo <owner/repo>] [--kind <kind>] [--json]",
|
|
6
6
|
" mergestorm memory search <query> [--repo <owner/repo>] [--limit <1-20>] [--json]",
|
|
7
|
-
" mergestorm memory add <text> --kind <kind> [--repo <owner/repo>] [--json]",
|
|
7
|
+
" mergestorm memory add <text> --kind <kind> [--repo <owner/repo>] [--keep-both] [--json]",
|
|
8
8
|
" mergestorm memory update <id> <text> [--kind <kind>] [--json]",
|
|
9
9
|
" mergestorm memory delete <id> [--json]",
|
|
10
10
|
" mergestorm memory mode [off|on_request|all] [--json]",
|
|
11
11
|
` kind is one of ${MEMORY_KINDS.join(", ")}. With --repo a memory is about that repository, otherwise about you.`,
|
|
12
|
+
" add refuses text close to a memory you have; update that memory, or add again with --keep-both when they are different things.",
|
|
12
13
|
" search covers your memories, and with --repo that repository's memories and standing rules too.",
|
|
13
14
|
" Inside a Breeze chat (MERGESTORM_CHAT_ID), add and search follow that chat's memory setting.",
|
|
14
15
|
].join("\n");
|
|
15
|
-
export const MEMORY_SUBCOMMANDS = ["list", "search", "add", "update", "delete", "mode"];
|
|
16
16
|
const REPO = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9._-]{1,100}$/;
|
|
17
17
|
function usage(message) {
|
|
18
18
|
return new CommandError(message ? `${message}\n${MEMORY_USAGE}` : MEMORY_USAGE, REVIEW_EXIT.usage, "usage");
|
|
@@ -29,6 +29,8 @@ export function parseMemoryFlags(args, allowed) {
|
|
|
29
29
|
const name = arg.startsWith("--") ? arg.slice(2) : null;
|
|
30
30
|
if (arg === "--json")
|
|
31
31
|
flags.json = true;
|
|
32
|
+
else if (name === "keep-both" && allowed.includes("keep-both"))
|
|
33
|
+
flags.keepBoth = true;
|
|
32
34
|
else if (name === "repo" && allowed.includes("repo")) {
|
|
33
35
|
const repo = value(arg, args[++i]).replace(/^https?:\/\/github\.com\//i, "").replace(/\.git$/i, "");
|
|
34
36
|
if (!REPO.test(repo))
|
|
@@ -108,14 +110,14 @@ export async function cmdMemory(args, deps = {}) {
|
|
|
108
110
|
return;
|
|
109
111
|
}
|
|
110
112
|
if (sub === "add" || sub === "save" || sub === "remember") {
|
|
111
|
-
const flags = parseMemoryFlags(rest, ["repo", "kind"]);
|
|
113
|
+
const flags = parseMemoryFlags(rest, ["repo", "kind", "keep-both"]);
|
|
112
114
|
const text = flags.positional.join(" ").trim();
|
|
113
115
|
if (!text)
|
|
114
116
|
throw usage("add needs the text to remember");
|
|
115
117
|
if (!flags.kind)
|
|
116
118
|
throw usage(`add needs --kind (${MEMORY_KINDS.join(", ")})`);
|
|
117
|
-
const saved = await api.saveMemory({ text, kind: flags.kind, ...(flags.repo ? { repo: flags.repo } : {}), chatId }, await config());
|
|
118
|
-
emit(flags.json, saved, () => [saved.merged ? `
|
|
119
|
+
const saved = await api.saveMemory({ text, kind: flags.kind, ...(flags.repo ? { repo: flags.repo } : {}), chatId, ...(flags.keepBoth ? { keepBoth: true } : {}) }, await config());
|
|
120
|
+
emit(flags.json, saved, () => [saved.merged ? `Memory ${saved.id} already says this.` : `Saved to memory as ${saved.id}.`]);
|
|
119
121
|
return;
|
|
120
122
|
}
|
|
121
123
|
if (sub === "update" || sub === "edit") {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { DEFAULT_API } from "../config.js";
|
|
1
|
+
import { DEFAULT_API, splitAccountFlag, withAccount } from "../config.js";
|
|
2
2
|
import { CommandError } from "../errors.js";
|
|
3
|
+
import { cmdAccount } from "./account.js";
|
|
3
4
|
import { cmdBranches, cmdChain } from "./branches.js";
|
|
4
5
|
import { canBrowse, openTabsBrowser } from "./browse.js";
|
|
5
6
|
import { cmdCredits } from "./credits.js";
|
|
@@ -24,9 +25,9 @@ import { cmdWhoami } from "./whoami.js";
|
|
|
24
25
|
export const COMMAND_REGISTRY = [
|
|
25
26
|
{
|
|
26
27
|
name: "login",
|
|
27
|
-
summary: "Sign in via browser (or paste a key)",
|
|
28
|
+
summary: "Sign in via browser (or paste a key); adds an account, up to 3",
|
|
28
29
|
usage: [
|
|
29
|
-
"mergestorm login Sign in via browser and
|
|
30
|
+
"mergestorm login Sign in via browser and add the account (up to 3 saved)",
|
|
30
31
|
"mergestorm login --start Print a sign-in URL and code, then exit (agents)",
|
|
31
32
|
"mergestorm login --finish Store the API key once the code from --start is approved",
|
|
32
33
|
"mergestorm login --key Paste an existing API key instead (headless/CI)",
|
|
@@ -39,14 +40,28 @@ export const COMMAND_REGISTRY = [
|
|
|
39
40
|
},
|
|
40
41
|
{
|
|
41
42
|
name: "logout",
|
|
42
|
-
summary: "
|
|
43
|
-
usage: ["mergestorm logout
|
|
43
|
+
summary: "Log out of the active account and revoke its key",
|
|
44
|
+
usage: ["mergestorm logout Log out of the active account (or --account <name>) and revoke its key"],
|
|
44
45
|
async run(_args, ctx) {
|
|
45
46
|
await cmdLogout();
|
|
46
47
|
if (ctx.afterAuth)
|
|
47
48
|
await ctx.afterAuth();
|
|
48
49
|
},
|
|
49
50
|
},
|
|
51
|
+
{
|
|
52
|
+
name: "account",
|
|
53
|
+
summary: "Saved accounts: list, use <name>, remove <name>",
|
|
54
|
+
usage: [
|
|
55
|
+
"mergestorm account list [--json] Saved accounts (up to 3), the active one marked",
|
|
56
|
+
"mergestorm account use <name> Make <name> the active account",
|
|
57
|
+
"mergestorm account remove <name> Log out of <name> and revoke its key",
|
|
58
|
+
],
|
|
59
|
+
async run(args, ctx) {
|
|
60
|
+
await cmdAccount(args);
|
|
61
|
+
if (ctx.afterAuth)
|
|
62
|
+
await ctx.afterAuth();
|
|
63
|
+
},
|
|
64
|
+
},
|
|
50
65
|
{
|
|
51
66
|
name: "review",
|
|
52
67
|
summary: "Review git diff (default origin/HEAD or main or master)",
|
|
@@ -102,11 +117,15 @@ export const COMMAND_REGISTRY = [
|
|
|
102
117
|
},
|
|
103
118
|
{
|
|
104
119
|
name: "issue",
|
|
105
|
-
summary: "Open a GitHub issue through Mergestorm
|
|
120
|
+
summary: "Open a GitHub issue through Mergestorm and wait for its Vortex review",
|
|
106
121
|
usage: [
|
|
107
|
-
"mergestorm issue create --title <title> [--body <text> | --body-file <path>] [--label <name>]… [--repo <owner/repo>] [--json]",
|
|
122
|
+
"mergestorm issue create --title <title> [--body <text> | --body-file <path>] [--label <name>]… [--repo <owner/repo>] [--wait [--timeout <s>]] [--json]",
|
|
108
123
|
" Opens the issue as the Surge App; needs write access on GitHub, uses existing labels only",
|
|
109
124
|
" The same title again returns the open issue it already made when it is among the newest 100 open issues; an older one, or two calls at once, can still create a duplicate",
|
|
125
|
+
" --wait then waits for the Vortex issue review, like issue wait",
|
|
126
|
+
"mergestorm issue wait <n> [--repo <owner/repo>] [--timeout <s>] [--json]",
|
|
127
|
+
" Waits for the Vortex issue review (default 900s); exits 0 once approved or when issue reviews are off",
|
|
128
|
+
" Exits 4 with the points to answer: reply in a comment on the issue, wait again, and open the PR only after approval",
|
|
110
129
|
],
|
|
111
130
|
async run(args) {
|
|
112
131
|
await cmdIssue(args);
|
|
@@ -186,7 +205,7 @@ export const COMMAND_REGISTRY = [
|
|
|
186
205
|
{
|
|
187
206
|
name: "whoami",
|
|
188
207
|
summary: "Key + plan (Status tab on a TTY)",
|
|
189
|
-
usage: ["mergestorm whoami [--json]
|
|
208
|
+
usage: ["mergestorm whoami [--json] Account + key + plan + API base"],
|
|
190
209
|
async run(args, ctx) {
|
|
191
210
|
await cmdWhoami(args, { mode: ctx.mode });
|
|
192
211
|
},
|
|
@@ -201,6 +220,7 @@ export const COMMAND_REGISTRY = [
|
|
|
201
220
|
" [--cyclone-skip-ci on|off]",
|
|
202
221
|
" [--cyclone-patch-unverified on|off] Patch languages we cannot typecheck",
|
|
203
222
|
" [--vortex-seam on|off] [--vortex-skip-all-clear on|off]",
|
|
223
|
+
" [--issue-review on|off] Account default for Vortex issue reviews; --repo overrides it per repository",
|
|
204
224
|
" [--auto-land on|off] Auto land default for new stacks",
|
|
205
225
|
" [--auto-land-settle <seconds>] How long Auto land waits before queueing (15-300, default 60)",
|
|
206
226
|
" [--merge-queue-batch on|off] Test several queued pull requests on one CI run",
|
|
@@ -225,7 +245,7 @@ export const COMMAND_REGISTRY = [
|
|
|
225
245
|
usage: [
|
|
226
246
|
"mergestorm memory list [--scope user|repo] [--repo <owner/repo>] [--kind <kind>] [--json]",
|
|
227
247
|
"mergestorm memory search <query> [--repo <owner/repo>] [--limit <1-20>] [--json]",
|
|
228
|
-
"mergestorm memory add <text> --kind rule|style|preference|fact|project [--repo <owner/repo>] [--json]",
|
|
248
|
+
"mergestorm memory add <text> --kind rule|style|preference|fact|project [--repo <owner/repo>] [--keep-both] [--json]",
|
|
229
249
|
"mergestorm memory update <id> <text> [--kind <kind>] [--json]",
|
|
230
250
|
"mergestorm memory delete <id> [--json]",
|
|
231
251
|
"mergestorm memory mode [off|on_request|all] [--json] Read or set whether Breeze remembers",
|
|
@@ -348,9 +368,12 @@ export function formatUsageBody() {
|
|
|
348
368
|
return `${cmdLines}
|
|
349
369
|
|
|
350
370
|
mg is a short alias for mergestorm (same binary).
|
|
371
|
+
--account <name> before or after any command runs it as that saved account
|
|
372
|
+
without changing the active one; MERGESTORM_ACCOUNT=<name> does the same.
|
|
351
373
|
|
|
352
374
|
Env:
|
|
353
|
-
MERGESTORM_API_KEY API key (overrides
|
|
375
|
+
MERGESTORM_API_KEY API key (overrides every saved account)
|
|
376
|
+
MERGESTORM_ACCOUNT Saved account to run as (default: the active one)
|
|
354
377
|
MERGESTORM_API_URL API base (default ${DEFAULT_API})
|
|
355
378
|
MERGESTORM_HOME State directory for login and stack state (default ~/.mergestorm)
|
|
356
379
|
`;
|
|
@@ -366,6 +389,7 @@ export async function dispatchCommand(name, args, ctx) {
|
|
|
366
389
|
// `shell` as a subcommand is oneshot-only.
|
|
367
390
|
if (cmd.name === "shell" && ctx.mode === "shell")
|
|
368
391
|
return false;
|
|
369
|
-
|
|
392
|
+
const split = splitAccountFlag(args);
|
|
393
|
+
await withAccount(split.account ?? ctx.account, () => cmd.run(split.args, ctx));
|
|
370
394
|
return true;
|
|
371
395
|
}
|
|
@@ -10,10 +10,8 @@ export declare const REVIEW_POLL_STRETCH_AFTER_MS = 60000;
|
|
|
10
10
|
export declare const REVIEW_POLL_STRETCH_SPAN_MS = 60000;
|
|
11
11
|
/** Same thread slug contract as POST /api/v1/reviews. */
|
|
12
12
|
export declare const REVIEW_THREAD_SLUG_RE: RegExp;
|
|
13
|
-
export declare const MAX_CONTEXT_TEXT_CHARS = 60000;
|
|
14
13
|
export declare const MAX_CONTEXT_FILES = 10;
|
|
15
14
|
export declare const MAX_CONTEXT_FILE_CHARS = 16000;
|
|
16
|
-
export declare const MAX_CONTEXT_FILES_TOTAL = 60000;
|
|
17
15
|
export declare const MAX_IDEMPOTENCY_KEY_CHARS = 128;
|
|
18
16
|
export type ReviewInput = {
|
|
19
17
|
thread: string;
|
|
@@ -57,7 +55,6 @@ export type LoadReviewContextOptions = {
|
|
|
57
55
|
};
|
|
58
56
|
/** Load --context / --context-file and enforce the API caps client-side. */
|
|
59
57
|
export declare function loadReviewContext(opts: LoadReviewContextOptions): Promise<ReviewContextPayload>;
|
|
60
|
-
export declare function reviewIdempotencyKey(input: ReviewInput, opts?: SubmitReviewOptions, afterJobId?: string | null): string;
|
|
61
58
|
export declare function submitReview(cfg: Config, input: ReviewInput, opts?: SubmitReviewOptions): Promise<{
|
|
62
59
|
status: number;
|
|
63
60
|
row: ReviewJobRow;
|
|
@@ -13,10 +13,10 @@ export const REVIEW_POLL_STRETCH_AFTER_MS = 60_000;
|
|
|
13
13
|
export const REVIEW_POLL_STRETCH_SPAN_MS = 60_000;
|
|
14
14
|
/** Same thread slug contract as POST /api/v1/reviews. */
|
|
15
15
|
export const REVIEW_THREAD_SLUG_RE = /^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,119}$/;
|
|
16
|
-
|
|
16
|
+
const MAX_CONTEXT_TEXT_CHARS = 60_000;
|
|
17
17
|
export const MAX_CONTEXT_FILES = 10;
|
|
18
18
|
export const MAX_CONTEXT_FILE_CHARS = 16_000;
|
|
19
|
-
|
|
19
|
+
const MAX_CONTEXT_FILES_TOTAL = 60_000;
|
|
20
20
|
export const MAX_IDEMPOTENCY_KEY_CHARS = 128;
|
|
21
21
|
export async function collectReviewInput(base, head, cwd = process.cwd(), sandboxRoot) {
|
|
22
22
|
let diff;
|
|
@@ -129,7 +129,7 @@ export async function loadReviewContext(opts) {
|
|
|
129
129
|
}
|
|
130
130
|
const REPLAYED_JOB_RESUBMIT_STATUSES = ["failed", "quota_exceeded"];
|
|
131
131
|
const MAX_REPLAYED_JOB_RESUBMITS = 5;
|
|
132
|
-
|
|
132
|
+
function reviewIdempotencyKey(input, opts = {}, afterJobId = null) {
|
|
133
133
|
const request = [
|
|
134
134
|
input.thread, input.branch, input.repo ?? null, input.baseLabel, input.headLabel, input.diff, input.files,
|
|
135
135
|
opts.routerMode ?? null, [...new Set(opts.specialists ?? [])].sort(), opts.context ?? null, opts.contextFiles ?? [], opts.webhookUrl ?? null,
|
|
@@ -10,7 +10,7 @@ export declare const VORTEX_ROUTER_CAPS: {
|
|
|
10
10
|
/**
|
|
11
11
|
* Pinnable L0 lanes only. Keep this list byte-identical to
|
|
12
12
|
* `pinnableVortexSpecialistIds(VORTEX_SPECIALIST_IDS)` in review-findings
|
|
13
|
-
* (`
|
|
13
|
+
* (`index.test.ts`). Do not add `governance` (round-gated) or `seam`
|
|
14
14
|
* (phase-gated). Those L1 lanes are not local pins.
|
|
15
15
|
*/
|
|
16
16
|
export declare const VORTEX_SPECIALIST_IDS: readonly ["security", "performance", "architecture", "tests", "data", "api", "frontend"];
|
package/dist/commands/review.js
CHANGED
|
@@ -16,7 +16,7 @@ export const VORTEX_ROUTER_CAPS = { standard: 3, max: 5 };
|
|
|
16
16
|
/**
|
|
17
17
|
* Pinnable L0 lanes only. Keep this list byte-identical to
|
|
18
18
|
* `pinnableVortexSpecialistIds(VORTEX_SPECIALIST_IDS)` in review-findings
|
|
19
|
-
* (`
|
|
19
|
+
* (`index.test.ts`). Do not add `governance` (round-gated) or `seam`
|
|
20
20
|
* (phase-gated). Those L1 lanes are not local pins.
|
|
21
21
|
*/
|
|
22
22
|
export const VORTEX_SPECIALIST_IDS = [
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type RepoSettingsPatch, type SettingsPatch, type SettingsResponse } from "../api.js";
|
|
2
2
|
/**
|
|
3
3
|
* CLI flag → Bearer settings key. Boolean flags take on|off. The connected flags are
|
|
4
4
|
* read-only on the API and deliberately have no flag here.
|
|
5
5
|
*/
|
|
6
|
-
export declare const SETTINGS_FLAGS: Record<string, "auto_review_enabled" | "auto_patch_enabled" | "auto_resolve_conflicts_enabled" | "auto_fix_ci_enabled" | "vortex_show_thinking_traces" | "repo_overview_enabled" | "cyclone_skip_ci_enabled" | "cyclone_patch_unverified_languages" | "vortex_auto_overflow_enabled" | "vortex_skip_all_clear_comments" | "vortex_seam_specialist_enabled" | "auto_land_default" | "auto_land_settle_seconds" | "merge_queue_batch_enabled" | "merge_queue_batch_size" | "merge_queue_lands_alone_paths" | "merge_queue_overdrive" | "ignored_bot_logins" | "vortex_bot_skip_check" | "vortex_findings_check" | "cyclone_patch_failure_check">;
|
|
6
|
+
export declare const SETTINGS_FLAGS: Record<string, "auto_review_enabled" | "auto_patch_enabled" | "auto_resolve_conflicts_enabled" | "auto_fix_ci_enabled" | "vortex_show_thinking_traces" | "repo_overview_enabled" | "cyclone_skip_ci_enabled" | "cyclone_patch_unverified_languages" | "vortex_auto_overflow_enabled" | "vortex_skip_all_clear_comments" | "vortex_seam_specialist_enabled" | "issue_review_enabled" | "issue_auto_review_enabled" | "auto_land_default" | "auto_land_settle_seconds" | "merge_queue_batch_enabled" | "merge_queue_batch_size" | "merge_queue_lands_alone_paths" | "merge_queue_overdrive" | "ignored_bot_logins" | "vortex_bot_skip_check" | "vortex_findings_check" | "cyclone_patch_failure_check">;
|
|
7
7
|
type IgnoreBotOperation = {
|
|
8
8
|
action: "add" | "remove";
|
|
9
9
|
login: string;
|
|
10
10
|
} | {
|
|
11
11
|
action: "clear";
|
|
12
12
|
};
|
|
13
|
-
|
|
13
|
+
type RepoSettingsScope = {
|
|
14
14
|
owner: string;
|
|
15
15
|
repo: string;
|
|
16
16
|
patch: RepoSettingsPatch;
|
|
@@ -22,8 +22,6 @@ export type SettingsArgs = {
|
|
|
22
22
|
repo?: RepoSettingsScope;
|
|
23
23
|
listRepos?: boolean;
|
|
24
24
|
};
|
|
25
|
-
export declare function parsePathsFlagValue(flag: string, value: string | undefined, defaults: readonly string[]): string[];
|
|
26
|
-
export declare function formatRepoSettingsLines(repos: readonly RepoSettings[]): string[];
|
|
27
25
|
/** Parse boolean and enum flags plus ordered ignore-list operations. */
|
|
28
26
|
export declare function parseSettingsArgs(rawArgs: string[]): SettingsArgs;
|
|
29
27
|
/** Static printout includes the Config toggles plus enum and login settings. */
|
|
@@ -18,8 +18,10 @@ const REPO_FLAG_LABELS = {
|
|
|
18
18
|
merge_queue_batch_size: "Merge queue batch size (pull requests)",
|
|
19
19
|
merge_queue_lands_alone_paths: "Merge queue lands-alone paths",
|
|
20
20
|
merge_queue_overdrive: "Merge queue overdrive",
|
|
21
|
+
issue_review_enabled: "Vortex issue reviews",
|
|
22
|
+
issue_auto_review_enabled: "Vortex issue auto review",
|
|
21
23
|
};
|
|
22
|
-
|
|
24
|
+
function parsePathsFlagValue(flag, value, defaults) {
|
|
23
25
|
if (value === undefined || value === "" || value.startsWith("--")) {
|
|
24
26
|
throw new CommandError(`${flag} takes a comma-separated list of path patterns, default, or none.`, 2, "usage");
|
|
25
27
|
}
|
|
@@ -79,7 +81,7 @@ function parseRepoSettingsArgs(slug, args) {
|
|
|
79
81
|
throw new CommandError(`Unknown settings flag: ${flag}. See \`mergestorm --help\`.`, 2, "usage");
|
|
80
82
|
}
|
|
81
83
|
if (!REPO_SETTINGS_KEYS.includes(key)) {
|
|
82
|
-
throw new CommandError(`${flag} is an account setting. With --repo only --auto-patch, --auto-review, --auto-land, --merge-queue-batch, --merge-queue-batch-size, --merge-queue-lands-alone
|
|
84
|
+
throw new CommandError(`${flag} is an account setting. With --repo only --auto-patch, --auto-review, --auto-land, --merge-queue-batch, --merge-queue-batch-size, --merge-queue-lands-alone, --merge-queue-overdrive, --issue-review and --issue-auto-review can be set.`, 2, "usage");
|
|
83
85
|
}
|
|
84
86
|
const value = eq >= 0 ? arg.slice(eq + 1) : args[++i];
|
|
85
87
|
const row = BEARER_SETTINGS.find((row) => row.key === key);
|
|
@@ -115,7 +117,7 @@ function repoSettingWords(value) {
|
|
|
115
117
|
return String(value);
|
|
116
118
|
return value ? "on" : "off";
|
|
117
119
|
}
|
|
118
|
-
|
|
120
|
+
function formatRepoSettingsLines(repos) {
|
|
119
121
|
if (repos.length === 0)
|
|
120
122
|
return [" No repositories are connected to this account."];
|
|
121
123
|
const width = Math.max(...Object.values(REPO_FLAG_LABELS).map((label) => label.length));
|
package/dist/commands/stack.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type StackRepairHint } from "../stack-blockers.js";
|
|
2
2
|
import { type StackWatchObligation } from "../stack-watch-obligation.js";
|
|
3
|
-
import { adoptStack, ensureUpperPark, findStackPull, getEnrichedStack, getStackPublication, listMergeQueueEntries, listStacks, openStackPull, claimStackPulls, setStackPolicy, type StackDto, type StackPolicyPatch } from "../api.js";
|
|
3
|
+
import { adoptStack, ensureUpperPark, findStackPull, getEnrichedStack, getIssueReview, type IssueReviewMode, getStackPublication, listMergeQueueEntries, listStacks, openStackPull, claimStackPulls, setStackPolicy, type StackDto, type StackPolicyPatch } from "../api.js";
|
|
4
4
|
import { loadConfig } from "../config.js";
|
|
5
5
|
import { createPrs, findOpenPrNumber } from "../gh.js";
|
|
6
6
|
import { parseGithubOriginRepo } from "../git.js";
|
|
@@ -11,9 +11,7 @@ import { gitTopLevel } from "../repo-identity.js";
|
|
|
11
11
|
import { loadStackMeta, saveStackMeta } from "../stack-meta.js";
|
|
12
12
|
import { pollStackWatch } from "../stack-watch.js";
|
|
13
13
|
import { runStackWatch } from "../stack-watch-loop.js";
|
|
14
|
-
export declare function stackHeldLine(held: StackHeldBlocker | null | undefined): string[];
|
|
15
14
|
export declare function stackRepairLine(repair: StackRepairHint | null | undefined): string[];
|
|
16
|
-
export declare function stackWatchTextLines(watch: StackWatchObligation | null | undefined): string[];
|
|
17
15
|
export declare function openStackWatch(stackId: string, opts?: {
|
|
18
16
|
unread?: boolean;
|
|
19
17
|
}): StackWatchObligation;
|
|
@@ -128,6 +126,22 @@ export type StackCreateDeps = {
|
|
|
128
126
|
};
|
|
129
127
|
export declare function cmdStackCreate(args: string[], deps?: StackCreateDeps): Promise<void>;
|
|
130
128
|
/** Injectable seams for unit tests (STRUCT-04). Production callers omit deps. */
|
|
129
|
+
export declare function issueReviewModeFor(cfg: Awaited<ReturnType<typeof loadConfig>>, owner: string, repo: string): Promise<IssueReviewMode>;
|
|
130
|
+
export type IssueGateLayer = {
|
|
131
|
+
branch: string;
|
|
132
|
+
texts: string[];
|
|
133
|
+
};
|
|
134
|
+
export type IssueGateDeps = {
|
|
135
|
+
getIssueReview?: typeof getIssueReview;
|
|
136
|
+
issueReviewMode?: typeof issueReviewModeFor;
|
|
137
|
+
};
|
|
138
|
+
export declare function assertLinkedIssuesApproved(input: {
|
|
139
|
+
cfg: Awaited<ReturnType<typeof loadConfig>>;
|
|
140
|
+
owner: string;
|
|
141
|
+
repo: string;
|
|
142
|
+
layers: readonly IssueGateLayer[];
|
|
143
|
+
warn: (line: string) => void;
|
|
144
|
+
}, deps?: IssueGateDeps): Promise<void>;
|
|
131
145
|
export type StackSubmitDeps = {
|
|
132
146
|
cwd?: string;
|
|
133
147
|
gitTopLevel?: (cwd?: string) => string;
|
|
@@ -155,7 +169,7 @@ export type StackSubmitDeps = {
|
|
|
155
169
|
adoptStack?: typeof adoptStack;
|
|
156
170
|
ensureUpperPark?: typeof ensureUpperPark;
|
|
157
171
|
env?: NodeJS.ProcessEnv;
|
|
158
|
-
};
|
|
172
|
+
} & IssueGateDeps;
|
|
159
173
|
export declare function cmdStackSubmit(args: string[], deps?: StackSubmitDeps): Promise<void>;
|
|
160
174
|
export declare function parseStackPublishArgs(args: string[]): {
|
|
161
175
|
branch?: string;
|
package/dist/commands/stack.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { cycloneStatusWords, stackBlockers, stackBlockersSummary } from "../stack-blockers.js";
|
|
2
2
|
import { STACK_WATCH_NOT_DONE_SENTENCE, STACK_WATCH_NOTIFY_PATTERN, stackTerminalReason, stackWatchObligation, } from "../stack-watch-obligation.js";
|
|
3
|
-
import { adoptStack, ensureUpperPark, findStackPull, getEnrichedStack, getStackPublication, landNextStack, listMergeQueueEntries, listStacks, openStackPull, claimStackPulls, restackStack, setStackPolicy, } from "../api.js";
|
|
3
|
+
import { adoptStack, ensureUpperPark, findStackPull, getEnrichedStack, getIssueReview, getIssueReviewMode, issueReviewBlocksPr, getSettings, getStackPublication, issueNotApprovedError, NO_LINKED_ISSUE, landNextStack, listRepoSettings, listMergeQueueEntries, listStacks, openStackPull, claimStackPulls, restackStack, setStackPolicy, } from "../api.js";
|
|
4
4
|
import { loadConfig } from "../config.js";
|
|
5
5
|
import { rateLimitedMessage, REVIEW_EXIT, CommandError } from "../errors.js";
|
|
6
6
|
import { createPrs, findOpenPrNumber, ghReady, githubApiFromEnv, } from "../gh.js";
|
|
7
7
|
import { parseGithubOriginRepo } from "../git.js";
|
|
8
8
|
import { branchExists, commitsAheadOf, createBranchFrom, createBranchFromHead, trunkStartRef, currentBranch, defaultLayerBranchName, deleteBranch, discoverTrunk, fetchRemoteBranch, pushBranch, tipCommitMessage, tipCommitSubject, worktreeDirty, } from "../git-stack.js";
|
|
9
|
-
import { buildPrBodyFromCommit } from "../pr-body.js";
|
|
9
|
+
import { buildPrBodyFromCommit, linkedIssueNumbers } from "../pr-body.js";
|
|
10
10
|
import { publishBranchServerSide, assertExplicitStacking, publishWorkingBranch, workingBranchPublication, } from "../server-publish.js";
|
|
11
11
|
import { gitTopLevel } from "../repo-identity.js";
|
|
12
12
|
import { activeLayers, appendLayer, emptyStackMeta, loadStackMeta, removeActiveStack, resetStackMeta, saveStackMeta, selectStackForCreate, setActivePolicy, } from "../stack-meta.js";
|
|
@@ -19,7 +19,7 @@ import { present } from "../ui/present.js";
|
|
|
19
19
|
import { canBrowse } from "./browse.js";
|
|
20
20
|
import { isTransientReviewPollError } from "./review-client.js";
|
|
21
21
|
import { openHelpBrowser } from "./help.js";
|
|
22
|
-
|
|
22
|
+
function stackHeldLine(held) {
|
|
23
23
|
if (!held)
|
|
24
24
|
return [];
|
|
25
25
|
const agents = held.waitingOn.map((agent) => agent === "cyclone" ? "Cyclone" : "Vortex").join(" and ");
|
|
@@ -32,7 +32,7 @@ export function stackRepairLine(repair) {
|
|
|
32
32
|
const note = "note" in repair && repair.note ? ` ${repair.note}` : "";
|
|
33
33
|
return [`Repair #${repair.prNumber} ${repair.kind}${files}: ${repair.steps}${note}`];
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
function stackWatchTextLines(watch) {
|
|
36
36
|
if (!watch)
|
|
37
37
|
return [];
|
|
38
38
|
if (watch.done)
|
|
@@ -680,6 +680,71 @@ export async function cmdStackCreate(args, deps = {}) {
|
|
|
680
680
|
created.push(ansi.dim(" authoring state: managed automatically outside the repository"));
|
|
681
681
|
await present("Stack", created);
|
|
682
682
|
}
|
|
683
|
+
/** Injectable seams for unit tests (STRUCT-04). Production callers omit deps. */
|
|
684
|
+
export async function issueReviewModeFor(cfg, owner, repo) {
|
|
685
|
+
const served = await getIssueReviewMode(cfg, { owner, repo }, { timeoutMs: STACK_GUARD_TIMEOUT_MS });
|
|
686
|
+
if (served)
|
|
687
|
+
return served;
|
|
688
|
+
const repos = await listRepoSettings(cfg, { timeoutMs: STACK_GUARD_TIMEOUT_MS });
|
|
689
|
+
const row = repos.find((r) => r.owner.toLowerCase() === owner.toLowerCase() && r.repo.toLowerCase() === repo.toLowerCase());
|
|
690
|
+
const settings = await getSettings(cfg, { timeoutMs: STACK_GUARD_TIMEOUT_MS });
|
|
691
|
+
if (!settings)
|
|
692
|
+
throw new CommandError("Could not read the account settings");
|
|
693
|
+
const account = settings;
|
|
694
|
+
const on = typeof row?.issue_review_enabled === "boolean" ? row.issue_review_enabled : account.issue_review_enabled === true;
|
|
695
|
+
if (!on)
|
|
696
|
+
return "off";
|
|
697
|
+
const auto = typeof row?.issue_auto_review_enabled === "boolean" ? row.issue_auto_review_enabled : account.issue_auto_review_enabled !== false;
|
|
698
|
+
return auto ? "auto" : "mention";
|
|
699
|
+
}
|
|
700
|
+
export async function assertLinkedIssuesApproved(input, deps = {}) {
|
|
701
|
+
const { cfg, owner, repo, layers, warn } = input;
|
|
702
|
+
if (layers.length === 0)
|
|
703
|
+
return;
|
|
704
|
+
const getIssueReviewFn = deps.getIssueReview ?? getIssueReview;
|
|
705
|
+
const unverified = (err) => {
|
|
706
|
+
if (err instanceof CommandError && (err.code === "auth_invalid" || err.code === "missing_api_key" || err.code === "rate_limited"))
|
|
707
|
+
return err;
|
|
708
|
+
return new CommandError(`Could not verify the Vortex reviews of the linked issues (${err instanceof Error ? err.message : String(err)}); ` +
|
|
709
|
+
"refusing to submit. Retry when the API is reachable.");
|
|
710
|
+
};
|
|
711
|
+
const linked = layers.map((layer) => ({ branch: layer.branch, numbers: linkedIssueNumbers({ owner, repo }, ...layer.texts) }));
|
|
712
|
+
const statuses = [];
|
|
713
|
+
for (const number of [...new Set(linked.flatMap((layer) => layer.numbers))].sort((a, b) => a - b)) {
|
|
714
|
+
let status;
|
|
715
|
+
try {
|
|
716
|
+
status = await getIssueReviewFn(cfg, { owner, repo, number }, { timeoutMs: STACK_GUARD_TIMEOUT_MS });
|
|
717
|
+
}
|
|
718
|
+
catch (err) {
|
|
719
|
+
throw unverified(err);
|
|
720
|
+
}
|
|
721
|
+
if (!status) {
|
|
722
|
+
warn(ansi.dim(" This Mergestorm server does not check issue reviews yet; submitting without the issue gate."));
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
statuses.push(status);
|
|
726
|
+
}
|
|
727
|
+
const unlinked = linked.filter((layer) => layer.numbers.length === 0);
|
|
728
|
+
if (unlinked.length > 0) {
|
|
729
|
+
let mode;
|
|
730
|
+
try {
|
|
731
|
+
mode = await (deps.issueReviewMode ?? issueReviewModeFor)(cfg, owner, repo);
|
|
732
|
+
}
|
|
733
|
+
catch (err) {
|
|
734
|
+
throw unverified(err);
|
|
735
|
+
}
|
|
736
|
+
if (mode === "auto") {
|
|
737
|
+
throw new CommandError([
|
|
738
|
+
`Issue reviews are on for ${owner}/${repo}, and ${unlinked.map((layer) => layer.branch).join(", ")} links no issue.`,
|
|
739
|
+
"Open one with `mergestorm issue create --title <problem> --wait`, reply once to any blocking points in the Vortex review,",
|
|
740
|
+
"then add `Fixes #N` to the tip commit message and submit again.",
|
|
741
|
+
].join("\n"), REVIEW_EXIT.failed, "issue_not_approved", { reason: NO_LINKED_ISSUE });
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
const blocking = statuses.filter(issueReviewBlocksPr);
|
|
745
|
+
if (blocking.length > 0)
|
|
746
|
+
throw issueNotApprovedError(blocking);
|
|
747
|
+
}
|
|
683
748
|
export async function cmdStackSubmit(args, deps = {}) {
|
|
684
749
|
const { extend, autoLand, autoReview, autoPatch, autoResolveConflicts, autoFixCi, rest, serverSide, asJson } = parseStackSubmitArgs(args);
|
|
685
750
|
const api = rest ? "rest" : githubApiFromEnv(deps.env ?? process.env);
|
|
@@ -803,6 +868,21 @@ export async function cmdStackSubmit(args, deps = {}) {
|
|
|
803
868
|
for (const plan of plans) {
|
|
804
869
|
existingPrs.set(plan.branch, await opener.findOpen(plan.branch));
|
|
805
870
|
}
|
|
871
|
+
await assertLinkedIssuesApproved({
|
|
872
|
+
cfg: await loadConfigFn(),
|
|
873
|
+
owner,
|
|
874
|
+
repo,
|
|
875
|
+
layers: plans
|
|
876
|
+
.filter((plan) => existingPrs.get(plan.branch) == null)
|
|
877
|
+
.map((plan) => {
|
|
878
|
+
if (!branchExistsFn(plan.branch, cwd)) {
|
|
879
|
+
throw new CommandError(`Stack layer branch missing locally: ${plan.branch}`);
|
|
880
|
+
}
|
|
881
|
+
const message = tipCommitMessageFn(plan.branch, cwd);
|
|
882
|
+
return { branch: plan.branch, texts: [message, buildPrBodyFn(message)] };
|
|
883
|
+
}),
|
|
884
|
+
warn: progress,
|
|
885
|
+
}, deps);
|
|
806
886
|
const newHeads = plans
|
|
807
887
|
.filter((plan) => existingPrs.get(plan.branch) == null)
|
|
808
888
|
.map((plan) => plan.branch);
|
|
@@ -1487,11 +1567,15 @@ async function cmdStackLand(args) {
|
|
|
1487
1567
|
]);
|
|
1488
1568
|
return;
|
|
1489
1569
|
}
|
|
1570
|
+
const cascadeFailed = body.cascadeFailed;
|
|
1490
1571
|
await present("Stack", [
|
|
1491
1572
|
ansi.brightGreen(` Landed` +
|
|
1492
1573
|
(pr != null ? ` #${pr}` : "") +
|
|
1493
1574
|
(branch ? ` (${branch})` : "") +
|
|
1494
1575
|
` on ${id}`),
|
|
1576
|
+
...(cascadeFailed
|
|
1577
|
+
? [ansi.yellow(` The layers above it were not updated: ${cascadeFailed}`)]
|
|
1578
|
+
: []),
|
|
1495
1579
|
]);
|
|
1496
1580
|
}
|
|
1497
1581
|
export async function cmdStack(args, options = {}) {
|