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 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, }} }