Swap in richer progress UI; fix fzf preview to use docker label filter
docker-update (Python): - Add compose_binary detection (docker compose v2 plugin or legacy docker-compose) - is_git_repo() now uses `git -C dir rev-parse` instead of .git dir check - Rich runner: Panel header showing project count / git / registry split, per-project sub-task with step labels (git pull, down, pull, up --build…), BarColumn(30) + TimeElapsedColumn, bold summary line - Plain runner: same header line, passes compose_cmd through - Summary uses bold green/yellow/red instead of plain colours - All update functions accept compose_cmd parameter docker-update-fzf (Bash): - project_status_by_name(): check running state via `docker ps --filter label=com.docker.compose.project=NAME` — works from any directory, no workdir required - Preview script rebuilt: shows container table + per-container logs via docker ps/logs --filter label=…, includes stopped containers fallback - show_action_menu() now uses project_status_by_name() for live dot / actions - Trap extended to INT/TERM for cleaner SSH exit https://claude.ai/code/session_01LtPxA1zDET2JQn6NYDDxKn
This commit is contained in:
+137
-47
@@ -24,12 +24,14 @@ from typing import Any
|
|||||||
# ── optional rich ──────────────────────────────────────────────────────────
|
# ── optional rich ──────────────────────────────────────────────────────────
|
||||||
try:
|
try:
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
from rich.panel import Panel
|
||||||
from rich.progress import (
|
from rich.progress import (
|
||||||
BarColumn,
|
BarColumn,
|
||||||
Progress,
|
Progress,
|
||||||
SpinnerColumn,
|
SpinnerColumn,
|
||||||
TaskProgressColumn,
|
TaskProgressColumn,
|
||||||
TextColumn,
|
TextColumn,
|
||||||
|
TimeElapsedColumn,
|
||||||
)
|
)
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
from rich.markup import escape as rich_escape
|
from rich.markup import escape as rich_escape
|
||||||
@@ -73,7 +75,7 @@ OUTCOME_COLORS: dict[str, str] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# ── subprocess helper ──────────────────────────────────────────────────────
|
# ── subprocess helpers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _run(cmd: list[str], cwd: str | None = None) -> subprocess.CompletedProcess[str]:
|
def _run(cmd: list[str], cwd: str | None = None) -> subprocess.CompletedProcess[str]:
|
||||||
return subprocess.run(
|
return subprocess.run(
|
||||||
@@ -81,12 +83,25 @@ def _run(cmd: list[str], cwd: str | None = None) -> subprocess.CompletedProcess[
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_compose_cmd() -> list[str]:
|
||||||
|
"""Detect docker compose v2 plugin or legacy docker-compose."""
|
||||||
|
if _run(["docker", "compose", "version"]).returncode == 0:
|
||||||
|
return ["docker", "compose"]
|
||||||
|
if shutil.which("docker-compose"):
|
||||||
|
return ["docker-compose"]
|
||||||
|
sys.exit("error: neither 'docker compose' nor 'docker-compose' found in PATH")
|
||||||
|
|
||||||
|
|
||||||
|
def _is_git_repo(directory: str) -> bool:
|
||||||
|
return _run(["git", "-C", directory, "rev-parse", "--is-inside-work-tree"]).returncode == 0
|
||||||
|
|
||||||
|
|
||||||
# ── project discovery ──────────────────────────────────────────────────────
|
# ── project discovery ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
def discover_projects() -> list[dict[str, Any]]:
|
def discover_projects(compose_cmd: list[str]) -> list[dict[str, Any]]:
|
||||||
"""Return sorted list of compose projects found on this host."""
|
"""Return sorted list of compose projects found on this host."""
|
||||||
try:
|
try:
|
||||||
r = _run(["docker", "compose", "ls", "--all", "--format", "json"])
|
r = _run(compose_cmd + ["ls", "--all", "--format", "json"])
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
sys.exit("error: 'docker' not found in PATH")
|
sys.exit("error: 'docker' not found in PATH")
|
||||||
|
|
||||||
@@ -108,7 +123,7 @@ def discover_projects() -> list[dict[str, Any]]:
|
|||||||
projects.append({
|
projects.append({
|
||||||
"name": name,
|
"name": name,
|
||||||
"workdir": workdir,
|
"workdir": workdir,
|
||||||
"is_git": Path(workdir, ".git").is_dir(),
|
"is_git": _is_git_repo(workdir),
|
||||||
"status": item.get("Status", ""),
|
"status": item.get("Status", ""),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -122,17 +137,17 @@ def _container_logs(workdir: str) -> str:
|
|||||||
return (r.stdout or r.stderr or "(no logs available)").strip()
|
return (r.stdout or r.stderr or "(no logs available)").strip()
|
||||||
|
|
||||||
|
|
||||||
def update_project_git(project: dict[str, Any]) -> dict[str, Any]:
|
def update_project_git(project: dict[str, Any], compose_cmd: list[str]) -> dict[str, Any]:
|
||||||
workdir = project["workdir"]
|
workdir = project["workdir"]
|
||||||
log_lines: list[str] = []
|
log_lines: list[str] = []
|
||||||
|
|
||||||
r = _run(["git", "pull"], cwd=workdir)
|
r = _run(["git", "-C", workdir, "pull"])
|
||||||
log_lines.append(f"=== git pull (exit {r.returncode}) ===\n{r.stdout}{r.stderr}")
|
log_lines.append(f"=== git pull (exit {r.returncode}) ===\n{r.stdout}{r.stderr}")
|
||||||
pull_ok = r.returncode == 0
|
pull_ok = r.returncode == 0
|
||||||
|
|
||||||
r = _run(["docker", "compose", "up", "-d", "--build"], cwd=workdir)
|
r = _run(compose_cmd + ["up", "-d", "--build"], cwd=workdir)
|
||||||
log_lines.append(
|
log_lines.append(
|
||||||
f"=== docker compose up -d --build (exit {r.returncode}) ===\n{r.stdout}{r.stderr}"
|
f"=== up -d --build (exit {r.returncode}) ===\n{r.stdout}{r.stderr}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if r.returncode != 0:
|
if r.returncode != 0:
|
||||||
@@ -148,27 +163,27 @@ def update_project_git(project: dict[str, Any]) -> dict[str, Any]:
|
|||||||
return {**project, "outcome": outcome, "log_lines": log_lines}
|
return {**project, "outcome": outcome, "log_lines": log_lines}
|
||||||
|
|
||||||
|
|
||||||
def update_project_registry(project: dict[str, Any]) -> dict[str, Any]:
|
def update_project_registry(project: dict[str, Any], compose_cmd: list[str]) -> dict[str, Any]:
|
||||||
workdir = project["workdir"]
|
workdir = project["workdir"]
|
||||||
log_lines: list[str] = []
|
log_lines: list[str] = []
|
||||||
|
|
||||||
r = _run(["docker", "compose", "down"], cwd=workdir)
|
r = _run(compose_cmd + ["down"], cwd=workdir)
|
||||||
log_lines.append(f"=== docker compose down (exit {r.returncode}) ===\n{r.stdout}{r.stderr}")
|
log_lines.append(f"=== down (exit {r.returncode}) ===\n{r.stdout}{r.stderr}")
|
||||||
if r.returncode != 0:
|
if r.returncode != 0:
|
||||||
log_lines.append(f"\nStep 'stop' failed (exit {r.returncode}):")
|
log_lines.append(f"\nStep 'stop' failed (exit {r.returncode}):")
|
||||||
log_lines.append(r.stderr)
|
log_lines.append(r.stderr)
|
||||||
return {**project, "outcome": FAILED_STOP, "log_lines": log_lines}
|
return {**project, "outcome": FAILED_STOP, "log_lines": log_lines}
|
||||||
|
|
||||||
r = _run(["docker", "compose", "pull"], cwd=workdir)
|
r = _run(compose_cmd + ["pull"], cwd=workdir)
|
||||||
log_lines.append(f"=== docker compose pull (exit {r.returncode}) ===\n{r.stdout}{r.stderr}")
|
log_lines.append(f"=== pull (exit {r.returncode}) ===\n{r.stdout}{r.stderr}")
|
||||||
pull_ok = r.returncode == 0
|
pull_ok = r.returncode == 0
|
||||||
if not pull_ok:
|
if not pull_ok:
|
||||||
log_lines.append(f"\nStep 'pull' failed (exit {r.returncode}) — restarting with existing image:")
|
log_lines.append(f"\nStep 'pull' failed (exit {r.returncode}) — restarting with existing image:")
|
||||||
log_lines.append(r.stderr)
|
log_lines.append(r.stderr)
|
||||||
|
|
||||||
r = _run(["docker", "compose", "up", "-d"], cwd=workdir)
|
r = _run(compose_cmd + ["up", "-d"], cwd=workdir)
|
||||||
log_lines.append(
|
log_lines.append(
|
||||||
f"=== docker compose up -d (exit {r.returncode}) ===\n{r.stdout}{r.stderr}"
|
f"=== up -d (exit {r.returncode}) ===\n{r.stdout}{r.stderr}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if r.returncode != 0:
|
if r.returncode != 0:
|
||||||
@@ -186,48 +201,119 @@ def update_project_registry(project: dict[str, Any]) -> dict[str, Any]:
|
|||||||
|
|
||||||
# ── rich runner ────────────────────────────────────────────────────────────
|
# ── rich runner ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _run_update_rich(projects: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
def _run_update_rich(projects: list[dict[str, Any]], compose_cmd: list[str]) -> list[dict[str, Any]]:
|
||||||
results: list[dict[str, Any]] = []
|
results: list[dict[str, Any]] = []
|
||||||
|
git_count = sum(1 for p in projects if p["is_git"])
|
||||||
|
reg_count = len(projects) - git_count
|
||||||
|
|
||||||
|
console.print(Panel.fit(
|
||||||
|
f"[bold cyan]docker-update[/bold cyan] "
|
||||||
|
f"[dim]{len(projects)} project(s) · {git_count} git · {reg_count} registry[/dim]",
|
||||||
|
border_style="cyan",
|
||||||
|
))
|
||||||
|
console.print()
|
||||||
|
|
||||||
with Progress(
|
with Progress(
|
||||||
SpinnerColumn(),
|
SpinnerColumn(),
|
||||||
TextColumn("[bold]{task.description:<32}"),
|
TextColumn("[progress.description]{task.description}"),
|
||||||
BarColumn(),
|
BarColumn(bar_width=30),
|
||||||
TaskProgressColumn(),
|
TaskProgressColumn(),
|
||||||
transient=False,
|
TimeElapsedColumn(),
|
||||||
console=console,
|
console=console,
|
||||||
) as prog:
|
transient=False,
|
||||||
task = prog.add_task("Updating stacks…", total=len(projects))
|
) as progress:
|
||||||
|
overall = progress.add_task("[bold white]Overall", total=len(projects))
|
||||||
|
|
||||||
for project in projects:
|
for project in projects:
|
||||||
prog.update(task, description=f" {project['name']}")
|
name = project["name"]
|
||||||
result = (
|
workdir = project["workdir"]
|
||||||
update_project_git(project)
|
is_git = project["is_git"]
|
||||||
if project["is_git"]
|
mode_tag = "[magenta]git[/magenta]" if is_git else "[blue]reg[/blue]"
|
||||||
else update_project_registry(project)
|
n_steps = 2 if is_git else 3
|
||||||
|
|
||||||
|
task = progress.add_task(
|
||||||
|
f"[cyan]{name}[/cyan] {mode_tag}",
|
||||||
|
total=n_steps,
|
||||||
)
|
)
|
||||||
results.append(result)
|
|
||||||
prog.advance(task)
|
if is_git:
|
||||||
color = OUTCOME_COLORS[result["outcome"]]
|
step_cmds = [
|
||||||
icon = OUTCOME_ICONS[result["outcome"]]
|
("git pull", ["git", "-C", workdir, "pull"]),
|
||||||
prog.console.print(
|
("up --build", compose_cmd + ["up", "-d", "--build"]),
|
||||||
f" [{color}]{icon}[/{color}] {rich_escape(result['name'])}"
|
]
|
||||||
|
else:
|
||||||
|
step_cmds = [
|
||||||
|
("down", compose_cmd + ["down"]),
|
||||||
|
("pull", compose_cmd + ["pull"]),
|
||||||
|
("up", compose_cmd + ["up", "-d"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
steps: dict[str, dict[str, Any]] = {}
|
||||||
|
pull_failed = False
|
||||||
|
outcome = SUCCEEDED
|
||||||
|
|
||||||
|
for label, cmd in step_cmds:
|
||||||
|
progress.update(
|
||||||
|
task,
|
||||||
|
description=f"[cyan]{name}[/cyan] {mode_tag} [yellow]{label}[/yellow]",
|
||||||
|
)
|
||||||
|
r = subprocess.run(cmd, cwd=workdir, capture_output=True, text=True)
|
||||||
|
rc, out, err = r.returncode, r.stdout, r.stderr
|
||||||
|
steps[label] = {"rc": rc, "out": out, "err": err}
|
||||||
|
progress.advance(task)
|
||||||
|
|
||||||
|
if rc != 0:
|
||||||
|
if label in ("git pull", "pull"):
|
||||||
|
pull_failed = True
|
||||||
|
elif label == "down":
|
||||||
|
outcome = FAILED_STOP
|
||||||
|
break
|
||||||
|
elif label in ("up", "up --build"):
|
||||||
|
outcome = FAILED_RESTART
|
||||||
|
break
|
||||||
|
|
||||||
|
if outcome == SUCCEEDED and pull_failed:
|
||||||
|
outcome = FAILED_PULL
|
||||||
|
|
||||||
|
color = OUTCOME_COLORS[outcome]
|
||||||
|
progress.update(
|
||||||
|
task,
|
||||||
|
description=f"[{color}]{OUTCOME_ICONS[outcome]} {name}[/{color}] {mode_tag}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Build log_lines for the text log writer
|
||||||
|
log_lines: list[str] = []
|
||||||
|
for step_label, sd in steps.items():
|
||||||
|
log_lines.append(
|
||||||
|
f"=== {step_label} (exit {sd['rc']}) ===\n{sd['out']}{sd['err']}"
|
||||||
|
)
|
||||||
|
if outcome != SUCCEEDED:
|
||||||
|
log_lines.append(
|
||||||
|
f"\nContainer logs ({name}):\n{_container_logs(workdir)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
results.append({**project, "outcome": outcome, "log_lines": log_lines})
|
||||||
|
progress.advance(overall)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
# ── plain runner ───────────────────────────────────────────────────────────
|
# ── plain runner ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _run_update_plain(projects: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
def _run_update_plain(projects: list[dict[str, Any]], compose_cmd: list[str]) -> list[dict[str, Any]]:
|
||||||
results: list[dict[str, Any]] = []
|
|
||||||
total = len(projects)
|
total = len(projects)
|
||||||
|
git_count = sum(1 for p in projects if p["is_git"])
|
||||||
|
reg_count = total - git_count
|
||||||
|
print(f"docker-update — {total} project(s) · {git_count} git · {reg_count} registry\n")
|
||||||
|
|
||||||
|
results: list[dict[str, Any]] = []
|
||||||
for i, project in enumerate(projects, 1):
|
for i, project in enumerate(projects, 1):
|
||||||
print(f"[{i}/{total}] Updating {project['name']}…", flush=True)
|
mode = "git" if project["is_git"] else "registry"
|
||||||
|
print(f"[{i}/{total}] {project['name']} ({mode}) — updating…", flush=True)
|
||||||
result = (
|
result = (
|
||||||
update_project_git(project)
|
update_project_git(project, compose_cmd)
|
||||||
if project["is_git"]
|
if project["is_git"]
|
||||||
else update_project_registry(project)
|
else update_project_registry(project, compose_cmd)
|
||||||
)
|
)
|
||||||
results.append(result)
|
results.append(result)
|
||||||
icon = OUTCOME_ICONS[result["outcome"]]
|
icon = OUTCOME_ICONS[result["outcome"]]
|
||||||
@@ -249,19 +335,17 @@ def _group_by_outcome(
|
|||||||
|
|
||||||
|
|
||||||
def _print_summary_rich(groups: dict[str, list[dict[str, Any]]]) -> None:
|
def _print_summary_rich(groups: dict[str, list[dict[str, Any]]]) -> None:
|
||||||
console.print()
|
|
||||||
console.rule("[bold]Summary")
|
|
||||||
console.print()
|
console.print()
|
||||||
parts: list[str] = []
|
parts: list[str] = []
|
||||||
if groups[SUCCEEDED]:
|
if groups[SUCCEEDED]:
|
||||||
parts.append(f"[green]✓ {len(groups[SUCCEEDED])} succeeded[/green]")
|
parts.append(f"[bold green]✓ {len(groups[SUCCEEDED])} succeeded[/bold green]")
|
||||||
if groups[FAILED_PULL]:
|
if groups[FAILED_PULL]:
|
||||||
parts.append(f"[yellow]⚠ {len(groups[FAILED_PULL])} failed to pull[/yellow]")
|
parts.append(f"[bold yellow]⚠ {len(groups[FAILED_PULL])} failed to pull[/bold yellow]")
|
||||||
if groups[FAILED_STOP]:
|
if groups[FAILED_STOP]:
|
||||||
parts.append(f"[red]✗ {len(groups[FAILED_STOP])} failed to stop[/red]")
|
parts.append(f"[bold red]✗ {len(groups[FAILED_STOP])} failed to stop[/bold red]")
|
||||||
if groups[FAILED_RESTART]:
|
if groups[FAILED_RESTART]:
|
||||||
parts.append(f"[red]✗ {len(groups[FAILED_RESTART])} failed to restart[/red]")
|
parts.append(f"[bold red]✗ {len(groups[FAILED_RESTART])} failed to restart[/bold red]")
|
||||||
console.print(" " + " ".join(parts))
|
console.print(" ".join(parts))
|
||||||
console.print()
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
@@ -347,12 +431,17 @@ def cmd_update() -> None:
|
|||||||
log_path = LOG_DIR / f"docker-update-{run_id}.log"
|
log_path = LOG_DIR / f"docker-update-{run_id}.log"
|
||||||
json_path = LOG_DIR / f"docker-update-{run_id}.json"
|
json_path = LOG_DIR / f"docker-update-{run_id}.json"
|
||||||
|
|
||||||
projects = discover_projects()
|
compose_cmd = _get_compose_cmd()
|
||||||
|
projects = discover_projects(compose_cmd)
|
||||||
if not projects:
|
if not projects:
|
||||||
print("No Docker Compose projects found.")
|
print("No Docker Compose projects found.")
|
||||||
return
|
return
|
||||||
|
|
||||||
results = _run_update_rich(projects) if HAS_RICH else _run_update_plain(projects)
|
results = (
|
||||||
|
_run_update_rich(projects, compose_cmd)
|
||||||
|
if HAS_RICH
|
||||||
|
else _run_update_plain(projects, compose_cmd)
|
||||||
|
)
|
||||||
|
|
||||||
groups = _group_by_outcome(results)
|
groups = _group_by_outcome(results)
|
||||||
_print_summary_rich(groups) if HAS_RICH else _print_summary_plain(groups)
|
_print_summary_rich(groups) if HAS_RICH else _print_summary_plain(groups)
|
||||||
@@ -424,7 +513,8 @@ def _cmd_log_plain() -> None:
|
|||||||
|
|
||||||
def cmd_debug() -> None:
|
def cmd_debug() -> None:
|
||||||
"""Dry-run: discover projects and show what would happen, no changes."""
|
"""Dry-run: discover projects and show what would happen, no changes."""
|
||||||
projects = discover_projects()
|
compose_cmd = _get_compose_cmd()
|
||||||
|
projects = discover_projects(compose_cmd)
|
||||||
if not projects:
|
if not projects:
|
||||||
print("No Docker Compose projects found.")
|
print("No Docker Compose projects found.")
|
||||||
return
|
return
|
||||||
|
|||||||
+45
-25
@@ -15,7 +15,7 @@ _DU_TMPDIR=""
|
|||||||
|
|
||||||
_setup_tmpdir() {
|
_setup_tmpdir() {
|
||||||
_DU_TMPDIR=$(mktemp -d /tmp/du-fzf-XXXXXX)
|
_DU_TMPDIR=$(mktemp -d /tmp/du-fzf-XXXXXX)
|
||||||
trap '_cleanup' EXIT
|
trap '_cleanup' EXIT INT TERM
|
||||||
}
|
}
|
||||||
|
|
||||||
_cleanup() {
|
_cleanup() {
|
||||||
@@ -27,6 +27,19 @@ die() {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── project status (by name, no workdir needed) ────────────────────────────
|
||||||
|
|
||||||
|
# Returns "running" or "stopped" for a compose project by name.
|
||||||
|
# Uses docker ps --filter so it works from any directory.
|
||||||
|
project_status_by_name() {
|
||||||
|
local name="$1"
|
||||||
|
local running
|
||||||
|
running=$(docker ps -q \
|
||||||
|
--filter "label=com.docker.compose.project=${name}" \
|
||||||
|
2>/dev/null | wc -l)
|
||||||
|
[[ "$running" -gt 0 ]] && echo "running" || echo "stopped"
|
||||||
|
}
|
||||||
|
|
||||||
# ── build_log_list ────────────────────────────────────────────────────────
|
# ── build_log_list ────────────────────────────────────────────────────────
|
||||||
# Outputs tab-delimited lines:
|
# Outputs tab-delimited lines:
|
||||||
# DISPLAY_TEXT \t FULL_LOG_PATH
|
# DISPLAY_TEXT \t FULL_LOG_PATH
|
||||||
@@ -138,29 +151,44 @@ PYEOF
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ── write_preview_script ──────────────────────────────────────────────────
|
# ── write_preview_script ──────────────────────────────────────────────────
|
||||||
# Writes a small bash script to $_DU_TMPDIR/preview.sh that fzf can call.
|
# Writes a bash script to $_DU_TMPDIR/preview.sh that fzf calls on hover.
|
||||||
# The script receives the full tab-delimited line as $1 and streams docker logs.
|
# Uses docker ps --filter label=... so it works from any directory.
|
||||||
write_preview_script() {
|
write_preview_script() {
|
||||||
local script="$_DU_TMPDIR/preview.sh"
|
local script="$_DU_TMPDIR/preview.sh"
|
||||||
cat > "$script" << 'PREVIEW_EOF'
|
cat > "$script" << 'PREVIEW_EOF'
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
line="$1"
|
line="$1"
|
||||||
name=$(printf '%s' "$line" | cut -f2)
|
name=$(printf '%s' "$line" | cut -f2)
|
||||||
workdir=$(printf '%s' "$line" | cut -f3)
|
|
||||||
|
|
||||||
if [[ "$name" == "HEADER" || -z "$workdir" ]]; then
|
if [[ "$name" == "HEADER" || -z "$name" ]]; then
|
||||||
printf '\n (hover over a project to see its live container logs)\n'
|
printf '\n (hover over a project to see its live container logs)\n'
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -d "$workdir" ]]; then
|
printf '\033[1m── containers: %s ──\033[0m\n' "$name"
|
||||||
printf 'Workdir not found: %s\n' "$workdir"
|
docker ps \
|
||||||
exit 1
|
--filter "label=com.docker.compose.project=${name}" \
|
||||||
|
--format ' {{.Names}} {{.Status}}' 2>/dev/null
|
||||||
|
printf '\n\033[1m── logs (last 40 lines) ──\033[0m\n'
|
||||||
|
|
||||||
|
# Grab up to 3 container IDs for this project and tail their logs
|
||||||
|
containers=$(docker ps -q \
|
||||||
|
--filter "label=com.docker.compose.project=${name}" \
|
||||||
|
2>/dev/null | head -3)
|
||||||
|
|
||||||
|
if [[ -z "$containers" ]]; then
|
||||||
|
# Also check stopped containers
|
||||||
|
containers=$(docker ps -aq \
|
||||||
|
--filter "label=com.docker.compose.project=${name}" \
|
||||||
|
2>/dev/null | head -3)
|
||||||
|
[[ -z "$containers" ]] && { printf ' (no containers found)\n'; exit 0; }
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf '\033[1m── live logs: %s ──\033[0m\n\n' "$name"
|
printf '%s\n' "$containers" | while IFS= read -r cid; do
|
||||||
cd "$workdir" && docker compose logs --tail=80 --no-color 2>&1 \
|
cname=$(docker inspect --format '{{.Name}}' "$cid" 2>/dev/null | sed 's|^/||')
|
||||||
|| printf 'No logs available\n'
|
printf '\n\033[2m--- %s ---\033[0m\n' "$cname"
|
||||||
|
docker logs --tail=40 --timestamps "$cid" 2>&1
|
||||||
|
done
|
||||||
PREVIEW_EOF
|
PREVIEW_EOF
|
||||||
chmod +x "$script"
|
chmod +x "$script"
|
||||||
echo "$script"
|
echo "$script"
|
||||||
@@ -189,22 +217,14 @@ LOGPREVIEW_EOF
|
|||||||
show_action_menu() {
|
show_action_menu() {
|
||||||
local name="$1" workdir="$2" is_git="$3" is_debug="$4"
|
local name="$1" workdir="$2" is_git="$3" is_debug="$4"
|
||||||
|
|
||||||
# Live status check
|
local status dot
|
||||||
local running_count stopped_count
|
status=$(project_status_by_name "$name")
|
||||||
running_count=$(cd "$workdir" 2>/dev/null \
|
[[ "$status" == "running" ]] && dot="●" || dot="○"
|
||||||
&& docker compose ps -q --status running 2>/dev/null | wc -l \
|
|
||||||
|| echo 0)
|
|
||||||
stopped_count=$(cd "$workdir" 2>/dev/null \
|
|
||||||
&& docker compose ps -q 2>/dev/null | wc -l \
|
|
||||||
|| echo 0)
|
|
||||||
|
|
||||||
local dot="○"
|
|
||||||
[[ "$running_count" -gt 0 ]] && dot="●"
|
|
||||||
|
|
||||||
local actions=""
|
local actions=""
|
||||||
[[ "$running_count" -eq 0 ]] && actions+=" ▶ Start\n"
|
[[ "$status" != "running" ]] && actions+=" ▶ Start\n"
|
||||||
actions+=" ↺ Update\n"
|
actions+=" ↺ Update\n"
|
||||||
[[ "$running_count" -gt 0 ]] && actions+=" ■ Stop\n"
|
[[ "$status" == "running" ]] && actions+=" ■ Stop\n"
|
||||||
|
|
||||||
local debug_note=""
|
local debug_note=""
|
||||||
[[ "$is_debug" == "true" ]] && debug_note=" \033[33m[DEBUG — no real changes]\033[0m"
|
[[ "$is_debug" == "true" ]] && debug_note=" \033[33m[DEBUG — no real changes]\033[0m"
|
||||||
|
|||||||
Reference in New Issue
Block a user