Skip to content

Commit 33c3d4a

Browse files
DavidsonGomesclaude
andcommitted
feat: auto-create evonexus user + systemd service on VPS setup
Setup now detects root-on-VPS (no SUDO_USER) and automatically: - Creates dedicated 'evonexus' user - Copies installation to /home/evonexus/evo-nexus - Installs uv + claude-code for the user - Creates systemd service (evo-nexus) with auto-start on boot - CLI update mode uses systemctl restart when service exists Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9ffde79 commit 33c3d4a

File tree

2 files changed

+166
-34
lines changed

2 files changed

+166
-34
lines changed

cli/bin/cli.mjs

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -222,18 +222,45 @@ async function main() {
222222
console.log(` ${YELLOW}!${RESET} Frontend build failed — run: cd dashboard/frontend && npm run build`);
223223
}
224224

225-
// Restart services if start-services.sh exists
225+
// Restart services — prefer systemd if available, fallback to start-services.sh
226+
const hasSystemd = check("systemctl is-active --quiet evo-nexus 2>/dev/null") ||
227+
check("systemctl is-enabled --quiet evo-nexus 2>/dev/null");
226228
const startScript = resolve(targetPath, "start-services.sh");
227-
if (existsSync(startScript)) {
229+
230+
if (hasSystemd) {
231+
console.log(`\n ${DIM}Restarting via systemd...${RESET}`);
232+
// If install dir differs from service dir, sync files
233+
try {
234+
const serviceDir = execSync("systemctl show evo-nexus -p WorkingDirectory --value 2>/dev/null", { encoding: "utf-8" }).trim();
235+
if (serviceDir && resolve(serviceDir) !== resolve(targetPath)) {
236+
console.log(` ${DIM}Syncing to service directory ${serviceDir}...${RESET}`);
237+
run(`rsync -a --delete --exclude='.venv' --exclude='node_modules' --exclude='logs' --exclude='dashboard/data' "${targetPath}/" "${serviceDir}/"`, { cwd: targetPath });
238+
// Rebuild in service dir
239+
const svcFrontend = resolve(serviceDir, "dashboard", "frontend");
240+
if (existsSync(resolve(svcFrontend, "package.json"))) {
241+
run("npm install --silent && npm run build --silent", { cwd: svcFrontend });
242+
}
243+
// Fix ownership
244+
const serviceUser = execSync("systemctl show evo-nexus -p User --value 2>/dev/null", { encoding: "utf-8" }).trim();
245+
if (serviceUser) {
246+
run(`chown -R ${serviceUser}:${serviceUser} "${serviceDir}"`);
247+
}
248+
}
249+
} catch {}
250+
run("systemctl restart evo-nexus");
251+
} else if (existsSync(startScript)) {
228252
console.log(`\n ${DIM}Restarting services...${RESET}`);
229253
run(`bash ${startScript}`, { cwd: targetPath });
230-
// Wait and verify
254+
}
255+
256+
// Wait and verify
257+
if (hasSystemd || existsSync(startScript)) {
231258
await new Promise(r => setTimeout(r, 3000));
232259
try {
233260
execSync("curl -sf http://localhost:8080/api/version", { timeout: 5000 });
234261
console.log(` ${GREEN}${RESET} Dashboard restarted`);
235262
} catch {
236-
console.log(` ${YELLOW}!${RESET} Dashboard may not have started — check logs/dashboard.log`);
263+
console.log(` ${YELLOW}!${RESET} Dashboard may not have started — check: journalctl -u evo-nexus -n 20`);
237264
}
238265
}
239266

@@ -274,15 +301,27 @@ async function main() {
274301
`);
275302

276303
if (isRemote) {
277-
// Remote mode: services already running, don't suggest make dashboard-app
278-
console.log(` ${BOLD}The dashboard is already running.${RESET}
304+
// Remote mode: services already running via systemd
305+
const hasSvc = check("systemctl is-enabled --quiet evo-nexus 2>/dev/null");
306+
if (hasSvc) {
307+
console.log(` ${BOLD}The dashboard is running via systemd.${RESET}
308+
Open the URL shown above to create your admin account.
309+
310+
${BOLD}Useful commands:${RESET}
311+
${CYAN}${RESET} ${BOLD}systemctl restart evo-nexus${RESET} — restart services
312+
${CYAN}${RESET} ${BOLD}systemctl status evo-nexus${RESET} — check status
313+
${CYAN}${RESET} ${BOLD}journalctl -u evo-nexus -f${RESET} — follow logs
314+
${CYAN}${RESET} ${BOLD}su - evonexus${RESET} — switch to service user
315+
`);
316+
} else {
317+
console.log(` ${BOLD}The dashboard is already running.${RESET}
279318
Open the URL shown above to create your admin account.
280319
281320
${BOLD}Useful commands:${RESET}
282321
${CYAN}${RESET} ${BOLD}./start-services.sh${RESET} — restart dashboard services
283-
${CYAN}${RESET} ${BOLD}make scheduler${RESET} — start automated routines
284322
${CYAN}${RESET} ${BOLD}make help${RESET} — see all available commands
285323
`);
324+
}
286325
} else {
287326
console.log(` ${BOLD}Next steps:${RESET}
288327
${CYAN}1.${RESET} cd ${targetDir}

setup.py

Lines changed: 120 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,44 @@ def create_folders(config: dict):
715715
print(f" {GREEN}{RESET} Created workspace folders ({count})")
716716

717717

718+
def _setup_systemd_service(service_user, install_dir, logs_dir):
719+
"""Create and start a systemd service for EvoNexus."""
720+
service_home = f"/home/{service_user}"
721+
service_name = "evo-nexus"
722+
service_file = f"/etc/systemd/system/{service_name}.service"
723+
724+
print(f" {DIM}Creating systemd service...{RESET}")
725+
726+
with open(service_file, "w") as f:
727+
f.write(f"""[Unit]
728+
Description=EvoNexus Dashboard + Scheduler + Terminal Server
729+
After=network.target
730+
Documentation=https://github.com/EvolutionAPI/evo-nexus
731+
732+
[Service]
733+
Type=oneshot
734+
RemainAfterExit=yes
735+
User={service_user}
736+
Group={service_user}
737+
WorkingDirectory={install_dir}
738+
Environment=PATH={service_home}/.local/bin:/usr/local/bin:/usr/bin:/bin
739+
Environment=HOME={service_home}
740+
ExecStart=/bin/bash {install_dir}/start-services.sh
741+
ExecStop=/bin/bash -c 'pkill -f "terminal-server/bin/server.js" 2>/dev/null; pkill -f "dashboard/backend.*app.py" 2>/dev/null'
742+
StandardOutput=append:{logs_dir}/service.log
743+
StandardError=append:{logs_dir}/service.log
744+
745+
[Install]
746+
WantedBy=multi-user.target
747+
""")
748+
749+
os.system("systemctl daemon-reload")
750+
os.system(f"systemctl enable {service_name} >/dev/null 2>&1")
751+
os.system(f"systemctl start {service_name}")
752+
print(f" {GREEN}{RESET} Systemd service created and enabled (auto-starts on boot)")
753+
print(f" {DIM} Manage with: systemctl {{start|stop|restart|status}} {service_name}{RESET}")
754+
755+
718756
def main():
719757
banner()
720758

@@ -845,41 +883,82 @@ def main():
845883
# Data dir for SQLite
846884
(WORKSPACE / "dashboard" / "data").mkdir(parents=True, exist_ok=True)
847885

848-
# Fix ownership BEFORE starting services.
849-
# When running with sudo, all files (including .venv, node_modules,
850-
# frontend dist, data dir) are created as root. The services MUST
851-
# run as the original user, so we chown everything now.
886+
# Determine the service user.
887+
# Priority: SUDO_USER (ran with sudo) > create 'evonexus' user (root on VPS) > current user
852888
sudo_user = os.environ.get("SUDO_USER", "")
853-
if sudo_user and os.getuid() == 0:
854-
print(f" {DIM}Fixing file ownership for {sudo_user}...{RESET}")
855-
os.system(f"chown -R {sudo_user}:{sudo_user} {WORKSPACE}")
856-
# Ensure .venv binaries are executable after chown
857-
os.system(f"chmod -R u+x {WORKSPACE}/.venv/bin/ 2>/dev/null")
858-
run_as = f"su - {sudo_user} -c"
859-
print(f" {GREEN}{RESET} Ownership fixed")
889+
service_user = sudo_user # may be empty
890+
891+
if os.getuid() == 0 and not sudo_user and is_remote:
892+
# Running as root directly (common on VPS) — create dedicated user
893+
service_user = "evonexus"
894+
print(f"\n {DIM}Creating dedicated service user '{service_user}'...{RESET}")
895+
ret = os.system(f"id {service_user} >/dev/null 2>&1")
896+
if ret != 0:
897+
os.system(f"useradd -m -s /bin/bash {service_user}")
898+
print(f" {GREEN}{RESET} User '{service_user}' created")
899+
else:
900+
print(f" {DIM}✓ User '{service_user}' already exists{RESET}")
901+
902+
# Copy installation to user home
903+
service_home = f"/home/{service_user}"
904+
service_dir = f"{service_home}/evo-nexus"
905+
if str(WORKSPACE.resolve()) != service_dir:
906+
print(f" {DIM}Copying installation to {service_dir}...{RESET}")
907+
os.system(f"rm -rf {service_dir}")
908+
os.system(f"cp -a {WORKSPACE} {service_dir}")
909+
print(f" {GREEN}{RESET} Copied to {service_dir}")
910+
# Update WORKSPACE reference for start-services.sh
911+
install_dir = Path(service_dir)
912+
913+
# Install uv + claude-code for the service user
914+
ret = os.system(f"su - {service_user} -c 'command -v uv' >/dev/null 2>&1")
915+
if ret != 0:
916+
print(f" {DIM}Installing uv for {service_user}...{RESET}")
917+
os.system(f"su - {service_user} -c 'curl -LsSf https://astral.sh/uv/install.sh | sh' >/dev/null 2>&1")
918+
print(f" {GREEN}{RESET} uv installed")
919+
920+
ret = os.system(f"su - {service_user} -c 'export PATH=$HOME/.local/bin:$PATH && command -v claude' >/dev/null 2>&1")
921+
if ret != 0:
922+
print(f" {DIM}Installing Claude Code for {service_user}...{RESET}")
923+
os.system(f"su - {service_user} -c 'npm install -g @anthropic-ai/claude-code --prefix ~/.local' >/dev/null 2>&1")
924+
print(f" {GREEN}{RESET} Claude Code installed")
925+
926+
# Sync deps as service user
927+
print(f" {DIM}Syncing dependencies as {service_user}...{RESET}")
928+
os.system(f"su - {service_user} -c 'export PATH=$HOME/.local/bin:$PATH && cd {service_dir} && uv sync -q' 2>/dev/null")
929+
930+
# Fix ownership
931+
os.system(f"chown -R {service_user}:{service_user} {service_dir}")
932+
os.system(f"chown -R {service_user}:{service_user} {service_home}")
860933
else:
861-
run_as = "bash -c"
934+
install_dir = WORKSPACE
935+
936+
# Fix ownership BEFORE starting services.
937+
if service_user and os.getuid() == 0:
938+
target_dir = install_dir if service_user == "evonexus" else WORKSPACE
939+
print(f" {DIM}Fixing file ownership for {service_user}...{RESET}")
940+
os.system(f"chown -R {service_user}:{service_user} {target_dir}")
941+
os.system(f"chmod -R u+x {target_dir}/.venv/bin/ 2>/dev/null")
942+
print(f" {GREEN}{RESET} Ownership fixed")
862943

863944
# Start dashboard services
864-
logs_dir = WORKSPACE / "logs"
945+
logs_dir = install_dir / "logs"
865946
logs_dir.mkdir(exist_ok=True)
866-
if sudo_user and os.getuid() == 0:
867-
os.system(f"chown -R {sudo_user}:{sudo_user} {logs_dir}")
947+
if service_user and os.getuid() == 0:
948+
os.system(f"chown -R {service_user}:{service_user} {logs_dir}")
949+
868950
print(f"\n {DIM}Starting dashboard services...{RESET}")
869-
# Stop any existing services (systemd, background processes)
870-
os.system("systemctl stop evonexus 2>/dev/null; systemctl disable evonexus 2>/dev/null")
951+
# Stop any existing services
952+
os.system("systemctl stop evo-nexus 2>/dev/null")
871953
os.system("pkill -f 'terminal-server/bin/server.js' 2>/dev/null")
872954
os.system("pkill -f 'app.py' 2>/dev/null")
873955
os.system("sleep 1")
874-
if sudo_user:
875-
print(f" {DIM}(services will run as {sudo_user}, not root){RESET}")
876956

877-
# Start terminal-server
878-
# Create a startup script that persists processes properly
879-
startup_script = WORKSPACE / "start-services.sh"
957+
# Create start-services.sh
958+
startup_script = install_dir / "start-services.sh"
880959
startup_script.write_text(f"""#!/bin/bash
881960
export PATH="/usr/local/bin:/usr/bin:/bin:$HOME/.local/bin"
882-
cd {WORKSPACE}
961+
cd {install_dir}
883962
884963
# Kill existing services
885964
pkill -f 'terminal-server/bin/server.js' 2>/dev/null
@@ -894,14 +973,19 @@ def main():
894973
895974
# Start Flask dashboard
896975
cd dashboard/backend
897-
nohup {WORKSPACE}/.venv/bin/python app.py > {logs_dir}/dashboard.log 2>&1 &
976+
nohup {install_dir}/.venv/bin/python app.py > {logs_dir}/dashboard.log 2>&1 &
898977
""", encoding="utf-8")
899978
os.chmod(startup_script, 0o755)
900979

901-
if sudo_user:
902-
os.system(f"su - {sudo_user} -c '{startup_script}'")
980+
# Create systemd service (remote/VPS only, when we have a service user)
981+
if is_remote and service_user and os.getuid() == 0:
982+
_setup_systemd_service(service_user, install_dir, logs_dir)
983+
elif service_user:
984+
print(f" {DIM}(services will run as {service_user}){RESET}")
985+
os.system(f"su - {service_user} -c '{startup_script}'")
903986
else:
904987
os.system(str(startup_script))
988+
905989
import time as _time
906990
_time.sleep(3)
907991
# Verify
@@ -920,6 +1004,15 @@ def main():
9201004
dashboard_url = access_config.get('url', f'http://localhost:{dashboard_port}')
9211005

9221006
if is_remote:
1007+
svc_msg = ""
1008+
if service_user == "evonexus":
1009+
svc_msg = f"""
1010+
Servico systemd:
1011+
{DIM}systemctl status evo-nexus{RESET} — verificar status
1012+
{DIM}systemctl restart evo-nexus{RESET} — reiniciar
1013+
{DIM}journalctl -u evo-nexus -f{RESET} — ver logs
1014+
{DIM}su - evonexus{RESET} — acessar usuario do servico
1015+
"""
9231016
print(f"""
9241017
{GREEN}{'='*50}{RESET}
9251018
{GREEN}Setup concluido!{RESET}
@@ -933,7 +1026,7 @@ def main():
9331026
1. Acesse o link acima e crie sua conta de administrador
9341027
2. Va em {BOLD}Providers{RESET} e configure o AI Provider
9351028
3. Abra um agente e comece a usar!
936-
""")
1029+
{svc_msg}""")
9371030
else:
9381031
print(f"""
9391032
{GREEN}Done!{RESET} Next steps:

0 commit comments

Comments
 (0)