Skip to content

Commit 53b4737

Browse files
Ark0Nclaude
andcommitted
fix: macOS support — HTML cache, launchd service, trust dialog
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 <noreply@anthropic.com>
1 parent 2cba393 commit 53b4737

3 files changed

Lines changed: 145 additions & 20 deletions

File tree

install.sh

Lines changed: 132 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
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+
805880
setup_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

src/session.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ export class Session extends EventEmitter {
274274
private _lastPromptTime: number = 0;
275275
private activityTimeout: NodeJS.Timeout | null = null;
276276
private _awaitingIdleConfirmation: boolean = false; // Prevents timeout reset during idle detection
277+
private _trustDialogAccepted: boolean = false; // Prevents repeated trust dialog auto-accept
277278
private _taskTracker: TaskTracker;
278279

279280
// Token tracking for auto-clear
@@ -1118,6 +1119,16 @@ export class Session extends EventEmitter {
11181119

11191120
this._handleTerminalOutput(data);
11201121

1122+
// === Auto-accept workspace trust dialog ===
1123+
// Claude CLI 2.x shows "Yes, I trust this folder" prompt on first launch per directory.
1124+
// Codeman sessions always use --dangerously-skip-permissions, so auto-accept.
1125+
if (!this._trustDialogAccepted && data.includes('trust this folder')) {
1126+
this._trustDialogAccepted = true;
1127+
console.log(`[Session] Auto-accepting workspace trust dialog for: ${this.id}`);
1128+
// Send Enter to accept the default selection ("Yes, I trust this folder")
1129+
this.writeViaMux('\r');
1130+
}
1131+
11211132
// === Idle/working detection runs on every chunk (latency-sensitive) ===
11221133
// Detect if Claude is working or at prompt
11231134
// The prompt line contains "❯" when waiting for input

src/web/server.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,8 @@ export class WebServer extends EventEmitter {
544544
cacheControl: false,
545545
preCompressed: true,
546546
setHeaders: (res, path) => {
547-
if (path.endsWith('.html')) {
547+
// Use .includes() not .endsWith() — preCompressed serves .html.br/.html.gz
548+
if (path.includes('.html')) {
548549
res.setHeader('Cache-Control', 'no-cache');
549550
} else {
550551
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');

0 commit comments

Comments
 (0)