# AEVS CLI — Implementation Plan > Пошаговый план для агента-разработчика. ## Общая информация **Проект:** AEVS CLI — инструмент синхронизации `.env` файлов между машинами одного пользователя. **Технологии:** - Go (golang) - CLI: `github.com/spf13/cobra` - S3: `github.com/aws/aws-sdk-go-v2` - YAML: `gopkg.in/yaml.v3` **Документация:** `.docs/02-opus-cli-docs/` --- ## Фаза 1: Инициализация проекта ### Шаг 1.1: Создание структуры проекта Создай следующую структуру директорий: ``` aevs/ ├── cmd/ │ └── aevs/ │ └── main.go # точка входа ├── internal/ │ ├── cli/ │ │ ├── root.go # root command │ │ ├── config.go # aevs config │ │ ├── init.go # aevs init │ │ ├── push.go # aevs push │ │ ├── pull.go # aevs pull │ │ ├── list.go # aevs list │ │ └── status.go # aevs status │ ├── config/ │ │ ├── global.go # GlobalConfig, загрузка/сохранение │ │ ├── project.go # ProjectConfig │ │ └── constants.go # константы │ ├── storage/ │ │ ├── storage.go # Storage interface │ │ └── s3.go # S3 implementation │ ├── archiver/ │ │ └── archiver.go # tar.gz создание/распаковка │ ├── scanner/ │ │ └── scanner.go # сканирование .env файлов │ └── types/ │ └── types.go # Metadata, FileStatus, etc. ├── go.mod ├── go.sum └── README.md ``` **Чеклист:** - [x] Создана директория `cmd/aevs/` - [x] Создана директория `internal/cli/` - [x] Создана директория `internal/config/` - [x] Создана директория `internal/storage/` - [x] Создана директория `internal/archiver/` - [x] Создана директория `internal/scanner/` - [x] Создана директория `internal/types/` - [x] Проверен другой моделью и составлен следующий шаг ### Шаг 1.2: Инициализация Go модуля ```bash go mod init github.com/user/aevs ``` **Чеклист:** - [x] Выполнена команда `go mod init` - [x] Создан файл `go.mod` - [x] Проверен другой моделью и составлен следующий шаг ### Шаг 1.3: Добавление зависимостей ```bash go get github.com/spf13/cobra@latest go get github.com/aws/aws-sdk-go-v2/config@latest go get github.com/aws/aws-sdk-go-v2/service/s3@latest go get github.com/aws/aws-sdk-go-v2/credentials@latest go get gopkg.in/yaml.v3@latest ``` **Чеклист:** - [x] Добавлена зависимость `github.com/spf13/cobra` - [x] Добавлена зависимость `github.com/aws/aws-sdk-go-v2/config` - [x] Добавлена зависимость `github.com/aws/aws-sdk-go-v2/service/s3` - [x] Добавлена зависимость `github.com/aws/aws-sdk-go-v2/credentials` - [x] Добавлена зависимость `gopkg.in/yaml.v3` - [x] Создан файл `go.sum` - [x] Проверен другой моделью и составлен следующий шаг --- ## Фаза 2: Types & Config ### Шаг 2.1: Создай `internal/types/types.go` Реализуй типы из документации (см. `.docs/02-opus-cli-docs/04-types.md`): - `Metadata` — метаданные в storage - `ProjectInfo` — для list - `FileStatus` — для status - `SyncStatus` — константы статусов **Чеклист:** - [x] Создан файл `internal/types/types.go` - [x] Реализован тип `Metadata` - [x] Реализован тип `ProjectInfo` - [x] Реализован тип `FileStatus` - [x] Реализован тип `SyncStatus` с константами - [x] Код компилируется без ошибок - [x] Проверен другой моделью и составлен следующий шаг ### Шаг 2.2: Создай `internal/config/constants.go` Константы из документации: - `DefaultConfigDir = ".config/aevs"` - `DefaultGlobalConfigFile = "config.yaml"` - `DefaultProjectConfigFile = "aevs.yaml"` - `ArchiveFileName = "envs.tar.gz"` - `MetadataFileName = "metadata.json"` - и т.д. **Чеклист:** - [x] Создан файл `internal/config/constants.go` - [x] Определена константа `DefaultConfigDir` - [x] Определена константа `DefaultGlobalConfigFile` - [x] Определена константа `DefaultProjectConfigFile` - [x] Определена константа `ArchiveFileName` - [x] Определена константа `MetadataFileName` - [x] Определены дефолтные значения для S3 - [x] Проверен другой моделью и составлен следующий шаг ### Шаг 2.3: Создай `internal/config/global.go` ```go type GlobalConfig struct { Storage StorageConfig `yaml:"storage"` } type StorageConfig struct { Type string `yaml:"type"` Endpoint string `yaml:"endpoint"` Region string `yaml:"region"` Bucket string `yaml:"bucket"` AccessKey string `yaml:"access_key"` SecretKey string `yaml:"secret_key"` } ``` Функции: - `LoadGlobalConfig() (*GlobalConfig, error)` — загрузка из `~/.config/aevs/config.yaml` - `SaveGlobalConfig(cfg *GlobalConfig) error` — сохранение с правами `0600` - `GlobalConfigPath() string` — путь к конфигу - `GlobalConfigExists() bool` — проверка существования **Чеклист:** - [x] Создан файл `internal/config/global.go` - [x] Реализован тип `GlobalConfig` - [x] Реализован тип `StorageConfig` - [x] Реализована функция `LoadGlobalConfig()` - [x] Реализована функция `SaveGlobalConfig()` с правами `0600` - [x] Реализована функция `GlobalConfigPath()` - [x] Реализована функция `GlobalConfigExists()` - [x] Код компилируется без ошибок - [x] Проверен другой моделью и составлен следующий шаг ### Шаг 2.4: Создай `internal/config/project.go` ```go type ProjectConfig struct { Project string `yaml:"project"` Files []string `yaml:"files"` } ``` Функции: - `LoadProjectConfig(path string) (*ProjectConfig, error)` - `SaveProjectConfig(path string, cfg *ProjectConfig) error` - `ValidateProjectName(name string) error` — проверка `[a-z0-9_-]` **Чеклист:** - [x] Создан файл `internal/config/project.go` - [x] Реализован тип `ProjectConfig` - [x] Реализована функция `LoadProjectConfig()` - [x] Реализована функция `SaveProjectConfig()` - [x] Реализована функция `ValidateProjectName()` с regex `[a-z0-9_-]` - [x] Код компилируется без ошибок - [x] Проверен другой моделью и составлен следующий шаг --- ## Фаза 3: Storage Layer ### Шаг 3.1: Создай `internal/storage/storage.go` Интерфейс Storage: ```go type Storage interface { Upload(ctx context.Context, key string, data io.Reader, size int64) error Download(ctx context.Context, key string) (io.ReadCloser, error) Delete(ctx context.Context, key string) error Exists(ctx context.Context, key string) (bool, error) List(ctx context.Context, prefix string) ([]string, error) ListProjects(ctx context.Context) ([]string, error) TestConnection(ctx context.Context) error } ``` **Чеклист:** - [x] Создан файл `internal/storage/storage.go` - [x] Определён интерфейс `Storage` - [x] Метод `Upload` в интерфейсе - [x] Метод `Download` в интерфейсе - [x] Метод `Delete` в интерфейсе - [x] Метод `Exists` в интерфейсе - [x] Метод `List` в интерфейсе - [x] Метод `ListProjects` в интерфейсе - [x] Метод `TestConnection` в интерфейсе - [x] Проверен другой моделью и составлен следующий шаг ### Шаг 3.2: Создай `internal/storage/s3.go` S3 реализация интерфейса Storage: ```go type S3Storage struct { client *s3.Client bucket string } func NewS3Storage(cfg *config.StorageConfig) (*S3Storage, error) ``` Методы: - `Upload` — `s3.PutObject` - `Download` — `s3.GetObject` - `Delete` — `s3.DeleteObject` - `Exists` — `s3.HeadObject` - `List` — `s3.ListObjectsV2` - `ListProjects` — list с delimiter `/` - `TestConnection` — `s3.ListBuckets` или `HeadBucket` **Важно:** Используй `aws.Config` с кастомным endpoint для поддержки MinIO, R2, Spaces. **Чеклист:** - [x] Создан файл `internal/storage/s3.go` - [x] Реализована структура `S3Storage` - [x] Реализована функция `NewS3Storage()` с кастомным endpoint - [x] Реализован метод `Upload()` - [x] Реализован метод `Download()` - [x] Реализован метод `Delete()` - [x] Реализован метод `Exists()` - [x] Реализован метод `List()` - [x] Реализован метод `ListProjects()` с delimiter - [x] Реализован метод `TestConnection()` - [x] S3Storage реализует интерфейс Storage - [x] Код компилируется без ошибок - [x] Проверен другой моделью и составлен следующий шаг --- ## Фаза 4: Scanner & Archiver ### Шаг 4.1: Создай `internal/scanner/scanner.go` Сканирование директории на `.env` файлы: ```go func Scan(rootDir string) ([]string, error) ``` Паттерны для включения: - `.env` - `.env.*` - `*.env` Исключения (директории): - `node_modules`, `.git`, `vendor`, `venv`, `.venv`, `__pycache__`, `.idea`, `.vscode`, `dist`, `build` Исключения (файлы): - `.env.example`, `.env.sample`, `.env.template` **Чеклист:** - [x] Создан файл `internal/scanner/scanner.go` - [x] Определены паттерны включения - [x] Определены исключаемые директории - [x] Определены исключаемые файлы - [x] Реализована функция `Scan()` - [x] Функция рекурсивно обходит директории - [x] Функция корректно фильтрует файлы - [x] Код компилируется без ошибок - [x] Проверен другой моделью и составлен следующий шаг ### Шаг 4.2: Создай `internal/archiver/archiver.go` ```go func Create(files []string, rootDir string) (io.Reader, int64, error) func Extract(archive io.Reader, destDir string) ([]string, error) func List(archive io.Reader) ([]string, error) ``` - `Create` — создаёт tar.gz архив из списка файлов - `Extract` — распаковывает архив в директорию, создаёт недостающие папки - `List` — возвращает список файлов в архиве без распаковки **Чеклист:** - [x] Создан файл `internal/archiver/archiver.go` - [x] Реализована функция `Create()` (tar.gz) - [x] Реализована функция `Extract()` с созданием директорий - [x] Реализована функция `List()` - [x] Пути в архиве сохраняются относительно rootDir - [x] Код компилируется без ошибок - [x] Проверен другой моделью и составлен следующий шаг --- ## Фаза 5: CLI Commands ### Шаг 5.1: Создай `internal/cli/root.go` Root command с cobra: ```go var rootCmd = &cobra.Command{ Use: "aevs", Short: "Sync .env files between machines", } func Execute() error { return rootCmd.Execute() } ``` Глобальные флаги: - `--verbose`, `-v` — verbose output - `--debug` — debug output **Чеклист:** - [x] Создан файл `internal/cli/root.go` - [x] Определён `rootCmd` с cobra - [x] Реализована функция `Execute()` - [x] Добавлен флаг `--verbose` / `-v` - [x] Добавлен флаг `--debug` - [x] Код компилируется без ошибок - [x] Проверен другой моделью и составлен следующий шаг ### Шаг 5.2: Создай `internal/cli/config.go` — команда `aevs config` Интерактивная настройка credentials: 1. Запросить storage type (default: `s3`) 2. Запросить endpoint (default: `https://s3.amazonaws.com`) 3. Запросить region (default: `us-east-1`) 4. Запросить bucket name 5. Запросить access key 6. Запросить secret key (скрытый ввод) 7. Проверить подключение (`TestConnection`) 8. Сохранить конфиг **Чеклист:** - [x] Создан файл `internal/cli/config.go` - [x] Определён `configCmd` с cobra - [x] Реализован интерактивный ввод storage type - [x] Реализован интерактивный ввод endpoint - [x] Реализован интерактивный ввод region - [x] Реализован интерактивный ввод bucket name - [x] Реализован интерактивный ввод access key - [x] Реализован скрытый ввод secret key - [x] Вызывается `TestConnection()` для проверки - [x] Конфиг сохраняется через `SaveGlobalConfig()` - [x] Команда зарегистрирована в `rootCmd` - [x] Код компилируется без ошибок - [x] Проверен другой моделью и составлен следующий шаг ### Шаг 5.3: Создай `internal/cli/init.go` — команда `aevs init` ```bash aevs init [project-name] ``` Флаги: - `--force`, `-f` — перезаписать существующий конфиг Логика: 1. Проверить существование `aevs.yaml` 2. Если существует и нет `--force`: - Сканировать на новые файлы - Предложить добавить 3. Если не существует: - Сканировать директорию - Определить имя проекта (аргумент или имя папки) - Создать `aevs.yaml` **Чеклист:** - [x] Создан файл `internal/cli/init.go` - [x] Определён `initCmd` с cobra - [x] Добавлен флаг `--force` / `-f` - [x] Реализована проверка существования `aevs.yaml` - [x] Реализовано сканирование директории через `scanner.Scan()` - [x] Реализовано определение имени проекта (аргумент или имя папки) - [x] Реализовано добавление новых файлов в существующий конфиг - [x] Создаётся `aevs.yaml` через `SaveProjectConfig()` - [x] Команда зарегистрирована в `rootCmd` - [x] Код компилируется без ошибок - [x] Проверен другой моделью и составлен следующий шаг ### Шаг 5.4: Создай `internal/cli/push.go` — команда `aevs push` Флаги: - `--config`, `-c` — путь к конфигу - `--dry-run` — показать что будет загружено Логика: 1. Загрузить глобальный конфиг 2. Загрузить проектный конфиг 3. Проверить существование всех файлов 4. Создать tar.gz архив 5. Загрузить архив в `{project}/envs.tar.gz` 6. Создать и загрузить metadata.json 7. Вывести результат **Чеклист:** - [x] Создан файл `internal/cli/push.go` - [x] Определён `pushCmd` с cobra - [x] Добавлен флаг `--config` / `-c` - [x] Добавлен флаг `--dry-run` - [x] Реализована загрузка глобального конфига - [x] Реализована загрузка проектного конфига - [x] Реализована проверка существования файлов - [x] Реализовано создание архива через `archiver.Create()` - [x] Реализована загрузка архива через `storage.Upload()` - [x] Реализовано создание и загрузка `metadata.json` - [x] Реализован режим `--dry-run` - [x] Команда зарегистрирована в `rootCmd` - [x] Код компилируется без ошибок - [x] Проверен другой моделью и составлен следующий шаг ### Шаг 5.5: Создай `internal/cli/pull.go` — команда `aevs pull` ```bash aevs pull [project-name] ``` Флаги: - `--config`, `-c` - `--force`, `-f` — перезаписать без подтверждения - `--dry-run` Логика: 1. Определить имя проекта (аргумент или из `aevs.yaml`) 2. Скачать metadata.json 3. Скачать envs.tar.gz 4. Для каждого файла: - Если не существует — создать - Если существует и отличается — спросить (или --force) 5. Вывести результат Обработка конфликтов: - `[o]verwrite` — перезаписать - `[s]kip` — пропустить - `[d]iff` — показать diff - `[O]verwrite all` - `[S]kip all` **Чеклист:** - [x] Создан файл `internal/cli/pull.go` - [x] Определён `pullCmd` с cobra - [x] Добавлен флаг `--config` / `-c` - [x] Добавлен флаг `--force` / `-f` - [x] Добавлен флаг `--dry-run` - [x] Реализовано определение имени проекта - [x] Реализовано скачивание metadata.json - [x] Реализовано скачивание envs.tar.gz - [x] Реализована распаковка через `archiver.Extract()` - [x] Реализована обработка конфликтов (o/s/d/O/S) - [x] Реализован режим `--force` - [x] Реализован режим `--dry-run` - [x] Команда зарегистрирована в `rootCmd` - [x] Код компилируется без ошибок - [x] Проверен другой моделью и составлен следующий шаг ### Шаг 5.6: Создай `internal/cli/list.go` — команда `aevs list` Флаги: - `--json` — вывод в JSON Логика: 1. Загрузить глобальный конфиг 2. Получить список проектов (`ListProjects`) 3. Для каждого проекта загрузить metadata.json 4. Вывести таблицу или JSON **Чеклист:** - [x] Создан файл `internal/cli/list.go` - [x] Определён `listCmd` с cobra - [x] Добавлен флаг `--json` - [x] Реализована загрузка глобального конфига - [x] Реализовано получение списка проектов через `ListProjects()` - [x] Реализована загрузка metadata.json для каждого проекта - [x] Реализован вывод в виде таблицы - [x] Реализован вывод в формате JSON - [x] Команда зарегистрирована в `rootCmd` - [x] Код компилируется без ошибок - [x] Проверен другой моделью и составлен следующий шаг ### Шаг 5.7: Создай `internal/cli/status.go` — команда `aevs status` Флаги: - `--config`, `-c` Логика: 1. Загрузить оба конфига 2. Скачать metadata.json из storage 3. Для каждого файла: - Сравнить размер и хеш локального vs remote - Определить статус 4. Вывести таблицу статусов **Чеклист:** - [x] Создан файл `internal/cli/status.go` - [x] Определён `statusCmd` с cobra - [x] Добавлен флаг `--config` / `-c` - [x] Реализована загрузка обоих конфигов - [x] Реализовано скачивание metadata.json - [x] Реализовано сравнение локальных и remote файлов - [x] Реализовано определение статуса каждого файла - [x] Реализован вывод таблицы статусов - [x] Команда зарегистрирована в `rootCmd` - [x] Код компилируется без ошибок - [x] Проверен другой моделью и составлен следующий шаг --- ## Фаза 6: Error Handling ### Шаг 6.1: Создай `internal/errors/errors.go` Определённые ошибки из документации: ```go var ( ErrNoGlobalConfig = errors.New("no storage configured; run 'aevs config' first") ErrNoProjectConfig = errors.New("no aevs.yaml found; run 'aevs init' first") ErrConfigExists = errors.New("aevs.yaml already exists; use --force to overwrite") ErrInvalidProject = errors.New("invalid project name; use only a-z, 0-9, -, _") ErrProjectNotFound = errors.New("project not found in storage") ErrAccessDenied = errors.New("access denied; check your credentials") ErrBucketNotFound = errors.New("bucket not found") ErrFileNotFound = errors.New("file not found") ErrNoEnvFiles = errors.New("no env files found") ) ``` **Чеклист:** - [x] Создан файл `internal/errors/errors.go` - [x] Определена ошибка `ErrNoGlobalConfig` - [x] Определена ошибка `ErrNoProjectConfig` - [x] Определена ошибка `ErrConfigExists` - [x] Определена ошибка `ErrInvalidProject` - [x] Определена ошибка `ErrProjectNotFound` - [x] Определена ошибка `ErrAccessDenied` - [x] Определена ошибка `ErrBucketNotFound` - [x] Определена ошибка `ErrFileNotFound` - [x] Определена ошибка `ErrNoEnvFiles` - [x] Код компилируется без ошибок - [x] Проверен другой моделью и составлен следующий шаг ### Шаг 6.2: Exit codes Реализуй exit codes: - `0` — Success - `1` — General error - `2` — Configuration error - `3` — File system error - `4` — Storage error - `5` — Network error - `130` — Interrupted **Чеклист:** - [x] Определены константы exit codes - [x] Реализована функция для определения exit code по типу ошибки - [x] Exit codes используются в `main.go` - [x] Проверен другой моделью и составлен следующий шаг --- ## Фаза 7: Entry Point ### Шаг 7.1: Создай `cmd/aevs/main.go` ```go package main import ( "os" "aevs/internal/cli" ) func main() { if err := cli.Execute(); err != nil { os.Exit(1) } } ``` **Чеклист:** - [x] Создан файл `cmd/aevs/main.go` - [x] Импортирован пакет `cli` - [x] Вызывается `cli.Execute()` - [x] Используются корректные exit codes - [x] Приложение компилируется: `go build ./cmd/aevs` - [x] Приложение запускается: `./aevs --help` - [x] Проверен другой моделью и составлен следующий шаг --- ## Фаза 8: Тестирование ### Шаг 8.1: Unit tests Создай тесты для: - `config/` — загрузка/сохранение конфигов - `scanner/` — сканирование файлов - `archiver/` — создание/распаковка архивов - `storage/` — mock тесты для S3 **Чеклист:** - [ ] Создан файл `internal/config/global_test.go` - [ ] Создан файл `internal/config/project_test.go` - [ ] Создан файл `internal/scanner/scanner_test.go` - [ ] Создан файл `internal/archiver/archiver_test.go` - [ ] Создан файл `internal/storage/s3_test.go` (mock) - [ ] Все тесты проходят: `go test ./...` - [ ] Проверен другой моделью и составлен следующий шаг ### Шаг 8.2: Integration tests Тесты с реальным S3 (или MinIO в Docker): - Полный цикл: config → init → push → pull → status → list **Чеклист:** - [ ] Настроен MinIO в Docker для тестов - [ ] Создан файл интеграционных тестов - [ ] Тест полного цикла работает - [ ] Тесты можно запустить локально - [ ] Проверен другой моделью и составлен следующий шаг --- ## Фаза 9: Build & Release ### Шаг 9.1: Makefile ```makefile .PHONY: build test clean build: go build -o bin/aevs ./cmd/aevs test: go test ./... clean: rm -rf bin/ ``` **Чеклист:** - [x] Создан файл `Makefile` - [x] Добавлена цель `build` - [x] Добавлена цель `test` - [x] Добавлена цель `clean` - [x] `make build` работает - [x] `make test` работает - [x] Проверен другой моделью и составлен следующий шаг ### Шаг 9.2: README.md Создай README с: - Описанием проекта - Установкой - Quick start - Командами **Чеклист:** - [x] Создан файл `README.md` - [x] Добавлено описание проекта - [x] Добавлены инструкции по установке - [x] Добавлен раздел Quick Start - [x] Добавлено описание всех команд - [x] Добавлены примеры использования - [x] Проверен другой моделью и составлен следующий шаг --- ## Порядок реализации (приоритет) 1. **Фаза 1** — структура проекта 2. **Фаза 2** — types & config 3. **Фаза 4** — scanner & archiver (можно тестировать локально) 4. **Фаза 3** — storage layer 5. **Фаза 5** — CLI commands в порядке: - `root.go` - `config.go` (нужен для всего остального) - `init.go` - `push.go` - `pull.go` - `list.go` - `status.go` 6. **Фаза 6** — error handling (можно делать параллельно) 7. **Фаза 7** — entry point 8. **Фаза 8** — тесты 9. **Фаза 9** — build & docs --- ## Ссылки на документацию - [01-overview.md](./../02-opus-cli-docs/01-overview.md) — обзор и архитектура - [02-configuration.md](./../02-opus-cli-docs/02-configuration.md) — конфигурация - [03-commands.md](./../02-opus-cli-docs/03-commands.md) — детали команд - [04-types.md](./../02-opus-cli-docs/04-types.md) — Go типы - [05-scenarios.md](./../02-opus-cli-docs/05-scenarios.md) — сценарии использования - [06-errors.md](./../02-opus-cli-docs/06-errors.md) — обработка ошибок