diff --git a/home-manager/services/cliproxyapi/README.md b/home-manager/services/cliproxyapi/README.md index fcdd075b3..09757f28d 100644 --- a/home-manager/services/cliproxyapi/README.md +++ b/home-manager/services/cliproxyapi/README.md @@ -1,287 +1,137 @@ # CLIProxyAPI Service Configuration -This directory contains the Nix-based configuration for the cliproxyapi service, including automatic auth file backup/recovery and OAuth token management. - -## Overview - -The cliproxyapi service provides a unified proxy for multiple AI providers (Claude, Codex, Gemini, OpenRouter, etc.) with automatic authentication management and cloud backup. +This directory contains the Nix-based configuration for the cliproxyapi service with S3-backed auth file management. ## Architecture ### Services -1. **cliproxyapi** - Main proxy server running on port 8317 -2. **cliproxyapi-backup** - File watcher that syncs auth files to R2 storage +1. **cliproxyapi** - Main proxy server on port 8317 +2. **cliproxyapi-backup** - File watcher that syncs auth files to S3 + +### Scripts + +| Script | Purpose | +|--------|---------| +| `hydrate.sh` | Pull S3 → local → CCS (runs at activation) | +| `backup.sh` | Push local → S3 → CCS (triggered by WatchPaths) | +| `start.sh` | Load .env, generate config, start binary | +| `wrapper.sh` | Load .env, exec binary (for CLI usage) | ### Directory Structure ``` ~/.cli-proxy-api/ -├── config.yaml # Generated config (from template) -├── config.template.yaml # Template with placeholders (symlink) +├── config.yaml # Generated config +├── config.template.yaml # Template with placeholders └── objectstore/ - ├── config/ - │ └── config.yaml # Cloud-synced config - └── auths/ # Auth files (cliproxyapi reads from here) - ├── claude-*.json # OAuth tokens for Claude - ├── codex-*.json # OAuth tokens for Codex - └── antigravity-*.json # OAuth tokens for other services - -~/dotfiles/objectstore/auths/ # Git-tracked backup (write-only) -~/.ccs/cliproxy/auth/ # CCS auth directory (two-way sync) - -R2 Storage (Cloudflare): -├── s3://cliproxyapi/auths/ # Primary cloud storage -├── s3://cliproxyapi/backup/auths/ # Redundant backup -└── s3://cliproxyapi/config/ # Config backup -``` - -**Important:** When object storage is enabled (via `OBJECTSTORE_ENDPOINT`), cliproxyapi reads auth files directly from `objectstore/auths/`. There is no separate `auths/` directory at the root level. + └── auths/ # Auth files (S3 is source of truth) -## Auth File Management +~/.ccs/cliproxy/auth/ # CCS auth directory (synced from local) -### On Service Start (`start.sh`) - -1. Pull auth files from R2 `auths/` → staged temp dir -2. Pull from R2 `backup/auths/` → staged temp dir (merge) -3. On macOS, merge `dotfiles/objectstore/auths/` (ignore-existing) → staged temp dir -4. Merge CCS auth dir (if present) → staged temp dir (CCS takes precedence) -5. **Atomic swap with recovery:** if the staged dir is empty, recover from CCS + dotfiles (macOS) + R2 backup, then do a safe two-phase swap into `objectstore/auths/`; only if still empty do we preserve the existing cache. -6. If objectstore still ends up empty after swap, copy from `dotfiles/objectstore/auths/` (bootstrapping) - -cliproxyapi then reads directly from `objectstore/auths/` (no additional sync needed). - -### On File Changes (`backup-auth.sh`) - -Triggered by launchd WatchPaths when files change in: -- `~/.cli-proxy-api/objectstore/auths/` -- `~/.ccs/cliproxy/auth/` - -**Flow:** -1. Pull from R2 `auths/` → `objectstore/auths/` -2. Pull from R2 `backup/auths/` → `objectstore/auths/` -3. Sync CCS auth files → `objectstore/auths/` (if exists) -4. Push `objectstore/auths/` → R2 `auths/` -5. Push `objectstore/auths/` → R2 `backup/auths/` -6. Sync `objectstore/auths/` → CCS auth dir -7. Sync `objectstore/auths/` → dotfiles (git tracking) +S3 Storage: +├── s3://cliproxyapi/auths/ # Primary storage +└── s3://cliproxyapi/backup/auths/ # Redundant backup +``` -### Data Flow +## Data Flow ``` -┌──────────────────┐ -│ R2 Storage │ -│ ┌────────────┐ │ ┌──────────────────────────────┐ -│ │ auths/ │◄─┼──────┤ objectstore/auths/ │ -│ └────────────┘ │ │ (cliproxyapi reads here) │ -│ ┌────────────┐ │ │ ┌────────────────────────┐ │ -│ │ backup/ │◄─┼──────┤ │ OAuth tokens stored │ │ -│ └────────────┘ │ │ │ - claude-*.json │ │ -└──────────────────┘ │ │ - codex-*.json │ │ - │ │ - antigravity-*.json │ │ - │ └────────────────────────┘ │ - └──────────────────────────────┘ - │ - ├─────► ccs/auth/ - └─────► dotfiles/ (git backup) - │ - └─► Bootstrap only +┌─────────────────────────────────────────────────────────┐ +│ S3 (Source of Truth) │ +│ ┌──────────────┐ ┌───────────────────────┐ │ +│ │ auths/ │ │ backup/auths/ │ │ +│ └──────┬───────┘ └───────────┬───────────┘ │ +└─────────┼─────────────────────────────┼─────────────────┘ + │ │ + ▼ hydrate.sh ▼ +┌─────────────────────────────────────────────────────────┐ +│ ~/.cli-proxy-api/objectstore/auths/ │ +│ (local cache) │ +└─────────────────────────┬───────────────────────────────┘ + │ + ▼ backup.sh (on file change) +┌─────────────────────────────────────────────────────────┐ +│ ~/.ccs/cliproxy/auth/ │ +│ (CCS compatibility) │ +└─────────────────────────────────────────────────────────┘ ``` -## Key Features +### Hydrate (on activation/switch) -### 1. OAuth Token Support +1. Pull from S3 `auths/` → local +2. Pull from S3 `backup/auths/` → local (takes precedence, overwrites conflicts) +3. Copy local → CCS auth dir -Auth files in `~/.cli-proxy-api/objectstore/auths/` are automatically used for OAuth-based API calls. This enables: -- `cliproxyapi --claude-login` for web-based authentication -- OAuth tokens (starting with `sk-ant-oat01-`) work with Claude API requests -- No need for manual API keys (starting with `sk-ant-api03-`) -- cliproxyapi reads directly from `objectstore/auths/` when object storage is enabled +### Backup (on file change) -### 2. Multi-Location Backup +1. Push local → S3 `auths/` +2. Push local → S3 `backup/auths/` +3. Copy local → CCS auth dir -Auth files are backed up to: -- **R2 Primary:** `s3://cliproxyapi/auths/` -- **R2 Backup:** `s3://cliproxyapi/backup/auths/` -- **Git:** `~/dotfiles/objectstore/auths/` (version controlled) -- **CCS:** `~/.ccs/cliproxy/auth/` (for CCS compatibility) +### WatchPaths (file watchers) -### 3. Automatic Recovery +The `cliproxyapi-backup` service watches these directories: +- `~/.cli-proxy-api/objectstore/auths` - main auth cache +- `~/.ccs/cliproxy/auth` - CCS auth directory -If auth files are lost locally, they are automatically recovered from: -1. R2 primary storage -2. R2 backup storage (if primary is missing files) -3. Git-tracked dotfiles (on fresh install) +**How it works (macOS launchd):** +- launchd monitors the directories for any file changes +- When a file is created, modified, or deleted, launchd triggers `backup.sh` +- Changes are detected within ~1 second -### 4. No Circular Loops +**How it works (Linux systemd):** +- systemd path unit watches the directories +- On change, triggers the `cliproxyapi-backup.service` oneshot +- Uses `PathChanged` directive for file monitoring -**Problem Solved:** Previously, `dotfiles/objectstore/auths` was both watched and written to, creating infinite sync loops. +**Why both directories?** +- `objectstore/auths`: cliproxyapi writes OAuth tokens here after login +- `ccs/cliproxy/auth`: CCS CLI may create tokens here independently -**Solution:** -- Removed dotfiles from WatchPaths -- Dotfiles is now **write-only** for output, but used as **read-only recovery source** -- Only `objectstore/auths` and `ccs/auth` trigger sync events - -### 5. Automatic Recovery from Git - -**Problem Solved:** Files could be lost if deleted from R2 (e.g., by cliproxyapi internals or manual deletion). - -**Solution:** -- Both `start.sh` and `backup-auth.sh` merge missing files from dotfiles -- Uses `rsync --ignore-existing` to never overwrite newer files from R2 -- Git-tracked dotfiles acts as a third backup location that survives R2 deletions -- Recovery is automatic and logged when files are restored - -### 6. Object Storage Integration - -When `OBJECTSTORE_ENDPOINT` is configured, cliproxyapi uses object-backed storage: -- Auth files stored in `objectstore/auths/` subdirectory -- cliproxyapi reads directly from this location (no separate `auths/` directory) -- Files are automatically synced to/from R2 on startup and file changes -- Local `objectstore/` acts as a cache for cloud storage +When either directory changes, `backup.sh` syncs everything to S3 and keeps both local directories in sync. ## Environment Variables Required in `~/dotfiles/.env`: ```bash -# Object Storage (Cloudflare R2) +# S3-compatible Object Storage OBJECTSTORE_ENDPOINT="https://....r2.cloudflarestorage.com" OBJECTSTORE_BUCKET="cliproxyapi" OBJECTSTORE_ACCESS_KEY="..." OBJECTSTORE_SECRET_KEY="..." -# Management Password +# Service Config CLIPROXY_MANAGEMENT_PASSWORD="..." - -# API Keys (injected into config) OPENROUTER_API_KEY="sk-or-v1-..." -ZAI_API_KEY="..." -AMP_UPSTREAM_API_KEY="sgamp_user_..." ``` ## Usage -### Initial Setup - ```bash -# Build and activate configuration +# Build and activate make build && make switch -# The service starts automatically -# Check status +# Check service status launchctl list | grep cliproxyapi # View logs tail -f /tmp/cliproxyapi.log -tail -f /tmp/cliproxyapi.error.log tail -f /tmp/cliproxyapi-backup.log -``` - -### OAuth Login -```bash -# Authenticate with Claude +# OAuth login cliproxyapi --claude-login - -# This will: -# 1. Open browser for OAuth -# 2. Save token to objectstore/auths/ -# 3. Trigger backup to R2 and dotfiles -# 4. Token is immediately usable (cliproxyapi reads from objectstore/auths/) ``` -### Testing - -```bash -# Test Claude API with OAuth -curl -X POST http://localhost:8317/v1/messages \ - -H "Content-Type: application/json" \ - -H "anthropic-version: 2023-06-01" \ - -d '{"model":"claude-opus-4-5-20251101","messages":[{"role":"user","content":"test"}],"max_tokens":10}' -``` - -### Troubleshooting - -```bash -# Restart service -make restart-cliproxyapi - -# Check auth files (with object storage) -ls -la ~/.cli-proxy-api/objectstore/auths/ - -# Verify R2 sync -aws s3 ls --endpoint-url="$OBJECTSTORE_ENDPOINT" s3://cliproxyapi/auths/ - -# Force backup -launchctl start org.nix-community.home.cliproxyapi-backup -``` - -## Recovery Scenarios - -### Scenario 1: Fresh Machine Setup - -1. Clone dotfiles: `git clone ... ~/dotfiles` -2. Run: `make install` -3. Service starts and bootstraps from `dotfiles/objectstore/auths/` -4. Auth files uploaded to R2 for cloud backup - -### Scenario 2: Auth File Accidentally Deleted from R2 - -1. Backup location still has the file -2. Next sync pulls from `backup/auths/` -3. File automatically restored to `auths/` and `backup/auths/` - -### Scenario 3: Local Machine Crash - -1. New machine or service restart -2. `start.sh` pulls from R2 on startup -3. All auth files recovered automatically - -### Scenario 4: Need to Roll Back Auth - -1. Check git history: `git log -- objectstore/auths/` -2. Restore old version from git -3. Remove `~/.cli-proxy-api/objectstore/auths/` -4. Restart service to bootstrap from dotfiles - -## Configuration Files - -### `default.nix` -- Defines launchd agents (macOS) or systemd services (Linux) -- Sets up WatchPaths for file monitoring -- Configures environment variables - -### `scripts/start.sh` -- Runs on service start -- Pulls auth files from R2 -- Bootstraps from dotfiles if needed -- Syncs to OAuth directory -- Starts cliproxyapi binary - -### `scripts/backup-auth.sh` -- Runs on file changes (WatchPaths) -- Pulls from R2 first (merge remote changes) -- Syncs from CCS if available -- Pushes to R2 (both locations) -- Syncs to all local locations - -### `scripts/backup-and-recover.sh` -- Wrapper script that sources `.env` -- Calls `backup-auth.sh` -- Used by launchd agent +## Dependencies -## Notes +This configuration depends on an upstream fix for a race condition in CLIProxyAPI: +https://github.com/router-for-me/CLIProxyAPI/pull/859 -- **Auth file naming:** Must match pattern `*-shunkakinoki@gmail.com.json` or `*-shunkakinoki_gmail_com.json` -- **OAuth tokens:** Start with `sk-ant-oat01-` (different from API keys which start with `sk-ant-api03-`) -- **File watchers:** Changes in watched directories trigger within ~1 second -- **Sync is idempotent:** Running multiple times is safe -- **Bootstrap runs once:** Only when objectstore is empty -- **Legacy files:** If you see `.json` files in `~/.cli-proxy-api/` (at root level), these are from before object storage was configured. They can be safely deleted - cliproxyapi only reads from `objectstore/auths/` when object storage is enabled. +Without this fix, auth files may be deleted from S3 on service restart. ## References - [CLIProxyAPI Documentation](https://help.router-for.me/) - [Object Storage Config](https://help.router-for.me/configuration/storage/s3) -- [Anthropic OAuth](https://docs.anthropic.com/en/api/oauth) diff --git a/home-manager/services/cliproxyapi/default.nix b/home-manager/services/cliproxyapi/default.nix index a1d9962be..cacffc8f6 100644 --- a/home-manager/services/cliproxyapi/default.nix +++ b/home-manager/services/cliproxyapi/default.nix @@ -1,42 +1,37 @@ { pkgs, ... }: let inherit (pkgs) lib; - # Use build-time HOME for paths that need it at plist generation homeDir = builtins.getEnv "HOME"; - # Create start script with paths substituted at build time - startScript = pkgs.replaceVars ./scripts/start.sh { + hydrateScript = pkgs.replaceVars ./scripts/hydrate.sh { aws = "${pkgs.awscli2}/bin/aws"; + }; + + backupScript = pkgs.replaceVars ./scripts/backup.sh { + aws = "${pkgs.awscli2}/bin/aws"; + }; + + startScript = pkgs.replaceVars ./scripts/start.sh { sed = "${pkgs.gnused}/bin/sed"; - rsync = "${pkgs.rsync}/bin/rsync"; }; - # Wrapper script that runs start.sh with docker group permissions - # Note: sg is from shadow package, available as system binary /usr/bin/sg dockerStartScript = pkgs.writeShellScript "cliproxyapi-docker-start" '' exec /usr/bin/sg docker -c "${pkgs.bash}/bin/bash ${startScript}" ''; - # Create backup scripts with paths substituted at build time - backupAuthScript = pkgs.replaceVars ./scripts/backup-auth.sh { - aws = "${pkgs.awscli2}/bin/aws"; - rsync = "${pkgs.rsync}/bin/rsync"; - }; - backupAndRecoverScript = pkgs.replaceVars ./scripts/backup-and-recover.sh { - bash = "${pkgs.bash}/bin/bash"; - backupAuthScript = backupAuthScript; - }; + cliWrapper = pkgs.writeShellScriptBin "cliproxyapi" (builtins.readFile ./scripts/wrapper.sh); in { - # Ensure auth cache is hydrated immediately after home-manager switch, - # so first CLI invocation after a rebuild doesn't hit missing auth files. + # Hydrate auth cache after home-manager switch home.activation = lib.optionalAttrs (lib ? hm && lib.hm ? dag) { hydrateCliproxyAuths = lib.hm.dag.entryAfter [ "writeBoundary" ] '' - ${pkgs.bash}/bin/bash ${backupAuthScript} || true + ${pkgs.bash}/bin/bash ${hydrateScript} || true ''; }; - # Main cliproxyapi service + home.packages = lib.mkIf pkgs.stdenv.isDarwin [ cliWrapper ]; + + # Main service launchd.agents.cliproxyapi = lib.mkIf pkgs.stdenv.isDarwin { enable = true; config = { @@ -61,42 +56,13 @@ in }; }; - systemd.user.services.cliproxyapi = lib.mkIf pkgs.stdenv.isLinux { - Unit = { - Description = "CLI Proxy API server"; - After = [ - "network.target" - "docker.service" - ]; - Wants = [ "docker.service" ]; - }; - Service = { - Type = "simple"; - Environment = "PATH=${ - lib.makeBinPath [ - pkgs.gnused - pkgs.bash - pkgs.coreutils - pkgs.awscli2 - pkgs.docker - ] - }"; - ExecStart = "${dockerStartScript}"; - Restart = "always"; - RestartSec = 3; - }; - Install = { - WantedBy = [ "default.target" ]; - }; - }; - - # Backup service with file watching for real-time sync + # Backup service - watches auth dir for changes launchd.agents.cliproxyapi-backup = lib.mkIf pkgs.stdenv.isDarwin { enable = true; config = { ProgramArguments = [ "${pkgs.bash}/bin/bash" - "${backupAndRecoverScript}" + "${backupScript}" ]; Environment = { HOME = homeDir; @@ -105,12 +71,9 @@ in pkgs.bash pkgs.coreutils pkgs.awscli2 - pkgs.rsync ] }:/opt/homebrew/bin:/usr/local/bin:/usr/bin"; }; - # Watch auth directories for changes - triggers sync immediately - # NOTE: dotfiles is excluded to prevent circular sync loops WatchPaths = [ "${homeDir}/.cli-proxy-api/objectstore/auths" "${homeDir}/.ccs/cliproxy/auth" @@ -121,6 +84,34 @@ in }; }; + # Linux systemd + systemd.user.services.cliproxyapi = lib.mkIf pkgs.stdenv.isLinux { + Unit = { + Description = "CLI Proxy API server"; + After = [ + "network.target" + "docker.service" + ]; + Wants = [ "docker.service" ]; + }; + Service = { + Type = "simple"; + Environment = "PATH=${ + lib.makeBinPath [ + pkgs.gnused + pkgs.bash + pkgs.coreutils + pkgs.awscli2 + pkgs.docker + ] + }"; + ExecStart = "${dockerStartScript}"; + Restart = "always"; + RestartSec = 3; + }; + Install.WantedBy = [ "default.target" ]; + }; + systemd.user.paths.cliproxyapi-backup = lib.mkIf pkgs.stdenv.isLinux { Unit.Description = "Watch auth directories for changes"; Path = { @@ -134,18 +125,15 @@ in }; systemd.user.services.cliproxyapi-backup = lib.mkIf pkgs.stdenv.isLinux { - Unit = { - Description = "CLIProxyAPI auth backup"; - }; + Unit.Description = "CLIProxyAPI auth backup"; Service = { Type = "oneshot"; - ExecStart = "${pkgs.bash}/bin/bash ${backupAndRecoverScript}"; + ExecStart = "${pkgs.bash}/bin/bash ${backupScript}"; Environment = "PATH=${ lib.makeBinPath [ pkgs.bash pkgs.awscli2 pkgs.coreutils - pkgs.rsync ] }"; }; diff --git a/home-manager/services/cliproxyapi/scripts/backup-and-recover.sh b/home-manager/services/cliproxyapi/scripts/backup-and-recover.sh deleted file mode 100644 index 2932f42e9..000000000 --- a/home-manager/services/cliproxyapi/scripts/backup-and-recover.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash -# Backup auth files to R2 (pull-first ensures recovery is handled) - -set -euo pipefail - -# Source .env for credentials -if [ -f "$HOME/dotfiles/.env" ]; then - set -a - # shellcheck source=/dev/null - source "$HOME/dotfiles/.env" - set +a -fi - -# Run backup (handles recovery via pull-first) -echo "[$(date)] Starting backup..." -@bash@ @backupAuthScript@ - -echo "[$(date)] Backup complete" diff --git a/home-manager/services/cliproxyapi/scripts/backup-auth.sh b/home-manager/services/cliproxyapi/scripts/backup-auth.sh deleted file mode 100755 index 41eba4c10..000000000 --- a/home-manager/services/cliproxyapi/scripts/backup-auth.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env bash -# Backup auth files to R2 before service start -# Protects against race condition deletions - -set -euo pipefail - -CONFIG_DIR="$HOME/.cli-proxy-api" -BACKUP_DIR="s3://cliproxyapi/backup/auths/" -MAIN_DIR="s3://cliproxyapi/auths/" -AUTH_DIR="$CONFIG_DIR/objectstore/auths" -CCS_AUTH_DIR="$HOME/.ccs/cliproxy/auth" -DOTFILES_AUTH_DIR="$HOME/dotfiles/objectstore/auths" - -# STEP 1: Pull from R2 to local (captures files created by cliproxyapi directly in R2) -mkdir -p "$AUTH_DIR" -if [ -n "${OBJECTSTORE_ENDPOINT:-}" ]; then - AWS_ACCESS_KEY_ID="${OBJECTSTORE_ACCESS_KEY}" \ - AWS_SECRET_ACCESS_KEY="${OBJECTSTORE_SECRET_KEY}" \ - @aws@ s3 sync \ - --endpoint-url="${OBJECTSTORE_ENDPOINT}" \ - --no-progress \ - "$MAIN_DIR" \ - "$AUTH_DIR/" 2>/dev/null && echo "✅ Pulled from R2 auths/" >&2 || true - - # Also pull from backup location to ensure we have all files - AWS_ACCESS_KEY_ID="${OBJECTSTORE_ACCESS_KEY}" \ - AWS_SECRET_ACCESS_KEY="${OBJECTSTORE_SECRET_KEY}" \ - @aws@ s3 sync \ - --endpoint-url="${OBJECTSTORE_ENDPOINT}" \ - --no-progress \ - "$BACKUP_DIR" \ - "$AUTH_DIR/" 2>/dev/null && echo "✅ Pulled from R2 backup/auths/" >&2 || true -fi - -# STEP 2: Sync from ccs auth dir (picks up files created by ccs's internal cliproxy) -if [ -d "$CCS_AUTH_DIR" ] && [ -n "$(ls -A "$CCS_AUTH_DIR" 2>/dev/null)" ]; then - @rsync@ -a "$CCS_AUTH_DIR/" "$AUTH_DIR/" - echo "✅ Synced from ccs auth dir to local cache" >&2 -fi - -# STEP 3: Recover missing files from git-tracked dotfiles backup (macOS only; Linux relies on R2) -if [ "$(uname)" = "Darwin" ] && [ -d "$DOTFILES_AUTH_DIR" ] && [ -n "$(ls -A "$DOTFILES_AUTH_DIR" 2>/dev/null)" ]; then - @rsync@ -a --ignore-existing "$DOTFILES_AUTH_DIR/" "$AUTH_DIR/" - echo "✅ Recovered missing auths from dotfiles backup (macOS)" >&2 -fi - -# Check if auth directory has files -if [ -d "$AUTH_DIR" ] && [ -n "$(ls -A "$AUTH_DIR" 2>/dev/null)" ]; then - echo "Syncing auth files to R2..." >&2 - - # Sync to main auths/ location (what cliproxyapi reads from) - if [ -z "${OBJECTSTORE_ENDPOINT:-}" ]; then - echo "⚠️ OBJECTSTORE_ENDPOINT not set, skipping R2 sync" >&2 - else - AWS_ACCESS_KEY_ID="${OBJECTSTORE_ACCESS_KEY}" \ - AWS_SECRET_ACCESS_KEY="${OBJECTSTORE_SECRET_KEY}" \ - @aws@ s3 sync \ - --endpoint-url="${OBJECTSTORE_ENDPOINT}" \ - --no-progress \ - "$AUTH_DIR/" \ - "$MAIN_DIR" && echo "✅ Synced to auths/" >&2 || echo "⚠️ Sync to auths/ failed: $?" >&2 - - # Also sync to backup location for redundancy - AWS_ACCESS_KEY_ID="${OBJECTSTORE_ACCESS_KEY}" \ - AWS_SECRET_ACCESS_KEY="${OBJECTSTORE_SECRET_KEY}" \ - @aws@ s3 sync \ - --endpoint-url="${OBJECTSTORE_ENDPOINT}" \ - --no-progress \ - "$AUTH_DIR/" \ - "$BACKUP_DIR" && echo "✅ Synced to backup/auths/" >&2 || echo "⚠️ Backup sync failed" >&2 - fi - - # Sync to ccs auth dir (so ccs can find the tokens) - mkdir -p "$CCS_AUTH_DIR" - @rsync@ -a "$AUTH_DIR/" "$CCS_AUTH_DIR/" - echo "✅ Synced to ccs auth dir" >&2 -fi diff --git a/home-manager/services/cliproxyapi/scripts/backup.sh b/home-manager/services/cliproxyapi/scripts/backup.sh new file mode 100644 index 000000000..325313837 --- /dev/null +++ b/home-manager/services/cliproxyapi/scripts/backup.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Push auth files from local cache to S3 +# shellcheck source=/dev/null +set -euo pipefail + +AUTH_DIR="${HOME}/.cli-proxy-api/objectstore/auths" +CCS_AUTH_DIR="${HOME}/.ccs/cliproxy/auth" +ENV_FILE="${HOME}/dotfiles/.env" + +if [ -f "$ENV_FILE" ]; then + set -a + . "$ENV_FILE" + set +a +fi + +strip_quotes() { + local v="$1" + v="${v%\"}" + v="${v#\"}" + printf '%s' "$v" +} +ENDPOINT="$(strip_quotes "${OBJECTSTORE_ENDPOINT:-}")" +ACCESS_KEY="$(strip_quotes "${OBJECTSTORE_ACCESS_KEY:-}")" +SECRET_KEY="$(strip_quotes "${OBJECTSTORE_SECRET_KEY:-}")" + +if [ -z "$ENDPOINT" ] || [ -z "$ACCESS_KEY" ] || [ -z "$SECRET_KEY" ]; then + echo "⚠️ Missing S3 credentials, skipping backup" >&2 + exit 0 +fi + +if [ ! -d "$AUTH_DIR" ] || [ -z "$(ls -A "$AUTH_DIR" 2>/dev/null)" ]; then + echo "⚠️ No auth files to backup" >&2 + exit 0 +fi + +echo "[$(date)] Backing up auth files..." >&2 + +AWS_ACCESS_KEY_ID="$ACCESS_KEY" \ + AWS_SECRET_ACCESS_KEY="$SECRET_KEY" \ + @aws@ s3 sync \ + --endpoint-url="$ENDPOINT" \ + --no-progress \ + "$AUTH_DIR/" \ + "s3://cliproxyapi/auths/" && echo "✅ Backed up to S3 auths/" >&2 + +AWS_ACCESS_KEY_ID="$ACCESS_KEY" \ + AWS_SECRET_ACCESS_KEY="$SECRET_KEY" \ + @aws@ s3 sync \ + --endpoint-url="$ENDPOINT" \ + --no-progress \ + "$AUTH_DIR/" \ + "s3://cliproxyapi/backup/auths/" && echo "✅ Backed up to S3 backup/auths/" >&2 + +# Also sync back to CCS auth dir so ccs can find the tokens +mkdir -p "$CCS_AUTH_DIR" +cp -u "$AUTH_DIR"/*.json "$CCS_AUTH_DIR/" 2>/dev/null || true diff --git a/home-manager/services/cliproxyapi/scripts/hydrate.sh b/home-manager/services/cliproxyapi/scripts/hydrate.sh new file mode 100644 index 000000000..4b0188683 --- /dev/null +++ b/home-manager/services/cliproxyapi/scripts/hydrate.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Pull auth files from S3 to local cache +# shellcheck source=/dev/null +set -euo pipefail + +AUTH_DIR="${HOME}/.cli-proxy-api/objectstore/auths" +CCS_AUTH_DIR="${HOME}/.ccs/cliproxy/auth" +ENV_FILE="${HOME}/dotfiles/.env" + +if [ -f "$ENV_FILE" ]; then + set -a + . "$ENV_FILE" + set +a +fi + +strip_quotes() { + local v="$1" + v="${v%\"}" + v="${v#\"}" + printf '%s' "$v" +} +ENDPOINT="$(strip_quotes "${OBJECTSTORE_ENDPOINT:-}")" +ACCESS_KEY="$(strip_quotes "${OBJECTSTORE_ACCESS_KEY:-}")" +SECRET_KEY="$(strip_quotes "${OBJECTSTORE_SECRET_KEY:-}")" + +if [ -z "$ENDPOINT" ] || [ -z "$ACCESS_KEY" ] || [ -z "$SECRET_KEY" ]; then + echo "⚠️ Missing S3 credentials, skipping hydrate" >&2 + exit 0 +fi + +mkdir -p "$AUTH_DIR" + +AWS_ACCESS_KEY_ID="$ACCESS_KEY" \ + AWS_SECRET_ACCESS_KEY="$SECRET_KEY" \ + @aws@ s3 sync \ + --endpoint-url="$ENDPOINT" \ + --no-progress \ + "s3://cliproxyapi/auths/" \ + "$AUTH_DIR/" && echo "✅ Hydrated from S3 auths/" >&2 + +AWS_ACCESS_KEY_ID="$ACCESS_KEY" \ + AWS_SECRET_ACCESS_KEY="$SECRET_KEY" \ + @aws@ s3 sync \ + --endpoint-url="$ENDPOINT" \ + --no-progress \ + "s3://cliproxyapi/backup/auths/" \ + "$AUTH_DIR/" && echo "✅ Hydrated from S3 backup/auths/" >&2 + +# Also sync to CCS auth dir so ccs can find the tokens +mkdir -p "$CCS_AUTH_DIR" +cp -u "$AUTH_DIR"/*.json "$CCS_AUTH_DIR/" 2>/dev/null || true diff --git a/home-manager/services/cliproxyapi/scripts/start.sh b/home-manager/services/cliproxyapi/scripts/start.sh index 978d23f61..c8c2c9dd5 100755 --- a/home-manager/services/cliproxyapi/scripts/start.sh +++ b/home-manager/services/cliproxyapi/scripts/start.sh @@ -1,142 +1,32 @@ #!/usr/bin/env bash - +# shellcheck source=/dev/null set -euo pipefail -CONFIG_DIR="$HOME/.cli-proxy-api" +CONFIG_DIR="${HOME}/.cli-proxy-api" TEMPLATE="$CONFIG_DIR/config.template.yaml" CONFIG="$CONFIG_DIR/config.yaml" -AUTH_DIR="$CONFIG_DIR/objectstore/auths" -DOTFILES_AUTH_DIR="$HOME/dotfiles/objectstore/auths" -CCS_AUTH_DIR="$HOME/.ccs/cliproxy/auth" -TMP_AUTH_DIR="$CONFIG_DIR/objectstore/auths.tmp" -# Use explicit path since $HOME may not be set correctly in launchd context -ENV_FILE="${HOME:-/Users/shunkakinoki}/dotfiles/.env" +ENV_FILE="${HOME}/dotfiles/.env" -# Source .env file to get API keys if [ -f "$ENV_FILE" ]; then set -a - # shellcheck source=/dev/null - source "$ENV_FILE" + . "$ENV_FILE" set +a fi -# Export management password for Management API (CLIProxyAPI requires MANAGEMENT_PASSWORD env var) -export MANAGEMENT_PASSWORD="${CLIPROXY_MANAGEMENT_PASSWORD:-}" - -# Export S3-compatible object storage env vars (needed for backup/recovery) -export OBJECTSTORE_ENDPOINT="${OBJECTSTORE_ENDPOINT:-${AWS_S3_ENDPOINT:-}}" -export OBJECTSTORE_BUCKET="${OBJECTSTORE_BUCKET:-${AWS_S3_BUCKET:-}}" -export OBJECTSTORE_ACCESS_KEY="${OBJECTSTORE_ACCESS_KEY:-${AWS_ACCESS_KEY_ID:-}}" -export OBJECTSTORE_SECRET_KEY="${OBJECTSTORE_SECRET_KEY:-${AWS_SECRET_ACCESS_KEY:-}}" - -# CRITICAL: Pull auth files from R2 before starting cliproxyapi. -# We stage into a temp dir and atomically swap into AUTH_DIR to avoid partial reads. -if [ -n "${OBJECTSTORE_ENDPOINT:-}" ] && [ -n "${OBJECTSTORE_ACCESS_KEY:-}" ]; then - echo "Syncing auth files from R2..." >&2 - rm -rf "$TMP_AUTH_DIR" - mkdir -p "$TMP_AUTH_DIR" - - # Pull from both active and backup locations to ensure we have all files - AWS_ACCESS_KEY_ID="${OBJECTSTORE_ACCESS_KEY}" \ - AWS_SECRET_ACCESS_KEY="${OBJECTSTORE_SECRET_KEY}" \ - @aws@ s3 sync \ - --endpoint-url="${OBJECTSTORE_ENDPOINT}" \ - --no-progress \ - "s3://cliproxyapi/auths/" \ - "$TMP_AUTH_DIR/" 2>/dev/null && echo "✅ Pulled from R2 auths/" >&2 || echo "⚠️ Failed to pull from R2 auths/" >&2 - - AWS_ACCESS_KEY_ID="${OBJECTSTORE_ACCESS_KEY}" \ - AWS_SECRET_ACCESS_KEY="${OBJECTSTORE_SECRET_KEY}" \ - @aws@ s3 sync \ - --endpoint-url="${OBJECTSTORE_ENDPOINT}" \ - --no-progress \ - "s3://cliproxyapi/backup/auths/" \ - "$TMP_AUTH_DIR/" 2>/dev/null && echo "✅ Pulled from R2 backup/auths/" >&2 || echo "⚠️ Failed to pull from R2 backup/auths/" >&2 - - # Recover missing files from git-tracked dotfiles backup (macOS only; Linux Docker uses R2) - if [ "$(uname)" = "Darwin" ] && [ -d "$DOTFILES_AUTH_DIR" ] && [ -n "$(ls -A "$DOTFILES_AUTH_DIR" 2>/dev/null)" ]; then - @rsync@ -a --ignore-existing "$DOTFILES_AUTH_DIR/" "$TMP_AUTH_DIR/" - echo "✅ Bootstrapped auth files from dotfiles backup (macOS)" >&2 - fi - - # Pull from CCS auth dir (if present) to pick up locally-created tokens (CCS takes precedence) - if [ -d "$CCS_AUTH_DIR" ] && [ -n "$(ls -A "$CCS_AUTH_DIR" 2>/dev/null)" ]; then - @rsync@ -a "$CCS_AUTH_DIR/" "$TMP_AUTH_DIR/" - echo "✅ Synced auths from CCS directory" >&2 - fi - - # Atomically replace AUTH_DIR with merged TMP_AUTH_DIR to avoid partial reads. - # If TMP is empty, attempt recovery from CCS/dotfiles/R2 backup; otherwise keep cache. - if [ -z "$(ls -A "$TMP_AUTH_DIR" 2>/dev/null)" ]; then - echo "⚠️ Temp auth dir empty; attempting recovery from CCS/dotfiles/R2 backup" >&2 - # CCS -> TMP - if [ -d "$CCS_AUTH_DIR" ] && [ -n "$(ls -A "$CCS_AUTH_DIR" 2>/dev/null)" ]; then - @rsync@ -a "$CCS_AUTH_DIR/" "$TMP_AUTH_DIR/" - fi - # dotfiles (macOS) -> TMP - if [ "$(uname)" = "Darwin" ] && [ -d "$DOTFILES_AUTH_DIR" ] && [ -n "$(ls -A "$DOTFILES_AUTH_DIR" 2>/dev/null)" ]; then - @rsync@ -a --ignore-existing "$DOTFILES_AUTH_DIR/" "$TMP_AUTH_DIR/" - fi - # R2 backup -> TMP - if [ -n "${OBJECTSTORE_ENDPOINT:-}" ] && [ -n "${OBJECTSTORE_ACCESS_KEY:-}" ]; then - AWS_ACCESS_KEY_ID="${OBJECTSTORE_ACCESS_KEY}" \ - AWS_SECRET_ACCESS_KEY="${OBJECTSTORE_SECRET_KEY}" \ - @aws@ s3 sync \ - --endpoint-url="${OBJECTSTORE_ENDPOINT}" \ - --no-progress \ - "s3://cliproxyapi/backup/auths/" \ - "$TMP_AUTH_DIR/" 2>/dev/null || true - fi - fi - - if [ -n "$(ls -A "$TMP_AUTH_DIR" 2>/dev/null)" ]; then - mv "$TMP_AUTH_DIR" "$AUTH_DIR.new" - rm -rf "$AUTH_DIR.old" 2>/dev/null || true - mv "$AUTH_DIR" "$AUTH_DIR.old" 2>/dev/null || true - mv "$AUTH_DIR.new" "$AUTH_DIR" - rm -rf "$AUTH_DIR.old" - else - echo "⚠️ No auth files recovered; preserving existing auth cache" >&2 - rm -rf "$TMP_AUTH_DIR" - fi - - # Bootstrap fallback: if AUTH_DIR is still empty after all syncs, copy from dotfiles - if [ ! -d "$AUTH_DIR" ] || [ -z "$(ls -A "$AUTH_DIR" 2>/dev/null)" ]; then - if [ -d "$DOTFILES_AUTH_DIR" ] && [ -n "$(ls -A "$DOTFILES_AUTH_DIR" 2>/dev/null)" ]; then - echo "Bootstrapping auth files from dotfiles backup..." >&2 - mkdir -p "$AUTH_DIR" - @rsync@ -a "$DOTFILES_AUTH_DIR/" "$AUTH_DIR/" - echo "✅ Bootstrapped from dotfiles" >&2 - fi - fi - - # CRITICAL: Always sync local auth files back to R2 after pulling - # This ensures any files that exist locally but not in R2 get uploaded, - # preventing "key does not exist" errors when cliproxyapi reads from object storage - if [ -d "$AUTH_DIR" ] && [ -n "$(ls -A "$AUTH_DIR" 2>/dev/null)" ]; then - echo "Syncing local auth files to R2..." >&2 - AWS_ACCESS_KEY_ID="${OBJECTSTORE_ACCESS_KEY}" \ - AWS_SECRET_ACCESS_KEY="${OBJECTSTORE_SECRET_KEY}" \ - @aws@ s3 sync \ - --endpoint-url="${OBJECTSTORE_ENDPOINT}" \ - --no-progress \ - "$AUTH_DIR/" \ - "s3://cliproxyapi/auths/" 2>&1 && echo "✅ Auth files synced to R2 auths/" >&2 || echo "⚠️ Failed to sync auth files to R2" >&2 - - # Also sync to backup location for redundancy - AWS_ACCESS_KEY_ID="${OBJECTSTORE_ACCESS_KEY}" \ - AWS_SECRET_ACCESS_KEY="${OBJECTSTORE_SECRET_KEY}" \ - @aws@ s3 sync \ - --endpoint-url="${OBJECTSTORE_ENDPOINT}" \ - --no-progress \ - "$AUTH_DIR/" \ - "s3://cliproxyapi/backup/auths/" 2>&1 && echo "✅ Auth files synced to R2 backup/" >&2 || true - fi -else - echo "⚠️ Skipping auth sync: OBJECTSTORE credentials not set" >&2 -fi - -# Generate config from template with secrets injected +strip_quotes() { + local v="$1" + v="${v%\"}" + v="${v#\"}" + printf '%s' "$v" +} +OBJECTSTORE_ENDPOINT="$(strip_quotes "${OBJECTSTORE_ENDPOINT:-}")" +OBJECTSTORE_BUCKET="$(strip_quotes "${OBJECTSTORE_BUCKET:-cliproxyapi}")" +OBJECTSTORE_ACCESS_KEY="$(strip_quotes "${OBJECTSTORE_ACCESS_KEY:-}")" +OBJECTSTORE_SECRET_KEY="$(strip_quotes "${OBJECTSTORE_SECRET_KEY:-}")" +MANAGEMENT_PASSWORD="${CLIPROXY_MANAGEMENT_PASSWORD:-}" +export OBJECTSTORE_ENDPOINT OBJECTSTORE_BUCKET OBJECTSTORE_ACCESS_KEY OBJECTSTORE_SECRET_KEY MANAGEMENT_PASSWORD + +# Generate config from template if [ -f "$TEMPLATE" ]; then @sed@ \ -e "s|__OPENROUTER_API_KEY__|${OPENROUTER_API_KEY:-}|g" \ @@ -145,49 +35,20 @@ if [ -f "$TEMPLATE" ]; then -e "s|__AMP_UPSTREAM_API_KEY__|${AMP_UPSTREAM_API_KEY:-}|g" \ "$TEMPLATE" >"$CONFIG" - # Linux: uncomment and enable api-keys for client authentication (only if key is set) - # macOS: leave api-keys commented for open access if [ "$(uname)" = "Linux" ] && [ -n "${CLIPROXY_API_KEY:-}" ]; then @sed@ -i \ -e "s|^# api-keys:|api-keys:|" \ -e "s|^# - \"__CLIPROXY_API_KEY__\"| - \"${CLIPROXY_API_KEY}\"|" \ "$CONFIG" fi - # Also copy to objectstore config location (cliproxyapi uses this for persistence) - mkdir -p "$CONFIG_DIR/objectstore/config" - cp "$CONFIG" "$CONFIG_DIR/objectstore/config/config.yaml" - - # Upload config to S3 to ensure backup is always correct - # This prevents corrupted configs from persisting across restarts - if [ -n "${OBJECTSTORE_ENDPOINT:-}" ] && [ -n "${OBJECTSTORE_ACCESS_KEY:-}" ]; then - echo "Uploading config to S3 backup..." >&2 - if AWS_ACCESS_KEY_ID="${OBJECTSTORE_ACCESS_KEY}" \ - AWS_SECRET_ACCESS_KEY="${OBJECTSTORE_SECRET_KEY}" \ - @aws@ s3 cp \ - --endpoint-url="${OBJECTSTORE_ENDPOINT}" \ - --no-progress \ - "$CONFIG" \ - "s3://cliproxyapi/config/config.yaml" 2>&1; then - echo "✅ Config backup uploaded" >&2 - else - echo "⚠️ Config backup failed (continuing anyway)" >&2 - fi - else - echo "⚠️ S3 config backup skipped: missing credentials" >&2 - fi fi -# Change to config dir so logs are created there cd "$CONFIG_DIR" -# On Linux, prefer Docker for easy upgrades +# Linux: Docker if [ "$(uname)" = "Linux" ] && command -v docker >/dev/null 2>&1; then - # Stop any existing container docker rm -f cliproxyapi 2>/dev/null || true - - # Create logs directory if it doesn't exist mkdir -p "$CONFIG_DIR/logs" - exec docker run --rm \ --name cliproxyapi \ --network host \ @@ -199,14 +60,12 @@ if [ "$(uname)" = "Linux" ] && command -v docker >/dev/null 2>&1; then eceasy/cli-proxy-api:latest fi -# macOS: use Homebrew binary +# macOS: Homebrew binary if [ -x /opt/homebrew/bin/cliproxyapi ]; then exec /opt/homebrew/bin/cliproxyapi -config "$CONFIG" "$@" elif [ -x /usr/local/bin/cliproxyapi ]; then exec /usr/local/bin/cliproxyapi -config "$CONFIG" "$@" else - echo 'cliproxyapi not found' >&2 - echo 'Linux: Docker should be available' >&2 - echo 'macOS: brew install cliproxyapi' >&2 + echo "cliproxyapi not found" >&2 exit 1 fi diff --git a/home-manager/services/cliproxyapi/scripts/wrapper.sh b/home-manager/services/cliproxyapi/scripts/wrapper.sh new file mode 100644 index 000000000..df53ae781 --- /dev/null +++ b/home-manager/services/cliproxyapi/scripts/wrapper.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# shellcheck source=/dev/null +set -euo pipefail + +ENV_FILE="${HOME}/dotfiles/.env" +if [ -f "$ENV_FILE" ]; then + set -a + . "$ENV_FILE" + set +a +fi + +strip_quotes() { + local v="$1" + v="${v%\"}" + v="${v#\"}" + printf '%s' "$v" +} +OBJECTSTORE_ENDPOINT="$(strip_quotes "${OBJECTSTORE_ENDPOINT:-}")" +OBJECTSTORE_BUCKET="$(strip_quotes "${OBJECTSTORE_BUCKET:-cliproxyapi}")" +OBJECTSTORE_ACCESS_KEY="$(strip_quotes "${OBJECTSTORE_ACCESS_KEY:-}")" +OBJECTSTORE_SECRET_KEY="$(strip_quotes "${OBJECTSTORE_SECRET_KEY:-}")" +export OBJECTSTORE_ENDPOINT OBJECTSTORE_BUCKET OBJECTSTORE_ACCESS_KEY OBJECTSTORE_SECRET_KEY + +exec /opt/homebrew/bin/cliproxyapi "$@" diff --git a/spec/cliproxyapi_backup_spec.sh b/spec/cliproxyapi_backup_spec.sh index d4b25a81d..94c269a54 100644 --- a/spec/cliproxyapi_backup_spec.sh +++ b/spec/cliproxyapi_backup_spec.sh @@ -4,39 +4,39 @@ Describe 'cliproxyapi backup scripts' SCRIPTS_DIR="$PWD/home-manager/services/cliproxyapi/scripts" -# Preprocess scripts once at describe-time (not in setup) -# This ensures the preprocessed paths are available before any tests run +# Preprocess scripts once at describe-time __PREPROCESSED_DIR=$(mktemp -d) -__BACKUP_AUTH_SCRIPT="$__PREPROCESSED_DIR/backup-auth.sh" -__BACKUP_RECOVER_SCRIPT="$__PREPROCESSED_DIR/backup-and-recover.sh" +__HYDRATE_SCRIPT="$__PREPROCESSED_DIR/hydrate.sh" +__BACKUP_SCRIPT="$__PREPROCESSED_DIR/backup.sh" -# Preprocess backup-auth.sh +# Preprocess hydrate.sh sed \ -e 's|@aws@|aws|g' \ - -e 's|@rsync@|rsync|g' \ - -e 's|@bash@|bash|g' \ - -e 's|@sed@|sed|g' \ - "$SCRIPTS_DIR/backup-auth.sh" >"$__BACKUP_AUTH_SCRIPT" -chmod +x "$__BACKUP_AUTH_SCRIPT" + "$SCRIPTS_DIR/hydrate.sh" >"$__HYDRATE_SCRIPT" +chmod +x "$__HYDRATE_SCRIPT" -# Preprocess backup-and-recover.sh with path to preprocessed backup-auth.sh +# Preprocess backup.sh sed \ -e 's|@aws@|aws|g' \ - -e 's|@rsync@|rsync|g' \ - -e 's|@bash@|bash|g' \ - -e 's|@sed@|sed|g' \ - -e "s|@backupAuthScript@|$__BACKUP_AUTH_SCRIPT|g" \ - "$SCRIPTS_DIR/backup-and-recover.sh" >"$__BACKUP_RECOVER_SCRIPT" -chmod +x "$__BACKUP_RECOVER_SCRIPT" + "$SCRIPTS_DIR/backup.sh" >"$__BACKUP_SCRIPT" +chmod +x "$__BACKUP_SCRIPT" -Describe 'backup-auth.sh' +Describe 'hydrate.sh' setup() { - mock_bin_setup aws rsync + mock_bin_setup aws TEMP_HOME=$(mktemp -d) mkdir -p "$TEMP_HOME/.cli-proxy-api/objectstore/auths" + mkdir -p "$TEMP_HOME/.ccs/cliproxy/auth" + mkdir -p "$TEMP_HOME/dotfiles" + + # Create .env with test credentials + cat >"$TEMP_HOME/dotfiles/.env" <<'ENV' +OBJECTSTORE_ACCESS_KEY=test_key +OBJECTSTORE_SECRET_KEY=test_secret +OBJECTSTORE_ENDPOINT=https://test.endpoint.com +ENV - # Unset objectstore credentials to ensure clean test environment unset OBJECTSTORE_ACCESS_KEY unset OBJECTSTORE_SECRET_KEY unset OBJECTSTORE_ENDPOINT @@ -50,31 +50,28 @@ cleanup() { Before 'setup' After 'cleanup' -It 'pulls from R2 but skips push when auth directory is empty' -When run bash -c 'HOME="'"$TEMP_HOME"'" OBJECTSTORE_ACCESS_KEY=key OBJECTSTORE_SECRET_KEY=secret OBJECTSTORE_ENDPOINT=https://example.com bash "'"$__BACKUP_AUTH_SCRIPT"'" 2>&1; cat "$MOCK_LOG" 2>/dev/null || true' +It 'pulls from S3 auths and backup/auths' +When run bash -c 'HOME="'"$TEMP_HOME"'" bash "'"$__HYDRATE_SCRIPT"'" 2>&1; cat "$MOCK_LOG" 2>/dev/null || true' The status should be success -# Should pull from R2 auths/ and backup/auths/ The output should include 's3://cliproxyapi/auths/' The output should include 's3://cliproxyapi/backup/auths/' -# Should NOT push to R2 since local is empty after pull -The output should not include 'Syncing auth files to R2' End -It 'calls aws s3 sync when auth files exist' -touch "$TEMP_HOME/.cli-proxy-api/objectstore/auths/test-auth.json" -When run bash -c 'HOME="'"$TEMP_HOME"'" OBJECTSTORE_ACCESS_KEY=key OBJECTSTORE_SECRET_KEY=secret OBJECTSTORE_ENDPOINT=https://example.com bash "'"$__BACKUP_AUTH_SCRIPT"'" 2>&1; cat "$MOCK_LOG"' +It 'skips when credentials are missing' +rm -f "$TEMP_HOME/dotfiles/.env" +When run bash -c 'HOME="'"$TEMP_HOME"'" bash "'"$__HYDRATE_SCRIPT"'" 2>&1' The status should be success -The output should include 's3 sync' -The output should include 's3://cliproxyapi/backup/auths/' +The output should include 'Missing S3 credentials' End End -Describe 'backup-and-recover.sh' +Describe 'backup.sh' setup() { - mock_bin_setup aws rsync + mock_bin_setup aws TEMP_HOME=$(mktemp -d) mkdir -p "$TEMP_HOME/.cli-proxy-api/objectstore/auths" + mkdir -p "$TEMP_HOME/.ccs/cliproxy/auth" mkdir -p "$TEMP_HOME/dotfiles" # Create .env with test credentials @@ -97,11 +94,26 @@ cleanup() { Before 'setup' After 'cleanup' -It 'sources .env and runs backup script' -When run bash -c 'HOME="'"$TEMP_HOME"'" bash "'"$__BACKUP_RECOVER_SCRIPT"'" 2>&1' +It 'skips when auth directory is empty' +When run bash -c 'HOME="'"$TEMP_HOME"'" bash "'"$__BACKUP_SCRIPT"'" 2>&1' +The status should be success +The output should include 'No auth files to backup' +End + +It 'pushes to S3 when auth files exist' +touch "$TEMP_HOME/.cli-proxy-api/objectstore/auths/test-auth.json" +When run bash -c 'HOME="'"$TEMP_HOME"'" bash "'"$__BACKUP_SCRIPT"'" 2>&1; cat "$MOCK_LOG" 2>/dev/null || true' +The status should be success +The output should include 's3://cliproxyapi/auths/' +The output should include 's3://cliproxyapi/backup/auths/' +End + +It 'skips when credentials are missing' +rm -f "$TEMP_HOME/dotfiles/.env" +touch "$TEMP_HOME/.cli-proxy-api/objectstore/auths/test-auth.json" +When run bash -c 'HOME="'"$TEMP_HOME"'" bash "'"$__BACKUP_SCRIPT"'" 2>&1' The status should be success -The output should include 'Starting backup' -The output should include 'Backup complete' +The output should include 'Missing S3 credentials' End End diff --git a/spec/cliproxyapi_spec.sh b/spec/cliproxyapi_spec.sh index e3eb3d149..5b74374ac 100644 --- a/spec/cliproxyapi_spec.sh +++ b/spec/cliproxyapi_spec.sh @@ -154,7 +154,7 @@ The status should be success End It 'script has Linux-specific api-keys uncommenting logic' -When run bash -c "grep -A 6 'Linux: uncomment and enable api-keys' '$SCRIPT'" +When run bash -c "grep -A 5 'uname.*Linux.*CLIPROXY_API_KEY' '$SCRIPT'" # shellcheck disable=SC2016 The output should include 'if [ "$(uname)" = "Linux" ] && [ -n "${CLIPROXY_API_KEY:-}" ]' The output should include 's|^# api-keys:|api-keys:|' @@ -209,9 +209,9 @@ When run bash -c "grep 'cliproxyapi not found' '$SCRIPT'" The output should include 'cliproxyapi not found' End -It 'suggests installation command in error message' -When run bash -c "grep 'brew install cliproxyapi' '$SCRIPT'" -The output should include 'brew install cliproxyapi' +It 'exits with error when binary not found' +When run bash -c "grep 'exit 1' '$SCRIPT'" +The output should include 'exit 1' End End diff --git a/spec/coverage_spec.sh b/spec/coverage_spec.sh index fac5bd4a0..4ff82da22 100644 --- a/spec/coverage_spec.sh +++ b/spec/coverage_spec.sh @@ -88,9 +88,10 @@ config/claude/statusline-git.sh home-manager/modules/local-binaries/sync-local-binaries.sh home-manager/programs/neovim/run_tests.sh home-manager/services/brew-upgrader/upgrade.sh -home-manager/services/cliproxyapi/scripts/backup-and-recover.sh -home-manager/services/cliproxyapi/scripts/backup-auth.sh +home-manager/services/cliproxyapi/scripts/backup.sh +home-manager/services/cliproxyapi/scripts/hydrate.sh home-manager/services/cliproxyapi/scripts/start.sh +home-manager/services/cliproxyapi/scripts/wrapper.sh home-manager/services/code-syncer/sync.sh home-manager/services/dotfiles-updater/update.sh home-manager/services/neverssl-keepalive/keepalive.sh