package main import ( "fmt" "io/ioutil" "os" "os/exec" "path/filepath" "strings" "github.com/spf13/cobra" "gopkg.in/yaml.v3" ) /* TODO I've finished writing the "contrib" half of `town contrib`. but what about the other half? this is two pieces: 1. as an admin, reviewing what's been submitted and: - discarding - clarifying - accepting 2. as a user, running `town` in order to discover and run contrib'ed commands. for 2, I think it's fine for now to just let the usage for town get long. as far as how to manage this, i think moving to a folder of yaml files that have an execPath and then dynamically building the command list up from the yaml files works. that way, the contrib yaml can just be copied over (renaming to command name and stripping comments) to the accepted path (/town/commands/contrib or whatever). for 1, it can just be manually copying the files but that could easily lead to brainrot and errors, so i want automation there. how about: town contrib --review? it would: - list all the commands that have been contribed so dupes can be looked at - iterate through each with: - edit - approve - reject - pass - contact (email) */ const binroot = "/town/commands" var rootCmd = &cobra.Command{ Use: "town", Short: "Run commands unique to tilde.town", // TODO Long example showing how to contribute a command } func parseCommands(targetCmd *cobra.Command, path string) error { binPath := filepath.Join(binroot, path) files, err := ioutil.ReadDir(binPath) if err != nil { return fmt.Errorf("failed to list directory %s: %s", binPath, err) } for _, file := range files { if strings.HasSuffix(file.Name(), "yml") { parseCommand(targetCmd, filepath.Join(binPath, file.Name())) } } return nil } type cmdDoc struct { CmdName string ExecPath string Maintainer string Category string ShortDesc string LongDesc string } func parseCommand(targetCmd *cobra.Command, yamlPath string) { yamlBytes, err := ioutil.ReadFile(yamlPath) if err != nil { fmt.Fprintf(os.Stderr, "could not read %s; skipping...\n", yamlPath) return } var doc cmdDoc err = yaml.Unmarshal(yamlBytes, &doc) if err != nil { fmt.Fprintf(os.Stderr, "could not parse %s; skipping...\n", yamlPath) return } parsedCmd := &cobra.Command{ Use: doc.CmdName, RunE: execWrapper(doc.ExecPath), DisableFlagParsing: true, } if doc.ShortDesc != "" { parsedCmd.Short = doc.ShortDesc } if doc.LongDesc != "" { parsedCmd.Long = doc.LongDesc } parsedCmd.Long += fmt.Sprintf("\nMaintained by %s; reach out to them via mail or chat with questions", doc.Maintainer) targetCmd.AddCommand(parsedCmd) } func execWrapper(executablePath string) func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { execCmd := exec.Command(executablePath, args...) execCmd.Stderr = os.Stderr execCmd.Stdout = os.Stdout execCmd.Stdin = os.Stdin return execCmd.Run() } } func cli() int { files, err := ioutil.ReadDir(binroot) if err != nil { fmt.Fprintf(os.Stderr, "failed to list directory %s: %s", binroot, err) } for _, file := range files { if strings.HasSuffix(file.Name(), "yml") { parseCommand(rootCmd, filepath.Join(binroot, file.Name())) } } // I feel like the example/documentation yaml can be frontmatter for non-binary files. to start // i'll just do the accompanying yaml file. rootCmd.Execute() return 0 } func main() { os.Exit(cli()) }