mergestorm 0.3.3 → 0.3.5
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 +13 -7
- package/dist/api.d.ts +10 -1
- package/dist/api.js +6 -3
- package/dist/cli.js +5 -3
- package/dist/commands/stack.d.ts +7 -0
- package/dist/commands/stack.js +219 -3
- package/dist/gh.d.ts +20 -0
- package/dist/gh.js +89 -0
- package/dist/git-stack.d.ts +24 -0
- package/dist/git-stack.js +129 -0
- package/dist/pr-body.d.ts +8 -0
- package/dist/pr-body.js +63 -0
- package/dist/shell.js +1 -1
- package/dist/stack-meta.d.ts +15 -0
- package/dist/stack-meta.js +41 -0
- package/dist/ui/banner.js +9 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -48,14 +48,14 @@ mergestorm # uses your local build
|
|
|
48
48
|
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
49
|
|
|
50
50
|
```
|
|
51
|
-
╭─ mergestorm v0.3.
|
|
51
|
+
╭─ mergestorm v0.3.5 ──────────────────────────────────────────────────────────╮
|
|
52
52
|
│ ▟██████████▛ Tips for getting started │
|
|
53
53
|
│ ▜████████▛ review review main...HEAD │
|
|
54
|
-
│ ▝▜████▛▘ stack
|
|
54
|
+
│ ▝▜████▛▘ stack create → submit → restack → land │
|
|
55
55
|
│ ▜██▛ /help list all commands │
|
|
56
56
|
│ ██ │
|
|
57
|
-
│ ▝▘ What's new in v0.3.
|
|
58
|
-
│ ● msk… · maelstrom • stack
|
|
57
|
+
│ ▝▘ What's new in v0.3.5 │
|
|
58
|
+
│ ● msk… · maelstrom • stack submit fills PR body from tip commit │
|
|
59
59
|
│ [████░░░░░░░░░░] 12% used │
|
|
60
60
|
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
61
61
|
mergestorm
|
|
@@ -92,14 +92,20 @@ mergestorm branches [n] Recently reviewed branches (arrow-pick on TTY)
|
|
|
92
92
|
mergestorm chain [slug] Branch review-chain timeline (default: current branch)
|
|
93
93
|
mergestorm whoami [--json] Key prefix, plan, API base, config path
|
|
94
94
|
mergestorm thread <slug> Jobs in a review thread
|
|
95
|
-
mergestorm stack
|
|
96
|
-
mergestorm stack
|
|
95
|
+
mergestorm stack create [name] New local stack layer (optional `--onto` / `--trunk`)
|
|
96
|
+
mergestorm stack submit Push layers, open chained PRs (`gh`), register via adopt
|
|
97
|
+
mergestorm stack list [--json] List registered stacks
|
|
98
|
+
mergestorm stack adopt <owner/repo>#<pr> Import an existing open PR chain
|
|
97
99
|
mergestorm stack restack <stack-id> Restack descendants
|
|
98
100
|
mergestorm stack land <stack-id> Land the bottom open PR
|
|
99
101
|
mergestorm stack auto-land on|off <stack-id> Toggle auto-land when green
|
|
100
102
|
```
|
|
101
103
|
|
|
102
|
-
`mg` is a short alias for `mergestorm` (same binary), e.g. `mg stack
|
|
104
|
+
`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`).
|
|
105
|
+
|
|
106
|
+
`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 appends `{ branch, parentBranch }` under **`.mergestorm/stack.json`**. No GitHub or API calls.
|
|
107
|
+
|
|
108
|
+
`stack submit` walks that meta: `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), then registers the stack with `POST /api/v1/stacks/adopt` internally (Mergestorm API key). Push/PR auth is your local `git` + `gh auth` — not a Cyclone app token.
|
|
103
109
|
|
|
104
110
|
### `login`
|
|
105
111
|
|
package/dist/api.d.ts
CHANGED
|
@@ -91,6 +91,13 @@ export type StackLayerDto = {
|
|
|
91
91
|
htmlUrl: string | null;
|
|
92
92
|
ciStatus: string;
|
|
93
93
|
reviewStatus: string;
|
|
94
|
+
checks: {
|
|
95
|
+
total: number;
|
|
96
|
+
success: number;
|
|
97
|
+
pending: number;
|
|
98
|
+
failure: number;
|
|
99
|
+
failingName: string | null;
|
|
100
|
+
} | null;
|
|
94
101
|
conflictDetail: string | null;
|
|
95
102
|
lastRestackedSha: string | null;
|
|
96
103
|
};
|
|
@@ -109,7 +116,9 @@ export declare function adoptStack(owner: string, repo: string, prNumber: number
|
|
|
109
116
|
/** Restack descendants (Bearer POST /api/v1/stacks/:id/restack). */
|
|
110
117
|
export declare function restackStack(stackId: string, cfg?: Config): Promise<unknown>;
|
|
111
118
|
/** Land the bottom open layer (Bearer POST /api/v1/stacks/:id/land-next). */
|
|
112
|
-
export declare function landNextStack(stackId: string, cfg?: Config
|
|
119
|
+
export declare function landNextStack(stackId: string, cfg?: Config, opts?: {
|
|
120
|
+
prNumber?: number;
|
|
121
|
+
}): Promise<unknown>;
|
|
113
122
|
/** Toggle auto-land when green (Bearer PATCH /api/v1/stacks/:id). */
|
|
114
123
|
export declare function setStackAutoLand(stackId: string, autoLandWhenGreen: boolean, cfg?: Config): Promise<unknown>;
|
|
115
124
|
/** Recently reviewed branches/threads (Bearer GET /api/v1/threads). */
|
package/dist/api.js
CHANGED
|
@@ -93,7 +93,7 @@ export async function adoptStack(owner, repo, prNumber, cfg) {
|
|
|
93
93
|
throw new CommandError("Stacks API is not available on this server yet. Deploy the API update or use the dashboard.");
|
|
94
94
|
}
|
|
95
95
|
if (status !== 200) {
|
|
96
|
-
throw new CommandError(`Failed to
|
|
96
|
+
throw new CommandError(`Failed to import stack (HTTP ${status}): ${JSON.stringify(body)}`);
|
|
97
97
|
}
|
|
98
98
|
return body;
|
|
99
99
|
}
|
|
@@ -119,8 +119,11 @@ export async function restackStack(stackId, cfg) {
|
|
|
119
119
|
return stackMutation(stackId, "/restack", "POST", {}, "Failed to restack", cfg);
|
|
120
120
|
}
|
|
121
121
|
/** Land the bottom open layer (Bearer POST /api/v1/stacks/:id/land-next). */
|
|
122
|
-
export async function landNextStack(stackId, cfg) {
|
|
123
|
-
|
|
122
|
+
export async function landNextStack(stackId, cfg, opts) {
|
|
123
|
+
const json = opts?.prNumber != null && Number.isInteger(opts.prNumber) && opts.prNumber > 0
|
|
124
|
+
? { prNumber: opts.prNumber }
|
|
125
|
+
: {};
|
|
126
|
+
return stackMutation(stackId, "/land-next", "POST", json, "Failed to land next", cfg);
|
|
124
127
|
}
|
|
125
128
|
/** Toggle auto-land when green (Bearer PATCH /api/v1/stacks/:id). */
|
|
126
129
|
export async function setStackAutoLand(stackId, autoLandWhenGreen, cfg) {
|
package/dist/cli.js
CHANGED
|
@@ -30,10 +30,12 @@ function usage() {
|
|
|
30
30
|
mergestorm chain [slug] Branch review-chain timeline (default: current)
|
|
31
31
|
mergestorm whoami [--json] Key + plan + API base
|
|
32
32
|
mergestorm thread <slug> Jobs in a review thread
|
|
33
|
-
mergestorm stack
|
|
34
|
-
mergestorm stack
|
|
33
|
+
mergestorm stack create [name] New local stack layer (.mergestorm/stack.json)
|
|
34
|
+
mergestorm stack submit Push layers, open chained PRs, register stack
|
|
35
|
+
mergestorm stack list [--json] List registered stacks
|
|
36
|
+
mergestorm stack adopt <owner/repo>#<pr> Import an existing open PR chain
|
|
35
37
|
mergestorm stack restack <stack-id> Restack stack descendants
|
|
36
|
-
mergestorm stack land <stack-id> Land
|
|
38
|
+
mergestorm stack land <stack-id> Land bottom PR (or promote into unit)
|
|
37
39
|
mergestorm stack auto-land on|off <stack-id> Toggle auto-land when green
|
|
38
40
|
|
|
39
41
|
mg is a short alias for mergestorm (same binary).
|
package/dist/commands/stack.d.ts
CHANGED
|
@@ -5,4 +5,11 @@ export declare function parseAdoptTarget(args: string[]): {
|
|
|
5
5
|
repo: string;
|
|
6
6
|
prNumber: number;
|
|
7
7
|
};
|
|
8
|
+
/** Parse `stack create` argv into options. Exported for tests. */
|
|
9
|
+
export declare function parseStackCreateArgs(args: string[]): {
|
|
10
|
+
name?: string;
|
|
11
|
+
onto?: string;
|
|
12
|
+
trunk?: string;
|
|
13
|
+
asJson: boolean;
|
|
14
|
+
};
|
|
8
15
|
export declare function cmdStack(args: string[]): Promise<void>;
|
package/dist/commands/stack.js
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import { adoptStack, landNextStack, listStacks, restackStack, setStackAutoLand, } from "../api.js";
|
|
2
2
|
import { loadConfig } from "../config.js";
|
|
3
3
|
import { CommandError } from "../errors.js";
|
|
4
|
+
import { createPr, findOpenPrNumber, requireGh } from "../gh.js";
|
|
5
|
+
import { branchExists, createBranchFromHead, currentBranch, defaultLayerBranchName, deleteBranch, discoverTrunk, pushBranch, tipCommitMessage, tipCommitSubject, worktreeDirty, } from "../git-stack.js";
|
|
6
|
+
import { buildPrBodyFromCommit } from "../pr-body.js";
|
|
7
|
+
import { appendLayer, loadStackMeta, saveStackMeta, } from "../stack-meta.js";
|
|
4
8
|
import { ansi } from "../ui/ansi.js";
|
|
9
|
+
import { parseGithubOriginRepo } from "./review.js";
|
|
5
10
|
const STACK_USAGE = `usage:
|
|
11
|
+
mergestorm stack create [name] [--onto <branch>] [--trunk <branch>] [--json]
|
|
12
|
+
mergestorm stack submit [--json]
|
|
6
13
|
mergestorm stack list [--json]
|
|
7
14
|
mergestorm stack adopt <owner/repo>#<pr>
|
|
8
15
|
mergestorm stack restack <stack-id> [--json]
|
|
9
16
|
mergestorm stack land <stack-id> [--json]
|
|
10
|
-
mergestorm stack auto-land on|off <stack-id> [--json]
|
|
17
|
+
mergestorm stack auto-land on|off <stack-id> [--json]
|
|
18
|
+
|
|
19
|
+
stack submit opens PRs with a body generated from each layer tip commit.
|
|
20
|
+
stack land promotes into the review unit when one exists; otherwise lands
|
|
21
|
+
the bottom open PR (unit-less stacks).`;
|
|
11
22
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
12
23
|
export function requireStackId(raw, usage) {
|
|
13
24
|
const id = raw?.trim() ?? "";
|
|
@@ -52,6 +63,196 @@ function formatStackHuman(s) {
|
|
|
52
63
|
}
|
|
53
64
|
return lines;
|
|
54
65
|
}
|
|
66
|
+
/** Parse `stack create` argv into options. Exported for tests. */
|
|
67
|
+
export function parseStackCreateArgs(args) {
|
|
68
|
+
let name;
|
|
69
|
+
let onto;
|
|
70
|
+
let trunk;
|
|
71
|
+
let asJson = false;
|
|
72
|
+
for (let i = 0; i < args.length; i++) {
|
|
73
|
+
const a = args[i];
|
|
74
|
+
if (a === "--json") {
|
|
75
|
+
asJson = true;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (a === "--onto") {
|
|
79
|
+
const v = args[++i];
|
|
80
|
+
if (!v)
|
|
81
|
+
throw new CommandError("usage: mergestorm stack create [name] [--onto <branch>]");
|
|
82
|
+
onto = v;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (a.startsWith("--onto=")) {
|
|
86
|
+
onto = a.slice("--onto=".length);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (a === "--trunk") {
|
|
90
|
+
const v = args[++i];
|
|
91
|
+
if (!v)
|
|
92
|
+
throw new CommandError("usage: mergestorm stack create [name] [--trunk <branch>]");
|
|
93
|
+
trunk = v;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (a.startsWith("--trunk=")) {
|
|
97
|
+
trunk = a.slice("--trunk=".length);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (a.startsWith("-")) {
|
|
101
|
+
throw new CommandError(`${STACK_USAGE}\nunknown flag: ${a}`);
|
|
102
|
+
}
|
|
103
|
+
if (name != null) {
|
|
104
|
+
throw new CommandError("usage: mergestorm stack create [name] [--onto <branch>] [--trunk <branch>]");
|
|
105
|
+
}
|
|
106
|
+
name = a;
|
|
107
|
+
}
|
|
108
|
+
return { name, onto, trunk, asJson };
|
|
109
|
+
}
|
|
110
|
+
async function cmdStackCreate(args) {
|
|
111
|
+
const { name, onto, trunk: trunkFlag, asJson } = parseStackCreateArgs(args);
|
|
112
|
+
const cwd = process.cwd();
|
|
113
|
+
if (worktreeDirty(cwd)) {
|
|
114
|
+
throw new CommandError("Working tree is dirty. Commit or stash changes before `stack create`.");
|
|
115
|
+
}
|
|
116
|
+
const parentBranch = onto?.trim() || currentBranch(cwd);
|
|
117
|
+
if (!parentBranch) {
|
|
118
|
+
throw new CommandError("Detached HEAD. Check out a branch (or pass --onto <branch>) before `stack create`.");
|
|
119
|
+
}
|
|
120
|
+
if (!branchExists(parentBranch, cwd)) {
|
|
121
|
+
throw new CommandError(`Parent branch not found: ${parentBranch}`);
|
|
122
|
+
}
|
|
123
|
+
const branch = defaultLayerBranchName(name);
|
|
124
|
+
if (branchExists(branch, cwd)) {
|
|
125
|
+
throw new CommandError(`Branch already exists: ${branch}`);
|
|
126
|
+
}
|
|
127
|
+
let meta = await loadStackMeta(cwd);
|
|
128
|
+
const trunk = trunkFlag?.trim() || meta?.trunk || discoverTrunk(cwd);
|
|
129
|
+
if (!meta) {
|
|
130
|
+
meta = { version: 1, trunk, layers: [] };
|
|
131
|
+
}
|
|
132
|
+
else if (trunkFlag?.trim() && meta.trunk !== trunk) {
|
|
133
|
+
throw new CommandError(`Stack trunk is ${meta.trunk}; refusing --trunk ${trunk}. Edit .mergestorm/stack.json or start fresh.`);
|
|
134
|
+
}
|
|
135
|
+
// Parent must be trunk or an existing layer (linear local stack).
|
|
136
|
+
if (meta.layers.length > 0 &&
|
|
137
|
+
parentBranch !== meta.trunk &&
|
|
138
|
+
!meta.layers.some((l) => l.branch === parentBranch)) {
|
|
139
|
+
throw new CommandError(`Parent ${parentBranch} is not trunk (${meta.trunk}) or a stack layer. Use --onto <branch>.`);
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
createBranchFromHead(branch, cwd);
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
throw new CommandError(err instanceof Error ? err.message : String(err));
|
|
146
|
+
}
|
|
147
|
+
let next;
|
|
148
|
+
try {
|
|
149
|
+
next = appendLayer(meta, { branch, parentBranch });
|
|
150
|
+
await saveStackMeta(next, cwd);
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
try {
|
|
154
|
+
deleteBranch(branch, cwd);
|
|
155
|
+
}
|
|
156
|
+
catch { /* rollback best-effort */ }
|
|
157
|
+
throw new CommandError(err instanceof Error ? err.message : String(err));
|
|
158
|
+
}
|
|
159
|
+
if (asJson) {
|
|
160
|
+
console.log(JSON.stringify({ branch, parentBranch, trunk: next.trunk, meta: next }, null, 2));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
console.log(ansi.green(` Created ${branch} (onto ${parentBranch})`));
|
|
164
|
+
console.log(ansi.dim(` trunk: ${next.trunk} · layers: ${next.layers.length}`));
|
|
165
|
+
console.log(ansi.dim(` meta: .mergestorm/stack.json`));
|
|
166
|
+
}
|
|
167
|
+
async function cmdStackSubmit(args) {
|
|
168
|
+
const asJson = args.includes("--json");
|
|
169
|
+
if (args.some((a) => a !== "--json")) {
|
|
170
|
+
throw new CommandError("usage: mergestorm stack submit [--json]");
|
|
171
|
+
}
|
|
172
|
+
const cwd = process.cwd();
|
|
173
|
+
const meta = await loadStackMeta(cwd);
|
|
174
|
+
if (!meta || meta.layers.length === 0) {
|
|
175
|
+
throw new CommandError("No local stack layers. Run `mg stack create` (and commit) before `stack submit`.");
|
|
176
|
+
}
|
|
177
|
+
const origin = parseGithubOriginRepo();
|
|
178
|
+
if (!origin) {
|
|
179
|
+
throw new CommandError("Could not parse GitHub owner/repo from `origin`. Set a github.com remote and retry.");
|
|
180
|
+
}
|
|
181
|
+
const { owner, repo } = origin;
|
|
182
|
+
requireGh(cwd);
|
|
183
|
+
const opened = [];
|
|
184
|
+
for (const layer of meta.layers) {
|
|
185
|
+
const base = layer.parentBranch || meta.trunk;
|
|
186
|
+
if (!branchExists(layer.branch, cwd)) {
|
|
187
|
+
throw new CommandError(`Stack layer branch missing locally: ${layer.branch}`);
|
|
188
|
+
}
|
|
189
|
+
console.log(ansi.dim(` Pushing ${layer.branch} …`));
|
|
190
|
+
try {
|
|
191
|
+
pushBranch(layer.branch, cwd);
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
throw new CommandError(err instanceof Error ? err.message : String(err));
|
|
195
|
+
}
|
|
196
|
+
let prNumber = findOpenPrNumber(owner, repo, layer.branch, cwd);
|
|
197
|
+
let created = false;
|
|
198
|
+
if (prNumber == null) {
|
|
199
|
+
const title = tipCommitSubject(layer.branch, cwd);
|
|
200
|
+
const body = buildPrBodyFromCommit(tipCommitMessage(layer.branch, cwd));
|
|
201
|
+
console.log(ansi.dim(` Opening PR ${layer.branch} → ${base} …`));
|
|
202
|
+
prNumber = createPr({
|
|
203
|
+
owner,
|
|
204
|
+
repo,
|
|
205
|
+
base,
|
|
206
|
+
head: layer.branch,
|
|
207
|
+
title,
|
|
208
|
+
body,
|
|
209
|
+
cwd,
|
|
210
|
+
});
|
|
211
|
+
created = true;
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
console.log(ansi.dim(` PR #${prNumber} already open for ${layer.branch}`));
|
|
215
|
+
}
|
|
216
|
+
opened.push({ branch: layer.branch, base, prNumber, created });
|
|
217
|
+
}
|
|
218
|
+
const registerPr = opened[0].prNumber;
|
|
219
|
+
const cfg = await loadConfig();
|
|
220
|
+
console.log(ansi.dim(` Registering stack via import (${owner}/${repo}#${registerPr}) …`));
|
|
221
|
+
const body = await adoptStack(owner, repo, registerPr, cfg);
|
|
222
|
+
if (typeof body !== "object" || body === null) {
|
|
223
|
+
throw new CommandError("Unexpected response shape from adopt API");
|
|
224
|
+
}
|
|
225
|
+
const data = body;
|
|
226
|
+
if (data.error) {
|
|
227
|
+
throw new CommandError(data.error);
|
|
228
|
+
}
|
|
229
|
+
if (!data.stack) {
|
|
230
|
+
throw new CommandError("Stack was not registered");
|
|
231
|
+
}
|
|
232
|
+
const stackId = data.stack.id;
|
|
233
|
+
if (asJson) {
|
|
234
|
+
console.log(JSON.stringify({
|
|
235
|
+
owner,
|
|
236
|
+
repo,
|
|
237
|
+
layers: opened,
|
|
238
|
+
stackId: stackId ?? null,
|
|
239
|
+
trunk: data.stack?.trunkBranch ?? meta.trunk,
|
|
240
|
+
chain: data.chain ?? null,
|
|
241
|
+
}, null, 2));
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
for (const row of opened) {
|
|
245
|
+
const verb = row.created ? "opened" : "exists";
|
|
246
|
+
console.log(ansi.green(` #${row.prNumber} ${row.branch} → ${row.base} (${verb})`));
|
|
247
|
+
}
|
|
248
|
+
if (stackId) {
|
|
249
|
+
console.log(ansi.green(` Registered stack ${stackId}`));
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
console.log(ansi.dim(" Stack registration returned no stack ID"));
|
|
253
|
+
}
|
|
254
|
+
console.log(ansi.dim(" Dashboard: https://mergestorm.ai/dashboard/stacks"));
|
|
255
|
+
}
|
|
55
256
|
async function cmdStackList(args) {
|
|
56
257
|
const asJson = args.includes("--json");
|
|
57
258
|
const cfg = await loadConfig();
|
|
@@ -61,7 +262,8 @@ async function cmdStackList(args) {
|
|
|
61
262
|
return;
|
|
62
263
|
}
|
|
63
264
|
if (stacks.length === 0) {
|
|
64
|
-
console.log(ansi.dim(" No stacks yet.
|
|
265
|
+
console.log(ansi.dim(" No stacks yet. Author with `stack create` → commit → `stack submit`."));
|
|
266
|
+
console.log(ansi.dim(" Import an existing chain only: `stack adopt owner/repo#pr`"));
|
|
65
267
|
console.log(ansi.dim(" Dashboard: https://mergestorm.ai/dashboard/stacks"));
|
|
66
268
|
return;
|
|
67
269
|
}
|
|
@@ -91,7 +293,7 @@ async function cmdStackAdopt(args) {
|
|
|
91
293
|
const data = body;
|
|
92
294
|
const stack = data.stack;
|
|
93
295
|
const chain = data.chain;
|
|
94
|
-
console.log(ansi.green(`
|
|
296
|
+
console.log(ansi.green(` Imported ${owner}/${repo}#${prNumber}` +
|
|
95
297
|
(stack?.id ? ` → ${stack.id}` : "") +
|
|
96
298
|
(Array.isArray(chain) ? ` (${chain.length} layer(s))` : "")));
|
|
97
299
|
if (stack?.trunkBranch) {
|
|
@@ -115,6 +317,7 @@ async function cmdStackLand(args) {
|
|
|
115
317
|
const asJson = args.includes("--json");
|
|
116
318
|
const id = requireStackId(args.find((a) => a !== "--json"), "usage: mergestorm stack land <stack-id>");
|
|
117
319
|
const cfg = await loadConfig();
|
|
320
|
+
// Server routes unit stacks to promote-next (#244 / AUD-07).
|
|
118
321
|
const body = await landNextStack(id, cfg);
|
|
119
322
|
if (asJson) {
|
|
120
323
|
console.log(JSON.stringify(body, null, 2));
|
|
@@ -122,6 +325,15 @@ async function cmdStackLand(args) {
|
|
|
122
325
|
}
|
|
123
326
|
const pr = body.mergedPrNumber;
|
|
124
327
|
const branch = body.mergedBranch;
|
|
328
|
+
const uNumber = body.uNumber;
|
|
329
|
+
if (typeof uNumber === "number" && uNumber > 0) {
|
|
330
|
+
console.log(ansi.green(` Promoted` +
|
|
331
|
+
(pr != null ? ` #${pr}` : "") +
|
|
332
|
+
` into U${uNumber}` +
|
|
333
|
+
(branch ? ` (${branch})` : "") +
|
|
334
|
+
` on ${id}`));
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
125
337
|
console.log(ansi.green(` Landed` +
|
|
126
338
|
(pr != null ? ` #${pr}` : "") +
|
|
127
339
|
(branch ? ` (${branch})` : "") +
|
|
@@ -149,6 +361,10 @@ export async function cmdStack(args) {
|
|
|
149
361
|
if (!sub || sub === "-h" || sub === "--help") {
|
|
150
362
|
throw new CommandError(STACK_USAGE);
|
|
151
363
|
}
|
|
364
|
+
if (sub === "create")
|
|
365
|
+
return cmdStackCreate(rest);
|
|
366
|
+
if (sub === "submit")
|
|
367
|
+
return cmdStackSubmit(rest);
|
|
152
368
|
if (sub === "list" || sub === "ls")
|
|
153
369
|
return cmdStackList(rest);
|
|
154
370
|
if (sub === "adopt")
|
package/dist/gh.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare function runGh(args: string[], cwd?: string): {
|
|
2
|
+
ok: true;
|
|
3
|
+
stdout: string;
|
|
4
|
+
} | {
|
|
5
|
+
ok: false;
|
|
6
|
+
stderr: string;
|
|
7
|
+
status: number | null;
|
|
8
|
+
};
|
|
9
|
+
export declare function requireGh(cwd?: string): void;
|
|
10
|
+
/** Open PR number for `head` branch in owner/repo, or null. */
|
|
11
|
+
export declare function findOpenPrNumber(owner: string, repo: string, headBranch: string, cwd?: string): number | null;
|
|
12
|
+
export declare function createPr(input: {
|
|
13
|
+
owner: string;
|
|
14
|
+
repo: string;
|
|
15
|
+
base: string;
|
|
16
|
+
head: string;
|
|
17
|
+
title: string;
|
|
18
|
+
body?: string;
|
|
19
|
+
cwd?: string;
|
|
20
|
+
}): number;
|
package/dist/gh.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { CommandError } from "./errors.js";
|
|
3
|
+
export function runGh(args, cwd = process.cwd()) {
|
|
4
|
+
const result = spawnSync("gh", args, { cwd, encoding: "utf8" });
|
|
5
|
+
if (result.error) {
|
|
6
|
+
const msg = result.error.message || String(result.error);
|
|
7
|
+
if (result.error.code === "ENOENT") {
|
|
8
|
+
return { ok: false, stderr: "gh not found", status: null };
|
|
9
|
+
}
|
|
10
|
+
return { ok: false, stderr: msg, status: result.status };
|
|
11
|
+
}
|
|
12
|
+
if (result.status !== 0) {
|
|
13
|
+
return {
|
|
14
|
+
ok: false,
|
|
15
|
+
stderr: (result.stderr || result.stdout || `gh ${args.join(" ")} failed`).trim(),
|
|
16
|
+
status: result.status,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return { ok: true, stdout: result.stdout ?? "" };
|
|
20
|
+
}
|
|
21
|
+
export function requireGh(cwd = process.cwd()) {
|
|
22
|
+
const check = runGh(["--version"], cwd);
|
|
23
|
+
if (!check.ok) {
|
|
24
|
+
throw new CommandError("GitHub CLI (`gh`) is required for `stack submit`. Install https://cli.github.com and run `gh auth login`.");
|
|
25
|
+
}
|
|
26
|
+
const auth = runGh(["auth", "status"], cwd);
|
|
27
|
+
if (!auth.ok) {
|
|
28
|
+
throw new CommandError("`gh` is installed but not authenticated. Run `gh auth login`, then retry `stack submit`.");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Open PR number for `head` branch in owner/repo, or null. */
|
|
32
|
+
export function findOpenPrNumber(owner, repo, headBranch, cwd = process.cwd()) {
|
|
33
|
+
const res = runGh([
|
|
34
|
+
"pr",
|
|
35
|
+
"list",
|
|
36
|
+
"--repo",
|
|
37
|
+
`${owner}/${repo}`,
|
|
38
|
+
"--head",
|
|
39
|
+
headBranch,
|
|
40
|
+
"--state",
|
|
41
|
+
"open",
|
|
42
|
+
"--json",
|
|
43
|
+
"number",
|
|
44
|
+
"--limit",
|
|
45
|
+
"1",
|
|
46
|
+
], cwd);
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
throw new CommandError(`Failed to list PRs for ${headBranch}: ${res.stderr}`);
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const rows = JSON.parse(res.stdout);
|
|
52
|
+
const n = rows[0]?.number;
|
|
53
|
+
return typeof n === "number" && n > 0 ? n : null;
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
throw new CommandError(`Failed to parse PR list response: ${e instanceof Error ? e.message : String(e)}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function createPr(input) {
|
|
60
|
+
const cwd = input.cwd ?? process.cwd();
|
|
61
|
+
const args = [
|
|
62
|
+
"pr",
|
|
63
|
+
"create",
|
|
64
|
+
"--repo",
|
|
65
|
+
`${input.owner}/${input.repo}`,
|
|
66
|
+
"--base",
|
|
67
|
+
input.base,
|
|
68
|
+
"--head",
|
|
69
|
+
input.head,
|
|
70
|
+
"--title",
|
|
71
|
+
input.title,
|
|
72
|
+
"--body",
|
|
73
|
+
input.body ?? "",
|
|
74
|
+
];
|
|
75
|
+
const res = runGh(args, cwd);
|
|
76
|
+
if (!res.ok) {
|
|
77
|
+
throw new CommandError(`Failed to open PR ${input.head} → ${input.base}: ${res.stderr}`);
|
|
78
|
+
}
|
|
79
|
+
// stdout is typically the PR URL
|
|
80
|
+
const url = res.stdout.trim();
|
|
81
|
+
const m = /\/pull\/(\d+)/.exec(url);
|
|
82
|
+
if (m?.[1])
|
|
83
|
+
return Number(m[1]);
|
|
84
|
+
// Fallback: re-list
|
|
85
|
+
const n = findOpenPrNumber(input.owner, input.repo, input.head, cwd);
|
|
86
|
+
if (n != null)
|
|
87
|
+
return n;
|
|
88
|
+
throw new CommandError(`Opened PR at ${url || "(unknown URL)"} but could not determine PR number. Check your repository's pull requests.`);
|
|
89
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Current branch name, or null when detached. */
|
|
2
|
+
export declare function currentBranch(cwd?: string): string | null;
|
|
3
|
+
/**
|
|
4
|
+
* True when the worktree has staged/unstaged/untracked changes that should
|
|
5
|
+
* block `stack create`. Ignores `.mergestorm/stack.json` so local stack meta
|
|
6
|
+
* written by create does not prevent the next layer.
|
|
7
|
+
*/
|
|
8
|
+
export declare function worktreeDirty(cwd?: string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Resolve trunk branch: --trunk flag, then origin/HEAD, then main/master if present.
|
|
11
|
+
*/
|
|
12
|
+
export declare function discoverTrunk(cwd?: string): string;
|
|
13
|
+
export declare function branchExists(name: string, cwd?: string): boolean;
|
|
14
|
+
export declare function deleteBranch(name: string, cwd?: string): void;
|
|
15
|
+
export declare function createBranchFromHead(name: string, cwd?: string): void;
|
|
16
|
+
/** Slugify a free-form name into a safe branch segment. */
|
|
17
|
+
export declare function slugifyBranchName(raw: string): string;
|
|
18
|
+
export declare function defaultLayerBranchName(explicit?: string): string;
|
|
19
|
+
/** Push branch to origin and set upstream. */
|
|
20
|
+
export declare function pushBranch(branch: string, cwd?: string): void;
|
|
21
|
+
/** First line of the tip commit on `branch` (for PR title). */
|
|
22
|
+
export declare function tipCommitSubject(branch: string, cwd?: string): string;
|
|
23
|
+
/** Full tip commit message on `branch` (subject + body; for PR description). */
|
|
24
|
+
export declare function tipCommitMessage(branch: string, cwd?: string): string;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { git } from "./git.js";
|
|
2
|
+
/** Current branch name, or null when detached. */
|
|
3
|
+
export function currentBranch(cwd = process.cwd()) {
|
|
4
|
+
try {
|
|
5
|
+
const name = git(["rev-parse", "--abbrev-ref", "HEAD"], cwd).trim();
|
|
6
|
+
if (!name || name === "HEAD")
|
|
7
|
+
return null;
|
|
8
|
+
return name;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* True when the worktree has staged/unstaged/untracked changes that should
|
|
16
|
+
* block `stack create`. Ignores `.mergestorm/stack.json` so local stack meta
|
|
17
|
+
* written by create does not prevent the next layer.
|
|
18
|
+
*/
|
|
19
|
+
export function worktreeDirty(cwd = process.cwd()) {
|
|
20
|
+
const out = git(["status", "--porcelain"], cwd);
|
|
21
|
+
for (const line of out.split("\n")) {
|
|
22
|
+
const trimmed = line.trimEnd();
|
|
23
|
+
if (!trimmed)
|
|
24
|
+
continue;
|
|
25
|
+
// porcelain: XY PATH or XY ORIG -> PATH (renames only)
|
|
26
|
+
const status = trimmed.slice(0, 2);
|
|
27
|
+
const pathPart = (status.includes("R") || status.includes("C"))
|
|
28
|
+
? trimmed.slice(3).split(" -> ").pop()?.trim() ?? ""
|
|
29
|
+
: trimmed.slice(3).trim();
|
|
30
|
+
if (pathPart === ".mergestorm/stack.json" || pathPart.endsWith("/.mergestorm/stack.json")) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolve trunk branch: --trunk flag, then origin/HEAD, then main/master if present.
|
|
39
|
+
*/
|
|
40
|
+
export function discoverTrunk(cwd = process.cwd()) {
|
|
41
|
+
try {
|
|
42
|
+
const sym = git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], cwd).trim();
|
|
43
|
+
// origin/main → main
|
|
44
|
+
const m = /^origin\/(.+)$/.exec(sym);
|
|
45
|
+
if (m?.[1])
|
|
46
|
+
return m[1];
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// no origin/HEAD
|
|
50
|
+
}
|
|
51
|
+
for (const candidate of ["main", "master"]) {
|
|
52
|
+
try {
|
|
53
|
+
git(["rev-parse", "--verify", candidate], cwd);
|
|
54
|
+
return candidate;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// try next
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
throw new Error("Could not discover trunk branch. Pass --trunk <name> or set origin/HEAD.");
|
|
61
|
+
}
|
|
62
|
+
export function branchExists(name, cwd = process.cwd()) {
|
|
63
|
+
try {
|
|
64
|
+
git(["rev-parse", "--verify", "--quiet", `refs/heads/${name}`], cwd);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function deleteBranch(name, cwd = process.cwd()) {
|
|
72
|
+
git(["branch", "-D", name], cwd);
|
|
73
|
+
}
|
|
74
|
+
export function createBranchFromHead(name, cwd = process.cwd()) {
|
|
75
|
+
git(["checkout", "-b", name], cwd);
|
|
76
|
+
}
|
|
77
|
+
/** Slugify a free-form name into a safe branch segment. */
|
|
78
|
+
export function slugifyBranchName(raw) {
|
|
79
|
+
const s = raw
|
|
80
|
+
.trim()
|
|
81
|
+
.toLowerCase()
|
|
82
|
+
.replace(/[^a-z0-9/_-]+/g, "-")
|
|
83
|
+
.replace(/\/+/g, "/")
|
|
84
|
+
.replace(/^-+|-+$/g, "");
|
|
85
|
+
return s || `layer-${Date.now().toString(36)}`;
|
|
86
|
+
}
|
|
87
|
+
export function defaultLayerBranchName(explicit) {
|
|
88
|
+
if (explicit?.trim()) {
|
|
89
|
+
const s = slugifyBranchName(explicit);
|
|
90
|
+
return s.includes("/") ? s : `ms/${s}`;
|
|
91
|
+
}
|
|
92
|
+
return `ms/${new Date().toISOString().slice(0, 10)}-${Date.now().toString(36).slice(-4)}`;
|
|
93
|
+
}
|
|
94
|
+
/** Push branch to origin and set upstream. */
|
|
95
|
+
export function pushBranch(branch, cwd = process.cwd()) {
|
|
96
|
+
try {
|
|
97
|
+
git(["push", "-u", "origin", branch], cwd);
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
101
|
+
throw new Error(`git push failed for ${branch}. Check remote auth (SSH key or credential helper).\n${msg}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** First line of the tip commit on `branch` (for PR title). */
|
|
105
|
+
export function tipCommitSubject(branch, cwd = process.cwd()) {
|
|
106
|
+
try {
|
|
107
|
+
const s = git(["log", "-1", "--format=%s", branch], cwd).trim();
|
|
108
|
+
if (s)
|
|
109
|
+
return s;
|
|
110
|
+
console.warn(`Warning: no commit message on ${branch}; using branch name as PR title`);
|
|
111
|
+
return branch;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
console.warn(`Warning: could not read commit message on ${branch}; using branch name as PR title`);
|
|
115
|
+
return branch;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Full tip commit message on `branch` (subject + body; for PR description). */
|
|
119
|
+
export function tipCommitMessage(branch, cwd = process.cwd()) {
|
|
120
|
+
try {
|
|
121
|
+
const s = git(["log", "-1", "--format=%B", branch], cwd);
|
|
122
|
+
if (s.trim())
|
|
123
|
+
return s;
|
|
124
|
+
return tipCommitSubject(branch, cwd);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return tipCommitSubject(branch, cwd);
|
|
128
|
+
}
|
|
129
|
+
}
|
package/dist/pr-body.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a GitHub PR body from a tip commit message (#217).
|
|
3
|
+
* Pure — no git / network.
|
|
4
|
+
*/
|
|
5
|
+
const TRAILER_RE = /^(fixes|closes|resolves)\s+#(\d+)\s*$/i;
|
|
6
|
+
/**
|
|
7
|
+
* Turn `git log -1 --format=%B` output into a non-empty PR description.
|
|
8
|
+
*/
|
|
9
|
+
export function buildPrBodyFromCommit(fullMessage) {
|
|
10
|
+
const lines = fullMessage.replace(/\r\n/g, "\n").split("\n");
|
|
11
|
+
const subject = (lines[0] ?? "").trim() || "Update";
|
|
12
|
+
const trailers = [];
|
|
13
|
+
const bodyLines = [];
|
|
14
|
+
for (const raw of lines.slice(1)) {
|
|
15
|
+
const line = raw.trim();
|
|
16
|
+
if (!line)
|
|
17
|
+
continue;
|
|
18
|
+
const m = TRAILER_RE.exec(line);
|
|
19
|
+
if (m) {
|
|
20
|
+
const verb = m[1][0].toUpperCase() + m[1].slice(1).toLowerCase();
|
|
21
|
+
trailers.push(`${verb} #${m[2]}`);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
bodyLines.push(line);
|
|
25
|
+
}
|
|
26
|
+
// Also pick trailers that appear as the whole subject (unusual but cheap).
|
|
27
|
+
const subjectTrailer = TRAILER_RE.exec(subject);
|
|
28
|
+
const summaryBullets = [];
|
|
29
|
+
if (subjectTrailer) {
|
|
30
|
+
const verb = subjectTrailer[1][0].toUpperCase() + subjectTrailer[1].slice(1).toLowerCase();
|
|
31
|
+
trailers.unshift(`${verb} #${subjectTrailer[2]}`);
|
|
32
|
+
summaryBullets.push(`- ${subject}`);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
summaryBullets.push(`- ${subject}`);
|
|
36
|
+
}
|
|
37
|
+
for (const para of bodyLines) {
|
|
38
|
+
summaryBullets.push(`- ${para}`);
|
|
39
|
+
}
|
|
40
|
+
const parts = [
|
|
41
|
+
"## Summary",
|
|
42
|
+
...summaryBullets,
|
|
43
|
+
"",
|
|
44
|
+
"## Test plan",
|
|
45
|
+
"- [ ] CI green",
|
|
46
|
+
];
|
|
47
|
+
if (trailers.length > 0) {
|
|
48
|
+
parts.push("", ...uniqueTrailers(trailers));
|
|
49
|
+
}
|
|
50
|
+
return parts.join("\n") + "\n";
|
|
51
|
+
}
|
|
52
|
+
function uniqueTrailers(trailers) {
|
|
53
|
+
const seen = new Set();
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const t of trailers) {
|
|
56
|
+
const key = t.toLowerCase();
|
|
57
|
+
if (seen.has(key))
|
|
58
|
+
continue;
|
|
59
|
+
seen.add(key);
|
|
60
|
+
out.push(t);
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
package/dist/shell.js
CHANGED
|
@@ -28,7 +28,7 @@ export const COMMANDS = [
|
|
|
28
28
|
{ name: "chain", summary: "Show a branch review-chain timeline" },
|
|
29
29
|
{ name: "whoami", summary: "Key + plan + API base" },
|
|
30
30
|
{ name: "thread", summary: "Jobs in a review thread" },
|
|
31
|
-
{ name: "stack", summary: "Stacks: list, adopt, restack, land
|
|
31
|
+
{ name: "stack", summary: "Stacks: create, submit, list, adopt, restack, land" },
|
|
32
32
|
{ name: "clear", summary: "Clear screen and reprint banner" },
|
|
33
33
|
{ name: "exit", summary: "Leave the shell" },
|
|
34
34
|
{ name: "quit", summary: "Leave the shell" },
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type StackLayerMeta = {
|
|
2
|
+
branch: string;
|
|
3
|
+
/** Parent branch name (trunk for the bottom layer). */
|
|
4
|
+
parentBranch: string;
|
|
5
|
+
};
|
|
6
|
+
export type StackMeta = {
|
|
7
|
+
version: 1;
|
|
8
|
+
trunk: string;
|
|
9
|
+
layers: StackLayerMeta[];
|
|
10
|
+
};
|
|
11
|
+
export declare const STACK_META_REL: string;
|
|
12
|
+
export declare function stackMetaPath(cwd?: string): string;
|
|
13
|
+
export declare function loadStackMeta(cwd?: string): Promise<StackMeta | null>;
|
|
14
|
+
export declare function saveStackMeta(meta: StackMeta, cwd?: string): Promise<void>;
|
|
15
|
+
export declare function appendLayer(meta: StackMeta, layer: StackLayerMeta): StackMeta;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local stack metadata for `mg stack create` / `submit`.
|
|
3
|
+
* Stored at `.mergestorm/stack.json` in the repo root.
|
|
4
|
+
*/
|
|
5
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
export const STACK_META_REL = path.join(".mergestorm", "stack.json");
|
|
8
|
+
export function stackMetaPath(cwd = process.cwd()) {
|
|
9
|
+
return path.join(cwd, STACK_META_REL);
|
|
10
|
+
}
|
|
11
|
+
export async function loadStackMeta(cwd = process.cwd()) {
|
|
12
|
+
try {
|
|
13
|
+
const raw = await readFile(stackMetaPath(cwd), "utf8");
|
|
14
|
+
const parsed = JSON.parse(raw);
|
|
15
|
+
if (!parsed ||
|
|
16
|
+
parsed.version !== 1 ||
|
|
17
|
+
typeof parsed.trunk !== "string" ||
|
|
18
|
+
!Array.isArray(parsed.layers) ||
|
|
19
|
+
!parsed.layers.every(l => typeof l.branch === 'string' && typeof l.parentBranch === 'string')) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return parsed;
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
const code = err?.code;
|
|
26
|
+
if (code === "ENOENT")
|
|
27
|
+
return null;
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function saveStackMeta(meta, cwd = process.cwd()) {
|
|
32
|
+
const file = stackMetaPath(cwd);
|
|
33
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
34
|
+
await writeFile(file, `${JSON.stringify(meta, null, 2)}\n`, "utf8");
|
|
35
|
+
}
|
|
36
|
+
export function appendLayer(meta, layer) {
|
|
37
|
+
if (meta.layers.some((l) => l.branch === layer.branch)) {
|
|
38
|
+
throw new Error(`branch already in stack: ${layer.branch}`);
|
|
39
|
+
}
|
|
40
|
+
return { ...meta, layers: [...meta.layers, layer] };
|
|
41
|
+
}
|
package/dist/ui/banner.js
CHANGED
|
@@ -38,6 +38,14 @@ const WHATS_NEW = {
|
|
|
38
38
|
"0.3.3": [
|
|
39
39
|
"`stack list` / `adopt` / `restack` / `land` / `auto-land`",
|
|
40
40
|
],
|
|
41
|
+
"0.3.4": [
|
|
42
|
+
"`stack create` / `submit` — author stacks with `mg` (not Graphite)",
|
|
43
|
+
"Adopt is import-only; happy path is create → submit → restack → land",
|
|
44
|
+
],
|
|
45
|
+
"0.3.5": [
|
|
46
|
+
"`stack submit` fills PR body from the tip commit (keeps Fixes/Closes)",
|
|
47
|
+
"Unit-stack `stack land` promotes (matches server gates)",
|
|
48
|
+
],
|
|
41
49
|
};
|
|
42
50
|
function formatStatusLines(me, key, barWidth) {
|
|
43
51
|
if (!key) {
|
|
@@ -61,7 +69,7 @@ function tipsLines() {
|
|
|
61
69
|
return [
|
|
62
70
|
ansi.dim("Tips for getting started"),
|
|
63
71
|
` ${ansi.green("review")} review main...HEAD`,
|
|
64
|
-
` ${ansi.green("
|
|
72
|
+
` ${ansi.green("stack")} create → submit → restack → land`,
|
|
65
73
|
` ${ansi.green("/help")} list all commands`,
|
|
66
74
|
];
|
|
67
75
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mergestorm",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "MergeStorm CLI — interactive shell and one-shot review commands",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"dev": "tsx src/cli.ts",
|
|
25
25
|
"start": "node dist/cli.js",
|
|
26
26
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
27
|
-
"test": "tsx --test src/**/*.test.ts"
|
|
27
|
+
"test": "tsx --test src/*.test.ts src/**/*.test.ts"
|
|
28
28
|
},
|
|
29
29
|
"engines": {
|
|
30
30
|
"node": ">=22.0.0"
|