mergestorm 0.3.64 → 0.3.66
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 +63 -11
- package/dist/api.js +195 -6
- package/dist/automation-catalog.d.ts +5 -1
- package/dist/automation-catalog.js +1 -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 +153 -31
- package/dist/commands/login.d.ts +2 -1
- package/dist/commands/login.js +50 -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 +4 -3
- package/dist/commands/stack.d.ts +19 -5
- package/dist/commands/stack.js +78 -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 +55 -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 +10 -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
|
@@ -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
|
/**
|
package/dist/commands/browse.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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>;
|
package/dist/commands/dismiss.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 });
|
package/dist/commands/help.js
CHANGED
|
@@ -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", "
|
|
47
|
+
cmd("logout", "log out of the active account"),
|
|
47
48
|
cmd("clear / exit", "reprint home · leave the shell"),
|
|
48
49
|
],
|
|
49
50
|
},
|
package/dist/commands/issue.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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 {};
|
package/dist/commands/issue.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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 =
|
|
49
|
+
title = flagValue(arg, args[++i]).trim();
|
|
32
50
|
else if (arg === "--body")
|
|
33
|
-
body =
|
|
51
|
+
body = flagValue(arg, args[++i]);
|
|
34
52
|
else if (arg === "--body-file")
|
|
35
|
-
bodyFile =
|
|
53
|
+
bodyFile = flagValue(arg, args[++i]);
|
|
36
54
|
else if (arg === "--label") {
|
|
37
|
-
const label =
|
|
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
|
-
|
|
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,102 @@ 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 {
|
|
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
|
+
const by = status.approvedBy === "human" ? "a person" : "Vortex";
|
|
139
|
+
return `${status.owner}/${status.repo}#${status.number} is approved by ${by}.`;
|
|
140
|
+
}
|
|
141
|
+
export async function waitForIssueReview(target, opts, deps = {}) {
|
|
142
|
+
const printJson = (status) => console.log(JSON.stringify(opts.jsonShape ? opts.jsonShape(status) : status, null, 2));
|
|
143
|
+
const progress = (line) => (opts.json ? console.error(line) : console.log(line));
|
|
144
|
+
let status;
|
|
145
|
+
try {
|
|
146
|
+
const cfg = await (deps.loadConfig ?? loadConfig)();
|
|
147
|
+
progress(`Waiting for the Vortex review of ${target.owner}/${target.repo}#${target.number} …`);
|
|
148
|
+
status = await (deps.pollIssueReview ?? pollIssueReview)(cfg, target, { timeoutMs: opts.timeoutMs });
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
if (err instanceof IssueReviewTimeoutError) {
|
|
152
|
+
if (opts.json)
|
|
153
|
+
printJson(err.lastStatus);
|
|
154
|
+
else
|
|
155
|
+
console.log(issueReviewLines(err.lastStatus).join("\n"));
|
|
156
|
+
}
|
|
157
|
+
throw asAuthExit(err);
|
|
158
|
+
}
|
|
159
|
+
if (!status) {
|
|
160
|
+
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");
|
|
161
|
+
}
|
|
162
|
+
if (opts.json)
|
|
163
|
+
printJson(status);
|
|
164
|
+
if (status.state === "approved" || status.state === "off") {
|
|
165
|
+
if (!opts.json) {
|
|
166
|
+
console.log(approvedLine(status));
|
|
167
|
+
if (status.next)
|
|
168
|
+
console.log(status.next);
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const refusal = issueNotApprovedError([status], `Vortex has not approved ${status.owner}/${status.repo}#${status.number} yet.`);
|
|
173
|
+
if (opts.json)
|
|
174
|
+
throw new CommandError(refusal.message.split("\n")[0], refusal.exitCode, refusal.code);
|
|
175
|
+
throw refusal;
|
|
66
176
|
}
|
|
67
177
|
export async function cmdIssue(args, deps = {}) {
|
|
68
178
|
const sub = args[0]?.toLowerCase();
|
|
69
179
|
const asksForHelp = (arg) => arg === "-h" || arg === "--help" || arg === "help";
|
|
70
|
-
const
|
|
180
|
+
const restAsksForHelp = (rest) => {
|
|
71
181
|
for (let i = 0; i < rest.length; i++) {
|
|
72
182
|
const arg = rest[i];
|
|
73
183
|
if (asksForHelp(arg))
|
|
@@ -77,22 +187,34 @@ export async function cmdIssue(args, deps = {}) {
|
|
|
77
187
|
}
|
|
78
188
|
return false;
|
|
79
189
|
};
|
|
80
|
-
if (!sub || asksForHelp(sub) || (sub === "create" &&
|
|
190
|
+
if (!sub || asksForHelp(sub) || ((sub === "create" || sub === "wait") && restAsksForHelp(args.slice(1)))) {
|
|
81
191
|
console.log(ISSUE_USAGE);
|
|
82
192
|
return;
|
|
83
193
|
}
|
|
194
|
+
if (sub === "wait") {
|
|
195
|
+
const parsed = parseIssueWaitArgs(args.slice(1));
|
|
196
|
+
const target = resolveTarget(parsed.repo, deps);
|
|
197
|
+
await waitForIssueReview({ ...target, number: parsed.number }, parsed, deps);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
84
200
|
if (sub !== "create")
|
|
85
201
|
throw usage(`unknown issue subcommand: ${sub}`);
|
|
86
202
|
const parsed = parseIssueCreateArgs(args.slice(1), deps.readBody);
|
|
87
|
-
const target = parsed.repo
|
|
88
|
-
if (!target) {
|
|
89
|
-
throw new CommandError("Could not parse GitHub owner/repo from `origin`. Pass --repo <owner/repo>.");
|
|
90
|
-
}
|
|
203
|
+
const target = resolveTarget(parsed.repo, deps);
|
|
91
204
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
205
|
+
const created = { owner: target.owner, repo: target.repo, ...issue };
|
|
206
|
+
if (parsed.json && !parsed.wait) {
|
|
207
|
+
console.log(JSON.stringify(created, null, 2));
|
|
95
208
|
}
|
|
96
|
-
|
|
97
|
-
|
|
209
|
+
else if (!parsed.json) {
|
|
210
|
+
console.log(`${issue.created ? "Opened" : "Already open:"} ${target.owner}/${target.repo}#${issue.number} ${issue.url}`);
|
|
211
|
+
console.log(`Put \`Fixes #${issue.number}\` in the commit or pull request body.`);
|
|
212
|
+
}
|
|
213
|
+
if (!parsed.wait)
|
|
214
|
+
return;
|
|
215
|
+
await waitForIssueReview({ ...target, number: issue.number }, {
|
|
216
|
+
timeoutMs: parsed.timeoutMs ?? ISSUE_REVIEW_DEFAULT_TIMEOUT_MS,
|
|
217
|
+
json: parsed.json,
|
|
218
|
+
jsonShape: (review) => ({ ...created, review }),
|
|
219
|
+
}, deps);
|
|
98
220
|
}
|
package/dist/commands/login.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { type Config } from "../config.js";
|
|
2
|
-
import { type MeResponse } from "../api.js";
|
|
2
|
+
import { type AccountIdentity, type MeResponse } from "../api.js";
|
|
3
3
|
/**
|
|
4
4
|
* Probe /api/v1/me with an in-memory config. Does not write disk.
|
|
5
5
|
* Throws CommandError when the key must not be saved.
|
|
6
6
|
*/
|
|
7
7
|
export declare function validateApiKey(cfg: Config): Promise<MeResponse>;
|
|
8
|
+
export declare function loggedInLine(account: AccountIdentity | null | undefined): string;
|
|
8
9
|
export declare function pendingLoginPath(): string;
|
|
9
10
|
export interface LoginDeps {
|
|
10
11
|
openBrowser: (url: string) => void;
|
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,33 @@ 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
|
+
export function loggedInLine(account) {
|
|
48
|
+
const label = accountLabel(account);
|
|
49
|
+
return label ? `Logged in as ${label}.` : "Logged in.";
|
|
50
|
+
}
|
|
51
|
+
async function storeLogin(cfg, me, opts) {
|
|
52
|
+
const apiKey = cfg.apiKey ?? "";
|
|
53
|
+
const name = accountNameFromMe(me) ?? provisionalAccountName(apiKey);
|
|
54
|
+
const before = await readStore();
|
|
55
|
+
const replaced = Object.hasOwn(before.accounts, name) ? configFor(before, name) : undefined;
|
|
56
|
+
try {
|
|
57
|
+
await saveAccount(name, { apiKey, apiBase: cfg.apiBase }, { activate: true });
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
if (opts.minted && err instanceof CommandError && err.code === "account_limit") {
|
|
61
|
+
await revokeOwnKey(cfg, { timeoutMs: 8_000 });
|
|
62
|
+
}
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
if (replaced?.apiKey && replaced.apiKey !== apiKey) {
|
|
66
|
+
await revokeOwnKey({ ...replaced, ignoreEnv: true }, { timeoutMs: 8_000 });
|
|
67
|
+
}
|
|
68
|
+
return name;
|
|
69
|
+
}
|
|
43
70
|
async function loginWithKey() {
|
|
44
71
|
// Always use muted secret input on TTY so the key never lands in
|
|
45
72
|
// question()/history echo.
|
|
@@ -47,12 +74,12 @@ async function loginWithKey() {
|
|
|
47
74
|
if (!key.startsWith("msk_live_")) {
|
|
48
75
|
throw new CommandError("Key should start with msk_live_");
|
|
49
76
|
}
|
|
50
|
-
const cfg = await
|
|
51
|
-
|
|
52
|
-
const me = await validateApiKey(
|
|
53
|
-
await
|
|
77
|
+
const cfg = await loginConfig();
|
|
78
|
+
const candidate = { apiKey: key, apiBase: apiBase(cfg), ignoreEnv: true };
|
|
79
|
+
const me = await validateApiKey(candidate);
|
|
80
|
+
const name = await storeLogin(candidate, me, { minted: false });
|
|
54
81
|
const prefix = me.key?.prefix?.trim() || "msk_live_…";
|
|
55
|
-
await present("Login", [` Verified ${prefix}… · saved to ${configPath()}`]);
|
|
82
|
+
await present("Login", [` Logged in as ${name}. Verified ${prefix}… · saved to ${configPath()}`]);
|
|
56
83
|
}
|
|
57
84
|
export function pendingLoginPath() {
|
|
58
85
|
return path.join(mergestormHome(), "login-pending.json");
|
|
@@ -140,12 +167,21 @@ async function pollDeviceLogin(pending, retryCommand, recovery) {
|
|
|
140
167
|
throw new CommandError(`Login failed: network error. Check your connection and run \`${retryCommand}\` again.`);
|
|
141
168
|
}
|
|
142
169
|
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);
|
|
170
|
+
const minted = { apiKey: poll.body.api_key, apiBase: pending.apiBase, ignoreEnv: true };
|
|
147
171
|
await clearPendingLogin(pending.deviceCode);
|
|
148
|
-
|
|
172
|
+
let me = accountLabel(poll.body?.account)
|
|
173
|
+
? { account: poll.body.account }
|
|
174
|
+
: null;
|
|
175
|
+
if (!me) {
|
|
176
|
+
try {
|
|
177
|
+
me = await getMe(minted, { timeoutMs: 8_000 });
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
me = null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const name = await storeLogin(minted, me, { minted: true });
|
|
184
|
+
await present("Login", [` Logged in as ${name}. Key saved to ${configPath()}`]);
|
|
149
185
|
return;
|
|
150
186
|
}
|
|
151
187
|
const err = poll.body?.error;
|
|
@@ -177,7 +213,7 @@ async function pollDeviceLogin(pending, retryCommand, recovery) {
|
|
|
177
213
|
throw new CommandError(`Login timed out. ${recovery}`);
|
|
178
214
|
}
|
|
179
215
|
async function loginStart() {
|
|
180
|
-
const cfg = await
|
|
216
|
+
const cfg = await loginConfig();
|
|
181
217
|
const base = apiBase(cfg);
|
|
182
218
|
const pending = pendingFromStart(base, await startDeviceLogin(base));
|
|
183
219
|
await savePendingLogin(pending);
|
|
@@ -213,7 +249,7 @@ export async function cmdLogin(args, deps = { openBrowser }) {
|
|
|
213
249
|
await loginFinish();
|
|
214
250
|
return;
|
|
215
251
|
}
|
|
216
|
-
const cfg = await
|
|
252
|
+
const cfg = await loginConfig();
|
|
217
253
|
const base = apiBase(cfg);
|
|
218
254
|
const pending = pendingFromStart(base, await startDeviceLogin(base));
|
|
219
255
|
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
|
}
|