mergestorm 0.2.0 → 0.3.1
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 +43 -10
- package/dist/api.d.ts +41 -4
- package/dist/api.js +14 -0
- package/dist/cli.js +8 -1
- package/dist/commands/branches.d.ts +10 -0
- package/dist/commands/branches.js +85 -0
- package/dist/commands/credits.js +10 -4
- package/dist/commands/jobs.d.ts +4 -0
- package/dist/commands/jobs.js +43 -5
- package/dist/commands/login.js +2 -0
- package/dist/commands/review.d.ts +5 -0
- package/dist/commands/review.js +22 -2
- package/dist/shell.d.ts +7 -0
- package/dist/shell.js +65 -62
- package/dist/ui/ansi.d.ts +4 -1
- package/dist/ui/ansi.js +16 -9
- package/dist/ui/banner.d.ts +1 -0
- package/dist/ui/banner.js +84 -14
- package/dist/ui/box.d.ts +31 -0
- package/dist/ui/box.js +99 -0
- package/dist/ui/ctrl-c-exit.d.ts +13 -0
- package/dist/ui/ctrl-c-exit.js +22 -0
- package/dist/ui/logo.d.ts +5 -0
- package/dist/ui/logo.js +12 -0
- package/dist/ui/prompt.d.ts +18 -0
- package/dist/ui/prompt.js +237 -0
- package/dist/ui/select.d.ts +13 -0
- package/dist/ui/select.js +103 -0
- package/dist/ui/usage.d.ts +24 -0
- package/dist/ui/usage.js +67 -0
- package/package.json +4 -3
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import * as readline from "node:readline";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
4
|
+
import { ansi } from "./ansi.js";
|
|
5
|
+
import { frameWidth, roundedBox, visibleWidth } from "./box.js";
|
|
6
|
+
import { CTRL_C_EXIT_HINT, CtrlCExitGate } from "./ctrl-c-exit.js";
|
|
7
|
+
/** Thrown when the user asks to close the prompt (Ctrl+D empty, or Ctrl+C twice). */
|
|
8
|
+
export class PromptClosedError extends Error {
|
|
9
|
+
constructor() {
|
|
10
|
+
super("prompt closed");
|
|
11
|
+
this.name = "PromptClosedError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const up = (n) => `[${n}A`;
|
|
15
|
+
const down = (n) => `[${n}B`;
|
|
16
|
+
const col = (n) => `[${n}G`;
|
|
17
|
+
const clearDown = "[0J";
|
|
18
|
+
async function fallbackAskLine(promptLabel) {
|
|
19
|
+
const rl = createInterface({ input, output, terminal: false });
|
|
20
|
+
try {
|
|
21
|
+
return await rl.question(promptLabel ? `${promptLabel}> ` : "> ");
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
rl.close();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Hand-rolled raw-mode line editor: bordered input box, history, and a live
|
|
29
|
+
* slash-command dropdown. Falls back to plain readline on a non-TTY stream.
|
|
30
|
+
*/
|
|
31
|
+
export async function askLine(opts) {
|
|
32
|
+
const commands = opts.commands ?? [];
|
|
33
|
+
const history = opts.history ?? [];
|
|
34
|
+
if (!input.isTTY || !output.isTTY) {
|
|
35
|
+
return fallbackAskLine(opts.prompt);
|
|
36
|
+
}
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
let buffer = "";
|
|
39
|
+
let cursor = 0;
|
|
40
|
+
let historyIndex = history.length;
|
|
41
|
+
let dropdownIndex = 0;
|
|
42
|
+
let linesDrawn = 0;
|
|
43
|
+
let finished = false;
|
|
44
|
+
const ctrlCExit = new CtrlCExitGate();
|
|
45
|
+
const wasRaw = Boolean(input.isRaw);
|
|
46
|
+
readline.emitKeypressEvents(input);
|
|
47
|
+
input.setRawMode(true);
|
|
48
|
+
input.resume();
|
|
49
|
+
// Row (0-based) of the "› buffer" content line among the rows we draw each frame.
|
|
50
|
+
const inputRow = (opts.prompt ? 1 : 0) + 1;
|
|
51
|
+
function currentDropdown() {
|
|
52
|
+
if (!buffer.startsWith("/"))
|
|
53
|
+
return [];
|
|
54
|
+
const term = buffer.slice(1).toLowerCase();
|
|
55
|
+
return commands.filter((c) => c.name.toLowerCase().startsWith(term));
|
|
56
|
+
}
|
|
57
|
+
function render() {
|
|
58
|
+
if (linesDrawn > 0) {
|
|
59
|
+
output.write(`${up(inputRow)}${clearDown}`);
|
|
60
|
+
}
|
|
61
|
+
const rows = [];
|
|
62
|
+
if (opts.prompt)
|
|
63
|
+
rows.push(ansi.dim(opts.prompt));
|
|
64
|
+
// Full terminal width input frame, not content-sized.
|
|
65
|
+
rows.push(...roundedBox([`${ansi.green("›")} ${buffer}`], {
|
|
66
|
+
padding: 1,
|
|
67
|
+
width: frameWidth(),
|
|
68
|
+
}));
|
|
69
|
+
const dropdown = currentDropdown();
|
|
70
|
+
if (dropdown.length) {
|
|
71
|
+
if (dropdownIndex >= dropdown.length)
|
|
72
|
+
dropdownIndex = dropdown.length - 1;
|
|
73
|
+
dropdown.forEach((c, i) => {
|
|
74
|
+
const label = ` /${c.name.padEnd(12)}${c.summary}`;
|
|
75
|
+
rows.push(i === dropdownIndex ? ansi.invert(label) : ansi.gray(label));
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
dropdownIndex = 0;
|
|
80
|
+
}
|
|
81
|
+
output.write(rows.join("\r\n") + "\r\n");
|
|
82
|
+
linesDrawn = rows.length;
|
|
83
|
+
const prefixWidth = visibleWidth(`${ansi.green("›")} `);
|
|
84
|
+
const targetCol = 1 + 1 + prefixWidth + cursor + 1;
|
|
85
|
+
output.write(`${up(linesDrawn - inputRow)}${col(targetCol)}`);
|
|
86
|
+
}
|
|
87
|
+
function cleanup() {
|
|
88
|
+
input.removeListener("keypress", onKeypress);
|
|
89
|
+
input.setRawMode(wasRaw);
|
|
90
|
+
// emitKeypressEvents leaves stdin flowing; pause it so an idle shell
|
|
91
|
+
// (no command running) doesn't keep the process alive after exit/EOF.
|
|
92
|
+
input.pause();
|
|
93
|
+
}
|
|
94
|
+
function finish(value, err) {
|
|
95
|
+
if (finished)
|
|
96
|
+
return;
|
|
97
|
+
finished = true;
|
|
98
|
+
cleanup();
|
|
99
|
+
const rowsBelow = Math.max(0, linesDrawn - inputRow - 1);
|
|
100
|
+
if (rowsBelow > 0)
|
|
101
|
+
output.write(down(rowsBelow));
|
|
102
|
+
output.write("\n");
|
|
103
|
+
if (err)
|
|
104
|
+
reject(err);
|
|
105
|
+
else
|
|
106
|
+
resolve(value ?? "");
|
|
107
|
+
}
|
|
108
|
+
function acceptDropdown(dropdown) {
|
|
109
|
+
buffer = `/${dropdown[dropdownIndex].name} `;
|
|
110
|
+
cursor = buffer.length;
|
|
111
|
+
dropdownIndex = 0;
|
|
112
|
+
}
|
|
113
|
+
function onKeypress(str, key) {
|
|
114
|
+
try {
|
|
115
|
+
if (finished || !key)
|
|
116
|
+
return;
|
|
117
|
+
if (!(key.ctrl && key.name === "c"))
|
|
118
|
+
ctrlCExit.reset();
|
|
119
|
+
const dropdown = currentDropdown();
|
|
120
|
+
if (key.ctrl && key.name === "c") {
|
|
121
|
+
if (ctrlCExit.press()) {
|
|
122
|
+
finish(null, new PromptClosedError());
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
buffer = "";
|
|
126
|
+
cursor = 0;
|
|
127
|
+
historyIndex = history.length;
|
|
128
|
+
const rowsBelow = Math.max(0, linesDrawn - inputRow - 1);
|
|
129
|
+
if (rowsBelow > 0)
|
|
130
|
+
output.write(down(rowsBelow));
|
|
131
|
+
output.write(`\n${ansi.dim(CTRL_C_EXIT_HINT)}\n`);
|
|
132
|
+
linesDrawn = 0;
|
|
133
|
+
render();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (key.ctrl && key.name === "d") {
|
|
137
|
+
if (!buffer)
|
|
138
|
+
finish(null, new PromptClosedError());
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (key.name === "return") {
|
|
142
|
+
if (dropdown.length)
|
|
143
|
+
buffer = `/${dropdown[dropdownIndex].name}`;
|
|
144
|
+
finish(buffer);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (key.name === "tab") {
|
|
148
|
+
if (dropdown.length)
|
|
149
|
+
acceptDropdown(dropdown);
|
|
150
|
+
render();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (key.name === "right") {
|
|
154
|
+
if (dropdown.length)
|
|
155
|
+
acceptDropdown(dropdown);
|
|
156
|
+
else
|
|
157
|
+
cursor = Math.min(buffer.length, cursor + 1);
|
|
158
|
+
render();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (key.name === "left") {
|
|
162
|
+
cursor = Math.max(0, cursor - 1);
|
|
163
|
+
render();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (key.name === "up") {
|
|
167
|
+
if (dropdown.length) {
|
|
168
|
+
dropdownIndex = (dropdownIndex - 1 + dropdown.length) % dropdown.length;
|
|
169
|
+
}
|
|
170
|
+
else if (history.length) {
|
|
171
|
+
historyIndex = Math.max(0, historyIndex - 1);
|
|
172
|
+
buffer = history[historyIndex] ?? "";
|
|
173
|
+
cursor = buffer.length;
|
|
174
|
+
}
|
|
175
|
+
render();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (key.name === "down") {
|
|
179
|
+
if (dropdown.length) {
|
|
180
|
+
dropdownIndex = (dropdownIndex + 1) % dropdown.length;
|
|
181
|
+
}
|
|
182
|
+
else if (history.length) {
|
|
183
|
+
historyIndex = Math.min(history.length, historyIndex + 1);
|
|
184
|
+
buffer = historyIndex >= history.length ? "" : history[historyIndex] ?? "";
|
|
185
|
+
cursor = buffer.length;
|
|
186
|
+
}
|
|
187
|
+
render();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (key.name === "home" || (key.ctrl && key.name === "a")) {
|
|
191
|
+
cursor = 0;
|
|
192
|
+
render();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (key.name === "end" || (key.ctrl && key.name === "e")) {
|
|
196
|
+
cursor = buffer.length;
|
|
197
|
+
render();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (key.ctrl && key.name === "u") {
|
|
201
|
+
buffer = buffer.slice(cursor);
|
|
202
|
+
cursor = 0;
|
|
203
|
+
render();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (key.ctrl && key.name === "k") {
|
|
207
|
+
buffer = buffer.slice(0, cursor);
|
|
208
|
+
render();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (key.name === "backspace") {
|
|
212
|
+
if (cursor > 0) {
|
|
213
|
+
buffer = buffer.slice(0, cursor - 1) + buffer.slice(cursor);
|
|
214
|
+
cursor -= 1;
|
|
215
|
+
}
|
|
216
|
+
render();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (key.name === "delete") {
|
|
220
|
+
buffer = buffer.slice(0, cursor) + buffer.slice(cursor + 1);
|
|
221
|
+
render();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (str && !key.ctrl && !key.meta) {
|
|
225
|
+
buffer = buffer.slice(0, cursor) + str + buffer.slice(cursor);
|
|
226
|
+
cursor += str.length;
|
|
227
|
+
render();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
finish(null, err);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
input.on("keypress", onKeypress);
|
|
235
|
+
render();
|
|
236
|
+
});
|
|
237
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type SelectOptions<T> = {
|
|
2
|
+
title?: string;
|
|
3
|
+
items: T[];
|
|
4
|
+
render: (item: T, index: number) => string;
|
|
5
|
+
/** Hint printed under the list (default: arrow/enter/esc keys). */
|
|
6
|
+
hint?: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Raw-mode arrow-key list selector (Claude-style). Returns the chosen item, or
|
|
10
|
+
* `null` if cancelled / empty. Non-TTY: prints a numbered list and returns null
|
|
11
|
+
* (caller should fall back to a table / prompt).
|
|
12
|
+
*/
|
|
13
|
+
export declare function selectFromList<T>(opts: SelectOptions<T>): Promise<T | null>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import * as readline from "node:readline";
|
|
2
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
import { ansi } from "./ansi.js";
|
|
4
|
+
const up = (n) => `\u001b[${n}A`;
|
|
5
|
+
const clearDown = "\u001b[0J";
|
|
6
|
+
/**
|
|
7
|
+
* Raw-mode arrow-key list selector (Claude-style). Returns the chosen item, or
|
|
8
|
+
* `null` if cancelled / empty. Non-TTY: prints a numbered list and returns null
|
|
9
|
+
* (caller should fall back to a table / prompt).
|
|
10
|
+
*/
|
|
11
|
+
export async function selectFromList(opts) {
|
|
12
|
+
const items = opts.items;
|
|
13
|
+
if (items.length === 0)
|
|
14
|
+
return null;
|
|
15
|
+
if (!input.isTTY || !output.isTTY) {
|
|
16
|
+
if (opts.title)
|
|
17
|
+
console.log(ansi.bold(` ${opts.title}`));
|
|
18
|
+
items.forEach((item, i) => {
|
|
19
|
+
console.log(` ${String(i + 1).padStart(2)}. ${opts.render(item, i)}`);
|
|
20
|
+
});
|
|
21
|
+
console.log(ansi.dim(opts.hint ?? " (non-interactive — use `chain <slug>` or re-run on a TTY to select)"));
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
let index = 0;
|
|
26
|
+
let linesDrawn = 0;
|
|
27
|
+
let finished = false;
|
|
28
|
+
const wasRaw = Boolean(input.isRaw);
|
|
29
|
+
readline.emitKeypressEvents(input);
|
|
30
|
+
input.setRawMode(true);
|
|
31
|
+
input.resume();
|
|
32
|
+
function render() {
|
|
33
|
+
if (linesDrawn > 0) {
|
|
34
|
+
output.write(`${up(linesDrawn)}${clearDown}`);
|
|
35
|
+
}
|
|
36
|
+
const rows = [];
|
|
37
|
+
if (opts.title)
|
|
38
|
+
rows.push(ansi.bold(opts.title));
|
|
39
|
+
items.forEach((item, i) => {
|
|
40
|
+
const body = opts.render(item, i);
|
|
41
|
+
const marker = i === index ? ansi.green("›") : " ";
|
|
42
|
+
const line = ` ${marker} ${body}`;
|
|
43
|
+
rows.push(i === index ? ansi.invert(line) : line);
|
|
44
|
+
});
|
|
45
|
+
rows.push(ansi.dim(opts.hint ?? "↑↓ navigate · enter select · esc cancel"));
|
|
46
|
+
output.write(rows.join("\r\n") + "\r\n");
|
|
47
|
+
linesDrawn = rows.length;
|
|
48
|
+
}
|
|
49
|
+
function cleanup() {
|
|
50
|
+
input.removeListener("keypress", onKeypress);
|
|
51
|
+
input.setRawMode(wasRaw);
|
|
52
|
+
input.pause();
|
|
53
|
+
}
|
|
54
|
+
function finish(value) {
|
|
55
|
+
if (finished)
|
|
56
|
+
return;
|
|
57
|
+
finished = true;
|
|
58
|
+
cleanup();
|
|
59
|
+
output.write("\n");
|
|
60
|
+
resolve(value);
|
|
61
|
+
}
|
|
62
|
+
function onKeypress(_str, key) {
|
|
63
|
+
if (finished || !key)
|
|
64
|
+
return;
|
|
65
|
+
if (key.name === "up") {
|
|
66
|
+
index = (index - 1 + items.length) % items.length;
|
|
67
|
+
try {
|
|
68
|
+
render();
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
finish(null);
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (key.name === "down") {
|
|
76
|
+
index = (index + 1) % items.length;
|
|
77
|
+
try {
|
|
78
|
+
render();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
finish(null);
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (key.name === "return") {
|
|
86
|
+
finish(items[index]);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (key.name === "escape" ||
|
|
90
|
+
key.name === "q" ||
|
|
91
|
+
(key.ctrl && key.name === "c")) {
|
|
92
|
+
finish(null);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
input.on("keypress", onKeypress);
|
|
96
|
+
try {
|
|
97
|
+
render();
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
finish(null);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type UsageBarOpts = {
|
|
2
|
+
/** Bar width in cells (default 56). */
|
|
3
|
+
width?: number;
|
|
4
|
+
/** When false, return plain text with no ANSI (default: respect ansi.enabled). */
|
|
5
|
+
color?: boolean;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Usage meter — long thin bar + percent only:
|
|
9
|
+
* `[████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 7% used`
|
|
10
|
+
*
|
|
11
|
+
* No used/limit clutter. Edge cases:
|
|
12
|
+
* - `limit === 0` → dim "not included" (premium on non-Maelstrom)
|
|
13
|
+
* - `limit == null` → "unlimited" (dev-bypass)
|
|
14
|
+
*/
|
|
15
|
+
export declare function usageBar(used: number, limit: number | null | undefined, opts?: UsageBarOpts): string;
|
|
16
|
+
/** Start of the next UTC calendar month (matches API `resets_at`). */
|
|
17
|
+
export declare function nextUtcMonthStart(now?: Date): string;
|
|
18
|
+
/**
|
|
19
|
+
* Reset footer: `Resets Aug 1, 12:00am (UTC)`.
|
|
20
|
+
* Falls back to next UTC month start when `iso` is missing/invalid.
|
|
21
|
+
*/
|
|
22
|
+
export declare function formatResetLabel(iso: string | null | undefined, now?: Date): string;
|
|
23
|
+
/** Alias kept for callers that still import `formatResetDate`. */
|
|
24
|
+
export declare function formatResetDate(iso: string | null | undefined): string;
|
package/dist/ui/usage.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ansi } from "./ansi.js";
|
|
2
|
+
/**
|
|
3
|
+
* Usage meter — long thin bar + percent only:
|
|
4
|
+
* `[████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 7% used`
|
|
5
|
+
*
|
|
6
|
+
* No used/limit clutter. Edge cases:
|
|
7
|
+
* - `limit === 0` → dim "not included" (premium on non-Maelstrom)
|
|
8
|
+
* - `limit == null` → "unlimited" (dev-bypass)
|
|
9
|
+
*/
|
|
10
|
+
export function usageBar(used, limit, opts = {}) {
|
|
11
|
+
const width = Math.max(8, opts.width ?? 56);
|
|
12
|
+
const color = opts.color ?? ansi.enabled;
|
|
13
|
+
if (limit == null) {
|
|
14
|
+
return color ? ansi.dim("unlimited") : "unlimited";
|
|
15
|
+
}
|
|
16
|
+
if (limit === 0) {
|
|
17
|
+
return color ? ansi.dim("not included") : "not included";
|
|
18
|
+
}
|
|
19
|
+
const safeUsed = Math.max(0, Number.isFinite(used) ? used : 0);
|
|
20
|
+
const ratio = Math.min(1, safeUsed / limit);
|
|
21
|
+
const filled = Math.round(ratio * width);
|
|
22
|
+
const empty = Math.max(0, width - filled);
|
|
23
|
+
const pct = Math.min(100, Math.round((safeUsed / limit) * 100));
|
|
24
|
+
// Filled segment colored; empty + label stay dim.
|
|
25
|
+
const filledSeg = "█".repeat(filled);
|
|
26
|
+
const emptySeg = "░".repeat(empty);
|
|
27
|
+
const suffix = ` ${pct}% used`;
|
|
28
|
+
if (!color) {
|
|
29
|
+
return `[${filledSeg}${emptySeg}]${suffix}`;
|
|
30
|
+
}
|
|
31
|
+
let fillColor = ansi.green;
|
|
32
|
+
if (ratio >= 0.9)
|
|
33
|
+
fillColor = ansi.red;
|
|
34
|
+
else if (ratio >= 0.75)
|
|
35
|
+
fillColor = ansi.yellow;
|
|
36
|
+
return (`[${fillColor(filledSeg)}${ansi.dim(emptySeg)}]` + ansi.dim(suffix));
|
|
37
|
+
}
|
|
38
|
+
/** Start of the next UTC calendar month (matches API `resets_at`). */
|
|
39
|
+
export function nextUtcMonthStart(now = new Date()) {
|
|
40
|
+
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1)).toISOString();
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Reset footer: `Resets Aug 1, 12:00am (UTC)`.
|
|
44
|
+
* Falls back to next UTC month start when `iso` is missing/invalid.
|
|
45
|
+
*/
|
|
46
|
+
export function formatResetLabel(iso, now = new Date()) {
|
|
47
|
+
const d = iso ? new Date(iso) : new Date(nextUtcMonthStart(now));
|
|
48
|
+
const when = Number.isNaN(d.getTime()) ? new Date(nextUtcMonthStart(now)) : d;
|
|
49
|
+
const months = [
|
|
50
|
+
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
51
|
+
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
|
|
52
|
+
];
|
|
53
|
+
const month = months[when.getUTCMonth()];
|
|
54
|
+
const day = when.getUTCDate();
|
|
55
|
+
let h = when.getUTCHours();
|
|
56
|
+
const m = when.getUTCMinutes();
|
|
57
|
+
const ampm = h >= 12 ? "pm" : "am";
|
|
58
|
+
h = h % 12;
|
|
59
|
+
if (h === 0)
|
|
60
|
+
h = 12;
|
|
61
|
+
const mm = String(m).padStart(2, "0");
|
|
62
|
+
return `Resets ${month} ${day}, ${h}:${mm}${ampm} (UTC)`;
|
|
63
|
+
}
|
|
64
|
+
/** Alias kept for callers that still import `formatResetDate`. */
|
|
65
|
+
export function formatResetDate(iso) {
|
|
66
|
+
return formatResetLabel(iso);
|
|
67
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mergestorm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "MergeStorm CLI — interactive shell and one-shot review commands",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,10 +19,11 @@
|
|
|
19
19
|
"dist"
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
|
-
"build": "tsc -p tsconfig.json",
|
|
22
|
+
"build": "tsc -p tsconfig.build.json",
|
|
23
23
|
"dev": "tsx src/cli.ts",
|
|
24
24
|
"start": "node dist/cli.js",
|
|
25
|
-
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
25
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
26
|
+
"test": "tsx --test src/**/*.test.ts"
|
|
26
27
|
},
|
|
27
28
|
"engines": {
|
|
28
29
|
"node": ">=22.0.0"
|