diff --git a/README.md b/README.md index 25c2bf5..d8c7cad 100644 --- a/README.md +++ b/README.md @@ -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). @@ -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 | @@ -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 | +| `/opencode-server-local` | Persistent password | +| `/opencode-server.pid` | PID file for running instance | +| `/opencode-server.log` | Server logs | +| `/opencode-certs/` | SSL certificates | +| `/opencode-caddyfile` | Generated Caddy configuration | + +`` is `.opencode-server/` (in git repos) or `~/.config/` (otherwise). ## Connecting diff --git a/opencode-server.sh b/opencode-server.sh index 128e58d..e319762 100755 --- a/opencode-server.sh +++ b/opencode-server.sh @@ -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" - 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" + + # 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 +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" diff --git a/tests/test-devcontainer.sh b/tests/test-devcontainer.sh index 3a57f90..76e4420 100755 --- a/tests/test-devcontainer.sh +++ b/tests/test-devcontainer.sh @@ -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 + +# ------------------------------------------------------------------------------ +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 + +# ------------------------------------------------------------------------------ +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 "========================================"