dockza.app 0.2.0

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.
@@ -0,0 +1,392 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.StacksTab = void 0;
7
+ const neo_blessed_1 = __importDefault(require("neo-blessed"));
8
+ const containers_1 = require("../../docker/containers");
9
+ const _theme_1 = require("../../theme");
10
+ const stacks_1 = require("../../utils/stacks");
11
+ const status_1 = require("../../utils/status");
12
+ const external_terminal_1 = require("../../utils/external-terminal");
13
+ const container_detail_1 = require("../containers/container-detail");
14
+ const confirm_dialog_1 = require("../containers/confirm-dialog");
15
+ const log_viewer_1 = require("../containers/log-viewer");
16
+ const stack_tree_1 = require("../stacks/stack-tree");
17
+ class StacksTab {
18
+ screen;
19
+ wrapper;
20
+ filterBox;
21
+ tree;
22
+ containerDetail;
23
+ confirmDialog;
24
+ logViewer;
25
+ containers = [];
26
+ stacks = [];
27
+ statsCache = new Map();
28
+ active = false;
29
+ filterOpen = false;
30
+ errorHandlers = [];
31
+ navigateHandlers = [];
32
+ stackUpdateHandlers = [];
33
+ detailOpenHandlers = [];
34
+ logOpenHandlers = [];
35
+ logFollowHandlers = [];
36
+ constructor(screen, dims) {
37
+ this.screen = screen;
38
+ this.wrapper = neo_blessed_1.default.box({
39
+ parent: screen,
40
+ top: dims.top,
41
+ left: dims.left,
42
+ width: dims.width,
43
+ height: dims.height,
44
+ hidden: true,
45
+ });
46
+ this.tree = new stack_tree_1.StackTree(this.wrapper, {
47
+ top: 0,
48
+ left: 0,
49
+ width: '100%',
50
+ height: '100%',
51
+ });
52
+ // Appended after the tree so the filter input draws on top when shown.
53
+ this.filterBox = neo_blessed_1.default.textbox({
54
+ parent: this.wrapper,
55
+ top: 0,
56
+ left: 0,
57
+ width: '100%',
58
+ height: 1,
59
+ inputOnFocus: true,
60
+ tags: true,
61
+ style: { bg: _theme_1.C.panel, fg: _theme_1.C.fg },
62
+ hidden: true,
63
+ });
64
+ this.containerDetail = new container_detail_1.ContainerDetail(screen, dims);
65
+ this.confirmDialog = new confirm_dialog_1.ConfirmDialog(screen);
66
+ this.logViewer = new log_viewer_1.LogViewer(screen, dims);
67
+ this.tree.on('navigate', (sel) => this.navigateHandlers.forEach((h) => h(sel)));
68
+ this.containerDetail.on('close', () => {
69
+ this.tree.focus();
70
+ this.screen.render();
71
+ });
72
+ this.logViewer.on('close', () => {
73
+ this.tree.focus();
74
+ this.screen.render();
75
+ });
76
+ this.logViewer.on('follow-change', (f) => this.logFollowHandlers.forEach((h) => h(f)));
77
+ this.filterBox.on('submit', (value) => {
78
+ this.applyFilter(value ?? '');
79
+ this.exitFilter();
80
+ });
81
+ this.filterBox.on('cancel', () => {
82
+ this.applyFilter('');
83
+ this.exitFilter();
84
+ });
85
+ }
86
+ on(event, handler) {
87
+ if (event === 'error')
88
+ this.errorHandlers.push(handler);
89
+ else if (event === 'navigate')
90
+ this.navigateHandlers.push(handler);
91
+ else if (event === 'stack-update')
92
+ this.stackUpdateHandlers.push(handler);
93
+ else if (event === 'detail-open')
94
+ this.detailOpenHandlers.push(handler);
95
+ else if (event === 'log-open')
96
+ this.logOpenHandlers.push(handler);
97
+ else
98
+ this.logFollowHandlers.push(handler);
99
+ }
100
+ show() {
101
+ this.active = true;
102
+ this.wrapper.show();
103
+ this.tree.focus();
104
+ this.screen.key(['enter'], this.handleEnter);
105
+ this.screen.key(['left'], this.handleLeft);
106
+ this.screen.key(['right'], this.handleRight);
107
+ this.screen.key(['l'], this.handleL);
108
+ this.screen.key(['s'], this.handleS);
109
+ this.screen.key(['r'], this.handleR);
110
+ this.screen.key(['k'], this.handleK);
111
+ this.screen.key(['S-s'], this.handleShiftS);
112
+ this.screen.key(['d'], this.handleD);
113
+ this.screen.key(['x'], this.handleX);
114
+ this.screen.key(['/'], this.handleSlash);
115
+ this.emitNavigate();
116
+ }
117
+ hide() {
118
+ this.active = false;
119
+ if (this.containerDetail.isVisible())
120
+ this.containerDetail.hide();
121
+ if (this.logViewer.isVisible())
122
+ this.logViewer.hide();
123
+ if (this.confirmDialog.isVisible())
124
+ this.confirmDialog.hide();
125
+ if (this.filterOpen)
126
+ this.exitFilter();
127
+ this.wrapper.hide();
128
+ this.screen.removeKey('enter', this.handleEnter);
129
+ this.screen.removeKey('left', this.handleLeft);
130
+ this.screen.removeKey('right', this.handleRight);
131
+ this.screen.removeKey('l', this.handleL);
132
+ this.screen.removeKey('s', this.handleS);
133
+ this.screen.removeKey('r', this.handleR);
134
+ this.screen.removeKey('k', this.handleK);
135
+ this.screen.removeKey('S-s', this.handleShiftS);
136
+ this.screen.removeKey('d', this.handleD);
137
+ this.screen.removeKey('x', this.handleX);
138
+ this.screen.removeKey('/', this.handleSlash);
139
+ this.navigateHandlers.forEach((h) => h(null));
140
+ }
141
+ showLoading() {
142
+ this.tree.showLoading();
143
+ }
144
+ isOverlayOpen() {
145
+ return this.isModalOpen() || this.containerDetail.isVisible();
146
+ }
147
+ isConfirmOpen() {
148
+ return this.confirmDialog.isVisible();
149
+ }
150
+ isFilterOpen() {
151
+ return this.filterOpen;
152
+ }
153
+ getSelected() {
154
+ return this.tree.getSelected();
155
+ }
156
+ jumpToStack(stackId) {
157
+ this.tree.jumpToStack(stackId);
158
+ }
159
+ getAggregateStats() {
160
+ let cpu = 0;
161
+ let mem = 0;
162
+ for (const c of this.containers) {
163
+ if (!(0, status_1.isActive)(c.status))
164
+ continue;
165
+ const s = this.statsCache.get(c.id);
166
+ if (s) {
167
+ cpu += s.cpuPercent;
168
+ mem += s.memUsageMB;
169
+ }
170
+ }
171
+ return { cpuPercent: cpu, memUsageMB: mem };
172
+ }
173
+ cleanup() {
174
+ if (this.containerDetail.isVisible())
175
+ this.containerDetail.hide();
176
+ if (this.logViewer.isVisible())
177
+ this.logViewer.hide();
178
+ if (this.confirmDialog.isVisible())
179
+ this.confirmDialog.hide();
180
+ }
181
+ setData(containers, stacks) {
182
+ this.containers = containers;
183
+ this.stacks = stacks ?? (0, stacks_1.groupIntoStacks)(containers);
184
+ this.tree.setData(this.stacks, this.statsCache);
185
+ if (this.containerDetail.isVisible()) {
186
+ const id = this.containerDetail.getContainerId();
187
+ if (id) {
188
+ const updated = containers.find((c) => c.id === id);
189
+ if (updated)
190
+ this.containerDetail.update(updated, this.statsCache.get(id));
191
+ else {
192
+ this.containerDetail.hide();
193
+ this.tree.focus();
194
+ }
195
+ }
196
+ }
197
+ this.stackUpdateHandlers.forEach((h) => h(this.stacks));
198
+ this.emitNavigate();
199
+ }
200
+ updateStats(id, stats) {
201
+ this.statsCache.set(id, stats);
202
+ if (this.containerDetail.isVisible() && this.containerDetail.getContainerId() === id) {
203
+ const container = this.containers.find((c) => c.id === id);
204
+ if (container)
205
+ this.containerDetail.update(container, stats);
206
+ }
207
+ }
208
+ refreshStats() {
209
+ this.tree.setData(this.stacks, this.statsCache);
210
+ }
211
+ redraw() {
212
+ this.tree.redraw();
213
+ }
214
+ async refresh() {
215
+ const containers = await (0, containers_1.listContainers)();
216
+ this.setData(containers);
217
+ }
218
+ emitNavigate() {
219
+ this.navigateHandlers.forEach((h) => h(this.tree.getSelected()));
220
+ }
221
+ emitError(err) {
222
+ const msg = err instanceof Error ? err.message : String(err);
223
+ this.errorHandlers.forEach((h) => h(msg));
224
+ }
225
+ isModalOpen() {
226
+ return this.confirmDialog.isVisible() || this.logViewer.isVisible() || this.isFilterOpen();
227
+ }
228
+ applyFilter(value) {
229
+ this.tree.setFilter(value.trim());
230
+ this.screen.render();
231
+ }
232
+ exitFilter() {
233
+ this.filterOpen = false;
234
+ this.filterBox.hide();
235
+ this.filterBox.setValue('');
236
+ this.tree.focus();
237
+ this.screen.render();
238
+ }
239
+ handleSlash = () => {
240
+ if (!this.active || this.isOverlayOpen())
241
+ return;
242
+ this.filterOpen = true;
243
+ this.filterBox.setValue(this.tree.getFilter());
244
+ this.filterBox.show();
245
+ this.filterBox.readInput();
246
+ this.screen.render();
247
+ };
248
+ handleEnter = () => {
249
+ if (!this.active || this.isOverlayOpen())
250
+ return;
251
+ const sel = this.tree.getSelected();
252
+ if (!sel)
253
+ return;
254
+ if (sel.kind === 'stack') {
255
+ this.tree.toggleExpansion();
256
+ return;
257
+ }
258
+ this.containerDetail.show(sel.container, this.statsCache.get(sel.container.id));
259
+ this.detailOpenHandlers.forEach((h) => h());
260
+ };
261
+ handleLeft = () => {
262
+ if (!this.active || this.isOverlayOpen())
263
+ return;
264
+ this.tree.collapseSelected();
265
+ this.screen.render();
266
+ };
267
+ handleRight = () => {
268
+ if (!this.active || this.isOverlayOpen())
269
+ return;
270
+ this.tree.expandSelected();
271
+ this.screen.render();
272
+ };
273
+ handleL = () => {
274
+ if (!this.active || this.isModalOpen())
275
+ return;
276
+ const c = this.getActionTarget();
277
+ if (!c)
278
+ return;
279
+ if (this.containerDetail.isVisible())
280
+ this.containerDetail.hide();
281
+ this.logViewer.show(c);
282
+ this.logOpenHandlers.forEach((h) => h());
283
+ };
284
+ handleX = () => {
285
+ if (!this.active || this.isModalOpen())
286
+ return;
287
+ const c = this.getActionTarget();
288
+ if (!c || c.status !== 'running') {
289
+ if (c)
290
+ this.emitError(`Cannot exec into ${c.name}: container is not running`);
291
+ return;
292
+ }
293
+ if (this.containerDetail.isVisible())
294
+ this.containerDetail.hide();
295
+ const result = (0, external_terminal_1.openExternalShell)(c.id);
296
+ if (!result.ok) {
297
+ this.emitError(result.error ?? `Failed to open external terminal for ${c.name}`);
298
+ }
299
+ this.screen.render();
300
+ };
301
+ handleS = () => {
302
+ if (!this.active || this.isModalOpen())
303
+ return;
304
+ const c = this.getActionTarget();
305
+ if (!c || !(0, status_1.isActive)(c.status))
306
+ return;
307
+ this.confirmAndRun('Stop container?', `${c.name} will be stopped.`, true, () => (0, containers_1.stopContainer)(c.id));
308
+ };
309
+ handleR = () => {
310
+ if (!this.active || this.isModalOpen())
311
+ return;
312
+ const c = this.getActionTarget();
313
+ if (!c || !(0, status_1.isActive)(c.status))
314
+ return;
315
+ this.confirmAndRun('Restart container?', `${c.name} will be restarted.`, false, () => (0, containers_1.restartContainer)(c.id));
316
+ };
317
+ handleK = () => {
318
+ if (!this.active || this.isModalOpen())
319
+ return;
320
+ const c = this.getActionTarget();
321
+ if (!c || !(0, status_1.isActive)(c.status))
322
+ return;
323
+ this.confirmAndRun('Kill container?', `${c.name} will be killed (SIGKILL).`, true, () => (0, containers_1.killContainer)(c.id));
324
+ };
325
+ handleShiftS = () => {
326
+ if (!this.active || this.isModalOpen())
327
+ return;
328
+ const c = this.getActionTarget();
329
+ if (!c)
330
+ return;
331
+ if ((0, status_1.isActive)(c.status)) {
332
+ this.emitError(`${c.name} is already ${c.status}`);
333
+ return;
334
+ }
335
+ if (c.status !== 'exited' && c.status !== 'created') {
336
+ this.emitError(`Cannot start ${c.name}: status is ${c.status}`);
337
+ return;
338
+ }
339
+ if (this.containerDetail.isVisible())
340
+ this.containerDetail.hide();
341
+ void (0, containers_1.startContainer)(c.id)
342
+ .then(() => this.refresh())
343
+ .catch((err) => this.emitError(err))
344
+ .finally(() => {
345
+ this.tree.focus();
346
+ this.screen.render();
347
+ });
348
+ };
349
+ handleD = () => {
350
+ if (!this.active || this.isModalOpen())
351
+ return;
352
+ const c = this.getActionTarget();
353
+ if (!c || (0, status_1.isActive)(c.status))
354
+ return;
355
+ this.confirmAndRun('Remove container?', `${c.name} will be permanently removed.`, true, () => (0, containers_1.removeContainer)(c.id));
356
+ };
357
+ /** Container targeted by tree/detail actions (detail takes precedence when open). */
358
+ getActionTarget() {
359
+ if (this.containerDetail.isVisible()) {
360
+ const id = this.containerDetail.getContainerId();
361
+ if (id)
362
+ return this.containers.find((c) => c.id === id) ?? null;
363
+ }
364
+ const sel = this.tree.getSelected();
365
+ if (!sel || sel.kind !== 'service')
366
+ return null;
367
+ return sel.container;
368
+ }
369
+ confirmAndRun(title, message, danger, action) {
370
+ if (this.containerDetail.isVisible())
371
+ this.containerDetail.hide();
372
+ this.confirmDialog.show({
373
+ title,
374
+ message,
375
+ danger,
376
+ onConfirm: () => {
377
+ void action()
378
+ .then(() => this.refresh())
379
+ .catch((err) => this.emitError(err))
380
+ .finally(() => {
381
+ this.tree.focus();
382
+ this.screen.render();
383
+ });
384
+ },
385
+ onCancel: () => {
386
+ this.tree.focus();
387
+ this.screen.render();
388
+ },
389
+ });
390
+ }
391
+ }
392
+ exports.StacksTab = StacksTab;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TopBar = void 0;
7
+ const module_1 = require("module");
8
+ const neo_blessed_1 = __importDefault(require("neo-blessed"));
9
+ const _theme_1 = require("../theme");
10
+ const format_1 = require("../utils/format");
11
+ const pkg = (0, module_1.createRequire)(__filename)('../../package.json');
12
+ const VERSION = pkg.version;
13
+ const SEP = ' ';
14
+ class TopBar {
15
+ box;
16
+ socketPath = '';
17
+ dockerVersion = '';
18
+ counters = { running: 0, errored: 0, stopped: 0 };
19
+ stats = null;
20
+ constructor(screen) {
21
+ this.box = neo_blessed_1.default.box({
22
+ parent: screen,
23
+ top: 0,
24
+ left: 0,
25
+ width: '100%',
26
+ height: 1,
27
+ tags: true,
28
+ style: { bg: _theme_1.C.bg, fg: _theme_1.C.fg },
29
+ });
30
+ }
31
+ setSocketPath(p) {
32
+ this.socketPath = p;
33
+ this.render();
34
+ }
35
+ setDockerVersion(v) {
36
+ this.dockerVersion = v;
37
+ this.render();
38
+ }
39
+ setCounters(c) {
40
+ this.counters = c;
41
+ this.render();
42
+ }
43
+ setStats(s) {
44
+ this.stats = s;
45
+ this.render();
46
+ }
47
+ render() {
48
+ const width = Number(this.box.width) || 80;
49
+ const right = this.buildRight(width);
50
+ const left = this.buildLeft(width, (0, format_1.visualLength)(right));
51
+ const gap = Math.max(1, width - (0, format_1.visualLength)(left) - (0, format_1.visualLength)(right));
52
+ this.box.setContent(left + ' '.repeat(gap) + right);
53
+ }
54
+ buildLeft(width, rightVisualLen) {
55
+ const dv = this.dockerVersion ? ` ${_theme_1.t.dim('· docker')} ${_theme_1.t.dim(this.dockerVersion)}` : '';
56
+ const prefix = ` ${_theme_1.t.accent('◆')} ${_theme_1.t.accent('dockza')} ${_theme_1.t.dim(`v${VERSION}`)}${dv} `;
57
+ const prefixLen = (0, format_1.visualLength)(prefix);
58
+ const sockBudget = Math.max(8, width - prefixLen - rightVisualLen - 4);
59
+ const sock = this.socketPath ? _theme_1.t.dim((0, format_1.truncateMiddle)(this.socketPath, sockBudget)) : '';
60
+ return prefix + sock;
61
+ }
62
+ buildRight(width) {
63
+ const counters = `${_theme_1.t.green('●')}${_theme_1.t.fg(String(this.counters.running))} ${_theme_1.t.dim('running')}` +
64
+ ` ${_theme_1.t.red('●')}${_theme_1.t.fg(String(this.counters.errored))} ${_theme_1.t.dim('err')}` +
65
+ ` ${_theme_1.t.fg(String(this.counters.stopped))} ${_theme_1.t.dim('stop')}`;
66
+ const stats = this.stats
67
+ ? `${_theme_1.t.dim('cpu')} ${_theme_1.t.dim(this.stats.cpuPercent.toFixed(1) + '%')}` +
68
+ ` ${_theme_1.t.dim('mem')} ${_theme_1.t.dim((0, format_1.humanSizeMB)(this.stats.memUsageMB))}`
69
+ : '';
70
+ const pieces = [];
71
+ if (width >= 120 && stats)
72
+ pieces.push(stats);
73
+ if (width >= 95)
74
+ pieces.push(counters);
75
+ return pieces.join(SEP) + ' ';
76
+ }
77
+ }
78
+ exports.TopBar = TopBar;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VolumesTab = void 0;
4
+ const volumes_1 = require("../../docker/volumes");
5
+ const _theme_1 = require("../../theme");
6
+ const format_1 = require("../../utils/format");
7
+ const resource_list_tab_1 = require("../resource-list-tab");
8
+ class VolumesTab extends resource_list_tab_1.ResourceListTab {
9
+ constructor(screen, dims) {
10
+ super(screen, dims, {
11
+ list: volumes_1.listVolumes,
12
+ remove: (vol) => (0, volumes_1.removeVolume)(vol.name),
13
+ getKey: (vol) => vol.name,
14
+ emptyMessage: 'No volumes',
15
+ confirmTitle: 'Remove volume?',
16
+ confirmLabel: (vol) => vol.name,
17
+ guards: [(vol) => (vol.inUse ? 'Volume in use — cannot delete' : null)],
18
+ columns: [
19
+ { header: 'NAME', weight: 0.28, render: (vol, w) => (0, format_1.truncate)(vol.name, w - 1) },
20
+ { header: 'DRIVER', weight: 0.12, render: (vol, w) => (0, format_1.truncate)(vol.driver, w - 1) },
21
+ {
22
+ header: 'MOUNTPOINT',
23
+ weight: 0.3,
24
+ render: (vol, w) => _theme_1.t.comment((0, format_1.truncate)(vol.mountpoint, w - 1)),
25
+ },
26
+ {
27
+ header: 'SIZE',
28
+ weight: 0.1,
29
+ render: (vol) => (vol.sizeMB > 0 ? (0, format_1.humanSizeMB)(vol.sizeMB) : _theme_1.t.comment('—')),
30
+ },
31
+ {
32
+ header: 'STATUS',
33
+ weight: 0,
34
+ render: (vol) => (vol.inUse ? _theme_1.t.green('● in use') : _theme_1.t.red('○ unused')),
35
+ },
36
+ ],
37
+ });
38
+ }
39
+ }
40
+ exports.VolumesTab = VolumesTab;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.listSelected = listSelected;
7
+ exports.createListWidget = createListWidget;
8
+ exports.createHeaderBar = createHeaderBar;
9
+ exports.createCenteredMessage = createCenteredMessage;
10
+ const neo_blessed_1 = __importDefault(require("neo-blessed"));
11
+ const _theme_1 = require("../theme");
12
+ function listSelected(list) {
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
+ const raw = list.selected;
15
+ return typeof raw === 'number' ? raw : 0;
16
+ }
17
+ function createListWidget(parent, opts = {}) {
18
+ return neo_blessed_1.default.list({
19
+ parent,
20
+ top: opts.top ?? 1,
21
+ left: opts.left ?? 0,
22
+ width: opts.width ?? '100%',
23
+ height: opts.height ?? '100%-1',
24
+ keys: true,
25
+ mouse: true,
26
+ vi: true,
27
+ scrollable: true,
28
+ scrollbar: { ch: '│', style: { fg: _theme_1.C.comment } },
29
+ border: { type: 'line' },
30
+ style: {
31
+ selected: { bg: _theme_1.C.selection, fg: _theme_1.C.fg },
32
+ item: { fg: _theme_1.C.fg },
33
+ border: { fg: _theme_1.C.selection },
34
+ focus: { border: { fg: _theme_1.C.purple } },
35
+ },
36
+ tags: true,
37
+ });
38
+ }
39
+ function createHeaderBar(parent, hidden = false) {
40
+ return neo_blessed_1.default.box({
41
+ parent,
42
+ top: 0,
43
+ left: 0,
44
+ width: '100%',
45
+ height: 1,
46
+ tags: true,
47
+ style: { bg: _theme_1.C.selection },
48
+ hidden,
49
+ });
50
+ }
51
+ function createCenteredMessage(parent) {
52
+ return neo_blessed_1.default.box({
53
+ parent,
54
+ top: 'center',
55
+ left: 'center',
56
+ width: '60%',
57
+ height: 1,
58
+ tags: true,
59
+ hidden: true,
60
+ });
61
+ }
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.openExternalShell = openExternalShell;
4
+ const child_process_1 = require("child_process");
5
+ const os_1 = require("os");
6
+ const SHELL_CMD = (id) => `docker exec -it ${id} sh -c 'command -v bash >/dev/null && exec bash || exec sh'`;
7
+ function which(cmd) {
8
+ try {
9
+ const probe = (0, os_1.platform)() === 'win32' ? 'where' : 'which';
10
+ const r = (0, child_process_1.spawnSync)(probe, [cmd], { stdio: 'ignore' });
11
+ return r.status === 0;
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }
17
+ function launch(cmd, args) {
18
+ try {
19
+ const child = (0, child_process_1.spawn)(cmd, args, { detached: true, stdio: 'ignore' });
20
+ child.on('error', () => { });
21
+ child.unref();
22
+ return { ok: true };
23
+ }
24
+ catch (err) {
25
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
26
+ }
27
+ }
28
+ function openOnMac(id) {
29
+ const cmd = SHELL_CMD(id).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
30
+ const script = `tell application "Terminal"\nactivate\ndo script "${cmd}"\nend tell`;
31
+ return launch('osascript', ['-e', script]);
32
+ }
33
+ function openOnWindows(id) {
34
+ const cmd = SHELL_CMD(id);
35
+ if (which('wt'))
36
+ return launch('wt', ['cmd', '/k', cmd]);
37
+ return launch('cmd', ['/c', 'start', '""', 'cmd', '/k', cmd]);
38
+ }
39
+ function openOnLinux(id) {
40
+ const cmd = SHELL_CMD(id);
41
+ const candidates = [
42
+ process.env.TERMINAL,
43
+ 'x-terminal-emulator',
44
+ 'gnome-terminal',
45
+ 'konsole',
46
+ 'alacritty',
47
+ 'kitty',
48
+ 'wezterm',
49
+ 'tilix',
50
+ 'xterm',
51
+ ].filter((t) => Boolean(t));
52
+ for (const term of candidates) {
53
+ if (!which(term))
54
+ continue;
55
+ const args = term === 'gnome-terminal' || term === 'tilix'
56
+ ? ['--', 'sh', '-c', cmd]
57
+ : term === 'wezterm'
58
+ ? ['start', '--', 'sh', '-c', cmd]
59
+ : term === 'kitty'
60
+ ? ['sh', '-c', cmd]
61
+ : ['-e', 'sh', '-c', cmd];
62
+ return launch(term, args);
63
+ }
64
+ return {
65
+ ok: false,
66
+ error: 'No terminal emulator found. Set $TERMINAL or install gnome-terminal, konsole, alacritty, kitty, or xterm.',
67
+ };
68
+ }
69
+ function openExternalShell(containerId) {
70
+ if (!which('docker')) {
71
+ return {
72
+ ok: false,
73
+ error: "The 'docker' CLI is not on your PATH. Install Docker or make sure it is in PATH to use shell-into-container.",
74
+ };
75
+ }
76
+ switch ((0, os_1.platform)()) {
77
+ case 'darwin':
78
+ return openOnMac(containerId);
79
+ case 'win32':
80
+ return openOnWindows(containerId);
81
+ default:
82
+ return openOnLinux(containerId);
83
+ }
84
+ }