- убраны комментарии из suite.toml и .conventions.toml: файл, который машина переписывает, комментарий через круг не проносит; объяснения ушли в README рядом, который suite init теперь заводит - удалена текстовая правка манифеста целиком — 520 строк ручного лексера TOML вместе со всем классом ошибок порчи данных - запись идёт из структур энкодером; ключ, которого инструмент не знает, запись останавливает, а не теряется молча - convy sync сверяет манифест и подводит под него раскладку файлов: чего не хватает — собирает, что осиротело — удаляет, копию с локальной частью не трогает никогда
156 lines
5.0 KiB
Go
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,
|
|
}}
|
|
}
|