diff --git a/AGENTS.md b/AGENTS.md index 59ea7a7..26666f0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,7 +28,7 @@ ## Testing Guidelines - Primary automated coverage is `go test ./...`. - Manual verification for VM lifecycle changes: `./banger vm create`, confirm SSH access, then stop/delete the VM. -- Rebuilt images now include `mise` plus `opencode` by default; if you change guest provisioning, document whether users need to rebuild `./runtime/rootfs-docker.ext4` or another base image to pick it up. +- Rebuilt images now include `mise`, `opencode`, and `tmux-resurrect`/`tmux-continuum` defaults for `root`; if you change guest provisioning, document whether users need to rebuild `./runtime/rootfs-docker.ext4` or another base image to pick it up. - If you add a new operational workflow, document how to exercise it in `README.md`. - For NAT changes, verify both guest outbound access and host rule cleanup, for example with `./verify.sh --nat`. diff --git a/README.md b/README.md index 8cd1aa6..14d0c08 100644 --- a/README.md +++ b/README.md @@ -183,8 +183,9 @@ banger image build --name docker-dev --docker ``` Rebuilt images install a pinned `mise` at `/usr/local/bin/mise`, activate it -for bash login and interactive shells, and install `opencode` through `mise` -by default. +for bash login and interactive shells, install `opencode` through `mise`, and +configure `tmux-resurrect` plus `tmux-continuum` for `root` with periodic +autosaves and manual-only restore by default. Show or delete images: ```bash diff --git a/customize.sh b/customize.sh index f9f034b..eb2c35d 100755 --- a/customize.sh +++ b/customize.sh @@ -106,6 +106,13 @@ INSTALL_DOCKER=0 MISE_VERSION="v2025.12.0" MISE_INSTALL_PATH="/usr/local/bin/mise" MISE_ACTIVATE_LINE='eval "$(/usr/local/bin/mise activate bash)"' +TMUX_PLUGIN_DIR="/root/.tmux/plugins" +TMUX_RESURRECT_DIR="/root/.tmux/resurrect" +TMUX_TPM_REPO="https://github.com/tmux-plugins/tpm" +TMUX_RESURRECT_REPO="https://github.com/tmux-plugins/tmux-resurrect" +TMUX_CONTINUUM_REPO="https://github.com/tmux-plugins/tmux-continuum" +TMUX_MANAGED_START="# >>> banger tmux plugins >>>" +TMUX_MANAGED_END="# <<< banger tmux plugins <<<" MODULES_DIR="$(bundle_path default_modules_dir "$RUNTIME_DIR/wtf/root/lib/modules/6.8.0-94-generic")" PACKAGES_FILE="$(banger_packages_file)" while [[ $# -gt 0 ]]; do @@ -413,9 +420,62 @@ if [[ \"$INSTALL_DOCKER\" == \"1\" ]]; then systemctl enable --now docker || true fi fi +rm -f /root/get-docker /root/get-docker.sh /tmp/get-docker /tmp/get-docker.sh git config --system init.defaultBranch main " +log "configuring tmux resurrect" +ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + "root@${GUEST_IP}" bash -se < "\$tmp_tmux_conf" +else + : > "\$tmp_tmux_conf" +fi +if [[ -s "\$tmp_tmux_conf" ]]; then + printf '\n' >> "\$tmp_tmux_conf" +fi +cat >> "\$tmp_tmux_conf" <<'TMUXCONF' +$TMUX_MANAGED_START +set -g @plugin 'tmux-plugins/tpm' +set -g @plugin 'tmux-plugins/tmux-resurrect' +set -g @plugin 'tmux-plugins/tmux-continuum' +set -g @continuum-save-interval '15' +set -g @continuum-restore 'off' +set -g @resurrect-dir '/root/.tmux/resurrect' +run '~/.tmux/plugins/tpm/tpm' +$TMUX_MANAGED_END +TMUXCONF +mv "\$tmp_tmux_conf" "\$TMUX_CONF" +chmod 0644 "\$TMUX_CONF" +EOF + if [[ -n "$MODULES_DIR" ]]; then MODULES_BASE="$(basename "$MODULES_DIR")" log "copying kernel modules ($MODULES_BASE) into guest" diff --git a/internal/daemon/imagebuild.go b/internal/daemon/imagebuild.go index 51aec52..70b4a40 100644 --- a/internal/daemon/imagebuild.go +++ b/internal/daemon/imagebuild.go @@ -24,6 +24,13 @@ const ( defaultMiseInstallPath = "/usr/local/bin/mise" defaultMiseActivateLine = `eval "$(/usr/local/bin/mise activate bash)"` defaultOpenCodeTool = "github:anomalyco/opencode" + defaultTPMRepo = "https://github.com/tmux-plugins/tpm" + defaultResurrectRepo = "https://github.com/tmux-plugins/tmux-resurrect" + defaultContinuumRepo = "https://github.com/tmux-plugins/tmux-continuum" + defaultTMUXPluginDir = "/root/.tmux/plugins" + defaultTMUXResurrectDir = "/root/.tmux/resurrect" + tmuxManagedBlockStart = "# >>> banger tmux plugins >>>" + tmuxManagedBlockEnd = "# <<< banger tmux plugins <<<" ) type imageBuildSpec struct { @@ -242,6 +249,7 @@ func buildProvisionScript(vmName, dnsServer string, packages []string, installDo fmt.Fprintf(&script, "PACKAGES=%s\n", shellArray(packages)) script.WriteString("DEBIAN_FRONTEND=noninteractive apt-get -y install \"${PACKAGES[@]}\"\n") appendMiseSetup(&script) + appendTmuxSetup(&script) if installDocker { script.WriteString("DEBIAN_FRONTEND=noninteractive apt-get -y remove containerd || true\n") script.WriteString("if ! DEBIAN_FRONTEND=noninteractive apt-get -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; then\n") @@ -249,6 +257,7 @@ func buildProvisionScript(vmName, dnsServer string, packages []string, installDo script.WriteString("fi\n") script.WriteString("if command -v systemctl >/dev/null 2>&1; then systemctl enable --now docker || true; fi\n") } + appendGuestCleanup(&script) script.WriteString("git config --system init.defaultBranch main\n") return script.String() } @@ -270,6 +279,54 @@ func appendMiseSetup(script *bytes.Buffer) { appendLineIfMissing(script, "/etc/bash.bashrc", defaultMiseActivateLine) } +func appendTmuxSetup(script *bytes.Buffer) { + fmt.Fprintf(script, "TMUX_PLUGIN_DIR=%s\n", shellQuote(defaultTMUXPluginDir)) + fmt.Fprintf(script, "TMUX_RESURRECT_DIR=%s\n", shellQuote(defaultTMUXResurrectDir)) + script.WriteString("mkdir -p \"$TMUX_PLUGIN_DIR\" \"$TMUX_RESURRECT_DIR\"\n") + appendGitRepo(script, "$TMUX_PLUGIN_DIR/tpm", defaultTPMRepo) + appendGitRepo(script, "$TMUX_PLUGIN_DIR/tmux-resurrect", defaultResurrectRepo) + appendGitRepo(script, "$TMUX_PLUGIN_DIR/tmux-continuum", defaultContinuumRepo) + script.WriteString("TMUX_CONF=/root/.tmux.conf\n") + fmt.Fprintf(script, "TMUX_MANAGED_START=%s\n", shellQuote(tmuxManagedBlockStart)) + fmt.Fprintf(script, "TMUX_MANAGED_END=%s\n", shellQuote(tmuxManagedBlockEnd)) + script.WriteString("tmp_tmux_conf=$(mktemp)\n") + script.WriteString("if [[ -f \"$TMUX_CONF\" ]]; then\n") + script.WriteString(" awk -v begin=\"$TMUX_MANAGED_START\" -v end=\"$TMUX_MANAGED_END\" '$0 == begin { skip = 1; next } $0 == end { skip = 0; next } !skip { print }' \"$TMUX_CONF\" > \"$tmp_tmux_conf\"\n") + script.WriteString("else\n") + script.WriteString(" : > \"$tmp_tmux_conf\"\n") + script.WriteString("fi\n") + script.WriteString("if [[ -s \"$tmp_tmux_conf\" ]]; then\n") + script.WriteString(" printf '\\n' >> \"$tmp_tmux_conf\"\n") + script.WriteString("fi\n") + script.WriteString("cat >> \"$tmp_tmux_conf\" <<'EOF'\n") + script.WriteString(tmuxManagedBlockStart + "\n") + script.WriteString("set -g @plugin 'tmux-plugins/tpm'\n") + script.WriteString("set -g @plugin 'tmux-plugins/tmux-resurrect'\n") + script.WriteString("set -g @plugin 'tmux-plugins/tmux-continuum'\n") + script.WriteString("set -g @continuum-save-interval '15'\n") + script.WriteString("set -g @continuum-restore 'off'\n") + script.WriteString("set -g @resurrect-dir '/root/.tmux/resurrect'\n") + script.WriteString("run '~/.tmux/plugins/tpm/tpm'\n") + script.WriteString(tmuxManagedBlockEnd + "\n") + script.WriteString("EOF\n") + script.WriteString("mv \"$tmp_tmux_conf\" \"$TMUX_CONF\"\n") + script.WriteString("chmod 0644 \"$TMUX_CONF\"\n") +} + +func appendGitRepo(script *bytes.Buffer, dir, repo string) { + fmt.Fprintf(script, "if [[ -d \"%s/.git\" ]]; then\n", dir) + fmt.Fprintf(script, " git -C \"%s\" fetch --depth 1 origin\n", dir) + fmt.Fprintf(script, " git -C \"%s\" reset --hard FETCH_HEAD\n", dir) + script.WriteString("else\n") + fmt.Fprintf(script, " rm -rf \"%s\"\n", dir) + fmt.Fprintf(script, " git clone --depth 1 %s \"%s\"\n", shellQuote(repo), dir) + script.WriteString("fi\n") +} + +func appendGuestCleanup(script *bytes.Buffer) { + script.WriteString("rm -f /root/get-docker /root/get-docker.sh /tmp/get-docker /tmp/get-docker.sh\n") +} + func appendLineIfMissing(script *bytes.Buffer, path, line string) { fmt.Fprintf(script, "touch %s\n", shellQuote(path)) fmt.Fprintf(script, "if ! grep -Fqx %s %s; then\n", shellQuote(line), shellQuote(path)) diff --git a/internal/daemon/imagebuild_test.go b/internal/daemon/imagebuild_test.go index 56d404b..0a241cc 100644 --- a/internal/daemon/imagebuild_test.go +++ b/internal/daemon/imagebuild_test.go @@ -5,7 +5,7 @@ import ( "testing" ) -func TestBuildProvisionScriptInstallsAndActivatesMise(t *testing.T) { +func TestBuildProvisionScriptInstallsDefaultTools(t *testing.T) { t.Parallel() script := buildProvisionScript("devbox", "1.1.1.1", []string{"git", "curl"}, false) @@ -16,6 +16,17 @@ func TestBuildProvisionScriptInstallsAndActivatesMise(t *testing.T) { "if [ -n \"${BASH_VERSION:-}\" ] && [ -x '/usr/local/bin/mise' ]; then", `eval "$(/usr/local/bin/mise activate bash)"`, `if ! grep -Fqx 'eval "$(/usr/local/bin/mise activate bash)"' '/etc/bash.bashrc'; then`, + `git clone --depth 1 'https://github.com/tmux-plugins/tpm' "$TMUX_PLUGIN_DIR/tpm"`, + `git clone --depth 1 'https://github.com/tmux-plugins/tmux-resurrect' "$TMUX_PLUGIN_DIR/tmux-resurrect"`, + `git clone --depth 1 'https://github.com/tmux-plugins/tmux-continuum' "$TMUX_PLUGIN_DIR/tmux-continuum"`, + "# >>> banger tmux plugins >>>", + "set -g @plugin 'tmux-plugins/tmux-resurrect'", + "set -g @plugin 'tmux-plugins/tmux-continuum'", + "set -g @continuum-save-interval '15'", + "set -g @continuum-restore 'off'", + "set -g @resurrect-dir '/root/.tmux/resurrect'", + "run '~/.tmux/plugins/tpm/tpm'", + "rm -f /root/get-docker /root/get-docker.sh /tmp/get-docker /tmp/get-docker.sh", } { if !strings.Contains(script, snippet) { t.Fatalf("buildProvisionScript missing snippet %q\nscript:\n%s", snippet, script)