Рыба проекта. Минимальная функциональность

This commit is contained in:
2026-08-03 22:22:24 +03:00
commit 8c8631ac9c
80 changed files with 10618 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
# Chore Skills
> **Русская версия:** [README.ru.md](README.ru.md)
Infrastructure and utility skills applicable across projects.
## Available Skills
| Skill | Description |
|-------|-------------|
| [dockerfile-skill](dockerfile-skill) | Best practices for writing production-ready Dockerfiles: multistage builds, BuildKit parallelism, security hardening (non-root user, pinned base images, no secrets in layers), healthchecks, custom entrypoints, `.dockerignore`, and integration with Makefile / Docker Compose. Includes a self-check checklist. |
+12
View File
@@ -0,0 +1,12 @@
# Chore Skills
> **English version:** [README.md](README.md)
> **Вернуться к оглавлению:** [README.ru.md](../../README.ru.md)
Инфраструктурные и утилитарные скиллы, применимые во всех проектах.
## Доступные скиллы
| Скилл | Описание |
|-------|----------|
| [dockerfile-skill](dockerfile-skill) | Лучшие практики написания production-ready Dockerfile: многоступенчатая сборка, параллельные стейджи BuildKit, харднинг безопасности (непривилегированный пользователь, фиксированные версии образов, отсутствие секретов в слоях), healthchecks, кастомные entrypoint-скрипты, `.dockerignore` и интеграция с Makefile / Docker Compose. Включает чеклист самопроверки. |
@@ -0,0 +1,289 @@
---
name: dockerfile-skill
description: Ты ОБЯЗАН использовать этот скилл, если хочешь писать Dockerfile
---
## Multistage
Используй Docker multistage, если тебе надо создать Dockerfile для сборки и запуска приложения – раздели его на стейдж со сборкой и запуском.
Не стесняйся использовать `COPY FROM`.
Объединяй несколько команд `RUN` в одну строку с помощью `&&` и очищай временные файлы внутри того же слоя (например, `rm -rf /var/lib/apt/lists/*`). Это уменьшает число слоёв и размер финального образа.
Для зависимостей, которые часто пересобираются (pip, npm, apt), используй `--mount=type=cache,target=/root/.cache/pip` (или аналогично) – это ускоряет повторные сборки за счёт кэширования на хосте.
## Parallel
Разделяй стейджи Dockerfile таким образом, чтобы они могли выполняться параллельно (например, сборка зависимостей и подготовка тестовых данных).
При использовании BuildKit можно объявить несколько независимых `FROM` и копировать между ними через `COPY --from=...`. Убедись, что стейджи не имеют неявных зависимостей друг от друга.
## Makefile
Если в проекте есть Makefile, постарайся использовать его.
Если Makefile нет, сделай его и вызывай команды через таргеты, а не напрямую в Dockerfile.
Также добавь в этот Makefile таргеты для сборки и запуска текущего приложения в Docker.
## Compose
Если его еще нет, сделай возможность запустить приложение, для которого пишешь Dockerfile, в Docker Compose.
## Security
Используй лучшие практики безопасности при написании Dockerfile.
* **Фиксируй версии базовых образов** – всегда используй конкретный тег (например, `python:3.11-slim-bookworm`) вместо `:latest` или плавающих тегов, чтобы обеспечить воспроизводимость сборки.
* **Создавай выделенного непривилегированного пользователя** внутри образа: `RUN addgroup -S app && adduser -S -G app app`. Затем переключайся на него с помощью `USER app`. Копируй файлы командой `COPY --chown=app:app`, чтобы они сразу принадлежали этому пользователю, избегая лишних операций с правами.
* **Не храни секреты в переменных окружения образа** – используй `ARG` только для нечувствительных данных (например, версий пакетов), а секреты передавай через `--build-arg` в момент сборки или монтируй через Docker Secrets в рантайме.
* **Запускай контейнер rootless**, если это возможно. После создания пользователя и переключения на него финальный процесс будет работать без прав root.
* **Добавь `HEALTHCHECK`** (например, `HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost:8080/ || exit 1`), чтобы оркестраторы могли корректно определять состояние приложения.
* **Проставляй метаданные через `LABEL`** например, `LABEL maintainer="team@example.com"`, `LABEL version="1.0.0"`, `LABEL description="..."`. Это стандарт для документирования образов.
* **Указывай в Dockerfile флаги уменьшения размера зависимостей** – для `apt` используй `--no-install-recommends`, для `pip` `--no-cache-dir`, для `npm` `--only=production`, для `composer` `--no-dev`.
* **Сканируй собранный образ на уязвимости** – в CI/CD используй инструменты вроде `trivy`, `docker scout` или `grype`. Это должно быть частью пайплайна сборки.
## Dockerignore
*Создай файл `.dockerignore` в корне проекта. Включи в него все ненужные файлы и директории: `.git`, `node_modules`, `venv`, `__pycache__`, `*.md`, `docker-compose.yml`, временные файлы. Это ускорит сборку и предотвратит случайную утечку секретов.*
## Пример корректного Dockerfile (мини-шаблон)
*Для быстрого старта используй команду `docker init`, которая сгенерирует корректные Dockerfile, compose и .dockerignore под твой язык.*
Хорошо, вот интеграция примеров в раздел **Entrypoint** исходного скилла. Я добавил два конкретных скрипта (Alpine/`su-exec` и Ubuntu/`gosu`) и описал, как их встроить в Dockerfile. Также учтена exec-форма и рекомендации по сигналам.
## Entrypoint
Сделай кастомный Docker-Entrypoint, который запускает все скрипты из папки `/etc/docker-custom-init/*.sh`, сортируя их по имени.
Если нужно запустить само приложение, вызови `app`. Тогда entrypoint должен заменить вызов вызовом оригинального приложения.
**Обязательно используй exec-форму** в `ENTRYPOINT` и `CMD` запись вида `["executable", "param"]`, а не `executable param`. Только exec-форма гарантирует, что сигналы (SIGTERM, SIGINT) будут корректно переданы основному процессу и контейнер сможет правильно завершиться.
### Пример для Alpine (использует `su-exec`)
Скрипт `docker-entrypoint.sh` (универсален для Alpine, BusyBox, минимальных образов):
```sh
#!/bin/sh
set -e
INIT_DIRS="${DOCKER_INITD_DIRS:-/etc/init-custom-docker.d}"
SCRIPTS_ENVS="${DOCKER_INIT_SCRIPTS_ENVS}"
INIT_SCRIPT="${DOCKER_INIT_SCRIPT}"
log() {
echo "[init] $1"
}
# 1. Run init scripts from directories (as root)
for dir in $(echo "$INIT_DIRS" | tr ',' ' '); do
if [ -d "$dir" ]; then
log "Processing init directory: $dir"
for script in $(find "$dir" -maxdepth 1 -name '*.sh' -type f 2>/dev/null | sort); do
log "Running script: $script"
sh "$script"
done
else
log "Directory not found, skipping: $dir"
fi
done
# 2. Run scripts from environment variables (as root)
if [ -n "$SCRIPTS_ENVS" ]; then
for env_name in $(echo "$SCRIPTS_ENVS" | tr ',' ' '); do
eval "script_content=\$$env_name"
if [ -z "$script_content" ]; then
log "Error: Environment variable $env_name specified but empty"
exit 1
fi
log "Running scripts from environment: $env_name"
echo "$script_content" | sh
done
fi
# 3. Run script from DOCKER_INIT_SCRIPT (as root)
if [ -n "$INIT_SCRIPT" ]; then
log "Running script from DOCKER_INIT_SCRIPT"
echo "$INIT_SCRIPT" | sh
fi
# Switch to appuser using su-exec
log "Switching to user: appuser"
log "Starting application: $@"
exec su-exec appuser:appgroup "$@"
```
Как добавить в Dockerfile (Alpine):
```dockerfile
RUN apk add --no-cache su-exec # для Alpine
RUN addgroup -g 1000 appgroup && adduser -u 1000 -G appgroup -D appuser
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["app"]
```
### Пример для Ubuntu/Debian (использует `gosu`)
Скрипт `docker-entrypoint.sh` (аналог для Debian-based образов):
```bash
#!/bin/bash
set -e
INIT_DIRS="${DOCKER_INITD_DIRS:-/etc/init-custom-docker.d}"
SCRIPTS_ENVS="${DOCKER_INIT_SCRIPTS_ENVS}"
INIT_SCRIPT="${DOCKER_INIT_SCRIPT}"
APP_USER="${APP_USER:-appuser}"
APP_GROUP="${APP_GROUP:-appgroup}"
log() {
echo "[init] $1"
}
# 1. Execute scripts from directories (as root)
IFS=',' read -ra dirs <<< "$INIT_DIRS"
for dir in "${dirs[@]}"; do
if [ -d "$dir" ]; then
log "Processing init directory: $dir"
for script in $(find "$dir" -maxdepth 1 -name '*.sh' -type f 2>/dev/null | sort); do
log "Running script: $script"
bash "$script"
done
else
log "Directory not found, skipping: $dir"
fi
done
# 2. Execute scripts from environment variables (as root)
if [ -n "$SCRIPTS_ENVS" ]; then
IFS=',' read -ra env_names <<< "$SCRIPTS_ENVS"
for env_name in "${env_names[@]}"; do
script_content="${!env_name}"
if [ -z "$script_content" ]; then
log "Error: Environment variable $env_name specified but empty"
exit 1
fi
log "Running script from environment: $env_name"
echo "$script_content" | bash
done
fi
# 3. Execute script from DOCKER_INIT_SCRIPT (as root)
if [ -n "$INIT_SCRIPT" ]; then
log "Running script from DOCKER_INIT_SCRIPT"
echo "$INIT_SCRIPT" | bash
fi
# Switch to appuser using gosu
log "Switching to user: $APP_USER"
log "Starting application: $@"
exec gosu "${APP_USER}:${APP_GROUP}" "$@"
```
Как добавить в Dockerfile (Ubuntu):
```dockerfile
# Установка gosu (два варианта: через apt или копирование статического бинарника)
RUN apt-get update && apt-get install -y --no-install-recommends gosu && rm -rf /var/lib/apt/lists/*
# Или: COPY --from=gosu/alpine:latest /usr/local/bin/gosu /usr/local/bin/gosu
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["app"]
```
### Важные замечания
- Внутри скрипта обязательно используй **exec** в конце (например, `exec su-exec appuser "$@"` или `exec gosu appuser "$@"`), чтобы заменить процесс entrypoint на основной процесс приложения.
- Пути к init-директориям по умолчанию можно переопределить через переменные окружения (`DOCKER_INITD_DIRS`). Убедись, что нужные директории существуют (создай их в Dockerfile или смонтируй как тома).
- Чтобы уменьшить количество слоёв, можно объединить команды `RUN groupadd ... && useradd ... && apt-get install ...` в один `RUN`.
# Self-check список для валидации Dockerfile
Используй этот список для проверки своего Dockerfile перед финальной сборкой. Отмечай каждый пункт как выполненный.
## 1. Структура и многоступенчатость (Multistage)
- [ ] Есть ли разделение на этап сборки и этап запуска (если приложение требует компиляции/установки зависимостей)?
- [ ] Используется ли `COPY --from=` для переноса артефактов между стейджами?
- [ ] Объединены ли команды RUN в один слой (через `&&`) и очищен ли временный кэш (например, `rm -rf /var/lib/apt/lists/*`)?
- [ ] Используется ли `--mount=type=cache` для ускорения повторных сборок (если применимо)?
## 2. Параллелизм (BuildKit)
- [ ] Можно ли независимые стейджи выполнять параллельно? (Зависимости, тесты, документация – разделены?)
- [ ] Нет ли неявных зависимостей между параллельными стейджами?
## 3. Безопасность
- [ ] Версия базового образа фиксирована (например `python:3.11-slim-bookworm`) вместо `:latest`?
- [ ] Создан непривилегированный пользователь (`useradd -r -g appgroup appuser`)?
- [ ] Финальный процесс запускается от этого пользователя (`USER appuser`)?
- [ ] Файлы скопированы с `--chown=appuser:appgroup`?
- [ ] Используются флаги уменьшения размера: `--no-install-recommends` (apt), `--no-cache-dir` (pip), `--only=production` (npm)?
- [ ] Нет секретов в переменных `ENV` или слоях образа (используются `ARG`/секреты BuildKit)?
- [ ] Добавлен `HEALTHCHECK` для продакшен-контейнеров?
- [ ] Присутствуют метаданные `LABEL` (maintainer, version, description)?
## 4. Entrypoint
- [ ] Написан кастомный скрипт entrypoint, который запускает init-скрипты из `/etc/docker-custom-init/*.sh` (или из переменных окружения)?
- [ ] Entrypoint и CMD записаны в exec-форме (`["entrypoint.sh", "param"]`)?
- [ ] Внутри скрипта используется `exec` для передачи управления основному процессу (сигналы корректно обрабатываются)?
- [ ] Пользователь переключается на непривилегированного (через `gosu` или `su-exec`) в entrypoint?
## 5. .dockerignore
- [ ] Существует файл `.dockerignore` в корне проекта?
- [ ] В него включены: `.git`, `node_modules`, `venv`, `__pycache__`, `*.md`, `docker-compose.yml`, временные файлы?
## 6. Makefile
- [ ] Есть Makefile с таргетами для сборки, запуска, возможно `docker init`?
- [ ] В Dockerfile или инструкциях не используется прямая сборка через `docker build`, а только через `make`?
## 7. Docker Compose
- [ ] Есть файл `docker-compose.yml` (или `docker-compose.yaml`) для локального запуска?
- [ ] В compose корректно указаны порты, volumes, переменные окружения, не используется непривилегированный пользователь без необходимости?
## 8. Сканирование уязвимостей
- [ ] В CI/CD добавлен этап сканирования готового образа (trivy, docker scout, grype)?
- [ ] Уязвимости критического уровня отсутствуют или задокументированы?
## 9. Тестирование
- [ ] Dockerfile успешно собирается без ошибок?
- [ ] Контейнер запускается и отвечает на healthcheck (если задан)?
- [ ] Init-скрипты выполняются в правильном порядке?
---
### Пример быстрой проверки (через команды)
```bash
# 1. Проверка на секреты (не должно быть hardcoded паролей)
grep -n 'password\|secret\|key=' Dockerfile
# 2. Проверка exec-формы
grep -n '^ENTRYPOINT\|^CMD' Dockerfile | grep -v '^\['
# 3. Проверка наличия пользователя
grep -n '^RUN.*useradd\|^USER' Dockerfile
# 4. Проверка healthcheck
grep 'HEALTHCHECK' Dockerfile || echo "HEALTHCHECK отсутствует"
```
+11
View File
@@ -0,0 +1,11 @@
# Common Skills
> **Русская версия:** [README.ru.md](README.ru.md)
Language-agnostic skills applicable across all projects.
## Available Skills
| Skill | Description |
|-------|-------------|
| [common-daemon-cli-skill](common-daemon-cli-skill) | Use this skill whenever you need to implement a system daemon mode in your CLI application. |
+12
View File
@@ -0,0 +1,12 @@
# Common Skills
> **English version:** [README.md](README.md)
> **Вернуться к оглавлению:** [README.ru.md](../../README.ru.md)
Языко-независимые скиллы, применимые во всех проектах.
## Доступные скиллы
| Скилл | Описание |
|-------|----------|
| [common-daemon-cli-skill](common-daemon-cli-skill) | Используйте этот скилл, когда необходимо реализовать режим системного демона в CLI-приложении. |
@@ -0,0 +1,69 @@
---
name: common-daemon-cli-skill
description: Use this skill whenever you need to implement a system daemon mode in your CLI application.
---
# The "daemon" Subcommand
Every CLI daemon must have a `daemon` subcommand with the following nested subcommands:
* logs - view logs of the active daemon
* enable - enable the daemon, copy configuration files and the executable itself to system directories
* disable - disable the daemon; if executable files or config were copied to a system directory, do not touch them
* status - show the daemon's status. Is it enabled? If it crashed, what error? The last 50 lines of logs if it crashed.
* env - allows modifying the environment variables with which the daemon runs.
* restart - restarts the daemon
For example:
```shell
myrootcommand daemon [ enable / disable / logs / status / env ]
```
# Enabling the Daemon
Enabling the daemon must support the following operating systems:
* Linux-based
* \+ Systemd
* \+ OpenRC
* \+ Init.D
* FreeBSD
* Windows
* MacOS
(The method by which the application is added to autostart is displayed in `status`)
Enabling includes:
* Copying the configuration file and the application executable to a location where the user won't accidentally delete them. For example, to `/var/lib/*` on Linux. If the `--no-copy` flag is provided, copying is skipped and files remain in their original locations.
* Adding the application to autostart with the specified config (if there is a config at all)
* Storing information somewhere publicly accessible that the daemon is enabled.
# Disabling
When disabling, check whether the daemon exists or not.
If the daemon does not exist, simply inform the user.
If the daemon is in autostart, remove it from there.
# Removing Copies
Removes any copies made during enabling, if they were created.
Activated by the `--remove` flag.
In this case, a random number between 1000 and 2000 is generated, which must be entered into an interactive input field to confirm.
The CLI explicitly and clearly notifies the user about this.
# Environment Configuration (env)
Allows editing / adding / deleting the daemon's environment variables.
Subcommands:
* add - adds a new value; overwrites the old one if it exists
* remove - removes an environment variable
* append - appends to an environment variable using the path separator — for example, on Linux: `$PATH:new_value` and so on.
```shell
myrootcommand daemon env append PATH /path/to/my/external/files
myrootcommand daemon restart
```
+12
View File
@@ -0,0 +1,12 @@
# Go Skills
> **Русская версия:** [README.ru.md](README.ru.md)
Skills specific to Go (Golang) projects.
## Available Skills
| Skill | Description |
|-------|-------------|
| [golang-flexible-config-skill](golang-flexible-config-skill) | Defines how an AI agent should implement configuration loading in any Go application. |
| [golang-tswf-codestyle-skill](golang-tswf-codestyle-skill) | Tswf.io Go codestyle conventions: naming, interfaces, constructors, line breaks, and project structure. |
+13
View File
@@ -0,0 +1,13 @@
# Go Skills
> **English version:** [README.md](README.md)
> **Вернуться к оглавлению:** [README.ru.md](../../README.ru.md)
Скиллы для проектов на Go (Golang).
## Доступные скиллы
| Скилл | Описание |
|-------|----------|
| [golang-flexible-config-skill](golang-flexible-config-skill) | Определяет, как AI-агент должен реализовать загрузку конфигурации в любом Go-приложении. |
| [golang-tswf-codestyle-skill](golang-tswf-codestyle-skill) | Конвенции кодстайла Go для tswf.io: именование, интерфейсы, конструкторы, переносы строк и структура проекта. |
@@ -0,0 +1,261 @@
---
name: golang-flexible-config-skill
description: This skill defines how an AI agent should implement configuration loading in any Go application.
---
## Skill: Go Configuration Pipeline (Launch Arg → Env → Config File → Defaults)
**Description**: This skill defines how an AI agent should implement configuration loading in any Go application. The configuration must be loaded in the following order of priority (each source overrides the previous):
1. **Launch arguments** (command-line flags) **must be handled via Cobra**.
2. **Environment variables** (uppercase, dots/dashes replaced by underscores).
3. **Configuration file** (YAML/JSON/TOML).
4. **Hardcoded defaults**.
The skill enforces consistent logging of the config file location (found or not), a well-defined search path list, and a `generate-config` subcommand that produces a commented example config in either English or Russian.
All subcommands and flag definitions **must use the Cobra library** (`github.com/spf13/cobra`).
### Env Variable Rules
- All environment variables must use **UPPER_CASE**.
- Dots (`.` ) and dashes (`-`) in config property names are replaced with underscores (`_`).
- Example: property `db.path` maps to env `APP_DB_PATH`, `log-level` maps to `APP_LOG_LEVEL`.
- Use `viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))`.
### Config File Search Order
The config file is searched in the following order (first found wins). **The agent must log the exact path where the file was found.** If no file is found, log "Configuration file not found" and log the **list of all checked paths** so the user knows where to place one.
1. **Environment variable `APP_CONFIG_LOCATION`** (or custom prefix) absolute path.
2. **Working directory** `./config.yaml` (or other extensions).
3. **User config directory** `~/.config/{app-name}/config.yaml`.
4. **Binary directory** directory containing the executable.
5. **System Config default** - directory such as /etc/* on unix systems
### Subcommand: `generate-config` (Cobra based)
The application must support a subcommand `generate-config` that writes an example config file with detailed comments.
- **Flag `--lang`** (default `"en"`, possible values `"en"` and `"ru"`).
- `en`: comments in English.
- `ru`: comments in Russian.
- **Flag `--output`** (optional; if not provided, prints to stdout; if given, writes to that file).
The generated file must include every configuration parameter with a meaningful comment describing its purpose and type.
### Example Implementation (Template for AI Agent Cobra version)
Below is a complete, productionready example. The agent should use this pattern or a very similar one.
```go
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// Config structure with mapstructure tags
type Config struct {
Port int `mapstructure:"port"`
DBPath string `mapstructure:"db_path"`
LogLevel string `mapstructure:"log_level"`
}
func main() {
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "MyApp configuration loader",
Long: `Loads configuration from defaults, file, env, and CLI flags (in order of priority).`,
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := loadConfig()
if err != nil {
return err
}
_ = cfg // use cfg in your application
return nil
},
}
// Define main flags
rootCmd.Flags().Int("port", 0, "listen port (overrides env/file)")
rootCmd.Flags().String("db-path", "", "path to database")
rootCmd.Flags().String("log-level", "", "log level (debug, info, warn, error)")
// Bind Viper to flags
viper.BindPFlag("port", rootCmd.Flags().Lookup("port"))
viper.BindPFlag("db_path", rootCmd.Flags().Lookup("db-path"))
viper.BindPFlag("log_level", rootCmd.Flags().Lookup("log-level"))
// Subcommand: generate-config
var generateCmd = &cobra.Command{
Use: "generate-config",
Short: "Generate an example config file",
Run: func(cmd *cobra.Command, args []string) {
lang, _ := cmd.Flags().GetString("lang")
output, _ := cmd.Flags().GetString("output")
generateConfig(lang, output)
},
}
generateCmd.Flags().String("lang", "en", "Language for comments: en or ru")
generateCmd.Flags().String("output", "", "Output file path (if empty, prints to stdout)")
rootCmd.AddCommand(generateCmd)
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
}
}
// loadConfig implements the ordered configuration loading
func loadConfig() (*Config, error) {
// 1. Defaults (lowest priority)
viper.SetDefault("port", 8080)
viper.SetDefault("db_path", "./data.db")
viper.SetDefault("log_level", "info")
// 2. Config file search with logging
viper.SetConfigName("config")
viper.SetConfigType("yaml") // can also support json, toml, etc.
// Collect all search paths for logging
searchPaths := []string{}
// a) Environment variable APP_CONFIG_LOCATION
if envLoc := os.Getenv("APP_CONFIG_LOCATION"); envLoc != "" {
viper.SetConfigFile(envLoc)
searchPaths = append(searchPaths, fmt.Sprintf("ENV: APP_CONFIG_LOCATION = %s", envLoc))
if err := viper.ReadInConfig(); err == nil {
log.Printf("Config file found: %s (from APP_CONFIG_LOCATION)", viper.ConfigFileUsed())
} else {
return nil, fmt.Errorf("config file specified in APP_CONFIG_LOCATION not found: %s", envLoc)
}
} else {
// b) Working directory
wd, _ := os.Getwd()
searchPaths = append(searchPaths, fmt.Sprintf("Working directory: %s", filepath.Join(wd, "config.yaml")))
viper.AddConfigPath(".")
// c) ~/.config/{app-name}/
homeDir, _ := os.UserHomeDir()
appName := "myapp" // replace with your application name
userConfigPath := filepath.Join(homeDir, ".config", appName)
searchPaths = append(searchPaths, fmt.Sprintf("User config: %s", filepath.Join(userConfigPath, "config.yaml")))
viper.AddConfigPath(userConfigPath)
// d) Binary directory
exePath, _ := os.Executable()
exeDir := filepath.Dir(exePath)
searchPaths = append(searchPaths, fmt.Sprintf("Binary directory: %s", filepath.Join(exeDir, "config.yaml")))
viper.AddConfigPath(exeDir)
// e) /etc/{app-name}/
etcPath := filepath.Join("/etc", appName)
searchPaths = append(searchPaths, fmt.Sprintf("System config: %s", filepath.Join(etcPath, "config.yaml")))
viper.AddConfigPath(etcPath)
// Try to read config
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
log.Println("Configuration file not found. Using defaults and env/args.")
log.Println("Search paths checked:")
for _, p := range searchPaths {
log.Println(" ", p)
}
} else {
return nil, fmt.Errorf("error reading config: %w", err)
}
} else {
log.Printf("Config file found: %s", viper.ConfigFileUsed())
}
}
// 3. Environment variables
viper.SetEnvPrefix("APP")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
viper.AutomaticEnv()
// 4. Command-line flags already bound via Cobra, Viper will read them
// No additional code needed, flags are already processed by Cobra
// 5. Decode into Config struct
var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("config unmarshal failed: %w", err)
}
return &cfg, nil
}
// generateConfig writes an example config file with English or Russian comments
func generateConfig(lang, output string) {
var commentLines []string
switch lang {
case "ru":
commentLines = []string{
"# Конфигурация приложения MyApp",
"# Все значения могут быть переопределены через переменные окружения (префикс APP_)",
"# или аргументы командной строки.",
"",
"port: 8080 # Порт, на котором будет слушать HTTP-сервер",
"db_path: \"./data.db\" # Путь к файлу базы данных SQLite",
"log_level: \"info\" # Уровень логирования: debug, info, warn, error",
}
default: // en
commentLines = []string{
"# MyApp configuration file",
"# All values can be overridden by environment variables (prefix APP_)",
"# or command-line flags.",
"",
"port: 8080 # HTTP server listening port",
"db_path: \"./data.db\" # Path to SQLite database file",
"log_level: \"info\" # Log level: debug, info, warn, error",
}
}
content := strings.Join(commentLines, "\n") + "\n"
if output != "" {
if err := os.WriteFile(output, []byte(content), 0644); err != nil {
log.Fatalf("Failed to write example config: %v", err)
}
log.Printf("Example config written to %s", output)
} else {
fmt.Print(content)
}
}
```
### Agent Instructions
- Use the code above as a baseline.
- **All subcommands and CLI flags must be implemented via Cobra** (`github.com/spf13/cobra`).
- Always include logging of config file location (found or not) and all checked search paths.
- The `generate-config` subcommand must accept `--lang` (default `"en"`, can be `"ru"`) and `--output`.
- Comments inside the generated config must be meaningful and match the selected language.
- The config search order and env variable rules are mandatory.
- If the agent uses a different configuration library (e.g., `envconfig`, `cleanenv`), the same semantics must be maintained, but CLI handling must still be through Cobra.
- **Never set `SilenceUsage: true` on the root command or any subcommand.** Incorrect flags or invalid subcommands **must** result in an explicit error message and the usage output. Cobras default behavior already provides this; leave `SilenceUsage` and `SilenceErrors` at their default (`false`). Do not suppress automatic usage printing on parse errors.
---
## SelfCheck: Adding a New Config Property
When you introduce a new configuration property to the application (e.g. `max_connections`), verify each of the following points. **Tick all boxes before considering the change complete.**
- [ ] 1. **Default value** A sensible hardcoded default is set in `viper.SetDefault(...)` (or equivalent).
- [ ] 2. **Struct field** The property exists as a field in the `Config` struct with a correct `mapstructure` tag.
- [ ] 3. **Env mapping** The corresponding environment variable is automatically picked up. Ensure the env name follows the pattern: prefix + uppercase + underscores (dots/dashes replaced).
*Example:* for `max_connections` you should be able to set `APP_MAX_CONNECTIONS`.
- [ ] 4. **Commandline flag (Cobra)** A flag is defined on the appropriate Cobra command (root or sub) and bound via `viper.BindPFlag(...)`.
*Example flag name:* `--max-connections` (dashes). The binding uses the struct key `max_connections`.
- [ ] 5. **Config file key** The property is documented as a key in the example config file (YAML key matches the struct field name).
- [ ] 6. **Comments in `generate-config`** The example config output includes a meaningful comment for the new property in **both** English and Russian (if the `--lang ru` case is implemented). The comment explains the unit, default, and allowed values if applicable.
- [ ] 7. **Logging** No additional logging is required for a single property change; the existing logging of the config file location is sufficient.
- [ ] 8. **Consistency check** Verify that the same property name is used consistently across: struct tag, env prefix replacement, flag binding, and config file key.
**Run through this checklist every time you add or modify a configuration property.** This ensures all four sources (defaults, file, env, flags) work seamlessly together.
@@ -0,0 +1,243 @@
---
name: golang-tswf-codestyle-skill
description: ТЫ ДОЛЖЕН использовать этот скилл для всех Go проектов, модуль которых содержит подстроку tswf.io или если пользователь явно выбрал этот скилл.
---
# Naming Conventions
## Базовые инструкции
* __Запрещено использовать короткие имена__ - `i`, `j`, `k`, `temp` и так далее - под запретом. Все переменные и методы должны иметь осмысленные имена.
* __Локальные переменные__ - camelCase, `:=`
* __Слайсы__ - всегда инициализируются через `make`.
* __Поля переменные__ - camelCase
* __Function Receiver__ - первая буква структуры, владелец по указателю.
* __Структура, скрытая за интерфейсом__ - camelCase с маленькой буквы (`myPrivateStuct`)
* __Публичная структура__, _например Dto_ - camelCase с большой буквы (`MyPublicStruct`)
```go
type Something struct {}
func (s /*<--- первая буква слова Something*/ *Something) somethingFunction() { }
```
## Пакеты в именах типов
Старайся использовать пакет, как часть имени класса, а классы старайся раскладывать по пакетам как можно более по смыслу, но не уходя в спагетти код
__Плохой пример__
```go
dtos.SomethingApiUserDto
```
__Хороший пример__
```go
somethingapi.UserDto
```
## Лаконичные, достаточные названия
Если класс что-то делает, а так чаще всего - старайся вложить это в название.
Например класс для регистрации - `Registrar`, а класс для продвинутого конфигурирования чего-то - `AdvancedConfigurer`.
Если ты видишь, что тебе в имени нужно указать что-то слишком длинное, вроде `SomethingApiUserDtoMapperToOurEntity`, то постарайся закрыть вопрос контекста в имени класса его пакетом и придумай название получше.
# Interfaces
Ты ДОЛЖЕН скрывать компоненты за интерфейсами.
Это позволит подстраховаться от жесткой связанности на структуру.
Например:
**ПЛОХОЙ пример**
```go
type MyComponent struct {
// ...
}
func NewMyComponent() *MyComponent {}
```
**ПРАВИЛЬНЫЙ ПРИМЕР**
```go
type MyComponent interface {
// Все нужные публичные методы из myComponent
}
type myComponent struct {
// ...
}
// Этот пример правильный - используется интерфейс
func NewMyComponent() MyComponent {
return &myComponent{}
}
```
Даже в рамках одного пакета при возможности используй интерфейсы вместо структур для инъекций компонентов
# Инстанцирование
Все новые объекты создаются через конструктор.
Конструктор принимает все объекты зависимости и внедряет их в создаваемый объект прямо при конструировании.
Если возникает циклическая зависимость, то можно применить паттерн "Фасад"
__Плохой пример__
```go
type MyComponent interface{
SetDependency1(dep Dependency1)
SetDependency2(dep Dependency2)
}
// Плохой пример
// Конструктор не гарантирует полного инстанцирования объекта.
// Такой конструктор может вернуть объект в некорректном состоянии (если вызывающий код сам не использует сеттеры для зависимостей)
func NewMyComponent() MyComponent { /** ... **/ }
```
__Хороший пример__
```go
type MyComponent interface{
}
// Хороший пример
// Конструктор явно принял все зависимости
// Параметры конструктора для читаемости каждый на новой строке
func NewMyComponent(
dep1 Dependency1,
dep2 Dependency1,
) MyComponent
{
/** ... **/
}
```
# Перенос строк
Для читаемости кода человеком и улучшения его визуальной структуры в коде применяются переносы строк.
Ты можешь применять их более креативно, но приведу два кейса: __вложенность__ и __много параметров__ функции
## Вложенность
Если вызовы вложены один в другой, то человеку-читателю легко потерять структуру. Например:
__Плохой пример__
```go
// Вся цепочка смешивается в кашу.
// Сразу не видно, кто в кого вложен. Читателю приходится напрягаться просто чтобы это понять. Это очень плохой пример
SomeMethod1(SomeMethod2(SomeMethod3()), SomeMethod4(&myStruct{1, 2, 3}))
```
__Хороший пример__
```go
// Вся цепочка вызовов структурно сразу визуализируется глазами читателя.
// Видно, кто в кого вложен. Это хороший пример
SomeMethod1(
SomeMethod2(
SomeMethod3(),
),
SomeMethod4(
&myStruct{1, 2, 3},
),
)
```
## Множество параметров функции
Если у функции есть параметры с длинными именами, типами или просто занимающими место - используй перенос строк, чтобы наглядно разделить их.
__Плохой пример__
```go
// Все в кучу. Чем больше параметров, тем тяжелее читать человеку
// Это плохой пример.
func funcOne(a string, b int, c bool, g pos.Position, e user.Controller) {}
func main() {
funcTwo("hello world", 1, false, resolvePos().absolute(), resolveController())
}
```
__Хороший пример__
```go
// Четко видно структуру. Человеку просто такое читать.
// Это хороший пример.
func funcOne(
a string,
b int,
c bool,
g pos.Position,
e user.Controller,
) {}
func main() {
funcTwo(
"hello world",
1,
false,
resolvePos().absolute(),
resolveController(),
)
}
```
# Project Structure
## Обязательно для любого Go проекта
* Makefile с кросс-платформенной сборкой
* README.md + русская версия
* Весь код лежит в папках `pkg`, `cmd` или `resources` ( код для доступа к embed ресурсам )
* Папка `bin` - туда попадают собранные через Make бинари. Она в gitignore
## Для микросервисов
Если это сервис, то добавляется
* Dockerfile
* Docker-compose
* k8s chart
## CLI приложение
* Папка `cmd`, в подпапках которой реализуются консольные команды на `cobra`(!)
## Структура файлов
* Root
* bin # ОБЯЗАТЕЛЬНО в .gitignore!!
* doc
* deploy
* docker (_тут если делаешь Docker, то добавь сразу compose. и посмотри релевантные скиллы для этого_)
* k8s
* e.t.c.
* pkg
* domain
* adapters
* infrastructure
* cmd
* \<cmdname\>
* resources
* \<embed go resources\>
* go.mod
* go.sum
* README.md
* README.ru.md
* .gitignore
* Makefile
# Имя модуля
Спроси имя модуля проекта, если пользователь явно его не задал.
Если пользователь дал базовый путь (он оканчивается на `/`), то прибавь к нему имя корневой папки проекта