- убраны комментарии из suite.toml и .conventions.toml: файл, который машина переписывает, комментарий через круг не проносит; объяснения ушли в README рядом, который suite init теперь заводит - удалена текстовая правка манифеста целиком — 520 строк ручного лексера TOML вместе со всем классом ошибок порчи данных - запись идёт из структур энкодером; ключ, которого инструмент не знает, запись останавливает, а не теряется молча - convy sync сверяет манифест и подводит под него раскладку файлов: чего не хватает — собирает, что осиротело — удаляет, копию с локальной частью не трогает никогда
155 lines
4.6 KiB
Go
155 lines
4.6 KiB
Go
package cli
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.vakhrushev.me/av/convy/internal/manifest"
|
|
"git.vakhrushev.me/av/convy/internal/project"
|
|
)
|
|
|
|
// convy add does two things at once, and they are one thing: it writes the
|
|
// subscription into the manifest and assembles the file. A subscription that
|
|
// left no file behind, or a file nothing subscribed to, is the state the model
|
|
// has no name for.
|
|
|
|
func runAdd(env Env, args []string) ExitCode {
|
|
fs := flag.NewFlagSet("convy add", flag.ContinueOnError)
|
|
fs.SetOutput(env.Err)
|
|
root := fs.String("root", "", "root of the project; it is looked up upwards by default")
|
|
forComponent := fs.String("for", "", "component to assemble for; needed when there is more than one")
|
|
topicFlag := fs.String("topic", "", "topic to subscribe to; may also be given as the first argument")
|
|
|
|
// The topic is taken off the front before the flags are parsed. The flag
|
|
// package stops at the first argument that is not a flag, so "convy add
|
|
// time --for backend" — the form the model writes — would otherwise lose
|
|
// the component silently.
|
|
topic := ""
|
|
rest := args
|
|
if len(rest) > 0 && !strings.HasPrefix(rest[0], "-") {
|
|
topic, rest = rest[0], rest[1:]
|
|
}
|
|
if err := fs.Parse(rest); err != nil {
|
|
return Usage
|
|
}
|
|
if *topicFlag != "" {
|
|
if topic != "" && topic != *topicFlag {
|
|
fmt.Fprintf(env.Err, "the topic is named twice and differently: %q and %q\n", topic, *topicFlag)
|
|
return Usage
|
|
}
|
|
topic = *topicFlag
|
|
}
|
|
if left := fs.Args(); len(left) > 0 {
|
|
fmt.Fprintf(env.Err, "convy add takes one topic, and %q came after it as well\n", left[0])
|
|
return Usage
|
|
}
|
|
|
|
o, code := openProject(env, *root)
|
|
if code != OK {
|
|
return code
|
|
}
|
|
defer o.Close()
|
|
|
|
if err := distinctDirs(o.Manifest); err != nil {
|
|
fmt.Fprintln(env.Err, err)
|
|
return Usage
|
|
}
|
|
|
|
name, c, code := componentOf(env, o.Manifest, *forComponent)
|
|
if code != OK {
|
|
return code
|
|
}
|
|
|
|
if topic == "" {
|
|
if !env.Interactive {
|
|
fmt.Fprintln(env.Err, "convy add without arguments asks which topic, and there is no terminal to ask on; name the topic as an argument")
|
|
return Usage
|
|
}
|
|
given := map[string]string{}
|
|
if err := askAll(env, []Field{topicField(o, c)}, given); err != nil {
|
|
return Usage
|
|
}
|
|
topic = given["topic"]
|
|
}
|
|
|
|
if !o.Suite.Manifest.TopicLive(topic) {
|
|
if o.Suite.Manifest.TopicRetired(topic) {
|
|
fmt.Fprintf(env.Err, "the suite has retired the topic %q: %s\n", topic, o.Suite.Manifest.Topics.Retired[topic])
|
|
return Usage
|
|
}
|
|
fmt.Fprintf(env.Err, "the suite declares no topic %q; it declares: %s\n", topic, strings.Join(o.Suite.Manifest.LiveTopics(), ", "))
|
|
return Usage
|
|
}
|
|
if c.Subscribed(topic) {
|
|
fmt.Fprintf(env.Err, "the component %q is subscribed to %q already; convy pull reassembles it\n", name, topic)
|
|
return Usage
|
|
}
|
|
|
|
made, err := project.Assemble(o.Suite, o.Root, c, topic)
|
|
if err != nil {
|
|
fmt.Fprintln(env.Err, err)
|
|
return Failed
|
|
}
|
|
|
|
// The manifest is written after the file: a subscription recorded against
|
|
// an assembly that failed would send the next pull looking for a copy that
|
|
// was never made.
|
|
o.Manifest.Subscribe(name, topic)
|
|
if err := o.Manifest.Save(); err != nil {
|
|
fmt.Fprintln(env.Err, err)
|
|
return Failed
|
|
}
|
|
|
|
guide, err := project.Reading(o.LangRoot, o.Suite.Manifest.Language.Reading, o.Root, c.Dir)
|
|
if err != nil {
|
|
fmt.Fprintf(env.Err, "warning: %s\n", err)
|
|
}
|
|
|
|
fmt.Fprintf(env.Out, "\n%s → %s\n", topic, made.Path)
|
|
for _, layer := range made.Layers {
|
|
fmt.Fprintf(env.Out, " %s\n", layer)
|
|
}
|
|
if guide != "" {
|
|
fmt.Fprintf(env.Out, "\n%s refreshed\n", guide)
|
|
}
|
|
fmt.Fprintf(env.Out, "subscribed the component %q in %s\n", name, manifest.ProjectName)
|
|
return OK
|
|
}
|
|
|
|
// topicField offers the topics the component has not taken yet.
|
|
func topicField(o *opened, c manifest.Component) Field {
|
|
var free []string
|
|
for _, t := range o.Suite.Manifest.LiveTopics() {
|
|
if !c.Subscribed(t) {
|
|
free = append(free, t)
|
|
}
|
|
}
|
|
return Field{
|
|
Flag: "topic",
|
|
Ask: "Topic",
|
|
Hint: "A topic is taken whole: the file gathers every layer of it the component fits. " + describeTopics(o, free),
|
|
Options: free,
|
|
Check: func(v string) error {
|
|
if !o.Suite.Manifest.TopicLive(v) {
|
|
return fmt.Errorf("the suite declares no topic %q", v)
|
|
}
|
|
if c.Subscribed(v) {
|
|
return fmt.Errorf("this component is subscribed to %q already", v)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func describeTopics(o *opened, free []string) string {
|
|
if len(free) == 0 {
|
|
return "The component is subscribed to everything the suite has."
|
|
}
|
|
parts := make([]string, 0, len(free))
|
|
for _, t := range free {
|
|
parts = append(parts, fmt.Sprintf("%s — %s", t, o.Suite.Manifest.Topics.Live[t]))
|
|
}
|
|
return "On offer: " + strings.Join(parts, "; ") + "."
|
|
}
|