package cli import ( "flag" "fmt" "os" "path/filepath" "git.vakhrushev.me/av/convy/internal/lang" "git.vakhrushev.me/av/convy/internal/manifest" ) // manifestSkeleton is what a suite starts as. The tables stand empty but // present: `convy suite add` splices entries into them, and a table that is not // there is a table an edit cannot find. // // The two documents about the language are left commented out on purpose. The // suite is expected to carry them, but the tool cannot author them, and a // manifest pointing at a file that does not exist is a manifest that fails its // own check on the first run. const manifestSkeleton = `# The manifest of a conventions suite. # # Two manifests exist in the model, each named after what it describes: # suite.toml here, in the suite, describes the suite itself; .conventions.toml # in a project describes what that project subscribed to. Which of the two lies # next to you tells you where you are. [language] version = %d lang = "%s" # description = "LANGUAGE.md" # the full account of the language, stays with the author # reading = "READING.md" # the short guide for a reader, travels into every copy # ─── Topics ───────────────────────────────────────────────────────────────── # # A topic is a set of rules about one focus of development, and the unit of # subscription. The value is the one line about what the topic is for; the # table of conventions in a consumer's README is built out of it. [topics.live] # Retired names land here together with a reason and a date, so that they can # never be handed to another topic: the name lives on in foreign repositories. [topics.retired] # ─── Rule prefixes ────────────────────────────────────────────────────────── # # A prefix is four uppercase Latin letters, unique across the suite, chosen for # a file rather than derived by a formula. The letter X is reserved for the # local rules of consumers and is never taken here. Paths are given from the # root of the repository. [prefixes.live] # Prefixes of deleted and split files land here, likewise never to be reissued. [prefixes.retired] ` func runSuiteInit(env Env, args []string) ExitCode { fs := flag.NewFlagSet("convy suite init", flag.ContinueOnError) fs.SetOutput(env.Err) path := fs.String("path", "", "directory of the suite; it is created if absent") code := fs.String("lang", "", "code of the natural language the suite is written in") version := fs.Int("language-version", 1, "version of the conventions language") if err := fs.Parse(args); err != nil { return Usage } fields := []Field{{ Flag: "path", Ask: "Directory of the suite", Hint: "Where the suite will live. The directory is created if it is not there yet.", Default: ".", }, { Flag: "lang", Ask: "Natural language of the suite", Hint: "The language the rules are written in. It settles the key words: ДОЛЖЕН and ПОЧЕМУ for ru, MUST and WHY for en.", Default: manifest.DefaultLanguageCode, Check: func(v string) error { _, err := lang.Lookup(*version, v) return err }, }} given := map[string]string{"path": *path, "lang": *code} if len(args) == 0 { if !env.Interactive { fmt.Fprintln(env.Err, "convy suite init without arguments asks questions, and there is no terminal to ask on; pass --path and --lang") return Usage } if err := askAll(env, fields, given); err != nil { return Usage } } else if err := resolve(fields, given); err != nil { fmt.Fprintln(env.Err, err) return Usage } root := given["path"] if _, err := os.Stat(filepath.Join(root, manifest.Name)); err == nil { fmt.Fprintf(env.Err, "%s already holds %s: this is a suite already\n", root, manifest.Name) return Usage } if err := os.MkdirAll(filepath.Join(root, "conventions"), 0o755); err != nil { fmt.Fprintln(env.Err, err) return Usage } body := fmt.Sprintf(manifestSkeleton, *version, given["lang"]) name := filepath.Join(root, manifest.Name) if err := os.WriteFile(name, []byte(body), 0o644); err != nil { fmt.Fprintln(env.Err, err) return Usage } fmt.Fprintf(env.Out, "\ncreated %s\ncreated %s\n", name, filepath.Join(root, "conventions")) fmt.Fprintf(env.Out, "\nthe suite speaks %s, conventions language version %d\n", given["lang"], *version) fmt.Fprint(env.Out, ` next: write the two documents about the language and name them in [language] convy suite add add the first convention convy suite check verify the suite holds together `) return OK } // askAll walks the fields in interactive mode, keeping answers already given on // the command line. func askAll(env Env, fields []Field, given map[string]string) error { d := newDialogue(env) for _, f := range fields { if given[f.Flag] != "" { continue } answer, err := d.ask(f) if err != nil { fmt.Fprintln(env.Err, "\ninterrupted, nothing was written") return err } given[f.Flag] = answer } return nil }