9.4 KiB
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.
aevs init
Behavior (new config):
- Interactive prompt for
api_url - Interactive prompt for
api_key - Runs
parseLocalsto scan for env files - Creates
aevs.yamlconfig with found environments projectfield left empty (will be set on first sync)
Behavior (config exists):
- Runs
parseLocalsto scan for new env files - Merges newly found files into existing config
- Displays list of added files
aevs project
Project management commands.
List projects
aevs project -l, --list
Output:
uuid | name
11111111-1111-1111-1111-0123456789ab | my-project
22222222-2222-2222-2222-0123456789ab | another-project
Update project name
aevs project save -n, --name "new_name"
Optional flags:
-c, --config <path>— config path to readproject_id-i, --id <uuid>— project ID directly (without config)
Delete project
aevs project remove -r, --id <uuid>
Behavior:
- Fetches project name from server
- Prompts user to confirm:
To delete project "my-project", type the name exactly: my-project > _ - If input matches → removes project and all versions from server
- If input doesn't match → abort with error
Read project versions
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.
aevs sync [-c, --config <path>]
Flags:
-c, --config <path>— path to config (default:./aevs.yaml)
Flow:
- Parse config via
parseConfig - If
projectis empty:- Generate UUID on client (
uuid.New()) - Prompt for project name
- Create project on server via
POST /projectswith generated ID - Save
projectID to config
- Generate UUID on client (
- Read local env files (paths from config only, no scanning)
- Request version data from server:
- If config version is inactive → server returns latest active version
- Build diff map between local and remote
- 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): - New variables from remote are added without prompts
- Always create new version (never update existing)
- Prompt for new version name
- Update local files and push new version to server
- Update
versionin 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.
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 readproject_id-p, --project <uuid>— project ID directly
Note: At least one of -c or -p must be provided.
Flow:
- Fetch all specified versions from server
- Interactive conflict resolution (same UX as sync)
- Prompt for branch name (default: current git branch via
git symbolic-ref --short HEAD) - Create new active version
- 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— stringapi_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)
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 (
-cflag) or current directory projectmay be empty initially (created on first sync)
Types
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
{
"error": "Human-readable error message",
"code": "ERROR_CODE"
}
Error codes:
PROJECT_NOT_FOUNDPROJECT_ALREADY_EXISTS— when POST /projects with existing IDVERSION_NOT_FOUNDUNAUTHORIZEDVALIDATION_ERRORINTERNAL_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:
// 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
const (
DefaultConfig = "aevs.yaml"
DefaultAPIURL = "https://api.aevs.io"
DefaultCreator = "root" // TODO: get from api_key user info
)