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 rest := fs.Args(); len(rest) > 0 { if topic != "" && topic != rest[0] { fmt.Fprintf(env.Err, "the topic is named twice and differently: %q and %q\n", topic, rest[0]) return Usage } topic = rest[0] } 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 } answer, err := newDialogue(env).ask(topicField(o, c)) if err != nil { fmt.Fprintln(env.Err, "\ninterrupted, nothing was written") return Usage } topic = answer } 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, "; ") + "." }