mergestorm 0.3.9 → 0.3.13
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 +57 -13
- package/dist/api.d.ts +27 -38
- package/dist/api.js +81 -4
- package/dist/client/index.d.ts +5 -0
- package/dist/client/index.js +5 -0
- package/dist/commands/credits.js +33 -12
- package/dist/commands/login.js +28 -7
- package/dist/commands/registry.d.ts +1 -1
- package/dist/commands/registry.js +26 -19
- package/dist/commands/review-client.d.ts +93 -0
- package/dist/commands/review-client.js +289 -0
- package/dist/commands/review.d.ts +30 -6
- package/dist/commands/review.js +385 -146
- package/dist/commands/stack.d.ts +34 -2
- package/dist/commands/stack.js +252 -58
- package/dist/commands/status.d.ts +16 -2
- package/dist/commands/status.js +221 -21
- package/dist/errors.d.ts +18 -2
- package/dist/errors.js +22 -1
- package/dist/git-stack.d.ts +2 -0
- package/dist/git-stack.js +4 -0
- package/dist/git.d.ts +12 -1
- package/dist/git.js +52 -7
- package/dist/shell.d.ts +1 -1
- package/dist/shell.js +6 -3
- package/dist/stack-dto.d.ts +185 -0
- package/dist/stack-dto.js +7 -0
- package/dist/submit-pr-base.d.ts +31 -0
- package/dist/submit-pr-base.js +69 -0
- package/dist/ui/banner.js +7 -0
- package/dist/ui/box.d.ts +13 -9
- package/dist/ui/box.js +28 -44
- package/dist/ui/findings.d.ts +2 -0
- package/dist/ui/findings.js +2 -1
- package/dist/ui/job-envelope.d.ts +68 -0
- package/dist/ui/job-envelope.js +35 -0
- package/dist/ui/prompt.d.ts +91 -0
- package/dist/ui/prompt.js +241 -62
- package/dist/ui/usage.d.ts +25 -1
- package/dist/ui/usage.js +50 -1
- package/dist/ui/width.d.ts +26 -0
- package/dist/ui/width.js +262 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ After install, both `mergestorm` and `mg` invoke the same CLI.
|
|
|
28
28
|
## Source
|
|
29
29
|
|
|
30
30
|
Public source (MIT): [github.com/marginsystems/mergestorm-cli](https://github.com/marginsystems/mergestorm-cli).
|
|
31
|
-
Tags there match npm versions (`v0.3.
|
|
31
|
+
Tags there match npm versions (`v0.3.13`, …). Clone, install, and build:
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
34
|
git clone https://github.com/marginsystems/mergestorm-cli.git
|
|
@@ -44,13 +44,13 @@ node dist/cli.js --help
|
|
|
44
44
|
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.
|
|
45
45
|
|
|
46
46
|
```
|
|
47
|
-
╭─ mergestorm v0.3.
|
|
47
|
+
╭─ mergestorm v0.3.13 ─────────────────────────────────────────────────────────╮
|
|
48
48
|
│ ▟██████████▛ Tips for getting started │
|
|
49
49
|
│ ▜████████▛ review review origin/HEAD or main │
|
|
50
50
|
│ ▝▜████▛▘ stack create → submit → restack → land │
|
|
51
51
|
│ ▜██▛ /help list all commands │
|
|
52
52
|
│ ██ │
|
|
53
|
-
│ ▝▘ What's new in v0.3.
|
|
53
|
+
│ ▝▘ What's new in v0.3.13 │
|
|
54
54
|
│ ● msk… · maelstrom • MIT license + public source (mergestorm-cli) │
|
|
55
55
|
│ [████░░░░░░░░░░] 12% used │
|
|
56
56
|
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
@@ -66,9 +66,9 @@ mergestorm> chain
|
|
|
66
66
|
mergestorm> exit
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
Bare command names still work exactly as before (`review`, `help`, `exit`, …) — typing `/` at the start of a line
|
|
69
|
+
Bare command names still work exactly as before (`review`, `help`, `exit`, …) — typing `/` at the start of a line opens a compact autocomplete overlay (aliases folded onto the primary name, Up/Down scroll a viewport, Esc dismisses, Tab completes, Enter accepts a prefix or submits an exact name as typed, Left/Right move the cursor). Arrow-key history, Ctrl+A/E, Ctrl+U/K work as in a regular shell. At the idle prompt, **Ctrl+C twice** (within ~1.5s) exits the shell; a single Ctrl+C clears the line and shows a confirm hint. While `review` / `status` is waiting, Ctrl+C detaches or aborts that wait (job may keep running) — it does not exit the shell. Ctrl+D on an empty line, or `exit` / `quit`, also leave the 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).
|
|
70
70
|
|
|
71
|
-
`credits` / `usage` print
|
|
71
|
+
`credits` / `usage` print one static panel: key · plan, a credit bar sized to the terminal, `Resets … (UTC)`, then the last five jobs. `--json` adds `recent_jobs`. `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>`).
|
|
72
72
|
|
|
73
73
|
## Commands
|
|
74
74
|
|
|
@@ -81,15 +81,15 @@ mergestorm login Sign in via browser; stores an API key in ~/.mer
|
|
|
81
81
|
mergestorm login --key Paste an existing API key instead (headless/CI)
|
|
82
82
|
mergestorm logout Remove the stored API key
|
|
83
83
|
mergestorm review [base] [head] Review git diff base...head (default: origin/HEAD or main or master)
|
|
84
|
-
mergestorm status <job_id> Fetch a review job
|
|
85
|
-
mergestorm credits [--json]
|
|
84
|
+
mergestorm status <job_id> Fetch a review job (envelope; --json --wait --timeout)
|
|
85
|
+
mergestorm credits [--json] Usage panel (bar + last 5 jobs)
|
|
86
86
|
mergestorm jobs [n] [--json] Recent review jobs (default 10, max 50)
|
|
87
87
|
mergestorm branches [n] Recently reviewed branches (arrow-pick on TTY)
|
|
88
88
|
mergestorm chain [slug] Branch review-chain timeline (default: current branch)
|
|
89
89
|
mergestorm whoami [--json] Key prefix, plan, API base, config path
|
|
90
90
|
mergestorm thread <slug> Jobs in a review thread
|
|
91
|
-
mergestorm stack create [name] New local stack layer (optional `--onto` / `--trunk`)
|
|
92
|
-
mergestorm stack submit Push layers, open
|
|
91
|
+
mergestorm stack create [name] New local stack layer (optional `--onto` / `--trunk` / `--extend`)
|
|
92
|
+
mergestorm stack submit Push layers, open PRs (`gh`), register via adopt (optional `--extend`)
|
|
93
93
|
mergestorm stack list [--json] List registered stacks
|
|
94
94
|
mergestorm stack adopt <owner/repo>#<pr> Import an existing open PR chain
|
|
95
95
|
mergestorm stack restack <stack-id> Restack descendants
|
|
@@ -100,7 +100,9 @@ mergestorm stack reset --force Clear local authoring state (not branches/PRs)
|
|
|
100
100
|
|
|
101
101
|
`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`).
|
|
102
102
|
|
|
103
|
-
`stack create`
|
|
103
|
+
`stack create` 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. `--trunk` only sets trunk metadata — it does **not** change the parent; use `--onto main` (etc.) for a fresh stack from trunk.
|
|
104
|
+
|
|
105
|
+
If the parent branch is already a layer of a **registered** (submitted) stack, `create` and `submit` refuse unless you pass **`--extend`**. That blocks accidentally gluing an unrelated PR onto an open unit. Growing a registered stack on purpose: `mg stack create --onto <tip> --extend` → commit → `mg stack submit --extend`.
|
|
104
106
|
|
|
105
107
|
`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.
|
|
106
108
|
|
|
@@ -118,19 +120,38 @@ Removes `~/.mergestorm/config.json` so the stored key is no longer used.
|
|
|
118
120
|
|
|
119
121
|
### `review [base] [head]`
|
|
120
122
|
|
|
121
|
-
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 (~
|
|
123
|
+
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 (~8 min max — Core plus a Max fleet can exceed 3 minutes). The thread slug defaults to the current branch (`local/<branch>`); pass `--thread` to override so follow-ups chain on a chosen slug. The CLI does not scan `.mergestorm/context/`.
|
|
122
124
|
|
|
123
125
|
```bash
|
|
124
126
|
mergestorm review # discovered trunk (origin/HEAD, main, or master)...HEAD
|
|
125
127
|
mergestorm review develop # develop...HEAD
|
|
126
128
|
mergestorm review main feat/x # main...feat/x
|
|
129
|
+
mergestorm review main --router max
|
|
130
|
+
mergestorm review main --router manual --specialists security,frontend
|
|
131
|
+
mergestorm review --json --context "focus on auth" --context-file docs/adr/007.md
|
|
127
132
|
```
|
|
128
133
|
|
|
129
|
-
|
|
134
|
+
`--router` is the same smart-router mode as Agents settings (`off` / `standard` / `max` / `manual`). `--specialists` pins or invokes lanes on that request (comma-separated). Omit both to use the signed-in user's saved fleet settings (paid default is Standard, same as the dashboard). Manual accepts up to 5 lane ids — the same cap as Agents (the smart router caps `standard` at 3 and `max` at 5).
|
|
135
|
+
|
|
136
|
+
`--context` is author guidance (or `--context -` to read stdin). `--context-file` is repeatable up to 10 files (16 KB each, 60 KB total). `--idempotency-key` replays a prior 202 for the same key. `--webhook-url` must be https; the one-time `webhook_secret` is printed only in `--json` output. Webhook delivery is best-effort (no retry).
|
|
137
|
+
|
|
138
|
+
Exit codes for `review` and `status`:
|
|
139
|
+
|
|
140
|
+
| Exit | Meaning |
|
|
141
|
+
|------|---------|
|
|
142
|
+
| 0 | Success (`completed` or `no_changes`) |
|
|
143
|
+
| 2 | Usage / bad flags |
|
|
144
|
+
| 3 | Quota (`402` or `quota_exceeded`) |
|
|
145
|
+
| 4 | Failed job or other submission error |
|
|
146
|
+
| 5 | Poll / request timeout |
|
|
147
|
+
| 6 | Auth (missing or invalid API key) |
|
|
148
|
+
| 7 | Rate limited (`429`) — wait `retry_after_seconds`, or check a running review with `mergestorm jobs` / `status` |
|
|
149
|
+
|
|
150
|
+
In the interactive shell, Ctrl+C during the wait detaches (job keeps running) — use `status <job_id>` later.
|
|
130
151
|
|
|
131
152
|
### `status <job_id>`
|
|
132
153
|
|
|
133
|
-
Fetches a job by id and prints the
|
|
154
|
+
Fetches a job by id. One-shot default (and `--json` in either mode) prints the same `mergestorm.review_job/v1` object as `review --json`. `--wait [--timeout seconds]` polls until the job finishes; timeout is exit 5 with the id in the object. In the shell, default output is pretty (requested vs run specialists and credits).
|
|
134
155
|
|
|
135
156
|
### `credits` / `jobs` / `whoami`
|
|
136
157
|
|
|
@@ -148,6 +169,29 @@ Require a logged-in key and API support for `GET /api/v1/me` and `GET /api/v1/re
|
|
|
148
169
|
| `MERGESTORM_API_URL` | API base URL | `https://api.mergestorm.ai` |
|
|
149
170
|
| `NO_COLOR` | Disable ANSI colors | — |
|
|
150
171
|
|
|
172
|
+
## Verify (public source vs npm)
|
|
173
|
+
|
|
174
|
+
Build **only** from [marginsystems/mergestorm-cli](https://github.com/marginsystems/mergestorm-cli). Tags there (`vX.Y.Z`) match npm versions. Compare your local pack to the registry hashes below (or `npm view mergestorm@VERSION dist`).
|
|
175
|
+
|
|
176
|
+
| npm | Public git tag | Public commit | npm `dist.shasum` (sha1) | npm `dist.integrity` |
|
|
177
|
+
|-----|----------------|---------------|--------------------------|----------------------|
|
|
178
|
+
| `0.3.9` | [`v0.3.9`](https://github.com/marginsystems/mergestorm-cli/tree/v0.3.9) | `89eaebc29cf793db52baf3e9af0f2bed6e42e458` | `f904f4f642a07c0f9716fe36ed9e5a813be47530` | `sha512-zkbEhwkog13xC70LcM77bWFzLTyoE7Q81daaxz/1Gk3IqVyEoyhKpR+PAtfTp+Mk3MqJ7yVhvZ8/9UG4hpT/Dw==` |
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
git clone https://github.com/marginsystems/mergestorm-cli.git
|
|
182
|
+
cd mergestorm-cli
|
|
183
|
+
git checkout v0.3.9
|
|
184
|
+
git rev-parse HEAD # expect 89eaebc29cf793db52baf3e9af0f2bed6e42e458
|
|
185
|
+
npm ci
|
|
186
|
+
npm run build
|
|
187
|
+
npm pack
|
|
188
|
+
shasum -a 1 mergestorm-0.3.9.tgz
|
|
189
|
+
# expect f904f4f642a07c0f9716fe36ed9e5a813be47530
|
|
190
|
+
# also: npm view mergestorm@0.3.9 dist.shasum dist.integrity
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
After each release, the new row’s git commit is the public `v*` tag tip; shasum/integrity come from the npm registry for that version.
|
|
194
|
+
|
|
151
195
|
## See also
|
|
152
196
|
|
|
153
197
|
- [Public source — mergestorm-cli](https://github.com/marginsystems/mergestorm-cli)
|
package/dist/api.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type Config } from "./config.js";
|
|
2
|
+
import type { StackDto } from "./stack-dto.js";
|
|
3
|
+
export type { StackBranchState, StackCiStatus, StackDto, StackLayerChecks, StackLayerDto, StackReviewStatus, StackTempestStatus, StackUnitDto, StackUnitMemberDto, } from "./stack-dto.js";
|
|
2
4
|
/** Default per-request timeout so a hung API cannot stall the shell/CI forever. */
|
|
3
5
|
export declare const DEFAULT_API_TIMEOUT_MS = 30000;
|
|
4
6
|
/** Message prefix of the CommandError thrown when a request exceeds its timeout. */
|
|
@@ -8,10 +10,17 @@ export type ApiFetchInit = RequestInit & {
|
|
|
8
10
|
/** Override {@link DEFAULT_API_TIMEOUT_MS}. */
|
|
9
11
|
timeoutMs?: number;
|
|
10
12
|
};
|
|
11
|
-
export
|
|
13
|
+
export type ApiFetchResult = {
|
|
12
14
|
status: number;
|
|
13
15
|
body: unknown;
|
|
14
|
-
|
|
16
|
+
retryAfterSeconds?: number;
|
|
17
|
+
};
|
|
18
|
+
/** Prefer the larger of `Retry-After` and JSON `retry_after_seconds`. */
|
|
19
|
+
export declare function parseRetryAfterSeconds(input: {
|
|
20
|
+
header?: string | null;
|
|
21
|
+
body?: unknown;
|
|
22
|
+
}): number | undefined;
|
|
23
|
+
export declare function apiFetch(cfg: Config, route: string, init?: ApiFetchInit): Promise<ApiFetchResult>;
|
|
15
24
|
export declare function devicePost(base: string, route: string, json: unknown, opts?: {
|
|
16
25
|
timeoutMs?: number;
|
|
17
26
|
signal?: AbortSignal;
|
|
@@ -28,7 +37,7 @@ export type MeResponse = {
|
|
|
28
37
|
};
|
|
29
38
|
plan_key: string;
|
|
30
39
|
plan_label_key?: string;
|
|
31
|
-
/**
|
|
40
|
+
/** When the current credit window ends (signup anniversary or Stripe period). Older APIs omit this. */
|
|
32
41
|
resets_at?: string;
|
|
33
42
|
usage: {
|
|
34
43
|
standard: {
|
|
@@ -36,11 +45,6 @@ export type MeResponse = {
|
|
|
36
45
|
limit: number | null;
|
|
37
46
|
remaining: number | null;
|
|
38
47
|
};
|
|
39
|
-
premium: {
|
|
40
|
-
used: number;
|
|
41
|
-
limit: number | null;
|
|
42
|
-
remaining: number | null;
|
|
43
|
-
};
|
|
44
48
|
};
|
|
45
49
|
};
|
|
46
50
|
export type JobListItem = {
|
|
@@ -53,7 +57,9 @@ export type JobListItem = {
|
|
|
53
57
|
head_label: string | null;
|
|
54
58
|
created_at: string;
|
|
55
59
|
finished_at: string | null;
|
|
56
|
-
credits:
|
|
60
|
+
credits: {
|
|
61
|
+
standard: number;
|
|
62
|
+
} | null;
|
|
57
63
|
};
|
|
58
64
|
export type ThreadListItem = {
|
|
59
65
|
slug: string;
|
|
@@ -93,38 +99,21 @@ export declare function getMe(cfg?: Config, opts?: {
|
|
|
93
99
|
timeoutMs?: number;
|
|
94
100
|
}): Promise<MeResponse | null>;
|
|
95
101
|
export declare function listJobs(limit?: number, cfg?: Config): Promise<JobListItem[]>;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
position: number;
|
|
101
|
-
state: string;
|
|
102
|
-
title: string | null;
|
|
103
|
-
htmlUrl: string | null;
|
|
104
|
-
ciStatus: string;
|
|
105
|
-
reviewStatus: string;
|
|
106
|
-
checks: {
|
|
107
|
-
total: number;
|
|
108
|
-
success: number;
|
|
109
|
-
pending: number;
|
|
110
|
-
failure: number;
|
|
111
|
-
failingName: string | null;
|
|
112
|
-
} | null;
|
|
113
|
-
conflictDetail: string | null;
|
|
114
|
-
lastRestackedSha: string | null;
|
|
115
|
-
};
|
|
116
|
-
export type StackDto = {
|
|
117
|
-
id: string;
|
|
118
|
-
owner: string;
|
|
119
|
-
repo: string;
|
|
120
|
-
trunkBranch: string;
|
|
121
|
-
autoPromoteWhenGreen: boolean;
|
|
122
|
-
layers: StackLayerDto[];
|
|
123
|
-
};
|
|
102
|
+
/** Fetch one review job (Bearer GET /api/v1/reviews/:id). */
|
|
103
|
+
export declare function getReview(jobId: string, cfg?: Config, opts?: {
|
|
104
|
+
signal?: AbortSignal;
|
|
105
|
+
}): Promise<unknown>;
|
|
124
106
|
/** List stacks (Bearer GET /api/v1/stacks). */
|
|
125
|
-
export declare function listStacks(cfg?: Config
|
|
107
|
+
export declare function listStacks(cfg?: Config, opts?: {
|
|
108
|
+
timeoutMs?: number;
|
|
109
|
+
}): Promise<StackDto[]>;
|
|
126
110
|
/** Adopt an open PR chain (Bearer POST /api/v1/stacks/adopt). */
|
|
127
111
|
export declare function adoptStack(owner: string, repo: string, prNumber: number, cfg?: Config): Promise<unknown>;
|
|
112
|
+
/** Mint or reuse the PR3+ park freeze (Bearer POST /api/v1/stacks/:id/ensure-upper-park). */
|
|
113
|
+
export declare function ensureUpperPark(stackId: string, cfg?: Config): Promise<{
|
|
114
|
+
freezeBranch: string;
|
|
115
|
+
created: boolean;
|
|
116
|
+
}>;
|
|
128
117
|
/** Restack descendants (Bearer POST /api/v1/stacks/:id/restack). */
|
|
129
118
|
export declare function restackStack(stackId: string, cfg?: Config): Promise<unknown>;
|
|
130
119
|
/** Land the bottom open layer (Bearer POST /api/v1/stacks/:id/land-next). */
|
package/dist/api.js
CHANGED
|
@@ -1,9 +1,42 @@
|
|
|
1
1
|
import { apiBase, loadConfig, resolveApiKey } from "./config.js";
|
|
2
|
-
import { CommandError } from "./errors.js";
|
|
2
|
+
import { CommandError, REVIEW_EXIT, rateLimitedMessage } from "./errors.js";
|
|
3
3
|
/** Default per-request timeout so a hung API cannot stall the shell/CI forever. */
|
|
4
4
|
export const DEFAULT_API_TIMEOUT_MS = 30_000;
|
|
5
5
|
/** Message prefix of the CommandError thrown when a request exceeds its timeout. */
|
|
6
6
|
export const API_TIMEOUT_PREFIX = "Request timed out after";
|
|
7
|
+
function asPositiveSeconds(value) {
|
|
8
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
if (typeof value === "string" && /^\d+(\.\d+)?$/.test(value.trim())) {
|
|
12
|
+
return Number(value.trim());
|
|
13
|
+
}
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
function parseRetryAfterHeader(header) {
|
|
17
|
+
if (!header)
|
|
18
|
+
return undefined;
|
|
19
|
+
const trimmed = header.trim();
|
|
20
|
+
const asNumber = asPositiveSeconds(trimmed);
|
|
21
|
+
if (asNumber !== undefined)
|
|
22
|
+
return asNumber;
|
|
23
|
+
const when = Date.parse(trimmed);
|
|
24
|
+
if (!Number.isFinite(when))
|
|
25
|
+
return undefined;
|
|
26
|
+
return Math.max(0, (when - Date.now()) / 1000);
|
|
27
|
+
}
|
|
28
|
+
/** Prefer the larger of `Retry-After` and JSON `retry_after_seconds`. */
|
|
29
|
+
export function parseRetryAfterSeconds(input) {
|
|
30
|
+
const fromBody = input.body && typeof input.body === "object"
|
|
31
|
+
? asPositiveSeconds(input.body.retry_after_seconds)
|
|
32
|
+
: undefined;
|
|
33
|
+
const fromHeader = parseRetryAfterHeader(input.header);
|
|
34
|
+
if (fromBody === undefined)
|
|
35
|
+
return fromHeader;
|
|
36
|
+
if (fromHeader === undefined)
|
|
37
|
+
return fromBody;
|
|
38
|
+
return Math.max(fromBody, fromHeader);
|
|
39
|
+
}
|
|
7
40
|
function isAbortLike(err) {
|
|
8
41
|
return (err instanceof Error &&
|
|
9
42
|
(err.name === "AbortError" || err.name === "TimeoutError"));
|
|
@@ -82,7 +115,15 @@ export async function apiFetch(cfg, route, init = {}) {
|
|
|
82
115
|
if (res.status === 401) {
|
|
83
116
|
throw new CommandError("API key invalid or revoked. Run `mergestorm login`.", 1, "auth_invalid");
|
|
84
117
|
}
|
|
85
|
-
|
|
118
|
+
const retryAfterSeconds = parseRetryAfterSeconds({
|
|
119
|
+
header: res.headers.get("retry-after"),
|
|
120
|
+
body: parsed,
|
|
121
|
+
});
|
|
122
|
+
return {
|
|
123
|
+
status: res.status,
|
|
124
|
+
body: parsed,
|
|
125
|
+
...(retryAfterSeconds !== undefined ? { retryAfterSeconds } : {}),
|
|
126
|
+
};
|
|
86
127
|
}
|
|
87
128
|
export async function devicePost(base, route, json, opts = {}) {
|
|
88
129
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_API_TIMEOUT_MS;
|
|
@@ -150,10 +191,31 @@ export async function listJobs(limit = 10, cfg) {
|
|
|
150
191
|
const items = body.items;
|
|
151
192
|
return Array.isArray(items) ? items : [];
|
|
152
193
|
}
|
|
194
|
+
/** Fetch one review job (Bearer GET /api/v1/reviews/:id). */
|
|
195
|
+
export async function getReview(jobId, cfg, opts) {
|
|
196
|
+
const resolved = cfg ?? (await loadConfig());
|
|
197
|
+
const id = jobId.trim();
|
|
198
|
+
if (!id) {
|
|
199
|
+
throw new CommandError("job_id is required", 2, "usage");
|
|
200
|
+
}
|
|
201
|
+
const { status, body, retryAfterSeconds } = await apiFetch(resolved, `/api/v1/reviews/${encodeURIComponent(id)}`, { signal: opts?.signal });
|
|
202
|
+
if (status === 404) {
|
|
203
|
+
throw new CommandError(`Review not found: ${id}`);
|
|
204
|
+
}
|
|
205
|
+
if (status === 429) {
|
|
206
|
+
throw new CommandError(rateLimitedMessage(retryAfterSeconds), REVIEW_EXIT.rate_limited, "rate_limited", { retryAfterSeconds });
|
|
207
|
+
}
|
|
208
|
+
if (status !== 200) {
|
|
209
|
+
throw new CommandError(`Failed to get review (HTTP ${status}): ${JSON.stringify(body)}`);
|
|
210
|
+
}
|
|
211
|
+
return body;
|
|
212
|
+
}
|
|
153
213
|
/** List stacks (Bearer GET /api/v1/stacks). */
|
|
154
|
-
export async function listStacks(cfg) {
|
|
214
|
+
export async function listStacks(cfg, opts) {
|
|
155
215
|
const resolved = cfg ?? (await loadConfig());
|
|
156
|
-
const { status, body } = await apiFetch(resolved, "/api/v1/stacks"
|
|
216
|
+
const { status, body } = await apiFetch(resolved, "/api/v1/stacks", {
|
|
217
|
+
timeoutMs: opts?.timeoutMs,
|
|
218
|
+
});
|
|
157
219
|
if (status === 404) {
|
|
158
220
|
throw new CommandError("Stacks API is not available on this server yet. Deploy the API update or use the dashboard.");
|
|
159
221
|
}
|
|
@@ -178,6 +240,21 @@ export async function adoptStack(owner, repo, prNumber, cfg) {
|
|
|
178
240
|
}
|
|
179
241
|
return body;
|
|
180
242
|
}
|
|
243
|
+
/** Mint or reuse the PR3+ park freeze (Bearer POST /api/v1/stacks/:id/ensure-upper-park). */
|
|
244
|
+
export async function ensureUpperPark(stackId, cfg) {
|
|
245
|
+
const body = await stackMutation(stackId, "/ensure-upper-park", "POST", {}, "Failed to ensure upper-park freeze", cfg);
|
|
246
|
+
if (typeof body !== "object" || body === null) {
|
|
247
|
+
throw new CommandError("Unexpected response shape from ensure-upper-park");
|
|
248
|
+
}
|
|
249
|
+
const freezeBranch = body.freezeBranch;
|
|
250
|
+
if (typeof freezeBranch !== "string" || !freezeBranch.trim()) {
|
|
251
|
+
throw new CommandError("ensure-upper-park did not return a freezeBranch");
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
freezeBranch: freezeBranch.trim(),
|
|
255
|
+
created: body.created === true,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
181
258
|
async function stackMutation(stackId, pathSuffix, method, json, failLabel, cfg) {
|
|
182
259
|
const resolved = cfg ?? (await loadConfig());
|
|
183
260
|
const { status, body } = await apiFetch(resolved, `/api/v1/stacks/${encodeURIComponent(stackId)}${pathSuffix}`, method === "PATCH"
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { DEFAULT_API, apiBase, clearConfig, configPath, keyDisplay, loadConfig, mergestormHome, resolveApiKey, saveConfig, type Config, } from "../config.js";
|
|
2
|
+
export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, adoptStack, apiFetch, devicePost, ensureUpperPark, getMe, getReview, landNextStack, listJobs, listStacks, listThreads, parseRetryAfterSeconds, restackStack, setStackAutoPromote, type ApiFetchInit, type ApiFetchResult, type JobListItem, type MeResponse, type ThreadDetail, type ThreadListItem, } from "../api.js";
|
|
3
|
+
export { CommandError, DetachedError, RATE_LIMITED_HINT, REVIEW_EXIT, isCommandErrorCode, rateLimitedMessage, type CommandErrorCode, type CommandErrorExtras, } from "../errors.js";
|
|
4
|
+
export { REVIEW_POLL_BACKOFF_MS, REVIEW_POLL_DEFAULT_TIMEOUT_MS, REVIEW_POLL_INTERVAL_MS, REVIEW_POLL_MAX_INTERVAL_MS, REVIEW_POLL_MAX_TRANSIENT_RETRIES, REVIEW_POLL_STRETCH_AFTER_MS, REVIEW_POLL_STRETCH_SPAN_MS, REVIEW_THREAD_SLUG_RE, ReviewPollTimeoutError, collectReviewInput, isTransientReviewPollError, isTransientReviewPollStatus, loadReviewContext, pollReview, stretchedPollIntervalMs, submitReview, transientRetryWaitMs, type PollReviewOptions, type ReviewInput, type SubmitReviewOptions, } from "../commands/review-client.js";
|
|
5
|
+
export { REVIEW_JOB_ENVELOPE_SCHEMA, toReviewJobEnvelope, type ReviewEnvelopeFallbacks, type ReviewJobEnvelope, type ReviewJobRow, } from "../ui/job-envelope.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { DEFAULT_API, apiBase, clearConfig, configPath, keyDisplay, loadConfig, mergestormHome, resolveApiKey, saveConfig, } from "../config.js";
|
|
2
|
+
export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, adoptStack, apiFetch, devicePost, ensureUpperPark, getMe, getReview, landNextStack, listJobs, listStacks, listThreads, parseRetryAfterSeconds, restackStack, setStackAutoPromote, } from "../api.js";
|
|
3
|
+
export { CommandError, DetachedError, RATE_LIMITED_HINT, REVIEW_EXIT, isCommandErrorCode, rateLimitedMessage, } from "../errors.js";
|
|
4
|
+
export { REVIEW_POLL_BACKOFF_MS, REVIEW_POLL_DEFAULT_TIMEOUT_MS, REVIEW_POLL_INTERVAL_MS, REVIEW_POLL_MAX_INTERVAL_MS, REVIEW_POLL_MAX_TRANSIENT_RETRIES, REVIEW_POLL_STRETCH_AFTER_MS, REVIEW_POLL_STRETCH_SPAN_MS, REVIEW_THREAD_SLUG_RE, ReviewPollTimeoutError, collectReviewInput, isTransientReviewPollError, isTransientReviewPollStatus, loadReviewContext, pollReview, stretchedPollIntervalMs, submitReview, transientRetryWaitMs, } from "../commands/review-client.js";
|
|
5
|
+
export { REVIEW_JOB_ENVELOPE_SCHEMA, toReviewJobEnvelope, } from "../ui/job-envelope.js";
|
package/dist/commands/credits.js
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
import { getMe } from "../api.js";
|
|
1
|
+
import { getMe, 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
|
-
import {
|
|
5
|
+
import { formatUsagePanel } from "../ui/usage.js";
|
|
6
|
+
import { relativeWhen } from "./jobs.js";
|
|
7
|
+
function toPanelJob(job) {
|
|
8
|
+
return {
|
|
9
|
+
job: job.job_id.slice(0, 8),
|
|
10
|
+
verdict: job.verdict ?? job.status ?? "-",
|
|
11
|
+
thread: job.thread_slug ?? "-",
|
|
12
|
+
credits: job.credits ? String(job.credits.standard) : "-",
|
|
13
|
+
when: relativeWhen(job.created_at),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
6
16
|
export async function cmdCredits(args) {
|
|
7
17
|
const asJson = args.includes("--json");
|
|
8
18
|
const cfg = await loadConfig();
|
|
@@ -11,18 +21,29 @@ export async function cmdCredits(args) {
|
|
|
11
21
|
throw new CommandError("Credits are not available (not logged in, offline, or API too old). " +
|
|
12
22
|
"Try `mergestorm login` or open https://mergestorm.ai/billing");
|
|
13
23
|
}
|
|
24
|
+
let recentJobs = [];
|
|
25
|
+
try {
|
|
26
|
+
recentJobs = await listJobs(5, cfg);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
recentJobs = [];
|
|
30
|
+
}
|
|
14
31
|
if (asJson) {
|
|
15
|
-
console.log(JSON.stringify({ usage: me.usage, resets_at: me.resets_at ?? null }, null, 2));
|
|
32
|
+
console.log(JSON.stringify({ usage: me.usage, resets_at: me.resets_at ?? null, recent_jobs: recentJobs }, null, 2));
|
|
16
33
|
return;
|
|
17
34
|
}
|
|
18
35
|
const s = me.usage.standard;
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
36
|
+
const columns = process.stdout.columns || 80;
|
|
37
|
+
const lines = formatUsagePanel({
|
|
38
|
+
keyPrefix: me.key.prefix,
|
|
39
|
+
plan: me.plan_label_key ?? me.plan_key,
|
|
40
|
+
used: s.used,
|
|
41
|
+
limit: s.limit,
|
|
42
|
+
resetsAt: me.resets_at,
|
|
43
|
+
jobs: recentJobs.map(toPanelJob),
|
|
44
|
+
columns,
|
|
45
|
+
color: ansi.enabled,
|
|
46
|
+
});
|
|
47
|
+
for (const line of lines)
|
|
48
|
+
console.log(line);
|
|
28
49
|
}
|
package/dist/commands/login.js
CHANGED
|
@@ -59,13 +59,29 @@ export async function cmdLogin(args) {
|
|
|
59
59
|
const cfg = await loadConfig();
|
|
60
60
|
const base = apiBase(cfg);
|
|
61
61
|
let start;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
let attempt = 0;
|
|
63
|
+
do {
|
|
64
|
+
try {
|
|
65
|
+
start = await devicePost(base, "/api/v1/cli/device", {});
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
throw new CommandError("Could not start device login. The API may be unreachable or misconfigured.\n" +
|
|
69
|
+
"Run `mergestorm login --key` to paste an existing API key instead.");
|
|
70
|
+
}
|
|
71
|
+
const err = start.body?.error;
|
|
72
|
+
if (err !== "rate_limited" && err !== "slow_down")
|
|
73
|
+
break;
|
|
74
|
+
if (start.status !== 429 && start.status !== 400)
|
|
75
|
+
break;
|
|
76
|
+
const backoff = Number(start.body?.retry_after_seconds);
|
|
77
|
+
const waitMs = Number.isFinite(backoff) && backoff > 0 ? backoff * 1000 : 5000;
|
|
78
|
+
if (attempt === 2) {
|
|
79
|
+
throw new CommandError(`Device login is rate limited for this IP. Wait ${Math.ceil(waitMs / 1000)}s, then run \`mergestorm login\` again.`);
|
|
80
|
+
}
|
|
81
|
+
console.log(`Device login is rate limited. Waiting ${Math.ceil(waitMs / 1000)}s before trying again…`);
|
|
82
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
83
|
+
attempt += 1;
|
|
84
|
+
} while (attempt < 3);
|
|
69
85
|
if (start.status !== 200 || !start.body?.device_code) {
|
|
70
86
|
throw new CommandError(`Could not start device login (HTTP ${start.status}). The API may be unreachable or misconfigured.\n` +
|
|
71
87
|
`Run \`mergestorm login --key\` to paste an existing API key instead.`);
|
|
@@ -100,6 +116,11 @@ export async function cmdLogin(args) {
|
|
|
100
116
|
process.stdout.write(".");
|
|
101
117
|
continue;
|
|
102
118
|
}
|
|
119
|
+
if (err === "slow_down" || err === "rate_limited") {
|
|
120
|
+
const backoff = Number(poll.body?.retry_after_seconds);
|
|
121
|
+
await new Promise((r) => setTimeout(r, Number.isFinite(backoff) && backoff > 0 ? backoff * 1000 : pollMs));
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
103
124
|
if (err === "key_limit_reached") {
|
|
104
125
|
throw new CommandError("You have reached the max active API keys. Revoke one at https://mergestorm.ai/settings#api and try again.");
|
|
105
126
|
}
|
|
@@ -22,7 +22,7 @@ export type RegistryCommand = {
|
|
|
22
22
|
*/
|
|
23
23
|
export declare const COMMAND_REGISTRY: RegistryCommand[];
|
|
24
24
|
export declare function findCommand(name: string): RegistryCommand | undefined;
|
|
25
|
-
/** Autocomplete / `/help` list for the interactive shell. */
|
|
25
|
+
/** Autocomplete / `/help` list for the interactive shell. Aliases fold onto the primary name. */
|
|
26
26
|
export declare function shellCommandSpecs(): CommandSpec[];
|
|
27
27
|
export declare function formatUsageBody(): string;
|
|
28
28
|
/**
|
|
@@ -41,7 +41,12 @@ export const COMMAND_REGISTRY = [
|
|
|
41
41
|
name: "review",
|
|
42
42
|
summary: "Review git diff (default origin/HEAD or main or master)",
|
|
43
43
|
usage: [
|
|
44
|
-
"mergestorm review [base] [head] Review git diff base...head
|
|
44
|
+
"mergestorm review [base] [head] Review git diff base...head",
|
|
45
|
+
" [--json] [--wait|--no-wait] [--timeout seconds]",
|
|
46
|
+
" [--router off|standard|max|manual] [--specialists id,id]",
|
|
47
|
+
" [--context text] [--context-file path] [--thread slug]",
|
|
48
|
+
" [--idempotency-key k] [--webhook-url https://…]",
|
|
49
|
+
" HTTP 429 exits 7 (rate_limited) with retry_after_seconds",
|
|
45
50
|
],
|
|
46
51
|
async run(args, ctx) {
|
|
47
52
|
if (ctx.mode === "shell") {
|
|
@@ -55,26 +60,27 @@ export const COMMAND_REGISTRY = [
|
|
|
55
60
|
name: "status",
|
|
56
61
|
summary: "Show a review job",
|
|
57
62
|
usage: [
|
|
58
|
-
"mergestorm status <job_id> Fetch a review job
|
|
63
|
+
"mergestorm status <job_id> Fetch a review job (same envelope as review --json)",
|
|
64
|
+
" [--json] [--wait] [--timeout seconds]",
|
|
65
|
+
" HTTP 429 exits 7 (rate_limited) with retry_after_seconds",
|
|
59
66
|
],
|
|
60
67
|
async run(args, ctx) {
|
|
61
|
-
if (!args[0]) {
|
|
62
|
-
throw new CommandError(ctx.mode === "shell"
|
|
63
|
-
? "usage: status <job_id>"
|
|
64
|
-
: "usage: mergestorm status <job_id>");
|
|
65
|
-
}
|
|
66
68
|
if (ctx.mode === "shell") {
|
|
67
|
-
await cmdStatus(args
|
|
69
|
+
await cmdStatus(args, {
|
|
70
|
+
defaultFormat: "pretty",
|
|
71
|
+
signal: ctx.signal,
|
|
72
|
+
interactive: true,
|
|
73
|
+
});
|
|
68
74
|
return;
|
|
69
75
|
}
|
|
70
|
-
await cmdStatus(args
|
|
76
|
+
await cmdStatus(args, { defaultFormat: "json" });
|
|
71
77
|
},
|
|
72
78
|
},
|
|
73
79
|
{
|
|
74
80
|
name: "credits",
|
|
75
81
|
aliases: ["usage"],
|
|
76
|
-
summary: "
|
|
77
|
-
usage: ["mergestorm credits [--json]
|
|
82
|
+
summary: "Usage panel: credit bar and last five jobs",
|
|
83
|
+
usage: ["mergestorm credits [--json] Usage panel (bar + last 5 jobs)"],
|
|
78
84
|
async run(args) {
|
|
79
85
|
await cmdCredits(args);
|
|
80
86
|
},
|
|
@@ -131,8 +137,8 @@ export const COMMAND_REGISTRY = [
|
|
|
131
137
|
name: "stack",
|
|
132
138
|
summary: "Stacks: create, submit, list, adopt, restack, land",
|
|
133
139
|
usage: [
|
|
134
|
-
"mergestorm stack create [name] New local stack layer (
|
|
135
|
-
"mergestorm stack submit Push active local stack, open PRs
|
|
140
|
+
"mergestorm stack create [name] New local stack layer ([--onto] [--trunk] [--extend])",
|
|
141
|
+
"mergestorm stack submit Push active local stack, open PRs ([--extend])",
|
|
136
142
|
"mergestorm stack reset --force Clear this repo's pre-submit authoring state",
|
|
137
143
|
"mergestorm stack list [--json] List registered stacks",
|
|
138
144
|
"mergestorm stack adopt <owner/repo>#<pr> Import an existing open PR chain",
|
|
@@ -168,18 +174,19 @@ export function findCommand(name) {
|
|
|
168
174
|
const key = name.toLowerCase();
|
|
169
175
|
return COMMAND_REGISTRY.find((c) => c.name === key || (c.aliases?.includes(key) ?? false));
|
|
170
176
|
}
|
|
171
|
-
/** Autocomplete / `/help` list for the interactive shell. */
|
|
177
|
+
/** Autocomplete / `/help` list for the interactive shell. Aliases fold onto the primary name. */
|
|
172
178
|
export function shellCommandSpecs() {
|
|
173
179
|
const specs = [{ name: "help", summary: SHELL_META[0].summary }];
|
|
174
180
|
for (const c of COMMAND_REGISTRY) {
|
|
175
181
|
if (SHELL_OMIT.has(c.name))
|
|
176
182
|
continue;
|
|
177
|
-
specs.push({
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
183
|
+
specs.push({
|
|
184
|
+
name: c.name,
|
|
185
|
+
summary: c.summary,
|
|
186
|
+
...(c.aliases?.length ? { aliases: c.aliases } : {}),
|
|
187
|
+
});
|
|
181
188
|
}
|
|
182
|
-
specs.push({ name: "clear", summary: SHELL_META[1].summary }, { name: "exit", summary: SHELL_META[2].summary
|
|
189
|
+
specs.push({ name: "clear", summary: SHELL_META[1].summary }, { name: "exit", summary: SHELL_META[2].summary, aliases: ["quit"] });
|
|
183
190
|
return specs;
|
|
184
191
|
}
|
|
185
192
|
export function formatUsageBody() {
|