- манифест читается так, как записан: решётка внутри строки не открывает комментарий, скобка внутри комментария не закрывает массив, имя внутри комментария не становится подпиской; новый ключ встаёт после массива, а не внутрь него - всё записываемое проходит через manifest.Quote — обратный слэш в пути делал файл, который инструмент сам не читает - маркер локальной части переехал в doc и пропускает огороженные блоки: процитированный в примере маркер больше не считается границей, а копия без маркера не перезаписывается молча - лишний позиционный аргумент отсекается: flag прекращал разбор и прятал флаги после себя, из-за чего pull, list и check игнорировали --for - заведены тесты проверок копий, включая молчание на исправной копии
165 lines
4.9 KiB
Go
165 lines
4.9 KiB
Go
package cli
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"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.
|
|
source, err := os.ReadFile(o.Manifest.Path)
|
|
if err != nil {
|
|
fmt.Fprintln(env.Err, err)
|
|
return Failed
|
|
}
|
|
updated, err := manifest.AddToList(source, "components."+name, "topics", topic)
|
|
if err != nil {
|
|
fmt.Fprintln(env.Err, err)
|
|
return Failed
|
|
}
|
|
if err := os.WriteFile(o.Manifest.Path, updated, 0o644); 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, "; ") + "."
|
|
}
|