diff --git a/.awf.yaml b/.awf.yaml new file mode 100644 index 0000000..5aca13a --- /dev/null +++ b/.awf.yaml @@ -0,0 +1,10 @@ +# AWF Configuration +# https://github.com/awf-project/cli + +version: "1" + +# Default log level: debug, info, warn, error +log_level: info + +# Output format: text, json, table, quiet +output_format: text diff --git a/.awf/config.yaml b/.awf/config.yaml new file mode 100644 index 0000000..1982799 --- /dev/null +++ b/.awf/config.yaml @@ -0,0 +1,14 @@ +# AWF Project Configuration +# https://github.com/awf-project/cli +# +# This file provides default values for workflow inputs. +# CLI --input flags override these values. +# +# IMPORTANT: Do not store secrets here - use environment variables instead. + +# Workflow inputs - pre-populate values for awf run +# Uncomment and modify the examples below: +inputs: + # project: my-project + # environment: staging + # debug: false diff --git a/.awf/scripts/create-feature/validate-name.sh b/.awf/scripts/create-feature/validate-name.sh new file mode 100755 index 0000000..d8cbf6c --- /dev/null +++ b/.awf/scripts/create-feature/validate-name.sh @@ -0,0 +1,46 @@ +#!/bin/bash +set -e + +# validate-name.sh — Derive or validate a devcontainer feature name +# +# Inputs (AWF interpolation): +# {{.inputs.description}} - Feature description (required) +# {{.inputs.name}} - Feature name override (optional, kebab-case) +# +# Output (stdout): plain feature ID (kebab-case, e.g. "zellij") + +DESCRIPTION="{{.inputs.description}}" +NAME="{{.inputs.name}}" + +if [ -z "$DESCRIPTION" ]; then + echo "ERROR: DESCRIPTION is required" + exit 1 +fi + +# Derive name from description if not provided +if [ -z "$NAME" ]; then + # Extract the tool/project name: take the word after "Installs" or first capitalized word + NAME=$(echo "$DESCRIPTION" \ + | sed -E 's/^[Ii]nstalls?\s+//' \ + | sed -E 's/^(the\s+|a\s+|an\s+)//i' \ + | awk '{print $1}' \ + | tr '[:upper:]' '[:lower:]' \ + | sed -E 's/[^a-z0-9-]//g') +fi + +# Validate: must be non-empty kebab-case +if [ -z "$NAME" ]; then + echo "ERROR: could not derive feature name from description: ${DESCRIPTION}" + exit 1 +fi + +if ! echo "$NAME" | grep -qE '^[a-z][a-z0-9-]*[a-z0-9]$'; then + # Single char names are also valid + if ! echo "$NAME" | grep -qE '^[a-z]$'; then + echo "ERROR: invalid feature name '${NAME}' — must be kebab-case (lowercase, hyphens, start with letter)" + exit 1 + fi +fi + +# Output just the ID (no JSON — output_format: json is only supported on agent steps) +printf '%s' "$NAME" diff --git a/.awf/workflows/create-feature.yaml b/.awf/workflows/create-feature.yaml new file mode 100644 index 0000000..53a96ca --- /dev/null +++ b/.awf/workflows/create-feature.yaml @@ -0,0 +1,368 @@ +name: create-feature +version: "1.0.0" +description: Scaffold a new devcontainer feature with all required files, tests, and documentation + +inputs: + - name: description + type: string + required: true + description: "What the feature installs/does (e.g. 'Installs Zellij terminal multiplexer')" + - name: name + type: string + required: false + description: "Feature name in kebab-case (auto-derived from description if empty)" + - name: force + type: boolean + required: false + default: false + description: "Bypass idempotency guards (overwrite existing feature)" + +# --------------------------------------------------------------------------- +# PHASE 0: NAME & BRANCH +# --------------------------------------------------------------------------- +states: + initial: validate_name + + validate_name: + type: step + description: Derive and validate feature name from description + script_file: "{{.awf.scripts_dir}}/create-feature/validate-name.sh" + on_success: check_existing + on_failure: {message: "Name validation failed", status: 1} + + check_existing: + type: step + description: Check if feature directory already exists + command: | + if [ -d "src/{{trimSpace .states.validate_name.Output}}" ] && [ "{{.inputs.force}}" != "true" ]; then + echo "Feature src/{{trimSpace .states.validate_name.Output}}/ already exists. Use force=true to overwrite." + exit 1 + fi + echo "OK" + on_success: create_branch + on_failure: check_branch + + check_branch: + type: step + description: Check if feature branch already exists (resume case) + command: | + BRANCH="feat/{{trimSpace .states.validate_name.Output}}" + CURRENT=$(git branch --show-current) + if [ "$CURRENT" = "$BRANCH" ]; then + echo "Already on branch ${BRANCH}" + else + echo "ERROR: feature exists but not on expected branch. Current: ${CURRENT}" + exit 1 + fi + on_success: check_metadata + on_failure: {message: "Branch check failed — feature exists and force not set", status: 1} + + create_branch: + type: step + description: Create and checkout feature branch + command: | + BRANCH="feat/{{trimSpace .states.validate_name.Output}}" + CURRENT=$(git branch --show-current) + if [ "$CURRENT" = "$BRANCH" ]; then + echo "Already on branch ${BRANCH}" + else + git checkout -b "$BRANCH" + echo "Created branch ${BRANCH}" + fi + on_success: check_metadata + on_failure: {message: "Failed to create feature branch", status: 1} + + # --------------------------------------------------------------------------- + # PHASE 1: SRC GENERATION + # --------------------------------------------------------------------------- + + check_metadata: + type: step + description: Check if devcontainer-feature.json already exists + command: | + if [ -f "src/{{trimSpace .states.validate_name.Output}}/devcontainer-feature.json" ] && [ "{{.inputs.force}}" != "true" ]; then + echo "EXISTS" + else + echo "MISSING" + fi + transitions: + - when: "states.check_metadata.Output contains 'EXISTS'" + goto: check_install + - goto: generate_metadata + + generate_metadata: + type: agent + description: Generate devcontainer-feature.json + provider: claude + prompt_file: prompts/create-feature/metadata.md + options: + model: opus + max_tokens: 4096 + allowedTools: "Read,Grep,Glob,Edit,Write" + dangerouslySkipPermissions: true + timeout: 120 + on_success: validate_feature_json + on_failure: error_generate + + validate_feature_json: + type: step + description: Validate devcontainer-feature.json syntax and required fields + command: | + FEATURE_JSON="src/{{trimSpace .states.validate_name.Output}}/devcontainer-feature.json" + if [ ! -f "$FEATURE_JSON" ]; then + echo "ERROR: $FEATURE_JSON not found" + exit 1 + fi + # Validate JSON syntax + if command -v python3 &>/dev/null; then + python3 -m json.tool "$FEATURE_JSON" > /dev/null + elif command -v jq &>/dev/null; then + jq empty "$FEATURE_JSON" + else + echo "SKIP: no JSON validator available" + fi + # Check required fields + for field in id version name description; do + if ! grep -q "\"${field}\"" "$FEATURE_JSON"; then + echo "ERROR: missing required field '${field}'" + exit 1 + fi + done + echo "OK: $FEATURE_JSON is valid" + on_success: check_install + on_failure: error_generate + + check_install: + type: step + description: Check if install.sh already exists + command: | + if [ -f "src/{{trimSpace .states.validate_name.Output}}/install.sh" ] && [ "{{.inputs.force}}" != "true" ]; then + echo "EXISTS" + else + echo "MISSING" + fi + transitions: + - when: "states.check_install.Output contains 'EXISTS'" + goto: check_test + - goto: generate_install + + generate_install: + type: agent + description: Generate install.sh and optional helper scripts + provider: claude + prompt_file: prompts/create-feature/install.md + options: + model: opus + max_tokens: 8192 + allowedTools: "Read,Grep,Glob,Edit,Write" + dangerouslySkipPermissions: true + timeout: 180 + on_success: check_test + on_failure: error_generate + + # --------------------------------------------------------------------------- + # PHASE 2: TEST GENERATION + # --------------------------------------------------------------------------- + + check_test: + type: step + description: Check if test files already exist + command: | + if [ -f "test/{{trimSpace .states.validate_name.Output}}/test.sh" ] && [ "{{.inputs.force}}" != "true" ]; then + echo "EXISTS" + else + echo "MISSING" + fi + transitions: + - when: "states.check_test.Output contains 'EXISTS'" + goto: shellcheck + - goto: generate_tests + + generate_tests: + type: agent + description: Generate test.sh, scenarios.json, and scenario scripts + provider: claude + prompt_file: prompts/create-feature/tests.md + options: + model: opus + max_tokens: 8192 + allowedTools: "Read,Grep,Glob,Edit,Write" + dangerouslySkipPermissions: true + timeout: 180 + on_success: shellcheck + on_failure: error_generate + + # --------------------------------------------------------------------------- + # PHASE 3: VALIDATION + # --------------------------------------------------------------------------- + + shellcheck: + type: step + description: Run ShellCheck on all generated scripts + command: | + FEATURE_ID="{{trimSpace .states.validate_name.Output}}" + find "src/${FEATURE_ID}" "test/${FEATURE_ID}" -name '*.sh' -type f | xargs -r shellcheck -S error 2>&1 + on_success: run_tests + on_failure: fix_shellcheck + + fix_shellcheck: + type: agent + description: Fix ShellCheck errors in generated scripts + provider: claude + prompt: | + ShellCheck found errors in the scripts for feature "{{trimSpace .states.validate_name.Output}}". + + ShellCheck output: + {{.states.shellcheck.Output}} + + Fix ALL ShellCheck errors (severity: error) in the following files: + - src/{{trimSpace .states.validate_name.Output}}/*.sh + - test/{{trimSpace .states.validate_name.Output}}/*.sh + + Read each file, fix the errors, and write the corrected version. + Do not change the logic — only fix ShellCheck compliance issues. + options: + model: opus + max_tokens: 8192 + allowedTools: "Read,Grep,Glob,Edit,Write" + dangerouslySkipPermissions: true + timeout: 180 + on_success: shellcheck_retry + on_failure: error_shellcheck + + shellcheck_retry: + type: step + description: Re-run ShellCheck after fix attempt + command: | + FEATURE_ID="{{trimSpace .states.validate_name.Output}}" + find "src/${FEATURE_ID}" "test/${FEATURE_ID}" -name '*.sh' -type f | xargs -r shellcheck -S error 2>&1 + on_success: run_tests + on_failure: error_shellcheck + + run_tests: + type: step + description: Run local feature tests via test-local.sh + command: | + if command -v devcontainer &>/dev/null; then + ./test-local.sh {{trimSpace .states.validate_name.Output}} + else + echo "SKIP: devcontainer CLI not installed — skipping local tests" + echo "Run manually: ./test-local.sh {{trimSpace .states.validate_name.Output}}" + fi + timeout: 300 + on_success: check_readme + on_failure: error_tests + + # --------------------------------------------------------------------------- + # PHASE 4: DOCUMENTATION + # --------------------------------------------------------------------------- + + check_readme: + type: step + description: Check if README already mentions this feature + command: | + FEATURE_ID="{{trimSpace .states.validate_name.Output}}" + NAME_HUMAN=$(echo "$FEATURE_ID" | awk -F'-' '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) substr($i,2)} 1') + if grep -q "### ${NAME_HUMAN}" README.md && [ "{{.inputs.force}}" != "true" ]; then + echo "EXISTS" + else + echo "MISSING" + fi + transitions: + - when: "states.check_readme.Output contains 'EXISTS'" + goto: write_tracking + - goto: update_readme + + update_readme: + type: agent + description: Add feature documentation to README.md + provider: claude + prompt_file: prompts/create-feature/readme.md + options: + model: sonnet + max_tokens: 4096 + allowedTools: "Read,Grep,Glob,Edit" + dangerouslySkipPermissions: true + timeout: 120 + on_success: write_tracking + on_failure: error_generate + + # --------------------------------------------------------------------------- + # PHASE 5: TRACKING & SUMMARY + # --------------------------------------------------------------------------- + + write_tracking: + type: step + description: Write execution tracking file + command: | + FEATURE_ID="{{trimSpace .states.validate_name.Output}}" + TRACKING_DIR=".awf/state/create-feature/${FEATURE_ID}" + mkdir -p "$TRACKING_DIR" + CREATED_AT=$(date -Iseconds) + cat > "${TRACKING_DIR}/status.md" </dev/null || true + ls -la "test/${FEATURE_ID}/" 2>/dev/null || true + echo "" + echo " Next steps:" + echo " 1. Review generated files" + echo " 2. ./test-local.sh ${FEATURE_ID}" + echo " 3. git add && git commit" + echo " 4. Push PR" + echo "" + on_success: done + + # --------------------------------------------------------------------------- + # TERMINAL STATES + # --------------------------------------------------------------------------- + + done: + type: terminal + status: success + message: "Feature {{trimSpace .states.validate_name.Output}} created successfully" + + error_generate: + type: terminal + status: failure + message: "Agent failed to generate files for feature {{trimSpace .states.validate_name.Output}}" + + error_shellcheck: + type: terminal + status: failure + message: "ShellCheck errors could not be fixed automatically" + + error_tests: + type: terminal + status: failure + message: "Feature tests failed — review test output and fix manually" diff --git a/.awf/workflows/prompts/create-feature/install.md b/.awf/workflows/prompts/create-feature/install.md new file mode 100644 index 0000000..46da4de --- /dev/null +++ b/.awf/workflows/prompts/create-feature/install.md @@ -0,0 +1,39 @@ +# Generate install.sh + +Create the installation script for: **{{.inputs.description}}** + +## Instructions + +1. Read `CLAUDE.md` section "install.sh Template" for the base template. +2. Read `src/{{trimSpace .states.validate_name.Output}}/devcontainer-feature.json` (already generated). +3. Read the closest reference script and follow its pattern: + - `src/rtk/install.sh` — multi-target binary (try multiple target triples) + - `src/claude-code/install.sh` — config persistence (volume mount + symlink helper) + - `src/tree-sitter/install.sh` — post-install compilation from source + - Use the CLAUDE.md template directly for a standard single-binary install +4. Create `src/{{trimSpace .states.validate_name.Output}}/install.sh` (and any helper scripts the metadata requires). + +## Script Structure Order + +1. Options from env vars (`VERSION="${VERSION:-latest}"`) +2. Dependency check (`apt-get update && install && clean`) +3. Version resolution (GitHub API if "latest") +4. Architecture detection (`x86_64`/`amd64`, `aarch64`/`arm64`, reject others) +5. Download and install to `/usr/local/bin/` +6. Helper scripts / post-install config (if any) +7. Verification and `==> feature install complete` message + +## ShellCheck Requirements + +- Quote all variable expansions: `"${VAR}"` not `$VAR` +- Use `|| true` on non-critical commands that may return non-zero +- Use `[ ]` not `[[ ]]` +- After `apt-get install`, always add: `apt-get clean && rm -rf /var/lib/apt/lists/*` +- Temp cleanup: `trap 'rm -rf "$TMP_DIR"' EXIT` + +## Constraints + +- Write only files under `src/{{trimSpace .states.validate_name.Output}}/` +- Do not modify any other existing files +- Do not use Bash to test the script — ShellCheck runs as a separate pipeline step +- All scripts must pass ShellCheck severity `error` diff --git a/.awf/workflows/prompts/create-feature/metadata.md b/.awf/workflows/prompts/create-feature/metadata.md new file mode 100644 index 0000000..93c77cb --- /dev/null +++ b/.awf/workflows/prompts/create-feature/metadata.md @@ -0,0 +1,54 @@ +# Generate devcontainer-feature.json + +Create the metadata file for a new devcontainer feature: **{{.inputs.description}}** + +## Instructions + +1. Read `src/rtk/devcontainer-feature.json` and `src/claude-code/devcontainer-feature.json` as reference patterns. +2. Create `src/{{trimSpace .states.validate_name.Output}}/devcontainer-feature.json` using the requirements below. + +## Required Fields + +| Field | Value | +|-------|-------| +| `id` | `{{trimSpace .states.validate_name.Output}}` | +| `version` | `1.0.0` | +| `name` | Capitalize each hyphen-separated word of the id (e.g. "mistral-vibe" → "Mistral Vibe") | +| `description` | One-line description derived from "{{.inputs.description}}" | +| `options.version` | `{ "type": "string", "default": "latest", "description": "Version to install: 'latest' or a specific version (e.g. '1.2.3')" }` | +| `installsAfter` | `["ghcr.io/devcontainers/features/common-utils"]` — use `dependsOn` instead if the feature cannot function without another feature | + +## Optional Fields (add only when justified by the description) + +- `documentationURL` / `licenseURL` — if inferable from the tool name +- `containerEnv` — if the tool requires PATH additions or environment variables +- `customizations.vscode.extensions` / `customizations.jetbrains.plugins` — if the tool has IDE integrations +- `mounts` + `postStartCommand` — if the tool needs persistent configuration (see `claude-code` for the pattern) +- Boolean `options` entries — only if the description implies optional sub-components + +## Minimal Valid Output + +```json +{ + "id": "{{trimSpace .states.validate_name.Output}}", + "version": "1.0.0", + "name": "", + "description": "", + "options": { + "version": { + "type": "string", + "default": "latest", + "description": "Version to install: 'latest' or a specific version (e.g. '1.2.3')" + } + }, + "installsAfter": ["ghcr.io/devcontainers/features/common-utils"] +} +``` + +Extend with optional fields as needed. JSON must be valid, 2-space indented. + +## Constraints + +- Write ONLY `src/{{trimSpace .states.validate_name.Output}}/devcontainer-feature.json` +- Do not create any other files +- Do not use Bash diff --git a/.awf/workflows/prompts/create-feature/readme.md b/.awf/workflows/prompts/create-feature/readme.md new file mode 100644 index 0000000..81ea087 --- /dev/null +++ b/.awf/workflows/prompts/create-feature/readme.md @@ -0,0 +1,65 @@ +# Update README.md + +Add documentation for a new devcontainer feature: **{{.inputs.description}}** + +Feature ID: `{{trimSpace .states.validate_name.Output}}` + +## Steps + +1. Read `README.md` to understand the documentation pattern +2. Read `src/{{trimSpace .states.validate_name.Output}}/devcontainer-feature.json` +3. Read `src/{{trimSpace .states.validate_name.Output}}/install.sh` + +## Changes + +### 1. Table of Contents + +Insert in `Available Features`, alphabetical by feature name: +```markdown +- [](#{{trimSpace .states.validate_name.Output}}) +``` +(Derive the human-readable feature name by capitalizing each hyphen-separated word of the ID) + +### 2. Feature Section + +Insert a `### ` section in alphabetical order. Structure: + +1. One-line description from `devcontainer-feature.json` +2. Default `devcontainer.json` usage snippet +3. `#### Options` table — **omit if no options defined** +4. `#### Examples` — always include a version-pinning example; add more for non-trivial option combinations +5. `#### Environment` — add only if the feature sets env vars or modifies `PATH` +6. Feature-specific subsections — add only for notable behaviors (e.g., persistent config, API key setup) + +Template for steps 1-3: + +```markdown +### + + + +\`\`\`jsonc +// devcontainer.json +{ + "features": { + "ghcr.io/awf-project/devcontainer-features/{{trimSpace .states.validate_name.Output}}:1": {} + } +} +\`\`\` + +#### Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| ... | +``` + +## Rules + +- Use `Edit`, not `Write` — this is an existing file +- Use `Read` to inspect the file before editing +- Do not use `Bash` +- Do not modify existing content +- Both TOC entry and section go in alphabetical order by feature name +- Omit `#### Options` if `devcontainer-feature.json` has no options +- Omit `#### Examples` if the feature has only a `version` option and the pattern is self-evident from the options table diff --git a/.awf/workflows/prompts/create-feature/tests.md b/.awf/workflows/prompts/create-feature/tests.md new file mode 100644 index 0000000..6f3017a --- /dev/null +++ b/.awf/workflows/prompts/create-feature/tests.md @@ -0,0 +1,72 @@ +# Generate Test Files + +Create test files for a devcontainer feature: **{{.inputs.description}}** + +Feature ID: `{{trimSpace .states.validate_name.Output}}` + +## Inputs to Read First + +1. `src/{{trimSpace .states.validate_name.Output}}/devcontainer-feature.json` — options, containerEnv +2. `src/{{trimSpace .states.validate_name.Output}}/install.sh` — binary name, default version +3. Reference tests: + - `test/rtk/test.sh` — minimal PATH + version assertions + - `test/rtk/scenarios.json` — version-pinned scenario + - `test/rtk/install_rtk_specific_version.sh` — exact version string check pattern + - `test/claude-code/test.sh` — extended assertions (binary location, env vars) + - `test/claude-code/scenarios.json` — two-scenario structure (latest + pinned) + +## Files to Create + +### `test/{{trimSpace .states.validate_name.Output}}/test.sh` + +Assertions (in order): +1. Binary on PATH: `command -v ` +2. Version output non-empty: `_VERSION=$( --version 2>&1)` +3. For each entry in `containerEnv`: assert variable is set and non-empty +4. Any feature-specific file or path checks (derive from install.sh) + +Output format: `PASS: ` or `FAIL: ` then `exit 1` +Final line: `echo "==> All feature tests passed"` (capitalize each word of the feature ID for the human-readable name) + +### `test/{{trimSpace .states.validate_name.Output}}/scenarios.json` + +Minimum scenarios (use the feature ID with hyphens replaced by underscores for scenario keys): + +```json +{ + "install__latest": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { "": {} } + }, + "install__specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { "": { "version": "" } } + } +} +``` + +For each boolean option in `devcontainer-feature.json`, add: +```json +"install__with_