361 lines
9.4 KiB
Markdown
361 lines
9.4 KiB
Markdown
# 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 <path>` — config path to read `project_id`
|
|
- `-i, --id <uuid>` — project ID directly (without config)
|
|
|
|
#### Delete project
|
|
```bash
|
|
aevs project remove -r, --id <uuid>
|
|
```
|
|
|
|
**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 <uuid>
|
|
aevs project read --name <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 <path>]
|
|
```
|
|
|
|
**Flags:**
|
|
- `-c, --config <path>` — 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 <path>] [-p, --project <uuid>]
|
|
```
|
|
|
|
**Flags:**
|
|
- `-v, --version <timestamps>` — **required**, space-separated list of version timestamps
|
|
- `-n, --name <name>` — new version name (default: current timestamp)
|
|
- `-c, --config <path>` — config path to read `project_id`
|
|
- `-p, --project <uuid>` — 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
|
|
)
|
|
```
|