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,65 @@
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.humanUptime = humanUptime;
7
+ exports.humanSizeMB = humanSizeMB;
8
+ exports.relativeTime = relativeTime;
9
+ exports.truncate = truncate;
10
+ exports.truncateMiddle = truncateMiddle;
11
+ exports.stripTags = stripTags;
12
+ exports.visualLength = visualLength;
13
+ exports.padEnd = padEnd;
14
+ const dayjs_1 = __importDefault(require("dayjs"));
15
+ const relativeTime_1 = __importDefault(require("dayjs/plugin/relativeTime"));
16
+ dayjs_1.default.extend(relativeTime_1.default);
17
+ function humanUptime(startedAt) {
18
+ const totalSeconds = Math.max(0, Math.floor((Date.now() - startedAt.getTime()) / 1000));
19
+ const days = Math.floor(totalSeconds / 86400);
20
+ const hours = Math.floor((totalSeconds % 86400) / 3600);
21
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
22
+ const seconds = totalSeconds % 60;
23
+ if (days > 0)
24
+ return `${days}d ${hours}h`;
25
+ if (hours > 0)
26
+ return `${hours}h ${minutes}m`;
27
+ if (minutes > 0)
28
+ return `${minutes}m ${seconds}s`;
29
+ return `${seconds}s`;
30
+ }
31
+ function humanSizeMB(mb) {
32
+ if (mb >= 1024)
33
+ return `${(mb / 1024).toFixed(1)} GiB`;
34
+ return `${Math.round(mb)} MiB`;
35
+ }
36
+ function relativeTime(date) {
37
+ return (0, dayjs_1.default)(date).fromNow();
38
+ }
39
+ function truncate(s, n) {
40
+ if (s.length <= n)
41
+ return s;
42
+ return s.slice(0, n - 1) + '…';
43
+ }
44
+ function truncateMiddle(s, maxLen, ellipsis = '…') {
45
+ if (s.length <= maxLen)
46
+ return s;
47
+ if (maxLen <= ellipsis.length)
48
+ return ellipsis.slice(0, maxLen);
49
+ const keep = maxLen - ellipsis.length;
50
+ const left = Math.ceil(keep / 2);
51
+ const right = Math.floor(keep / 2);
52
+ return s.slice(0, left) + ellipsis + s.slice(s.length - right);
53
+ }
54
+ function stripTags(s) {
55
+ return s.replace(/\{[^}]+\}/g, '');
56
+ }
57
+ function visualLength(s) {
58
+ return stripTags(s).length;
59
+ }
60
+ function padEnd(s, visualLen) {
61
+ const pad = visualLen - visualLength(s);
62
+ if (pad <= 0)
63
+ return s;
64
+ return s + ' '.repeat(pad);
65
+ }
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NO_STACK = void 0;
4
+ exports.classifyContainer = classifyContainer;
5
+ exports.groupIntoStacks = groupIntoStacks;
6
+ exports.NO_STACK = '(no stack)';
7
+ const COMPOSE_PROJECT_LABEL = 'com.docker.compose.project';
8
+ const DEFAULT_NETWORK_SUFFIX = '_default';
9
+ function classifyContainer(c) {
10
+ if (c.status === 'running' || c.status === 'paused' || c.status === 'restarting') {
11
+ return 'running';
12
+ }
13
+ if ((c.status === 'exited' || c.status === 'dead') && c.exitCode !== 0) {
14
+ return 'errored';
15
+ }
16
+ return 'stopped';
17
+ }
18
+ function detectStack(c) {
19
+ const labeled = c.labels[COMPOSE_PROJECT_LABEL];
20
+ if (labeled)
21
+ return { id: labeled, isCompose: true };
22
+ for (const net of c.networks) {
23
+ if (net.endsWith(DEFAULT_NETWORK_SUFFIX)) {
24
+ return { id: net.slice(0, -DEFAULT_NETWORK_SUFFIX.length), isCompose: true };
25
+ }
26
+ }
27
+ return { id: exports.NO_STACK, isCompose: false };
28
+ }
29
+ const CLASS_ORDER = { running: 0, errored: 1, stopped: 2 };
30
+ function compareServices(a, b) {
31
+ const ca = CLASS_ORDER[classifyContainer(a)];
32
+ const cb = CLASS_ORDER[classifyContainer(b)];
33
+ if (ca !== cb)
34
+ return ca - cb;
35
+ return a.name.localeCompare(b.name);
36
+ }
37
+ function groupIntoStacks(containers) {
38
+ const grouped = new Map();
39
+ for (const c of containers) {
40
+ const { id, isCompose } = detectStack(c);
41
+ const existing = grouped.get(id);
42
+ if (existing) {
43
+ existing.services.push(c);
44
+ if (isCompose)
45
+ existing.isCompose = true;
46
+ }
47
+ else {
48
+ grouped.set(id, { isCompose, services: [c] });
49
+ }
50
+ }
51
+ const stacks = [];
52
+ for (const [id, { isCompose, services }] of grouped) {
53
+ const counts = { running: 0, errored: 0, stopped: 0 };
54
+ for (const c of services)
55
+ counts[classifyContainer(c)]++;
56
+ stacks.push({
57
+ id,
58
+ isCompose,
59
+ services: [...services].sort(compareServices),
60
+ counts,
61
+ isLive: counts.running > 0,
62
+ });
63
+ }
64
+ stacks.sort((a, b) => {
65
+ if (a.id === exports.NO_STACK && b.id !== exports.NO_STACK)
66
+ return 1;
67
+ if (b.id === exports.NO_STACK && a.id !== exports.NO_STACK)
68
+ return -1;
69
+ if (a.isLive !== b.isLive)
70
+ return a.isLive ? -1 : 1;
71
+ if (a.isLive)
72
+ return b.counts.running - a.counts.running;
73
+ return b.services.length - a.services.length;
74
+ });
75
+ return stacks;
76
+ }
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.calcCPUPercent = calcCPUPercent;
4
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
+ function calcCPUPercent(stats) {
6
+ const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
7
+ const sysDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
8
+ const cpuCount = stats.cpu_stats.online_cpus ?? stats.cpu_stats.cpu_usage.percpu_usage?.length ?? 1;
9
+ if (sysDelta <= 0)
10
+ return 0;
11
+ return (cpuDelta / sysDelta) * cpuCount * 100;
12
+ }
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ACTIVE_STATUSES = void 0;
4
+ exports.isActive = isActive;
5
+ exports.statusDot = statusDot;
6
+ exports.statusLabel = statusLabel;
7
+ exports.colorByStatus = colorByStatus;
8
+ exports.cpuColor = cpuColor;
9
+ exports.memColor = memColor;
10
+ exports.formatCpuCell = formatCpuCell;
11
+ exports.formatMemCell = formatMemCell;
12
+ const _theme_1 = require("../theme");
13
+ exports.ACTIVE_STATUSES = new Set(['running', 'paused', 'restarting']);
14
+ function isActive(status) {
15
+ return exports.ACTIVE_STATUSES.has(status);
16
+ }
17
+ function statusDot(c) {
18
+ switch (c.status) {
19
+ case 'running':
20
+ return _theme_1.t.green('●');
21
+ case 'paused':
22
+ return _theme_1.t.yellow('●');
23
+ case 'restarting':
24
+ return _theme_1.t.orange('●');
25
+ case 'exited':
26
+ return c.exitCode === 0 ? _theme_1.t.comment('●') : _theme_1.t.red('●');
27
+ case 'dead':
28
+ return _theme_1.t.red('●');
29
+ default:
30
+ return _theme_1.t.comment('●');
31
+ }
32
+ }
33
+ function statusLabel(c) {
34
+ switch (c.status) {
35
+ case 'exited':
36
+ return c.exitCode === 0 ? 'exited(0)' : `exited(${c.exitCode})`;
37
+ default:
38
+ return c.status;
39
+ }
40
+ }
41
+ function colorByStatus(c, text) {
42
+ switch (c.status) {
43
+ case 'running':
44
+ return _theme_1.t.green(text);
45
+ case 'paused':
46
+ return _theme_1.t.yellow(text);
47
+ case 'restarting':
48
+ return _theme_1.t.orange(text);
49
+ case 'exited':
50
+ return c.exitCode === 0 ? _theme_1.t.comment(text) : _theme_1.t.red(text);
51
+ case 'dead':
52
+ return _theme_1.t.red(text);
53
+ default:
54
+ return _theme_1.t.comment(text);
55
+ }
56
+ }
57
+ function cpuColor(cpu) {
58
+ if (cpu >= 80)
59
+ return _theme_1.t.red;
60
+ if (cpu >= 50)
61
+ return _theme_1.t.orange;
62
+ return _theme_1.t.purple;
63
+ }
64
+ function memColor(mem) {
65
+ if (mem >= 80)
66
+ return _theme_1.t.red;
67
+ if (mem >= 60)
68
+ return _theme_1.t.orange;
69
+ return _theme_1.t.cyan;
70
+ }
71
+ function formatCpuCell(c, cpu) {
72
+ if (!isActive(c.status) || cpu === undefined)
73
+ return _theme_1.t.faint('—');
74
+ return cpuColor(cpu)(`${cpu.toFixed(1)}%`);
75
+ }
76
+ function formatMemCell(c, mem) {
77
+ if (!isActive(c.status) || mem === undefined)
78
+ return _theme_1.t.faint('—');
79
+ return memColor(mem)(`${mem.toFixed(1)}%`);
80
+ }
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectColors = detectColors;
4
+ function detectColors(env = process.env) {
5
+ const colorterm = env.COLORTERM;
6
+ if (colorterm === 'truecolor' || colorterm === '24bit')
7
+ return 'truecolor';
8
+ const term = env.TERM ?? '';
9
+ if (term.includes('truecolor') || term.includes('24bit'))
10
+ return 'truecolor';
11
+ if (term.includes('256color') || term === 'xterm-kitty')
12
+ return '256';
13
+ return 'low';
14
+ }
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "dockza.app",
3
+ "version": "0.2.0",
4
+ "description": "A lightweight terminal UI for Docker — manage containers, images, volumes, and networks without leaving your terminal",
5
+ "bin": {
6
+ "dockza": "dist/cli.js"
7
+ },
8
+ "files": [
9
+ "dist",
10
+ "README.md",
11
+ "LICENSE",
12
+ "NOTICE"
13
+ ],
14
+ "engines": {
15
+ "node": ">=20"
16
+ },
17
+ "license": "MIT",
18
+ "author": "Dmytro Shandyba <shandyba@gmail.com>",
19
+ "contributors": [
20
+ "Achraf El Fadili <contact@elfadiliachraf.tech> (original author of docktui, which dockza is forked from)"
21
+ ],
22
+ "homepage": "https://dockza.app",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/shandyba/dockza.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/shandyba/dockza/issues"
29
+ },
30
+ "keywords": [
31
+ "docker",
32
+ "tui",
33
+ "terminal",
34
+ "cli",
35
+ "containers",
36
+ "dockerode",
37
+ "blessed",
38
+ "neo-blessed",
39
+ "compose",
40
+ "networks",
41
+ "devops"
42
+ ],
43
+ "scripts": {
44
+ "prebuild": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
45
+ "build": "tsc && tsc-alias",
46
+ "dev": "ts-node -r tsconfig-paths/register src/cli.ts",
47
+ "start": "node dist/cli.js",
48
+ "test": "vitest run",
49
+ "test:watch": "vitest",
50
+ "test:coverage": "vitest run --coverage",
51
+ "lint": "eslint .",
52
+ "lint:fix": "eslint . --fix",
53
+ "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
54
+ "format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"",
55
+ "prepublishOnly": "npm run lint && npm run format:check && npm run build && npm test"
56
+ },
57
+ "dependencies": {
58
+ "dayjs": "^1.11.13",
59
+ "dockerode": "^5.0.1",
60
+ "neo-blessed": "^0.2.0"
61
+ },
62
+ "devDependencies": {
63
+ "@types/blessed": "^0.1.25",
64
+ "@types/dockerode": "^4.0.1",
65
+ "@types/node": "^20.14.0",
66
+ "@vitest/coverage-v8": "^4.1.6",
67
+ "eslint": "^9.13.0",
68
+ "eslint-config-prettier": "^9.1.0",
69
+ "prettier": "^3.3.3",
70
+ "ts-node": "^10.9.2",
71
+ "tsc-alias": "^1.8.10",
72
+ "tsconfig-paths": "^4.2.0",
73
+ "typescript": "^5.4.5",
74
+ "typescript-eslint": "^8.12.0",
75
+ "vitest": "^4.1.6"
76
+ }
77
+ }