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_excludes := "--exclude-from=.rsyncignore"
|
||||||
|
|
||||||
upload:
|
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 }}/; \
|
rsync {{ rsync_flags }} {{ upload_excludes }} . {{ remote_root }}/; \
|
||||||
fi
|
|
||||||
|
|
||||||
download:
|
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 .; \
|
rsync {{ rsync_flags }} {{ remote_root }}/outputs .; \
|
||||||
fi
|
|
||||||
|
|
||||||
sync-pkgs:
|
sync-pkgs:
|
||||||
uv sync --inexact
|
uv sync --inexact
|
||||||
@@ -61,9 +53,3 @@ remote-check:
|
|||||||
|
|
||||||
remote-dry cmd:
|
remote-dry cmd:
|
||||||
uv run python scripts/remote_docker_run.py run --dry-run -- {{ quote(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",
|
"$schema": "https://opencode.ai/config.json",
|
||||||
"permission": {
|
"permission": {
|
||||||
"bash": {
|
"bash": {
|
||||||
|
"python": "deny",
|
||||||
|
"python *": "deny",
|
||||||
"make": "deny",
|
"make": "deny",
|
||||||
"make *": "deny"
|
"make *": "deny",
|
||||||
|
"just remote *": "ask"
|
||||||
|
},
|
||||||
|
"read": {
|
||||||
|
".env": "deny"
|
||||||
|
},
|
||||||
|
"edit": {
|
||||||
|
".env": "deny"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,13 @@
|
|||||||
"-h",
|
"-h",
|
||||||
"-v"
|
"-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
|
import typer
|
||||||
|
|
||||||
|
from common import (
|
||||||
|
build_remote_script,
|
||||||
|
build_ssh_command,
|
||||||
|
load_env_file,
|
||||||
|
parse_timeout_seconds,
|
||||||
|
)
|
||||||
|
|
||||||
app = typer.Typer(
|
app = typer.Typer(
|
||||||
help="Run commands inside a Docker container on a remote host over SSH.",
|
help="Run commands inside a Docker container on a remote host over SSH.",
|
||||||
no_args_is_help=True,
|
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:
|
def required_env(name: str) -> str:
|
||||||
value = os.environ.get(name)
|
value = os.environ.get(name)
|
||||||
if not value:
|
if not value:
|
||||||
@@ -49,75 +33,6 @@ def required_env(name: str) -> str:
|
|||||||
return value
|
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:
|
def build_user_command(command_parts: list[str]) -> str:
|
||||||
if command_parts and command_parts[0] == "--":
|
if command_parts and command_parts[0] == "--":
|
||||||
command_parts = command_parts[1:]
|
command_parts = command_parts[1:]
|
||||||
|
|||||||
Reference in New Issue
Block a user