town/cmd/launcher/main.go

188 lines
4.5 KiB
Go
Raw Normal View History

2022-07-30 13:45:37 +00:00
package main
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"os/user"
"path/filepath"
"strings"
2022-07-31 08:57:45 +00:00
townuser "git.tilde.town/tildetown/town/user"
2022-07-30 13:45:37 +00:00
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)
2022-10-29 19:20:10 +00:00
/* 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)
*/
2022-07-30 13:45:37 +00:00
const binroot = "/town/commands"
var rootCmd = &cobra.Command{
Use: "town",
Short: "Run commands unique to tilde.town",
}
var adminCmd = &cobra.Command{
Use: "admin",
Short: "Run administrative commands",
}
func isAdmin() (bool, error) {
u, err := user.Current()
if err != nil {
return false, fmt.Errorf("failed to get information about current user: %w", err)
}
2022-07-31 08:57:45 +00:00
return townuser.IsAdmin(u)
2022-07-30 13:45:37 +00:00
}
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 commandDoc struct {
ShortDesc string `yaml:"shortDesc"`
LongDesc string `yaml:"longDesc"`
Examples string
Maintainer string
}
func parseCommand(targetCmd *cobra.Command, yamlPath string) {
executablePath := strings.TrimSuffix(yamlPath, ".yml")
// TODO handle when files lack executable bit
_, err := os.Stat(executablePath)
if err != nil {
fmt.Fprintf(os.Stderr, "could not find matching executable for %s; skipping...\n", yamlPath)
return
}
yamlBytes, err := ioutil.ReadFile(yamlPath)
if err != nil {
fmt.Fprintf(os.Stderr, "could not read %s; skipping...\n", yamlPath)
return
}
doc := commandDoc{}
err = yaml.Unmarshal(yamlBytes, &doc)
if err != nil {
fmt.Fprintf(os.Stderr, "could not parse %s; skipping...\n", yamlPath)
return
}
if doc.Maintainer == "" {
fmt.Fprintf(os.Stderr, "%s is missing maintainer field; skipping...\n", yamlPath)
return
}
parsedCmd := &cobra.Command{
Use: filepath.Base(executablePath),
RunE: execWrapper(executablePath),
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)
if doc.Examples != "" {
parsedCmd.Example = doc.Examples
}
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 {
err := parseCommands(rootCmd, "core")
if err != nil {
fmt.Fprintf(os.Stderr, "failed to parse core commands: %s", err)
return 1
}
err = parseCommands(rootCmd, "contrib")
if err != nil {
fmt.Fprintf(os.Stderr, "failed to parse contrib commands: %s", err)
return 1
}
admin, err := isAdmin()
if err != nil {
fmt.Fprintf(os.Stderr, "failed to check admin status: %s", err)
return 2
}
if admin {
rootCmd.AddCommand(adminCmd)
err = parseCommands(adminCmd, "admin")
if err != nil {
fmt.Fprintf(os.Stderr, "failed to parse admin commands: %s", err)
return 1
}
}
// 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())
}