aenvs/.docs/01-opus-docs/01-sprint.md

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):

  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

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 read project_id
  • -i, --id <uuid> — project ID directly (without config)

Delete project

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

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:

  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.

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)

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

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_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:

// 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
)