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 }