hypervibed
This commit is contained in:
@@ -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/
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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" "$@"
|
||||
|
||||
Executable
+26
@@ -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
|
||||
Executable
+92
@@ -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" "$@"
|
||||
@@ -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
|
||||
Executable
+28
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = <port>' to use 'dnc web'.")
|
||||
return
|
||||
|
||||
if cmd == "rm":
|
||||
dnc_dir, name = resolve_container_name()
|
||||
if name is None:
|
||||
|
||||
+60
-17
@@ -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:
|
||||
|
||||
+39
-1
@@ -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
|
||||
|
||||
+6
-3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user