aenvs/.docs/03-opus-plans/implementation-plan.md

731 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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) — обработка ошибок