recomposable 1.1.2 → 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 -129
- 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/dist/index.d.ts
CHANGED
|
@@ -27,12 +27,16 @@ export declare function doCascadeRebuild(state: AppState): void;
|
|
|
27
27
|
export declare function enterExecInline(state: AppState): void;
|
|
28
28
|
export declare function enterExec(state: AppState): void;
|
|
29
29
|
export declare function exitExec(state: AppState): void;
|
|
30
|
+
export declare function shellEscape(str: string): string;
|
|
30
31
|
export declare function runExecCommand(state: AppState): void;
|
|
31
32
|
export declare function enterLogs(state: AppState): void;
|
|
32
33
|
export declare function exitLogs(state: AppState): void;
|
|
34
|
+
export declare function loadMoreLogHistory(state: AppState): void;
|
|
33
35
|
export declare function executeLogSearch(state: AppState): void;
|
|
34
36
|
export declare function jumpToNextMatch(state: AppState): void;
|
|
35
37
|
export declare function jumpToPrevMatch(state: AppState): void;
|
|
38
|
+
export declare function executeBottomSearch(state: AppState): void;
|
|
39
|
+
export declare function clearBottomSearch(state: AppState): void;
|
|
36
40
|
export declare function handleKeypress(state: AppState, key: string): void;
|
|
37
41
|
export declare function createInputHandler(state: AppState): (data: Buffer | string) => void;
|
|
38
42
|
export declare function cleanup(state: AppState): void;
|
package/dist/index.js
CHANGED
|
@@ -24,12 +24,16 @@ exports.doCascadeRebuild = doCascadeRebuild;
|
|
|
24
24
|
exports.enterExecInline = enterExecInline;
|
|
25
25
|
exports.enterExec = enterExec;
|
|
26
26
|
exports.exitExec = exitExec;
|
|
27
|
+
exports.shellEscape = shellEscape;
|
|
27
28
|
exports.runExecCommand = runExecCommand;
|
|
28
29
|
exports.enterLogs = enterLogs;
|
|
29
30
|
exports.exitLogs = exitLogs;
|
|
31
|
+
exports.loadMoreLogHistory = loadMoreLogHistory;
|
|
30
32
|
exports.executeLogSearch = executeLogSearch;
|
|
31
33
|
exports.jumpToNextMatch = jumpToNextMatch;
|
|
32
34
|
exports.jumpToPrevMatch = jumpToPrevMatch;
|
|
35
|
+
exports.executeBottomSearch = executeBottomSearch;
|
|
36
|
+
exports.clearBottomSearch = clearBottomSearch;
|
|
33
37
|
exports.handleKeypress = handleKeypress;
|
|
34
38
|
exports.createInputHandler = createInputHandler;
|
|
35
39
|
exports.cleanup = cleanup;
|
|
@@ -69,7 +73,33 @@ function loadConfig() {
|
|
|
69
73
|
};
|
|
70
74
|
const configPath = path_1.default.join(process.cwd(), 'recomposable.json');
|
|
71
75
|
if (fs_1.default.existsSync(configPath)) {
|
|
72
|
-
|
|
76
|
+
const raw = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
|
|
77
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
78
|
+
if (Array.isArray(raw.composeFiles) && raw.composeFiles.every((f) => typeof f === 'string')) {
|
|
79
|
+
defaults.composeFiles = raw.composeFiles;
|
|
80
|
+
}
|
|
81
|
+
if (Array.isArray(raw.logScanPatterns) && raw.logScanPatterns.every((p) => typeof p === 'string')) {
|
|
82
|
+
defaults.logScanPatterns = raw.logScanPatterns;
|
|
83
|
+
}
|
|
84
|
+
const numericFields = [
|
|
85
|
+
{ key: 'pollInterval', min: 500, max: 300000 },
|
|
86
|
+
{ key: 'logTailLines', min: 1, max: 50000 },
|
|
87
|
+
{ key: 'logScanLines', min: 1, max: 50000 },
|
|
88
|
+
{ key: 'logScanInterval', min: 1000, max: 600000 },
|
|
89
|
+
{ key: 'statsInterval', min: 1000, max: 600000 },
|
|
90
|
+
{ key: 'statsBufferSize', min: 1, max: 100 },
|
|
91
|
+
{ key: 'bottomLogCount', min: 1, max: 200 },
|
|
92
|
+
{ key: 'cpuWarnThreshold', min: 0, max: 10000 },
|
|
93
|
+
{ key: 'cpuDangerThreshold', min: 0, max: 10000 },
|
|
94
|
+
{ key: 'memWarnThreshold', min: 0, max: 1048576 },
|
|
95
|
+
{ key: 'memDangerThreshold', min: 0, max: 1048576 },
|
|
96
|
+
];
|
|
97
|
+
for (const { key, min, max } of numericFields) {
|
|
98
|
+
if (typeof raw[key] === 'number' && isFinite(raw[key]) && raw[key] >= min && raw[key] <= max) {
|
|
99
|
+
defaults[key] = raw[key];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
73
103
|
}
|
|
74
104
|
const args = process.argv.slice(2);
|
|
75
105
|
const cliFiles = [];
|
|
@@ -244,20 +274,26 @@ function pollContainerStats(state) {
|
|
|
244
274
|
}
|
|
245
275
|
// --- Rendering ---
|
|
246
276
|
function render(state) {
|
|
247
|
-
let
|
|
277
|
+
let view = '';
|
|
248
278
|
if (state.mode === state_1.MODE.LIST) {
|
|
249
|
-
|
|
279
|
+
view = (0, renderer_1.renderListView)(state);
|
|
250
280
|
}
|
|
251
281
|
else if (state.mode === state_1.MODE.LOGS) {
|
|
252
|
-
|
|
282
|
+
view = (0, renderer_1.renderLogView)(state);
|
|
253
283
|
}
|
|
254
284
|
else if (state.mode === state_1.MODE.EXEC) {
|
|
255
|
-
|
|
285
|
+
view = (0, renderer_1.renderExecView)(state);
|
|
256
286
|
}
|
|
257
|
-
|
|
287
|
+
// View functions already embed CLEAR_EOL per line; just clear below last line
|
|
288
|
+
process.stdout.write((0, renderer_1.clearScreen)() + view + renderer_1.CLEAR_EOL + renderer_1.CLEAR_EOS);
|
|
258
289
|
}
|
|
259
290
|
function stripAnsi(str) {
|
|
260
|
-
return str.replace(
|
|
291
|
+
return str.replace(
|
|
292
|
+
// CSI sequences: \x1b[ ... letter
|
|
293
|
+
// OSC sequences: \x1b] ... BEL or \x1b] ... ST
|
|
294
|
+
// DCS/APC/PM/SOS sequences: \x1bP/\x1b_/\x1b^/\x1bX ... ST (where ST = \x1b\\)
|
|
295
|
+
// Two-byte escape sequences: \x1b + any char
|
|
296
|
+
/\x1b\[[0-9;?]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[P_^X][^\x1b]*(?:\x1b\\|\x07)|\x1b[^[\]P_^X]/g, '');
|
|
261
297
|
}
|
|
262
298
|
function throttledRender(state) {
|
|
263
299
|
const now = Date.now();
|
|
@@ -284,6 +320,7 @@ function updateSelectedLogs(state) {
|
|
|
284
320
|
return;
|
|
285
321
|
state.bottomSearchQuery = '';
|
|
286
322
|
state.bottomSearchActive = false;
|
|
323
|
+
clearBottomSearch(state);
|
|
287
324
|
if (moduleState.logFetchTimer) {
|
|
288
325
|
clearTimeout(moduleState.logFetchTimer);
|
|
289
326
|
moduleState.logFetchTimer = null;
|
|
@@ -351,7 +388,7 @@ function doRebuild(state) {
|
|
|
351
388
|
state.bottomLogTails.get(sk).kill('SIGTERM');
|
|
352
389
|
state.bottomLogTails.delete(sk);
|
|
353
390
|
}
|
|
354
|
-
const child = (0, docker_1.rebuildService)(entry.file, entry.service, { noCache: state.noCache });
|
|
391
|
+
const child = (0, docker_1.rebuildService)(entry.file, entry.service, { noCache: state.noCache, noDeps: state.noDeps });
|
|
355
392
|
state.rebuilding.set(sk, child);
|
|
356
393
|
state.bottomLogLines.set(sk, { action: 'rebuilding', service: entry.service, lines: [] });
|
|
357
394
|
let lineBuf = '';
|
|
@@ -366,24 +403,34 @@ function doRebuild(state) {
|
|
|
366
403
|
if (newLines.length === 0)
|
|
367
404
|
return;
|
|
368
405
|
info.lines.push(...newLines);
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
406
|
+
if (state.mode === state_1.MODE.LOGS && state.logBuildKey === sk) {
|
|
407
|
+
state.logLines.push(...newLines);
|
|
408
|
+
if (state.logAutoScroll)
|
|
409
|
+
throttledRender(state);
|
|
410
|
+
}
|
|
372
411
|
if (state.mode === state_1.MODE.LIST)
|
|
373
412
|
throttledRender(state);
|
|
374
413
|
};
|
|
375
414
|
child.stdout.on('data', onData);
|
|
376
415
|
child.stderr.on('data', onData);
|
|
377
416
|
render(state);
|
|
378
|
-
child.on('close', () => {
|
|
417
|
+
child.on('close', (code) => {
|
|
379
418
|
state.rebuilding.delete(sk);
|
|
380
419
|
state.containerStatsHistory.delete(sk);
|
|
381
420
|
state.containerStats.delete(sk);
|
|
382
421
|
pollStatuses(state);
|
|
383
422
|
const info = state.bottomLogLines.get(sk);
|
|
423
|
+
if (code !== 0 && code !== null) {
|
|
424
|
+
if (info)
|
|
425
|
+
info.action = 'build_failed';
|
|
426
|
+
if (state.mode === state_1.MODE.LIST)
|
|
427
|
+
render(state);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
384
430
|
if (info) {
|
|
385
431
|
info.action = 'started';
|
|
386
|
-
|
|
432
|
+
if (state.logBuildKey !== sk)
|
|
433
|
+
info.lines = [];
|
|
387
434
|
}
|
|
388
435
|
startBottomLogTail(state, sk, entry.file, entry.service);
|
|
389
436
|
if (state.mode === state_1.MODE.LIST)
|
|
@@ -405,12 +452,19 @@ function doRestart(state) {
|
|
|
405
452
|
state.restarting.set(sk, child);
|
|
406
453
|
state.bottomLogLines.set(sk, { action: 'restarting', service: entry.service, lines: [] });
|
|
407
454
|
render(state);
|
|
408
|
-
child.on('close', () => {
|
|
455
|
+
child.on('close', (code) => {
|
|
409
456
|
state.restarting.delete(sk);
|
|
410
457
|
state.containerStatsHistory.delete(sk);
|
|
411
458
|
state.containerStats.delete(sk);
|
|
412
459
|
pollStatuses(state);
|
|
413
460
|
const info = state.bottomLogLines.get(sk);
|
|
461
|
+
if (code !== 0 && code !== null) {
|
|
462
|
+
if (info)
|
|
463
|
+
info.action = 'restart_failed';
|
|
464
|
+
if (state.mode === state_1.MODE.LIST)
|
|
465
|
+
render(state);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
414
468
|
if (info) {
|
|
415
469
|
info.action = 'started';
|
|
416
470
|
info.lines = [];
|
|
@@ -438,9 +492,16 @@ function doStop(state) {
|
|
|
438
492
|
state.stopping.set(sk, child);
|
|
439
493
|
state.bottomLogLines.set(sk, { action: 'stopping', service: entry.service, lines: [] });
|
|
440
494
|
render(state);
|
|
441
|
-
child.on('close', () => {
|
|
495
|
+
child.on('close', (code) => {
|
|
442
496
|
state.stopping.delete(sk);
|
|
443
|
-
|
|
497
|
+
if (code !== 0 && code !== null) {
|
|
498
|
+
const info = state.bottomLogLines.get(sk);
|
|
499
|
+
if (info)
|
|
500
|
+
info.action = 'stop_failed';
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
state.bottomLogLines.delete(sk);
|
|
504
|
+
}
|
|
444
505
|
pollStatuses(state);
|
|
445
506
|
if (state.mode === state_1.MODE.LIST)
|
|
446
507
|
render(state);
|
|
@@ -460,10 +521,17 @@ function doStart(state) {
|
|
|
460
521
|
state.starting.set(sk, child);
|
|
461
522
|
state.bottomLogLines.set(sk, { action: 'starting', service: entry.service, lines: [] });
|
|
462
523
|
render(state);
|
|
463
|
-
child.on('close', () => {
|
|
524
|
+
child.on('close', (code) => {
|
|
464
525
|
state.starting.delete(sk);
|
|
465
526
|
pollStatuses(state);
|
|
466
527
|
const info = state.bottomLogLines.get(sk);
|
|
528
|
+
if (code !== 0 && code !== null) {
|
|
529
|
+
if (info)
|
|
530
|
+
info.action = 'start_failed';
|
|
531
|
+
if (state.mode === state_1.MODE.LIST)
|
|
532
|
+
render(state);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
467
535
|
if (info) {
|
|
468
536
|
info.action = 'started';
|
|
469
537
|
info.lines = [];
|
|
@@ -633,10 +701,9 @@ function executeCascadeStep(state, file, sk, cascade) {
|
|
|
633
701
|
return;
|
|
634
702
|
}
|
|
635
703
|
step.status = 'in_progress';
|
|
636
|
-
const maxLines = state.config.bottomLogCount || 10;
|
|
637
704
|
let child;
|
|
638
705
|
if (step.action === 'rebuild') {
|
|
639
|
-
child = (0, docker_1.rebuildService)(file, step.service, { noCache: state.noCache });
|
|
706
|
+
child = (0, docker_1.rebuildService)(file, step.service, { noCache: state.noCache, noDeps: state.noDeps });
|
|
640
707
|
}
|
|
641
708
|
else {
|
|
642
709
|
child = (0, docker_1.restartService)(file, step.service);
|
|
@@ -654,8 +721,11 @@ function executeCascadeStep(state, file, sk, cascade) {
|
|
|
654
721
|
if (newLines.length === 0)
|
|
655
722
|
return;
|
|
656
723
|
info.lines.push(...newLines);
|
|
657
|
-
if (
|
|
658
|
-
|
|
724
|
+
if (state.mode === state_1.MODE.LOGS && state.logBuildKey === sk) {
|
|
725
|
+
state.logLines.push(...newLines);
|
|
726
|
+
if (state.logAutoScroll)
|
|
727
|
+
throttledRender(state);
|
|
728
|
+
}
|
|
659
729
|
if (state.mode === state_1.MODE.LIST)
|
|
660
730
|
throttledRender(state);
|
|
661
731
|
};
|
|
@@ -753,13 +823,18 @@ function isCdCommand(cmd) {
|
|
|
753
823
|
return null;
|
|
754
824
|
return match[2] ? match[2].trim() : '';
|
|
755
825
|
}
|
|
826
|
+
function shellEscape(str) {
|
|
827
|
+
return "'" + str.replace(/'/g, "'\\''") + "'";
|
|
828
|
+
}
|
|
756
829
|
function runExecCommand(state) {
|
|
757
830
|
const cmd = state.execInput.trim();
|
|
758
831
|
if (!cmd || !state.execContainerId)
|
|
759
832
|
return;
|
|
760
|
-
// Add to history
|
|
833
|
+
// Add to history (capped at 1000 entries)
|
|
761
834
|
if (state.execHistory.length === 0 || state.execHistory[state.execHistory.length - 1] !== cmd) {
|
|
762
835
|
state.execHistory.push(cmd);
|
|
836
|
+
if (state.execHistory.length > 1000)
|
|
837
|
+
state.execHistory.shift();
|
|
763
838
|
}
|
|
764
839
|
state.execHistoryIdx = -1;
|
|
765
840
|
state.execInput = '';
|
|
@@ -772,7 +847,7 @@ function runExecCommand(state) {
|
|
|
772
847
|
// Handle cd commands — resolve new working directory
|
|
773
848
|
const cdTarget = isCdCommand(cmd);
|
|
774
849
|
if (cdTarget !== null) {
|
|
775
|
-
const resolveCmd = cdTarget ? `cd ${cdTarget} && pwd` : 'cd && pwd';
|
|
850
|
+
const resolveCmd = cdTarget ? `cd ${shellEscape(cdTarget)} && pwd` : 'cd && pwd';
|
|
776
851
|
const child = (0, docker_1.execInContainer)(state.execContainerId, resolveCmd, state.execCwd || undefined);
|
|
777
852
|
state.execChild = child;
|
|
778
853
|
let stdout = '';
|
|
@@ -785,8 +860,9 @@ function runExecCommand(state) {
|
|
|
785
860
|
if (code === 0) {
|
|
786
861
|
const lines = stdout.trim().split('\n');
|
|
787
862
|
const newCwd = lines[lines.length - 1].trim();
|
|
788
|
-
if (newCwd)
|
|
863
|
+
if (newCwd && newCwd.startsWith('/') && newCwd.length < 4096 && !/[\x00-\x1f]/.test(newCwd)) {
|
|
789
864
|
state.execCwd = newCwd;
|
|
865
|
+
}
|
|
790
866
|
}
|
|
791
867
|
else {
|
|
792
868
|
const errLines = stderr.trim().split('\n').filter(Boolean);
|
|
@@ -841,44 +917,65 @@ function enterLogs(state) {
|
|
|
841
917
|
clearTimeout(moduleState.logFetchTimer);
|
|
842
918
|
moduleState.logFetchTimer = null;
|
|
843
919
|
}
|
|
920
|
+
// Carry over bottom panel search query to full log search
|
|
921
|
+
const carryQuery = state.bottomSearchQuery || '';
|
|
922
|
+
clearBottomSearch(state);
|
|
923
|
+
const sk = (0, state_1.statusKey)(entry.file, entry.service);
|
|
924
|
+
const info = state.bottomLogLines.get(sk);
|
|
925
|
+
const isBuilding = state.rebuilding.has(sk) || state.cascading.has(sk);
|
|
926
|
+
const isBuildFailed = info && info.action === 'build_failed';
|
|
844
927
|
state.mode = state_1.MODE.LOGS;
|
|
845
928
|
state.logLines = [];
|
|
846
929
|
state.logScrollOffset = 0;
|
|
847
930
|
state.logAutoScroll = true;
|
|
848
|
-
state.logSearchQuery =
|
|
931
|
+
state.logSearchQuery = carryQuery;
|
|
849
932
|
state.logSearchActive = false;
|
|
850
933
|
state.logSearchMatches = [];
|
|
851
934
|
state.logSearchMatchIdx = -1;
|
|
852
|
-
|
|
853
|
-
state.
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
state.logLines.push(stripAnsi(line));
|
|
935
|
+
state.logFetchedTailCount = 200;
|
|
936
|
+
state.logHistoryLoaded = false;
|
|
937
|
+
state.logHistoryLoading = false;
|
|
938
|
+
state.logSearchPending = !!carryQuery;
|
|
939
|
+
state.logHistoryChild = null;
|
|
940
|
+
if (isBuilding || isBuildFailed) {
|
|
941
|
+
// Show build output instead of runtime logs
|
|
942
|
+
state.logBuildKey = sk;
|
|
943
|
+
if (info) {
|
|
944
|
+
state.logLines = [...info.lines];
|
|
863
945
|
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
946
|
+
state.logHistoryLoaded = true;
|
|
947
|
+
}
|
|
948
|
+
else {
|
|
949
|
+
state.logBuildKey = null;
|
|
950
|
+
const child = (0, docker_1.tailLogs)(entry.file, entry.service, 200);
|
|
951
|
+
state.logChild = child;
|
|
952
|
+
let lineBuf = '';
|
|
953
|
+
const onData = (data) => {
|
|
954
|
+
lineBuf += data.toString();
|
|
955
|
+
const parts = lineBuf.split(/\r?\n|\r/);
|
|
956
|
+
lineBuf = parts.pop();
|
|
957
|
+
if (parts.length === 0)
|
|
958
|
+
return;
|
|
959
|
+
for (const line of parts) {
|
|
960
|
+
state.logLines.push(stripAnsi(line));
|
|
869
961
|
}
|
|
962
|
+
if (state.logAutoScroll) {
|
|
963
|
+
throttledRender(state);
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
child.stdout.on('data', onData);
|
|
967
|
+
child.stderr.on('data', onData);
|
|
968
|
+
child.on('close', () => {
|
|
969
|
+
if (state.logChild === child) {
|
|
970
|
+
state.logChild = null;
|
|
971
|
+
}
|
|
972
|
+
});
|
|
973
|
+
// If carrying a search query from bottom panel, load full history to search
|
|
974
|
+
if (carryQuery) {
|
|
975
|
+
state.logFetchedTailCount = 5000;
|
|
976
|
+
loadMoreLogHistory(state);
|
|
870
977
|
}
|
|
871
|
-
|
|
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
|
-
});
|
|
978
|
+
}
|
|
882
979
|
render(state);
|
|
883
980
|
}
|
|
884
981
|
function exitLogs(state) {
|
|
@@ -886,11 +983,72 @@ function exitLogs(state) {
|
|
|
886
983
|
state.logChild.kill('SIGTERM');
|
|
887
984
|
state.logChild = null;
|
|
888
985
|
}
|
|
986
|
+
if (state.logHistoryChild) {
|
|
987
|
+
state.logHistoryChild.kill('SIGTERM');
|
|
988
|
+
state.logHistoryChild = null;
|
|
989
|
+
}
|
|
889
990
|
state.logLines = [];
|
|
991
|
+
state.logBuildKey = null;
|
|
992
|
+
state.logHistoryLoaded = false;
|
|
993
|
+
state.logHistoryLoading = false;
|
|
994
|
+
state.logSearchPending = false;
|
|
890
995
|
state.mode = state_1.MODE.LIST;
|
|
891
996
|
pollStatuses(state);
|
|
892
997
|
render(state);
|
|
893
998
|
}
|
|
999
|
+
// --- Log History Loading ---
|
|
1000
|
+
function loadMoreLogHistory(state) {
|
|
1001
|
+
if (state.logHistoryLoaded || state.logHistoryLoading)
|
|
1002
|
+
return;
|
|
1003
|
+
const entry = (0, state_1.selectedEntry)(state);
|
|
1004
|
+
if (!entry)
|
|
1005
|
+
return;
|
|
1006
|
+
// Escalate: 200 → 1000 → 5000 → all
|
|
1007
|
+
let nextTail;
|
|
1008
|
+
if (state.logFetchedTailCount < 1000)
|
|
1009
|
+
nextTail = 1000;
|
|
1010
|
+
else if (state.logFetchedTailCount < 5000)
|
|
1011
|
+
nextTail = 5000;
|
|
1012
|
+
else
|
|
1013
|
+
nextTail = 'all';
|
|
1014
|
+
state.logHistoryLoading = true;
|
|
1015
|
+
const snapshotLen = state.logLines.length;
|
|
1016
|
+
const child = (0, docker_1.fetchServiceLogs)(entry.file, entry.service, nextTail);
|
|
1017
|
+
state.logHistoryChild = child;
|
|
1018
|
+
let output = '';
|
|
1019
|
+
child.stdout.on('data', (d) => { output += d.toString(); });
|
|
1020
|
+
child.stderr.on('data', (d) => { output += d.toString(); });
|
|
1021
|
+
child.on('close', () => {
|
|
1022
|
+
if (state.logHistoryChild === child) {
|
|
1023
|
+
state.logHistoryChild = null;
|
|
1024
|
+
}
|
|
1025
|
+
state.logHistoryLoading = false;
|
|
1026
|
+
const fetchedLines = output.split(/\r?\n|\r/).filter(l => l.length > 0).map(stripAnsi).filter(Boolean);
|
|
1027
|
+
if (fetchedLines.length <= snapshotLen) {
|
|
1028
|
+
// No more history available
|
|
1029
|
+
state.logHistoryLoaded = true;
|
|
1030
|
+
}
|
|
1031
|
+
else {
|
|
1032
|
+
// Merge: fetched history + any new lines that arrived during the fetch
|
|
1033
|
+
const newLiveLines = state.logLines.slice(snapshotLen);
|
|
1034
|
+
const oldOffset = state.logScrollOffset;
|
|
1035
|
+
const added = fetchedLines.length - snapshotLen;
|
|
1036
|
+
state.logLines = [...fetchedLines, ...newLiveLines];
|
|
1037
|
+
// Adjust scroll offset to maintain visual position
|
|
1038
|
+
if (!state.logAutoScroll) {
|
|
1039
|
+
state.logScrollOffset = oldOffset + added;
|
|
1040
|
+
}
|
|
1041
|
+
state.logFetchedTailCount = nextTail === 'all' ? Infinity : nextTail;
|
|
1042
|
+
if (nextTail === 'all')
|
|
1043
|
+
state.logHistoryLoaded = true;
|
|
1044
|
+
}
|
|
1045
|
+
if (state.logSearchPending) {
|
|
1046
|
+
state.logSearchPending = false;
|
|
1047
|
+
executeLogSearch(state);
|
|
1048
|
+
}
|
|
1049
|
+
render(state);
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
894
1052
|
// --- Log Search ---
|
|
895
1053
|
function executeLogSearch(state) {
|
|
896
1054
|
const query = state.logSearchQuery;
|
|
@@ -914,7 +1072,8 @@ function scrollToLogLine(state, lineIdx) {
|
|
|
914
1072
|
const headerHeight = 9;
|
|
915
1073
|
const availableRows = Math.max(1, rows - headerHeight);
|
|
916
1074
|
const totalLines = state.logLines.length;
|
|
917
|
-
|
|
1075
|
+
const maxOffset = Math.max(0, totalLines - availableRows);
|
|
1076
|
+
state.logScrollOffset = Math.min(maxOffset, Math.max(0, totalLines - lineIdx - Math.floor(availableRows / 2)));
|
|
918
1077
|
state.logAutoScroll = state.logScrollOffset === 0;
|
|
919
1078
|
render(state);
|
|
920
1079
|
}
|
|
@@ -930,6 +1089,78 @@ function jumpToPrevMatch(state) {
|
|
|
930
1089
|
state.logSearchMatchIdx = (state.logSearchMatchIdx - 1 + state.logSearchMatches.length) % state.logSearchMatches.length;
|
|
931
1090
|
scrollToLogLine(state, state.logSearchMatches[state.logSearchMatchIdx]);
|
|
932
1091
|
}
|
|
1092
|
+
// --- Bottom Panel Search ---
|
|
1093
|
+
function executeBottomSearch(state) {
|
|
1094
|
+
const entry = (0, state_1.selectedEntry)(state);
|
|
1095
|
+
if (!entry || !state.bottomSearchQuery)
|
|
1096
|
+
return;
|
|
1097
|
+
const sk = (0, state_1.statusKey)(entry.file, entry.service);
|
|
1098
|
+
const info = state.bottomLogLines.get(sk);
|
|
1099
|
+
if (!info)
|
|
1100
|
+
return;
|
|
1101
|
+
// Save current tail lines so we can restore them later
|
|
1102
|
+
if (!state.bottomSearchSavedLines.has(sk)) {
|
|
1103
|
+
state.bottomSearchSavedLines.set(sk, [...info.lines]);
|
|
1104
|
+
}
|
|
1105
|
+
// Kill any previous search fetch
|
|
1106
|
+
if (state.bottomSearchChild) {
|
|
1107
|
+
state.bottomSearchChild.kill('SIGTERM');
|
|
1108
|
+
state.bottomSearchChild = null;
|
|
1109
|
+
}
|
|
1110
|
+
state.bottomSearchLoading = true;
|
|
1111
|
+
state.bottomSearchTotalMatches = 0;
|
|
1112
|
+
render(state);
|
|
1113
|
+
const child = (0, docker_1.fetchServiceLogs)(entry.file, entry.service, 'all');
|
|
1114
|
+
state.bottomSearchChild = child;
|
|
1115
|
+
let output = '';
|
|
1116
|
+
child.stdout.on('data', (d) => { output += d.toString(); });
|
|
1117
|
+
child.stderr.on('data', (d) => { output += d.toString(); });
|
|
1118
|
+
child.on('close', () => {
|
|
1119
|
+
if (state.bottomSearchChild !== child)
|
|
1120
|
+
return; // superseded
|
|
1121
|
+
state.bottomSearchChild = null;
|
|
1122
|
+
state.bottomSearchLoading = false;
|
|
1123
|
+
const query = state.bottomSearchQuery;
|
|
1124
|
+
if (!query) {
|
|
1125
|
+
clearBottomSearch(state);
|
|
1126
|
+
render(state);
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
const lowerQuery = query.toLowerCase();
|
|
1130
|
+
const allLines = output.split(/\r?\n|\r/).filter(l => l.trim().length > 0).map(stripAnsi).filter(Boolean);
|
|
1131
|
+
const matchingLines = [];
|
|
1132
|
+
for (const line of allLines) {
|
|
1133
|
+
if (line.toLowerCase().includes(lowerQuery)) {
|
|
1134
|
+
matchingLines.push(line);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
state.bottomSearchTotalMatches = matchingLines.length;
|
|
1138
|
+
// Show the last N matching lines in the bottom panel
|
|
1139
|
+
const maxLines = state.config.bottomLogCount || 10;
|
|
1140
|
+
const currentInfo = state.bottomLogLines.get(sk);
|
|
1141
|
+
if (currentInfo) {
|
|
1142
|
+
currentInfo.lines = matchingLines.slice(-maxLines);
|
|
1143
|
+
}
|
|
1144
|
+
if (state.mode === state_1.MODE.LIST)
|
|
1145
|
+
render(state);
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
function clearBottomSearch(state) {
|
|
1149
|
+
if (state.bottomSearchChild) {
|
|
1150
|
+
state.bottomSearchChild.kill('SIGTERM');
|
|
1151
|
+
state.bottomSearchChild = null;
|
|
1152
|
+
}
|
|
1153
|
+
state.bottomSearchLoading = false;
|
|
1154
|
+
state.bottomSearchTotalMatches = 0;
|
|
1155
|
+
// Restore saved tail lines
|
|
1156
|
+
if (state.selectedLogKey && state.bottomSearchSavedLines.has(state.selectedLogKey)) {
|
|
1157
|
+
const info = state.bottomLogLines.get(state.selectedLogKey);
|
|
1158
|
+
if (info) {
|
|
1159
|
+
info.lines = state.bottomSearchSavedLines.get(state.selectedLogKey);
|
|
1160
|
+
}
|
|
1161
|
+
state.bottomSearchSavedLines.delete(state.selectedLogKey);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
933
1164
|
// --- Input Handling ---
|
|
934
1165
|
function handleKeypress(state, key) {
|
|
935
1166
|
if (key === '\x03' && state.mode !== state_1.MODE.EXEC && !state.execActive) {
|
|
@@ -1002,8 +1233,22 @@ function handleKeypress(state, key) {
|
|
|
1002
1233
|
}
|
|
1003
1234
|
else if (key === '\r') {
|
|
1004
1235
|
state.logSearchActive = false;
|
|
1005
|
-
|
|
1006
|
-
|
|
1236
|
+
if (!state.logHistoryLoaded && !state.logHistoryLoading) {
|
|
1237
|
+
// Load all history, then search
|
|
1238
|
+
state.logSearchPending = true;
|
|
1239
|
+
state.logFetchedTailCount = 5000; // jump straight to 'all'
|
|
1240
|
+
loadMoreLogHistory(state);
|
|
1241
|
+
render(state);
|
|
1242
|
+
}
|
|
1243
|
+
else if (state.logHistoryLoading) {
|
|
1244
|
+
// Already loading — search will run when it finishes
|
|
1245
|
+
state.logSearchPending = true;
|
|
1246
|
+
render(state);
|
|
1247
|
+
}
|
|
1248
|
+
else {
|
|
1249
|
+
executeLogSearch(state);
|
|
1250
|
+
render(state);
|
|
1251
|
+
}
|
|
1007
1252
|
}
|
|
1008
1253
|
else if (key === '\x7f' || key === '\b') {
|
|
1009
1254
|
state.logSearchQuery = state.logSearchQuery.slice(0, -1);
|
|
@@ -1017,12 +1262,31 @@ function handleKeypress(state, key) {
|
|
|
1017
1262
|
}
|
|
1018
1263
|
const rows = process.stdout.rows ?? 24;
|
|
1019
1264
|
const pageSize = Math.max(1, Math.floor(rows / 2));
|
|
1020
|
-
const
|
|
1265
|
+
const availableRows = Math.max(1, rows - 9);
|
|
1266
|
+
const maxOffset = Math.max(0, state.logLines.length - availableRows);
|
|
1267
|
+
// Trigger lazy history load when scrolled near the top
|
|
1268
|
+
const checkLoadMore = () => {
|
|
1269
|
+
const availableRows = Math.max(1, rows - 9);
|
|
1270
|
+
const linesFromTop = state.logLines.length - state.logScrollOffset - availableRows;
|
|
1271
|
+
if (linesFromTop < availableRows) {
|
|
1272
|
+
loadMoreLogHistory(state);
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1021
1275
|
switch (key) {
|
|
1022
1276
|
case 'f':
|
|
1023
|
-
case '\x1b':
|
|
1024
1277
|
exitLogs(state);
|
|
1025
1278
|
break;
|
|
1279
|
+
case '\x1b':
|
|
1280
|
+
if (state.logSearchQuery) {
|
|
1281
|
+
state.logSearchQuery = '';
|
|
1282
|
+
state.logSearchMatches = [];
|
|
1283
|
+
state.logSearchMatchIdx = -1;
|
|
1284
|
+
render(state);
|
|
1285
|
+
}
|
|
1286
|
+
else {
|
|
1287
|
+
exitLogs(state);
|
|
1288
|
+
}
|
|
1289
|
+
break;
|
|
1026
1290
|
case 'q':
|
|
1027
1291
|
cleanup(state);
|
|
1028
1292
|
process.exit(0);
|
|
@@ -1031,6 +1295,7 @@ function handleKeypress(state, key) {
|
|
|
1031
1295
|
case '\x1b[A':
|
|
1032
1296
|
state.logAutoScroll = false;
|
|
1033
1297
|
state.logScrollOffset = Math.min(maxOffset, state.logScrollOffset + 1);
|
|
1298
|
+
checkLoadMore();
|
|
1034
1299
|
render(state);
|
|
1035
1300
|
break;
|
|
1036
1301
|
case 'j':
|
|
@@ -1050,6 +1315,7 @@ function handleKeypress(state, key) {
|
|
|
1050
1315
|
case '\x15': // Ctrl+U
|
|
1051
1316
|
state.logAutoScroll = false;
|
|
1052
1317
|
state.logScrollOffset = Math.min(maxOffset, state.logScrollOffset + pageSize);
|
|
1318
|
+
checkLoadMore();
|
|
1053
1319
|
render(state);
|
|
1054
1320
|
break;
|
|
1055
1321
|
case '\x04': // Ctrl+D
|
|
@@ -1135,10 +1401,14 @@ function handleKeypress(state, key) {
|
|
|
1135
1401
|
if (key === '\x1b') {
|
|
1136
1402
|
state.bottomSearchActive = false;
|
|
1137
1403
|
state.bottomSearchQuery = '';
|
|
1404
|
+
clearBottomSearch(state);
|
|
1138
1405
|
render(state);
|
|
1139
1406
|
}
|
|
1140
1407
|
else if (key === '\r') {
|
|
1141
1408
|
state.bottomSearchActive = false;
|
|
1409
|
+
if (state.bottomSearchQuery) {
|
|
1410
|
+
executeBottomSearch(state);
|
|
1411
|
+
}
|
|
1142
1412
|
render(state);
|
|
1143
1413
|
}
|
|
1144
1414
|
else if (key === '\x7f' || key === '\b') {
|
|
@@ -1201,6 +1471,10 @@ function handleKeypress(state, key) {
|
|
|
1201
1471
|
state.noCache = !state.noCache;
|
|
1202
1472
|
render(state);
|
|
1203
1473
|
break;
|
|
1474
|
+
case 'o':
|
|
1475
|
+
state.noDeps = !state.noDeps;
|
|
1476
|
+
render(state);
|
|
1477
|
+
break;
|
|
1204
1478
|
case 'f':
|
|
1205
1479
|
case '\r':
|
|
1206
1480
|
enterLogs(state);
|
|
@@ -1276,7 +1550,14 @@ function createInputHandler(state) {
|
|
|
1276
1550
|
}
|
|
1277
1551
|
else if (state.mode === state_1.MODE.LOGS) {
|
|
1278
1552
|
state.logAutoScroll = false;
|
|
1279
|
-
|
|
1553
|
+
const ggRows = process.stdout.rows ?? 24;
|
|
1554
|
+
const ggAvailable = Math.max(1, ggRows - 9);
|
|
1555
|
+
state.logScrollOffset = Math.max(0, state.logLines.length - ggAvailable);
|
|
1556
|
+
// Load all history so we can scroll to the very top
|
|
1557
|
+
if (!state.logHistoryLoaded) {
|
|
1558
|
+
state.logFetchedTailCount = 5000; // jump to 'all'
|
|
1559
|
+
loadMoreLogHistory(state);
|
|
1560
|
+
}
|
|
1280
1561
|
}
|
|
1281
1562
|
render(state);
|
|
1282
1563
|
continue;
|
|
@@ -1330,6 +1611,10 @@ function cleanup(state) {
|
|
|
1330
1611
|
state.execChild = null;
|
|
1331
1612
|
}
|
|
1332
1613
|
state.execActive = false;
|
|
1614
|
+
if (state.bottomSearchChild) {
|
|
1615
|
+
state.bottomSearchChild.kill('SIGTERM');
|
|
1616
|
+
state.bottomSearchChild = null;
|
|
1617
|
+
}
|
|
1333
1618
|
for (const [, child] of state.bottomLogTails) {
|
|
1334
1619
|
child.kill('SIGTERM');
|
|
1335
1620
|
}
|