mirror of
https://github.com/SikongJueluo/Mini-Nav.git
synced 2026-07-12 20:15:31 +08:00
refactor(scripts): extract shared utilities into common module
- Move load_env_file, parse_timeout_seconds, build_remote_script, and build_ssh_command to scripts/common.py - Update remote_docker_run.py to import from common module - Improves code organization and reusability
This commit is contained in:
14
.justfile
14
.justfile
@@ -10,18 +10,10 @@ rsync_flags := "-avLh --progress --stats --itemize-changes"
|
||||
upload_excludes := "--exclude-from=.rsyncignore"
|
||||
|
||||
upload:
|
||||
if command -v cygpath >/dev/null 2>&1 || test -n "${MSYSTEM:-}" || test -n "${CYGWIN:-}"; then \
|
||||
env MSYS2_ARG_CONV_EXCL='*' rsync {{ rsync_flags }} {{ upload_excludes }} . {{ remote_root }}/; \
|
||||
else \
|
||||
rsync {{ rsync_flags }} {{ upload_excludes }} . {{ remote_root }}/; \
|
||||
fi
|
||||
|
||||
download:
|
||||
if command -v cygpath >/dev/null 2>&1 || test -n "${MSYSTEM:-}" || test -n "${CYGWIN:-}"; then \
|
||||
env MSYS2_ARG_CONV_EXCL='*' rsync {{ rsync_flags }} {{ remote_root }}/outputs .; \
|
||||
else \
|
||||
rsync {{ rsync_flags }} {{ remote_root }}/outputs .; \
|
||||
fi
|
||||
|
||||
sync-pkgs:
|
||||
uv sync --inexact
|
||||
@@ -61,9 +53,3 @@ remote-check:
|
||||
|
||||
remote-dry cmd:
|
||||
uv run python scripts/remote_docker_run.py run --dry-run -- {{ quote(cmd) }}
|
||||
|
||||
remote-test:
|
||||
uv run python scripts/remote_docker_run.py run -- "uv run pytest -q"
|
||||
|
||||
remote-test-one path:
|
||||
uv run python scripts/remote_docker_run.py run -- "uv run pytest -q {{ path }}"
|
||||
|
||||
@@ -2,8 +2,17 @@
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"permission": {
|
||||
"bash": {
|
||||
"python": "deny",
|
||||
"python *": "deny",
|
||||
"make": "deny",
|
||||
"make *": "deny"
|
||||
"make *": "deny",
|
||||
"just remote *": "ask"
|
||||
},
|
||||
"read": {
|
||||
".env": "deny"
|
||||
},
|
||||
"edit": {
|
||||
".env": "deny"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,13 @@
|
||||
"-h",
|
||||
"-v"
|
||||
],
|
||||
"reason": "uv run is blocked in this project. Use direct command execution instead."
|
||||
"reason": "Local Environment is not avaiable. Please use \"just remote <cmd>\" to run commands in the remote docker. And remember to run \"just upload\" before run commands in the remote docker."
|
||||
},
|
||||
{
|
||||
"name": "block-uv-sync",
|
||||
"command": "uv",
|
||||
"block_args": ["sync"],
|
||||
"reason": "The project is managed by micromamba and uv. Do not sync packages directly by uv. Use \"just sync-pkgs\" or \"uv sync --inexact\" instead."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Shared utilities for remote Docker / SSH scripts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shlex
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_env_file(path: Path) -> None:
|
||||
if not path.exists():
|
||||
return
|
||||
|
||||
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
||||
line = raw_line.strip()
|
||||
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
|
||||
key, value = line.split("=", 1)
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
|
||||
if not key or key in os.environ:
|
||||
continue
|
||||
|
||||
if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
|
||||
value = value[1:-1]
|
||||
|
||||
os.environ[key] = value
|
||||
|
||||
|
||||
def parse_timeout_seconds(value: str) -> float:
|
||||
value = value.strip().lower()
|
||||
|
||||
if value.endswith("ms"):
|
||||
return float(value[:-2]) / 1000
|
||||
if value.endswith("s"):
|
||||
return float(value[:-1])
|
||||
if value.endswith("m"):
|
||||
return float(value[:-1]) * 60
|
||||
if value.endswith("h"):
|
||||
return float(value[:-1]) * 3600
|
||||
|
||||
return float(value)
|
||||
|
||||
|
||||
def build_remote_script(container: str, workdir: str, command: str) -> str:
|
||||
q_container = shlex.quote(container)
|
||||
q_workdir = shlex.quote(workdir)
|
||||
|
||||
inner_command = (
|
||||
"set -Eeuo pipefail; "
|
||||
'if [ -f "$HOME/.bashrc" ]; then source "$HOME/.bashrc"; fi; '
|
||||
"if command -v direnv >/dev/null 2>&1 && [ -f .envrc ]; then "
|
||||
" direnv allow . >/dev/null 2>&1 || true; "
|
||||
' eval "$(direnv export bash)"; '
|
||||
"fi; "
|
||||
f"{command}"
|
||||
)
|
||||
q_command = shlex.quote(inner_command)
|
||||
|
||||
return f"""
|
||||
set -Eeuo pipefail
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo 'error: docker not found on remote host' >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
if ! docker inspect {q_container} >/dev/null 2>&1; then
|
||||
echo 'error: docker container not found: {container}' >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
if [ "$(docker inspect -f '{{{{.State.Running}}}}' {q_container})" != "true" ]; then
|
||||
echo 'error: docker container is not running: {container}' >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
docker exec -i \\
|
||||
-w {q_workdir} \\
|
||||
{q_container} \\
|
||||
bash -lc {q_command}
|
||||
""".strip()
|
||||
|
||||
|
||||
def build_ssh_command(
|
||||
*,
|
||||
ssh_target: str,
|
||||
ssh_port: str,
|
||||
ssh_opts: str,
|
||||
) -> list[str]:
|
||||
ssh_cmd = [
|
||||
"ssh",
|
||||
"-p",
|
||||
ssh_port,
|
||||
"-o",
|
||||
"BatchMode=yes",
|
||||
"-o",
|
||||
"StrictHostKeyChecking=accept-new",
|
||||
]
|
||||
|
||||
if ssh_opts:
|
||||
ssh_cmd.extend(shlex.split(ssh_opts))
|
||||
|
||||
ssh_cmd.extend([ssh_target, "bash", "-s", "--"])
|
||||
|
||||
return ssh_cmd
|
||||
|
||||
@@ -8,35 +8,19 @@ from pathlib import Path
|
||||
|
||||
import typer
|
||||
|
||||
from common import (
|
||||
build_remote_script,
|
||||
build_ssh_command,
|
||||
load_env_file,
|
||||
parse_timeout_seconds,
|
||||
)
|
||||
|
||||
app = typer.Typer(
|
||||
help="Run commands inside a Docker container on a remote host over SSH.",
|
||||
no_args_is_help=True,
|
||||
)
|
||||
|
||||
|
||||
def load_env_file(path: Path) -> None:
|
||||
if not path.exists():
|
||||
return
|
||||
|
||||
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
||||
line = raw_line.strip()
|
||||
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
|
||||
key, value = line.split("=", 1)
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
|
||||
if not key or key in os.environ:
|
||||
continue
|
||||
|
||||
if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
|
||||
value = value[1:-1]
|
||||
|
||||
os.environ[key] = value
|
||||
|
||||
|
||||
def required_env(name: str) -> str:
|
||||
value = os.environ.get(name)
|
||||
if not value:
|
||||
@@ -49,75 +33,6 @@ def required_env(name: str) -> str:
|
||||
return value
|
||||
|
||||
|
||||
def parse_timeout_seconds(value: str) -> float:
|
||||
value = value.strip().lower()
|
||||
|
||||
if value.endswith("ms"):
|
||||
return float(value[:-2]) / 1000
|
||||
if value.endswith("s"):
|
||||
return float(value[:-1])
|
||||
if value.endswith("m"):
|
||||
return float(value[:-1]) * 60
|
||||
if value.endswith("h"):
|
||||
return float(value[:-1]) * 3600
|
||||
|
||||
return float(value)
|
||||
|
||||
|
||||
def build_remote_script(container: str, workdir: str, command: str) -> str:
|
||||
q_container = shlex.quote(container)
|
||||
q_workdir = shlex.quote(workdir)
|
||||
q_command = shlex.quote(command)
|
||||
|
||||
return f"""
|
||||
set -Eeuo pipefail
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo 'error: docker not found on remote host' >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
if ! docker inspect {q_container} >/dev/null 2>&1; then
|
||||
echo 'error: docker container not found: {container}' >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
if [ "$(docker inspect -f '{{{{.State.Running}}}}' {q_container})" != "true" ]; then
|
||||
echo 'error: docker container is not running: {container}' >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
docker exec -i \\
|
||||
-w {q_workdir} \\
|
||||
{q_container} \\
|
||||
bash -lc {q_command}
|
||||
""".strip()
|
||||
|
||||
|
||||
def build_ssh_command(
|
||||
*,
|
||||
ssh_target: str,
|
||||
ssh_port: str,
|
||||
ssh_opts: str,
|
||||
) -> list[str]:
|
||||
ssh_cmd = [
|
||||
"ssh",
|
||||
"-p",
|
||||
ssh_port,
|
||||
"-o",
|
||||
"BatchMode=yes",
|
||||
"-o",
|
||||
"StrictHostKeyChecking=accept-new",
|
||||
]
|
||||
|
||||
if ssh_opts:
|
||||
ssh_cmd.extend(shlex.split(ssh_opts))
|
||||
|
||||
ssh_cmd.extend([ssh_target, "bash", "-s", "--"])
|
||||
|
||||
return ssh_cmd
|
||||
|
||||
|
||||
def build_user_command(command_parts: list[str]) -> str:
|
||||
if command_parts and command_parts[0] == "--":
|
||||
command_parts = command_parts[1:]
|
||||
|
||||
Reference in New Issue
Block a user