From 53b473708f549359df9f13c8dc0b31c5a4a28d39 Mon Sep 17 00:00:00 2001 From: arkon Date: Wed, 1 Apr 2026 08:51:37 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20macOS=20support=20=E2=80=94=20HTML=20cac?= =?UTF-8?q?he,=20launchd=20service,=20trust=20dialog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fixes for macOS deployments: 1. HTML cache bug: @fastify/static with preCompressed serves .html.br/.html.gz files, so path.endsWith('.html') missed them — HTML got 1-year immutable cache headers instead of no-cache, causing stale pages after deploys. 2. Installer launchd support: macOS now gets proper LaunchAgent setup (like systemd on Linux). Removes competing LaunchDaemons to prevent duplicate services fighting over the port. Update/uninstall also handle launchd. 3. Trust dialog auto-accept: Claude CLI 2.x shows a workspace trust prompt on first launch per directory. Sessions detect "trust this folder" in PTY output and auto-send Enter, preventing sessions from hanging on startup. Co-Authored-By: Claude Opus 4.6 --- install.sh | 151 ++++++++++++++++++++++++++++++++++++++++------ src/session.ts | 11 ++++ src/web/server.ts | 3 +- 3 files changed, 145 insertions(+), 20 deletions(-) diff --git a/install.sh b/install.sh index bb9103dc..4ced9a39 100755 --- a/install.sh +++ b/install.sh @@ -7,7 +7,7 @@ # Environment variables: # CODEMAN_NONINTERACTIVE=1 - Skip all prompts (for CI/automation) # CODEMAN_INSTALL_DIR - Custom install directory (default: ~/.codeman/app) -# CODEMAN_SKIP_SYSTEMD=1 - Skip systemd service setup prompt +# CODEMAN_SKIP_SYSTEMD=1 - Skip systemd/launchd service setup prompt # CODEMAN_NODE_VERSION - Node.js major version to install (default: 22) # CODEMAN_REPO_URL - Custom git repository URL (default: upstream Codeman) # CODEMAN_BRANCH - Git branch to install (default: master) @@ -799,9 +799,84 @@ setup_sc_alias() { } # ============================================================================ -# Systemd Service Setup (Linux only) +# Service Setup (Linux systemd / macOS launchd) # ============================================================================ +setup_launchd_service() { + local plist_label="com.codeman.web" + local agent_dir="$HOME/Library/LaunchAgents" + local agent_plist="$agent_dir/$plist_label.plist" + local daemon_plist="/Library/LaunchDaemons/$plist_label.plist" + + info "Setting up macOS LaunchAgent..." + + # Remove any existing LaunchDaemon (system-level) to prevent duplicates. + # We standardize on LaunchAgent (user-level) — it doesn't require sudo, + # inherits the user's environment, and is the correct choice for user apps. + if [[ -f "$daemon_plist" ]]; then + warn "Found system-level LaunchDaemon at $daemon_plist — removing to prevent duplicate" + sudo launchctl unload "$daemon_plist" 2>/dev/null || true + sudo rm -f "$daemon_plist" + success "Removed duplicate LaunchDaemon" + fi + + # Unload existing agent before overwriting + if [[ -f "$agent_plist" ]]; then + launchctl unload "$agent_plist" 2>/dev/null || true + fi + + mkdir -p "$agent_dir" + + # Build PATH: ensure /opt/homebrew/bin (Apple Silicon) and ~/.local/bin are included + local svc_path="/opt/homebrew/bin:/usr/local/bin:$HOME/.local/bin:/usr/bin:/bin:/usr/sbin:/sbin" + + # Find node binary path + local node_path + node_path=$(command -v node) + + cat > "$agent_plist" << EOF + + + + + Label + $plist_label + ProgramArguments + + $node_path + $INSTALL_DIR/dist/index.js + web + + EnvironmentVariables + + PATH + $svc_path + HOME + $HOME + LANG + en_US.UTF-8 + + WorkingDirectory + $HOME + RunAtLoad + + KeepAlive + + ThrottleInterval + 10 + StandardOutPath + /tmp/codeman.log + StandardErrorPath + /tmp/codeman.log + + +EOF + + launchctl load "$agent_plist" 2>/dev/null || true + + success "LaunchAgent installed and started" +} + setup_systemd_service() { local service_dir="$HOME/.config/systemd/user" local service_file="$service_dir/codeman-web.service" @@ -1151,17 +1226,25 @@ main() { echo "" local launch_choice="" - local has_systemd=false + local has_service=false + local service_type="" if [[ "$os" == "linux" ]] && [[ "$SKIP_SYSTEMD" != "1" ]] && command -v systemctl &>/dev/null; then - has_systemd=true + has_service=true + service_type="systemd" + elif [[ "$os" == "macos" ]] && [[ "$SKIP_SYSTEMD" != "1" ]]; then + has_service=true + service_type="launchd" fi - if [[ "$has_systemd" == "true" ]]; then + if [[ "$has_service" == "true" ]]; then + local service_label="systemd service" + [[ "$service_type" == "launchd" ]] && service_label="LaunchAgent" + echo -e " ${BOLD}How would you like to run Codeman?${NC}" echo "" echo -e " ${CYAN}1)${NC} Run now in this terminal" - echo -e " ${CYAN}2)${NC} Install as systemd service (auto-start on boot)" + echo -e " ${CYAN}2)${NC} Install as $service_label (auto-start on boot)" echo -e " ${CYAN}3)${NC} Don't start — I'll run it later" echo "" @@ -1178,7 +1261,7 @@ main() { done fi else - # macOS or no systemd — only offer run now or skip + # No service manager available — only offer run now or skip echo -e " ${BOLD}Would you like to start Codeman now?${NC}" echo "" echo -e " ${CYAN}1)${NC} Run now in this terminal" @@ -1204,12 +1287,16 @@ main() { echo "" - # Handle systemd setup + # Handle service setup if [[ "$launch_choice" == "2" ]]; then - setup_systemd_service + if [[ "$service_type" == "launchd" ]]; then + setup_launchd_service + else + setup_systemd_service + fi - # Offer tunnel service if cloudflared is available - if check_cloudflared && [[ -f "$INSTALL_DIR/scripts/codeman-tunnel.service" ]]; then + # Offer tunnel service if cloudflared is available (Linux only — systemd tunnel service) + if [[ "$service_type" == "systemd" ]] && check_cloudflared && [[ -f "$INSTALL_DIR/scripts/codeman-tunnel.service" ]]; then echo "" if prompt_yes_no "Also set up Cloudflare tunnel service? (requires CODEMAN_PASSWORD)" "n"; then setup_tunnel_service @@ -1224,10 +1311,16 @@ main() { echo "" echo -e " ${BOLD}Manage the service:${NC}" echo "" - echo -e " ${CYAN}systemctl --user stop codeman-web${NC} # Stop" - echo -e " ${CYAN}systemctl --user restart codeman-web${NC} # Restart" - echo -e " ${CYAN}systemctl --user status codeman-web${NC} # Check status" - echo -e " ${CYAN}journalctl --user -u codeman-web -f${NC} # View logs" + if [[ "$service_type" == "launchd" ]]; then + echo -e " ${CYAN}launchctl unload ~/Library/LaunchAgents/com.codeman.web.plist${NC} # Stop" + echo -e " ${CYAN}launchctl load ~/Library/LaunchAgents/com.codeman.web.plist${NC} # Start" + echo -e " ${CYAN}tail -f /tmp/codeman.log${NC} # View logs" + else + echo -e " ${CYAN}systemctl --user stop codeman-web${NC} # Stop" + echo -e " ${CYAN}systemctl --user restart codeman-web${NC} # Restart" + echo -e " ${CYAN}systemctl --user status codeman-web${NC} # Check status" + echo -e " ${CYAN}journalctl --user -u codeman-web -f${NC} # View logs" + fi echo "" fi @@ -1301,11 +1394,17 @@ update() { success "Updated to $(node -e "console.log(require('./package.json').version)")" echo "" - # Auto-restart systemd service if it's running, otherwise tell the user - if systemctl --user is-active codeman-web.service &>/dev/null; then + # Auto-restart service if running, otherwise tell the user + local agent_plist="$HOME/Library/LaunchAgents/com.codeman.web.plist" + if systemctl --user is-active codeman-web.service &>/dev/null 2>&1; then info "Restarting codeman-web service..." systemctl --user restart codeman-web.service success "codeman-web service restarted" + elif [[ -f "$agent_plist" ]]; then + info "Restarting LaunchAgent..." + launchctl unload "$agent_plist" 2>/dev/null || true + launchctl load "$agent_plist" 2>/dev/null || true + success "LaunchAgent restarted" else echo -e " ${DIM}Restart codeman web to use the new version:${NC}" echo -e " ${CYAN}pkill -f 'codeman.*web'; codeman web &${NC}" @@ -1318,9 +1417,9 @@ uninstall() { info "Uninstalling Codeman..." echo "" - # Stop and remove systemd services + # Stop and remove systemd services (Linux) for svc in codeman-web codeman-tunnel; do - if systemctl --user is-active "${svc}.service" &>/dev/null; then + if systemctl --user is-active "${svc}.service" &>/dev/null 2>&1; then info "Stopping ${svc} service..." systemctl --user stop "${svc}.service" fi @@ -1336,6 +1435,20 @@ uninstall() { done systemctl --user daemon-reload 2>/dev/null || true + # Stop and remove launchd services (macOS) + local agent_plist="$HOME/Library/LaunchAgents/com.codeman.web.plist" + local daemon_plist="/Library/LaunchDaemons/com.codeman.web.plist" + if [[ -f "$agent_plist" ]]; then + launchctl unload "$agent_plist" 2>/dev/null || true + rm -f "$agent_plist" + success "Removed LaunchAgent" + fi + if [[ -f "$daemon_plist" ]]; then + sudo launchctl unload "$daemon_plist" 2>/dev/null || true + sudo rm -f "$daemon_plist" + success "Removed LaunchDaemon" + fi + # Remove symlinks local symlink_dir="$HOME/.local/bin" if [[ -L "$symlink_dir/codeman" ]]; then diff --git a/src/session.ts b/src/session.ts index a60557cb..918714b6 100644 --- a/src/session.ts +++ b/src/session.ts @@ -274,6 +274,7 @@ export class Session extends EventEmitter { private _lastPromptTime: number = 0; private activityTimeout: NodeJS.Timeout | null = null; private _awaitingIdleConfirmation: boolean = false; // Prevents timeout reset during idle detection + private _trustDialogAccepted: boolean = false; // Prevents repeated trust dialog auto-accept private _taskTracker: TaskTracker; // Token tracking for auto-clear @@ -1118,6 +1119,16 @@ export class Session extends EventEmitter { this._handleTerminalOutput(data); + // === Auto-accept workspace trust dialog === + // Claude CLI 2.x shows "Yes, I trust this folder" prompt on first launch per directory. + // Codeman sessions always use --dangerously-skip-permissions, so auto-accept. + if (!this._trustDialogAccepted && data.includes('trust this folder')) { + this._trustDialogAccepted = true; + console.log(`[Session] Auto-accepting workspace trust dialog for: ${this.id}`); + // Send Enter to accept the default selection ("Yes, I trust this folder") + this.writeViaMux('\r'); + } + // === Idle/working detection runs on every chunk (latency-sensitive) === // Detect if Claude is working or at prompt // The prompt line contains "❯" when waiting for input diff --git a/src/web/server.ts b/src/web/server.ts index 5395d04b..f918dcbd 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -544,7 +544,8 @@ export class WebServer extends EventEmitter { cacheControl: false, preCompressed: true, setHeaders: (res, path) => { - if (path.endsWith('.html')) { + // Use .includes() not .endsWith() — preCompressed serves .html.br/.html.gz + if (path.includes('.html')) { res.setHeader('Cache-Control', 'no-cache'); } else { res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');