- suite init создаёт директорию и манифест со скелетом таблиц; suite add пишет файл конвенции и вправляет запись в suite.toml, сохраняя комментарии - без аргументов команды спрашивают поля с подсказками, с флагами берут всё сразу и не спрашивают ничего; без терминала пустой вызов отказывает - строка о версии языка генерируется из словаря набора, поэтому созданный файл проходит suite check без правок - починено разрешение extends: короткая форма бралась по суффиксу и могла указать на сам файл; теперь неоднозначность либо избегается при записи, либо сообщается ошибкой
146 lines
5.1 KiB
Go
146 lines
5.1 KiB
Go
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
|
|
}
|