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.
- package/LICENSE +22 -0
- package/NOTICE +69 -0
- package/README.md +203 -0
- package/dist/cli.js +33 -0
- package/dist/docker/client.js +76 -0
- package/dist/docker/containers.js +180 -0
- package/dist/docker/images.js +41 -0
- package/dist/docker/networks.js +66 -0
- package/dist/docker/volumes.js +44 -0
- package/dist/models/docker.js +2 -0
- package/dist/theme.js +55 -0
- package/dist/ui/app.js +517 -0
- package/dist/ui/containers/confirm-dialog.js +84 -0
- package/dist/ui/containers/container-detail.js +189 -0
- package/dist/ui/containers/container-list.js +120 -0
- package/dist/ui/containers/containers-tab.js +314 -0
- package/dist/ui/containers/log-viewer.js +190 -0
- package/dist/ui/footer.js +193 -0
- package/dist/ui/help-overlay.js +122 -0
- package/dist/ui/images/images-tab.js +37 -0
- package/dist/ui/networks/networks-tab.js +45 -0
- package/dist/ui/resource-list-tab.js +183 -0
- package/dist/ui/side-rail.js +132 -0
- package/dist/ui/stacks/stack-tree.js +346 -0
- package/dist/ui/stacks/stacks-tab.js +392 -0
- package/dist/ui/top-bar.js +78 -0
- package/dist/ui/volumes/volumes-tab.js +40 -0
- package/dist/ui/widgets.js +61 -0
- package/dist/utils/external-terminal.js +84 -0
- package/dist/utils/format.js +65 -0
- package/dist/utils/stacks.js +76 -0
- package/dist/utils/stats.js +12 -0
- package/dist/utils/status.js +80 -0
- package/dist/utils/term-caps.js +14 -0
- package/package.json +77 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Achraf El Fadili
|
|
4
|
+
Copyright (c) 2026 Dmytro Shandyba
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/NOTICE
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
dockza
|
|
2
|
+
Copyright (c) 2026 Dmytro Shandyba
|
|
3
|
+
|
|
4
|
+
This product includes software originally developed as "docktui"
|
|
5
|
+
Copyright (c) 2026 Achraf El Fadili
|
|
6
|
+
|
|
7
|
+
================================================================================
|
|
8
|
+
Origin and history
|
|
9
|
+
================================================================================
|
|
10
|
+
|
|
11
|
+
dockza is a fork of docktui, a terminal UI for Docker originally written by
|
|
12
|
+
Achraf El Fadili and released under the MIT License.
|
|
13
|
+
|
|
14
|
+
Original project : docktui
|
|
15
|
+
Original author : Achraf El Fadili <contact@elfadiliachraf.tech>
|
|
16
|
+
Original source : https://github.com/0xShady/docktui
|
|
17
|
+
Original license : MIT (see LICENSE)
|
|
18
|
+
|
|
19
|
+
The fork was made from docktui v0.1.1. Upstream development appeared to have
|
|
20
|
+
stopped, and this fork continues the work independently. It is not affiliated
|
|
21
|
+
with, endorsed by, or supported by the original author or the original project.
|
|
22
|
+
|
|
23
|
+
Former names of this software:
|
|
24
|
+
|
|
25
|
+
docktui (upstream project and npm package, through v0.1.1)
|
|
26
|
+
dockza (this fork, from v0.2.0 onward)
|
|
27
|
+
|
|
28
|
+
The complete authorship record is preserved in this repository's git history,
|
|
29
|
+
which retains every original commit and its author attribution unmodified.
|
|
30
|
+
|
|
31
|
+
Both the original code and the modifications made in this fork are distributed
|
|
32
|
+
under the MIT License. The LICENSE file carries the copyright notices of the
|
|
33
|
+
original author and of the fork maintainer, as the MIT License requires.
|
|
34
|
+
|
|
35
|
+
Names, logos, domains and published packages belonging to the original project
|
|
36
|
+
(including the docktui name, the docktui logo, the docktui.com domain and the
|
|
37
|
+
"docktui" npm package) remain the property of the original author and are not
|
|
38
|
+
used by this fork. dockza carries its own name and its own logo.
|
|
39
|
+
|
|
40
|
+
================================================================================
|
|
41
|
+
Third-party software
|
|
42
|
+
================================================================================
|
|
43
|
+
|
|
44
|
+
This software depends on the following third-party packages. They are declared
|
|
45
|
+
as runtime dependencies and are not bundled into this distribution; each is
|
|
46
|
+
retrieved and installed by the package manager under its own license.
|
|
47
|
+
|
|
48
|
+
dayjs MIT https://github.com/iamkun/dayjs
|
|
49
|
+
dockerode Apache-2.0 https://github.com/apocas/dockerode
|
|
50
|
+
neo-blessed MIT https://github.com/embarklabs/neo-blessed
|
|
51
|
+
|
|
52
|
+
dockerode is licensed under the Apache License, Version 2.0. A copy of that
|
|
53
|
+
license is available at https://www.apache.org/licenses/LICENSE-2.0. If this
|
|
54
|
+
software is ever redistributed in a form that bundles or vendors dockerode's
|
|
55
|
+
source or object code, the Apache-2.0 attribution requirements (Section 4)
|
|
56
|
+
apply and this NOTICE must be extended accordingly.
|
|
57
|
+
|
|
58
|
+
================================================================================
|
|
59
|
+
Trademarks
|
|
60
|
+
================================================================================
|
|
61
|
+
|
|
62
|
+
Docker and the Docker logo are trademarks or registered trademarks of Docker,
|
|
63
|
+
Inc. dockza is an independent, unofficial tool. It is not produced, endorsed,
|
|
64
|
+
sponsored, or supported by Docker, Inc., and the "dockza" name is not a Docker,
|
|
65
|
+
Inc. product name.
|
|
66
|
+
|
|
67
|
+
Podman is a trademark of Red Hat, Inc. Any other product names, logos and brands
|
|
68
|
+
referenced in this software or its documentation are the property of their
|
|
69
|
+
respective owners and are used for identification purposes only.
|
package/README.md
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/shandyba/dockza/main/assets/icon.png" alt="dockza" width="180">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">dockza</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
A lightweight terminal UI for Docker. Manage containers, images, volumes, and networks from your terminal — Compose-aware, no mouse required.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/dockza"><img src="https://img.shields.io/npm/v/dockza.svg" alt="npm version"></a>
|
|
13
|
+
<a href="https://github.com/shandyba/dockza/actions/workflows/ci.yml"><img src="https://github.com/shandyba/dockza/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
14
|
+
<a href="https://nodejs.org/"><img src="https://img.shields.io/node/v/dockza.svg" alt="Node"></a>
|
|
15
|
+
<a href="./LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<!-- TODO: record a new asciicast under the dockza name and link it here. -->
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
Run it without installing:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx dockza
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or install globally:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install -g dockza
|
|
32
|
+
dockza
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- **Compose-aware Stacks view** — containers grouped by project with running / errored / stopped counts, collapsible per stack.
|
|
38
|
+
- **Containers / Images / Volumes** — live CPU and memory stats, start / stop / restart / kill / remove with confirm dialogs.
|
|
39
|
+
- **Networks** — list Docker networks with driver, scope, attached-container count, creation time, and in-use status; remove unused networks (built-in networks and any with attached containers are protected).
|
|
40
|
+
- **Detail panel** — full metadata, mounts, environment, live CPU/MEM bars.
|
|
41
|
+
- **Streaming log viewer** with follow mode, scrollback cap, and color-coded stdout/stderr.
|
|
42
|
+
- **Filter** — press `/` from Stacks to filter by stack, service, or image name.
|
|
43
|
+
- **Side rail navigation** — `1`–`5` or `Tab` cycles views; click a live stack to jump to it.
|
|
44
|
+
- **Help overlay** — press `h` anywhere (except confirm dialogs / the stack filter) to toggle the key-binding reference.
|
|
45
|
+
- **Shell into containers** — `x` opens an external terminal `exec`'d into the selected container (macOS Terminal, Windows Terminal, common Linux emulators).
|
|
46
|
+
- **Honors `DOCKER_HOST`** — works with rootless Docker, podman, and remote daemons.
|
|
47
|
+
|
|
48
|
+
## Requirements
|
|
49
|
+
|
|
50
|
+
- Node.js **20** or later
|
|
51
|
+
- A running Docker daemon (Docker Desktop on macOS/Windows, Docker Engine on Linux, or any podman/rootless setup)
|
|
52
|
+
- A **256-color** or **truecolor** terminal (`TERM=xterm-256color` or `COLORTERM=truecolor`)
|
|
53
|
+
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
### Custom Docker host
|
|
57
|
+
|
|
58
|
+
dockza auto-detects the daemon socket. To point it elsewhere, set `DOCKER_HOST`:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
DOCKER_HOST=unix:///run/user/1000/docker.sock dockza # rootless docker
|
|
62
|
+
DOCKER_HOST=unix:///run/user/1000/podman/podman.sock dockza # podman
|
|
63
|
+
DOCKER_HOST=tcp://10.0.0.5:2375 dockza # remote daemon
|
|
64
|
+
DOCKER_HOST=npipe:////./pipe/docker_engine dockza # Windows named pipe
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Shell-into-container (`x` key)
|
|
68
|
+
|
|
69
|
+
Requires the `docker` CLI to be on your `PATH`. dockza launches your platform's native terminal with `docker exec -it <id> sh` (falls back to `bash` if available inside the container).
|
|
70
|
+
|
|
71
|
+
## Key bindings
|
|
72
|
+
|
|
73
|
+
### Global
|
|
74
|
+
|
|
75
|
+
| Key | Action |
|
|
76
|
+
|-----|--------|
|
|
77
|
+
| `1` – `5` | Jump to Stacks / Containers / Images / Volumes / Networks |
|
|
78
|
+
| `Tab` / `Shift+Tab` | Cycle views |
|
|
79
|
+
| `h` | Toggle help overlay |
|
|
80
|
+
| `q` / `Ctrl+C` | Quit |
|
|
81
|
+
|
|
82
|
+
### Stacks
|
|
83
|
+
|
|
84
|
+
| Key | Action |
|
|
85
|
+
|-----|--------|
|
|
86
|
+
| `↑ ↓` / `j k` | Navigate tree |
|
|
87
|
+
| `→` / `←` | Expand / collapse stack |
|
|
88
|
+
| `Enter` | On a stack: toggle expand. On a service: open detail panel |
|
|
89
|
+
| `/` | Open filter (`Esc` to clear) |
|
|
90
|
+
| `l` | Open log viewer |
|
|
91
|
+
| `x` | Shell into selected running container |
|
|
92
|
+
| `s` / `r` / `k` | Stop / restart / kill running container (confirm) |
|
|
93
|
+
| `S` | Start stopped container |
|
|
94
|
+
| `d` | Remove stopped container (confirm) |
|
|
95
|
+
|
|
96
|
+
### Containers
|
|
97
|
+
|
|
98
|
+
Same per-container actions as Stacks. `Enter` always opens the detail panel.
|
|
99
|
+
|
|
100
|
+
### Detail panel
|
|
101
|
+
|
|
102
|
+
Same container actions as the list/tree (`l`, `s`, `r`, `k`, `S`, `d`, `x`) plus:
|
|
103
|
+
|
|
104
|
+
| Key | Action |
|
|
105
|
+
|-----|--------|
|
|
106
|
+
| `e` | Toggle environment variables (expanded / collapsed) |
|
|
107
|
+
| `↑ ↓` | Scroll |
|
|
108
|
+
| `Esc` | Close |
|
|
109
|
+
|
|
110
|
+
### Log viewer
|
|
111
|
+
|
|
112
|
+
| Key | Action |
|
|
113
|
+
|-----|--------|
|
|
114
|
+
| `f` | Toggle follow mode |
|
|
115
|
+
| `g` | Scroll to top |
|
|
116
|
+
| `G` | Scroll to bottom, re-enable follow |
|
|
117
|
+
| `↑` / `k` | Scroll up (disables follow) |
|
|
118
|
+
| `Esc` | Close |
|
|
119
|
+
|
|
120
|
+
### Images, Volumes & Networks
|
|
121
|
+
|
|
122
|
+
| Key | Action |
|
|
123
|
+
|-----|--------|
|
|
124
|
+
| `↑ ↓` / `j k` | Navigate list |
|
|
125
|
+
| `d` | Delete unused item (confirm) |
|
|
126
|
+
|
|
127
|
+
Networks additionally protect the built-in `bridge` / `host` / `none` networks and any network with attached containers — `d` on those shows a message instead of a confirm dialog. In-use detection is consistent with Images and Volumes: a network counts as in use whenever a container is attached, running or stopped.
|
|
128
|
+
|
|
129
|
+
### Confirm dialog
|
|
130
|
+
|
|
131
|
+
| `y` / `Y` to confirm · `n` / `N` / `Esc` to cancel |
|
|
132
|
+
|-----------------------------------------------------|
|
|
133
|
+
|
|
134
|
+
## Troubleshooting
|
|
135
|
+
|
|
136
|
+
### `permission denied` on the Docker socket (Linux)
|
|
137
|
+
|
|
138
|
+
Add your user to the `docker` group, then log out and back in:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
sudo usermod -aG docker $USER
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Windows: cannot connect to the daemon
|
|
145
|
+
|
|
146
|
+
By default dockza tries the named pipe `//./pipe/docker_engine`. If that fails, enable TCP in Docker Desktop (**Settings → General → Expose daemon on `tcp://localhost:2375` without TLS**) and run:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
DOCKER_HOST=tcp://localhost:2375 dockza
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### `dockza requires a 256-color (or truecolor) terminal`
|
|
153
|
+
|
|
154
|
+
Your `$TERM` is too narrow. Set it explicitly or use a modern terminal:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
TERM=xterm-256color dockza
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### `The 'docker' CLI is not on your PATH`
|
|
161
|
+
|
|
162
|
+
You hit `x` (shell into container) but the `docker` binary isn't installed or isn't on `PATH`. Install Docker CLI or skip the `x` action — everything else works without it.
|
|
163
|
+
|
|
164
|
+
## Development
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
git clone https://github.com/shandyba/dockza.git
|
|
168
|
+
cd dockza
|
|
169
|
+
npm install
|
|
170
|
+
|
|
171
|
+
npm run dev # run from source with ts-node
|
|
172
|
+
npm run build # compile to dist/
|
|
173
|
+
npm start # run compiled output
|
|
174
|
+
|
|
175
|
+
npm test # vitest, one-shot
|
|
176
|
+
npm run test:coverage # generate ./coverage/index.html
|
|
177
|
+
npm run lint
|
|
178
|
+
npm run format
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for the pre-PR checklist and [CHANGELOG.md](./CHANGELOG.md) for release history.
|
|
182
|
+
|
|
183
|
+
## Credits
|
|
184
|
+
|
|
185
|
+
dockza is a fork of [**docktui**](https://github.com/0xShady/docktui) by
|
|
186
|
+
[Achraf El Fadili](https://github.com/0xShady), released under the MIT License.
|
|
187
|
+
Upstream development appeared to have stopped, so this fork continues the work
|
|
188
|
+
independently under a new name. Every original commit and its authorship is
|
|
189
|
+
preserved in this repository's git history.
|
|
190
|
+
|
|
191
|
+
This project is not affiliated with, endorsed by, or supported by the original
|
|
192
|
+
author or the original project. Bug reports and feature requests belong
|
|
193
|
+
[here](https://github.com/shandyba/dockza/issues), not upstream.
|
|
194
|
+
|
|
195
|
+
Docker and the Docker logo are trademarks or registered trademarks of Docker,
|
|
196
|
+
Inc. dockza is an independent, unofficial tool and is not a Docker, Inc.
|
|
197
|
+
product.
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
[MIT](./LICENSE) © Achraf El Fadili (original work) · © Dmytro Shandyba (fork and subsequent work)
|
|
202
|
+
|
|
203
|
+
See [NOTICE](./NOTICE) for attribution details and third-party licenses.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const client_1 = require("./docker/client");
|
|
5
|
+
const app_1 = require("./ui/app");
|
|
6
|
+
const term_caps_1 = require("./utils/term-caps");
|
|
7
|
+
async function main() {
|
|
8
|
+
const colors = (0, term_caps_1.detectColors)();
|
|
9
|
+
if (colors === 'low') {
|
|
10
|
+
process.stderr.write('dockza requires a 256-color (or truecolor) terminal. Set TERM=xterm-256color or use a modern terminal emulator.\n');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
await (0, client_1.pingDocker)();
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const app = new app_1.App();
|
|
22
|
+
await app.start();
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
process.stderr.write(`Fatal error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
main().catch((err) => {
|
|
31
|
+
process.stderr.write(`Fatal error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
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.dockerode = void 0;
|
|
7
|
+
exports.getDockerSocketLabel = getDockerSocketLabel;
|
|
8
|
+
exports.pingDocker = pingDocker;
|
|
9
|
+
exports.getDockerVersion = getDockerVersion;
|
|
10
|
+
const fs_1 = require("fs");
|
|
11
|
+
const dockerode_1 = __importDefault(require("dockerode"));
|
|
12
|
+
let socketLabel = '';
|
|
13
|
+
function createDockerode() {
|
|
14
|
+
// Honor DOCKER_HOST when set (rootless docker, podman, remote daemons).
|
|
15
|
+
// Supported forms: unix:///path/to/sock, npipe:////./pipe/name, tcp://host:port
|
|
16
|
+
const host = process.env.DOCKER_HOST;
|
|
17
|
+
if (host) {
|
|
18
|
+
if (host.startsWith('unix://')) {
|
|
19
|
+
const sock = host.slice('unix://'.length);
|
|
20
|
+
socketLabel = `unix://${sock}`;
|
|
21
|
+
return new dockerode_1.default({ socketPath: sock });
|
|
22
|
+
}
|
|
23
|
+
if (host.startsWith('npipe://')) {
|
|
24
|
+
const sock = host.slice('npipe://'.length);
|
|
25
|
+
socketLabel = `npipe://${sock}`;
|
|
26
|
+
return new dockerode_1.default({ socketPath: sock });
|
|
27
|
+
}
|
|
28
|
+
if (host.startsWith('tcp://') || host.startsWith('http://') || host.startsWith('https://')) {
|
|
29
|
+
const url = new URL(host);
|
|
30
|
+
socketLabel = host;
|
|
31
|
+
return new dockerode_1.default({
|
|
32
|
+
host: url.hostname,
|
|
33
|
+
port: Number(url.port) || (url.protocol === 'https:' ? 2376 : 2375),
|
|
34
|
+
protocol: url.protocol === 'https:' ? 'https' : 'http',
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
throw new Error(`Unrecognized DOCKER_HOST scheme: '${host}'. Expected unix://…, npipe://…, tcp://…, http://…, or https://…`);
|
|
38
|
+
}
|
|
39
|
+
if (process.platform === 'win32') {
|
|
40
|
+
socketLabel = 'npipe:////./pipe/docker_engine';
|
|
41
|
+
return new dockerode_1.default({ socketPath: '//./pipe/docker_engine' });
|
|
42
|
+
}
|
|
43
|
+
// macOS Docker Desktop sometimes only exposes the per-user socket; prefer it when present.
|
|
44
|
+
const userSock = `${process.env.HOME ?? ''}/.docker/run/docker.sock`;
|
|
45
|
+
if (process.platform === 'darwin' && process.env.HOME && (0, fs_1.existsSync)(userSock)) {
|
|
46
|
+
socketLabel = `unix://${userSock}`;
|
|
47
|
+
return new dockerode_1.default({ socketPath: userSock });
|
|
48
|
+
}
|
|
49
|
+
socketLabel = 'unix:///var/run/docker.sock';
|
|
50
|
+
return new dockerode_1.default({ socketPath: '/var/run/docker.sock' });
|
|
51
|
+
}
|
|
52
|
+
exports.dockerode = createDockerode();
|
|
53
|
+
function getDockerSocketLabel() {
|
|
54
|
+
return socketLabel;
|
|
55
|
+
}
|
|
56
|
+
async function pingDocker() {
|
|
57
|
+
try {
|
|
58
|
+
await exports.dockerode.ping();
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
62
|
+
if (/EACCES|permission denied/i.test(raw) && process.platform === 'linux') {
|
|
63
|
+
throw new Error(`Cannot access the Docker socket (permission denied). Add your user to the 'docker' group: sudo usermod -aG docker $USER (then log out and back in). (${raw})`);
|
|
64
|
+
}
|
|
65
|
+
throw new Error(`Docker is not running or not accessible. Make sure the Docker daemon is started. (${raw})`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function getDockerVersion() {
|
|
69
|
+
try {
|
|
70
|
+
const info = (await exports.dockerode.version());
|
|
71
|
+
return String(info?.Version ?? '');
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return 'unknown';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toContainerInfo = toContainerInfo;
|
|
4
|
+
exports.listContainers = listContainers;
|
|
5
|
+
exports.toContainerStats = toContainerStats;
|
|
6
|
+
exports.fetchStats = fetchStats;
|
|
7
|
+
exports.startContainer = startContainer;
|
|
8
|
+
exports.stopContainer = stopContainer;
|
|
9
|
+
exports.restartContainer = restartContainer;
|
|
10
|
+
exports.killContainer = killContainer;
|
|
11
|
+
exports.removeContainer = removeContainer;
|
|
12
|
+
exports.streamLogs = streamLogs;
|
|
13
|
+
const client_1 = require("./client");
|
|
14
|
+
const format_1 = require("../utils/format");
|
|
15
|
+
const stats_1 = require("../utils/stats");
|
|
16
|
+
const VALID_STATUSES = new Set([
|
|
17
|
+
'running',
|
|
18
|
+
'paused',
|
|
19
|
+
'exited',
|
|
20
|
+
'restarting',
|
|
21
|
+
'dead',
|
|
22
|
+
'created',
|
|
23
|
+
'removing',
|
|
24
|
+
]);
|
|
25
|
+
function toContainerInfo(raw, inspected) {
|
|
26
|
+
const name = (raw.Names?.[0] ?? raw.Id).replace(/^\//, '');
|
|
27
|
+
const status = VALID_STATUSES.has(raw.State)
|
|
28
|
+
? raw.State
|
|
29
|
+
: 'exited';
|
|
30
|
+
let uptime = raw.Status;
|
|
31
|
+
if (inspected) {
|
|
32
|
+
if (status === 'running' || status === 'paused' || status === 'restarting') {
|
|
33
|
+
const startedAt = new Date(inspected.State.StartedAt);
|
|
34
|
+
if (startedAt.getTime() > 0)
|
|
35
|
+
uptime = (0, format_1.humanUptime)(startedAt);
|
|
36
|
+
}
|
|
37
|
+
else if (status === 'exited' || status === 'dead') {
|
|
38
|
+
const finishedAt = new Date(inspected.State.FinishedAt);
|
|
39
|
+
if (finishedAt.getTime() > 0)
|
|
40
|
+
uptime = (0, format_1.relativeTime)(finishedAt);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// The daemon sends null (not []) for these when a container publishes no ports
|
|
44
|
+
// or is attached to no networks, so every access here has to tolerate null.
|
|
45
|
+
const ports = (raw.Ports ?? []).map((p) => p.PublicPort
|
|
46
|
+
? `${p.IP ? `${p.IP}:` : ''}${p.PublicPort}->${p.PrivatePort}/${p.Type}`
|
|
47
|
+
: `${p.PrivatePort}/${p.Type}`);
|
|
48
|
+
const rawNetworks = raw.NetworkSettings?.Networks ?? {};
|
|
49
|
+
const networks = Object.keys(rawNetworks);
|
|
50
|
+
const ip = networks.length > 0 ? (rawNetworks[networks[0]]?.IPAddress ?? '') : '';
|
|
51
|
+
const mounts = (raw.Mounts ?? []).map((m) => ({
|
|
52
|
+
source: m.Source ?? '',
|
|
53
|
+
destination: m.Destination ?? '',
|
|
54
|
+
mode: m.Mode ?? '',
|
|
55
|
+
rw: m.RW ?? false,
|
|
56
|
+
type: m.Type ?? 'bind',
|
|
57
|
+
}));
|
|
58
|
+
return {
|
|
59
|
+
id: raw.Id,
|
|
60
|
+
name,
|
|
61
|
+
image: raw.Image,
|
|
62
|
+
status,
|
|
63
|
+
exitCode: inspected?.State?.ExitCode ?? 0,
|
|
64
|
+
uptime,
|
|
65
|
+
ports,
|
|
66
|
+
networks,
|
|
67
|
+
ip,
|
|
68
|
+
mounts,
|
|
69
|
+
env: inspected?.Config?.Env ?? [],
|
|
70
|
+
restartPolicy: inspected?.HostConfig?.RestartPolicy?.Name ?? 'no',
|
|
71
|
+
pids: inspected?.State?.Pid ?? 0,
|
|
72
|
+
labels: raw.Labels ?? {},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async function listContainers() {
|
|
76
|
+
try {
|
|
77
|
+
const rawList = await client_1.dockerode.listContainers({ all: true });
|
|
78
|
+
return await Promise.all(rawList.map(async (raw) => {
|
|
79
|
+
let inspected = null;
|
|
80
|
+
try {
|
|
81
|
+
inspected = await client_1.dockerode.getContainer(raw.Id).inspect();
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Fall back to list data if inspect fails
|
|
85
|
+
}
|
|
86
|
+
return toContainerInfo(raw, inspected);
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
throw new Error(`Failed to list containers: ${err instanceof Error ? err.message : String(err)}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
94
|
+
function toContainerStats(stats) {
|
|
95
|
+
const usage = stats.memory_stats.usage;
|
|
96
|
+
const limit = stats.memory_stats.limit;
|
|
97
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
98
|
+
const blkio = stats.blkio_stats?.io_service_bytes_recursive ?? [];
|
|
99
|
+
const readBytes = blkio
|
|
100
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
101
|
+
.filter((b) => b.op === 'Read' || b.op === 'read')
|
|
102
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
103
|
+
.reduce((sum, b) => sum + (b.value ?? 0), 0);
|
|
104
|
+
const writeBytes = blkio
|
|
105
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
106
|
+
.filter((b) => b.op === 'Write' || b.op === 'write')
|
|
107
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
108
|
+
.reduce((sum, b) => sum + (b.value ?? 0), 0);
|
|
109
|
+
return {
|
|
110
|
+
cpuPercent: (0, stats_1.calcCPUPercent)(stats),
|
|
111
|
+
memUsageMB: usage / 1024 / 1024,
|
|
112
|
+
memLimitMB: limit / 1024 / 1024,
|
|
113
|
+
memPercent: limit > 0 ? (usage / limit) * 100 : 0,
|
|
114
|
+
diskReadMB: readBytes / 1024 / 1024,
|
|
115
|
+
diskWriteMB: writeBytes / 1024 / 1024,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
async function fetchStats(id) {
|
|
119
|
+
try {
|
|
120
|
+
const stats = await client_1.dockerode.getContainer(id).stats({ stream: false });
|
|
121
|
+
return toContainerStats(stats);
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
throw new Error(`Failed to fetch stats for container ${id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function startContainer(id) {
|
|
128
|
+
try {
|
|
129
|
+
await client_1.dockerode.getContainer(id).start();
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
throw new Error(`Failed to start container ${id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function stopContainer(id) {
|
|
136
|
+
try {
|
|
137
|
+
await client_1.dockerode.getContainer(id).stop();
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
throw new Error(`Failed to stop container ${id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function restartContainer(id) {
|
|
144
|
+
try {
|
|
145
|
+
await client_1.dockerode.getContainer(id).restart();
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
throw new Error(`Failed to restart container ${id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async function killContainer(id) {
|
|
152
|
+
try {
|
|
153
|
+
await client_1.dockerode.getContainer(id).kill();
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
throw new Error(`Failed to kill container ${id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function removeContainer(id) {
|
|
160
|
+
try {
|
|
161
|
+
await client_1.dockerode.getContainer(id).remove({ force: true });
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
throw new Error(`Failed to remove container ${id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function streamLogs(id, tail) {
|
|
168
|
+
try {
|
|
169
|
+
return await client_1.dockerode.getContainer(id).logs({
|
|
170
|
+
follow: true,
|
|
171
|
+
stdout: true,
|
|
172
|
+
stderr: true,
|
|
173
|
+
tail,
|
|
174
|
+
timestamps: false,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
throw new Error(`Failed to stream logs for container ${id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.listImages = listImages;
|
|
4
|
+
exports.removeImage = removeImage;
|
|
5
|
+
const client_1 = require("./client");
|
|
6
|
+
async function listImages() {
|
|
7
|
+
try {
|
|
8
|
+
const [rawImages, rawContainers] = await Promise.all([
|
|
9
|
+
client_1.dockerode.listImages({ all: false }),
|
|
10
|
+
client_1.dockerode.listContainers({ all: true }),
|
|
11
|
+
]);
|
|
12
|
+
const usedImageIds = new Set(rawContainers.map((c) => c.ImageID));
|
|
13
|
+
return rawImages
|
|
14
|
+
.sort((a, b) => (a.RepoTags?.[0] ?? '').localeCompare(b.RepoTags?.[0] ?? ''))
|
|
15
|
+
.map((img) => {
|
|
16
|
+
const repoTag = img.RepoTags?.[0] ?? '<none>:<none>';
|
|
17
|
+
const colonIdx = repoTag.lastIndexOf(':');
|
|
18
|
+
const repository = colonIdx >= 0 ? repoTag.slice(0, colonIdx) || '<none>' : '<none>';
|
|
19
|
+
const tag = colonIdx >= 0 ? repoTag.slice(colonIdx + 1) || '<none>' : '<none>';
|
|
20
|
+
return {
|
|
21
|
+
id: img.Id,
|
|
22
|
+
repository,
|
|
23
|
+
tag,
|
|
24
|
+
sizeMB: img.Size / 1024 / 1024,
|
|
25
|
+
created: new Date(img.Created * 1000),
|
|
26
|
+
inUse: usedImageIds.has(img.Id),
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
throw new Error(`Failed to list images: ${err instanceof Error ? err.message : String(err)}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function removeImage(id) {
|
|
35
|
+
try {
|
|
36
|
+
await client_1.dockerode.getImage(id).remove();
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
throw new Error(`Failed to remove image ${id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
40
|
+
}
|
|
41
|
+
}
|