From 72477923d73e19b8ab383beb5ea668f53102c685 Mon Sep 17 00:00:00 2001 From: temeteke Date: Sun, 18 Oct 2026 06:53:26 +0900 Subject: [PATCH 1/3] feat: add standalone DeepSeek Harness Dev Container Feature --- .github/workflows/validate.yaml | 22 +++++++++++++ README.md | 5 ++- src/dsh/README.md | 55 +++++++++++++++++++++++++++++++ src/dsh/devcontainer-feature.json | 36 ++++++++++++++++++++ src/dsh/install.sh | 25 ++++++++++++++ src/dsh/setup-persistence.sh | 35 ++++++++++++++++++++ 6 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/validate.yaml create mode 100644 src/dsh/README.md create mode 100644 src/dsh/devcontainer-feature.json create mode 100644 src/dsh/install.sh create mode 100644 src/dsh/setup-persistence.sh diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml new file mode 100644 index 0000000..8147204 --- /dev/null +++ b/.github/workflows/validate.yaml @@ -0,0 +1,22 @@ +name: Validate Dev Container Features + +on: + pull_request: + paths: + - "src/**" + - "test/**" + - ".github/workflows/validate.yaml" + push: + branches: [main] + paths: + - "src/**" + - "test/**" + - ".github/workflows/validate.yaml" + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Validate JSON and shell syntax + run: bash test/validate.sh diff --git a/README.md b/README.md index 6a65ebb..ee32fe6 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ agent-specific repository. It provides the following features: - Claude Code - Codex - OpenCode +- DeepSeek Harness (dsh) - Cline - Roo Code - Playwright Agent CLI @@ -34,6 +35,7 @@ This keeps the setting out of each project's `.devcontainer/devcontainer.json`. "ghcr.io/temeteke/devcontainer-features/claude-code:0": {}, "ghcr.io/temeteke/devcontainer-features/codex:0": {}, "ghcr.io/temeteke/devcontainer-features/opencode:0": {}, + "ghcr.io/temeteke/devcontainer-features/dsh:0": {}, "ghcr.io/temeteke/devcontainer-features/cline:0": {}, "ghcr.io/temeteke/devcontainer-features/roo-code:0": {}, "ghcr.io/temeteke/devcontainer-features/playwright:0": {}, @@ -86,6 +88,7 @@ other sensitive data. Since they are stored directly in your host home directory | `claude-code` | Coding tool via official Feature | `~/.claude` | | `codex` | Coding tool: `@openai/codex` via npm, VS Code extension `openai.chatgpt` | `~/.codex` | | `opencode` | Coding tool: `@opencode/cli` (OpenCode 2) via npm, VS Code extension `sst-dev.opencode` | `~/.config/opencode`, `~/.local/share/opencode`, `~/.agents` via `agents` | +| `dsh` | Agent harness: `@deepseek-ai/dsh` via npm | `~/.dsh` | | `cline` | Coding tool: VS Code extension `saoudrizwan.claude-dev` | `~/Documents/Cline`, `~/.cline` | | `roo-code` | Coding tool: VS Code extension `rooveterinaryinc.roo-cline` | `~/.roo`, VS Code globalStorage | | `playwright` | AI agent tool: Playwright Agent CLI and browser runtime only | none | @@ -115,7 +118,7 @@ read-only bind mount properties. This supports standard dotfile layouts, including Vim configurations that install plugins or write undo data under `~/.vim/`. -Before starting a Dev Container, ensure the corresponding host paths exist. +Before starting a Dev Container, ensure the corresponding host paths exist (including `~/.dsh` when enabled). A missing bind-mount source may prevent the container from starting. These Features do not mount the entire host HOME directory. diff --git a/src/dsh/README.md b/src/dsh/README.md new file mode 100644 index 0000000..9f870b5 --- /dev/null +++ b/src/dsh/README.md @@ -0,0 +1,55 @@ +# DeepSeek Harness (dsh) Feature + +Installs the official [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) +CLI (command: `dsh`) using npm. + +## Options + +- `install`: default `true` +- `persist`: default `true` +- `version`: npm version for `@deepseek-ai/dsh`, default `latest` + +## Persistence + +The default `~/.dsh` directory is bind-mounted from the host and linked into +the container home. It includes profiles, session state, and configuration. +The feature does not override `DSH_HOME`; a custom home must be managed +separately. + +**Before creating the container**, run `mkdir -p ~/.dsh` on the +Docker/Dev Container host so the bind-mount source exists. This directory is +writable inside the container; protect credentials and session data. + +The feature depends on the upstream Node.js feature (LTS). DSH is in +developer preview; new versions can introduce breaking changes. Pin +`version` when reproducible behavior is important. Native dependencies may +require a compiler toolchain if suitable prebuilt modules are unavailable. + +## Usage + +Examples inside a running Dev Container: + +```sh +dsh --help +dsh --profile headless "Summarize this repository" +dsh web --no-open +``` + +With the disposable `dcs` wrapper: + +```sh +dcs dsh --help +dcs dsh dsh --profile headless "Summarize this repository" +``` + +Official DSH is a launcher for profiles, including Web and headless modes, +not a Pi-style built-in terminal chat. The Web UI requires a reachable +container port and appropriate listening/forwarding configuration; `dcs` +does not publish Web UI ports automatically. Use a properly configured +persistent Dev Container for Web mode. + +Provider/model credentials are configured separately; installing the CLI +does not automatically configure an API provider. Third-party TUI profiles +are not installed by this Feature. + +This is an unofficial Feature, not a DeepSeek-maintained distribution. diff --git a/src/dsh/devcontainer-feature.json b/src/dsh/devcontainer-feature.json new file mode 100644 index 0000000..e42ccbc --- /dev/null +++ b/src/dsh/devcontainer-feature.json @@ -0,0 +1,36 @@ +{ + "id": "dsh", + "version": "0.1.0", + "name": "DeepSeek Harness", + "description": "Installs official DeepSeek Harness CLI and persists default ~/.dsh state.", + "options": { + "install": { + "type": "boolean", + "default": true, + "description": "Install the tool." + }, + "persist": { + "type": "boolean", + "default": true, + "description": "Link the default user-state directory to its bind-mounted host directory." + }, + "version": { + "type": "string", + "default": "latest", + "description": "npm version for @deepseek-ai/dsh; latest or a concrete version." + } + }, + "dependsOn": { + "ghcr.io/devcontainers/features/node:1": { + "version": "lts" + } + }, + "mounts": [ + { + "type": "bind", + "source": "${localEnv:HOME}/.dsh", + "target": "/mnt/devcontainer-features/dsh" + } + ], + "postCreateCommand": "/usr/local/share/devcontainer-features/dsh/setup-persistence.sh" +} diff --git a/src/dsh/install.sh b/src/dsh/install.sh new file mode 100644 index 0000000..4f5db5e --- /dev/null +++ b/src/dsh/install.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +FEATURE_ID="dsh" +FEATURE_DIR="/usr/local/share/devcontainer-features/${FEATURE_ID}" +BASE_DIR="/mnt/devcontainer-features/${FEATURE_ID}" + +mkdir -p "$FEATURE_DIR" "$BASE_DIR" +cp "${0%/*}/setup-persistence.sh" "$FEATURE_DIR/" +chmod +x "$FEATURE_DIR/setup-persistence.sh" + +cat > "${FEATURE_DIR}/feature.env" <&2 +fi + +if [ -L "$target_dir" ] && [ "$(readlink "$target_dir")" = "$source_dir" ]; then + exit 0 +fi + +if [ -L "$target_dir" ]; then + rm -f "$target_dir" +elif [ -e "$target_dir" ]; then + cp -a "$target_dir/." "$source_dir/" 2>/dev/null || true + rm -rf "$target_dir" +fi +ln -s "$source_dir" "$target_dir" +echo "[${FEATURE_ID}] Linked $target_dir -> $source_dir" From 7b8375071353befe3f81fcd88d57fe1357ccb81a Mon Sep 17 00:00:00 2001 From: temeteke Date: Thu, 5 Nov 2026 17:34:02 +0900 Subject: [PATCH 2/3] fix(dsh): preserve existing container state safely during persistence setup --- src/dsh/setup-persistence.sh | 144 ++++++++++++++++++++++++++++++----- 1 file changed, 123 insertions(+), 21 deletions(-) diff --git a/src/dsh/setup-persistence.sh b/src/dsh/setup-persistence.sh index 1baba04..d065c28 100644 --- a/src/dsh/setup-persistence.sh +++ b/src/dsh/setup-persistence.sh @@ -3,33 +3,135 @@ set -euo pipefail FEATURE_ID="dsh" FEATURE_DIR="/usr/local/share/devcontainer-features/${FEATURE_ID}" -if [ -f "${FEATURE_DIR}/feature.env" ]; then +FEATURE_ENV="${FEATURE_DIR}/feature.env" +if [ -f "${FEATURE_ENV}" ]; then # shellcheck disable=SC1090 - . "${FEATURE_DIR}/feature.env" + . "${FEATURE_ENV}" fi -if [ "${PERSIST:-true}" != "true" ]; then - echo "[${FEATURE_ID}] Persistence disabled." - exit 0 -fi +PERSIST="${PERSIST:-true}" +BASE_DIR="/mnt/devcontainer-features/${FEATURE_ID}" -source_dir="/mnt/devcontainer-features/${FEATURE_ID}" -target_dir="$HOME/.dsh" +log() { + echo "[${FEATURE_ID}] $*" +} -mkdir -p "$source_dir" "$(dirname "$target_dir")" -if [ ! -w "$source_dir" ]; then - echo "[${FEATURE_ID}] Warning: $source_dir is not writable by $(id -un)." >&2 -fi +is_true() { + case "${1:-}" in + true|True|TRUE|1|yes|YES|y|Y) return 0 ;; + *) return 1 ;; + esac +} + +ensure_writable_dir() { + local dir="$1" + mkdir -p "$dir" + if [ ! -w "$dir" ]; then + log "Warning: $dir is not writable by $(id -un). Trying to continue." + fi +} + +nearest_existing() { + local path="$1" + while [ ! -e "$path" ] && [ "$path" != "/" ]; do + path="$(dirname "$path")" + done + printf '%s\n' "$path" +} + +mount_of() { + local path + local mnt + + path="$(nearest_existing "$1")" + + if command -v findmnt >/dev/null 2>&1; then + mnt="$(findmnt -n -o TARGET -T "$path" 2>/dev/null | head -n1 || true)" + if [ -n "$mnt" ]; then + printf '%s\n' "$mnt" + return 0 + fi + fi + + if command -v stat >/dev/null 2>&1; then + mnt="$(stat -c '%m' "$path" 2>/dev/null || true)" + if [ -n "$mnt" ]; then + printf '%s\n' "$mnt" + return 0 + fi + fi +} + +link_dir() { + local src="$1" + local dst="$2" + local backup + local mnt + local src_mnt -if [ -L "$target_dir" ] && [ "$(readlink "$target_dir")" = "$source_dir" ]; then + src_mnt="$(mount_of "$src")" + if [ -z "$src_mnt" ] || [ "$src_mnt" = "/" ]; then + log "Warning: $src is not on a mounted host path. Leaving $dst untouched." + return 0 + fi + + ensure_writable_dir "$src" + + if [ -L "$dst" ]; then + local current + current="$(readlink "$dst" || true)" + if [ "$current" = "$src" ]; then + log "Already linked: $dst -> $src" + return 0 + fi + fi + + mnt="$(mount_of "$dst")" + if [ -z "$mnt" ]; then + log "Warning: Cannot determine the mount for $dst. Leaving it untouched." + return 0 + fi + if [ "$mnt" != "/" ]; then + log "Warning: $dst is on mount $mnt. Leaving it untouched." + return 0 + fi + + mkdir -p "$(dirname "$dst")" + + if [ -e "$dst" ] || [ -L "$dst" ]; then + if [ -d "$dst" ] && rmdir "$dst" 2>/dev/null; then + log "Removed empty directory: $dst" + else + backup="${dst}.devcontainer-backup" + if [ -e "$backup" ] || [ -L "$backup" ]; then + local index=1 + while [ -e "${backup}.${index}" ] || [ -L "${backup}.${index}" ]; do + index=$((index + 1)) + done + backup="${backup}.${index}" + fi + if mv "$dst" "$backup"; then + log "Moved existing $dst to $backup" + else + log "Error: Failed to move $dst. Persistence was not configured." + return 1 + fi + fi + fi + + if ln -sfn "$src" "$dst"; then + log "Linked: $dst -> $src" + else + log "Error: Failed to create symlink $dst -> $src. Persistence was not configured." + return 1 + fi +} + +if ! is_true "$PERSIST"; then + log "Persistence disabled." exit 0 fi -if [ -L "$target_dir" ]; then - rm -f "$target_dir" -elif [ -e "$target_dir" ]; then - cp -a "$target_dir/." "$source_dir/" 2>/dev/null || true - rm -rf "$target_dir" -fi -ln -s "$source_dir" "$target_dir" -echo "[${FEATURE_ID}] Linked $target_dir -> $source_dir" +ensure_writable_dir "$BASE_DIR" + +link_dir "$BASE_DIR" "$HOME/.dsh" From f8de2f351e5c38a82140784e74538be0ff5cb3a8 Mon Sep 17 00:00:00 2001 From: temeteke Date: Thu, 5 Nov 2026 17:39:55 +0900 Subject: [PATCH 3/3] docs(dsh): explicitly name command when forwarding CLI arguments --- src/dsh/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dsh/README.md b/src/dsh/README.md index 9f870b5..618abb5 100644 --- a/src/dsh/README.md +++ b/src/dsh/README.md @@ -38,7 +38,7 @@ dsh web --no-open With the disposable `dcs` wrapper: ```sh -dcs dsh --help +dcs dsh dsh --help dcs dsh dsh --profile headless "Summarize this repository" ```