Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ jobs:
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> $GITHUB_PATH

- name: Install system dependencies
run: sudo apt-get install -y redis-tools

- name: Install dependencies
run: pip install -e ".[test]"

Expand Down
50 changes: 30 additions & 20 deletions bench_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,28 @@
if typing.TYPE_CHECKING:
from bench_cli.core.bench import Bench

_OWN_COMMANDS = frozenset([
"new",
"init",
"start",
"stop",
"restart",
"get-app",
"new-site",
"frappe",
"build",
"update",
"upgrade",
"build-admin",
"setup",
"volume",
"remove-app",
"uninstall-app",
"list-apps",
])
_OWN_COMMANDS = frozenset(
[
"new",
"init",
"start",
"stop",
"restart",
"get-app",
"new-site",
"frappe",
"build",
"update",
"upgrade",
"build-admin",
"setup",
"volume",
"remove-app",
"uninstall-app",
"list-apps",
"status",
]
)
_OWN_GROUP_OPTIONS = frozenset(["--verbose", "--yes", "-y", "--bench", "-b", "--help", "-h"])

# Global bench name selected via -b / --bench; set in main() before dispatch.
Expand Down Expand Up @@ -156,6 +159,7 @@ def _make_parser() -> argparse.ArgumentParser:
p_uninstall.add_argument("app", help="App name to uninstall.")

sub.add_parser("list-apps", help="List apps installed in the bench.")
sub.add_parser("status", help="Show bench status summary.")

p_newsite = sub.add_parser("new-site", help="Create a new site and add it to bench.toml.")
p_newsite.add_argument("name", help="Site name (e.g. site2.localhost).")
Expand Down Expand Up @@ -241,6 +245,7 @@ def main() -> None:
_active_bench = args.bench

import time

verbose = getattr(args, "verbose", False)
_t0 = time.monotonic()
try:
Expand Down Expand Up @@ -342,6 +347,11 @@ def _dispatch(args: argparse.Namespace) -> None:

UpgradeCommand().run()

elif cmd == "status":
from bench_cli.commands.status import StatusCommand

StatusCommand(_load_bench()).run()

elif cmd == "setup":
_dispatch_setup(args)

Expand Down Expand Up @@ -388,7 +398,7 @@ def _dispatch_setup(args: argparse.Namespace) -> None:
SetupLetsEncryptCommand(_load_bench()).run()
elif setup_cmd == "production":
from bench_cli.commands.setup.production import SetupProductionCommand

SetupProductionCommand(_load_bench()).run()
elif setup_cmd == "requirements":
from bench_cli.commands.setup.requirements import SetupRequirementsCommand
Expand Down
11 changes: 4 additions & 7 deletions bench_cli/commands/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@
from bench_cli.exceptions import BenchError
from bench_cli.utils import run_command

_ADMIN_RELEASE_URL = (
"https://github.com/frappe/bench-cli/releases/download/latest-build/admin-frontend.tar.gz"
)
_ADMIN_RELEASE_URL = "https://github.com/frappe/bench-cli/releases/download/latest-build/admin-frontend.tar.gz"


def _cli_root() -> Path:
import bench_cli as _pkg

return Path(_pkg.__file__).parent.parent


Expand Down Expand Up @@ -52,14 +51,12 @@ def run(self) -> None:
if not (frontend / "node_modules").exists():
print("Running npm install...")
run_command(["npm", "install"], cwd=frontend, stream_output=True)
print("Running npm build")
run_command(["npm", "run", "build"], cwd=frontend, stream_output=True)
print("\nAdmin frontend rebuilt successfully.")

def _find_frontend(self) -> Path:
candidate = _cli_root() / "admin" / "frontend"
if (candidate / "package.json").exists():
return candidate
raise BenchError(
"admin/frontend not found. "
"This command requires the bench-cli source directory with admin/frontend/."
)
raise BenchError("admin/frontend not found. This command requires the bench-cli source directory with admin/frontend/.")
2 changes: 1 addition & 1 deletion bench_cli/commands/drop_site.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def _remove_from_bench_toml(self) -> None:
write_toml(bench_toml, raw)

def _reload_nginx(self) -> None:
if not self.bench.config.nginx.enabled:
if not self.bench.config.production.nginx:
return
from bench_cli.managers.nginx_manager import NginxManager
mgr = NginxManager(self.bench)
Expand Down
30 changes: 20 additions & 10 deletions bench_cli/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def __init__(self, bench: Bench) -> None:
self._total_steps = 0

def run(self) -> None:
production = self.bench.config.nginx.enabled
production = self.bench.config.production.nginx
volume_enabled = self.bench.config.volume.enabled
self._total_steps = 10 + (3 if production else 0) + (1 if volume_enabled else 0)

Expand Down Expand Up @@ -62,8 +62,8 @@ def run(self) -> None:
ProcessManagerFactory.create(self.bench).generate_config()

if production:
self._step("Setup supervisor")
self._setup_supervisor()
self._step("Setup process manager")
self._setup_process_manager()
self._step("Setup nginx")
self._setup_nginx()
self._step("Setup Let's Encrypt SSL")
Expand Down Expand Up @@ -92,6 +92,10 @@ def _install_system_packages(self) -> None:
from bench_cli.managers.mariadb_manager import MariaDBManager
from bench_cli.platform import get_package_manager, is_linux

pkg = get_package_manager()
if is_linux():
pkg.update()

mariadb_manager = MariaDBManager(self.bench.config.mariadb)
mariadb_manager.install()
mariadb_manager.start()
Expand All @@ -116,13 +120,19 @@ def _write_common_config_for_production(self, production: bool) -> None:
existing["dns_multitenant"] = 1
common_config_path.write_text(json.dumps(existing, indent=2))

def _setup_supervisor(self) -> None:
from bench_cli.platform import get_package_manager

get_package_manager().install("supervisor")
from bench_cli.managers.supervisor_process_manager import SupervisorProcessManager

mgr = SupervisorProcessManager(self.bench)
def _setup_process_manager(self) -> None:
if self.bench.config.production.lightweight:
from bench_cli.managers.systemd_process_manager import SystemdProcessManager
mgr = SystemdProcessManager(self.bench)
else:
import subprocess
from bench_cli.platform import get_package_manager, is_linux
pkg = get_package_manager()
if is_linux() and not pkg.is_installed("supervisor"):
pkg.install("supervisor")
subprocess.run(["sudo", "systemctl", "disable", "--now", "supervisor"], check=False)
from bench_cli.managers.supervisor_process_manager import SupervisorProcessManager
mgr = SupervisorProcessManager(self.bench)
mgr.install_config()
mgr.reload()

Expand Down
8 changes: 8 additions & 0 deletions bench_cli/commands/new.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@
timeout = 180
password = "{admin_password}"

# ── Production (optional) ─────────────────────────────────────────────────
# Uncomment to enable production mode (nginx + process manager).
# Without this section, 'bench start' uses Procfile-based dev mode.
#
# [production]
# nginx = false # enable nginx reverse proxy
# lightweight = true # true = systemd (lower memory), false = supervisor

# ── Volume (ZFS, optional) ────────────────────────────────────────────────
# Uncomment and configure to use ZFS-based volume management.
# Requires Linux and the zfsutils-linux package.
Expand Down
2 changes: 1 addition & 1 deletion bench_cli/commands/new_site.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def _validate(self) -> None:
raise BenchError(f"App '{app}' is not installed. Run 'bench get-app <repo>' first.")

def _reload_nginx(self) -> None:
if not self.bench.config.nginx.enabled:
if not self.bench.config.production.nginx:
return
from bench_cli.managers.nginx_manager import NginxManager
mgr = NginxManager(self.bench)
Expand Down
2 changes: 1 addition & 1 deletion bench_cli/commands/restart.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def __init__(self, bench: Bench) -> None:
self.bench = bench

def run(self) -> None:
if not self.bench.config.nginx.enabled:
if not self.bench.config.production.enabled:
raise BenchError("'bench restart' is only available in production mode. Use 'bench stop' and 'bench start' in dev mode.")
from bench_cli.managers.supervisor_process_manager import SupervisorProcessManager
manager = ProcessManagerFactory.create(self.bench)
Expand Down
8 changes: 6 additions & 2 deletions bench_cli/commands/setup/nginx.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@ def run(self) -> None:
self._print_site_urls()

def _validate_nginx_enabled(self) -> None:
if not self.bench.config.nginx.enabled:
if not self.bench.config.production.enabled:
raise ConfigError(
"nginx.enabled must be true in bench.toml to run setup nginx."
"[production] is not configured in bench.toml. Add a [production] section to enable production setup."
)
if not self.bench.config.production.nginx:
raise ConfigError(
"production.nginx must be true in bench.toml to run setup nginx."
)

def _ensure_nginx_config_directory(self) -> None:
Expand Down
49 changes: 43 additions & 6 deletions bench_cli/commands/setup/production.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,33 @@ def __init__(self, bench: "Bench") -> None:

def run(self) -> None:
self.bench.config.validate()
self._require_production_enabled()
self._require_linux()
self._write_dns_multitenancy()
self._setup_supervisor()
self._setup_nginx()
self._setup_letsencrypt_if_needed()
if self.bench.config.production.lightweight:
self._setup_systemd()
else:
self._setup_supervisor()
if self.bench.config.production.nginx:
self._setup_nginx()
self._setup_letsencrypt_if_needed()

self._build_admin_for_production()

self._print_summary()

def _require_production_enabled(self) -> None:
if not self.bench.config.production.enabled:
print(
"Error: [production] is not configured in bench.toml. Add a [production] section to enable production setup.",
file=sys.stderr,
)
sys.exit(1)

def _require_linux(self) -> None:
if not is_linux():
print(
"Error: bench setup production only runs on Linux servers.\n"
"On macOS, use 'bench start' for local development.",
"Error: bench setup production only runs on Linux servers.\nOn macOS, use 'bench start' for local development.",
file=sys.stderr,
)
sys.exit(1)
Expand All @@ -41,26 +56,48 @@ def _write_dns_multitenancy(self) -> None:
common_config_path.write_text(json.dumps(existing_data, indent=2))

def _setup_supervisor(self) -> None:
import subprocess
from bench_cli.platform import get_package_manager
get_package_manager().install("supervisor")

pkg = get_package_manager()
if not pkg.is_installed("supervisor"):
pkg.install("supervisor")
subprocess.run(["sudo", "systemctl", "disable", "--now", "supervisor"], check=False)
from bench_cli.managers.supervisor_process_manager import SupervisorProcessManager

mgr = SupervisorProcessManager(self.bench)
mgr.generate_config()
mgr.install_config()
mgr.reload()

def _setup_systemd(self) -> None:
from bench_cli.managers.systemd_process_manager import SystemdProcessManager

mgr = SystemdProcessManager(self.bench)
mgr.generate_config()
mgr.install_config()
mgr.reload()

def _setup_nginx(self) -> None:
from bench_cli.commands.setup.nginx import SetupNginxCommand

SetupNginxCommand(self.bench).run()

def _setup_letsencrypt_if_needed(self) -> None:
if not any(site.config.ssl for site in self.bench.sites()):
return
from bench_cli.commands.setup.letsencrypt import SetupLetsEncryptCommand

SetupLetsEncryptCommand(self.bench).run()

def _build_admin_for_production(self) -> None:
from bench_cli.commands.admin import BuildAdminCommand

BuildAdminCommand().run()

def _print_summary(self) -> None:
from bench_cli.managers.nginx_manager import NginxManager

nginx_manager = NginxManager(self.bench)
print("\nProduction setup complete.")
print("Sites:")
Expand Down
4 changes: 2 additions & 2 deletions bench_cli/commands/setup/requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from typing import TYPE_CHECKING

from bench_cli.utils import run_command
from bench_cli.utils import get_yarn_bin, run_command

if TYPE_CHECKING:
from bench_cli.core.bench import Bench
Expand Down Expand Up @@ -37,4 +37,4 @@ def _install_js(self) -> None:
if not (app.path / "package.json").exists():
continue
print(f"Installing JS requirements for {app.config.name}...")
run_command(["yarn", "install"], cwd=app.path, stream_output=True)
run_command([get_yarn_bin(), "install"], cwd=app.path, stream_output=True)
2 changes: 1 addition & 1 deletion bench_cli/commands/setup_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def run(self) -> None:
print("Updating common_site_config.json...")
self.bench.write_common_site_config()

if self.bench.config.nginx.enabled:
if self.bench.config.production.nginx:
print("Updating nginx configs...")
NginxManager(self.bench).generate_config()
print(" Note: run 'bench setup nginx' to reload nginx with the new config.")
20 changes: 1 addition & 19 deletions bench_cli/commands/start.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from __future__ import annotations

from bench_cli.core.bench import Bench
from bench_cli.exceptions import BenchError
from bench_cli.managers.process_manager import ProcessManagerFactory


Expand All @@ -10,21 +9,4 @@ def __init__(self, bench: Bench) -> None:
self.bench = bench

def run(self) -> None:
process_manager = ProcessManagerFactory.create(self.bench)
if self.bench.config.nginx.enabled:
from bench_cli.managers.supervisor_process_manager import SupervisorProcessManager
assert isinstance(process_manager, SupervisorProcessManager)
if not process_manager.supervisor_conf_path.exists():
raise BenchError(
"Supervisor config not found. "
"Run 'bench setup production' first."
)
process_manager.generate_config()
process_manager.reload()
else:
if not process_manager.procfile_path.exists():
raise BenchError(
f"Procfile not found at {process_manager.procfile_path}. "
"Run 'bench init' first to initialise the bench."
)
process_manager.start()
ProcessManagerFactory.create(self.bench).start()
Loading
Loading