dockscope 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Manuel Tomé
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,168 @@
1
+ # DockScope
2
+
3
+ **Visual, interactive Docker infrastructure debugger.**
4
+
5
+ A browser-based 3D dependency graph of your Docker services with live health status, log streams, metrics, and container actions. Think of it as a mission control dashboard for your Docker Compose stacks.
6
+
7
+ ## Features
8
+
9
+ - **3D Force Graph** — Containers rendered as interactive nodes with dependency and network links
10
+ - **Live Health Status** — Real-time container state with color-coded nodes (healthy, unhealthy, stopped)
11
+ - **Log Streaming** — Live container logs with ANSI color support, timestamp shortening, and search
12
+ - **Container Actions** — Start, stop, restart containers directly from the UI
13
+ - **Compose Project Management** — Up, down, stop, restart entire compose projects
14
+ - **Environment Inspector** — View env vars (with secret masking), labels, mounts, and config
15
+ - **Metrics & Sparklines** — CPU, memory, and network I/O with 5-minute history charts
16
+ - **Node Importance Heuristic** — Nodes sized by exposed ports, connections, dependency depth, CPU, memory, network I/O, and network count
17
+ - **Project Clustering** — Compose projects grouped with translucent enclosure spheres and labels
18
+ - **Health Propagation** — Pulsing warning rings on containers whose dependencies are broken
19
+ - **Search & Filter** — Search by name/image, filter by status (running/stopped/unhealthy)
20
+ - **Keyboard Shortcuts** — `/` search, `F` zoom-to-fit, `R` reset camera, `Esc` close, `?` help
21
+ - **Resizable Panels** — Drag to resize sidebar and status bar
22
+ - **Event Stream** — Live Docker events with health check toggle
23
+ - **System Info** — Docker version, CPU count, total memory
24
+
25
+ ## Quick Start
26
+
27
+ ```bash
28
+ npx dockscope up
29
+ ```
30
+
31
+ Or install globally:
32
+
33
+ ```bash
34
+ npm install -g dockscope
35
+ dockscope up
36
+ ```
37
+
38
+ Requires Docker to be running. Opens a browser at `http://localhost:4681`.
39
+
40
+ ### Options
41
+
42
+ ```
43
+ dockscope up [options]
44
+
45
+ -p, --port <port> Server port (default: 4681)
46
+ -f, --file <path> Docker Compose file path (default: auto-detect)
47
+ --no-open Don't open browser automatically
48
+ ```
49
+
50
+ ### Scan Mode
51
+
52
+ Output the container graph as JSON (no UI):
53
+
54
+ ```bash
55
+ dockscope scan
56
+ ```
57
+
58
+ ## Development
59
+
60
+ ```bash
61
+ git clone https://github.com/ManuelR-T/dockscope.git
62
+ cd dockscope
63
+ npm install
64
+ npm run dev
65
+ ```
66
+
67
+ This starts the backend on port `4681` and the Vite dev server on port `4680` with hot reload.
68
+
69
+ ### Scripts
70
+
71
+ | Command | Description |
72
+ |---------|-------------|
73
+ | `npm run dev` | Start dev server (backend + frontend with HMR) |
74
+ | `npm run build` | Production build |
75
+ | `npm run start` | Run production build |
76
+ | `npm run lint` | ESLint check |
77
+ | `npm run lint:fix` | ESLint auto-fix |
78
+ | `npm run format` | Prettier format all files |
79
+ | `npm run format:check` | Prettier check |
80
+ | `npm run typecheck` | TypeScript type check |
81
+
82
+ ## Tech Stack
83
+
84
+ | Layer | Technology |
85
+ |-------|-----------|
86
+ | **Frontend** | Svelte 5 (runes), Three.js, 3d-force-graph, three-spritetext |
87
+ | **Backend** | Express, WebSocket (ws), dockerode |
88
+ | **Build** | Vite, TypeScript (ESNext) |
89
+ | **CLI** | Commander |
90
+ | **Linting** | ESLint, Prettier, commitlint (conventional commits) |
91
+ | **CI/CD** | GitHub Actions (lint, typecheck, build, auto-release to npm) |
92
+
93
+ ## Architecture
94
+
95
+ ```
96
+ Docker daemon
97
+ |
98
+ +--> dockerode (client.ts, metrics.ts, logs.ts, links.ts)
99
+ | |
100
+ | +--> Express REST API (routes.ts)
101
+ | +--> WebSocket broadcasts (server/index.ts)
102
+ | |
103
+ | +--> Svelte 5 store (docker.svelte.ts)
104
+ | |
105
+ | +--> GraphView.svelte (3d-force-graph + Three.js)
106
+ | +--> Sidebar (Info / Env / Logs tabs)
107
+ | +--> StatusBar (event stream + system info)
108
+ |
109
+ +--> Compose parser (compose.ts)
110
+ |
111
+ +--> depends_on + network link extraction
112
+ ```
113
+
114
+ ### API Endpoints
115
+
116
+ | Method | Path | Description |
117
+ |--------|------|-------------|
118
+ | GET | `/api/graph` | Full graph data (nodes + links) |
119
+ | GET | `/api/containers/:id/stats` | CPU, memory, network I/O |
120
+ | GET | `/api/containers/:id/logs` | Container logs |
121
+ | GET | `/api/containers/:id/inspect` | Env, labels, mounts, config |
122
+ | GET | `/api/containers/:id/history` | Metric history (sparkline data) |
123
+ | POST | `/api/containers/:id/start\|stop\|restart` | Container actions |
124
+ | GET | `/api/projects` | List compose projects |
125
+ | POST | `/api/projects/:name/up\|down\|stop\|start\|restart` | Project actions |
126
+ | GET | `/api/system` | Docker version, CPUs, memory |
127
+ | WS | `/ws` | Real-time graph, stats, events, log streaming |
128
+
129
+ ### Keyboard Shortcuts
130
+
131
+ | Key | Action |
132
+ |-----|--------|
133
+ | `/` or `Ctrl+K` | Focus search |
134
+ | `Escape` | Close panel / clear search |
135
+ | `F` | Zoom to fit |
136
+ | `R` | Reset camera |
137
+ | `?` | Toggle shortcut help |
138
+
139
+ ## Contributing
140
+
141
+ This project uses [Conventional Commits](https://www.conventionalcommits.org/). All commit messages must follow the format:
142
+
143
+ ```
144
+ type(scope): description
145
+
146
+ feat: add new feature
147
+ fix: fix a bug
148
+ docs: update documentation
149
+ refactor: code restructuring
150
+ chore: maintenance tasks
151
+ ```
152
+
153
+ ### Setup
154
+
155
+ 1. Fork the repo on GitHub
156
+ 2. Clone your fork: `git clone https://github.com/<your-username>/dockscope.git`
157
+ 3. `npm install`
158
+ 4. Create a branch: `git checkout -b feat/my-feature`
159
+ 5. `npm run dev` to start development
160
+ 6. Make your changes
161
+ 7. `npm run lint && npm run format:check && npm run build` to verify
162
+ 8. Commit with conventional messages: `git commit -m "feat: add my feature"`
163
+ 9. Push to your fork: `git push origin feat/my-feature`
164
+ 10. Open a PR against `main` on the upstream repo
165
+
166
+ ## License
167
+
168
+ [MIT](LICENSE)
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { startServer } from './server/index.js';
4
+ import { buildGraph, checkConnection } from './docker/client.js';
5
+ const program = new Command();
6
+ program
7
+ .name('dockscope')
8
+ .description('Visual, interactive Docker infrastructure debugger')
9
+ .version('0.1.0');
10
+ program
11
+ .command('up')
12
+ .description('Start the DockScope dashboard')
13
+ .option('-p, --port <port>', 'Server port', '4681')
14
+ .option('-f, --file <path>', 'Docker Compose file path', 'docker-compose.yml')
15
+ .option('--no-open', "Don't open browser automatically")
16
+ .action(async (opts) => {
17
+ const port = parseInt(opts.port, 10);
18
+ console.log(`
19
+ ____ _ ____
20
+ | _ \\ ___ ___| | _/ ___| ___ ___ _ __ ___
21
+ | | | |/ _ \\ / __| |/ \\___ \\ / __/ _ \\| '_ \\ / _ \\
22
+ | |_| | (_) | (__| < ___) | (_| (_) | |_) | __/
23
+ |____/ \\___/ \\___|_|\\_\\____/ \\___\\___/| .__/ \\___|
24
+ |_| v0.1.0
25
+ `);
26
+ await startServer({ port, composeFile: opts.file, open: opts.open !== false });
27
+ const url = `http://localhost:${port}`;
28
+ console.log(` Dashboard: ${url}`);
29
+ console.log(` API: ${url}/api/graph`);
30
+ console.log(` WebSocket: ws://localhost:${port}/ws\n`);
31
+ console.log('');
32
+ console.log(' Press Ctrl+C to stop\n');
33
+ if (opts.open !== false) {
34
+ const open = (await import('open')).default;
35
+ await open(url);
36
+ }
37
+ });
38
+ program
39
+ .command('scan')
40
+ .description('Scan Docker environment and output graph data as JSON')
41
+ .option('-f, --file <path>', 'Docker Compose file path', 'docker-compose.yml')
42
+ .action(async (opts) => {
43
+ const connected = await checkConnection();
44
+ if (!connected) {
45
+ console.error('Cannot connect to Docker daemon. Is Docker running?');
46
+ process.exit(1);
47
+ }
48
+ const graph = await buildGraph(opts.file);
49
+ console.log(JSON.stringify(graph, null, 2));
50
+ });
51
+ program.parse();
@@ -0,0 +1,18 @@
1
+ import type { GraphData, ContainerStats, ContainerInspect, SystemInfo, DockerEvent } from '../types.js';
2
+ export declare function checkConnection(): Promise<boolean>;
3
+ export declare function buildGraph(composeFile?: string): Promise<GraphData>;
4
+ export declare const getContainerStats: (id: string) => Promise<ContainerStats>;
5
+ export declare const getContainerLogs: (id: string, tail?: number) => Promise<string>;
6
+ export declare const streamContainerLogs: (id: string, onData: (t: string) => void, onError?: (e: Error) => void) => () => void;
7
+ /** List all compose projects (live + cached) with their container counts */
8
+ export declare function listComposeProjects(): Promise<{
9
+ name: string;
10
+ running: number;
11
+ stopped: number;
12
+ }[]>;
13
+ /** Run a docker compose action on a specific project */
14
+ export declare function composeAction(project: string, action: 'up' | 'down' | 'stop' | 'start' | 'restart'): Promise<string>;
15
+ export declare function containerAction(containerId: string, action: 'start' | 'stop' | 'restart'): Promise<void>;
16
+ export declare function inspectContainer(containerId: string): Promise<ContainerInspect>;
17
+ export declare function getSystemInfo(): Promise<SystemInfo>;
18
+ export declare function watchEvents(callback: (event: DockerEvent) => void, onError?: (err: Error) => void): () => void;
@@ -0,0 +1,250 @@
1
+ import Dockerode from 'dockerode';
2
+ import { exec } from 'child_process';
3
+ import { promisify } from 'util';
4
+ import { getContainerStats as _getStats } from './metrics.js';
5
+ import { getContainerLogs as _getLogs, streamContainerLogs as _streamLogs } from './logs.js';
6
+ import { extractDependsOnFromLabels, extractDependsOnFromFile, extractNetworkLinks, } from './links.js';
7
+ const docker = new Dockerode();
8
+ export async function checkConnection() {
9
+ try {
10
+ await docker.ping();
11
+ return true;
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }
17
+ export async function buildGraph(composeFile) {
18
+ const containers = await docker.listContainers({ all: true });
19
+ const nodes = [];
20
+ const networkMap = new Map();
21
+ const containerProject = new Map();
22
+ for (const container of containers) {
23
+ const composeService = container.Labels['com.docker.compose.service'];
24
+ const project = container.Labels['com.docker.compose.project'] || '';
25
+ const rawName = composeService || container.Names[0]?.replace(/^\//, '') || container.Id.substring(0, 12);
26
+ const hasDuplicate = containers.some((c) => c.Id !== container.Id &&
27
+ (c.Labels['com.docker.compose.service'] || c.Names[0]?.replace(/^\//, '')) === rawName &&
28
+ (c.Labels['com.docker.compose.project'] || '') !== project);
29
+ const serviceName = hasDuplicate && project ? `${project}/${rawName}` : rawName;
30
+ const shortId = container.Id.substring(0, 12);
31
+ containerProject.set(shortId, project);
32
+ const healthStatus = container.Status?.toLowerCase() || '';
33
+ let health = 'none';
34
+ if (healthStatus.includes('healthy') && !healthStatus.includes('unhealthy'))
35
+ health = 'healthy';
36
+ else if (healthStatus.includes('unhealthy'))
37
+ health = 'unhealthy';
38
+ else if (healthStatus.includes('starting') || healthStatus.includes('health:'))
39
+ health = 'starting';
40
+ nodes.push({
41
+ id: shortId,
42
+ name: serviceName,
43
+ project,
44
+ containerId: container.Id,
45
+ image: container.Image,
46
+ status: container.State,
47
+ health,
48
+ ports: [
49
+ ...new Set(container.Ports.map((p) => `${p.PublicPort ? p.PublicPort + ':' : ''}${p.PrivatePort}/${p.Type}`)),
50
+ ],
51
+ networks: Object.keys(container.NetworkSettings?.Networks || {}),
52
+ cpu: 0,
53
+ memory: 0,
54
+ memoryLimit: 0,
55
+ networkRx: 0,
56
+ networkTx: 0,
57
+ networkRxRate: 0,
58
+ networkTxRate: 0,
59
+ });
60
+ for (const net of Object.keys(container.NetworkSettings?.Networks || {})) {
61
+ if (!networkMap.has(net))
62
+ networkMap.set(net, []);
63
+ networkMap.get(net).push(shortId);
64
+ }
65
+ }
66
+ // Build links from all sources
67
+ const { links: labelLinks, seen } = extractDependsOnFromLabels(containers, nodes, containerProject);
68
+ const fileLinks = await extractDependsOnFromFile(composeFile, nodes, containerProject, seen);
69
+ const netLinks = extractNetworkLinks(networkMap);
70
+ return { nodes, links: [...labelLinks, ...fileLinks, ...netLinks] };
71
+ }
72
+ // --- Delegate to extracted modules ---
73
+ export const getContainerStats = (id) => _getStats(docker, id);
74
+ export const getContainerLogs = (id, tail) => _getLogs(docker, id, tail);
75
+ export const streamContainerLogs = (id, onData, onError) => _streamLogs(docker, id, onData, onError);
76
+ const execAsync = promisify(exec);
77
+ const projectCache = new Map();
78
+ /** Cache project metadata from container labels */
79
+ function cacheProjectMeta(containers) {
80
+ for (const c of containers) {
81
+ const project = c.Labels['com.docker.compose.project'];
82
+ const workDir = c.Labels['com.docker.compose.project.working_dir'];
83
+ const configFiles = c.Labels['com.docker.compose.project.config_files'];
84
+ if (project && workDir && configFiles && !projectCache.has(project)) {
85
+ projectCache.set(project, { workDir, configFiles });
86
+ }
87
+ }
88
+ }
89
+ /** List all compose projects (live + cached) with their container counts */
90
+ export async function listComposeProjects() {
91
+ const containers = await docker.listContainers({ all: true });
92
+ cacheProjectMeta(containers);
93
+ const projects = new Map();
94
+ // Live containers
95
+ for (const c of containers) {
96
+ const project = c.Labels['com.docker.compose.project'];
97
+ if (!project)
98
+ continue;
99
+ if (!projects.has(project))
100
+ projects.set(project, { running: 0, stopped: 0 });
101
+ const p = projects.get(project);
102
+ if (c.State === 'running')
103
+ p.running++;
104
+ else
105
+ p.stopped++;
106
+ }
107
+ // Cached projects with no live containers (after down)
108
+ for (const [name] of projectCache) {
109
+ if (!projects.has(name)) {
110
+ projects.set(name, { running: 0, stopped: 0 });
111
+ }
112
+ }
113
+ return [...projects.entries()]
114
+ .map(([name, counts]) => ({ name, ...counts }))
115
+ .sort((a, b) => a.name.localeCompare(b.name));
116
+ }
117
+ /** Get containers for a compose project */
118
+ async function getProjectContainers(project) {
119
+ return docker.listContainers({
120
+ all: true,
121
+ filters: { label: [`com.docker.compose.project=${project}`] },
122
+ });
123
+ }
124
+ /** Build the docker compose command from container labels or cache */
125
+ function getComposeCommand(project, containers) {
126
+ // Try live container labels first
127
+ let workDir = containers[0]?.Labels['com.docker.compose.project.working_dir'];
128
+ let configFiles = containers[0]?.Labels['com.docker.compose.project.config_files'];
129
+ // Fall back to cache (for projects that were downed)
130
+ if (!workDir || !configFiles) {
131
+ const cached = projectCache.get(project);
132
+ if (cached) {
133
+ workDir = cached.workDir;
134
+ configFiles = cached.configFiles;
135
+ }
136
+ }
137
+ if (!workDir || !configFiles)
138
+ return null;
139
+ const fileFlags = configFiles
140
+ .split(',')
141
+ .map((f) => `-f "${f.trim()}"`)
142
+ .join(' ');
143
+ return { cmd: `docker compose ${fileFlags}`, cwd: workDir };
144
+ }
145
+ /** Run a docker compose action on a specific project */
146
+ export async function composeAction(project, action) {
147
+ const containers = await getProjectContainers(project);
148
+ if (action === 'up' || action === 'down') {
149
+ const compose = getComposeCommand(project, containers);
150
+ if (compose) {
151
+ const subCmd = action === 'up' ? 'up -d' : 'down';
152
+ const { stdout, stderr } = await execAsync(`${compose.cmd} ${subCmd}`, { cwd: compose.cwd });
153
+ return stdout || stderr || `${action} completed`;
154
+ }
155
+ if (action === 'up') {
156
+ for (const c of containers) {
157
+ if (c.State !== 'running')
158
+ await docker.getContainer(c.Id).start();
159
+ }
160
+ return `Started containers in project ${project}`;
161
+ }
162
+ return 'Could not find compose config for down';
163
+ }
164
+ // stop / start / restart — act on individual containers
165
+ for (const c of containers) {
166
+ const container = docker.getContainer(c.Id);
167
+ if (action === 'stop' && c.State === 'running')
168
+ await container.stop();
169
+ else if (action === 'start' && c.State !== 'running')
170
+ await container.start();
171
+ else if (action === 'restart' && c.State === 'running')
172
+ await container.restart();
173
+ }
174
+ return `${action} completed for project ${project}`;
175
+ }
176
+ export async function containerAction(containerId, action) {
177
+ const container = docker.getContainer(containerId);
178
+ await container[action]();
179
+ }
180
+ export async function inspectContainer(containerId) {
181
+ const container = docker.getContainer(containerId);
182
+ const info = await container.inspect();
183
+ return {
184
+ id: info.Id.substring(0, 12),
185
+ env: info.Config.Env || [],
186
+ labels: info.Config.Labels || {},
187
+ mounts: (info.Mounts || []).map((m) => ({
188
+ type: m.Type || 'bind',
189
+ source: m.Source || '',
190
+ destination: m.Destination || '',
191
+ mode: m.Mode || 'rw',
192
+ })),
193
+ restartPolicy: info.HostConfig.RestartPolicy?.Name || 'no',
194
+ entrypoint: info.Config.Entrypoint || null,
195
+ cmd: info.Config.Cmd || null,
196
+ workingDir: info.Config.WorkingDir || '/',
197
+ created: info.Created,
198
+ };
199
+ }
200
+ export async function getSystemInfo() {
201
+ const info = await docker.info();
202
+ return {
203
+ dockerVersion: info.ServerVersion || 'unknown',
204
+ os: `${info.OperatingSystem || 'unknown'} (${info.Architecture || ''})`,
205
+ totalMemory: info.MemTotal || 0,
206
+ cpus: info.NCPU || 0,
207
+ containersRunning: info.ContainersRunning || 0,
208
+ containersStopped: info.ContainersStopped || 0,
209
+ images: info.Images || 0,
210
+ };
211
+ }
212
+ export function watchEvents(callback, onError) {
213
+ let destroyed = false;
214
+ let stream = null;
215
+ docker.getEvents({}, (err, eventStream) => {
216
+ if (err || !eventStream) {
217
+ onError?.(err || new Error('Failed to get event stream'));
218
+ return;
219
+ }
220
+ if (destroyed) {
221
+ eventStream.destroy?.();
222
+ return;
223
+ }
224
+ stream = eventStream;
225
+ eventStream.on('data', (chunk) => {
226
+ try {
227
+ const raw = JSON.parse(chunk.toString());
228
+ callback({
229
+ id: (raw.Actor?.ID || raw.id || '').substring(0, 12),
230
+ type: raw.Type || 'unknown',
231
+ action: raw.Action || raw.status || 'unknown',
232
+ actor: raw.Actor?.Attributes?.name ||
233
+ raw.Actor?.Attributes?.['com.docker.compose.service'] ||
234
+ raw.Actor?.ID?.substring(0, 12) ||
235
+ 'unknown',
236
+ time: raw.time || Math.floor(Date.now() / 1000),
237
+ message: `${raw.Type || ''} ${raw.Action || ''}: ${raw.Actor?.Attributes?.name || raw.Actor?.ID?.substring(0, 12) || ''}`,
238
+ });
239
+ }
240
+ catch {
241
+ /* ignore */
242
+ }
243
+ });
244
+ eventStream.on('error', (e) => onError?.(e));
245
+ });
246
+ return () => {
247
+ destroyed = true;
248
+ stream?.destroy?.();
249
+ };
250
+ }
@@ -0,0 +1,25 @@
1
+ export interface ComposeService {
2
+ name: string;
3
+ image: string;
4
+ ports: string[];
5
+ networks: string[];
6
+ dependsOn: string[];
7
+ volumes: string[];
8
+ environment: Record<string, string>;
9
+ labels: Record<string, string>;
10
+ healthcheck: {
11
+ test: string;
12
+ interval?: string;
13
+ timeout?: string;
14
+ retries?: number;
15
+ } | null;
16
+ resourceLimits: {
17
+ cpus?: string;
18
+ memory?: string;
19
+ } | null;
20
+ }
21
+ export interface ComposeData {
22
+ services: ComposeService[];
23
+ networks: string[];
24
+ }
25
+ export declare function parseComposeFile(filePath: string): Promise<ComposeData>;
@@ -0,0 +1,111 @@
1
+ import { readFile } from 'fs/promises';
2
+ import { parse } from 'yaml';
3
+ export async function parseComposeFile(filePath) {
4
+ const content = await readFile(filePath, 'utf-8');
5
+ const compose = parse(content);
6
+ if (!compose?.services) {
7
+ return { services: [], networks: [] };
8
+ }
9
+ const services = [];
10
+ for (const [name, svc] of Object.entries(compose.services)) {
11
+ const dependsOn = parseDependsOn(svc.depends_on);
12
+ services.push({
13
+ name,
14
+ image: svc.image || `${name}:latest`,
15
+ ports: Array.isArray(svc.ports) ? svc.ports.map(String) : [],
16
+ networks: parseNetworks(svc.networks),
17
+ dependsOn,
18
+ volumes: Array.isArray(svc.volumes) ? svc.volumes.map(String) : [],
19
+ environment: parseEnvironment(svc.environment),
20
+ labels: parseLabels(svc.labels),
21
+ healthcheck: parseHealthcheck(svc.healthcheck),
22
+ resourceLimits: parseResourceLimits(svc.deploy),
23
+ });
24
+ }
25
+ const topLevelNetworks = compose.networks ? Object.keys(compose.networks) : [];
26
+ return { services, networks: topLevelNetworks };
27
+ }
28
+ function parseDependsOn(dep) {
29
+ if (!dep)
30
+ return [];
31
+ // Simple form: depends_on: [db, redis]
32
+ if (Array.isArray(dep))
33
+ return dep.map(String);
34
+ // Extended form: depends_on: { db: { condition: service_healthy } }
35
+ if (typeof dep === 'object')
36
+ return Object.keys(dep);
37
+ return [];
38
+ }
39
+ function parseNetworks(nets) {
40
+ if (!nets)
41
+ return [];
42
+ if (Array.isArray(nets))
43
+ return nets.map(String);
44
+ if (typeof nets === 'object')
45
+ return Object.keys(nets);
46
+ return [];
47
+ }
48
+ function parseEnvironment(env) {
49
+ if (!env)
50
+ return {};
51
+ if (Array.isArray(env)) {
52
+ const result = {};
53
+ for (const item of env) {
54
+ const s = String(item);
55
+ const eqIdx = s.indexOf('=');
56
+ if (eqIdx > 0)
57
+ result[s.substring(0, eqIdx)] = s.substring(eqIdx + 1);
58
+ else
59
+ result[s] = '';
60
+ }
61
+ return result;
62
+ }
63
+ if (typeof env === 'object') {
64
+ return Object.fromEntries(Object.entries(env).map(([k, v]) => [k, String(v ?? '')]));
65
+ }
66
+ return {};
67
+ }
68
+ function parseLabels(labels) {
69
+ if (!labels)
70
+ return {};
71
+ if (Array.isArray(labels)) {
72
+ const result = {};
73
+ for (const item of labels) {
74
+ const s = String(item);
75
+ const eqIdx = s.indexOf('=');
76
+ if (eqIdx > 0)
77
+ result[s.substring(0, eqIdx)] = s.substring(eqIdx + 1);
78
+ }
79
+ return result;
80
+ }
81
+ if (typeof labels === 'object') {
82
+ return Object.fromEntries(Object.entries(labels).map(([k, v]) => [k, String(v ?? '')]));
83
+ }
84
+ return {};
85
+ }
86
+ function parseHealthcheck(hc) {
87
+ if (!hc || typeof hc !== 'object')
88
+ return null;
89
+ const h = hc;
90
+ const test = Array.isArray(h.test) ? h.test.join(' ') : String(h.test || '');
91
+ if (!test)
92
+ return null;
93
+ return {
94
+ test,
95
+ interval: h.interval,
96
+ timeout: h.timeout,
97
+ retries: h.retries,
98
+ };
99
+ }
100
+ function parseResourceLimits(deploy) {
101
+ if (!deploy || typeof deploy !== 'object')
102
+ return null;
103
+ const d = deploy;
104
+ const limits = d.resources?.limits;
105
+ if (!limits)
106
+ return null;
107
+ return {
108
+ cpus: limits.cpus ? String(limits.cpus) : undefined,
109
+ memory: limits.memory ? String(limits.memory) : undefined,
110
+ };
111
+ }
@@ -0,0 +1,10 @@
1
+ import type { ServiceNode, ServiceLink } from '../types.js';
2
+ /** Extract depends_on links from container labels (runtime) */
3
+ export declare function extractDependsOnFromLabels(containers: any[], nodes: ServiceNode[], containerProject: Map<string, string>): {
4
+ links: ServiceLink[];
5
+ seen: Set<string>;
6
+ };
7
+ /** Extract depends_on links from compose file */
8
+ export declare function extractDependsOnFromFile(composeFile: string | undefined, nodes: ServiceNode[], containerProject: Map<string, string>, seen: Set<string>): Promise<ServiceLink[]>;
9
+ /** Extract network-based links (containers sharing non-default networks) */
10
+ export declare function extractNetworkLinks(networkMap: Map<string, string[]>): ServiceLink[];