town/cmd/launcher/main.go

140 lines
3.4 KiB
Go

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())
}