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}"
|
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" "$@"
|
||||||
|
|||||||
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_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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user