mergestorm 0.1.1 → 0.3.0

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.
Files changed (45) hide show
  1. package/README.md +64 -8
  2. package/dist/api.d.ts +85 -0
  3. package/dist/api.js +85 -0
  4. package/dist/cli.js +72 -353
  5. package/dist/commands/branches.d.ts +10 -0
  6. package/dist/commands/branches.js +85 -0
  7. package/dist/commands/credits.d.ts +1 -0
  8. package/dist/commands/credits.js +28 -0
  9. package/dist/commands/jobs.d.ts +6 -0
  10. package/dist/commands/jobs.js +107 -0
  11. package/dist/commands/login.d.ts +2 -0
  12. package/dist/commands/login.js +103 -0
  13. package/dist/commands/logout.d.ts +1 -0
  14. package/dist/commands/logout.js +11 -0
  15. package/dist/commands/review.d.ts +11 -0
  16. package/dist/commands/review.js +129 -0
  17. package/dist/commands/status.d.ts +5 -0
  18. package/dist/commands/status.js +39 -0
  19. package/dist/commands/whoami.d.ts +1 -0
  20. package/dist/commands/whoami.js +35 -0
  21. package/dist/config.d.ts +13 -0
  22. package/dist/config.js +41 -0
  23. package/dist/errors.d.ts +10 -0
  24. package/dist/errors.js +18 -0
  25. package/dist/git.d.ts +9 -0
  26. package/dist/git.js +66 -0
  27. package/dist/shell.d.ts +8 -0
  28. package/dist/shell.js +183 -0
  29. package/dist/ui/ansi.d.ts +12 -0
  30. package/dist/ui/ansi.js +27 -0
  31. package/dist/ui/banner.d.ts +4 -0
  32. package/dist/ui/banner.js +98 -0
  33. package/dist/ui/box.d.ts +11 -0
  34. package/dist/ui/box.js +36 -0
  35. package/dist/ui/logo.d.ts +5 -0
  36. package/dist/ui/logo.js +12 -0
  37. package/dist/ui/prompt.d.ts +18 -0
  38. package/dist/ui/prompt.js +220 -0
  39. package/dist/ui/select.d.ts +13 -0
  40. package/dist/ui/select.js +103 -0
  41. package/dist/ui/usage.d.ts +24 -0
  42. package/dist/ui/usage.js +67 -0
  43. package/dist/version.d.ts +1 -0
  44. package/dist/version.js +11 -0
  45. package/package.json +5 -4
@@ -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 — Claude-length). */
3
+ width?: number;
4
+ /** When false, return plain text with no ANSI (default: respect ansi.enabled). */
5
+ color?: boolean;
6
+ };
7
+ /**
8
+ * Claude-style 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
+ * Claude-style 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;
@@ -0,0 +1,67 @@
1
+ import { ansi } from "./ansi.js";
2
+ /**
3
+ * Claude-style 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 — matches Claude's quiet meter.
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
+ * Claude-style 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
+ }
@@ -0,0 +1 @@
1
+ export declare function cliVersion(): string;
@@ -0,0 +1,11 @@
1
+ import { createRequire } from "node:module";
2
+ const require = createRequire(import.meta.url);
3
+ export function cliVersion() {
4
+ try {
5
+ const pkg = require("../package.json");
6
+ return pkg.version ?? "0.0.0";
7
+ }
8
+ catch {
9
+ return "0.0.0";
10
+ }
11
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mergestorm",
3
- "version": "0.1.1",
4
- "description": "MergeStorm CLI — review local diffs via the Review Jobs API",
3
+ "version": "0.3.0",
4
+ "description": "MergeStorm CLI — interactive shell and one-shot review commands",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "mergestorm": "./dist/cli.js"
@@ -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"