манифесты стали данными, заведён convy sync

- убраны комментарии из suite.toml и .conventions.toml: файл, который
  машина переписывает, комментарий через круг не проносит; объяснения
  ушли в README рядом, который suite init теперь заводит
- удалена текстовая правка манифеста целиком — 520 строк ручного
  лексера TOML вместе со всем классом ошибок порчи данных
- запись идёт из структур энкодером; ключ, которого инструмент не
  знает, запись останавливает, а не теряется молча
- convy sync сверяет манифест и подводит под него раскладку файлов:
  чего не хватает — собирает, что осиротело — удаляет, копию с
  локальной частью не трогает никогда
This commit is contained in:
av
2026-07-28 09:45:10 +03:00
parent b6b0976c19
commit 92bd1f463d
18 changed files with 851 additions and 1055 deletions
+18 -55
View File
@@ -5,42 +5,16 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"git.vakhrushev.me/av/convy/internal/manifest"
"git.vakhrushev.me/av/convy/internal/source"
"git.vakhrushev.me/av/convy/internal/suite"
)
// projectSkeleton is what a project manifest starts as. It is written as text
// with its comments in place for the same reason the suite manifest is: the
// file is read far more often than a tool touches it, and what a component is
// for is not deducible from three keys.
const projectSkeleton = `# What this repository takes from a conventions suite.
#
# Two manifests exist in the model, each named after what it describes:
# suite.toml in a suite describes the suite itself; .conventions.toml here
# describes the subscription — where the copies come from and who takes what.
#
# Copies are committed. Nothing is fetched on the fly, and the answer to "what
# did it look like last time" is given by git rather than by a lock file.
# Where the copies come from: a path on disk, relative to this file or absolute,
# or a git repository over http or https. A trailing #branch, #tag or #commit
# pins a revision.
source = %s
# ─── Components ─────────────────────────────────────────────────────────────
#
# A component is a region of the repository where every selected layer holds at
# once: one language, one set of tools, one kind of application. Each one has
# its own directory, and the directories differ — that is the only thing that
# tells two copies of one topic apart.
#
# lang and stack choose the layers: a layer travels when the axis it declares is
# among them, and a layer declaring no axis travels always. topics is the
# subscription itself, by the names the suite gives its topics.
`
// A project manifest is data, and the tool rewrites it whole on every
// subscription. So it carries no comment: what a component is for is said in
// the documentation, which stays put, rather than in a file that a machine
// re-encodes behind the author's back.
func runInit(env Env, args []string) ExitCode {
fs := flag.NewFlagSet("convy init", flag.ContinueOnError)
@@ -105,27 +79,25 @@ func runInit(env Env, args []string) ExitCode {
return Failed
}
entries := [][2]string{{"dir", manifest.Quote(given["dir"])}}
if list := split(given["lang"]); len(list) > 0 {
entries = append(entries, [2]string{"lang", array(list)})
}
if list := split(given["stack"]); len(list) > 0 {
entries = append(entries, [2]string{"stack", array(list)})
}
entries = append(entries, [2]string{"topics", "[]"})
body, err := manifest.AddTable(fmt.Appendf(nil, projectSkeleton, manifest.Quote(given["source"])),
"components."+given["component"], entries)
if err != nil {
fmt.Fprintln(env.Err, err)
return Failed
}
if err := os.MkdirAll(where, 0o755); err != nil {
fmt.Fprintln(env.Err, err)
return Failed
}
name := filepath.Join(where, manifest.ProjectName)
if err := os.WriteFile(name, body, 0o644); err != nil {
m := &manifest.Project{
Source: given["source"],
Components: map[string]manifest.Component{
given["component"]: {
Dir: given["dir"],
Lang: split(given["lang"]),
Stack: split(given["stack"]),
Topics: []string{},
},
},
Path: name,
Root: where,
}
if err := m.Save(); err != nil {
fmt.Fprintln(env.Err, err)
return Failed
}
@@ -181,12 +153,3 @@ func initFields() []Field {
Optional: true,
}}
}
// array writes a TOML array of strings.
func array(values []string) string {
parts := make([]string, len(values))
for i, v := range values {
parts[i] = manifest.Quote(v)
}
return "[" + strings.Join(parts, ", ") + "]"
}