# AEVS CLI Tool — Sprint 1 Specification ## Overview CLI tool for syncing `.env` variables between machines and team members. - **Language:** Go (golang) - **Architecture:** CLI communicates with a REST API server - **Sprint scope:** CLI implementation with mock server interface --- ## Commands ### `aevs init` Initializes or updates project configuration. ```bash aevs init ``` **Behavior (new config):** 1. Interactive prompt for `api_url` 2. Interactive prompt for `api_key` 3. Runs `parseLocals` to scan for env files 4. Creates `aevs.yaml` config with found environments 5. `project` field left empty (will be set on first sync) **Behavior (config exists):** 1. Runs `parseLocals` to scan for new env files 2. Merges newly found files into existing config 3. Displays list of added files --- ### `aevs project` Project management commands. #### List projects ```bash aevs project -l, --list ``` **Output:** ``` uuid | name 11111111-1111-1111-1111-0123456789ab | my-project 22222222-2222-2222-2222-0123456789ab | another-project ``` #### Update project name ```bash aevs project save -n, --name "new_name" ``` **Optional flags:** - `-c, --config ` — config path to read `project_id` - `-i, --id ` — project ID directly (without config) #### Delete project ```bash aevs project remove -r, --id ``` **Behavior:** 1. Fetches project name from server 2. Prompts user to confirm: ``` To delete project "my-project", type the name exactly: my-project > _ ``` 3. If input matches → removes project and all versions from server 4. If input doesn't match → abort with error #### Read project versions ```bash aevs project read --id aevs project read --name ``` **Output:** ``` timestamp | version_name | creator | branch | state 1706000000000000000 | v1.0 | root | main | active - ./envs/.env.prod - ./envs/.env.dev 1705900000000000000 | initial | root | feature/xyz | inactive - ./envs/.env.prod ``` --- ### `aevs sync` Synchronizes local environment files with server. ```bash aevs sync [-c, --config ] ``` **Flags:** - `-c, --config ` — path to config (default: `./aevs.yaml`) **Flow:** 1. Parse config via `parseConfig` 2. If `project` is empty: - Generate UUID on client (`uuid.New()`) - Prompt for project name - Create project on server via `POST /projects` with generated ID - Save `project` ID to config 3. Read local env files (paths from config only, no scanning) 4. Request version data from server: - If config version is inactive → server returns latest **active** version 5. Build diff map between local and remote 6. Interactive conflict resolution: ``` ENVIRONMENT: .env.prod VARIABLE: DATABASE_URL [1] local: postgres://localhost:5432/dev [2] remote: postgres://prod.db:5432/prod Select (1/2): ``` 7. New variables from remote are added without prompts 8. **Always create new version** (never update existing) 9. Prompt for new version name 10. Update local files and push new version to server 11. Update `version` in config **Edge case — no changes:** - If local = remote → message: "Already up to date" (no new version created) **Edge case — no active version exists:** - If local files exist → create new version (prompt for name) - If no local files → error: "No active versions and no local env files found" --- ### `aevs merge` Merges multiple versions into one. ```bash aevs merge -v, --version "ts1 ts2 ts3" [-n, --name "merged"] [-c, --config ] [-p, --project ] ``` **Flags:** - `-v, --version ` — **required**, space-separated list of version timestamps - `-n, --name ` — new version name (default: current timestamp) - `-c, --config ` — config path to read `project_id` - `-p, --project ` — project ID directly **Note:** At least one of `-c` or `-p` must be provided. **Flow:** 1. Fetch all specified versions from server 2. Interactive conflict resolution (same UX as sync) 3. Prompt for branch name (default: current git branch via `git symbolic-ref --short HEAD`) 4. Create new active version 5. Mark all source versions as `inactive` --- ## Functions ### `parseConfig` Reads `aevs.yaml` configuration file. **Input:** File path (or `DEFAULT_CONFIG`) **Returns:** - `project_id` — uuid.UUID (empty for new projects, set on first sync) - `version_id` — int64 (unix nanoseconds) - `api_key` — string - `api_url` — string (server base URL) - `environments` — []string (list of file paths) ### `parseLocals` Scans directory tree for env files. **Only used during `aevs init`.** **Patterns (include):** - `.env` — exact match - `*.env.*` — e.g., `.env.prod`, `config.env.local` - `.env.example`, `.env.sample` — included (not ignored) **Patterns (ignore directories):** - `node_modules/` - `.git/` - `vendor/` **Returns:** `map[string]map[string]string` - Key: relative file path (e.g., `./docker/.env.prod`) - Value: parsed KEY=VALUE pairs **Note:** After config exists, only paths listed in config are used. Re-running `init` merges new files. --- ## Configuration **File:** `aevs.yaml` (or path specified via `-c, --config`) ```yaml api_url: "https://api.aevs.io" api_key: "your-api-key-here" project: "11111111-1111-1111-1111-0123456789ab" # empty until first sync version: 1706000000000000000 # unix nanoseconds environments: - "./envs/.env.prod" - "./envs/.env.dev" - "./.env.local" ``` **Rules:** - Single config file per project (no inheritance) - Path is either explicit (`-c` flag) or current directory - `project` may be empty initially (created on first sync) --- ## Types ```go package types import "github.com/google/uuid" type Project struct { ID uuid.UUID `json:"id"` Name string `json:"name"` } type State int const ( Active State = 1 Inactive State = -1 ) type Version struct { Ts int64 `json:"ts"` // unix nanoseconds (time.Now().UnixNano()) Name string `json:"name"` Creator string `json:"creator"` // TODO: from api_key user, hardcode "root" for now Branch string `json:"branch"` State State `json:"state"` Envs []Env `json:"envs"` } type Env struct { Path string `json:"path"` // relative file path = identifier (e.g., "./.env.prod") Vars map[string]string `json:"vars"` // KEY=VALUE pairs (all strings, as in .env files) } ``` **Note on Env.Vars:** All values are stored as strings because `.env` files only support string values. Type interpretation (number, bool, JSON) is handled by the application reading the env file. --- ## Server API Interface **Base URL:** Configurable via `api_url` in config **Authentication:** `Authorization: Bearer {api_key}` **Content-Type:** `application/json` ### Endpoints | Method | Path | Description | Request | Response | |--------|------|-------------|---------|----------| | GET | `/projects` | List projects (only accessible to api_key) | — | `[]Project` | | POST | `/projects` | Create project | `{id, name}` | `Project` | | PATCH | `/projects/{id}` | Update project name | `{name}` | `Project` | | DELETE | `/projects/{id}` | Delete project | — | `204` | | GET | `/projects/{id}/versions` | List versions | — | `[]Version` | | POST | `/projects/{id}/versions` | Create version | `Version` | `Version` | | GET | `/projects/{id}/versions/{ts}` | Get version with envs | — | `Version` | | PATCH | `/projects/{id}/versions/{ts}` | Update version state | `{state}` | `Version` | ### Error Response Format ```json { "error": "Human-readable error message", "code": "ERROR_CODE" } ``` **Error codes:** - `PROJECT_NOT_FOUND` - `PROJECT_ALREADY_EXISTS` — when POST /projects with existing ID - `VERSION_NOT_FOUND` - `UNAUTHORIZED` - `VALIDATION_ERROR` - `INTERNAL_ERROR` ### Mock Server For development and testing, implement mock server that: - Stores data in memory - Implements all endpoints above - Validates api_key (hardcoded keys for MVP) - **Authorization isolation:** Pre-populate with test data for different api_keys to verify users can't see other users' projects **Test setup:** ```go // Mock data for testing authorization mockData := map[string][]Project{ "api-key-user-1": { {ID: uuid.MustParse("..."), Name: "user1-project"}, }, "api-key-user-2": { {ID: uuid.MustParse("..."), Name: "user2-project"}, }, } // GET /projects with "api-key-user-1" must NOT return "user2-project" ``` --- ## Error Handling | Scenario | Behavior | |----------|----------| | No config found | Error: "Config not found. Run `aevs init` first." | | Network failure | Error with message, preserve local state | | Invalid api_key | Error: "Authentication failed" | | Project not found | Error: "Project {id} not found" | | Version not found | Error: "Version {ts} not found" | --- ## Git Integration - **Branch tracking:** Before sync/merge, run `git symbolic-ref --short HEAD` - **Purpose:** Visual indicator of which git branches used this version - **Merge behavior:** Current branch overwrites version branch (with optional custom input) --- ## Concurrency - **Concurrent sync:** Multiple users syncing simultaneously will each create their own new versions - **No locking:** No optimistic/pessimistic locking mechanism - **Resolution:** Conflicts between versions are resolved via `aevs merge` --- ## Constants ```go const ( DefaultConfig = "aevs.yaml" DefaultAPIURL = "https://api.aevs.io" DefaultCreator = "root" // TODO: get from api_key user info ) ```