- убраны комментарии из suite.toml и .conventions.toml: файл, который машина переписывает, комментарий через круг не проносит; объяснения ушли в README рядом, который suite init теперь заводит - удалена текстовая правка манифеста целиком — 520 строк ручного лексера TOML вместе со всем классом ошибок порчи данных - запись идёт из структур энкодером; ключ, которого инструмент не знает, запись останавливает, а не теряется молча - convy sync сверяет манифест и подводит под него раскладку файлов: чего не хватает — собирает, что осиротело — удаляет, копию с локальной частью не трогает никогда
166 lines
5.7 KiB
Go
166 lines
5.7 KiB
Go
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
|
|
|
|
` + "`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
|
|
}
|