mergestorm 0.1.0 → 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 +37 -9
- 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 +11 -2
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { apiFetch, listJobs } from "../api.js";
|
|
2
|
+
import { loadConfig } from "../config.js";
|
|
3
|
+
import { CommandError } from "../errors.js";
|
|
4
|
+
import { ansi } from "../ui/ansi.js";
|
|
5
|
+
function relativeWhen(iso) {
|
|
6
|
+
const t = new Date(iso).getTime();
|
|
7
|
+
if (Number.isNaN(t))
|
|
8
|
+
return "—";
|
|
9
|
+
const sec = Math.max(0, Math.floor((Date.now() - t) / 1000));
|
|
10
|
+
if (sec < 60)
|
|
11
|
+
return `${sec}s ago`;
|
|
12
|
+
const min = Math.floor(sec / 60);
|
|
13
|
+
if (min < 60)
|
|
14
|
+
return `${min}m ago`;
|
|
15
|
+
const hr = Math.floor(min / 60);
|
|
16
|
+
if (hr < 48)
|
|
17
|
+
return `${hr}h ago`;
|
|
18
|
+
const days = Math.floor(hr / 24);
|
|
19
|
+
return `${days}d ago`;
|
|
20
|
+
}
|
|
21
|
+
export async function cmdJobs(args) {
|
|
22
|
+
const asJson = args.includes("--json");
|
|
23
|
+
const nArg = args.find((a) => /^\d+$/.test(a));
|
|
24
|
+
const limit = nArg ? Number.parseInt(nArg, 10) : 10;
|
|
25
|
+
const cfg = await loadConfig();
|
|
26
|
+
const items = await listJobs(limit, cfg);
|
|
27
|
+
if (asJson) {
|
|
28
|
+
console.log(JSON.stringify({ items }, null, 2));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (items.length === 0) {
|
|
32
|
+
console.log(ansi.dim(" No review jobs yet. Run `review` to submit one."));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const header = ["JOB", "STATUS", "VERDICT", "THREAD", "WHEN"];
|
|
36
|
+
const rows = items.map((j) => [
|
|
37
|
+
j.job_id.slice(0, 8),
|
|
38
|
+
j.status,
|
|
39
|
+
j.verdict ?? "-",
|
|
40
|
+
(j.thread_slug ?? "-").slice(0, 20),
|
|
41
|
+
relativeWhen(j.created_at),
|
|
42
|
+
]);
|
|
43
|
+
const widths = header.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i]?.length ?? 0)));
|
|
44
|
+
const fmt = (cols) => " " + cols.map((c, i) => c.padEnd(widths[i])).join(" ");
|
|
45
|
+
console.log(ansi.dim(fmt(header)));
|
|
46
|
+
for (const r of rows)
|
|
47
|
+
console.log(fmt(r));
|
|
48
|
+
console.log(ansi.dim(" full history: https://mergestorm.ai/dashboard/api"));
|
|
49
|
+
}
|
|
50
|
+
export async function cmdThread(slug, args = []) {
|
|
51
|
+
const asJson = args.includes("--json");
|
|
52
|
+
const cfg = await loadConfig();
|
|
53
|
+
const { status, body } = await apiFetch(cfg, `/api/v1/threads/${encodeURIComponent(slug)}`);
|
|
54
|
+
if (status === 404) {
|
|
55
|
+
throw new CommandError(`Thread not found: ${slug}`);
|
|
56
|
+
}
|
|
57
|
+
if (status !== 200) {
|
|
58
|
+
throw new CommandError(`Failed to load thread (HTTP ${status}): ${JSON.stringify(body)}`);
|
|
59
|
+
}
|
|
60
|
+
if (asJson) {
|
|
61
|
+
console.log(JSON.stringify(body, null, 2));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const data = body;
|
|
65
|
+
console.log(` Thread ${ansi.bold(data.slug)} · ${data.job_count} job(s)`);
|
|
66
|
+
for (const j of data.jobs ?? []) {
|
|
67
|
+
console.log(` ${j.job_id.slice(0, 8)} ${j.status.padEnd(12)} ${(j.verdict ?? "-").padEnd(16)} ${relativeWhen(j.created_at)}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { platform } from "node:os";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
4
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
5
|
+
import { apiBase, configPath, loadConfig, saveConfig } from "../config.js";
|
|
6
|
+
import { devicePost } from "../api.js";
|
|
7
|
+
import { CommandError } from "../errors.js";
|
|
8
|
+
import { ansi } from "../ui/ansi.js";
|
|
9
|
+
function openBrowser(url) {
|
|
10
|
+
const os = platform();
|
|
11
|
+
const cmd = os === "darwin" ? "open" : os === "win32" ? "cmd" : "xdg-open";
|
|
12
|
+
const args = os === "win32" ? ["/c", "start", "", url] : [url];
|
|
13
|
+
try {
|
|
14
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
15
|
+
child.on("error", () => { });
|
|
16
|
+
child.unref();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// URL is printed for manual open
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function loginWithKey(existingRl) {
|
|
23
|
+
const rl = existingRl ?? createInterface({ input, output });
|
|
24
|
+
const ownRl = !existingRl;
|
|
25
|
+
try {
|
|
26
|
+
const key = (await rl.question("Paste your MergeStorm API key (msk_live_...): ")).trim();
|
|
27
|
+
if (!key.startsWith("msk_live_")) {
|
|
28
|
+
throw new CommandError("Key should start with msk_live_");
|
|
29
|
+
}
|
|
30
|
+
const cfg = await loadConfig();
|
|
31
|
+
cfg.apiKey = key;
|
|
32
|
+
await saveConfig(cfg);
|
|
33
|
+
console.log(`Saved to ${configPath()}`);
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
if (ownRl)
|
|
37
|
+
rl.close();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export async function cmdLogin(args, rl) {
|
|
41
|
+
if (args.includes("--key")) {
|
|
42
|
+
await loginWithKey(rl);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const cfg = await loadConfig();
|
|
46
|
+
const base = apiBase(cfg);
|
|
47
|
+
let start;
|
|
48
|
+
try {
|
|
49
|
+
start = await devicePost(base, "/api/v1/cli/device", {});
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
throw new CommandError("Could not start device login. The API may be unreachable or misconfigured.\n" +
|
|
53
|
+
"Run `mergestorm login --key` to paste an existing API key instead.");
|
|
54
|
+
}
|
|
55
|
+
if (start.status !== 200 || !start.body?.device_code) {
|
|
56
|
+
throw new CommandError(`Could not start device login (HTTP ${start.status}). The API may be unreachable or misconfigured.\n` +
|
|
57
|
+
`Run \`mergestorm login --key\` to paste an existing API key instead.`);
|
|
58
|
+
}
|
|
59
|
+
const { device_code, user_code, verification_uri, verification_uri_complete, interval, expires_in, } = start.body;
|
|
60
|
+
const openUrl = verification_uri_complete || verification_uri;
|
|
61
|
+
console.log("To sign in, open this URL and confirm the code:\n");
|
|
62
|
+
console.log(` ${ansi.green(openUrl)}`);
|
|
63
|
+
console.log(`\n Code: ${ansi.bold(user_code)}\n`);
|
|
64
|
+
openBrowser(openUrl);
|
|
65
|
+
console.log("Waiting for approval…");
|
|
66
|
+
const pollMs = Math.max(interval || 5, 2) * 1000;
|
|
67
|
+
const deadline = Date.now() + (expires_in || 600) * 1000;
|
|
68
|
+
while (Date.now() < deadline) {
|
|
69
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
70
|
+
let poll;
|
|
71
|
+
try {
|
|
72
|
+
poll = await devicePost(base, "/api/v1/cli/device/token", { device_code });
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
throw new CommandError("Login failed: network error. Check your connection and run `mergestorm login` again.");
|
|
76
|
+
}
|
|
77
|
+
if (poll.status === 200 && poll.body?.api_key) {
|
|
78
|
+
const next = await loadConfig();
|
|
79
|
+
next.apiKey = poll.body.api_key;
|
|
80
|
+
await saveConfig(next);
|
|
81
|
+
console.log(`\nLogged in. Key saved to ${configPath()}`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const err = poll.body?.error;
|
|
85
|
+
if (err === "authorization_pending") {
|
|
86
|
+
process.stdout.write(".");
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (err === "key_limit_reached") {
|
|
90
|
+
throw new CommandError("You have reached the max active API keys. Revoke one at https://mergestorm.ai/dashboard/api and try again.");
|
|
91
|
+
}
|
|
92
|
+
if (err === "expired_token") {
|
|
93
|
+
throw new CommandError("Login code expired. Run `mergestorm login` again.");
|
|
94
|
+
}
|
|
95
|
+
if (err === "already_redeemed") {
|
|
96
|
+
throw new CommandError("This code was already used. Run `mergestorm login` again.");
|
|
97
|
+
}
|
|
98
|
+
throw new CommandError(`Login failed: ${typeof err === "string" ? err : JSON.stringify(poll.body)}`);
|
|
99
|
+
}
|
|
100
|
+
throw new CommandError("Login timed out. Run `mergestorm login` again.");
|
|
101
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function cmdLogout(): Promise<void>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { clearConfig } from "../config.js";
|
|
2
|
+
import { CommandError } from "../errors.js";
|
|
3
|
+
export async function cmdLogout() {
|
|
4
|
+
try {
|
|
5
|
+
await clearConfig();
|
|
6
|
+
console.log("Logged out. Removed stored API key.");
|
|
7
|
+
}
|
|
8
|
+
catch (err) {
|
|
9
|
+
throw new CommandError(`Could not remove config: ${err instanceof Error ? err.message : String(err)}`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { apiFetch, getMe } from "../api.js";
|
|
2
|
+
import { loadConfig } from "../config.js";
|
|
3
|
+
import { CommandError, DetachedError } from "../errors.js";
|
|
4
|
+
import { collectChangedFiles, collectContextFiles, git } from "../git.js";
|
|
5
|
+
import { ansi } from "../ui/ansi.js";
|
|
6
|
+
function sleep(ms, signal) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
if (signal?.aborted) {
|
|
9
|
+
reject(Object.assign(new Error("Aborted"), { name: "AbortError" }));
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const t = setTimeout(resolve, ms);
|
|
13
|
+
const onAbort = () => {
|
|
14
|
+
clearTimeout(t);
|
|
15
|
+
reject(Object.assign(new Error("Aborted"), { name: "AbortError" }));
|
|
16
|
+
};
|
|
17
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export async function cmdReview(args, opts = {}) {
|
|
21
|
+
const base = args[0] || "main";
|
|
22
|
+
const head = args[1] || "HEAD";
|
|
23
|
+
const cfg = await loadConfig();
|
|
24
|
+
const meBefore = opts.interactive ? await getMe(cfg) : null;
|
|
25
|
+
console.log(`Collecting diff ${base}...${head} …`);
|
|
26
|
+
const diff = git(["diff", `${base}...${head}`]);
|
|
27
|
+
if (!diff.trim()) {
|
|
28
|
+
console.log("No changes to review.");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const files = await collectChangedFiles(base, head);
|
|
32
|
+
const context_files = await collectContextFiles(process.cwd());
|
|
33
|
+
const branch = git(["rev-parse", "--abbrev-ref", "HEAD"]).trim() || "local";
|
|
34
|
+
const thread = `local/${branch}`.replace(/[^a-zA-Z0-9._/-]/g, "-").slice(0, 120);
|
|
35
|
+
console.log(`Submitting review (${Buffer.byteLength(diff, "utf8")} byte diff, ${files.length} file(s), ${context_files.length} context file(s))…`);
|
|
36
|
+
const { status, body } = await apiFetch(cfg, "/api/v1/reviews", {
|
|
37
|
+
method: "POST",
|
|
38
|
+
json: {
|
|
39
|
+
thread,
|
|
40
|
+
base_label: base,
|
|
41
|
+
head_label: head,
|
|
42
|
+
diff,
|
|
43
|
+
files,
|
|
44
|
+
context_files,
|
|
45
|
+
},
|
|
46
|
+
signal: opts.signal,
|
|
47
|
+
});
|
|
48
|
+
if (status === 402) {
|
|
49
|
+
throw new CommandError("Monthly review limit reached. Upgrade at https://mergestorm.ai/billing");
|
|
50
|
+
}
|
|
51
|
+
if (status !== 202 && status !== 200) {
|
|
52
|
+
throw new CommandError(`Failed: ${JSON.stringify(body, null, 2)}`);
|
|
53
|
+
}
|
|
54
|
+
const job = body;
|
|
55
|
+
const waitHint = opts.interactive
|
|
56
|
+
? ` Waiting… (${ansi.dim("ctrl+c to detach — job keeps running")})`
|
|
57
|
+
: " Waiting…";
|
|
58
|
+
console.log(`Job ${job.job_id} (${job.status}).${waitHint}`);
|
|
59
|
+
try {
|
|
60
|
+
for (let i = 0; i < 90; i += 1) {
|
|
61
|
+
await sleep(2000, opts.signal);
|
|
62
|
+
const poll = await apiFetch(cfg, `/api/v1/reviews/${job.job_id}`, { signal: opts.signal });
|
|
63
|
+
if (poll.status !== 200) {
|
|
64
|
+
throw new CommandError(`Poll failed: ${JSON.stringify(poll.body, null, 2)}`);
|
|
65
|
+
}
|
|
66
|
+
const row = poll.body;
|
|
67
|
+
if (row.status === "queued" || row.status === "in_progress") {
|
|
68
|
+
process.stdout.write(".");
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
console.log("");
|
|
72
|
+
if (row.status === "failed" || row.status === "quota_exceeded") {
|
|
73
|
+
throw new CommandError(`Review ${row.status}: ${row.error ?? ""}`);
|
|
74
|
+
}
|
|
75
|
+
console.log(`\nVerdict: ${ansi.bold(row.verdict ?? "comment")}`);
|
|
76
|
+
console.log(row.summary ?? "");
|
|
77
|
+
const inline = row.findings?.inline ?? [];
|
|
78
|
+
const off = row.findings?.off_diff ?? row.findings?.offDiff ?? [];
|
|
79
|
+
for (const c of inline) {
|
|
80
|
+
console.log(`\n[${c.severity}] ${c.path}:${c.line}${c.title ? ` — ${c.title}` : ""}`);
|
|
81
|
+
console.log(c.body);
|
|
82
|
+
}
|
|
83
|
+
for (const c of off) {
|
|
84
|
+
console.log(`\n[${c.severity}] off-diff ${c.path ?? ""}`);
|
|
85
|
+
console.log(c.body);
|
|
86
|
+
}
|
|
87
|
+
if (inline.length === 0 && off.length === 0) {
|
|
88
|
+
console.log("\nNo findings.");
|
|
89
|
+
}
|
|
90
|
+
if (opts.interactive && meBefore) {
|
|
91
|
+
const meAfter = await getMe(cfg);
|
|
92
|
+
if (meAfter) {
|
|
93
|
+
const used = Math.max(0, meBefore.usage.standard.remaining - meAfter.usage.standard.remaining);
|
|
94
|
+
if (used > 0) {
|
|
95
|
+
console.log(ansi.dim(`\n${used} credit${used === 1 ? "" : "s"} used · ${meAfter.usage.standard.remaining} remaining`));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
if (opts.interactive && err instanceof Error && err.name === "AbortError") {
|
|
104
|
+
throw new DetachedError(job.job_id);
|
|
105
|
+
}
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
throw new CommandError(`Timed out waiting for review. Check: mergestorm status ${job.job_id}`);
|
|
109
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { apiFetch } 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 cmdStatus(jobId, opts = {}) {
|
|
6
|
+
const format = opts.format ?? "json";
|
|
7
|
+
const cfg = await loadConfig();
|
|
8
|
+
const { status, body } = await apiFetch(cfg, `/api/v1/reviews/${jobId}`, {
|
|
9
|
+
signal: opts.signal,
|
|
10
|
+
});
|
|
11
|
+
if (status !== 200) {
|
|
12
|
+
throw new CommandError(JSON.stringify(body, null, 2));
|
|
13
|
+
}
|
|
14
|
+
if (format === "json") {
|
|
15
|
+
console.log(JSON.stringify(body, null, 2));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const row = body;
|
|
19
|
+
console.log(`Status: ${row.status}`);
|
|
20
|
+
if (row.verdict)
|
|
21
|
+
console.log(`Verdict: ${ansi.bold(row.verdict)}`);
|
|
22
|
+
if (row.summary)
|
|
23
|
+
console.log(row.summary);
|
|
24
|
+
if (row.error)
|
|
25
|
+
console.log(ansi.red(`Error: ${row.error}`));
|
|
26
|
+
const inline = row.findings?.inline ?? [];
|
|
27
|
+
const off = row.findings?.off_diff ?? row.findings?.offDiff ?? [];
|
|
28
|
+
for (const c of inline) {
|
|
29
|
+
console.log(`\n[${c.severity}] ${c.path}:${c.line}${c.title ? ` — ${c.title}` : ""}`);
|
|
30
|
+
console.log(c.body);
|
|
31
|
+
}
|
|
32
|
+
for (const c of off) {
|
|
33
|
+
console.log(`\n[${c.severity}] off-diff ${c.path ?? ""}`);
|
|
34
|
+
console.log(c.body);
|
|
35
|
+
}
|
|
36
|
+
if (inline.length === 0 && off.length === 0 && row.status === "completed") {
|
|
37
|
+
console.log("\nNo findings.");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function cmdWhoami(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { getMe } from "../api.js";
|
|
2
|
+
import { apiBase, configPath, loadConfig, resolveApiKey, keyDisplay } from "../config.js";
|
|
3
|
+
import { CommandError } from "../errors.js";
|
|
4
|
+
import { ansi } from "../ui/ansi.js";
|
|
5
|
+
export async function cmdWhoami(args) {
|
|
6
|
+
const asJson = args.includes("--json");
|
|
7
|
+
const cfg = await loadConfig();
|
|
8
|
+
const key = resolveApiKey(cfg);
|
|
9
|
+
if (!key) {
|
|
10
|
+
throw new CommandError("Not logged in. Run `mergestorm login`.");
|
|
11
|
+
}
|
|
12
|
+
const me = await getMe(cfg);
|
|
13
|
+
const payload = {
|
|
14
|
+
key_prefix: me?.key.prefix ?? keyDisplay(key),
|
|
15
|
+
key_name: me?.key.name ?? null,
|
|
16
|
+
plan_key: me?.plan_key ?? null,
|
|
17
|
+
plan_label_key: me?.plan_label_key ?? null,
|
|
18
|
+
api_base: apiBase(cfg),
|
|
19
|
+
config_path: configPath(),
|
|
20
|
+
usage: me?.usage ?? null,
|
|
21
|
+
};
|
|
22
|
+
if (asJson) {
|
|
23
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
console.log(` Key ${ansi.bold(String(payload.key_prefix))}${payload.key_name ? ` (${payload.key_name})` : ""}`);
|
|
27
|
+
if (payload.plan_key) {
|
|
28
|
+
console.log(` Plan ${payload.plan_label_key ?? payload.plan_key}`);
|
|
29
|
+
}
|
|
30
|
+
console.log(` API ${payload.api_base}`);
|
|
31
|
+
console.log(` Config ${payload.config_path}`);
|
|
32
|
+
if (!me) {
|
|
33
|
+
console.log(ansi.dim(" (live account details unavailable — older API or offline)"));
|
|
34
|
+
}
|
|
35
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const DEFAULT_API = "https://api.mergestorm.ai";
|
|
2
|
+
export type Config = {
|
|
3
|
+
apiKey?: string;
|
|
4
|
+
apiBase?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function configPath(): string;
|
|
7
|
+
export declare function loadConfig(): Promise<Config>;
|
|
8
|
+
export declare function saveConfig(cfg: Config): Promise<void>;
|
|
9
|
+
export declare function clearConfig(): Promise<void>;
|
|
10
|
+
export declare function apiBase(cfg: Config): string;
|
|
11
|
+
export declare function resolveApiKey(cfg: Config): string | undefined;
|
|
12
|
+
/** Short display form of a stored key (prefix only). */
|
|
13
|
+
export declare function keyDisplay(key: string | undefined): string | null;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile, rm } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export const DEFAULT_API = "https://api.mergestorm.ai";
|
|
5
|
+
export function configPath() {
|
|
6
|
+
return path.join(homedir(), ".mergestorm", "config.json");
|
|
7
|
+
}
|
|
8
|
+
export async function loadConfig() {
|
|
9
|
+
try {
|
|
10
|
+
const raw = await readFile(configPath(), "utf8");
|
|
11
|
+
return JSON.parse(raw);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export async function saveConfig(cfg) {
|
|
18
|
+
const dir = path.dirname(configPath());
|
|
19
|
+
await mkdir(dir, { recursive: true });
|
|
20
|
+
await writeFile(configPath(), JSON.stringify(cfg, null, 2) + "\n", {
|
|
21
|
+
encoding: "utf8",
|
|
22
|
+
mode: 0o600,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export async function clearConfig() {
|
|
26
|
+
await rm(configPath(), { force: true });
|
|
27
|
+
}
|
|
28
|
+
export function apiBase(cfg) {
|
|
29
|
+
return (process.env.MERGESTORM_API_URL?.trim() || cfg.apiBase || DEFAULT_API).replace(/\/$/, "");
|
|
30
|
+
}
|
|
31
|
+
export function resolveApiKey(cfg) {
|
|
32
|
+
return process.env.MERGESTORM_API_KEY?.trim() || cfg.apiKey;
|
|
33
|
+
}
|
|
34
|
+
/** Short display form of a stored key (prefix only). */
|
|
35
|
+
export function keyDisplay(key) {
|
|
36
|
+
if (!key)
|
|
37
|
+
return null;
|
|
38
|
+
if (key.length <= 16)
|
|
39
|
+
return key;
|
|
40
|
+
return `${key.slice(0, 12)}…`;
|
|
41
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Thrown by command handlers; one-shot mode maps `exitCode` onto process.exitCode. */
|
|
2
|
+
export declare class CommandError extends Error {
|
|
3
|
+
readonly exitCode: number;
|
|
4
|
+
constructor(message: string, exitCode?: number);
|
|
5
|
+
}
|
|
6
|
+
/** Review/status poll aborted by Ctrl+C — shell returns to the prompt. */
|
|
7
|
+
export declare class DetachedError extends Error {
|
|
8
|
+
readonly jobId: string;
|
|
9
|
+
constructor(jobId: string);
|
|
10
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Thrown by command handlers; one-shot mode maps `exitCode` onto process.exitCode. */
|
|
2
|
+
export class CommandError extends Error {
|
|
3
|
+
exitCode;
|
|
4
|
+
constructor(message, exitCode = 1) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "CommandError";
|
|
7
|
+
this.exitCode = exitCode;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/** Review/status poll aborted by Ctrl+C — shell returns to the prompt. */
|
|
11
|
+
export class DetachedError extends Error {
|
|
12
|
+
jobId;
|
|
13
|
+
constructor(jobId) {
|
|
14
|
+
super(`detached — check later with: status ${jobId}`);
|
|
15
|
+
this.name = "DetachedError";
|
|
16
|
+
this.jobId = jobId;
|
|
17
|
+
}
|
|
18
|
+
}
|
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function git(args: string[], cwd?: string): string;
|
|
2
|
+
export declare function collectContextFiles(root: string): Promise<{
|
|
3
|
+
path: string;
|
|
4
|
+
content: string;
|
|
5
|
+
}[]>;
|
|
6
|
+
export declare function collectChangedFiles(base: string, head: string): Promise<{
|
|
7
|
+
path: string;
|
|
8
|
+
content: string;
|
|
9
|
+
}[]>;
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const CONTEXT_DIR = ".mergestorm";
|
|
5
|
+
export function git(args, cwd = process.cwd()) {
|
|
6
|
+
try {
|
|
7
|
+
const result = spawnSync("git", args, { cwd, encoding: "utf8" });
|
|
8
|
+
if (result.status !== 0) {
|
|
9
|
+
throw new Error(result.stderr?.trim() || `git ${args.join(" ")} failed`);
|
|
10
|
+
}
|
|
11
|
+
return result.stdout;
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
throw new Error(`git ${args.join(" ")} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export async function collectContextFiles(root) {
|
|
18
|
+
const pack = path.join(root, CONTEXT_DIR);
|
|
19
|
+
let entries;
|
|
20
|
+
try {
|
|
21
|
+
entries = await readdir(pack, { withFileTypes: true });
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
const files = [];
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (entry.isFile() &&
|
|
29
|
+
(entry.name.toLowerCase().endsWith(".md") || entry.name.toLowerCase().endsWith(".txt"))) {
|
|
30
|
+
const content = await readFile(path.join(pack, entry.name), "utf8");
|
|
31
|
+
files.push({ path: entry.name, content });
|
|
32
|
+
}
|
|
33
|
+
else if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
34
|
+
const nested = await readdir(path.join(pack, entry.name), { withFileTypes: true }).catch(() => []);
|
|
35
|
+
for (const child of nested) {
|
|
36
|
+
if (child.isFile() &&
|
|
37
|
+
(child.name.toLowerCase().endsWith(".md") || child.name.toLowerCase().endsWith(".txt"))) {
|
|
38
|
+
const rel = `${entry.name}/${child.name}`;
|
|
39
|
+
const content = await readFile(path.join(pack, entry.name, child.name), "utf8");
|
|
40
|
+
files.push({ path: rel, content });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return files;
|
|
46
|
+
}
|
|
47
|
+
export async function collectChangedFiles(base, head) {
|
|
48
|
+
const names = git(["diff", "--name-only", "--diff-filter=ACMR", `${base}...${head}`])
|
|
49
|
+
.split("\n")
|
|
50
|
+
.map((s) => s.trim())
|
|
51
|
+
.filter(Boolean);
|
|
52
|
+
const files = [];
|
|
53
|
+
for (const name of names.slice(0, 40)) {
|
|
54
|
+
try {
|
|
55
|
+
const st = await stat(path.join(process.cwd(), name));
|
|
56
|
+
if (!st.isFile() || st.size > 400_000)
|
|
57
|
+
continue;
|
|
58
|
+
const content = await readFile(path.join(process.cwd(), name), "utf8");
|
|
59
|
+
files.push({ path: name, content });
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// deleted or unreadable
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return files;
|
|
66
|
+
}
|
package/dist/shell.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runShell(): Promise<void>;
|