kctl-api 0.2.0__py3-none-any.whl

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.
Files changed (66) hide show
  1. kctl_api/__init__.py +3 -0
  2. kctl_api/__main__.py +5 -0
  3. kctl_api/cli.py +238 -0
  4. kctl_api/commands/__init__.py +1 -0
  5. kctl_api/commands/ai.py +250 -0
  6. kctl_api/commands/aliases.py +84 -0
  7. kctl_api/commands/apps.py +172 -0
  8. kctl_api/commands/auth.py +313 -0
  9. kctl_api/commands/automation.py +242 -0
  10. kctl_api/commands/build_cmd.py +87 -0
  11. kctl_api/commands/clean.py +182 -0
  12. kctl_api/commands/config_cmd.py +443 -0
  13. kctl_api/commands/dashboard.py +139 -0
  14. kctl_api/commands/db.py +599 -0
  15. kctl_api/commands/deploy.py +84 -0
  16. kctl_api/commands/deps.py +289 -0
  17. kctl_api/commands/dev.py +136 -0
  18. kctl_api/commands/docker_cmd.py +252 -0
  19. kctl_api/commands/doctor_cmd.py +286 -0
  20. kctl_api/commands/env.py +289 -0
  21. kctl_api/commands/files.py +250 -0
  22. kctl_api/commands/fmt_cmd.py +58 -0
  23. kctl_api/commands/health.py +479 -0
  24. kctl_api/commands/jobs.py +169 -0
  25. kctl_api/commands/lint_cmd.py +81 -0
  26. kctl_api/commands/logs.py +258 -0
  27. kctl_api/commands/marketplace.py +316 -0
  28. kctl_api/commands/monitor_cmd.py +243 -0
  29. kctl_api/commands/notifications.py +132 -0
  30. kctl_api/commands/odoo_proxy.py +182 -0
  31. kctl_api/commands/openapi.py +299 -0
  32. kctl_api/commands/perf.py +307 -0
  33. kctl_api/commands/rate_limit.py +223 -0
  34. kctl_api/commands/realtime.py +100 -0
  35. kctl_api/commands/redis_cmd.py +609 -0
  36. kctl_api/commands/routes_cmd.py +277 -0
  37. kctl_api/commands/saas.py +145 -0
  38. kctl_api/commands/scaffold.py +362 -0
  39. kctl_api/commands/security_cmd.py +350 -0
  40. kctl_api/commands/services.py +191 -0
  41. kctl_api/commands/shell.py +197 -0
  42. kctl_api/commands/skill_cmd.py +58 -0
  43. kctl_api/commands/streams.py +309 -0
  44. kctl_api/commands/stripe_cmd.py +105 -0
  45. kctl_api/commands/tenant_ai.py +169 -0
  46. kctl_api/commands/test_cmd.py +95 -0
  47. kctl_api/commands/users.py +302 -0
  48. kctl_api/commands/webhooks.py +56 -0
  49. kctl_api/commands/workflows.py +127 -0
  50. kctl_api/commands/ws.py +323 -0
  51. kctl_api/core/__init__.py +1 -0
  52. kctl_api/core/async_client.py +120 -0
  53. kctl_api/core/callbacks.py +88 -0
  54. kctl_api/core/client.py +190 -0
  55. kctl_api/core/config.py +260 -0
  56. kctl_api/core/db.py +65 -0
  57. kctl_api/core/exceptions.py +43 -0
  58. kctl_api/core/output.py +5 -0
  59. kctl_api/core/plugins.py +26 -0
  60. kctl_api/core/redis.py +35 -0
  61. kctl_api/core/resolve.py +47 -0
  62. kctl_api/core/utils.py +109 -0
  63. kctl_api-0.2.0.dist-info/METADATA +34 -0
  64. kctl_api-0.2.0.dist-info/RECORD +66 -0
  65. kctl_api-0.2.0.dist-info/WHEEL +4 -0
  66. kctl_api-0.2.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,289 @@
1
+ """Dependency management commands for kctl-api.
2
+
3
+ Audit, outdated, licenses, graph, and size analysis.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import subprocess
9
+ from typing import Annotated
10
+
11
+ import typer
12
+
13
+ from kctl_api.core.callbacks import AppContext
14
+ from kctl_api.core.utils import find_project_root
15
+
16
+ app = typer.Typer(
17
+ name="deps", help="Dependency management — audit, outdated, licenses, graph, size.", no_args_is_help=True
18
+ )
19
+
20
+
21
+ def _run_uv(args: list[str], cwd: str | None = None) -> subprocess.CompletedProcess:
22
+ return subprocess.run(["uv", *args], capture_output=True, text=True, cwd=cwd)
23
+
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # audit
27
+ # ---------------------------------------------------------------------------
28
+ @app.command()
29
+ def audit(
30
+ ctx: typer.Context,
31
+ fix: Annotated[bool, typer.Option("--fix", help="Attempt to upgrade vulnerable packages.")] = False,
32
+ ) -> None:
33
+ """Run security audit via uv or pip-audit."""
34
+ actx: AppContext = ctx.obj
35
+ out = actx.output
36
+
37
+ root = find_project_root()
38
+ out.info("Running dependency security audit ...")
39
+
40
+ # Try pip-audit first (more detailed), fall back to uv
41
+ import shutil
42
+
43
+ if shutil.which("pip-audit"):
44
+ cmd = ["pip-audit", "--format", "json"]
45
+ result = subprocess.run(cmd, capture_output=True, text=True, cwd=str(root))
46
+
47
+ if result.returncode == 0:
48
+ import json
49
+
50
+ try:
51
+ data = json.loads(result.stdout)
52
+ vulns = data.get("vulnerabilities", [])
53
+ if not vulns:
54
+ out.success("No vulnerabilities found (pip-audit).")
55
+ return
56
+
57
+ rows = [
58
+ [v.get("name", ""), v.get("version", ""), v.get("id", ""), v.get("fix_versions", [""])[0]]
59
+ for v in vulns
60
+ ]
61
+ out.table(
62
+ title=f"Vulnerabilities ({len(vulns)})",
63
+ columns=[("Package", "bold"), ("Version", ""), ("ID", "red"), ("Fix Version", "green")],
64
+ rows=rows,
65
+ data_for_json=vulns,
66
+ )
67
+ raise typer.Exit(1)
68
+ except Exception:
69
+ pass
70
+
71
+ out.text(result.stdout)
72
+ if result.returncode != 0:
73
+ out.text(result.stderr)
74
+ else:
75
+ # Fall back to uv audit (uv >= 0.4)
76
+ result = _run_uv(["audit"], cwd=str(root))
77
+ if result.returncode == 0 and not result.stdout.strip():
78
+ out.success("No vulnerabilities found (uv audit).")
79
+ else:
80
+ out.text(result.stdout)
81
+ if result.stderr:
82
+ out.text(result.stderr)
83
+ out.info("For detailed audit: uv add --dev pip-audit && pip-audit")
84
+
85
+
86
+ # ---------------------------------------------------------------------------
87
+ # outdated
88
+ # ---------------------------------------------------------------------------
89
+ @app.command()
90
+ def outdated(ctx: typer.Context) -> None:
91
+ """Show outdated packages across all workspace members."""
92
+ actx: AppContext = ctx.obj
93
+ out = actx.output
94
+
95
+ root = find_project_root()
96
+ out.info("Checking for outdated packages ...")
97
+
98
+ result = _run_uv(["pip", "list", "--outdated", "--format", "json"], cwd=str(root))
99
+
100
+ if result.returncode != 0:
101
+ # Try without --format json
102
+ result = _run_uv(["pip", "list", "--outdated"], cwd=str(root))
103
+ out.text(result.stdout or "(no output)")
104
+ return
105
+
106
+ import json
107
+
108
+ try:
109
+ packages = json.loads(result.stdout)
110
+ except Exception:
111
+ out.text(result.stdout or "(no output)")
112
+ return
113
+
114
+ if not packages:
115
+ out.success("All packages are up to date.")
116
+ return
117
+
118
+ rows = [[p.get("name", ""), p.get("version", ""), p.get("latest_version", "")] for p in packages]
119
+ out.table(
120
+ title=f"Outdated Packages ({len(packages)})",
121
+ columns=[("Package", "bold"), ("Current", "yellow"), ("Latest", "green")],
122
+ rows=rows,
123
+ data_for_json=packages,
124
+ )
125
+ out.info("Upgrade: uv sync --upgrade")
126
+
127
+
128
+ # ---------------------------------------------------------------------------
129
+ # licenses
130
+ # ---------------------------------------------------------------------------
131
+ @app.command()
132
+ def licenses(
133
+ ctx: typer.Context,
134
+ allow: Annotated[str | None, typer.Option("--allow", help="Comma-separated allowed license patterns.")] = None,
135
+ ) -> None:
136
+ """Generate a license report for all installed packages."""
137
+ actx: AppContext = ctx.obj
138
+ out = actx.output
139
+
140
+ root = find_project_root()
141
+
142
+ # Try pip-licenses
143
+ import shutil
144
+
145
+ if shutil.which("pip-licenses"):
146
+ result = subprocess.run(["pip-licenses", "--format", "json"], capture_output=True, text=True, cwd=str(root))
147
+ if result.returncode == 0:
148
+ import json
149
+
150
+ try:
151
+ data = json.loads(result.stdout)
152
+ allowed_list = [a.strip().upper() for a in (allow or "").split(",")] if allow else []
153
+
154
+ flagged = []
155
+ for pkg in data:
156
+ lic = pkg.get("License", "UNKNOWN").upper()
157
+ if allowed_list and not any(a in lic for a in allowed_list):
158
+ flagged.append(pkg)
159
+
160
+ display = flagged if (allowed_list and flagged) else data
161
+ rows = [[p.get("Name", ""), p.get("Version", ""), p.get("License", "")] for p in display[:100]]
162
+ out.table(
163
+ title=f"Package Licenses ({len(display)} of {len(data)})",
164
+ columns=[("Package", "bold"), ("Version", ""), ("License", "")],
165
+ rows=rows,
166
+ data_for_json=display,
167
+ )
168
+ if allowed_list and flagged:
169
+ out.warn(f"{len(flagged)} packages with non-allowed licenses.")
170
+ raise typer.Exit(1)
171
+ return
172
+ except Exception:
173
+ pass
174
+
175
+ # Fall back: parse from uv pip list
176
+ out.info("pip-licenses not installed. Install: uv add --dev pip-licenses")
177
+ result = _run_uv(["pip", "show", "--format", "columns"], cwd=str(root))
178
+ if result.stdout:
179
+ out.text(result.stdout[:3000])
180
+
181
+
182
+ # ---------------------------------------------------------------------------
183
+ # graph
184
+ # ---------------------------------------------------------------------------
185
+ @app.command()
186
+ def graph(ctx: typer.Context) -> None:
187
+ """Show the dependency graph via uv tree."""
188
+ actx: AppContext = ctx.obj
189
+ out = actx.output
190
+
191
+ root = find_project_root()
192
+ out.info("Generating dependency graph ...")
193
+
194
+ result = _run_uv(["tree", "--all-packages"], cwd=str(root))
195
+ if result.returncode != 0:
196
+ # Try without --all-packages
197
+ result = _run_uv(["tree"], cwd=str(root))
198
+
199
+ if result.stdout:
200
+ typer.echo(result.stdout)
201
+ if result.stderr and result.returncode != 0:
202
+ out.error(result.stderr.strip())
203
+ raise typer.Exit(result.returncode)
204
+
205
+
206
+ # ---------------------------------------------------------------------------
207
+ # size
208
+ # ---------------------------------------------------------------------------
209
+ @app.command()
210
+ def size(
211
+ ctx: typer.Context,
212
+ top: Annotated[int, typer.Option("--top", "-n", help="Show top N packages by size.")] = 20,
213
+ ) -> None:
214
+ """Show installed package sizes."""
215
+ actx: AppContext = ctx.obj
216
+ out = actx.output
217
+
218
+ import os
219
+
220
+ out.info(f"Analyzing package sizes (top {top}) ...")
221
+
222
+ # Get site-packages directory
223
+ try:
224
+ import site
225
+
226
+ site_dirs = site.getsitepackages()
227
+ except Exception:
228
+ site_dirs = []
229
+
230
+ result = _run_uv(["pip", "list", "--format", "json"])
231
+ packages: list[dict] = []
232
+ if result.returncode == 0:
233
+ import json
234
+
235
+ try:
236
+ pkg_list = json.loads(result.stdout)
237
+ except Exception:
238
+ pkg_list = []
239
+
240
+ for site_dir in site_dirs:
241
+ if not os.path.isdir(site_dir):
242
+ continue
243
+ for pkg_info in pkg_list:
244
+ pkg_name = pkg_info.get("name", "")
245
+ # Estimate size from dist-info directory
246
+ dist_name = pkg_name.replace("-", "_")
247
+ size_bytes = 0
248
+ for entry in os.scandir(site_dir):
249
+ if entry.name.lower().startswith(dist_name.lower()):
250
+ try:
251
+ for root_dir, _dirs, files in os.walk(entry.path):
252
+ size_bytes += sum(
253
+ os.path.getsize(os.path.join(root_dir, f))
254
+ for f in files
255
+ if not os.path.islink(os.path.join(root_dir, f))
256
+ )
257
+ except Exception:
258
+ pass
259
+ if size_bytes > 0:
260
+ packages.append(
261
+ {
262
+ "name": pkg_name,
263
+ "version": pkg_info.get("version", ""),
264
+ "size_bytes": size_bytes,
265
+ "size_kb": round(size_bytes / 1024),
266
+ }
267
+ )
268
+ break # Only use first site dir
269
+
270
+ if not packages:
271
+ out.warn(
272
+ "Could not determine package sizes. "
273
+ "Try: du -sh $(python -c 'import site; print(site.getsitepackages()[0])')"
274
+ )
275
+ return
276
+
277
+ packages.sort(key=lambda x: -x["size_bytes"])
278
+ display = packages[:top]
279
+
280
+ rows = [[p["name"], p["version"], f"{p['size_kb']} KB"] for p in display]
281
+ out.table(
282
+ title=f"Top {top} Packages by Size",
283
+ columns=[("Package", "bold"), ("Version", ""), ("Size", "")],
284
+ rows=rows,
285
+ data_for_json=display,
286
+ )
287
+
288
+ total_mb = round(sum(p["size_bytes"] for p in packages) / 1024 / 1024, 1)
289
+ out.info(f"Total installed: ~{total_mb} MB ({len(packages)} packages)")
@@ -0,0 +1,136 @@
1
+ """Development server commands for kctl-api.
2
+
3
+ Start, stop, rebuild, and manage the dev environment.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import subprocess
9
+ from typing import Annotated
10
+
11
+ import typer
12
+
13
+ from kctl_api.core.callbacks import AppContext
14
+ from kctl_api.core.utils import find_project_root
15
+
16
+ app = typer.Typer(name="dev", help="Development environment — up, down, rebuild, logs.", no_args_is_help=True)
17
+
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # up
21
+ # ---------------------------------------------------------------------------
22
+ @app.command()
23
+ def up(
24
+ ctx: typer.Context,
25
+ build: Annotated[bool, typer.Option("--build", help="Rebuild images before starting.")] = False,
26
+ odoo_mm: Annotated[bool, typer.Option("--odoo-mm", help="Include Odoo-MM.")] = False,
27
+ plane_mm: Annotated[bool, typer.Option("--plane-mm", help="Include Plane-MM.")] = False,
28
+ all_services: Annotated[bool, typer.Option("--all", help="Include all optional services.")] = False,
29
+ ) -> None:
30
+ """Start the development stack via scripts/dev."""
31
+ actx: AppContext = ctx.obj
32
+ out = actx.output
33
+
34
+ root = find_project_root()
35
+ cmd = [str(root / "scripts" / "dev")]
36
+ if build:
37
+ cmd.append("--build")
38
+ if odoo_mm:
39
+ cmd.append("--odoo-mm")
40
+ if plane_mm:
41
+ cmd.append("--plane-mm")
42
+ if all_services:
43
+ cmd.append("--all")
44
+
45
+ out.info("Starting dev environment ...")
46
+ result = subprocess.run(cmd, cwd=str(root), capture_output=False)
47
+ if result.returncode != 0:
48
+ out.error("Failed to start dev environment.")
49
+ raise typer.Exit(result.returncode)
50
+
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # down
54
+ # ---------------------------------------------------------------------------
55
+ @app.command()
56
+ def down(ctx: typer.Context) -> None:
57
+ """Stop the development stack."""
58
+ actx: AppContext = ctx.obj
59
+ out = actx.output
60
+
61
+ root = find_project_root()
62
+ cmd = ["docker", "compose", "-f", str(root / "docker-compose.yml"), "down"]
63
+
64
+ out.info("Stopping dev environment ...")
65
+ result = subprocess.run(cmd, capture_output=False)
66
+ if result.returncode != 0:
67
+ out.error("Failed to stop dev environment.")
68
+ raise typer.Exit(result.returncode)
69
+ out.success("Dev environment stopped.")
70
+
71
+
72
+ # ---------------------------------------------------------------------------
73
+ # rebuild
74
+ # ---------------------------------------------------------------------------
75
+ @app.command()
76
+ def rebuild(
77
+ ctx: typer.Context,
78
+ service: Annotated[str | None, typer.Argument(help="Service name (all if omitted).")] = None,
79
+ ) -> None:
80
+ """Rebuild and restart docker compose services."""
81
+ actx: AppContext = ctx.obj
82
+ out = actx.output
83
+
84
+ root = find_project_root()
85
+ cmd = ["docker", "compose", "-f", str(root / "docker-compose.yml"), "up", "--build", "-d"]
86
+ if service:
87
+ cmd.append(service)
88
+
89
+ out.info(f"Rebuilding {service if service else 'all services'} ...")
90
+ result = subprocess.run(cmd, capture_output=False)
91
+ if result.returncode != 0:
92
+ out.error("Rebuild failed.")
93
+ raise typer.Exit(result.returncode)
94
+ out.success("Rebuild complete.")
95
+
96
+
97
+ # ---------------------------------------------------------------------------
98
+ # logs
99
+ # ---------------------------------------------------------------------------
100
+ @app.command()
101
+ def logs(
102
+ ctx: typer.Context,
103
+ service: Annotated[str | None, typer.Argument(help="Service name (all if omitted).")] = None,
104
+ follow: Annotated[bool, typer.Option("--follow", "-f", help="Follow log output.")] = False,
105
+ tail: Annotated[int, typer.Option("--tail", "-n", help="Number of lines.")] = 100,
106
+ ) -> None:
107
+ """View docker compose logs."""
108
+ root = find_project_root()
109
+ cmd = ["docker", "compose", "-f", str(root / "docker-compose.yml"), "logs", "--tail", str(tail)]
110
+ if follow:
111
+ cmd.append("--follow")
112
+ if service:
113
+ cmd.append(service)
114
+ subprocess.run(cmd, capture_output=False)
115
+
116
+
117
+ # ---------------------------------------------------------------------------
118
+ # attach
119
+ # ---------------------------------------------------------------------------
120
+ @app.command()
121
+ def attach(
122
+ ctx: typer.Context,
123
+ service: Annotated[str, typer.Argument(help="Service name to attach to.")],
124
+ ) -> None:
125
+ """Attach to a running container's shell."""
126
+ actx: AppContext = ctx.obj
127
+ out = actx.output
128
+
129
+ root = find_project_root()
130
+ cmd = ["docker", "compose", "-f", str(root / "docker-compose.yml"), "exec", service, "/bin/sh"]
131
+
132
+ out.info(f"Attaching to {service} ...")
133
+ result = subprocess.run(cmd, capture_output=False)
134
+ if result.returncode != 0:
135
+ out.error(f"Failed to attach to {service}.")
136
+ raise typer.Exit(result.returncode)
@@ -0,0 +1,252 @@
1
+ """Docker management commands for kctl-api.
2
+
3
+ Image management, container inspection, network, volumes, and system info.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import subprocess
9
+ from typing import Annotated
10
+
11
+ import typer
12
+
13
+ from kctl_api.core.callbacks import AppContext
14
+ from kctl_api.core.utils import find_project_root
15
+
16
+ app = typer.Typer(
17
+ name="docker", help="Docker management — images, containers, network, volumes, system.", no_args_is_help=True
18
+ )
19
+
20
+
21
+ def _compose_cmd(args: list[str], compose_file: str | None = None) -> list[str]:
22
+ root = find_project_root()
23
+ cf = compose_file or str(root / "docker-compose.yml")
24
+ return ["docker", "compose", "-f", cf, *args]
25
+
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # images
29
+ # ---------------------------------------------------------------------------
30
+ @app.command()
31
+ def images(ctx: typer.Context) -> None:
32
+ """List Docker images for the project."""
33
+ actx: AppContext = ctx.obj
34
+ out = actx.output
35
+
36
+ result = subprocess.run(
37
+ ["docker", "images", "--format", "json"],
38
+ capture_output=True,
39
+ text=True,
40
+ )
41
+ if result.returncode != 0:
42
+ out.error(f"docker images failed: {result.stderr.strip()}")
43
+ raise typer.Exit(1)
44
+
45
+ import json
46
+
47
+ image_list: list[dict] = []
48
+ for line in result.stdout.strip().splitlines():
49
+ if line.strip():
50
+ try:
51
+ image_list.append(json.loads(line))
52
+ except json.JSONDecodeError:
53
+ continue
54
+
55
+ # Filter to project images
56
+ root = find_project_root()
57
+ project_name = root.name.replace("-", "")
58
+
59
+ project_images = [
60
+ img
61
+ for img in image_list
62
+ if project_name in (img.get("Repository") or "").lower() or "kodemeio" in (img.get("Repository") or "").lower()
63
+ ] or image_list[:20]
64
+
65
+ rows = [
66
+ [
67
+ img.get("Repository", ""),
68
+ img.get("Tag", ""),
69
+ img.get("Size", ""),
70
+ img.get("CreatedSince", img.get("CreatedAt", "")),
71
+ ]
72
+ for img in project_images
73
+ ]
74
+ out.table(
75
+ title=f"Docker Images ({len(project_images)})",
76
+ columns=[("Repository", "bold"), ("Tag", ""), ("Size", ""), ("Created", "")],
77
+ rows=rows,
78
+ data_for_json=project_images,
79
+ )
80
+
81
+
82
+ # ---------------------------------------------------------------------------
83
+ # containers
84
+ # ---------------------------------------------------------------------------
85
+ @app.command()
86
+ def containers(
87
+ ctx: typer.Context,
88
+ all_containers: Annotated[bool, typer.Option("--all", "-a", help="Show stopped containers too.")] = False,
89
+ ) -> None:
90
+ """List running (or all) Docker containers."""
91
+ actx: AppContext = ctx.obj
92
+ out = actx.output
93
+
94
+ cmd = ["docker", "ps", "--format", "json"]
95
+ if all_containers:
96
+ cmd.append("--all")
97
+
98
+ result = subprocess.run(cmd, capture_output=True, text=True)
99
+ if result.returncode != 0:
100
+ out.error(f"docker ps failed: {result.stderr.strip()}")
101
+ raise typer.Exit(1)
102
+
103
+ import json
104
+
105
+ container_list: list[dict] = []
106
+ for line in result.stdout.strip().splitlines():
107
+ if line.strip():
108
+ try:
109
+ container_list.append(json.loads(line))
110
+ except json.JSONDecodeError:
111
+ continue
112
+
113
+ rows = [
114
+ [
115
+ c.get("Names", c.get("Name", "")),
116
+ c.get("Image", ""),
117
+ c.get("Status", ""),
118
+ c.get("Ports", ""),
119
+ ]
120
+ for c in container_list
121
+ ]
122
+ out.table(
123
+ title=f"Containers ({len(container_list)})",
124
+ columns=[("Name", "bold"), ("Image", ""), ("Status", ""), ("Ports", "")],
125
+ rows=rows,
126
+ data_for_json=container_list,
127
+ )
128
+
129
+
130
+ # ---------------------------------------------------------------------------
131
+ # network
132
+ # ---------------------------------------------------------------------------
133
+ @app.command()
134
+ def network(ctx: typer.Context) -> None:
135
+ """List Docker networks and show which containers are attached."""
136
+ actx: AppContext = ctx.obj
137
+ out = actx.output
138
+
139
+ result = subprocess.run(
140
+ ["docker", "network", "ls", "--format", "json"],
141
+ capture_output=True,
142
+ text=True,
143
+ )
144
+ if result.returncode != 0:
145
+ out.error(f"docker network ls failed: {result.stderr.strip()}")
146
+ raise typer.Exit(1)
147
+
148
+ import json
149
+
150
+ networks: list[dict] = []
151
+ for line in result.stdout.strip().splitlines():
152
+ if line.strip():
153
+ try:
154
+ networks.append(json.loads(line))
155
+ except json.JSONDecodeError:
156
+ continue
157
+
158
+ rows = [
159
+ [
160
+ n.get("Name", ""),
161
+ n.get("Driver", ""),
162
+ n.get("Scope", ""),
163
+ "[green]yes[/green]" if n.get("Name") == "dokploy-network" else "",
164
+ ]
165
+ for n in networks
166
+ ]
167
+ out.table(
168
+ title=f"Docker Networks ({len(networks)})",
169
+ columns=[("Name", "bold"), ("Driver", ""), ("Scope", ""), ("Dokploy", "")],
170
+ rows=rows,
171
+ data_for_json=networks,
172
+ )
173
+
174
+ # Check dokploy-network
175
+ has_dokploy = any(n.get("Name") == "dokploy-network" for n in networks)
176
+ if not has_dokploy:
177
+ out.warn("dokploy-network not found. Create it: docker network create dokploy-network")
178
+
179
+
180
+ # ---------------------------------------------------------------------------
181
+ # volumes
182
+ # ---------------------------------------------------------------------------
183
+ @app.command()
184
+ def volumes(ctx: typer.Context) -> None:
185
+ """List Docker volumes used by the project."""
186
+ actx: AppContext = ctx.obj
187
+ out = actx.output
188
+
189
+ result = subprocess.run(
190
+ ["docker", "volume", "ls", "--format", "json"],
191
+ capture_output=True,
192
+ text=True,
193
+ )
194
+ if result.returncode != 0:
195
+ out.error(f"docker volume ls failed: {result.stderr.strip()}")
196
+ raise typer.Exit(1)
197
+
198
+ import json
199
+
200
+ volume_list: list[dict] = []
201
+ for line in result.stdout.strip().splitlines():
202
+ if line.strip():
203
+ try:
204
+ volume_list.append(json.loads(line))
205
+ except json.JSONDecodeError:
206
+ continue
207
+
208
+ rows = [[v.get("Name", ""), v.get("Driver", ""), v.get("Labels", "")] for v in volume_list]
209
+ out.table(
210
+ title=f"Docker Volumes ({len(volume_list)})",
211
+ columns=[("Name", "bold"), ("Driver", ""), ("Labels", "")],
212
+ rows=rows,
213
+ data_for_json=volume_list,
214
+ )
215
+
216
+
217
+ # ---------------------------------------------------------------------------
218
+ # system
219
+ # ---------------------------------------------------------------------------
220
+ @app.command()
221
+ def system(ctx: typer.Context) -> None:
222
+ """Show Docker system disk usage and resource summary."""
223
+ actx: AppContext = ctx.obj
224
+ out = actx.output
225
+
226
+ result = subprocess.run(
227
+ ["docker", "system", "df"],
228
+ capture_output=True,
229
+ text=True,
230
+ )
231
+ if result.returncode != 0:
232
+ out.error(f"docker system df failed: {result.stderr.strip()}")
233
+ raise typer.Exit(1)
234
+
235
+ out.text(result.stdout)
236
+
237
+ # Also show info summary
238
+ info_result = subprocess.run(
239
+ [
240
+ "docker",
241
+ "info",
242
+ "--format",
243
+ "Containers: {{.Containers}} running / Images: {{.Images}} / Memory: {{.MemTotal}}",
244
+ ],
245
+ capture_output=True,
246
+ text=True,
247
+ )
248
+ if info_result.returncode == 0:
249
+ out.text(info_result.stdout.strip())
250
+
251
+ if actx.json_mode:
252
+ out.raw_json({"output": result.stdout})