Plan: Switch from Bind Mounts to Named Volumes
Prerequisite / Ordering
This is part 2 of a two-part refactor. Part 1 is omnideck-dev/omnideck#110 (rename computron → omnideck inside the container image). Part 1 must be merged and its image published to ghcr.io/omnideck-dev/omnideck:main before this lands, because:
- The container mount targets change in part 1:
/home/computron → /home/omnideck and /var/lib/computron → /var/lib/omnideck. All mount specs in this plan use the new paths.
- The CLI's
DefaultImage already points at ghcr.io/omnideck-dev/omnideck:main, so no image change is needed here — but if this CLI change ran against a pre-rename image, the volumes would mount at paths the old image never writes to, and no data would persist.
Goal
Replace host-path bind mounts (-v /Users/user/Omnideck:/home/computron) with Docker/Podman named volumes (-v omnideck-home:/home/omnideck) across all platforms (Linux, macOS, Windows) and all container runtimes (Docker Desktop, Colima, Podman, Docker Engine). This eliminates:
- File ownership/permission issues on macOS (Colima, Docker Desktop)
- SELinux
:Z/:U flag complexity on Linux
- The need for
--user flags on Linux
sudo rm -rf during uninstall
- Platform-conditional volume mount logic
Design Decisions
Volume Naming Convention
- Home volume:
{containerName}-home (e.g. omnideck-home)
- State volume:
{containerName}-state (e.g. omnideck-state)
- Derived from
ContainerName in config — guarantees uniqueness across multi-instance installs.
- Derivation happens at use-time, not construction-time. Consumers call accessors —
cfg.HomeVolumeName() / cfg.StateVolumeName() — which return the explicit HomeVolume/StateVolume field if set, else derive {ContainerName}-home / {ContainerName}-state. This avoids two bugs:
DefaultConfig() baking in omnideck-home and then --name/TUI changing ContainerName afterward, leaving a second instance pointed at the first instance's volumes.
- Pre-existing instance YAMLs (which have
shared_dir/state_dir but no volume keys) loading with empty volume names and producing a broken -v :/home/omnideck mount. The derive-on-empty accessor is the one backwards-compat shim we bake in — old configs transparently resolve to the correct derived volume names.
What Happens to SharedDir / StateDir Config Fields?
- SharedDir and StateDir are removed from Config entirely. They are replaced by
HomeVolume and StateVolume fields (string volume names), both optional overrides — empty means "derive from container name" (see accessors above).
- The install TUI no longer asks the user for a shared directory path — the volume name is auto-derived from the container name.
- The
config set command's valid keys change: shared_dir/state_dir → home_volume/state_volume.
- No data migration in the CLI. Old configs load fine (unknown YAML keys are ignored; missing volume keys derive from the container name), but the CLI does nothing about data sitting in old bind-mount directories. Users with existing bind-mount installs run a separate manual migration script (see below) to transfer data and clean up old directories.
Volume Lifecycle
- Install: Volumes are created explicitly via
docker volume create before docker run (ensures they exist and can be labeled/named predictably).
- Uninstall: User is prompted to delete volumes (with optional backup via volume export).
- Update: Volumes are preserved (not deleted) — only the container is recreated. Data persists across image updates.
Backup Mechanism (Runtime-Specific)
- Podman: Use native
podman volume export <name> (writes tar to stdout by default) and podman volume import <name> - (reads from stdin). Clean, purpose-built commands.
- Docker (Docker Desktop, Docker Engine, Colima): No native volume export. Use a throwaway container to tar the volume to stdout:
# Export
docker run --rm -v omnideck-home:/data alpine tar -C /data -c . | gzip > backup.tar.gz
# Import (restore)
docker run --rm -i -v omnideck-home:/data alpine tar -C /data -x < backup.tar.gz
- Both approaches work identically across all platforms (Linux, macOS, Windows) since they operate inside the container runtime.
Platform-Specific Logic That Gets REMOVED
:Z SELinux flags on Linux → gone (named volumes don't need them)
:U user-namespace remap on Linux Podman → gone
--user uid:gid on Linux → gone (named volumes are owned by the container runtime, not the host user)
safeRemoveAll() and its system-path protection → gone (we remove volumes, not host directories)
- The macOS "Docker Desktop" warning about
--network host → gone (we never used --network host anyway)
What Stays Platform-Conditional
OLLAMA_HOST — still needs host.docker.internal on macOS/Windows and host-gateway on Linux. This is about networking, not volumes, and is unchanged.
--add-host=host-gateway:host-gateway on Linux — unchanged (networking, not volumes).
- Memory detection (
/proc/meminfo vs sysctl) — unchanged.
Dev Workflow (Justfile) — Not Impacted
The Omnideck app's Justfile (just dev, just e2e, just manual-test) uses bind mounts directly via docker run — it does NOT go through the omnideck CLI. Bind mounts are intentional in dev because:
- Developers want to inspect files at
~/.computron_9000/home on the host
- E2e/manual-test use temp dirs with cleanup traps that
chown + rm -rf (only works with bind mounts)
The CLI change only affects production installs managed by the omnideck binary. The Justfile is a separate code path and is not modified.
Optional App-Side Cleanup (omnideck repo, not CLI repo)
(Paths below are the post-#110 names.) With named volumes, the general chown -R in container/entrypoint.sh becomes a no-op (named volumes inherit image ownership on first mount). The targeted chowns are still needed. This is an optional cleanup in the omnideck repo:
- Remove:
chown -R omnideck:omnideck /home/omnideck /var/lib/omnideck — redundant with named volumes. Caution: the Justfile dev/e2e flows still bind-mount host dirs into these paths, and they rely on this chown. Only remove it if dev flows are verified unaffected — otherwise keep it (it is harmless with volumes).
- Keep:
chown omnideck:broker /home/omnideck/downloads — sets cross-user ownership
- Keep:
chmod 3770 /home/omnideck/downloads — special setgid+sticky mode
- Keep:
chown -R broker:broker /var/lib/omnideck/vault — credential isolation
- Update comment: the "Set up home dir" comment block references "bind-mounted host dirs" — update to mention named volumes
This is not required for the CLI change to work (the chown is harmless), but is a cleanliness improvement.
Files to Change (in dependency order)
1. config/config.go — Config struct changes
- Replace
SharedDir string and StateDir string with HomeVolume string and StateVolume string (optional overrides, normally empty).
- Add accessors
HomeVolumeName() / StateVolumeName(): return the field if non-empty, else derive {ContainerName}-home / {ContainerName}-state. All consumers go through the accessors, never the raw fields.
DefaultConfig() leaves HomeVolume/StateVolume empty (derived).
- Remove
expandHome calls for SharedDir/StateDir in Load().
- No
MigrateVolumes() method — the derive-on-empty accessors already make old configs (no volume keys) resolve correctly.
2. config/config_test.go — Update tests
- Replace
SharedDir/StateDir assertions with HomeVolume/StateVolume.
- Update
TestDefaultConfig to check volume names.
- No MigrateVolumes test — no backwards compatibility.
3. engine/engine.go — RunOptions struct + Engine interface
- Replace
SharedDir string and StateDir string with HomeVolume string and StateVolume string.
- Add to the
Engine interface:
CreateVolume(name string) error
VolumeExists(name string) (bool, error)
RemoveVolume(name string) error
ExportVolume(name string, w io.Writer) error
4. engine/docker.go — Docker engine implementation
- Add
CreateVolume(name string) error — runs docker volume create name.
- Add
VolumeExists(name string) (bool, error) — runs docker volume inspect name, checks exit code.
- Add
RemoveVolume(name string) error — runs docker volume rm name.
- Add
ExportVolume(name string, w io.Writer) error — runs a throwaway container (docker run --rm -v <name>:/data alpine tar -C /data -c .) piped to the writer.
- Update
buildRunArgs(): replace bind mount args with -v homeVolume:/home/omnideck -v stateVolume:/var/lib/omnideck (the post-#110 container paths). Remove all :Z, :U, --user, and platform-conditional volume logic.
5. engine/podman.go — Podman engine implementation
- Add
CreateVolume(name string) error — runs podman volume create name.
- Add
VolumeExists(name string) (bool, error) — runs podman volume inspect name.
- Add
RemoveVolume(name string) error — runs podman volume rm name.
- Add
ExportVolume(name string, w io.Writer) error — uses Podman's native podman volume export <name> (tar to stdout) piped to the writer (more efficient than throwaway container).
- Update
buildPodmanRunArgs(): same simplification as docker — remove :Z,U, use named volumes mounted at /home/omnideck and /var/lib/omnideck.
6. engine/engine_test.go — Update engine tests
- Update
TestBuildRunArgsLinux: replace bind mount assertions with named volume assertions. Remove :Z checks. Remove --user check.
- Update
TestBuildRunArgsMacOS: replace bind mount assertions with named volume assertions.
- Update
TestBuildRunArgsLinuxSecondInstance: update volume name assertions.
- Update
TestBuildPodmanRunArgsHasReplace: remove :Z,U checks, add named volume checks.
- Update
TestBuildPodmanRunArgsMacOS: remove :Z,U checks, add named volume checks.
- Update
TestBuildRunArgsMemorySet/TestBuildRunArgsMemoryEmpty: update RunOptions fields.
- Add tests for
CreateVolume/VolumeExists/RemoveVolume (mock-based or interface-level).
7. tui/install.go — Install wizard
- Remove
inputSharedDir from the input constants and all related TUI logic.
- Remove the "Shared directory" config input field from the form.
- Update
buildConfig() to stop setting SharedDir/StateDir (volume names derive from the container name via the accessors).
- Update
startInstallStep():
- Step 0: "Create home volume" →
eng.CreateVolume(cfg.HomeVolumeName()) instead of os.MkdirAll(cfg.SharedDir).
- Step 1: "Create state volume" →
eng.CreateVolume(cfg.StateVolumeName()) instead of os.MkdirAll(cfg.StateDir).
- Step 4:
RunContainer opts use HomeVolume/StateVolume instead of SharedDir/StateDir.
- Update
installStepLabels: "Create shared directory" → "Create home volume", "Create state directory" → "Create state volume".
- Update
viewConfig(): remove the shared directory field from the form display.
- Update
viewConfirm(): show volume names instead of directory paths.
- Update
viewDone(): show volume names instead of "Shared dir".
- Remove the macOS
--network host warning in buildConfirmWarnings().
- Update
validateCurrentInput(): remove the inputSharedDir case.
8. tui/install_test.go — Update install tests
- Remove
inputSharedDir references.
- Update
TestBuildConfig to check derived volume names instead of SharedDir/StateDir.
- Update
TestNewInstallModelDefaults to check volume defaults.
- Remove shared dir validation tests if any.
8b. tui/install_tn.go — Tokyo-Night install renderer (added since this plan was written)
- References
inputSharedDir at lines 144 and 186 (sharedDir value + "Shared dir" summary row).
- Remove the shared-dir input rendering; show derived volume names in the summary instead.
8c. cmd/install.go — Headless install path (--plain, added since this plan was written)
- Remove the
--shared-dir flag (installSharedFlag) — volume names derive from --name.
runInstallPlain() steps: replace the two os.MkdirAll steps ("Create shared directory"/"Create state directory") with eng.CreateVolume(cfg.HomeVolumeName()) / eng.CreateVolume(cfg.StateVolumeName()).
RunOptions: HomeVolume: cfg.HomeVolumeName(), StateVolume: cfg.StateVolumeName().
suggestInstallDefaults()-equivalent logic in this file (sets d.SharedDir/d.StateDir for instance N+1 naming) — drop the dir fields; container-name suffixing alone now differentiates instances.
8d. tui/dashboard.go — Dashboard (added since this plan was written)
- Config editor (
configFields, ~line 339): remove the shared_dir path field; optionally show home_volume/state_volume as fields backed by the raw override fields (empty = derived).
suggestInstallDefaults() (~line 435): stop setting SharedDir/StateDir.
8e. tui/menu.go + tui/menu_test.go — Menu/status pane (added since this plan was written)
- Replace
dirExists(cfg.SharedDir) / dirExists(cfg.StateDir) (~line 351) with eng.VolumeExists(cfg.HomeVolumeName()) / eng.VolumeExists(cfg.StateVolumeName()).
- Labels "SHARED DIR"/"STATE DIR" (~line 394) → "HOME VOLUME"/"STATE VOLUME", showing volume names.
- Update
menu_test.go accordingly.
9. tui/update.go — Update wizard
- Update
startUpdateStep() step 3 (Run container): use cfg.HomeVolumeName()/cfg.StateVolumeName() in RunOptions.
10. cmd/uninstall.go — Uninstall command
- Replace directory deletion logic with volume deletion:
- Replace
backupDirs() with backupVolumes() that uses eng.ExportVolume().
- Replace
os.RemoveAll(dir) with eng.RemoveVolume(name).
- Replace
safeRemoveAll() path safety checks (no longer needed for volumes, but keep a basic name validation).
- Remove the
os.IsPermission / sudo rm -rf error handling (not possible with volumes).
- Remove the
addDirToTar() / backupDirs() / safeRemoveAll() functions entirely.
- Update prompts: "Delete data directories" → "Delete data volumes".
11. cmd/status.go — Status command
- Replace
os.Stat(cfg.SharedDir) / os.Stat(cfg.StateDir) with eng.VolumeExists(cfg.HomeVolumeName()) / eng.VolumeExists(cfg.StateVolumeName()).
- Update display labels: "Shared dir" → "Home volume", "State dir" → "State volume".
12. cmd/config.go — Config command
- Update
validConfigKeys: replace shared_dir, state_dir with home_volume, state_volume.
- Update
runConfigShow(): display home_volume and state_volume instead of shared_dir and state_dir.
- Update
runConfigSet(): handle home_volume and state_volume keys.
13. cmd/config_test.go — Config command tests
- Update
validConfigKeys test to use new key names.
14. tui/doctor.go — Doctor checks
- Update check 6 (Shared dir) and check 7 (State dir) to check volume existence via
eng.VolumeExists() instead of os.Stat().
- Update labels: "Shared dir" → "Home volume", "State dir" → "State volume".
- Update
dirCheck() → volumeCheck() or adapt to use the engine.
15. tui/doctor_test.go — Doctor test
- Update test data if any references to "Shared dir"/"State dir".
16. checks/ollama.go — No changes needed
- Ollama host detection is about networking, not volumes. Unchanged.
17. checks/memory.go — No changes needed
- Memory detection is about host RAM, not volumes. Unchanged.
18. CLAUDE.md — Update documentation
- Update the platform rules table: remove the SELinux/volume row.
- Update the config struct documentation.
- Update the naming conventions (add volume naming).
19. SPEC.md / PHASES.md — no longer exist
- Both files were deleted from the repo (main @ 9a11ce4). Nothing to do.
20. scripts/migrate-to-named-volumes.sh — New standalone migration script
- Reads old config YAML to extract container name, engine (docker/podman), and old host paths (
shared_dir/state_dir)
- Uses that engine's binary for every step (don't assume docker)
- Stops and removes the old container
- Creates named volumes (
{name}-home, {name}-state)
- Copies data from old bind-mount dirs into volumes via throwaway containers; mount the source dir read-only, and on SELinux-enforcing hosts run the copy container with
--security-opt label=disable (avoids relabeling the source dir with :Z)
- Prints cleanup instructions for old host directories
- Not invoked by the CLI — user runs it manually
Migration for Existing Installations (Manual Script)
The CLI does not handle backwards compatibility. Users with existing bind-mount installs run a separate shell script (scripts/migrate-to-named-volumes.sh) that:
- Reads the old config to get the container name and old host paths
- Stops the existing container
- Creates the named volumes (
{name}-home, {name}-state)
- Copies data from old bind-mount directories into the new volumes using throwaway containers (
$ENGINE is docker or podman from the old config; add --security-opt label=disable on SELinux-enforcing hosts):
$ENGINE run --rm -v /old/path:/src:ro -v {name}-home:/dst alpine sh -c "cp -a /src/. /dst/"
$ENGINE run --rm -v /old/state:/src:ro -v {name}-state:/dst alpine sh -c "cp -a /src/. /dst/"
- Removes the old container
- Prints instructions to delete old host directories (
rm -rf ~/Omnideck)
- Tells the user to run
omnideck install (or omnideck update) with the new CLI to recreate the container with named volumes
The script is a standalone bash file in the repo at scripts/migrate-to-named-volumes.sh. It is not invoked by the CLI — the user runs it manually.
Test Plan
Unit Tests
config_test.go: volume-name accessors (explicit override, derive-on-empty, old YAML with only shared_dir/state_dir still derives correctly), Save/Load with new fields
engine_test.go: buildRunArgs with named volumes mounted at /home/omnideck + /var/lib/omnideck (all platforms), no :Z/:U/--user flags
install_test.go: buildConfig produces volume names, no shared dir input
menu_test.go: volume labels/checks
config_test.go (cmd): valid keys include home_volume/state_volume
doctor_test.go: updated labels
tests/smoke: re-run; update any assertions touching shared/state dirs
Integration Tests (manual or CI)
omnideck install creates volumes and starts container with named volumes
omnideck status shows volume existence
omnideck uninstall removes volumes (with and without backup)
omnideck update preserves volumes across container recreation
- Manual migration script transfers data from old bind mounts to named volumes
Build Verification
go build ./... compiles
go vet ./... passes
go test ./... all pass
Plan: Switch from Bind Mounts to Named Volumes
Prerequisite / Ordering
This is part 2 of a two-part refactor. Part 1 is omnideck-dev/omnideck#110 (rename
computron→omnideckinside the container image). Part 1 must be merged and its image published toghcr.io/omnideck-dev/omnideck:mainbefore this lands, because:/home/computron→/home/omnideckand/var/lib/computron→/var/lib/omnideck. All mount specs in this plan use the new paths.DefaultImagealready points atghcr.io/omnideck-dev/omnideck:main, so no image change is needed here — but if this CLI change ran against a pre-rename image, the volumes would mount at paths the old image never writes to, and no data would persist.Goal
Replace host-path bind mounts (
-v /Users/user/Omnideck:/home/computron) with Docker/Podman named volumes (-v omnideck-home:/home/omnideck) across all platforms (Linux, macOS, Windows) and all container runtimes (Docker Desktop, Colima, Podman, Docker Engine). This eliminates::Z/:Uflag complexity on Linux--userflags on Linuxsudo rm -rfduring uninstallDesign Decisions
Volume Naming Convention
{containerName}-home(e.g.omnideck-home){containerName}-state(e.g.omnideck-state)ContainerNamein config — guarantees uniqueness across multi-instance installs.cfg.HomeVolumeName()/cfg.StateVolumeName()— which return the explicitHomeVolume/StateVolumefield if set, else derive{ContainerName}-home/{ContainerName}-state. This avoids two bugs:DefaultConfig()baking inomnideck-homeand then--name/TUI changingContainerNameafterward, leaving a second instance pointed at the first instance's volumes.shared_dir/state_dirbut no volume keys) loading with empty volume names and producing a broken-v :/home/omnideckmount. The derive-on-empty accessor is the one backwards-compat shim we bake in — old configs transparently resolve to the correct derived volume names.What Happens to SharedDir / StateDir Config Fields?
HomeVolumeandStateVolumefields (string volume names), both optional overrides — empty means "derive from container name" (see accessors above).config setcommand's valid keys change:shared_dir/state_dir→home_volume/state_volume.Volume Lifecycle
docker volume createbeforedocker run(ensures they exist and can be labeled/named predictably).Backup Mechanism (Runtime-Specific)
podman volume export <name>(writes tar to stdout by default) andpodman volume import <name> -(reads from stdin). Clean, purpose-built commands.Platform-Specific Logic That Gets REMOVED
:ZSELinux flags on Linux → gone (named volumes don't need them):Uuser-namespace remap on Linux Podman → gone--user uid:gidon Linux → gone (named volumes are owned by the container runtime, not the host user)safeRemoveAll()and its system-path protection → gone (we remove volumes, not host directories)--network host→ gone (we never used--network hostanyway)What Stays Platform-Conditional
OLLAMA_HOST— still needshost.docker.internalon macOS/Windows andhost-gatewayon Linux. This is about networking, not volumes, and is unchanged.--add-host=host-gateway:host-gatewayon Linux — unchanged (networking, not volumes)./proc/meminfovssysctl) — unchanged.Dev Workflow (Justfile) — Not Impacted
The Omnideck app's Justfile (
just dev,just e2e,just manual-test) uses bind mounts directly viadocker run— it does NOT go through theomnideckCLI. Bind mounts are intentional in dev because:~/.computron_9000/homeon the hostchown+rm -rf(only works with bind mounts)The CLI change only affects production installs managed by the
omnideckbinary. The Justfile is a separate code path and is not modified.Optional App-Side Cleanup (omnideck repo, not CLI repo)
(Paths below are the post-#110 names.) With named volumes, the general
chown -Rincontainer/entrypoint.shbecomes a no-op (named volumes inherit image ownership on first mount). The targeted chowns are still needed. This is an optional cleanup in the omnideck repo:chown -R omnideck:omnideck /home/omnideck /var/lib/omnideck— redundant with named volumes. Caution: the Justfile dev/e2e flows still bind-mount host dirs into these paths, and they rely on this chown. Only remove it if dev flows are verified unaffected — otherwise keep it (it is harmless with volumes).chown omnideck:broker /home/omnideck/downloads— sets cross-user ownershipchmod 3770 /home/omnideck/downloads— special setgid+sticky modechown -R broker:broker /var/lib/omnideck/vault— credential isolationThis is not required for the CLI change to work (the chown is harmless), but is a cleanliness improvement.
Files to Change (in dependency order)
1.
config/config.go— Config struct changesSharedDir stringandStateDir stringwithHomeVolume stringandStateVolume string(optional overrides, normally empty).HomeVolumeName()/StateVolumeName(): return the field if non-empty, else derive{ContainerName}-home/{ContainerName}-state. All consumers go through the accessors, never the raw fields.DefaultConfig()leavesHomeVolume/StateVolumeempty (derived).expandHomecalls for SharedDir/StateDir inLoad().MigrateVolumes()method — the derive-on-empty accessors already make old configs (no volume keys) resolve correctly.2.
config/config_test.go— Update testsSharedDir/StateDirassertions withHomeVolume/StateVolume.TestDefaultConfigto check volume names.3.
engine/engine.go— RunOptions struct + Engine interfaceSharedDir stringandStateDir stringwithHomeVolume stringandStateVolume string.Engineinterface:CreateVolume(name string) errorVolumeExists(name string) (bool, error)RemoveVolume(name string) errorExportVolume(name string, w io.Writer) error4.
engine/docker.go— Docker engine implementationCreateVolume(name string) error— runsdocker volume create name.VolumeExists(name string) (bool, error)— runsdocker volume inspect name, checks exit code.RemoveVolume(name string) error— runsdocker volume rm name.ExportVolume(name string, w io.Writer) error— runs a throwaway container (docker run --rm -v <name>:/data alpine tar -C /data -c .) piped to the writer.buildRunArgs(): replace bind mount args with-v homeVolume:/home/omnideck -v stateVolume:/var/lib/omnideck(the post-#110 container paths). Remove all:Z,:U,--user, and platform-conditional volume logic.5.
engine/podman.go— Podman engine implementationCreateVolume(name string) error— runspodman volume create name.VolumeExists(name string) (bool, error)— runspodman volume inspect name.RemoveVolume(name string) error— runspodman volume rm name.ExportVolume(name string, w io.Writer) error— uses Podman's nativepodman volume export <name>(tar to stdout) piped to the writer (more efficient than throwaway container).buildPodmanRunArgs(): same simplification as docker — remove:Z,U, use named volumes mounted at/home/omnideckand/var/lib/omnideck.6.
engine/engine_test.go— Update engine testsTestBuildRunArgsLinux: replace bind mount assertions with named volume assertions. Remove:Zchecks. Remove--usercheck.TestBuildRunArgsMacOS: replace bind mount assertions with named volume assertions.TestBuildRunArgsLinuxSecondInstance: update volume name assertions.TestBuildPodmanRunArgsHasReplace: remove:Z,Uchecks, add named volume checks.TestBuildPodmanRunArgsMacOS: remove:Z,Uchecks, add named volume checks.TestBuildRunArgsMemorySet/TestBuildRunArgsMemoryEmpty: update RunOptions fields.CreateVolume/VolumeExists/RemoveVolume(mock-based or interface-level).7.
tui/install.go— Install wizardinputSharedDirfrom the input constants and all related TUI logic.buildConfig()to stop settingSharedDir/StateDir(volume names derive from the container name via the accessors).startInstallStep():eng.CreateVolume(cfg.HomeVolumeName())instead ofos.MkdirAll(cfg.SharedDir).eng.CreateVolume(cfg.StateVolumeName())instead ofos.MkdirAll(cfg.StateDir).RunContaineropts useHomeVolume/StateVolumeinstead ofSharedDir/StateDir.installStepLabels: "Create shared directory" → "Create home volume", "Create state directory" → "Create state volume".viewConfig(): remove the shared directory field from the form display.viewConfirm(): show volume names instead of directory paths.viewDone(): show volume names instead of "Shared dir".--network hostwarning inbuildConfirmWarnings().validateCurrentInput(): remove theinputSharedDircase.8.
tui/install_test.go— Update install testsinputSharedDirreferences.TestBuildConfigto check derived volume names instead ofSharedDir/StateDir.TestNewInstallModelDefaultsto check volume defaults.8b.
tui/install_tn.go— Tokyo-Night install renderer (added since this plan was written)inputSharedDirat lines 144 and 186 (sharedDirvalue + "Shared dir" summary row).8c.
cmd/install.go— Headless install path (--plain, added since this plan was written)--shared-dirflag (installSharedFlag) — volume names derive from--name.runInstallPlain()steps: replace the twoos.MkdirAllsteps ("Create shared directory"/"Create state directory") witheng.CreateVolume(cfg.HomeVolumeName())/eng.CreateVolume(cfg.StateVolumeName()).RunOptions:HomeVolume: cfg.HomeVolumeName(),StateVolume: cfg.StateVolumeName().suggestInstallDefaults()-equivalent logic in this file (setsd.SharedDir/d.StateDirfor instance N+1 naming) — drop the dir fields; container-name suffixing alone now differentiates instances.8d.
tui/dashboard.go— Dashboard (added since this plan was written)configFields, ~line 339): remove theshared_dirpath field; optionally showhome_volume/state_volumeas fields backed by the raw override fields (empty = derived).suggestInstallDefaults()(~line 435): stop settingSharedDir/StateDir.8e.
tui/menu.go+tui/menu_test.go— Menu/status pane (added since this plan was written)dirExists(cfg.SharedDir)/dirExists(cfg.StateDir)(~line 351) witheng.VolumeExists(cfg.HomeVolumeName())/eng.VolumeExists(cfg.StateVolumeName()).menu_test.goaccordingly.9.
tui/update.go— Update wizardstartUpdateStep()step 3 (Run container): usecfg.HomeVolumeName()/cfg.StateVolumeName()inRunOptions.10.
cmd/uninstall.go— Uninstall commandbackupDirs()withbackupVolumes()that useseng.ExportVolume().os.RemoveAll(dir)witheng.RemoveVolume(name).safeRemoveAll()path safety checks (no longer needed for volumes, but keep a basic name validation).os.IsPermission/sudo rm -rferror handling (not possible with volumes).addDirToTar()/backupDirs()/safeRemoveAll()functions entirely.11.
cmd/status.go— Status commandos.Stat(cfg.SharedDir)/os.Stat(cfg.StateDir)witheng.VolumeExists(cfg.HomeVolumeName())/eng.VolumeExists(cfg.StateVolumeName()).12.
cmd/config.go— Config commandvalidConfigKeys: replaceshared_dir,state_dirwithhome_volume,state_volume.runConfigShow(): displayhome_volumeandstate_volumeinstead ofshared_dirandstate_dir.runConfigSet(): handlehome_volumeandstate_volumekeys.13.
cmd/config_test.go— Config command testsvalidConfigKeystest to use new key names.14.
tui/doctor.go— Doctor checkseng.VolumeExists()instead ofos.Stat().dirCheck()→volumeCheck()or adapt to use the engine.15.
tui/doctor_test.go— Doctor test16.
checks/ollama.go— No changes needed17.
checks/memory.go— No changes needed18.
CLAUDE.md— Update documentation19.
— no longer existSPEC.md/PHASES.md20.
scripts/migrate-to-named-volumes.sh— New standalone migration scriptshared_dir/state_dir){name}-home,{name}-state)--security-opt label=disable(avoids relabeling the source dir with:Z)Migration for Existing Installations (Manual Script)
The CLI does not handle backwards compatibility. Users with existing bind-mount installs run a separate shell script (
scripts/migrate-to-named-volumes.sh) that:{name}-home,{name}-state)$ENGINEis docker or podman from the old config; add--security-opt label=disableon SELinux-enforcing hosts):rm -rf ~/Omnideck)omnideck install(oromnideck update) with the new CLI to recreate the container with named volumesThe script is a standalone bash file in the repo at
scripts/migrate-to-named-volumes.sh. It is not invoked by the CLI — the user runs it manually.Test Plan
Unit Tests
config_test.go: volume-name accessors (explicit override, derive-on-empty, old YAML with onlyshared_dir/state_dirstill derives correctly), Save/Load with new fieldsengine_test.go: buildRunArgs with named volumes mounted at/home/omnideck+/var/lib/omnideck(all platforms), no:Z/:U/--userflagsinstall_test.go: buildConfig produces volume names, no shared dir inputmenu_test.go: volume labels/checksconfig_test.go(cmd): valid keys includehome_volume/state_volumedoctor_test.go: updated labelstests/smoke: re-run; update any assertions touching shared/state dirsIntegration Tests (manual or CI)
omnideck installcreates volumes and starts container with named volumesomnideck statusshows volume existenceomnideck uninstallremoves volumes (with and without backup)omnideck updatepreserves volumes across container recreationBuild Verification
go build ./...compilesgo vet ./...passesgo test ./...all pass