- suite init создаёт директорию и манифест со скелетом таблиц; suite add пишет файл конвенции и вправляет запись в suite.toml, сохраняя комментарии - без аргументов команды спрашивают поля с подсказками, с флагами берут всё сразу и не спрашивают ничего; без терминала пустой вызов отказывает - строка о версии языка генерируется из словаря набора, поэтому созданный файл проходит suite check без правок - починено разрешение extends: короткая форма бралась по суффиксу и могла указать на сам файл; теперь неоднозначность либо избегается при записи, либо сообщается ошибкой
130 lines
3.6 KiB
Go
130 lines
3.6 KiB
Go
// Package cli lays out the commands of the tool.
|
|
//
|
|
// The depth of a command reflects how often it runs and whom it addresses:
|
|
// project commands run in every repository and often, tending a suite runs in
|
|
// one repository and rarely. That is why `check` stays at the top level and
|
|
// whatever makes no sense in a project goes under `suite`. There are no
|
|
// synonyms: `convy suite pull` is not introduced next to `convy pull`.
|
|
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
)
|
|
|
|
// ExitCode is the exit status of the process.
|
|
type ExitCode int
|
|
|
|
const (
|
|
// OK means the check passed and the work is done.
|
|
OK ExitCode = 0
|
|
// Failed means the check found errors.
|
|
Failed ExitCode = 1
|
|
// Usage means the command was typed wrong or run in the wrong context.
|
|
Usage ExitCode = 2
|
|
)
|
|
|
|
// Env is the environment of a run. It is pulled out so that commands can be
|
|
// tested without a process.
|
|
type Env struct {
|
|
Dir string
|
|
In io.Reader
|
|
Out io.Writer
|
|
Err io.Writer
|
|
// Interactive says whether input comes from a terminal. A command that
|
|
// asks questions refuses to start without one rather than blocking on an
|
|
// answer nobody is there to give.
|
|
Interactive bool
|
|
}
|
|
|
|
// Run parses the arguments and executes the command.
|
|
func Run(env Env, args []string) ExitCode {
|
|
if len(args) == 0 {
|
|
usage(env.Out)
|
|
return Usage
|
|
}
|
|
|
|
switch args[0] {
|
|
case "suite":
|
|
return runSuite(env, args[1:])
|
|
case "add", "pull", "list", "check":
|
|
fmt.Fprintf(env.Err, "the %q command is not implemented yet\n", args[0])
|
|
return Usage
|
|
case "help", "-h", "--help":
|
|
usage(env.Out)
|
|
return OK
|
|
default:
|
|
fmt.Fprintf(env.Err, "unknown command %q\n\n", args[0])
|
|
usage(env.Err)
|
|
return Usage
|
|
}
|
|
}
|
|
|
|
func runSuite(env Env, args []string) ExitCode {
|
|
if len(args) == 0 {
|
|
fmt.Fprintln(env.Err, "convy suite: a subcommand is required — init, add or check")
|
|
return Usage
|
|
}
|
|
switch args[0] {
|
|
case "check":
|
|
return runSuiteCheck(env, args[1:])
|
|
case "init":
|
|
return runSuiteInit(env, args[1:])
|
|
case "add":
|
|
return runSuiteAdd(env, args[1:])
|
|
default:
|
|
fmt.Fprintf(env.Err, "unknown subcommand %q for convy suite\n", args[0])
|
|
return Usage
|
|
}
|
|
}
|
|
|
|
// usage prints the help grouped under headings: a flat list hides the levels.
|
|
func usage(w io.Writer) {
|
|
fmt.Fprint(w, `convy — tending development conventions.
|
|
|
|
In a project:
|
|
convy add <topic> subscribe and assemble (not implemented)
|
|
convy pull reassemble what is subscribed (not implemented)
|
|
convy list what is wired up and available (not implemented)
|
|
convy check check the form of what is here (not implemented)
|
|
|
|
In a suite:
|
|
convy suite init start a suite: a directory and a manifest
|
|
convy suite add add a convention: a file, a topic and a prefix
|
|
convy suite check suite integrity: prefixes, topics, axes, links, form
|
|
|
|
The commands that change something run in two modes. Bare, they ask for every
|
|
field with a hint attached — that mode is for a person. With flags, they take
|
|
everything at once and ask nothing — that mode is for agents and scripts.
|
|
|
|
`)
|
|
}
|
|
|
|
// Main is the entry point of the process.
|
|
func Main() int {
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "cannot determine the current directory:", err)
|
|
return int(Usage)
|
|
}
|
|
env := Env{
|
|
Dir: dir,
|
|
In: os.Stdin,
|
|
Out: os.Stdout,
|
|
Err: os.Stderr,
|
|
Interactive: terminal(os.Stdin),
|
|
}
|
|
return int(Run(env, os.Args[1:]))
|
|
}
|
|
|
|
// terminal reports whether a file is a character device, which is as close as
|
|
// the standard library gets to asking whether a person is on the other end.
|
|
func terminal(f *os.File) bool {
|
|
info, err := f.Stat()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return info.Mode()&os.ModeCharDevice != 0
|
|
}
|