supabase-cli/docs/main.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
}