# 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
```
