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 870205e..c90bea7 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 @@ -38,6 +39,7 @@ Add Features to `.devcontainer/devcontainer.json`: { "features": { "ghcr.io/temeteke/devcontainer-features/codex:0": {}, + "ghcr.io/temeteke/devcontainer-features/dsh:0": {}, "ghcr.io/temeteke/devcontainer-features/github-cli:0": {}, "ghcr.io/temeteke/devcontainer-features/gitlab-cli:0": { "version": "1.126.0" @@ -135,6 +137,7 @@ Features only for projects you trust. | `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`, `~/.cache/opencode/npm` via `devcontainer-opencode-npm-cache` volume, `~/.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 | diff --git a/src/dsh/README.md b/src/dsh/README.md new file mode 100644 index 0000000..618abb5 --- /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 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" </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 + + 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 + +ensure_writable_dir "$BASE_DIR" + +link_dir "$BASE_DIR" "$HOME/.dsh"