Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/validate.yaml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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 |
Expand Down
55 changes: 55 additions & 0 deletions src/dsh/README.md
Original file line number Diff line number Diff line change
@@ -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.
36 changes: 36 additions & 0 deletions src/dsh/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -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"
}
25 changes: 25 additions & 0 deletions src/dsh/install.sh
Original file line number Diff line number Diff line change
@@ -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" <<ENVEOF
PERSIST="${PERSIST:-true}"
ENVEOF

if [ "${INSTALL:-true}" != "true" ]; then
echo "[${FEATURE_ID}] Installation disabled."
exit 0
fi

package="@deepseek-ai/dsh"
if [ "${VERSION:-latest}" != "latest" ]; then
package+="@${VERSION}"
fi
npm install --global "$package"
137 changes: 137 additions & 0 deletions src/dsh/setup-persistence.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#!/usr/bin/env bash
set -euo pipefail

FEATURE_ID="dsh"
FEATURE_DIR="/usr/local/share/devcontainer-features/${FEATURE_ID}"
FEATURE_ENV="${FEATURE_DIR}/feature.env"
if [ -f "${FEATURE_ENV}" ]; then
# shellcheck disable=SC1090
. "${FEATURE_ENV}"
fi

PERSIST="${PERSIST:-true}"
BASE_DIR="/mnt/devcontainer-features/${FEATURE_ID}"

log() {
echo "[${FEATURE_ID}] $*"
}

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

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"
Loading