v3, heavy refactor needed
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
IMAGE="${DNC_IMAGE:-ghcr.io/lstmnemodel/dnc:latest}"
|
||||
|
||||
GPU=none
|
||||
if command -v nvidia-smi >/dev/null 2>&1; then
|
||||
GPU=nvidia
|
||||
fi
|
||||
|
||||
DRI=""
|
||||
[ -d /dev/dri ] && DRI=1
|
||||
|
||||
KFD=""
|
||||
[ -e /dev/kfd ] && KFD=1
|
||||
|
||||
DOCKER_SOCK="/var/run/docker.sock"
|
||||
|
||||
ROOTLESS=$(docker info --format '{{.ClientInfo.Context}}' 2>/dev/null | grep -ci rootless || true)
|
||||
if [ "$ROOTLESS" -gt 0 ] 2>/dev/null ; then
|
||||
echo "ERROR: dnc requires rootful Docker. Rootless Docker is not supported." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DNC_CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/dnc"
|
||||
mkdir -p "$DNC_CACHE"
|
||||
|
||||
TTY_FLAG=""
|
||||
[ -t 0 ] && TTY_FLAG="-t"
|
||||
|
||||
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" \
|
||||
-e "DNC_HOST_GPU=$GPU" \
|
||||
-e "DNC_HOST_DRI=$DRI" \
|
||||
-e "DNC_HOST_KFD=$KFD" \
|
||||
-e "DNC_HOST_UID=$(id -u)" \
|
||||
-e "DNC_HOST_GID=$(id -g)" \
|
||||
-e "DNC_HOST_USER=$USER" \
|
||||
-e "DNC_HOST_HOME=$HOME" \
|
||||
-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 \
|
||||
"$IMAGE" "$@"
|
||||
@@ -1,16 +0,0 @@
|
||||
ARG BASE_IMAGE_DIGEST
|
||||
FROM archlinux@${BASE_IMAGE_DIGEST}
|
||||
|
||||
ARG ALA_DATE
|
||||
|
||||
COPY pacman.conf /etc/pacman.conf
|
||||
COPY packages.list /tmp/packages.list
|
||||
|
||||
RUN sed "s|\$ala_date|$ALA_DATE|g" -i /etc/pacman.conf && \
|
||||
pacman -Syu --noconfirm && \
|
||||
pacman -S --noconfirm $(grep -v '^#' /tmp/packages.list | grep -v '^$') && \
|
||||
rm -f /tmp/packages.list
|
||||
|
||||
COPY config/ /usr/share/nnm/config/
|
||||
|
||||
RUN git clone --depth=1 --single-branch https://github.com/ohmyzsh/ohmyzsh.git /usr/share/ohmyzsh
|
||||
@@ -1,46 +0,0 @@
|
||||
font_family JetBrainsMono Nerd Font
|
||||
font_size 13.0
|
||||
disable_ligatures never
|
||||
cursor_shape beam
|
||||
cursor_blink_interval 0.5
|
||||
window_padding_width 6
|
||||
hide_window_decorations no
|
||||
background_opacity 0.97
|
||||
|
||||
background #001e26
|
||||
foreground #9bc1c2
|
||||
cursor #f34a00
|
||||
selection_background #003747
|
||||
color0 #002731
|
||||
color8 #006388
|
||||
color1 #d01b24
|
||||
color9 #f4153b
|
||||
color2 #6bbe6c
|
||||
color10 #50ee84
|
||||
color3 #a57705
|
||||
color11 #b17e28
|
||||
color4 #2075c7
|
||||
color12 #178dc7
|
||||
color5 #c61b6e
|
||||
color13 #e14d8e
|
||||
color6 #259185
|
||||
color14 #00b29e
|
||||
color7 #e9e2cb
|
||||
color15 #fcf4dc
|
||||
selection_foreground #001e26
|
||||
|
||||
mouse_hide_wait -3.0
|
||||
|
||||
map ctrl+grave send_text all \x1b[96;5u
|
||||
|
||||
tab_bar_style powerline
|
||||
tab_powerline_style slanted
|
||||
map ctrl+shift+t new_tab_with_cwd
|
||||
|
||||
enabled_layouts splits,stack
|
||||
map ctrl+shift+\ launch --location=vsplit --cwd=current
|
||||
map ctrl+shift+- launch --location=hsplit --cwd=current
|
||||
|
||||
scrollback_pager nvim --noplugin -R -c "set nonumber norelativenumber" -
|
||||
|
||||
map ctrl+shift+enter launch --cwd=current --type=window nvim
|
||||
@@ -1,38 +0,0 @@
|
||||
vim.g.mapleader = " "
|
||||
vim.g.maplocalleader = " "
|
||||
|
||||
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
|
||||
if not vim.loop.fs_stat(lazypath) then
|
||||
vim.fn.system({
|
||||
"git",
|
||||
"clone",
|
||||
"--filter=blob:none",
|
||||
"https://github.com/folke/lazy.nvim.git",
|
||||
"--branch=stable",
|
||||
lazypath,
|
||||
})
|
||||
end
|
||||
vim.opt.rtp:prepend(lazypath)
|
||||
|
||||
require("lazy").setup({
|
||||
spec = {
|
||||
{ "LazyVim/LazyVim", import = "lazyvim.plugins" },
|
||||
{ import = "plugins" },
|
||||
},
|
||||
defaults = {
|
||||
lazy = false,
|
||||
version = false,
|
||||
},
|
||||
install = { colorscheme = { "tokyonight" } },
|
||||
checker = { enabled = true },
|
||||
performance = {
|
||||
cache = { enabled = true },
|
||||
rtp = {
|
||||
disabled_plugins = {
|
||||
"gzip", "matchit", "matchparen",
|
||||
"netrwPlugin", "tarPlugin", "tohtml",
|
||||
"tutor", "zipPlugin",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,60 +0,0 @@
|
||||
return {
|
||||
{
|
||||
"LazyVim/LazyVim",
|
||||
opts = {
|
||||
colorscheme = "tokyonight",
|
||||
},
|
||||
},
|
||||
{
|
||||
"folke/tokyonight.nvim",
|
||||
opts = {
|
||||
style = "night",
|
||||
on_highlights = function(hl, c)
|
||||
hl.Normal = { bg = "#001e26", fg = "#9bc1c2" }
|
||||
hl.NormalFloat = { bg = "#002731" }
|
||||
hl.LineNr = { fg = "#006388" }
|
||||
hl.CursorLine = { bg = "#002731" }
|
||||
hl.CursorLineNr = { fg = "#9bc1c2" }
|
||||
hl.Visual = { bg = "#003747" }
|
||||
hl.VisualNOS = { bg = "#003747" }
|
||||
hl.Search = { bg = "#a57705", fg = "#001e26" }
|
||||
hl.IncSearch = { bg = "#f34a00", fg = "#001e26" }
|
||||
hl.Comment = { fg = "#006388", italic = true }
|
||||
hl.String = { fg = "#6bbe6c" }
|
||||
hl.Function = { fg = "#2075c7" }
|
||||
hl.Number = { fg = "#c61b6e" }
|
||||
hl.Boolean = { fg = "#c61b6e" }
|
||||
hl.Type = { fg = "#259185" }
|
||||
hl.Keyword = { fg = "#d01b24" }
|
||||
hl.Identifier = { fg = "#9bc1c2" }
|
||||
hl.Constant = { fg = "#a57705" }
|
||||
hl.Special = { fg = "#e14d8e" }
|
||||
hl.Title = { fg = "#178dc7", bold = true }
|
||||
hl.Todo = { bg = "#a57705", fg = "#001e26" }
|
||||
hl.Error = { fg = "#f4153b" }
|
||||
hl.WarningMsg = { fg = "#b17e28" }
|
||||
hl.MoreMsg = { fg = "#50ee84" }
|
||||
hl.Question = { fg = "#178dc7" }
|
||||
hl.Pmenu = { bg = "#002731", fg = "#9bc1c2" }
|
||||
hl.PmenuSel = { bg = "#003747", fg = "#e9e2cb" }
|
||||
hl.PmenuSbar = { bg = "#002731" }
|
||||
hl.PmenuThumb = { bg = "#006388" }
|
||||
hl.StatusLine = { bg = "#003747", fg = "#9bc1c2" }
|
||||
hl.StatusLineNC = { bg = "#002731", fg = "#006388" }
|
||||
hl.TabLine = { bg = "#002731", fg = "#006388" }
|
||||
hl.TabLineSel = { bg = "#003747", fg = "#e9e2cb" }
|
||||
hl.TabLineFill = { bg = "#001e26" }
|
||||
hl.NonText = { fg = "#006388" }
|
||||
hl.SpecialKey = { fg = "#00b29e" }
|
||||
hl.MatchParen = { bg = "#003747", fg = "#f34a00" }
|
||||
hl.DiffAdd = { bg = "#1a3a1a", fg = "#50ee84" }
|
||||
hl.DiffChange = { bg = "#1a2a3a", fg = "#178dc7" }
|
||||
hl.DiffDelete = { bg = "#3a1a1a", fg = "#f4153b" }
|
||||
hl.DiffText = { bg = "#1a3a3a", fg = "#00b29e" }
|
||||
hl.SpellBad = { undercurl = true, sp = "#f4153b" }
|
||||
hl.SpellCap = { undercurl = true, sp = "#b17e28" }
|
||||
hl.Whitespace = { fg = "#004455" }
|
||||
end,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
return {
|
||||
{
|
||||
"mbbill/undotree",
|
||||
keys = {
|
||||
{ "<leader>u", vim.cmd.UndotreeToggle, desc = "Undotree" },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
ZSH=/usr/share/ohmyzsh
|
||||
ZSH_THEME=agnoster
|
||||
source $ZSH/oh-my-zsh.sh
|
||||
|
||||
export TERM=xterm-256color
|
||||
|
||||
prompt_context() { prompt_segment magenta black "%n" }
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
command -v podman >/dev/null 2>&1 || { echo "Error: podman not found"; exit 1; }
|
||||
command -v distrobox >/dev/null 2>&1 || { echo "Error: distrobox not found"; exit 1; }
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
CONTAINER="nnm"
|
||||
|
||||
ALA_DATE="$(grep '^ala_date' "$SCRIPT_DIR/manifest.toml" | sed 's/.*"\(.*\)"/\1/')"
|
||||
BASE_DIGEST="$(grep '^base_image_digest' "$SCRIPT_DIR/manifest.toml" | sed 's/.*"\(.*\)"/\1/')"
|
||||
|
||||
TAG_INPUT="$(find "$SCRIPT_DIR/config" "$SCRIPT_DIR/Containerfile" -type f | sort | xargs -d'\n' sha256sum; cat "$SCRIPT_DIR/packages.list" "$SCRIPT_DIR/manifest.toml")"
|
||||
IMAGE_TAG="nnm:$(echo -n "$TAG_INPUT" | sha256sum | cut -c1-16)"
|
||||
IMAGE="localhost/$IMAGE_TAG"
|
||||
|
||||
if ! podman image exists "$IMAGE"; then
|
||||
echo "==> Building image..."
|
||||
podman build \
|
||||
--build-arg "ALA_DATE=$ALA_DATE" \
|
||||
--build-arg "BASE_IMAGE_DIGEST=$BASE_DIGEST" \
|
||||
-t "$IMAGE" -f "$SCRIPT_DIR/Containerfile" "$SCRIPT_DIR"
|
||||
fi
|
||||
|
||||
echo "==> Copying configs to ~/.config/nvimnemodel/..."
|
||||
mkdir -p ~/.config/nvimnemodel
|
||||
cp -r "$SCRIPT_DIR/config/"* ~/.config/nvimnemodel/
|
||||
|
||||
if ! distrobox list 2>/dev/null | grep -qw "$CONTAINER"; then
|
||||
echo "==> Creating distrobox container..."
|
||||
NVIDIA_FLAG=""
|
||||
[[ -d /dev/nvidia0 ]] && NVIDIA_FLAG="--nvidia"
|
||||
distrobox create --name "$CONTAINER" --image "$IMAGE" $NVIDIA_FLAG
|
||||
else
|
||||
echo "==> Upgrading container..."
|
||||
distrobox upgrade "$CONTAINER"
|
||||
fi
|
||||
|
||||
echo "==> Installing /usr/local/bin/nnm..."
|
||||
sudo cp "$SCRIPT_DIR/nnm" /usr/local/bin/nnm
|
||||
sudo chmod +x /usr/local/bin/nnm
|
||||
|
||||
echo ""
|
||||
echo "==> nnm installed"
|
||||
echo " Run 'nnm' to open a container terminal"
|
||||
echo " Run 'nnm init' to create .envrc for direnv"
|
||||
@@ -1,2 +0,0 @@
|
||||
ala_date = "2026/05/17"
|
||||
base_image_digest = "sha256:445aaca11be510adf10b9ddcf30259d4ae4c8474530a8d5d71ca93199184ee70"
|
||||
@@ -1,37 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER="nnm"
|
||||
CONFIG_DIR="$HOME/.config/nvimnemodel"
|
||||
|
||||
case "${1:-}" in
|
||||
init)
|
||||
DIR="${2:-$PWD}"
|
||||
ENVRC="$DIR/.envrc"
|
||||
if [[ -f "$ENVRC" ]]; then
|
||||
echo ".envrc already exists at $ENVRC"
|
||||
else
|
||||
cat > "$ENVRC" <<-ENVEOF
|
||||
if [ -z "\${NNM_ACTIVE:-}" ]; then
|
||||
export NNM_ACTIVE=1
|
||||
distrobox-enter $CONTAINER
|
||||
fi
|
||||
ENVEOF
|
||||
echo "Created $ENVRC — run 'direnv allow' to activate"
|
||||
fi
|
||||
;;
|
||||
-)
|
||||
shift
|
||||
exec distrobox-enter "$CONTAINER" -- "$@"
|
||||
;;
|
||||
*)
|
||||
distrobox-enter "$CONTAINER" -- \
|
||||
env XDG_CONFIG_HOME="$CONFIG_DIR" \
|
||||
ZDOTDIR="$CONFIG_DIR/zsh" \
|
||||
kitty --class="nnm" \
|
||||
--config "$CONFIG_DIR/kitty/kitty.conf" \
|
||||
--directory "$PWD" \
|
||||
zsh </dev/null 2>/tmp/nnm-kitty.log &
|
||||
disown
|
||||
;;
|
||||
esac
|
||||
@@ -1,8 +0,0 @@
|
||||
kitty
|
||||
neovim
|
||||
zsh
|
||||
git
|
||||
base-devel
|
||||
mesa
|
||||
vulkan-intel
|
||||
vulkan-radeon
|
||||
@@ -1,11 +0,0 @@
|
||||
[options]
|
||||
HoldPkg = pacman glibc
|
||||
Architecture = auto
|
||||
SigLevel = Required DatabaseOptional
|
||||
LocalFileSigLevel = Optional
|
||||
|
||||
[core]
|
||||
Server = https://archive.archlinux.org/repos/$ala_date/$repo/os/$arch
|
||||
|
||||
[extra]
|
||||
Server = https://archive.archlinux.org/repos/$ala_date/$repo/os/$arch
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "This will remove the nnm container and /usr/local/bin/nnm."
|
||||
read -rp "Are you sure? [y/N] " confirm
|
||||
[[ "$confirm" =~ ^[yY] ]] || { echo "Aborted."; exit 0; }
|
||||
|
||||
echo "==> Removing container..."
|
||||
distrobox rm --force nnm 2>/dev/null || true
|
||||
|
||||
echo "==> Removing image..."
|
||||
podman image rm localhost/nnm:* 2>/dev/null || true
|
||||
|
||||
echo "==> Removing /usr/local/bin/nnm..."
|
||||
sudo rm -f /usr/local/bin/nnm
|
||||
|
||||
echo "==> nnm uninstalled."
|
||||
@@ -0,0 +1,15 @@
|
||||
ARG PYTHON_IMAGE
|
||||
FROM ${PYTHON_IMAGE}
|
||||
|
||||
RUN pip install --no-cache-dir docker
|
||||
|
||||
ARG DOCKER_VERSION
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \
|
||||
&& curl -fsSL "https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz" \
|
||||
| tar xz -C /usr/local/bin --strip-components=1 docker/docker \
|
||||
&& apt-get purge -y curl ca-certificates && apt-get autoremove -y \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY src/dnc /opt/dnc/lib/dnc
|
||||
ENV PYTHONPATH=/opt/dnc/lib:$PYTHONPATH
|
||||
ENTRYPOINT ["python3", "-m", "dnc"]
|
||||
@@ -0,0 +1,26 @@
|
||||
__version__ = "0.2.0"
|
||||
DEFAULT_IMAGE = "ubuntu:latest"
|
||||
DNC_FILE = ".dnc"
|
||||
|
||||
LABEL_CONTAINER = "dnc.container"
|
||||
LABEL_PROJECT = "dnc.project"
|
||||
LABEL_VERSION = "dnc.version"
|
||||
LABEL_IMAGE = "dnc.image"
|
||||
LABEL_GPU = "dnc.gpu"
|
||||
|
||||
HOST_CACHE = "/opt/dnc/host"
|
||||
INIT_SRC = "/opt/dnc/lib/dnc/init.sh"
|
||||
INIT_DST = f"{HOST_CACHE}/init.sh"
|
||||
ENTRY_SRC = "/opt/dnc/lib/dnc/entry.sh"
|
||||
ENTRY_DST = f"{HOST_CACHE}/entry.sh"
|
||||
|
||||
HOST_GPU = "DNC_HOST_GPU"
|
||||
HOST_DRI = "DNC_HOST_DRI"
|
||||
HOST_KFD = "DNC_HOST_KFD"
|
||||
HOST_UID = "DNC_HOST_UID"
|
||||
HOST_GID = "DNC_HOST_GID"
|
||||
HOST_USER = "DNC_HOST_USER"
|
||||
HOST_HOME = "DNC_HOST_HOME"
|
||||
HOST_SHELL = "DNC_HOST_SHELL"
|
||||
HOST_CACHE_HOST = "DNC_CACHE_HOST"
|
||||
HOST_GIDS = "DNC_HOST_GIDS"
|
||||
@@ -0,0 +1,3 @@
|
||||
from dnc.cli import main
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,56 @@
|
||||
import os
|
||||
|
||||
DNC_MD_CONTENT = """# dnc
|
||||
|
||||
This project uses dnc (dockernecontainer). When `.dnc` is present,
|
||||
run ALL commands inside the container by prefixing them with `dnc -`:
|
||||
|
||||
dnc - nvim
|
||||
dnc - npm install
|
||||
dnc - python manage.py migrate
|
||||
"""
|
||||
|
||||
OPENCODE_INSTRUCTIONS = """## dnc
|
||||
|
||||
Refer to DNC.md for container usage instructions.
|
||||
"""
|
||||
|
||||
CURSOR_RULES = """description: dnc container
|
||||
globs: *
|
||||
---
|
||||
Refer to DNC.md for container usage instructions.
|
||||
"""
|
||||
|
||||
|
||||
def create_dnc_md(project_dir: str):
|
||||
path = os.path.join(project_dir, "DNC.md")
|
||||
if os.path.exists(path):
|
||||
print("DNC.md already exists (use --force to overwrite)")
|
||||
return
|
||||
with open(path, "w") as f:
|
||||
f.write(DNC_MD_CONTENT)
|
||||
print("Created DNC.md")
|
||||
|
||||
|
||||
def create_opencode(project_dir: str):
|
||||
opencode_dir = os.path.join(project_dir, ".opencode")
|
||||
os.makedirs(opencode_dir, exist_ok=True)
|
||||
path = os.path.join(opencode_dir, "instructions.md")
|
||||
if os.path.exists(path):
|
||||
print(".opencode/instructions.md already exists (use --force to overwrite)")
|
||||
return
|
||||
with open(path, "w") as f:
|
||||
f.write(OPENCODE_INSTRUCTIONS)
|
||||
print("Created .opencode/instructions.md")
|
||||
|
||||
|
||||
def create_cursor(project_dir: str):
|
||||
cursor_dir = os.path.join(project_dir, ".cursor", "rules")
|
||||
os.makedirs(cursor_dir, exist_ok=True)
|
||||
path = os.path.join(cursor_dir, "dnc.mdc")
|
||||
if os.path.exists(path):
|
||||
print(".cursor/rules/dnc.mdc already exists (use --force to overwrite)")
|
||||
return
|
||||
with open(path, "w") as f:
|
||||
f.write(CURSOR_RULES)
|
||||
print("Created .cursor/rules/dnc.mdc")
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
from dnc import __version__, DEFAULT_IMAGE, DNC_FILE
|
||||
from dnc import agents as agents_mod
|
||||
from dnc import container as ctr
|
||||
|
||||
|
||||
def resolve_container_name():
|
||||
current = os.getcwd()
|
||||
while True:
|
||||
path = os.path.join(current, DNC_FILE)
|
||||
if os.path.exists(path):
|
||||
name = _read_field(path, "container")
|
||||
if name:
|
||||
return current, name
|
||||
print(f"Error: {DNC_FILE} at {current} is malformed")
|
||||
sys.exit(1)
|
||||
parent = os.path.dirname(current)
|
||||
if parent == current:
|
||||
return None, None
|
||||
current = parent
|
||||
|
||||
|
||||
def _read_field(path: str, field: str) -> str | None:
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line.startswith(f"{field} = "):
|
||||
return line.split("=", 1)[1].strip()
|
||||
return None
|
||||
|
||||
|
||||
def _write_dnc(dnc_dir: str, container_name: str, version: str,
|
||||
image: str, gpu: str):
|
||||
path = os.path.join(dnc_dir, DNC_FILE)
|
||||
with open(path, "w") as f:
|
||||
f.write(f"container = {container_name}\n")
|
||||
f.write(f"version = {version}\n")
|
||||
f.write(f"image = {image}\n")
|
||||
f.write(f"gpu = {gpu}\n")
|
||||
|
||||
|
||||
def _show_help():
|
||||
print(f"""dnc — dockernecontainer v{__version__}
|
||||
|
||||
Usage:
|
||||
dnc Enter container
|
||||
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 agent [--opencode] [--cursor] Create DNC.md + agent rules
|
||||
dnc version Show version
|
||||
dnc help Show this help""")
|
||||
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
|
||||
if not args:
|
||||
dnc_dir, name = resolve_container_name()
|
||||
if name is None:
|
||||
print("No .dnc found. Run 'dnc setup' first.")
|
||||
sys.exit(1)
|
||||
ctr.enter(name)
|
||||
return
|
||||
|
||||
cmd = args[0]
|
||||
|
||||
if cmd in ("--help", "-h", "help"):
|
||||
_show_help()
|
||||
return
|
||||
|
||||
if cmd == "version":
|
||||
print(f"dnc v{__version__}")
|
||||
return
|
||||
|
||||
if cmd in ("list", "ls"):
|
||||
containers = ctr.list_containers()
|
||||
if not containers:
|
||||
print("No dnc containers found.")
|
||||
return
|
||||
print(f"{'NAME':20} {'PROJECT':30} {'IMAGE':25} {'GPU':10} {'STATUS':10}")
|
||||
print("-" * 95)
|
||||
for c in containers:
|
||||
proj = c["project"]
|
||||
if len(proj) > 30:
|
||||
proj = proj[:28] + ".."
|
||||
print(
|
||||
f"{c['name']:20} {proj:30} "
|
||||
f"{c['image'][:23]:25} {c['gpu']:10} {c['status']:10}"
|
||||
)
|
||||
return
|
||||
|
||||
if cmd == "setup":
|
||||
parser = argparse.ArgumentParser(prog="dnc setup")
|
||||
parser.add_argument("--image", "-i", default=None)
|
||||
parsed, _ = parser.parse_known_args(args[1:])
|
||||
|
||||
project_dir = os.getcwd()
|
||||
dnc_path = os.path.join(project_dir, DNC_FILE)
|
||||
if os.path.exists(dnc_path):
|
||||
print(f"{DNC_FILE} already exists. Run 'dnc rm' first.")
|
||||
return
|
||||
|
||||
image = parsed.image or DEFAULT_IMAGE
|
||||
name = ctr._default_container_name(project_dir)
|
||||
gpu = ctr._gpu()
|
||||
|
||||
ctr._pull_image(image)
|
||||
|
||||
print(f"Creating container '{name}'...")
|
||||
container = ctr.create(image, name, project_dir)
|
||||
|
||||
_write_dnc(project_dir, name, __version__, image, gpu)
|
||||
print(f"Created {DNC_FILE} — container starting in background.")
|
||||
return
|
||||
|
||||
if cmd == "info":
|
||||
dnc_dir, name = resolve_container_name()
|
||||
if name is None:
|
||||
print("No .dnc found.")
|
||||
sys.exit(1)
|
||||
info = ctr.inspect(name)
|
||||
print(f"Container: {info['name']}")
|
||||
print(f"Status: {info['status']}")
|
||||
print(f"Image: {info['image']}")
|
||||
print(f"GPU: {info['gpu']}")
|
||||
print(f"Project: {info['project']}")
|
||||
print(f"Created: {info['created']}")
|
||||
print("\nLabels:")
|
||||
for k, v in info["labels"].items():
|
||||
print(f" {k}: {v}")
|
||||
print("\nMounts:")
|
||||
for m in info["mounts"]:
|
||||
src = m.get("source") or "(anonymous)"
|
||||
print(f" {src} → {m['destination']} ({m['mode']})")
|
||||
print("\nEnvironment:")
|
||||
for k, v in sorted(info["env"].items()):
|
||||
if not k.startswith("DNC_"):
|
||||
display = v[:80] + "..." if len(v) > 80 else v
|
||||
print(f" {k}={display}")
|
||||
return
|
||||
|
||||
if cmd == "agent":
|
||||
parser = argparse.ArgumentParser(prog="dnc agent")
|
||||
parser.add_argument("--opencode", action="store_true")
|
||||
parser.add_argument("--cursor", action="store_true")
|
||||
parsed, _ = parser.parse_known_args(args[1:])
|
||||
agents_mod.create_dnc_md(os.getcwd())
|
||||
if parsed.opencode:
|
||||
agents_mod.create_opencode(os.getcwd())
|
||||
if parsed.cursor:
|
||||
agents_mod.create_cursor(os.getcwd())
|
||||
return
|
||||
|
||||
if cmd == "rm":
|
||||
dnc_dir, name = resolve_container_name()
|
||||
if name is None:
|
||||
print("No .dnc found.")
|
||||
sys.exit(1)
|
||||
ctr.remove(name)
|
||||
os.remove(os.path.join(dnc_dir, DNC_FILE))
|
||||
print(f"Removed container '{name}' and {DNC_FILE}.")
|
||||
return
|
||||
|
||||
if cmd == "-":
|
||||
dnc_dir, name = resolve_container_name()
|
||||
if name is None:
|
||||
print("No .dnc found. Run 'dnc setup' first.")
|
||||
sys.exit(1)
|
||||
rest = args[1:]
|
||||
if not rest:
|
||||
ctr.enter(name)
|
||||
else:
|
||||
interactive = sys.stdin.isatty()
|
||||
ctr.exec_passthrough(name, rest, interactive=interactive)
|
||||
return
|
||||
|
||||
_show_help()
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,442 @@
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
import docker
|
||||
from docker.types import DeviceRequest
|
||||
|
||||
from dnc import (
|
||||
__version__,
|
||||
ENTRY_DST,
|
||||
ENTRY_SRC,
|
||||
HOST_CACHE,
|
||||
HOST_CACHE_HOST,
|
||||
HOST_DRI,
|
||||
HOST_GID,
|
||||
HOST_GIDS,
|
||||
HOST_HOME,
|
||||
HOST_KFD,
|
||||
HOST_SHELL,
|
||||
HOST_UID,
|
||||
HOST_USER,
|
||||
HOST_GPU,
|
||||
INIT_DST,
|
||||
INIT_SRC,
|
||||
)
|
||||
|
||||
_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 = {
|
||||
"HOME",
|
||||
"PATH",
|
||||
"PWD",
|
||||
"SHELL",
|
||||
"container",
|
||||
"CONTAINER_ID",
|
||||
HOST_GPU,
|
||||
HOST_DRI,
|
||||
HOST_KFD,
|
||||
HOST_UID,
|
||||
HOST_GID,
|
||||
HOST_GIDS,
|
||||
HOST_USER,
|
||||
HOST_HOME,
|
||||
HOST_SHELL,
|
||||
HOST_CACHE_HOST,
|
||||
}
|
||||
|
||||
|
||||
def _docker_client():
|
||||
try:
|
||||
return docker.from_env()
|
||||
except docker.errors.DockerException as e:
|
||||
print("Error: cannot connect to Docker daemon.", file=sys.stderr)
|
||||
print(f" {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _host_cache_setup():
|
||||
os.makedirs(HOST_CACHE, exist_ok=True)
|
||||
shutil.copy2(INIT_SRC, INIT_DST)
|
||||
os.chmod(INIT_DST, 0o755)
|
||||
shutil.copy2(ENTRY_SRC, ENTRY_DST)
|
||||
os.chmod(ENTRY_DST, 0o755)
|
||||
|
||||
|
||||
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]
|
||||
return f"dnc-{safe}-{h}"
|
||||
|
||||
|
||||
def _gpu() -> str:
|
||||
return os.environ.get(HOST_GPU, "none")
|
||||
|
||||
|
||||
def _pull_image(image: str):
|
||||
client = _docker_client()
|
||||
try:
|
||||
client.images.get(image)
|
||||
return
|
||||
except docker.errors.ImageNotFound:
|
||||
pass
|
||||
try:
|
||||
print(f"Pulling {image}...")
|
||||
client.images.pull(image)
|
||||
except docker.errors.ImageNotFound:
|
||||
print(f"Error: image '{image}' not found.", file=sys.stderr)
|
||||
print(f" Check the name and try again.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except docker.errors.APIError as e:
|
||||
print(f"Error: failed to pull image '{image}': {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _require_container(client, name: str):
|
||||
try:
|
||||
return client.containers.get(name)
|
||||
except docker.errors.NotFound:
|
||||
print(f"Error: container '{name}' not found.", file=sys.stderr)
|
||||
print(f" It may have been deleted outside dnc.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def create(image: str, name: str, project_dir: str):
|
||||
_host_cache_setup()
|
||||
client = _docker_client()
|
||||
gpu = _gpu()
|
||||
|
||||
uid = int(os.environ[HOST_UID])
|
||||
gid = int(os.environ[HOST_GID])
|
||||
host_home = os.environ[HOST_HOME]
|
||||
shell = os.environ.get(HOST_SHELL, "/bin/bash")
|
||||
host_cache = os.environ[HOST_CACHE_HOST]
|
||||
|
||||
volumes = {
|
||||
host_home: {"bind": "/run/host/home", "mode": "ro"},
|
||||
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}/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_dri = os.environ.get(HOST_DRI, "")
|
||||
host_kfd = os.environ.get(HOST_KFD, "")
|
||||
|
||||
device_requests = []
|
||||
if gpu == "nvidia":
|
||||
device_requests = [
|
||||
DeviceRequest(count=-1, capabilities=[["gpu"]])
|
||||
]
|
||||
|
||||
if host_dri:
|
||||
volumes["/dev/dri"] = {"bind": "/dev/dri", "mode": "rw"}
|
||||
|
||||
if host_kfd:
|
||||
volumes["/dev/kfd"] = {"bind": "/dev/kfd", "mode": "rw"}
|
||||
|
||||
env = {
|
||||
"HOME": host_home,
|
||||
"SHELL": os.path.basename(shell),
|
||||
"container": "docker",
|
||||
"CONTAINER_ID": name,
|
||||
HOST_UID: uid,
|
||||
HOST_GID: gid,
|
||||
HOST_USER: os.environ[HOST_USER],
|
||||
HOST_HOME: host_home,
|
||||
HOST_SHELL: shell,
|
||||
}
|
||||
|
||||
for key in _FORWARD_ENV:
|
||||
val = os.environ.get(key)
|
||||
if val:
|
||||
env[key] = val
|
||||
|
||||
if gpu == "nvidia":
|
||||
env["NVIDIA_VISIBLE_DEVICES"] = "all"
|
||||
env["NVIDIA_DRIVER_CAPABILITIES"] = "all"
|
||||
|
||||
host_gids = os.environ.get(HOST_GIDS, "")
|
||||
group_add = [f"{g}" for g in host_gids.split() if g] if host_gids else None
|
||||
|
||||
device_cgroup_rules = []
|
||||
if host_dri:
|
||||
device_cgroup_rules.append("c 226:* rwm")
|
||||
if host_kfd:
|
||||
device_cgroup_rules.append("c 253:* rwm")
|
||||
|
||||
labels = {
|
||||
"dnc.container": "true",
|
||||
"dnc.project": project_dir,
|
||||
"dnc.version": __version__,
|
||||
"dnc.image": image,
|
||||
"dnc.gpu": gpu,
|
||||
}
|
||||
|
||||
try:
|
||||
container = client.containers.create(
|
||||
image,
|
||||
name=name,
|
||||
entrypoint=["/usr/bin/dnc-init"],
|
||||
command=["/bin/bash"],
|
||||
stdin_open=True,
|
||||
tty=True,
|
||||
cap_add=[
|
||||
"SYS_PTRACE",
|
||||
"SYS_ADMIN",
|
||||
"NET_RAW",
|
||||
"NET_ADMIN",
|
||||
"SYS_NICE",
|
||||
"IPC_LOCK",
|
||||
],
|
||||
security_opt=["label=disable"],
|
||||
pid_mode="host",
|
||||
network_mode="host",
|
||||
ipc_mode="host",
|
||||
pids_limit=-1,
|
||||
user="root:root",
|
||||
group_add=group_add,
|
||||
device_cgroup_rules=device_cgroup_rules or None,
|
||||
environment=env,
|
||||
volumes=volumes,
|
||||
device_requests=device_requests,
|
||||
labels=labels,
|
||||
detach=True,
|
||||
)
|
||||
container.start()
|
||||
except docker.errors.APIError as e:
|
||||
print(f"Error: failed to create/start container '{name}'.", file=sys.stderr)
|
||||
print(f" {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return container
|
||||
|
||||
|
||||
def _shell_needed(cmd: list[str]) -> bool:
|
||||
metachars = set('|>&;<>$`\'"!#(){}[]*?~ \t')
|
||||
return any(
|
||||
any(c in metachars for c in arg)
|
||||
for arg in cmd
|
||||
)
|
||||
|
||||
|
||||
def _ensure_executable(cmd: list[str]) -> list[str]:
|
||||
if _shell_needed(cmd):
|
||||
return ["sh", "-c", " ".join(cmd)]
|
||||
return cmd
|
||||
|
||||
|
||||
def _wait_init(container):
|
||||
max_retries = 120
|
||||
for _ in range(max_retries):
|
||||
try:
|
||||
exit_code, _ = container.exec_run(
|
||||
"test -f /.dnc-setup-done", tty=False
|
||||
)
|
||||
if exit_code == 0:
|
||||
return
|
||||
except docker.errors.APIError:
|
||||
pass
|
||||
time.sleep(0.2)
|
||||
|
||||
|
||||
def enter(name: str, cmd: list[str] | None = None):
|
||||
client = _docker_client()
|
||||
container = _require_container(client, name)
|
||||
|
||||
if container.status != "running":
|
||||
try:
|
||||
container.start()
|
||||
container.reload()
|
||||
while container.status != "running":
|
||||
time.sleep(0.1)
|
||||
container.reload()
|
||||
except docker.errors.APIError as e:
|
||||
print(f"Error: failed to start container '{name}'.", file=sys.stderr)
|
||||
print(f" {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
_wait_init(container)
|
||||
|
||||
uid = os.environ[HOST_UID]
|
||||
gid = os.environ[HOST_GID]
|
||||
|
||||
env_args = []
|
||||
for key, val in os.environ.items():
|
||||
if key in _SKIP_ENV:
|
||||
continue
|
||||
if '"' in val or "`" in val or "$" in val:
|
||||
continue
|
||||
env_args += ["-e", f"{key}={val}"]
|
||||
|
||||
fhs = [
|
||||
"/usr/local/sbin", "/usr/local/bin", "/usr/sbin",
|
||||
"/usr/bin", "/sbin", "/bin",
|
||||
]
|
||||
path = os.environ.get("PATH", "")
|
||||
for p in fhs:
|
||||
if p not in path:
|
||||
path = f"{path}:{p}"
|
||||
env_args += ["-e", f"PATH={path}"]
|
||||
|
||||
workdir = os.getcwd()
|
||||
|
||||
docker_args = [
|
||||
"docker",
|
||||
"exec",
|
||||
"-i",
|
||||
"--user", f"{uid}:{gid}",
|
||||
]
|
||||
if sys.stdin.isatty():
|
||||
docker_args.append("-t")
|
||||
docker_args += [
|
||||
"--workdir", workdir,
|
||||
*env_args,
|
||||
"--detach-keys", "",
|
||||
name,
|
||||
]
|
||||
if cmd:
|
||||
docker_args.extend(_ensure_executable(cmd))
|
||||
else:
|
||||
docker_args.append("/usr/bin/dnc-entry")
|
||||
|
||||
try:
|
||||
os.execvp("docker", docker_args)
|
||||
except FileNotFoundError:
|
||||
print("Error: 'docker' executable not found on PATH.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def exec_passthrough(name: str, cmd: list[str], interactive: bool = False):
|
||||
if interactive and sys.stdin.isatty():
|
||||
enter(name, cmd=cmd)
|
||||
return
|
||||
|
||||
client = _docker_client()
|
||||
container = _require_container(client, name)
|
||||
|
||||
if container.status != "running":
|
||||
try:
|
||||
container.start()
|
||||
container.reload()
|
||||
while container.status != "running":
|
||||
time.sleep(0.1)
|
||||
container.reload()
|
||||
except docker.errors.APIError as e:
|
||||
print(f"Error: failed to start container '{name}'.", file=sys.stderr)
|
||||
print(f" {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
_wait_init(container)
|
||||
|
||||
uid = os.environ[HOST_UID]
|
||||
gid = os.environ[HOST_GID]
|
||||
|
||||
env = {}
|
||||
for key, val in os.environ.items():
|
||||
if key in _SKIP_ENV:
|
||||
continue
|
||||
if '"' in val or "`" in val or "$" in val:
|
||||
continue
|
||||
env[key] = val
|
||||
|
||||
exit_code, output = container.exec_run(
|
||||
_ensure_executable(cmd),
|
||||
user=f"{uid}:{gid}",
|
||||
workdir=os.getcwd(),
|
||||
environment=env,
|
||||
tty=False,
|
||||
stream=False,
|
||||
stdin=True,
|
||||
)
|
||||
|
||||
if output:
|
||||
sys.stdout.buffer.write(output)
|
||||
sys.stdout.buffer.flush()
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
def list_containers() -> list[dict]:
|
||||
client = _docker_client()
|
||||
results = []
|
||||
for c in client.containers.list(
|
||||
all=True, filters={"label": "dnc.container=true"}
|
||||
):
|
||||
lbl = c.labels
|
||||
results.append({
|
||||
"name": c.name,
|
||||
"project": lbl.get("dnc.project", "?"),
|
||||
"image": lbl.get("dnc.image", "?"),
|
||||
"gpu": lbl.get("dnc.gpu", "?"),
|
||||
"status": c.status,
|
||||
"created": c.attrs.get("Created", ""),
|
||||
})
|
||||
results.sort(key=lambda x: x["created"], reverse=True)
|
||||
return results
|
||||
|
||||
|
||||
def remove(name: str):
|
||||
client = _docker_client()
|
||||
try:
|
||||
c = client.containers.get(name)
|
||||
except docker.errors.NotFound:
|
||||
print(f"Warning: container '{name}' does not exist (already removed?).", file=sys.stderr)
|
||||
return
|
||||
c.remove(force=True, v=True)
|
||||
|
||||
|
||||
def inspect(name: str) -> dict:
|
||||
client = _docker_client()
|
||||
c = _require_container(client, name)
|
||||
|
||||
raw_env = c.attrs.get("Config", {}).get("Env", [])
|
||||
parsed_env = {}
|
||||
for e in raw_env:
|
||||
if "=" in e:
|
||||
k, v = e.split("=", 1)
|
||||
parsed_env[k] = v
|
||||
|
||||
mounts = [
|
||||
{
|
||||
"source": m.get("Source"),
|
||||
"destination": m.get("Destination"),
|
||||
"mode": m.get("Mode"),
|
||||
}
|
||||
for m in c.attrs.get("Mounts", [])
|
||||
]
|
||||
|
||||
return {
|
||||
"name": c.name,
|
||||
"status": c.status,
|
||||
"image": c.image.tags[0] if c.image.tags else "?",
|
||||
"labels": dict(c.labels),
|
||||
"created": c.attrs.get("Created"),
|
||||
"env": parsed_env,
|
||||
"mounts": mounts,
|
||||
"gpu": c.labels.get("dnc.gpu", "none"),
|
||||
"project": c.labels.get("dnc.project", "?"),
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
if [ -d /run/host/home ]; then
|
||||
for f in .gitconfig .ssh; do
|
||||
src="/run/host/home/$f"
|
||||
dst="$HOME/$f"
|
||||
if [ -e "$src" ] && [ ! -e "$dst" ]; then
|
||||
cp -r "$src" "$dst" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if [ -x /opt/dnc/entrypoint.sh ]; then
|
||||
exec /opt/dnc/entrypoint.sh
|
||||
fi
|
||||
exec "${SHELL:-/bin/bash}" -l
|
||||
@@ -0,0 +1,56 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
HOST_USER="${DNC_HOST_USER:-$(id -un)}"
|
||||
HOST_UID="${DNC_HOST_UID:-1000}"
|
||||
HOST_GID="${DNC_HOST_GID:-1000}"
|
||||
HOST_HOME="${DNC_HOST_HOME:-/home/$HOST_USER}"
|
||||
|
||||
if ! getent group "$HOST_GID" >/dev/null 2>&1; then
|
||||
groupadd -g "$HOST_GID" "$HOST_USER" 2>/dev/null || \
|
||||
printf "%s:x:%s:\n" "$HOST_USER" "$HOST_GID" >> /etc/group
|
||||
fi
|
||||
|
||||
EXISTING_USER=$(getent passwd "$HOST_UID" 2>/dev/null | cut -d: -f1)
|
||||
if [ -n "$EXISTING_USER" ] && [ "$EXISTING_USER" != "$HOST_USER" ]; then
|
||||
EXISTING_GROUP=$(getent group "$HOST_GID" 2>/dev/null | cut -d: -f1)
|
||||
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" \
|
||||
-m "$EXISTING_USER" 2>/dev/null || true
|
||||
elif [ -z "$EXISTING_USER" ]; then
|
||||
useradd -M -u "$HOST_UID" -g "$HOST_GID" -s /bin/bash \
|
||||
-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
|
||||
SUDO_USER="$HOST_USER"
|
||||
|
||||
if ! command -v sudo >/dev/null 2>&1; then
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
DEBIAN_FRONTEND=noninteractive apt-get update -qq && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq sudo || true
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
dnf install -y sudo || true
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
yum install -y sudo || true
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
apk add sudo || true
|
||||
elif command -v zypper >/dev/null 2>&1; then
|
||||
zypper install -y sudo || true
|
||||
fi
|
||||
fi
|
||||
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
mkdir -p /etc/sudoers.d
|
||||
printf "%s ALL=(ALL) NOPASSWD:ALL\n" "$SUDO_USER" > /etc/sudoers.d/dnc-user
|
||||
chmod 0440 /etc/sudoers.d/dnc-user
|
||||
if sudo --version 2>&1 | head -1 | grep -qv sudo-rs; then
|
||||
printf "Defaults !requiretty\n" >> /etc/sudoers
|
||||
fi
|
||||
fi
|
||||
|
||||
touch /.dnc-setup-done
|
||||
|
||||
exec "$@"
|
||||
@@ -0,0 +1,2 @@
|
||||
PYTHON_IMAGE="python:3.14-slim"
|
||||
DOCKER_VERSION="29.5.2"
|
||||
Reference in New Issue
Block a user