diff --git a/.gitignore b/.gitignore index 6e4bc162..5cae88f4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ .agents/ skills-lock.json +# Written by install.sh into end-user clones when setup finishes +.install-complete + # Dependencies node_modules/ diff --git a/CHANGELOG.md b/CHANGELOG.md index aa392fda..2f9b43d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # aicodeman +## 1.6.2 + +### Patch Changes + +- Installer (install.sh) reliability and safety overhaul, prompted by a review of the Linux flow: + - Install-completion marker (`.install-complete`): a bare re-run only takes the quiet update path when a previous install actually finished. Previously, a first install that failed during npm install/build (or was interrupted) left `.git` behind, so the retry silently became an "update" and the user never got the launch menu, the `codeman`/`tmux-chooser` symlinks, the PATH entry, or the `sc` alias. The marker is refreshed by updates and cleared by uninstall when the app dir is kept; added to .gitignore for end-user clones. + - `update` no longer runs an unconditional `git reset --hard` over local changes: interactive runs are asked to stash (declining keeps everything and skips the update), headless runs auto-stash with a dated message (same policy as scripts/self-update.sh). + - Service setup is verified instead of asserted: after starting codeman-web, the installer polls `systemctl --user is-active` (up to 6s) and only then prints "Codeman is running now!"; failures print an honest warning plus status/journalctl hints. Uses `restart` instead of `start` so re-running the installer over an already-running service actually loads the new build. A missing user D-Bus session (e.g. bare `ssh host 'curl | bash'`) is detected up front with copy-paste recovery commands instead of dying mid-setup via `set -e`. macOS gets the equivalent `launchctl list` verification, and the update path verifies its service restart too. The Cloudflare tunnel-service offer is skipped when service setup failed. + - Headless consent guard: with no interactive terminal AND no explicit `CODEMAN_NONINTERACTIVE=1`, the installer now refuses (with instructions) to run sudo package installs (git/node/tmux) or third-party `curl | bash` AI CLI installers, instead of silently taking the default-yes prompts. Explicit `CODEMAN_NONINTERACTIVE=1` keeps the previous full-auto behavior for CI/automation. + - AI CLI gate now recognizes Codex and Gemini (search paths mirrored from the CLI resolvers), so a box with only Codex or Gemini installed is no longer forced to install Claude Code/OpenCode. The install menu gains a "Skip" option (with npm install hints for Codex/Gemini), and the final reminder lists all four CLIs. + + Docs: CLAUDE.md documents `src/remote-reconnect.ts` (pure COD-108 auto-reconnect backoff/eligibility logic) in the Infra table and the remote-sessions pattern. + ## 1.6.1 ### Patch Changes diff --git a/CLAUDE.md b/CLAUDE.md index c556b3c9..da77345a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -60,7 +60,7 @@ When user says "COM": CI runs `npm run check:lockfile` on every push/PR, so lockfile drift fails the build even if the `version-packages` script is bypassed. -**Version**: 1.6.1 (must match `package.json`) +**Version**: 1.6.2 (must match `package.json`) ## Project Overview @@ -135,7 +135,7 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph | **AI** | `src/ai-checker-base.ts`, `src/ai-idle-checker.ts`, `src/ai-plan-checker.ts` | | | **Tasks** | `src/task.ts`, `src/task-queue.ts`, `src/task-tracker.ts` | | | **State** | `src/state-store.ts`, `src/run-summary.ts`, `src/session-lifecycle-log.ts` | | -| **Infra** | `src/hooks-config.ts`, `src/push-store.ts`, `src/tunnel-manager.ts`, `src/image-watcher.ts`, `src/file-stream-manager.ts`, `src/remote-hosts.ts` (remote SSH hosts/cases — see Key Patterns) | | +| **Infra** | `src/hooks-config.ts`, `src/push-store.ts`, `src/tunnel-manager.ts`, `src/image-watcher.ts`, `src/file-stream-manager.ts`, `src/remote-hosts.ts` (remote SSH hosts/cases — see Key Patterns), `src/remote-reconnect.ts` (pure COD-108 auto-reconnect backoff/eligibility; watcher lives in `tmux-manager.ts`) | | | **Search** | `src/search-service.ts` | Pure in-memory core for `GET /api/search` — see Key Patterns | | **Attachments** | `src/attachment-registry.ts`, `src/attachment-magic.ts`, `src/generated-artifact-attachments.ts` (Codex `Saved to:` artifacts), `src/session-attachment-history.ts`, `src/document-preview-cache.ts`, `src/document-thumbnailer.ts`, `src/document-conversion-limiter.ts`, `src/config/attachment-guard.ts` | See Key Patterns | | **Plan** | `src/plan-orchestrator.ts`, `src/prompts/*.ts`, `src/templates/` (`claude-md.ts` + `case-template.md`, the CLAUDE.md scaffold generated into new cases) | | @@ -172,7 +172,7 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph **Cron (cron-style `CronJob`s)**: saved, named jobs with a recurring schedule (`once`/`interval`/`daily`/`weekly`), enable/disable, Run Now, next-run calc, and per-job run history (`CronJobRun`). ⚠️ **Distinct from the legacy `ScheduledRun`** (`/api/scheduled`, a run-now duration-bounded autonomous loop) — the two never interact; the legacy concept keeps the `Scheduled*` names, the recurring-job feature is `Cron*`. `CronService` (`src/cron/cron-service.ts`) owns CRUD + the 30s background due-tick (`tickDueJobs`, registered via `cleanup.setInterval` in `server.ts`; `init()` recomputes nextRunAt on boot) and **reuses the existing session layer** (create → `addSession` → `setupSessionListeners` → `startInteractive`/`startShell` → prompt via `writeViaMux`/`write`) rather than rebuilding tmux logic. Next-run math is pure/unit-tested in `cron-time.ts` (SERVER-LOCAL timezone for daily/weekly). Dup-launch guard = `lastDueKey` (jobId:fireTime); schedule is advanced BEFORE launch so a slow launch can't re-trigger. `once` jobs self-disable after firing (`completedOnce`). Persisted via `AppState.cronJobs`/`cronJobRuns` (StateStore accessors). Routes `/api/cron/jobs*` + `/api/cron/runs` (`cron-routes.ts`, `CronPort`); schema `CronJobSchema` (cross-field `superRefine`; the `.partial()` update schema does NOT re-run it); SSE `cron:*`. Frontend `cron-ui.js` (#cronModal). Claude/shell/opencode/codex/gemini agent types. Tests: `test/cron-time.test.ts`, `test/cron-service.test.ts`. Design: `docs/cron-discovery.md`. -**Remote sessions (SSH)**: Sessions can run the agent inside a durable `tmux -L codeman-remote new-session -A` **on a remote host** so it survives the SSH drop (COD-104), and can also **discover + attach** to `codeman-*` sessions another Codeman launched there — attached (`owned:false`) sessions **detach, never kill** on tab close (COD-105). **Shared/collaborative** (COD-106): remote set-options are scoped per-session (never `-g`) and `window-size latest` lets multiple clients attach the same session at different viewports without clamping to the smallest; a client count surfaces a "shared · N" badge. **Auto-reconnect** (COD-108): a bounded-backoff watcher re-establishes a dropped remote session's local ssh pane and reattaches the still-running durable remote tmux (kill-switch `remoteAutoReconnect`, default ON). Owned sessions propagate `kill-session` to the remote on close; non-owned never do. ⚠️ Command-injection surface (COD-107): all ssh command lines flow through the single shell-safe `buildSshConnectionArgs()` — every user field (`-J jumpHost`, `-i identity`, `-o`) is `shellescape`d; never hand-build an ssh line elsewhere. Full design: `docs/remote-sessions.md`. +**Remote sessions (SSH)**: Sessions can run the agent inside a durable `tmux -L codeman-remote new-session -A` **on a remote host** so it survives the SSH drop (COD-104), and can also **discover + attach** to `codeman-*` sessions another Codeman launched there — attached (`owned:false`) sessions **detach, never kill** on tab close (COD-105). **Shared/collaborative** (COD-106): remote set-options are scoped per-session (never `-g`) and `window-size latest` lets multiple clients attach the same session at different viewports without clamping to the smallest; a client count surfaces a "shared · N" badge. **Auto-reconnect** (COD-108): a bounded-backoff watcher re-establishes a dropped remote session's local ssh pane and reattaches the still-running durable remote tmux (kill-switch `remoteAutoReconnect`, default ON); the pure pieces (backoff schedule, per-session reconnect state, `decideReconnect` eligibility) live in `src/remote-reconnect.ts` (tests: `test/remote-auto-reconnect.test.ts`), while `tmux-manager.ts` owns the live pane probe + timers. Owned sessions propagate `kill-session` to the remote on close; non-owned never do. ⚠️ Command-injection surface (COD-107): all ssh command lines flow through the single shell-safe `buildSshConnectionArgs()` — every user field (`-J jumpHost`, `-i identity`, `-o`) is `shellescape`d; never hand-build an ssh line elsewhere. Full design: `docs/remote-sessions.md`. **External CLI modes (OpenCode, Codex, Gemini)**: `isExternalCliMode()` in `session.ts` (`mode === 'opencode' || 'codex' || 'gemini'`) gates Claude-specific behavior — Ralph tracker, BashToolParser, token/CLI-info parsing, and ❯-prompt readiness detection are all skipped (these CLIs render their own TUIs; readiness = output stabilization instead). All three modes **require tmux — no direct PTY fallback** — because secrets are injected via `tmux setenv` (socket-scoped `${this.tmux()} setenv`, never on the spawn command line): OpenCode gets `OPENCODE_CONFIG_CONTENT` etc., Codex gets `OPENAI_API_KEY`/`CODEX_API_KEY`/`CODEX_HOME` (`setCodexEnvVars`), Gemini gets `GEMINI_API_KEY`/`GOOGLE_API_KEY`/`GOOGLE_CLOUD_PROJECT`/`GOOGLE_APPLICATION_CREDENTIALS`/`GOOGLE_GENAI_USE_VERTEXAI` etc. (`setGeminiEnvVars`, all in `tmux-manager.ts`). Codex specifics: command built by `buildCodexCommand()` (`--model`, `resume `, `--dangerously-bypass-approvals-and-sandbox` from the `codexConfig` payload / `codexDangerouslyBypassApprovals` app setting; `renderMode` is schema-coerced to `'hybrid'`, the only supported mode). Gemini specifics: command built by `buildGeminiCommand()` (`--skip-trust` always, `--approval-mode ` defaulting to `yolo` for parity with Claude's `--dangerously-skip-permissions`, `--model`, `--resume` from the `geminiConfig` payload); availability via `GET /api/gemini/status` — session/quick-start routes fail with `OPERATION_FAILED` + install hint (`npm install -g @google/gemini-cli`) when missing. Codex AND Gemini export `COLORTERM=truecolor` + unset `NO_COLOR` (other modes unset `COLORTERM`); Gemini joins `isAltScreenStripMode()` (Codex/Claude/Gemini are Ink TUIs that repaint inline → strip alt-screen/`3J` so scrollback survives). Codex availability via `GET /api/codex/status`. Frontend: run-mode dropdown → `runCodex()`/`runGemini()` in `session-ui.js` ("Run CX"/"Run GM" labels), App Settings → Codex CLI tab; Respawn/Ralph options are Claude-only, so session options open on the Summary tab for external CLI sessions. ⚠️ `run*()` MUST unwrap the `{success,data}` envelope (`(await res.json()).data.available` / `data.data.sessionId`) — reading the raw shape silently breaks the run. Tests: `test/run-mode-ui.test.ts` + `test/gemini-mode.test.ts` (vm-sandbox harness, no real DOM). diff --git a/install.sh b/install.sh index 8c19bf99..56edb483 100755 --- a/install.sh +++ b/install.sh @@ -5,7 +5,11 @@ # Usage: curl -fsSL https://raw.githubusercontent.com/Ark0N/Codeman/master/install.sh | bash # # Environment variables: -# CODEMAN_NONINTERACTIVE=1 - Skip all prompts (for CI/automation) +# CODEMAN_NONINTERACTIVE=1 - Skip all prompts and accept their defaults +# (CI/automation). Required for headless runs +# that need system changes (sudo package +# installs, AI CLI download); without it those +# steps abort instead of running silently. # CODEMAN_INSTALL_DIR - Custom install directory (default: ~/.codeman/app) # CODEMAN_SKIP_SYSTEMD=1 - Skip systemd/launchd service setup prompt # CODEMAN_NODE_VERSION - Node.js major version to install (default: 22) @@ -53,6 +57,26 @@ OPENCODE_SEARCH_PATHS=( "$HOME/bin/opencode" ) +# Codex CLI search paths (from src/utils/codex-cli-resolver.ts) +CODEX_SEARCH_PATHS=( + "$HOME/.codex/bin/codex" + "$HOME/.local/bin/codex" + "/usr/local/bin/codex" + "$HOME/.bun/bin/codex" + "$HOME/.npm-global/bin/codex" + "$HOME/bin/codex" +) + +# Gemini CLI search paths (from src/utils/gemini-cli-resolver.ts) +GEMINI_SEARCH_PATHS=( + "$HOME/.gemini/bin/gemini" + "$HOME/.local/bin/gemini" + "/usr/local/bin/gemini" + "$HOME/.bun/bin/gemini" + "$HOME/.npm-global/bin/gemini" + "$HOME/bin/gemini" +) + # ============================================================================ # Color Output # ============================================================================ @@ -336,6 +360,62 @@ get_opencode_path() { done } +check_codex() { + if command -v codex &>/dev/null; then + return 0 + fi + + for path in "${CODEX_SEARCH_PATHS[@]}"; do + if [[ -x "$path" ]]; then + return 0 + fi + done + + return 1 +} + +get_codex_path() { + if command -v codex &>/dev/null; then + command -v codex + return + fi + + for path in "${CODEX_SEARCH_PATHS[@]}"; do + if [[ -x "$path" ]]; then + echo "$path" + return + fi + done +} + +check_gemini() { + if command -v gemini &>/dev/null; then + return 0 + fi + + for path in "${GEMINI_SEARCH_PATHS[@]}"; do + if [[ -x "$path" ]]; then + return 0 + fi + done + + return 1 +} + +get_gemini_path() { + if command -v gemini &>/dev/null; then + command -v gemini + return + fi + + for path in "${GEMINI_SEARCH_PATHS[@]}"; do + if [[ -x "$path" ]]; then + echo "$path" + return + fi + done +} + check_cloudflared() { # Check ~/.local/bin first (matches tunnel-manager.ts resolution order) if [[ -x "$HOME/.local/bin/cloudflared" ]]; then @@ -701,6 +781,21 @@ read_reply() { fi } +# headless_guard : refuse consequential system changes (sudo package +# installs, third-party curl | bash installers) when nobody can consent, i.e. +# no terminal AND no explicit CODEMAN_NONINTERACTIVE=1 opt-in. Interactive +# runs fall through to their normal prompt; opted-in automation proceeds with +# the prompt defaults as before. +headless_guard() { + local action="$1" + if [[ "$NONINTERACTIVE" == "1" ]] || has_tty; then + return 0 + fi + error "No interactive terminal, but the installer would need to: $action." + error "Re-run from a terminal to be prompted, or set CODEMAN_NONINTERACTIVE=1 to approve such steps in automation." + exit 1 +} + prompt_yes_no() { local prompt="$1" local default="${2:-y}" @@ -835,6 +930,20 @@ setup_sc_alias() { # Service Setup (Linux systemd / macOS launchd) # ============================================================================ +# Wait briefly for codeman-web.service to report active. A bad node path or a +# busy port makes the unit crash within the first seconds (then sit in +# activating/auto-restart), so a blind "started!" message would be a lie. +verify_systemd_active() { + local attempt + for attempt in 1 2 3; do + sleep 2 + if systemctl --user is-active --quiet codeman-web.service 2>/dev/null; then + return 0 + fi + done + return 1 +} + setup_launchd_service() { local plist_label="com.codeman.web" local agent_dir="$HOME/Library/LaunchAgents" @@ -907,7 +1016,15 @@ EOF launchctl load "$agent_plist" 2>/dev/null || true - success "LaunchAgent installed and started" + # launchctl load is silent about many failures: confirm the agent is loaded + sleep 2 + if launchctl list "$plist_label" &>/dev/null; then + success "LaunchAgent installed and started" + return 0 + fi + warn "LaunchAgent did not load." + warn "Inspect: launchctl list | grep codeman ; tail -20 /tmp/codeman.log" + return 1 } setup_systemd_service() { @@ -941,8 +1058,15 @@ Environment=PATH=$PATH WantedBy=default.target EOF - # Reload systemd - systemctl --user daemon-reload + # Reload systemd. A user D-Bus session is required for systemctl --user + # (missing under bare `ssh host 'curl | bash'` provisioning), so detect + # that up front instead of dying mid-setup with a cryptic trap message. + if ! systemctl --user daemon-reload 2>/dev/null; then + warn "systemctl --user is unavailable (no user D-Bus session?); cannot manage user services here." + warn "Unit written to $service_file. From a normal login shell, enable it with:" + warn " systemctl --user daemon-reload && systemctl --user enable --now codeman-web" + return 1 + fi # Enable service systemctl --user enable codeman-web.service 2>/dev/null || true @@ -952,10 +1076,17 @@ EOF loginctl enable-linger "$USER" 2>/dev/null || true fi - # Start the service immediately - systemctl --user start codeman-web.service 2>/dev/null || true + # (Re)start the service. restart, not start: on a re-run over an existing + # running service, start would be a no-op and leave the OLD build running. + systemctl --user restart codeman-web.service 2>/dev/null || true - success "Systemd service installed and started" + if verify_systemd_active; then + success "Systemd service installed and started" + return 0 + fi + warn "codeman-web.service did not become active." + warn "Inspect: systemctl --user status codeman-web ; journalctl --user -u codeman-web -e" + return 1 } setup_tunnel_service() { @@ -1039,6 +1170,7 @@ main() { # Git info "Checking Git..." if ! check_git; then + headless_guard "install Git (system package via sudo)" if prompt_yes_no "Git is not installed. Install it now?"; then install_dependency "git" "$os" "$distro" else @@ -1057,6 +1189,7 @@ main() { warn "Node.js $node_version is installed but version $MIN_NODE_VERSION+ is required." fi + headless_guard "install Node.js v$TARGET_NODE_VERSION (system package via sudo)" if prompt_yes_no "Install Node.js v$TARGET_NODE_VERSION?"; then install_dependency "node" "$os" "$distro" @@ -1081,6 +1214,7 @@ main() { if check_tmux; then success "tmux is installed" else + headless_guard "install tmux (system package via sudo)" if prompt_yes_no "tmux is not installed. Install it now?"; then install_dependency "tmux" "$os" "$distro" else @@ -1088,9 +1222,11 @@ main() { fi fi - # AI CLI (at least one required: Claude Code or OpenCode) + # AI CLI (Codeman drives one of: Claude Code, OpenCode, Codex, Gemini) local has_claude=false local has_opencode=false + local has_codex=false + local has_gemini=false info "Checking AI CLI tools..." if check_claude; then @@ -1101,29 +1237,39 @@ main() { has_opencode=true success "OpenCode found at $(get_opencode_path)" fi + if check_codex; then + has_codex=true + success "Codex found at $(get_codex_path)" + fi + if check_gemini; then + has_gemini=true + success "Gemini CLI found at $(get_gemini_path)" + fi - if [[ "$has_claude" == "false" ]] && [[ "$has_opencode" == "false" ]]; then + if [[ "$has_claude" == "false" && "$has_opencode" == "false" && "$has_codex" == "false" && "$has_gemini" == "false" ]]; then echo "" - warn "No AI CLI found. Codeman requires at least one: Claude Code or OpenCode." + warn "No AI CLI found. Codeman needs at least one: Claude Code, OpenCode, Codex, or Gemini." + headless_guard "install an AI CLI (curl | bash from its vendor)" echo "" echo -e " ${BOLD}Which AI CLI would you like to install?${NC}" echo -e " ${CYAN}1)${NC} Claude Code (Anthropic)" echo -e " ${CYAN}2)${NC} OpenCode (open-source)" echo -e " ${CYAN}3)${NC} Both" + echo -e " ${CYAN}4)${NC} Skip (I'll install one myself, e.g. Codex or Gemini)" echo "" local cli_choice="" if [[ "$NONINTERACTIVE" == "1" ]] || ! has_tty; then - # Non-interactive: default to Claude Code + # Explicit automation opt-in: default to Claude Code cli_choice="1" - info "No interactive terminal detected: defaulting to Claude Code" + info "CODEMAN_NONINTERACTIVE=1: defaulting to Claude Code" else while true; do - echo -en "${CYAN}Choose [1/2/3]:${NC} " >&2 + echo -en "${CYAN}Choose [1/2/3/4]:${NC} " >&2 read_reply cli_choice || { cli_choice="1"; break; } case "$cli_choice" in - 1|2|3) break ;; - *) echo "Please enter 1, 2, or 3." >&2 ;; + 1|2|3|4) break ;; + *) echo "Please enter 1, 2, 3, or 4." >&2 ;; esac done fi @@ -1152,8 +1298,12 @@ main() { fi fi - if [[ "$has_claude" == "false" ]] && [[ "$has_opencode" == "false" ]]; then - die "At least one AI CLI is required. Install manually and re-run the installer." + if [[ "$cli_choice" == "4" ]]; then + warn "Skipping AI CLI install. Codeman will run, but sessions need a CLI to drive." + info "Install one later, e.g.: npm install -g @openai/codex (Codex)" + info " or: npm install -g @google/gemini-cli (Gemini)" + elif [[ "$has_claude" == "false" ]] && [[ "$has_opencode" == "false" ]]; then + die "The selected AI CLI failed to install. Install one manually and re-run the installer." fi fi @@ -1249,6 +1399,16 @@ main() { fi fi + # ======================================================================== + # Mark install complete + # ======================================================================== + + # The dispatcher at the bottom only routes a bare re-run to the quiet + # update path when this marker exists, so an aborted first install + # (failed npm install/build, Ctrl+C) re-runs the full setup flow + # (symlinks, PATH, launch menu) instead of silently "updating". + date -u +%Y-%m-%dT%H:%M:%SZ > "$INSTALL_DIR/.install-complete" + # ======================================================================== # Launch Options # ======================================================================== @@ -1325,14 +1485,16 @@ main() { # Handle service setup if [[ "$launch_choice" == "2" ]]; then + local service_ok=true if [[ "$service_type" == "launchd" ]]; then - setup_launchd_service + setup_launchd_service || service_ok=false else - setup_systemd_service + setup_systemd_service || service_ok=false fi - # 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 + # Offer tunnel service if cloudflared is available (Linux only: systemd tunnel service). + # Skipped when service setup failed: it needs the same systemctl --user access. + if [[ "$service_ok" == "true" ]] && [[ "$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 @@ -1340,10 +1502,15 @@ main() { fi echo "" - echo -e " ${GREEN}${BOLD}Codeman is running now!${NC}" - echo "" - echo -e " ${CYAN}# Open in browser${NC}" - echo -e " http://localhost:3000" + if [[ "$service_ok" == "true" ]]; then + echo -e " ${GREEN}${BOLD}Codeman is running now!${NC}" + echo "" + echo -e " ${CYAN}# Open in browser${NC}" + echo -e " http://localhost:3000" + else + echo -e " ${YELLOW}${BOLD}The service was set up but is not running yet${NC} (see warnings above)." + echo -e " ${DIM}You can always run it directly:${NC} ${CYAN}codeman web${NC}" + fi echo "" echo -e " ${BOLD}Manage the service:${NC}" echo "" @@ -1392,10 +1559,12 @@ main() { echo -e " https://github.com/Ark0N/Codeman" echo "" - if ! check_claude && ! check_opencode; then + if ! check_claude && ! check_opencode && ! check_codex && ! check_gemini; then echo -e " ${YELLOW}${BOLD}Reminder:${NC} Install at least one AI CLI to start using Codeman:" echo -e " ${CYAN}curl -fsSL https://claude.ai/install.sh | bash${NC} # Claude Code" echo -e " ${CYAN}curl -fsSL https://opencode.ai/install | bash${NC} # OpenCode" + echo -e " ${CYAN}npm install -g @openai/codex${NC} # Codex" + echo -e " ${CYAN}npm install -g @google/gemini-cli${NC} # Gemini" echo "" fi @@ -1427,10 +1596,26 @@ update() { info "Updating Codeman..." cd "$INSTALL_DIR" git remote set-url origin "$REPO_URL" 2>/dev/null || true + + # Never blow away local changes silently (this used to be an unconditional + # reset --hard). Interactive users get a choice; headless runs auto-stash + # so the changes stay recoverable, the same policy as scripts/self-update.sh. + if ! git diff --quiet 2>/dev/null || ! git diff --staged --quiet 2>/dev/null; then + warn "Local changes detected in $INSTALL_DIR" + if prompt_yes_no "Stash local changes and update? (recover with: git stash pop)"; then + git stash push --quiet -m "codeman-installer auto-stash $(date -u +%Y-%m-%dT%H:%M:%SZ)" + info "Local changes stashed (see 'git stash list' in $INSTALL_DIR)" + else + info "Keeping local changes; update skipped." + return 0 + fi + fi + git fetch --quiet origin git reset --hard "origin/$BRANCH" --quiet npm install --quiet --no-fund --no-audit 2>/dev/null || npm install --no-fund --no-audit npm run build --quiet 2>/dev/null || npm run build + date -u +%Y-%m-%dT%H:%M:%SZ > "$INSTALL_DIR/.install-complete" success "Updated to $(node -e "console.log(require('./package.json').version)")" echo "" @@ -1438,8 +1623,13 @@ update() { 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" + systemctl --user restart codeman-web.service 2>/dev/null || true + if verify_systemd_active; then + success "codeman-web service restarted" + else + warn "codeman-web.service did not come back up." + warn "Inspect: systemctl --user status codeman-web ; journalctl --user -u codeman-web -e" + fi elif [[ -f "$agent_plist" ]]; then info "Restarting LaunchAgent..." launchctl unload "$agent_plist" 2>/dev/null || true @@ -1508,6 +1698,9 @@ uninstall() { rm -rf "$INSTALL_DIR" success "Removed $INSTALL_DIR" else + # Clear the marker so a future installer run does full setup again + # (the symlinks and services being removed here need recreating). + rm -f "$INSTALL_DIR/.install-complete" info "Kept $INSTALL_DIR" fi fi @@ -1537,7 +1730,10 @@ case "${1:-}" in update) update ;; uninstall) uninstall ;; *) - if [[ -z "${1:-}" && -d "$INSTALL_DIR/.git" ]]; then + # Only a COMPLETED install re-runs as a quiet update. A partial one + # (clone succeeded but build/menu never finished) lacks the marker and + # re-runs the full flow, so a failed first attempt can actually finish. + if [[ -z "${1:-}" && -d "$INSTALL_DIR/.git" && -f "$INSTALL_DIR/.install-complete" ]]; then print_banner update else diff --git a/package-lock.json b/package-lock.json index 1106393b..0038fb62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "aicodeman", - "version": "1.6.1", + "version": "1.6.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aicodeman", - "version": "1.6.1", + "version": "1.6.2", "hasInstallScript": true, "license": "MIT", "workspaces": [ diff --git a/package.json b/package.json index f9d091bf..05e32124 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aicodeman", - "version": "1.6.1", + "version": "1.6.2", "description": "Mission control for AI coding agents - run 20 autonomous agents with real-time monitoring and session persistence", "type": "module", "main": "dist/index.js",