mergestorm 0.3.9 → 0.3.15
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 +67 -24
- package/dist/api.d.ts +106 -40
- package/dist/api.js +245 -9
- package/dist/client/index.d.ts +5 -0
- package/dist/client/index.js +5 -0
- package/dist/commands/branches.d.ts +6 -2
- package/dist/commands/branches.js +29 -14
- package/dist/commands/browse.d.ts +39 -0
- package/dist/commands/browse.js +259 -0
- package/dist/commands/credits.js +30 -12
- package/dist/commands/help.d.ts +5 -0
- package/dist/commands/help.js +93 -0
- package/dist/commands/jobs.d.ts +11 -2
- package/dist/commands/jobs.js +64 -15
- package/dist/commands/login.js +32 -10
- package/dist/commands/logout.js +2 -1
- package/dist/commands/pr.d.ts +22 -0
- package/dist/commands/pr.js +127 -0
- package/dist/commands/queue.d.ts +12 -0
- package/dist/commands/queue.js +160 -0
- package/dist/commands/registry.d.ts +1 -1
- package/dist/commands/registry.js +115 -33
- package/dist/commands/review-client.d.ts +106 -0
- package/dist/commands/review-client.js +399 -0
- package/dist/commands/review.d.ts +36 -6
- package/dist/commands/review.js +395 -145
- package/dist/commands/settings.d.ts +17 -0
- package/dist/commands/settings.js +89 -0
- package/dist/commands/skill.d.ts +20 -0
- package/dist/commands/skill.js +145 -0
- package/dist/commands/stack.d.ts +36 -2
- package/dist/commands/stack.js +341 -113
- package/dist/commands/status.d.ts +18 -2
- package/dist/commands/status.js +236 -21
- package/dist/commands/whoami.d.ts +3 -1
- package/dist/commands/whoami.js +21 -7
- 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 +39 -28
- package/dist/stack-dto.d.ts +266 -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/ansi.d.ts +2 -0
- package/dist/ui/ansi.js +2 -0
- package/dist/ui/banner.d.ts +34 -2
- package/dist/ui/banner.js +170 -79
- package/dist/ui/box.d.ts +13 -9
- package/dist/ui/box.js +28 -44
- package/dist/ui/findings.d.ts +4 -1
- package/dist/ui/findings.js +14 -7
- package/dist/ui/job-envelope.d.ts +68 -0
- package/dist/ui/job-envelope.js +35 -0
- package/dist/ui/line-tabs.d.ts +27 -0
- package/dist/ui/line-tabs.js +170 -0
- package/dist/ui/logo.d.ts +10 -2
- package/dist/ui/logo.js +13 -9
- package/dist/ui/present.d.ts +10 -0
- package/dist/ui/present.js +13 -0
- package/dist/ui/prompt.d.ts +148 -0
- package/dist/ui/prompt.js +433 -71
- package/dist/ui/select.js +1 -1
- package/dist/ui/tabs.d.ts +125 -0
- package/dist/ui/tabs.js +480 -0
- package/dist/ui/usage.d.ts +29 -1
- package/dist/ui/usage.js +71 -2
- package/dist/ui/width.d.ts +26 -0
- package/dist/ui/width.js +262 -0
- package/package.json +6 -1
- package/skill/SKILL.md +98 -0
- package/skill/mergestorm-pr-loop/SKILL.md +76 -0
package/dist/ui/banner.js
CHANGED
|
@@ -3,9 +3,9 @@ import { CommandError } from "../errors.js";
|
|
|
3
3
|
import { keyDisplay, loadConfig, resolveApiKey } from "../config.js";
|
|
4
4
|
import { cliVersion } from "../version.js";
|
|
5
5
|
import { ansi } from "./ansi.js";
|
|
6
|
-
import { frameWidth, padVisible, preferBoxedUi, roundedBox,
|
|
7
|
-
import {
|
|
8
|
-
import { usageBar } from "./usage.js";
|
|
6
|
+
import { frameWidth, padVisible, preferBoxedUi, roundedBox, terminalColumns, visibleWidth, } from "./box.js";
|
|
7
|
+
import { STORM_MARK, STORM_MARK_WIDTH } from "./logo.js";
|
|
8
|
+
import { formatResetLabel, usageBar } from "./usage.js";
|
|
9
9
|
const WORDMARK = `
|
|
10
10
|
__ __ ___ ___ ___ ___ ___ _____ ___ ___ __ __
|
|
11
11
|
| \\/ | __| _ \\/ __| __/ __|_ _/ _ \\| _ \\ \\/ |
|
|
@@ -14,7 +14,7 @@ const WORDMARK = `
|
|
|
14
14
|
`.trimEnd();
|
|
15
15
|
/** Used by the non-interactive `usage()` path in cli.ts — the boxed banner is shell-only. */
|
|
16
16
|
export function printWordmark() {
|
|
17
|
-
console.log(ansi.
|
|
17
|
+
console.log(ansi.boldBrightGreen(WORDMARK));
|
|
18
18
|
console.log(ansi.dim(` review local diffs fast · mergestorm.ai · v${cliVersion()}`));
|
|
19
19
|
console.log("");
|
|
20
20
|
}
|
|
@@ -66,53 +66,173 @@ const WHATS_NEW = {
|
|
|
66
66
|
"npm description points at MIT build-from-source URL",
|
|
67
67
|
"Dashboard links → Settings → API / Work (product URL cleanup)",
|
|
68
68
|
],
|
|
69
|
+
"0.3.10": [
|
|
70
|
+
"README: verify public git SHA + npm integrity (build from mergestorm-cli)",
|
|
71
|
+
],
|
|
72
|
+
"0.3.12": [
|
|
73
|
+
"review --router off|standard|max|manual (same smart router as Agents)",
|
|
74
|
+
"review --specialists security,frontend,… to pin or invoke lanes",
|
|
75
|
+
],
|
|
76
|
+
"0.3.14": [
|
|
77
|
+
"Status / Usage / Jobs tabs (open with usage)",
|
|
78
|
+
"resize-safe home screen",
|
|
79
|
+
],
|
|
80
|
+
"0.3.15": [
|
|
81
|
+
"mg pr --wait --after-sha, mg settings, mergestorm-pr-loop skill on npm",
|
|
82
|
+
"mg skill install copies review + pr-loop from the packed tarball",
|
|
83
|
+
],
|
|
69
84
|
};
|
|
70
|
-
function
|
|
71
|
-
if (
|
|
72
|
-
return
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
85
|
+
function densityFromVariant(variant) {
|
|
86
|
+
if (variant === "mini" || variant === "nano")
|
|
87
|
+
return variant;
|
|
88
|
+
return variant ? "compact" : "full";
|
|
89
|
+
}
|
|
90
|
+
/** Home usage bars stay compact (Claude-style), not full-bleed. */
|
|
91
|
+
const BANNER_BAR_MAX = 30;
|
|
92
|
+
/** `[` + `]` + ` 100% used` around the bar cells. */
|
|
93
|
+
const BAR_CHROME_CELLS = 12;
|
|
94
|
+
function statusLine(state) {
|
|
95
|
+
if (!state.key) {
|
|
96
|
+
return `${ansi.dim("○")} not logged in · type ${ansi.brightGreen("login")} to sign in`;
|
|
76
97
|
}
|
|
77
|
-
if (keyInvalid) {
|
|
78
|
-
return
|
|
79
|
-
`${ansi.dim("○")} API key invalid or revoked — type ${ansi.green("login")} to sign in again`,
|
|
80
|
-
];
|
|
98
|
+
if (state.keyInvalid) {
|
|
99
|
+
return `${ansi.dim("○")} API key invalid or revoked · type ${ansi.brightGreen("login")} to sign in again`;
|
|
81
100
|
}
|
|
82
|
-
const display = me?.key.prefix ?? keyDisplay(key) ?? "msk_live_…";
|
|
83
|
-
if (!me) {
|
|
84
|
-
return
|
|
101
|
+
const display = state.me?.key.prefix ?? keyDisplay(state.key) ?? "msk_live_…";
|
|
102
|
+
if (!state.me) {
|
|
103
|
+
return `${ansi.brightGreen("●")} ${ansi.bold(display)} ${ansi.dim("· account details unavailable")}`;
|
|
85
104
|
}
|
|
86
|
-
const plan = me.plan_label_key ?? me.plan_key;
|
|
87
|
-
|
|
88
|
-
return [
|
|
89
|
-
`${ansi.green("●")} ${ansi.bold(display)} · ${plan}`,
|
|
90
|
-
usageBar(s.used, s.limit, { width: barWidth }),
|
|
91
|
-
];
|
|
105
|
+
const plan = state.me.plan_label_key ?? state.me.plan_key;
|
|
106
|
+
return `${ansi.brightGreen("●")} ${ansi.bold(display)} · ${plan}`;
|
|
92
107
|
}
|
|
93
|
-
function
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
108
|
+
function usageLine(me, contentCells, indent) {
|
|
109
|
+
const room = Math.max(8, contentCells - indent - BAR_CHROME_CELLS);
|
|
110
|
+
const bar = usageBar(me.usage.standard.used, me.usage.standard.limit, {
|
|
111
|
+
width: Math.min(BANNER_BAR_MAX, room),
|
|
112
|
+
});
|
|
113
|
+
const reset = ansi.dim(`· ${formatResetLabel(me.resets_at)}`);
|
|
114
|
+
const combined = `${bar} ${reset}`;
|
|
115
|
+
return visibleWidth(combined) <= contentCells - indent ? combined : bar;
|
|
116
|
+
}
|
|
117
|
+
/** First candidate that fits. Never mid-word clip — omit if nothing fits. */
|
|
118
|
+
function firstFit(options, cells) {
|
|
119
|
+
for (const opt of options) {
|
|
120
|
+
if (visibleWidth(opt) <= cells)
|
|
121
|
+
return opt;
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
function tipsLine(cells) {
|
|
126
|
+
const sep = ansi.dim(" · ");
|
|
127
|
+
return firstFit([
|
|
128
|
+
[
|
|
129
|
+
`${ansi.brightGreen("review")} ${ansi.dim("a diff")}`,
|
|
130
|
+
`${ansi.brightGreen("usage")} ${ansi.dim("for tabs")}`,
|
|
131
|
+
`${ansi.brightGreen("/help")} ${ansi.dim("for all commands")}`,
|
|
132
|
+
].join(sep),
|
|
133
|
+
[ansi.brightGreen("review"), ansi.brightGreen("usage"), ansi.brightGreen("/help")].join(sep),
|
|
134
|
+
[ansi.brightGreen("review"), ansi.brightGreen("/help")].join(sep),
|
|
135
|
+
], cells);
|
|
136
|
+
}
|
|
137
|
+
function tagline(cells) {
|
|
138
|
+
return firstFit([
|
|
139
|
+
ansi.dim("local reviews + stacked PRs · mergestorm.ai"),
|
|
140
|
+
ansi.dim("reviews + stacked PRs"),
|
|
141
|
+
ansi.dim("reviews · stacks"),
|
|
142
|
+
], cells);
|
|
100
143
|
}
|
|
101
|
-
|
|
144
|
+
/** Shorten until it fits. Never clip mid-word (that was "…Jobs tabs (open with usa"). */
|
|
145
|
+
function whatsNewLine(version, cells) {
|
|
102
146
|
const entries = WHATS_NEW[version];
|
|
103
147
|
if (!entries || !entries.length)
|
|
104
|
-
return
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
148
|
+
return null;
|
|
149
|
+
const first = entries[0];
|
|
150
|
+
const noParen = first.replace(/\s*\([^)]*\)\s*$/, "").trim();
|
|
151
|
+
const candidates = [
|
|
152
|
+
`New in v${version}: ${first}`,
|
|
153
|
+
`New in v${version}: ${noParen}`,
|
|
154
|
+
`New: ${noParen}`,
|
|
155
|
+
`New in v${version}`,
|
|
108
156
|
];
|
|
157
|
+
const extra = entries.slice(1);
|
|
158
|
+
const withExtra = `${candidates[0]} · ${extra.join(" · ")}`;
|
|
159
|
+
const options = extra.length > 0 ? [withExtra, ...candidates] : candidates;
|
|
160
|
+
const hit = firstFit(options, cells);
|
|
161
|
+
return hit ? ansi.dim(hit) : null;
|
|
109
162
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
163
|
+
/**
|
|
164
|
+
* The whole welcome panel as rows, reflowed to `columns`. Every row fits in
|
|
165
|
+
* `columns - 1` cells so a redraw's cursor math stays honest; below
|
|
166
|
+
* MIN_BOXED_COLUMNS the frame drops away instead of wrapping.
|
|
167
|
+
*/
|
|
168
|
+
export function buildBannerRows(state, columns, opts) {
|
|
169
|
+
const boxed = preferBoxedUi(columns);
|
|
170
|
+
const density = opts?.density ?? (opts?.compact ? "compact" : "full");
|
|
171
|
+
const outer = frameWidth(80, columns);
|
|
172
|
+
const contentCells = boxed
|
|
173
|
+
? Math.max(20, outer - 4)
|
|
174
|
+
: Math.max(20, terminalColumns(80, columns) - 1);
|
|
175
|
+
const indent = STORM_MARK_WIDTH + 2;
|
|
176
|
+
const pad = " ".repeat(indent);
|
|
177
|
+
const clip = (line) => visibleWidth(line) > contentCells ? padVisible(line, contentCells) : line;
|
|
178
|
+
const mark = (row) => padVisible(ansi.brightGreen(STORM_MARK[row] ?? ""), indent);
|
|
179
|
+
const title = `${ansi.boldBrightGreen("mergestorm")} ${ansi.dim(`v${state.version}`)}`;
|
|
180
|
+
if (density === "nano") {
|
|
181
|
+
const one = `${mark(0)}${title} ${statusLine(state)}`;
|
|
182
|
+
return [clip(one)];
|
|
183
|
+
}
|
|
184
|
+
if (density === "mini") {
|
|
185
|
+
// Short pane: mark + who you are, then the same cues as the full home.
|
|
186
|
+
const line0 = clip(`${mark(0)}${title} ${statusLine(state)}`);
|
|
187
|
+
const line1 = state.me && !state.keyInvalid
|
|
188
|
+
? clip(`${mark(1)}${usageLine(state.me, contentCells, indent)}`)
|
|
189
|
+
: clip(mark(1));
|
|
190
|
+
const room = contentCells - indent;
|
|
191
|
+
const blurb = tagline(room);
|
|
192
|
+
const tips = tipsLine(room);
|
|
193
|
+
const fresh = whatsNewLine(state.version, room);
|
|
194
|
+
return [
|
|
195
|
+
line0,
|
|
196
|
+
line1,
|
|
197
|
+
...(blurb ? [clip(`${pad}${blurb}`)] : []),
|
|
198
|
+
...(tips ? [clip(`${pad}${tips}`)] : []),
|
|
199
|
+
...(fresh ? [clip(`${pad}${fresh}`)] : []),
|
|
200
|
+
];
|
|
201
|
+
}
|
|
202
|
+
const content = [
|
|
203
|
+
clip(`${pad}${title}`),
|
|
204
|
+
clip(`${mark(0)}${statusLine(state)}`),
|
|
205
|
+
];
|
|
206
|
+
if (state.me && !state.keyInvalid) {
|
|
207
|
+
content.push(clip(`${mark(1)}${usageLine(state.me, contentCells, indent)}`));
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
content.push(clip(mark(1)));
|
|
211
|
+
}
|
|
212
|
+
const room = contentCells - indent;
|
|
213
|
+
const blurb = tagline(room);
|
|
214
|
+
const tips = tipsLine(room);
|
|
215
|
+
const fresh = whatsNewLine(state.version, room);
|
|
216
|
+
if (blurb)
|
|
217
|
+
content.push(clip(`${pad}${blurb}`));
|
|
218
|
+
if (tips)
|
|
219
|
+
content.push(clip(`${pad}${tips}`));
|
|
220
|
+
if (fresh)
|
|
221
|
+
content.push(clip(`${pad}${fresh}`));
|
|
222
|
+
if (boxed && density === "full") {
|
|
223
|
+
content.splice(3, 0, "");
|
|
224
|
+
return [...roundedBox(content, { width: outer, padding: 1 })];
|
|
225
|
+
}
|
|
226
|
+
if (boxed) {
|
|
227
|
+
return [...roundedBox(content, { width: outer, padding: 1 })];
|
|
228
|
+
}
|
|
229
|
+
return content.map(clip);
|
|
114
230
|
}
|
|
115
|
-
|
|
231
|
+
/**
|
|
232
|
+
* Print the welcome panel and return a handle whose `rows()` reflows the
|
|
233
|
+
* same data for a new width (used by the prompt's resize redraw).
|
|
234
|
+
*/
|
|
235
|
+
export async function printBannerHeader(opts) {
|
|
116
236
|
const cfg = await loadConfig();
|
|
117
237
|
const key = resolveApiKey(cfg);
|
|
118
238
|
let me = null;
|
|
@@ -131,46 +251,17 @@ export async function printBannerHeader() {
|
|
|
131
251
|
}
|
|
132
252
|
}
|
|
133
253
|
}
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const inner = Math.max(40, outer - 4);
|
|
138
|
-
// Two columns: left = logo + status, right = tips + what's new.
|
|
139
|
-
const leftColW = Math.max(28, Math.floor(inner * 0.42));
|
|
140
|
-
const rightColW = Math.max(24, inner - leftColW - 2);
|
|
141
|
-
const barWidth = Math.max(12, leftColW - 4);
|
|
142
|
-
const logoLines = TORNADO_LOGO.map((line) => centerLine(ansi.brightGreen(line), leftColW));
|
|
143
|
-
const leftCol = [
|
|
144
|
-
...logoLines,
|
|
145
|
-
"",
|
|
146
|
-
...formatStatusLines(me, key, barWidth, keyInvalid),
|
|
147
|
-
];
|
|
148
|
-
const rightCol = [
|
|
149
|
-
...tipsLines(),
|
|
150
|
-
"",
|
|
151
|
-
...whatsNewLines(version),
|
|
152
|
-
];
|
|
153
|
-
// Clip right column lines that overflow the right pane.
|
|
154
|
-
const rightClipped = rightCol.map((line) => visibleWidth(line) <= rightColW ? line : padVisible(line, rightColW));
|
|
155
|
-
const rows = sideBySide(leftCol, rightClipped, leftColW, 2);
|
|
156
|
-
if (!preferBoxedUi()) {
|
|
157
|
-
// Narrow panes — plain lines beat wrapped box frames.
|
|
158
|
-
console.log(ansi.boldGreen(`mergestorm v${version}`));
|
|
159
|
-
for (const line of rows)
|
|
254
|
+
const state = { version: cliVersion(), me, key, keyInvalid };
|
|
255
|
+
if (opts?.paint !== false) {
|
|
256
|
+
for (const line of buildBannerRows(state)) {
|
|
160
257
|
console.log(line);
|
|
161
|
-
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
const panel = roundedBox(rows, {
|
|
165
|
-
title: `mergestorm v${version}`,
|
|
166
|
-
width: outer,
|
|
167
|
-
padding: 1,
|
|
168
|
-
});
|
|
169
|
-
for (const line of panel) {
|
|
170
|
-
console.log(line);
|
|
258
|
+
}
|
|
171
259
|
}
|
|
172
|
-
|
|
260
|
+
return {
|
|
261
|
+
rows: (columns, variant) => buildBannerRows(state, columns, { density: densityFromVariant(variant) }),
|
|
262
|
+
};
|
|
173
263
|
}
|
|
174
|
-
export function shellPrompt(
|
|
175
|
-
|
|
264
|
+
export function shellPrompt(_loggedIn) {
|
|
265
|
+
// Bright mark green. Never dim, never 32, never the word "mergestorm".
|
|
266
|
+
return ansi.boldBrightGreen("mg");
|
|
176
267
|
}
|
package/dist/ui/box.d.ts
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
|
-
|
|
1
|
+
import { padVisible, sliceVisible, visibleWidth } from "./width.js";
|
|
2
|
+
export { padVisible, sliceVisible, visibleWidth };
|
|
2
3
|
/** Below this width, boxed chrome wraps on real TTYs — prefer plain output. */
|
|
3
4
|
export declare const MIN_BOXED_COLUMNS = 48;
|
|
4
|
-
/**
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Usable terminal columns (TTY), with a sensible fallback for pipes/tests.
|
|
7
|
+
* Pass `columns` to measure a stream other than process.stdout (or a
|
|
8
|
+
* post-resize value); `undefined` reads stdout at call time.
|
|
9
|
+
*/
|
|
10
|
+
export declare function terminalColumns(fallback?: number, columns?: number): number;
|
|
6
11
|
/**
|
|
7
12
|
* Whether rounded/full-bleed boxes fit without wrapping.
|
|
8
13
|
* Non-TTY (unknown columns) keeps boxes for tests and CI logs.
|
|
9
14
|
*/
|
|
10
|
-
export declare function preferBoxedUi(): boolean;
|
|
15
|
+
export declare function preferBoxedUi(columns?: number): boolean;
|
|
11
16
|
/**
|
|
12
17
|
* Outer box width for Claude-style full-bleed frames.
|
|
13
18
|
* Never wider than the terminal (minus 1 so the right border doesn't wrap).
|
|
14
19
|
*/
|
|
15
|
-
export declare function frameWidth(fallback?: number): number;
|
|
20
|
+
export declare function frameWidth(fallback?: number, columns?: number): number;
|
|
16
21
|
export interface RoundedBoxOptions {
|
|
17
22
|
title?: string;
|
|
18
23
|
padding?: number;
|
|
19
24
|
/**
|
|
20
|
-
* Exact outer width (including borders). When set, content is padded to
|
|
21
|
-
*
|
|
25
|
+
* Exact outer width (including borders). When set, content is padded to
|
|
26
|
+
* fill and *clipped* to fit: no row is ever wider than this, because a row
|
|
27
|
+
* that wraps is a row the redraw math doesn't know about.
|
|
22
28
|
*/
|
|
23
29
|
width?: number;
|
|
24
30
|
/** Grow to at least this outer width (content may still be wider). */
|
|
@@ -29,8 +35,6 @@ export interface RoundedBoxOptions {
|
|
|
29
35
|
* `minWidth` to stretch to the terminal like Claude Code.
|
|
30
36
|
*/
|
|
31
37
|
export declare function roundedBox(lines: string[], opts?: RoundedBoxOptions): string[];
|
|
32
|
-
/** Pad/truncate a line to an exact visible width (ANSI-safe). */
|
|
33
|
-
export declare function padVisible(line: string, width: number): string;
|
|
34
38
|
/**
|
|
35
39
|
* Join left/right columns into rows of equal left-column width.
|
|
36
40
|
* Extra rows on either side are blank-padded.
|
package/dist/ui/box.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { stdout } from "node:process";
|
|
2
2
|
import { ansi } from "./ansi.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
3
|
+
import { padVisible, sliceVisible, visibleWidth } from "./width.js";
|
|
4
|
+
// Cell arithmetic lives in width.ts; re-exported so existing `./box.js`
|
|
5
|
+
// importers (banner, prompt) keep working.
|
|
6
|
+
export { padVisible, sliceVisible, visibleWidth };
|
|
7
7
|
/** Below this width, boxed chrome wraps on real TTYs — prefer plain output. */
|
|
8
8
|
export const MIN_BOXED_COLUMNS = 48;
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Usable terminal columns (TTY), with a sensible fallback for pipes/tests.
|
|
11
|
+
* Pass `columns` to measure a stream other than process.stdout (or a
|
|
12
|
+
* post-resize value); `undefined` reads stdout at call time.
|
|
13
|
+
*/
|
|
14
|
+
export function terminalColumns(fallback = 80, columns) {
|
|
15
|
+
const cols = columns ?? stdout.columns;
|
|
12
16
|
if (typeof cols === "number" && Number.isFinite(cols) && cols >= 1) {
|
|
13
17
|
return cols;
|
|
14
18
|
}
|
|
@@ -18,8 +22,8 @@ export function terminalColumns(fallback = 80) {
|
|
|
18
22
|
* Whether rounded/full-bleed boxes fit without wrapping.
|
|
19
23
|
* Non-TTY (unknown columns) keeps boxes for tests and CI logs.
|
|
20
24
|
*/
|
|
21
|
-
export function preferBoxedUi() {
|
|
22
|
-
const cols = stdout.columns;
|
|
25
|
+
export function preferBoxedUi(columns) {
|
|
26
|
+
const cols = columns ?? stdout.columns;
|
|
23
27
|
if (typeof cols !== "number" || !Number.isFinite(cols))
|
|
24
28
|
return true;
|
|
25
29
|
return cols >= MIN_BOXED_COLUMNS;
|
|
@@ -28,8 +32,8 @@ export function preferBoxedUi() {
|
|
|
28
32
|
* Outer box width for Claude-style full-bleed frames.
|
|
29
33
|
* Never wider than the terminal (minus 1 so the right border doesn't wrap).
|
|
30
34
|
*/
|
|
31
|
-
export function frameWidth(fallback = 80) {
|
|
32
|
-
return Math.max(20, terminalColumns(fallback) - 1);
|
|
35
|
+
export function frameWidth(fallback = 80, columns) {
|
|
36
|
+
return Math.max(20, terminalColumns(fallback, columns) - 1);
|
|
33
37
|
}
|
|
34
38
|
const ROUNDED = { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│" };
|
|
35
39
|
const ASCII = { tl: "+", tr: "+", bl: "+", br: "+", h: "-", v: "|" };
|
|
@@ -49,12 +53,17 @@ function centerLabel(label, width, fill) {
|
|
|
49
53
|
export function roundedBox(lines, opts = {}) {
|
|
50
54
|
const padding = Math.max(0, opts.padding ?? 1);
|
|
51
55
|
const chars = boxChars();
|
|
52
|
-
|
|
56
|
+
let title = opts.title ? ` ${opts.title} ` : "";
|
|
53
57
|
const contentInner = Math.max(0, ...lines.map(visibleWidth), visibleWidth(title)) + padding * 2;
|
|
54
58
|
let innerWidth = contentInner;
|
|
59
|
+
let body = lines;
|
|
55
60
|
if (opts.width !== undefined) {
|
|
56
|
-
// outer = inner + 2 border cols
|
|
57
|
-
innerWidth = Math.max(
|
|
61
|
+
// outer = inner + 2 border cols; clamp, never grow.
|
|
62
|
+
innerWidth = Math.max(0, opts.width - 2);
|
|
63
|
+
const contentCells = Math.max(0, innerWidth - padding * 2);
|
|
64
|
+
body = lines.map((line) => visibleWidth(line) > contentCells ? padVisible(line, contentCells) : line);
|
|
65
|
+
if (visibleWidth(title) > innerWidth)
|
|
66
|
+
title = sliceVisible(title, 0, innerWidth);
|
|
58
67
|
}
|
|
59
68
|
else if (opts.minWidth !== undefined) {
|
|
60
69
|
innerWidth = Math.max(contentInner, opts.minWidth - 2);
|
|
@@ -63,37 +72,12 @@ export function roundedBox(lines, opts = {}) {
|
|
|
63
72
|
? `${chars.tl}${centerLabel(title, innerWidth, chars.h)}${chars.tr}`
|
|
64
73
|
: `${chars.tl}${chars.h.repeat(innerWidth)}${chars.tr}`);
|
|
65
74
|
const bottom = ansi.gray(`${chars.bl}${chars.h.repeat(innerWidth)}${chars.br}`);
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
const leftPad = Math.min(padding, innerWidth);
|
|
76
|
+
const rows = body.map((line) => {
|
|
77
|
+
const gap = Math.max(0, innerWidth - leftPad - visibleWidth(line));
|
|
78
|
+
return `${ansi.gray(chars.v)}${" ".repeat(leftPad)}${line}${" ".repeat(gap)}${ansi.gray(chars.v)}`;
|
|
69
79
|
});
|
|
70
|
-
return [top, ...
|
|
71
|
-
}
|
|
72
|
-
/** Pad/truncate a line to an exact visible width (ANSI-safe). */
|
|
73
|
-
export function padVisible(line, width) {
|
|
74
|
-
const w = visibleWidth(line);
|
|
75
|
-
if (w === width)
|
|
76
|
-
return line;
|
|
77
|
-
if (w < width)
|
|
78
|
-
return `${line}${" ".repeat(width - w)}`;
|
|
79
|
-
// Truncate while preserving ANSI codes: walk the string counting only
|
|
80
|
-
// visible characters, then slice the original at that position.
|
|
81
|
-
let visible = 0;
|
|
82
|
-
let i = 0;
|
|
83
|
-
const re = /\u001b\[[0-9;]*m/g;
|
|
84
|
-
while (visible < width && i < line.length) {
|
|
85
|
-
re.lastIndex = i;
|
|
86
|
-
const m = re.exec(line);
|
|
87
|
-
if (m && m.index === i) {
|
|
88
|
-
i = re.lastIndex;
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
const code = line.charCodeAt(i);
|
|
92
|
-
i += code >= 0xd800 && code <= 0xdbff ? 2 : 1;
|
|
93
|
-
visible++;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return line.slice(0, i);
|
|
80
|
+
return [top, ...rows, bottom];
|
|
97
81
|
}
|
|
98
82
|
/**
|
|
99
83
|
* Join left/right columns into rows of equal left-column width.
|
package/dist/ui/findings.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/** Shared review-job findings payload (API may send snake or camel for off-diff). */
|
|
2
2
|
export type ReviewFindings = {
|
|
3
|
+
specialists_run?: string[];
|
|
3
4
|
inline?: {
|
|
4
5
|
path: string;
|
|
5
6
|
line: number;
|
|
6
7
|
severity: string;
|
|
7
8
|
body: string;
|
|
8
9
|
title?: string;
|
|
10
|
+
specialist?: string;
|
|
9
11
|
}[];
|
|
10
12
|
off_diff?: {
|
|
11
13
|
path?: string;
|
|
@@ -24,5 +26,6 @@ export type RenderFindingsOptions = {
|
|
|
24
26
|
/** When true and both lists are empty, print the empty message. */
|
|
25
27
|
emptyMessage?: boolean;
|
|
26
28
|
};
|
|
27
|
-
|
|
29
|
+
export declare function formatFindingsLines(findings: ReviewFindings | null | undefined, opts?: RenderFindingsOptions): string[];
|
|
30
|
+
/** Print inline + off-diff findings the same way for oneshot `review` / `status --pretty`. */
|
|
28
31
|
export declare function renderFindings(findings: ReviewFindings | null | undefined, opts?: RenderFindingsOptions): void;
|
package/dist/ui/findings.js
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
export function formatFindingsLines(findings, opts = {}) {
|
|
2
|
+
const lines = [];
|
|
3
3
|
const inline = findings?.inline ?? [];
|
|
4
4
|
const off = findings?.off_diff ?? findings?.offDiff ?? [];
|
|
5
5
|
for (const c of inline) {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
const lane = c.specialist ? `[${c.specialist}]` : "";
|
|
7
|
+
lines.push(`[${c.severity}]${lane} ${c.path}:${c.line}${c.title ? ` — ${c.title}` : ""}`);
|
|
8
|
+
lines.push(c.body);
|
|
8
9
|
}
|
|
9
10
|
for (const c of off) {
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
lines.push(`[${c.severity}] off-diff ${c.path ?? ""}`);
|
|
12
|
+
lines.push(c.body);
|
|
12
13
|
}
|
|
13
14
|
if (opts.emptyMessage && inline.length === 0 && off.length === 0) {
|
|
14
|
-
|
|
15
|
+
lines.push("No findings.");
|
|
15
16
|
}
|
|
17
|
+
return lines;
|
|
18
|
+
}
|
|
19
|
+
/** Print inline + off-diff findings the same way for oneshot `review` / `status --pretty`. */
|
|
20
|
+
export function renderFindings(findings, opts = {}) {
|
|
21
|
+
for (const line of formatFindingsLines(findings, opts))
|
|
22
|
+
console.log(line);
|
|
16
23
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ReviewFindings } from "./findings.js";
|
|
2
|
+
export declare const REVIEW_JOB_ENVELOPE_SCHEMA: "mergestorm.review_job/v1";
|
|
3
|
+
export type ReviewJobRow = {
|
|
4
|
+
job_id?: string | null;
|
|
5
|
+
id?: string | null;
|
|
6
|
+
status?: string | null;
|
|
7
|
+
thread_slug?: string | null;
|
|
8
|
+
thread_job_number?: number | null;
|
|
9
|
+
base_label?: string | null;
|
|
10
|
+
head_label?: string | null;
|
|
11
|
+
router_mode?: string | null;
|
|
12
|
+
specialists_requested?: string[] | null;
|
|
13
|
+
specialists_run?: string[] | null;
|
|
14
|
+
verdict?: string | null;
|
|
15
|
+
summary?: string | null;
|
|
16
|
+
findings?: ReviewFindings | null;
|
|
17
|
+
truncated?: boolean | null;
|
|
18
|
+
error?: string | null;
|
|
19
|
+
credits?: {
|
|
20
|
+
standard: number;
|
|
21
|
+
} | null;
|
|
22
|
+
webhook_secret?: string | null;
|
|
23
|
+
created_at?: string | null;
|
|
24
|
+
finished_at?: string | null;
|
|
25
|
+
retry_after_seconds?: number | null;
|
|
26
|
+
};
|
|
27
|
+
export type ReviewJobEnvelope = {
|
|
28
|
+
schema: typeof REVIEW_JOB_ENVELOPE_SCHEMA;
|
|
29
|
+
job_id: string | null;
|
|
30
|
+
status: string;
|
|
31
|
+
thread: {
|
|
32
|
+
slug: string;
|
|
33
|
+
job_number: number | null;
|
|
34
|
+
} | null;
|
|
35
|
+
base_label: string | null;
|
|
36
|
+
head_label: string | null;
|
|
37
|
+
router_mode: string | null;
|
|
38
|
+
specialists_requested: string[];
|
|
39
|
+
specialists_run: string[];
|
|
40
|
+
verdict: string | null;
|
|
41
|
+
summary: string | null;
|
|
42
|
+
findings: ReviewFindings | null;
|
|
43
|
+
truncated: boolean;
|
|
44
|
+
error: string | null;
|
|
45
|
+
credits: {
|
|
46
|
+
standard: number;
|
|
47
|
+
} | null;
|
|
48
|
+
webhook_secret: string | null;
|
|
49
|
+
created_at: string | null;
|
|
50
|
+
finished_at: string | null;
|
|
51
|
+
retry_after_seconds?: number;
|
|
52
|
+
};
|
|
53
|
+
export type ReviewEnvelopeFallbacks = {
|
|
54
|
+
jobId?: string | null;
|
|
55
|
+
status?: string;
|
|
56
|
+
threadSlug?: string | null;
|
|
57
|
+
baseLabel?: string | null;
|
|
58
|
+
headLabel?: string | null;
|
|
59
|
+
routerMode?: string | null;
|
|
60
|
+
specialistsRequested?: string[];
|
|
61
|
+
error?: string | null;
|
|
62
|
+
retryAfterSeconds?: number | null;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Stable machine envelope shared by submit and terminal review output.
|
|
66
|
+
* Fields added by the API-envelope work are nullable until that API lands.
|
|
67
|
+
*/
|
|
68
|
+
export declare function toReviewJobEnvelope(row: ReviewJobRow | null | undefined, fallback?: ReviewEnvelopeFallbacks): ReviewJobEnvelope;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const REVIEW_JOB_ENVELOPE_SCHEMA = "mergestorm.review_job/v1";
|
|
2
|
+
/**
|
|
3
|
+
* Stable machine envelope shared by submit and terminal review output.
|
|
4
|
+
* Fields added by the API-envelope work are nullable until that API lands.
|
|
5
|
+
*/
|
|
6
|
+
export function toReviewJobEnvelope(row, fallback = {}) {
|
|
7
|
+
const slug = row?.thread_slug ?? fallback.threadSlug ?? null;
|
|
8
|
+
const findings = row?.findings ?? null;
|
|
9
|
+
const retryAfter = fallback.retryAfterSeconds ?? row?.retry_after_seconds ?? undefined;
|
|
10
|
+
return {
|
|
11
|
+
schema: REVIEW_JOB_ENVELOPE_SCHEMA,
|
|
12
|
+
job_id: row?.job_id ?? row?.id ?? fallback.jobId ?? null,
|
|
13
|
+
status: row?.status ?? fallback.status ?? "failed",
|
|
14
|
+
thread: slug
|
|
15
|
+
? { slug, job_number: row?.thread_job_number ?? null }
|
|
16
|
+
: null,
|
|
17
|
+
base_label: row?.base_label ?? fallback.baseLabel ?? null,
|
|
18
|
+
head_label: row?.head_label ?? fallback.headLabel ?? null,
|
|
19
|
+
router_mode: row?.router_mode ?? fallback.routerMode ?? null,
|
|
20
|
+
specialists_requested: row?.specialists_requested ?? fallback.specialistsRequested ?? [],
|
|
21
|
+
specialists_run: row?.specialists_run ?? findings?.specialists_run ?? [],
|
|
22
|
+
verdict: row?.verdict ?? null,
|
|
23
|
+
summary: row?.summary ?? null,
|
|
24
|
+
findings,
|
|
25
|
+
truncated: row?.truncated ?? false,
|
|
26
|
+
error: fallback.error !== undefined ? fallback.error : (row?.error ?? null),
|
|
27
|
+
credits: row?.credits ?? null,
|
|
28
|
+
webhook_secret: row?.webhook_secret ?? null,
|
|
29
|
+
created_at: row?.created_at ?? null,
|
|
30
|
+
finished_at: row?.finished_at ?? null,
|
|
31
|
+
...(retryAfter != null && Number.isFinite(retryAfter)
|
|
32
|
+
? { retry_after_seconds: retryAfter }
|
|
33
|
+
: {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type TabsLayout } from "./tabs.js";
|
|
2
|
+
export type LineTab = {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
lines: string[];
|
|
6
|
+
};
|
|
7
|
+
export type LineTabsState = {
|
|
8
|
+
index: number;
|
|
9
|
+
scroll: number[];
|
|
10
|
+
};
|
|
11
|
+
export declare function initialLineTabsState(tabCount: number, start?: number): LineTabsState;
|
|
12
|
+
export declare function reduceLineTabsKey(state: LineTabsState, key: "left" | "right" | "up" | "down" | "escape" | "q", tabCount: number, maxScroll: number): {
|
|
13
|
+
kind: "close";
|
|
14
|
+
} | {
|
|
15
|
+
kind: "state";
|
|
16
|
+
state: LineTabsState;
|
|
17
|
+
};
|
|
18
|
+
export declare function layoutLineTabs(tabs: LineTab[], state: LineTabsState, columns?: number, rows?: number, color?: boolean): TabsLayout;
|
|
19
|
+
/** Hold-to-read panel. TTY: tabbed frame. Pipe: print the lines. */
|
|
20
|
+
export declare function showLinePanel(label: string, lines: string[]): Promise<void>;
|
|
21
|
+
export declare function runLineTabsBrowser(opts: {
|
|
22
|
+
tabs: LineTab[];
|
|
23
|
+
initial?: number;
|
|
24
|
+
input?: NodeJS.ReadStream;
|
|
25
|
+
output?: NodeJS.WriteStream;
|
|
26
|
+
color?: boolean;
|
|
27
|
+
}): Promise<void>;
|