mergestorm 0.3.24 → 0.3.26
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 +6 -6
- package/dist/client/index.d.ts +4 -3
- package/dist/client/index.js +4 -3
- package/dist/commands/queue.js +5 -1
- package/dist/commands/registry.js +1 -1
- package/dist/commands/stack.d.ts +8 -0
- package/dist/commands/stack.js +68 -7
- package/dist/shell.js +1 -1
- package/dist/stack-blockers.d.ts +54 -2
- package/dist/stack-blockers.js +139 -6
- package/dist/stack-dto.d.ts +17 -0
- package/dist/stack-dto.js +28 -0
- package/dist/stack-watch-obligation.d.ts +43 -0
- package/dist/stack-watch-obligation.js +79 -0
- package/dist/stack-watch.d.ts +8 -1
- package/dist/stack-watch.js +44 -23
- package/dist/ui/ansi.d.ts +14 -1
- package/dist/ui/ansi.js +25 -5
- package/dist/ui/banner.d.ts +2 -1
- package/dist/ui/banner.js +26 -47
- package/dist/ui/logo.d.ts +13 -8
- package/dist/ui/logo.js +25 -13
- package/dist/ui/prompt.d.ts +11 -1
- package/dist/ui/prompt.js +92 -9
- package/package.json +1 -1
- package/skill/SKILL.md +2 -2
- package/skill/mergestorm-bounce-watch/SKILL.md +8 -4
- package/skill/mergestorm-pr-loop/SKILL.md +3 -3
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { StackDto } from "./stack-dto.js";
|
|
2
|
+
export declare const STACK_WATCH_NOT_DONE_SENTENCE = "This stack is not landed. Your task is not done. Call stack_wait again with this cursor.";
|
|
3
|
+
export type StackWatchDoneReason = "landed" | "closed" | "archived" | "not_found";
|
|
4
|
+
export type StackWatchReason = StackWatchDoneReason | "open" | "attention" | "unread" | "rate_limited" | "failed";
|
|
5
|
+
export type StackWatchNextArgs = {
|
|
6
|
+
stack_id: string;
|
|
7
|
+
enrolled_head_sha?: string | null;
|
|
8
|
+
after_finished_at?: string | null;
|
|
9
|
+
bounce_id?: string | null;
|
|
10
|
+
timeout_s: 45;
|
|
11
|
+
};
|
|
12
|
+
export type StackWatchNext = {
|
|
13
|
+
tool: "stack_wait";
|
|
14
|
+
args: StackWatchNextArgs;
|
|
15
|
+
command: string;
|
|
16
|
+
};
|
|
17
|
+
export type StackWatchObligation = {
|
|
18
|
+
done: boolean;
|
|
19
|
+
until: "landed";
|
|
20
|
+
reason: StackWatchReason;
|
|
21
|
+
next: StackWatchNext | null;
|
|
22
|
+
message: string;
|
|
23
|
+
};
|
|
24
|
+
export type StackWatchObligationInput = {
|
|
25
|
+
stackId: string;
|
|
26
|
+
terminal: StackWatchDoneReason | null;
|
|
27
|
+
cursor?: {
|
|
28
|
+
enrolledHeadSha?: string | null;
|
|
29
|
+
afterFinishedAt?: string | null;
|
|
30
|
+
bounceId?: string | null;
|
|
31
|
+
} | null;
|
|
32
|
+
status?: string;
|
|
33
|
+
attention?: {
|
|
34
|
+
prNumber: number;
|
|
35
|
+
blocker: string;
|
|
36
|
+
} | null;
|
|
37
|
+
retryAfterSeconds?: number;
|
|
38
|
+
unread?: boolean;
|
|
39
|
+
freshCursor?: boolean;
|
|
40
|
+
};
|
|
41
|
+
export declare function stackTerminalReason(stack: Pick<StackDto, "archivedAt" | "layers" | "unit"> | null | undefined): StackWatchDoneReason | null;
|
|
42
|
+
export declare function stackWatchNextArgs(stackId: string, cursor: StackWatchObligationInput["cursor"]): StackWatchNextArgs;
|
|
43
|
+
export declare function stackWatchObligation(input: StackWatchObligationInput): StackWatchObligation;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export const STACK_WATCH_NOT_DONE_SENTENCE = "This stack is not landed. Your task is not done. Call stack_wait again with this cursor.";
|
|
2
|
+
const DONE_MESSAGES = {
|
|
3
|
+
landed: "This stack is landed. The watch is done; stop calling stack_wait for it.",
|
|
4
|
+
closed: "Every PR in this stack is merged or closed and nothing is left to land, but not every PR landed. The watch is done; tell the human which PRs were closed without landing.",
|
|
5
|
+
archived: "This stack is archived: the human took it back. The watch is done; stop calling stack_wait for it and do not push to its PRs.",
|
|
6
|
+
not_found: "No stack with this id exists for this account. Mergestorm deletes a stack once none of its layers is open, so a stack you were watching has most likely landed; a mistyped id or another account's stack reads the same. The watch is done; confirm each PR is merged (gh pr view <n> --json state) before reporting it landed.",
|
|
7
|
+
};
|
|
8
|
+
export function stackTerminalReason(stack) {
|
|
9
|
+
if (!stack)
|
|
10
|
+
return "not_found";
|
|
11
|
+
if (stack.archivedAt)
|
|
12
|
+
return "archived";
|
|
13
|
+
const layers = stack.layers;
|
|
14
|
+
if (layers.some((layer) => layer.state !== "merged" && layer.state !== "closed"))
|
|
15
|
+
return null;
|
|
16
|
+
const unit = stack.unit;
|
|
17
|
+
if (layers.length === 0 && !unit)
|
|
18
|
+
return null;
|
|
19
|
+
const landPr = unit?.landPr;
|
|
20
|
+
if (landPr && landPr.prNumber > 0 && landPr.state !== "merged" && landPr.state !== "closed")
|
|
21
|
+
return null;
|
|
22
|
+
if (unit && unit.state !== "landed" && unit.state !== "abandoned")
|
|
23
|
+
return null;
|
|
24
|
+
const everyLayerMerged = layers.every((layer) => layer.state === "merged");
|
|
25
|
+
return everyLayerMerged && (!unit || unit.state === "landed") ? "landed" : "closed";
|
|
26
|
+
}
|
|
27
|
+
export function stackWatchNextArgs(stackId, cursor) {
|
|
28
|
+
return {
|
|
29
|
+
stack_id: stackId,
|
|
30
|
+
...(cursor && cursor.enrolledHeadSha !== undefined ? { enrolled_head_sha: cursor.enrolledHeadSha } : {}),
|
|
31
|
+
...(cursor && cursor.afterFinishedAt !== undefined ? { after_finished_at: cursor.afterFinishedAt } : {}),
|
|
32
|
+
...(cursor && cursor.bounceId !== undefined ? { bounce_id: cursor.bounceId } : {}),
|
|
33
|
+
timeout_s: 45,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function stackWatchObligation(input) {
|
|
37
|
+
if (input.terminal) {
|
|
38
|
+
return {
|
|
39
|
+
done: true,
|
|
40
|
+
until: "landed",
|
|
41
|
+
reason: input.terminal,
|
|
42
|
+
next: null,
|
|
43
|
+
message: DONE_MESSAGES[input.terminal],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const args = stackWatchNextArgs(input.stackId, input.cursor);
|
|
47
|
+
const reason = input.status === "rate_limited"
|
|
48
|
+
? "rate_limited"
|
|
49
|
+
: input.status === "failed"
|
|
50
|
+
? "failed"
|
|
51
|
+
: input.attention ? "attention" : input.unread ? "unread" : "open";
|
|
52
|
+
const parts = [
|
|
53
|
+
STACK_WATCH_NOT_DONE_SENTENCE,
|
|
54
|
+
`Watch stack ${input.stackId} as a whole until it lands or the human takes it back. A clean push, a submit, or one merged layer does not finish it: merging a lower layer can break a layer above it.`,
|
|
55
|
+
];
|
|
56
|
+
if (reason === "rate_limited") {
|
|
57
|
+
parts.push(`Wait ${input.retryAfterSeconds !== undefined ? `${input.retryAfterSeconds}s` : "retry_after_seconds"} first, then call stack_wait with the same cursor.`);
|
|
58
|
+
}
|
|
59
|
+
else if (reason === "failed") {
|
|
60
|
+
parts.push(`This read failed, which does not end the task. Call stack_wait once more with the same cursor; if that fails too, tell the human that stack ${input.stackId} is not landed and is no longer being watched.`);
|
|
61
|
+
}
|
|
62
|
+
else if (reason === "attention" && input.attention) {
|
|
63
|
+
parts.push(`First fix #${input.attention.prNumber} (${input.attention.blocker}); repair, when present, names the fix. Calling stack_wait again before fixing it returns the same attention. After your push, call stack_wait with this cursor and enrolled_head_sha set to the SHA you pushed.`);
|
|
64
|
+
}
|
|
65
|
+
else if (reason === "unread") {
|
|
66
|
+
parts.push("This tool did not read the stack's state; stack_wait will.");
|
|
67
|
+
}
|
|
68
|
+
parts.push(`Next call: stack_wait ${JSON.stringify(args)}.`);
|
|
69
|
+
if (input.freshCursor) {
|
|
70
|
+
parts.push("If you already hold a cursor from an earlier stack_wait on this stack, keep passing that cursor instead.");
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
done: false,
|
|
74
|
+
until: "landed",
|
|
75
|
+
reason,
|
|
76
|
+
next: { tool: "stack_wait", args, command: `mg stack wait ${input.stackId} --json` },
|
|
77
|
+
message: parts.join(" "),
|
|
78
|
+
};
|
|
79
|
+
}
|
package/dist/stack-watch.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { type StackBlocker } from "./stack-blockers.js";
|
|
1
|
+
import { type StackAgentBusy, type StackBlocker, type StackLayerAgents, type StackLandGatePending, type StackRepairHint } from "./stack-blockers.js";
|
|
2
|
+
import { type StackWatchObligation } from "./stack-watch-obligation.js";
|
|
2
3
|
import { apiFetch } from "./api.js";
|
|
3
4
|
import type { Config } from "./config.js";
|
|
4
5
|
export declare const STACK_WATCH_SCHEMA: "mergestorm.stack_watch/v1";
|
|
5
6
|
export declare const STACK_WATCH_DEFAULT_TIMEOUT_MS = 45000;
|
|
7
|
+
export declare const STACK_WATCH_BUSY_RECHECK_S = 15;
|
|
6
8
|
/** Resume selectors are fixed for the entire wait, including transient retries. */
|
|
7
9
|
export type StackWatchCursor = {
|
|
8
10
|
readonly stackId: string;
|
|
@@ -25,6 +27,11 @@ export type StackWatchEnvelope = {
|
|
|
25
27
|
headSha: string | null;
|
|
26
28
|
} | null;
|
|
27
29
|
assessment: "available" | "unavailable";
|
|
30
|
+
busy: StackAgentBusy[];
|
|
31
|
+
agents: StackLayerAgents | null;
|
|
32
|
+
repair: StackRepairHint | null;
|
|
33
|
+
landGatePending: StackLandGatePending | null;
|
|
34
|
+
watch: StackWatchObligation;
|
|
28
35
|
};
|
|
29
36
|
export type PollStackWatchOptions = {
|
|
30
37
|
timeoutMs?: number;
|
package/dist/stack-watch.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { currentLayer, stackBlockers } from "./stack-blockers.js";
|
|
1
|
+
import { currentLayer, layerAgents, stackBlockers, } from "./stack-blockers.js";
|
|
2
|
+
import { stackTerminalReason, stackWatchObligation, } from "./stack-watch-obligation.js";
|
|
2
3
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
3
4
|
import { apiFetch } from "./api.js";
|
|
4
5
|
import { REVIEW_POLL_INTERVAL_MS, REVIEW_POLL_MAX_TRANSIENT_RETRIES, isTransientReviewPollError, isTransientReviewPollStatus, transientRetryWaitMs, } from "./commands/review-client.js";
|
|
5
6
|
export const STACK_WATCH_SCHEMA = "mergestorm.stack_watch/v1";
|
|
6
7
|
export const STACK_WATCH_DEFAULT_TIMEOUT_MS = 45_000;
|
|
8
|
+
export const STACK_WATCH_BUSY_RECHECK_S = 15;
|
|
7
9
|
export class StackWatchTimeoutError extends Error {
|
|
8
10
|
lastEnvelope;
|
|
9
11
|
constructor(lastEnvelope) {
|
|
@@ -46,10 +48,20 @@ export async function pollStackWatch(cfg, stackId, opts = {}) {
|
|
|
46
48
|
let lastEnvelope = {
|
|
47
49
|
schema: STACK_WATCH_SCHEMA, status: "failed", stackId: id, blocker: null,
|
|
48
50
|
bounceKind: null, prNumber: null, headSha: null, cursor,
|
|
49
|
-
issues: [], currentCandidate: null, assessment: "unavailable",
|
|
51
|
+
issues: [], currentCandidate: null, assessment: "unavailable", busy: [], agents: null, repair: null,
|
|
52
|
+
landGatePending: null,
|
|
50
53
|
};
|
|
54
|
+
let terminal = null;
|
|
55
|
+
const seal = (snapshot, retryAfterSeconds) => ({
|
|
56
|
+
...snapshot,
|
|
57
|
+
watch: stackWatchObligation({
|
|
58
|
+
stackId: id, terminal, cursor: snapshot.cursor, status: snapshot.status, retryAfterSeconds,
|
|
59
|
+
attention: snapshot.status === "attention" && snapshot.blocker && snapshot.prNumber != null
|
|
60
|
+
? { prNumber: snapshot.prNumber, blocker: snapshot.blocker } : null,
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
51
63
|
let transientFailures = 0;
|
|
52
|
-
const timeout = () => new StackWatchTimeoutError(lastEnvelope);
|
|
64
|
+
const timeout = () => new StackWatchTimeoutError(seal(lastEnvelope));
|
|
53
65
|
const pause = async (ms) => {
|
|
54
66
|
const remaining = Math.min(ms, Math.max(0, deadline - now()));
|
|
55
67
|
if (remaining > 0)
|
|
@@ -75,11 +87,11 @@ export async function pollStackWatch(cfg, stackId, opts = {}) {
|
|
|
75
87
|
throw timeout();
|
|
76
88
|
if (timeoutMs !== 0 && isTransientReviewPollError(err) && transientFailures < REVIEW_POLL_MAX_TRANSIENT_RETRIES) {
|
|
77
89
|
transientFailures += 1;
|
|
78
|
-
opts.onTick?.(lastEnvelope);
|
|
90
|
+
opts.onTick?.(seal(lastEnvelope));
|
|
79
91
|
await pause(transientRetryWaitMs({ failureCount: transientFailures, random: opts.random }));
|
|
80
92
|
continue;
|
|
81
93
|
}
|
|
82
|
-
throw new StackWatchError(err instanceof Error ? err.message : String(err), { ...lastEnvelope, status: "failed", assessment: "unavailable" }, { cause: err });
|
|
94
|
+
throw new StackWatchError(err instanceof Error ? err.message : String(err), seal({ ...lastEnvelope, status: "failed", assessment: "unavailable" }), { cause: err });
|
|
83
95
|
}
|
|
84
96
|
if (response.status === 200)
|
|
85
97
|
return response.body;
|
|
@@ -89,14 +101,14 @@ export async function pollStackWatch(cfg, stackId, opts = {}) {
|
|
|
89
101
|
if (timeoutMs !== 0 && isTransientReviewPollStatus(response.status) && transientFailures < REVIEW_POLL_MAX_TRANSIENT_RETRIES &&
|
|
90
102
|
(response.status !== 429 || retryWaitMs < deadline - now())) {
|
|
91
103
|
transientFailures += 1;
|
|
92
|
-
opts.onTick?.({ ...lastEnvelope, status: response.status === 429 ? "rate_limited" : lastEnvelope.status });
|
|
104
|
+
opts.onTick?.(seal({ ...lastEnvelope, status: response.status === 429 ? "rate_limited" : lastEnvelope.status }, response.status === 429 ? response.retryAfterSeconds : undefined));
|
|
93
105
|
await pause(retryWaitMs);
|
|
94
106
|
continue;
|
|
95
107
|
}
|
|
96
|
-
throw new StackWatchError(`Stack poll failed (HTTP ${response.status})`, {
|
|
108
|
+
throw new StackWatchError(`Stack poll failed (HTTP ${response.status})`, seal({
|
|
97
109
|
...lastEnvelope, status: response.status === 429 ? "rate_limited" : "failed",
|
|
98
110
|
assessment: "unavailable",
|
|
99
|
-
}, undefined, response.retryAfterSeconds);
|
|
111
|
+
}, response.retryAfterSeconds), undefined, response.retryAfterSeconds);
|
|
100
112
|
}
|
|
101
113
|
throw timeout();
|
|
102
114
|
};
|
|
@@ -105,16 +117,22 @@ export async function pollStackWatch(cfg, stackId, opts = {}) {
|
|
|
105
117
|
const stack = snapshot && Array.isArray(snapshot.stacks)
|
|
106
118
|
? snapshot.stacks.find((candidate) => candidate.id.trim().toLowerCase() === id)
|
|
107
119
|
: undefined;
|
|
120
|
+
if (snapshot && Array.isArray(snapshot.stacks) && !stack) {
|
|
121
|
+
terminal = "not_found";
|
|
122
|
+
throw new StackWatchError("Stack not found or not owned by the current user", seal({ ...lastEnvelope, status: "failed", assessment: "unavailable", blocker: null, issues: [], busy: [],
|
|
123
|
+
prNumber: null, headSha: null, repair: null, agents: null, currentCandidate: null, landGatePending: null }));
|
|
124
|
+
}
|
|
108
125
|
if (!stack || !Array.isArray(stack.layers)) {
|
|
109
|
-
throw new StackWatchError("Stack snapshot missing or invalid", { ...lastEnvelope, status: "failed", assessment: "unavailable" });
|
|
126
|
+
throw new StackWatchError("Stack snapshot missing or invalid", seal({ ...lastEnvelope, status: "failed", assessment: "unavailable" }));
|
|
110
127
|
}
|
|
128
|
+
terminal = stackTerminalReason(stack);
|
|
111
129
|
return stack;
|
|
112
130
|
};
|
|
113
131
|
const queueRoute = `/api/v1/stacks/queue?stackId=${encodeURIComponent(id)}`;
|
|
114
132
|
const readQueue = async (route, finalSnapshot = false) => {
|
|
115
133
|
const queue = await request(route, finalSnapshot);
|
|
116
134
|
if (!queue || !Array.isArray(queue.entries)) {
|
|
117
|
-
throw new StackWatchError("Invalid merge queue response", { ...lastEnvelope, status: "failed", assessment: "unavailable" });
|
|
135
|
+
throw new StackWatchError("Invalid merge queue response", seal({ ...lastEnvelope, status: "failed", assessment: "unavailable" }));
|
|
118
136
|
}
|
|
119
137
|
return {
|
|
120
138
|
entries: queue.entries.filter((entry) => entry.stackId.trim().toLowerCase() === id),
|
|
@@ -122,15 +140,16 @@ export async function pollStackWatch(cfg, stackId, opts = {}) {
|
|
|
122
140
|
};
|
|
123
141
|
};
|
|
124
142
|
const evaluate = (stack, entries) => {
|
|
125
|
-
const { attention, issues, currentCandidate, bounce } = stackBlockers(stack, entries, cursor);
|
|
143
|
+
const { attention, issues, currentCandidate, bounce, busy, agents, repair, landGatePending } = stackBlockers(stack, entries, cursor);
|
|
126
144
|
if (bounce)
|
|
127
145
|
cursor = Object.freeze({ ...cursor, bounceId: bounce.id,
|
|
128
146
|
...(bounce.finishedAt !== undefined ? { afterFinishedAt: bounce.finishedAt } : {}) });
|
|
129
|
-
|
|
147
|
+
const held = busy[0];
|
|
148
|
+
lastEnvelope = { ...lastEnvelope, cursor, issues, currentCandidate, assessment: "available", busy, agents, repair, landGatePending,
|
|
130
149
|
blocker: attention?.blocker ?? null, bounceKind: attention?.bounceKind ?? null,
|
|
131
|
-
prNumber: attention?.prNumber ?? currentCandidate?.prNumber ?? null,
|
|
132
|
-
headSha: attention ? attention.headSha : currentCandidate?.headSha ?? null,
|
|
133
|
-
status: attention ? "attention" : entries.some((entry) => ["queued", "running", "waiting"].includes(entry.state))
|
|
150
|
+
prNumber: attention?.prNumber ?? held?.prNumber ?? currentCandidate?.prNumber ?? null,
|
|
151
|
+
headSha: attention ? attention.headSha : held ? held.headSha : currentCandidate?.headSha ?? null,
|
|
152
|
+
status: attention ? "attention" : busy.length || landGatePending || entries.some((entry) => ["queued", "running", "waiting"].includes(entry.state))
|
|
134
153
|
? "in_progress" : "waiting" };
|
|
135
154
|
};
|
|
136
155
|
let stack = await enrich(timeoutMs === 0);
|
|
@@ -139,14 +158,15 @@ export async function pollStackWatch(cfg, stackId, opts = {}) {
|
|
|
139
158
|
cursor = Object.freeze({ stackId: id, enrolledHeadSha: layer?.headSha ?? null });
|
|
140
159
|
enrolled = true;
|
|
141
160
|
}
|
|
142
|
-
lastEnvelope = { ...lastEnvelope, cursor, prNumber: layer?.prNumber ?? null, headSha: layer?.headSha ?? null
|
|
161
|
+
lastEnvelope = { ...lastEnvelope, cursor, prNumber: layer?.prNumber ?? null, headSha: layer?.headSha ?? null,
|
|
162
|
+
agents: layer ? layerAgents(layer) : null };
|
|
143
163
|
let queue = await readQueue(queueRoute, timeoutMs === 0);
|
|
144
164
|
evaluate(stack, queue.entries);
|
|
145
|
-
if (lastEnvelope.status === "attention" || timeoutMs === 0)
|
|
146
|
-
return lastEnvelope;
|
|
165
|
+
if (lastEnvelope.status === "attention" || timeoutMs === 0 || terminal)
|
|
166
|
+
return seal(lastEnvelope);
|
|
147
167
|
while (now() < deadline) {
|
|
148
168
|
const heldAt = now();
|
|
149
|
-
const seconds = Math.min(45, Math.max(1, Math.ceil((deadline - heldAt) / 1000)));
|
|
169
|
+
const seconds = Math.min(lastEnvelope.busy.length ? STACK_WATCH_BUSY_RECHECK_S : 45, Math.max(1, Math.ceil((deadline - heldAt) / 1000)));
|
|
150
170
|
const seen = queue.fingerprint ? `&seen=${encodeURIComponent(queue.fingerprint)}` : "";
|
|
151
171
|
const held = await readQueue(`${queueRoute}&wait=${seconds}${seen}`);
|
|
152
172
|
if (JSON.stringify(held.entries) === JSON.stringify(queue.entries)) {
|
|
@@ -155,12 +175,13 @@ export async function pollStackWatch(cfg, stackId, opts = {}) {
|
|
|
155
175
|
queue = held;
|
|
156
176
|
stack = await enrich(true);
|
|
157
177
|
const refreshed = currentLayer(stack);
|
|
158
|
-
lastEnvelope = { ...lastEnvelope, prNumber: refreshed?.prNumber ?? null, headSha: refreshed?.headSha ?? null
|
|
178
|
+
lastEnvelope = { ...lastEnvelope, prNumber: refreshed?.prNumber ?? null, headSha: refreshed?.headSha ?? null,
|
|
179
|
+
agents: refreshed ? layerAgents(refreshed) : null };
|
|
159
180
|
transientFailures = 0;
|
|
160
181
|
evaluate(stack, queue.entries);
|
|
161
|
-
if (lastEnvelope.status === "attention")
|
|
162
|
-
return lastEnvelope;
|
|
163
|
-
opts.onTick?.(lastEnvelope);
|
|
182
|
+
if (lastEnvelope.status === "attention" || terminal)
|
|
183
|
+
return seal(lastEnvelope);
|
|
184
|
+
opts.onTick?.(seal(lastEnvelope));
|
|
164
185
|
}
|
|
165
186
|
throw timeout();
|
|
166
187
|
}
|
package/dist/ui/ansi.d.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brand ink. xterm-256 77 (#5FAF5F) � muted green that Terminal.app
|
|
3
|
+
* actually paints. 24-bit `38;2;61;�` was read as palette 61 (purple).
|
|
4
|
+
*/
|
|
5
|
+
export declare const BRAND_GREEN: {
|
|
6
|
+
readonly hex: "#5FAF5F";
|
|
7
|
+
readonly index: 77;
|
|
8
|
+
};
|
|
9
|
+
export declare const BRAND_GREEN_SGR: string;
|
|
10
|
+
/** Full-cell fill. Glyphs like ? leave a hairline; a space with this bg does not. */
|
|
11
|
+
export declare const BRAND_FILL_SGR: string;
|
|
1
12
|
export declare const ansi: {
|
|
2
13
|
readonly enabled: boolean;
|
|
3
14
|
green: (t: string) => string;
|
|
@@ -7,8 +18,10 @@ export declare const ansi: {
|
|
|
7
18
|
yellow: (t: string) => string;
|
|
8
19
|
gray: (t: string) => string;
|
|
9
20
|
brightGreen: (t: string) => string;
|
|
21
|
+
/** Solid cell (space + fg/bg 77). Stacks flush � no ? hairline. */
|
|
22
|
+
brandFill: (t: string) => string;
|
|
10
23
|
invert: (t: string) => string;
|
|
11
24
|
boldGreen: (t: string) => string;
|
|
12
|
-
/** Prompt / mark ink. Never 32
|
|
25
|
+
/** Prompt / mark ink. Never ANSI 32, 92, or 24-bit. */
|
|
13
26
|
boldBrightGreen: (t: string) => string;
|
|
14
27
|
};
|
package/dist/ui/ansi.js
CHANGED
|
@@ -9,7 +9,25 @@ function computeEnabled() {
|
|
|
9
9
|
function wrap(code, text) {
|
|
10
10
|
if (!computeEnabled())
|
|
11
11
|
return text;
|
|
12
|
-
return
|
|
12
|
+
return `\x1b[${code}m${text}\x1b[0m`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Brand ink. xterm-256 77 (#5FAF5F) � muted green that Terminal.app
|
|
16
|
+
* actually paints. 24-bit `38;2;61;�` was read as palette 61 (purple).
|
|
17
|
+
*/
|
|
18
|
+
export const BRAND_GREEN = { hex: "#5FAF5F", index: 77 };
|
|
19
|
+
export const BRAND_GREEN_SGR = `38;5;${BRAND_GREEN.index}`;
|
|
20
|
+
/** Full-cell fill. Glyphs like ? leave a hairline; a space with this bg does not. */
|
|
21
|
+
export const BRAND_FILL_SGR = `${BRAND_GREEN_SGR};48;5;${BRAND_GREEN.index}`;
|
|
22
|
+
function brandGreen(text, bold = false) {
|
|
23
|
+
if (!computeEnabled())
|
|
24
|
+
return text;
|
|
25
|
+
return `\x1b[${bold ? "1;" : ""}${BRAND_GREEN_SGR}m${text}\x1b[0m`;
|
|
26
|
+
}
|
|
27
|
+
function brandFill(text) {
|
|
28
|
+
if (!computeEnabled())
|
|
29
|
+
return text;
|
|
30
|
+
return `\x1b[${BRAND_FILL_SGR}m${text}\x1b[0m`;
|
|
13
31
|
}
|
|
14
32
|
export const ansi = {
|
|
15
33
|
get enabled() {
|
|
@@ -21,9 +39,11 @@ export const ansi = {
|
|
|
21
39
|
red: (t) => wrap("31", t),
|
|
22
40
|
yellow: (t) => wrap("33", t),
|
|
23
41
|
gray: (t) => wrap("90", t),
|
|
24
|
-
brightGreen: (t) =>
|
|
42
|
+
brightGreen: (t) => brandGreen(t),
|
|
43
|
+
/** Solid cell (space + fg/bg 77). Stacks flush � no ? hairline. */
|
|
44
|
+
brandFill: (t) => brandFill(t),
|
|
25
45
|
invert: (t) => wrap("7", t),
|
|
26
|
-
boldGreen: (t) => (computeEnabled() ?
|
|
27
|
-
/** Prompt / mark ink. Never 32
|
|
28
|
-
boldBrightGreen: (t) => (
|
|
46
|
+
boldGreen: (t) => (computeEnabled() ? `\x1b[1;32m${t}\x1b[0m` : t),
|
|
47
|
+
/** Prompt / mark ink. Never ANSI 32, 92, or 24-bit. */
|
|
48
|
+
boldBrightGreen: (t) => brandGreen(t, true),
|
|
29
49
|
};
|
package/dist/ui/banner.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export type BannerDensity = "full" | "compact" | "mini" | "nano";
|
|
|
14
14
|
export type BannerVariant = boolean | "mini" | "nano";
|
|
15
15
|
/** Handle returned by {@link printBannerHeader}; `rows` reflows for a resize. */
|
|
16
16
|
export type BannerHandle = {
|
|
17
|
-
rows(columns?: number, variant?: BannerVariant): string[];
|
|
17
|
+
rows(columns?: number, variant?: BannerVariant, frame?: number): string[];
|
|
18
18
|
};
|
|
19
19
|
/**
|
|
20
20
|
* The whole welcome panel as rows, reflowed to `columns`. Every row fits in
|
|
@@ -24,6 +24,7 @@ export type BannerHandle = {
|
|
|
24
24
|
export declare function buildBannerRows(state: BannerState, columns?: number, opts?: {
|
|
25
25
|
compact?: boolean;
|
|
26
26
|
density?: BannerDensity;
|
|
27
|
+
frame?: number;
|
|
27
28
|
}): string[];
|
|
28
29
|
/**
|
|
29
30
|
* Print the welcome panel and return a handle whose `rows()` reflows the
|
package/dist/ui/banner.js
CHANGED
|
@@ -3,8 +3,8 @@ 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, terminalColumns, visibleWidth, } from "./box.js";
|
|
7
|
-
import {
|
|
6
|
+
import { frameWidth, padVisible, preferBoxedUi, roundedBox, sideBySide, terminalColumns, visibleWidth, } from "./box.js";
|
|
7
|
+
import { paintSpriteRow, TORNADO_LOGO, TORNADO_LOGO_WIDTH } from "./logo.js";
|
|
8
8
|
import { formatResetLabel, usageBar } from "./usage.js";
|
|
9
9
|
const WORDMARK = `
|
|
10
10
|
__ __ ___ ___ ___ ___ ___ _____ ___ ___ __ __
|
|
@@ -105,6 +105,12 @@ const WHATS_NEW = {
|
|
|
105
105
|
"0.3.24": [
|
|
106
106
|
"mg settings --auto-land-settle (15-300 seconds)",
|
|
107
107
|
],
|
|
108
|
+
"0.3.25": [
|
|
109
|
+
"stack wait holds a PR while Cyclone or Vortex works",
|
|
110
|
+
],
|
|
111
|
+
"0.3.26": [
|
|
112
|
+
"stack results say the watch ends only at land",
|
|
113
|
+
],
|
|
108
114
|
};
|
|
109
115
|
function densityFromVariant(variant) {
|
|
110
116
|
if (variant === "mini" || variant === "nano")
|
|
@@ -196,57 +202,30 @@ export function buildBannerRows(state, columns, opts) {
|
|
|
196
202
|
const contentCells = boxed
|
|
197
203
|
? Math.max(20, outer - 4)
|
|
198
204
|
: Math.max(20, terminalColumns(80, columns) - 1);
|
|
199
|
-
const
|
|
200
|
-
const
|
|
205
|
+
const markLines = TORNADO_LOGO;
|
|
206
|
+
const markWidth = TORNADO_LOGO_WIDTH;
|
|
207
|
+
const gap = 2;
|
|
208
|
+
const rightCells = Math.max(8, contentCells - markWidth - gap);
|
|
201
209
|
const clip = (line) => visibleWidth(line) > contentCells ? padVisible(line, contentCells) : line;
|
|
202
|
-
const
|
|
210
|
+
const clipRight = (line) => visibleWidth(line) > rightCells ? padVisible(line, rightCells) : line;
|
|
203
211
|
const title = `${ansi.boldBrightGreen("mergestorm")} ${ansi.dim(`v${state.version}`)}`;
|
|
204
|
-
|
|
205
|
-
const one = `${mark(0)}${title} ${statusLine(state)}`;
|
|
206
|
-
return [clip(one)];
|
|
207
|
-
}
|
|
208
|
-
if (density === "mini") {
|
|
209
|
-
// Short pane: mark + who you are, then the same cues as the full home.
|
|
210
|
-
const line0 = clip(`${mark(0)}${title} ${statusLine(state)}`);
|
|
211
|
-
const line1 = state.me && !state.keyInvalid
|
|
212
|
-
? clip(`${mark(1)}${usageLine(state.me, contentCells, indent)}`)
|
|
213
|
-
: clip(mark(1));
|
|
214
|
-
const room = contentCells - indent;
|
|
215
|
-
const blurb = tagline(room);
|
|
216
|
-
const tips = tipsLine(room);
|
|
217
|
-
const fresh = whatsNewLine(state.version, room);
|
|
218
|
-
return [
|
|
219
|
-
line0,
|
|
220
|
-
line1,
|
|
221
|
-
...(blurb ? [clip(`${pad}${blurb}`)] : []),
|
|
222
|
-
...(tips ? [clip(`${pad}${tips}`)] : []),
|
|
223
|
-
...(fresh ? [clip(`${pad}${fresh}`)] : []),
|
|
224
|
-
];
|
|
225
|
-
}
|
|
226
|
-
const content = [
|
|
227
|
-
clip(`${pad}${title}`),
|
|
228
|
-
clip(`${mark(0)}${statusLine(state)}`),
|
|
229
|
-
];
|
|
212
|
+
const right = [clipRight(title), clipRight(statusLine(state))];
|
|
230
213
|
if (state.me && !state.keyInvalid) {
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
else {
|
|
234
|
-
content.push(clip(mark(1)));
|
|
214
|
+
right.push(clipRight(usageLine(state.me, rightCells, 0)));
|
|
235
215
|
}
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
const
|
|
239
|
-
const fresh = whatsNewLine(state.version, room);
|
|
216
|
+
const blurb = tagline(rightCells);
|
|
217
|
+
const tips = tipsLine(rightCells);
|
|
218
|
+
const fresh = whatsNewLine(state.version, rightCells);
|
|
240
219
|
if (blurb)
|
|
241
|
-
|
|
220
|
+
right.push(clipRight(blurb));
|
|
242
221
|
if (tips)
|
|
243
|
-
|
|
222
|
+
right.push(clipRight(tips));
|
|
244
223
|
if (fresh)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
224
|
+
right.push(clipRight(fresh));
|
|
225
|
+
const markCol = markLines.map((line) => padVisible(paintSpriteRow(line), markWidth));
|
|
226
|
+
const content = sideBySide(markCol, density === "nano" ? right.slice(0, 3) : right, markWidth, gap).map(clip);
|
|
227
|
+
if (density === "mini" || density === "nano")
|
|
228
|
+
return content;
|
|
250
229
|
if (boxed) {
|
|
251
230
|
return [...roundedBox(content, { width: outer, padding: 1 })];
|
|
252
231
|
}
|
|
@@ -282,7 +261,7 @@ export async function printBannerHeader(opts) {
|
|
|
282
261
|
}
|
|
283
262
|
}
|
|
284
263
|
return {
|
|
285
|
-
rows: (columns, variant) => buildBannerRows(state, columns, { density: densityFromVariant(variant) }),
|
|
264
|
+
rows: (columns, variant, frame) => buildBannerRows(state, columns, { density: densityFromVariant(variant), frame }),
|
|
286
265
|
};
|
|
287
266
|
}
|
|
288
267
|
export function shellPrompt(_loggedIn) {
|
package/dist/ui/logo.d.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* The home mark: a tornado drawn as five horizontal bars and one square tail,
|
|
3
|
+
* each an upper-half band (▀). One column is one bar-height wide; half-column
|
|
4
|
+
* ends use the upper quadrants (▝ ▘) so every edge lands where the source art
|
|
5
|
+
* puts it. Same X/width as the SVG, shifted up half a cell. Painted fg 77.
|
|
4
6
|
*/
|
|
5
|
-
export declare const STORM_MARK: string[];
|
|
6
|
-
/** Widest row of the mark, in cells (every mark glyph is single-cell). */
|
|
7
|
-
export declare const STORM_MARK_WIDTH: number;
|
|
8
|
-
/** @deprecated Alias — the home panel uses {@link STORM_MARK}. */
|
|
9
7
|
export declare const TORNADO_LOGO: string[];
|
|
10
|
-
/**
|
|
8
|
+
/** Single idle frame. Kept for callers that index frames. */
|
|
9
|
+
export declare const TORNADO_FRAMES: string[][];
|
|
10
|
+
/** Every sprite glyph is one column. */
|
|
11
11
|
export declare const TORNADO_LOGO_WIDTH: number;
|
|
12
|
-
/** @deprecated Compact home is the full two-row mark. */
|
|
13
12
|
export declare const TORNADO_LOGO_COMPACT: string[];
|
|
13
|
+
/** @deprecated Use {@link TORNADO_LOGO}. */
|
|
14
|
+
export declare const STORM_MARK: string[];
|
|
15
|
+
/** @deprecated Use {@link TORNADO_LOGO_WIDTH}. */
|
|
16
|
+
export declare const STORM_MARK_WIDTH: number;
|
|
17
|
+
/** Paint one sprite row: each run of glyphs in brand green 77, spaces left bare. */
|
|
18
|
+
export declare function paintSpriteRow(row: string): string;
|
package/dist/ui/logo.js
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
|
+
import { ansi } from "./ansi.js";
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
3
|
+
* The home mark: a tornado drawn as five horizontal bars and one square tail,
|
|
4
|
+
* each an upper-half band (▀). One column is one bar-height wide; half-column
|
|
5
|
+
* ends use the upper quadrants (▝ ▘) so every edge lands where the source art
|
|
6
|
+
* puts it. Same X/width as the SVG, shifted up half a cell. Painted fg 77.
|
|
4
7
|
*/
|
|
5
|
-
export const
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
+
export const TORNADO_LOGO = [
|
|
9
|
+
" ▀▀▀▀▀▀▀▀▀▀▘",
|
|
10
|
+
"▀▀▀▀▀▀▀▀▀ ",
|
|
11
|
+
" ▀▀▀▀▀▀ ",
|
|
12
|
+
" ▝▀▀▀▀▀ ",
|
|
13
|
+
" ▝▀▀▘ ",
|
|
14
|
+
" ▀ ",
|
|
8
15
|
];
|
|
9
|
-
/**
|
|
10
|
-
export const
|
|
11
|
-
/**
|
|
12
|
-
export const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
/** Single idle frame. Kept for callers that index frames. */
|
|
17
|
+
export const TORNADO_FRAMES = [TORNADO_LOGO];
|
|
18
|
+
/** Every sprite glyph is one column. */
|
|
19
|
+
export const TORNADO_LOGO_WIDTH = Math.max(...TORNADO_LOGO.map((line) => [...line].length));
|
|
20
|
+
export const TORNADO_LOGO_COMPACT = TORNADO_LOGO;
|
|
21
|
+
/** @deprecated Use {@link TORNADO_LOGO}. */
|
|
22
|
+
export const STORM_MARK = TORNADO_LOGO;
|
|
23
|
+
/** @deprecated Use {@link TORNADO_LOGO_WIDTH}. */
|
|
24
|
+
export const STORM_MARK_WIDTH = TORNADO_LOGO_WIDTH;
|
|
25
|
+
/** Paint one sprite row: each run of glyphs in brand green 77, spaces left bare. */
|
|
26
|
+
export function paintSpriteRow(row) {
|
|
27
|
+
return row.replace(/[^ ]+/g, (run) => ansi.brightGreen(run));
|
|
28
|
+
}
|
package/dist/ui/prompt.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface AskLineOptions {
|
|
|
23
23
|
* resize cannot desync CUU counts. Pass `true` for compact, `"mini"` /
|
|
24
24
|
* `"nano"` for a short pane that still shows the mark.
|
|
25
25
|
*/
|
|
26
|
-
header?: (columns?: number, variant?: boolean | "mini" | "nano") => string[];
|
|
26
|
+
header?: (columns?: number, variant?: boolean | "mini" | "nano", frame?: number) => string[];
|
|
27
27
|
}
|
|
28
28
|
/** Thrown when the user asks to close the prompt (Ctrl+D empty, or Ctrl+C twice). */
|
|
29
29
|
export declare class PromptClosedError extends Error {
|
|
@@ -47,6 +47,16 @@ export declare function promptClearOverlay(boxed: boolean): string;
|
|
|
47
47
|
* frame's row count is unknowable after the terminal rewraps.
|
|
48
48
|
*/
|
|
49
49
|
export declare const HEADER_REDRAW_PREFIX = "\u001B[H\u001B[0J";
|
|
50
|
+
export declare const MOUSE_ENABLE = "\u001B[?1000h\u001B[?1006h";
|
|
51
|
+
export declare const MOUSE_DISABLE = "\u001B[?1000l\u001B[?1006l";
|
|
52
|
+
export type SpriteRect = {
|
|
53
|
+
col: number;
|
|
54
|
+
row: number;
|
|
55
|
+
width: number;
|
|
56
|
+
height: number;
|
|
57
|
+
};
|
|
58
|
+
/** SGR coordinates and the rectangle are both 1-based. */
|
|
59
|
+
export declare function hitTestSprite(col: number, row: number, rect: SpriteRect): boolean;
|
|
50
60
|
export declare const DROPDOWN_MAX_COMMAND_ROWS = 8;
|
|
51
61
|
export declare function formatSlashLabel(spec: CommandSpec): string;
|
|
52
62
|
export declare function specMatchesTerm(spec: CommandSpec, term: string): boolean;
|