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/usage.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { ansi } from "./ansi.js";
|
|
2
|
+
import { frameWidth } from "./box.js";
|
|
3
|
+
import { padVisible, visibleWidth } from "./width.js";
|
|
2
4
|
/**
|
|
3
5
|
* Usage meter — long thin bar + percent only:
|
|
4
6
|
* `[████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 7% used`
|
|
5
7
|
*
|
|
6
8
|
* No used/limit clutter. Edge cases:
|
|
7
|
-
* - `limit === 0` → dim "not included"
|
|
9
|
+
* - `limit === 0` → dim "not included"
|
|
8
10
|
* - `limit == null` → "unlimited" (dev-bypass)
|
|
9
11
|
*/
|
|
10
12
|
export function usageBar(used, limit, opts = {}) {
|
|
@@ -28,7 +30,7 @@ export function usageBar(used, limit, opts = {}) {
|
|
|
28
30
|
if (!color) {
|
|
29
31
|
return `[${filledSeg}${emptySeg}]${suffix}`;
|
|
30
32
|
}
|
|
31
|
-
let fillColor = ansi.
|
|
33
|
+
let fillColor = ansi.brightGreen;
|
|
32
34
|
if (ratio >= 0.9)
|
|
33
35
|
fillColor = ansi.red;
|
|
34
36
|
else if (ratio >= 0.75)
|
|
@@ -61,7 +63,74 @@ export function formatResetLabel(iso, now = new Date()) {
|
|
|
61
63
|
const mm = String(m).padStart(2, "0");
|
|
62
64
|
return `Resets ${month} ${day}, ${h}:${mm}${ampm} (UTC)`;
|
|
63
65
|
}
|
|
66
|
+
/** How long until `iso` (the API `resets_at`). Empty when the date is unusable. */
|
|
67
|
+
export function formatDaysLeft(iso, now = new Date()) {
|
|
68
|
+
if (!iso)
|
|
69
|
+
return "";
|
|
70
|
+
const d = new Date(iso);
|
|
71
|
+
if (Number.isNaN(d.getTime()))
|
|
72
|
+
return "";
|
|
73
|
+
const ms = d.getTime() - now.getTime();
|
|
74
|
+
if (ms <= 0)
|
|
75
|
+
return "reset due";
|
|
76
|
+
const days = Math.floor(ms / 86_400_000);
|
|
77
|
+
const hours = Math.floor((ms % 86_400_000) / 3_600_000);
|
|
78
|
+
if (days >= 2)
|
|
79
|
+
return `${days} days left`;
|
|
80
|
+
if (days === 1)
|
|
81
|
+
return "1 day left";
|
|
82
|
+
if (hours >= 1)
|
|
83
|
+
return `${hours}h left`;
|
|
84
|
+
return "resets soon";
|
|
85
|
+
}
|
|
64
86
|
/** Alias kept for callers that still import `formatResetDate`. */
|
|
65
87
|
export function formatResetDate(iso) {
|
|
66
88
|
return formatResetLabel(iso);
|
|
67
89
|
}
|
|
90
|
+
/** Bar cells so ` [bar] 100% used` stays inside the terminal. */
|
|
91
|
+
export function usageBarWidth(columns) {
|
|
92
|
+
return Math.max(8, frameWidth(80, columns) - 13);
|
|
93
|
+
}
|
|
94
|
+
function clipLine(line, columns) {
|
|
95
|
+
if (visibleWidth(line) <= columns)
|
|
96
|
+
return line;
|
|
97
|
+
return padVisible(line, columns);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Static `/usage` panel: identity, a bar sized from `frameWidth()`, reset
|
|
101
|
+
* line, then the last five jobs. Every row is clipped to `columns`.
|
|
102
|
+
*/
|
|
103
|
+
export function formatUsagePanel(input) {
|
|
104
|
+
const columns = Math.max(20, input.columns);
|
|
105
|
+
const color = input.color ?? false;
|
|
106
|
+
const identity = `● ${input.keyPrefix} · ${input.plan}`;
|
|
107
|
+
const bar = usageBar(input.used, input.limit, {
|
|
108
|
+
width: usageBarWidth(columns),
|
|
109
|
+
color,
|
|
110
|
+
});
|
|
111
|
+
const lines = [
|
|
112
|
+
` ${identity}`,
|
|
113
|
+
` ${bar}`,
|
|
114
|
+
` ${formatResetLabel(input.resetsAt)}`,
|
|
115
|
+
"",
|
|
116
|
+
];
|
|
117
|
+
if (input.jobs.length === 0) {
|
|
118
|
+
lines.push(input.jobsError ? ` ${input.jobsError}` : " No review jobs yet.");
|
|
119
|
+
return lines.map((line) => clipLine(line, columns));
|
|
120
|
+
}
|
|
121
|
+
const inner = Math.max(20, columns - 2);
|
|
122
|
+
const jobW = 8;
|
|
123
|
+
const verdictW = 15;
|
|
124
|
+
const creditsW = 7;
|
|
125
|
+
const whenW = 8;
|
|
126
|
+
const gaps = 8;
|
|
127
|
+
const threadW = Math.max(6, inner - jobW - verdictW - creditsW - whenW - gaps);
|
|
128
|
+
const header = ["JOB", "VERDICT", "THREAD", "CREDITS", "WHEN"];
|
|
129
|
+
const widths = [jobW, verdictW, threadW, creditsW, whenW];
|
|
130
|
+
const fmt = (cols) => " " + cols.map((c, i) => padVisible(c, widths[i])).join(" ");
|
|
131
|
+
lines.push(fmt(header));
|
|
132
|
+
for (const job of input.jobs.slice(0, 5)) {
|
|
133
|
+
lines.push(fmt([job.job, job.verdict, job.thread, job.credits, job.when]));
|
|
134
|
+
}
|
|
135
|
+
return lines.map((line) => clipLine(line, columns));
|
|
136
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal cell arithmetic. Everything that frames, pads, or parks a cursor
|
|
3
|
+
* must count *cells*, not UTF-16 units: CJK is two cells, combining marks
|
|
4
|
+
* and ZWJ are zero, an astral emoji is one grapheme. Getting this wrong is
|
|
5
|
+
* why the right border of the prompt box drifted on non-Latin input.
|
|
6
|
+
*/
|
|
7
|
+
export declare function stripSgr(str: string): string;
|
|
8
|
+
/** Cells for one code point on its own (no cluster context). */
|
|
9
|
+
export declare function codePointWidth(cp: number): number;
|
|
10
|
+
/**
|
|
11
|
+
* Cells one grapheme cluster occupies. A cluster is as wide as its widest
|
|
12
|
+
* base (a ZWJ family emoji is 2, not 6); VS16 promotes a text-style symbol
|
|
13
|
+
* to emoji presentation (2 cells); a cluster of nothing but marks is 0.
|
|
14
|
+
*/
|
|
15
|
+
export declare function graphemeWidth(cluster: string): number;
|
|
16
|
+
/** Terminal cells a string occupies, ignoring SGR colour codes. */
|
|
17
|
+
export declare function visibleWidth(str: string): number;
|
|
18
|
+
/**
|
|
19
|
+
* Take the cells `[start, start + cells)` of `str`, keeping every SGR code
|
|
20
|
+
* (so colours survive) and substituting spaces for a wide glyph cut by
|
|
21
|
+
* either edge, so the result's geometry is exactly what the caller asked
|
|
22
|
+
* for even when a CJK character straddles the window.
|
|
23
|
+
*/
|
|
24
|
+
export declare function sliceVisible(str: string, start: number, cells: number): string;
|
|
25
|
+
/** Pad or truncate to an exact cell width (ANSI-safe). */
|
|
26
|
+
export declare function padVisible(line: string, width: number): string;
|
package/dist/ui/width.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal cell arithmetic. Everything that frames, pads, or parks a cursor
|
|
3
|
+
* must count *cells*, not UTF-16 units: CJK is two cells, combining marks
|
|
4
|
+
* and ZWJ are zero, an astral emoji is one grapheme. Getting this wrong is
|
|
5
|
+
* why the right border of the prompt box drifted on non-Latin input.
|
|
6
|
+
*/
|
|
7
|
+
const SGR_PATTERN = /\u001b\[[0-9;]*m/g;
|
|
8
|
+
const SGR_SPLIT = /(\u001b\[[0-9;]*m)/;
|
|
9
|
+
export function stripSgr(str) {
|
|
10
|
+
return str.replace(SGR_PATTERN, "");
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* East Asian Wide / Fullwidth ranges (plus emoji presentation blocks).
|
|
14
|
+
* Deliberately excludes Ambiguous-width ranges such as box drawing
|
|
15
|
+
* (U+2500..257F) and block elements (U+2580..259F, the TORNADO_LOGO
|
|
16
|
+
* glyphs), which every terminal we target renders in one cell.
|
|
17
|
+
*/
|
|
18
|
+
const WIDE_RANGES = [
|
|
19
|
+
[0x1100, 0x115f], // Hangul Jamo
|
|
20
|
+
[0x231a, 0x231b],
|
|
21
|
+
[0x2329, 0x232a],
|
|
22
|
+
[0x23e9, 0x23ec],
|
|
23
|
+
[0x23f0, 0x23f0],
|
|
24
|
+
[0x23f3, 0x23f3],
|
|
25
|
+
[0x25fd, 0x25fe],
|
|
26
|
+
[0x2614, 0x2615],
|
|
27
|
+
[0x2648, 0x2653],
|
|
28
|
+
[0x267f, 0x267f],
|
|
29
|
+
[0x2693, 0x2693],
|
|
30
|
+
[0x26a1, 0x26a1],
|
|
31
|
+
[0x26aa, 0x26ab],
|
|
32
|
+
[0x26bd, 0x26be],
|
|
33
|
+
[0x26c4, 0x26c5],
|
|
34
|
+
[0x26ce, 0x26ce],
|
|
35
|
+
[0x26d4, 0x26d4],
|
|
36
|
+
[0x26ea, 0x26ea],
|
|
37
|
+
[0x26f2, 0x26f3],
|
|
38
|
+
[0x26f5, 0x26f5],
|
|
39
|
+
[0x26fa, 0x26fa],
|
|
40
|
+
[0x26fd, 0x26fd],
|
|
41
|
+
[0x2705, 0x2705],
|
|
42
|
+
[0x270a, 0x270b],
|
|
43
|
+
[0x2728, 0x2728],
|
|
44
|
+
[0x274c, 0x274c],
|
|
45
|
+
[0x274e, 0x274e],
|
|
46
|
+
[0x2753, 0x2755],
|
|
47
|
+
[0x2757, 0x2757],
|
|
48
|
+
[0x2795, 0x2797],
|
|
49
|
+
[0x27b0, 0x27b0],
|
|
50
|
+
[0x27bf, 0x27bf],
|
|
51
|
+
[0x2b1b, 0x2b1c],
|
|
52
|
+
[0x2b50, 0x2b50],
|
|
53
|
+
[0x2b55, 0x2b55],
|
|
54
|
+
[0x2e80, 0x303e], // CJK radicals, Kangxi, ideographic description, CJK symbols
|
|
55
|
+
[0x3041, 0x33ff], // Hiragana, Katakana, Bopomofo, Hangul compat, enclosed CJK
|
|
56
|
+
[0x3400, 0x4dbf], // CJK ext A
|
|
57
|
+
[0x4e00, 0x9fff], // CJK unified
|
|
58
|
+
[0xa000, 0xa4cf], // Yi
|
|
59
|
+
[0xa960, 0xa97f], // Hangul Jamo ext A
|
|
60
|
+
[0xac00, 0xd7a3], // Hangul syllables
|
|
61
|
+
[0xf900, 0xfaff], // CJK compat ideographs
|
|
62
|
+
[0xfe10, 0xfe19], // vertical forms
|
|
63
|
+
[0xfe30, 0xfe6f], // CJK compat forms, small forms
|
|
64
|
+
[0xff00, 0xff60], // fullwidth forms
|
|
65
|
+
[0xffe0, 0xffe6],
|
|
66
|
+
[0x16fe0, 0x16fe4],
|
|
67
|
+
[0x17000, 0x18aff], // Tangut, Khitan
|
|
68
|
+
[0x1b000, 0x1b2ff], // Kana supplement / ext
|
|
69
|
+
[0x1f004, 0x1f004],
|
|
70
|
+
[0x1f0cf, 0x1f0cf],
|
|
71
|
+
[0x1f18e, 0x1f18e],
|
|
72
|
+
[0x1f191, 0x1f19a],
|
|
73
|
+
[0x1f1e6, 0x1f1ff], // regional indicators (flags pair to one 2-cell cluster)
|
|
74
|
+
[0x1f200, 0x1f202],
|
|
75
|
+
[0x1f210, 0x1f23b],
|
|
76
|
+
[0x1f240, 0x1f248],
|
|
77
|
+
[0x1f250, 0x1f251],
|
|
78
|
+
[0x1f260, 0x1f265],
|
|
79
|
+
[0x1f300, 0x1f320],
|
|
80
|
+
[0x1f32d, 0x1f335],
|
|
81
|
+
[0x1f337, 0x1f37c],
|
|
82
|
+
[0x1f37e, 0x1f393],
|
|
83
|
+
[0x1f3a0, 0x1f3ca],
|
|
84
|
+
[0x1f3cf, 0x1f3d3],
|
|
85
|
+
[0x1f3e0, 0x1f3f0],
|
|
86
|
+
[0x1f3f4, 0x1f3f4],
|
|
87
|
+
[0x1f3f8, 0x1f43e],
|
|
88
|
+
[0x1f440, 0x1f440],
|
|
89
|
+
[0x1f442, 0x1f4fc],
|
|
90
|
+
[0x1f4ff, 0x1f53d],
|
|
91
|
+
[0x1f54b, 0x1f54e],
|
|
92
|
+
[0x1f550, 0x1f567],
|
|
93
|
+
[0x1f57a, 0x1f57a],
|
|
94
|
+
[0x1f595, 0x1f596],
|
|
95
|
+
[0x1f5a4, 0x1f5a4],
|
|
96
|
+
[0x1f5fb, 0x1f64f],
|
|
97
|
+
[0x1f680, 0x1f6c5],
|
|
98
|
+
[0x1f6cc, 0x1f6cc],
|
|
99
|
+
[0x1f6d0, 0x1f6d2],
|
|
100
|
+
[0x1f6d5, 0x1f6d7],
|
|
101
|
+
[0x1f6dc, 0x1f6df],
|
|
102
|
+
[0x1f6eb, 0x1f6ec],
|
|
103
|
+
[0x1f6f4, 0x1f6fc],
|
|
104
|
+
[0x1f7e0, 0x1f7eb],
|
|
105
|
+
[0x1f7f0, 0x1f7f0],
|
|
106
|
+
[0x1f90c, 0x1f93a],
|
|
107
|
+
[0x1f93c, 0x1f945],
|
|
108
|
+
[0x1f947, 0x1f9ff],
|
|
109
|
+
[0x1fa70, 0x1faff],
|
|
110
|
+
[0x20000, 0x2fffd], // CJK ext B..F
|
|
111
|
+
[0x30000, 0x3fffd], // CJK ext G..
|
|
112
|
+
];
|
|
113
|
+
const ZERO_WIDTH_RE = /^[\p{M}\p{Cc}\p{Cf}\u200b-\u200f\u2028-\u202e\u2060-\u2064\ufeff]$/u;
|
|
114
|
+
function isWide(cp) {
|
|
115
|
+
// Binary search over the sorted range table.
|
|
116
|
+
let lo = 0;
|
|
117
|
+
let hi = WIDE_RANGES.length - 1;
|
|
118
|
+
while (lo <= hi) {
|
|
119
|
+
const mid = (lo + hi) >> 1;
|
|
120
|
+
const [start, end] = WIDE_RANGES[mid];
|
|
121
|
+
if (cp < start)
|
|
122
|
+
hi = mid - 1;
|
|
123
|
+
else if (cp > end)
|
|
124
|
+
lo = mid + 1;
|
|
125
|
+
else
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
/** Cells for one code point on its own (no cluster context). */
|
|
131
|
+
export function codePointWidth(cp) {
|
|
132
|
+
if (cp === 0xfe0f)
|
|
133
|
+
return 0; // VS16 handled at cluster level
|
|
134
|
+
const ch = String.fromCodePoint(cp);
|
|
135
|
+
if (ZERO_WIDTH_RE.test(ch))
|
|
136
|
+
return 0;
|
|
137
|
+
if (cp >= 0xfe00 && cp <= 0xfe0f)
|
|
138
|
+
return 0; // variation selectors
|
|
139
|
+
if (cp >= 0xe0100 && cp <= 0xe01ef)
|
|
140
|
+
return 0; // VS supplement
|
|
141
|
+
if (cp >= 0xe0020 && cp <= 0xe007f)
|
|
142
|
+
return 0; // emoji tag sequence
|
|
143
|
+
if (cp >= 0xd800 && cp <= 0xdfff)
|
|
144
|
+
return 1; // lone surrogate (broken slice)
|
|
145
|
+
return isWide(cp) ? 2 : 1;
|
|
146
|
+
}
|
|
147
|
+
let segmenter;
|
|
148
|
+
/**
|
|
149
|
+
* Grapheme clusters of `text`, yielded lazily so an early exit (e.g.
|
|
150
|
+
* `sliceVisible` cutting a cell window) never segments the whole tail of a
|
|
151
|
+
* long buffer. Falls back to code-point iteration without Intl.Segmenter.
|
|
152
|
+
*/
|
|
153
|
+
function* graphemes(text) {
|
|
154
|
+
if (segmenter === undefined) {
|
|
155
|
+
try {
|
|
156
|
+
segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
segmenter = null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (segmenter) {
|
|
163
|
+
for (const seg of segmenter.segment(text))
|
|
164
|
+
yield seg.segment;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
for (const ch of text)
|
|
168
|
+
yield ch;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Cells one grapheme cluster occupies. A cluster is as wide as its widest
|
|
172
|
+
* base (a ZWJ family emoji is 2, not 6); VS16 promotes a text-style symbol
|
|
173
|
+
* to emoji presentation (2 cells); a cluster of nothing but marks is 0.
|
|
174
|
+
*/
|
|
175
|
+
export function graphemeWidth(cluster) {
|
|
176
|
+
let width = 0;
|
|
177
|
+
let vs16 = false;
|
|
178
|
+
for (const ch of cluster) {
|
|
179
|
+
const cp = ch.codePointAt(0);
|
|
180
|
+
if (cp === 0xfe0f) {
|
|
181
|
+
vs16 = true;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
width = Math.max(width, codePointWidth(cp));
|
|
185
|
+
}
|
|
186
|
+
if (vs16 && width === 1)
|
|
187
|
+
return 2;
|
|
188
|
+
return width;
|
|
189
|
+
}
|
|
190
|
+
/** Terminal cells a string occupies, ignoring SGR colour codes. */
|
|
191
|
+
export function visibleWidth(str) {
|
|
192
|
+
const plain = stripSgr(str);
|
|
193
|
+
if (!plain)
|
|
194
|
+
return 0;
|
|
195
|
+
let total = 0;
|
|
196
|
+
for (const cluster of graphemes(plain))
|
|
197
|
+
total += graphemeWidth(cluster);
|
|
198
|
+
return total;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Take the cells `[start, start + cells)` of `str`, keeping every SGR code
|
|
202
|
+
* (so colours survive) and substituting spaces for a wide glyph cut by
|
|
203
|
+
* either edge, so the result's geometry is exactly what the caller asked
|
|
204
|
+
* for even when a CJK character straddles the window.
|
|
205
|
+
*/
|
|
206
|
+
export function sliceVisible(str, start, cells) {
|
|
207
|
+
if (cells <= 0)
|
|
208
|
+
return "";
|
|
209
|
+
const end = start + cells;
|
|
210
|
+
let out = "";
|
|
211
|
+
let offset = 0;
|
|
212
|
+
const parts = str.split(SGR_SPLIT);
|
|
213
|
+
for (let i = 0; i < parts.length; i++) {
|
|
214
|
+
const part = parts[i];
|
|
215
|
+
if (!part)
|
|
216
|
+
continue;
|
|
217
|
+
if (i % 2 === 1) {
|
|
218
|
+
out += part;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
// Offsets only grow, so once we're past the window no later content can
|
|
222
|
+
// land in it; stop segmenting the tail of a long buffer.
|
|
223
|
+
if (offset > end)
|
|
224
|
+
continue;
|
|
225
|
+
for (const cluster of graphemes(part)) {
|
|
226
|
+
if (offset > end)
|
|
227
|
+
break;
|
|
228
|
+
const w = graphemeWidth(cluster);
|
|
229
|
+
const from = offset;
|
|
230
|
+
const to = offset + w;
|
|
231
|
+
offset = to;
|
|
232
|
+
if (w === 0) {
|
|
233
|
+
// Marks attach to the previous glyph; keep them only when that glyph
|
|
234
|
+
// was emitted.
|
|
235
|
+
if (from > start && from <= end)
|
|
236
|
+
out += cluster;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (to <= start || from >= end)
|
|
240
|
+
continue;
|
|
241
|
+
if (from >= start && to <= end) {
|
|
242
|
+
out += cluster;
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
out += " ".repeat(Math.min(to, end) - Math.max(from, start));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
/** Pad or truncate to an exact cell width (ANSI-safe). */
|
|
252
|
+
export function padVisible(line, width) {
|
|
253
|
+
const target = Math.max(0, width);
|
|
254
|
+
const w = visibleWidth(line);
|
|
255
|
+
if (w === target)
|
|
256
|
+
return line;
|
|
257
|
+
if (w < target)
|
|
258
|
+
return `${line}${" ".repeat(target - w)}`;
|
|
259
|
+
const cut = sliceVisible(line, 0, target);
|
|
260
|
+
const cutWidth = visibleWidth(cut);
|
|
261
|
+
return cutWidth < target ? `${cut}${" ".repeat(target - cutWidth)}` : cut;
|
|
262
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mergestorm",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.15",
|
|
4
4
|
"description": "Mergestorm CLI (mg) — local reviews & stacked PRs. MIT; build from source: https://github.com/marginsystems/mergestorm-cli",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
"mergestorm": "./dist/cli.js",
|
|
19
19
|
"mg": "./dist/cli.js"
|
|
20
20
|
},
|
|
21
|
+
"exports": {
|
|
22
|
+
".": "./dist/cli.js",
|
|
23
|
+
"./client": "./dist/client/index.js"
|
|
24
|
+
},
|
|
21
25
|
"homepage": "https://mergestorm.ai/docs#review-jobs-api",
|
|
22
26
|
"repository": {
|
|
23
27
|
"type": "git",
|
|
@@ -28,6 +32,7 @@
|
|
|
28
32
|
},
|
|
29
33
|
"files": [
|
|
30
34
|
"dist",
|
|
35
|
+
"skill",
|
|
31
36
|
"LICENSE",
|
|
32
37
|
"README.md"
|
|
33
38
|
],
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mergestorm-review
|
|
3
|
+
description: Review a local branch with Mergestorm Vortex specialists before opening a PR, then apply the findings yourself.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Mergestorm review
|
|
7
|
+
|
|
8
|
+
Use this skill to review a local git branch with Mergestorm, then apply the findings yourself.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
|
|
12
|
+
Before opening a PR; after a large refactor; when the user says review, audit, second opinion, or "is this safe to merge".
|
|
13
|
+
|
|
14
|
+
Not for merging, landing, or pushing. For a PR that is already open on GitHub, use the `mergestorm-pr-loop` skill instead.
|
|
15
|
+
|
|
16
|
+
## Install (Claude Code / Cursor)
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
mg skill install --claude
|
|
20
|
+
mg skill install --cursor
|
|
21
|
+
# or both: mg skill install --claude --cursor
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
That writes `.claude/skills/mergestorm-review/SKILL.md` and/or `.cursor/skills/mergestorm-review/SKILL.md` in the current repo.
|
|
25
|
+
|
|
26
|
+
Hand-copy still works: Claude Code project skill `.claude/skills/mergestorm-review/SKILL.md` (or `~/.claude/skills/mergestorm-review/SKILL.md`). Cursor: the same file under `.cursor/skills/mergestorm-review/SKILL.md`; otherwise a rule at `.cursor/rules/mergestorm-review.mdc` with this description and `alwaysApply: false`.
|
|
27
|
+
|
|
28
|
+
MCP server:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
curl -fsSL https://mergestorm.ai/install.sh | bash -s -- --mcp
|
|
32
|
+
# or: npm i -g mergestorm mergestorm-mcp
|
|
33
|
+
claude mcp add mergestorm -- npx -y mergestorm-mcp
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Cursor `~/.cursor/mcp.json`:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"mcpServers": {
|
|
41
|
+
"mergestorm": {
|
|
42
|
+
"command": "npx",
|
|
43
|
+
"args": ["-y", "mergestorm-mcp"]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Preconditions
|
|
50
|
+
|
|
51
|
+
1. Prefer the `mergestorm` MCP server. Call `whoami`. If MCP is missing, require `mg` on PATH with `mg whoami --json` exit 0. If neither is available, stop and print the install one-liner above.
|
|
52
|
+
2. Call `credits` (or `mg credits --json`). If remaining standard credits are 0, say so and stop.
|
|
53
|
+
|
|
54
|
+
## Hire policy
|
|
55
|
+
|
|
56
|
+
Default: omit `router` (account setting).
|
|
57
|
+
|
|
58
|
+
- `router: "off"` for tiny diffs or docs-only
|
|
59
|
+
- `manual` with pins when the change is clearly one domain: `security` (auth/crypto/input), `frontend` (components), `data` (migrations), `api` (route contracts), `tests` (mostly tests), `performance` (hot paths), `architecture` (cross-module moves)
|
|
60
|
+
- `max` only when the user asks for a full pass
|
|
61
|
+
- Never request `governance` or `seam`. Neither exists for local reviews. Seam runs automatically during Mergestorm stack promotes and is controlled by the Agents settings toggle (default on).
|
|
62
|
+
|
|
63
|
+
## Submit
|
|
64
|
+
|
|
65
|
+
Call `review_submit` on the repo cwd (git collect when `diff` is omitted). Pass one or two sentences of intent in `context`. Pass an ADR or spec as `context_files` when one exists in the repo under review — never from outside it. Do not paste the diff into context.
|
|
66
|
+
|
|
67
|
+
Optional: `thread`, `idempotency_key`, `wait`, `timeout_s`. Default wait is 300s; on timeout the tool returns `status: in_progress` and the job id — then call `review_wait`.
|
|
68
|
+
|
|
69
|
+
Use `review_get` or `review_list` to resume a known job.
|
|
70
|
+
|
|
71
|
+
Status handling:
|
|
72
|
+
|
|
73
|
+
- status is `no_changes`: success. The branch matches the selected base, no review job was submitted, and no review credit was used. Stop unless the user expected a different base.
|
|
74
|
+
- status is `rate_limited`: wait `retry_after_seconds`. Use `review_list`, `review_get`, or `review_wait` for an existing in-flight job before resubmitting. Do not immediately resubmit the same diff.
|
|
75
|
+
|
|
76
|
+
## Apply findings
|
|
77
|
+
|
|
78
|
+
Read `findings.inline` (and `off_diff` / `offDiff`) sorted by severity. Fix or explain each. Never silence a finding by deleting the test. Then submit again on the same branch so the thread chains, with `context` stating what was addressed.
|
|
79
|
+
|
|
80
|
+
## Never
|
|
81
|
+
|
|
82
|
+
- land, merge, push, or open a PR
|
|
83
|
+
- call stack authoring or land commands
|
|
84
|
+
- wrap the CLI in a shell escape hatch
|
|
85
|
+
- claim a finding is fixed without a diff that touches its path
|
|
86
|
+
- resubmit an identical failing diff (that burns a credit)
|
|
87
|
+
- treat `no_changes` or `rate_limited` as a failed review that needs an immediate resubmit
|
|
88
|
+
- pass `--context-file` or `--context` contents from outside the repo under review
|
|
89
|
+
|
|
90
|
+
## Fallback (no MCP)
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
mg review --json --context "…"
|
|
94
|
+
# optional: --router … --specialists … --context-file … --thread … --wait / --no-wait --timeout
|
|
95
|
+
# context files must live inside the repo under review
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Parse stdout as one JSON object (`mergestorm.review_job/v1`). Exit 0 includes `no_changes`. Exit 5 means detach and poll with `mg status <id> --json --wait`. Exit 7 means `rate_limited`: wait `retry_after_seconds` before trying again.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mergestorm-pr-loop
|
|
3
|
+
description: Patch an already-open GitHub PR from Mergestorm Vortex findings until it comes back clean. Wait for the PR's Vortex pass, fix, push the PR head, wait for the pass at the pushed SHA. Not for local pre-PR reviews (use mergestorm-review for those).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Mergestorm PR loop
|
|
7
|
+
|
|
8
|
+
Vortex already reviews every push to an open PR on a monitored repo; nothing here starts a review. This skill reads the latest pass, patches the PR head branch, pushes, and waits for the follow-up pass.
|
|
9
|
+
|
|
10
|
+
Two worlds, never mixed: reviewing a local branch before a PR exists is the `mergestorm-review` skill. This skill is only for a PR that is already open on GitHub.
|
|
11
|
+
|
|
12
|
+
## Install (Claude Code / Cursor)
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
mg skill install --claude --cursor
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Preconditions (in order)
|
|
19
|
+
|
|
20
|
+
1. There is an already-open GitHub PR on a Vortex-monitored repo. No PR, wrong skill.
|
|
21
|
+
2. Auto-patch (Cyclone) must be off so you are the only patcher. Do not take anyone's word for it: the settings API is the source of truth, and step 1 of the loop verifies it before anything else runs.
|
|
22
|
+
3. `whoami` works on the `mergestorm` MCP server (or `mg whoami --json` exits 0). Otherwise stop and ask for install/login.
|
|
23
|
+
|
|
24
|
+
## Loop (in order)
|
|
25
|
+
|
|
26
|
+
1. Call `settings_get` and read `auto_patch_enabled`.
|
|
27
|
+
- If `settings_get` errors or its result has no `auto_patch_enabled`: refuse the loop. Do not wait, patch, or push; tell the human that Cyclone auto-patch could not be verified.
|
|
28
|
+
- If it is `false`: continue. You are the only patcher.
|
|
29
|
+
- If it is `true` and the human asked you to turn Cyclone off: call `settings_set` with `auto_patch_enabled: false`, then call `settings_get` again and confirm it now reads `false`.
|
|
30
|
+
- If it is `true` otherwise: refuse the loop. Do not wait, patch, or push; tell the human Cyclone auto-patch is still on and where to turn it off.
|
|
31
|
+
- Never set `auto_patch_enabled` to `true`.
|
|
32
|
+
2. Call `review_wait_pr` with the PR's owner, repo, and pr_number (`review_get_pr` works for a first look at the resting state).
|
|
33
|
+
3. Confirm the envelope's `head_sha` is the commit you have checked out as the PR head (compare its leading 7+ hex characters with `git rev-parse HEAD`). If it is not, someone else pushed after this pass and its findings are not yours to patch: do not edit. Fetch and check out the PR head, then go to step 7 with `after_sha` set to that head.
|
|
34
|
+
4. Read `findings.inline` and `findings.offDiff` (or `off_diff`) sorted by severity. Fix or explain each.
|
|
35
|
+
5. Patch on the PR head branch only.
|
|
36
|
+
6. Push that head branch with an ordinary push. If the push is rejected because the remote branch moved, another patcher already pushed for this pass. Do not rebase, do not force, do not retry the push: stop and tell the human that a second patcher is active on this PR.
|
|
37
|
+
7. Call `review_wait_pr` again with `after_sha` set to the SHA you just pushed (7+ hex). An envelope whose `head_sha` is an older commit is a previous pass, not yours; keep waiting.
|
|
38
|
+
8. Repeat from step 3 until a stop rule fires.
|
|
39
|
+
|
|
40
|
+
## Stop rules
|
|
41
|
+
|
|
42
|
+
Map these to `mergestorm.pr_review/v1` envelope fields; do not invent statuses.
|
|
43
|
+
|
|
44
|
+
- `finding_count` is 0 (empty inline and offDiff): stop. Tell the human the PR is clear and that Auto land owns landing. Never merge it yourself.
|
|
45
|
+
- status is `rate_limited`, or `patch_policy.mode` is `"hold"`: surface the findings to the human and stop. Do not patch.
|
|
46
|
+
- status is `failed`: report to the human. Do not keep pushing to retrigger.
|
|
47
|
+
- status is `in_progress` and the envelope has a `head_sha`: the pass is still running; call `review_wait_pr` again with the same `after_sha`.
|
|
48
|
+
- status is `in_progress` and the envelope has no `head_sha`: the wait timed out before Vortex wrote a pass for your SHA. Call `review_wait_pr` once more with the same `after_sha`. If it comes back the same way a second time in a row, stop and tell the human that Vortex did not pick up the push. Do not push again to retrigger.
|
|
49
|
+
- status is `skipped`, `stopped`, `trial_expired`, or `synced`: surface the status and reason to the human and stop. Do not patch or push.
|
|
50
|
+
|
|
51
|
+
## Never
|
|
52
|
+
|
|
53
|
+
- submit a local review job, or call the job-id tools `review_wait` / `review_get` here. Local jobs are `mergestorm-review`'s world: they cost credits and do not fire Vortex on the PR.
|
|
54
|
+
- land, merge, enqueue, or touch stack authoring or the merge queue
|
|
55
|
+
- turn `auto_patch_enabled` on, from this skill, for any reason
|
|
56
|
+
- push any branch other than the PR head
|
|
57
|
+
- force-push, or rebase and retry a push that was rejected because the remote moved
|
|
58
|
+
- patch a pass whose `head_sha` is not the commit you have checked out
|
|
59
|
+
- wrap the CLI in a shell escape hatch
|
|
60
|
+
- claim a finding is fixed without a diff that touches its path
|
|
61
|
+
|
|
62
|
+
## Fallback (no MCP)
|
|
63
|
+
|
|
64
|
+
Verify auto-patch with the CLI instead of `settings_get`:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
mg settings --json
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Read `auto_patch_enabled` from stdout. If the command errors or the result has no `auto_patch_enabled`, refuse the loop and tell the human that Cyclone auto-patch could not be verified. If it is `true` and the human asked you to turn Cyclone off, run `mg settings --auto-patch off` and re-read; otherwise refuse the loop, same rule as step 1.
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
mg pr <owner/repo>#<n> --json --wait --after-sha <sha> --timeout <s>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Parse stdout as one `mergestorm.pr_review/v1` object. Same fields, same stop rules.
|