From 7fbe884880701b7e819200b0324bc1d4a3089687 Mon Sep 17 00:00:00 2001 From: jsburckhardt Date: Sun, 21 Sep 2025 04:09:07 +0000 Subject: [PATCH 1/2] feat: add OpenCode feature with installation script and tests --- .github/copilot-instructions.md | 1 + .../prompts/generate-commit-message.prompt.md | 18 -- .github/prompts/new-feature.prompt.md | 152 +++++++++++++++ .github/workflows/test.yaml | 1 + README.md | 19 ++ src/opencode/devcontainer-feature.json | 21 ++ src/opencode/install.sh | 184 ++++++++++++++++++ test/_global/all-tools.sh | 1 + test/_global/opencode-specific-version.sh | 7 + test/_global/scenarios.json | 11 +- test/opencode/test.sh | 7 + 11 files changed, 403 insertions(+), 19 deletions(-) delete mode 100644 .github/prompts/generate-commit-message.prompt.md create mode 100644 .github/prompts/new-feature.prompt.md create mode 100644 src/opencode/devcontainer-feature.json create mode 100755 src/opencode/install.sh create mode 100755 test/_global/opencode-specific-version.sh create mode 100755 test/opencode/test.sh diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index bfe7d72..7d45458 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -14,3 +14,4 @@ When creating a new feature, follow these steps: - Update the ../../test/_global/all-tools.sh to validate the new feature is installed. - Update the ../../test/_global/scenarios.json to include the new feature. - include the feature in the workflows +- run `devcontainer features test -f $(feature) -i ubuntu:latest` diff --git a/.github/prompts/generate-commit-message.prompt.md b/.github/prompts/generate-commit-message.prompt.md deleted file mode 100644 index ec332a4..0000000 --- a/.github/prompts/generate-commit-message.prompt.md +++ /dev/null @@ -1,18 +0,0 @@ -Generate a Git commit message using the Conventional Commits format. Then, include three --trailer fields: - -Assistant-model: The name of the AI model (e.g., GPT-4o) - -LLM-Contrib: An estimated percentage of contribution from the AI (e.g., 60%) - -Prompt: A brief human instruction or goal (e.g., "Refactor for readability") - -Format the final output like this: - -bash -Copy -Edit -git commit --message "type(scope): concise description" \ - --trailer "Assistant-model: GPT-4o" \ - --trailer "LLM-Contrib: 60%" \ - --trailer "Prompt: " -Use an appropriate Conventional Commit type (like feat, fix, refactor, docs, etc.) and optionally include a scope if relevant. diff --git a/.github/prompts/new-feature.prompt.md b/.github/prompts/new-feature.prompt.md new file mode 100644 index 0000000..61ebc6c --- /dev/null +++ b/.github/prompts/new-feature.prompt.md @@ -0,0 +1,152 @@ +--- +mode: 'agent' +description: 'Implement a new Dev Container Feature in this repository.' +--- + +# New Devcontainer Feature Generator + +## Purpose +Implement a new Dev Container Feature in this repository. + +## Invocation Pattern (Human Usage Examples) +- `/new-feature just in @casey/just` +- `/new-feature ripgrep in @BurntSushi/ripgrep` +- `/new-feature mise in @jdx/mise version=2024.7.1` +- `/new-feature protolint in @yoheimuta/protolint checksum=true` + +Copilot: Parse the user’s invocation to fill the Variables section below before generating changes. + +## Variables (to be derived or defaulted) +Extract from user command or infer: +- FEATURE_ID: Lowercase id (no spaces). Example: `just`, `ripgrep`, `mise`. +- FEATURE_DISPLAY_NAME: Human-friendly (capitalize if appropriate). Example: `Just`, `Ripgrep`. +- SOURCE_REPO: GitHub `owner/name` the binary/tool comes from. Example: `casey/just`. +- BINARY_NAME: Name of the installed executable (often equals FEATURE_ID; override if different). +- REQUESTED_VERSION: Explicit version if user supplied `version=...`; else "latest". +- SUPPORT_CHECKSUM: Boolean if user included `checksum=true`; default false. +- EXTRA_OPTIONS: Optional structured options (e.g. additional feature options) if user appended `opt:key=value` pairs. +- INITIAL_FEATURE_VERSION: Always start at `1.0.0` (Feature’s internal semver, NOT the tool upstream version). +- ARCH_LIST: Default `[x86_64, i686, aarch64, armv7]` (omit unsupported ones if known). +- RELEASE_ASSET_PATTERN: Infer typical asset naming scheme once you inspect the SOURCE_REPO’s latest release assets (handle suffix variations like `linux-x86_64.tar.gz`, `.zip`, plain binary, etc.). + +If inference is ambiguous, favor adding commented fallback logic rather than guessing incorrectly. + +## High-Level Task +Create a new Feature at `src/FEATURE_ID` that: +1. Installs the tool from GitHub releases (or alternative upstream if tool uses another distribution method). +2. Supports a `version` option (default `"latest"`) resolving to the latest stable release (exclude pre-releases unless user explicitly requests one). +3. Implements multi-architecture support mapping `uname -m` to release asset names. +4. Provides graceful fallback if GitHub API calls fail (e.g. rate limit / firewall) by: + - Trying API first (`https://api.github.com/repos/SOURCE_REPO/releases/latest`) + - Falling back to parsing HTML of `https://github.com/SOURCE_REPO/releases/latest` or using a lightweight strategy (document what you did). +5. (Optional) Performs checksum verification if SUPPORT_CHECKSUM is true AND the upstream provides checksums (e.g. `SHA256SUMS` file). +6. Adds tests: basic install + pinned version test; checksum test if applicable. +7. Updates root README to list the feature with usage examples. +8. Integrates tests into existing CI matrix (mirroring the just feature pattern). +9. Uses consistent shell style and safety (`set -euo pipefail`, traps for cleanup). +10. Leaves unrelated files untouched (NO incidental formatting changes, NO kyverno/zarf edits, NO drive-by refactors). + +## File/Directory Deliverables +Create: +- `src/FEATURE_ID/devcontainer-feature.json` +- `src/FEATURE_ID/install.sh` +- `test/FEATURE_ID/test.sh` +- `test/_global/FEATURE_ID-specific-version.sh` +update +- `test/_global/all-tools.sh` +- `test/_global/scenarios.json` +- `.github/workflows/test.yaml` + +Modify minimally: +- Root `README.md` (add new feature entry & examples) + +## devcontainer-feature.json Requirements +- `id`: FEATURE_ID +- `version`: INITIAL_FEATURE_VERSION +- `name`: FEATURE_DISPLAY_NAME +- `description`: Concise summary of what the tool does +- `documentationURL`: Link to this feature’s README +- `options.version`: string, default "latest" +- Add any EXTRA_OPTIONS (preserve ordering) +- `instantiationMode`: "onCreate" + +## install.sh Requirements +1. `#!/usr/bin/env bash` +2. `set -euo pipefail` +3. Parse inputs: `VERSION=$VERSION` (from feature option) -> treat "latest" specially. +4. Architecture map example (adjust names per RELEASE_ASSET_PATTERN): + - `x86_64` or `amd64` -> `x86_64` + - `aarch64` or `arm64` -> `aarch64` + - `armv7l` -> `armv7` + - `i386` or `i686` -> `i686` +5. Resolve version: + - If VERSION == "latest": + - Try GitHub API → extract tag_name minus leading `v` if present + - Fallback to scraping or light pattern detection from releases page HTML + - Else trust user-supplied +6. Construct download URL (document pattern). +7. Download to temp dir. Use curl with retry/backoff flags similar to just feature style. +8. Extract / move binary: + - If archive: detect extension (.tar.gz, .zip), extract accordingly. + - Ensure final binary path: `/usr/local/bin/BINARY_NAME` and `chmod 0755`. +9. (Optional) Checksum: + - Download checksums file, grep the asset, verify via `sha256sum -c -` +10. Verification: + - Run `${BINARY_NAME} --version` or fallback command; ensure it prints something containing resolved version if version != "latest". +11. Output a success message. + + +## CI Integration +- Update workflow to include FEATURE_ID in matrix if that’s how others are integrated. +- Keep ordering logical (append near similar tooling features). +- Avoid reorganizing unrelated entries. + +## Commit / PR Conventions +Suggested (may auto-squash): +1. `chore: scaffold FEATURE_ID feature` +2. `feat: implement FEATURE_ID install logic` +3. `test: add tests for FEATURE_ID` +4. `docs: add FEATURE_ID feature documentation` + +PR Title: +`feat: add FEATURE_ID devcontainer feature` + +PR Body Should Include: +- Summary +- Version handling strategy +- Architecture support +- Checksum support (yes/no) +- Usage examples +- Tests added +- Statement: “No unrelated changes; excludes kyverno/zarf fixes.” + +## Constraints & Non-Goals +- Do NOT refactor global scripts. +- Do NOT adjust unrelated workflow triggers. +- Do NOT rename existing features. +- Do NOT remove or reorder existing README content beyond adding new entry. + +## Acceptance Criteria +- All new tests pass in CI. +- Feature listed in root README. +- `install.sh` resilient to GitHub API failure with fallback. +- Pinning a version works. +- Latest resolution works. +- No unrelated file diffs (confirm with `git diff` scope). +- If checksum requested and available upstream, verification implemented (skip gracefully if not). + +## Output Format Instruction for Copilot +When generating the PR: +1. Create new branch (named `feature/FEATURE_ID`). +2. Add/modify files exactly as specified. +3. Open a pull request with the defined title and body. +4. Include only relevant changes. + +## Post-Generation Self-Check (Copilot) +Before finalizing: +- Re-open each new file; ensure placeholders replaced. +- Ensure executable bits on `install.sh` and test scripts. +- Ensure JSON is valid (no trailing commas). +- Ensure shell uses POSIX-compatible constructs (or justify bash usage). +- Verify version extraction logic with both “latest” and pinned flows (mentally or via test design). +- Confirm absence of kyverno/zarf references / modifications. diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 56306fa..09c96e1 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -29,6 +29,7 @@ jobs: - codex - bat - just + - opencode baseImage: - debian:latest - ubuntu:latest diff --git a/README.md b/README.md index f72f195..e099ffd 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ This repository contains a _collection_ of Features. | just | https://github.com/casey/just | A command runner. Just is a handy way to save and run project-specific commands. | | UV/UVX | https://docs.astral.sh/uv/ | An extremely fast Python package and project manager, written in Rust. A single tool to replace pip, pip-tools, pipx, poetry, pyenv, virtualenv, and more. | | Ruff | https://docs.astral.sh/ruff/ | An extremely fast Python linter and code formatter, written in Rust. | +| OpenCode | https://opencode.ai/ | AI coding agent, built for the terminal. An open-source alternative to Claude Code with support for multiple LLM providers. | | Codex-cli | https://github.com/openai/codex | Codex CLI is an experimental project under active development. | @@ -283,6 +284,24 @@ Running `k3d` inside the built container will print the help menu of k3d. ```bash k3d --version ``` + +### `opencode` + +Running `opencode` inside the built container will allow you to use the AI coding agent. + +```jsonc +{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "ghcr.io/jsburckhardt/devcontainer-features/opencode:1": {} + } +} +``` + +```bash +opencode --version +``` + ### `Codex-CLI` Running `codex` inside the built container will print the help menu of codex. diff --git a/src/opencode/devcontainer-feature.json b/src/opencode/devcontainer-feature.json new file mode 100644 index 0000000..e9e4df8 --- /dev/null +++ b/src/opencode/devcontainer-feature.json @@ -0,0 +1,21 @@ +{ + "name": "OpenCode", + "id": "opencode", + "version": "1.0.0", + "description": "AI coding agent, built for the terminal. An open-source alternative to Claude Code with support for multiple LLM providers.", + "documentationURL": "https://opencode.ai/docs", + "options": { + "version": { + "type": "string", + "default": "latest", + "description": "Version of opencode to install from GitHub releases e.g. 0.10.4" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "sst-dev.opencode" + ] + } + } +} diff --git a/src/opencode/install.sh b/src/opencode/install.sh new file mode 100755 index 0000000..a72c268 --- /dev/null +++ b/src/opencode/install.sh @@ -0,0 +1,184 @@ +#!/usr/bin/env bash + +# Variables +REPO_OWNER="sst" +REPO_NAME="opencode" +OPENCODE_VERSION="${VERSION:-"latest"}" + +set -euo pipefail + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Clean up +rm -rf /var/lib/apt/lists/* + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" >/dev/null 2>&1; then + if [ "$(find /var/lib/apt/lists/* | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update -y + fi + apt-get -y install --no-install-recommends "$@" + fi +} + +# Make sure we have curl, ca-certificates, and unzip +check_packages curl ca-certificates unzip jq + +echo "Installing OpenCode version: $OPENCODE_VERSION" + +# Determine the OS and architecture +OS=$(uname -s | tr '[:upper:]' '[:lower:]') +ARCH=$(uname -m) + +case "$ARCH" in + x86_64) + ARCH_SUFFIX="x64" + ;; + aarch64 | arm64) + ARCH_SUFFIX="arm64" + ;; + *) + echo "ERROR: Unsupported architecture: $ARCH" + echo "Supported architectures: x86_64, aarch64/arm64" + exit 1 + ;; +esac + +case "$OS" in + linux) + PLATFORM="linux" + ;; + *) + echo "ERROR: Unsupported OS: $OS" + echo "Supported OS: Linux" + exit 1 + ;; +esac + +# Function to resolve the latest version using GitHub API +resolve_latest_version() { + echo "Resolving latest version using GitHub API..." >&2 + local api_response + api_response=$(curl -s --max-time 10 "https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest" 2>/dev/null || echo "") + + if [ -n "$api_response" ] && echo "$api_response" | jq -e '.tag_name' >/dev/null 2>&1; then + local version_tag + version_tag=$(echo "$api_response" | jq -r '.tag_name') + # Remove 'v' prefix if present + echo "${version_tag#v}" + return 0 + else + echo "GitHub API failed, falling back to HTML parsing..." >&2 + return 1 + fi +} + +# Function to resolve latest version by parsing HTML (fallback) +resolve_latest_version_fallback() { + echo "Attempting to resolve latest version from releases page HTML..." >&2 + local releases_page + releases_page=$(curl -s --max-time 10 "https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/latest" 2>/dev/null || echo "") + + if [ -n "$releases_page" ]; then + # Look for version tag in the HTML + local version_tag + version_tag=$(echo "$releases_page" | grep -oE 'releases/tag/v?[0-9]+\.[0-9]+\.[0-9]+' | head -1 | sed 's/.*releases\/tag\/v\?\([0-9]\+\.[0-9]\+\.[0-9]\+\).*/\1/') + + if [ -n "$version_tag" ] && [[ "$version_tag" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "$version_tag" + return 0 + fi + fi + + echo "Failed to resolve version from HTML, using known fallback version..." >&2 + echo "0.10.4" # Known recent version as last resort + return 1 +} + +# Resolve version +if [ "$OPENCODE_VERSION" = "latest" ]; then + if ! RESOLVED_VERSION=$(resolve_latest_version); then + RESOLVED_VERSION=$(resolve_latest_version_fallback) + fi + echo "Resolved latest version to: $RESOLVED_VERSION" + OPENCODE_VERSION="$RESOLVED_VERSION" +else + echo "Using specified version: $OPENCODE_VERSION" + # Remove 'v' prefix if present in user input + OPENCODE_VERSION="${OPENCODE_VERSION#v}" +fi + +# Construct download URL based on opencode's release pattern +# Asset name format: opencode-{platform}-{arch}.zip +ASSET_NAME="opencode-${PLATFORM}-${ARCH_SUFFIX}.zip" +DOWNLOAD_URL="https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/v${OPENCODE_VERSION}/${ASSET_NAME}" + +echo "Downloading OpenCode from: ${DOWNLOAD_URL}" + +# Create temporary directory +TMP_DIR=$(mktemp -d) +trap 'rm -rf "$TMP_DIR"' EXIT + +cd "$TMP_DIR" + +# Download with retries and proper error handling +for attempt in 1 2 3; do + if curl -fL --retry 3 --retry-delay 2 -o "${ASSET_NAME}" "${DOWNLOAD_URL}"; then + echo "Download successful on attempt $attempt" + break + else + echo "Download attempt $attempt failed" + if [ $attempt -eq 3 ]; then + echo "ERROR: Failed to download after 3 attempts" + exit 1 + fi + sleep 2 + fi +done + +# Verify the download +if [ ! -f "${ASSET_NAME}" ] || [ ! -s "${ASSET_NAME}" ]; then + echo "ERROR: Downloaded file is missing or empty" + exit 1 +fi + +echo "Extracting OpenCode..." +unzip -q "${ASSET_NAME}" + +# Find the binary in the extracted contents +# The archive should contain the opencode binary +if [ -f "opencode" ]; then + echo "Installing opencode..." + mv "opencode" /usr/local/bin/ + chmod +x /usr/local/bin/opencode +elif [ -f "bin/opencode" ]; then + echo "Installing opencode from bin directory..." + mv "bin/opencode" /usr/local/bin/ + chmod +x /usr/local/bin/opencode +else + echo "ERROR: Could not find opencode binary in archive" + echo "Archive contents:" + ls -la + exit 1 +fi + +# Clean up +cd - >/dev/null +rm -rf /var/lib/apt/lists/* + +# Verify installation +echo "Verifying installation..." +if opencode --version >/dev/null 2>&1; then + opencode --version + echo "OpenCode installation completed successfully!" +else + echo "Warning: OpenCode installed but version check failed. This might be expected behavior." + echo "OpenCode installation completed!" +fi + +echo "Done!" diff --git a/test/_global/all-tools.sh b/test/_global/all-tools.sh index 4151ed4..98cc7cd 100755 --- a/test/_global/all-tools.sh +++ b/test/_global/all-tools.sh @@ -17,5 +17,6 @@ check "jnv" jnv -V check "zarf" zarf version check "codex" codex --version check "just" just --version +check "opencode" opencode --version reportResults diff --git a/test/_global/opencode-specific-version.sh b/test/_global/opencode-specific-version.sh new file mode 100755 index 0000000..1eb087d --- /dev/null +++ b/test/_global/opencode-specific-version.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -e +source dev-container-features-test-lib +check "opencode with specific version" /bin/bash -c "opencode --version | grep '0.10.0'" + +reportResults diff --git a/test/_global/scenarios.json b/test/_global/scenarios.json index 542f16c..f2545e1 100644 --- a/test/_global/scenarios.json +++ b/test/_global/scenarios.json @@ -18,7 +18,8 @@ "uv": {}, "zarf": {}, "codex": {}, - "just": {} + "just": {}, + "opencode": {} } }, "flux-specific-version": { @@ -138,5 +139,13 @@ "version": "1.42.0" } } + }, + "opencode-specific-version": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "opencode": { + "version": "0.10.0" + } + } } } diff --git a/test/opencode/test.sh b/test/opencode/test.sh new file mode 100755 index 0000000..80db629 --- /dev/null +++ b/test/opencode/test.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -e + +source dev-container-features-test-lib +check "opencode" opencode --version +reportResults From 0caff1596819c202ae45b249b889ed4af6e07b29 Mon Sep 17 00:00:00 2001 From: jsburckhardt Date: Sun, 21 Sep 2025 04:14:38 +0000 Subject: [PATCH 2/2] fix: remove deprecated GitHub CLI feature and replace with latest version --- .devcontainer.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.devcontainer.json b/.devcontainer.json index 7e2fb76..66ee671 100644 --- a/.devcontainer.json +++ b/.devcontainer.json @@ -34,9 +34,7 @@ "features": { "ghcr.io/devcontainers/features/docker-in-docker:2": {}, "ghcr.io/stuartleeks/dev-container-features/shell-history:0": {}, - "ghcr.io/jsburckhardt/devcontainer-features/gic:1": {} - - + "ghcr.io/devcontainers/features/github-cli:latest": {} }, "updateContentCommand": "npm install -g @devcontainers/cli", "remoteUser": "node",