77# Environment variables:
88# CODEMAN_NONINTERACTIVE=1 - Skip all prompts (for CI/automation)
99# CODEMAN_INSTALL_DIR - Custom install directory (default: ~/.codeman/app)
10- # CODEMAN_SKIP_SYSTEMD=1 - Skip systemd service setup prompt
10+ # CODEMAN_SKIP_SYSTEMD=1 - Skip systemd/launchd service setup prompt
1111# CODEMAN_NODE_VERSION - Node.js major version to install (default: 22)
1212# CODEMAN_REPO_URL - Custom git repository URL (default: upstream Codeman)
1313# CODEMAN_BRANCH - Git branch to install (default: master)
@@ -799,9 +799,84 @@ setup_sc_alias() {
799799}
800800
801801# ============================================================================
802- # Systemd Service Setup (Linux only )
802+ # Service Setup (Linux systemd / macOS launchd )
803803# ============================================================================
804804
805+ setup_launchd_service () {
806+ local plist_label=" com.codeman.web"
807+ local agent_dir=" $HOME /Library/LaunchAgents"
808+ local agent_plist=" $agent_dir /$plist_label .plist"
809+ local daemon_plist=" /Library/LaunchDaemons/$plist_label .plist"
810+
811+ info " Setting up macOS LaunchAgent..."
812+
813+ # Remove any existing LaunchDaemon (system-level) to prevent duplicates.
814+ # We standardize on LaunchAgent (user-level) — it doesn't require sudo,
815+ # inherits the user's environment, and is the correct choice for user apps.
816+ if [[ -f " $daemon_plist " ]]; then
817+ warn " Found system-level LaunchDaemon at $daemon_plist — removing to prevent duplicate"
818+ sudo launchctl unload " $daemon_plist " 2> /dev/null || true
819+ sudo rm -f " $daemon_plist "
820+ success " Removed duplicate LaunchDaemon"
821+ fi
822+
823+ # Unload existing agent before overwriting
824+ if [[ -f " $agent_plist " ]]; then
825+ launchctl unload " $agent_plist " 2> /dev/null || true
826+ fi
827+
828+ mkdir -p " $agent_dir "
829+
830+ # Build PATH: ensure /opt/homebrew/bin (Apple Silicon) and ~/.local/bin are included
831+ local svc_path=" /opt/homebrew/bin:/usr/local/bin:$HOME /.local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
832+
833+ # Find node binary path
834+ local node_path
835+ node_path=$( command -v node)
836+
837+ cat > " $agent_plist " << EOF
838+ <?xml version="1.0" encoding="UTF-8"?>
839+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
840+ <plist version="1.0">
841+ <dict>
842+ <key>Label</key>
843+ <string>$plist_label </string>
844+ <key>ProgramArguments</key>
845+ <array>
846+ <string>$node_path </string>
847+ <string>$INSTALL_DIR /dist/index.js</string>
848+ <string>web</string>
849+ </array>
850+ <key>EnvironmentVariables</key>
851+ <dict>
852+ <key>PATH</key>
853+ <string>$svc_path </string>
854+ <key>HOME</key>
855+ <string>$HOME </string>
856+ <key>LANG</key>
857+ <string>en_US.UTF-8</string>
858+ </dict>
859+ <key>WorkingDirectory</key>
860+ <string>$HOME </string>
861+ <key>RunAtLoad</key>
862+ <true/>
863+ <key>KeepAlive</key>
864+ <true/>
865+ <key>ThrottleInterval</key>
866+ <integer>10</integer>
867+ <key>StandardOutPath</key>
868+ <string>/tmp/codeman.log</string>
869+ <key>StandardErrorPath</key>
870+ <string>/tmp/codeman.log</string>
871+ </dict>
872+ </plist>
873+ EOF
874+
875+ launchctl load " $agent_plist " 2> /dev/null || true
876+
877+ success " LaunchAgent installed and started"
878+ }
879+
805880setup_systemd_service () {
806881 local service_dir=" $HOME /.config/systemd/user"
807882 local service_file=" $service_dir /codeman-web.service"
@@ -1151,17 +1226,25 @@ main() {
11511226 echo " "
11521227
11531228 local launch_choice=" "
1154- local has_systemd=false
1229+ local has_service=false
1230+ local service_type=" "
11551231
11561232 if [[ " $os " == " linux" ]] && [[ " $SKIP_SYSTEMD " != " 1" ]] && command -v systemctl & > /dev/null; then
1157- has_systemd=true
1233+ has_service=true
1234+ service_type=" systemd"
1235+ elif [[ " $os " == " macos" ]] && [[ " $SKIP_SYSTEMD " != " 1" ]]; then
1236+ has_service=true
1237+ service_type=" launchd"
11581238 fi
11591239
1160- if [[ " $has_systemd " == " true" ]]; then
1240+ if [[ " $has_service " == " true" ]]; then
1241+ local service_label=" systemd service"
1242+ [[ " $service_type " == " launchd" ]] && service_label=" LaunchAgent"
1243+
11611244 echo -e " ${BOLD} How would you like to run Codeman?${NC} "
11621245 echo " "
11631246 echo -e " ${CYAN} 1)${NC} Run now in this terminal"
1164- echo -e " ${CYAN} 2)${NC} Install as systemd service (auto-start on boot)"
1247+ echo -e " ${CYAN} 2)${NC} Install as $service_label (auto-start on boot)"
11651248 echo -e " ${CYAN} 3)${NC} Don't start — I'll run it later"
11661249 echo " "
11671250
@@ -1178,7 +1261,7 @@ main() {
11781261 done
11791262 fi
11801263 else
1181- # macOS or no systemd — only offer run now or skip
1264+ # No service manager available — only offer run now or skip
11821265 echo -e " ${BOLD} Would you like to start Codeman now?${NC} "
11831266 echo " "
11841267 echo -e " ${CYAN} 1)${NC} Run now in this terminal"
@@ -1204,12 +1287,16 @@ main() {
12041287
12051288 echo " "
12061289
1207- # Handle systemd setup
1290+ # Handle service setup
12081291 if [[ " $launch_choice " == " 2" ]]; then
1209- setup_systemd_service
1292+ if [[ " $service_type " == " launchd" ]]; then
1293+ setup_launchd_service
1294+ else
1295+ setup_systemd_service
1296+ fi
12101297
1211- # Offer tunnel service if cloudflared is available
1212- if check_cloudflared && [[ -f " $INSTALL_DIR /scripts/codeman-tunnel.service" ]]; then
1298+ # Offer tunnel service if cloudflared is available (Linux only — systemd tunnel service)
1299+ if [[ " $service_type " == " systemd " ]] && check_cloudflared && [[ -f " $INSTALL_DIR /scripts/codeman-tunnel.service" ]]; then
12131300 echo " "
12141301 if prompt_yes_no " Also set up Cloudflare tunnel service? (requires CODEMAN_PASSWORD)" " n" ; then
12151302 setup_tunnel_service
@@ -1224,10 +1311,16 @@ main() {
12241311 echo " "
12251312 echo -e " ${BOLD} Manage the service:${NC} "
12261313 echo " "
1227- echo -e " ${CYAN} systemctl --user stop codeman-web${NC} # Stop"
1228- echo -e " ${CYAN} systemctl --user restart codeman-web${NC} # Restart"
1229- echo -e " ${CYAN} systemctl --user status codeman-web${NC} # Check status"
1230- echo -e " ${CYAN} journalctl --user -u codeman-web -f${NC} # View logs"
1314+ if [[ " $service_type " == " launchd" ]]; then
1315+ echo -e " ${CYAN} launchctl unload ~/Library/LaunchAgents/com.codeman.web.plist${NC} # Stop"
1316+ echo -e " ${CYAN} launchctl load ~/Library/LaunchAgents/com.codeman.web.plist${NC} # Start"
1317+ echo -e " ${CYAN} tail -f /tmp/codeman.log${NC} # View logs"
1318+ else
1319+ echo -e " ${CYAN} systemctl --user stop codeman-web${NC} # Stop"
1320+ echo -e " ${CYAN} systemctl --user restart codeman-web${NC} # Restart"
1321+ echo -e " ${CYAN} systemctl --user status codeman-web${NC} # Check status"
1322+ echo -e " ${CYAN} journalctl --user -u codeman-web -f${NC} # View logs"
1323+ fi
12311324 echo " "
12321325 fi
12331326
@@ -1301,11 +1394,17 @@ update() {
13011394 success " Updated to $( node -e " console.log(require('./package.json').version)" ) "
13021395 echo " "
13031396
1304- # Auto-restart systemd service if it's running, otherwise tell the user
1305- if systemctl --user is-active codeman-web.service & > /dev/null; then
1397+ # Auto-restart service if running, otherwise tell the user
1398+ local agent_plist=" $HOME /Library/LaunchAgents/com.codeman.web.plist"
1399+ if systemctl --user is-active codeman-web.service & > /dev/null 2>&1 ; then
13061400 info " Restarting codeman-web service..."
13071401 systemctl --user restart codeman-web.service
13081402 success " codeman-web service restarted"
1403+ elif [[ -f " $agent_plist " ]]; then
1404+ info " Restarting LaunchAgent..."
1405+ launchctl unload " $agent_plist " 2> /dev/null || true
1406+ launchctl load " $agent_plist " 2> /dev/null || true
1407+ success " LaunchAgent restarted"
13091408 else
13101409 echo -e " ${DIM} Restart codeman web to use the new version:${NC} "
13111410 echo -e " ${CYAN} pkill -f 'codeman.*web'; codeman web &${NC} "
@@ -1318,9 +1417,9 @@ uninstall() {
13181417 info " Uninstalling Codeman..."
13191418 echo " "
13201419
1321- # Stop and remove systemd services
1420+ # Stop and remove systemd services (Linux)
13221421 for svc in codeman-web codeman-tunnel; do
1323- if systemctl --user is-active " ${svc} .service" & > /dev/null; then
1422+ if systemctl --user is-active " ${svc} .service" & > /dev/null 2>&1 ; then
13241423 info " Stopping ${svc} service..."
13251424 systemctl --user stop " ${svc} .service"
13261425 fi
@@ -1336,6 +1435,20 @@ uninstall() {
13361435 done
13371436 systemctl --user daemon-reload 2> /dev/null || true
13381437
1438+ # Stop and remove launchd services (macOS)
1439+ local agent_plist=" $HOME /Library/LaunchAgents/com.codeman.web.plist"
1440+ local daemon_plist=" /Library/LaunchDaemons/com.codeman.web.plist"
1441+ if [[ -f " $agent_plist " ]]; then
1442+ launchctl unload " $agent_plist " 2> /dev/null || true
1443+ rm -f " $agent_plist "
1444+ success " Removed LaunchAgent"
1445+ fi
1446+ if [[ -f " $daemon_plist " ]]; then
1447+ sudo launchctl unload " $daemon_plist " 2> /dev/null || true
1448+ sudo rm -f " $daemon_plist "
1449+ success " Removed LaunchDaemon"
1450+ fi
1451+
13391452 # Remove symlinks
13401453 local symlink_dir=" $HOME /.local/bin"
13411454 if [[ -L " $symlink_dir /codeman" ]]; then
0 commit comments