Files
convy/internal/cli/init.go
T
av 92bd1f463d манифесты стали данными, заведён convy sync
- убраны комментарии из suite.toml и .conventions.toml: файл, который
  машина переписывает, комментарий через круг не проносит; объяснения
  ушли в README рядом, который suite init теперь заводит
- удалена текстовая правка манифеста целиком — 520 строк ручного
  лексера TOML вместе со всем классом ошибок порчи данных
- запись идёт из структур энкодером; ключ, которого инструмент не
  знает, запись останавливает, а не теряется молча
- convy sync сверяет манифест и подводит под него раскладку файлов:
  чего не хватает — собирает, что осиротело — удаляет, копию с
  локальной частью не трогает никогда
2026-07-28 09:45:10 +03:00

156 lines
5.0 KiB
Go

package cli
import (
"flag"
"fmt"
"os"
"path/filepath"
"git.vakhrushev.me/av/convy/internal/manifest"
"git.vakhrushev.me/av/convy/internal/source"
"git.vakhrushev.me/av/convy/internal/suite"
)
// 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)
fs.SetOutput(env.Err)
root := fs.String("root", "", "root of the project; the current directory by default")
from := fs.String("source", "", "reference to the suite: a path on disk or a git repository")
component := fs.String("component", "", "name of the first component")
dir := fs.String("dir", "", "directory the copies of that component go into")
langAxis := fs.String("lang", "", "language of the component")
stackAxis := fs.String("stack", "", "stack of the component, comma-separated")
if err := fs.Parse(args); err != nil {
return Usage
}
if code := noStrayArgs(env, "convy init", fs.Args()); code != OK {
return code
}
where := *root
if where == "" {
where = env.Dir
}
if exists(filepath.Join(where, manifest.ProjectName)) {
fmt.Fprintf(env.Err, "%s already holds %s: the conventions are wired up already\n", where, manifest.ProjectName)
fmt.Fprintln(env.Err, "convy add subscribes to one more topic")
return Usage
}
given := map[string]string{
"source": *from, "component": *component, "dir": *dir,
"lang": *langAxis, "stack": *stackAxis,
}
if len(args) == 0 {
if !env.Interactive {
fmt.Fprintln(env.Err, "convy init without arguments asks questions, and there is no terminal to ask on; pass --source, --component and --dir")
return Usage
}
if err := askAll(env, initFields(), given); err != nil {
return Usage
}
} else if err := resolve(initFields(), given); err != nil {
fmt.Fprintln(env.Err, err)
return Usage
}
// The suite is reached before the manifest is written. A manifest naming a
// suite nobody can reach passes every check the tool has and helps no one.
ref, err := source.Parse(given["source"])
if err != nil {
fmt.Fprintln(env.Err, err)
return Usage
}
tree, err := source.Open(ref, where)
if err != nil {
fmt.Fprintln(env.Err, err)
return Failed
}
defer tree.Close()
s, err := suite.Load(tree.Dir())
if err != nil {
fmt.Fprintf(env.Err, "the source %s is not a conventions suite: %s\n", ref, tree.Describe(err))
return Failed
}
if err := os.MkdirAll(where, 0o755); err != nil {
fmt.Fprintln(env.Err, err)
return Failed
}
name := filepath.Join(where, manifest.ProjectName)
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
}
fmt.Fprintf(env.Out, "\ncreated %s\n", name)
fmt.Fprintf(env.Out, "the suite speaks %s, conventions language version %d\n",
s.Manifest.Language.Lang, s.Manifest.Language.Version)
topics := s.Manifest.LiveTopics()
if len(topics) > 0 {
fmt.Fprintf(env.Out, "\n%s to take from:\n", plural(len(topics), "topic"))
for _, t := range topics {
fmt.Fprintf(env.Out, " %-20s %s\n", t, s.Manifest.Topics.Live[t])
}
}
fmt.Fprint(env.Out, `
next:
convy add <topic> subscribe and assemble
convy list what is wired up and what else is there
`)
return OK
}
func initFields() []Field {
return []Field{{
Flag: "source",
Ask: "Where the copies come from",
Hint: "A path to the suite on disk — relative to this repository or absolute — or a git repository over http or https. A trailing #branch, #tag or #commit pins a revision.",
}, {
Flag: "component",
Ask: "Name of the component",
Hint: "A region of the repository where all the chosen layers hold at once: one language, one set of tools. The name is not internal — it is how the tool answers what it assembled and where.",
Check: func(v string) error {
if !nameRe.MatchString(v) {
return fmt.Errorf("a component is named by a plain identifier: letters, digits, a dash")
}
return nil
},
}, {
Flag: "dir",
Ask: "Directory of the copies",
Hint: "Where the assembled files go. Every component has its own, and two components never share one: the copies of a topic would collide by name.",
Default: "docs/conventions",
}, {
Flag: "lang",
Ask: "Language of the component",
Hint: "Chooses the language layers. Empty when the suite is flat and has no axes at all.",
Optional: true,
}, {
Flag: "stack",
Ask: "Stack of the component",
Hint: "Chooses the stack layers — the storage, the transport, the tools. Several are allowed, comma-separated: sqlite and postgres hold together, being different tables of one service.",
Optional: true,
}}
}