package cli import ( "flag" "fmt" "os" "path/filepath" "git.vakhrushev.me/av/convy/internal/lang" "git.vakhrushev.me/av/convy/internal/manifest" ) // A suite starts as a manifest holding only what it knows about itself, and a // README next to it holding everything a person needs to know to fill it in. // // The two are split because the manifest is data the tool rewrites on every // `suite add` and every `suite retire`, while the README is prose nothing // touches. Keeping the explanations inside the manifest would mean losing them // the first time a command wrote the file. // // The README is a starting point, in the tool's own language, for its author to // replace. What the suite itself is about, only its author knows. const readmeSkeleton = `# Conventions suite The rules live in ` + "`conventions/`" + `. What each file is and what it may do is settled by the conventions language; this README is the place to say what this particular suite is for and how it is kept. ## The manifest ` + "`.conventions-suite.toml`" + ` holds the identity of the suite: the language its rules are written in, its topics and its rule prefixes. It is written by ` + "`convy`" + ` and carries no comments — a command rewrites the whole file, and a comment would not survive that. Explanations belong here instead. ## Topics A topic is a set of rules about one focus of development — time, configuration, the database schema — and it is the unit of subscription: a consumer takes it whole. A name is never renamed and never reissued, because it lives on in foreign repositories: in the ` + "`origin:`" + ` header of every copy and in the subscription of every consumer. Since the name is permanent, a topic is named after a decision and whom it addresses rather than after the role some part of today's project plays. ## Prefixes A prefix is four uppercase Latin letters, unique across the suite, chosen for a file rather than derived by a formula. Rules are addressed by identifier — ` + "`KEYS-5`" + ` — with no path, so the identifier survives a file moving between axes. A prefix is never reissued either. The letter X in first position is reserved for the local rules of consuming repositories. The suite never takes it, so a local prefix can never collide with a future one here. ## Retirement Nothing leaves the manifest. A topic or a prefix that is done moves to the retired half together with a reason and a date, so that the name can never be handed to something else. ` 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 } name := filepath.Join(root, manifest.Name) m := &manifest.Manifest{ Language: manifest.Language{Version: *version, Lang: given["lang"]}, Path: name, } if err := m.Save(); err != nil { fmt.Fprintln(env.Err, err) return Usage } fmt.Fprintf(env.Out, "\ncreated %s\ncreated %s\n", name, filepath.Join(root, "conventions")) // The README is written only when there is none: it is a starting point, // and a starting point that overwrites what somebody already wrote is not // one. readme := filepath.Join(root, "README.md") if !exists(readme) { if err := os.WriteFile(readme, []byte(readmeSkeleton), 0o644); err != nil { fmt.Fprintln(env.Err, err) return Usage } fmt.Fprintf(env.Out, "created %s\n", readme) } fmt.Fprintf(env.Out, "\nthe suite speaks %s, conventions language version %d\n", given["lang"], *version) fmt.Fprint(env.Out, ` next: rewrite README.md — it says what a topic and a prefix are, not what this suite is for 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 }