Merge pull request #357 from dignfei/feat/docker-adopt-existing-container

feat(docker): attach a case to an already-running container

Conflicts came from work that landed after the PR was opened, and each is
resolved onto the newer abstraction rather than by keeping the older code:

- `defaultDockerCommandForMode` is registry-driven since #347, so the PR's
  `runsAsRoot` arm became `overlays.docker.rootCommand` (claude only). Claude
  Code still refuses `--dangerously-skip-permissions` as root in 2.1.261 and the
  refusal is visible only inside the container, so an adopted root container
  otherwise just shows a dead pane. Which flag to drop is a per-CLI fact, and
  `test/cli-registry-no-id-branching.test.ts` forbids expressing it as a branch.

- The probe's mode list and its mode -> binary table both duplicated the
  registry. They now read `enabledCliIds()` / `discovery.binaries[0]`, which is
  also what fixes the merge's silent regression: the hand-written list predates
  `omp`, and the run menu gates every docker case on this probe, so owned
  containers would have lost that mode. `shell` needs no arm — it declares no
  binary, so it is dropped from the lookup and reported available regardless.

- The per-mode `mode === 'claude' && !cliDir` chain in `tmux-manager.ts` is one
  `missingCliMessage(mode)` gate since #347; the PR's docker exemption moved onto
  it. Its test now pins the single gate instead of counting seven arms.

- The create arm keeps #349's swap-limit warning filter, which the adopted arm
  never reaches; the run-mode list gains `omp` from #353.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TecFD9hvPYJ1mkkMtBQbT1
This commit is contained in:
Codeman maintainer
2026-09-05 16:21:51 +02:00
144 changed files with 11972 additions and 1772 deletions
+17 -4
View File
@@ -8,7 +8,7 @@
* - SessionConfig — creation-time config (id, workingDir, createdAt)
* - SessionOutput — captured stdout/stderr/exitCode
* - SessionStatus — 'idle' | 'busy' | 'stopped' | 'error'
* - SessionMode — 'claude' | 'shell' | 'opencode' | 'codex' | 'gemini' | 'antigravity' | 'pi' | 'grok' | 'deepseek' (which CLI backend)
* - SessionMode — 'claude' | 'shell' | 'opencode' | 'codex' | 'gemini' | 'antigravity' | 'pi' | 'grok' | 'deepseek' | 'omp' (which CLI backend)
* - ClaudeMode — CLI permission mode ('dangerously-skip-permissions' | 'auto' | 'normal' | 'allowedTools')
* - SessionColor — visual differentiation color
* - OpenCodeConfig — OpenCode-specific settings (model, autoAllowTools, continueSession)
@@ -55,11 +55,12 @@ export type SessionMode =
| 'antigravity'
| 'pi'
| 'grok'
| 'deepseek';
| 'deepseek'
| 'omp';
export type RemoteCommandMode = Extract<
SessionMode,
'shell' | 'claude' | 'opencode' | 'codex' | 'gemini' | 'antigravity' | 'pi' | 'grok' | 'deepseek'
'shell' | 'claude' | 'opencode' | 'codex' | 'gemini' | 'antigravity' | 'pi' | 'grok' | 'deepseek' | 'omp'
>;
/**
@@ -168,7 +169,7 @@ export interface RemoteSessionInfo {
/** Which CLI backends a Docker case can run (same set as remote). */
export type DockerCommandMode = Extract<
SessionMode,
'shell' | 'claude' | 'opencode' | 'codex' | 'gemini' | 'antigravity' | 'pi' | 'grok' | 'deepseek'
'shell' | 'claude' | 'opencode' | 'codex' | 'gemini' | 'antigravity' | 'pi' | 'grok' | 'deepseek' | 'omp'
>;
/** Container engine. Docker and Podman differ in the uid/userns + host-gateway alias. */
@@ -383,6 +384,16 @@ export interface AntigravityConfig {
resumeConversationId?: string;
}
/** OMP CLI session configuration */
export interface OmpConfig {
/** Model identifier (e.g., "crof/glm-5.2"). Passed via --model. */
model?: string;
/** Resume a previous conversation (passed via --resume). */
resumeSessionId?: string;
/** Continue the most recent session in this directory (passed via --continue). */
continueSession?: boolean;
}
/**
* Pi CLI (pi.dev) session configuration.
*
@@ -660,6 +671,8 @@ export interface SessionState {
grokConfig?: GrokConfig;
/** DeepSeek Harness configuration (only for mode === 'deepseek') */
deepSeekConfig?: DeepSeekConfig;
/** OMP-specific configuration (only for mode === 'omp') */
ompConfig?: OmpConfig;
/** Claude conversation session ID to resume after reboot (set by restore script) */
resumeSessionId?: string;
/** Claude CLI effort level (soft default via --settings, switchable in-session via /effort) */
+54 -3
View File
@@ -7,6 +7,10 @@
* (see `dataPath('update-status.json')`) that the browser polls across the
* restart boundary.
*
* The Docker Compose deployment updates in place too (same script, same status
* file) — see `docs/docker-self-update.md` for how the container restarts itself
* and what the environment gate refuses.
*
* Backend logic: `src/web/self-update.ts`. Routes: `src/web/routes/system-routes.ts`
* (`/api/system/update/check`, `POST /api/system/update`, `/api/system/update/status`).
*
@@ -17,11 +21,56 @@
* Which init system supervises the running server (decides how we restart it).
* `launchd-daemon` = a KeepAlive system-level LaunchDaemon (headless Macs, no GUI
* login): restart works by killing the server and letting launchd respawn it.
* `docker-compose` = the Compose deployment (`docker/docker-compose.yaml`): the
* "restart" is the server exiting so the container's `restart: unless-stopped`
* policy relaunches it on the freshly built `dist/`.
*/
export type SupervisorKind = 'systemd' | 'launchd' | 'launchd-daemon' | 'none';
export type SupervisorKind = 'systemd' | 'launchd' | 'launchd-daemon' | 'docker-compose' | 'none';
/** How Codeman was installed — only `git` installs can self-update in place. */
export type InstallKind = 'git' | 'npm' | 'unknown';
/**
* How Codeman was installed. `git` and `docker-compose` can self-update in
* place; `docker-compose` is a git checkout bind-mounted into the container, so
* the pull/build happen on the host filesystem and survive container recreation.
*/
export type InstallKind = 'git' | 'docker-compose' | 'npm' | 'unknown';
/**
* Why an in-place container update is refused. Each is derived mechanically from
* the target release's own files — nothing here depends on a human remembering
* to declare something at release time.
*
* - `dockerfile-changed` / `compose-changed`: the release changes the ENVIRONMENT,
* which a self-restart cannot apply (a restart reuses the existing container's
* image and config). Needs a rebuild + recreate from the host.
* - `env-keys-missing`: the release's `docker/.env.example` gained keys the user's
* `docker/.env` has no value for. Compose interpolates an unset `${VAR}` to the
* EMPTY STRING and starts anyway, so without this check a new required setting
* arrives as a silently blank env var.
* - `no-auto-restart`: the container's restart policy would not bring it back
* after the server exits, so applying the update would take Codeman down.
*/
export type EnvironmentBlockerKind = 'dockerfile-changed' | 'compose-changed' | 'env-keys-missing' | 'no-auto-restart';
/** One reason an in-place container update is refused, with UI-ready text. */
export interface EnvironmentBlocker {
kind: EnvironmentBlockerKind;
/** One-line explanation shown in App Settings → Updates. */
message: string;
/** Optional specifics (e.g. the names of the missing env keys). */
details?: string[];
}
/**
* Result of the environment gate for a candidate release. `checked: false` means
* the gate did not run (not a container install, or the target tag's files could
* not be read) — callers must not treat that as "no blockers".
*/
export interface EnvironmentGate {
checked: boolean;
blockers: EnvironmentBlocker[];
/** The host command that resolves every blocker. */
hostCommand: string;
}
/**
* Lifecycle of a single update run. `idle`/`completed`/`failed`/
@@ -96,6 +145,8 @@ export interface UpdateCheckResult {
/** epoch ms of the check. */
checkedAt: number;
source: 'github-api' | 'git-ls-remote' | 'none';
/** Environment gate for THIS candidate release (container installs only). */
environment?: EnvironmentGate;
error?: string;
}