mergestorm 0.3.6 → 0.3.8
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/LICENSE +21 -0
- package/README.md +33 -37
- package/dist/api.d.ts +16 -4
- package/dist/api.js +98 -17
- package/dist/cli.js +20 -68
- package/dist/commands/login.d.ts +8 -2
- package/dist/commands/login.js +32 -20
- package/dist/commands/registry.d.ts +32 -0
- package/dist/commands/registry.js +219 -0
- package/dist/commands/review.d.ts +14 -5
- package/dist/commands/review.js +97 -31
- package/dist/commands/stack.d.ts +27 -0
- package/dist/commands/stack.js +52 -20
- package/dist/commands/status.js +2 -13
- package/dist/errors.d.ts +8 -1
- package/dist/errors.js +6 -1
- package/dist/git-stack.d.ts +5 -0
- package/dist/git-stack.js +12 -0
- package/dist/git.d.ts +6 -1
- package/dist/git.js +31 -5
- package/dist/node-version.d.ts +11 -0
- package/dist/node-version.js +19 -0
- package/dist/pr-body.d.ts +1 -1
- package/dist/pr-body.js +1 -1
- package/dist/repo-identity.js +22 -3
- package/dist/shell.d.ts +2 -2
- package/dist/shell.js +14 -70
- package/dist/stack-meta.js +4 -8
- package/dist/ui/banner.js +42 -6
- package/dist/ui/box.d.ts +8 -1
- package/dist/ui/box.js +15 -3
- package/dist/ui/findings.d.ts +28 -0
- package/dist/ui/findings.js +16 -0
- package/dist/ui/prompt.js +15 -8
- package/dist/ui/secret-input.d.ts +5 -0
- package/dist/ui/secret-input.js +73 -0
- package/package.json +20 -7
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Margin Systems
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,46 +1,41 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Mergestorm CLI
|
|
2
2
|
|
|
3
|
-
Review a local git diff with
|
|
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
4
|
|
|
5
5
|
Requires **Node.js 22+**.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Recommended — install from npm (same package the one-liner uses):
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
|
|
12
|
+
npm install -g mergestorm
|
|
13
|
+
mergestorm --version # confirm install
|
|
13
14
|
mergestorm login
|
|
15
|
+
# or the short alias (same binary):
|
|
16
|
+
mg login
|
|
14
17
|
```
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
Optional curl installer (checks Node 22+, then runs `npm install -g mergestorm`):
|
|
17
20
|
|
|
18
21
|
```bash
|
|
19
|
-
|
|
22
|
+
curl -fsSL https://mergestorm.ai/install.sh | bash
|
|
20
23
|
mergestorm login
|
|
21
|
-
# or the short alias (same binary):
|
|
22
|
-
mg login
|
|
23
24
|
```
|
|
24
25
|
|
|
25
26
|
After install, both `mergestorm` and `mg` invoke the same CLI.
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
## Source
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
Public source (MIT): [github.com/marginsystems/mergestorm-cli](https://github.com/marginsystems/mergestorm-cli).
|
|
31
|
+
Tags there match npm versions (`v0.3.8`, …).
|
|
30
32
|
|
|
31
33
|
```bash
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
node packages/mergestorm-cli/dist/cli.js
|
|
38
|
-
|
|
39
|
-
# Optional: expose a local binary as `mergestorm` without publishing
|
|
40
|
-
npm run build --workspace=mergestorm
|
|
41
|
-
npm link --workspace=mergestorm
|
|
42
|
-
mergestorm # uses your local build
|
|
43
|
-
# later: npm unlink -g mergestorm && npm install -g mergestorm
|
|
34
|
+
git clone https://github.com/marginsystems/mergestorm-cli.git
|
|
35
|
+
cd mergestorm-cli
|
|
36
|
+
npm install
|
|
37
|
+
npm run build
|
|
38
|
+
node dist/cli.js --help
|
|
44
39
|
```
|
|
45
40
|
|
|
46
41
|
## Interactive shell
|
|
@@ -48,21 +43,21 @@ mergestorm # uses your local build
|
|
|
48
43
|
On a TTY, bare `mergestorm` (or `mergestorm shell`) opens a branded REPL: a **full-width** welcome panel (logo + status on the left, tips / what's new on the right) and a **full-width** bordered input box with a live `/` slash-command dropdown. Non-TTY (CI/pipes) prints usage instead — no hanging prompt.
|
|
49
44
|
|
|
50
45
|
```
|
|
51
|
-
╭─ mergestorm v0.3.
|
|
46
|
+
╭─ mergestorm v0.3.8 ──────────────────────────────────────────────────────────╮
|
|
52
47
|
│ ▟██████████▛ Tips for getting started │
|
|
53
|
-
│ ▜████████▛ review review main
|
|
48
|
+
│ ▜████████▛ review review origin/HEAD or main │
|
|
54
49
|
│ ▝▜████▛▘ stack create → submit → restack → land │
|
|
55
50
|
│ ▜██▛ /help list all commands │
|
|
56
51
|
│ ██ │
|
|
57
|
-
│ ▝▘ What's new in v0.3.
|
|
58
|
-
│ ● msk… · maelstrom •
|
|
52
|
+
│ ▝▘ What's new in v0.3.8 │
|
|
53
|
+
│ ● msk… · maelstrom • MIT license + public source (mergestorm-cli) │
|
|
59
54
|
│ [████░░░░░░░░░░] 12% used │
|
|
60
55
|
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
61
56
|
mergestorm
|
|
62
57
|
╭──────────────────────────────────────────────────────────────────────────────╮
|
|
63
58
|
│ › /re │
|
|
64
59
|
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
65
|
-
/review Review git diff (default main
|
|
60
|
+
/review Review git diff (default origin/HEAD or main or master)
|
|
66
61
|
|
|
67
62
|
mergestorm> credits
|
|
68
63
|
mergestorm> branches
|
|
@@ -84,8 +79,8 @@ mergestorm shell Explicit shell entry
|
|
|
84
79
|
mergestorm login Sign in via browser; stores an API key in ~/.mergestorm/config.json
|
|
85
80
|
mergestorm login --key Paste an existing API key instead (headless/CI)
|
|
86
81
|
mergestorm logout Remove the stored API key
|
|
87
|
-
mergestorm review [base] [head] Review git diff base...head (default: main
|
|
88
|
-
mergestorm status <job_id>
|
|
82
|
+
mergestorm review [base] [head] Review git diff base...head (default: origin/HEAD or main or master)
|
|
83
|
+
mergestorm status <job_id> Fetch a review job and print JSON (one shot)
|
|
89
84
|
mergestorm credits [--json] Standard + premium credit balance (usage bars)
|
|
90
85
|
mergestorm jobs [n] [--json] Recent review jobs (default 10, max 50)
|
|
91
86
|
mergestorm branches [n] Recently reviewed branches (arrow-pick on TTY)
|
|
@@ -97,16 +92,16 @@ mergestorm stack submit Push layers, open chained PRs (`gh`), register v
|
|
|
97
92
|
mergestorm stack list [--json] List registered stacks
|
|
98
93
|
mergestorm stack adopt <owner/repo>#<pr> Import an existing open PR chain
|
|
99
94
|
mergestorm stack restack <stack-id> Restack descendants
|
|
100
|
-
mergestorm stack land <stack-id> Land
|
|
101
|
-
mergestorm stack auto-
|
|
95
|
+
mergestorm stack land <stack-id> Land / promote (into review unit when present)
|
|
96
|
+
mergestorm stack auto-promote on|off <stack-id> Toggle auto-promote when green
|
|
102
97
|
mergestorm stack reset --force Clear local authoring state (not branches/PRs)
|
|
103
98
|
```
|
|
104
99
|
|
|
105
|
-
`mg` is a short alias for `mergestorm` (same binary), e.g. `mg stack create`. Happy path: **create → commit → submit → restack → land**. `stack adopt` is for importing a chain that already exists on GitHub (legacy / Graphite). Restack/land use the same login key (`/api/v1/stacks`).
|
|
100
|
+
`mg` is a short alias for `mergestorm` (same binary), e.g. `mg stack create`. Happy path: **create → commit → submit → restack → land**. On review-unit stacks, `stack land` **promotes** the tip into the unit (same gates as the dashboard); otherwise it lands the bottom open PR. `stack auto-promote` turns on land-when-green for a registered stack (`auto-land` remains a deprecated alias). `stack adopt` is for importing a chain that already exists on GitHub (legacy / Graphite). Restack/land use the same login key (`/api/v1/stacks`).
|
|
106
101
|
|
|
107
102
|
`stack create` is local-only: it checks out a new branch from the current tip (or `--onto <branch>`), discovers trunk (`main` / `master` / `origin/HEAD`, overridable with `--trunk`), and records `{ branch, parentBranch }` in CLI-managed state under **`~/.mergestorm/stacks/<repo-id>/stack.json`**. The CLI updates this automatically; never edit it. Linked worktrees share state, while independent clones remain isolated. Creating onto trunk with a non-empty active stack starts a new one. No repository file, GitHub call, or API call is made.
|
|
108
103
|
|
|
109
|
-
`stack submit` walks the **active** local stack: `git push -u origin` each layer, opens a PR with `gh` (`--base` = parent or trunk; skips heads that already have an open PR; body from the tip commit — Summary + Test plan, preserving Fixes/Closes/Resolves), registers the stack with `POST /api/v1/stacks/adopt` internally (Mergestorm API key), then drops that local stack entry so the next create onto trunk is clean. Push/PR auth is your local `git` + `gh auth` — not a
|
|
104
|
+
`stack submit` walks the **active** local stack: `git push -u origin` each layer, opens a PR with `gh` (`--base` = parent or trunk; skips heads that already have an open PR; body from the tip commit — Summary + Test plan, preserving Fixes/Closes/Resolves), registers the stack with `POST /api/v1/stacks/adopt` internally (Mergestorm API key), then drops that local stack entry so the next create onto trunk is clean. Push/PR auth is your local `git` + `gh auth` — not a GitHub App installation token.
|
|
110
105
|
|
|
111
106
|
Legacy repo-local `.mergestorm/stack.json` state migrates automatically on the next stack command. If local authoring state is stale or malformed, use `mg stack reset --force`; this clears only pre-submit CLI state and never deletes branches, PRs, or registered stacks.
|
|
112
107
|
|
|
@@ -114,7 +109,7 @@ Legacy repo-local `.mergestorm/stack.json` state migrates automatically on the n
|
|
|
114
109
|
|
|
115
110
|
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.
|
|
116
111
|
|
|
117
|
-
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)).
|
|
112
|
+
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)). On a TTY the paste is hidden (not echoed); the CLI verifies the key with the API before saving.
|
|
118
113
|
|
|
119
114
|
### `logout`
|
|
120
115
|
|
|
@@ -122,10 +117,10 @@ Removes `~/.mergestorm/config.json` so the stored key is no longer used.
|
|
|
122
117
|
|
|
123
118
|
### `review [base] [head]`
|
|
124
119
|
|
|
125
|
-
Collects `git diff base...head` (default `
|
|
120
|
+
Collects `git diff base...head` (default: discovered trunk — `origin/HEAD`, `main`, or `master`), attaches the changed file contents (up to 40 files, skipping files > 400 KB), 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. Programmatic callers can provide explicit review context through the Review Jobs API.
|
|
126
121
|
|
|
127
122
|
```bash
|
|
128
|
-
mergestorm review # main...HEAD
|
|
123
|
+
mergestorm review # discovered trunk (origin/HEAD, main, or master)...HEAD
|
|
129
124
|
mergestorm review develop # develop...HEAD
|
|
130
125
|
mergestorm review main feat/x # main...feat/x
|
|
131
126
|
```
|
|
@@ -154,5 +149,6 @@ Require a logged-in key and API support for `GET /api/v1/me` and `GET /api/v1/re
|
|
|
154
149
|
|
|
155
150
|
## See also
|
|
156
151
|
|
|
157
|
-
- [
|
|
152
|
+
- [Public source — mergestorm-cli](https://github.com/marginsystems/mergestorm-cli)
|
|
158
153
|
- [Docs — Review Jobs API & CLI](https://mergestorm.ai/docs#review-jobs-api) — product docs on the site.
|
|
154
|
+
- [npm — mergestorm](https://www.npmjs.com/package/mergestorm)
|
package/dist/api.d.ts
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { type Config } from "./config.js";
|
|
2
|
-
|
|
2
|
+
/** Default per-request timeout so a hung API cannot stall the shell/CI forever. */
|
|
3
|
+
export declare const DEFAULT_API_TIMEOUT_MS = 30000;
|
|
4
|
+
/** Message prefix of the CommandError thrown when a request exceeds its timeout. */
|
|
5
|
+
export declare const API_TIMEOUT_PREFIX = "Request timed out after";
|
|
6
|
+
export type ApiFetchInit = RequestInit & {
|
|
3
7
|
json?: unknown;
|
|
4
|
-
}
|
|
8
|
+
/** Override {@link DEFAULT_API_TIMEOUT_MS}. */
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
};
|
|
11
|
+
export declare function apiFetch(cfg: Config, route: string, init?: ApiFetchInit): Promise<{
|
|
5
12
|
status: number;
|
|
6
13
|
body: unknown;
|
|
7
14
|
}>;
|
|
8
|
-
export declare function devicePost(base: string, route: string, json: unknown
|
|
15
|
+
export declare function devicePost(base: string, route: string, json: unknown, opts?: {
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
signal?: AbortSignal;
|
|
18
|
+
}): Promise<{
|
|
9
19
|
status: number;
|
|
10
20
|
body: any;
|
|
11
21
|
}>;
|
|
@@ -79,7 +89,9 @@ export type ThreadDetail = {
|
|
|
79
89
|
}[];
|
|
80
90
|
};
|
|
81
91
|
/** Fetch /api/v1/me; returns null on 404/network so callers can degrade. */
|
|
82
|
-
export declare function getMe(cfg?: Config
|
|
92
|
+
export declare function getMe(cfg?: Config, opts?: {
|
|
93
|
+
timeoutMs?: number;
|
|
94
|
+
}): Promise<MeResponse | null>;
|
|
83
95
|
export declare function listJobs(limit?: number, cfg?: Config): Promise<JobListItem[]>;
|
|
84
96
|
export type StackLayerDto = {
|
|
85
97
|
branch: string;
|
package/dist/api.js
CHANGED
|
@@ -1,21 +1,77 @@
|
|
|
1
1
|
import { apiBase, loadConfig, resolveApiKey } from "./config.js";
|
|
2
2
|
import { CommandError } from "./errors.js";
|
|
3
|
+
/** Default per-request timeout so a hung API cannot stall the shell/CI forever. */
|
|
4
|
+
export const DEFAULT_API_TIMEOUT_MS = 30_000;
|
|
5
|
+
/** Message prefix of the CommandError thrown when a request exceeds its timeout. */
|
|
6
|
+
export const API_TIMEOUT_PREFIX = "Request timed out after";
|
|
7
|
+
function isAbortLike(err) {
|
|
8
|
+
return (err instanceof Error &&
|
|
9
|
+
(err.name === "AbortError" || err.name === "TimeoutError"));
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Combine an optional caller signal with a ref'd timeout.
|
|
13
|
+
* (Built-in `AbortSignal.timeout` unrefs its timer, which can leave hung
|
|
14
|
+
* fetches without a live timer in edge cases / tests.)
|
|
15
|
+
*/
|
|
16
|
+
function withRequestSignal(userSignal, timeoutMs) {
|
|
17
|
+
const ac = new AbortController();
|
|
18
|
+
const timer = setTimeout(() => {
|
|
19
|
+
ac.abort(new DOMException("The operation was aborted due to timeout", "TimeoutError"));
|
|
20
|
+
}, timeoutMs);
|
|
21
|
+
const onUserAbort = () => {
|
|
22
|
+
ac.abort(userSignal.reason);
|
|
23
|
+
};
|
|
24
|
+
if (userSignal) {
|
|
25
|
+
if (userSignal.aborted)
|
|
26
|
+
onUserAbort();
|
|
27
|
+
else
|
|
28
|
+
userSignal.addEventListener("abort", onUserAbort, { once: true });
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
signal: ac.signal,
|
|
32
|
+
dispose: () => {
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
userSignal?.removeEventListener("abort", onUserAbort);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Map hung-request aborts to CommandError; preserve caller cancel (Ctrl+C detach). */
|
|
39
|
+
function rethrowFetchAbort(err, timeoutMs) {
|
|
40
|
+
if (isAbortLike(err)) {
|
|
41
|
+
if (err.name === "AbortError")
|
|
42
|
+
throw err;
|
|
43
|
+
throw new CommandError(`${API_TIMEOUT_PREFIX} ${Math.round(timeoutMs / 1000)}s. Check network connectivity and API status.`, 1, "api_timeout");
|
|
44
|
+
}
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
3
47
|
export async function apiFetch(cfg, route, init = {}) {
|
|
4
48
|
const key = resolveApiKey(cfg);
|
|
5
49
|
if (!key) {
|
|
6
|
-
throw new CommandError("No API key. Run `mergestorm login` or set MERGESTORM_API_KEY.");
|
|
50
|
+
throw new CommandError("No API key. Run `mergestorm login` or set MERGESTORM_API_KEY.", 1, "missing_api_key");
|
|
7
51
|
}
|
|
52
|
+
const { json, timeoutMs = DEFAULT_API_TIMEOUT_MS, signal: userSignal, ...rest } = init;
|
|
8
53
|
const headers = {
|
|
9
54
|
Authorization: `Bearer ${key}`,
|
|
10
|
-
...
|
|
55
|
+
...rest.headers,
|
|
11
56
|
};
|
|
12
|
-
let body =
|
|
13
|
-
if (
|
|
57
|
+
let body = rest.body;
|
|
58
|
+
if (json !== undefined) {
|
|
14
59
|
headers["Content-Type"] = "application/json";
|
|
15
|
-
body = JSON.stringify(
|
|
60
|
+
body = JSON.stringify(json);
|
|
61
|
+
}
|
|
62
|
+
const { signal, dispose } = withRequestSignal(userSignal, timeoutMs);
|
|
63
|
+
let res;
|
|
64
|
+
let text = "";
|
|
65
|
+
try {
|
|
66
|
+
res = await fetch(`${apiBase(cfg)}${route}`, { ...rest, headers, body, signal });
|
|
67
|
+
text = await res.text();
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
rethrowFetchAbort(err, timeoutMs);
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
dispose();
|
|
16
74
|
}
|
|
17
|
-
const res = await fetch(`${apiBase(cfg)}${route}`, { ...init, headers, body });
|
|
18
|
-
const text = await res.text();
|
|
19
75
|
let parsed = text;
|
|
20
76
|
try {
|
|
21
77
|
parsed = JSON.parse(text);
|
|
@@ -23,15 +79,31 @@ export async function apiFetch(cfg, route, init = {}) {
|
|
|
23
79
|
catch {
|
|
24
80
|
// keep text
|
|
25
81
|
}
|
|
82
|
+
if (res.status === 401) {
|
|
83
|
+
throw new CommandError("API key invalid or revoked. Run `mergestorm login`.", 1, "auth_invalid");
|
|
84
|
+
}
|
|
26
85
|
return { status: res.status, body: parsed };
|
|
27
86
|
}
|
|
28
|
-
export async function devicePost(base, route, json) {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
87
|
+
export async function devicePost(base, route, json, opts = {}) {
|
|
88
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_API_TIMEOUT_MS;
|
|
89
|
+
const { signal, dispose } = withRequestSignal(opts.signal, timeoutMs);
|
|
90
|
+
let res;
|
|
91
|
+
let text = "";
|
|
92
|
+
try {
|
|
93
|
+
res = await fetch(`${base}${route}`, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: { "Content-Type": "application/json" },
|
|
96
|
+
body: JSON.stringify(json),
|
|
97
|
+
signal,
|
|
98
|
+
});
|
|
99
|
+
text = await res.text();
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
rethrowFetchAbort(err, timeoutMs);
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
dispose();
|
|
106
|
+
}
|
|
35
107
|
let parsed = text;
|
|
36
108
|
try {
|
|
37
109
|
parsed = JSON.parse(text);
|
|
@@ -42,17 +114,26 @@ export async function devicePost(base, route, json) {
|
|
|
42
114
|
return { status: res.status, body: parsed };
|
|
43
115
|
}
|
|
44
116
|
/** Fetch /api/v1/me; returns null on 404/network so callers can degrade. */
|
|
45
|
-
export async function getMe(cfg) {
|
|
117
|
+
export async function getMe(cfg, opts) {
|
|
46
118
|
const resolved = cfg ?? (await loadConfig());
|
|
47
119
|
try {
|
|
48
|
-
const { status, body } = await apiFetch(resolved, "/api/v1/me"
|
|
120
|
+
const { status, body } = await apiFetch(resolved, "/api/v1/me", {
|
|
121
|
+
timeoutMs: opts?.timeoutMs,
|
|
122
|
+
});
|
|
49
123
|
if (status === 404)
|
|
50
124
|
return null;
|
|
51
125
|
if (status !== 200)
|
|
52
126
|
return null;
|
|
53
127
|
return body;
|
|
54
128
|
}
|
|
55
|
-
catch {
|
|
129
|
+
catch (err) {
|
|
130
|
+
// 401 (and missing-key) must surface — do not misdiagnose as offline/old API.
|
|
131
|
+
// Timeouts degrade like other network failures (banner / soft probes).
|
|
132
|
+
if (err instanceof CommandError) {
|
|
133
|
+
if (err.code === "api_timeout")
|
|
134
|
+
return null;
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
56
137
|
return null;
|
|
57
138
|
}
|
|
58
139
|
}
|
package/dist/cli.js
CHANGED
|
@@ -1,84 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
-
import { DEFAULT_API } from "./config.js";
|
|
4
3
|
import { CommandError } from "./errors.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { cmdJobs, cmdThread } from "./commands/jobs.js";
|
|
8
|
-
import { cmdLogin } from "./commands/login.js";
|
|
9
|
-
import { cmdLogout } from "./commands/logout.js";
|
|
10
|
-
import { cmdReview } from "./commands/review.js";
|
|
11
|
-
import { cmdStack } from "./commands/stack.js";
|
|
12
|
-
import { cmdStatus } from "./commands/status.js";
|
|
13
|
-
import { cmdWhoami } from "./commands/whoami.js";
|
|
4
|
+
import { dispatchCommand, formatUsageBody, } from "./commands/registry.js";
|
|
5
|
+
import { isSupportedNode, unsupportedNodeMessage, } from "./node-version.js";
|
|
14
6
|
import { runShell } from "./shell.js";
|
|
15
7
|
import { ansi } from "./ui/ansi.js";
|
|
16
8
|
import { printWordmark } from "./ui/banner.js";
|
|
9
|
+
import { cliVersion } from "./version.js";
|
|
10
|
+
if (!isSupportedNode()) {
|
|
11
|
+
console.error(unsupportedNodeMessage());
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
17
14
|
function usage() {
|
|
18
15
|
printWordmark();
|
|
19
16
|
console.log(`Usage:
|
|
20
|
-
|
|
21
|
-
mergestorm shell Explicit shell entry
|
|
22
|
-
mergestorm login Sign in via browser and store an API key
|
|
23
|
-
mergestorm login --key Paste an existing API key instead (headless/CI)
|
|
24
|
-
mergestorm logout Remove the stored API key
|
|
25
|
-
mergestorm review [base] [head] Review git diff base...head (default: main...HEAD)
|
|
26
|
-
mergestorm status <job_id> Poll a review job (JSON)
|
|
27
|
-
mergestorm credits [--json] Credit balance (usage bars)
|
|
28
|
-
mergestorm jobs [n] [--json] Recent review jobs
|
|
29
|
-
mergestorm branches [n] Recently reviewed branches (pick on TTY)
|
|
30
|
-
mergestorm chain [slug] Branch review-chain timeline (default: current)
|
|
31
|
-
mergestorm whoami [--json] Key + plan + API base
|
|
32
|
-
mergestorm thread <slug> Jobs in a review thread
|
|
33
|
-
mergestorm stack create [name] New local stack layer (CLI-managed authoring state)
|
|
34
|
-
mergestorm stack submit Push active local stack, open PRs, register stack
|
|
35
|
-
mergestorm stack reset --force Clear this repo's pre-submit authoring state
|
|
36
|
-
mergestorm stack list [--json] List registered stacks
|
|
37
|
-
mergestorm stack adopt <owner/repo>#<pr> Import an existing open PR chain
|
|
38
|
-
mergestorm stack restack <stack-id> Restack stack descendants
|
|
39
|
-
mergestorm stack land <stack-id> Land bottom PR (or promote into unit)
|
|
40
|
-
mergestorm stack auto-promote on|off <stack-id> Toggle auto-promote when green
|
|
41
|
-
|
|
42
|
-
mg is a short alias for mergestorm (same binary).
|
|
43
|
-
|
|
44
|
-
Env:
|
|
45
|
-
MERGESTORM_API_KEY API key (overrides config)
|
|
46
|
-
MERGESTORM_API_URL API base (default ${DEFAULT_API})
|
|
17
|
+
${formatUsageBody()}
|
|
47
18
|
`);
|
|
48
19
|
}
|
|
49
20
|
async function runCommand(cmd, args) {
|
|
50
|
-
if (cmd === "
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return cmdLogout();
|
|
54
|
-
if (cmd === "review")
|
|
55
|
-
return cmdReview(args);
|
|
56
|
-
if (cmd === "status") {
|
|
57
|
-
if (!args[0])
|
|
58
|
-
throw new CommandError("usage: mergestorm status <job_id>");
|
|
59
|
-
return cmdStatus(args[0], { format: "json" });
|
|
21
|
+
if (cmd === "help" || cmd === "?") {
|
|
22
|
+
usage();
|
|
23
|
+
return;
|
|
60
24
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (cmd === "branches" || cmd === "chains")
|
|
66
|
-
return cmdBranches(args);
|
|
67
|
-
if (cmd === "chain")
|
|
68
|
-
return cmdChain(args);
|
|
69
|
-
if (cmd === "whoami")
|
|
70
|
-
return cmdWhoami(args);
|
|
71
|
-
if (cmd === "thread") {
|
|
72
|
-
if (!args[0])
|
|
73
|
-
throw new CommandError("usage: mergestorm thread <slug>");
|
|
74
|
-
return cmdThread(args[0], args.slice(1));
|
|
25
|
+
const ok = await dispatchCommand(cmd, args, { mode: "oneshot" });
|
|
26
|
+
if (!ok) {
|
|
27
|
+
usage();
|
|
28
|
+
throw new CommandError(`unknown command: ${cmd}`);
|
|
75
29
|
}
|
|
76
|
-
if (cmd === "stack")
|
|
77
|
-
return cmdStack(args);
|
|
78
|
-
if (cmd === "shell")
|
|
79
|
-
return runShell();
|
|
80
|
-
usage();
|
|
81
|
-
throw new CommandError(`unknown command: ${cmd}`);
|
|
82
30
|
}
|
|
83
31
|
async function main() {
|
|
84
32
|
const [, , cmd, ...args] = process.argv;
|
|
@@ -90,10 +38,14 @@ async function main() {
|
|
|
90
38
|
usage();
|
|
91
39
|
return;
|
|
92
40
|
}
|
|
93
|
-
if (cmd === "-h" || cmd === "--help") {
|
|
41
|
+
if (cmd === "-h" || cmd === "--help" || cmd === "help" || cmd === "?") {
|
|
94
42
|
usage();
|
|
95
43
|
return;
|
|
96
44
|
}
|
|
45
|
+
if (cmd === "-v" || cmd === "--version" || cmd === "version") {
|
|
46
|
+
console.log(cliVersion());
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
97
49
|
try {
|
|
98
50
|
await runCommand(cmd, args);
|
|
99
51
|
}
|
package/dist/commands/login.d.ts
CHANGED
|
@@ -1,2 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { type Config } from "../config.js";
|
|
2
|
+
import { type MeResponse } from "../api.js";
|
|
3
|
+
/**
|
|
4
|
+
* Probe /api/v1/me with an in-memory config. Does not write disk.
|
|
5
|
+
* Throws CommandError when the key must not be saved.
|
|
6
|
+
*/
|
|
7
|
+
export declare function validateApiKey(cfg: Config): Promise<MeResponse>;
|
|
8
|
+
export declare function cmdLogin(args: string[]): Promise<void>;
|
package/dist/commands/login.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
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
3
|
import { apiBase, configPath, loadConfig, saveConfig } from "../config.js";
|
|
6
|
-
import { devicePost } from "../api.js";
|
|
4
|
+
import { devicePost, getMe } from "../api.js";
|
|
7
5
|
import { CommandError } from "../errors.js";
|
|
8
6
|
import { ansi } from "../ui/ansi.js";
|
|
7
|
+
import { readSecretLine } from "../ui/secret-input.js";
|
|
9
8
|
function openBrowser(url) {
|
|
10
9
|
const os = platform();
|
|
11
10
|
const cmd = os === "darwin" ? "open" : os === "win32" ? "cmd" : "xdg-open";
|
|
@@ -19,29 +18,42 @@ function openBrowser(url) {
|
|
|
19
18
|
// URL is printed for manual open
|
|
20
19
|
}
|
|
21
20
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Probe /api/v1/me with an in-memory config. Does not write disk.
|
|
23
|
+
* Throws CommandError when the key must not be saved.
|
|
24
|
+
*/
|
|
25
|
+
export async function validateApiKey(cfg) {
|
|
27
26
|
try {
|
|
28
|
-
const
|
|
29
|
-
if (!
|
|
30
|
-
throw new CommandError("Key
|
|
27
|
+
const me = await getMe(cfg);
|
|
28
|
+
if (!me) {
|
|
29
|
+
throw new CommandError("Could not verify API key (API unreachable or too old). Key was not saved.");
|
|
31
30
|
}
|
|
32
|
-
|
|
33
|
-
cfg.apiKey = key;
|
|
34
|
-
await saveConfig(cfg);
|
|
35
|
-
console.log(`Saved to ${configPath()}`);
|
|
31
|
+
return me;
|
|
36
32
|
}
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
|
|
33
|
+
catch (err) {
|
|
34
|
+
if (err instanceof CommandError && err.code === "auth_invalid") {
|
|
35
|
+
throw new CommandError("API key invalid or revoked. Key was not saved. Check the key and run `mergestorm login --key` again.", 1, "auth_invalid");
|
|
36
|
+
}
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function loginWithKey() {
|
|
41
|
+
// Always use muted secret input on TTY so the key never lands in
|
|
42
|
+
// question()/history echo.
|
|
43
|
+
const key = (await readSecretLine("Paste your Mergestorm API key (msk_live_...): ")).trim();
|
|
44
|
+
if (!key.startsWith("msk_live_")) {
|
|
45
|
+
throw new CommandError("Key should start with msk_live_");
|
|
40
46
|
}
|
|
47
|
+
const cfg = await loadConfig();
|
|
48
|
+
cfg.apiKey = key;
|
|
49
|
+
const me = await validateApiKey(cfg);
|
|
50
|
+
await saveConfig(cfg);
|
|
51
|
+
const prefix = me.key?.prefix?.trim() || "msk_live_…";
|
|
52
|
+
console.log(`Verified ${prefix}… · saved to ${configPath()}`);
|
|
41
53
|
}
|
|
42
|
-
export async function cmdLogin(args
|
|
54
|
+
export async function cmdLogin(args) {
|
|
43
55
|
if (args.includes("--key")) {
|
|
44
|
-
await loginWithKey(
|
|
56
|
+
await loginWithKey();
|
|
45
57
|
return;
|
|
46
58
|
}
|
|
47
59
|
const cfg = await loadConfig();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { CommandSpec } from "../ui/prompt.js";
|
|
2
|
+
/** Shared dispatch context for oneshot argv and the interactive shell. */
|
|
3
|
+
export type CommandContext = {
|
|
4
|
+
mode: "oneshot" | "shell";
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
/** Shell: refresh banner after login/logout. */
|
|
7
|
+
afterAuth?: () => Promise<void>;
|
|
8
|
+
};
|
|
9
|
+
export type RegistryCommand = {
|
|
10
|
+
name: string;
|
|
11
|
+
/** Alternate names that resolve to the same handler (also listed in shell autocomplete). */
|
|
12
|
+
aliases?: string[];
|
|
13
|
+
/** Short line for shell `/help` and autocomplete. */
|
|
14
|
+
summary: string;
|
|
15
|
+
/** Lines under oneshot `Usage:` (already prefixed with `mergestorm …`). */
|
|
16
|
+
usage: string[];
|
|
17
|
+
run: (args: string[], ctx: CommandContext) => Promise<void>;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Single source of truth for CLI commands (STRUCT-01).
|
|
21
|
+
* Derive oneshot usage, shell COMMANDS, and both dispatchers from this table.
|
|
22
|
+
*/
|
|
23
|
+
export declare const COMMAND_REGISTRY: RegistryCommand[];
|
|
24
|
+
export declare function findCommand(name: string): RegistryCommand | undefined;
|
|
25
|
+
/** Autocomplete / `/help` list for the interactive shell. */
|
|
26
|
+
export declare function shellCommandSpecs(): CommandSpec[];
|
|
27
|
+
export declare function formatUsageBody(): string;
|
|
28
|
+
/**
|
|
29
|
+
* Run a registered command. Returns false when `name` is unknown
|
|
30
|
+
* (caller prints usage / shell hint).
|
|
31
|
+
*/
|
|
32
|
+
export declare function dispatchCommand(name: string, args: string[], ctx: CommandContext): Promise<boolean>;
|