Files
Das-OS/srcpkgs/deployer/files/python/deployer/cli.py
T
dasandClaude Sonnet 5.5 60af93df74
build / build (push) Successful in 21m27s
publisher, deployer: same path logic as the originals
The packaged copies use config.default_path() and pipeline._history_path():
files next to the sources in a checkout, ~/.config/<name> and
~/.local/state/<name> when installed. The copies now differ from the
originals only in the neutral defaults.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 14:55:40 +02:00

172 lines
6.8 KiB
Python

"""Command-line mode: the same pipeline as the TUI, for scripts and quick deploys."""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from . import __version__, config, detect, pipeline, remote, snapshots
from .kinds import Options, Plan
C = {"new": "\033[32m+", "update": "\033[33m~", "delete": "\033[31m-"}
RESET = "\033[0m"
def color(on: bool):
if not on:
C.update(new="+", update="~", delete="-")
globals()["RESET"] = ""
def print_plan(plan: Plan) -> None:
a = plan.app
print(f"\n{a.name} [{a.kind}] {plan.rsync_src} -> {plan.server.target}:{plan.dest}"
f" {('rev ' + plan.git_rev) if plan.git_rev else ''}")
for c in plan.changes:
print(f" {C[c.action]} {c.path}{RESET}")
if not plan.changes:
print(" (no file changes)")
for n in plan.notes:
print(f" ! {n}")
def confirm(question: str, yes: bool) -> bool:
if yes:
return True
if not sys.stdin.isatty():
print("not a terminal: pass --yes to confirm", file=sys.stderr)
return False
return input(f"{question} [y/N] ").strip().lower() in ("y", "yes", "т", "так")
def options(args) -> Options:
return Options(build=not args.no_build, install_deps=args.deps, restart=not args.no_restart,
copy_configs=not args.no_configs, delete=args.delete)
def cmd_list(cfg, args):
st = {} if args.offline else pipeline.status_all(cfg)
for a in cfg.apps:
state, last, code = st.get(a.name, ("", "", ""))
print(f"{a.name:22} {a.kind:15} {state:14} {code:8} {cfg.dest_path(a):48} {last}")
def cmd_deploy(cfg, args):
app = cfg.app(args.app)
plan = pipeline.preview(cfg, app, options(args), log=print)
print_plan(plan)
if args.dry_run:
return 0
if not plan.changes and not plan.first_install and not args.force:
print("nothing to sync: use --force to run install/restart anyway")
return 0
if not confirm("deploy?", args.yes):
return 1
res = pipeline.execute(plan, log=print)
if not res.ok and res.snapshot and confirm(f"roll back to {res.snapshot}?", False):
res = pipeline.rollback(cfg, app, res.snapshot, log=print)
return 0 if res.ok else 2
def cmd_rollback(cfg, args):
app = cfg.app(args.app)
snaps = snapshots.listing(cfg.server, app.name)
if args.list or not snaps:
print("\n".join(snaps) or "no snapshots")
return 0
ts = args.ts or snaps[0]
if not confirm(f"restore {app.name} from {ts}?", args.yes):
return 1
res = pipeline.rollback(cfg, app, ts, log=print)
print(res.message)
return 0 if res.ok else 2
def cmd_logs(cfg, args):
return remote.interactive(cfg.server, pipeline.logs_command(cfg, cfg.app(args.app)))
def cmd_import(cfg, args):
found = detect.import_server(cfg, log=print)
if not found:
print("nothing new on the server")
return 0
ready = [i.app for i in found if i.matched and i.app.kind.startswith("python")]
drafts = [i.app for i in found if i.app not in ready]
print(f"\n{len(ready)} ready, {len(drafts)} as commented drafts")
if confirm(f"append to {cfg.path}?", args.yes):
config.append_apps(cfg.path, ready, drafts)
return 0
def cmd_add(cfg, args):
app = detect.new_app(Path(args.path), args.name, args.kind)
print(config.app_to_toml(app))
if confirm(f"append to {cfg.path}?", args.yes):
config.append_apps(cfg.path, [app])
return 0
def main(argv=None) -> int:
p = argparse.ArgumentParser(prog="deploy", description="Deploy local projects to the server. "
"No command: start the TUI.")
p.add_argument("--config", default=None, help="deploy.toml (default: next to deploy.py)")
p.add_argument("--no-color", action="store_true")
p.add_argument("-V", "--version", action="version", version=__version__)
sub = p.add_subparsers(dest="cmd")
s = sub.add_parser("list", help="apps and their state on the server")
s.add_argument("--offline", action="store_true", help="do not ask the server")
for name, hlp in (("deploy", "preview and deploy an app"), ("preview", "show what a deploy would change")):
s = sub.add_parser(name, help=hlp)
s.add_argument("app")
s.add_argument("-y", "--yes", action="store_true", help="do not ask for confirmation")
s.add_argument("-n", "--dry-run", action="store_true", default=name == "preview")
s.add_argument("--force", action="store_true", help="install/restart even without file changes")
s.add_argument("--no-build", action="store_true")
s.add_argument("--no-restart", action="store_true", help="no restart, no post command, no health check")
s.add_argument("--no-configs", action="store_true", help="do not create missing config links")
s.add_argument("--deps", action="store_true", help="pip install even if requirements.txt is unchanged")
s.add_argument("--delete", action=argparse.BooleanOptionalAction, default=None,
help="delete server files that are absent locally (default: app setting)")
s = sub.add_parser("rollback", help="restore the latest (or given) snapshot")
s.add_argument("app")
s.add_argument("ts", nargs="?")
s.add_argument("--list", action="store_true")
s.add_argument("-y", "--yes", action="store_true")
s = sub.add_parser("logs", help="follow the app log")
s.add_argument("app")
s = sub.add_parser("import", help="add apps that already run on the server to deploy.toml")
s.add_argument("-y", "--yes", action="store_true")
s = sub.add_parser("add", help="add a local project to deploy.toml")
s.add_argument("path")
s.add_argument("--name")
s.add_argument("--kind", choices=config.KINDS)
s.add_argument("-y", "--yes", action="store_true")
sub.add_parser("check", help="validate deploy.toml")
sub.add_parser("tui", help="start the TUI (default)")
args = p.parse_args(argv)
color(sys.stdout.isatty() and not args.no_color)
cfg_path = Path(args.config) if args.config else config.default_path()
try:
cfg = config.load(cfg_path)
except (OSError, config.ConfigError) as e:
print(f"config: {e}", file=sys.stderr)
return 1
if args.cmd in (None, "tui"):
from .tui import run
return run(cfg)
if args.cmd == "check":
print(f"{cfg_path}: {len(cfg.apps)} apps, OK")
return 0
handlers = {"list": cmd_list, "deploy": cmd_deploy, "preview": cmd_deploy, "rollback": cmd_rollback,
"logs": cmd_logs, "import": cmd_import, "add": cmd_add}
try:
return handlers[args.cmd](cfg, args) or 0
except (config.ConfigError, pipeline.DeployError, remote.RemoteError) as e:
print(f"error: {e}", file=sys.stderr)
return 1
except KeyboardInterrupt:
return 130