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