taskfile 0.3.74__tar.gz → 0.3.76__tar.gz

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 (133) hide show
  1. {taskfile-0.3.74/src/taskfile.egg-info → taskfile-0.3.76}/PKG-INFO +1 -1
  2. {taskfile-0.3.74 → taskfile-0.3.76}/pyproject.toml +1 -1
  3. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/__init__.py +1 -1
  4. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cli/interactive/wizards.py +109 -100
  5. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/diagnostics/checks.py +210 -455
  6. taskfile-0.3.76/src/taskfile/diagnostics/checks_ports.py +157 -0
  7. taskfile-0.3.76/src/taskfile/diagnostics/checks_ssh.py +230 -0
  8. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/models.py +8 -2
  9. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/runner/commands.py +71 -53
  10. {taskfile-0.3.74 → taskfile-0.3.76/src/taskfile.egg-info}/PKG-INFO +1 -1
  11. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile.egg-info/SOURCES.txt +2 -0
  12. {taskfile-0.3.74 → taskfile-0.3.76}/tests/test_dsl_commands.py +13 -0
  13. {taskfile-0.3.74 → taskfile-0.3.76}/LICENSE +0 -0
  14. {taskfile-0.3.74 → taskfile-0.3.76}/README.md +0 -0
  15. {taskfile-0.3.74 → taskfile-0.3.76}/setup.cfg +0 -0
  16. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/__main__.py +0 -0
  17. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/addons/__init__.py +0 -0
  18. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/addons/monitoring.py +0 -0
  19. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/addons/postgres.py +0 -0
  20. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/addons/redis_addon.py +0 -0
  21. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/api/__init__.py +0 -0
  22. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/api/app.py +0 -0
  23. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/api/models.py +0 -0
  24. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cache.py +0 -0
  25. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cigen/__init__.py +0 -0
  26. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cigen/base.py +0 -0
  27. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cigen/drone.py +0 -0
  28. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cigen/gitea.py +0 -0
  29. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cigen/github.py +0 -0
  30. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cigen/gitlab.py +0 -0
  31. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cigen/jenkins.py +0 -0
  32. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cigen/makefile.py +0 -0
  33. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cirunner.py +0 -0
  34. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cli/__init__.py +0 -0
  35. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cli/api_cmd.py +0 -0
  36. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cli/auth.py +0 -0
  37. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cli/cache_cmds.py +0 -0
  38. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cli/ci.py +0 -0
  39. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cli/click_compat.py +0 -0
  40. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cli/completion.py +0 -0
  41. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cli/deploy.py +0 -0
  42. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cli/diagnostics.py +0 -0
  43. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cli/docker_cmds.py +0 -0
  44. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cli/e2e_cmd.py +0 -0
  45. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cli/explain_cmd.py +0 -0
  46. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cli/fleet.py +0 -0
  47. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cli/health.py +0 -0
  48. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cli/import_export.py +0 -0
  49. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cli/info_cmd.py +0 -0
  50. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cli/interactive/__init__.py +0 -0
  51. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cli/interactive/menu.py +0 -0
  52. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cli/main.py +0 -0
  53. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cli/quadlet.py +0 -0
  54. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cli/registry_cmds.py +0 -0
  55. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cli/release.py +0 -0
  56. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cli/setup.py +0 -0
  57. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/cli/version.py +0 -0
  58. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/compose.py +0 -0
  59. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/converters.py +0 -0
  60. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/deploy_recipes.py +0 -0
  61. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/deploy_utils.py +0 -0
  62. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/diagnostics/__init__.py +0 -0
  63. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/diagnostics/fixes.py +0 -0
  64. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/diagnostics/llm_repair.py +0 -0
  65. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/diagnostics/models.py +0 -0
  66. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/diagnostics/report.py +0 -0
  67. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/fleet.py +0 -0
  68. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/graph.py +0 -0
  69. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/health.py +0 -0
  70. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/importer.py +0 -0
  71. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/landing.py +0 -0
  72. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/notifications.py +0 -0
  73. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/parser.py +0 -0
  74. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/provisioner.py +0 -0
  75. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/quadlet.py +0 -0
  76. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/registry.py +0 -0
  77. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/runner/__init__.py +0 -0
  78. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/runner/core.py +0 -0
  79. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/runner/error_presenter.py +0 -0
  80. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/runner/explainer.py +0 -0
  81. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/runner/functions.py +0 -0
  82. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/runner/resolver.py +0 -0
  83. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/runner/ssh.py +0 -0
  84. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/scaffold/__init__.py +0 -0
  85. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/scaffold/codereview.py +0 -0
  86. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/scaffold/full.py +0 -0
  87. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/scaffold/minimal.py +0 -0
  88. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/scaffold/multiplatform.py +0 -0
  89. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/scaffold/podman.py +0 -0
  90. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/scaffold/publish.py +0 -0
  91. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/scaffold/templates/codereview.yml +0 -0
  92. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/scaffold/templates/full.yml +0 -0
  93. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/scaffold/templates/iot.yml +0 -0
  94. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/scaffold/templates/kubernetes.yml +0 -0
  95. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/scaffold/templates/minimal.yml +0 -0
  96. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/scaffold/templates/multiplatform.yml +0 -0
  97. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/scaffold/templates/podman.yml +0 -0
  98. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/scaffold/templates/publish.yml +0 -0
  99. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/scaffold/templates/saas.yml +0 -0
  100. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/scaffold/templates/terraform.yml +0 -0
  101. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/scaffold/templates/web.yml +0 -0
  102. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/scaffold/web.py +0 -0
  103. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/ssh.py +0 -0
  104. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/watch.py +0 -0
  105. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/webui/__init__.py +0 -0
  106. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/webui/dashboard.py +0 -0
  107. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/webui/handlers.py +0 -0
  108. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile/webui/server.py +0 -0
  109. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile.egg-info/dependency_links.txt +0 -0
  110. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile.egg-info/entry_points.txt +0 -0
  111. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile.egg-info/requires.txt +0 -0
  112. {taskfile-0.3.74 → taskfile-0.3.76}/src/taskfile.egg-info/top_level.txt +0 -0
  113. {taskfile-0.3.74 → taskfile-0.3.76}/tests/test_api.py +0 -0
  114. {taskfile-0.3.74 → taskfile-0.3.76}/tests/test_auth.py +0 -0
  115. {taskfile-0.3.74 → taskfile-0.3.76}/tests/test_cigen.py +0 -0
  116. {taskfile-0.3.74 → taskfile-0.3.76}/tests/test_cli.py +0 -0
  117. {taskfile-0.3.74 → taskfile-0.3.76}/tests/test_compose.py +0 -0
  118. {taskfile-0.3.74 → taskfile-0.3.76}/tests/test_diagnostics.py +0 -0
  119. {taskfile-0.3.74 → taskfile-0.3.76}/tests/test_docker_e2e.py +0 -0
  120. {taskfile-0.3.74 → taskfile-0.3.76}/tests/test_doctor_e2e.py +0 -0
  121. {taskfile-0.3.74 → taskfile-0.3.76}/tests/test_e2e_examples.py +0 -0
  122. {taskfile-0.3.74 → taskfile-0.3.76}/tests/test_fleet.py +0 -0
  123. {taskfile-0.3.74 → taskfile-0.3.76}/tests/test_health.py +0 -0
  124. {taskfile-0.3.74 → taskfile-0.3.76}/tests/test_landing.py +0 -0
  125. {taskfile-0.3.74 → taskfile-0.3.76}/tests/test_models.py +0 -0
  126. {taskfile-0.3.74 → taskfile-0.3.76}/tests/test_parser.py +0 -0
  127. {taskfile-0.3.74 → taskfile-0.3.76}/tests/test_provisioner.py +0 -0
  128. {taskfile-0.3.74 → taskfile-0.3.76}/tests/test_quadlet.py +0 -0
  129. {taskfile-0.3.74 → taskfile-0.3.76}/tests/test_release.py +0 -0
  130. {taskfile-0.3.74 → taskfile-0.3.76}/tests/test_resolver.py +0 -0
  131. {taskfile-0.3.74 → taskfile-0.3.76}/tests/test_runner.py +0 -0
  132. {taskfile-0.3.74 → taskfile-0.3.76}/tests/test_scaffold.py +0 -0
  133. {taskfile-0.3.74 → taskfile-0.3.76}/tests/test_setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: taskfile
3
- Version: 0.3.74
3
+ Version: 0.3.76
4
4
  Summary: Universal Taskfile runner with multi-environment deploy support. CI/CD agnostic — run locally or from any pipeline.
5
5
  Author-email: Tom Sapletta <tom@sapletta.com>
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "taskfile"
7
- version = "0.3.74"
7
+ version = "0.3.76"
8
8
  description = "Universal Taskfile runner with multi-environment deploy support. CI/CD agnostic — run locally or from any pipeline."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -12,7 +12,7 @@ Features:
12
12
  - @remote SSH command execution
13
13
  """
14
14
 
15
- __version__ = "0.3.74"
15
+ __version__ = "0.3.76"
16
16
  __author__ = "Softreck"
17
17
 
18
18
  from taskfile.runner import TaskfileRunner
@@ -342,126 +342,135 @@ def _run_llm_assist(diagnostics: ProjectDiagnostics) -> None:
342
342
  console.print(f" 💡 {suggestion}")
343
343
 
344
344
 
345
- def _run_remote_diagnostics(diagnostics: ProjectDiagnostics) -> None:
346
- """Extended remote diagnostics check containers, images, system status on remote server."""
347
- from taskfile.parser import find_taskfile, load_taskfile
348
- from taskfile.deploy_utils import test_ssh_connection
349
- from taskfile.diagnostics.checks import _resolve_env_fields
345
+ def _ssh_run(ssh_cmd: str, remote_cmd: str, timeout: int = 10) -> subprocess.CompletedProcess | None:
346
+ """Run a single command over SSH. Returns CompletedProcess or None on timeout/error."""
347
+ try:
348
+ return subprocess.run(
349
+ f"{ssh_cmd} '{remote_cmd}'",
350
+ shell=True, capture_output=True, text=True, timeout=timeout,
351
+ )
352
+ except (subprocess.TimeoutExpired, OSError):
353
+ return None
354
+
355
+
356
+ def _check_remote_podman(ssh_cmd: str, host: str) -> None:
357
+ """Check podman version on remote host."""
358
+ result = _ssh_run(ssh_cmd, 'podman --version 2>/dev/null || echo "NOT_INSTALLED"')
359
+ if result and result.returncode == 0 and "NOT_INSTALLED" not in result.stdout:
360
+ version = result.stdout.strip().replace("podman version ", "").replace("Version: ", "")
361
+ console.print(f"[green] ✓ Podman: {version}[/]")
362
+ else:
363
+ console.print(f"[yellow] ⚠ Podman not installed on {host}[/]")
350
364
 
351
- console.print("\n[bold cyan]🔍 Remote Server Diagnostics[/]")
352
365
 
353
- try:
354
- tf = find_taskfile()
355
- config = load_taskfile(tf)
356
- except Exception:
357
- console.print("[yellow] Could not load Taskfile config for remote diagnostics[/]")
358
- return
366
+ def _check_remote_disk(ssh_cmd: str) -> None:
367
+ """Check disk space on remote host."""
368
+ result = _ssh_run(ssh_cmd, 'df -h / | tail -1')
369
+ if result and result.returncode == 0:
370
+ parts = result.stdout.strip().split()
371
+ if len(parts) >= 5:
372
+ console.print(f"[dim] Disk: {parts[4]} used ({parts[2]} / {parts[1]})[/]")
359
373
 
360
- # Respect --env flag from parent group (taskfile --env prod doctor --remote)
374
+
375
+ def _check_remote_memory(ssh_cmd: str) -> None:
376
+ """Check memory on remote host."""
377
+ result = _ssh_run(ssh_cmd, 'free -h 2>/dev/null | grep Mem || echo "N/A"')
378
+ if result and result.returncode == 0 and result.stdout.strip() != "N/A":
379
+ mem_parts = result.stdout.strip().split()
380
+ if len(mem_parts) >= 4:
381
+ console.print(f"[dim] Memory: {mem_parts[2]} used / {mem_parts[1]} total[/]")
382
+
383
+
384
+ def _check_remote_containers(ssh_cmd: str) -> None:
385
+ """Check running container and image counts on remote host."""
386
+ result = _ssh_run(ssh_cmd, 'podman ps -q 2>/dev/null | wc -l')
387
+ if result and result.returncode == 0:
388
+ console.print(f"[dim] Running containers: {result.stdout.strip()}[/]")
389
+
390
+ result = _ssh_run(ssh_cmd, 'podman images -q 2>/dev/null | wc -l')
391
+ if result and result.returncode == 0:
392
+ console.print(f"[dim] Stored images: {result.stdout.strip()}[/]")
393
+
394
+
395
+ def _resolve_selected_env() -> str | None:
396
+ """Read --env flag from parent click context (taskfile --env prod doctor --remote)."""
361
397
  ctx = click.get_current_context(silent=True)
362
- selected_env = None
363
- if ctx:
364
- parent = ctx.parent
365
- while parent:
366
- selected_env = (parent.params or {}).get("env_name")
367
- if selected_env:
368
- break
369
- parent = parent.parent
370
-
371
- # Build list of remote environments to check
372
- remote_envs = []
398
+ if not ctx:
399
+ return None
400
+ parent = ctx.parent
401
+ while parent:
402
+ selected = (parent.params or {}).get("env_name")
403
+ if selected:
404
+ return selected
405
+ parent = parent.parent
406
+ return None
407
+
408
+
409
+ def _collect_remote_envs(config, selected_env: str | None) -> list[tuple] | None:
410
+ """Build list of remote environments to check. Returns None on user-facing error."""
373
411
  if selected_env:
374
412
  env_obj = config.environments.get(selected_env)
375
413
  if env_obj and env_obj.is_remote:
376
- remote_envs = [(selected_env, env_obj)]
377
- elif env_obj:
414
+ return [(selected_env, env_obj)]
415
+ if env_obj:
378
416
  console.print(f"[yellow] Environment '{selected_env}' is not remote (no ssh_host)[/]")
379
- return
380
417
  else:
381
418
  console.print(f"[yellow] Environment '{selected_env}' not found in Taskfile.yml[/]")
382
- return
383
- else:
384
- remote_envs = [(name, env) for name, env in config.environments.items() if env.is_remote]
385
-
386
- if not remote_envs:
419
+ return None
420
+ envs = [(name, env) for name, env in config.environments.items() if env.is_remote]
421
+ if not envs:
387
422
  console.print("[yellow] No remote environments configured in Taskfile.yml[/]")
388
- return
423
+ return None
424
+ return envs
389
425
 
390
- taskfile_dir = tf.parent
391
426
 
392
- for env_name, env in remote_envs:
393
- # Resolve ${VAR:-default} patterns in SSH fields using .env file
394
- _resolve_env_fields(env, taskfile_dir)
427
+ def _diagnose_single_remote_env(env_name, env, taskfile_dir) -> None:
428
+ """Run all remote diagnostics for a single environment."""
429
+ from taskfile.deploy_utils import test_ssh_connection
430
+ from taskfile.diagnostics.checks import _resolve_env_fields
395
431
 
396
- console.print(f"\n[bold]Environment: {env_name}[/] ({env.ssh_host})")
432
+ _resolve_env_fields(env, taskfile_dir)
433
+ console.print(f"\n[bold]Environment: {env_name}[/] ({env.ssh_host})")
397
434
 
398
- # Test SSH first
399
- ssh_result = test_ssh_connection(env.ssh_host, env.ssh_user, env.ssh_port)
400
- if not ssh_result.success:
401
- detail = ssh_result.output.strip() if ssh_result.output and ssh_result.output.strip() else f"exit code {ssh_result.exit_code}"
402
- console.print(f"[red] SSH connection failed: {detail}[/]")
403
- console.print(f"[dim] ssh {env.ssh_opts} {env.ssh_user}@{env.ssh_host} 'echo ok'[/]")
404
- continue
435
+ ssh_result = test_ssh_connection(env.ssh_host, env.ssh_user, env.ssh_port)
436
+ if not ssh_result.success:
437
+ detail = ssh_result.output.strip() if ssh_result.output and ssh_result.output.strip() else f"exit code {ssh_result.exit_code}"
438
+ console.print(f"[red] ✗ SSH connection failed: {detail}[/]")
439
+ console.print(f"[dim] ssh {env.ssh_opts} {env.ssh_user}@{env.ssh_host} 'echo ok'[/]")
440
+ return
405
441
 
406
- console.print(f"[green] ✓ SSH connected to {env.ssh_host}[/]")
442
+ console.print(f"[green] ✓ SSH connected to {env.ssh_host}[/]")
443
+ ssh_cmd = f"ssh {env.ssh_opts} {env.ssh_user}@{env.ssh_host}"
407
444
 
408
- # Use env.ssh_opts for consistent SSH options
409
- ssh_cmd = f"ssh {env.ssh_opts} {env.ssh_user}@{env.ssh_host}"
445
+ try:
446
+ _check_remote_podman(ssh_cmd, env.ssh_host)
447
+ _check_remote_disk(ssh_cmd)
448
+ _check_remote_memory(ssh_cmd)
449
+ _check_remote_containers(ssh_cmd)
450
+ except Exception as e:
451
+ console.print(f"[yellow] Remote diagnostics error: {e}[/]")
410
452
 
411
- try:
412
- # Check podman version
413
- result = subprocess.run(
414
- f"{ssh_cmd} 'podman --version 2>/dev/null || echo \"NOT_INSTALLED\"'",
415
- shell=True, capture_output=True, text=True, timeout=10
416
- )
417
- if result.returncode == 0 and "NOT_INSTALLED" not in result.stdout:
418
- version = result.stdout.strip().replace("podman version ", "").replace("Version: ", "")
419
- console.print(f"[green] ✓ Podman: {version}[/]")
420
- else:
421
- console.print(f"[yellow] ⚠ Podman not installed on {env.ssh_host}[/]")
422
453
 
423
- # Check disk space
424
- result = subprocess.run(
425
- f"{ssh_cmd} 'df -h / | tail -1'",
426
- shell=True, capture_output=True, text=True, timeout=10
427
- )
428
- if result.returncode == 0:
429
- parts = result.stdout.strip().split()
430
- if len(parts) >= 5:
431
- console.print(f"[dim] Disk: {parts[4]} used ({parts[2]} / {parts[1]})[/]")
432
-
433
- # Check memory
434
- result = subprocess.run(
435
- f"{ssh_cmd} 'free -h 2>/dev/null | grep Mem || echo \"N/A\"'",
436
- shell=True, capture_output=True, text=True, timeout=10
437
- )
438
- if result.returncode == 0 and result.stdout.strip() != "N/A":
439
- mem_parts = result.stdout.strip().split()
440
- if len(mem_parts) >= 4:
441
- console.print(f"[dim] Memory: {mem_parts[2]} used / {mem_parts[1]} total[/]")
442
-
443
- # Check running containers count
444
- result = subprocess.run(
445
- f"{ssh_cmd} 'podman ps -q 2>/dev/null | wc -l'",
446
- shell=True, capture_output=True, text=True, timeout=10
447
- )
448
- if result.returncode == 0:
449
- container_count = result.stdout.strip()
450
- console.print(f"[dim] Running containers: {container_count}[/]")
451
-
452
- # Check images
453
- result = subprocess.run(
454
- f"{ssh_cmd} 'podman images -q 2>/dev/null | wc -l'",
455
- shell=True, capture_output=True, text=True, timeout=10
456
- )
457
- if result.returncode == 0:
458
- image_count = result.stdout.strip()
459
- console.print(f"[dim] Stored images: {image_count}[/]")
454
+ def _run_remote_diagnostics(diagnostics: ProjectDiagnostics) -> None:
455
+ """Extended remote diagnostics — check containers, images, system status on remote server."""
456
+ from taskfile.parser import find_taskfile, load_taskfile
460
457
 
461
- except subprocess.TimeoutExpired:
462
- console.print(f"[yellow] ⚠ Remote check timed out for {env.ssh_host}[/]")
463
- except Exception as e:
464
- console.print(f"[yellow] Remote diagnostics error: {e}[/]")
458
+ console.print("\n[bold cyan]🔍 Remote Server Diagnostics[/]")
459
+
460
+ try:
461
+ tf = find_taskfile()
462
+ config = load_taskfile(tf)
463
+ except Exception:
464
+ console.print("[yellow] Could not load Taskfile config for remote diagnostics[/]")
465
+ return
466
+
467
+ selected_env = _resolve_selected_env()
468
+ remote_envs = _collect_remote_envs(config, selected_env)
469
+ if remote_envs is None:
470
+ return
471
+
472
+ for env_name, env in remote_envs:
473
+ _diagnose_single_remote_env(env_name, env, tf.parent)
465
474
 
466
475
 
467
476
  class _nullcontext: