diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..48efccf --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,94 @@ +name: Build and Package dnc + +on: + push: + tags: + - 'v*' + pull_request: + workflow_dispatch: + inputs: + version: + description: 'Version to build (e.g., 0.3.0)' + required: false + default: '' + +env: + KITTY_VERSION: '0.46.2' + GO_VERSION: '1.22' + +jobs: + build-packages: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Determine version + id: version + shell: bash + run: | + if [ -n "${{ inputs.version }}" ]; then + VERSION="${{ inputs.version }}" + elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then + VERSION="${GITHUB_REF_NAME#v}" + else + VERSION="0.2.0-$(git rev-parse --short HEAD)" + fi + echo "VERSION=$VERSION" >> $GITHUB_OUTPUT + + - name: Download kitty binary + run: | + set -e + mkdir -p build + KITTY_ARCHIVE="kitty-${KITTY_VERSION}-x86_64.txz" + KITTY_URL="https://github.com/kovidgoyal/kitty/releases/download/v${KITTY_VERSION}/${KITTY_ARCHIVE}" + echo "Downloading kitty from: $KITTY_URL" + curl -fL "$KITTY_URL" -o "build/$KITTY_ARCHIVE" + tar -xJf "build/$KITTY_ARCHIVE" -C build/ + mv build/kitty.app build/kitty + ls -la build/ + ls -la build/kitty/bin/ + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '${{ env.GO_VERSION }}' + + - name: Install nfpm + run: | + go install github.com/goreleaser/nfpm/v2/cmd/nfpm@latest + export PATH="$PATH:$(go env GOPATH)/bin" + nfpm --version + + - name: Build DEB package + env: + VERSION: '${{ steps.version.outputs.VERSION }}' + run: | + export PATH="$PATH:$(go env GOPATH)/bin" + mkdir -p dist + nfpm package \ + --packager deb \ + --config packaging/nfpm.yaml \ + --target dist/ + + - name: Build RPM package + env: + VERSION: '${{ steps.version.outputs.VERSION }}' + run: | + export PATH="$PATH:$(go env GOPATH)/bin" + mkdir -p dist + nfpm package \ + --packager rpm \ + --config packaging/nfpm.yaml \ + --target dist/ + + - name: List artifacts + run: | + echo "Packages built:" + ls -lh dist/ + + - name: Upload packages as artifacts + uses: actions/upload-artifact@v4 + with: + name: dnc-packages + path: dist/ diff --git a/README.md b/README.md index 6f5410b..d249570 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,139 @@ -# dockernecontainer +# dockernecontainer (dnc) -Dockernecontainer is a helper for creating and managing virtual development environments. \ No newline at end of file +Containerized per-project development environments with TUI and GPU support. + +## Dependencies + +**Required**: +- Linux with **rootful** Docker (≥20.10). Rootless Docker is detected and rejected. +- Project directory you want to containerize + +**GPU (choose what applies)**: +- NVIDIA: `nvidia-smi` on PATH + `nvidia-container-toolkit` installed +- Intel/AMD DRI: `/dev/dri` present on host (auto-detected) +- AMD compute (KFD): `/dev/kfd` present on host (auto-detected) + +## Usage + +```bash +# Create container from an image with your tools +dnc setup --image ghcr.io/lstmnemodel/dnc-test-arch:latest + +# Launch bundled kitty terminal (all tabs/splits exec into container) +dnc + +# Non-graphical fallback (useful for SSH sessions without display) +dnc -- /bin/bash + +# Run a command in container (non-interactive or interactive) +dnc -- pacman -Syu + +# Show container details +dnc info + +# List all containers +dnc list + +# Remove container +dnc rm +``` + +Each project directory gets its own container. A `.dnc` file in the project root tracks the container name. `dnc` walks up the directory tree to find it. + +## How It Works + +When you run `dnc` with no arguments: + +1. **Finds your project**: Walks up from `$PWD` to find `.dnc` file +2. **Extracts kitty config**: Copies `~/.config/kitty/` from INSIDE your project container +3. **Launches kitty**: Bundled kitty terminal with config from container +4. **Every new tab/window**: Automatically calls `docker exec -it` into your container + +Kitty runs on your host (for GPU rendering and display), but every shell you open in it runs inside your project container via `docker exec`. + +## What gets forwarded + +| Category | What | +|----------|------| +| Network | `--network=host`, `--pid=host`, `--ipc=host` | +| GPU | `--gpus all` (NVIDIA), `/dev/dri` bind-mount (Intel/AMD DRI), `/dev/kfd` (AMD compute) | +| Runtime dirs | `/tmp`, `/run/user/$UID` (for clipboard, temp files) | +| Git / SSH | SSH agent socket forwarded; `~/.ssh/`, `~/.gitconfig`, `~/.git-credentials` mounted at `/run/host/ssh`, `/run/host/gitconfig`, `/run/host/git-credentials` | +| User config | `~/.config/dnc/` mounted read-write at `/run/host/config/` — images can read `dnc.toml` for colors, keybinds, IDE settings | +| Shell history | Per-container history persists across rebuilds (`HISTFILE` set for bash/zsh, fish) | +| nvim state | Per-container swap/undo persists across rebuilds (`~/.local/state/nvim`) | +| Home | Fresh container home (not host bind-mount). Host home mounted read-only at `/run/host/home` | +| Host env | All host environment variables dumped to `/run/host/env` (shell-sourceable) | +| Project dir | Bind-mounted read-write at its host path | +| Sudo | Installed automatically via `pacman`/`dnf`/`apt`/etc., NOPASSWD for the container user | + +## Kitty Configuration + +**Kitty config lives inside your project container**, not on the host: + +``` +Inside container: ~/.config/kitty/kitty.conf +``` + +This way: +- Different projects can have different kitty configs +- Config travels with your container image +- New tabs automatically exec into your container (dnc overrides the `shell` directive) + +If no config is found in the container, kitty runs with defaults plus the dnc exec wrapper. + +## Extensibility via /run/host + +dnc provides host data at well-known paths inside the container for optional integration: + +| Path | Description | +|------|-------------| +| `/run/host/home` | Read-only bind-mount of host `$HOME` | +| `/run/host/env` | Shell-sourceable dump of all host environment variables | +| `/run/host/config` | Read-write bind-mount of `~/.config/dnc/` — user IDE config | +| `/run/host/ssh` | Read-only bind-mount of `~/.ssh/` | +| `/run/host/gitconfig` | Read-only bind-mount of `~/.gitconfig` (if present) | +| `/run/host/git-credentials` | Read-only bind-mount of `~/.git-credentials` (if present) | +| `/run/host/kitty` | Read-only mount of bundled kitty bundle — provides `kitten` binary, terminfo, and `kitten run-shell` for shell integration | +| `/run/host/state/history` | Per-container shell history (persists across rebuilds) | +| `/run/host/state/nvim` | Per-container nvim swap/undo files (persists across rebuilds) | + +### Config convention + +Images are expected to read `/run/host/config/dnc.toml` (if present) to generate native config files for the IDE. This lets you swap images while keeping your colors, keybinds, and preferences — write once in TOML, use across any compliant image. + +### Git / SSH + +SSH agent socket is forwarded automatically via `$SSH_AUTH_SOCK`. Git operations (clone, push, pull) work inside the container without additional setup if your host has SSH keys loaded and `gitconfig` configured. + +### Kitty shell integration + +When `TERM=xterm-kitty` and the bundled kitty bundle is available, `entry.sh` automatically delegates to `kitten run-shell`. This gives you: + +- Correct terminfo (`xterm-kitty`) — neovim and TUIs get true color, italics, styled underlines, cursor shapes +- Shell integration — directory tracking in window title, clickable file paths, scroll marks +- Clean shutdown — processes exit properly on window close (no warning dialog) + +No image-side configuration needed. The fallback (when `TERM` is not `xterm-kitty` or the bundle is absent) launches the user's login shell directly. + +## Limitations + +- **Rootful Docker only**: Rootless lacks the `--pid=host` capabilities dnc relies on. +- **Container user = host uid**: UID 1:1 mapping (required for rootful Docker). The container user gets `NOPASSWD` sudo so package installs still work. +- **No `--init` or systemd**: PID 1 is `init.sh`, not init/systemd. `systemctl` won't work. + +## Commands + +``` +dnc Launch kitty (bundled) with container context +dnc -- Non-graphical interactive shell (docker exec) +dnc -- Run command in container +dnc setup [--image IMG] Create container for this project +dnc rm Remove container + .dnc +dnc list List all dnc containers +dnc info Show container details +dnc web Show web IDE URL (if web_port set in .dnc) +dnc agent [--opencode] [--cursor] Create DNC.md + agent rules +dnc version Show version +dnc help Show this help +``` diff --git a/bin/dnc b/bin/dnc index 33aac84..770512b 100755 --- a/bin/dnc +++ b/bin/dnc @@ -3,6 +3,22 @@ set -e IMAGE="${DNC_IMAGE:-ghcr.io/lstmnemodel/dnc:latest}" +if [ $# -eq 0 ]; then + if [ -x "/opt/dnc/libexec/dnc-kitty-launcher" ]; then + exec /opt/dnc/libexec/dnc-kitty-launcher + else + script_dir="$(cd "$(dirname "$0")" && pwd)" + if [ -x "$script_dir/../libexec/dnc-kitty-launcher" ]; then + exec "$script_dir/../libexec/dnc-kitty-launcher" + elif command -v dnc-kitty-launcher >/dev/null 2>&1; then + exec dnc-kitty-launcher + else + echo "Error: dnc-kitty-launcher not found" >&2 + exit 1 + fi + fi +fi + GPU=none if command -v nvidia-smi >/dev/null 2>&1; then GPU=nvidia @@ -25,13 +41,26 @@ fi DNC_CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/dnc" mkdir -p "$DNC_CACHE" +DNC_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/dnc" +mkdir -p "$DNC_CONFIG" + +printenv > "$DNC_CACHE/host.env" + TTY_FLAG="" [ -t 0 ] && TTY_FLAG="-t" +# SSH agent socket forwarding +SSH_SOCK="${SSH_AUTH_SOCK:-}" + +# Kitvy bundle path (for kitten run-shell inside containers) +DNC_KITTY_PATH="" +[ -d /opt/dnc/kitty ] && DNC_KITTY_PATH=/opt/dnc/kitty + exec docker run --rm -i ${TTY_FLAG} \ -v "$DOCKER_SOCK:/var/run/docker.sock:ro" \ -v "$PWD:$PWD" -w "$PWD" \ -v "$DNC_CACHE:/opt/dnc/host:rw" \ + -v "$DNC_CACHE/host.env:/run/host/env:ro" \ -e "DNC_HOST_GPU=$GPU" \ -e "DNC_HOST_DRI=$DRI" \ -e "DNC_HOST_KFD=$KFD" \ @@ -42,8 +71,11 @@ exec docker run --rm -i ${TTY_FLAG} \ -e "DNC_HOST_SHELL=$SHELL" \ -e "DNC_HOST_GIDS=$(id -G)" \ -e "DNC_CACHE_HOST=$DNC_CACHE" \ - -e DISPLAY -e WAYLAND_DISPLAY -e XAUTHORITY \ - -e DBUS_SESSION_BUS_ADDRESS -e XDG_RUNTIME_DIR \ - -e PULSE_SERVER -e PIPEWIRE_RUNTIME_DIR \ - -e SSH_AUTH_SOCK -e TERM -e LANG \ + -e "DNC_CONFIG_HOST=$DNC_CONFIG" \ + -e "DNC_HOST_SSH=$HOME/.ssh" \ + -e "DNC_HOST_GITCONFIG=$HOME/.gitconfig" \ + -e "DNC_HOST_GITCRED=$HOME/.git-credentials" \ + ${DNC_KITTY_PATH:+-e "DNC_KITTY_PATH=$DNC_KITTY_PATH"} \ + ${SSH_SOCK:+-e "SSH_AUTH_SOCK=$SSH_SOCK"} \ + -e TERM -e LANG \ "$IMAGE" "$@" diff --git a/libexec/dnc-exec b/libexec/dnc-exec new file mode 100755 index 0000000..d6d21ec --- /dev/null +++ b/libexec/dnc-exec @@ -0,0 +1,26 @@ +#!/bin/bash +# dnc-exec - Kitty shell wrapper for dnc containerized mode + +if [ -z "$DNC_CONTAINER" ]; then + echo "Error: DNC_CONTAINER environment variable not set" >&2 + echo "This script should be run via dnc-kitty-launcher" >&2 + exit 1 +fi + +workdir="$PWD" + +SSH_ARGS="" +[ -n "$SSH_AUTH_SOCK" ] && SSH_ARGS="-e SSH_AUTH_SOCK=$SSH_AUTH_SOCK" + +if [ $# -eq 0 ]; then + exec docker exec -it $SSH_ARGS -w "$workdir" "$DNC_CONTAINER" /usr/bin/dnc-entry +else + for arg in "$@"; do + case "$arg" in + *'|'*|*'&'*|*';'*|*'<'*|*'>'*|*'$'*|*'`'*|*'"'*|*"'"*) + exec docker exec -it $SSH_ARGS -w "$workdir" "$DNC_CONTAINER" sh -c "$*" + ;; + esac + done + exec docker exec -it $SSH_ARGS -w "$workdir" "$DNC_CONTAINER" "$@" +fi diff --git a/libexec/dnc-kitty-launcher b/libexec/dnc-kitty-launcher new file mode 100755 index 0000000..b656217 --- /dev/null +++ b/libexec/dnc-kitty-launcher @@ -0,0 +1,92 @@ +#!/bin/bash +# dnc-kitty-launcher - Launch kitty with container context + +find_dnc_file() { + local dir="$PWD" + while [ "$dir" != "/" ]; do + if [ -f "$dir/.dnc" ]; then + echo "$dir/.dnc" + return + fi + dir="$(dirname "$dir")" + done +} + +dnc_file=$(find_dnc_file) +if [ -z "$dnc_file" ]; then + echo "Error: .dnc file not found. Run 'dnc setup' first." >&2 + exit 1 +fi + +container="" +dnc_dir="$(dirname "$dnc_file")" +while IFS= read -r line; do + case "$line" in + container*) + container="$(echo "$line" | cut -d= -f2 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" + ;; + esac +done < "$dnc_file" + +if [ -z "$container" ]; then + echo "Error: container= not found in $dnc_file" >&2 + exit 1 +fi + +container_status=$(docker inspect --format='{{.State.Status}}' "$container" 2>/dev/null || true) +if [ "$container_status" != "running" ]; then + echo "Starting container $container..." >&2 + docker start "$container" >/dev/null +fi + +tmpdir=$(mktemp -d -t dnc-kitty-XXXXXX) +cleanup() { rm -rf "$tmpdir"; } +trap cleanup EXIT + +uid=$(id -u) + +container_home=$(docker exec "$container" sh -c " + user_name=\$(getent passwd $uid | cut -d: -f1 2>/dev/null || echo '') + if [ -z \"\$user_name\" ]; then + echo /root + else + eval echo ~\$user_name + fi +") + +config_src="$container_home/.config/kitty" +docker cp "$container:$config_src/." "$tmpdir/" 2>/dev/null || true + +if [ -x "/opt/dnc/kitty/bin/kitty" ]; then + kitty="/opt/dnc/kitty/bin/kitty" +elif command -v kitty >/dev/null 2>&1; then + kitty="kitty" +else + echo "Error: kitty not found (not installed and no system kitty)" >&2 + exit 1 +fi + +mkdir -p "$tmpdir" + +if [ -x "/opt/dnc/libexec/dnc-exec" ]; then + dnc_exec="/opt/dnc/libexec/dnc-exec" +else + script_dir="$(cd "$(dirname "$0")" && pwd)" + dnc_exec="$script_dir/dnc-exec" +fi + +if [ -f "$tmpdir/kitty.conf" ]; then + echo "" >> "$tmpdir/kitty.conf" + echo "# dnc: all tabs go through docker exec" >> "$tmpdir/kitty.conf" +else + : +fi + +echo "shell $dnc_exec" >> "$tmpdir/kitty.conf" + +export KITTY_CONFIG_DIRECTORY="$tmpdir" +export DNC_CONTAINER="$container" + +cd "$dnc_dir" || exit 1 + +exec "$kitty" "$@" diff --git a/packaging/nfpm.yaml b/packaging/nfpm.yaml new file mode 100644 index 0000000..66d17e1 --- /dev/null +++ b/packaging/nfpm.yaml @@ -0,0 +1,64 @@ +name: "dnc" +arch: "amd64" +platform: "linux" +version: "${VERSION}" +section: "devel" +priority: "optional" +maintainer: "dnc" +description: | + dockernecontainer - Containerized per-project development environments. + TUI-focused with bundled kitty terminal. All tabs/splits run via + docker exec inside a project container. +vendor: "dnc" +license: "MIT" +homepage: "https://projects.vibe4d.com/lstmnemodel/nvimnemodel" + +contents: + - src: build/kitty/ + dst: /opt/dnc/kitty/ + type: tree + file_info: + mode: 0755 + + - src: bin/dnc + dst: /usr/bin/dnc + type: file + file_info: + mode: 0755 + + - src: libexec/dnc-kitty-launcher + dst: /opt/dnc/libexec/dnc-kitty-launcher + type: file + file_info: + mode: 0755 + + - src: libexec/dnc-exec + dst: /opt/dnc/libexec/dnc-exec + type: file + file_info: + mode: 0755 + + - src: src/dnc/ + dst: /opt/dnc/lib/dnc/ + type: tree + file_info: + mode: 0644 + + - src: README.md + dst: /usr/share/doc/dnc/README.md + type: file + +overrides: + deb: + depends: + - docker-ce | docker.io | containerd.io + provides: + - dnc + rpm: + depends: + - docker + provides: + - dnc + +scripts: + postinstall: packaging/scripts/postinst diff --git a/packaging/scripts/postinst b/packaging/scripts/postinst new file mode 100755 index 0000000..8088b1d --- /dev/null +++ b/packaging/scripts/postinst @@ -0,0 +1,28 @@ +#!/bin/bash +set -e + +cat <<'EOF' + +dnc installed successfully! + +Quick start: + dnc setup --image Create container for your project + dnc Launch bundled kitty in container mode + dnc -- /bin/bash Non-graphical shell (for SSH/no-display) + dnc -- Run command in container + dnc help Show full help + +Installed files: + /usr/bin/dnc Main command + /opt/dnc/kitty/ Bundled kitty terminal (v0.46.2) + /opt/dnc/lib/dnc/ Python CLI modules + /opt/dnc/libexec/ Helper scripts + +Kitty configuration: + Kitty config is read from INSIDE your project container: + ~/.config/kitty/kitty.conf + + New tabs in kitty automatically exec into your project container via docker. + +EOF +exit 0 diff --git a/src/dnc/__init__.py b/src/dnc/__init__.py index 6fd2df2..cc9a24c 100644 --- a/src/dnc/__init__.py +++ b/src/dnc/__init__.py @@ -14,6 +14,14 @@ INIT_DST = f"{HOST_CACHE}/init.sh" ENTRY_SRC = "/opt/dnc/lib/dnc/entry.sh" ENTRY_DST = f"{HOST_CACHE}/entry.sh" +# Inside project container paths +CONFIG_DIR = "/run/host/config" +STATE_DIR = "/run/host/state" +SSH_DIR = "/run/host/ssh" +GITCONFIG = "/run/host/gitconfig" +GITCRED = "/run/host/git-credentials" + +# Env vars set by bin/dnc for the CLI container HOST_GPU = "DNC_HOST_GPU" HOST_DRI = "DNC_HOST_DRI" HOST_KFD = "DNC_HOST_KFD" @@ -24,3 +32,12 @@ HOST_HOME = "DNC_HOST_HOME" HOST_SHELL = "DNC_HOST_SHELL" HOST_CACHE_HOST = "DNC_CACHE_HOST" HOST_GIDS = "DNC_HOST_GIDS" +HOST_CONFIG = "DNC_CONFIG_HOST" +HOST_STATE = "DNC_STATE_HOST" +HOST_SSH = "DNC_HOST_SSH" +HOST_GITCONFIG = "DNC_HOST_GITCONFIG" +HOST_GITCRED = "DNC_HOST_GITCRED" +HOST_KITTY_PATH = "DNC_KITTY_PATH" + +# Inside project container paths +KITTY_DIR = "/run/host/kitty" diff --git a/src/dnc/agents.py b/src/dnc/agents.py index fec9615..254d285 100644 --- a/src/dnc/agents.py +++ b/src/dnc/agents.py @@ -8,6 +8,17 @@ run ALL commands inside the container by prefixing them with `dnc -`: dnc - nvim dnc - npm install dnc - python manage.py migrate + +## Config + +User config lives in `~/.config/dnc/` (mounted at `/run/host/config/`). +Images can read `/run/host/config/dnc.toml` for colors, keybinds, etc. + +## Git / SSH + +SSH agent socket is forwarded automatically. SSH keys from `~/.ssh/` +and gitconfig from `~/.gitconfig` are available inside the container. +git push/pull works without additional setup. """ OPENCODE_INSTRUCTIONS = """## dnc diff --git a/src/dnc/cli.py b/src/dnc/cli.py index 975f380..77046ac 100644 --- a/src/dnc/cli.py +++ b/src/dnc/cli.py @@ -52,6 +52,7 @@ Usage: dnc rm Remove container + .dnc dnc list List all dnc containers dnc info Show container details + dnc web Show web IDE URL (if applicable) dnc agent [--opencode] [--cursor] Create DNC.md + agent rules dnc version Show version dnc help Show this help""") @@ -157,6 +158,19 @@ def main(): agents_mod.create_cursor(os.getcwd()) return + if cmd == "web": + dnc_dir, name = resolve_container_name() + if name is None: + print("No .dnc found.") + sys.exit(1) + port = _read_field(dnc_dir, "web_port") + if port: + print(f"http://localhost:{port}") + else: + print(f"Container '{name}' is running with --network=host.") + print("No web_port configured in .dnc. Add 'web_port = ' to use 'dnc web'.") + return + if cmd == "rm": dnc_dir, name = resolve_container_name() if name is None: diff --git a/src/dnc/container.py b/src/dnc/container.py index b4ebbae..7401cb5 100644 --- a/src/dnc/container.py +++ b/src/dnc/container.py @@ -11,37 +11,38 @@ from docker.types import DeviceRequest from dnc import ( __version__, + CONFIG_DIR, ENTRY_DST, ENTRY_SRC, + GITCONFIG, + GITCRED, HOST_CACHE, HOST_CACHE_HOST, + HOST_CONFIG, HOST_DRI, HOST_GID, HOST_GIDS, + HOST_GITCONFIG, + HOST_GITCRED, HOST_HOME, HOST_KFD, + HOST_KITTY_PATH, HOST_SHELL, + HOST_SSH, + HOST_STATE, HOST_UID, HOST_USER, HOST_GPU, INIT_DST, INIT_SRC, + KITTY_DIR, + SSH_DIR, + STATE_DIR, ) _FORWARD_ENV = { - "DISPLAY", - "WAYLAND_DISPLAY", - "XAUTHORITY", - "XDG_SESSION_TYPE", - "DBUS_SESSION_BUS_ADDRESS", - "XDG_RUNTIME_DIR", - "PULSE_SERVER", - "PIPEWIRE_RUNTIME_DIR", - "SSH_AUTH_SOCK", "TERM", "LANG", - "NVIDIA_VISIBLE_DEVICES", - "NVIDIA_DRIVER_CAPABILITIES", } _SKIP_ENV = { @@ -61,6 +62,12 @@ _SKIP_ENV = { HOST_HOME, HOST_SHELL, HOST_CACHE_HOST, + HOST_CONFIG, + HOST_STATE, + HOST_SSH, + HOST_GITCONFIG, + HOST_GITCRED, + HOST_KITTY_PATH, } @@ -81,6 +88,17 @@ def _host_cache_setup(): os.chmod(ENTRY_DST, 0o755) +def _state_dir_setup(name: str): + host_cache = os.environ.get(HOST_CACHE_HOST, "") + if not host_cache: + return "", "" + cli_state = os.path.join(HOST_CACHE, "state", name) + for sub in ("history", "nvim"): + os.makedirs(os.path.join(cli_state, sub), exist_ok=True) + host_state = os.path.join(host_cache, "state", name) + return host_state, host_state + + def _default_container_name(project_dir: str) -> str: safe = re.sub(r"[^a-zA-Z0-9_-]", "_", os.path.basename(project_dir)) h = hashlib.sha256(project_dir.encode()).hexdigest()[:8] @@ -135,15 +153,38 @@ def create(image: str, name: str, project_dir: str): project_dir: {"bind": project_dir, "mode": "rw"}, "/tmp": {"bind": "/tmp", "mode": "rw"}, f"/run/user/{uid}": {"bind": f"/run/user/{uid}", "mode": "rw"}, - "/tmp/.X11-unix": {"bind": "/tmp/.X11-unix", "mode": "rw"}, - "/etc/hosts": {"bind": "/etc/hosts", "mode": "ro"}, - "/etc/resolv.conf": {"bind": "/etc/resolv.conf", "mode": "ro"}, + f"{host_cache}/host.env": {"bind": "/run/host/env", "mode": "ro"}, f"{host_cache}/init.sh": {"bind": "/usr/bin/dnc-init", "mode": "ro"}, f"{host_cache}/entry.sh": {"bind": "/usr/bin/dnc-entry", "mode": "ro"}, } - if os.path.exists("/etc/hostname"): - volumes["/etc/hostname"] = {"bind": "/etc/hostname", "mode": "ro"} + host_state, _ = _state_dir_setup(name) + if host_state: + volumes[host_state] = {"bind": STATE_DIR, "mode": "rw"} + + host_config = os.environ.get(HOST_CONFIG, "") + if host_config: + volumes[host_config] = {"bind": CONFIG_DIR, "mode": "rw"} + + host_ssh_path = os.environ.get(HOST_SSH, "") + if host_ssh_path: + volumes[host_ssh_path] = {"bind": SSH_DIR, "mode": "ro"} + + host_gitconfig_path = os.environ.get(HOST_GITCONFIG, "") + if host_gitconfig_path: + volumes[host_gitconfig_path] = {"bind": GITCONFIG, "mode": "ro"} + + host_gitcred_path = os.environ.get(HOST_GITCRED, "") + if host_gitcred_path: + volumes[host_gitcred_path] = {"bind": GITCRED, "mode": "ro"} + + ssh_auth_sock = os.environ.get("SSH_AUTH_SOCK", "") + if ssh_auth_sock: + volumes[ssh_auth_sock] = {"bind": ssh_auth_sock, "mode": "rw"} + + host_kitty_path = os.environ.get(HOST_KITTY_PATH, "") + if host_kitty_path: + volumes[host_kitty_path] = {"bind": KITTY_DIR, "mode": "ro"} host_dri = os.environ.get(HOST_DRI, "") host_kfd = os.environ.get(HOST_KFD, "") @@ -162,7 +203,6 @@ def create(image: str, name: str, project_dir: str): env = { "HOME": host_home, - "SHELL": os.path.basename(shell), "container": "docker", "CONTAINER_ID": name, HOST_UID: uid, @@ -172,6 +212,9 @@ def create(image: str, name: str, project_dir: str): HOST_SHELL: shell, } + if ssh_auth_sock: + env["SSH_AUTH_SOCK"] = ssh_auth_sock + for key in _FORWARD_ENV: val = os.environ.get(key) if val: diff --git a/src/dnc/entry.sh b/src/dnc/entry.sh index af3b66b..5fe44c1 100644 --- a/src/dnc/entry.sh +++ b/src/dnc/entry.sh @@ -1,4 +1,5 @@ #!/bin/sh +# Legacy: copy .gitconfig/.ssh from host home read-only mount if [ -d /run/host/home ]; then for f in .gitconfig .ssh; do src="/run/host/home/$f" @@ -8,7 +9,44 @@ if [ -d /run/host/home ]; then fi done fi + +# SSH: symlink host ~/.ssh/* into container home +if [ -d /run/host/ssh ]; then + mkdir -p "$HOME/.ssh" + for f in /run/host/ssh/*; do + [ -f "$f" ] && ln -sf "$f" "$HOME/.ssh/" 2>/dev/null || true + done +fi + +# Git: include host gitconfig for credential helpers, aliases, etc. +if [ -f /run/host/gitconfig ]; then + git config --global include.path /run/host/gitconfig 2>/dev/null || true +fi + +# nvim state: persist swap/undo across rebuilds +if [ -d /run/host/state/nvim ]; then + mkdir -p "$HOME/.local/state" + ln -sfn /run/host/state/nvim "$HOME/.local/state/nvim" +fi + +# Shell history: per-container, persists across rebuilds +if [ -d /run/host/state/history ]; then + case "${SHELL##*/}" in + bash) export HISTFILE=/run/host/state/history/bash_history ;; + zsh) export HISTFILE=/run/host/state/history/zsh_history ;; + fish) export _history_filename=/run/host/state/history/fish_history ;; + esac +fi + +# Kitty shell integration via kitten run-shell +if [ -x /run/host/kitty/bin/kitten ] && [ "$TERM" = "xterm-kitty" ]; then + exec /run/host/kitty/bin/kitten run-shell +fi + +export DNC_CONFIG=/run/host/config + if [ -x /opt/dnc/entrypoint.sh ]; then exec /opt/dnc/entrypoint.sh fi -exec "${SHELL:-/bin/bash}" -l +user_shell=$(getent passwd "$(id -u)" 2>/dev/null | cut -d: -f7) +exec "${user_shell:-${SHELL:-/bin/bash}}" -l diff --git a/src/dnc/init.sh b/src/dnc/init.sh index c639bf7..558e911 100644 --- a/src/dnc/init.sh +++ b/src/dnc/init.sh @@ -17,14 +17,17 @@ if [ -n "$EXISTING_USER" ] && [ "$EXISTING_USER" != "$HOST_USER" ]; then if [ -n "$EXISTING_GROUP" ] && [ "$EXISTING_GROUP" != "$HOST_USER" ]; then groupmod -n "$HOST_USER" "$EXISTING_GROUP" 2>/dev/null || true fi - usermod -l "$HOST_USER" -s /bin/bash -d "$HOST_HOME" \ + usermod -l "$HOST_USER" -d "$HOST_HOME" \ -m "$EXISTING_USER" 2>/dev/null || true elif [ -z "$EXISTING_USER" ]; then - useradd -M -u "$HOST_UID" -g "$HOST_GID" -s /bin/bash \ + useradd -M -u "$HOST_UID" -g "$HOST_GID" \ -d "$HOST_HOME" "$HOST_USER" 2>/dev/null || true fi mkdir -p "$HOST_HOME" -chown "$HOST_UID:$HOST_GID" "$HOST_HOME" 2>/dev/null || true +if [ -d /etc/skel ] && [ -n "$(ls -A /etc/skel 2>/dev/null)" ]; then + cp -r /etc/skel/. "$HOST_HOME/" 2>/dev/null || true +fi +chown -R "$HOST_UID:$HOST_GID" "$HOST_HOME" 2>/dev/null || true SUDO_USER="$HOST_USER" if ! command -v sudo >/dev/null 2>&1; then