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.
Files changed (76) hide show
  1. package/README.md +28 -9
  2. package/dist/api.d.ts +77 -11
  3. package/dist/api.js +228 -6
  4. package/dist/automation-catalog.d.ts +9 -1
  5. package/dist/automation-catalog.js +2 -0
  6. package/dist/cli.js +21 -19
  7. package/dist/client/index.d.ts +3 -2
  8. package/dist/client/index.js +3 -2
  9. package/dist/commands/account.d.ts +3 -0
  10. package/dist/commands/account.js +75 -0
  11. package/dist/commands/browse.d.ts +1 -9
  12. package/dist/commands/browse.js +10 -8
  13. package/dist/commands/dismiss.d.ts +0 -2
  14. package/dist/commands/dismiss.js +2 -2
  15. package/dist/commands/help.js +3 -2
  16. package/dist/commands/issue.d.ts +24 -6
  17. package/dist/commands/issue.js +157 -31
  18. package/dist/commands/login.js +46 -14
  19. package/dist/commands/logout.js +6 -7
  20. package/dist/commands/memory.d.ts +2 -3
  21. package/dist/commands/memory.js +8 -6
  22. package/dist/commands/registry.d.ts +1 -0
  23. package/dist/commands/registry.js +35 -11
  24. package/dist/commands/review-client.d.ts +0 -3
  25. package/dist/commands/review-client.js +3 -3
  26. package/dist/commands/review.d.ts +1 -1
  27. package/dist/commands/review.js +1 -1
  28. package/dist/commands/settings.d.ts +3 -5
  29. package/dist/commands/settings.js +5 -3
  30. package/dist/commands/stack.d.ts +19 -5
  31. package/dist/commands/stack.js +88 -4
  32. package/dist/commands/whoami.js +14 -10
  33. package/dist/config.d.ts +55 -2
  34. package/dist/config.js +264 -13
  35. package/dist/errors.d.ts +1 -1
  36. package/dist/gh.d.ts +0 -4
  37. package/dist/gh.js +4 -4
  38. package/dist/git.d.ts +1 -1
  39. package/dist/git.js +2 -2
  40. package/dist/memory-api.d.ts +1 -0
  41. package/dist/memory-api.js +2 -2
  42. package/dist/node-version.d.ts +0 -2
  43. package/dist/node-version.js +1 -1
  44. package/dist/pr-body.d.ts +4 -0
  45. package/dist/pr-body.js +15 -0
  46. package/dist/repo-identity.d.ts +2 -1
  47. package/dist/repo-identity.js +8 -4
  48. package/dist/server-publish.d.ts +1 -24
  49. package/dist/server-publish.js +9 -9
  50. package/dist/stack-blockers.d.ts +12 -38
  51. package/dist/stack-blockers.js +57 -33
  52. package/dist/stack-dto.d.ts +3 -0
  53. package/dist/stack-dto.js +15 -2
  54. package/dist/stack-meta.d.ts +2 -2
  55. package/dist/stack-meta.js +1 -1
  56. package/dist/stack-watch-loop.d.ts +2 -13
  57. package/dist/stack-watch-loop.js +12 -12
  58. package/dist/submit-pr-base.d.ts +0 -1
  59. package/dist/submit-pr-base.js +1 -1
  60. package/dist/ui/ansi.d.ts +0 -10
  61. package/dist/ui/ansi.js +2 -2
  62. package/dist/ui/banner.d.ts +3 -1
  63. package/dist/ui/banner.js +13 -3
  64. package/dist/ui/box.d.ts +0 -2
  65. package/dist/ui/box.js +1 -1
  66. package/dist/ui/prompt.d.ts +0 -11
  67. package/dist/ui/prompt.js +7 -7
  68. package/dist/ui/tabs.d.ts +2 -2
  69. package/dist/ui/tabs.js +1 -1
  70. package/dist/ui/width.d.ts +0 -9
  71. package/dist/ui/width.js +3 -3
  72. package/dist/web-api.d.ts +17 -0
  73. package/dist/web-api.js +30 -0
  74. package/package.json +1 -1
  75. package/skill/SKILL.md +4 -0
  76. package/skill/mergestorm-bounce-watch/SKILL.md +1 -1
@@ -1,6 +1,6 @@
1
- export { DEFAULT_API, apiBase, clearConfig, configPath, keyDisplay, loadConfig, mergestormHome, resolveApiKey, saveConfig, type Config, } from "../config.js";
1
+ export { ACCOUNT_LIMIT, DEFAULT_API, accountNameFromMe, apiBase, configPath, keyDisplay, listAccounts, loadConfig, mergestormHome, removeAccount, resolveApiKey, saveAccount, settleAccountName, splitAccountFlag, useAccount, withAccount, type AccountSummary, type Config, type StoredAccount, } 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, dismissThreadsLine, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, listRepoSettings, patchRepoSettings, REPO_SETTINGS_KEYS, getStackPublication, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, openStackIssue, parseRetryAfterSeconds, patchSettings, restackStack, 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 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, accountLabel, apiFetch, cancelMergeQueueEntry, devicePost, dismissPrFindings, dismissThreadsLine, enqueueStack, ensureUpperPark, getEnrichedStack, getIssueReview, ISSUE_REVIEW_DEFAULT_TIMEOUT_MS, ISSUE_REVIEW_POLL_INTERVAL_MS, ISSUE_REVIEW_STATES, IssueReviewTimeoutError, NO_LINKED_ISSUE, issueNotApprovedError, issueNotApprovedRefusal, issueReviewLines, parseIssueReviewStatus, pollIssueReview, type IssueReviewPoint, type IssueReviewState, type IssueReviewStatus, type IssueReviewTarget, type PollIssueReviewOptions, getMe, getPrVortexReview, revokeOwnKey, getReview, getSettings, listRepoSettings, patchRepoSettings, REPO_SETTINGS_KEYS, getStackPublication, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, openStackIssue, parseRetryAfterSeconds, patchSettings, restackStack, setStackPolicy, stackPolicyBody, submitStackPublication, type ApiFetchInit, type ApiFetchResult, type JobListItem, type AccountIdentity, 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 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";
@@ -12,3 +12,4 @@ export { BREEZE_BRANCH_PREFIX, stackBlockers, stackBlockersSummary, sameHead, la
12
12
  export { STACK_WATCH_NOT_DONE_SENTENCE, STACK_WATCH_NOTIFY_PATTERN, stackWatchBackgroundCommand, stackTerminalReason, stackWatchNextArgs, stackWatchObligation, type StackWatchDoneReason, type StackWatchNext, type StackWatchNextArgs, type StackWatchObligation, type StackWatchObligationInput, type StackWatchReason, } from "../stack-watch-obligation.js";
13
13
  export { READ_ONLY_GIT, publishBranchServerSide, publishWorkingBranch, workingBranchPublication, type GitRead, type ServerPublishDeps, type ServerPublishInput, type ServerPublishResult, type WorkingBranchInput, } from "../server-publish.js";
14
14
  export { MEMORY_KINDS, MEMORY_MODES, MEMORY_SCOPES, MEMORY_SEARCH_LIMIT_MAX, deleteMemory, getMemoryMode, listMemories, memoryChatId, saveMemory, searchMemories, setMemoryMode, updateMemory, type MemoryItem, type MemoryKind, type MemoryList, type MemoryMode, type MemoryScope, type MemorySearch, } from "../memory-api.js";
15
+ export { WEB_SEARCH_KINDS, WEB_SEARCH_PAGE_MAX, WEB_SEARCH_RECENCIES, readWebPage, searchWeb, type WebSearchInput, type WebText, } from "../web-api.js";
@@ -1,6 +1,6 @@
1
- export { DEFAULT_API, apiBase, clearConfig, configPath, keyDisplay, loadConfig, mergestormHome, resolveApiKey, saveConfig, } from "../config.js";
1
+ export { ACCOUNT_LIMIT, DEFAULT_API, accountNameFromMe, apiBase, configPath, keyDisplay, listAccounts, loadConfig, mergestormHome, removeAccount, resolveApiKey, saveAccount, settleAccountName, splitAccountFlag, useAccount, withAccount, } 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, dismissThreadsLine, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, listRepoSettings, patchRepoSettings, REPO_SETTINGS_KEYS, getStackPublication, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, openStackIssue, parseRetryAfterSeconds, patchSettings, restackStack, setStackPolicy, stackPolicyBody, submitStackPublication, } from "../api.js";
3
+ export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, SETTINGS_WRITABLE_KEYS, adoptStack, accountLabel, apiFetch, cancelMergeQueueEntry, devicePost, dismissPrFindings, dismissThreadsLine, enqueueStack, ensureUpperPark, getEnrichedStack, getIssueReview, ISSUE_REVIEW_DEFAULT_TIMEOUT_MS, ISSUE_REVIEW_POLL_INTERVAL_MS, ISSUE_REVIEW_STATES, IssueReviewTimeoutError, NO_LINKED_ISSUE, issueNotApprovedError, issueNotApprovedRefusal, issueReviewLines, parseIssueReviewStatus, pollIssueReview, getMe, getPrVortexReview, revokeOwnKey, getReview, getSettings, listRepoSettings, patchRepoSettings, REPO_SETTINGS_KEYS, getStackPublication, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, openStackIssue, parseRetryAfterSeconds, patchSettings, restackStack, 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";
@@ -12,3 +12,4 @@ export { BREEZE_BRANCH_PREFIX, stackBlockers, stackBlockersSummary, sameHead, la
12
12
  export { STACK_WATCH_NOT_DONE_SENTENCE, STACK_WATCH_NOTIFY_PATTERN, stackWatchBackgroundCommand, stackTerminalReason, stackWatchNextArgs, stackWatchObligation, } from "../stack-watch-obligation.js";
13
13
  export { READ_ONLY_GIT, publishBranchServerSide, publishWorkingBranch, workingBranchPublication, } from "../server-publish.js";
14
14
  export { MEMORY_KINDS, MEMORY_MODES, MEMORY_SCOPES, MEMORY_SEARCH_LIMIT_MAX, deleteMemory, getMemoryMode, listMemories, memoryChatId, saveMemory, searchMemories, setMemoryMode, updateMemory, } from "../memory-api.js";
15
+ export { WEB_SEARCH_KINDS, WEB_SEARCH_PAGE_MAX, WEB_SEARCH_RECENCIES, readWebPage, searchWeb, } from "../web-api.js";
@@ -0,0 +1,3 @@
1
+ export declare const ACCOUNT_USAGE = "usage: mergestorm account list [--json] | use <name> | remove <name>";
2
+ export declare function logoutAccount(name: string): Promise<string[]>;
3
+ export declare function cmdAccount(args: string[]): Promise<void>;
@@ -0,0 +1,75 @@
1
+ import { revokeOwnKey } from "../api.js";
2
+ import { ACCOUNT_LIMIT, apiBase, configFor, listAccounts, readStore, removeAccount, useAccount, } from "../config.js";
3
+ import { CommandError } from "../errors.js";
4
+ import { ansi } from "../ui/ansi.js";
5
+ import { present } from "../ui/present.js";
6
+ export const ACCOUNT_USAGE = "usage: mergestorm account list [--json] | use <name> | remove <name>";
7
+ export async function logoutAccount(name) {
8
+ const store = await readStore();
9
+ const cfg = configFor(store, Object.hasOwn(store.accounts, name) ? name : undefined);
10
+ if (!cfg.account)
11
+ await removeAccount(name);
12
+ cfg.apiBase = apiBase(cfg);
13
+ cfg.ignoreEnv = true;
14
+ const revoked = await revokeOwnKey(cfg, { timeoutMs: 8_000 });
15
+ const after = await removeAccount(name);
16
+ const lines = [
17
+ revoked
18
+ ? ` Logged out ${name}. Its API key is revoked.`
19
+ : ` Logged out ${name}. Its API key could not be revoked; revoke it at https://mergestorm.ai/settings#api if it should not stay valid.`,
20
+ ];
21
+ if (after.activeAccount)
22
+ lines.push(` Active account: ${after.activeAccount}`);
23
+ else
24
+ lines.push(" No accounts left. Run `mergestorm login` to sign in.");
25
+ return lines;
26
+ }
27
+ async function accountList(asJson) {
28
+ const accounts = await listAccounts();
29
+ if (asJson) {
30
+ console.log(JSON.stringify({ active: accounts.find((a) => a.active)?.name ?? null, limit: ACCOUNT_LIMIT, accounts }, null, 2));
31
+ return;
32
+ }
33
+ if (accounts.length === 0) {
34
+ await present("Accounts", [" No accounts. Run `mergestorm login` to sign in."]);
35
+ return;
36
+ }
37
+ const width = Math.max(...accounts.map((a) => a.name.length));
38
+ const lines = accounts.map((a) => {
39
+ const mark = a.active ? ansi.brightGreen("●") : ansi.dim("○");
40
+ const name = a.active ? ansi.bold(a.name.padEnd(width)) : a.name.padEnd(width);
41
+ return ` ${mark} ${name} ${ansi.dim(a.keyPrefix)} ${ansi.dim(a.apiBase)}`;
42
+ });
43
+ lines.push("", ` ${ansi.dim(`${accounts.length} of ${ACCOUNT_LIMIT} · account use <name> switches · --account <name> runs one command as another`)}`);
44
+ await present("Accounts", lines);
45
+ }
46
+ function nameArg(rest, verb) {
47
+ const name = rest.find((arg) => !arg.startsWith("-"));
48
+ if (!name) {
49
+ throw new CommandError(`mergestorm account ${verb} needs an account name.\n${ACCOUNT_USAGE}`, 1, "usage");
50
+ }
51
+ return name;
52
+ }
53
+ export async function cmdAccount(args) {
54
+ const asJson = args.includes("--json");
55
+ const [sub = "list", ...rest] = args.filter((arg) => arg !== "--json");
56
+ if (sub === "list") {
57
+ await accountList(asJson);
58
+ return;
59
+ }
60
+ if (sub === "use") {
61
+ const name = nameArg(rest, "use");
62
+ await useAccount(name);
63
+ await present("Account", [` Active account: ${name}`]);
64
+ return;
65
+ }
66
+ if (sub === "remove") {
67
+ await present("Account", await logoutAccount(nameArg(rest, "remove")));
68
+ return;
69
+ }
70
+ if (sub === "--help" || sub === "-h" || sub === "help") {
71
+ console.log(ACCOUNT_USAGE);
72
+ return;
73
+ }
74
+ throw new CommandError(`unknown account subcommand: ${sub}\n${ACCOUNT_USAGE}`, 1, "usage");
75
+ }
@@ -7,6 +7,7 @@ export declare function canBrowse(): boolean;
7
7
  /** Shared job row mapper for the Usage tab and the static `credits` panel. */
8
8
  export declare function toPanelJob(job: JobListItem): UsagePanelJob;
9
9
  export type StatusTabInput = {
10
+ account?: string | null;
10
11
  keyPrefix: string;
11
12
  keyName: string | null;
12
13
  plan: string | null;
@@ -20,19 +21,10 @@ export type StatusTabInput = {
20
21
  resetsAt?: string | null;
21
22
  now?: Date;
22
23
  };
23
- /** Short chrome note — lives on the hint row, not in the Status body. */
24
- export declare function statusReachabilityNote(input: Pick<StatusTabInput, "reachable" | "keyInvalid">): string;
25
24
  /** Status tab body: whoami + credits from `/me`. Reachability is the footer. */
26
25
  export declare function buildStatusTabLines(input: StatusTabInput): string[];
27
26
  /** In-place job detail for the Jobs tab (same envelope as `status <id>`). */
28
27
  export declare function formatJobDetailLines(row: ReviewJobRow): string[];
29
- /** Human labels for the Config tab and the static `settings` printout. */
30
- export declare const SETTINGS_LABELS: Record<"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", string>;
31
- export declare const STACKS_READY_LABEL = "Stacks, merge queue, Auto land";
32
- export declare const STACKS_READY_WORDS: {
33
- readonly on: "ready";
34
- readonly off: "not ready (install Mergestorm Surge)";
35
- };
36
28
  /** Config rows in a stable order: writable toggles, then connected flags. */
37
29
  export declare function buildConfigRows(settings: SettingsResponse): TabsConfigRow[];
38
30
  /**
@@ -1,6 +1,6 @@
1
1
  import { getMe, getReview, getSettings, listJobs, patchSettings, } from "../api.js";
2
2
  import { BEARER_BOOLEAN_SETTINGS_KEYS, BEARER_SETTINGS_LABELS } from "../automation-catalog.js";
3
- import { apiBase, keyDisplay, loadConfig, resolveApiKey } from "../config.js";
3
+ import { apiBase, keyDisplay, loadConfig, resolveApiKey, settleAccountName } from "../config.js";
4
4
  import { CommandError } from "../errors.js";
5
5
  import { ansi } from "../ui/ansi.js";
6
6
  import { humanSummary } from "../ui/human-summary.js";
@@ -23,7 +23,7 @@ export function toPanelJob(job) {
23
23
  };
24
24
  }
25
25
  /** Short chrome note — lives on the hint row, not in the Status body. */
26
- export function statusReachabilityNote(input) {
26
+ function statusReachabilityNote(input) {
27
27
  if (input.keyInvalid)
28
28
  return "key revoked";
29
29
  if (!input.reachable)
@@ -50,9 +50,10 @@ function creditsValue(input) {
50
50
  }
51
51
  /** Status tab body: whoami + credits from `/me`. Reachability is the footer. */
52
52
  export function buildStatusTabLines(input) {
53
- const lines = [
54
- field("Key", `${ansi.bold(input.keyPrefix)}${input.keyName ? ` (${input.keyName})` : ""}`),
55
- ];
53
+ const lines = [];
54
+ if (input.account)
55
+ lines.push(field("Account", ansi.bold(input.account)));
56
+ lines.push(field("Key", `${ansi.bold(input.keyPrefix)}${input.keyName ? ` (${input.keyName})` : ""}`));
56
57
  if (input.plan)
57
58
  lines.push(field("Plan", input.plan));
58
59
  lines.push(field("API", input.apiBase));
@@ -120,9 +121,9 @@ export function formatJobDetailLines(row) {
120
121
  return lines;
121
122
  }
122
123
  /** Human labels for the Config tab and the static `settings` printout. */
123
- export const SETTINGS_LABELS = BEARER_SETTINGS_LABELS;
124
- export const STACKS_READY_LABEL = "Stacks, merge queue, Auto land";
125
- export const STACKS_READY_WORDS = {
124
+ const SETTINGS_LABELS = BEARER_SETTINGS_LABELS;
125
+ const STACKS_READY_LABEL = "Stacks, merge queue, Auto land";
126
+ const STACKS_READY_WORDS = {
126
127
  on: "ready",
127
128
  off: "not ready (install Mergestorm Surge)",
128
129
  };
@@ -219,6 +220,7 @@ export async function openTabsBrowser(initial) {
219
220
  }
220
221
  const data = {
221
222
  statusLines: buildStatusTabLines({
223
+ account: me ? await settleAccountName(cfg, me) : cfg.account,
222
224
  keyPrefix: me?.key.prefix ?? keyDisplay(key) ?? "msk_live_…",
223
225
  keyName: me?.key.name ?? null,
224
226
  plan: me ? (me.plan_label_key ?? me.plan_key) : null,
@@ -1,12 +1,10 @@
1
1
  import { type PrFindingDismissInput, type PrFindingDismissOutcome, type PrFindingDismissResult } from "../api.js";
2
2
  import { type Config } from "../config.js";
3
3
  import { type StackWatchObligation } from "../stack-watch-obligation.js";
4
- export declare const DISMISS_USAGE: string;
5
4
  export type ParsedDismissArgs = PrFindingDismissInput & {
6
5
  json: boolean;
7
6
  };
8
7
  export declare function parseDismissArgs(args: string[]): ParsedDismissArgs;
9
- export declare function dismissWatch(result: PrFindingDismissResult): StackWatchObligation | null;
10
8
  export declare function formatDismissResult(result: PrFindingDismissResult, watch: StackWatchObligation | null): string;
11
9
  export type DismissDeps = {
12
10
  loadConfig?: () => Promise<Config>;
@@ -3,7 +3,7 @@ import { loadConfig } from "../config.js";
3
3
  import { CommandError, REVIEW_EXIT } from "../errors.js";
4
4
  import { stackWatchObligation } from "../stack-watch-obligation.js";
5
5
  import { parseAdoptTarget, withWatch } from "./stack.js";
6
- export const DISMISS_USAGE = [
6
+ const DISMISS_USAGE = [
7
7
  "usage: mergestorm dismiss <owner/repo>#<n> --head <sha> --review <id>",
8
8
  " (--finding <id> [--finding <id>…] | --all) --reason <text> [--evidence <url>] [--json]",
9
9
  " mergestorm dismiss <owner/repo>#<n> --head <sha> --review <id> --preview [--json]",
@@ -84,7 +84,7 @@ export function parseDismissArgs(args) {
84
84
  json,
85
85
  };
86
86
  }
87
- export function dismissWatch(result) {
87
+ function dismissWatch(result) {
88
88
  if (!result.stack_id)
89
89
  return null;
90
90
  return stackWatchObligation({ stackId: result.stack_id, terminal: null, unread: true, freshCursor: true });
@@ -41,9 +41,10 @@ export function buildHelpTabs() {
41
41
  "",
42
42
  rule("Account"),
43
43
  cmd("login", "sign in (--key; agents: --start)"),
44
- cmd("whoami", "key · plan · API"),
44
+ cmd("whoami", "account · key · plan · API"),
45
+ cmd("account", "list · use <name> · remove <name>"),
45
46
  cmd("settings", "automation toggles · Config tab"),
46
- cmd("logout", "forget the stored key"),
47
+ cmd("logout", "log out of the active account"),
47
48
  cmd("clear / exit", "reprint home · leave the shell"),
48
49
  ],
49
50
  },
@@ -1,23 +1,41 @@
1
- import { openStackIssue } from "../api.js";
1
+ import { openStackIssue, pollIssueReview, type IssueReviewStatus } from "../api.js";
2
2
  import { loadConfig } from "../config.js";
3
3
  import { parseGithubOriginRepo } from "../git.js";
4
- export declare const ISSUE_USAGE: string;
4
+ type RepoTarget = {
5
+ owner: string;
6
+ repo: string;
7
+ };
5
8
  export type ParsedIssueArgs = {
6
9
  title: string;
7
10
  body: string;
8
11
  labels: string[];
9
- repo?: {
10
- owner: string;
11
- repo: string;
12
- };
12
+ repo?: RepoTarget;
13
+ json: boolean;
14
+ wait?: boolean;
15
+ timeoutMs?: number;
16
+ };
17
+ export type ParsedIssueWaitArgs = {
18
+ number: number;
19
+ repo?: RepoTarget;
20
+ timeoutMs: number;
13
21
  json: boolean;
14
22
  };
15
23
  export declare function parseIssueCreateArgs(args: string[], readBody?: (path: string) => string): ParsedIssueArgs;
24
+ export declare function parseIssueWaitArgs(args: string[]): ParsedIssueWaitArgs;
16
25
  export type IssueDeps = {
17
26
  cwd?: string;
18
27
  loadConfig?: typeof loadConfig;
19
28
  openStackIssue?: typeof openStackIssue;
29
+ pollIssueReview?: typeof pollIssueReview;
20
30
  parseGithubOriginRepo?: typeof parseGithubOriginRepo;
21
31
  readBody?: (path: string) => string;
22
32
  };
33
+ export declare function waitForIssueReview(target: RepoTarget & {
34
+ number: number;
35
+ }, opts: {
36
+ timeoutMs: number;
37
+ json: boolean;
38
+ jsonShape?: (status: IssueReviewStatus) => unknown;
39
+ }, deps?: IssueDeps): Promise<void>;
23
40
  export declare function cmdIssue(args: string[], deps?: IssueDeps): Promise<void>;
41
+ export {};
@@ -1,18 +1,39 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { openStackIssue } from "../api.js";
2
+ import { ISSUE_REVIEW_DEFAULT_TIMEOUT_MS, IssueReviewTimeoutError, issueNotApprovedError, issueReviewLines, openStackIssue, pollIssueReview, } from "../api.js";
3
3
  import { loadConfig } from "../config.js";
4
4
  import { CommandError, REVIEW_EXIT } from "../errors.js";
5
5
  import { parseGithubOriginRepo } from "../git.js";
6
- export const ISSUE_USAGE = [
6
+ const ISSUE_USAGE = [
7
7
  "usage: mergestorm issue create --title <title> [--body <text> | --body-file <path>]",
8
- " [--label <name>]… [--repo <owner/repo>] [--json]",
8
+ " [--label <name>]… [--repo <owner/repo>] [--wait [--timeout <seconds>]] [--json]",
9
9
  " Opens a GitHub issue through Mergestorm as the Surge App. Needs no gh and no GitHub login.",
10
10
  " Uses existing labels only. Running it again with the same title returns the open issue it already made when that issue is among the newest 100 open issues; an older one, or two calls at once, can still create a duplicate.",
11
+ " --wait waits for the Vortex issue review afterwards, like issue wait.",
12
+ " mergestorm issue wait <number> [--repo <owner/repo>] [--timeout <seconds>] [--json]",
13
+ ` Waits for the Vortex review of the issue (default ${ISSUE_REVIEW_DEFAULT_TIMEOUT_MS / 1000}s). Exits 0 once the issue is approved or issue reviews are off.`,
14
+ ` Exits ${REVIEW_EXIT.failed} when the review has points to answer: reply in a comment on the issue, then wait again. Open the pull request only after approval.`,
11
15
  ].join("\n");
12
- const VALUE_FLAGS = new Set(["--title", "--body", "--body-file", "--label", "--repo"]);
16
+ const VALUE_FLAGS = new Set(["--title", "--body", "--body-file", "--label", "--repo", "--timeout"]);
13
17
  function usage(message) {
14
18
  return new CommandError(message ? `${message}\n${ISSUE_USAGE}` : ISSUE_USAGE, REVIEW_EXIT.usage, "usage");
15
19
  }
20
+ function flagValue(flag, raw) {
21
+ if (raw === undefined || raw.startsWith("--"))
22
+ throw usage(`${flag} needs a value`);
23
+ return raw;
24
+ }
25
+ function parseRepo(raw) {
26
+ const match = /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.exec(raw.trim());
27
+ if (!match)
28
+ throw usage("--repo needs <owner/repo>");
29
+ return { owner: match[1], repo: match[2] };
30
+ }
31
+ function parseTimeoutMs(raw) {
32
+ const seconds = Number(raw);
33
+ if (!Number.isInteger(seconds) || seconds < 1)
34
+ throw usage("--timeout needs a whole number of seconds, at least 1");
35
+ return seconds * 1000;
36
+ }
16
37
  export function parseIssueCreateArgs(args, readBody = (path) => readFileSync(path, "utf8")) {
17
38
  let title = "";
18
39
  let body;
@@ -20,33 +41,30 @@ export function parseIssueCreateArgs(args, readBody = (path) => readFileSync(pat
20
41
  let repo;
21
42
  const labels = [];
22
43
  let json = false;
23
- const value = (flag, raw) => {
24
- if (raw === undefined || raw.startsWith("--"))
25
- throw usage(`${flag} needs a value`);
26
- return raw;
27
- };
44
+ let wait = false;
45
+ let timeoutMs;
28
46
  for (let i = 0; i < args.length; i++) {
29
47
  const arg = args[i];
30
48
  if (arg === "--title")
31
- title = value(arg, args[++i]).trim();
49
+ title = flagValue(arg, args[++i]).trim();
32
50
  else if (arg === "--body")
33
- body = value(arg, args[++i]);
51
+ body = flagValue(arg, args[++i]);
34
52
  else if (arg === "--body-file")
35
- bodyFile = value(arg, args[++i]);
53
+ bodyFile = flagValue(arg, args[++i]);
36
54
  else if (arg === "--label") {
37
- const label = value(arg, args[++i]).trim();
55
+ const label = flagValue(arg, args[++i]).trim();
38
56
  if (!label)
39
57
  throw usage("--label needs a non-empty name");
40
58
  labels.push(label);
41
59
  }
42
- else if (arg === "--repo") {
43
- const match = /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.exec(value(arg, args[++i]).trim());
44
- if (!match)
45
- throw usage("--repo needs <owner/repo>");
46
- repo = { owner: match[1], repo: match[2] };
47
- }
60
+ else if (arg === "--repo")
61
+ repo = parseRepo(flagValue(arg, args[++i]));
48
62
  else if (arg === "--json")
49
63
  json = true;
64
+ else if (arg === "--wait")
65
+ wait = true;
66
+ else if (arg === "--timeout")
67
+ timeoutMs = parseTimeoutMs(flagValue(arg, args[++i]));
50
68
  else
51
69
  throw usage(`unknown argument ${arg}`);
52
70
  }
@@ -54,6 +72,8 @@ export function parseIssueCreateArgs(args, readBody = (path) => readFileSync(pat
54
72
  throw usage("--title is required");
55
73
  if (body !== undefined && bodyFile !== undefined)
56
74
  throw usage("pass --body or --body-file, not both");
75
+ if (timeoutMs !== undefined && !wait)
76
+ throw usage("--timeout requires --wait");
57
77
  if (bodyFile !== undefined) {
58
78
  try {
59
79
  body = readBody(bodyFile);
@@ -62,12 +82,106 @@ export function parseIssueCreateArgs(args, readBody = (path) => readFileSync(pat
62
82
  throw new CommandError(`Could not read ${bodyFile}: ${err instanceof Error ? err.message : String(err)}`);
63
83
  }
64
84
  }
65
- return { title, body: body ?? "", labels, ...(repo ? { repo } : {}), json };
85
+ return {
86
+ title,
87
+ body: body ?? "",
88
+ labels,
89
+ ...(repo ? { repo } : {}),
90
+ json,
91
+ ...(wait ? { wait } : {}),
92
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
93
+ };
94
+ }
95
+ export function parseIssueWaitArgs(args) {
96
+ let number;
97
+ let repo;
98
+ let timeoutMs = ISSUE_REVIEW_DEFAULT_TIMEOUT_MS;
99
+ let json = false;
100
+ for (let i = 0; i < args.length; i++) {
101
+ const arg = args[i];
102
+ if (arg === "--repo")
103
+ repo = parseRepo(flagValue(arg, args[++i]));
104
+ else if (arg === "--timeout")
105
+ timeoutMs = parseTimeoutMs(flagValue(arg, args[++i]));
106
+ else if (arg === "--json")
107
+ json = true;
108
+ else if (!arg.startsWith("--") && number === undefined) {
109
+ const match = /^#?(\d+)$/.exec(arg.trim());
110
+ const n = match ? Number(match[1]) : NaN;
111
+ if (!Number.isSafeInteger(n) || n < 1)
112
+ throw usage(`issue wait needs an issue number, got ${arg}`);
113
+ number = n;
114
+ }
115
+ else
116
+ throw usage(`unknown argument ${arg}`);
117
+ }
118
+ if (number === undefined)
119
+ throw usage("issue wait needs an issue number");
120
+ return { number, ...(repo ? { repo } : {}), timeoutMs, json };
121
+ }
122
+ function resolveTarget(repo, deps) {
123
+ const target = repo ?? (deps.parseGithubOriginRepo ?? parseGithubOriginRepo)(deps.cwd ?? process.cwd());
124
+ if (!target) {
125
+ throw new CommandError("Could not parse GitHub owner/repo from `origin`. Pass --repo <owner/repo>.");
126
+ }
127
+ return target;
128
+ }
129
+ function asAuthExit(err) {
130
+ if (err instanceof CommandError && (err.code === "auth_invalid" || err.code === "missing_api_key")) {
131
+ return new CommandError(err.message, REVIEW_EXIT.auth, err.code);
132
+ }
133
+ return err;
134
+ }
135
+ function approvedLine(status) {
136
+ if (status.state === "off")
137
+ return `Issue reviews are off for ${status.owner}/${status.repo}; #${status.number} needs no approval.`;
138
+ if (status.state === "unreviewed")
139
+ return `Issue reviews for ${status.owner}/${status.repo} run on mention; #${status.number} needs no approval.`;
140
+ if (status.approvedBy === null)
141
+ return `The Vortex review of ${status.owner}/${status.repo}#${status.number} failed, so it does not hold the pull request back.`;
142
+ const by = status.approvedBy === "human" ? "a person" : "Vortex";
143
+ return `${status.owner}/${status.repo}#${status.number} is approved by ${by}.`;
144
+ }
145
+ export async function waitForIssueReview(target, opts, deps = {}) {
146
+ const printJson = (status) => console.log(JSON.stringify(opts.jsonShape ? opts.jsonShape(status) : status, null, 2));
147
+ const progress = (line) => (opts.json ? console.error(line) : console.log(line));
148
+ let status;
149
+ try {
150
+ const cfg = await (deps.loadConfig ?? loadConfig)();
151
+ progress(`Waiting for the Vortex review of ${target.owner}/${target.repo}#${target.number} …`);
152
+ status = await (deps.pollIssueReview ?? pollIssueReview)(cfg, target, { timeoutMs: opts.timeoutMs });
153
+ }
154
+ catch (err) {
155
+ if (err instanceof IssueReviewTimeoutError) {
156
+ if (opts.json)
157
+ printJson(err.lastStatus);
158
+ else
159
+ console.log(issueReviewLines(err.lastStatus).join("\n"));
160
+ }
161
+ throw asAuthExit(err);
162
+ }
163
+ if (!status) {
164
+ throw new CommandError("This Mergestorm server does not review issues yet. Update the API, or open the pull request as before.", 1, "issue_review_unavailable");
165
+ }
166
+ if (opts.json)
167
+ printJson(status);
168
+ if (status.state === "approved" || status.state === "off" || (status.mode === "mention" && status.state === "unreviewed")) {
169
+ if (!opts.json) {
170
+ console.log(approvedLine(status));
171
+ if (status.next)
172
+ console.log(status.next);
173
+ }
174
+ return;
175
+ }
176
+ const refusal = issueNotApprovedError([status], `Vortex has not approved ${status.owner}/${status.repo}#${status.number} yet.`);
177
+ if (opts.json)
178
+ throw new CommandError(refusal.message.split("\n")[0], refusal.exitCode, refusal.code);
179
+ throw refusal;
66
180
  }
67
181
  export async function cmdIssue(args, deps = {}) {
68
182
  const sub = args[0]?.toLowerCase();
69
183
  const asksForHelp = (arg) => arg === "-h" || arg === "--help" || arg === "help";
70
- const createAsksForHelp = (rest) => {
184
+ const restAsksForHelp = (rest) => {
71
185
  for (let i = 0; i < rest.length; i++) {
72
186
  const arg = rest[i];
73
187
  if (asksForHelp(arg))
@@ -77,22 +191,34 @@ export async function cmdIssue(args, deps = {}) {
77
191
  }
78
192
  return false;
79
193
  };
80
- if (!sub || asksForHelp(sub) || (sub === "create" && createAsksForHelp(args.slice(1)))) {
194
+ if (!sub || asksForHelp(sub) || ((sub === "create" || sub === "wait") && restAsksForHelp(args.slice(1)))) {
81
195
  console.log(ISSUE_USAGE);
82
196
  return;
83
197
  }
198
+ if (sub === "wait") {
199
+ const parsed = parseIssueWaitArgs(args.slice(1));
200
+ const target = resolveTarget(parsed.repo, deps);
201
+ await waitForIssueReview({ ...target, number: parsed.number }, parsed, deps);
202
+ return;
203
+ }
84
204
  if (sub !== "create")
85
205
  throw usage(`unknown issue subcommand: ${sub}`);
86
206
  const parsed = parseIssueCreateArgs(args.slice(1), deps.readBody);
87
- const target = parsed.repo ?? (deps.parseGithubOriginRepo ?? parseGithubOriginRepo)(deps.cwd ?? process.cwd());
88
- if (!target) {
89
- throw new CommandError("Could not parse GitHub owner/repo from `origin`. Pass --repo <owner/repo>.");
90
- }
207
+ const target = resolveTarget(parsed.repo, deps);
91
208
  const issue = await (deps.openStackIssue ?? openStackIssue)({ owner: target.owner, repo: target.repo, title: parsed.title, body: parsed.body, labels: parsed.labels }, await (deps.loadConfig ?? loadConfig)());
92
- if (parsed.json) {
93
- console.log(JSON.stringify({ owner: target.owner, repo: target.repo, ...issue }, null, 2));
94
- return;
209
+ const created = { owner: target.owner, repo: target.repo, ...issue };
210
+ if (parsed.json && !parsed.wait) {
211
+ console.log(JSON.stringify(created, null, 2));
95
212
  }
96
- console.log(`${issue.created ? "Opened" : "Already open:"} ${target.owner}/${target.repo}#${issue.number} ${issue.url}`);
97
- console.log(`Put \`Fixes #${issue.number}\` in the commit or pull request body.`);
213
+ else if (!parsed.json) {
214
+ console.log(`${issue.created ? "Opened" : "Already open:"} ${target.owner}/${target.repo}#${issue.number} ${issue.url}`);
215
+ console.log(`Put \`Fixes #${issue.number}\` in the commit or pull request body.`);
216
+ }
217
+ if (!parsed.wait)
218
+ return;
219
+ await waitForIssueReview({ ...target, number: issue.number }, {
220
+ timeoutMs: parsed.timeoutMs ?? ISSUE_REVIEW_DEFAULT_TIMEOUT_MS,
221
+ json: parsed.json,
222
+ jsonShape: (review) => ({ ...created, review }),
223
+ }, deps);
98
224
  }