recomposable 1.1.0 → 1.1.2

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.js ADDED
@@ -0,0 +1,1418 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.createModuleState = createModuleState;
8
+ exports.loadConfig = loadConfig;
9
+ exports.discoverServices = discoverServices;
10
+ exports.pollStatuses = pollStatuses;
11
+ exports.pollLogCounts = pollLogCounts;
12
+ exports.pollContainerStats = pollContainerStats;
13
+ exports.render = render;
14
+ exports.stripAnsi = stripAnsi;
15
+ exports.throttledRender = throttledRender;
16
+ exports.updateSelectedLogs = updateSelectedLogs;
17
+ exports.doRebuild = doRebuild;
18
+ exports.doRestart = doRestart;
19
+ exports.doStop = doStop;
20
+ exports.doStart = doStart;
21
+ exports.doWatch = doWatch;
22
+ exports.initDepGraphs = initDepGraphs;
23
+ exports.doCascadeRebuild = doCascadeRebuild;
24
+ exports.enterExecInline = enterExecInline;
25
+ exports.enterExec = enterExec;
26
+ exports.exitExec = exitExec;
27
+ exports.runExecCommand = runExecCommand;
28
+ exports.enterLogs = enterLogs;
29
+ exports.exitLogs = exitLogs;
30
+ exports.executeLogSearch = executeLogSearch;
31
+ exports.jumpToNextMatch = jumpToNextMatch;
32
+ exports.jumpToPrevMatch = jumpToPrevMatch;
33
+ exports.handleKeypress = handleKeypress;
34
+ exports.createInputHandler = createInputHandler;
35
+ exports.cleanup = cleanup;
36
+ exports._getModuleState = _getModuleState;
37
+ exports._setModuleState = _setModuleState;
38
+ const fs_1 = __importDefault(require("fs"));
39
+ const path_1 = __importDefault(require("path"));
40
+ const docker_1 = require("./lib/docker");
41
+ const state_1 = require("./lib/state");
42
+ const renderer_1 = require("./lib/renderer");
43
+ function createModuleState() {
44
+ return {
45
+ logScanActive: false,
46
+ statsPollActive: false,
47
+ lastRenderTime: 0,
48
+ pendingRender: null,
49
+ logFetchTimer: null,
50
+ };
51
+ }
52
+ let moduleState = createModuleState();
53
+ // --- Config ---
54
+ function loadConfig() {
55
+ const defaults = {
56
+ composeFiles: [],
57
+ pollInterval: 3000,
58
+ logTailLines: 100,
59
+ logScanPatterns: ['WRN]', 'ERR]'],
60
+ logScanLines: 1000,
61
+ logScanInterval: 10000,
62
+ statsInterval: 5000,
63
+ statsBufferSize: 6,
64
+ bottomLogCount: 10,
65
+ cpuWarnThreshold: 50,
66
+ cpuDangerThreshold: 100,
67
+ memWarnThreshold: 512,
68
+ memDangerThreshold: 1024,
69
+ };
70
+ const configPath = path_1.default.join(process.cwd(), 'recomposable.json');
71
+ if (fs_1.default.existsSync(configPath)) {
72
+ Object.assign(defaults, JSON.parse(fs_1.default.readFileSync(configPath, 'utf8')));
73
+ }
74
+ const args = process.argv.slice(2);
75
+ const cliFiles = [];
76
+ for (let i = 0; i < args.length; i++) {
77
+ if (args[i] === '-f' && args[i + 1]) {
78
+ cliFiles.push(args[++i]);
79
+ }
80
+ }
81
+ if (cliFiles.length > 0) {
82
+ defaults.composeFiles = cliFiles;
83
+ }
84
+ if (defaults.composeFiles.length === 0) {
85
+ process.stderr.write('No compose files configured. Add them to recomposable.json or pass -f <file>.\n');
86
+ process.exit(1);
87
+ }
88
+ return defaults;
89
+ }
90
+ // --- Service Discovery ---
91
+ function discoverServices(config) {
92
+ const groups = [];
93
+ for (const file of config.composeFiles) {
94
+ const resolved = path_1.default.resolve(file);
95
+ const label = path_1.default.basename(file, path_1.default.extname(file)).replace(/^docker-compose\.?/, '') || path_1.default.basename(file);
96
+ let services = [];
97
+ let error = null;
98
+ try {
99
+ services = (0, docker_1.listServices)(resolved);
100
+ }
101
+ catch (e) {
102
+ const msg = e instanceof Error ? e.message : String(e);
103
+ error = msg.split('\n')[0].substring(0, 60);
104
+ }
105
+ groups.push({ file: resolved, label, services, error });
106
+ }
107
+ return groups;
108
+ }
109
+ // --- Status Polling ---
110
+ function pollStatuses(state) {
111
+ for (const group of state.groups) {
112
+ if (group.error)
113
+ continue;
114
+ const statuses = (0, docker_1.getStatuses)(group.file);
115
+ for (const [svc, st] of statuses) {
116
+ state.statuses.set((0, state_1.statusKey)(group.file, svc), st);
117
+ }
118
+ }
119
+ }
120
+ // --- Log Pattern Scanning ---
121
+ function pollLogCounts(state) {
122
+ if (moduleState.logScanActive)
123
+ return;
124
+ const scanPatterns = state.config.logScanPatterns || [];
125
+ if (scanPatterns.length === 0)
126
+ return;
127
+ const tailLines = state.config.logScanLines || 1000;
128
+ const toScan = [];
129
+ for (const group of state.groups) {
130
+ if (group.error)
131
+ continue;
132
+ for (const service of group.services) {
133
+ const sk = (0, state_1.statusKey)(group.file, service);
134
+ const st = state.statuses.get(sk);
135
+ if (!st || st.state !== 'running' || !st.id)
136
+ continue;
137
+ toScan.push({ sk, containerId: st.id });
138
+ }
139
+ }
140
+ if (toScan.length === 0)
141
+ return;
142
+ moduleState.logScanActive = true;
143
+ let remaining = toScan.length;
144
+ for (const { sk, containerId } of toScan) {
145
+ const child = (0, docker_1.fetchContainerLogs)(containerId, tailLines);
146
+ let output = '';
147
+ child.stdout.on('data', (d) => { output += d.toString(); });
148
+ child.stderr.on('data', (d) => { output += d.toString(); });
149
+ child.on('close', () => {
150
+ const counts = new Map();
151
+ for (const pattern of scanPatterns) {
152
+ let count = 0;
153
+ let idx = 0;
154
+ while ((idx = output.indexOf(pattern, idx)) !== -1) {
155
+ count++;
156
+ idx += pattern.length;
157
+ }
158
+ counts.set(pattern, count);
159
+ }
160
+ state.logCounts.set(sk, counts);
161
+ remaining--;
162
+ if (remaining === 0) {
163
+ moduleState.logScanActive = false;
164
+ if (state.mode === state_1.MODE.LIST)
165
+ throttledRender(state);
166
+ }
167
+ });
168
+ child.on('error', () => {
169
+ remaining--;
170
+ if (remaining === 0) {
171
+ moduleState.logScanActive = false;
172
+ if (state.mode === state_1.MODE.LIST)
173
+ throttledRender(state);
174
+ }
175
+ });
176
+ }
177
+ }
178
+ // --- Stats Polling ---
179
+ function pollContainerStats(state) {
180
+ if (moduleState.statsPollActive)
181
+ return;
182
+ const idToKey = new Map();
183
+ for (const group of state.groups) {
184
+ if (group.error)
185
+ continue;
186
+ for (const service of group.services) {
187
+ const sk = (0, state_1.statusKey)(group.file, service);
188
+ const st = state.statuses.get(sk);
189
+ if (!st || st.state !== 'running' || !st.id)
190
+ continue;
191
+ idToKey.set(st.id, sk);
192
+ }
193
+ }
194
+ const ids = [...idToKey.keys()];
195
+ if (ids.length === 0)
196
+ return;
197
+ moduleState.statsPollActive = true;
198
+ const child = (0, docker_1.fetchContainerStats)(ids);
199
+ let output = '';
200
+ child.stdout.on('data', (d) => { output += d.toString(); });
201
+ child.stderr.on('data', () => { });
202
+ child.on('close', () => {
203
+ moduleState.statsPollActive = false;
204
+ const bufferSize = state.config.statsBufferSize || 6;
205
+ for (const line of output.trim().split('\n')) {
206
+ if (!line.trim())
207
+ continue;
208
+ const parsed = (0, docker_1.parseStatsLine)(line);
209
+ if (!parsed)
210
+ continue;
211
+ let sk = null;
212
+ for (const [id, key] of idToKey) {
213
+ if (parsed.id.startsWith(id) || id.startsWith(parsed.id)) {
214
+ sk = key;
215
+ break;
216
+ }
217
+ }
218
+ if (!sk)
219
+ continue;
220
+ if (!state.containerStatsHistory.has(sk)) {
221
+ state.containerStatsHistory.set(sk, { cpu: new Array(bufferSize).fill(0), mem: new Array(bufferSize).fill(0), idx: 0, count: 0 });
222
+ }
223
+ const hist = state.containerStatsHistory.get(sk);
224
+ hist.cpu[hist.idx] = parsed.cpuPercent;
225
+ hist.mem[hist.idx] = parsed.memUsageBytes;
226
+ hist.idx = (hist.idx + 1) % bufferSize;
227
+ hist.count = Math.min(hist.count + 1, bufferSize);
228
+ let cpuSum = 0, memSum = 0;
229
+ for (let i = 0; i < hist.count; i++) {
230
+ cpuSum += hist.cpu[i];
231
+ memSum += hist.mem[i];
232
+ }
233
+ state.containerStats.set(sk, {
234
+ cpuPercent: cpuSum / hist.count,
235
+ memUsageBytes: memSum / hist.count,
236
+ });
237
+ }
238
+ if (state.mode === state_1.MODE.LIST)
239
+ throttledRender(state);
240
+ });
241
+ child.on('error', () => {
242
+ moduleState.statsPollActive = false;
243
+ });
244
+ }
245
+ // --- Rendering ---
246
+ function render(state) {
247
+ let output = (0, renderer_1.clearScreen)();
248
+ if (state.mode === state_1.MODE.LIST) {
249
+ output += (0, renderer_1.renderListView)(state);
250
+ }
251
+ else if (state.mode === state_1.MODE.LOGS) {
252
+ output += (0, renderer_1.renderLogView)(state);
253
+ }
254
+ else if (state.mode === state_1.MODE.EXEC) {
255
+ output += (0, renderer_1.renderExecView)(state);
256
+ }
257
+ process.stdout.write(output);
258
+ }
259
+ function stripAnsi(str) {
260
+ return str.replace(/\x1b\[[0-9;?]*[a-zA-Z]|\x1b\][^\x07]*\x07|\x1b[^[\]]/g, '');
261
+ }
262
+ function throttledRender(state) {
263
+ const now = Date.now();
264
+ const elapsed = now - moduleState.lastRenderTime;
265
+ if (elapsed >= 150) {
266
+ moduleState.lastRenderTime = now;
267
+ render(state);
268
+ }
269
+ else if (!moduleState.pendingRender) {
270
+ moduleState.pendingRender = setTimeout(() => {
271
+ moduleState.pendingRender = null;
272
+ moduleState.lastRenderTime = Date.now();
273
+ render(state);
274
+ }, 150 - elapsed);
275
+ }
276
+ }
277
+ // --- Actions ---
278
+ function updateSelectedLogs(state) {
279
+ const entry = (0, state_1.selectedEntry)(state);
280
+ if (!entry)
281
+ return;
282
+ const sk = (0, state_1.statusKey)(entry.file, entry.service);
283
+ if (state.selectedLogKey === sk)
284
+ return;
285
+ state.bottomSearchQuery = '';
286
+ state.bottomSearchActive = false;
287
+ if (moduleState.logFetchTimer) {
288
+ clearTimeout(moduleState.logFetchTimer);
289
+ moduleState.logFetchTimer = null;
290
+ }
291
+ if (state.selectedLogKey) {
292
+ const oldInfo = state.bottomLogLines.get(state.selectedLogKey);
293
+ if (oldInfo && (oldInfo.action === 'logs' || oldInfo.action === 'started')) {
294
+ if (!state.rebuilding.has(state.selectedLogKey) && !state.restarting.has(state.selectedLogKey)) {
295
+ state.bottomLogLines.delete(state.selectedLogKey);
296
+ if (state.bottomLogTails.has(state.selectedLogKey)) {
297
+ state.bottomLogTails.get(state.selectedLogKey).kill('SIGTERM');
298
+ state.bottomLogTails.delete(state.selectedLogKey);
299
+ }
300
+ }
301
+ }
302
+ }
303
+ state.selectedLogKey = sk;
304
+ if (state.bottomLogLines.has(sk))
305
+ return;
306
+ state.bottomLogLines.set(sk, { action: 'logs', service: entry.service, lines: [] });
307
+ moduleState.logFetchTimer = setTimeout(() => {
308
+ moduleState.logFetchTimer = null;
309
+ startBottomLogTail(state, sk, entry.file, entry.service);
310
+ }, 500);
311
+ }
312
+ function startBottomLogTail(state, sk, file, service) {
313
+ if (state.bottomLogTails.has(sk)) {
314
+ state.bottomLogTails.get(sk).kill('SIGTERM');
315
+ state.bottomLogTails.delete(sk);
316
+ }
317
+ const containerId = (0, docker_1.getContainerId)(file, service);
318
+ if (!containerId)
319
+ return;
320
+ const maxLines = state.config.bottomLogCount || 10;
321
+ const logChild = (0, docker_1.tailContainerLogs)(containerId, maxLines);
322
+ state.bottomLogTails.set(sk, logChild);
323
+ let buf = '';
324
+ const onData = (data) => {
325
+ const info = state.bottomLogLines.get(sk);
326
+ if (!info)
327
+ return;
328
+ buf += data.toString();
329
+ const parts = buf.split(/\r?\n|\r/);
330
+ buf = parts.pop();
331
+ const newLines = parts.filter(l => l.trim().length > 0).map(stripAnsi).filter(Boolean);
332
+ if (newLines.length === 0)
333
+ return;
334
+ info.lines.push(...newLines);
335
+ if (info.lines.length > maxLines)
336
+ info.lines = info.lines.slice(-maxLines);
337
+ if (state.mode === state_1.MODE.LIST)
338
+ throttledRender(state);
339
+ };
340
+ logChild.stdout.on('data', onData);
341
+ logChild.stderr.on('data', onData);
342
+ }
343
+ function doRebuild(state) {
344
+ const entry = (0, state_1.selectedEntry)(state);
345
+ if (!entry)
346
+ return;
347
+ const sk = (0, state_1.statusKey)(entry.file, entry.service);
348
+ if (state.rebuilding.has(sk))
349
+ return;
350
+ if (state.bottomLogTails.has(sk)) {
351
+ state.bottomLogTails.get(sk).kill('SIGTERM');
352
+ state.bottomLogTails.delete(sk);
353
+ }
354
+ const child = (0, docker_1.rebuildService)(entry.file, entry.service, { noCache: state.noCache });
355
+ state.rebuilding.set(sk, child);
356
+ state.bottomLogLines.set(sk, { action: 'rebuilding', service: entry.service, lines: [] });
357
+ let lineBuf = '';
358
+ const onData = (data) => {
359
+ const info = state.bottomLogLines.get(sk);
360
+ if (!info)
361
+ return;
362
+ lineBuf += data.toString();
363
+ const parts = lineBuf.split(/\r?\n|\r/);
364
+ lineBuf = parts.pop();
365
+ const newLines = parts.filter(l => l.trim().length > 0).map(stripAnsi).filter(Boolean);
366
+ if (newLines.length === 0)
367
+ return;
368
+ info.lines.push(...newLines);
369
+ const maxLines = state.config.bottomLogCount || 10;
370
+ if (info.lines.length > maxLines)
371
+ info.lines = info.lines.slice(-maxLines);
372
+ if (state.mode === state_1.MODE.LIST)
373
+ throttledRender(state);
374
+ };
375
+ child.stdout.on('data', onData);
376
+ child.stderr.on('data', onData);
377
+ render(state);
378
+ child.on('close', () => {
379
+ state.rebuilding.delete(sk);
380
+ state.containerStatsHistory.delete(sk);
381
+ state.containerStats.delete(sk);
382
+ pollStatuses(state);
383
+ const info = state.bottomLogLines.get(sk);
384
+ if (info) {
385
+ info.action = 'started';
386
+ info.lines = [];
387
+ }
388
+ startBottomLogTail(state, sk, entry.file, entry.service);
389
+ if (state.mode === state_1.MODE.LIST)
390
+ render(state);
391
+ });
392
+ }
393
+ function doRestart(state) {
394
+ const entry = (0, state_1.selectedEntry)(state);
395
+ if (!entry)
396
+ return;
397
+ const sk = (0, state_1.statusKey)(entry.file, entry.service);
398
+ if (state.restarting.has(sk) || state.rebuilding.has(sk))
399
+ return;
400
+ if (state.bottomLogTails.has(sk)) {
401
+ state.bottomLogTails.get(sk).kill('SIGTERM');
402
+ state.bottomLogTails.delete(sk);
403
+ }
404
+ const child = (0, docker_1.restartService)(entry.file, entry.service);
405
+ state.restarting.set(sk, child);
406
+ state.bottomLogLines.set(sk, { action: 'restarting', service: entry.service, lines: [] });
407
+ render(state);
408
+ child.on('close', () => {
409
+ state.restarting.delete(sk);
410
+ state.containerStatsHistory.delete(sk);
411
+ state.containerStats.delete(sk);
412
+ pollStatuses(state);
413
+ const info = state.bottomLogLines.get(sk);
414
+ if (info) {
415
+ info.action = 'started';
416
+ info.lines = [];
417
+ }
418
+ startBottomLogTail(state, sk, entry.file, entry.service);
419
+ if (state.mode === state_1.MODE.LIST)
420
+ render(state);
421
+ });
422
+ }
423
+ function doStop(state) {
424
+ const entry = (0, state_1.selectedEntry)(state);
425
+ if (!entry)
426
+ return;
427
+ const sk = (0, state_1.statusKey)(entry.file, entry.service);
428
+ if (state.stopping.has(sk) || state.rebuilding.has(sk) || state.restarting.has(sk))
429
+ return;
430
+ const st = state.statuses.get(sk);
431
+ if (!st || st.state !== 'running')
432
+ return;
433
+ if (state.bottomLogTails.has(sk)) {
434
+ state.bottomLogTails.get(sk).kill('SIGTERM');
435
+ state.bottomLogTails.delete(sk);
436
+ }
437
+ const child = (0, docker_1.stopService)(entry.file, entry.service);
438
+ state.stopping.set(sk, child);
439
+ state.bottomLogLines.set(sk, { action: 'stopping', service: entry.service, lines: [] });
440
+ render(state);
441
+ child.on('close', () => {
442
+ state.stopping.delete(sk);
443
+ state.bottomLogLines.delete(sk);
444
+ pollStatuses(state);
445
+ if (state.mode === state_1.MODE.LIST)
446
+ render(state);
447
+ });
448
+ }
449
+ function doStart(state) {
450
+ const entry = (0, state_1.selectedEntry)(state);
451
+ if (!entry)
452
+ return;
453
+ const sk = (0, state_1.statusKey)(entry.file, entry.service);
454
+ if (state.starting.has(sk) || state.rebuilding.has(sk) || state.restarting.has(sk) || state.stopping.has(sk))
455
+ return;
456
+ const st = state.statuses.get(sk);
457
+ if (st && st.state === 'running')
458
+ return;
459
+ const child = (0, docker_1.startService)(entry.file, entry.service);
460
+ state.starting.set(sk, child);
461
+ state.bottomLogLines.set(sk, { action: 'starting', service: entry.service, lines: [] });
462
+ render(state);
463
+ child.on('close', () => {
464
+ state.starting.delete(sk);
465
+ pollStatuses(state);
466
+ const info = state.bottomLogLines.get(sk);
467
+ if (info) {
468
+ info.action = 'started';
469
+ info.lines = [];
470
+ }
471
+ startBottomLogTail(state, sk, entry.file, entry.service);
472
+ if (state.mode === state_1.MODE.LIST)
473
+ render(state);
474
+ });
475
+ }
476
+ // --- Watch ---
477
+ function doWatch(state) {
478
+ const entry = (0, state_1.selectedEntry)(state);
479
+ if (!entry)
480
+ return;
481
+ const sk = (0, state_1.statusKey)(entry.file, entry.service);
482
+ // Toggle off if already watching
483
+ if (state.watching.has(sk)) {
484
+ state.watching.get(sk).kill('SIGTERM');
485
+ state.watching.delete(sk);
486
+ const info = state.bottomLogLines.get(sk);
487
+ if (info && info.action === 'watching') {
488
+ state.bottomLogLines.delete(sk);
489
+ }
490
+ render(state);
491
+ return;
492
+ }
493
+ // Check availability on first use
494
+ if (state.watchAvailable === null) {
495
+ state.watchAvailable = (0, docker_1.isWatchAvailable)();
496
+ }
497
+ if (!state.watchAvailable) {
498
+ state.bottomLogLines.set(sk, { action: 'watching', service: entry.service, lines: ['docker compose watch is not available (requires Docker Compose v2.22+)'] });
499
+ state.showBottomLogs = true;
500
+ render(state);
501
+ return;
502
+ }
503
+ const child = (0, docker_1.watchService)(entry.file, entry.service);
504
+ state.watching.set(sk, child);
505
+ state.bottomLogLines.set(sk, { action: 'watching', service: entry.service, lines: [] });
506
+ state.showBottomLogs = true;
507
+ let lineBuf = '';
508
+ const maxLines = state.config.bottomLogCount || 10;
509
+ const onData = (data) => {
510
+ const info = state.bottomLogLines.get(sk);
511
+ if (!info)
512
+ return;
513
+ lineBuf += data.toString();
514
+ const parts = lineBuf.split(/\r?\n|\r/);
515
+ lineBuf = parts.pop();
516
+ const newLines = parts.filter(l => l.trim().length > 0).map(stripAnsi).filter(Boolean);
517
+ if (newLines.length === 0)
518
+ return;
519
+ info.lines.push(...newLines);
520
+ if (info.lines.length > maxLines)
521
+ info.lines = info.lines.slice(-maxLines);
522
+ if (state.mode === state_1.MODE.LIST)
523
+ throttledRender(state);
524
+ };
525
+ child.stdout.on('data', onData);
526
+ child.stderr.on('data', onData);
527
+ child.on('close', () => {
528
+ state.watching.delete(sk);
529
+ if (state.mode === state_1.MODE.LIST)
530
+ render(state);
531
+ });
532
+ render(state);
533
+ }
534
+ // --- Dependency-Aware Rebuild ---
535
+ function initDepGraphs(state) {
536
+ for (const group of state.groups) {
537
+ if (group.error)
538
+ continue;
539
+ try {
540
+ state.depGraphs.set(group.file, (0, docker_1.parseDependencyGraph)(group.file));
541
+ }
542
+ catch {
543
+ // Ignore — no dep info for this file
544
+ }
545
+ }
546
+ }
547
+ function getTransitiveDependents(graph, service) {
548
+ const visited = new Set();
549
+ const queue = [service];
550
+ while (queue.length > 0) {
551
+ const current = queue.shift();
552
+ const deps = graph.dependedBy.get(current) || [];
553
+ for (const dep of deps) {
554
+ if (!visited.has(dep)) {
555
+ visited.add(dep);
556
+ queue.push(dep);
557
+ }
558
+ }
559
+ }
560
+ return [...visited];
561
+ }
562
+ function topoSortDependents(graph, services, root) {
563
+ // Topological sort of the dependent services, so prerequisites come first
564
+ const serviceSet = new Set(services);
565
+ const inDegree = new Map();
566
+ const adj = new Map();
567
+ for (const svc of services) {
568
+ inDegree.set(svc, 0);
569
+ adj.set(svc, []);
570
+ }
571
+ for (const svc of services) {
572
+ const deps = (graph.dependsOn.get(svc) || []).filter(d => serviceSet.has(d) || d === root);
573
+ for (const dep of deps) {
574
+ if (serviceSet.has(dep)) {
575
+ adj.get(dep).push(svc);
576
+ inDegree.set(svc, (inDegree.get(svc) || 0) + 1);
577
+ }
578
+ }
579
+ }
580
+ const sorted = [];
581
+ const queue = services.filter(s => (inDegree.get(s) || 0) === 0);
582
+ while (queue.length > 0) {
583
+ const current = queue.shift();
584
+ sorted.push(current);
585
+ for (const neighbor of (adj.get(current) || [])) {
586
+ const deg = (inDegree.get(neighbor) || 1) - 1;
587
+ inDegree.set(neighbor, deg);
588
+ if (deg === 0)
589
+ queue.push(neighbor);
590
+ }
591
+ }
592
+ return sorted;
593
+ }
594
+ function doCascadeRebuild(state) {
595
+ const entry = (0, state_1.selectedEntry)(state);
596
+ if (!entry)
597
+ return;
598
+ const sk = (0, state_1.statusKey)(entry.file, entry.service);
599
+ if (state.rebuilding.has(sk) || state.cascading.has(sk))
600
+ return;
601
+ const graph = state.depGraphs.get(entry.file);
602
+ if (!graph) {
603
+ // No graph available, fall back to regular rebuild
604
+ doRebuild(state);
605
+ return;
606
+ }
607
+ const dependents = getTransitiveDependents(graph, entry.service);
608
+ if (dependents.length === 0) {
609
+ // No dependents, fall back to regular rebuild
610
+ doRebuild(state);
611
+ return;
612
+ }
613
+ const sorted = topoSortDependents(graph, dependents, entry.service);
614
+ const steps = [
615
+ { action: 'rebuild', service: entry.service, status: 'pending' },
616
+ ...sorted.map(svc => ({ action: 'restart', service: svc, status: 'pending' })),
617
+ ];
618
+ const cascade = { steps, currentStepIdx: 0, child: null };
619
+ state.cascading.set(sk, cascade);
620
+ state.bottomLogLines.set(sk, { action: 'cascading', service: entry.service, lines: [] });
621
+ state.showBottomLogs = true;
622
+ executeCascadeStep(state, entry.file, sk, cascade);
623
+ render(state);
624
+ }
625
+ function executeCascadeStep(state, file, sk, cascade) {
626
+ const step = cascade.steps[cascade.currentStepIdx];
627
+ if (!step) {
628
+ // All done
629
+ state.cascading.delete(sk);
630
+ pollStatuses(state);
631
+ if (state.mode === state_1.MODE.LIST)
632
+ render(state);
633
+ return;
634
+ }
635
+ step.status = 'in_progress';
636
+ const maxLines = state.config.bottomLogCount || 10;
637
+ let child;
638
+ if (step.action === 'rebuild') {
639
+ child = (0, docker_1.rebuildService)(file, step.service, { noCache: state.noCache });
640
+ }
641
+ else {
642
+ child = (0, docker_1.restartService)(file, step.service);
643
+ }
644
+ cascade.child = child;
645
+ let lineBuf = '';
646
+ const onData = (data) => {
647
+ const info = state.bottomLogLines.get(sk);
648
+ if (!info)
649
+ return;
650
+ lineBuf += data.toString();
651
+ const parts = lineBuf.split(/\r?\n|\r/);
652
+ lineBuf = parts.pop();
653
+ const newLines = parts.filter(l => l.trim().length > 0).map(stripAnsi).filter(Boolean);
654
+ if (newLines.length === 0)
655
+ return;
656
+ info.lines.push(...newLines);
657
+ if (info.lines.length > maxLines)
658
+ info.lines = info.lines.slice(-maxLines);
659
+ if (state.mode === state_1.MODE.LIST)
660
+ throttledRender(state);
661
+ };
662
+ const childProcess = child;
663
+ childProcess.stdout.on('data', onData);
664
+ childProcess.stderr.on('data', onData);
665
+ childProcess.on('close', (code) => {
666
+ if (code !== 0 && code !== null) {
667
+ step.status = 'failed';
668
+ state.cascading.delete(sk);
669
+ pollStatuses(state);
670
+ if (state.mode === state_1.MODE.LIST)
671
+ render(state);
672
+ return;
673
+ }
674
+ step.status = 'completed';
675
+ cascade.currentStepIdx++;
676
+ cascade.child = null;
677
+ // Reset stats for rebuilt/restarted service
678
+ const stepSk = (0, state_1.statusKey)(file, step.service);
679
+ state.containerStatsHistory.delete(stepSk);
680
+ state.containerStats.delete(stepSk);
681
+ if (cascade.currentStepIdx < cascade.steps.length) {
682
+ executeCascadeStep(state, file, sk, cascade);
683
+ }
684
+ else {
685
+ state.cascading.delete(sk);
686
+ pollStatuses(state);
687
+ const info = state.bottomLogLines.get(sk);
688
+ if (info) {
689
+ info.action = 'started';
690
+ info.lines = [];
691
+ }
692
+ startBottomLogTail(state, sk, file, state.flatList[state.cursor]?.service || '');
693
+ }
694
+ if (state.mode === state_1.MODE.LIST)
695
+ render(state);
696
+ });
697
+ }
698
+ // --- Exec ---
699
+ function initExecState(state) {
700
+ const entry = (0, state_1.selectedEntry)(state);
701
+ if (!entry)
702
+ return false;
703
+ const sk = (0, state_1.statusKey)(entry.file, entry.service);
704
+ const st = state.statuses.get(sk);
705
+ if (!st || st.state !== 'running' || !st.id)
706
+ return false;
707
+ state.execInput = '';
708
+ state.execOutputLines = [];
709
+ state.execHistoryIdx = -1;
710
+ state.execContainerId = st.id;
711
+ state.execService = entry.service;
712
+ state.execChild = null;
713
+ state.execCwd = null;
714
+ return true;
715
+ }
716
+ function enterExecInline(state) {
717
+ if (!initExecState(state))
718
+ return;
719
+ state.execActive = true;
720
+ state.showBottomLogs = true;
721
+ render(state);
722
+ }
723
+ function enterExec(state) {
724
+ if (!state.execActive) {
725
+ if (!initExecState(state))
726
+ return;
727
+ }
728
+ state.execActive = false;
729
+ state.mode = state_1.MODE.EXEC;
730
+ render(state);
731
+ }
732
+ function exitExec(state) {
733
+ if (state.execChild) {
734
+ state.execChild.kill('SIGTERM');
735
+ state.execChild = null;
736
+ }
737
+ const wasFullscreen = state.mode === state_1.MODE.EXEC;
738
+ state.mode = state_1.MODE.LIST;
739
+ state.execActive = false;
740
+ state.execInput = '';
741
+ state.execOutputLines = [];
742
+ state.execHistoryIdx = -1;
743
+ state.execContainerId = null;
744
+ state.execService = null;
745
+ state.execCwd = null;
746
+ if (wasFullscreen)
747
+ pollStatuses(state);
748
+ render(state);
749
+ }
750
+ function isCdCommand(cmd) {
751
+ const match = cmd.match(/^cd(\s+(.*))?$/);
752
+ if (!match)
753
+ return null;
754
+ return match[2] ? match[2].trim() : '';
755
+ }
756
+ function runExecCommand(state) {
757
+ const cmd = state.execInput.trim();
758
+ if (!cmd || !state.execContainerId)
759
+ return;
760
+ // Add to history
761
+ if (state.execHistory.length === 0 || state.execHistory[state.execHistory.length - 1] !== cmd) {
762
+ state.execHistory.push(cmd);
763
+ }
764
+ state.execHistoryIdx = -1;
765
+ state.execInput = '';
766
+ // Kill previous exec if still running
767
+ if (state.execChild) {
768
+ state.execChild.kill('SIGTERM');
769
+ state.execChild = null;
770
+ }
771
+ state.execOutputLines.push(`$ ${cmd}`);
772
+ // Handle cd commands — resolve new working directory
773
+ const cdTarget = isCdCommand(cmd);
774
+ if (cdTarget !== null) {
775
+ const resolveCmd = cdTarget ? `cd ${cdTarget} && pwd` : 'cd && pwd';
776
+ const child = (0, docker_1.execInContainer)(state.execContainerId, resolveCmd, state.execCwd || undefined);
777
+ state.execChild = child;
778
+ let stdout = '';
779
+ let stderr = '';
780
+ child.stdout.on('data', (d) => { stdout += d.toString(); });
781
+ child.stderr.on('data', (d) => { stderr += d.toString(); });
782
+ child.on('close', (code) => {
783
+ if (state.execChild === child)
784
+ state.execChild = null;
785
+ if (code === 0) {
786
+ const lines = stdout.trim().split('\n');
787
+ const newCwd = lines[lines.length - 1].trim();
788
+ if (newCwd)
789
+ state.execCwd = newCwd;
790
+ }
791
+ else {
792
+ const errLines = stderr.trim().split('\n').filter(Boolean);
793
+ for (const line of errLines) {
794
+ state.execOutputLines.push(stripAnsi(line));
795
+ }
796
+ }
797
+ if (state.mode === state_1.MODE.EXEC || state.execActive)
798
+ throttledRender(state);
799
+ });
800
+ render(state);
801
+ return;
802
+ }
803
+ const child = (0, docker_1.execInContainer)(state.execContainerId, cmd, state.execCwd || undefined);
804
+ state.execChild = child;
805
+ let lineBuf = '';
806
+ const onData = (data) => {
807
+ lineBuf += data.toString();
808
+ const parts = lineBuf.split(/\r?\n|\r/);
809
+ lineBuf = parts.pop();
810
+ const newLines = parts.filter(l => l.length > 0).map(stripAnsi).filter(Boolean);
811
+ if (newLines.length === 0)
812
+ return;
813
+ state.execOutputLines.push(...newLines);
814
+ if (state.execOutputLines.length > 200) {
815
+ state.execOutputLines = state.execOutputLines.slice(-200);
816
+ }
817
+ if (state.mode === state_1.MODE.EXEC || state.execActive)
818
+ throttledRender(state);
819
+ };
820
+ child.stdout.on('data', onData);
821
+ child.stderr.on('data', onData);
822
+ child.on('close', () => {
823
+ if (state.execChild === child) {
824
+ state.execChild = null;
825
+ }
826
+ // Flush remaining buffer
827
+ if (lineBuf.trim()) {
828
+ state.execOutputLines.push(stripAnsi(lineBuf));
829
+ lineBuf = '';
830
+ }
831
+ if (state.mode === state_1.MODE.EXEC || state.execActive)
832
+ throttledRender(state);
833
+ });
834
+ render(state);
835
+ }
836
+ function enterLogs(state) {
837
+ const entry = (0, state_1.selectedEntry)(state);
838
+ if (!entry)
839
+ return;
840
+ if (moduleState.logFetchTimer) {
841
+ clearTimeout(moduleState.logFetchTimer);
842
+ moduleState.logFetchTimer = null;
843
+ }
844
+ state.mode = state_1.MODE.LOGS;
845
+ state.logLines = [];
846
+ state.logScrollOffset = 0;
847
+ state.logAutoScroll = true;
848
+ state.logSearchQuery = '';
849
+ state.logSearchActive = false;
850
+ state.logSearchMatches = [];
851
+ state.logSearchMatchIdx = -1;
852
+ const child = (0, docker_1.tailLogs)(entry.file, entry.service, state.config.logTailLines);
853
+ state.logChild = child;
854
+ let lineBuf = '';
855
+ const onData = (data) => {
856
+ lineBuf += data.toString();
857
+ const parts = lineBuf.split(/\r?\n|\r/);
858
+ lineBuf = parts.pop();
859
+ if (parts.length === 0)
860
+ return;
861
+ for (const line of parts) {
862
+ state.logLines.push(stripAnsi(line));
863
+ }
864
+ if (state.logLines.length > 10000) {
865
+ const excess = state.logLines.length - 10000;
866
+ state.logLines.splice(0, excess);
867
+ if (!state.logAutoScroll) {
868
+ state.logScrollOffset = Math.max(0, state.logScrollOffset - excess);
869
+ }
870
+ }
871
+ if (state.logAutoScroll) {
872
+ throttledRender(state);
873
+ }
874
+ };
875
+ child.stdout.on('data', onData);
876
+ child.stderr.on('data', onData);
877
+ child.on('close', () => {
878
+ if (state.logChild === child) {
879
+ state.logChild = null;
880
+ }
881
+ });
882
+ render(state);
883
+ }
884
+ function exitLogs(state) {
885
+ if (state.logChild) {
886
+ state.logChild.kill('SIGTERM');
887
+ state.logChild = null;
888
+ }
889
+ state.logLines = [];
890
+ state.mode = state_1.MODE.LIST;
891
+ pollStatuses(state);
892
+ render(state);
893
+ }
894
+ // --- Log Search ---
895
+ function executeLogSearch(state) {
896
+ const query = state.logSearchQuery;
897
+ state.logSearchMatches = [];
898
+ state.logSearchMatchIdx = -1;
899
+ if (!query)
900
+ return;
901
+ const lowerQuery = query.toLowerCase();
902
+ for (let i = 0; i < state.logLines.length; i++) {
903
+ if (state.logLines[i].toLowerCase().includes(lowerQuery)) {
904
+ state.logSearchMatches.push(i);
905
+ }
906
+ }
907
+ if (state.logSearchMatches.length > 0) {
908
+ state.logSearchMatchIdx = 0;
909
+ scrollToLogLine(state, state.logSearchMatches[0]);
910
+ }
911
+ }
912
+ function scrollToLogLine(state, lineIdx) {
913
+ const rows = process.stdout.rows ?? 24;
914
+ const headerHeight = 9;
915
+ const availableRows = Math.max(1, rows - headerHeight);
916
+ const totalLines = state.logLines.length;
917
+ state.logScrollOffset = Math.max(0, totalLines - lineIdx - Math.floor(availableRows / 2));
918
+ state.logAutoScroll = state.logScrollOffset === 0;
919
+ render(state);
920
+ }
921
+ function jumpToNextMatch(state) {
922
+ if (state.logSearchMatches.length === 0)
923
+ return;
924
+ state.logSearchMatchIdx = (state.logSearchMatchIdx + 1) % state.logSearchMatches.length;
925
+ scrollToLogLine(state, state.logSearchMatches[state.logSearchMatchIdx]);
926
+ }
927
+ function jumpToPrevMatch(state) {
928
+ if (state.logSearchMatches.length === 0)
929
+ return;
930
+ state.logSearchMatchIdx = (state.logSearchMatchIdx - 1 + state.logSearchMatches.length) % state.logSearchMatches.length;
931
+ scrollToLogLine(state, state.logSearchMatches[state.logSearchMatchIdx]);
932
+ }
933
+ // --- Input Handling ---
934
+ function handleKeypress(state, key) {
935
+ if (key === '\x03' && state.mode !== state_1.MODE.EXEC && !state.execActive) {
936
+ cleanup(state);
937
+ process.exit(0);
938
+ }
939
+ if (state.mode === state_1.MODE.EXEC) {
940
+ if (key === '\x1b') {
941
+ exitExec(state);
942
+ }
943
+ else if (key === '\r') {
944
+ runExecCommand(state);
945
+ }
946
+ else if (key === '\x7f' || key === '\b') {
947
+ state.execInput = state.execInput.slice(0, -1);
948
+ render(state);
949
+ }
950
+ else if (key === '\x1b[A') {
951
+ // Up arrow — history navigation
952
+ if (state.execHistory.length > 0) {
953
+ if (state.execHistoryIdx === -1) {
954
+ state.execHistoryIdx = state.execHistory.length - 1;
955
+ }
956
+ else if (state.execHistoryIdx > 0) {
957
+ state.execHistoryIdx--;
958
+ }
959
+ state.execInput = state.execHistory[state.execHistoryIdx] || '';
960
+ render(state);
961
+ }
962
+ }
963
+ else if (key === '\x1b[B') {
964
+ // Down arrow — history navigation
965
+ if (state.execHistoryIdx !== -1) {
966
+ if (state.execHistoryIdx < state.execHistory.length - 1) {
967
+ state.execHistoryIdx++;
968
+ state.execInput = state.execHistory[state.execHistoryIdx] || '';
969
+ }
970
+ else {
971
+ state.execHistoryIdx = -1;
972
+ state.execInput = '';
973
+ }
974
+ render(state);
975
+ }
976
+ }
977
+ else if (key === '\x03') {
978
+ // Ctrl+C — kill current exec child
979
+ if (state.execChild) {
980
+ state.execChild.kill('SIGTERM');
981
+ state.execChild = null;
982
+ state.execOutputLines.push('^C');
983
+ render(state);
984
+ }
985
+ else {
986
+ cleanup(state);
987
+ process.exit(0);
988
+ }
989
+ }
990
+ else if (key.length === 1 && key >= ' ') {
991
+ state.execInput += key;
992
+ render(state);
993
+ }
994
+ return;
995
+ }
996
+ if (state.mode === state_1.MODE.LOGS) {
997
+ if (state.logSearchActive) {
998
+ if (key === '\x1b') {
999
+ state.logSearchActive = false;
1000
+ state.logSearchQuery = '';
1001
+ render(state);
1002
+ }
1003
+ else if (key === '\r') {
1004
+ state.logSearchActive = false;
1005
+ executeLogSearch(state);
1006
+ render(state);
1007
+ }
1008
+ else if (key === '\x7f' || key === '\b') {
1009
+ state.logSearchQuery = state.logSearchQuery.slice(0, -1);
1010
+ render(state);
1011
+ }
1012
+ else if (key.length === 1 && key >= ' ') {
1013
+ state.logSearchQuery += key;
1014
+ render(state);
1015
+ }
1016
+ return;
1017
+ }
1018
+ const rows = process.stdout.rows ?? 24;
1019
+ const pageSize = Math.max(1, Math.floor(rows / 2));
1020
+ const maxOffset = Math.max(0, state.logLines.length - 1);
1021
+ switch (key) {
1022
+ case 'f':
1023
+ case '\x1b':
1024
+ exitLogs(state);
1025
+ break;
1026
+ case 'q':
1027
+ cleanup(state);
1028
+ process.exit(0);
1029
+ break;
1030
+ case 'k':
1031
+ case '\x1b[A':
1032
+ state.logAutoScroll = false;
1033
+ state.logScrollOffset = Math.min(maxOffset, state.logScrollOffset + 1);
1034
+ render(state);
1035
+ break;
1036
+ case 'j':
1037
+ case '\x1b[B':
1038
+ if (state.logScrollOffset > 0) {
1039
+ state.logScrollOffset--;
1040
+ if (state.logScrollOffset === 0)
1041
+ state.logAutoScroll = true;
1042
+ }
1043
+ render(state);
1044
+ break;
1045
+ case 'G':
1046
+ state.logScrollOffset = 0;
1047
+ state.logAutoScroll = true;
1048
+ render(state);
1049
+ break;
1050
+ case '\x15': // Ctrl+U
1051
+ state.logAutoScroll = false;
1052
+ state.logScrollOffset = Math.min(maxOffset, state.logScrollOffset + pageSize);
1053
+ render(state);
1054
+ break;
1055
+ case '\x04': // Ctrl+D
1056
+ state.logScrollOffset = Math.max(0, state.logScrollOffset - pageSize);
1057
+ if (state.logScrollOffset === 0)
1058
+ state.logAutoScroll = true;
1059
+ render(state);
1060
+ break;
1061
+ case '/':
1062
+ state.logSearchActive = true;
1063
+ state.logSearchQuery = '';
1064
+ render(state);
1065
+ break;
1066
+ case 'n':
1067
+ jumpToNextMatch(state);
1068
+ break;
1069
+ case 'N':
1070
+ jumpToPrevMatch(state);
1071
+ break;
1072
+ }
1073
+ return;
1074
+ }
1075
+ // LIST mode - inline exec input
1076
+ if (state.execActive) {
1077
+ if (key === '\x1b') {
1078
+ exitExec(state);
1079
+ }
1080
+ else if (key === '\r') {
1081
+ runExecCommand(state);
1082
+ }
1083
+ else if (key === '\x7f' || key === '\b') {
1084
+ state.execInput = state.execInput.slice(0, -1);
1085
+ render(state);
1086
+ }
1087
+ else if (key === '\x1b[A') {
1088
+ if (state.execHistory.length > 0) {
1089
+ if (state.execHistoryIdx === -1) {
1090
+ state.execHistoryIdx = state.execHistory.length - 1;
1091
+ }
1092
+ else if (state.execHistoryIdx > 0) {
1093
+ state.execHistoryIdx--;
1094
+ }
1095
+ state.execInput = state.execHistory[state.execHistoryIdx] || '';
1096
+ render(state);
1097
+ }
1098
+ }
1099
+ else if (key === '\x1b[B') {
1100
+ if (state.execHistoryIdx !== -1) {
1101
+ if (state.execHistoryIdx < state.execHistory.length - 1) {
1102
+ state.execHistoryIdx++;
1103
+ state.execInput = state.execHistory[state.execHistoryIdx] || '';
1104
+ }
1105
+ else {
1106
+ state.execHistoryIdx = -1;
1107
+ state.execInput = '';
1108
+ }
1109
+ render(state);
1110
+ }
1111
+ }
1112
+ else if (key === '\x03') {
1113
+ if (state.execChild) {
1114
+ state.execChild.kill('SIGTERM');
1115
+ state.execChild = null;
1116
+ state.execOutputLines.push('^C');
1117
+ render(state);
1118
+ }
1119
+ else {
1120
+ cleanup(state);
1121
+ process.exit(0);
1122
+ }
1123
+ }
1124
+ else if (key === 'x') {
1125
+ enterExec(state);
1126
+ }
1127
+ else if (key.length === 1 && key >= ' ') {
1128
+ state.execInput += key;
1129
+ render(state);
1130
+ }
1131
+ return;
1132
+ }
1133
+ // LIST mode - bottom panel search input
1134
+ if (state.bottomSearchActive) {
1135
+ if (key === '\x1b') {
1136
+ state.bottomSearchActive = false;
1137
+ state.bottomSearchQuery = '';
1138
+ render(state);
1139
+ }
1140
+ else if (key === '\r') {
1141
+ state.bottomSearchActive = false;
1142
+ render(state);
1143
+ }
1144
+ else if (key === '\x7f' || key === '\b') {
1145
+ state.bottomSearchQuery = state.bottomSearchQuery.slice(0, -1);
1146
+ render(state);
1147
+ }
1148
+ else if (key.length === 1 && key >= ' ') {
1149
+ state.bottomSearchQuery += key;
1150
+ render(state);
1151
+ }
1152
+ return;
1153
+ }
1154
+ // LIST mode
1155
+ switch (key) {
1156
+ case 'j':
1157
+ case '\x1b[B':
1158
+ (0, state_1.moveCursor)(state, 1);
1159
+ updateSelectedLogs(state);
1160
+ render(state);
1161
+ break;
1162
+ case 'k':
1163
+ case '\x1b[A':
1164
+ (0, state_1.moveCursor)(state, -1);
1165
+ updateSelectedLogs(state);
1166
+ render(state);
1167
+ break;
1168
+ case 'b':
1169
+ doRebuild(state);
1170
+ break;
1171
+ case 'd':
1172
+ doCascadeRebuild(state);
1173
+ break;
1174
+ case 'w':
1175
+ doWatch(state);
1176
+ break;
1177
+ case 'e':
1178
+ enterExecInline(state);
1179
+ break;
1180
+ case 'x':
1181
+ enterExec(state);
1182
+ break;
1183
+ case 's': {
1184
+ const sEntry = (0, state_1.selectedEntry)(state);
1185
+ if (sEntry) {
1186
+ const sSk = (0, state_1.statusKey)(sEntry.file, sEntry.service);
1187
+ const sSt = state.statuses.get(sSk);
1188
+ if (sSt && sSt.state === 'running') {
1189
+ doRestart(state);
1190
+ }
1191
+ else {
1192
+ doStart(state);
1193
+ }
1194
+ }
1195
+ break;
1196
+ }
1197
+ case 'p':
1198
+ doStop(state);
1199
+ break;
1200
+ case 'n':
1201
+ state.noCache = !state.noCache;
1202
+ render(state);
1203
+ break;
1204
+ case 'f':
1205
+ case '\r':
1206
+ enterLogs(state);
1207
+ break;
1208
+ case 'l':
1209
+ state.showBottomLogs = !state.showBottomLogs;
1210
+ render(state);
1211
+ break;
1212
+ case 'q':
1213
+ cleanup(state);
1214
+ process.exit(0);
1215
+ break;
1216
+ case 'G':
1217
+ state.cursor = state.flatList.length - 1;
1218
+ updateSelectedLogs(state);
1219
+ render(state);
1220
+ break;
1221
+ case 'g':
1222
+ break;
1223
+ case '/':
1224
+ if (state.showBottomLogs) {
1225
+ state.bottomSearchActive = true;
1226
+ state.bottomSearchQuery = '';
1227
+ render(state);
1228
+ }
1229
+ break;
1230
+ }
1231
+ }
1232
+ // --- Arrow key sequence buffering ---
1233
+ function createInputHandler(state) {
1234
+ let buf = '';
1235
+ let gPending = false;
1236
+ return function onData(data) {
1237
+ const str = data.toString();
1238
+ buf += str;
1239
+ while (buf.length > 0) {
1240
+ if (buf === '\x1b') {
1241
+ setTimeout(() => {
1242
+ if (buf === '\x1b') {
1243
+ handleKeypress(state, '\x1b');
1244
+ buf = '';
1245
+ }
1246
+ }, 50);
1247
+ return;
1248
+ }
1249
+ if (buf.startsWith('\x1b[A')) {
1250
+ handleKeypress(state, '\x1b[A');
1251
+ buf = buf.slice(3);
1252
+ continue;
1253
+ }
1254
+ if (buf.startsWith('\x1b[B')) {
1255
+ handleKeypress(state, '\x1b[B');
1256
+ buf = buf.slice(3);
1257
+ continue;
1258
+ }
1259
+ if (buf.startsWith('\x1b[')) {
1260
+ buf = buf.slice(buf.length);
1261
+ continue;
1262
+ }
1263
+ const ch = buf[0];
1264
+ buf = buf.slice(1);
1265
+ if (state.logSearchActive || state.bottomSearchActive || state.mode === state_1.MODE.EXEC || state.execActive) {
1266
+ handleKeypress(state, ch);
1267
+ continue;
1268
+ }
1269
+ if (ch === 'g') {
1270
+ if (gPending) {
1271
+ gPending = false;
1272
+ if (state.mode === state_1.MODE.LIST) {
1273
+ state.cursor = 0;
1274
+ state.scrollOffset = 0;
1275
+ updateSelectedLogs(state);
1276
+ }
1277
+ else if (state.mode === state_1.MODE.LOGS) {
1278
+ state.logAutoScroll = false;
1279
+ state.logScrollOffset = Math.max(0, state.logLines.length - 1);
1280
+ }
1281
+ render(state);
1282
+ continue;
1283
+ }
1284
+ gPending = true;
1285
+ setTimeout(() => {
1286
+ if (gPending) {
1287
+ gPending = false;
1288
+ }
1289
+ }, 300);
1290
+ continue;
1291
+ }
1292
+ gPending = false;
1293
+ handleKeypress(state, ch);
1294
+ }
1295
+ };
1296
+ }
1297
+ // --- Cleanup ---
1298
+ function cleanup(state) {
1299
+ if (state.logChild) {
1300
+ state.logChild.kill('SIGTERM');
1301
+ state.logChild = null;
1302
+ }
1303
+ for (const [, child] of state.rebuilding) {
1304
+ child.kill('SIGTERM');
1305
+ }
1306
+ state.rebuilding.clear();
1307
+ for (const [, child] of state.restarting) {
1308
+ child.kill('SIGTERM');
1309
+ }
1310
+ state.restarting.clear();
1311
+ for (const [, child] of state.stopping) {
1312
+ child.kill('SIGTERM');
1313
+ }
1314
+ state.stopping.clear();
1315
+ for (const [, child] of state.starting) {
1316
+ child.kill('SIGTERM');
1317
+ }
1318
+ state.starting.clear();
1319
+ for (const [, child] of state.watching) {
1320
+ child.kill('SIGTERM');
1321
+ }
1322
+ state.watching.clear();
1323
+ for (const [, cascade] of state.cascading) {
1324
+ if (cascade.child)
1325
+ cascade.child.kill('SIGTERM');
1326
+ }
1327
+ state.cascading.clear();
1328
+ if (state.execChild) {
1329
+ state.execChild.kill('SIGTERM');
1330
+ state.execChild = null;
1331
+ }
1332
+ state.execActive = false;
1333
+ for (const [, child] of state.bottomLogTails) {
1334
+ child.kill('SIGTERM');
1335
+ }
1336
+ state.bottomLogTails.clear();
1337
+ if (moduleState.logFetchTimer) {
1338
+ clearTimeout(moduleState.logFetchTimer);
1339
+ moduleState.logFetchTimer = null;
1340
+ }
1341
+ if (moduleState.pendingRender) {
1342
+ clearTimeout(moduleState.pendingRender);
1343
+ moduleState.pendingRender = null;
1344
+ }
1345
+ if (state.logScanTimer) {
1346
+ clearInterval(state.logScanTimer);
1347
+ }
1348
+ if (state.pollTimer) {
1349
+ clearInterval(state.pollTimer);
1350
+ }
1351
+ if (state.statsTimer) {
1352
+ clearInterval(state.statsTimer);
1353
+ }
1354
+ process.stdout.write('\x1b[r' + (0, renderer_1.showCursor)() + '\x1b[0m');
1355
+ }
1356
+ // Expose for testing
1357
+ function _getModuleState() {
1358
+ return moduleState;
1359
+ }
1360
+ function _setModuleState(ms) {
1361
+ moduleState = ms;
1362
+ }
1363
+ // --- Main ---
1364
+ function main() {
1365
+ const config = loadConfig();
1366
+ const state = (0, state_1.createState)(config);
1367
+ state.groups = discoverServices(config);
1368
+ state.flatList = (0, state_1.buildFlatList)(state.groups);
1369
+ if (state.flatList.length === 0) {
1370
+ process.stderr.write('No services found in any compose file.\n');
1371
+ process.exit(1);
1372
+ }
1373
+ pollStatuses(state);
1374
+ initDepGraphs(state);
1375
+ if (process.stdin.isTTY) {
1376
+ process.stdin.setRawMode(true);
1377
+ }
1378
+ process.stdin.resume();
1379
+ process.stdin.setEncoding('utf8');
1380
+ process.stdin.on('data', createInputHandler(state));
1381
+ pollLogCounts(state);
1382
+ updateSelectedLogs(state);
1383
+ render(state);
1384
+ state.pollTimer = setInterval(() => {
1385
+ if (state.mode === state_1.MODE.LIST) {
1386
+ pollStatuses(state);
1387
+ render(state);
1388
+ }
1389
+ }, config.pollInterval);
1390
+ state.logScanTimer = setInterval(() => {
1391
+ if (state.mode === state_1.MODE.LIST) {
1392
+ pollLogCounts(state);
1393
+ }
1394
+ }, config.logScanInterval || 10000);
1395
+ pollContainerStats(state);
1396
+ state.statsTimer = setInterval(() => {
1397
+ if (state.mode === state_1.MODE.LIST) {
1398
+ pollContainerStats(state);
1399
+ }
1400
+ }, config.statsInterval || 5000);
1401
+ process.stdout.on('resize', () => {
1402
+ render(state);
1403
+ });
1404
+ process.on('exit', () => cleanup(state));
1405
+ process.on('SIGINT', () => {
1406
+ cleanup(state);
1407
+ process.exit(0);
1408
+ });
1409
+ process.on('SIGTERM', () => {
1410
+ cleanup(state);
1411
+ process.exit(0);
1412
+ });
1413
+ }
1414
+ // Only run main when executed directly (not when imported for testing)
1415
+ if (require.main === module) {
1416
+ main();
1417
+ }
1418
+ //# sourceMappingURL=index.js.map