Files
s-b-repo-rustsploit/scripts/setup_stack.py
T
2026-05-10 20:28:09 +02:00

786 lines
27 KiB
Python

#!/usr/bin/env python3
"""
RustSploit + ArcticAlopex bootstrap helper.
Discovers the two halves of the stack (Rust backend in `Cargo.toml`,
Next.js control plane in `arcticalopex/package.json`) starting from the
current directory or `--path <dir>`, then either runs the install / build
/ start steps automatically, or prints the exact commands so the user can
run them by hand.
Usage:
# auto-pilot the whole stack from the repo root
python3 scripts/setup_stack.py auto
# auto from anywhere, pointing at an explicit checkout
python3 scripts/setup_stack.py auto --path /home/me/rustsploit-main
# don't run anything; print the commands the auto path would issue
python3 scripts/setup_stack.py manual
# individual steps (composable)
python3 scripts/setup_stack.py doctor
python3 scripts/setup_stack.py docker up
python3 scripts/setup_stack.py rust build
python3 scripts/setup_stack.py rust run
python3 scripts/setup_stack.py arctic install
python3 scripts/setup_stack.py arctic env # generate .env w/ secrets
python3 scripts/setup_stack.py arctic migrate
python3 scripts/setup_stack.py arctic dev # foreground dev server
python3 scripts/setup_stack.py arctic build # production build
python3 scripts/setup_stack.py arctic start # serve the prod build
python3 scripts/setup_stack.py status # what's installed/running
The tool only uses Python's standard library. It runs on Linux and macOS;
Windows is best-effort (subprocess works, ANSI colors fall back to plain).
"""
from __future__ import annotations
import argparse
import os
import secrets
import shutil
import socket
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, Sequence
# ─── Terminal helpers ────────────────────────────────────────────────────
def _supports_color() -> bool:
if os.environ.get("NO_COLOR"):
return False
return sys.stdout.isatty()
_COLOR = _supports_color()
def _c(code: str, s: str) -> str:
return f"\033[{code}m{s}\033[0m" if _COLOR else s
def info(s: str) -> None:
print(_c("36", "[*]"), s)
def ok(s: str) -> None:
print(_c("32", "[+]"), s)
def warn(s: str) -> None:
print(_c("33", "[!]"), s)
def err(s: str) -> None:
print(_c("31", "[-]"), s, file=sys.stderr)
def head(s: str) -> None:
print()
print(_c("1;36", f"── {s} ──"))
# ─── Repo discovery ──────────────────────────────────────────────────────
@dataclass
class Repo:
root: Path
has_rust: bool
has_arctic: bool
has_compose: bool
@property
def arctic(self) -> Path:
return self.root / "arcticalopex"
@property
def cargo_toml(self) -> Path:
return self.root / "Cargo.toml"
@property
def compose(self) -> Path:
return self.arctic / "docker-compose.yml"
@property
def env_example(self) -> Path:
return self.arctic / ".env.example"
@property
def env_file(self) -> Path:
return self.arctic / ".env"
def discover(start: Path) -> Repo:
"""Walk up from `start` looking for the first dir that contains either
`Cargo.toml` or `arcticalopex/package.json`. We don't insist both are
present — the tool degrades to whichever half exists.
"""
start = start.resolve()
candidate = start
while True:
rust = (candidate / "Cargo.toml").is_file()
arctic = (candidate / "arcticalopex" / "package.json").is_file()
if rust or arctic:
return Repo(
root=candidate,
has_rust=rust,
has_arctic=arctic,
has_compose=(candidate / "arcticalopex" / "docker-compose.yml").is_file(),
)
if candidate.parent == candidate:
err(
"Could not find a RustSploit checkout — neither Cargo.toml nor "
"arcticalopex/package.json exists in the current directory or "
"any parent. Pass --path <dir> to point at a checkout."
)
sys.exit(2)
candidate = candidate.parent
# ─── Tool detection ──────────────────────────────────────────────────────
@dataclass
class Tool:
name: str
cmd: str
install_hint: str
needed_for: str
_TOOLS = [
Tool("rustc", "rustc", "https://rustup.rs/", "rust build/run"),
Tool("cargo", "cargo", "https://rustup.rs/", "rust build/run"),
Tool("node", "node", "https://nodejs.org/ (>=20)", "arctic install/dev/build"),
Tool("pnpm", "pnpm", "npm install -g pnpm", "arctic install/dev/build"),
Tool("docker", "docker", "https://docs.docker.com/engine/install/", "docker up"),
Tool("openssl", "openssl", "system package manager (used to mint MASTER_KEY)", "arctic env"),
]
def which(name: str) -> str | None:
return shutil.which(name)
def doctor(repo: Repo) -> int:
"""Report installed tools. Returns the count of *missing* tools."""
head("Doctor")
info(f"Repo root: {repo.root}")
info(f"Rust backend: {'present' if repo.has_rust else 'missing'}")
info(f"ArcticAlopex: {'present' if repo.has_arctic else 'missing'}")
info(f"docker-compose: {'present' if repo.has_compose else 'missing'}")
missing = 0
print()
for t in _TOOLS:
path = which(t.cmd)
if path:
ok(f"{t.name:<10}{path}")
else:
warn(f"{t.name:<10} — not found ({t.install_hint})")
missing += 1
return missing
# ─── Command runner ──────────────────────────────────────────────────────
def run(
cmd: Sequence[str] | str,
*,
cwd: Path | None = None,
env: dict[str, str] | None = None,
check: bool = True,
dry_run: bool = False,
) -> int:
"""Run a command, streaming its output to the parent terminal. If
`dry_run` is True we just print what *would* run and return 0."""
if isinstance(cmd, str):
rendered = cmd
cmd_list: list[str] | str = cmd
shell = True
else:
rendered = " ".join(_quote(c) for c in cmd)
cmd_list = list(cmd)
shell = False
cwd_str = f" (cwd: {cwd})" if cwd else ""
print(_c("90", f"$ {rendered}{cwd_str}"))
if dry_run:
return 0
proc = subprocess.run(cmd_list, cwd=cwd, env=env, shell=shell)
if check and proc.returncode != 0:
err(f"Command failed (exit {proc.returncode}): {rendered}")
sys.exit(proc.returncode)
return proc.returncode
def _quote(s: str) -> str:
if not s or any(c in s for c in " \t\"'$`\\!"):
return "'" + s.replace("'", "'\\''") + "'"
return s
# ─── Port checks ─────────────────────────────────────────────────────────
def _port_in_use(host: str, port: int) -> bool:
"""Best-effort `is anything listening on host:port`. Checks both IPv4
and IPv6 loopback when the caller passes 127.0.0.1, because Node and
Next dev servers commonly bind to [::1] only and the v4 probe would
otherwise report 'free' while the dev server is plainly running.
"""
candidates: list[tuple[int, str]] = []
if host == "127.0.0.1":
candidates.extend([(socket.AF_INET, "127.0.0.1"), (socket.AF_INET6, "::1")])
elif host == "::1":
candidates.append((socket.AF_INET6, "::1"))
else:
try:
for info in socket.getaddrinfo(host, port, type=socket.SOCK_STREAM):
candidates.append((info[0], info[4][0]))
except socket.gaierror:
return False
for family, addr in candidates:
try:
with socket.socket(family, socket.SOCK_STREAM) as s:
s.settimeout(0.5)
if s.connect_ex((addr, port)) == 0:
return True
except OSError:
continue
return False
def check_ports(ports: Iterable[tuple[str, int, str]]) -> None:
"""Warn (don't fail) when a port we care about is already taken."""
for host, port, label in ports:
if _port_in_use(host, port):
warn(f"{label} port {host}:{port} is already in use")
# ─── .env generation ─────────────────────────────────────────────────────
_ENV_KEYS = [
"POSTGRES_PASSWORD",
"DATABASE_URL",
"REDIS_PASSWORD",
"REDIS_URL",
"MINIO_ENDPOINT",
"MINIO_ROOT_USER",
"MINIO_ROOT_PASSWORD",
"MINIO_ACCESS_KEY",
"MINIO_SECRET_KEY",
"MINIO_BUCKET",
"MASTER_KEY",
"WS_PORT",
"WS_SERVER_URL",
"APP_URL",
"SESSION_TTL_SECONDS",
"INVITE_TTL_SECONDS",
]
def gen_env(repo: Repo, *, overwrite: bool = False, dry_run: bool = False) -> None:
"""Generate `arcticalopex/.env` from `.env.example` with random secrets.
The template's `CHANGE_ME_*` placeholders are replaced with values
minted via `secrets.token_hex(32)`. Existing files are preserved
unless `overwrite=True`.
"""
head("Generate arcticalopex/.env")
if not repo.env_example.is_file():
err(f"Template not found: {repo.env_example}")
sys.exit(1)
if repo.env_file.is_file() and not overwrite:
ok(f"{repo.env_file.relative_to(repo.root)} already exists — leaving it alone")
return
pg_pw = secrets.token_hex(32)
redis_pw = secrets.token_hex(32)
minio_user = "arctic-" + secrets.token_hex(4)
minio_pw = secrets.token_hex(32)
master = secrets.token_hex(32)
body = (
"# Generated by scripts/setup_stack.py — do not commit.\n"
f"POSTGRES_PASSWORD={pg_pw}\n"
f"DATABASE_URL=postgres://arcticalopex:{pg_pw}@127.0.0.1:5432/arcticalopex\n"
f"REDIS_PASSWORD={redis_pw}\n"
f"REDIS_URL=redis://:{redis_pw}@127.0.0.1:6379\n"
f"MINIO_ENDPOINT=http://127.0.0.1:9000\n"
f"MINIO_ROOT_USER={minio_user}\n"
f"MINIO_ROOT_PASSWORD={minio_pw}\n"
f"MINIO_ACCESS_KEY={minio_user}\n"
f"MINIO_SECRET_KEY={minio_pw}\n"
f"MINIO_BUCKET=arcticalopex\n"
f"MASTER_KEY={master}\n"
f"WS_PORT=3001\n"
f"WS_SERVER_URL=ws://127.0.0.1:3001\n"
f"APP_URL=http://127.0.0.1:3000\n"
f"SESSION_TTL_SECONDS=28800\n"
f"INVITE_TTL_SECONDS=259200\n"
)
if dry_run:
info(f"would write {repo.env_file} ({len(body)} bytes, secrets randomised)")
return
repo.env_file.parent.mkdir(parents=True, exist_ok=True)
repo.env_file.write_text(body)
try:
os.chmod(repo.env_file, 0o600)
except OSError:
pass
ok(f"wrote {repo.env_file.relative_to(repo.root)} (mode 0600, secrets randomised)")
def env_keys_used(_unused: tuple[str, ...] = tuple(_ENV_KEYS)) -> tuple[str, ...]:
"""Returned for tests / introspection — the list of keys we manage."""
return _unused
# ─── docker-compose helpers ──────────────────────────────────────────────
def _docker_compose_args(repo: Repo) -> list[str]:
"""Return the executable + leading flags for `docker compose -f …`. We
always run with `--env-file` pointing at the generated `.env` so the
compose template's variable substitution lands the random secrets
instead of the placeholder strings from `.env.example`.
"""
return [
"docker",
"compose",
"-f",
str(repo.compose),
"--env-file",
str(repo.env_file),
]
def docker_up(repo: Repo, *, dry_run: bool = False) -> None:
head("Bring up docker-compose dependencies")
if not repo.has_compose:
err(f"docker-compose.yml not found at {repo.compose}")
sys.exit(1)
if not repo.env_file.is_file():
warn("arcticalopex/.env not found — generating it first.")
gen_env(repo, dry_run=dry_run)
check_ports(
[
("127.0.0.1", 5432, "Postgres"),
("127.0.0.1", 6379, "Redis"),
("127.0.0.1", 9000, "MinIO"),
]
)
run(_docker_compose_args(repo) + ["up", "-d", "postgres", "redis", "minio"], dry_run=dry_run)
def docker_down(repo: Repo, *, dry_run: bool = False) -> None:
head("Stop docker-compose dependencies")
if not repo.has_compose:
err(f"docker-compose.yml not found at {repo.compose}")
sys.exit(1)
run(_docker_compose_args(repo) + ["down"], dry_run=dry_run)
def docker_logs(repo: Repo, follow: bool = False, *, dry_run: bool = False) -> None:
cmd = _docker_compose_args(repo) + ["logs"]
if follow:
cmd.append("-f")
run(cmd, dry_run=dry_run, check=False)
# ─── Rust ────────────────────────────────────────────────────────────────
def rust_build(repo: Repo, *, release: bool = False, dry_run: bool = False) -> None:
head(f"cargo build{' --release' if release else ''}")
if not repo.has_rust:
err("Cargo.toml not found")
sys.exit(1)
cmd = ["cargo", "build"]
if release:
cmd.append("--release")
run(cmd, cwd=repo.root, dry_run=dry_run)
def rust_run(
repo: Repo,
*,
bind: str = "127.0.0.1:8080",
release: bool = False,
dry_run: bool = False,
) -> None:
head("Run RustSploit API server")
if not repo.has_rust:
err("Cargo.toml not found")
sys.exit(1)
check_ports([("127.0.0.1", int(bind.split(":")[-1]), "Rust API")])
cmd = ["cargo", "run"]
if release:
cmd.append("--release")
cmd += ["--", "--api", "--interface", bind]
run(cmd, cwd=repo.root, dry_run=dry_run)
# ─── Arctic ──────────────────────────────────────────────────────────────
def arctic_install(repo: Repo, *, dry_run: bool = False) -> None:
head("Install ArcticAlopex dependencies (pnpm install)")
if not repo.has_arctic:
err("arcticalopex/package.json not found")
sys.exit(1)
run(["pnpm", "install"], cwd=repo.arctic, dry_run=dry_run)
def arctic_migrate(repo: Repo, *, dry_run: bool = False) -> None:
head("Run database migrations (pnpm db:migrate)")
if not repo.env_file.is_file():
warn("arcticalopex/.env not found — generating it first.")
gen_env(repo, dry_run=dry_run)
run(["pnpm", "db:migrate"], cwd=repo.arctic, dry_run=dry_run)
def arctic_dev(repo: Repo, *, dry_run: bool = False) -> None:
head("Start ArcticAlopex dev server (pnpm dev)")
check_ports([("127.0.0.1", 3000, "ArcticAlopex dev"), ("::1", 3000, "ArcticAlopex dev")])
run(["pnpm", "dev"], cwd=repo.arctic, dry_run=dry_run)
def arctic_build(repo: Repo, *, dry_run: bool = False) -> None:
head("Production build (pnpm build)")
run(["pnpm", "build"], cwd=repo.arctic, dry_run=dry_run)
def arctic_start(repo: Repo, *, dry_run: bool = False) -> None:
head("Start ArcticAlopex production server (pnpm start)")
check_ports([("127.0.0.1", 3000, "ArcticAlopex prod")])
run(["pnpm", "start"], cwd=repo.arctic, dry_run=dry_run)
# ─── auto / manual ───────────────────────────────────────────────────────
# `--only` picker — what slice of the stack to operate on.
ONLY_CHOICES = ("both", "rust", "arctic")
def _pick_only(repo: Repo) -> str:
"""Resolve `--only` when the user didn't pass it.
On a TTY we ask. Off a TTY (CI, scripts, piped) we fall back to whatever
the checkout supports — both halves if both exist, otherwise the one
half that does.
"""
if repo.has_rust and not repo.has_arctic:
return "rust"
if repo.has_arctic and not repo.has_rust:
return "arctic"
if not (sys.stdin.isatty() and sys.stdout.isatty()):
return "both"
print()
print(_c("1;36", "Which framework do you want to set up?"))
print(f" [{_c('1', '1')}] both — RustSploit backend + ArcticAlopex frontend (default)")
print(f" [{_c('1', '2')}] rust — RustSploit backend only")
print(f" [{_c('1', '3')}] arctic — ArcticAlopex frontend only")
while True:
try:
ans = input("Choice [1/2/3] (Enter = 1): ").strip().lower()
except EOFError:
return "both"
if ans in ("", "1", "both"):
return "both"
if ans in ("2", "rust", "r"):
return "rust"
if ans in ("3", "arctic", "a", "arcticalopex"):
return "arctic"
warn("Please enter 1, 2, or 3.")
def _resolve_only(repo: Repo, only: str) -> tuple[bool, bool]:
"""Return `(do_rust, do_arctic)` after applying the user's `--only`
choice and dropping things the checkout doesn't actually have. Errors
if the user asked for a half that isn't present so we don't silently
do nothing."""
do_rust = only in ("both", "rust")
do_arctic = only in ("both", "arctic")
if do_rust and not repo.has_rust:
if only == "rust":
err("--only rust was requested but Cargo.toml is not in this checkout.")
sys.exit(2)
do_rust = False
if do_arctic and not repo.has_arctic:
if only == "arctic":
err("--only arctic was requested but arcticalopex/package.json is not in this checkout.")
sys.exit(2)
do_arctic = False
if not (do_rust or do_arctic):
err("Nothing to do — neither half of the stack is present in the resolved repo.")
sys.exit(2)
return do_rust, do_arctic
def auto(
repo: Repo,
*,
only: str = "both",
skip_docker: bool = False,
release: bool = False,
dry_run: bool = False,
) -> None:
do_rust, do_arctic = _resolve_only(repo, only)
head(f"Auto setup ({'rust+arctic' if do_rust and do_arctic else 'rust only' if do_rust else 'arctic only'})")
if doctor(repo) > 0 and not dry_run:
warn("Some tools are missing — install them and re-run, or use 'manual' to print commands.")
# 1. .env (random secrets)
if do_arctic:
gen_env(repo, dry_run=dry_run)
# 2. infrastructure (only matters for the arctic half)
if do_arctic and repo.has_compose and not skip_docker:
docker_up(repo, dry_run=dry_run)
elif do_arctic and skip_docker:
warn("Skipping docker (--no-docker): you must provide Postgres / Redis / MinIO yourself.")
# 3. install + migrate frontend
if do_arctic:
arctic_install(repo, dry_run=dry_run)
arctic_migrate(repo, dry_run=dry_run)
# 4. build backend
if do_rust:
rust_build(repo, release=release, dry_run=dry_run)
head("Done")
info("Next steps:")
if do_rust:
print(_c("90", f" $ python3 {Path(__file__).name} rust run"))
if do_arctic:
print(_c("90", f" $ python3 {Path(__file__).name} arctic dev"))
info("Then open http://127.0.0.1:3000")
def manual(
repo: Repo,
*,
only: str = "both",
skip_docker: bool = False,
release: bool = False,
) -> None:
"""Print every command the auto path would run, in order, without
executing anything. Useful when the user wants to copy-paste into a
notes doc, a runbook, or another shell.
"""
do_rust, do_arctic = _resolve_only(repo, only)
head(f"Manual command list ({'rust+arctic' if do_rust and do_arctic else 'rust only' if do_rust else 'arctic only'})")
info("Run these in order, from anywhere — paths are absolute:")
print()
arctic = repo.arctic
n = 1
if do_arctic:
print(_c("1", f"# {n}. Generate arcticalopex/.env with random secrets"))
print(_c("90", f" python3 {Path(__file__).resolve()} arctic env --path {repo.root}"))
print()
n += 1
if do_arctic and repo.has_compose and not skip_docker:
print(_c("1", f"# {n}. Bring up Postgres / Redis / MinIO"))
compose = " ".join(_quote(c) for c in _docker_compose_args(repo))
print(_c("90", f" {compose} up -d postgres redis minio"))
print()
n += 1
if do_arctic:
print(_c("1", f"# {n}. Install frontend dependencies"))
print(_c("90", f" cd {arctic} && pnpm install"))
print()
n += 1
print(_c("1", f"# {n}. Apply DB migrations"))
print(_c("90", f" cd {arctic} && pnpm db:migrate"))
print()
n += 1
if do_rust:
flag = " --release" if release else ""
print(_c("1", f"# {n}. Build the Rust backend"))
print(_c("90", f" cd {repo.root} && cargo build{flag}"))
print()
n += 1
print(_c("1", f"# {n}. Run the Rust API server (terminal 1)"))
print(
_c(
"90",
f" cd {repo.root} && cargo run{flag} -- --api --interface 127.0.0.1:8080",
)
)
print()
n += 1
if do_arctic:
print(_c("1", f"# {n}. Start the ArcticAlopex dev server (terminal {2 if do_rust else 1})"))
print(_c("90", f" cd {arctic} && pnpm dev"))
print()
info("That's the full sequence. Each step is also exposed as its own subcommand.")
# ─── status ──────────────────────────────────────────────────────────────
def status(repo: Repo) -> None:
head("Status")
info(f"Repo: {repo.root}")
info(f".env file: {'present' if repo.env_file.is_file() else 'missing'}")
if repo.has_compose and which("docker"):
try:
out = subprocess.check_output(
_docker_compose_args(repo) + ["ps", "--format", "{{.Service}}\t{{.State}}\t{{.Health}}"],
text=True,
stderr=subprocess.DEVNULL,
)
print()
print(_c("1", "docker-compose services:"))
print(out.rstrip() or " (none up)")
except subprocess.CalledProcessError:
warn("docker compose ps failed (is Docker running?)")
except FileNotFoundError:
warn("docker not found")
print()
print(_c("1", "ports:"))
for host, port, label in [
("127.0.0.1", 5432, "Postgres"),
("127.0.0.1", 6379, "Redis"),
("127.0.0.1", 9000, "MinIO"),
("127.0.0.1", 8080, "Rust API (default)"),
("127.0.0.1", 3000, "ArcticAlopex (default)"),
("127.0.0.1", 3001, "ArcticAlopex WS"),
]:
marker = _c("32", "open ") if _port_in_use(host, port) else _c("90", "free ")
print(f" {marker} {host}:{port} {label}")
# ─── argparse ────────────────────────────────────────────────────────────
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
prog="setup_stack",
description="Bootstrap RustSploit + ArcticAlopex (auto, manual, or step-by-step).",
)
p.add_argument(
"--path",
type=Path,
default=Path.cwd(),
help="Path to a checkout root (default: cwd).",
)
p.add_argument(
"--dry-run",
action="store_true",
help="Print commands instead of running them (applies to every subcommand).",
)
sub = p.add_subparsers(dest="cmd", required=True)
sub_auto = sub.add_parser("auto", help="Run the full setup sequence end-to-end.")
sub_auto.add_argument(
"--only",
choices=ONLY_CHOICES,
default=None,
help="Which framework(s) to set up. Default: prompt on a TTY, else 'both'.",
)
sub_auto.add_argument("--no-docker", action="store_true", help="Do not bring up docker-compose.")
sub_auto.add_argument("--release", action="store_true", help="Build cargo --release.")
sub_manual = sub.add_parser("manual", help="Print the commands without running them.")
sub_manual.add_argument(
"--only",
choices=ONLY_CHOICES,
default=None,
help="Which framework(s) to print commands for. Default: prompt on a TTY, else 'both'.",
)
sub_manual.add_argument("--no-docker", action="store_true")
sub_manual.add_argument("--release", action="store_true")
sub.add_parser("doctor", help="Check installed tools.")
sub.add_parser("status", help="Show service / port status.")
sub_docker = sub.add_parser("docker", help="docker-compose subcommands.")
sub_docker.add_argument("op", choices=["up", "down", "logs", "logs-follow"])
sub_rust = sub.add_parser("rust", help="Rust backend operations.")
sub_rust.add_argument("op", choices=["build", "run"])
sub_rust.add_argument("--release", action="store_true")
sub_rust.add_argument("--bind", default="127.0.0.1:8080", help="host:port for `rust run`.")
sub_arctic = sub.add_parser("arctic", help="ArcticAlopex frontend operations.")
sub_arctic.add_argument("op", choices=["install", "env", "migrate", "dev", "build", "start"])
sub_arctic.add_argument("--overwrite", action="store_true", help="Overwrite an existing .env (op=env).")
return p
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
repo = discover(Path(args.path))
try:
if args.cmd == "auto":
only = args.only or _pick_only(repo)
auto(repo, only=only, skip_docker=args.no_docker, release=args.release, dry_run=args.dry_run)
elif args.cmd == "manual":
only = args.only or _pick_only(repo)
manual(repo, only=only, skip_docker=args.no_docker, release=args.release)
elif args.cmd == "doctor":
sys.exit(1 if doctor(repo) else 0)
elif args.cmd == "status":
status(repo)
elif args.cmd == "docker":
if args.op == "up":
docker_up(repo, dry_run=args.dry_run)
elif args.op == "down":
docker_down(repo, dry_run=args.dry_run)
elif args.op == "logs":
docker_logs(repo, follow=False, dry_run=args.dry_run)
elif args.op == "logs-follow":
docker_logs(repo, follow=True, dry_run=args.dry_run)
elif args.cmd == "rust":
if args.op == "build":
rust_build(repo, release=args.release, dry_run=args.dry_run)
else: # run
rust_run(repo, bind=args.bind, release=args.release, dry_run=args.dry_run)
elif args.cmd == "arctic":
if args.op == "install":
arctic_install(repo, dry_run=args.dry_run)
elif args.op == "env":
gen_env(repo, overwrite=args.overwrite, dry_run=args.dry_run)
elif args.op == "migrate":
arctic_migrate(repo, dry_run=args.dry_run)
elif args.op == "dev":
arctic_dev(repo, dry_run=args.dry_run)
elif args.op == "build":
arctic_build(repo, dry_run=args.dry_run)
elif args.op == "start":
arctic_start(repo, dry_run=args.dry_run)
except KeyboardInterrupt:
warn("Interrupted")
return 130
return 0
if __name__ == "__main__":
sys.exit(main())