mergestorm 0.1.0
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 +81 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +381 -0
- package/package.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# mergestorm CLI
|
|
2
|
+
|
|
3
|
+
Review a local git diff with MergeStorm without opening a GitHub PR — for local loops and CI. Diffs are reviewed by the same engine as PR reviews and each review consumes one standard credit.
|
|
4
|
+
|
|
5
|
+
Requires **Node.js 22+**.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
One-liner (installs the global `mergestorm` binary, then sign in via your browser):
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
curl -fsSL https://mergestorm.ai/install.sh | bash
|
|
13
|
+
mergestorm login
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Or install directly with npm:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g mergestorm
|
|
20
|
+
mergestorm login
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### From this monorepo (development)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm run build --workspace=mergestorm
|
|
27
|
+
node packages/mergestorm-cli/dist/cli.js <command>
|
|
28
|
+
# or skip the build during dev (runs src/cli.ts via tsx):
|
|
29
|
+
npm run dev --workspace=mergestorm -- <command>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Commands
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
mergestorm login Sign in via browser; stores an API key in ~/.mergestorm/config.json
|
|
36
|
+
mergestorm login --key Paste an existing API key instead (headless/CI)
|
|
37
|
+
mergestorm logout Remove the stored API key
|
|
38
|
+
mergestorm review [base] [head] Review git diff base...head (default: main...HEAD)
|
|
39
|
+
mergestorm status <job_id> Poll a review job and print its result
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### `login`
|
|
43
|
+
|
|
44
|
+
Runs a browser device-authorization flow: prints a short code, opens `https://mergestorm.ai/cli/auth`, and — once you approve — mints and stores an API key in `~/.mergestorm/config.json` (mode `600`). No key copy/paste needed.
|
|
45
|
+
|
|
46
|
+
For headless or CI environments, use `mergestorm login --key` to paste an existing `msk_live_…` key (create one on the dashboard [Account page](https://mergestorm.ai/dashboard/account) → **API keys**).
|
|
47
|
+
|
|
48
|
+
### `logout`
|
|
49
|
+
|
|
50
|
+
Removes `~/.mergestorm/config.json` so the stored key is no longer used.
|
|
51
|
+
|
|
52
|
+
### `review [base] [head]`
|
|
53
|
+
|
|
54
|
+
Collects `git diff base...head` (default `main...HEAD`), attaches the changed file contents (up to 40 files, skipping files > 400 KB), auto-attaches context from `./.mergestorm/**/*.md|*.txt`, submits the job, and polls until it finishes (~3 min max). The thread slug is derived from the current branch (`local/<branch>`), so repeated runs on the same branch chain their findings.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
mergestorm review # main...HEAD
|
|
58
|
+
mergestorm review develop # develop...HEAD
|
|
59
|
+
mergestorm review main feat/x # main...feat/x
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Exit code is non-zero on quota limit (402), submission failure, a `failed`/`quota_exceeded` job, or a poll timeout.
|
|
63
|
+
|
|
64
|
+
### `status <job_id>`
|
|
65
|
+
|
|
66
|
+
Fetches a job by id and prints the raw JSON (status, verdict, summary, findings).
|
|
67
|
+
|
|
68
|
+
## Context pack (`.mergestorm/`)
|
|
69
|
+
|
|
70
|
+
Any `*.md` / `*.txt` files under `./.mergestorm/` (one level of nesting) are attached as context so the reviewer understands intent, conventions, or migration notes without you resending history.
|
|
71
|
+
|
|
72
|
+
## Environment
|
|
73
|
+
|
|
74
|
+
| Variable | Purpose | Default |
|
|
75
|
+
|----------|---------|---------|
|
|
76
|
+
| `MERGESTORM_API_KEY` | API key (overrides the stored config) | — |
|
|
77
|
+
| `MERGESTORM_API_URL` | API base URL | `https://api.mergestorm.ai` |
|
|
78
|
+
|
|
79
|
+
## See also
|
|
80
|
+
|
|
81
|
+
- [Review Jobs API reference](../../docs/review-jobs-api.md) — raw HTTP endpoints, webhook signing, thread chains, quota.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
3
|
+
import { mkdir, readFile, writeFile, rm, readdir, stat } from "node:fs/promises";
|
|
4
|
+
import { homedir, platform } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
7
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
8
|
+
const DEFAULT_API = "https://api.mergestorm.ai";
|
|
9
|
+
const CONTEXT_DIR = ".mergestorm";
|
|
10
|
+
function configPath() {
|
|
11
|
+
return path.join(homedir(), ".mergestorm", "config.json");
|
|
12
|
+
}
|
|
13
|
+
async function loadConfig() {
|
|
14
|
+
try {
|
|
15
|
+
const raw = await readFile(configPath(), "utf8");
|
|
16
|
+
return JSON.parse(raw);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function saveConfig(cfg) {
|
|
23
|
+
const dir = path.dirname(configPath());
|
|
24
|
+
await mkdir(dir, { recursive: true });
|
|
25
|
+
await writeFile(configPath(), JSON.stringify(cfg, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
|
|
26
|
+
}
|
|
27
|
+
function apiBase(cfg) {
|
|
28
|
+
return (process.env.MERGESTORM_API_URL?.trim() || cfg.apiBase || DEFAULT_API).replace(/\/$/, "");
|
|
29
|
+
}
|
|
30
|
+
async function apiFetch(cfg, route, init = {}) {
|
|
31
|
+
const key = process.env.MERGESTORM_API_KEY?.trim() || cfg.apiKey;
|
|
32
|
+
if (!key) {
|
|
33
|
+
throw new Error("No API key. Run `mergestorm login` or set MERGESTORM_API_KEY.");
|
|
34
|
+
}
|
|
35
|
+
const headers = {
|
|
36
|
+
Authorization: `Bearer ${key}`,
|
|
37
|
+
...init.headers,
|
|
38
|
+
};
|
|
39
|
+
let body = init.body;
|
|
40
|
+
if (init.json !== undefined) {
|
|
41
|
+
headers["Content-Type"] = "application/json";
|
|
42
|
+
body = JSON.stringify(init.json);
|
|
43
|
+
}
|
|
44
|
+
const res = await fetch(`${apiBase(cfg)}${route}`, { ...init, headers, body });
|
|
45
|
+
const text = await res.text();
|
|
46
|
+
let parsed = text;
|
|
47
|
+
try {
|
|
48
|
+
parsed = JSON.parse(text);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// keep text
|
|
52
|
+
}
|
|
53
|
+
return { status: res.status, body: parsed };
|
|
54
|
+
}
|
|
55
|
+
function git(args, cwd = process.cwd()) {
|
|
56
|
+
const result = spawnSync("git", args, { cwd, encoding: "utf8" });
|
|
57
|
+
if (result.status !== 0) {
|
|
58
|
+
throw new Error(result.stderr?.trim() || `git ${args.join(" ")} failed`);
|
|
59
|
+
}
|
|
60
|
+
return result.stdout;
|
|
61
|
+
}
|
|
62
|
+
async function collectContextFiles(root) {
|
|
63
|
+
const pack = path.join(root, CONTEXT_DIR);
|
|
64
|
+
let entries;
|
|
65
|
+
try {
|
|
66
|
+
entries = await readdir(pack, { withFileTypes: true });
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
const files = [];
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
if (entry.isFile() && (entry.name.toLowerCase().endsWith(".md") || entry.name.toLowerCase().endsWith(".txt"))) {
|
|
74
|
+
const content = await readFile(path.join(pack, entry.name), "utf8");
|
|
75
|
+
files.push({ path: entry.name, content });
|
|
76
|
+
}
|
|
77
|
+
else if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
78
|
+
const nested = await readdir(path.join(pack, entry.name), { withFileTypes: true }).catch(() => []);
|
|
79
|
+
for (const child of nested) {
|
|
80
|
+
if (child.isFile() && (child.name.toLowerCase().endsWith(".md") || child.name.toLowerCase().endsWith(".txt"))) {
|
|
81
|
+
const rel = `${entry.name}/${child.name}`;
|
|
82
|
+
const content = await readFile(path.join(pack, entry.name, child.name), "utf8");
|
|
83
|
+
files.push({ path: rel, content });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return files;
|
|
89
|
+
}
|
|
90
|
+
async function collectChangedFiles(base, head) {
|
|
91
|
+
const names = git(["diff", "--name-only", "--diff-filter=ACMR", `${base}...${head}`])
|
|
92
|
+
.split("\n")
|
|
93
|
+
.map((s) => s.trim())
|
|
94
|
+
.filter(Boolean);
|
|
95
|
+
const files = [];
|
|
96
|
+
for (const name of names.slice(0, 40)) {
|
|
97
|
+
try {
|
|
98
|
+
const st = await stat(path.join(process.cwd(), name));
|
|
99
|
+
if (!st.isFile() || st.size > 400_000)
|
|
100
|
+
continue;
|
|
101
|
+
const content = await readFile(path.join(process.cwd(), name), "utf8");
|
|
102
|
+
files.push({ path: name, content });
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// deleted or unreadable
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return files;
|
|
109
|
+
}
|
|
110
|
+
function printBanner() {
|
|
111
|
+
console.log(`
|
|
112
|
+
┌─────────────────────────────┐
|
|
113
|
+
│ MergeStorm CLI │
|
|
114
|
+
│ review local diffs fast │
|
|
115
|
+
└─────────────────────────────┘
|
|
116
|
+
`);
|
|
117
|
+
}
|
|
118
|
+
function usage() {
|
|
119
|
+
printBanner();
|
|
120
|
+
console.log(`Usage:
|
|
121
|
+
mergestorm login Sign in via browser and store an API key
|
|
122
|
+
mergestorm login --key Paste an existing API key instead (headless/CI)
|
|
123
|
+
mergestorm logout Remove the stored API key
|
|
124
|
+
mergestorm review [base] [head] Review git diff base...head (default: main...HEAD)
|
|
125
|
+
mergestorm status <job_id> Poll a review job
|
|
126
|
+
|
|
127
|
+
Env:
|
|
128
|
+
MERGESTORM_API_KEY API key (overrides config)
|
|
129
|
+
MERGESTORM_API_URL API base (default ${DEFAULT_API})
|
|
130
|
+
`);
|
|
131
|
+
}
|
|
132
|
+
/** Best-effort open a URL in the user's browser; silent if no opener exists. */
|
|
133
|
+
function openBrowser(url) {
|
|
134
|
+
const os = platform();
|
|
135
|
+
const cmd = os === "darwin" ? "open" : os === "win32" ? "cmd" : "xdg-open";
|
|
136
|
+
const args = os === "win32" ? ["/c", "start", "", url] : [url];
|
|
137
|
+
try {
|
|
138
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
139
|
+
child.on("error", () => { });
|
|
140
|
+
child.unref();
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// no-op — the URL is printed for manual open
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function devicePost(base, route, json) {
|
|
147
|
+
const res = await fetch(`${base}${route}`, {
|
|
148
|
+
method: "POST",
|
|
149
|
+
headers: { "Content-Type": "application/json" },
|
|
150
|
+
body: JSON.stringify(json),
|
|
151
|
+
});
|
|
152
|
+
const text = await res.text();
|
|
153
|
+
let parsed = text;
|
|
154
|
+
try {
|
|
155
|
+
parsed = JSON.parse(text);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// keep text
|
|
159
|
+
}
|
|
160
|
+
return { status: res.status, body: parsed };
|
|
161
|
+
}
|
|
162
|
+
async function cmdLoginWithKey() {
|
|
163
|
+
const rl = createInterface({ input, output });
|
|
164
|
+
try {
|
|
165
|
+
const key = (await rl.question("Paste your MergeStorm API key (msk_live_...): ")).trim();
|
|
166
|
+
if (!key.startsWith("msk_live_")) {
|
|
167
|
+
console.error("Key should start with msk_live_");
|
|
168
|
+
process.exitCode = 1;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const cfg = await loadConfig();
|
|
172
|
+
cfg.apiKey = key;
|
|
173
|
+
await saveConfig(cfg);
|
|
174
|
+
console.log(`Saved to ${configPath()}`);
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
rl.close();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function cmdLogin(args) {
|
|
181
|
+
printBanner();
|
|
182
|
+
if (args.includes("--key")) {
|
|
183
|
+
await cmdLoginWithKey();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const cfg = await loadConfig();
|
|
187
|
+
const base = apiBase(cfg);
|
|
188
|
+
let start;
|
|
189
|
+
try {
|
|
190
|
+
start = await devicePost(base, "/api/v1/cli/device", {});
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
console.error("Could not start device login. The API may be unreachable or misconfigured.\n" +
|
|
194
|
+
"Run `mergestorm login --key` to paste an existing API key instead.");
|
|
195
|
+
process.exitCode = 1;
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (start.status !== 200 || !start.body?.device_code) {
|
|
199
|
+
console.error(`Could not start device login (HTTP ${start.status}). The API may be unreachable or misconfigured.\n` +
|
|
200
|
+
`Run \`mergestorm login --key\` to paste an existing API key instead.`);
|
|
201
|
+
process.exitCode = 1;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const { device_code, user_code, verification_uri, verification_uri_complete, interval, expires_in, } = start.body;
|
|
205
|
+
const openUrl = verification_uri_complete || verification_uri;
|
|
206
|
+
console.log("To sign in, open this URL and confirm the code:\n");
|
|
207
|
+
console.log(` ${openUrl}`);
|
|
208
|
+
console.log(`\n Code: ${user_code}\n`);
|
|
209
|
+
openBrowser(openUrl);
|
|
210
|
+
console.log("Waiting for approval…");
|
|
211
|
+
const pollMs = Math.max((interval || 5), 2) * 1000;
|
|
212
|
+
const deadline = Date.now() + (expires_in || 600) * 1000;
|
|
213
|
+
while (Date.now() < deadline) {
|
|
214
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
215
|
+
let poll;
|
|
216
|
+
try {
|
|
217
|
+
poll = await devicePost(base, "/api/v1/cli/device/token", { device_code });
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
console.error("\nLogin failed: network error. Check your connection and run `mergestorm login` again.");
|
|
221
|
+
process.exitCode = 1;
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (poll.status === 200 && poll.body?.api_key) {
|
|
225
|
+
const next = await loadConfig();
|
|
226
|
+
next.apiKey = poll.body.api_key;
|
|
227
|
+
await saveConfig(next);
|
|
228
|
+
console.log(`\nLogged in. Key saved to ${configPath()}`);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const err = poll.body?.error;
|
|
232
|
+
if (err === "authorization_pending") {
|
|
233
|
+
process.stdout.write(".");
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (err === "key_limit_reached") {
|
|
237
|
+
console.error("\nYou have reached the max active API keys. Revoke one at https://mergestorm.ai/dashboard/account and try again.");
|
|
238
|
+
process.exitCode = 1;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (err === "expired_token") {
|
|
242
|
+
console.error("\nLogin code expired. Run `mergestorm login` again.");
|
|
243
|
+
process.exitCode = 1;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (err === "already_redeemed") {
|
|
247
|
+
console.error("\nThis code was already used. Run `mergestorm login` again.");
|
|
248
|
+
process.exitCode = 1;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
console.error(`\nLogin failed: ${typeof err === "string" ? err : JSON.stringify(poll.body)}`);
|
|
252
|
+
process.exitCode = 1;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
console.error("\nLogin timed out. Run `mergestorm login` again.");
|
|
256
|
+
process.exitCode = 1;
|
|
257
|
+
}
|
|
258
|
+
async function cmdLogout() {
|
|
259
|
+
try {
|
|
260
|
+
await rm(configPath(), { force: true });
|
|
261
|
+
console.log("Logged out. Removed stored API key.");
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
console.error("Could not remove config:", err instanceof Error ? err.message : err);
|
|
265
|
+
process.exitCode = 1;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
async function cmdReview(args) {
|
|
269
|
+
const base = args[0] || "main";
|
|
270
|
+
const head = args[1] || "HEAD";
|
|
271
|
+
const cfg = await loadConfig();
|
|
272
|
+
console.log(`Collecting diff ${base}...${head} …`);
|
|
273
|
+
const diff = git(["diff", `${base}...${head}`]);
|
|
274
|
+
if (!diff.trim()) {
|
|
275
|
+
console.log("No changes to review.");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const files = await collectChangedFiles(base, head);
|
|
279
|
+
const context_files = await collectContextFiles(process.cwd());
|
|
280
|
+
const branch = git(["rev-parse", "--abbrev-ref", "HEAD"]).trim() || "local";
|
|
281
|
+
const thread = `local/${branch}`.replace(/[^a-zA-Z0-9._/-]/g, "-").slice(0, 120);
|
|
282
|
+
console.log(`Submitting review (${Buffer.byteLength(diff, "utf8")} byte diff, ${files.length} file(s), ${context_files.length} context file(s))…`);
|
|
283
|
+
const { status, body } = await apiFetch(cfg, "/api/v1/reviews", {
|
|
284
|
+
method: "POST",
|
|
285
|
+
json: {
|
|
286
|
+
thread,
|
|
287
|
+
base_label: base,
|
|
288
|
+
head_label: head,
|
|
289
|
+
diff,
|
|
290
|
+
files,
|
|
291
|
+
context_files,
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
if (status === 402) {
|
|
295
|
+
console.error("Monthly review limit reached. Upgrade at https://mergestorm.ai/billing");
|
|
296
|
+
process.exitCode = 1;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (status !== 202 && status !== 200) {
|
|
300
|
+
console.error("Failed:", JSON.stringify(body, null, 2));
|
|
301
|
+
process.exitCode = 1;
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const job = body;
|
|
305
|
+
console.log(`Job ${job.job_id} (${job.status}). Waiting…`);
|
|
306
|
+
for (let i = 0; i < 90; i += 1) {
|
|
307
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
308
|
+
const poll = await apiFetch(cfg, `/api/v1/reviews/${job.job_id}`);
|
|
309
|
+
if (poll.status !== 200) {
|
|
310
|
+
console.error("\nPoll failed:", JSON.stringify(poll.body, null, 2));
|
|
311
|
+
process.exitCode = 1;
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const row = poll.body;
|
|
315
|
+
if (row.status === "queued" || row.status === "in_progress") {
|
|
316
|
+
process.stdout.write(".");
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
console.log("");
|
|
320
|
+
if (row.status === "failed" || row.status === "quota_exceeded") {
|
|
321
|
+
console.error(`Review ${row.status}: ${row.error ?? ""}`);
|
|
322
|
+
process.exitCode = 1;
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
console.log(`\nVerdict: ${row.verdict ?? "comment"}`);
|
|
326
|
+
console.log(row.summary ?? "");
|
|
327
|
+
const inline = row.findings?.inline ?? [];
|
|
328
|
+
const off = row.findings?.off_diff ?? row.findings?.offDiff ?? [];
|
|
329
|
+
for (const c of inline) {
|
|
330
|
+
console.log(`\n[${c.severity}] ${c.path}:${c.line}${c.title ? ` — ${c.title}` : ""}`);
|
|
331
|
+
console.log(c.body);
|
|
332
|
+
}
|
|
333
|
+
for (const c of off) {
|
|
334
|
+
console.log(`\n[${c.severity}] off-diff ${c.path ?? ""}`);
|
|
335
|
+
console.log(c.body);
|
|
336
|
+
}
|
|
337
|
+
if (inline.length === 0 && off.length === 0) {
|
|
338
|
+
console.log("\nNo findings.");
|
|
339
|
+
}
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
console.error("\nTimed out waiting for review. Check: mergestorm status " + job.job_id);
|
|
343
|
+
process.exitCode = 1;
|
|
344
|
+
}
|
|
345
|
+
async function cmdStatus(jobId) {
|
|
346
|
+
const cfg = await loadConfig();
|
|
347
|
+
const { status, body } = await apiFetch(cfg, `/api/v1/reviews/${jobId}`);
|
|
348
|
+
if (status !== 200) {
|
|
349
|
+
console.error(JSON.stringify(body, null, 2));
|
|
350
|
+
process.exitCode = 1;
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
console.log(JSON.stringify(body, null, 2));
|
|
354
|
+
}
|
|
355
|
+
async function main() {
|
|
356
|
+
const [, , cmd, ...args] = process.argv;
|
|
357
|
+
if (!cmd || cmd === "-h" || cmd === "--help") {
|
|
358
|
+
usage();
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (cmd === "login")
|
|
362
|
+
return cmdLogin(args);
|
|
363
|
+
if (cmd === "logout")
|
|
364
|
+
return cmdLogout();
|
|
365
|
+
if (cmd === "review")
|
|
366
|
+
return cmdReview(args);
|
|
367
|
+
if (cmd === "status") {
|
|
368
|
+
if (!args[0]) {
|
|
369
|
+
console.error("usage: mergestorm status <job_id>");
|
|
370
|
+
process.exitCode = 1;
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
return cmdStatus(args[0]);
|
|
374
|
+
}
|
|
375
|
+
usage();
|
|
376
|
+
process.exitCode = 1;
|
|
377
|
+
}
|
|
378
|
+
main().catch((err) => {
|
|
379
|
+
console.error(err instanceof Error ? err.message : err);
|
|
380
|
+
process.exitCode = 1;
|
|
381
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mergestorm",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MergeStorm CLI — review local diffs via the Review Jobs API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mergestorm": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc -p tsconfig.json",
|
|
14
|
+
"dev": "tsx src/cli.ts",
|
|
15
|
+
"start": "node dist/cli.js",
|
|
16
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=22.0.0"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^22.10.2",
|
|
24
|
+
"tsx": "^4.19.2",
|
|
25
|
+
"typescript": "^5.7.2"
|
|
26
|
+
}
|
|
27
|
+
}
|