mergestorm 0.3.9 → 0.3.15
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 +67 -24
- package/dist/api.d.ts +106 -40
- package/dist/api.js +245 -9
- package/dist/client/index.d.ts +5 -0
- package/dist/client/index.js +5 -0
- package/dist/commands/branches.d.ts +6 -2
- package/dist/commands/branches.js +29 -14
- package/dist/commands/browse.d.ts +39 -0
- package/dist/commands/browse.js +259 -0
- package/dist/commands/credits.js +30 -12
- package/dist/commands/help.d.ts +5 -0
- package/dist/commands/help.js +93 -0
- package/dist/commands/jobs.d.ts +11 -2
- package/dist/commands/jobs.js +64 -15
- package/dist/commands/login.js +32 -10
- package/dist/commands/logout.js +2 -1
- package/dist/commands/pr.d.ts +22 -0
- package/dist/commands/pr.js +127 -0
- package/dist/commands/queue.d.ts +12 -0
- package/dist/commands/queue.js +160 -0
- package/dist/commands/registry.d.ts +1 -1
- package/dist/commands/registry.js +115 -33
- package/dist/commands/review-client.d.ts +106 -0
- package/dist/commands/review-client.js +399 -0
- package/dist/commands/review.d.ts +36 -6
- package/dist/commands/review.js +395 -145
- package/dist/commands/settings.d.ts +17 -0
- package/dist/commands/settings.js +89 -0
- package/dist/commands/skill.d.ts +20 -0
- package/dist/commands/skill.js +145 -0
- package/dist/commands/stack.d.ts +36 -2
- package/dist/commands/stack.js +341 -113
- package/dist/commands/status.d.ts +18 -2
- package/dist/commands/status.js +236 -21
- package/dist/commands/whoami.d.ts +3 -1
- package/dist/commands/whoami.js +21 -7
- package/dist/errors.d.ts +18 -2
- package/dist/errors.js +22 -1
- package/dist/git-stack.d.ts +2 -0
- package/dist/git-stack.js +4 -0
- package/dist/git.d.ts +12 -1
- package/dist/git.js +52 -7
- package/dist/shell.d.ts +1 -1
- package/dist/shell.js +39 -28
- package/dist/stack-dto.d.ts +266 -0
- package/dist/stack-dto.js +7 -0
- package/dist/submit-pr-base.d.ts +31 -0
- package/dist/submit-pr-base.js +69 -0
- package/dist/ui/ansi.d.ts +2 -0
- package/dist/ui/ansi.js +2 -0
- package/dist/ui/banner.d.ts +34 -2
- package/dist/ui/banner.js +170 -79
- package/dist/ui/box.d.ts +13 -9
- package/dist/ui/box.js +28 -44
- package/dist/ui/findings.d.ts +4 -1
- package/dist/ui/findings.js +14 -7
- package/dist/ui/job-envelope.d.ts +68 -0
- package/dist/ui/job-envelope.js +35 -0
- package/dist/ui/line-tabs.d.ts +27 -0
- package/dist/ui/line-tabs.js +170 -0
- package/dist/ui/logo.d.ts +10 -2
- package/dist/ui/logo.js +13 -9
- package/dist/ui/present.d.ts +10 -0
- package/dist/ui/present.js +13 -0
- package/dist/ui/prompt.d.ts +148 -0
- package/dist/ui/prompt.js +433 -71
- package/dist/ui/select.js +1 -1
- package/dist/ui/tabs.d.ts +125 -0
- package/dist/ui/tabs.js +480 -0
- package/dist/ui/usage.d.ts +29 -1
- package/dist/ui/usage.js +71 -2
- package/dist/ui/width.d.ts +26 -0
- package/dist/ui/width.js +262 -0
- package/package.json +6 -1
- package/skill/SKILL.md +98 -0
- package/skill/mergestorm-pr-loop/SKILL.md +76 -0
|
@@ -1,5 +1,21 @@
|
|
|
1
|
+
export type ParsedStatusArgs = {
|
|
2
|
+
jobId: string;
|
|
3
|
+
format: "json" | "pretty";
|
|
4
|
+
wait: boolean;
|
|
5
|
+
timeoutMs: number;
|
|
6
|
+
};
|
|
1
7
|
export type StatusOptions = {
|
|
2
|
-
|
|
8
|
+
/** Oneshot defaults to json; the shell defaults to pretty. */
|
|
9
|
+
defaultFormat?: "json" | "pretty";
|
|
3
10
|
signal?: AbortSignal;
|
|
11
|
+
/** When true, Ctrl+C / per-request timeout detaches instead of exiting 5. */
|
|
12
|
+
interactive?: boolean;
|
|
13
|
+
/** Test seam; production uses the two-second poll interval. */
|
|
14
|
+
pollIntervalMs?: number;
|
|
15
|
+
/** Shell: route --json through the hold-to-read panel instead of console.log. */
|
|
16
|
+
mode?: "oneshot" | "shell";
|
|
4
17
|
};
|
|
5
|
-
export declare function
|
|
18
|
+
export declare function parseStatusArgs(args: string[], defaults?: {
|
|
19
|
+
format: "json" | "pretty";
|
|
20
|
+
}): ParsedStatusArgs;
|
|
21
|
+
export declare function cmdStatus(args: string[], opts?: StatusOptions): Promise<void>;
|
package/dist/commands/status.js
CHANGED
|
@@ -1,28 +1,243 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getReview } from "../api.js";
|
|
2
2
|
import { loadConfig } from "../config.js";
|
|
3
|
-
import {
|
|
3
|
+
import { showLinePanel } from "../ui/line-tabs.js";
|
|
4
|
+
import { canBrowse, formatJobDetailLines } from "./browse.js";
|
|
5
|
+
import { CommandError, DetachedError, REVIEW_EXIT, isCommandErrorCode, } from "../errors.js";
|
|
4
6
|
import { ansi } from "../ui/ansi.js";
|
|
5
7
|
import { renderFindings } from "../ui/findings.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
import { toReviewJobEnvelope, } from "../ui/job-envelope.js";
|
|
9
|
+
import { REVIEW_POLL_DEFAULT_TIMEOUT_MS, ReviewPollTimeoutError, pollReview, } from "./review-client.js";
|
|
10
|
+
const STATUS_USAGE = "usage: mergestorm status <job_id> [--json] [--wait] [--timeout seconds]";
|
|
11
|
+
function usageError(message = STATUS_USAGE) {
|
|
12
|
+
return new CommandError(message, REVIEW_EXIT.usage, "usage");
|
|
13
|
+
}
|
|
14
|
+
export function parseStatusArgs(args, defaults = { format: "json" }) {
|
|
15
|
+
let jobId;
|
|
16
|
+
let format = defaults.format;
|
|
17
|
+
let wait = false;
|
|
18
|
+
let timeoutMs = REVIEW_POLL_DEFAULT_TIMEOUT_MS;
|
|
19
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
20
|
+
const a = args[i];
|
|
21
|
+
if (a === "--json") {
|
|
22
|
+
format = "json";
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (a === "--pretty") {
|
|
26
|
+
format = "pretty";
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (a === "--wait") {
|
|
30
|
+
wait = true;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (a === "--no-wait") {
|
|
34
|
+
wait = false;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (a === "--timeout" || a.startsWith("--timeout=")) {
|
|
38
|
+
const value = a === "--timeout" ? args[++i] : a.slice("--timeout=".length);
|
|
39
|
+
const seconds = Number(value);
|
|
40
|
+
if (!value || !Number.isFinite(seconds) || seconds <= 0) {
|
|
41
|
+
throw usageError("--timeout must be a positive number of seconds");
|
|
42
|
+
}
|
|
43
|
+
timeoutMs = Math.round(seconds * 1_000);
|
|
44
|
+
wait = true;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (a.startsWith("-")) {
|
|
48
|
+
throw usageError(`unknown flag: ${a}`);
|
|
49
|
+
}
|
|
50
|
+
if (jobId)
|
|
51
|
+
throw usageError();
|
|
52
|
+
jobId = a;
|
|
53
|
+
}
|
|
54
|
+
if (!jobId)
|
|
55
|
+
throw usageError();
|
|
56
|
+
return { jobId, format, wait, timeoutMs };
|
|
57
|
+
}
|
|
58
|
+
function printPretty(row) {
|
|
59
|
+
const envelope = toReviewJobEnvelope(row);
|
|
60
|
+
console.log(`Status: ${envelope.status}`);
|
|
61
|
+
if (envelope.verdict)
|
|
62
|
+
console.log(`Verdict: ${ansi.bold(envelope.verdict)}`);
|
|
63
|
+
if (envelope.router_mode)
|
|
64
|
+
console.log(`Router: ${envelope.router_mode}`);
|
|
65
|
+
if (envelope.specialists_requested.length > 0) {
|
|
66
|
+
console.log(`Requested: ${envelope.specialists_requested.join(", ")}`);
|
|
67
|
+
}
|
|
68
|
+
if (envelope.specialists_run.length > 0) {
|
|
69
|
+
console.log(`Run: ${envelope.specialists_run.join(", ")}`);
|
|
70
|
+
}
|
|
71
|
+
if (envelope.credits) {
|
|
72
|
+
console.log(`Credits: ${envelope.credits.standard} standard`);
|
|
14
73
|
}
|
|
15
|
-
if (
|
|
16
|
-
console.log(
|
|
74
|
+
if (envelope.summary)
|
|
75
|
+
console.log(envelope.summary);
|
|
76
|
+
if (envelope.error)
|
|
77
|
+
console.log(ansi.red(`Error: ${envelope.error}`));
|
|
78
|
+
renderFindings(envelope.findings, {
|
|
79
|
+
emptyMessage: envelope.status === "completed",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
async function showPretty(row) {
|
|
83
|
+
if (canBrowse()) {
|
|
84
|
+
await showLinePanel("Job", formatJobDetailLines(row));
|
|
17
85
|
return;
|
|
18
86
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
87
|
+
printPretty(row);
|
|
88
|
+
}
|
|
89
|
+
export async function cmdStatus(args, opts = {}) {
|
|
90
|
+
const parsed = parseStatusArgs(args, {
|
|
91
|
+
format: opts.defaultFormat ?? "json",
|
|
92
|
+
});
|
|
93
|
+
const json = parsed.format === "json";
|
|
94
|
+
const interactive = Boolean(opts.interactive && !json);
|
|
95
|
+
const fallback = { jobId: parsed.jobId };
|
|
96
|
+
let jsonPrinted = false;
|
|
97
|
+
const printJson = async (row, extra = {}) => {
|
|
98
|
+
const text = JSON.stringify(toReviewJobEnvelope(row, { ...fallback, ...extra }), null, opts.mode === "shell" ? 2 : undefined);
|
|
99
|
+
if (opts.mode === "shell") {
|
|
100
|
+
await showLinePanel("Job", text.split("\n"));
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
console.log(text);
|
|
104
|
+
}
|
|
105
|
+
jsonPrinted = true;
|
|
106
|
+
};
|
|
107
|
+
try {
|
|
108
|
+
const cfg = await loadConfig();
|
|
109
|
+
if (!parsed.wait) {
|
|
110
|
+
let body;
|
|
111
|
+
try {
|
|
112
|
+
body = await getReview(parsed.jobId, cfg, { signal: opts.signal });
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
if (err instanceof CommandError &&
|
|
116
|
+
(err.code === "auth_invalid" || err.code === "missing_api_key")) {
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
if (err instanceof Error && err.name === "AbortError")
|
|
120
|
+
throw err;
|
|
121
|
+
if (isCommandErrorCode(err, "api_timeout")) {
|
|
122
|
+
if (json)
|
|
123
|
+
await printJson(null, { status: "in_progress", error: err.message });
|
|
124
|
+
throw new CommandError(err.message, REVIEW_EXIT.timeout, "review_timeout");
|
|
125
|
+
}
|
|
126
|
+
if (isCommandErrorCode(err, "rate_limited")) {
|
|
127
|
+
if (json) {
|
|
128
|
+
await printJson(null, {
|
|
129
|
+
status: "rate_limited",
|
|
130
|
+
error: err.message,
|
|
131
|
+
retryAfterSeconds: err.retryAfterSeconds,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
console.log(`Status: rate_limited`);
|
|
136
|
+
console.log(err.message);
|
|
137
|
+
}
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
140
|
+
throw new CommandError(err instanceof Error ? err.message : String(err), REVIEW_EXIT.failed, "review_failed");
|
|
141
|
+
}
|
|
142
|
+
const row = body && typeof body === "object"
|
|
143
|
+
? { ...body, job_id: parsed.jobId }
|
|
144
|
+
: { job_id: parsed.jobId, status: "failed", error: "Invalid review response" };
|
|
145
|
+
if (row.status === "failed" || row.status === "quota_exceeded") {
|
|
146
|
+
if (json)
|
|
147
|
+
await printJson(row);
|
|
148
|
+
else
|
|
149
|
+
await showPretty(row);
|
|
150
|
+
throw new CommandError(`Review ${row.status}: ${row.error ?? ""}`, row.status === "quota_exceeded" ? REVIEW_EXIT.quota : REVIEW_EXIT.failed, row.status === "quota_exceeded" ? "review_quota" : "review_failed");
|
|
151
|
+
}
|
|
152
|
+
if (json)
|
|
153
|
+
await printJson(row);
|
|
154
|
+
else
|
|
155
|
+
await showPretty(row);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (!json) {
|
|
159
|
+
const waitHint = interactive
|
|
160
|
+
? ` Waiting… (${ansi.dim("ctrl+c to detach — job keeps running")})`
|
|
161
|
+
: " Waiting…";
|
|
162
|
+
console.log(`Job ${parsed.jobId}.${waitHint}`);
|
|
163
|
+
}
|
|
164
|
+
let row;
|
|
165
|
+
try {
|
|
166
|
+
row = await pollReview(cfg, parsed.jobId, {
|
|
167
|
+
timeoutMs: parsed.timeoutMs,
|
|
168
|
+
intervalMs: opts.pollIntervalMs,
|
|
169
|
+
signal: opts.signal,
|
|
170
|
+
propagateTimeout: interactive,
|
|
171
|
+
onTick(kind) {
|
|
172
|
+
if (json)
|
|
173
|
+
console.error(kind === "retry" ? "?" : ".");
|
|
174
|
+
else
|
|
175
|
+
process.stdout.write(kind === "retry" ? "?" : ".");
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
if (opts.interactive && err instanceof Error && err.name === "AbortError") {
|
|
181
|
+
throw new DetachedError(parsed.jobId);
|
|
182
|
+
}
|
|
183
|
+
if (interactive && isCommandErrorCode(err, "api_timeout")) {
|
|
184
|
+
throw new DetachedError(parsed.jobId);
|
|
185
|
+
}
|
|
186
|
+
if (err instanceof ReviewPollTimeoutError) {
|
|
187
|
+
const message = `Timed out waiting for review ${parsed.jobId}`;
|
|
188
|
+
if (json) {
|
|
189
|
+
await printJson(err.lastRow, {
|
|
190
|
+
jobId: parsed.jobId,
|
|
191
|
+
status: err.lastRow.status ?? "in_progress",
|
|
192
|
+
error: "Timed out waiting for review.",
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
throw new CommandError(message, REVIEW_EXIT.timeout, "review_timeout");
|
|
196
|
+
}
|
|
197
|
+
if (isCommandErrorCode(err, "rate_limited")) {
|
|
198
|
+
if (json) {
|
|
199
|
+
await printJson(null, {
|
|
200
|
+
status: "rate_limited",
|
|
201
|
+
error: err.message,
|
|
202
|
+
retryAfterSeconds: err.retryAfterSeconds,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
console.log(`Status: rate_limited`);
|
|
207
|
+
console.log(err.message);
|
|
208
|
+
}
|
|
209
|
+
throw err;
|
|
210
|
+
}
|
|
211
|
+
throw err;
|
|
212
|
+
}
|
|
213
|
+
if (!json)
|
|
214
|
+
console.log("");
|
|
215
|
+
if (row.status === "failed" || row.status === "quota_exceeded") {
|
|
216
|
+
const message = `Review ${row.status}: ${row.error ?? ""}`;
|
|
217
|
+
if (json)
|
|
218
|
+
await printJson(row);
|
|
219
|
+
else
|
|
220
|
+
await showPretty(row);
|
|
221
|
+
throw new CommandError(message, row.status === "quota_exceeded" ? REVIEW_EXIT.quota : REVIEW_EXIT.failed, row.status === "quota_exceeded" ? "review_quota" : "review_failed");
|
|
222
|
+
}
|
|
223
|
+
if (json)
|
|
224
|
+
await printJson(row);
|
|
225
|
+
else
|
|
226
|
+
await showPretty(row);
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
if (err instanceof DetachedError)
|
|
230
|
+
throw err;
|
|
231
|
+
if (json &&
|
|
232
|
+
!jsonPrinted &&
|
|
233
|
+
!(err instanceof Error && err.name === "AbortError")) {
|
|
234
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
235
|
+
await printJson(null, { status: "failed", error: message });
|
|
236
|
+
}
|
|
237
|
+
if (err instanceof CommandError &&
|
|
238
|
+
(err.code === "auth_invalid" || err.code === "missing_api_key")) {
|
|
239
|
+
throw new CommandError(err.message, REVIEW_EXIT.auth, err.code);
|
|
240
|
+
}
|
|
241
|
+
throw err;
|
|
242
|
+
}
|
|
28
243
|
}
|
package/dist/commands/whoami.js
CHANGED
|
@@ -2,13 +2,19 @@ import { getMe } from "../api.js";
|
|
|
2
2
|
import { apiBase, configPath, loadConfig, resolveApiKey, keyDisplay } from "../config.js";
|
|
3
3
|
import { CommandError } from "../errors.js";
|
|
4
4
|
import { ansi } from "../ui/ansi.js";
|
|
5
|
-
|
|
5
|
+
import { present } from "../ui/present.js";
|
|
6
|
+
import { canBrowse, openTabsBrowser } from "./browse.js";
|
|
7
|
+
export async function cmdWhoami(args, opts = {}) {
|
|
6
8
|
const asJson = args.includes("--json");
|
|
7
9
|
const cfg = await loadConfig();
|
|
8
10
|
const key = resolveApiKey(cfg);
|
|
9
11
|
if (!key) {
|
|
10
12
|
throw new CommandError("Not logged in. Run `mergestorm login`.");
|
|
11
13
|
}
|
|
14
|
+
if (!asJson && canBrowse()) {
|
|
15
|
+
await openTabsBrowser("status");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
12
18
|
const me = await getMe(cfg);
|
|
13
19
|
const payload = {
|
|
14
20
|
key_prefix: me?.key.prefix ?? keyDisplay(key),
|
|
@@ -20,16 +26,24 @@ export async function cmdWhoami(args) {
|
|
|
20
26
|
usage: me?.usage ?? null,
|
|
21
27
|
};
|
|
22
28
|
if (asJson) {
|
|
23
|
-
|
|
29
|
+
const text = JSON.stringify(payload, null, 2);
|
|
30
|
+
if (opts.mode === "shell") {
|
|
31
|
+
await present("Account", text.split("\n"));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
console.log(text);
|
|
24
35
|
return;
|
|
25
36
|
}
|
|
26
|
-
|
|
37
|
+
const lines = [
|
|
38
|
+
` Key ${ansi.bold(String(payload.key_prefix))}${payload.key_name ? ` (${payload.key_name})` : ""}`,
|
|
39
|
+
];
|
|
27
40
|
if (payload.plan_key) {
|
|
28
|
-
|
|
41
|
+
lines.push(` Plan ${payload.plan_label_key ?? payload.plan_key}`);
|
|
29
42
|
}
|
|
30
|
-
|
|
31
|
-
|
|
43
|
+
lines.push(` API ${payload.api_base}`);
|
|
44
|
+
lines.push(` Config ${payload.config_path}`);
|
|
32
45
|
if (!me) {
|
|
33
|
-
|
|
46
|
+
lines.push(ansi.dim(" (live account details unavailable — older API or offline)"));
|
|
34
47
|
}
|
|
48
|
+
await present("Account", lines);
|
|
35
49
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -2,13 +2,29 @@
|
|
|
2
2
|
* Stable machine codes for CommandError control flow.
|
|
3
3
|
* UX copy in `message` may change freely; match on `code`, never on message text.
|
|
4
4
|
*/
|
|
5
|
-
export type CommandErrorCode = "auth_invalid" | "api_timeout" | "not_a_repo" | "missing_api_key";
|
|
5
|
+
export type CommandErrorCode = "auth_invalid" | "api_timeout" | "not_a_repo" | "not_found" | "missing_api_key" | "registered_parent" | "usage" | "review_quota" | "review_failed" | "review_timeout" | "rate_limited";
|
|
6
|
+
/** Stable one-shot exit codes for machine callers of `mergestorm review`. */
|
|
7
|
+
export declare const REVIEW_EXIT: {
|
|
8
|
+
readonly success: 0;
|
|
9
|
+
readonly usage: 2;
|
|
10
|
+
readonly quota: 3;
|
|
11
|
+
readonly failed: 4;
|
|
12
|
+
readonly timeout: 5;
|
|
13
|
+
readonly auth: 6;
|
|
14
|
+
readonly rate_limited: 7;
|
|
15
|
+
};
|
|
16
|
+
export type CommandErrorExtras = {
|
|
17
|
+
retryAfterSeconds?: number;
|
|
18
|
+
};
|
|
6
19
|
/** Thrown by command handlers; one-shot mode maps `exitCode` onto process.exitCode. */
|
|
7
20
|
export declare class CommandError extends Error {
|
|
8
21
|
readonly exitCode: number;
|
|
9
22
|
readonly code?: CommandErrorCode;
|
|
10
|
-
|
|
23
|
+
readonly retryAfterSeconds?: number;
|
|
24
|
+
constructor(message: string, exitCode?: number, code?: CommandErrorCode, extras?: CommandErrorExtras);
|
|
11
25
|
}
|
|
26
|
+
export declare const RATE_LIMITED_HINT = "Rate limited. Wait for a running review or mergestorm jobs.";
|
|
27
|
+
export declare function rateLimitedMessage(retryAfterSeconds?: number): string;
|
|
12
28
|
export declare function isCommandErrorCode(err: unknown, code: CommandErrorCode): err is CommandError;
|
|
13
29
|
/** Review/status poll aborted by Ctrl+C — shell returns to the prompt. */
|
|
14
30
|
export declare class DetachedError extends Error {
|
package/dist/errors.js
CHANGED
|
@@ -1,14 +1,35 @@
|
|
|
1
|
+
/** Stable one-shot exit codes for machine callers of `mergestorm review`. */
|
|
2
|
+
export const REVIEW_EXIT = {
|
|
3
|
+
success: 0,
|
|
4
|
+
usage: 2,
|
|
5
|
+
quota: 3,
|
|
6
|
+
failed: 4,
|
|
7
|
+
timeout: 5,
|
|
8
|
+
auth: 6,
|
|
9
|
+
rate_limited: 7,
|
|
10
|
+
};
|
|
1
11
|
/** Thrown by command handlers; one-shot mode maps `exitCode` onto process.exitCode. */
|
|
2
12
|
export class CommandError extends Error {
|
|
3
13
|
exitCode;
|
|
4
14
|
code;
|
|
5
|
-
|
|
15
|
+
retryAfterSeconds;
|
|
16
|
+
constructor(message, exitCode = 1, code, extras) {
|
|
6
17
|
super(message);
|
|
7
18
|
this.name = "CommandError";
|
|
8
19
|
this.exitCode = exitCode;
|
|
9
20
|
this.code = code;
|
|
21
|
+
if (extras?.retryAfterSeconds !== undefined) {
|
|
22
|
+
this.retryAfterSeconds = extras.retryAfterSeconds;
|
|
23
|
+
}
|
|
10
24
|
}
|
|
11
25
|
}
|
|
26
|
+
export const RATE_LIMITED_HINT = "Rate limited. Wait for a running review or mergestorm jobs.";
|
|
27
|
+
export function rateLimitedMessage(retryAfterSeconds) {
|
|
28
|
+
if (retryAfterSeconds == null || !Number.isFinite(retryAfterSeconds)) {
|
|
29
|
+
return RATE_LIMITED_HINT;
|
|
30
|
+
}
|
|
31
|
+
return `${RATE_LIMITED_HINT} Retry after ${Math.ceil(retryAfterSeconds)}s.`;
|
|
32
|
+
}
|
|
12
33
|
export function isCommandErrorCode(err, code) {
|
|
13
34
|
return err instanceof CommandError && err.code === code;
|
|
14
35
|
}
|
package/dist/git-stack.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export declare function defaultLayerBranchName(explicit?: string): string;
|
|
|
23
23
|
* (`git rev-list --count base..head`). Used by stack submit to refuse empty layers.
|
|
24
24
|
*/
|
|
25
25
|
export declare function commitsAheadOf(base: string, head: string, cwd?: string): number;
|
|
26
|
+
/** Fetch one remote branch so submit can count commits ahead of a park freeze. */
|
|
27
|
+
export declare function fetchRemoteBranch(name: string, cwd?: string): void;
|
|
26
28
|
/** Push branch to origin and set upstream. */
|
|
27
29
|
export declare function pushBranch(branch: string, cwd?: string): void;
|
|
28
30
|
/** First line of the tip commit on `branch` (for PR title). */
|
package/dist/git-stack.js
CHANGED
|
@@ -107,6 +107,10 @@ export function commitsAheadOf(base, head, cwd = process.cwd()) {
|
|
|
107
107
|
}
|
|
108
108
|
return n;
|
|
109
109
|
}
|
|
110
|
+
/** Fetch one remote branch so submit can count commits ahead of a park freeze. */
|
|
111
|
+
export function fetchRemoteBranch(name, cwd = process.cwd()) {
|
|
112
|
+
git(["fetch", "origin", name], cwd);
|
|
113
|
+
}
|
|
110
114
|
/** Push branch to origin and set upstream. */
|
|
111
115
|
export function pushBranch(branch, cwd = process.cwd()) {
|
|
112
116
|
try {
|
package/dist/git.d.ts
CHANGED
|
@@ -4,7 +4,18 @@ export declare function parseGithubOriginRepo(cwd?: string): {
|
|
|
4
4
|
owner: string;
|
|
5
5
|
repo: string;
|
|
6
6
|
} | null;
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Repo-root absolute path for the current working tree.
|
|
9
|
+
* Kept local (not imported from repo-identity) to avoid a git ↔ repo-identity cycle.
|
|
10
|
+
*/
|
|
11
|
+
export declare function repoRoot(cwd?: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Repo-root-relative names of files changed between base and head that resolve
|
|
14
|
+
* inside the sandbox root. Bounds both the uploaded file contents and the diff
|
|
15
|
+
* itself to the sandbox so tracked files outside it never reach the API.
|
|
16
|
+
*/
|
|
17
|
+
export declare function changedNamesInSandbox(base: string, head: string, cwd?: string, sandboxRoot?: string): Promise<string[]>;
|
|
18
|
+
export declare function collectChangedFiles(base: string, head: string, cwd?: string, sandboxRoot?: string): Promise<{
|
|
8
19
|
path: string;
|
|
9
20
|
content: string;
|
|
10
21
|
}[]>;
|
package/dist/git.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { realpathSync } from "node:fs";
|
|
3
|
-
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { readFile, realpath, stat } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
export function git(args, cwd = process.cwd()) {
|
|
6
6
|
try {
|
|
@@ -33,15 +33,61 @@ export function parseGithubOriginRepo(cwd = process.cwd()) {
|
|
|
33
33
|
* Repo-root absolute path for the current working tree.
|
|
34
34
|
* Kept local (not imported from repo-identity) to avoid a git ↔ repo-identity cycle.
|
|
35
35
|
*/
|
|
36
|
-
function repoRoot(cwd = process.cwd()) {
|
|
36
|
+
export function repoRoot(cwd = process.cwd()) {
|
|
37
37
|
return realpathSync(git(["rev-parse", "--show-toplevel"], cwd).trim());
|
|
38
38
|
}
|
|
39
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Repo-root-relative names of files changed between base and head that resolve
|
|
41
|
+
* inside the sandbox root. Bounds both the uploaded file contents and the diff
|
|
42
|
+
* itself to the sandbox so tracked files outside it never reach the API.
|
|
43
|
+
*/
|
|
44
|
+
export async function changedNamesInSandbox(base, head, cwd = process.cwd(), sandboxRoot) {
|
|
40
45
|
const root = repoRoot(cwd);
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
const bound = sandboxRoot ? await realpath(path.resolve(sandboxRoot)) : root;
|
|
47
|
+
// D/T are included so deletions and type changes stay in the review diff,
|
|
48
|
+
// even though their contents cannot be read from the working tree for upload.
|
|
49
|
+
// -z disables core.quotePath quoting and NUL-terminates names, so paths with
|
|
50
|
+
// spaces or non-ASCII characters survive the split verbatim.
|
|
51
|
+
const names = git(["diff", "--name-only", "-z", "--diff-filter=ACMRDT", `${base}...${head}`], cwd)
|
|
52
|
+
.split("\0")
|
|
44
53
|
.filter(Boolean);
|
|
54
|
+
const kept = [];
|
|
55
|
+
for (const name of names) {
|
|
56
|
+
const abs = path.join(root, name);
|
|
57
|
+
let real;
|
|
58
|
+
try {
|
|
59
|
+
real = await realpath(abs);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Deleted from the working tree: resolve the nearest existing ancestor so
|
|
63
|
+
// the path can still be bounded against the sandbox for the diff pathspec.
|
|
64
|
+
let ancestor = path.dirname(abs);
|
|
65
|
+
let realAncestor;
|
|
66
|
+
while (!realAncestor) {
|
|
67
|
+
try {
|
|
68
|
+
realAncestor = await realpath(ancestor);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
const up = path.dirname(ancestor);
|
|
72
|
+
if (up === ancestor)
|
|
73
|
+
throw new Error(`cannot resolve ${abs}`);
|
|
74
|
+
ancestor = up;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
real = path.join(realAncestor, path.relative(ancestor, abs));
|
|
78
|
+
}
|
|
79
|
+
const relToBound = path.relative(bound, real);
|
|
80
|
+
if (relToBound === ".." || relToBound.startsWith(`..${path.sep}`)) {
|
|
81
|
+
console.warn(`Warning: skipping ${name} for review upload: it resolves outside the ${sandboxRoot ? "sandbox root" : "repo root"}`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
kept.push(name);
|
|
85
|
+
}
|
|
86
|
+
return kept;
|
|
87
|
+
}
|
|
88
|
+
export async function collectChangedFiles(base, head, cwd = process.cwd(), sandboxRoot) {
|
|
89
|
+
const root = repoRoot(cwd);
|
|
90
|
+
const names = await changedNamesInSandbox(base, head, cwd, sandboxRoot);
|
|
45
91
|
const files = [];
|
|
46
92
|
for (const name of names.slice(0, 40)) {
|
|
47
93
|
const abs = path.join(root, name);
|
|
@@ -53,7 +99,6 @@ export async function collectChangedFiles(base, head, cwd = process.cwd()) {
|
|
|
53
99
|
files.push({ path: name, content });
|
|
54
100
|
}
|
|
55
101
|
catch {
|
|
56
|
-
// deleted or unreadable — do not silently drop without a hint
|
|
57
102
|
console.warn(`Warning: could not read ${name} for review upload (resolved from repo root); skipping file contents`);
|
|
58
103
|
}
|
|
59
104
|
}
|
package/dist/shell.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Shell autocomplete + `/help`
|
|
1
|
+
/** Shell autocomplete + `/help` � derived from the command registry (STRUCT-01). */
|
|
2
2
|
export declare const COMMANDS: import("./ui/prompt.js").CommandSpec[];
|
|
3
3
|
/** Strips an optional leading "/" so both "review" and "/review" work. */
|
|
4
4
|
export declare function parseLine(line: string): {
|