321 lines
8.5 KiB
Go
321 lines
8.5 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"embed"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/pflag"
|
|
cli "github.com/supabase/cli/cmd"
|
|
"github.com/supabase/cli/internal/utils"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
const tagOthers = "other-commands"
|
|
|
|
var (
|
|
examples map[string][]ExampleDoc
|
|
//go:embed templates/examples.yaml
|
|
exampleSpec string
|
|
//go:embed supabase/*
|
|
docsDir embed.FS
|
|
)
|
|
|
|
func main() {
|
|
semver := "latest"
|
|
if len(os.Args) > 1 {
|
|
semver = os.Args[1]
|
|
}
|
|
// Trim version tag
|
|
if semver[0] == 'v' {
|
|
semver = semver[1:]
|
|
}
|
|
|
|
if err := generate(semver); err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
}
|
|
|
|
func generate(version string) error {
|
|
dec := yaml.NewDecoder(strings.NewReader(exampleSpec))
|
|
if err := dec.Decode(&examples); err != nil {
|
|
return err
|
|
}
|
|
root := cli.GetRootCmd()
|
|
root.InitDefaultCompletionCmd()
|
|
root.InitDefaultHelpFlag()
|
|
spec := SpecDoc{
|
|
Clispec: "001",
|
|
Info: InfoDoc{
|
|
Id: "cli",
|
|
Version: version,
|
|
Title: strings.TrimSpace(root.Short),
|
|
Description: forceMultiLine("Supabase CLI provides you with tools to develop your application locally, and deploy your application to the Supabase platform."),
|
|
Language: "sh",
|
|
Source: "https://github.com/supabase/cli",
|
|
Bugs: "https://github.com/supabase/cli/issues",
|
|
Spec: "https://github.com/supabase/spec/cli_v1_commands.yaml",
|
|
Tags: getTags(root),
|
|
},
|
|
}
|
|
root.Flags().VisitAll(func(flag *pflag.Flag) {
|
|
if !flag.Hidden {
|
|
spec.Flags = append(spec.Flags, getFlags(flag))
|
|
}
|
|
})
|
|
cobra.CheckErr(root.MarkFlagRequired("experimental"))
|
|
// Generate, serialise, and print
|
|
yamlDoc := GenYamlDoc(root, &spec)
|
|
spec.Info.Options = yamlDoc.Options
|
|
// Reverse commands list
|
|
for i, j := 0, len(spec.Commands)-1; i < j; i, j = i+1, j-1 {
|
|
spec.Commands[i], spec.Commands[j] = spec.Commands[j], spec.Commands[i]
|
|
}
|
|
// Write to stdout
|
|
encoder := yaml.NewEncoder(os.Stdout)
|
|
encoder.SetIndent(2)
|
|
return encoder.Encode(spec)
|
|
}
|
|
|
|
type TagDoc struct {
|
|
Id string `yaml:",omitempty"`
|
|
Title string `yaml:",omitempty"`
|
|
Description string `yaml:",omitempty"`
|
|
}
|
|
|
|
type InfoDoc struct {
|
|
Id string `yaml:",omitempty"`
|
|
Version string `yaml:",omitempty"`
|
|
Title string `yaml:",omitempty"`
|
|
Language string `yaml:",omitempty"`
|
|
Source string `yaml:",omitempty"`
|
|
Bugs string `yaml:",omitempty"`
|
|
Spec string `yaml:",omitempty"`
|
|
Description string `yaml:",omitempty"`
|
|
Options string `yaml:",omitempty"`
|
|
Tags []TagDoc `yaml:",omitempty"`
|
|
}
|
|
|
|
type ValueDoc struct {
|
|
Id string `yaml:",omitempty"`
|
|
Name string `yaml:",omitempty"`
|
|
Type string `yaml:",omitempty"`
|
|
Description string `yaml:",omitempty"`
|
|
}
|
|
|
|
type FlagDoc struct {
|
|
Id string `yaml:",omitempty"`
|
|
Name string `yaml:",omitempty"`
|
|
Description string `yaml:",omitempty"`
|
|
Required bool `yaml:",omitempty"`
|
|
DefaultValue string `yaml:"default_value"`
|
|
AcceptedValues []ValueDoc `yaml:"accepted_values,omitempty"`
|
|
}
|
|
|
|
type ExampleDoc struct {
|
|
Id string `yaml:",omitempty"`
|
|
Name string `yaml:",omitempty"`
|
|
Code string `yaml:",omitempty"`
|
|
Response string `yaml:",omitempty"`
|
|
}
|
|
|
|
type CmdDoc struct {
|
|
Id string `yaml:",omitempty"`
|
|
Title string `yaml:",omitempty"`
|
|
Summary string `yaml:",omitempty"`
|
|
Source string `yaml:",omitempty"`
|
|
Description string `yaml:",omitempty"`
|
|
Examples []ExampleDoc `yaml:",omitempty"`
|
|
Tags []string `yaml:""`
|
|
Links []LinkDoc `yaml:""`
|
|
Usage string `yaml:",omitempty"`
|
|
Subcommands []string `yaml:""`
|
|
Options string `yaml:",omitempty"`
|
|
Flags []FlagDoc `yaml:""`
|
|
}
|
|
|
|
type LinkDoc struct {
|
|
Name string `yaml:",omitempty"`
|
|
Link string `yaml:",omitempty"`
|
|
}
|
|
|
|
type ParamDoc struct {
|
|
Id string `yaml:",omitempty"`
|
|
Title string `yaml:",omitempty"`
|
|
Description string `yaml:",omitempty"`
|
|
Required bool `yaml:",omitempty"`
|
|
Default string `yaml:",omitempty"`
|
|
Tags []string `yaml:",omitempty"`
|
|
Links []LinkDoc `yaml:""`
|
|
}
|
|
|
|
type SpecDoc struct {
|
|
Clispec string `yaml:",omitempty"`
|
|
Info InfoDoc `yaml:",omitempty"`
|
|
Flags []FlagDoc `yaml:",omitempty"`
|
|
Commands []CmdDoc `yaml:",omitempty"`
|
|
Parameters []FlagDoc `yaml:",omitempty"`
|
|
}
|
|
|
|
// DFS on command tree to generate documentation specs.
|
|
func GenYamlDoc(cmd *cobra.Command, root *SpecDoc) CmdDoc {
|
|
var subcommands []string
|
|
for _, c := range cmd.Commands() {
|
|
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
|
|
continue
|
|
}
|
|
sub := GenYamlDoc(c, root)
|
|
if !cmd.HasParent() && len(sub.Tags) == 0 {
|
|
sub.Tags = append(sub.Tags, tagOthers)
|
|
}
|
|
root.Commands = append(root.Commands, sub)
|
|
subcommands = append(subcommands, sub.Id)
|
|
}
|
|
|
|
yamlDoc := CmdDoc{
|
|
Id: strings.ReplaceAll(cmd.CommandPath(), " ", "-"),
|
|
Title: cmd.CommandPath(),
|
|
Summary: forceMultiLine(cmd.Short),
|
|
Description: forceMultiLine(strings.ReplaceAll(cmd.Long, "\t", " ")),
|
|
Subcommands: subcommands,
|
|
}
|
|
|
|
names := strings.Fields(cmd.CommandPath())
|
|
if len(names) > 3 {
|
|
base := strings.Join(names[2:], "-")
|
|
names = append(names[:2], base)
|
|
}
|
|
path := filepath.Join(names...) + ".md"
|
|
if contents, err := docsDir.ReadFile(path); err == nil {
|
|
noHeader := bytes.TrimLeftFunc(contents, func(r rune) bool {
|
|
return r != '\n'
|
|
})
|
|
yamlDoc.Description = forceMultiLine(string(noHeader))
|
|
}
|
|
|
|
if eg, ok := examples[yamlDoc.Id]; ok {
|
|
yamlDoc.Examples = eg
|
|
}
|
|
|
|
if len(cmd.GroupID) > 0 {
|
|
yamlDoc.Tags = append(yamlDoc.Tags, cmd.GroupID)
|
|
}
|
|
|
|
if cmd.Runnable() {
|
|
yamlDoc.Usage = forceMultiLine(cmd.UseLine())
|
|
}
|
|
|
|
// Only print flags for root and leaf commands
|
|
if !cmd.HasSubCommands() {
|
|
flags := cmd.LocalFlags()
|
|
flags.VisitAll(func(flag *pflag.Flag) {
|
|
if !flag.Hidden {
|
|
yamlDoc.Flags = append(yamlDoc.Flags, getFlags(flag))
|
|
}
|
|
})
|
|
// Print required flag for experimental commands
|
|
globalFlags := cmd.Root().Flags()
|
|
if cli.IsExperimental(cmd) {
|
|
flag := globalFlags.Lookup("experimental")
|
|
yamlDoc.Flags = append(yamlDoc.Flags, getFlags(flag))
|
|
}
|
|
// Leaf commands should inherit parent flags except root
|
|
parentFlags := cmd.InheritedFlags()
|
|
parentFlags.VisitAll(func(flag *pflag.Flag) {
|
|
if !flag.Hidden && globalFlags.Lookup(flag.Name) == nil {
|
|
yamlDoc.Flags = append(yamlDoc.Flags, getFlags(flag))
|
|
}
|
|
})
|
|
}
|
|
|
|
return yamlDoc
|
|
}
|
|
|
|
func getFlags(flag *pflag.Flag) FlagDoc {
|
|
doc := FlagDoc{
|
|
Id: flag.Name,
|
|
Name: getName(flag),
|
|
Description: forceMultiLine(getUsage(flag)),
|
|
DefaultValue: flag.DefValue,
|
|
Required: flag.Annotations[cobra.BashCompOneRequiredFlag] != nil,
|
|
}
|
|
if f, ok := flag.Value.(*utils.EnumFlag); ok {
|
|
for _, v := range f.Allowed {
|
|
doc.AcceptedValues = append(doc.AcceptedValues, ValueDoc{
|
|
Id: v,
|
|
Name: v,
|
|
Type: flag.Value.Type(),
|
|
})
|
|
}
|
|
}
|
|
return doc
|
|
}
|
|
|
|
// Prints a human readable flag name.
|
|
//
|
|
// -f, --flag `string`
|
|
func getName(flag *pflag.Flag) (line string) {
|
|
// Prefix: shorthand
|
|
if flag.Shorthand != "" && flag.ShorthandDeprecated == "" {
|
|
line += fmt.Sprintf("-%s, ", flag.Shorthand)
|
|
}
|
|
line += fmt.Sprintf("--%s", flag.Name)
|
|
// Suffix: type
|
|
if varname, _ := pflag.UnquoteUsage(flag); varname != "" {
|
|
line += fmt.Sprintf(" <%s>", varname)
|
|
}
|
|
// Not used by our cmd but kept here for consistency
|
|
if flag.NoOptDefVal != "" {
|
|
switch flag.Value.Type() {
|
|
case "string":
|
|
line += fmt.Sprintf("[=\"%s\"]", flag.NoOptDefVal)
|
|
case "bool":
|
|
if flag.NoOptDefVal != "true" {
|
|
line += fmt.Sprintf("[=%s]", flag.NoOptDefVal)
|
|
}
|
|
case "count":
|
|
if flag.NoOptDefVal != "+1" {
|
|
line += fmt.Sprintf("[=%s]", flag.NoOptDefVal)
|
|
}
|
|
default:
|
|
line += fmt.Sprintf("[=%s]", flag.NoOptDefVal)
|
|
}
|
|
}
|
|
return line
|
|
}
|
|
|
|
// Prints flag usage and default value.
|
|
//
|
|
// Select a plan. (default "free")
|
|
func getUsage(flag *pflag.Flag) string {
|
|
_, usage := pflag.UnquoteUsage(flag)
|
|
return usage
|
|
}
|
|
|
|
// Yaml lib generates incorrect yaml with long strings that do not contain \n.
|
|
//
|
|
// example: 'a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a
|
|
// a a a a a a '
|
|
func forceMultiLine(s string) string {
|
|
if len(s) > 60 && !strings.Contains(s, "\n") {
|
|
s = s + "\n"
|
|
}
|
|
return s
|
|
}
|
|
|
|
func getTags(cmd *cobra.Command) (tags []TagDoc) {
|
|
for _, group := range cmd.Groups() {
|
|
tags = append(tags, TagDoc{
|
|
Id: group.ID,
|
|
Title: group.Title[:len(group.Title)-1],
|
|
})
|
|
}
|
|
tags = append(tags, TagDoc{Id: tagOthers, Title: "Additional Commands"})
|
|
return tags
|
|
}
|