Skip to content

Commit 629a019

Browse files
committed
test 2
1 parent 997eb7f commit 629a019

7 files changed

Lines changed: 139 additions & 61 deletions

File tree

dot_files/devcube/Containerfile

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -258,27 +258,33 @@ RUN mkdir -p /etc/profile.d /etc/fish/conf.d \
258258
# =============================================================================
259259
# LAYER 14: Bake the Neovim configuration + pre-install plugins
260260
# =============================================================================
261-
# The full LazyVim config is baked into $HOME/.config/nvim. Personal overrides
262-
# are bind-mounted at runtime from the host (~/.config/hypercube/nvim) and
263-
# layered on top via the runtimepath (see lua/config/lazy.lua).
261+
# The full LazyVim config is baked into a PRISTINE, non-volume path under
262+
# /usr/share/hypercube/config. The entrypoint syncs it into $HOME/.config/nvim
263+
# on every start, so config updates ship with the image instead of going stale
264+
# on the persisted (copy-up-seeded) home volume. Personal overrides are
265+
# bind-mounted at runtime from the host (~/.config/hypercube/nvim) and layered
266+
# on top via the runtimepath (see lua/config/lazy.lua).
264267
# NOTE: the build context is dot_files/, so the nvim config lives under nvim/.
265-
COPY nvim/config/ /root/.config/nvim/
268+
COPY nvim/config/ /usr/share/hypercube/config/nvim/
266269

267270
# Pre-install all plugins + build treesitter parsers so first launch is fast and
268-
# offline. HOME=/root and PATH already include the brew nvim binary.
269-
RUN nvim --headless "+Lazy! sync" +qa
271+
# offline. Point nvim at the pristine config via XDG_CONFIG_HOME; plugins still
272+
# land in stdpath('data') = /root/.local/share/nvim, which IS on the home volume
273+
# (seeded via copy-up on first run, matching the baked lazy-lock.json).
274+
RUN XDG_CONFIG_HOME=/usr/share/hypercube/config nvim --headless "+Lazy! sync" +qa
270275

271276
# =============================================================================
272277
# LAYER 14b: Bake the shell / multiplexer / orchestrator configs
273278
# =============================================================================
274279
# fish + starship give a good in-container shell/prompt; zellij + workmux drive
275-
# the parallel-agent workflow. These seed into the persisted home volume on
276-
# first run (podman copy-up), like the nvim config above. starship.toml lands at
277-
# the exact path dot_files/fish/config.fish hardcodes for STARSHIP_CONFIG, so
278-
# fish needs no edits (and it lives outside /root, so it's always present).
279-
COPY fish/ /root/.config/fish/
280-
COPY zellij/ /root/.config/zellij/
281-
COPY workmux/ /root/.config/workmux/
280+
# the parallel-agent workflow. Like the nvim config above, these are baked into
281+
# the pristine /usr/share/hypercube/config path and synced into $HOME/.config by
282+
# the entrypoint on every start (so config updates ship with the image rather
283+
# than going stale on the home volume). starship.toml already lives here at the
284+
# exact path dot_files/fish/config.fish hardcodes for STARSHIP_CONFIG.
285+
COPY fish/ /usr/share/hypercube/config/fish/
286+
COPY zellij/ /usr/share/hypercube/config/zellij/
287+
COPY workmux/ /usr/share/hypercube/config/workmux/
282288
COPY starship/ /usr/share/hypercube/config/starship/
283289

284290
# Make fish root's login shell so every interactive shell in the container

dot_files/devcube/Justfile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
local_image := "localhost/devcube"
77
remote_image := "ghcr.io/binarypie-dev/devcube:latest"
88
# Throwaway volumes so local testing never touches the real per-project state.
9+
# Passing DEVCUBE_VOLUME explicitly forces this single fixed home volume instead
10+
# of the wrapper's per-project derivation, keeping local testing predictable.
911
test_volume := "hypercube-devcube-test"
1012
test_wt_prefix := "devcube-wt-test"
1113

@@ -80,6 +82,11 @@ test-build: build
8082
# The entrypoint must pin $SHELL to fish so workmux drops new worktrees into
8183
# fish (its default-shell fallback is /bin/sh otherwise).
8284
podman run --rm {{local_image}} sh -c '[ "$SHELL" = "$(command -v fish)" ]'
85+
# Baked config lives at the pristine path, NOT under /root (the home volume),
86+
# so config updates ship with the image instead of going stale on the volume.
87+
podman run --rm --entrypoint sh {{local_image}} -c 'for c in nvim fish zellij workmux; do test -d /usr/share/hypercube/config/$c || exit 1; done && ! test -e /root/.config/nvim'
88+
# ...and the entrypoint syncs it into /root/.config on every start.
89+
podman run --rm {{local_image}} sh -c 'test -d /root/.config/nvim && test -f /root/.config/fish/config.fish && test -d /root/.config/zellij && test -d /root/.config/workmux'
8390
@echo ""
8491
@echo "All tests passed!"
8592

dot_files/devcube/README.md

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@ Linux desktop, a remote box, and macOS — no distrobox required.
4242
| `devc codex` | OpenAI Codex CLI, run directly |
4343
| `devc agy` | Antigravity CLI, run directly |
4444

45-
All entry points share one home volume, so an AI login from any of them works in
46-
all of them.
45+
Within a project, all entry points share that project's home volume, so an AI
46+
login from any of them works in all of them. The home volume is **per-project**
47+
(derived from the launch path), so logins and state never leak between
48+
workspaces — you log in once per project.
4749

4850
### Parallel agents
4951

@@ -88,17 +90,20 @@ only what's needed. The mounts:
8890

8991
| Mounted in | Purpose |
9092
|---|---|
91-
| named volume `hypercube-devcube-home``/root` | plugin/mason updates, sessions, shada, **and AI-CLI auth** — seeded from the image on first run, persisted after |
93+
| per-project volume `hypercube-devcube-home-<proj>-<hash>``/root` | plugin/mason updates, sessions, shada, **and AI-CLI auth** — seeded from the image on first run, persisted after; one per project so creds never leak between workspaces |
9294
| current directory | the project you're editing |
9395
| per-project volume `devcube-wt-<proj>-<hash>``/worktrees` | worktrees + workmux state (orchestrators only) |
9496
| `~/.config/hypercube/nvim` | **your** plugin overrides, layered on top of the baked config |
9597
| `~/.gitconfig` (ro) | commit identity |
9698
| `$SSH_AUTH_SOCK` (Linux) | SSH agent for git/gh |
9799

98-
Everything else (the LazyVim config, plugins, AI CLIs, zellij/workmux/fish/
99-
starship config) lives in the image. The container runs as `--user 0:0` so files
100-
you edit are owned by your host user under rootless podman. Multiple sessions run
101-
concurrently.
100+
The AI CLIs and nvim plugins live in the image and seed onto the home volume on
101+
first run. The **baked config** (LazyVim, zellij, workmux, fish, starship) is
102+
image-owned: it's stored at a pristine path and synced into `/root/.config` by
103+
the entrypoint on **every** start, so config updates ship with `podman pull`
104+
no volume reset needed (plugin *version* bumps still want `:Lazy update`). The
105+
container runs as `--user 0:0` so files you edit are owned by your host user
106+
under rootless podman. Multiple sessions run concurrently.
102107

103108
Clipboard uses **OSC 52** through the terminal, so yank/paste works locally
104109
(Ghostty) and over SSH without forwarding a Wayland/pbcopy socket.
@@ -114,8 +119,8 @@ devc nvim file.rs # ...or just the editor
114119
| Command | Description |
115120
|---------|-------------|
116121
| `ujust devcube-setup` | pull image + install the `devc` wrapper |
117-
| `ujust devcube-upgrade` | pull the latest image + refresh the wrapper |
118-
| `ujust devcube-reset` | drop the home volume to re-seed (clears plugin state + AI logins); optionally prune per-project worktree volumes |
122+
| `ujust devcube-upgrade` | pull the latest image + refresh the wrapper (baked config refreshes itself on next launch) |
123+
| `ujust devcube-reset` | drop all per-project home volumes to re-seed (clears plugin state + AI logins); optionally prune per-project worktree volumes |
119124
| `ujust devcube-shell` | debug shell inside a throwaway container |
120125

121126
## Setup (macOS / any podman host)

dot_files/devcube/entrypoint.sh

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,35 @@ if [ -z "${TERM:-}" ] || ! infocmp "$TERM" >/dev/null 2>&1; then
2626
export TERM=xterm-256color
2727
fi
2828

29+
# Refresh image-baked editor/shell/multiplexer config into the home volume on
30+
# every start, so config updates ship with `podman pull` instead of forcing a
31+
# volume wipe. Only these fully image-owned dirs are replaced; auth + state
32+
# (~/.local, ~/.claude, ~/.cache, plugin downloads + lazy-lock, shell history,
33+
# zellij session serialization) live elsewhere on the volume and are untouched.
34+
# rm -rf + cp -a (not rsync, which isn't installed) so files removed from the
35+
# image also drop from the dir. The personal nvim override (~/.config/hypercube/
36+
# nvim) is a different dir and is never touched.
37+
config_src="/usr/share/hypercube/config"
38+
for c in nvim fish zellij workmux; do
39+
src="$config_src/$c"
40+
dest="$HOME/.config/$c"
41+
[ -d "$src" ] || continue
42+
mkdir -p "$HOME/.config"
43+
# fish writes its universal variables (set -U, e.g. fish_user_paths/colors)
44+
# to $dest/fish_variables -- that's runtime STATE, not baked config, so carry
45+
# it across the refresh rather than wiping it with the config dir.
46+
saved=""
47+
if [ "$c" = fish ] && [ -f "$dest/fish_variables" ]; then
48+
saved="$(mktemp)" && cp -a "$dest/fish_variables" "$saved"
49+
fi
50+
rm -rf "$dest"
51+
cp -a "$src" "$dest"
52+
if [ -n "$saved" ]; then
53+
cp -a "$saved" "$dest/fish_variables"
54+
rm -f "$saved"
55+
fi
56+
done
57+
2958
if [ $# -eq 0 ]; then
3059
exec nvim
3160
else

dot_files/devcube/scripts/devcube.sh

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
# devc agy -> Antigravity CLI, run directly
1515
#
1616
# Extra args after the tool are passed through (e.g. `devc nvim file.rs`,
17-
# `devc claude --resume`). All entry points share the same home volume, so
18-
# AI-CLI logins persist across them -- log in once and every tool is authed.
17+
# `devc claude --resume`). Within one project all entry points share that
18+
# project's home volume, so an AI-CLI login from any of them works in all of
19+
# them -- but the home volume is PER-PROJECT (derived from the launch path like
20+
# the worktree volume below), so creds/state never leak between workspaces.
1921
#
2022
# zellij/workmux run the parallel-agent flow: each `workmux add <branch>`
2123
# creates a git worktree on a PER-PROJECT named volume mounted at /worktrees,
@@ -29,9 +31,12 @@
2931
# SSH agent, terminal). Portable across Linux and macOS.
3032
#
3133
# Env overrides:
32-
# DEVCUBE_IMAGE container image (default ghcr.io/binarypie-dev/devcube:latest)
33-
# DEVCUBE_VOLUME named volume holding state + AI auth (default hypercube-devcube-home)
34-
# DEVCUBE_WT_PREFIX prefix for the per-project worktree volume (default devcube-wt)
34+
# DEVCUBE_IMAGE container image (default ghcr.io/binarypie-dev/devcube:latest)
35+
# DEVCUBE_VOLUME exact home-volume name (default: a per-project name derived
36+
# from DEVCUBE_HOME_PREFIX + the launch path). Set this to
37+
# force a single fixed volume (the test harness does this).
38+
# DEVCUBE_HOME_PREFIX prefix for the per-project home volume (default hypercube-devcube-home)
39+
# DEVCUBE_WT_PREFIX prefix for the per-project worktree volume (default devcube-wt)
3540
set -euo pipefail
3641

3742
# The tool to run is the first argument; default to the workmux workspace.
@@ -48,7 +53,6 @@ nvim | claude | codex | agy | zellij | workmux)
4853
esac
4954

5055
IMAGE="${DEVCUBE_IMAGE:-ghcr.io/binarypie-dev/devcube:latest}"
51-
VOLUME="${DEVCUBE_VOLUME:-hypercube-devcube-home}"
5256
OVERRIDE_DIR="$HOME/.config/hypercube/nvim"
5357

5458
# Personal overrides are layered on top of the baked config. Ensure the dir
@@ -70,6 +74,21 @@ if [ "$TOOL" = workmux ] && [ -z "$git_root" ]; then
7074
exit 1
7175
fi
7276

77+
# Stable, repo-root-derived names so a project's state persists across restarts,
78+
# is shared no matter which subdir you launch from, and never collides with
79+
# another project's. cksum is available on both Linux and macOS (the wrapper
80+
# stays portable). Used for the per-project home volume here and the per-project
81+
# worktree volume + zellij session name in the orchestrator case below.
82+
slug="$(basename "$project_dir" | tr -c 'a-zA-Z0-9_.-' '-')"
83+
phash="$(printf '%s' "$project_dir" | cksum | cut -d' ' -f1)"
84+
85+
# Home volume: PER-PROJECT by default so AI auth + state stay isolated between
86+
# workspaces. DEVCUBE_VOLUME forces an exact name (the test harness relies on
87+
# this); otherwise the name is derived from DEVCUBE_HOME_PREFIX + project, just
88+
# like the worktree volume.
89+
HOME_PREFIX="${DEVCUBE_HOME_PREFIX:-hypercube-devcube-home}"
90+
VOLUME="${DEVCUBE_VOLUME:-${HOME_PREFIX}-${slug}-${phash}}"
91+
7392
# Where the project shows up inside the container. By default it's mounted at
7493
# its own host path (identity) so file paths line up on both sides. The
7594
# orchestrators override this below.
@@ -82,18 +101,14 @@ container_cmd=("$TOOL")
82101
extra=()
83102
case "$TOOL" in
84103
zellij | workmux)
85-
# Stable, repo-root-derived names so a project's worktrees + workmux state
86-
# persist across restarts, are shared no matter which subdir you launch from,
87-
# and never collide with another project's. cksum is available on both Linux
88-
# and macOS (the wrapper stays portable).
89-
slug="$(basename "$project_dir" | tr -c 'a-zA-Z0-9_.-' '-')"
90-
phash="$(printf '%s' "$project_dir" | cksum | cut -d' ' -f1)"
104+
# Per-project worktree volume + zellij session name, from the same slug/phash
105+
# computed above so a project's worktrees + workmux state persist across
106+
# restarts and never collide with another project's.
91107
wt_volume="${DEVCUBE_WT_PREFIX:-devcube-wt}-${slug}-${phash}"
92108
# Stable, per-project zellij session name so closing the devcube (Ctrl+Space
93-
# q -> Save) and reopening it resumes the SAME session. The home volume
94-
# (/root) is shared across every project, so the name must be unique per
95-
# project -> include phash. zellij session names can't contain '.', so the
96-
# slug's dots are squashed to '-' (the volume name keeps them; it's separate).
109+
# q -> Save) and reopening it resumes the SAME session. zellij session names
110+
# can't contain '.', so the slug's dots are squashed to '-' (the volume names
111+
# keep them; they're separate).
97112
session="devcube-$(printf '%s' "$slug" | tr '.' '-')-${phash}"
98113
# Mount the project at a fixed /workspace -- a stable, branch-independent path
99114
# regardless of which branch the host checkout is on -- kept separate from the
@@ -131,9 +146,11 @@ args=(
131146
-e TERM
132147
-e COLORTERM
133148
"${extra[@]}"
134-
# State + plugin updates + AI auth. Empty volume is seeded from the image's
135-
# /root on first run (podman copy-up); never use a fixed container --name so
136-
# multiple sessions can run concurrently.
149+
# Per-project home volume: AI auth + plugin/state updates + shell history.
150+
# Empty volume is seeded from the image's /root on first run (podman copy-up);
151+
# baked config (nvim/fish/zellij/workmux) is NOT here -- the entrypoint syncs
152+
# it from the image on every start so config updates ship without a wipe.
153+
# Never use a fixed container --name so multiple sessions run concurrently.
137154
-v "${VOLUME}:/root"
138155
# The project -- the git repo root, or the launch dir when not in a repo.
139156
# Mounted at its host path for the direct tools, or at /workspace for the

dot_files/devcube/zj-quit.sh

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
# Launched in a floating zellij pane (see dot_files/zellij/config.kdl). It asks
55
# whether to keep the session before dropping back to your host shell:
66
#
7-
# Save -> Quit the session. With session_serialization on, zellij keeps it
8-
# as a resurrectable session; the next `devc` reattaches to it
9-
# (same tabs / worktrees / panes). See devcube-session.sh.
7+
# Save -> End the session via `kill-session`. With session_serialization
8+
# on, zellij keeps it as a resurrectable session; the next `devc`
9+
# reattaches to it (same tabs / worktrees / panes). See
10+
# devcube-session.sh.
1011
# Discard -> Delete this session (and its serialized state) outright, so the
1112
# next `devc` starts fresh from the workmux layout.
1213
# Cancel -> Close this prompt and stay where you are.
@@ -32,17 +33,27 @@ choice="$(
3233

3334
case "$choice" in
3435
Save*)
35-
# Quit -> resurrectable (serialization is enabled in config.kdl).
36-
exec zellij action quit
36+
# End this session but KEEP it resurrectable. There is no `zellij action
37+
# quit` CLI subcommand (Quit is only a keybinding action), so use
38+
# kill-session: it terminates the session -- dropping you back to the host
39+
# shell -- while leaving the serialized snapshot on disk (serialization is
40+
# enabled in config.kdl), so the next `devc` resurrects it. delete-session
41+
# (below) is the one that removes that snapshot.
42+
if [ -n "$session" ]; then
43+
exec zellij kill-session "$session"
44+
else
45+
# ZELLIJ_SESSION_NAME is always set inside a pane; this is just a guard.
46+
exec zellij kill-session
47+
fi
3748
;;
3849
Discard*)
3950
# Kill the running session AND remove its serialized copy in one shot, so
40-
# nothing is left to resurrect. Fall back to a plain quit if, for whatever
51+
# nothing is left to resurrect. Fall back to a plain kill if, for whatever
4152
# reason, we don't know our own session name.
4253
if [ -n "$session" ]; then
4354
exec zellij delete-session "$session" --force
4455
else
45-
exec zellij action quit
56+
exec zellij kill-session
4657
fi
4758
;;
4859
*)

system_files/shared/usr/share/ublue-os/just/61-devcube.just

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
# toolchain, and zellij + workmux ship as a single portable podman image. A thin
66
# wrapper at ~/.local/bin/devc runs it: `devc` opens the workmux parallel-agent
77
# workspace, and `devc nvim` / `devc claude` / `devc codex` / `devc agy` /
8-
# `devc zellij` launch a single tool. One home volume is shared across them, so
9-
# AI logins persist. Personal plugin overrides live in
8+
# `devc zellij` launch a single tool. Each project gets its OWN home volume
9+
# (derived from the launch path), so AI logins persist per workspace and never
10+
# leak between projects. Personal plugin overrides live in
1011
# ~/.config/hypercube/nvim/lua/plugins/.
1112

1213
image := "ghcr.io/binarypie-dev/devcube:latest"
1314
wrapper := "/usr/share/hypercube/config/devcube/scripts/devcube.sh"
14-
volume := "hypercube-devcube-home"
15+
# Per-project home volumes share this prefix (see devcube.sh DEVCUBE_HOME_PREFIX).
16+
volume_prefix := "hypercube-devcube-home"
1517

1618
# Pull the image and install the devc wrapper to ~/.local/bin
1719
devcube-setup:
@@ -41,21 +43,21 @@ devcube-upgrade:
4143
install -m 0755 {{wrapper}} "$HOME/.local/bin/devc"
4244

4345
echo "Done!"
44-
echo "Tip: run ':Lazy update' inside nvim to update plugins. Baked config"
45-
echo "updates (zellij/workmux/fish) only apply to a fresh home volume — run"
46-
echo "'ujust devcube-reset' to re-seed (this clears AI logins)."
46+
echo "Baked config (nvim/fish/zellij/workmux) refreshes automatically on the"
47+
echo "next launch no reset needed. For nvim plugin updates run ':Lazy update'"
48+
echo "(or ':Lazy restore' to match the image's pinned versions) inside nvim."
4749

48-
# Reset persisted state so the next launch re-seeds from the image.
49-
# WARNING: removes plugin state, sessions, AND AI CLI logins stored in the volume.
50+
# WARNING: removes plugin state, sessions, AND AI CLI logins stored in the volumes.
51+
# Reset persisted state in all per-project home volumes so the next launch re-seeds.
5052
devcube-reset:
5153
#!/usr/bin/bash
5254
set -euo pipefail
5355

54-
read -p "Remove the {{volume}} volume (plugin state + AI logins)? [y/N] " -n 1 -r
56+
read -p "Remove ALL per-project home volumes ({{volume_prefix}}-*) (plugin state + AI logins)? [y/N] " -n 1 -r
5557
echo
5658
if [[ $REPLY =~ ^[Yy]$ ]]; then
57-
podman volume rm -f {{volume}} 2>/dev/null || true
58-
echo "Volume removed. The next 'devc' launch will re-seed from the image."
59+
podman volume ls -q | grep '^{{volume_prefix}}' | xargs -r podman volume rm -f
60+
echo "Home volumes removed. The next 'devc' launch will re-seed from the image."
5961
fi
6062

6163
read -p "Also remove ALL per-project worktree volumes (devcube-wt-*)? [y/N] " -n 1 -r
@@ -67,13 +69,14 @@ devcube-reset:
6769
echo "Kept per-project worktree volumes."
6870
fi
6971

70-
# Open a debug shell inside a throwaway devcube container
72+
# No home volume is mounted (state is per-project, and this is a scratch shell),
73+
# so /root is ephemeral, though the entrypoint still lays down the baked config.
74+
# Open a debug shell inside a throwaway devcube container.
7175
devcube-shell:
7276
#!/usr/bin/bash
7377
set -euo pipefail
7478
exec podman run --rm -it --init \
7579
--user 0:0 --security-opt label=disable \
7680
-e DEVCUBE=1 -e TERM \
77-
-v {{volume}}:/root \
7881
-v "$(pwd):$(pwd):rw" -w "$(pwd)" \
7982
{{image}} bash

0 commit comments

Comments
 (0)