hypervibed

This commit is contained in:
2026-05-25 03:01:46 +03:00
parent 3de3d9c94a
commit 810e8cc7f2
13 changed files with 625 additions and 27 deletions
+94
View File
@@ -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/
+138 -2
View File
@@ -1,3 +1,139 @@
# dockernecontainer # dockernecontainer (dnc)
Dockernecontainer is a helper for creating and managing virtual development environments. 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 -- <cmd> 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
```
+36 -4
View File
@@ -3,6 +3,22 @@ set -e
IMAGE="${DNC_IMAGE:-ghcr.io/lstmnemodel/dnc:latest}" 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 GPU=none
if command -v nvidia-smi >/dev/null 2>&1; then if command -v nvidia-smi >/dev/null 2>&1; then
GPU=nvidia GPU=nvidia
@@ -25,13 +41,26 @@ fi
DNC_CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/dnc" DNC_CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/dnc"
mkdir -p "$DNC_CACHE" mkdir -p "$DNC_CACHE"
DNC_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/dnc"
mkdir -p "$DNC_CONFIG"
printenv > "$DNC_CACHE/host.env"
TTY_FLAG="" TTY_FLAG=""
[ -t 0 ] && TTY_FLAG="-t" [ -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} \ exec docker run --rm -i ${TTY_FLAG} \
-v "$DOCKER_SOCK:/var/run/docker.sock:ro" \ -v "$DOCKER_SOCK:/var/run/docker.sock:ro" \
-v "$PWD:$PWD" -w "$PWD" \ -v "$PWD:$PWD" -w "$PWD" \
-v "$DNC_CACHE:/opt/dnc/host:rw" \ -v "$DNC_CACHE:/opt/dnc/host:rw" \
-v "$DNC_CACHE/host.env:/run/host/env:ro" \
-e "DNC_HOST_GPU=$GPU" \ -e "DNC_HOST_GPU=$GPU" \
-e "DNC_HOST_DRI=$DRI" \ -e "DNC_HOST_DRI=$DRI" \
-e "DNC_HOST_KFD=$KFD" \ -e "DNC_HOST_KFD=$KFD" \
@@ -42,8 +71,11 @@ exec docker run --rm -i ${TTY_FLAG} \
-e "DNC_HOST_SHELL=$SHELL" \ -e "DNC_HOST_SHELL=$SHELL" \
-e "DNC_HOST_GIDS=$(id -G)" \ -e "DNC_HOST_GIDS=$(id -G)" \
-e "DNC_CACHE_HOST=$DNC_CACHE" \ -e "DNC_CACHE_HOST=$DNC_CACHE" \
-e DISPLAY -e WAYLAND_DISPLAY -e XAUTHORITY \ -e "DNC_CONFIG_HOST=$DNC_CONFIG" \
-e DBUS_SESSION_BUS_ADDRESS -e XDG_RUNTIME_DIR \ -e "DNC_HOST_SSH=$HOME/.ssh" \
-e PULSE_SERVER -e PIPEWIRE_RUNTIME_DIR \ -e "DNC_HOST_GITCONFIG=$HOME/.gitconfig" \
-e SSH_AUTH_SOCK -e TERM -e LANG \ -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" "$@" "$IMAGE" "$@"
+26
View File
@@ -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
+92
View File
@@ -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" "$@"
+64
View File
@@ -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
+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
set -e
cat <<'EOF'
dnc installed successfully!
Quick start:
dnc setup --image <image> Create container for your project
dnc Launch bundled kitty in container mode
dnc -- /bin/bash Non-graphical shell (for SSH/no-display)
dnc -- <command> 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
+17
View File
@@ -14,6 +14,14 @@ INIT_DST = f"{HOST_CACHE}/init.sh"
ENTRY_SRC = "/opt/dnc/lib/dnc/entry.sh" ENTRY_SRC = "/opt/dnc/lib/dnc/entry.sh"
ENTRY_DST = f"{HOST_CACHE}/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_GPU = "DNC_HOST_GPU"
HOST_DRI = "DNC_HOST_DRI" HOST_DRI = "DNC_HOST_DRI"
HOST_KFD = "DNC_HOST_KFD" HOST_KFD = "DNC_HOST_KFD"
@@ -24,3 +32,12 @@ HOST_HOME = "DNC_HOST_HOME"
HOST_SHELL = "DNC_HOST_SHELL" HOST_SHELL = "DNC_HOST_SHELL"
HOST_CACHE_HOST = "DNC_CACHE_HOST" HOST_CACHE_HOST = "DNC_CACHE_HOST"
HOST_GIDS = "DNC_HOST_GIDS" 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"
+11
View File
@@ -8,6 +8,17 @@ run ALL commands inside the container by prefixing them with `dnc -`:
dnc - nvim dnc - nvim
dnc - npm install dnc - npm install
dnc - python manage.py migrate 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 OPENCODE_INSTRUCTIONS = """## dnc
+14
View File
@@ -52,6 +52,7 @@ Usage:
dnc rm Remove container + .dnc dnc rm Remove container + .dnc
dnc list List all dnc containers dnc list List all dnc containers
dnc info Show container details dnc info Show container details
dnc web Show web IDE URL (if applicable)
dnc agent [--opencode] [--cursor] Create DNC.md + agent rules dnc agent [--opencode] [--cursor] Create DNC.md + agent rules
dnc version Show version dnc version Show version
dnc help Show this help""") dnc help Show this help""")
@@ -157,6 +158,19 @@ def main():
agents_mod.create_cursor(os.getcwd()) agents_mod.create_cursor(os.getcwd())
return 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 = <port>' to use 'dnc web'.")
return
if cmd == "rm": if cmd == "rm":
dnc_dir, name = resolve_container_name() dnc_dir, name = resolve_container_name()
if name is None: if name is None:
+60 -17
View File
@@ -11,37 +11,38 @@ from docker.types import DeviceRequest
from dnc import ( from dnc import (
__version__, __version__,
CONFIG_DIR,
ENTRY_DST, ENTRY_DST,
ENTRY_SRC, ENTRY_SRC,
GITCONFIG,
GITCRED,
HOST_CACHE, HOST_CACHE,
HOST_CACHE_HOST, HOST_CACHE_HOST,
HOST_CONFIG,
HOST_DRI, HOST_DRI,
HOST_GID, HOST_GID,
HOST_GIDS, HOST_GIDS,
HOST_GITCONFIG,
HOST_GITCRED,
HOST_HOME, HOST_HOME,
HOST_KFD, HOST_KFD,
HOST_KITTY_PATH,
HOST_SHELL, HOST_SHELL,
HOST_SSH,
HOST_STATE,
HOST_UID, HOST_UID,
HOST_USER, HOST_USER,
HOST_GPU, HOST_GPU,
INIT_DST, INIT_DST,
INIT_SRC, INIT_SRC,
KITTY_DIR,
SSH_DIR,
STATE_DIR,
) )
_FORWARD_ENV = { _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", "TERM",
"LANG", "LANG",
"NVIDIA_VISIBLE_DEVICES",
"NVIDIA_DRIVER_CAPABILITIES",
} }
_SKIP_ENV = { _SKIP_ENV = {
@@ -61,6 +62,12 @@ _SKIP_ENV = {
HOST_HOME, HOST_HOME,
HOST_SHELL, HOST_SHELL,
HOST_CACHE_HOST, 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) 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: def _default_container_name(project_dir: str) -> str:
safe = re.sub(r"[^a-zA-Z0-9_-]", "_", os.path.basename(project_dir)) safe = re.sub(r"[^a-zA-Z0-9_-]", "_", os.path.basename(project_dir))
h = hashlib.sha256(project_dir.encode()).hexdigest()[:8] 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"}, project_dir: {"bind": project_dir, "mode": "rw"},
"/tmp": {"bind": "/tmp", "mode": "rw"}, "/tmp": {"bind": "/tmp", "mode": "rw"},
f"/run/user/{uid}": {"bind": f"/run/user/{uid}", "mode": "rw"}, f"/run/user/{uid}": {"bind": f"/run/user/{uid}", "mode": "rw"},
"/tmp/.X11-unix": {"bind": "/tmp/.X11-unix", "mode": "rw"}, f"{host_cache}/host.env": {"bind": "/run/host/env", "mode": "ro"},
"/etc/hosts": {"bind": "/etc/hosts", "mode": "ro"},
"/etc/resolv.conf": {"bind": "/etc/resolv.conf", "mode": "ro"},
f"{host_cache}/init.sh": {"bind": "/usr/bin/dnc-init", "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"}, f"{host_cache}/entry.sh": {"bind": "/usr/bin/dnc-entry", "mode": "ro"},
} }
if os.path.exists("/etc/hostname"): host_state, _ = _state_dir_setup(name)
volumes["/etc/hostname"] = {"bind": "/etc/hostname", "mode": "ro"} 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_dri = os.environ.get(HOST_DRI, "")
host_kfd = os.environ.get(HOST_KFD, "") host_kfd = os.environ.get(HOST_KFD, "")
@@ -162,7 +203,6 @@ def create(image: str, name: str, project_dir: str):
env = { env = {
"HOME": host_home, "HOME": host_home,
"SHELL": os.path.basename(shell),
"container": "docker", "container": "docker",
"CONTAINER_ID": name, "CONTAINER_ID": name,
HOST_UID: uid, HOST_UID: uid,
@@ -172,6 +212,9 @@ def create(image: str, name: str, project_dir: str):
HOST_SHELL: shell, HOST_SHELL: shell,
} }
if ssh_auth_sock:
env["SSH_AUTH_SOCK"] = ssh_auth_sock
for key in _FORWARD_ENV: for key in _FORWARD_ENV:
val = os.environ.get(key) val = os.environ.get(key)
if val: if val:
+39 -1
View File
@@ -1,4 +1,5 @@
#!/bin/sh #!/bin/sh
# Legacy: copy .gitconfig/.ssh from host home read-only mount
if [ -d /run/host/home ]; then if [ -d /run/host/home ]; then
for f in .gitconfig .ssh; do for f in .gitconfig .ssh; do
src="/run/host/home/$f" src="/run/host/home/$f"
@@ -8,7 +9,44 @@ if [ -d /run/host/home ]; then
fi fi
done done
fi 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 if [ -x /opt/dnc/entrypoint.sh ]; then
exec /opt/dnc/entrypoint.sh exec /opt/dnc/entrypoint.sh
fi 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
+6 -3
View File
@@ -17,14 +17,17 @@ if [ -n "$EXISTING_USER" ] && [ "$EXISTING_USER" != "$HOST_USER" ]; then
if [ -n "$EXISTING_GROUP" ] && [ "$EXISTING_GROUP" != "$HOST_USER" ]; then if [ -n "$EXISTING_GROUP" ] && [ "$EXISTING_GROUP" != "$HOST_USER" ]; then
groupmod -n "$HOST_USER" "$EXISTING_GROUP" 2>/dev/null || true groupmod -n "$HOST_USER" "$EXISTING_GROUP" 2>/dev/null || true
fi 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 -m "$EXISTING_USER" 2>/dev/null || true
elif [ -z "$EXISTING_USER" ]; then 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 -d "$HOST_HOME" "$HOST_USER" 2>/dev/null || true
fi fi
mkdir -p "$HOST_HOME" 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" SUDO_USER="$HOST_USER"
if ! command -v sudo >/dev/null 2>&1; then if ! command -v sudo >/dev/null 2>&1; then