This commit massively overhauls the project's structure to simplify development. Most parts are now correctly compartmentalized and dependencies are passed in a sane way rather than global variables galore xd.
234 lines
5.7 KiB
Go
234 lines
5.7 KiB
Go
package main
|
|
|
|
import (
|
|
_ "embed"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/agnivade/levenshtein"
|
|
"github.com/juls0730/flux/cmd/cli/commands"
|
|
util "github.com/juls0730/flux/internal/util/cli"
|
|
"github.com/juls0730/flux/pkg"
|
|
"github.com/juls0730/flux/pkg/API"
|
|
"github.com/mattn/go-isatty"
|
|
)
|
|
|
|
func isInteractive() bool {
|
|
return isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())
|
|
}
|
|
|
|
//go:embed config.json
|
|
var config []byte
|
|
|
|
var configPath = filepath.Join(os.Getenv("HOME"), "/.config/flux")
|
|
|
|
var version = pkg.Version
|
|
|
|
var helpStr = `Usage:
|
|
flux <command>
|
|
|
|
Available Commands:
|
|
%s
|
|
|
|
Available Flags:
|
|
--help, -h: Show this help message
|
|
|
|
Use "flux <command> --help" for more information about a command.
|
|
`
|
|
|
|
var maxDistance = 3
|
|
|
|
type Command struct {
|
|
Help string
|
|
HandlerFunc commands.CommandFunc
|
|
}
|
|
|
|
type CommandHandler struct {
|
|
commands map[string]Command
|
|
aliases map[string]string
|
|
}
|
|
|
|
func NewCommandHandler() CommandHandler {
|
|
return CommandHandler{
|
|
commands: make(map[string]Command),
|
|
aliases: make(map[string]string),
|
|
}
|
|
}
|
|
|
|
func (h *CommandHandler) RegisterCmd(name string, handler commands.CommandFunc, help string) {
|
|
coomand := Command{
|
|
Help: help,
|
|
HandlerFunc: handler,
|
|
}
|
|
|
|
h.commands[name] = coomand
|
|
}
|
|
|
|
func (h *CommandHandler) RegisterAlias(alias string, command string) {
|
|
h.aliases[alias] = command
|
|
}
|
|
|
|
// returns the command and whether or not it exists
|
|
func (h *CommandHandler) GetCommand(command string) (Command, bool) {
|
|
if command, ok := h.aliases[command]; ok {
|
|
return h.commands[command], true
|
|
}
|
|
|
|
commandStruct, ok := h.commands[command]
|
|
return commandStruct, ok
|
|
}
|
|
|
|
var helpPadding = 13
|
|
|
|
func (h *CommandHandler) GetHelp() {
|
|
commandsStr := ""
|
|
for command := range h.commands {
|
|
curLine := ""
|
|
|
|
curLine += command
|
|
for alias, aliasCommand := range h.aliases {
|
|
if aliasCommand == command {
|
|
curLine += fmt.Sprintf(", %s", alias)
|
|
}
|
|
}
|
|
|
|
curLine += strings.Repeat(" ", helpPadding-(len(curLine)-2))
|
|
commandsStr += fmt.Sprintf(" %s %s\n", curLine, h.commands[command].Help)
|
|
}
|
|
|
|
fmt.Printf(helpStr, strings.TrimRight(commandsStr, "\n"))
|
|
}
|
|
|
|
func (h *CommandHandler) GetHelpCmd(commands.CommandCtx, []string) error {
|
|
h.GetHelp()
|
|
return nil
|
|
}
|
|
|
|
func runCommand(command string, args []string, config pkg.CLIConfig, info API.Info, cmdHandler CommandHandler) error {
|
|
commandCtx := commands.CommandCtx{
|
|
Config: config,
|
|
Info: info,
|
|
Interactive: isInteractive(),
|
|
}
|
|
|
|
commandStruct, ok := cmdHandler.commands[command]
|
|
if ok {
|
|
return commandStruct.HandlerFunc(commandCtx, args)
|
|
}
|
|
|
|
// diff the command against the list of commands and if we find a command that is more than 80% similar, ask if that's what the user meant
|
|
var closestMatch struct {
|
|
name string
|
|
score int
|
|
}
|
|
for cmdName := range cmdHandler.commands {
|
|
distance := levenshtein.ComputeDistance(cmdName, command)
|
|
|
|
if distance <= maxDistance {
|
|
if closestMatch.name == "" || distance < closestMatch.score {
|
|
closestMatch.name = cmdName
|
|
closestMatch.score = distance
|
|
}
|
|
}
|
|
}
|
|
|
|
if closestMatch.name == "" {
|
|
return fmt.Errorf("unknown command: %s", command)
|
|
}
|
|
|
|
var response string
|
|
// new line ommitted because it will be produced when the user presses enter to submit their response
|
|
fmt.Printf("No command found with the name '%s'. Did you mean '%s'? (y/N)", command, closestMatch.name)
|
|
fmt.Scanln(&response)
|
|
|
|
if strings.ToLower(response) == "y" || strings.ToLower(response) == "yes" {
|
|
command = closestMatch.name
|
|
} else {
|
|
return nil
|
|
}
|
|
|
|
// re-run command after accepting the suggestion
|
|
return runCommand(command, args, config, info, cmdHandler)
|
|
}
|
|
|
|
func main() {
|
|
if !isInteractive() {
|
|
fmt.Printf("Flux is being run non-interactively\n")
|
|
}
|
|
|
|
cmdHandler := NewCommandHandler()
|
|
|
|
cmdHandler.RegisterCmd("init", commands.InitCommand, "Initialize a new project")
|
|
cmdHandler.RegisterCmd("deploy", commands.DeployCommand, "Deploy a new version of the app")
|
|
cmdHandler.RegisterCmd("start", commands.StartCommand, "Start the app")
|
|
cmdHandler.RegisterCmd("stop", commands.StopCommand, "Stop the app")
|
|
cmdHandler.RegisterCmd("list", commands.ListCommand, "List all the apps")
|
|
cmdHandler.RegisterCmd("delete", commands.DeleteCommand, "Delete the app")
|
|
|
|
fs := flag.NewFlagSet("flux", flag.ExitOnError)
|
|
fs.Usage = func() {
|
|
cmdHandler.GetHelp()
|
|
}
|
|
|
|
err := fs.Parse(os.Args[1:])
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if len(os.Args) < 2 {
|
|
cmdHandler.GetHelp()
|
|
os.Exit(1)
|
|
}
|
|
|
|
if _, err := os.Stat(filepath.Join(configPath, "config.json")); err != nil {
|
|
if err := os.MkdirAll(configPath, 0755); err != nil {
|
|
fmt.Printf("Failed to create config directory: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if err = os.WriteFile(filepath.Join(configPath, "config.json"), config, 0644); err != nil {
|
|
fmt.Printf("Failed to write config file: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
var config pkg.CLIConfig
|
|
configBytes, err := os.ReadFile(filepath.Join(configPath, "config.json"))
|
|
if err != nil {
|
|
fmt.Printf("Failed to read config file: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if err := json.Unmarshal(configBytes, &config); err != nil {
|
|
fmt.Printf("Failed to parse config file: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if config.DaemonURL == "" {
|
|
fmt.Printf("Daemon URL is empty\n")
|
|
os.Exit(1)
|
|
}
|
|
|
|
info, err := util.GetRequest[API.Info](config.DaemonURL + "/heartbeat")
|
|
if err != nil {
|
|
fmt.Printf("Failed to connect to daemon\n")
|
|
os.Exit(1)
|
|
}
|
|
|
|
if info.Version != version {
|
|
fmt.Printf("Version mismatch, daemon is running version %s, but you are running version %s\n", info.Version, version)
|
|
os.Exit(1)
|
|
}
|
|
|
|
err = runCommand(os.Args[1], fs.Args()[1:], config, *info, cmdHandler)
|
|
if err != nil {
|
|
fmt.Printf("Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|