проектные команды и ссылки на источник
- заведён internal/source: уровни ссылаются друг на друга путём на диске или git-репозиторием, ревизия закрепляется хвостом #ref; клон делается заново и удаляется, кэша нет - добавлены init, add, pull, list, check в проекте — манифест .conventions.toml, сборка копий по разу на компонент, маркер локальной части, READING.md рядом - проверки формы развязаны с набором: принимают lang.Vocabulary, а язык копии узнаётся по строке о версии — манифеста рядом с ней нет
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.vakhrushev.me/av/convy/internal/manifest"
|
||||
"git.vakhrushev.me/av/convy/internal/source"
|
||||
"git.vakhrushev.me/av/convy/internal/suite"
|
||||
)
|
||||
|
||||
// projectSkeleton is what a project manifest starts as. It is written as text
|
||||
// with its comments in place for the same reason the suite manifest is: the
|
||||
// file is read far more often than a tool touches it, and what a component is
|
||||
// for is not deducible from three keys.
|
||||
const projectSkeleton = `# What this repository takes from a conventions suite.
|
||||
#
|
||||
# Two manifests exist in the model, each named after what it describes:
|
||||
# suite.toml in a suite describes the suite itself; .conventions.toml here
|
||||
# describes the subscription — where the copies come from and who takes what.
|
||||
#
|
||||
# Copies are committed. Nothing is fetched on the fly, and the answer to "what
|
||||
# did it look like last time" is given by git rather than by a lock file.
|
||||
|
||||
# Where the copies come from: a path on disk, relative to this file or absolute,
|
||||
# or a git repository over http or https. A trailing #branch, #tag or #commit
|
||||
# pins a revision.
|
||||
source = "%s"
|
||||
|
||||
# ─── Components ─────────────────────────────────────────────────────────────
|
||||
#
|
||||
# A component is a region of the repository where every selected layer holds at
|
||||
# once: one language, one set of tools, one kind of application. Each one has
|
||||
# its own directory, and the directories differ — that is the only thing that
|
||||
# tells two copies of one topic apart.
|
||||
#
|
||||
# lang and stack choose the layers: a layer travels when the axis it declares is
|
||||
# among them, and a layer declaring no axis travels always. topics is the
|
||||
# subscription itself, by the names the suite gives its topics.
|
||||
`
|
||||
|
||||
func runInit(env Env, args []string) ExitCode {
|
||||
fs := flag.NewFlagSet("convy init", flag.ContinueOnError)
|
||||
fs.SetOutput(env.Err)
|
||||
root := fs.String("root", "", "root of the project; the current directory by default")
|
||||
from := fs.String("source", "", "reference to the suite: a path on disk or a git repository")
|
||||
component := fs.String("component", "", "name of the first component")
|
||||
dir := fs.String("dir", "", "directory the copies of that component go into")
|
||||
langAxis := fs.String("lang", "", "language of the component")
|
||||
stackAxis := fs.String("stack", "", "stack of the component, comma-separated")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return Usage
|
||||
}
|
||||
|
||||
where := *root
|
||||
if where == "" {
|
||||
where = env.Dir
|
||||
}
|
||||
if exists(filepath.Join(where, manifest.ProjectName)) {
|
||||
fmt.Fprintf(env.Err, "%s already holds %s: the conventions are wired up already\n", where, manifest.ProjectName)
|
||||
fmt.Fprintln(env.Err, "convy add subscribes to one more topic")
|
||||
return Usage
|
||||
}
|
||||
|
||||
given := map[string]string{
|
||||
"source": *from, "component": *component, "dir": *dir,
|
||||
"lang": *langAxis, "stack": *stackAxis,
|
||||
}
|
||||
if len(args) == 0 {
|
||||
if !env.Interactive {
|
||||
fmt.Fprintln(env.Err, "convy init without arguments asks questions, and there is no terminal to ask on; pass --source, --component and --dir")
|
||||
return Usage
|
||||
}
|
||||
if err := askAll(env, initFields(), given); err != nil {
|
||||
return Usage
|
||||
}
|
||||
} else if err := resolve(initFields(), given); err != nil {
|
||||
fmt.Fprintln(env.Err, err)
|
||||
return Usage
|
||||
}
|
||||
|
||||
// The suite is reached before the manifest is written. A manifest naming a
|
||||
// suite nobody can reach passes every check the tool has and helps no one.
|
||||
ref, err := source.Parse(given["source"])
|
||||
if err != nil {
|
||||
fmt.Fprintln(env.Err, err)
|
||||
return Usage
|
||||
}
|
||||
tree, err := source.Open(ref, where)
|
||||
if err != nil {
|
||||
fmt.Fprintln(env.Err, err)
|
||||
return Failed
|
||||
}
|
||||
defer tree.Close()
|
||||
|
||||
s, err := suite.Load(tree.Dir())
|
||||
if err != nil {
|
||||
fmt.Fprintf(env.Err, "the source %s is not a conventions suite: %s\n", ref, tree.Describe(err))
|
||||
return Failed
|
||||
}
|
||||
|
||||
entries := [][2]string{{"dir", quote(given["dir"])}}
|
||||
if list := split(given["lang"]); len(list) > 0 {
|
||||
entries = append(entries, [2]string{"lang", array(list)})
|
||||
}
|
||||
if list := split(given["stack"]); len(list) > 0 {
|
||||
entries = append(entries, [2]string{"stack", array(list)})
|
||||
}
|
||||
entries = append(entries, [2]string{"topics", "[]"})
|
||||
|
||||
body, err := manifest.AddTable([]byte(fmt.Sprintf(projectSkeleton, given["source"])),
|
||||
"components."+given["component"], entries)
|
||||
if err != nil {
|
||||
fmt.Fprintln(env.Err, err)
|
||||
return Failed
|
||||
}
|
||||
if err := os.MkdirAll(where, 0o755); err != nil {
|
||||
fmt.Fprintln(env.Err, err)
|
||||
return Failed
|
||||
}
|
||||
name := filepath.Join(where, manifest.ProjectName)
|
||||
if err := os.WriteFile(name, body, 0o644); err != nil {
|
||||
fmt.Fprintln(env.Err, err)
|
||||
return Failed
|
||||
}
|
||||
|
||||
fmt.Fprintf(env.Out, "\ncreated %s\n", name)
|
||||
fmt.Fprintf(env.Out, "the suite speaks %s, conventions language version %d\n",
|
||||
s.Manifest.Language.Lang, s.Manifest.Language.Version)
|
||||
|
||||
topics := s.Manifest.LiveTopics()
|
||||
if len(topics) > 0 {
|
||||
fmt.Fprintf(env.Out, "\n%s to take from:\n", plural(len(topics), "topic"))
|
||||
for _, t := range topics {
|
||||
fmt.Fprintf(env.Out, " %-20s %s\n", t, s.Manifest.Topics.Live[t])
|
||||
}
|
||||
}
|
||||
fmt.Fprint(env.Out, `
|
||||
next:
|
||||
convy add <topic> subscribe and assemble
|
||||
convy list what is wired up and what else is there
|
||||
`)
|
||||
return OK
|
||||
}
|
||||
|
||||
func initFields() []Field {
|
||||
return []Field{{
|
||||
Flag: "source",
|
||||
Ask: "Where the copies come from",
|
||||
Hint: "A path to the suite on disk — relative to this repository or absolute — or a git repository over http or https. A trailing #branch, #tag or #commit pins a revision.",
|
||||
}, {
|
||||
Flag: "component",
|
||||
Ask: "Name of the component",
|
||||
Hint: "A region of the repository where all the chosen layers hold at once: one language, one set of tools. The name is not internal — it is how the tool answers what it assembled and where.",
|
||||
Check: func(v string) error {
|
||||
if !nameRe.MatchString(v) {
|
||||
return fmt.Errorf("a component is named by a plain identifier: letters, digits, a dash")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}, {
|
||||
Flag: "dir",
|
||||
Ask: "Directory of the copies",
|
||||
Hint: "Where the assembled files go. Every component has its own, and two components never share one: the copies of a topic would collide by name.",
|
||||
Default: "docs/conventions",
|
||||
}, {
|
||||
Flag: "lang",
|
||||
Ask: "Language of the component",
|
||||
Hint: "Chooses the language layers. Empty when the suite is flat and has no axes at all.",
|
||||
Optional: true,
|
||||
}, {
|
||||
Flag: "stack",
|
||||
Ask: "Stack of the component",
|
||||
Hint: "Chooses the stack layers — the storage, the transport, the tools. Several are allowed, comma-separated: sqlite and postgres hold together, being different tables of one service.",
|
||||
Optional: true,
|
||||
}}
|
||||
}
|
||||
|
||||
func quote(s string) string {
|
||||
return `"` + strings.ReplaceAll(s, `"`, `\"`) + `"`
|
||||
}
|
||||
|
||||
func array(values []string) string {
|
||||
parts := make([]string, len(values))
|
||||
for i, v := range values {
|
||||
parts[i] = quote(v)
|
||||
}
|
||||
return "[" + strings.Join(parts, ", ") + "]"
|
||||
}
|
||||
Reference in New Issue
Block a user