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

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, "; ") + "."
}