mergestorm 0.2.0 → 0.3.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 -6
- package/dist/api.d.ts +41 -4
- package/dist/api.js +14 -0
- package/dist/cli.js +8 -1
- package/dist/commands/branches.d.ts +10 -0
- package/dist/commands/branches.js +85 -0
- package/dist/commands/credits.js +10 -4
- package/dist/commands/jobs.d.ts +4 -0
- package/dist/commands/jobs.js +43 -5
- package/dist/commands/login.js +2 -0
- package/dist/commands/review.d.ts +5 -0
- package/dist/commands/review.js +22 -2
- package/dist/shell.d.ts +7 -0
- package/dist/shell.js +50 -54
- package/dist/ui/ansi.d.ts +4 -1
- package/dist/ui/ansi.js +16 -9
- package/dist/ui/banner.d.ts +1 -0
- package/dist/ui/banner.js +65 -14
- package/dist/ui/box.d.ts +11 -0
- package/dist/ui/box.js +36 -0
- package/dist/ui/logo.d.ts +5 -0
- package/dist/ui/logo.js +12 -0
- package/dist/ui/prompt.d.ts +18 -0
- package/dist/ui/prompt.js +220 -0
- package/dist/ui/select.d.ts +13 -0
- package/dist/ui/select.js +103 -0
- package/dist/ui/usage.d.ts +24 -0
- package/dist/ui/usage.js +67 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -31,18 +31,41 @@ npm run dev --workspace=mergestorm -- <command>
|
|
|
31
31
|
|
|
32
32
|
## Interactive shell
|
|
33
33
|
|
|
34
|
-
On a TTY, bare `mergestorm` (or `mergestorm shell`) opens a branded REPL
|
|
34
|
+
On a TTY, bare `mergestorm` (or `mergestorm shell`) opens a branded REPL: a boxed welcome panel (logo, auth/credits status, tips, what's new) and a bordered input box with a live `/` slash-command dropdown. Non-TTY (CI/pipes) prints usage instead — no hanging prompt.
|
|
35
35
|
|
|
36
36
|
```
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
╭──────────────────── mergestorm v0.3.0 ────────────────────╮
|
|
38
|
+
│ ▟██████████▛ │
|
|
39
|
+
│ ▜████████▛ │
|
|
40
|
+
│ ▝▜████▛▘ │
|
|
41
|
+
│ ▜██▛ │
|
|
42
|
+
│ ██ │
|
|
43
|
+
│ ▝▘ │
|
|
44
|
+
│ │
|
|
45
|
+
│ ● msk_live_a1b2… · maelstrom │
|
|
46
|
+
│ [████████████████████░░░░░░░░░░░░] 63% used │
|
|
47
|
+
│ │
|
|
48
|
+
│ Tips for getting started: │
|
|
49
|
+
│ review review the diff for main...HEAD │
|
|
50
|
+
│ branches pick a recently reviewed branch │
|
|
51
|
+
│ /help list all commands │
|
|
52
|
+
╰─────────────────────────────────────────────────────────────╯
|
|
53
|
+
mergestorm
|
|
54
|
+
╭─────────────────────────╮
|
|
55
|
+
│ › /re │
|
|
56
|
+
╰─────────────────────────╯
|
|
57
|
+
⁄review Review git diff (default main...HEAD)
|
|
40
58
|
|
|
41
59
|
mergestorm> credits
|
|
42
|
-
mergestorm>
|
|
60
|
+
mergestorm> branches
|
|
61
|
+
mergestorm> chain
|
|
43
62
|
mergestorm> exit
|
|
44
63
|
```
|
|
45
64
|
|
|
65
|
+
Bare command names still work exactly as before (`review`, `help`, `exit`, …) — typing `/` at the start of a line just opens the live autocomplete dropdown (filtered as you type, navigate with Up/Down, accept with Tab/Enter/Right). Arrow-key history, Ctrl+A/E, Ctrl+U/K, and Ctrl+C-to-clear all work as in a regular shell. On a dumb terminal, without color, or when piped, the box falls back to plain ASCII borders (or no shell at all for non-TTY input).
|
|
66
|
+
|
|
67
|
+
`credits` / `usage` print a Claude-style meter — long bar + `N% used`, then `Resets … (UTC)` — for standard and premium. `branches` (alias `chains`) lists recently reviewed branches; on a TTY you arrow-select one to open its review-chain timeline. `chain [slug]` shows that timeline directly (defaults to `local/<current-git-branch>`).
|
|
68
|
+
|
|
46
69
|
## Commands
|
|
47
70
|
|
|
48
71
|
One-shot subcommands work the same inside the shell and from argv (scripts/CI):
|
|
@@ -55,8 +78,10 @@ mergestorm login --key Paste an existing API key instead (headless/CI)
|
|
|
55
78
|
mergestorm logout Remove the stored API key
|
|
56
79
|
mergestorm review [base] [head] Review git diff base...head (default: main...HEAD)
|
|
57
80
|
mergestorm status <job_id> Poll a review job and print JSON
|
|
58
|
-
mergestorm credits [--json] Standard + premium credit balance
|
|
81
|
+
mergestorm credits [--json] Standard + premium credit balance (usage bars)
|
|
59
82
|
mergestorm jobs [n] [--json] Recent review jobs (default 10, max 50)
|
|
83
|
+
mergestorm branches [n] Recently reviewed branches (arrow-pick on TTY)
|
|
84
|
+
mergestorm chain [slug] Branch review-chain timeline (default: current branch)
|
|
60
85
|
mergestorm whoami [--json] Key prefix, plan, API base, config path
|
|
61
86
|
mergestorm thread <slug> Jobs in a review thread
|
|
62
87
|
```
|
|
@@ -91,6 +116,10 @@ Fetches a job by id and prints the raw JSON (status, verdict, summary, findings)
|
|
|
91
116
|
|
|
92
117
|
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).
|
|
93
118
|
|
|
119
|
+
### `branches` / `chain`
|
|
120
|
+
|
|
121
|
+
`branches` calls `GET /api/v1/threads` for recently active review threads (branches). On a TTY it opens an arrow-key selector; Enter loads that thread's job timeline. `chain [slug]` is the direct timeline view — omit the slug to use `local/<current-git-branch>` (same slug `review` stamps).
|
|
122
|
+
|
|
94
123
|
## Context pack (`.mergestorm/`)
|
|
95
124
|
|
|
96
125
|
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.
|
package/dist/api.d.ts
CHANGED
|
@@ -18,16 +18,18 @@ export type MeResponse = {
|
|
|
18
18
|
};
|
|
19
19
|
plan_key: string;
|
|
20
20
|
plan_label_key?: string;
|
|
21
|
+
/** Start of next UTC month (usage meters reset). Older APIs omit this. */
|
|
22
|
+
resets_at?: string;
|
|
21
23
|
usage: {
|
|
22
24
|
standard: {
|
|
23
25
|
used: number;
|
|
24
|
-
limit: number;
|
|
25
|
-
remaining: number;
|
|
26
|
+
limit: number | null;
|
|
27
|
+
remaining: number | null;
|
|
26
28
|
};
|
|
27
29
|
premium: {
|
|
28
30
|
used: number;
|
|
29
|
-
limit: number;
|
|
30
|
-
remaining: number;
|
|
31
|
+
limit: number | null;
|
|
32
|
+
remaining: number | null;
|
|
31
33
|
};
|
|
32
34
|
};
|
|
33
35
|
};
|
|
@@ -43,6 +45,41 @@ export type JobListItem = {
|
|
|
43
45
|
finished_at: string | null;
|
|
44
46
|
credits: number;
|
|
45
47
|
};
|
|
48
|
+
export type ThreadListItem = {
|
|
49
|
+
slug: string;
|
|
50
|
+
branch: string | null;
|
|
51
|
+
owner: string | null;
|
|
52
|
+
repo: string | null;
|
|
53
|
+
pr_number: number | null;
|
|
54
|
+
status: string;
|
|
55
|
+
job_count: number;
|
|
56
|
+
last_verdict: string | null;
|
|
57
|
+
last_activity_at: string;
|
|
58
|
+
};
|
|
59
|
+
export type ThreadDetail = {
|
|
60
|
+
thread_id: string;
|
|
61
|
+
slug: string;
|
|
62
|
+
source?: string;
|
|
63
|
+
branch: string | null;
|
|
64
|
+
owner: string | null;
|
|
65
|
+
repo: string | null;
|
|
66
|
+
pr_number: number | null;
|
|
67
|
+
status: string;
|
|
68
|
+
pr_linked_at: string | null;
|
|
69
|
+
job_count: number;
|
|
70
|
+
created_at: string;
|
|
71
|
+
jobs: {
|
|
72
|
+
job_id: string;
|
|
73
|
+
status: string;
|
|
74
|
+
summary?: string | null;
|
|
75
|
+
verdict?: string | null;
|
|
76
|
+
thread_job_number?: number | null;
|
|
77
|
+
created_at: string;
|
|
78
|
+
finished_at?: string | null;
|
|
79
|
+
}[];
|
|
80
|
+
};
|
|
46
81
|
/** Fetch /api/v1/me; returns null on 404/network so callers can degrade. */
|
|
47
82
|
export declare function getMe(cfg?: Config): Promise<MeResponse | null>;
|
|
48
83
|
export declare function listJobs(limit?: number, cfg?: Config): Promise<JobListItem[]>;
|
|
84
|
+
/** Recently reviewed branches/threads (Bearer GET /api/v1/threads). */
|
|
85
|
+
export declare function listThreads(limit?: number, cfg?: Config): Promise<ThreadListItem[]>;
|
package/dist/api.js
CHANGED
|
@@ -69,3 +69,17 @@ export async function listJobs(limit = 10, cfg) {
|
|
|
69
69
|
const items = body.items;
|
|
70
70
|
return Array.isArray(items) ? items : [];
|
|
71
71
|
}
|
|
72
|
+
/** Recently reviewed branches/threads (Bearer GET /api/v1/threads). */
|
|
73
|
+
export async function listThreads(limit = 20, cfg) {
|
|
74
|
+
const resolved = cfg ?? (await loadConfig());
|
|
75
|
+
const capped = Math.min(Math.max(limit, 1), 50);
|
|
76
|
+
const { status, body } = await apiFetch(resolved, `/api/v1/threads?limit=${capped}`);
|
|
77
|
+
if (status === 404) {
|
|
78
|
+
throw new CommandError("Branch list is not available on this server yet. Deploy the API update or use the dashboard.");
|
|
79
|
+
}
|
|
80
|
+
if (status !== 200) {
|
|
81
|
+
throw new CommandError(`Failed to list branches (HTTP ${status}): ${JSON.stringify(body)}`);
|
|
82
|
+
}
|
|
83
|
+
const items = body.items;
|
|
84
|
+
return Array.isArray(items) ? items : [];
|
|
85
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { stdin as input, stdout as output } from "node:process";
|
|
3
3
|
import { DEFAULT_API } from "./config.js";
|
|
4
4
|
import { CommandError } from "./errors.js";
|
|
5
|
+
import { cmdBranches, cmdChain } from "./commands/branches.js";
|
|
5
6
|
import { cmdCredits } from "./commands/credits.js";
|
|
6
7
|
import { cmdJobs, cmdThread } from "./commands/jobs.js";
|
|
7
8
|
import { cmdLogin } from "./commands/login.js";
|
|
@@ -22,8 +23,10 @@ function usage() {
|
|
|
22
23
|
mergestorm logout Remove the stored API key
|
|
23
24
|
mergestorm review [base] [head] Review git diff base...head (default: main...HEAD)
|
|
24
25
|
mergestorm status <job_id> Poll a review job (JSON)
|
|
25
|
-
mergestorm credits [--json] Credit balance
|
|
26
|
+
mergestorm credits [--json] Credit balance (usage bars)
|
|
26
27
|
mergestorm jobs [n] [--json] Recent review jobs
|
|
28
|
+
mergestorm branches [n] Recently reviewed branches (pick on TTY)
|
|
29
|
+
mergestorm chain [slug] Branch review-chain timeline (default: current)
|
|
27
30
|
mergestorm whoami [--json] Key + plan + API base
|
|
28
31
|
mergestorm thread <slug> Jobs in a review thread
|
|
29
32
|
|
|
@@ -48,6 +51,10 @@ async function runCommand(cmd, args) {
|
|
|
48
51
|
return cmdCredits(args);
|
|
49
52
|
if (cmd === "jobs")
|
|
50
53
|
return cmdJobs(args);
|
|
54
|
+
if (cmd === "branches" || cmd === "chains")
|
|
55
|
+
return cmdBranches(args);
|
|
56
|
+
if (cmd === "chain")
|
|
57
|
+
return cmdChain(args);
|
|
51
58
|
if (cmd === "whoami")
|
|
52
59
|
return cmdWhoami(args);
|
|
53
60
|
if (cmd === "thread") {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ThreadListItem } from "../api.js";
|
|
2
|
+
/** One-line label for a branch/thread row (also used by tests). */
|
|
3
|
+
export declare function formatBranchRow(item: ThreadListItem): string;
|
|
4
|
+
/** Interactive list of recently reviewed branches → open chain on select. */
|
|
5
|
+
export declare function cmdBranches(args?: string[]): Promise<void>;
|
|
6
|
+
/**
|
|
7
|
+
* Show the review-chain timeline for a branch/thread.
|
|
8
|
+
* Defaults to `local/<current-git-branch>` when no slug is given.
|
|
9
|
+
*/
|
|
10
|
+
export declare function cmdChain(args?: string[]): Promise<void>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
2
|
+
import { listThreads } from "../api.js";
|
|
3
|
+
import { loadConfig } from "../config.js";
|
|
4
|
+
import { CommandError } from "../errors.js";
|
|
5
|
+
import { git } from "../git.js";
|
|
6
|
+
import { ansi } from "../ui/ansi.js";
|
|
7
|
+
import { selectFromList } from "../ui/select.js";
|
|
8
|
+
import { cmdThread, relativeWhen } from "./jobs.js";
|
|
9
|
+
/** One-line label for a branch/thread row (also used by tests). */
|
|
10
|
+
export function formatBranchRow(item) {
|
|
11
|
+
const name = (item.branch?.trim() || item.slug).slice(0, 36);
|
|
12
|
+
const jobs = `${item.job_count} job${item.job_count === 1 ? "" : "s"}`;
|
|
13
|
+
const verdict = item.last_verdict ?? "-";
|
|
14
|
+
const when = relativeWhen(item.last_activity_at);
|
|
15
|
+
const pr = item.owner && item.repo && item.pr_number != null
|
|
16
|
+
? ` PR #${item.pr_number}`
|
|
17
|
+
: "";
|
|
18
|
+
const closed = item.status === "closed" ? " closed" : "";
|
|
19
|
+
return `${name.padEnd(36)} ${jobs.padEnd(8)} ${verdict.padEnd(12)} ${when}${pr}${closed}`;
|
|
20
|
+
}
|
|
21
|
+
function printBranchTable(items) {
|
|
22
|
+
const header = ["BRANCH / SLUG", "JOBS", "VERDICT", "WHEN", "PR"];
|
|
23
|
+
const rows = items.map((t) => [
|
|
24
|
+
(t.branch?.trim() || t.slug).slice(0, 36),
|
|
25
|
+
String(t.job_count),
|
|
26
|
+
t.last_verdict ?? "-",
|
|
27
|
+
relativeWhen(t.last_activity_at),
|
|
28
|
+
t.pr_number != null ? `#${t.pr_number}` : "-",
|
|
29
|
+
]);
|
|
30
|
+
const widths = header.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i]?.length ?? 0)));
|
|
31
|
+
const fmt = (cols) => " " + cols.map((c, i) => c.padEnd(widths[i])).join(" ");
|
|
32
|
+
console.log(ansi.dim(fmt(header)));
|
|
33
|
+
for (const r of rows)
|
|
34
|
+
console.log(fmt(r));
|
|
35
|
+
console.log(ansi.dim(" open a chain: chain <slug> · or re-run on a TTY to pick"));
|
|
36
|
+
}
|
|
37
|
+
/** Interactive list of recently reviewed branches → open chain on select. */
|
|
38
|
+
export async function cmdBranches(args = []) {
|
|
39
|
+
const asJson = args.includes("--json");
|
|
40
|
+
const nArg = args.find((a) => /^\d+$/.test(a));
|
|
41
|
+
const limit = nArg ? Number.parseInt(nArg, 10) : 20;
|
|
42
|
+
const cfg = await loadConfig();
|
|
43
|
+
const items = await listThreads(limit, cfg);
|
|
44
|
+
if (asJson) {
|
|
45
|
+
console.log(JSON.stringify({ items }, null, 2));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (items.length === 0) {
|
|
49
|
+
console.log(ansi.dim(" No reviewed branches yet. Run `review` on a branch first."));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (!input.isTTY || !output.isTTY) {
|
|
53
|
+
printBranchTable(items);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const picked = await selectFromList({
|
|
57
|
+
title: "Recently reviewed branches",
|
|
58
|
+
items,
|
|
59
|
+
render: (item) => formatBranchRow(item),
|
|
60
|
+
});
|
|
61
|
+
if (!picked) {
|
|
62
|
+
console.log(ansi.dim(" cancelled"));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
await cmdThread(picked.slug, []);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Show the review-chain timeline for a branch/thread.
|
|
69
|
+
* Defaults to `local/<current-git-branch>` when no slug is given.
|
|
70
|
+
*/
|
|
71
|
+
export async function cmdChain(args = []) {
|
|
72
|
+
const asJson = args.includes("--json");
|
|
73
|
+
const positional = args.filter((a) => a !== "--json");
|
|
74
|
+
let slug = positional[0]?.trim() ?? "";
|
|
75
|
+
if (!slug) {
|
|
76
|
+
try {
|
|
77
|
+
const branch = git(["rev-parse", "--abbrev-ref", "HEAD"]).trim() || "local";
|
|
78
|
+
slug = `local/${branch}`.replace(/[^a-zA-Z0-9._/-]/g, "-").slice(0, 120);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
throw new CommandError("usage: chain <slug> (or run inside a git repo to use the current branch)");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
await cmdThread(slug, asJson ? ["--json"] : []);
|
|
85
|
+
}
|
package/dist/commands/credits.js
CHANGED
|
@@ -2,6 +2,7 @@ import { getMe } from "../api.js";
|
|
|
2
2
|
import { loadConfig } from "../config.js";
|
|
3
3
|
import { CommandError } from "../errors.js";
|
|
4
4
|
import { ansi } from "../ui/ansi.js";
|
|
5
|
+
import { formatResetLabel, usageBar } from "../ui/usage.js";
|
|
5
6
|
export async function cmdCredits(args) {
|
|
6
7
|
const asJson = args.includes("--json");
|
|
7
8
|
const cfg = await loadConfig();
|
|
@@ -11,12 +12,17 @@ export async function cmdCredits(args) {
|
|
|
11
12
|
"Try `mergestorm login` or open https://mergestorm.ai/billing");
|
|
12
13
|
}
|
|
13
14
|
if (asJson) {
|
|
14
|
-
console.log(JSON.stringify(me.usage, null, 2));
|
|
15
|
+
console.log(JSON.stringify({ usage: me.usage, resets_at: me.resets_at ?? null }, null, 2));
|
|
15
16
|
return;
|
|
16
17
|
}
|
|
17
18
|
const s = me.usage.standard;
|
|
18
19
|
const p = me.usage.premium;
|
|
19
|
-
|
|
20
|
-
console.log(`
|
|
21
|
-
console.log(
|
|
20
|
+
// Claude Usage tab layout: label → long bar + "N% used" → Resets line.
|
|
21
|
+
console.log(` ${ansi.bold("Standard")}`);
|
|
22
|
+
console.log(` ${usageBar(s.used, s.limit)}`);
|
|
23
|
+
console.log("");
|
|
24
|
+
console.log(` ${ansi.bold("Premium")}`);
|
|
25
|
+
console.log(` ${usageBar(p.used, p.limit)}`);
|
|
26
|
+
console.log("");
|
|
27
|
+
console.log(` ${ansi.dim(formatResetLabel(me.resets_at))}`);
|
|
22
28
|
}
|
package/dist/commands/jobs.d.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
|
+
import { type ThreadDetail } from "../api.js";
|
|
2
|
+
export declare function relativeWhen(iso: string): string;
|
|
1
3
|
export declare function cmdJobs(args: string[]): Promise<void>;
|
|
4
|
+
/** Numbered chain timeline for a thread (branch/PR meta when present). */
|
|
5
|
+
export declare function formatThreadTimeline(data: ThreadDetail): string[];
|
|
2
6
|
export declare function cmdThread(slug: string, args?: string[]): Promise<void>;
|
package/dist/commands/jobs.js
CHANGED
|
@@ -2,7 +2,7 @@ import { apiFetch, listJobs } from "../api.js";
|
|
|
2
2
|
import { loadConfig } from "../config.js";
|
|
3
3
|
import { CommandError } from "../errors.js";
|
|
4
4
|
import { ansi } from "../ui/ansi.js";
|
|
5
|
-
function relativeWhen(iso) {
|
|
5
|
+
export function relativeWhen(iso) {
|
|
6
6
|
const t = new Date(iso).getTime();
|
|
7
7
|
if (Number.isNaN(t))
|
|
8
8
|
return "—";
|
|
@@ -45,7 +45,37 @@ export async function cmdJobs(args) {
|
|
|
45
45
|
console.log(ansi.dim(fmt(header)));
|
|
46
46
|
for (const r of rows)
|
|
47
47
|
console.log(fmt(r));
|
|
48
|
-
console.log(ansi.dim(" full history: https://mergestorm.ai/dashboard/api"));
|
|
48
|
+
console.log(ansi.dim(" branches: `branches` · full history: https://mergestorm.ai/dashboard/api"));
|
|
49
|
+
}
|
|
50
|
+
/** Numbered chain timeline for a thread (branch/PR meta when present). */
|
|
51
|
+
export function formatThreadTimeline(data) {
|
|
52
|
+
const lines = [];
|
|
53
|
+
const title = data.branch?.trim()
|
|
54
|
+
? `${data.branch} (${data.slug})`
|
|
55
|
+
: data.slug;
|
|
56
|
+
lines.push(` ${ansi.bold(title)} · ${data.job_count} job(s) · ${data.status}`);
|
|
57
|
+
if (data.owner && data.repo) {
|
|
58
|
+
const pr = data.pr_number != null
|
|
59
|
+
? `https://github.com/${data.owner}/${data.repo}/pull/${data.pr_number}`
|
|
60
|
+
: `${data.owner}/${data.repo}`;
|
|
61
|
+
lines.push(` ${ansi.dim(pr)}`);
|
|
62
|
+
}
|
|
63
|
+
const jobs = [...(data.jobs ?? [])].sort((a, b) => {
|
|
64
|
+
const an = a.thread_job_number ?? Infinity;
|
|
65
|
+
const bn = b.thread_job_number ?? Infinity;
|
|
66
|
+
if (an !== bn)
|
|
67
|
+
return an - bn;
|
|
68
|
+
return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
|
|
69
|
+
});
|
|
70
|
+
if (jobs.length === 0) {
|
|
71
|
+
lines.push(ansi.dim(" (no jobs in this chain yet)"));
|
|
72
|
+
return lines;
|
|
73
|
+
}
|
|
74
|
+
for (const j of jobs) {
|
|
75
|
+
const n = j.thread_job_number != null ? `#${j.thread_job_number}` : "#?";
|
|
76
|
+
lines.push(` ${n.padEnd(4)} ${j.job_id.slice(0, 8)} ${(j.status ?? "-").padEnd(12)} ${(j.verdict ?? "-").padEnd(16)} ${relativeWhen(j.created_at)}`);
|
|
77
|
+
}
|
|
78
|
+
return lines;
|
|
49
79
|
}
|
|
50
80
|
export async function cmdThread(slug, args = []) {
|
|
51
81
|
const asJson = args.includes("--json");
|
|
@@ -62,8 +92,16 @@ export async function cmdThread(slug, args = []) {
|
|
|
62
92
|
return;
|
|
63
93
|
}
|
|
64
94
|
const data = body;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
95
|
+
for (const line of formatThreadTimeline({
|
|
96
|
+
...data,
|
|
97
|
+
branch: data.branch ?? null,
|
|
98
|
+
owner: data.owner ?? null,
|
|
99
|
+
repo: data.repo ?? null,
|
|
100
|
+
pr_number: data.pr_number ?? null,
|
|
101
|
+
status: data.status ?? "active",
|
|
102
|
+
pr_linked_at: data.pr_linked_at ?? null,
|
|
103
|
+
jobs: data.jobs ?? [],
|
|
104
|
+
})) {
|
|
105
|
+
console.log(line);
|
|
68
106
|
}
|
|
69
107
|
}
|
package/dist/commands/login.js
CHANGED
|
@@ -3,4 +3,9 @@ export type ReviewOptions = {
|
|
|
3
3
|
/** When true, show Ctrl+C detach hint during poll. */
|
|
4
4
|
interactive?: boolean;
|
|
5
5
|
};
|
|
6
|
+
/** Parse `origin` remote into GitHub owner/repo when possible. */
|
|
7
|
+
export declare function parseGithubOriginRepo(): {
|
|
8
|
+
owner: string;
|
|
9
|
+
repo: string;
|
|
10
|
+
} | null;
|
|
6
11
|
export declare function cmdReview(args: string[], opts?: ReviewOptions): Promise<void>;
|
package/dist/commands/review.js
CHANGED
|
@@ -3,6 +3,21 @@ import { loadConfig } from "../config.js";
|
|
|
3
3
|
import { CommandError, DetachedError } from "../errors.js";
|
|
4
4
|
import { collectChangedFiles, collectContextFiles, git } from "../git.js";
|
|
5
5
|
import { ansi } from "../ui/ansi.js";
|
|
6
|
+
/** Parse `origin` remote into GitHub owner/repo when possible. */
|
|
7
|
+
export function parseGithubOriginRepo() {
|
|
8
|
+
try {
|
|
9
|
+
const url = git(["remote", "get-url", "origin"]).trim();
|
|
10
|
+
if (!url)
|
|
11
|
+
return null;
|
|
12
|
+
const m = url.match(/^(?:https?:\/\/|git@)?github\.com[:/]([A-Za-z0-9_-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?\/?$/i);
|
|
13
|
+
if (!m?.[1] || !m[2])
|
|
14
|
+
return null;
|
|
15
|
+
return { owner: m[1], repo: m[2] };
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
6
21
|
function sleep(ms, signal) {
|
|
7
22
|
return new Promise((resolve, reject) => {
|
|
8
23
|
if (signal?.aborted) {
|
|
@@ -32,11 +47,14 @@ export async function cmdReview(args, opts = {}) {
|
|
|
32
47
|
const context_files = await collectContextFiles(process.cwd());
|
|
33
48
|
const branch = git(["rev-parse", "--abbrev-ref", "HEAD"]).trim() || "local";
|
|
34
49
|
const thread = `local/${branch}`.replace(/[^a-zA-Z0-9._/-]/g, "-").slice(0, 120);
|
|
50
|
+
const originRepo = parseGithubOriginRepo();
|
|
35
51
|
console.log(`Submitting review (${Buffer.byteLength(diff, "utf8")} byte diff, ${files.length} file(s), ${context_files.length} context file(s))…`);
|
|
36
52
|
const { status, body } = await apiFetch(cfg, "/api/v1/reviews", {
|
|
37
53
|
method: "POST",
|
|
38
54
|
json: {
|
|
39
55
|
thread,
|
|
56
|
+
branch,
|
|
57
|
+
...(originRepo ? { repo: `${originRepo.owner}/${originRepo.repo}` } : {}),
|
|
40
58
|
base_label: base,
|
|
41
59
|
head_label: head,
|
|
42
60
|
diff,
|
|
@@ -90,9 +108,11 @@ export async function cmdReview(args, opts = {}) {
|
|
|
90
108
|
if (opts.interactive && meBefore) {
|
|
91
109
|
const meAfter = await getMe(cfg);
|
|
92
110
|
if (meAfter) {
|
|
93
|
-
const
|
|
111
|
+
const beforeLeft = meBefore.usage.standard.remaining ?? 0;
|
|
112
|
+
const afterLeft = meAfter.usage.standard.remaining ?? 0;
|
|
113
|
+
const used = Math.max(0, beforeLeft - afterLeft);
|
|
94
114
|
if (used > 0) {
|
|
95
|
-
console.log(ansi.dim(`\n${used} credit${used === 1 ? "" : "s"} used · ${
|
|
115
|
+
console.log(ansi.dim(`\n${used} credit${used === 1 ? "" : "s"} used · ${afterLeft} remaining`));
|
|
96
116
|
}
|
|
97
117
|
}
|
|
98
118
|
}
|
package/dist/shell.d.ts
CHANGED
|
@@ -1 +1,8 @@
|
|
|
1
|
+
import { type CommandSpec } from "./ui/prompt.js";
|
|
2
|
+
export declare const COMMANDS: CommandSpec[];
|
|
3
|
+
/** Strips an optional leading "/" so both "review" and "/review" work. */
|
|
4
|
+
export declare function parseLine(line: string): {
|
|
5
|
+
cmd: string;
|
|
6
|
+
args: string[];
|
|
7
|
+
} | null;
|
|
1
8
|
export declare function runShell(): Promise<void>;
|
package/dist/shell.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { stdin as input, stdout as output } from "node:process";
|
|
1
|
+
import { stdout as output } from "node:process";
|
|
3
2
|
import { loadConfig, resolveApiKey } from "./config.js";
|
|
4
3
|
import { CommandError, DetachedError } from "./errors.js";
|
|
4
|
+
import { cmdBranches, cmdChain } from "./commands/branches.js";
|
|
5
5
|
import { cmdCredits } from "./commands/credits.js";
|
|
6
6
|
import { cmdJobs, cmdThread } from "./commands/jobs.js";
|
|
7
7
|
import { cmdLogin } from "./commands/login.js";
|
|
@@ -11,45 +11,41 @@ import { cmdStatus } from "./commands/status.js";
|
|
|
11
11
|
import { cmdWhoami } from "./commands/whoami.js";
|
|
12
12
|
import { ansi } from "./ui/ansi.js";
|
|
13
13
|
import { printBannerHeader, shellPrompt } from "./ui/banner.js";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"usage",
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
14
|
+
import { askLine, PromptClosedError } from "./ui/prompt.js";
|
|
15
|
+
export const COMMANDS = [
|
|
16
|
+
{ name: "help", summary: "Show available commands" },
|
|
17
|
+
{ name: "login", summary: "Sign in via browser (or paste a key)" },
|
|
18
|
+
{ name: "logout", summary: "Remove the stored API key" },
|
|
19
|
+
{ name: "review", summary: "Review git diff (default main...HEAD)" },
|
|
20
|
+
{ name: "status", summary: "Show a review job" },
|
|
21
|
+
{ name: "credits", summary: "Credit balance with usage bars" },
|
|
22
|
+
{ name: "usage", summary: "Credit balance with usage bars" },
|
|
23
|
+
{ name: "jobs", summary: "Recent review jobs (default 10)" },
|
|
24
|
+
{ name: "branches", summary: "Pick a recently reviewed branch" },
|
|
25
|
+
{ name: "chains", summary: "Alias for branches" },
|
|
26
|
+
{ name: "chain", summary: "Show a branch review-chain timeline" },
|
|
27
|
+
{ name: "whoami", summary: "Key + plan + API base" },
|
|
28
|
+
{ name: "thread", summary: "Jobs in a review thread" },
|
|
29
|
+
{ name: "clear", summary: "Clear screen and reprint banner" },
|
|
30
|
+
{ name: "exit", summary: "Leave the shell" },
|
|
31
|
+
{ name: "quit", summary: "Leave the shell" },
|
|
28
32
|
];
|
|
29
|
-
function completer(line) {
|
|
30
|
-
const hits = COMMANDS.filter((c) => c.startsWith(line));
|
|
31
|
-
return [hits.length ? [...hits] : [...COMMANDS], line];
|
|
32
|
-
}
|
|
33
33
|
function printHelp() {
|
|
34
|
+
const rows = COMMANDS.map((c) => ` ${ansi.green("/" + c.name.padEnd(10))} ${c.summary}`).join("\n");
|
|
34
35
|
console.log(`
|
|
35
36
|
${ansi.bold("Commands")}
|
|
36
|
-
|
|
37
|
-
logout Remove the stored API key
|
|
38
|
-
review [base] [head] Review git diff (default main...HEAD)
|
|
39
|
-
status <job_id> Show a review job
|
|
40
|
-
credits / usage Credit balance
|
|
41
|
-
jobs [n] Recent review jobs (default 10)
|
|
42
|
-
whoami Key + plan + API base
|
|
43
|
-
thread <slug> Jobs in a review thread
|
|
44
|
-
clear Clear screen and reprint banner
|
|
45
|
-
help This help
|
|
46
|
-
exit / quit Leave the shell
|
|
37
|
+
${rows}
|
|
47
38
|
|
|
48
39
|
Dashboard: https://mergestorm.ai/dashboard/api
|
|
49
40
|
`);
|
|
50
41
|
}
|
|
51
|
-
|
|
52
|
-
|
|
42
|
+
/** Strips an optional leading "/" so both "review" and "/review" work. */
|
|
43
|
+
export function parseLine(line) {
|
|
44
|
+
let trimmed = line.trim();
|
|
45
|
+
if (!trimmed)
|
|
46
|
+
return null;
|
|
47
|
+
if (trimmed.startsWith("/"))
|
|
48
|
+
trimmed = trimmed.slice(1).trim();
|
|
53
49
|
if (!trimmed)
|
|
54
50
|
return null;
|
|
55
51
|
const parts = trimmed.split(/\s+/);
|
|
@@ -61,18 +57,10 @@ export async function runShell() {
|
|
|
61
57
|
await printBannerHeader();
|
|
62
58
|
}
|
|
63
59
|
catch { /* banner is best-effort */ }
|
|
64
|
-
const
|
|
65
|
-
input,
|
|
66
|
-
output,
|
|
67
|
-
completer,
|
|
68
|
-
historySize: 200,
|
|
69
|
-
terminal: Boolean(output.isTTY),
|
|
70
|
-
});
|
|
60
|
+
const history = [];
|
|
71
61
|
let busyAbort = null;
|
|
72
62
|
let sigintHint = false;
|
|
73
|
-
let sigintDuringPrompt = false;
|
|
74
63
|
const onSigint = () => {
|
|
75
|
-
sigintDuringPrompt = true;
|
|
76
64
|
if (busyAbort) {
|
|
77
65
|
busyAbort.abort();
|
|
78
66
|
return;
|
|
@@ -88,26 +76,29 @@ export async function runShell() {
|
|
|
88
76
|
process.on("SIGINT", onSigint);
|
|
89
77
|
try {
|
|
90
78
|
while (true) {
|
|
91
|
-
sigintDuringPrompt = false;
|
|
92
79
|
const cfg = await loadConfig();
|
|
93
80
|
const loggedIn = Boolean(resolveApiKey(cfg));
|
|
94
81
|
let line;
|
|
95
82
|
try {
|
|
96
|
-
line = await
|
|
83
|
+
line = await askLine({
|
|
84
|
+
prompt: shellPrompt(loggedIn),
|
|
85
|
+
history,
|
|
86
|
+
commands: COMMANDS,
|
|
87
|
+
});
|
|
97
88
|
}
|
|
98
|
-
catch {
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
89
|
+
catch (err) {
|
|
90
|
+
if (err instanceof PromptClosedError) {
|
|
91
|
+
console.log("");
|
|
92
|
+
break;
|
|
102
93
|
}
|
|
103
|
-
|
|
104
|
-
console.log("");
|
|
105
|
-
break;
|
|
94
|
+
throw err;
|
|
106
95
|
}
|
|
107
96
|
sigintHint = false;
|
|
108
97
|
const parsed = parseLine(line);
|
|
109
98
|
if (!parsed)
|
|
110
99
|
continue;
|
|
100
|
+
if (line.trim())
|
|
101
|
+
history.push(line);
|
|
111
102
|
const { cmd, args } = parsed;
|
|
112
103
|
if (cmd === "exit" || cmd === "quit")
|
|
113
104
|
break;
|
|
@@ -116,7 +107,7 @@ export async function runShell() {
|
|
|
116
107
|
continue;
|
|
117
108
|
}
|
|
118
109
|
if (cmd === "clear") {
|
|
119
|
-
output.write("
|
|
110
|
+
output.write("[2J[H");
|
|
120
111
|
try {
|
|
121
112
|
await printBannerHeader();
|
|
122
113
|
}
|
|
@@ -126,7 +117,7 @@ export async function runShell() {
|
|
|
126
117
|
busyAbort = new AbortController();
|
|
127
118
|
try {
|
|
128
119
|
if (cmd === "login") {
|
|
129
|
-
await cmdLogin(args
|
|
120
|
+
await cmdLogin(args);
|
|
130
121
|
await printBannerHeader();
|
|
131
122
|
}
|
|
132
123
|
else if (cmd === "logout") {
|
|
@@ -148,6 +139,12 @@ export async function runShell() {
|
|
|
148
139
|
else if (cmd === "jobs") {
|
|
149
140
|
await cmdJobs(args);
|
|
150
141
|
}
|
|
142
|
+
else if (cmd === "branches" || cmd === "chains") {
|
|
143
|
+
await cmdBranches(args);
|
|
144
|
+
}
|
|
145
|
+
else if (cmd === "chain") {
|
|
146
|
+
await cmdChain(args);
|
|
147
|
+
}
|
|
151
148
|
else if (cmd === "whoami") {
|
|
152
149
|
await cmdWhoami(args);
|
|
153
150
|
}
|
|
@@ -182,6 +179,5 @@ export async function runShell() {
|
|
|
182
179
|
}
|
|
183
180
|
finally {
|
|
184
181
|
process.off("SIGINT", onSigint);
|
|
185
|
-
rl.close();
|
|
186
182
|
}
|
|
187
183
|
}
|
package/dist/ui/ansi.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
export declare const ansi: {
|
|
2
|
-
enabled: boolean;
|
|
2
|
+
readonly enabled: boolean;
|
|
3
3
|
green: (t: string) => string;
|
|
4
4
|
bold: (t: string) => string;
|
|
5
5
|
dim: (t: string) => string;
|
|
6
6
|
red: (t: string) => string;
|
|
7
7
|
yellow: (t: string) => string;
|
|
8
|
+
gray: (t: string) => string;
|
|
9
|
+
brightGreen: (t: string) => string;
|
|
10
|
+
invert: (t: string) => string;
|
|
8
11
|
boldGreen: (t: string) => string;
|
|
9
12
|
};
|