mergestorm 0.3.54 → 0.3.56

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 CHANGED
@@ -45,12 +45,12 @@ On a TTY, bare `mergestorm` (or `mergestorm shell`) opens a branded REPL: a comp
45
45
 
46
46
  ```
47
47
  ╭──────────────────────────────────────────────────────────────────────────────╮
48
- │ ▀▀▀▀▀▀▀▀▀▀▘ mergestorm v0.3.54 │
48
+ │ ▀▀▀▀▀▀▀▀▀▀▘ mergestorm v0.3.56 │
49
49
  │ ▀▀▀▀▀▀▀▀▀ ● msk_live_… · maelstrom │
50
50
  │ ▀▀▀▀▀▀ [████████░░░░░░░░░░░░░░░░░░░░░░] 25% used │
51
51
  │ ▝▀▀▀▀▀ local reviews + stacked PRs · mergestorm.ai │
52
52
  │ ▝▀▀▘ review a diff · usage for tabs · /help for all commands │
53
- │ ▀ New in v0.3.54: Publish without gh: stack submit --server-side │
53
+ │ ▀ New in v0.3.56: Set automation per repository │
54
54
  ╰──────────────────────────────────────────────────────────────────────────────╯
55
55
  mergestorm
56
56
  ╭──────────────────────────────────────────────────────────────────────────────╮
@@ -164,13 +164,14 @@ mg stack publish --status # absent | pushed | opened | registered
164
164
  - **Exact contents.** Every commit travels with its Git tree SHA and the whole content of each changed file, so binaries, deletions, renames, executable bits and symlinks arrive unchanged. Mergestorm moves the branch only when GitHub builds the same tree, and `mg` checks the returned tree against your local branch.
165
165
  - **Never overwrites surprises.** `mg` looks at the branch on GitHub first. If its files match one of your local commits, only the commits after that one are appended. If they match none (someone else pushed, or you rebased), `mg` stops with `head_changed`; `--replace-head <sha>` replaces exactly that head and nothing else. Mergestorm refuses `main`, `master`, the default branch, protected branches and its own `mg-stack-*`, `mg-park-*` and `mq-batch-*` branches.
166
166
  - **Safe to run again.** After a timeout or an error, run the same command again. It finds the branch, pull request and stack it already made and finishes the missing step; it does not create duplicates.
167
+ - **Read-only home directory.** `mg` keeps the saved login and the local stack state of `stack create` / `stack submit` under `~/.mergestorm`. Where the home directory cannot be written, set `MERGESTORM_HOME` to a writable directory (outside the repository) and every command uses that instead. `MERGESTORM_API_KEY` supplies the key without `mg login`.
167
168
  - **Limits.** 20 commits, 500 changed paths, 2 MiB a file and 8 MiB in total per publication. Submodules and merge commits are refused with the path or the reason. `stack submit --server-side` publishes the first two layers of a stack; a third layer needs a park freeze, which this mode does not create, so add it once the first two are registered.
168
169
 
169
170
  Legacy repo-local `.mergestorm/stack.json` state migrates automatically on the next stack command. If local authoring state is stale or malformed, use `mg stack reset --force`; this clears only pre-submit CLI state and never deletes branches, PRs, or registered stacks.
170
171
 
171
172
  ### `login`
172
173
 
173
- Runs a browser device-authorization flow: prints a short code, opens `https://mergestorm.ai/cli/auth`, and — once you approve — mints and stores an API key in `~/.mergestorm/config.json` (mode `600`). No key copy/paste needed.
174
+ Runs a browser device-authorization flow: prints a short code, opens `https://mergestorm.ai/cli/auth`, and — once you approve — mints and stores an API key in `~/.mergestorm/config.json` (mode `600`), or in `$MERGESTORM_HOME/config.json` when `MERGESTORM_HOME` is set. No key copy/paste needed.
174
175
 
175
176
  For headless or CI environments, use `mergestorm login --key` to paste an existing `msk_live_…` key (create one on the dashboard [Settings → API](https://mergestorm.ai/settings#api)). On a TTY the paste is hidden (not echoed); the CLI verifies the key with the API before saving.
176
177
 
@@ -178,7 +179,7 @@ For an agent, which cannot sit in a prompt while a person signs in, use two step
178
179
 
179
180
  ### `logout`
180
181
 
181
- Removes `~/.mergestorm/config.json` so the stored key is no longer used.
182
+ Removes `~/.mergestorm/config.json` (or `$MERGESTORM_HOME/config.json` when `MERGESTORM_HOME` is set) so the stored key is no longer used.
182
183
 
183
184
  ### `review [base] [head]`
184
185
 
@@ -229,6 +230,7 @@ Require a logged-in key and API support for `GET /api/v1/me` and `GET /api/v1/re
229
230
  |----------|---------|---------|
230
231
  | `MERGESTORM_API_KEY` | API key (overrides the stored config) | — |
231
232
  | `MERGESTORM_API_URL` | API base URL | `https://api.mergestorm.ai` |
233
+ | `MERGESTORM_HOME` | Directory for the saved login and local stack state, for machines whose home directory is read-only | `~/.mergestorm` |
232
234
  | `NO_COLOR` | Disable ANSI colors | — |
233
235
 
234
236
  ## Verify (public source vs npm)
package/dist/api.d.ts CHANGED
@@ -175,6 +175,19 @@ export type SettingsResponse = import("./automation-catalog.js").BearerSettingsV
175
175
  github_connected: boolean;
176
176
  stacks_ready?: boolean;
177
177
  };
178
+ export declare const REPO_SETTINGS_KEYS: readonly ["auto_patch_enabled", "auto_review_enabled", "auto_land_default"];
179
+ export type RepoSettingsKey = (typeof REPO_SETTINGS_KEYS)[number];
180
+ export type RepoSettings = {
181
+ owner: string;
182
+ repo: string;
183
+ } & Record<RepoSettingsKey, boolean | null>;
184
+ export type RepoSettingsPatch = Partial<Record<RepoSettingsKey, boolean | null>>;
185
+ export declare function listRepoSettings(cfg?: Config, opts?: {
186
+ timeoutMs?: number;
187
+ }): Promise<RepoSettings[]>;
188
+ export declare function patchRepoSettings(owner: string, repo: string, patch: RepoSettingsPatch, cfg?: Config, opts?: {
189
+ timeoutMs?: number;
190
+ }): Promise<RepoSettings>;
178
191
  /**
179
192
  * Fetch Bearer /api/v1/settings; null on 404/network/timeouts so panels can
180
193
  * degrade (same contract as {@link getMe}), while 401 still surfaces.
package/dist/api.js CHANGED
@@ -172,6 +172,44 @@ export function applyPrReviewQuery(query, opts) {
172
172
  * The connected flags are read-only there and deliberately absent here.
173
173
  */
174
174
  export { SETTINGS_WRITABLE_KEYS };
175
+ export const REPO_SETTINGS_KEYS = ["auto_patch_enabled", "auto_review_enabled", "auto_land_default"];
176
+ function repoSettingsFailure(status, body, retryAfterSeconds, action) {
177
+ if (status === 429) {
178
+ return new CommandError(rateLimitedMessage(retryAfterSeconds), REVIEW_EXIT.rate_limited, "rate_limited", {
179
+ retryAfterSeconds,
180
+ });
181
+ }
182
+ const message = body?.message;
183
+ if (typeof message === "string" && message.trim())
184
+ return new CommandError(message);
185
+ if (status === 404) {
186
+ return new CommandError("Repository settings are not available on this server yet. Deploy the API update or use the dashboard.");
187
+ }
188
+ return new CommandError(`Failed to ${action} repository settings (HTTP ${status}): ${JSON.stringify(body)}`);
189
+ }
190
+ export async function listRepoSettings(cfg, opts) {
191
+ const resolved = cfg ?? (await loadConfig());
192
+ const { status, body, retryAfterSeconds } = await apiFetch(resolved, "/api/v1/settings/repos", {
193
+ timeoutMs: opts?.timeoutMs,
194
+ });
195
+ if (status !== 200)
196
+ throw repoSettingsFailure(status, body, retryAfterSeconds, "load");
197
+ const repos = body?.repos;
198
+ if (!Array.isArray(repos)) {
199
+ throw new CommandError(`Repository settings came back in an unexpected shape: ${JSON.stringify(body)}`);
200
+ }
201
+ return repos;
202
+ }
203
+ export async function patchRepoSettings(owner, repo, patch, cfg, opts) {
204
+ const resolved = cfg ?? (await loadConfig());
205
+ if (Object.keys(patch).length === 0) {
206
+ throw new CommandError("No repository settings to update.", 2, "usage");
207
+ }
208
+ const { status, body, retryAfterSeconds } = await apiFetch(resolved, `/api/v1/settings/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`, { method: "PATCH", json: patch, timeoutMs: opts?.timeoutMs });
209
+ if (status !== 200)
210
+ throw repoSettingsFailure(status, body, retryAfterSeconds, "update");
211
+ return body;
212
+ }
175
213
  /**
176
214
  * Fetch Bearer /api/v1/settings; null on 404/network/timeouts so panels can
177
215
  * degrade (same contract as {@link getMe}), while 401 still surfaces.
@@ -1,6 +1,6 @@
1
1
  export { DEFAULT_API, apiBase, clearConfig, configPath, keyDisplay, loadConfig, mergestormHome, resolveApiKey, saveConfig, type Config, } from "../config.js";
2
2
  export { BEARER_SETTINGS, BEARER_BOOLEAN_SETTINGS_KEYS, BEARER_SETTINGS_FLAGS, BEARER_SETTINGS_KEYS, BEARER_SETTINGS_LABELS, type BearerSettingsKey, } from "../automation-catalog.js";
3
- export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, SETTINGS_WRITABLE_KEYS, adoptStack, apiFetch, cancelMergeQueueEntry, devicePost, dismissPrFindings, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, getStackPublication, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, openStackIssue, parseRetryAfterSeconds, patchSettings, restackStack, setStackAutoLand, setStackPolicy, stackPolicyBody, submitStackPublication, type ApiFetchInit, type ApiFetchResult, type JobListItem, type MeResponse, type MergeQueueBounceDetail, type MergeQueueBounceKind, type MergeQueueEnqueuedBy, type MergeQueueEnqueuedVia, type MergeQueueEntryDto, type MergeQueueEntryState, type PrDismissedFinding, type PrFindingDismissInput, type PrFindingDismissOutcome, type PrFindingDismissResult, type PrReviewFinding, type PrReviewFindings, type PrVortexReview, type SettingsPatch, type SettingsResponse, type SettingsWritableKey, type StackDto, type StackPolicyPatch, type StackPublication, type StackPublicationRequest, type StackPublicationStatus, type ThreadDetail, type ThreadListItem, } from "../api.js";
3
+ export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, SETTINGS_WRITABLE_KEYS, adoptStack, apiFetch, cancelMergeQueueEntry, devicePost, dismissPrFindings, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, listRepoSettings, patchRepoSettings, REPO_SETTINGS_KEYS, getStackPublication, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, openStackIssue, parseRetryAfterSeconds, patchSettings, restackStack, setStackAutoLand, setStackPolicy, stackPolicyBody, submitStackPublication, type ApiFetchInit, type ApiFetchResult, type JobListItem, type MeResponse, type MergeQueueBounceDetail, type MergeQueueBounceKind, type MergeQueueEnqueuedBy, type MergeQueueEnqueuedVia, type MergeQueueEntryDto, type MergeQueueEntryState, type PrDismissedFinding, type PrFindingDismissInput, type PrFindingDismissOutcome, type PrFindingDismissResult, type PrReviewFinding, type PrReviewFindings, type PrVortexReview, type SettingsPatch, type RepoSettings, type RepoSettingsKey, type RepoSettingsPatch, type SettingsResponse, type SettingsWritableKey, type StackDto, type StackPolicyPatch, type StackPublication, type StackPublicationRequest, type StackPublicationStatus, type ThreadDetail, type ThreadListItem, } from "../api.js";
4
4
  export { CommandError, DetachedError, RATE_LIMITED_HINT, REVIEW_EXIT, isCommandErrorCode, rateLimitedMessage, type CommandErrorCode, type CommandErrorExtras, } from "../errors.js";
5
5
  export { REVIEW_POLL_BACKOFF_MS, REVIEW_POLL_DEFAULT_TIMEOUT_MS, REVIEW_POLL_INTERVAL_MS, REVIEW_POLL_MAX_INTERVAL_MS, REVIEW_POLL_MAX_TRANSIENT_RETRIES, REVIEW_POLL_STRETCH_AFTER_MS, REVIEW_POLL_STRETCH_SPAN_MS, REVIEW_THREAD_SLUG_RE, PrReviewPollTimeoutError, ReviewPollTimeoutError, noPrReviewYetMessage, collectReviewInput, formatReviewSubmitError, isTransientReviewPollError, isTransientReviewPollStatus, loadReviewContext, pollPrVortexReview, pollReview, stretchedPollIntervalMs, submitReview, transientRetryWaitMs, type PollReviewOptions, type PollPrVortexReviewOptions, type ReviewInput, type SubmitReviewOptions, } from "../commands/review-client.js";
6
6
  export { REVIEW_JOB_ENVELOPE_SCHEMA, toReviewJobEnvelope, type ReviewEnvelopeFallbacks, type ReviewJobEnvelope, type ReviewJobRow, } from "../ui/job-envelope.js";
@@ -1,6 +1,6 @@
1
1
  export { DEFAULT_API, apiBase, clearConfig, configPath, keyDisplay, loadConfig, mergestormHome, resolveApiKey, saveConfig, } from "../config.js";
2
2
  export { BEARER_SETTINGS, BEARER_BOOLEAN_SETTINGS_KEYS, BEARER_SETTINGS_FLAGS, BEARER_SETTINGS_KEYS, BEARER_SETTINGS_LABELS, } from "../automation-catalog.js";
3
- export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, SETTINGS_WRITABLE_KEYS, adoptStack, apiFetch, cancelMergeQueueEntry, devicePost, dismissPrFindings, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, getStackPublication, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, openStackIssue, parseRetryAfterSeconds, patchSettings, restackStack, setStackAutoLand, setStackPolicy, stackPolicyBody, submitStackPublication, } from "../api.js";
3
+ export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, SETTINGS_WRITABLE_KEYS, adoptStack, apiFetch, cancelMergeQueueEntry, devicePost, dismissPrFindings, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, listRepoSettings, patchRepoSettings, REPO_SETTINGS_KEYS, getStackPublication, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, openStackIssue, parseRetryAfterSeconds, patchSettings, restackStack, setStackAutoLand, setStackPolicy, stackPolicyBody, submitStackPublication, } from "../api.js";
4
4
  export { CommandError, DetachedError, RATE_LIMITED_HINT, REVIEW_EXIT, isCommandErrorCode, rateLimitedMessage, } from "../errors.js";
5
5
  export { REVIEW_POLL_BACKOFF_MS, REVIEW_POLL_DEFAULT_TIMEOUT_MS, REVIEW_POLL_INTERVAL_MS, REVIEW_POLL_MAX_INTERVAL_MS, REVIEW_POLL_MAX_TRANSIENT_RETRIES, REVIEW_POLL_STRETCH_AFTER_MS, REVIEW_POLL_STRETCH_SPAN_MS, REVIEW_THREAD_SLUG_RE, PrReviewPollTimeoutError, ReviewPollTimeoutError, noPrReviewYetMessage, collectReviewInput, formatReviewSubmitError, isTransientReviewPollError, isTransientReviewPollStatus, loadReviewContext, pollPrVortexReview, pollReview, stretchedPollIntervalMs, submitReview, transientRetryWaitMs, } from "../commands/review-client.js";
6
6
  export { REVIEW_JOB_ENVELOPE_SCHEMA, toReviewJobEnvelope, } from "../ui/job-envelope.js";
@@ -208,6 +208,10 @@ export const COMMAND_REGISTRY = [
208
208
  " [--vortex-skip-check none|neutral] [--vortex-findings failure|neutral|success]",
209
209
  " [--cyclone-fail-check failure|neutral]",
210
210
  " With flags: PATCH those settings and print the stored result",
211
+ "mergestorm settings --repos [--json] Repository-level Auto patch, Auto review and Auto land for every connected repository",
212
+ "mergestorm settings --repo <owner>/<repo> [--auto-patch on|off|inherit] [--auto-review on|off|inherit] [--auto-land on|off|inherit] [--json]",
213
+ " A repository value beats the account setting; a stack override (mergestorm stack set) beats the repository.",
214
+ " inherit clears the repository value so the account setting applies. --auto-land seeds new stacks only.",
211
215
  ],
212
216
  async run(args, ctx) {
213
217
  await cmdSettings(args, { mode: ctx.mode });
@@ -331,6 +335,7 @@ export function formatUsageBody() {
331
335
  Env:
332
336
  MERGESTORM_API_KEY API key (overrides config)
333
337
  MERGESTORM_API_URL API base (default ${DEFAULT_API})
338
+ MERGESTORM_HOME State directory for login and stack state (default ~/.mergestorm)
334
339
  `;
335
340
  }
336
341
  /**
@@ -1,4 +1,4 @@
1
- import { type SettingsPatch, type SettingsResponse } from "../api.js";
1
+ import { type RepoSettings, 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.
@@ -10,13 +10,21 @@ type IgnoreBotOperation = {
10
10
  } | {
11
11
  action: "clear";
12
12
  };
13
+ export type RepoSettingsScope = {
14
+ owner: string;
15
+ repo: string;
16
+ patch: RepoSettingsPatch;
17
+ };
13
18
  export type SettingsArgs = {
14
19
  json: boolean;
15
20
  patch: SettingsPatch;
16
21
  ignoreBots?: IgnoreBotOperation[];
22
+ repo?: RepoSettingsScope;
23
+ listRepos?: boolean;
17
24
  };
25
+ export declare function formatRepoSettingsLines(repos: readonly RepoSettings[]): string[];
18
26
  /** Parse boolean and enum flags plus ordered ignore-list operations. */
19
- export declare function parseSettingsArgs(args: string[]): SettingsArgs;
27
+ export declare function parseSettingsArgs(rawArgs: string[]): SettingsArgs;
20
28
  /** Static printout includes the Config toggles plus enum and login settings. */
21
29
  export declare function formatSettingsLines(settings: SettingsResponse): string[];
22
30
  export declare function cmdSettings(args: string[], opts?: {
@@ -1,4 +1,4 @@
1
- import { getSettings, patchSettings, } from "../api.js";
1
+ import { getSettings, listRepoSettings, patchRepoSettings, patchSettings, REPO_SETTINGS_KEYS, } from "../api.js";
2
2
  import { BEARER_SETTINGS, BEARER_SETTINGS_FLAGS } from "../automation-catalog.js";
3
3
  import { loadConfig } from "../config.js";
4
4
  import { CommandError } from "../errors.js";
@@ -10,6 +10,107 @@ import { buildConfigRows, canBrowse, openTabsBrowser } from "./browse.js";
10
10
  * read-only on the API and deliberately have no flag here.
11
11
  */
12
12
  export const SETTINGS_FLAGS = BEARER_SETTINGS_FLAGS;
13
+ const REPO_FLAG_LABELS = {
14
+ auto_patch_enabled: "Auto patch",
15
+ auto_review_enabled: "Auto review",
16
+ auto_land_default: "Auto land (new stacks)",
17
+ };
18
+ function parseRepoSlug(raw) {
19
+ const match = /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.exec(raw ?? "");
20
+ if (!match)
21
+ throw new CommandError("--repo takes <owner>/<repo>.", 2, "usage");
22
+ return { owner: match[1], repo: match[2] };
23
+ }
24
+ function repoArg(args) {
25
+ const rest = [];
26
+ let slug;
27
+ let found = false;
28
+ let listRepos = false;
29
+ for (let i = 0; i < args.length; i++) {
30
+ const arg = args[i];
31
+ if (arg === "--repos")
32
+ listRepos = true;
33
+ else if (arg === "--repo") {
34
+ found = true;
35
+ slug = args[++i];
36
+ }
37
+ else if (arg.startsWith("--repo=")) {
38
+ found = true;
39
+ slug = arg.slice("--repo=".length);
40
+ }
41
+ else
42
+ rest.push(arg);
43
+ }
44
+ if (found && slug === undefined)
45
+ slug = "";
46
+ return { slug, rest, listRepos };
47
+ }
48
+ function parseRepoSettingsArgs(slug, args) {
49
+ const scope = { ...parseRepoSlug(slug), patch: {} };
50
+ let json = false;
51
+ for (let i = 0; i < args.length; i++) {
52
+ const arg = args[i];
53
+ if (arg === "--json") {
54
+ json = true;
55
+ continue;
56
+ }
57
+ const eq = arg.indexOf("=");
58
+ const flag = eq >= 0 ? arg.slice(0, eq) : arg;
59
+ const key = SETTINGS_FLAGS[flag];
60
+ if (!key) {
61
+ throw new CommandError(`Unknown settings flag: ${flag}. See \`mergestorm --help\`.`, 2, "usage");
62
+ }
63
+ if (!REPO_SETTINGS_KEYS.includes(key)) {
64
+ throw new CommandError(`${flag} is an account setting. With --repo only --auto-patch, --auto-review and --auto-land can be set.`, 2, "usage");
65
+ }
66
+ const value = eq >= 0 ? arg.slice(eq + 1) : args[++i];
67
+ if (value !== "on" && value !== "off" && value !== "inherit") {
68
+ throw new CommandError(`${flag} takes on, off or inherit with --repo.`, 2, "usage");
69
+ }
70
+ scope.patch[key] = value === "inherit" ? null : value === "on";
71
+ }
72
+ return { json, patch: {}, repo: scope };
73
+ }
74
+ function repoSettingWords(value) {
75
+ return value === null ? "inherit (follows the account)" : value ? "on" : "off";
76
+ }
77
+ export function formatRepoSettingsLines(repos) {
78
+ if (repos.length === 0)
79
+ return [" No repositories are connected to this account."];
80
+ const width = Math.max(...Object.values(REPO_FLAG_LABELS).map((label) => label.length));
81
+ return repos.flatMap((entry) => [
82
+ ` ${entry.owner}/${entry.repo}`,
83
+ ...REPO_SETTINGS_KEYS.map((key) => ` ${REPO_FLAG_LABELS[key].padEnd(width + 2)}${repoSettingWords(entry[key])}`),
84
+ ]);
85
+ }
86
+ async function cmdRepoSettings(parsed, opts) {
87
+ const cfg = await loadConfig();
88
+ let repos;
89
+ const scope = parsed.repo;
90
+ if (scope && Object.keys(scope.patch).length > 0) {
91
+ repos = [await patchRepoSettings(scope.owner, scope.repo, scope.patch, cfg)];
92
+ }
93
+ else {
94
+ repos = await listRepoSettings(cfg);
95
+ if (scope) {
96
+ const wanted = `${scope.owner}/${scope.repo}`.toLowerCase();
97
+ repos = repos.filter((entry) => `${entry.owner}/${entry.repo}`.toLowerCase() === wanted);
98
+ if (repos.length === 0) {
99
+ throw new CommandError(`${scope.owner}/${scope.repo} is not connected to this account.`);
100
+ }
101
+ }
102
+ }
103
+ if (parsed.json) {
104
+ const text = JSON.stringify(scope ? repos[0] : { repos }, null, 2);
105
+ if (opts.mode === "shell") {
106
+ await present("Repository settings", text.split("\n"));
107
+ return;
108
+ }
109
+ console.log(text);
110
+ return;
111
+ }
112
+ await present("Repository settings", formatRepoSettingsLines(repos));
113
+ }
13
114
  function canonicalLogin(raw) {
14
115
  const login = raw.trim().toLowerCase().replace(/\[bot\]$/, "");
15
116
  if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(login)) {
@@ -18,7 +119,19 @@ function canonicalLogin(raw) {
18
119
  return login;
19
120
  }
20
121
  /** Parse boolean and enum flags plus ordered ignore-list operations. */
21
- export function parseSettingsArgs(args) {
122
+ export function parseSettingsArgs(rawArgs) {
123
+ const { slug, rest: args, listRepos } = repoArg(rawArgs);
124
+ if (slug !== undefined) {
125
+ if (listRepos)
126
+ throw new CommandError("--repos and --repo cannot be combined.", 2, "usage");
127
+ return parseRepoSettingsArgs(slug, args);
128
+ }
129
+ if (listRepos) {
130
+ const other = args.find((arg) => arg !== "--json");
131
+ if (other)
132
+ throw new CommandError(`--repos lists repository settings and takes only --json, not ${other}.`, 2, "usage");
133
+ return { json: args.includes("--json"), patch: {}, listRepos: true };
134
+ }
22
135
  let json = false;
23
136
  const ignoreBots = [];
24
137
  const patch = {};
@@ -97,7 +210,12 @@ export function formatSettingsLines(settings) {
97
210
  });
98
211
  }
99
212
  export async function cmdSettings(args, opts = {}) {
100
- const { json, patch, ignoreBots } = parseSettingsArgs(args);
213
+ const parsed = parseSettingsArgs(args);
214
+ if (parsed.repo || parsed.listRepos) {
215
+ await cmdRepoSettings(parsed, opts);
216
+ return;
217
+ }
218
+ const { json, patch, ignoreBots } = parsed;
101
219
  const hasPatch = Object.keys(patch).length > 0 || Boolean(ignoreBots?.length);
102
220
  // Bare `settings` on a TTY: the tabbed browser, opened on Config.
103
221
  if (!hasPatch && !json && canBrowse()) {
package/dist/config.d.ts CHANGED
@@ -3,7 +3,7 @@ export type Config = {
3
3
  apiKey?: string;
4
4
  apiBase?: string;
5
5
  };
6
- export declare function mergestormHome(): string;
6
+ export declare function mergestormHome(env?: NodeJS.ProcessEnv): string;
7
7
  export declare function configPath(): string;
8
8
  export declare function loadConfig(): Promise<Config>;
9
9
  export declare function saveConfig(cfg: Config): Promise<void>;
package/dist/config.js CHANGED
@@ -2,8 +2,9 @@ import { mkdir, readFile, writeFile, rm } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import path from "node:path";
4
4
  export const DEFAULT_API = "https://api.mergestorm.ai";
5
- export function mergestormHome() {
6
- return path.join(homedir(), ".mergestorm");
5
+ export function mergestormHome(env = process.env) {
6
+ const override = env.MERGESTORM_HOME?.trim();
7
+ return override ? path.resolve(override) : path.join(homedir(), ".mergestorm");
7
8
  }
8
9
  export function configPath() {
9
10
  return path.join(mergestormHome(), "config.json");
@@ -81,6 +81,8 @@ const CYCLONE_HANDOFF_WORDS = {
81
81
  failed: "Cyclone's patch failed, so it pushed nothing",
82
82
  auto_patch_off: "auto-patch is off for this stack",
83
83
  stack_auto_patch_off: "auto-patch is off for this stack",
84
+ repo_auto_patch_off: "auto-patch is off for this repository and this stack does not turn it on",
85
+ repo_setting_unavailable: "the repository's auto-patch setting could not be read, so Cyclone will not patch until it can",
84
86
  account_auto_patch_off: "auto-patch is off for the account and this stack does not turn it on",
85
87
  cyclone_not_connected: "Cyclone is not connected to this repo",
86
88
  infra_failure: "Cyclone could not start the patch: GitHub sign-in, comment load or checkout kept failing",
@@ -220,7 +222,22 @@ function vortexSkippedSteps(stack, layer, skipReason) {
220
222
  const head = layer.headSha ?? "its live head";
221
223
  const mention = `comment @mergestorm-vortex review on #${layer.prNumber}`;
222
224
  if (skipReason === "auto_review_off") {
223
- return `Vortex auto-review is off for ${stack.owner}/${stack.repo} or for this stack, so it skipped #${layer.prNumber} at ${head} and Auto land waits for a review that will not come. Do not patch or push for this. Tell the human: turning auto-review on (mg stack set ${stack.id} --auto-review on, or the account setting in the dashboard) queues a review of this head by itself. To review only this head and leave auto-review off, ${mention}.`;
225
+ const slug = `${stack.owner}/${stack.repo}`;
226
+ const where = stack.vortexReviewOff === "stack_auto_review_off"
227
+ ? "for this stack"
228
+ : stack.vortexReviewOff === "repo_auto_review_off"
229
+ ? `for the repository ${slug}`
230
+ : stack.vortexReviewOff === "account_auto_review_off"
231
+ ? "for the account"
232
+ : `for ${slug} or for this stack`;
233
+ const lead = `Vortex auto-review is off ${where}, so it skipped #${layer.prNumber} at ${head} and Auto land waits for a review that will not come. Do not patch or push for this.`;
234
+ if (stack.vortexReviewOff === "repo_auto_review_off") {
235
+ return `${lead} Tell the human: turning auto-review on for this stack only (mg stack set ${stack.id} --auto-review on) queues a review of this head by itself. Turning it on for the repository (mg settings --repo ${slug} --auto-review on) applies from the next push and queues nothing, so this head still needs a mention. To review only this head and leave auto-review off, ${mention}.`;
236
+ }
237
+ const sinceTurnedOn = stack.vortexReviewOff
238
+ ? ""
239
+ : ` If auto-review has been turned back on since this head was pushed, the skip is from before that and nothing re-queues it: ${mention}.`;
240
+ return `${lead} Tell the human: turning auto-review on (mg stack set ${stack.id} --auto-review on, or the account setting in the dashboard) queues a review of this head by itself. To review only this head and leave auto-review off, ${mention}.${sinceTurnedOn}`;
224
241
  }
225
242
  if (skipReason === "ignored_author") {
226
243
  return `Vortex skipped #${layer.prNumber} at ${head} because the PR's author is on the account's ignored authors list, so Auto land waits for a review that will not come. Do not patch or push for this. To review this head once, ${mention}. To have this author's PRs reviewed by themselves, tell the human to remove the author from the ignored authors list in the Mergestorm dashboard settings.`;
@@ -313,7 +313,8 @@ export type StackUnitDto = {
313
313
  landPr: StackLayerDto | null;
314
314
  };
315
315
  export type CycloneOwnerMatch = "same" | "different" | "none" | "lookup_failed";
316
- export type CyclonePatchOffReason = "stack_auto_patch_off" | "account_auto_patch_off" | "cyclone_not_connected";
316
+ export type CyclonePatchOffReason = "stack_auto_patch_off" | "repo_auto_patch_off" | "repo_setting_unavailable" | "account_auto_patch_off" | "cyclone_not_connected";
317
+ export type VortexReviewOffReason = "stack_auto_review_off" | "repo_auto_review_off" | "account_auto_review_off";
317
318
  export type CycloneConflictOffReason = "stack_auto_resolve_off" | "account_auto_resolve_off" | "cyclone_not_connected";
318
319
  export type CycloneCiOffReason = "stack_auto_fix_ci_off" | "account_auto_fix_ci_off" | "cyclone_not_connected";
319
320
  export type StackDto = {
@@ -364,12 +365,14 @@ export type StackDto = {
364
365
  } | null;
365
366
  /**
366
367
  * Per-stack Vortex auto-review override. `null` (or missing) follows the
367
- * account `auto_review_enabled` flag; a boolean wins in both directions.
368
+ * repository setting and then the account `auto_review_enabled` flag; a
369
+ * boolean wins in both directions.
368
370
  */
369
371
  autoReviewOverride?: boolean | null;
370
372
  /**
371
373
  * Per-stack Cyclone auto-patch override. `null` (or missing) follows the
372
- * account `auto_patch_enabled` flag; a boolean wins in both directions.
374
+ * repository setting and then the account `auto_patch_enabled` flag; a
375
+ * boolean wins in both directions.
373
376
  */
374
377
  autoPatchOverride?: boolean | null;
375
378
  autoResolveConflictsOverride?: boolean | null;
@@ -395,6 +398,11 @@ export type StackDto = {
395
398
  */
396
399
  cycloneOwnerMatch?: CycloneOwnerMatch;
397
400
  cyclonePatchOff?: CyclonePatchOffReason;
401
+ /**
402
+ * Which level turned Auto review off for this stack: its own override, the
403
+ * repository setting, or the account. Absent while Auto review is on.
404
+ */
405
+ vortexReviewOff?: VortexReviewOffReason;
398
406
  cycloneConflictOff?: CycloneConflictOffReason;
399
407
  cycloneCiOff?: CycloneCiOffReason;
400
408
  infrastructureInstallUserId?: string | null;
package/dist/ui/banner.js CHANGED
@@ -200,6 +200,12 @@ const WHATS_NEW = {
200
200
  "0.3.54": [
201
201
  "Publish without gh: stack submit --server-side",
202
202
  ],
203
+ "0.3.55": [
204
+ "MERGESTORM_HOME moves login and stack state",
205
+ ],
206
+ "0.3.56": [
207
+ "Set automation per repository",
208
+ ],
203
209
  };
204
210
  function densityFromVariant(variant) {
205
211
  if (variant === "mini" || variant === "nano")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mergestorm",
3
- "version": "0.3.54",
3
+ "version": "0.3.56",
4
4
  "description": "Mergestorm CLI (mg) — local reviews & stacked PRs. MIT; build from source: https://github.com/marginsystems/mergestorm-cli",
5
5
  "license": "MIT",
6
6
  "type": "module",