Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 21 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ curl -fsSL https://raw.githubusercontent.com/PabloZaiden/opencode-server-runner/

VS Code will automatically detect the exposed port and offer to forward it.

## GitHub Copilot Authentication
## Authentication

On first run, if you're not authenticated with GitHub Copilot, the script will initiate an authentication flow:
On first run, if you're not authenticated, the script will initiate an authentication flow:

1. A URL and device code will be displayed in the terminal
2. Visit the URL in your browser
3. Enter the device code
4. Authorize OpenCode to access GitHub Copilot
4. Authorize OpenCode to access your chosen provider
5. The script will continue once authentication is complete

Your authentication persists in `~/.local/share/opencode/auth.json` and will survive container restarts (but not rebuilds).
Expand Down Expand Up @@ -75,13 +75,19 @@ OPENCODE_PORT=5001 ./opencode-server.sh

1. Installs OpenCode CLI (if not present)
2. Installs Caddy (if not present, Linux only)
3. Authenticates with GitHub Copilot (if not already authenticated)
4. Generates a persistent password (stored in `~/.config/opencode-server-local`)
5. Creates a self-signed SSL certificate (stored in `~/.config/opencode-certs/`)
3. Authenticates with a provider (if not already authenticated)
4. Generates a persistent password
5. Creates a self-signed SSL certificate
6. Starts OpenCode server on localhost:4097
7. Starts Caddy as an HTTPS reverse proxy on the configured port
8. Prints connection info (IP, port, username, password)

## Data Storage

When run inside a **git repository**, all server data (password, PID file, certificates, logs, Caddyfile) is stored in `.opencode-server/` at the repository root. This directory is automatically added to `.git/info/exclude` so it won't be committed.

When run **outside a git repository**, data is stored in `~/.config/` (the previous default behavior).

## Configuration

| Environment Variable | Default | Description |
Expand All @@ -90,14 +96,16 @@ OPENCODE_PORT=5001 ./opencode-server.sh

## Files

| Path | Description |
| File | Description |
|------|-------------|
| `~/.local/share/opencode/auth.json` | GitHub Copilot authentication |
| `~/.config/opencode-server-local` | Persistent password |
| `~/.config/opencode-server.pid` | PID file for running instance |
| `~/.config/opencode-server.log` | Server logs |
| `~/.config/opencode-certs/` | SSL certificates |
| `~/.config/opencode-caddyfile` | Generated Caddy configuration |
| `~/.local/share/opencode/auth.json` | Provider authentication |
| `<data-dir>/opencode-server-local` | Persistent password |
| `<data-dir>/opencode-server.pid` | PID file for running instance |
| `<data-dir>/opencode-server.log` | Server logs |
| `<data-dir>/opencode-certs/` | SSL certificates |
| `<data-dir>/opencode-caddyfile` | Generated Caddy configuration |

`<data-dir>` is `.opencode-server/` (in git repos) or `~/.config/` (otherwise).

## Connecting

Expand Down
43 changes: 30 additions & 13 deletions opencode-server.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,35 +28,52 @@ for arg in "$@"; do
fi
done

# Check if authenticated with GitHub Copilot (skip if --skip-auth or --stop)
# Check if authenticated (skip if --skip-auth or --stop)
if [ "$SKIP_AUTH" = "false" ] && [ "$1" != "--stop" ]; then
AUTH_FILE="$HOME/.local/share/opencode/auth.json"
Comment on lines +31 to 33
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The auth gate is skipped only when --stop is the first argument ($1). If --stop is provided after other flags, the script may incorrectly prompt for auth and the stop handler won’t run. Consider parsing --stop the same way as --skip-auth (scan all args) so stop works regardless of argument order.

Copilot uses AI. Check for mistakes.
if [ ! -f "$AUTH_FILE" ] || ! grep -q "github-copilot" "$AUTH_FILE" 2>/dev/null; then
if [ ! -f "$AUTH_FILE" ] || [ ! -s "$AUTH_FILE" ]; then
echo ""
echo "GitHub Copilot authentication required."
echo "OpenCode authentication required."
echo "A browser/device code flow will be initiated..."
echo ""
opencode auth login github-copilot
opencode auth login

# Verify auth succeeded
if [ ! -f "$AUTH_FILE" ] || ! grep -q "github-copilot" "$AUTH_FILE" 2>/dev/null; then
echo "GitHub Copilot authentication failed or was cancelled."
if [ ! -f "$AUTH_FILE" ] || [ ! -s "$AUTH_FILE" ]; then
echo "Authentication failed or was cancelled."
exit 1
fi
echo "GitHub Copilot authentication successful."
echo "Authentication successful."
echo ""
fi
fi

# Determine data directory: use .opencode-server in the repo root if inside a git
# repo, otherwise fall back to ~/.config
if git rev-parse --show-toplevel &>/dev/null; then
GIT_ROOT="$(git rev-parse --show-toplevel)"
DATA_DIR="$GIT_ROOT/.opencode-server"
mkdir -p "$DATA_DIR"

Comment on lines +53 to +57
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When running inside a git repo, the script unconditionally switches to "$GIT_ROOT/.opencode-server" without checking whether the repo root is writable. If the workspace/repo is mounted read-only (common in some containers/CI), subsequent writes (PID/logs/certs/password) will fail and the server start/stop flow will break. Consider falling back to "$HOME/.config" when creating/writing "$DATA_DIR" fails (e.g., check mkdir exit status and/or a simple write test).

Copilot uses AI. Check for mistakes.
# Locally ignore the data directory so it's never committed
EXCLUDE_FILE="$GIT_ROOT/.git/info/exclude"
mkdir -p "$(dirname "$EXCLUDE_FILE")"
if ! grep -qxF '.opencode-server' "$EXCLUDE_FILE" 2>/dev/null; then
echo '.opencode-server' >> "$EXCLUDE_FILE"
fi
Comment on lines +58 to +63
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using "$GIT_ROOT/.git/info/exclude" assumes ".git" is a directory, but in git worktrees/submodules it can be a file pointing elsewhere; in that case "mkdir -p $GIT_ROOT/.git/info" will fail and the ignore entry won’t be applied. Prefer resolving the exclude path via git itself (e.g., git -C "$GIT_ROOT" rev-parse --git-path info/exclude) and then append/check that file.

Copilot uses AI. Check for mistakes.
else
DATA_DIR="$HOME/.config"
fi

# Configuration
PASSWORD_FILE="$HOME/.config/opencode-server-local"
PID_FILE="$HOME/.config/opencode-server.pid"
STOP_FLAG="$HOME/.config/opencode-server.stop"
CERT_DIR="$HOME/.config/opencode-certs"
PASSWORD_FILE="$DATA_DIR/opencode-server-local"
PID_FILE="$DATA_DIR/opencode-server.pid"
STOP_FLAG="$DATA_DIR/opencode-server.stop"
CERT_DIR="$DATA_DIR/opencode-certs"
CERT_FILE="$CERT_DIR/cert.pem"
KEY_FILE="$CERT_DIR/key.pem"
CADDYFILE="$HOME/.config/opencode-caddyfile"
LOG_FILE="$HOME/.config/opencode-server.log"
CADDYFILE="$DATA_DIR/opencode-caddyfile"
LOG_FILE="$DATA_DIR/opencode-server.log"
MONITOR_INTERVAL=${OPENCODE_MONITOR_INTERVAL:-5}

OPENCODE_INTERNAL_PORT="4097"
Expand Down
146 changes: 146 additions & 0 deletions tests/test-devcontainer.sh
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,152 @@ else
echo "$PROCS"
fi

# ==============================================================================
# GIT REPO DATA DIRECTORY TESTS
# ==============================================================================
echo ""
echo -e "${YELLOW}Setting up git repo tests...${NC}"

# Stop any running server and clean up state from previous tests
docker exec "$CONTAINER_NAME" bash /workspace/opencode-server.sh --stop >/dev/null 2>&1
sleep 2

# Create a git repo inside the container
docker exec "$CONTAINER_NAME" bash -c \
'mkdir -p /tmp/test-repo && cd /tmp/test-repo && git init && git config user.email "test@test.com" && git config user.name "Test"' >/dev/null 2>&1

# Copy the script into the repo (since /workspace is read-only)
docker exec "$CONTAINER_NAME" bash -c 'cp /workspace/opencode-server.sh /tmp/test-repo/' >/dev/null 2>&1

# ------------------------------------------------------------------------------
echo "=== Test 15: Data is stored in .opencode-server/ inside a git repo ==="
OUTPUT=$(docker exec "$CONTAINER_NAME" bash -c \
"cd /tmp/test-repo && OPENCODE_PORT=$TEST_PORT bash opencode-server.sh --skip-auth 2>&1")
if echo "$OUTPUT" | grep -q "OpenCode server started"; then
# Check that data dir was created inside the repo
if docker exec "$CONTAINER_NAME" bash -c 'test -d /tmp/test-repo/.opencode-server'; then
pass "Data directory .opencode-server/ created inside git repo"
else
fail "Data directory .opencode-server/ not found inside git repo"
fi
else
fail "Script did not start server from git repo"
echo "$OUTPUT"
fi

# ------------------------------------------------------------------------------
echo "=== Test 16: .git/info/exclude contains .opencode-server ==="
EXCLUDE_CONTENT=$(docker exec "$CONTAINER_NAME" bash -c 'cat /tmp/test-repo/.git/info/exclude' 2>/dev/null)
if echo "$EXCLUDE_CONTENT" | grep -qxF '.opencode-server'; then
pass ".opencode-server is in .git/info/exclude"
else
fail ".opencode-server not found in .git/info/exclude"
echo "Exclude file contents: $EXCLUDE_CONTENT"
fi
Comment on lines +300 to +307
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this test script runs with set -e, capturing exclude contents via EXCLUDE_CONTENT=$(docker exec ... cat ...) will abort the entire suite if cat fails (e.g., if the exclude file wasn’t created). To ensure the test reports a clean failure instead of exiting early, guard the command substitution (e.g., ... || true) and then assert on the result/file existence explicitly.

Copilot uses AI. Check for mistakes.

# ------------------------------------------------------------------------------
echo "=== Test 17: Password file is inside .opencode-server/ ==="
GIT_PASSWORD=$(docker exec "$CONTAINER_NAME" bash -c 'cat /tmp/test-repo/.opencode-server/opencode-server-local' 2>/dev/null)
if [ -n "$GIT_PASSWORD" ]; then
pass "Password file found in .opencode-server/: ${GIT_PASSWORD:0:8}..."
else
fail "Password file not found in .opencode-server/"
fi
Comment on lines +310 to +316
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same set -e issue here: GIT_PASSWORD=$(docker exec ... cat ...) will cause the whole test run to exit if the password file is missing/unreadable, instead of recording a failed assertion. Consider guarding the substitution (|| true) or checking file existence first, then reading it.

Copilot uses AI. Check for mistakes.

# ------------------------------------------------------------------------------
echo "=== Test 18: PID file is inside .opencode-server/ ==="
if docker exec "$CONTAINER_NAME" bash -c 'test -f /tmp/test-repo/.opencode-server/opencode-server.pid'; then
pass "PID file found in .opencode-server/"
else
fail "PID file not found in .opencode-server/"
fi

# ------------------------------------------------------------------------------
echo "=== Test 19: Certs are inside .opencode-server/ ==="
if docker exec "$CONTAINER_NAME" bash -c 'test -f /tmp/test-repo/.opencode-server/opencode-certs/cert.pem'; then
pass "Certificate found in .opencode-server/opencode-certs/"
else
fail "Certificate not found in .opencode-server/opencode-certs/"
fi

# ------------------------------------------------------------------------------
echo "=== Test 20: Log file is inside .opencode-server/ ==="
if docker exec "$CONTAINER_NAME" bash -c 'test -f /tmp/test-repo/.opencode-server/opencode-server.log'; then
pass "Log file found in .opencode-server/"
else
fail "Log file not found in .opencode-server/"
fi

# ------------------------------------------------------------------------------
echo "=== Test 21: Server responds on HTTPS port (git repo mode) ==="
HTTP_CODE=$(docker exec "$CONTAINER_NAME" bash -c \
"curl -k -s -o /dev/null -w '%{http_code}' https://127.0.0.1:$TEST_PORT" 2>&1)
if [ "$HTTP_CODE" = "401" ]; then
pass "Server responds with 401 on port $TEST_PORT (git repo mode)"
else
fail "Expected HTTP 401, got: $HTTP_CODE (git repo mode)"
fi

# ------------------------------------------------------------------------------
echo "=== Test 22: Stop works in git repo mode ==="
docker exec "$CONTAINER_NAME" bash -c \
"cd /tmp/test-repo && bash opencode-server.sh --stop" >/dev/null 2>&1
sleep 2
LISTENING=$(docker exec "$CONTAINER_NAME" bash -c "ss -tlnp | grep $TEST_PORT || echo 'not listening'" 2>&1)
if [[ "$LISTENING" == *"not listening"* ]]; then
pass "Server stopped in git repo mode"
else
fail "Server still listening after stop in git repo mode"
echo "$LISTENING"
fi

# ------------------------------------------------------------------------------
echo "=== Test 23: Password persists after restart in git repo mode ==="
docker exec "$CONTAINER_NAME" bash -c \
"cd /tmp/test-repo && OPENCODE_PORT=$TEST_PORT bash opencode-server.sh --skip-auth" >/dev/null 2>&1
GIT_PASSWORD2=$(docker exec "$CONTAINER_NAME" bash -c 'cat /tmp/test-repo/.opencode-server/opencode-server-local' 2>/dev/null)
if [ "$GIT_PASSWORD" = "$GIT_PASSWORD2" ]; then
pass "Password persisted across restart in git repo mode"
else
fail "Password changed after restart in git repo mode"
fi

# Stop server before next test
docker exec "$CONTAINER_NAME" bash -c \
"cd /tmp/test-repo && bash opencode-server.sh --stop" >/dev/null 2>&1
sleep 2

# ------------------------------------------------------------------------------
echo "=== Test 24: .git/info/exclude entry is not duplicated on repeated runs ==="
EXCLUDE_COUNT=$(docker exec "$CONTAINER_NAME" bash -c \
'grep -cx ".opencode-server" /tmp/test-repo/.git/info/exclude' 2>/dev/null)
if [ "$EXCLUDE_COUNT" = "1" ]; then
pass ".opencode-server appears exactly once in .git/info/exclude"
else
fail ".opencode-server appears $EXCLUDE_COUNT times in .git/info/exclude (expected 1)"
fi

# ------------------------------------------------------------------------------
echo "=== Test 25: Data stored in ~/.config/ when not in a git repo ==="
# Run from /tmp which is not a git repo
OUTPUT=$(docker exec "$CONTAINER_NAME" bash -c \
"cd /tmp && OPENCODE_PORT=$TEST_PORT bash /workspace/opencode-server.sh --skip-auth 2>&1")
if echo "$OUTPUT" | grep -q "OpenCode server started"; then
if docker exec "$CONTAINER_NAME" bash -c 'test -f ~/.config/opencode-server.pid'; then
pass "Data stored in ~/.config/ when not in a git repo"
else
fail "PID file not found in ~/.config/ when not in a git repo"
fi
else
fail "Script did not start server from non-git directory"
echo "$OUTPUT"
fi

# Stop the non-git-repo server
docker exec "$CONTAINER_NAME" bash -c \
"cd /tmp && bash /workspace/opencode-server.sh --stop" >/dev/null 2>&1
sleep 2

# ------------------------------------------------------------------------------
echo ""
echo "========================================"
Expand Down