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

This commit is contained in:
2026-08-03 22:22:24 +03:00
commit 8c8631ac9c
80 changed files with 10618 additions and 0 deletions
+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
# Имя модуля
Спроси имя модуля проекта, если пользователь явно его не задал.
Если пользователь дал базовый путь (он оканчивается на `/`), то прибавь к нему имя корневой папки проекта