mergestorm 0.1.1 → 0.2.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 +35 -8
- package/dist/api.d.ts +48 -0
- package/dist/api.js +71 -0
- package/dist/cli.js +65 -353
- package/dist/commands/credits.d.ts +1 -0
- package/dist/commands/credits.js +22 -0
- package/dist/commands/jobs.d.ts +2 -0
- package/dist/commands/jobs.js +69 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +101 -0
- package/dist/commands/logout.d.ts +1 -0
- package/dist/commands/logout.js +11 -0
- package/dist/commands/review.d.ts +6 -0
- package/dist/commands/review.js +109 -0
- package/dist/commands/status.d.ts +5 -0
- package/dist/commands/status.js +39 -0
- package/dist/commands/whoami.d.ts +1 -0
- package/dist/commands/whoami.js +35 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +41 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.js +18 -0
- package/dist/git.d.ts +9 -0
- package/dist/git.js +66 -0
- package/dist/shell.d.ts +1 -0
- package/dist/shell.js +187 -0
- package/dist/ui/ansi.d.ts +9 -0
- package/dist/ui/ansi.js +20 -0
- package/dist/ui/banner.d.ts +3 -0
- package/dist/ui/banner.js +47 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +11 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -29,21 +29,43 @@ node packages/mergestorm-cli/dist/cli.js <command>
|
|
|
29
29
|
npm run dev --workspace=mergestorm -- <command>
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
## Interactive shell
|
|
33
|
+
|
|
34
|
+
On a TTY, bare `mergestorm` (or `mergestorm shell`) opens a branded REPL with auth/credits status, green prompt, and in-shell commands. Non-TTY (CI/pipes) prints usage instead — no hanging prompt.
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
$ mergestorm
|
|
38
|
+
__ __ ___ ___ …
|
|
39
|
+
● msk_live_a1b2… · pro · standard credits: 63 of 100 left
|
|
40
|
+
|
|
41
|
+
mergestorm> credits
|
|
42
|
+
mergestorm> review
|
|
43
|
+
mergestorm> exit
|
|
44
|
+
```
|
|
45
|
+
|
|
32
46
|
## Commands
|
|
33
47
|
|
|
48
|
+
One-shot subcommands work the same inside the shell and from argv (scripts/CI):
|
|
49
|
+
|
|
34
50
|
```
|
|
35
|
-
mergestorm
|
|
36
|
-
mergestorm
|
|
37
|
-
mergestorm
|
|
38
|
-
mergestorm
|
|
39
|
-
mergestorm
|
|
51
|
+
mergestorm Interactive shell (TTY only)
|
|
52
|
+
mergestorm shell Explicit shell entry
|
|
53
|
+
mergestorm login Sign in via browser; stores an API key in ~/.mergestorm/config.json
|
|
54
|
+
mergestorm login --key Paste an existing API key instead (headless/CI)
|
|
55
|
+
mergestorm logout Remove the stored API key
|
|
56
|
+
mergestorm review [base] [head] Review git diff base...head (default: main...HEAD)
|
|
57
|
+
mergestorm status <job_id> Poll a review job and print JSON
|
|
58
|
+
mergestorm credits [--json] Standard + premium credit balance
|
|
59
|
+
mergestorm jobs [n] [--json] Recent review jobs (default 10, max 50)
|
|
60
|
+
mergestorm whoami [--json] Key prefix, plan, API base, config path
|
|
61
|
+
mergestorm thread <slug> Jobs in a review thread
|
|
40
62
|
```
|
|
41
63
|
|
|
42
64
|
### `login`
|
|
43
65
|
|
|
44
66
|
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
67
|
|
|
46
|
-
For headless or CI environments, use `mergestorm login --key` to paste an existing `msk_live_…` key (create one on the dashboard [
|
|
68
|
+
For headless or CI environments, use `mergestorm login --key` to paste an existing `msk_live_…` key (create one on the dashboard [API tab](https://mergestorm.ai/dashboard/api)).
|
|
47
69
|
|
|
48
70
|
### `logout`
|
|
49
71
|
|
|
@@ -59,11 +81,15 @@ mergestorm review develop # develop...HEAD
|
|
|
59
81
|
mergestorm review main feat/x # main...feat/x
|
|
60
82
|
```
|
|
61
83
|
|
|
62
|
-
Exit code is non-zero on quota limit (402), submission failure, a `failed`/`quota_exceeded` job, or a poll timeout.
|
|
84
|
+
Exit code is non-zero on quota limit (402), submission failure, a `failed`/`quota_exceeded` job, or a poll timeout. In the interactive shell, Ctrl+C during the wait detaches (job keeps running) — use `status <job_id>` later.
|
|
63
85
|
|
|
64
86
|
### `status <job_id>`
|
|
65
87
|
|
|
66
|
-
Fetches a job by id and prints the raw JSON (status, verdict, summary, findings).
|
|
88
|
+
Fetches a job by id and prints the raw JSON (status, verdict, summary, findings). In the shell, output is formatted for reading.
|
|
89
|
+
|
|
90
|
+
### `credits` / `jobs` / `whoami`
|
|
91
|
+
|
|
92
|
+
Require a logged-in key and API support for `GET /api/v1/me` and `GET /api/v1/reviews`. Older API deployments degrade gracefully (banner shows local key only).
|
|
67
93
|
|
|
68
94
|
## Context pack (`.mergestorm/`)
|
|
69
95
|
|
|
@@ -75,6 +101,7 @@ Any `*.md` / `*.txt` files under `./.mergestorm/` (one level of nesting) are att
|
|
|
75
101
|
|----------|---------|---------|
|
|
76
102
|
| `MERGESTORM_API_KEY` | API key (overrides the stored config) | — |
|
|
77
103
|
| `MERGESTORM_API_URL` | API base URL | `https://api.mergestorm.ai` |
|
|
104
|
+
| `NO_COLOR` | Disable ANSI colors | — |
|
|
78
105
|
|
|
79
106
|
## See also
|
|
80
107
|
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type Config } from "./config.js";
|
|
2
|
+
export declare function apiFetch(cfg: Config, route: string, init?: RequestInit & {
|
|
3
|
+
json?: unknown;
|
|
4
|
+
}): Promise<{
|
|
5
|
+
status: number;
|
|
6
|
+
body: unknown;
|
|
7
|
+
}>;
|
|
8
|
+
export declare function devicePost(base: string, route: string, json: unknown): Promise<{
|
|
9
|
+
status: number;
|
|
10
|
+
body: any;
|
|
11
|
+
}>;
|
|
12
|
+
export type MeResponse = {
|
|
13
|
+
key: {
|
|
14
|
+
prefix: string;
|
|
15
|
+
name: string;
|
|
16
|
+
created_at: string;
|
|
17
|
+
last_used_at: string | null;
|
|
18
|
+
};
|
|
19
|
+
plan_key: string;
|
|
20
|
+
plan_label_key?: string;
|
|
21
|
+
usage: {
|
|
22
|
+
standard: {
|
|
23
|
+
used: number;
|
|
24
|
+
limit: number;
|
|
25
|
+
remaining: number;
|
|
26
|
+
};
|
|
27
|
+
premium: {
|
|
28
|
+
used: number;
|
|
29
|
+
limit: number;
|
|
30
|
+
remaining: number;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
export type JobListItem = {
|
|
35
|
+
job_id: string;
|
|
36
|
+
thread_slug: string | null;
|
|
37
|
+
status: string;
|
|
38
|
+
verdict: string | null;
|
|
39
|
+
summary: string | null;
|
|
40
|
+
base_label: string | null;
|
|
41
|
+
head_label: string | null;
|
|
42
|
+
created_at: string;
|
|
43
|
+
finished_at: string | null;
|
|
44
|
+
credits: number;
|
|
45
|
+
};
|
|
46
|
+
/** Fetch /api/v1/me; returns null on 404/network so callers can degrade. */
|
|
47
|
+
export declare function getMe(cfg?: Config): Promise<MeResponse | null>;
|
|
48
|
+
export declare function listJobs(limit?: number, cfg?: Config): Promise<JobListItem[]>;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { apiBase, loadConfig, resolveApiKey } from "./config.js";
|
|
2
|
+
import { CommandError } from "./errors.js";
|
|
3
|
+
export async function apiFetch(cfg, route, init = {}) {
|
|
4
|
+
const key = resolveApiKey(cfg);
|
|
5
|
+
if (!key) {
|
|
6
|
+
throw new CommandError("No API key. Run `mergestorm login` or set MERGESTORM_API_KEY.");
|
|
7
|
+
}
|
|
8
|
+
const headers = {
|
|
9
|
+
Authorization: `Bearer ${key}`,
|
|
10
|
+
...init.headers,
|
|
11
|
+
};
|
|
12
|
+
let body = init.body;
|
|
13
|
+
if (init.json !== undefined) {
|
|
14
|
+
headers["Content-Type"] = "application/json";
|
|
15
|
+
body = JSON.stringify(init.json);
|
|
16
|
+
}
|
|
17
|
+
const res = await fetch(`${apiBase(cfg)}${route}`, { ...init, headers, body });
|
|
18
|
+
const text = await res.text();
|
|
19
|
+
let parsed = text;
|
|
20
|
+
try {
|
|
21
|
+
parsed = JSON.parse(text);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// keep text
|
|
25
|
+
}
|
|
26
|
+
return { status: res.status, body: parsed };
|
|
27
|
+
}
|
|
28
|
+
export async function devicePost(base, route, json) {
|
|
29
|
+
const res = await fetch(`${base}${route}`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { "Content-Type": "application/json" },
|
|
32
|
+
body: JSON.stringify(json),
|
|
33
|
+
});
|
|
34
|
+
const text = await res.text();
|
|
35
|
+
let parsed = text;
|
|
36
|
+
try {
|
|
37
|
+
parsed = JSON.parse(text);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// keep text
|
|
41
|
+
}
|
|
42
|
+
return { status: res.status, body: parsed };
|
|
43
|
+
}
|
|
44
|
+
/** Fetch /api/v1/me; returns null on 404/network so callers can degrade. */
|
|
45
|
+
export async function getMe(cfg) {
|
|
46
|
+
const resolved = cfg ?? (await loadConfig());
|
|
47
|
+
try {
|
|
48
|
+
const { status, body } = await apiFetch(resolved, "/api/v1/me");
|
|
49
|
+
if (status === 404)
|
|
50
|
+
return null;
|
|
51
|
+
if (status !== 200)
|
|
52
|
+
return null;
|
|
53
|
+
return body;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export async function listJobs(limit = 10, cfg) {
|
|
60
|
+
const resolved = cfg ?? (await loadConfig());
|
|
61
|
+
const capped = Math.min(Math.max(limit, 1), 50);
|
|
62
|
+
const { status, body } = await apiFetch(resolved, `/api/v1/reviews?limit=${capped}`);
|
|
63
|
+
if (status === 404) {
|
|
64
|
+
throw new CommandError("Job list is not available on this server yet. Update the API or use the dashboard.");
|
|
65
|
+
}
|
|
66
|
+
if (status !== 200) {
|
|
67
|
+
throw new CommandError(`Failed to list jobs (HTTP ${status}): ${JSON.stringify(body)}`);
|
|
68
|
+
}
|
|
69
|
+
const items = body.items;
|
|
70
|
+
return Array.isArray(items) ? items : [];
|
|
71
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1,381 +1,93 @@
|
|
|
1
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
2
|
import { stdin as input, stdout as output } from "node:process";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
}
|
|
3
|
+
import { DEFAULT_API } from "./config.js";
|
|
4
|
+
import { CommandError } from "./errors.js";
|
|
5
|
+
import { cmdCredits } from "./commands/credits.js";
|
|
6
|
+
import { cmdJobs, cmdThread } from "./commands/jobs.js";
|
|
7
|
+
import { cmdLogin } from "./commands/login.js";
|
|
8
|
+
import { cmdLogout } from "./commands/logout.js";
|
|
9
|
+
import { cmdReview } from "./commands/review.js";
|
|
10
|
+
import { cmdStatus } from "./commands/status.js";
|
|
11
|
+
import { cmdWhoami } from "./commands/whoami.js";
|
|
12
|
+
import { runShell } from "./shell.js";
|
|
13
|
+
import { ansi } from "./ui/ansi.js";
|
|
14
|
+
import { printWordmark } from "./ui/banner.js";
|
|
118
15
|
function usage() {
|
|
119
|
-
|
|
16
|
+
printWordmark();
|
|
120
17
|
console.log(`Usage:
|
|
121
|
-
mergestorm
|
|
122
|
-
mergestorm
|
|
123
|
-
mergestorm
|
|
124
|
-
mergestorm
|
|
125
|
-
mergestorm
|
|
18
|
+
mergestorm Interactive shell (TTY)
|
|
19
|
+
mergestorm shell Explicit shell entry
|
|
20
|
+
mergestorm login Sign in via browser and store an API key
|
|
21
|
+
mergestorm login --key Paste an existing API key instead (headless/CI)
|
|
22
|
+
mergestorm logout Remove the stored API key
|
|
23
|
+
mergestorm review [base] [head] Review git diff base...head (default: main...HEAD)
|
|
24
|
+
mergestorm status <job_id> Poll a review job (JSON)
|
|
25
|
+
mergestorm credits [--json] Credit balance
|
|
26
|
+
mergestorm jobs [n] [--json] Recent review jobs
|
|
27
|
+
mergestorm whoami [--json] Key + plan + API base
|
|
28
|
+
mergestorm thread <slug> Jobs in a review thread
|
|
126
29
|
|
|
127
30
|
Env:
|
|
128
31
|
MERGESTORM_API_KEY API key (overrides config)
|
|
129
32
|
MERGESTORM_API_URL API base (default ${DEFAULT_API})
|
|
130
33
|
`);
|
|
131
34
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
return { status: res.status, body: parsed };
|
|
35
|
+
async function runCommand(cmd, args) {
|
|
36
|
+
if (cmd === "login")
|
|
37
|
+
return cmdLogin(args);
|
|
38
|
+
if (cmd === "logout")
|
|
39
|
+
return cmdLogout();
|
|
40
|
+
if (cmd === "review")
|
|
41
|
+
return cmdReview(args);
|
|
42
|
+
if (cmd === "status") {
|
|
43
|
+
if (!args[0])
|
|
44
|
+
throw new CommandError("usage: mergestorm status <job_id>");
|
|
45
|
+
return cmdStatus(args[0], { format: "json" });
|
|
46
|
+
}
|
|
47
|
+
if (cmd === "credits" || cmd === "usage")
|
|
48
|
+
return cmdCredits(args);
|
|
49
|
+
if (cmd === "jobs")
|
|
50
|
+
return cmdJobs(args);
|
|
51
|
+
if (cmd === "whoami")
|
|
52
|
+
return cmdWhoami(args);
|
|
53
|
+
if (cmd === "thread") {
|
|
54
|
+
if (!args[0])
|
|
55
|
+
throw new CommandError("usage: mergestorm thread <slug>");
|
|
56
|
+
return cmdThread(args[0], args.slice(1));
|
|
57
|
+
}
|
|
58
|
+
if (cmd === "shell")
|
|
59
|
+
return runShell();
|
|
60
|
+
usage();
|
|
61
|
+
throw new CommandError(`unknown command: ${cmd}`);
|
|
161
62
|
}
|
|
162
|
-
async function
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
console.error("Key should start with msk_live_");
|
|
168
|
-
process.exitCode = 1;
|
|
63
|
+
async function main() {
|
|
64
|
+
const [, , cmd, ...args] = process.argv;
|
|
65
|
+
if (!cmd) {
|
|
66
|
+
if (input.isTTY && output.isTTY) {
|
|
67
|
+
await runShell();
|
|
169
68
|
return;
|
|
170
69
|
}
|
|
171
|
-
|
|
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;
|
|
70
|
+
usage();
|
|
202
71
|
return;
|
|
203
72
|
}
|
|
204
|
-
|
|
205
|
-
|
|
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;
|
|
73
|
+
if (cmd === "-h" || cmd === "--help") {
|
|
74
|
+
usage();
|
|
253
75
|
return;
|
|
254
76
|
}
|
|
255
|
-
console.error("\nLogin timed out. Run `mergestorm login` again.");
|
|
256
|
-
process.exitCode = 1;
|
|
257
|
-
}
|
|
258
|
-
async function cmdLogout() {
|
|
259
77
|
try {
|
|
260
|
-
await
|
|
261
|
-
console.log("Logged out. Removed stored API key.");
|
|
78
|
+
await runCommand(cmd, args);
|
|
262
79
|
}
|
|
263
80
|
catch (err) {
|
|
264
|
-
|
|
265
|
-
|
|
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;
|
|
81
|
+
if (err instanceof CommandError) {
|
|
82
|
+
console.error(err.message);
|
|
83
|
+
process.exitCode = err.exitCode;
|
|
312
84
|
return;
|
|
313
85
|
}
|
|
314
|
-
|
|
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));
|
|
86
|
+
console.error(err instanceof Error ? err.message : err);
|
|
350
87
|
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
88
|
}
|
|
375
|
-
usage();
|
|
376
|
-
process.exitCode = 1;
|
|
377
89
|
}
|
|
378
90
|
main().catch((err) => {
|
|
379
|
-
console.error(err instanceof Error ? err.message : err);
|
|
91
|
+
console.error(ansi.red(err instanceof Error ? err.message : String(err)));
|
|
380
92
|
process.exitCode = 1;
|
|
381
93
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function cmdCredits(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getMe } from "../api.js";
|
|
2
|
+
import { loadConfig } from "../config.js";
|
|
3
|
+
import { CommandError } from "../errors.js";
|
|
4
|
+
import { ansi } from "../ui/ansi.js";
|
|
5
|
+
export async function cmdCredits(args) {
|
|
6
|
+
const asJson = args.includes("--json");
|
|
7
|
+
const cfg = await loadConfig();
|
|
8
|
+
const me = await getMe(cfg);
|
|
9
|
+
if (!me) {
|
|
10
|
+
throw new CommandError("Credits are not available (not logged in, offline, or API too old). " +
|
|
11
|
+
"Try `mergestorm login` or open https://mergestorm.ai/billing");
|
|
12
|
+
}
|
|
13
|
+
if (asJson) {
|
|
14
|
+
console.log(JSON.stringify(me.usage, null, 2));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const s = me.usage.standard;
|
|
18
|
+
const p = me.usage.premium;
|
|
19
|
+
console.log(` Standard ${s.used} used / ${s.limit} (${s.remaining} remaining)`);
|
|
20
|
+
console.log(` Premium ${p.used} used / ${p.limit} (${p.remaining} remaining)`);
|
|
21
|
+
console.log(ansi.dim(" Resets with your billing cycle · manage at https://mergestorm.ai/billing"));
|
|
22
|
+
}
|