recomposable 1.1.2 → 1.1.4
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 +8 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +607 -80
- package/dist/index.js.map +1 -1
- package/dist/lib/docker.d.ts +7 -2
- package/dist/lib/docker.js +124 -13
- package/dist/lib/docker.js.map +1 -1
- package/dist/lib/renderer.d.ts +4 -1
- package/dist/lib/renderer.js +272 -130
- package/dist/lib/renderer.js.map +1 -1
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +27 -0
- package/dist/lib/state.js.map +1 -1
- package/dist/lib/types.d.ts +33 -3
- package/package.json +1 -1
- package/screenshots/exec-view.png +0 -0
- package/screenshots/list-view.png +0 -0
- package/screenshots/logs-view.png +0 -0
package/dist/lib/renderer.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CLEAR_EOS = exports.CLEAR_EOL = void 0;
|
|
3
4
|
exports.visLen = visLen;
|
|
4
5
|
exports.padVisible = padVisible;
|
|
5
6
|
exports.padVisibleStart = padVisibleStart;
|
|
@@ -13,6 +14,7 @@ exports.renderLegend = renderLegend;
|
|
|
13
14
|
exports.renderListView = renderListView;
|
|
14
15
|
exports.truncateLine = truncateLine;
|
|
15
16
|
exports.highlightSearchInLine = highlightSearchInLine;
|
|
17
|
+
exports.wrapPlainLine = wrapPlainLine;
|
|
16
18
|
exports.renderLogView = renderLogView;
|
|
17
19
|
exports.renderExecView = renderExecView;
|
|
18
20
|
const state_1 = require("./state");
|
|
@@ -46,7 +48,22 @@ function padVisibleStart(str, width) {
|
|
|
46
48
|
const pad = Math.max(0, width - visLen(str));
|
|
47
49
|
return ' '.repeat(pad) + str;
|
|
48
50
|
}
|
|
51
|
+
exports.CLEAR_EOL = `${ESC}K`;
|
|
52
|
+
exports.CLEAR_EOS = `${ESC}J`;
|
|
49
53
|
const PATTERN_COLORS = [FG_YELLOW, FG_RED, FG_CYAN, FG_WHITE];
|
|
54
|
+
function logLineColor(line, patterns) {
|
|
55
|
+
let color = null;
|
|
56
|
+
for (let pi = 0; pi < patterns.length; pi++) {
|
|
57
|
+
const group = Array.isArray(patterns[pi]) ? patterns[pi] : [patterns[pi]];
|
|
58
|
+
if (group.some(p => line.includes(p))) {
|
|
59
|
+
color = PATTERN_COLORS[pi % PATTERN_COLORS.length];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return color;
|
|
63
|
+
}
|
|
64
|
+
// Cached separator line — recomputed only when terminal width changes
|
|
65
|
+
let cachedSepColumns = 0;
|
|
66
|
+
let cachedSepLine = '';
|
|
50
67
|
function patternLabel(pattern) {
|
|
51
68
|
return pattern.replace(/^[\[\(\{<]/, '').replace(/[\]\)\}>]$/, '');
|
|
52
69
|
}
|
|
@@ -76,7 +93,14 @@ function relativeTime(ts) {
|
|
|
76
93
|
return `${DIM}${days}d ago${RESET}`;
|
|
77
94
|
}
|
|
78
95
|
function clearScreen() {
|
|
79
|
-
return `${ESC}
|
|
96
|
+
return `${ESC}H${ESC}?25l`;
|
|
97
|
+
}
|
|
98
|
+
function separatorLine(columns) {
|
|
99
|
+
if (columns !== cachedSepColumns) {
|
|
100
|
+
cachedSepColumns = columns;
|
|
101
|
+
cachedSepLine = ` ${FG_GRAY}${'\u2500'.repeat(Math.max(0, columns - 2))}${RESET}`;
|
|
102
|
+
}
|
|
103
|
+
return cachedSepLine;
|
|
80
104
|
}
|
|
81
105
|
function showCursor() {
|
|
82
106
|
return `${ESC}?25h`;
|
|
@@ -133,12 +157,19 @@ function formatMem(bytes) {
|
|
|
133
157
|
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}G`;
|
|
134
158
|
}
|
|
135
159
|
function renderLegend(opts = {}) {
|
|
136
|
-
const { logPanelActive = false, logsScrollMode = false, noCacheActive = false, watchActive = false, execMode = false, execInline = false } = opts;
|
|
160
|
+
const { logPanelActive = false, logsScrollMode = false, noCacheActive = false, noDepsActive = false, watchActive = false, execMode = false, execInline = false, worktreePickerActive = false } = opts;
|
|
137
161
|
const item = (text, active) => {
|
|
138
162
|
if (active)
|
|
139
163
|
return `${BG_HIGHLIGHT} ${text} ${RESET}`;
|
|
140
164
|
return `${DIM}${text}${RESET}`;
|
|
141
165
|
};
|
|
166
|
+
if (worktreePickerActive) {
|
|
167
|
+
return [
|
|
168
|
+
item('[Esc] cancel', false),
|
|
169
|
+
item('[Enter] switch', false),
|
|
170
|
+
item('[j/k] navigate', false),
|
|
171
|
+
].join(' ');
|
|
172
|
+
}
|
|
142
173
|
if (execMode) {
|
|
143
174
|
return [
|
|
144
175
|
item('[Esc] back', false),
|
|
@@ -159,8 +190,9 @@ function renderLegend(opts = {}) {
|
|
|
159
190
|
].join(' ');
|
|
160
191
|
}
|
|
161
192
|
if (logsScrollMode) {
|
|
193
|
+
const hasSearch = opts.hasLogSearch || false;
|
|
162
194
|
return [
|
|
163
|
-
item('[Esc] back', false),
|
|
195
|
+
item(hasSearch ? '[Esc] clear search' : '[Esc] back', false),
|
|
164
196
|
item('[j/k] scroll', false),
|
|
165
197
|
item('[G] bottom', false),
|
|
166
198
|
item('[gg] top', false),
|
|
@@ -176,9 +208,11 @@ function renderLegend(opts = {}) {
|
|
|
176
208
|
item('Sto[P]', false),
|
|
177
209
|
item('[W]atch', watchActive),
|
|
178
210
|
item('[N]o cache', noCacheActive),
|
|
211
|
+
item('n[O] deps', noDepsActive),
|
|
179
212
|
item('[e]xec', false),
|
|
180
213
|
item('[F]ull logs', false),
|
|
181
214
|
item('[L]og panel', logPanelActive),
|
|
215
|
+
item('Switch [t]ree', false),
|
|
182
216
|
item('[Q]uit', false),
|
|
183
217
|
].join(' ');
|
|
184
218
|
}
|
|
@@ -186,6 +220,7 @@ function renderListView(state) {
|
|
|
186
220
|
const columns = process.stdout.columns ?? 80;
|
|
187
221
|
const rows = process.stdout.rows ?? 24;
|
|
188
222
|
const patterns = state.config.logScanPatterns || [];
|
|
223
|
+
const sep = separatorLine(columns);
|
|
189
224
|
const buf = [];
|
|
190
225
|
for (const line of LOGO) {
|
|
191
226
|
buf.push(line);
|
|
@@ -193,13 +228,41 @@ function renderListView(state) {
|
|
|
193
228
|
const watchActive = state.watching.size > 0;
|
|
194
229
|
const help = state.execActive
|
|
195
230
|
? renderLegend({ execInline: true })
|
|
196
|
-
:
|
|
197
|
-
|
|
231
|
+
: state.worktreePickerActive
|
|
232
|
+
? renderLegend({ worktreePickerActive: true })
|
|
233
|
+
: renderLegend({ logPanelActive: state.showBottomLogs, noCacheActive: state.noCache, noDepsActive: state.noDeps, watchActive });
|
|
234
|
+
buf.push(sep);
|
|
198
235
|
buf.push(` ${help}`);
|
|
236
|
+
// Single column header row (not repeated per group)
|
|
237
|
+
let colHeader = `${DIM} ${'SERVICE'.padEnd(24)} `;
|
|
238
|
+
colHeader += `${'STATUS'.padEnd(22)} ${'BUILT'.padEnd(12)} ${'RESTARTED'.padEnd(12)}`;
|
|
239
|
+
for (const p of patterns)
|
|
240
|
+
colHeader += patternLabel(Array.isArray(p) ? p[0] : p).padStart(5) + ' ';
|
|
241
|
+
colHeader += ` ${'CPU/MEM'.padStart(16)} ${'PORTS'.padEnd(14)}`;
|
|
242
|
+
if (state.showWorktreeColumn)
|
|
243
|
+
colHeader += ` ${'WORKTREE'.padEnd(15)}`;
|
|
244
|
+
buf.push(colHeader + RESET);
|
|
199
245
|
const headerHeight = buf.length;
|
|
200
246
|
const bottomBuf = [];
|
|
201
|
-
if (state.
|
|
202
|
-
|
|
247
|
+
if (state.worktreePickerActive) {
|
|
248
|
+
const selEntry = state.flatList[state.cursor];
|
|
249
|
+
if (selEntry) {
|
|
250
|
+
bottomBuf.push(sep);
|
|
251
|
+
bottomBuf.push(` ${FG_CYAN}switch worktree ${BOLD}${selEntry.service}${RESET}`);
|
|
252
|
+
bottomBuf.push(` ${DIM}j/k navigate Enter confirm Esc cancel${RESET}`);
|
|
253
|
+
for (let wi = 0; wi < state.worktreePickerEntries.length; wi++) {
|
|
254
|
+
const wt = state.worktreePickerEntries[wi];
|
|
255
|
+
const isSelected = wi === state.worktreePickerCursor;
|
|
256
|
+
const prefix = isSelected ? `${REVERSE}` : '';
|
|
257
|
+
const suffix = isSelected ? `${RESET}` : '';
|
|
258
|
+
const currentTag = (state.worktreePickerCurrentPath && state.worktreePickerCurrentPath === wt.path)
|
|
259
|
+
? ` ${DIM}(current)${RESET}` : '';
|
|
260
|
+
bottomBuf.push(` ${prefix} ${wt.branch} ${DIM}${wt.path}${RESET}${currentTag}${suffix}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
else if (state.execActive && state.execService) {
|
|
265
|
+
bottomBuf.push(sep);
|
|
203
266
|
const runningIndicator = state.execChild ? `${FG_YELLOW}running${RESET}` : `${FG_GREEN}ready${RESET}`;
|
|
204
267
|
const cwdInfo = state.execCwd ? ` ${DIM}${state.execCwd}${RESET}` : '';
|
|
205
268
|
bottomBuf.push(` ${FG_CYAN}exec ${BOLD}${state.execService}${RESET} ${runningIndicator}${cwdInfo}`);
|
|
@@ -217,7 +280,7 @@ function renderListView(state) {
|
|
|
217
280
|
// Check for cascade progress
|
|
218
281
|
const cascade = state.cascading.get(sk);
|
|
219
282
|
if (cascade) {
|
|
220
|
-
bottomBuf.push(
|
|
283
|
+
bottomBuf.push(sep);
|
|
221
284
|
bottomBuf.push(` ${FG_YELLOW}cascading ${BOLD}${selEntry.service}${RESET}`);
|
|
222
285
|
for (let si = 0; si < cascade.steps.length; si++) {
|
|
223
286
|
const step = cascade.steps[si];
|
|
@@ -240,22 +303,36 @@ function renderListView(state) {
|
|
|
240
303
|
const info = state.bottomLogLines.get(sk);
|
|
241
304
|
if (info) {
|
|
242
305
|
if (!cascade) {
|
|
243
|
-
bottomBuf.push(
|
|
306
|
+
bottomBuf.push(sep);
|
|
244
307
|
}
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
308
|
+
const isFailed = info.action === 'build_failed' || info.action === 'restart_failed' || info.action === 'stop_failed' || info.action === 'start_failed' || info.action === 'switch_failed';
|
|
309
|
+
const actionColor = isFailed ? FG_RED
|
|
310
|
+
: info.action === 'rebuilding' || info.action === 'restarting' || info.action === 'stopping' || info.action === 'starting' || info.action === 'cascading' || info.action === 'switching' ? FG_YELLOW
|
|
311
|
+
: info.action === 'watching' ? FG_CYAN : FG_GREEN;
|
|
312
|
+
const actionLabel = isFailed ? info.action.replace('_', ' ').toUpperCase() : info.action;
|
|
313
|
+
let headerLine = ` ${actionColor}${actionLabel} ${BOLD}${info.service}${RESET}`;
|
|
248
314
|
const bq = state.bottomSearchQuery || '';
|
|
249
315
|
if (bq && !state.bottomSearchActive) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
316
|
+
if (state.bottomSearchLoading) {
|
|
317
|
+
headerLine += ` ${FG_YELLOW}searching "${bq}"...${RESET}`;
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
const totalMatches = state.bottomSearchTotalMatches;
|
|
321
|
+
headerLine += totalMatches > 0
|
|
322
|
+
? ` ${DIM}search: "${bq}" (${totalMatches} match${totalMatches !== 1 ? 'es' : ''} in full log)${RESET}`
|
|
323
|
+
: ` ${FG_RED}search: "${bq}" (no matches)${RESET}`;
|
|
324
|
+
}
|
|
254
325
|
}
|
|
255
326
|
bottomBuf.push(headerLine);
|
|
327
|
+
if (info.lines.length === 0 && info.action === 'logs') {
|
|
328
|
+
bottomBuf.push(` ${DIM}loading...${RESET}`);
|
|
329
|
+
}
|
|
256
330
|
const searchQuery = bq && !state.bottomSearchActive ? bq : '';
|
|
257
|
-
|
|
331
|
+
const maxBottomLines = state.config.bottomLogCount || 10;
|
|
332
|
+
const visibleLines = info.lines.slice(-maxBottomLines);
|
|
333
|
+
for (const line of visibleLines) {
|
|
258
334
|
let coloredLine = line.substring(0, columns - 4);
|
|
335
|
+
const lineColor = logLineColor(coloredLine, patterns) || FG_GRAY;
|
|
259
336
|
if (searchQuery) {
|
|
260
337
|
const lowerLine = coloredLine.toLowerCase();
|
|
261
338
|
const lowerQ = searchQuery.toLowerCase();
|
|
@@ -269,20 +346,13 @@ function renderListView(state) {
|
|
|
269
346
|
break;
|
|
270
347
|
}
|
|
271
348
|
result += coloredLine.substring(pos, idx);
|
|
272
|
-
result += `${REVERSE}${FG_YELLOW}${coloredLine.substring(idx, idx + searchQuery.length)}${RESET}${
|
|
349
|
+
result += `${REVERSE}${FG_YELLOW}${coloredLine.substring(idx, idx + searchQuery.length)}${RESET}${lineColor}`;
|
|
273
350
|
pos = idx + searchQuery.length;
|
|
274
351
|
}
|
|
275
352
|
coloredLine = result;
|
|
276
353
|
}
|
|
277
354
|
}
|
|
278
|
-
|
|
279
|
-
const p = patterns[pi];
|
|
280
|
-
if (coloredLine.includes(p)) {
|
|
281
|
-
const color = PATTERN_COLORS[pi % PATTERN_COLORS.length];
|
|
282
|
-
coloredLine = coloredLine.split(p).join(`${color}${p}${RESET}${FG_GRAY}`);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
bottomBuf.push(` ${FG_GRAY}${coloredLine}${RESET}`);
|
|
355
|
+
bottomBuf.push(` ${lineColor}${coloredLine}${RESET}`);
|
|
286
356
|
}
|
|
287
357
|
if (state.bottomSearchActive) {
|
|
288
358
|
bottomBuf.push(`${BOLD}/${RESET}${state.bottomSearchQuery}${BOLD}_${RESET}`);
|
|
@@ -291,100 +361,116 @@ function renderListView(state) {
|
|
|
291
361
|
}
|
|
292
362
|
}
|
|
293
363
|
const bottomHeight = bottomBuf.length;
|
|
294
|
-
|
|
364
|
+
// Pass 1: build lightweight stubs (type + index only, no text computation)
|
|
365
|
+
const stubs = [];
|
|
295
366
|
let currentGroup = -1;
|
|
296
367
|
for (let i = 0; i < state.flatList.length; i++) {
|
|
297
368
|
const entry = state.flatList[i];
|
|
298
369
|
if (entry.groupIdx !== currentGroup) {
|
|
299
370
|
currentGroup = entry.groupIdx;
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const label = ` ${BOLD}${group.label}${RESET}`;
|
|
304
|
-
if (group.error) {
|
|
305
|
-
lines.push({ type: 'header', text: `${label} ${FG_RED}(${group.error})${RESET}` });
|
|
306
|
-
}
|
|
307
|
-
else {
|
|
308
|
-
lines.push({ type: 'header', text: label });
|
|
309
|
-
}
|
|
310
|
-
let colHeader = `${DIM} ${'SERVICE'.padEnd(24)} ${'STATUS'.padEnd(22)} ${'BUILT'.padEnd(12)} ${'RESTARTED'.padEnd(12)}`;
|
|
311
|
-
for (const p of patterns)
|
|
312
|
-
colHeader += patternLabel(p).padStart(5) + ' ';
|
|
313
|
-
colHeader += ` ${'CPU/MEM'.padStart(16)} ${'PORTS'.padEnd(14)}`;
|
|
314
|
-
lines.push({ type: 'colheader', text: colHeader + RESET });
|
|
315
|
-
}
|
|
316
|
-
const sk = (0, state_1.statusKey)(entry.file, entry.service);
|
|
317
|
-
const st = state.statuses.get(sk);
|
|
318
|
-
const rebuilding = state.rebuilding.has(sk);
|
|
319
|
-
const restarting = state.restarting.has(sk);
|
|
320
|
-
const stopping = state.stopping.has(sk);
|
|
321
|
-
const starting = state.starting.has(sk);
|
|
322
|
-
const isWatching = state.watching.has(sk);
|
|
323
|
-
const isCascading = state.cascading.has(sk);
|
|
324
|
-
const icon = statusIcon(st, rebuilding || isCascading, restarting, stopping, starting);
|
|
325
|
-
const stext = statusText(st, rebuilding || isCascading, restarting, stopping, starting);
|
|
326
|
-
const watchIndicator = isWatching ? `${FG_CYAN}W${RESET}` : ' ';
|
|
327
|
-
const name = entry.service.padEnd(24);
|
|
328
|
-
const statusPadded = padVisible(stext, 22);
|
|
329
|
-
let cpuMemStr;
|
|
330
|
-
const stats = state.containerStats ? state.containerStats.get(sk) : null;
|
|
331
|
-
if (stats && st && st.state === 'running') {
|
|
332
|
-
const cpu = stats.cpuPercent;
|
|
333
|
-
const mem = stats.memUsageBytes;
|
|
334
|
-
const cpuWarn = state.config.cpuWarnThreshold || 50;
|
|
335
|
-
const cpuDanger = state.config.cpuDangerThreshold || 100;
|
|
336
|
-
const memWarn = (state.config.memWarnThreshold || 512) * 1024 * 1024;
|
|
337
|
-
const memDanger = (state.config.memDangerThreshold || 1024) * 1024 * 1024;
|
|
338
|
-
let color = DIM;
|
|
339
|
-
if (cpu > cpuDanger || mem > memDanger)
|
|
340
|
-
color = FG_RED;
|
|
341
|
-
else if (cpu > cpuWarn || mem > memWarn)
|
|
342
|
-
color = FG_YELLOW;
|
|
343
|
-
const cpuText = cpu.toFixed(1) + '%';
|
|
344
|
-
const memText = formatMem(mem);
|
|
345
|
-
cpuMemStr = padVisible(`${color}${cpuText} / ${memText}${RESET}`, 16);
|
|
346
|
-
}
|
|
347
|
-
else {
|
|
348
|
-
cpuMemStr = padVisible(`${DIM}-${RESET}`, 16);
|
|
349
|
-
}
|
|
350
|
-
let portsStr;
|
|
351
|
-
if (st && st.ports && st.ports.length > 0) {
|
|
352
|
-
const portsText = st.ports.map(p => p.published).join(' ');
|
|
353
|
-
portsStr = padVisible(`${DIM}${portsText}${RESET}`, 14);
|
|
354
|
-
}
|
|
355
|
-
else {
|
|
356
|
-
portsStr = padVisible(`${DIM}-${RESET}`, 14);
|
|
357
|
-
}
|
|
358
|
-
const built = padVisible(relativeTime(st ? st.createdAt : null), 12);
|
|
359
|
-
const restarted = padVisible(relativeTime(st ? st.startedAt : null), 12);
|
|
360
|
-
const pointer = i === state.cursor ? `${REVERSE}` : '';
|
|
361
|
-
const endPointer = i === state.cursor ? `${RESET}` : '';
|
|
362
|
-
let countsStr = '';
|
|
363
|
-
const logCounts = state.logCounts.get(sk);
|
|
364
|
-
for (let pi = 0; pi < patterns.length; pi++) {
|
|
365
|
-
const count = logCounts ? (logCounts.get(patterns[pi]) || 0) : 0;
|
|
366
|
-
const color = count > 0 ? PATTERN_COLORS[pi % PATTERN_COLORS.length] : DIM;
|
|
367
|
-
const countText = count > 0 ? `${color}${count}${RESET}` : `${color}-${RESET}`;
|
|
368
|
-
countsStr += padVisibleStart(countText, 5) + ' ';
|
|
371
|
+
if (stubs.length > 0)
|
|
372
|
+
stubs.push({ type: 'blank', flatIdx: -1, groupIdx: entry.groupIdx });
|
|
373
|
+
stubs.push({ type: 'header', flatIdx: -1, groupIdx: entry.groupIdx });
|
|
369
374
|
}
|
|
370
|
-
|
|
371
|
-
type: 'service',
|
|
372
|
-
text: `${pointer} ${watchIndicator}${icon} ${FG_WHITE}${name}${RESET} ${statusPadded} ${built} ${restarted}${countsStr} ${cpuMemStr} ${portsStr}${endPointer}`,
|
|
373
|
-
flatIdx: i,
|
|
374
|
-
});
|
|
375
|
+
stubs.push({ type: 'service', flatIdx: i, groupIdx: entry.groupIdx });
|
|
375
376
|
}
|
|
376
377
|
const availableRows = Math.max(3, rows - headerHeight - bottomHeight);
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
378
|
+
// Find cursor position in stubs
|
|
379
|
+
const cursorStubIdx = stubs.findIndex(s => s.type === 'service' && s.flatIdx === state.cursor);
|
|
380
|
+
if (cursorStubIdx < state.scrollOffset) {
|
|
381
|
+
state.scrollOffset = cursorStubIdx;
|
|
380
382
|
}
|
|
381
|
-
else if (
|
|
382
|
-
state.scrollOffset =
|
|
383
|
+
else if (cursorStubIdx >= state.scrollOffset + availableRows) {
|
|
384
|
+
state.scrollOffset = cursorStubIdx - availableRows + 1;
|
|
383
385
|
}
|
|
384
|
-
state.scrollOffset = Math.max(0, Math.min(
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
386
|
+
state.scrollOffset = Math.max(0, Math.min(stubs.length - availableRows, state.scrollOffset));
|
|
387
|
+
// Pass 2: render text only for visible stubs
|
|
388
|
+
const visEnd = Math.min(stubs.length, state.scrollOffset + availableRows);
|
|
389
|
+
for (let si = state.scrollOffset; si < visEnd; si++) {
|
|
390
|
+
const stub = stubs[si];
|
|
391
|
+
switch (stub.type) {
|
|
392
|
+
case 'blank':
|
|
393
|
+
buf.push('');
|
|
394
|
+
break;
|
|
395
|
+
case 'header': {
|
|
396
|
+
const group = state.groups[stub.groupIdx];
|
|
397
|
+
const label = ` ${BOLD}${group.label}${RESET}`;
|
|
398
|
+
buf.push(group.error ? `${label} ${FG_RED}(${group.error})${RESET}` : label);
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
case 'service': {
|
|
402
|
+
const i = stub.flatIdx;
|
|
403
|
+
const entry = state.flatList[i];
|
|
404
|
+
const sk = (0, state_1.statusKey)(entry.file, entry.service);
|
|
405
|
+
const st = state.statuses.get(sk);
|
|
406
|
+
const rebuilding = state.rebuilding.has(sk);
|
|
407
|
+
const restarting = state.restarting.has(sk);
|
|
408
|
+
const stopping = state.stopping.has(sk);
|
|
409
|
+
const starting = state.starting.has(sk);
|
|
410
|
+
const isWatching = state.watching.has(sk);
|
|
411
|
+
const isCascading = state.cascading.has(sk);
|
|
412
|
+
const icon = statusIcon(st, rebuilding || isCascading, restarting, stopping, starting);
|
|
413
|
+
const stext = statusText(st, rebuilding || isCascading, restarting, stopping, starting);
|
|
414
|
+
const watchIndicator = isWatching ? `${FG_CYAN}W${RESET}` : ' ';
|
|
415
|
+
const wtBranch = st ? st.worktree : null;
|
|
416
|
+
const name = entry.service.padEnd(24);
|
|
417
|
+
const statusPadded = padVisible(stext, 22);
|
|
418
|
+
let cpuMemStr;
|
|
419
|
+
const stats = state.containerStats ? state.containerStats.get(sk) : null;
|
|
420
|
+
if (stats && st && st.state === 'running') {
|
|
421
|
+
const cpu = stats.cpuPercent;
|
|
422
|
+
const mem = stats.memUsageBytes;
|
|
423
|
+
const cpuWarn = state.config.cpuWarnThreshold || 50;
|
|
424
|
+
const cpuDanger = state.config.cpuDangerThreshold || 100;
|
|
425
|
+
const memWarn = (state.config.memWarnThreshold || 512) * 1024 * 1024;
|
|
426
|
+
const memDanger = (state.config.memDangerThreshold || 1024) * 1024 * 1024;
|
|
427
|
+
let color = DIM;
|
|
428
|
+
if (cpu > cpuDanger || mem > memDanger)
|
|
429
|
+
color = FG_RED;
|
|
430
|
+
else if (cpu > cpuWarn || mem > memWarn)
|
|
431
|
+
color = FG_YELLOW;
|
|
432
|
+
const cpuText = cpu.toFixed(1) + '%';
|
|
433
|
+
const memText = formatMem(mem);
|
|
434
|
+
cpuMemStr = padVisible(`${color}${cpuText} / ${memText}${RESET}`, 16);
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
cpuMemStr = padVisible(`${DIM}-${RESET}`, 16);
|
|
438
|
+
}
|
|
439
|
+
let portsStr;
|
|
440
|
+
if (st && st.ports && st.ports.length > 0) {
|
|
441
|
+
const portsText = st.ports.map(p => p.published).join(' ');
|
|
442
|
+
portsStr = padVisible(`${DIM}${portsText}${RESET}`, 14);
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
portsStr = padVisible(`${DIM}-${RESET}`, 14);
|
|
446
|
+
}
|
|
447
|
+
const built = padVisible(relativeTime(st ? st.createdAt : null), 12);
|
|
448
|
+
const restarted = padVisible(relativeTime(st ? st.startedAt : null), 12);
|
|
449
|
+
const isSelected = i === state.cursor;
|
|
450
|
+
let countsStr = '';
|
|
451
|
+
const logCounts = state.logCounts.get(sk);
|
|
452
|
+
for (let pi = 0; pi < patterns.length; pi++) {
|
|
453
|
+
const key = Array.isArray(patterns[pi]) ? patterns[pi][0] : patterns[pi];
|
|
454
|
+
const count = logCounts ? (logCounts.get(key) || 0) : 0;
|
|
455
|
+
const color = count > 0 ? PATTERN_COLORS[pi % PATTERN_COLORS.length] : DIM;
|
|
456
|
+
const countText = count > 0 ? `${color}${count}${RESET}` : `${color}-${RESET}`;
|
|
457
|
+
countsStr += padVisibleStart(countText, 5) + ' ';
|
|
458
|
+
}
|
|
459
|
+
let worktreeCol = '';
|
|
460
|
+
if (state.showWorktreeColumn) {
|
|
461
|
+
const wtLabel = (0, state_1.worktreeLabel)(st ? st.worktree : null);
|
|
462
|
+
const wtColor = (wtBranch && wtBranch !== 'main') ? FG_YELLOW : DIM;
|
|
463
|
+
worktreeCol = ` ${wtColor}${wtLabel.padEnd(15)}${RESET}`;
|
|
464
|
+
}
|
|
465
|
+
let row = ` ${watchIndicator}${icon} ${FG_WHITE}${name}${RESET} ${statusPadded} ${built} ${restarted}${countsStr} ${cpuMemStr} ${portsStr}${worktreeCol}`;
|
|
466
|
+
if (isSelected) {
|
|
467
|
+
// Re-apply BG after every RESET so highlight spans the full row
|
|
468
|
+
row = `${BG_HIGHLIGHT}${row.replace(/\x1b\[0m/g, `${RESET}${BG_HIGHLIGHT}`)}${' '.repeat(Math.max(0, columns - visLen(row)))}${RESET}`;
|
|
469
|
+
}
|
|
470
|
+
buf.push(row);
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
388
474
|
}
|
|
389
475
|
const usedLines = buf.length + bottomHeight;
|
|
390
476
|
const paddingNeeded = Math.max(0, rows - usedLines);
|
|
@@ -392,7 +478,7 @@ function renderListView(state) {
|
|
|
392
478
|
buf.push('');
|
|
393
479
|
}
|
|
394
480
|
buf.push(...bottomBuf);
|
|
395
|
-
return buf.join('\n');
|
|
481
|
+
return buf.join(exports.CLEAR_EOL + '\n');
|
|
396
482
|
}
|
|
397
483
|
function truncateLine(str, maxWidth) {
|
|
398
484
|
let visPos = 0;
|
|
@@ -418,11 +504,12 @@ function truncateLine(str, maxWidth) {
|
|
|
418
504
|
}
|
|
419
505
|
return str;
|
|
420
506
|
}
|
|
421
|
-
function highlightSearchInLine(line, query) {
|
|
507
|
+
function highlightSearchInLine(line, query, baseColor) {
|
|
422
508
|
if (!query)
|
|
423
509
|
return line;
|
|
424
510
|
const lowerLine = line.toLowerCase();
|
|
425
511
|
const lowerQuery = query.toLowerCase();
|
|
512
|
+
const restore = baseColor || '';
|
|
426
513
|
let result = '';
|
|
427
514
|
let pos = 0;
|
|
428
515
|
while (pos < line.length) {
|
|
@@ -432,11 +519,20 @@ function highlightSearchInLine(line, query) {
|
|
|
432
519
|
break;
|
|
433
520
|
}
|
|
434
521
|
result += line.substring(pos, idx);
|
|
435
|
-
result += `${REVERSE}${FG_YELLOW}${line.substring(idx, idx + query.length)}${RESET}`;
|
|
522
|
+
result += `${REVERSE}${FG_YELLOW}${line.substring(idx, idx + query.length)}${RESET}${restore}`;
|
|
436
523
|
pos = idx + query.length;
|
|
437
524
|
}
|
|
438
525
|
return result;
|
|
439
526
|
}
|
|
527
|
+
function wrapPlainLine(line, width) {
|
|
528
|
+
if (width <= 0 || line.length <= width)
|
|
529
|
+
return [line];
|
|
530
|
+
const result = [];
|
|
531
|
+
for (let i = 0; i < line.length; i += width) {
|
|
532
|
+
result.push(line.substring(i, i + width));
|
|
533
|
+
}
|
|
534
|
+
return result;
|
|
535
|
+
}
|
|
440
536
|
function renderLogView(state) {
|
|
441
537
|
const columns = process.stdout.columns ?? 80;
|
|
442
538
|
const rows = process.stdout.rows ?? 24;
|
|
@@ -444,17 +540,43 @@ function renderLogView(state) {
|
|
|
444
540
|
for (const line of LOGO) {
|
|
445
541
|
buf.push(line);
|
|
446
542
|
}
|
|
447
|
-
buf.push(
|
|
448
|
-
|
|
543
|
+
buf.push(separatorLine(columns));
|
|
544
|
+
const hasLogSearch = !!state.logSearchQuery && !state.logSearchActive;
|
|
545
|
+
buf.push(` ${renderLegend({ logsScrollMode: true, hasLogSearch })}`);
|
|
449
546
|
const entry = state.flatList[state.cursor];
|
|
450
547
|
const serviceName = entry ? entry.service : '???';
|
|
451
548
|
const totalLines = state.logLines.length;
|
|
452
|
-
let statusLine
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
549
|
+
let statusLine;
|
|
550
|
+
if (state.logBuildKey) {
|
|
551
|
+
const buildInfo = state.bottomLogLines.get(state.logBuildKey);
|
|
552
|
+
const isBuilding = state.rebuilding.has(state.logBuildKey) || state.cascading.has(state.logBuildKey);
|
|
553
|
+
if (buildInfo && buildInfo.action === 'build_failed') {
|
|
554
|
+
statusLine = ` ${FG_RED}build failed ${BOLD}${serviceName}${RESET}`;
|
|
555
|
+
}
|
|
556
|
+
else if (isBuilding) {
|
|
557
|
+
statusLine = ` ${FG_YELLOW}rebuilding ${BOLD}${serviceName}${RESET}`;
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
statusLine = ` ${FG_GREEN}build logs ${BOLD}${serviceName}${RESET}`;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
statusLine = ` ${FG_GREEN}full logs ${BOLD}${serviceName}${RESET}`;
|
|
565
|
+
}
|
|
566
|
+
let scrollStatus;
|
|
567
|
+
if (state.logAutoScroll) {
|
|
568
|
+
scrollStatus = `${FG_GREEN}live${RESET}`;
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
const currentLine = Math.max(1, totalLines - state.logScrollOffset);
|
|
572
|
+
const pct = totalLines > 0 ? Math.round((currentLine / totalLines) * 100) : 100;
|
|
573
|
+
scrollStatus = `${FG_YELLOW}paused ${DIM}line ${currentLine} / ${totalLines} (${pct}%)${RESET}`;
|
|
574
|
+
}
|
|
456
575
|
statusLine += ` ${scrollStatus}`;
|
|
457
|
-
if (state.
|
|
576
|
+
if (state.logSearchPending || state.logHistoryLoading) {
|
|
577
|
+
statusLine += ` ${FG_YELLOW}loading history...${RESET}`;
|
|
578
|
+
}
|
|
579
|
+
else if (state.logSearchQuery && state.logSearchMatches.length > 0) {
|
|
458
580
|
statusLine += ` ${DIM}match ${state.logSearchMatchIdx + 1}/${state.logSearchMatches.length}${RESET}`;
|
|
459
581
|
}
|
|
460
582
|
else if (state.logSearchQuery && state.logSearchMatches.length === 0) {
|
|
@@ -469,18 +591,38 @@ function renderLogView(state) {
|
|
|
469
591
|
endLine = totalLines;
|
|
470
592
|
}
|
|
471
593
|
else {
|
|
472
|
-
endLine = Math.max(
|
|
594
|
+
endLine = Math.max(Math.min(availableRows, totalLines), totalLines - state.logScrollOffset);
|
|
595
|
+
}
|
|
596
|
+
if (totalLines === 0) {
|
|
597
|
+
buf.push(` ${DIM}loading...${RESET}`);
|
|
473
598
|
}
|
|
474
|
-
const startLine = Math.max(0, endLine - availableRows);
|
|
475
599
|
const searchQuery = state.logSearchQuery || '';
|
|
476
600
|
const matchSet = searchQuery ? new Set(state.logSearchMatches) : null;
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
601
|
+
const patterns = state.config.logScanPatterns || [];
|
|
602
|
+
// Build display lines by wrapping log lines, working backwards from endLine
|
|
603
|
+
const displayLines = [];
|
|
604
|
+
for (let i = endLine - 1; i >= 0 && displayLines.length < availableRows; i--) {
|
|
605
|
+
const line = state.logLines[i];
|
|
606
|
+
const wrapped = wrapPlainLine(line, columns);
|
|
607
|
+
const isMatch = matchSet && matchSet.has(i);
|
|
608
|
+
const lineColor = logLineColor(line, patterns);
|
|
609
|
+
for (let w = wrapped.length - 1; w >= 0; w--) {
|
|
610
|
+
let segment = wrapped[w];
|
|
611
|
+
if (isMatch) {
|
|
612
|
+
segment = highlightSearchInLine(segment, searchQuery, lineColor || undefined);
|
|
613
|
+
}
|
|
614
|
+
if (lineColor) {
|
|
615
|
+
segment = `${lineColor}${segment}${RESET}`;
|
|
616
|
+
}
|
|
617
|
+
displayLines.push(segment);
|
|
481
618
|
}
|
|
482
|
-
buf.push(truncateLine(line, columns));
|
|
483
619
|
}
|
|
620
|
+
displayLines.reverse();
|
|
621
|
+
// Trim to fit available rows (keep the bottom portion)
|
|
622
|
+
const trimmed = displayLines.length > availableRows
|
|
623
|
+
? displayLines.slice(displayLines.length - availableRows)
|
|
624
|
+
: displayLines;
|
|
625
|
+
buf.push(...trimmed);
|
|
484
626
|
const targetRows = rows - bottomReserved;
|
|
485
627
|
for (let i = buf.length; i < targetRows; i++) {
|
|
486
628
|
buf.push('');
|
|
@@ -488,7 +630,7 @@ function renderLogView(state) {
|
|
|
488
630
|
if (state.logSearchActive) {
|
|
489
631
|
buf.push(`${BOLD}/${RESET}${state.logSearchQuery}${BOLD}_${RESET}`);
|
|
490
632
|
}
|
|
491
|
-
return buf.join('\n');
|
|
633
|
+
return buf.join(exports.CLEAR_EOL + '\n');
|
|
492
634
|
}
|
|
493
635
|
function renderExecView(state) {
|
|
494
636
|
const columns = process.stdout.columns ?? 80;
|
|
@@ -497,7 +639,7 @@ function renderExecView(state) {
|
|
|
497
639
|
for (const line of LOGO) {
|
|
498
640
|
buf.push(line);
|
|
499
641
|
}
|
|
500
|
-
buf.push(
|
|
642
|
+
buf.push(separatorLine(columns));
|
|
501
643
|
buf.push(` ${renderLegend({ execMode: true })}`);
|
|
502
644
|
const serviceName = state.execService || '???';
|
|
503
645
|
const runningIndicator = state.execChild ? `${FG_YELLOW}running${RESET}` : `${FG_GREEN}ready${RESET}`;
|
|
@@ -519,6 +661,6 @@ function renderExecView(state) {
|
|
|
519
661
|
}
|
|
520
662
|
// Command prompt
|
|
521
663
|
buf.push(`${FG_GREEN}$ ${RESET}${state.execInput}${BOLD}_${RESET}`);
|
|
522
|
-
return buf.join('\n');
|
|
664
|
+
return buf.join(exports.CLEAR_EOL + '\n');
|
|
523
665
|
}
|
|
524
666
|
//# sourceMappingURL=renderer.js.map
|