recomposable 1.0.2 → 1.1.1
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 +78 -11
- package/dist/index.d.ts +40 -0
- package/dist/index.js +1418 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/docker.d.ts +19 -0
- package/dist/lib/docker.js +332 -0
- package/dist/lib/docker.js.map +1 -0
- package/dist/lib/renderer.d.ts +22 -0
- package/dist/lib/renderer.js +526 -0
- package/dist/lib/renderer.js.map +1 -0
- package/dist/lib/state.d.ts +7 -0
- package/dist/lib/state.js +81 -0
- package/dist/lib/state.js.map +1 -0
- package/dist/lib/types.d.ts +183 -0
- package/dist/lib/types.js +6 -0
- package/dist/lib/types.js.map +1 -0
- package/package.json +19 -5
- package/screenshots/exec-inline-view.png +0 -0
- package/screenshots/exec-view.png +0 -0
- package/screenshots/list-view.png +0 -0
- package/screenshots/logs-view.png +0 -0
- package/index.js +0 -662
- package/lib/docker.js +0 -123
- package/lib/renderer.js +0 -315
- package/lib/state.js +0 -52
package/lib/docker.js
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { execFileSync, spawn } = require('child_process');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
|
|
6
|
-
function listServices(file) {
|
|
7
|
-
const cwd = path.dirname(path.resolve(file));
|
|
8
|
-
const args = ['compose', '-f', path.resolve(file), 'config', '--services'];
|
|
9
|
-
const out = execFileSync('docker', args, { cwd, encoding: 'utf8', timeout: 10000 });
|
|
10
|
-
return out.trim().split('\n').filter(Boolean);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function getStatuses(file) {
|
|
14
|
-
const cwd = path.dirname(path.resolve(file));
|
|
15
|
-
const args = ['compose', '-f', path.resolve(file), 'ps', '--format', 'json'];
|
|
16
|
-
let out;
|
|
17
|
-
try {
|
|
18
|
-
out = execFileSync('docker', args, { cwd, encoding: 'utf8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
19
|
-
} catch {
|
|
20
|
-
return new Map();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const trimmed = out.trim();
|
|
24
|
-
if (!trimmed) return new Map();
|
|
25
|
-
|
|
26
|
-
const statuses = new Map();
|
|
27
|
-
let containers;
|
|
28
|
-
|
|
29
|
-
// docker compose ps outputs NDJSON (one object per line) or a JSON array
|
|
30
|
-
if (trimmed.startsWith('[')) {
|
|
31
|
-
containers = JSON.parse(trimmed);
|
|
32
|
-
} else {
|
|
33
|
-
containers = trimmed.split('\n').filter(Boolean).map(line => JSON.parse(line));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const idToService = new Map();
|
|
37
|
-
|
|
38
|
-
for (const c of containers) {
|
|
39
|
-
const name = c.Service || c.Name;
|
|
40
|
-
const state = (c.State || '').toLowerCase();
|
|
41
|
-
const health = (c.Health || '').toLowerCase();
|
|
42
|
-
const createdAt = c.CreatedAt || null;
|
|
43
|
-
const id = c.ID || null;
|
|
44
|
-
statuses.set(name, { state, health, createdAt, startedAt: null, id: id || null });
|
|
45
|
-
if (id) idToService.set(id, name);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Batch docker inspect to get startedAt timestamps
|
|
49
|
-
const ids = [...idToService.keys()];
|
|
50
|
-
if (ids.length > 0) {
|
|
51
|
-
try {
|
|
52
|
-
const inspectOut = execFileSync('docker', ['inspect', ...ids], {
|
|
53
|
-
encoding: 'utf8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe']
|
|
54
|
-
});
|
|
55
|
-
const inspected = JSON.parse(inspectOut);
|
|
56
|
-
for (const info of inspected) {
|
|
57
|
-
for (const [id, svc] of idToService) {
|
|
58
|
-
if (info.Id && info.Id.startsWith(id)) {
|
|
59
|
-
const status = statuses.get(svc);
|
|
60
|
-
if (status && info.State) {
|
|
61
|
-
status.startedAt = info.State.StartedAt || null;
|
|
62
|
-
}
|
|
63
|
-
break;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
} catch {
|
|
68
|
-
// Ignore inspect errors
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return statuses;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function rebuildService(file, service) {
|
|
76
|
-
const cwd = path.dirname(path.resolve(file));
|
|
77
|
-
const args = ['compose', '-f', path.resolve(file), 'up', '-d', '--build', service];
|
|
78
|
-
const child = spawn('docker', args, {
|
|
79
|
-
cwd, stdio: ['ignore', 'pipe', 'pipe'], detached: false,
|
|
80
|
-
env: { ...process.env, BUILDKIT_PROGRESS: 'plain' },
|
|
81
|
-
});
|
|
82
|
-
return child;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function tailLogs(file, service, tailLines) {
|
|
86
|
-
const cwd = path.dirname(path.resolve(file));
|
|
87
|
-
const args = ['compose', '-f', path.resolve(file), 'logs', '-f', '--tail', String(tailLines), service];
|
|
88
|
-
const child = spawn('docker', args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
89
|
-
return child;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function getContainerId(file, service) {
|
|
93
|
-
const cwd = path.dirname(path.resolve(file));
|
|
94
|
-
const args = ['compose', '-f', path.resolve(file), 'ps', '-q', service];
|
|
95
|
-
try {
|
|
96
|
-
const out = execFileSync('docker', args, { cwd, encoding: 'utf8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
97
|
-
return out.trim() || null;
|
|
98
|
-
} catch {
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function tailContainerLogs(containerId, tailLines) {
|
|
104
|
-
const args = ['logs', '-f', '--tail', String(tailLines), containerId];
|
|
105
|
-
const child = spawn('docker', args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
106
|
-
return child;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function fetchContainerLogs(containerId, tailLines) {
|
|
110
|
-
const child = spawn('docker', ['logs', '--tail', String(tailLines), containerId], {
|
|
111
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
112
|
-
});
|
|
113
|
-
return child;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function restartService(file, service) {
|
|
117
|
-
const cwd = path.dirname(path.resolve(file));
|
|
118
|
-
const args = ['compose', '-f', path.resolve(file), 'restart', service];
|
|
119
|
-
const child = spawn('docker', args, { cwd, stdio: ['ignore', 'pipe', 'pipe'], detached: false });
|
|
120
|
-
return child;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
module.exports = { listServices, getStatuses, rebuildService, restartService, tailLogs, getContainerId, tailContainerLogs, fetchContainerLogs };
|
package/lib/renderer.js
DELETED
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { statusKey, MODE } = require('./state');
|
|
4
|
-
|
|
5
|
-
const ESC = '\x1b[';
|
|
6
|
-
const RESET = `${ESC}0m`;
|
|
7
|
-
const BOLD = `${ESC}1m`;
|
|
8
|
-
const DIM = `${ESC}2m`;
|
|
9
|
-
const REVERSE = `${ESC}7m`;
|
|
10
|
-
const FG_GREEN = `${ESC}32m`;
|
|
11
|
-
const FG_YELLOW = `${ESC}33m`;
|
|
12
|
-
const FG_RED = `${ESC}31m`;
|
|
13
|
-
const FG_GRAY = `${ESC}90m`;
|
|
14
|
-
const FG_CYAN = `${ESC}36m`;
|
|
15
|
-
const FG_WHITE = `${ESC}37m`;
|
|
16
|
-
|
|
17
|
-
const ITALIC = `${ESC}3m`;
|
|
18
|
-
const BG_HIGHLIGHT = `${ESC}48;5;237m`;
|
|
19
|
-
|
|
20
|
-
const LOGO = [
|
|
21
|
-
` ${ITALIC}${BOLD}${FG_CYAN}┌─┐┌─┐┌─┐┌─┐┌┬┐┌─┐┌─┐┌─┐┌─┐┌┐ ┬ ┌─┐${RESET}`,
|
|
22
|
-
` ${ITALIC}${BOLD}${FG_CYAN}├┬┘├┤ │ │ ││││├─┘│ │└─┐├─┤├┴┐│ ├┤${RESET}`,
|
|
23
|
-
` ${ITALIC}${BOLD}${FG_CYAN}┴└─└─┘└─┘└─┘┴ ┴┴ └─┘└─┘┴ ┴└─┘┴─┘└─┘${RESET}`,
|
|
24
|
-
``,
|
|
25
|
-
` ${DIM}docker compose manager${RESET}`,
|
|
26
|
-
``,
|
|
27
|
-
];
|
|
28
|
-
|
|
29
|
-
function visLen(str) {
|
|
30
|
-
return str.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function padVisible(str, width) {
|
|
34
|
-
const pad = Math.max(0, width - visLen(str));
|
|
35
|
-
return str + ' '.repeat(pad);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function padVisibleStart(str, width) {
|
|
39
|
-
const pad = Math.max(0, width - visLen(str));
|
|
40
|
-
return ' '.repeat(pad) + str;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const PATTERN_COLORS = [FG_YELLOW, FG_RED, FG_CYAN, FG_WHITE];
|
|
44
|
-
|
|
45
|
-
function patternLabel(pattern) {
|
|
46
|
-
return pattern.replace(/^[\[\(\{<]/, '').replace(/[\]\)\}>]$/, '');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function parseTimestamp(ts) {
|
|
50
|
-
if (!ts) return null;
|
|
51
|
-
// Strip trailing timezone abbreviation (e.g., "UTC", "CET")
|
|
52
|
-
const cleaned = ts.replace(/ [A-Z]{2,5}$/, '');
|
|
53
|
-
const d = new Date(cleaned);
|
|
54
|
-
return isNaN(d.getTime()) ? null : d;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function relativeTime(ts) {
|
|
58
|
-
const date = parseTimestamp(ts);
|
|
59
|
-
if (!date) return `${FG_GRAY}-${RESET}`;
|
|
60
|
-
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
61
|
-
if (seconds < 0) return `${FG_GRAY}-${RESET}`;
|
|
62
|
-
if (seconds < 60) return `${DIM}${seconds}s ago${RESET}`;
|
|
63
|
-
const minutes = Math.floor(seconds / 60);
|
|
64
|
-
if (minutes < 60) return `${DIM}${minutes}m ago${RESET}`;
|
|
65
|
-
const hours = Math.floor(minutes / 60);
|
|
66
|
-
if (hours < 24) return `${DIM}${hours}h ago${RESET}`;
|
|
67
|
-
const days = Math.floor(hours / 24);
|
|
68
|
-
return `${DIM}${days}d ago${RESET}`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function clearScreen() {
|
|
72
|
-
return `${ESC}2J${ESC}H${ESC}?25l`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function showCursor() {
|
|
76
|
-
return `${ESC}?25h`;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function statusIcon(status, isRebuilding, isRestarting) {
|
|
80
|
-
if (isRebuilding || isRestarting) return `${FG_YELLOW}\u25CF${RESET}`;
|
|
81
|
-
if (!status) return `${FG_GRAY}\u25CB${RESET}`;
|
|
82
|
-
|
|
83
|
-
const { state, health } = status;
|
|
84
|
-
if (state === 'running') {
|
|
85
|
-
if (health === 'unhealthy') return `${FG_RED}\u25CF${RESET}`;
|
|
86
|
-
return `${FG_GREEN}\u25CF${RESET}`;
|
|
87
|
-
}
|
|
88
|
-
if (state === 'restarting') return `${FG_YELLOW}\u25CF${RESET}`;
|
|
89
|
-
return `${FG_GRAY}\u25CB${RESET}`;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function statusText(status, isRebuilding, isRestarting) {
|
|
93
|
-
if (isRestarting) return `${FG_YELLOW}RESTARTING...${RESET}`;
|
|
94
|
-
if (isRebuilding) return `${FG_YELLOW}REBUILDING...${RESET}`;
|
|
95
|
-
if (!status) return `${FG_GRAY}stopped${RESET}`;
|
|
96
|
-
|
|
97
|
-
const { state, health } = status;
|
|
98
|
-
let text = state;
|
|
99
|
-
if (health && health !== 'none' && health !== '') {
|
|
100
|
-
text += ` (${health})`;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (state === 'running') {
|
|
104
|
-
if (health === 'unhealthy') return `${FG_RED}${text}${RESET}`;
|
|
105
|
-
return `${FG_GREEN}${text}${RESET}`;
|
|
106
|
-
}
|
|
107
|
-
if (state === 'exited') return `${FG_GRAY}${text}${RESET}`;
|
|
108
|
-
if (state === 'restarting') return `${FG_YELLOW}${text}${RESET}`;
|
|
109
|
-
return `${DIM}${text}${RESET}`;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function renderLegend(opts = {}) {
|
|
113
|
-
const { logPanelActive = false, fullLogsActive = false, logsScrollMode = false } = opts;
|
|
114
|
-
const item = (text, active) => {
|
|
115
|
-
if (active) return `${BG_HIGHLIGHT} ${text} ${RESET}`;
|
|
116
|
-
return `${DIM}${text}${RESET}`;
|
|
117
|
-
};
|
|
118
|
-
if (logsScrollMode) {
|
|
119
|
-
return [
|
|
120
|
-
item('[Esc] back', false),
|
|
121
|
-
item('[j/k] scroll', false),
|
|
122
|
-
item('[G] bottom', false),
|
|
123
|
-
item('[gg] top', false),
|
|
124
|
-
item('[Q]uit', false),
|
|
125
|
-
].join(' ');
|
|
126
|
-
}
|
|
127
|
-
return [
|
|
128
|
-
item('[R]ebuild', false),
|
|
129
|
-
item('[S]restart', false),
|
|
130
|
-
item('[F]ull logs', fullLogsActive),
|
|
131
|
-
item('[L]og panel', logPanelActive),
|
|
132
|
-
item('[Q]uit', false),
|
|
133
|
-
].join(' ');
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function renderListView(state) {
|
|
137
|
-
const { columns = 80, rows = 24 } = process.stdout;
|
|
138
|
-
const patterns = state.config.logScanPatterns || [];
|
|
139
|
-
const buf = [];
|
|
140
|
-
|
|
141
|
-
// Logo
|
|
142
|
-
for (const line of LOGO) {
|
|
143
|
-
buf.push(line);
|
|
144
|
-
}
|
|
145
|
-
const help = renderLegend({ logPanelActive: state.showBottomLogs });
|
|
146
|
-
buf.push(` ${FG_GRAY}${'─'.repeat(Math.max(0, columns - 2))}${RESET}`);
|
|
147
|
-
buf.push(` ${help}`);
|
|
148
|
-
|
|
149
|
-
const headerHeight = buf.length;
|
|
150
|
-
|
|
151
|
-
// Build bottom panel content — show logs for the currently selected container
|
|
152
|
-
const bottomBuf = [];
|
|
153
|
-
if (state.showBottomLogs) {
|
|
154
|
-
const selEntry = state.flatList[state.cursor];
|
|
155
|
-
if (selEntry) {
|
|
156
|
-
const sk = statusKey(selEntry.file, selEntry.service);
|
|
157
|
-
const info = state.bottomLogLines.get(sk);
|
|
158
|
-
if (info) {
|
|
159
|
-
bottomBuf.push(` ${FG_GRAY}${'─'.repeat(Math.max(0, columns - 2))}${RESET}`);
|
|
160
|
-
const actionColor = info.action === 'rebuilding' || info.action === 'restarting' ? FG_YELLOW : FG_GREEN;
|
|
161
|
-
bottomBuf.push(` ${actionColor}${info.action} ${BOLD}${info.service}${RESET}`);
|
|
162
|
-
for (const line of info.lines) {
|
|
163
|
-
bottomBuf.push(` ${FG_GRAY}${line.substring(0, columns - 4)}${RESET}`);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
const bottomHeight = bottomBuf.length;
|
|
169
|
-
|
|
170
|
-
// Build all display lines
|
|
171
|
-
const lines = [];
|
|
172
|
-
let currentGroup = -1;
|
|
173
|
-
|
|
174
|
-
for (let i = 0; i < state.flatList.length; i++) {
|
|
175
|
-
const entry = state.flatList[i];
|
|
176
|
-
|
|
177
|
-
// Group header
|
|
178
|
-
if (entry.groupIdx !== currentGroup) {
|
|
179
|
-
currentGroup = entry.groupIdx;
|
|
180
|
-
const group = state.groups[entry.groupIdx];
|
|
181
|
-
if (lines.length > 0) lines.push({ type: 'blank' });
|
|
182
|
-
const label = ` ${BOLD}${group.label}${RESET}`;
|
|
183
|
-
if (group.error) {
|
|
184
|
-
lines.push({ type: 'header', text: `${label} ${FG_RED}(${group.error})${RESET}` });
|
|
185
|
-
} else {
|
|
186
|
-
lines.push({ type: 'header', text: label });
|
|
187
|
-
}
|
|
188
|
-
let colHeader = `${DIM} ${'SERVICE'.padEnd(24)} ${'STATUS'.padEnd(22)} ${'BUILT'.padEnd(12)} ${'RESTARTED'.padEnd(12)}`;
|
|
189
|
-
for (const p of patterns) colHeader += patternLabel(p).padStart(5) + ' ';
|
|
190
|
-
lines.push({ type: 'colheader', text: colHeader + RESET });
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const sk = statusKey(entry.file, entry.service);
|
|
194
|
-
const st = state.statuses.get(sk);
|
|
195
|
-
const rebuilding = state.rebuilding.has(sk);
|
|
196
|
-
const restarting = state.restarting.has(sk);
|
|
197
|
-
const icon = statusIcon(st, rebuilding, restarting);
|
|
198
|
-
const stext = statusText(st, rebuilding, restarting);
|
|
199
|
-
const name = entry.service.padEnd(24);
|
|
200
|
-
const statusPadded = padVisible(stext, 22);
|
|
201
|
-
const built = padVisible(relativeTime(st ? st.createdAt : null), 12);
|
|
202
|
-
const restarted = padVisible(relativeTime(st ? st.startedAt : null), 12);
|
|
203
|
-
const pointer = i === state.cursor ? `${REVERSE}` : '';
|
|
204
|
-
const endPointer = i === state.cursor ? `${RESET}` : '';
|
|
205
|
-
|
|
206
|
-
let countsStr = '';
|
|
207
|
-
const logCounts = state.logCounts.get(sk);
|
|
208
|
-
for (let pi = 0; pi < patterns.length; pi++) {
|
|
209
|
-
const count = logCounts ? (logCounts.get(patterns[pi]) || 0) : 0;
|
|
210
|
-
const color = count > 0 ? PATTERN_COLORS[pi % PATTERN_COLORS.length] : DIM;
|
|
211
|
-
const countText = count > 0 ? `${color}${count}${RESET}` : `${color}-${RESET}`;
|
|
212
|
-
countsStr += padVisibleStart(countText, 5) + ' ';
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
lines.push({
|
|
216
|
-
type: 'service',
|
|
217
|
-
text: `${pointer} ${icon} ${FG_WHITE}${name}${RESET} ${statusPadded} ${built} ${restarted}${countsStr}${endPointer}`,
|
|
218
|
-
flatIdx: i,
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Scrolling
|
|
223
|
-
const availableRows = Math.max(3, rows - headerHeight - bottomHeight);
|
|
224
|
-
|
|
225
|
-
// Find line index of cursor
|
|
226
|
-
const cursorLineIdx = lines.findIndex(l => l.type === 'service' && l.flatIdx === state.cursor);
|
|
227
|
-
|
|
228
|
-
// Adjust scroll offset
|
|
229
|
-
if (cursorLineIdx < state.scrollOffset) {
|
|
230
|
-
state.scrollOffset = cursorLineIdx;
|
|
231
|
-
} else if (cursorLineIdx >= state.scrollOffset + availableRows) {
|
|
232
|
-
state.scrollOffset = cursorLineIdx - availableRows + 1;
|
|
233
|
-
}
|
|
234
|
-
state.scrollOffset = Math.max(0, Math.min(lines.length - availableRows, state.scrollOffset));
|
|
235
|
-
|
|
236
|
-
const visible = lines.slice(state.scrollOffset, state.scrollOffset + availableRows);
|
|
237
|
-
for (const line of visible) {
|
|
238
|
-
buf.push(line.text || '');
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Pad to push bottom panel to the bottom of the terminal
|
|
242
|
-
const usedLines = buf.length + bottomHeight;
|
|
243
|
-
const paddingNeeded = Math.max(0, rows - usedLines);
|
|
244
|
-
for (let i = 0; i < paddingNeeded; i++) {
|
|
245
|
-
buf.push('');
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Bottom panel
|
|
249
|
-
buf.push(...bottomBuf);
|
|
250
|
-
|
|
251
|
-
return buf.join('\n');
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function truncateLine(str, maxWidth) {
|
|
255
|
-
let visPos = 0;
|
|
256
|
-
let rawPos = 0;
|
|
257
|
-
while (rawPos < str.length) {
|
|
258
|
-
if (str[rawPos] === '\x1b') {
|
|
259
|
-
const match = str.substring(rawPos).match(/^\x1b\[[0-9;?]*[a-zA-Z]/);
|
|
260
|
-
if (match) { rawPos += match[0].length; continue; }
|
|
261
|
-
const oscMatch = str.substring(rawPos).match(/^\x1b\][^\x07]*\x07/);
|
|
262
|
-
if (oscMatch) { rawPos += oscMatch[0].length; continue; }
|
|
263
|
-
}
|
|
264
|
-
if (visPos >= maxWidth) {
|
|
265
|
-
return str.substring(0, rawPos) + RESET;
|
|
266
|
-
}
|
|
267
|
-
visPos++;
|
|
268
|
-
rawPos++;
|
|
269
|
-
}
|
|
270
|
-
return str;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function renderLogView(state) {
|
|
274
|
-
const { columns = 80, rows = 24 } = process.stdout;
|
|
275
|
-
const buf = [];
|
|
276
|
-
|
|
277
|
-
for (const line of LOGO) {
|
|
278
|
-
buf.push(line);
|
|
279
|
-
}
|
|
280
|
-
buf.push(` ${FG_GRAY}${'─'.repeat(Math.max(0, columns - 2))}${RESET}`);
|
|
281
|
-
buf.push(` ${renderLegend({ logsScrollMode: true })}`);
|
|
282
|
-
|
|
283
|
-
const entry = state.flatList[state.cursor];
|
|
284
|
-
const serviceName = entry ? entry.service : '???';
|
|
285
|
-
const totalLines = state.logLines.length;
|
|
286
|
-
|
|
287
|
-
const scrollStatus = state.logAutoScroll
|
|
288
|
-
? `${FG_GREEN}live${RESET}`
|
|
289
|
-
: `${FG_YELLOW}paused ${DIM}line ${Math.max(1, totalLines - state.logScrollOffset)} / ${totalLines}${RESET}`;
|
|
290
|
-
buf.push(` ${FG_GREEN}full logs ${BOLD}${serviceName}${RESET} ${scrollStatus}`);
|
|
291
|
-
|
|
292
|
-
const headerHeight = buf.length;
|
|
293
|
-
const availableRows = Math.max(1, rows - headerHeight);
|
|
294
|
-
|
|
295
|
-
let endLine;
|
|
296
|
-
if (state.logAutoScroll || state.logScrollOffset === 0) {
|
|
297
|
-
endLine = totalLines;
|
|
298
|
-
} else {
|
|
299
|
-
endLine = Math.max(0, totalLines - state.logScrollOffset);
|
|
300
|
-
}
|
|
301
|
-
const startLine = Math.max(0, endLine - availableRows);
|
|
302
|
-
|
|
303
|
-
for (let i = startLine; i < endLine; i++) {
|
|
304
|
-
buf.push(truncateLine(state.logLines[i], columns));
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Pad to fill screen (prevents ghost content from previous render)
|
|
308
|
-
for (let i = buf.length; i < rows; i++) {
|
|
309
|
-
buf.push('');
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return buf.join('\n');
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
module.exports = { clearScreen, showCursor, renderListView, renderLogView };
|
package/lib/state.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const MODE = { LIST: 'LIST', LOGS: 'LOGS' };
|
|
4
|
-
|
|
5
|
-
function createState(config) {
|
|
6
|
-
return {
|
|
7
|
-
mode: MODE.LIST,
|
|
8
|
-
groups: [], // [{ file, label, services: string[], error: string|null }]
|
|
9
|
-
flatList: [], // [{ groupIdx, serviceIdx, service, file }]
|
|
10
|
-
cursor: 0,
|
|
11
|
-
statuses: new Map(), // "file::service" -> { state, health }
|
|
12
|
-
rebuilding: new Map(), // "file::service" -> childProcess
|
|
13
|
-
restarting: new Map(), // "file::service" -> childProcess
|
|
14
|
-
logChild: null,
|
|
15
|
-
scrollOffset: 0,
|
|
16
|
-
showBottomLogs: true,
|
|
17
|
-
bottomLogLines: new Map(), // statusKey -> { action, service, lines: [] }
|
|
18
|
-
bottomLogTails: new Map(), // statusKey -> childProcess (log tail after restart)
|
|
19
|
-
selectedLogKey: null, // statusKey of cursor-selected container for log tailing
|
|
20
|
-
logCounts: new Map(), // statusKey -> Map<pattern, count>
|
|
21
|
-
logLines: [], // buffered log lines for full log view
|
|
22
|
-
logScrollOffset: 0, // lines from bottom (0 = at bottom)
|
|
23
|
-
logAutoScroll: true, // auto-scroll to bottom on new data
|
|
24
|
-
config,
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function statusKey(file, service) {
|
|
29
|
-
return `${file}::${service}`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function buildFlatList(groups) {
|
|
33
|
-
const list = [];
|
|
34
|
-
for (let gi = 0; gi < groups.length; gi++) {
|
|
35
|
-
const g = groups[gi];
|
|
36
|
-
for (let si = 0; si < g.services.length; si++) {
|
|
37
|
-
list.push({ groupIdx: gi, serviceIdx: si, service: g.services[si], file: g.file });
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return list;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function moveCursor(state, delta) {
|
|
44
|
-
if (state.flatList.length === 0) return;
|
|
45
|
-
state.cursor = Math.max(0, Math.min(state.flatList.length - 1, state.cursor + delta));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function selectedEntry(state) {
|
|
49
|
-
return state.flatList[state.cursor] || null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
module.exports = { MODE, createState, statusKey, buildFlatList, moveCursor, selectedEntry };
|