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..99a831c 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ agent-specific repository. It provides the following features: - Claude Code - Codex - OpenCode +- Pi Coding Agent - 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/pi: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` | +| `pi` | Coding tool: `@earendil-works/pi-coding-agent` via npm | `~/.pi/agent` | | `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/pi/README.md b/src/pi/README.md new file mode 100644 index 0000000..87f707c --- /dev/null +++ b/src/pi/README.md @@ -0,0 +1,44 @@ +# Pi Coding Agent Feature + +Installs the [Pi Coding Agent](https://pi.dev/) CLI with npm (command: `pi`). + +## Options + +- `install`: default `true` +- `persist`: default `true` +- `version`: npm version for `@earendil-works/pi-coding-agent`, default `latest` + +## Persistence + +The default `~/.pi/agent` directory is bind-mounted from the host and linked into +the container's user home. This includes authentication, settings, installed +extensions, and sessions. Project-local `.pi/` files remain in the workspace. +The feature uses the default agent directory; custom `PI_CODING_AGENT_DIR` +locations are not automatically mounted. + +**Before creating the container**, run `mkdir -p ~/.pi/agent` on the +Docker/Dev Container host so the bind-mount source exists. Data is writable +from within the container and survives deletion of a disposable container. + +The feature depends on the upstream Node.js feature (LTS). It uses the +upstream-recommended `npm install -g --ignore-scripts` form. + +## Usage + +Add this Feature to your `devcontainer.json`: + +```json +{ + "features": { + "ghcr.io/temeteke/devcontainer-features/pi:0": {} + } +} +``` + +Then open the Dev Container and run `pi` in its terminal. + +First configure your preferred provider with `/login` or supply an API key +through your chosen environment configuration. Host credentials in +`~/.pi/agent` are exposed to the container. + +The independent upstream project is not affiliated with this Feature. diff --git a/src/pi/devcontainer-feature.json b/src/pi/devcontainer-feature.json new file mode 100644 index 0000000..4ebed35 --- /dev/null +++ b/src/pi/devcontainer-feature.json @@ -0,0 +1,36 @@ +{ + "id": "pi", + "version": "0.1.0", + "name": "Pi Coding Agent", + "description": "Installs the Pi CLI and persists default ~/.pi/agent 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 @earendil-works/pi-coding-agent; latest or a concrete version." + } + }, + "dependsOn": { + "ghcr.io/devcontainers/features/node:1": { + "version": "lts" + } + }, + "mounts": [ + { + "type": "bind", + "source": "${localEnv:HOME}/.pi/agent", + "target": "/mnt/devcontainer-features/pi" + } + ], + "postCreateCommand": "/usr/local/share/devcontainer-features/pi/setup-persistence.sh" +} diff --git a/src/pi/install.sh b/src/pi/install.sh new file mode 100644 index 0000000..abb4027 --- /dev/null +++ b/src/pi/install.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +FEATURE_ID="pi" +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/.pi/agent"