nightralph 0.0.50 → 0.0.53
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 +70 -8
- package/dist/display.js +80 -62
- package/dist/display.js.map +2 -2
- package/dist/index.js +162 -94
- package/dist/index.js.map +2 -2
- package/dist/meta.json +6 -6
- package/dist/orchestrator.js +86 -33
- package/dist/orchestrator.js.map +2 -2
- package/dist/src/display.d.ts +35 -33
- package/dist/src/display.d.ts.map +1 -1
- package/dist/src/orchestrator.d.ts +10 -0
- package/dist/src/orchestrator.d.ts.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -32,9 +32,11 @@ so they can't drift as long as you install the skills with this package.
|
|
|
32
32
|
* [Select a ticket](#select-a-ticket)
|
|
33
33
|
* [Worktrees](#worktrees)
|
|
34
34
|
* [Progress](#progress)
|
|
35
|
+
* [Status display](#status-display)
|
|
35
36
|
- [Keep Your Machine Awake](#keep-your-machine-awake)
|
|
36
37
|
* [Mac OS](#mac-os)
|
|
37
38
|
+ [Claude Opus 4.6](#claude-opus-46)
|
|
39
|
+
+ [Claude Opus 5.5](#claude-opus-55)
|
|
38
40
|
+ [Claude Sonnet 5](#claude-sonnet-5)
|
|
39
41
|
+ [Claude Fable 5.1](#claude-fable-51)
|
|
40
42
|
+ [`pi` + OpenRouter model](#pi--openrouter-model)
|
|
@@ -161,7 +163,8 @@ Flags:
|
|
|
161
163
|
| `--dry-run` | no | -- | Show wave order and prompt without running |
|
|
162
164
|
| `-X` | no | -- | Stop on merge conflict instead of re-spawning |
|
|
163
165
|
| `--timeout` | no | `3600` | Kill the agent after N seconds. A killed agent is not a failure: its work is kept (see Notes) |
|
|
164
|
-
| `--test-cmd <cmd>` | no | auto | Run in each worktree after the agent exits; non-zero exit rejects the ticket. Auto-detects `npm test`. Pass `""` to disable. Shares the agent's `--timeout`.
|
|
166
|
+
| `--test-cmd <cmd>` | no | auto | Run in each worktree after the agent exits; non-zero exit rejects the ticket. Auto-detects `npm test`. Pass `""` to disable. Shares the agent's `--timeout`. Dependencies are already installed by `--install-cmd` |
|
|
167
|
+
| `--install-cmd <cmd>` | no | auto | Run in each fresh worktree before the agent starts, and again after a merge-conflict rebase; non-zero exit fails the attempt (it is retried like any other failure). Auto-detects from the lockfile: `pnpm-lock.yaml` -> `pnpm install --prefer-offline --frozen-lockfile`, `package-lock.json` -> `npm ci --prefer-offline --no-audit --no-fund`, a `package.json` with no lockfile -> `npm install --prefer-offline --no-audit --no-fund --no-package-lock`, no `package.json` -> no install. Pass `""` to disable. Shares the agent's `--timeout`. Output goes to `<ticket>.install.log` |
|
|
165
168
|
| `--max-turns <n>` | no | -- | Max agentic turns per ticket. Forwarded verbatim as `--max-turns <n>` to the provider CLI. Only `claude` accepts it; `codex` and `pi` reject it as an unknown option and the agent exits immediately. Applies to every agent spawn, including a re-spawn after a merge conflict. When omitted, `claude` applies no turn limit, so `--timeout` is the only cap. Reaching the limit is an error exit, so the ticket is recorded as failed |
|
|
166
169
|
| `--effort <level>` | no | -- | Starting effort/reasoning level. Each provider has its own ladder: `claude` supports `low`, `medium`, `high`, `xhigh`, `max` (forwarded as `--effort`); `codex` supports `low`, `medium`, `high` (forwarded as `-c model_reasoning_effort=<level>`); `pi` supports `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max` (forwarded as `--thinking`). Omitted = provider default. Each retry steps up from here (see `--retries`) |
|
|
167
170
|
| `--retries <n>` | no | `3` | Extra attempts per ticket after a failed run. Each retry starts from a fresh worktree and raises the effort level one rung (see Notes). `0` disables retries |
|
|
@@ -188,7 +191,8 @@ Flags:
|
|
|
188
191
|
command fails, the ticket is left `ready-for-agent` for a retry
|
|
189
192
|
and the test output is written to `<dir>/../logs/<ticket>.test.log`.
|
|
190
193
|
Failed tickets (non-zero exit) keep their status and block
|
|
191
|
-
dependents.
|
|
194
|
+
dependents. The status display marks those dependents as blocked
|
|
195
|
+
and names the failed ticket.
|
|
192
196
|
* A failed attempt is retried, up to `--retries` times, before the
|
|
193
197
|
ticket is recorded as failed. Non-zero exit, exit 0 with no new
|
|
194
198
|
commits, and a red test command all count as failures; a timeout
|
|
@@ -254,13 +258,18 @@ is `.scratch/<name>/issues/`.
|
|
|
254
258
|
|
|
255
259
|
## How It Works
|
|
256
260
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
ticket slug.
|
|
261
|
+
Tickets run in waves. A wave is a batch of tickets whose blockers are
|
|
262
|
+
all done. The tickets in a wave run in parallel, up to `--concurrency`
|
|
263
|
+
at once, and each ticket gets a worktree named after the ticket slug.
|
|
264
|
+
nightralph waits for the whole batch to finish, merges the branches of
|
|
265
|
+
the tickets that succeeded, and marks them `done`. Then it looks for
|
|
266
|
+
tickets that are newly unblocked. Those form the next wave. This
|
|
267
|
+
continues until no ticket is ready to run.
|
|
260
268
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
269
|
+
The wave total in the status display header (the `3` in `Wave 2/3`) is
|
|
270
|
+
a prediction made at startup. It assumes every ticket succeeds. When a
|
|
271
|
+
ticket fails, its dependents never become ready, so the run can end
|
|
272
|
+
before it reaches the predicted last wave.
|
|
264
273
|
|
|
265
274
|
|
|
266
275
|
### Select a ticket
|
|
@@ -278,6 +287,13 @@ completes, successful branches are merged back into the base branch.
|
|
|
278
287
|
The first branch merges directly, and subsequent branches rebase
|
|
279
288
|
onto the updated base before merging.
|
|
280
289
|
|
|
290
|
+
A fresh worktree has only tracked files, so there is no
|
|
291
|
+
`node_modules`. Before the agent starts, the orchestrator installs
|
|
292
|
+
dependencies with the command that matches the project's lockfile
|
|
293
|
+
(see `--install-cmd`), and the prompt tells the agent not to run
|
|
294
|
+
another installer. The install never writes a lockfile, so ticket
|
|
295
|
+
branches do not commit one by accident.
|
|
296
|
+
|
|
281
297
|
If a merge conflict occurs, the behavior depends on the `-X` flag: by
|
|
282
298
|
default the agent is re-spawned to resolve it; with `-X`,
|
|
283
299
|
the run stops immediately.
|
|
@@ -288,6 +304,47 @@ A file `progress.md` is written to the `.scratch/<feature>/` directory
|
|
|
288
304
|
and committed after each ticket completes, so you can monitor the run from
|
|
289
305
|
another terminal by reading the file.
|
|
290
306
|
|
|
307
|
+
### Status display
|
|
308
|
+
|
|
309
|
+
In a terminal, nightralph shows a header line, one row per ticket, and
|
|
310
|
+
a log of recent messages. The ticket list holds every ticket that was
|
|
311
|
+
`ready-for-agent` when the run started, and it stays the same for the
|
|
312
|
+
whole run. Rows do not move, disappear, or reappear between waves.
|
|
313
|
+
|
|
314
|
+
Each row starts with a dot that shows the ticket's state:
|
|
315
|
+
|
|
316
|
+
* Dim empty dot: pending. The ticket has not started yet.
|
|
317
|
+
* Yellow dot: running. The agent is working, the test command is
|
|
318
|
+
running, or the ticket is waiting out a retry delay.
|
|
319
|
+
* Cyan dot: waiting to merge. The agent and the test command
|
|
320
|
+
succeeded, and the branch waits for the rest of the wave to finish.
|
|
321
|
+
The dot stays cyan while the branch is merged, including while a
|
|
322
|
+
re-spawned agent resolves a merge conflict.
|
|
323
|
+
* Green dot: done.
|
|
324
|
+
* Red dot: failed.
|
|
325
|
+
* Dim filled dot: blocked. A ticket upstream failed or is not agent
|
|
326
|
+
work (for example, it is `ready-for-human`), so this ticket cannot
|
|
327
|
+
run in this session. The row names the blocking tickets, as in
|
|
328
|
+
`auth/03 (blocked by auth/01)`.
|
|
329
|
+
|
|
330
|
+
The header looks like this:
|
|
331
|
+
|
|
332
|
+
```
|
|
333
|
+
Wave 2/3 - 5/8 done - 63%
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
The counts cover only this run's tickets, the ones in the list.
|
|
337
|
+
Tickets finished by an earlier run are not counted.
|
|
338
|
+
|
|
339
|
+
When stdout is not a terminal, for example when it is piped to a file,
|
|
340
|
+
nightralph prints one line per wave that names the wave's tickets,
|
|
341
|
+
followed by plain log lines.
|
|
342
|
+
|
|
343
|
+
Known limit: with `-X`, the run stops at the first merge conflict. A
|
|
344
|
+
ticket whose branch succeeded but came after the conflicting branch is
|
|
345
|
+
never merged, and its row stays on the cyan "waiting to merge" dot when
|
|
346
|
+
the run stops.
|
|
347
|
+
|
|
291
348
|
|
|
292
349
|
---
|
|
293
350
|
|
|
@@ -306,6 +363,11 @@ keep the machine awake, but the display can still go dark.
|
|
|
306
363
|
caffeinate -i npx nightralph claude -m claude-opus-4-6 --effort low
|
|
307
364
|
```
|
|
308
365
|
|
|
366
|
+
#### Claude Opus 5.5
|
|
367
|
+
|
|
368
|
+
```sh
|
|
369
|
+
caffeinate -i npx nightralph claude -m claude-opus-5-5 --effort low
|
|
370
|
+
```
|
|
309
371
|
#### Claude Sonnet 5
|
|
310
372
|
|
|
311
373
|
```sh
|
package/dist/display.js
CHANGED
|
@@ -58,6 +58,14 @@ const DEFAULT_COLUMNS = 80;
|
|
|
58
58
|
const MIN_LOG_ROWS = 1;
|
|
59
59
|
const MAX_MESSAGE_BUFFER = 200;
|
|
60
60
|
const ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]/g;
|
|
61
|
+
function rowText(ticket) {
|
|
62
|
+
const label = ticketLabel(ticket);
|
|
63
|
+
if (ticket.status !== "blocked" || !ticket.blockedBy?.length) {
|
|
64
|
+
return label;
|
|
65
|
+
}
|
|
66
|
+
return `${label} (blocked by ${ticket.blockedBy.join(", ")})`;
|
|
67
|
+
}
|
|
68
|
+
__name(rowText, "rowText");
|
|
61
69
|
function fitLine(line, width) {
|
|
62
70
|
const visible = line.replace(ANSI_RE, "");
|
|
63
71
|
if (visible.length <= width) return line;
|
|
@@ -81,13 +89,16 @@ function renderDot(status) {
|
|
|
81
89
|
case "in-progress":
|
|
82
90
|
return color.yellow(FILLED_DOT);
|
|
83
91
|
// The agent and its test gate are finished; the branch
|
|
84
|
-
// waits for the rest of the wave before it is merged.
|
|
92
|
+
// waits for the rest of the wave before it is merged. It
|
|
93
|
+
// stays cyan through the merge and any conflict respawn.
|
|
85
94
|
case "awaiting-merge":
|
|
86
95
|
return color.cyan(FILLED_DOT);
|
|
87
96
|
case "done":
|
|
88
97
|
return color.green(FILLED_DOT);
|
|
89
98
|
case "failed":
|
|
90
99
|
return color.red(FILLED_DOT);
|
|
100
|
+
case "blocked":
|
|
101
|
+
return color.dim(FILLED_DOT);
|
|
91
102
|
}
|
|
92
103
|
}
|
|
93
104
|
__name(renderDot, "renderDot");
|
|
@@ -96,8 +107,8 @@ class StatusDisplay {
|
|
|
96
107
|
__name(this, "StatusDisplay");
|
|
97
108
|
}
|
|
98
109
|
wave = null;
|
|
99
|
-
|
|
100
|
-
|
|
110
|
+
tickets = [];
|
|
111
|
+
hasRun = false;
|
|
101
112
|
messages = [];
|
|
102
113
|
renderedLines = 0;
|
|
103
114
|
config;
|
|
@@ -114,20 +125,28 @@ class StatusDisplay {
|
|
|
114
125
|
const available = termHeight - overhead;
|
|
115
126
|
return Math.max(1, available);
|
|
116
127
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
};
|
|
126
|
-
this.remaining = (remaining ?? []).map((t) => ({
|
|
128
|
+
findTicket(num, feature) {
|
|
129
|
+
const found = feature ? this.tickets.find(
|
|
130
|
+
(t) => t.num === num && t.feature === feature
|
|
131
|
+
) : this.tickets.find((t) => t.num === num);
|
|
132
|
+
return found ?? null;
|
|
133
|
+
}
|
|
134
|
+
startRun(tickets) {
|
|
135
|
+
this.tickets = tickets.map((t) => ({
|
|
127
136
|
...t,
|
|
128
137
|
status: "pending"
|
|
129
138
|
}));
|
|
130
|
-
this.
|
|
139
|
+
this.hasRun = true;
|
|
140
|
+
this.viewportStart = 0;
|
|
141
|
+
}
|
|
142
|
+
startWave(waveNum, totalWaves, tickets, _progress) {
|
|
143
|
+
if (!this.hasRun) {
|
|
144
|
+
this.startRun(tickets);
|
|
145
|
+
}
|
|
146
|
+
this.wave = {
|
|
147
|
+
waveNum,
|
|
148
|
+
totalWaves
|
|
149
|
+
};
|
|
131
150
|
this.messages = [];
|
|
132
151
|
this.viewportStart = 0;
|
|
133
152
|
this.config.stream.write(ansi.cursorHide());
|
|
@@ -147,28 +166,31 @@ class StatusDisplay {
|
|
|
147
166
|
this.render();
|
|
148
167
|
}
|
|
149
168
|
setTicketStatus(num, status, feature) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
this.viewportStart = ticketIndex;
|
|
162
|
-
} else if (ticketIndex >= this.viewportStart + availableRows) {
|
|
163
|
-
this.viewportStart = ticketIndex - availableRows + 3;
|
|
164
|
-
}
|
|
165
|
-
this.viewportStart = Math.max(
|
|
166
|
-
0,
|
|
167
|
-
Math.min(this.viewportStart, totalTickets - 1)
|
|
168
|
-
);
|
|
169
|
+
const ticket = this.findTicket(num, feature);
|
|
170
|
+
if (!ticket) return;
|
|
171
|
+
ticket.status = status;
|
|
172
|
+
if (status === "in-progress") {
|
|
173
|
+
const ticketIndex = this.tickets.indexOf(ticket);
|
|
174
|
+
const availableRows = this.getAvailableRows();
|
|
175
|
+
const totalTickets = this.tickets.length;
|
|
176
|
+
if (ticketIndex < this.viewportStart) {
|
|
177
|
+
this.viewportStart = ticketIndex;
|
|
178
|
+
} else if (ticketIndex >= this.viewportStart + availableRows) {
|
|
179
|
+
this.viewportStart = ticketIndex - availableRows + 3;
|
|
169
180
|
}
|
|
170
|
-
this.
|
|
181
|
+
this.viewportStart = Math.max(
|
|
182
|
+
0,
|
|
183
|
+
Math.min(this.viewportStart, totalTickets - 1)
|
|
184
|
+
);
|
|
171
185
|
}
|
|
186
|
+
this.render();
|
|
187
|
+
}
|
|
188
|
+
setBlocked(num, feature, blockedBy) {
|
|
189
|
+
const ticket = this.findTicket(num, feature);
|
|
190
|
+
if (!ticket) return;
|
|
191
|
+
ticket.status = "blocked";
|
|
192
|
+
ticket.blockedBy = [...blockedBy];
|
|
193
|
+
this.render();
|
|
172
194
|
}
|
|
173
195
|
log(message) {
|
|
174
196
|
if (!this.wave) {
|
|
@@ -195,59 +217,52 @@ class StatusDisplay {
|
|
|
195
217
|
if (this.renderedLines > 0) {
|
|
196
218
|
output += ansi.eraseLines(this.renderedLines + 1);
|
|
197
219
|
}
|
|
198
|
-
const
|
|
220
|
+
const done = this.tickets.filter(
|
|
199
221
|
(t) => t.status === "done"
|
|
200
222
|
).length;
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
const pct = total === 0 ? 0 : Math.round(completed / total * 100);
|
|
223
|
+
const total = this.tickets.length;
|
|
224
|
+
const pct = total === 0 ? 0 : Math.round(done / total * 100);
|
|
204
225
|
const width = Math.max(
|
|
205
226
|
1,
|
|
206
227
|
(this.config.stream.columns ?? DEFAULT_COLUMNS) - 1
|
|
207
228
|
);
|
|
208
|
-
|
|
209
|
-
|
|
229
|
+
const header = `Wave ${this.wave.waveNum}/${this.wave.totalWaves} - ${done}/${total} done - ${pct}%`;
|
|
230
|
+
output += fitLine(header, width) + "\n";
|
|
210
231
|
const availableRows = this.getAvailableRows();
|
|
211
|
-
const tickets = [
|
|
212
|
-
...this.wave.tickets,
|
|
213
|
-
...this.remaining
|
|
214
|
-
];
|
|
215
232
|
let renderedTicketCount = 0;
|
|
216
|
-
if (tickets.length <= availableRows) {
|
|
217
|
-
for (const ticket of tickets) {
|
|
233
|
+
if (this.tickets.length <= availableRows) {
|
|
234
|
+
for (const ticket of this.tickets) {
|
|
218
235
|
const dot = renderDot(ticket.status);
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
width - 2
|
|
222
|
-
);
|
|
236
|
+
const text = rowText(ticket);
|
|
237
|
+
const name = fitLine(text, width - 2);
|
|
223
238
|
output += `${dot} ${name}
|
|
224
239
|
`;
|
|
225
240
|
}
|
|
226
|
-
renderedTicketCount = tickets.length;
|
|
241
|
+
renderedTicketCount = this.tickets.length;
|
|
227
242
|
} else {
|
|
228
243
|
this.viewportStart = Math.max(
|
|
229
244
|
0,
|
|
230
245
|
Math.min(
|
|
231
246
|
this.viewportStart,
|
|
232
|
-
tickets.length - 1
|
|
247
|
+
this.tickets.length - 1
|
|
233
248
|
)
|
|
234
249
|
);
|
|
235
250
|
let hasMoreAbove = this.viewportStart > 0;
|
|
236
251
|
let tentativeCount = availableRows - (hasMoreAbove ? 1 : 0);
|
|
237
|
-
let hasMoreBelow = this.viewportStart + tentativeCount < tickets.length;
|
|
252
|
+
let hasMoreBelow = this.viewportStart + tentativeCount < this.tickets.length;
|
|
238
253
|
let ticketsToShow = Math.max(
|
|
239
254
|
0,
|
|
240
255
|
tentativeCount - (hasMoreBelow ? 1 : 0)
|
|
241
256
|
);
|
|
242
257
|
const maxStart = Math.max(
|
|
243
258
|
0,
|
|
244
|
-
tickets.length - Math.max(1, ticketsToShow)
|
|
259
|
+
this.tickets.length - Math.max(1, ticketsToShow)
|
|
245
260
|
);
|
|
246
261
|
if (this.viewportStart > maxStart) {
|
|
247
262
|
this.viewportStart = maxStart;
|
|
248
263
|
hasMoreAbove = this.viewportStart > 0;
|
|
249
264
|
tentativeCount = availableRows - (hasMoreAbove ? 1 : 0);
|
|
250
|
-
hasMoreBelow = this.viewportStart + tentativeCount < tickets.length;
|
|
265
|
+
hasMoreBelow = this.viewportStart + tentativeCount < this.tickets.length;
|
|
251
266
|
ticketsToShow = Math.max(
|
|
252
267
|
0,
|
|
253
268
|
tentativeCount - (hasMoreBelow ? 1 : 0)
|
|
@@ -258,21 +273,19 @@ class StatusDisplay {
|
|
|
258
273
|
output += ` ... ${moreCount} more above
|
|
259
274
|
`;
|
|
260
275
|
}
|
|
261
|
-
const visibleTickets = tickets.slice(
|
|
276
|
+
const visibleTickets = this.tickets.slice(
|
|
262
277
|
this.viewportStart,
|
|
263
278
|
this.viewportStart + ticketsToShow
|
|
264
279
|
);
|
|
265
280
|
for (const ticket of visibleTickets) {
|
|
266
281
|
const dot = renderDot(ticket.status);
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
width - 2
|
|
270
|
-
);
|
|
282
|
+
const text = rowText(ticket);
|
|
283
|
+
const name = fitLine(text, width - 2);
|
|
271
284
|
output += `${dot} ${name}
|
|
272
285
|
`;
|
|
273
286
|
}
|
|
274
287
|
if (hasMoreBelow) {
|
|
275
|
-
const moreCount = tickets.length - (this.viewportStart + ticketsToShow);
|
|
288
|
+
const moreCount = this.tickets.length - (this.viewportStart + ticketsToShow);
|
|
276
289
|
output += ` ... ${moreCount} more below
|
|
277
290
|
`;
|
|
278
291
|
}
|
|
@@ -304,7 +317,9 @@ class PlainDisplay {
|
|
|
304
317
|
static {
|
|
305
318
|
__name(this, "PlainDisplay");
|
|
306
319
|
}
|
|
307
|
-
|
|
320
|
+
startRun(_tickets) {
|
|
321
|
+
}
|
|
322
|
+
startWave(waveNum, _totalWaves, tickets, progress) {
|
|
308
323
|
const pct = progress && progress.total > 0 ? ` (${progress.completed}/${progress.total} done)` : "";
|
|
309
324
|
console.log(
|
|
310
325
|
`
|
|
@@ -313,6 +328,8 @@ Wave ${waveNum}${pct}: ` + tickets.map((t) => ticketLabel(t)).join(", ")
|
|
|
313
328
|
}
|
|
314
329
|
setTicketStatus(_num, _status, _feature) {
|
|
315
330
|
}
|
|
331
|
+
setBlocked(_num, _feature, _blockedBy) {
|
|
332
|
+
}
|
|
316
333
|
log(message) {
|
|
317
334
|
console.log(message);
|
|
318
335
|
}
|
|
@@ -335,6 +352,7 @@ export {
|
|
|
335
352
|
createDisplay,
|
|
336
353
|
fitLine,
|
|
337
354
|
renderDot,
|
|
355
|
+
ticketLabel,
|
|
338
356
|
wrapLine
|
|
339
357
|
};
|
|
340
358
|
//# sourceMappingURL=display.js.map
|
package/dist/display.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/display.ts"],
|
|
4
|
-
"sourcesContent": ["export type TicketStatus =\n | 'pending'\n | 'in-progress'\n | 'awaiting-merge'\n | 'done'\n | 'failed'\n\nexport type DisplayTicket = {\n readonly num:number\n readonly filename:string\n readonly feature?:string\n status:TicketStatus\n}\n\nexport type WaveState = {\n readonly waveNum:number\n readonly totalWaves:number\n readonly tickets:Array<DisplayTicket>\n}\n\nexport type DisplayConfig = {\n readonly stream:NodeJS.WriteStream\n}\n\n/**\n * Progress across the whole run, not just the current wave.\n * `completed` counts tickets done before the wave started;\n * tickets that finish during the wave are added on top.\n */\nexport type RunProgress = {\n readonly completed:number\n readonly total:number\n}\n\nexport type Display = {\n startWave(\n waveNum:number,\n totalWaves:number,\n tickets:ReadonlyArray<{\n num:number\n filename:string\n feature?:string\n }>,\n progress?:RunProgress,\n remaining?:ReadonlyArray<{\n num:number\n filename:string\n feature?:string\n }>,\n ):void\n setTicketStatus(\n num:number,\n status:TicketStatus,\n feature?:string,\n ):void\n log(message:string):void\n cleanup():void\n}\n\nconst ESC = '\\x1b['\n\nexport const ansi = {\n cursorUp (n:number):string {\n return `${ESC}${n}A`\n },\n cursorTo (col:number):string {\n return `${ESC}${col}G`\n },\n eraseLine ():string {\n return `${ESC}2K`\n },\n eraseLines (n:number):string {\n let out = ''\n for (let i = 0; i < n; i++) {\n out += ansi.eraseLine()\n if (i < n - 1) out += ansi.cursorUp(1)\n }\n return out\n },\n cursorHide ():string {\n return `${ESC}?25l`\n },\n cursorShow ():string {\n return `${ESC}?25h`\n },\n} as const\n\nexport const color = {\n green (text:string):string {\n return `${ESC}32m${text}${ESC}0m`\n },\n yellow (text:string):string {\n return `${ESC}33m${text}${ESC}0m`\n },\n red (text:string):string {\n return `${ESC}31m${text}${ESC}0m`\n },\n cyan (text:string):string {\n return `${ESC}36m${text}${ESC}0m`\n },\n dim (text:string):string {\n return `${ESC}2m${text}${ESC}0m`\n },\n} as const\n\nfunction ticketLabel (\n ticket:{ num:number; filename:string; feature?:string },\n):string {\n if (ticket.feature) {\n const padded = String(ticket.num).padStart(2, '0')\n return `${ticket.feature}/${padded}`\n }\n return ticket.filename\n}\n\nconst FILLED_DOT = '\u25CF'\nconst EMPTY_DOT = '\u25CB'\nconst SEPARATOR_WIDTH = 50\nconst DEFAULT_COLUMNS = 80\nconst MIN_LOG_ROWS = 1\nconst MAX_MESSAGE_BUFFER = 200\n\n// eslint-disable-next-line no-control-regex\nconst ANSI_RE = /\\x1b\\[[0-9;?]*[A-Za-z]/g\n\n/**\n * Clamp a line to `width` visible columns so it can never wrap.\n * A wrapped line occupies more terminal rows than `render`\n * counts, which leaves stale rows behind on the next erase.\n */\nexport function fitLine (line:string, width:number):string {\n const visible = line.replace(ANSI_RE, '')\n if (visible.length <= width) return line\n return visible.slice(0, width)\n}\n\n/**\n * Split a line into rows of at most `width` visible columns,\n * the way a terminal would wrap it. Short lines keep their\n * ANSI styling; long lines are wrapped on visible text only,\n * so `render` can count every row it emits.\n */\nexport function wrapLine (line:string, width:number):Array<string> {\n const visible = line.replace(ANSI_RE, '')\n if (visible.length <= width) return [line]\n const rows:Array<string> = []\n for (let i = 0; i < visible.length; i += width) {\n rows.push(visible.slice(i, i + width))\n }\n return rows\n}\n\nexport function renderDot (status:TicketStatus):string {\n switch (status) {\n case 'pending':\n return color.dim(EMPTY_DOT)\n case 'in-progress':\n return color.yellow(FILLED_DOT)\n // The agent and its test gate are finished; the branch\n // waits for the rest of the wave before it is merged.\n case 'awaiting-merge':\n return color.cyan(FILLED_DOT)\n case 'done':\n return color.green(FILLED_DOT)\n case 'failed':\n return color.red(FILLED_DOT)\n }\n}\n\nexport class StatusDisplay implements Display {\n private wave:WaveState|null = null\n private progress:RunProgress|null = null\n private remaining:Array<DisplayTicket> = []\n private messages:Array<string> = []\n private renderedLines:number = 0\n private config:DisplayConfig\n private viewportStart:number = 0\n private resizeListener:(() => void)|null = null\n\n constructor (config?:Readonly<Partial<DisplayConfig>>) {\n this.config = {\n stream:config?.stream ?? process.stdout,\n }\n }\n\n private getAvailableRows ():number {\n const termHeight = this.config.stream.rows ?? 24\n const overhead = 2 + MIN_LOG_ROWS\n const available = termHeight - overhead\n return Math.max(1, available)\n }\n\n startWave (\n waveNum:number,\n totalWaves:number,\n tickets:Array<Readonly<{\n num:number\n filename:string\n feature?:string\n }>>,\n progress?:RunProgress,\n remaining?:ReadonlyArray<{\n num:number\n filename:string\n feature?:string\n }>,\n ):void {\n this.wave = {\n waveNum,\n totalWaves,\n tickets:tickets.map(t => ({\n ...t,\n status:'pending' as const,\n })),\n }\n this.remaining = (remaining ?? []).map(t => ({\n ...t,\n status:'pending' as const,\n }))\n this.progress = progress ?? null\n this.messages = []\n this.viewportStart = 0\n this.config.stream.write(ansi.cursorHide())\n\n if (this.config.stream.isTTY) {\n if (this.resizeListener) {\n this.config.stream.removeListener(\n 'resize', this.resizeListener,\n )\n }\n this.resizeListener = () => this.render()\n this.config.stream.on(\n 'resize', this.resizeListener,\n )\n }\n\n this.render()\n }\n\n setTicketStatus (\n num:number,\n status:TicketStatus,\n feature?:string,\n ):void {\n if (!this.wave) return\n\n const ticket = feature ?\n this.wave.tickets.find(\n t => t.num === num && t.feature === feature,\n ) :\n this.wave.tickets.find(t => t.num === num)\n if (ticket) {\n ticket.status = status\n\n if (status === 'in-progress') {\n const ticketIndex = this.wave.tickets.indexOf(ticket)\n const availableRows = this.getAvailableRows()\n const totalTickets = this.wave.tickets.length\n\n if (ticketIndex < this.viewportStart) {\n this.viewportStart = ticketIndex\n } else if (ticketIndex >= this.viewportStart + availableRows) {\n // offset by 3 to keep context visible below\n // the active ticket\n this.viewportStart = ticketIndex - availableRows + 3\n }\n this.viewportStart = Math.max(\n 0,\n Math.min(this.viewportStart, totalTickets - 1),\n )\n }\n\n this.render()\n }\n }\n\n log (message:string):void {\n // Before the first wave there is no grid to repaint,\n // so a message would otherwise be dropped. Write it\n // straight to the stream instead. A later render\n // never erases these rows, since renderedLines is 0.\n if (!this.wave) {\n this.config.stream.write(message + '\\n')\n return\n }\n for (const line of message.split('\\n')) {\n this.messages.push(line)\n }\n while (this.messages.length > MAX_MESSAGE_BUFFER) {\n this.messages.shift()\n }\n this.render()\n }\n\n cleanup ():void {\n // Remove resize listener if it exists\n if (this.resizeListener && this.config.stream.isTTY) {\n this.config.stream.removeListener('resize', this.resizeListener)\n }\n this.config.stream.write(ansi.cursorShow())\n }\n\n private render ():void {\n if (!this.wave) return\n\n let output = ''\n\n if (this.renderedLines > 0) {\n output += ansi.eraseLines(this.renderedLines + 1)\n }\n\n const doneInWave = this.wave.tickets.filter(\n t => t.status === 'done',\n ).length\n const completed = doneInWave + (this.progress?.completed ?? 0)\n const total = this.progress?.total ?? this.wave.tickets.length\n const pct = total === 0 ? 0 : Math.round((completed / total) * 100)\n const width = Math.max(\n 1,\n (this.config.stream.columns ?? DEFAULT_COLUMNS) - 1,\n )\n\n output += `Wave ${this.wave.waveNum}/` +\n `${this.wave.totalWaves} - ` +\n `${pct}% complete\\n`\n\n // Pagination logic\n const availableRows = this.getAvailableRows()\n const tickets:ReadonlyArray<DisplayTicket> = [\n ...this.wave.tickets,\n ...this.remaining,\n ]\n let renderedTicketCount = 0\n\n if (tickets.length <= availableRows) {\n // All tickets fit, no pagination\n for (const ticket of tickets) {\n const dot = renderDot(ticket.status)\n const name = fitLine(\n ticketLabel(ticket), width - 2,\n )\n output += `${dot} ${name}\\n`\n }\n renderedTicketCount = tickets.length\n } else {\n // Need pagination -- iterative clamp\n this.viewportStart = Math.max(\n 0,\n Math.min(\n this.viewportStart,\n tickets.length - 1,\n ),\n )\n\n let hasMoreAbove = this.viewportStart > 0\n let tentativeCount =\n availableRows - (hasMoreAbove ? 1 : 0)\n let hasMoreBelow =\n this.viewportStart + tentativeCount <\n tickets.length\n let ticketsToShow = Math.max(\n 0,\n tentativeCount - (hasMoreBelow ? 1 : 0),\n )\n\n const maxStart = Math.max(\n 0,\n tickets.length - Math.max(1, ticketsToShow),\n )\n if (this.viewportStart > maxStart) {\n this.viewportStart = maxStart\n hasMoreAbove = this.viewportStart > 0\n tentativeCount = availableRows - (hasMoreAbove ? 1 : 0)\n hasMoreBelow = (\n this.viewportStart +\n tentativeCount < tickets.length\n )\n ticketsToShow = Math.max(\n 0,\n tentativeCount - (hasMoreBelow ? 1 : 0)\n )\n }\n\n // Show \"more above\" indicator if needed\n if (hasMoreAbove) {\n const moreCount = this.viewportStart\n output += ` ... ${moreCount} more above\\n`\n }\n\n // Get visible tickets and render them\n const visibleTickets = tickets.slice(\n this.viewportStart,\n this.viewportStart + ticketsToShow,\n )\n\n for (const ticket of visibleTickets) {\n const dot = renderDot(ticket.status)\n const name = fitLine(\n ticketLabel(ticket), width - 2,\n )\n output += `${dot} ${name}\\n`\n }\n\n // Show \"more below\" indicator if needed\n if (hasMoreBelow) {\n const moreCount = (\n tickets.length -\n (this.viewportStart + ticketsToShow)\n )\n output += ` ... ${moreCount} more below\\n`\n }\n\n renderedTicketCount = (hasMoreAbove ? 1 : 0) +\n visibleTickets.length +\n (hasMoreBelow ? 1 : 0)\n }\n\n output += '-'.repeat(Math.min(SEPARATOR_WIDTH, width)) + '\\n'\n\n const termHeight = this.config.stream.rows ?? 24\n const logRows = Math.max(\n 0,\n termHeight - 3 - renderedTicketCount,\n )\n const wrapped = this.messages.flatMap(\n msg => wrapLine(msg, width),\n )\n const visibleMessages = wrapped.slice(-logRows)\n for (const row of visibleMessages) {\n output += `${row}\\n`\n }\n const emptyRows = logRows - visibleMessages.length\n for (let i = 0; i < emptyRows; i++) {\n output += '\\n'\n }\n\n this.renderedLines = 2 + renderedTicketCount + logRows\n this.config.stream.write(output)\n }\n}\n\nexport class PlainDisplay implements Display {\n startWave (\n waveNum:number,\n _totalWaves:number,\n tickets:ReadonlyArray<{\n num:number\n filename:string\n feature?:string\n }>,\n progress?:RunProgress,\n _remaining?:ReadonlyArray<{\n num:number\n filename:string\n feature?:string\n }>,\n ):void {\n const pct = progress && progress.total > 0 ?\n ` (${progress.completed}/${progress.total} done)` :\n ''\n console.log(\n `\\nWave ${waveNum}${pct}: ` +\n tickets.map(t => ticketLabel(t)).join(', ')\n )\n }\n\n setTicketStatus (\n _num:number,\n _status:TicketStatus,\n _feature?:string,\n ):void {\n // no-op: plain mode reports via log()\n }\n\n log (message:string):void {\n console.log(message)\n }\n\n cleanup ():void {\n // no-op\n }\n}\n\nexport function createDisplay (stream?:NodeJS.WriteStream):Display {\n const s = stream ?? process.stdout\n if (s.isTTY) {\n return new StatusDisplay({ stream:s })\n }\n\n return new PlainDisplay()\n}\n"],
|
|
5
|
-
"mappings": ";;
|
|
4
|
+
"sourcesContent": ["export type TicketStatus =\n | 'pending'\n | 'in-progress'\n | 'awaiting-merge'\n | 'done'\n | 'failed'\n | 'blocked'\n\nexport type DisplayTicket = {\n readonly num:number\n readonly filename:string\n readonly feature?:string\n status:TicketStatus\n blockedBy?:ReadonlyArray<string>\n}\n\nexport type TicketRef = {\n readonly num:number\n readonly filename:string\n readonly feature?:string\n}\n\nexport type WaveState = {\n readonly waveNum:number\n readonly totalWaves:number\n}\n\nexport type DisplayConfig = {\n readonly stream:NodeJS.WriteStream\n}\n\n/**\n * Progress across the whole run, not just the current wave.\n * `completed` counts this run's tickets done before the wave\n * started; `total` is the size of this run's ticket list. Only\n * PlainDisplay prints it; StatusDisplay counts from its own list.\n */\nexport type RunProgress = {\n readonly completed:number\n readonly total:number\n}\n\nexport type Display = {\n /**\n * Called once at the start of the run, fixes the ticket list\n * for the entire session.\n */\n startRun(tickets:ReadonlyArray<TicketRef>):void\n startWave(\n waveNum:number,\n totalWaves:number,\n tickets:ReadonlyArray<TicketRef>,\n progress?:RunProgress,\n ):void\n setTicketStatus(\n num:number,\n status:TicketStatus,\n feature?:string,\n ):void\n /**\n * Mark a ticket as blocked; `blockedBy` holds display labels\n * of the upstream tickets that failed or need a human.\n */\n setBlocked(\n num:number,\n feature:string|undefined,\n blockedBy:ReadonlyArray<string>,\n ):void\n log(message:string):void\n cleanup():void\n}\n\nconst ESC = '\\x1b['\n\nexport const ansi = {\n cursorUp (n:number):string {\n return `${ESC}${n}A`\n },\n cursorTo (col:number):string {\n return `${ESC}${col}G`\n },\n eraseLine ():string {\n return `${ESC}2K`\n },\n eraseLines (n:number):string {\n let out = ''\n for (let i = 0; i < n; i++) {\n out += ansi.eraseLine()\n if (i < n - 1) out += ansi.cursorUp(1)\n }\n return out\n },\n cursorHide ():string {\n return `${ESC}?25l`\n },\n cursorShow ():string {\n return `${ESC}?25h`\n },\n} as const\n\nexport const color = {\n green (text:string):string {\n return `${ESC}32m${text}${ESC}0m`\n },\n yellow (text:string):string {\n return `${ESC}33m${text}${ESC}0m`\n },\n red (text:string):string {\n return `${ESC}31m${text}${ESC}0m`\n },\n cyan (text:string):string {\n return `${ESC}36m${text}${ESC}0m`\n },\n dim (text:string):string {\n return `${ESC}2m${text}${ESC}0m`\n },\n} as const\n\nexport function ticketLabel (\n ticket:{ num:number; filename:string; feature?:string },\n):string {\n if (ticket.feature) {\n const padded = String(ticket.num).padStart(2, '0')\n return `${ticket.feature}/${padded}`\n }\n return ticket.filename\n}\n\nconst FILLED_DOT = '\u25CF'\nconst EMPTY_DOT = '\u25CB'\nconst SEPARATOR_WIDTH = 50\nconst DEFAULT_COLUMNS = 80\nconst MIN_LOG_ROWS = 1\nconst MAX_MESSAGE_BUFFER = 200\n\n// eslint-disable-next-line no-control-regex\nconst ANSI_RE = /\\x1b\\[[0-9;?]*[A-Za-z]/g\n\nfunction rowText (ticket:DisplayTicket):string {\n const label = ticketLabel(ticket)\n if (ticket.status !== 'blocked' || !ticket.blockedBy?.length) {\n return label\n }\n return `${label} (blocked by ${ticket.blockedBy.join(', ')})`\n}\n\n/**\n * Clamp a line to `width` visible columns so it can never wrap.\n * A wrapped line occupies more terminal rows than `render`\n * counts, which leaves stale rows behind on the next erase.\n */\nexport function fitLine (line:string, width:number):string {\n const visible = line.replace(ANSI_RE, '')\n if (visible.length <= width) return line\n return visible.slice(0, width)\n}\n\n/**\n * Split a line into rows of at most `width` visible columns,\n * the way a terminal would wrap it. Short lines keep their\n * ANSI styling; long lines are wrapped on visible text only,\n * so `render` can count every row it emits.\n */\nexport function wrapLine (line:string, width:number):Array<string> {\n const visible = line.replace(ANSI_RE, '')\n if (visible.length <= width) return [line]\n const rows:Array<string> = []\n for (let i = 0; i < visible.length; i += width) {\n rows.push(visible.slice(i, i + width))\n }\n return rows\n}\n\nexport function renderDot (status:TicketStatus):string {\n switch (status) {\n case 'pending':\n return color.dim(EMPTY_DOT)\n case 'in-progress':\n return color.yellow(FILLED_DOT)\n // The agent and its test gate are finished; the branch\n // waits for the rest of the wave before it is merged. It\n // stays cyan through the merge and any conflict respawn.\n case 'awaiting-merge':\n return color.cyan(FILLED_DOT)\n case 'done':\n return color.green(FILLED_DOT)\n case 'failed':\n return color.red(FILLED_DOT)\n case 'blocked':\n // Ticket cannot run this session because an upstream\n // ticket failed or needs a human.\n return color.dim(FILLED_DOT)\n }\n}\n\nexport class StatusDisplay implements Display {\n private wave:WaveState|null = null\n private tickets:Array<DisplayTicket> = []\n private hasRun:boolean = false\n private messages:Array<string> = []\n private renderedLines:number = 0\n private config:DisplayConfig\n private viewportStart:number = 0\n private resizeListener:(() => void)|null = null\n\n constructor (config?:Readonly<Partial<DisplayConfig>>) {\n this.config = {\n stream:config?.stream ?? process.stdout,\n }\n }\n\n private getAvailableRows ():number {\n const termHeight = this.config.stream.rows ?? 24\n const overhead = 2 + MIN_LOG_ROWS\n const available = termHeight - overhead\n return Math.max(1, available)\n }\n\n private findTicket (\n num:number,\n feature?:string,\n ):DisplayTicket|null {\n const found = feature ?\n this.tickets.find(\n t => t.num === num && t.feature === feature,\n ) :\n this.tickets.find(t => t.num === num)\n return found ?? null\n }\n\n startRun (tickets:ReadonlyArray<TicketRef>):void {\n this.tickets = tickets.map(t => ({\n ...t,\n status:'pending' as const,\n }))\n this.hasRun = true\n this.viewportStart = 0\n }\n\n startWave (\n waveNum:number,\n totalWaves:number,\n tickets:ReadonlyArray<TicketRef>,\n _progress?:RunProgress,\n ):void {\n if (!this.hasRun) {\n this.startRun(tickets)\n }\n\n this.wave = {\n waveNum,\n totalWaves,\n }\n this.messages = []\n this.viewportStart = 0\n this.config.stream.write(ansi.cursorHide())\n\n if (this.config.stream.isTTY) {\n if (this.resizeListener) {\n this.config.stream.removeListener(\n 'resize', this.resizeListener,\n )\n }\n this.resizeListener = () => this.render()\n this.config.stream.on(\n 'resize', this.resizeListener,\n )\n }\n\n this.render()\n }\n\n setTicketStatus (\n num:number,\n status:TicketStatus,\n feature?:string,\n ):void {\n const ticket = this.findTicket(num, feature)\n if (!ticket) return\n\n ticket.status = status\n\n if (status === 'in-progress') {\n const ticketIndex = this.tickets.indexOf(ticket)\n const availableRows = this.getAvailableRows()\n const totalTickets = this.tickets.length\n\n if (ticketIndex < this.viewportStart) {\n this.viewportStart = ticketIndex\n } else if (ticketIndex >= this.viewportStart + availableRows) {\n // offset by 3 to keep context visible below\n // the active ticket\n this.viewportStart = ticketIndex - availableRows + 3\n }\n this.viewportStart = Math.max(\n 0,\n Math.min(this.viewportStart, totalTickets - 1),\n )\n }\n\n this.render()\n }\n\n setBlocked (\n num:number,\n feature:string|undefined,\n blockedBy:ReadonlyArray<string>,\n ):void {\n const ticket = this.findTicket(num, feature)\n if (!ticket) return\n\n ticket.status = 'blocked'\n ticket.blockedBy = [...blockedBy]\n this.render()\n }\n\n log (message:string):void {\n // Before the first wave there is no grid to repaint,\n // so a message would otherwise be dropped. Write it\n // straight to the stream instead. A later render\n // never erases these rows, since renderedLines is 0.\n if (!this.wave) {\n this.config.stream.write(message + '\\n')\n return\n }\n for (const line of message.split('\\n')) {\n this.messages.push(line)\n }\n while (this.messages.length > MAX_MESSAGE_BUFFER) {\n this.messages.shift()\n }\n this.render()\n }\n\n cleanup ():void {\n // Remove resize listener if it exists\n if (this.resizeListener && this.config.stream.isTTY) {\n this.config.stream.removeListener('resize', this.resizeListener)\n }\n this.config.stream.write(ansi.cursorShow())\n }\n\n private render ():void {\n if (!this.wave) return\n\n let output = ''\n\n if (this.renderedLines > 0) {\n output += ansi.eraseLines(this.renderedLines + 1)\n }\n\n const done = this.tickets.filter(\n t => t.status === 'done',\n ).length\n const total = this.tickets.length\n const pct = total === 0 ? 0 : Math.round((done / total) * 100)\n const width = Math.max(\n 1,\n (this.config.stream.columns ?? DEFAULT_COLUMNS) - 1,\n )\n\n const header = `Wave ${this.wave.waveNum}/` +\n `${this.wave.totalWaves} - ` +\n `${done}/${total} done - ${pct}%`\n output += fitLine(header, width) + '\\n'\n\n // Pagination logic\n const availableRows = this.getAvailableRows()\n let renderedTicketCount = 0\n\n if (this.tickets.length <= availableRows) {\n // All tickets fit, no pagination\n for (const ticket of this.tickets) {\n const dot = renderDot(ticket.status)\n const text = rowText(ticket)\n const name = fitLine(text, width - 2)\n output += `${dot} ${name}\\n`\n }\n renderedTicketCount = this.tickets.length\n } else {\n // Need pagination -- iterative clamp\n this.viewportStart = Math.max(\n 0,\n Math.min(\n this.viewportStart,\n this.tickets.length - 1,\n ),\n )\n\n let hasMoreAbove = this.viewportStart > 0\n let tentativeCount =\n availableRows - (hasMoreAbove ? 1 : 0)\n let hasMoreBelow =\n this.viewportStart + tentativeCount <\n this.tickets.length\n let ticketsToShow = Math.max(\n 0,\n tentativeCount - (hasMoreBelow ? 1 : 0),\n )\n\n const maxStart = Math.max(\n 0,\n this.tickets.length - Math.max(1, ticketsToShow),\n )\n if (this.viewportStart > maxStart) {\n this.viewportStart = maxStart\n hasMoreAbove = this.viewportStart > 0\n tentativeCount = availableRows - (hasMoreAbove ? 1 : 0)\n hasMoreBelow = (\n this.viewportStart +\n tentativeCount < this.tickets.length\n )\n ticketsToShow = Math.max(\n 0,\n tentativeCount - (hasMoreBelow ? 1 : 0)\n )\n }\n\n // Show \"more above\" indicator if needed\n if (hasMoreAbove) {\n const moreCount = this.viewportStart\n output += ` ... ${moreCount} more above\\n`\n }\n\n // Get visible tickets and render them\n const visibleTickets = this.tickets.slice(\n this.viewportStart,\n this.viewportStart + ticketsToShow,\n )\n\n for (const ticket of visibleTickets) {\n const dot = renderDot(ticket.status)\n const text = rowText(ticket)\n const name = fitLine(text, width - 2)\n output += `${dot} ${name}\\n`\n }\n\n // Show \"more below\" indicator if needed\n if (hasMoreBelow) {\n const moreCount = (\n this.tickets.length -\n (this.viewportStart + ticketsToShow)\n )\n output += ` ... ${moreCount} more below\\n`\n }\n\n renderedTicketCount = (hasMoreAbove ? 1 : 0) +\n visibleTickets.length +\n (hasMoreBelow ? 1 : 0)\n }\n\n output += '-'.repeat(Math.min(SEPARATOR_WIDTH, width)) + '\\n'\n\n const termHeight = this.config.stream.rows ?? 24\n const logRows = Math.max(\n 0,\n termHeight - 3 - renderedTicketCount,\n )\n const wrapped = this.messages.flatMap(\n msg => wrapLine(msg, width),\n )\n const visibleMessages = wrapped.slice(-logRows)\n for (const row of visibleMessages) {\n output += `${row}\\n`\n }\n const emptyRows = logRows - visibleMessages.length\n for (let i = 0; i < emptyRows; i++) {\n output += '\\n'\n }\n\n this.renderedLines = 2 + renderedTicketCount + logRows\n this.config.stream.write(output)\n }\n}\n\nexport class PlainDisplay implements Display {\n startRun (_tickets:ReadonlyArray<TicketRef>):void {\n // no-op: plain mode has no fixed list\n }\n\n startWave (\n waveNum:number,\n _totalWaves:number,\n tickets:ReadonlyArray<TicketRef>,\n progress?:RunProgress,\n ):void {\n const pct = progress && progress.total > 0 ?\n ` (${progress.completed}/${progress.total} done)` :\n ''\n console.log(\n `\\nWave ${waveNum}${pct}: ` +\n tickets.map(t => ticketLabel(t)).join(', ')\n )\n }\n\n setTicketStatus (\n _num:number,\n _status:TicketStatus,\n _feature?:string,\n ):void {\n // no-op: plain mode reports via log()\n }\n\n setBlocked (\n _num:number,\n _feature:string|undefined,\n _blockedBy:ReadonlyArray<string>,\n ):void {\n // no-op: plain mode has no fixed list\n }\n\n log (message:string):void {\n console.log(message)\n }\n\n cleanup ():void {\n // no-op\n }\n}\n\nexport function createDisplay (stream?:NodeJS.WriteStream):Display {\n const s = stream ?? process.stdout\n if (s.isTTY) {\n return new StatusDisplay({ stream:s })\n }\n\n return new PlainDisplay()\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|