# Script Interface Contracts **Feature**: 002-update-opencode **Date**: 2025-11-11 **Purpose**: Define input/output contracts for bash helper scripts --- ## Overview All scripts follow bash best practices: - Shebang: `#!/usr/bin/env bash` - Error handling: `set -euo pipefail` - Exit codes: 0 = success, 1 = error - Output: Results to stdout, errors to stderr - Logging: Use `echo "message" >&2` for errors --- ## 1. check-version.sh **Purpose**: Compare current OpenCode version against latest GitHub release. ### Interface ```bash ./check-version.sh [--dotfiles PATH] ``` **Arguments:** - `--dotfiles PATH`: Optional. Path to dotfiles repo (default: ~/proj/dotfiles) **Exit Codes:** - `0`: Success (version check completed) - `1`: Error (file not found, API failure, parsing error) **Output (stdout):** ``` current=1.0.44 latest=1.0.58 update_available=yes ``` Or if up-to-date: ``` current=1.0.58 latest=1.0.58 update_available=no ``` **Output Format:** - Key-value pairs, one per line - `update_available` values: `yes` or `no` - Can be sourced in bash: `eval "$(./check-version.sh)"` **Error Output (stderr):** ``` Error: Nix package file not found: /path/to/default.nix Error: GitHub API request failed (network error) Error: Failed to parse version from Nix file ``` **Dependencies:** - `curl` (GitHub API) - `jq` (JSON parsing) - `grep` (version extraction) --- ## 2. fetch-sha256.sh **Purpose**: Fetch SHA256 hash for a specific OpenCode binary release using nix-prefetch-url. ### Interface ```bash ./fetch-sha256.sh VERSION ``` **Arguments:** - `VERSION`: Required. Version to fetch (e.g., "1.0.58", no 'v' prefix) **Exit Codes:** - `0`: Success (hash fetched and converted to SRI) - `1`: Error (invalid version, network failure, nix-prefetch-url failed, conversion failed) **Output (stdout):** ``` sha256-q3K5w1lhzd7GhBesKbaD3jDo2B/twUDLmi8wrkWzWh4= ``` **Output Format:** - Single line containing SRI-formatted hash (sha256-...) - Base64-encoded with possible trailing `=` padding - No trailing newline issues (use `$(...)` safely) **Error Output (stderr):** ``` Error: Version argument required Error: Invalid version format: X.Y.Z Error: nix-prefetch-url failed for version 1.0.58 Error: GitHub binary release not found (404) Error: Failed to convert hash to SRI format ``` **Dependencies:** - `nix-prefetch-url` (fetching binary zip) - `nix hash to-sri` (converting to SRI format) **Performance:** - Typical: 2-5 seconds (download binary zip ~50MB + conversion) - Cached: <0.5 seconds (Nix store hit + conversion) **Implementation Detail:** ```bash # Downloads: https://github.com/sst/opencode/releases/download/v${VERSION}/opencode-linux-x64.zip # Converts: Nix base-32 → SRI format (sha256-...) ``` --- ## 3. update-nix-file.sh **Purpose**: Update version and sha256 fields in Nix package definition file. ### Interface ```bash ./update-nix-file.sh VERSION SHA256 [--dotfiles PATH] [--dry-run] ``` **Arguments:** - `VERSION`: Required. New version string (e.g., "1.0.58") - `SHA256`: Required. New sha256 hash in SRI format (sha256-...) - `--dotfiles PATH`: Optional. Path to dotfiles repo (default: ~/proj/dotfiles) - `--dry-run`: Optional. Show changes without writing **Exit Codes:** - `0`: Success (file updated or dry-run completed) - `1`: Error (file not found, not writable, pattern match failed) **Output (stdout) in dry-run mode:** ```diff --- /home/user/proj/dotfiles/pkgs/opencode/default.nix +++ /home/user/proj/dotfiles/pkgs/opencode/default.nix @@ -2,7 +2,7 @@ - version = "1.0.44"; + version = "1.0.58"; - sha256 = "sha256-q3K5w1lhzd7GhBesKbaD3jDo2B/twUDLmi8wrkWzWh4="; + sha256 = "sha256-ABC123NewHashHere=="; ``` **Output (stdout) in normal mode:** ``` Updated pkgs/opencode/default.nix: version: 1.0.44 → 1.0.58 sha256: sha256-q3K5...Wh4= → sha256-ABC1...e== ``` **Error Output (stderr):** ``` Error: Nix package file not found: /path/to/default.nix Error: File not writable: /path/to/default.nix Error: Version pattern not found in file Error: SHA256 pattern not found in file Error: Invalid SHA256 format (must start with sha256-) ``` **Dependencies:** - `sed` (file modification) - `grep` (pattern validation) **Safety:** - Validates patterns exist before modification - Uses atomic sed -i (backup created automatically) - Fails if either version or sha256 pattern not found exactly once --- ## 4. verify-update.sh **Purpose**: Verify OpenCode was updated to expected version after rebuild. ### Interface ```bash ./verify-update.sh EXPECTED_VERSION [--timeout SECONDS] ``` **Arguments:** - `EXPECTED_VERSION`: Required. Version that should be installed (e.g., "1.0.58") - `--timeout SECONDS`: Optional. Max wait time for opencode command (default: 5) **Exit Codes:** - `0`: Success (version matches) - `1`: Error (command not found, version mismatch, timeout) **Output (stdout):** ``` Verified: OpenCode version 1.0.58 installed successfully ``` Or on mismatch: ``` Warning: Expected 1.0.58 but found 1.0.44 Note: You may need to restart your shell or logout/login ``` **Error Output (stderr):** ``` Error: opencode command not found in PATH Error: opencode --version timed out after 5 seconds Error: Could not parse version from opencode output ``` **Dependencies:** - `opencode` (the CLI being updated) - `timeout` command (for safety) **Behavior:** - Runs `opencode --version` to check installed version - Compares output against expected version - Exit code 1 on mismatch but still outputs helpful message --- ## Environment Variables All scripts may use: - `DOTFILES_PATH`: Override default dotfiles location (default: ~/proj/dotfiles) - `DEBUG`: If set to "1", enable verbose logging to stderr - `NO_COLOR`: If set, disable color output in error messages **Example:** ```bash DOTFILES_PATH=/custom/path DEBUG=1 ./check-version.sh ``` --- ## Error Handling Patterns All scripts follow these patterns: **Dependency checking:** ```bash for cmd in jq curl grep; do if ! command -v "$cmd" >/dev/null 2>&1; then echo "Error: Required command not found: $cmd" >&2 exit 1 fi done ``` **Input validation:** ```bash if [[ $# -lt 1 ]]; then echo "Error: Version argument required" >&2 echo "Usage: $0 VERSION" >&2 exit 1 fi ``` **Network error handling:** ```bash if ! response=$(curl -s --fail https://...); then echo "Error: GitHub API request failed" >&2 exit 1 fi ``` **Pattern matching validation:** ```bash if ! grep -q 'version = "[^"]*"' "$file"; then echo "Error: Version pattern not found in $file" >&2 exit 1 fi ``` --- ## Testing Contracts Each script should be testable independently: **Syntax validation:** ```bash bash -n script.sh # Check for syntax errors ``` **Unit test pattern:** ```bash # Test successful execution result=$(./script.sh args) || { echo "FAIL: Expected success"; exit 1; } echo "PASS: Script executed successfully" # Test error handling ./script.sh invalid-args 2>/dev/null && { echo "FAIL: Should have errored"; exit 1; } echo "PASS: Error handled correctly" ``` **Integration test:** ```bash # Full workflow test version_info=$(./check-version.sh) || exit 1 eval "$version_info" if [[ "$update_available" == "yes" ]]; then hash=$(./fetch-sha256.sh "$latest") || exit 1 ./update-nix-file.sh "$latest" "$hash" --dry-run || exit 1 fi ```