Compare commits

..

30 Commits
v0.3.2 ... main

Author SHA1 Message Date
0e50a4908b readme update
All checks were successful
code scans / scans (push) Successful in 38s
2025-06-23 11:17:31 -04:00
5e91816682 version bump
All checks were successful
code scans / scans (push) Successful in 34s
2025-06-19 18:03:53 -04:00
5de46fecc1 Merge pull request 'fix/sqlite' (#10) from fix/sqlite into main
All checks were successful
code scans / scans (push) Successful in 35s
Reviewed-on: #10
2025-06-19 21:58:10 +00:00
b480efc6ec vuln patch
All checks were successful
code scans / scans (pull_request) Successful in 48s
2025-06-19 17:55:11 -04:00
5d580e7766 readme update
Some checks failed
code scans / scans (pull_request) Failing after 13s
2025-06-19 17:53:12 -04:00
f421bd906a small fixes before merge
Some checks failed
code scans / scans (pull_request) Failing after 14s
2025-06-19 17:34:53 -04:00
71ace969d3 null pointer fix
- need to nil check before pulling value
2025-06-19 17:28:56 -04:00
7ff43c82c2 [fix] Testing fixes
- sqlite error handling and wrapping
- more responsive commands
- database updates
2025-06-19 17:23:17 -04:00
0d7fbffaf6 readme update 2025-06-19 15:29:40 -04:00
5e950f5e84 command run return value bugfix 2025-06-19 15:28:21 -04:00
d0bc383d71 [fix] base64 updates
- decoding fix
- removing extra logging
2025-06-19 15:23:56 -04:00
2791ca83f9 logging server for tests 2025-06-19 15:20:24 -04:00
fc109338a6 bugfixes
- comment out extra sql pull
- addressing ignored error
2025-06-19 15:17:38 -04:00
433e4302c2 active server bugfix
- missing name sql param
2025-06-19 15:13:16 -04:00
2045a75d68 [fix] bugfixes from testing
- sqlite wrapper func context bug
2025-06-19 15:05:09 -04:00
473bebb04d sql fixes 2025-06-19 00:07:52 -04:00
e4984c8941 timeout changes 2025-06-18 23:57:16 -04:00
c15c16be8d finishing sqlite changes
- finished default/active server logic
- dev done
- needs testing
2025-06-18 18:38:01 -04:00
fe37cac2da lots o progress
- added 'command' subcommand
- removed viper
- setup commands added
- still a WIP
- readme TODO update
2025-06-17 23:12:49 -04:00
77bb3166c4 starting sqlite rewrite
- adding db connector
- starting rewrite of commands
- WIP
2025-06-16 19:17:52 -04:00
58ece42142 mcr update and readme (#9)
All checks were successful
code scans / scans (push) Successful in 24s
Reviewed-on: #9
Co-authored-by: jake <jake.young.dev@gmail.com>
Co-committed-by: jake <jake.young.dev@gmail.com>
2025-05-31 15:11:03 +00:00
49c508aae7 [ci] workflow version bump
All checks were successful
code scans / scans (push) Successful in 23s
2025-05-17 13:50:04 -04:00
dcfad2dfee [ci] workflow update
Some checks failed
code scans / scans (push) Failing after 2s
- runner change
- workflow version bumps
2025-05-17 13:49:04 -04:00
5b6ddc71cf completion removal and workflow trigger update
All checks were successful
code scans / scans (push) Successful in 1m18s
2025-05-11 00:36:11 -04:00
f8282c3676 new/clear-command (#8)
Reviewed-on: #8
Co-authored-by: jake <jake.young.dev@gmail.com>
Co-committed-by: jake <jake.young.dev@gmail.com>
2025-05-11 04:27:47 +00:00
386a766185 Update .gitea/workflows/security.yaml (#7)
All checks were successful
code scans / scans (push) Successful in 1m15s
Reviewed-on: #7
2025-05-10 05:37:49 +00:00
26c50085d6 new/pipeline (#6)
All checks were successful
code scans / scans (push) Successful in 1m27s
Reviewed-on: #6
Co-authored-by: jake <jake.young.dev@gmail.com>
Co-committed-by: jake <jake.young.dev@gmail.com>
2025-04-24 18:22:16 +00:00
13d3b2cef3 feature/view-all (#5)
Reviewed-on: #5
Co-authored-by: jake <jake.young.dev@gmail.com>
Co-committed-by: jake <jake.young.dev@gmail.com>
2025-04-20 05:28:56 +00:00
4100762986 Update README.md 2025-04-19 05:18:26 +00:00
a3527d3388 dev/standards (#4)
Reviewed-on: #4
Co-authored-by: jake <jake.young.dev@gmail.com>
Co-committed-by: jake <jake.young.dev@gmail.com>
2025-04-19 05:13:53 +00:00
28 changed files with 1169 additions and 543 deletions

View File

@ -0,0 +1,17 @@
name: "code scans"
on:
push:
branches:
- main
tags:
- v*
pull_request:
jobs:
scans:
runs-on: fire
steps:
- uses: actions/checkout@v4
- name: "dependency scan and static code analysis"
uses: https://code.jakeyoungdev.com/actions/donotpassgo@v1.1.0

143
README.md
View File

@ -1,99 +1,110 @@
# mctl # mctl
mctl is a terminal-friendly remote connection client mctl is a terminal-friendly remote console client
# Index
1. [Installation](#installation)
2. [Use](#use)
4. [Security](#security)
5. [Development](#development)
## Installation
Install mctl using golang
```bash
go install code.jakeyoungdev.com/jake/mctl@main #it is recommended to use a tagged version
```
<br /> <br />
## Setup # Installation
Install mctl using golang
```bash
go install code.jakeyoungdev.com/jake/mctl@main
```
<br />
# Use
### Configuring mctl ### Configuring mctl
mctl requires a one-time setup via the 'config' command before interacting with any servers, password is entered securely from the terminal mctl requires a one-time setup via the 'init' command before using any other commands
```bash ```bash
mctl config -s <serveraddress> -p <rconport> mctl init
``` ```
### Connecting to server <br />
Once the client has been configured commands can be sent to the server using the 'login' command. This will authenticate you with the game server and enter the command loop, the session is logged out when the command loop is broken
### Add a server
To communicate with servers, they must be first added to the server list
```bash ```bash
mctl login #makes auth request to server with saved password mctl server add
Server alias: home
Server address: x.x.x.x
Server port: 61695
Password:
``` ```
### Sending commands <br />
If login is successful the app will enter the command loop, which allows commands to be sent directly to the server until 'mctl' is sent. Commands are sent as-is to the server, there is no validation of command syntax within mctl
```
Logging into X.X.X.X on port 61695
Connected! Type 'mctl' to close
RCON@X.X.X.X /> list
There are 0 of a max of 20 players online: ### Activating a server
``` Servers can be activated to be used as the default server for mctl operations, the server alias must be
provided to activate it
### Saving commands
Commands can be saved under an alias for quick execution later, saved commands can contain placeholders '%s' that can be populated at runtime to allow for commands with unique runtime args to still be saved:
```bash ```bash
mctl save <name> mctl server activate <name>
``` ```
### Viewing commands <br />
Saved commands can be viewed with:
### Login to server
To login to the activated server and start sending commands
```bash ```bash
mctl view <name> mctl login
``` ```
The -s flag can be set to login to a non-activated server
### Running saved commands
Commands that have been saved can be run with:
```bash ```bash
mctl run <name> mctl login -s <name>
``` ```
If the saved command contains placeholders, the necessary arguments must be supplied:
<br />
### Savind a command
Commands can be saved to be run later
```bash ```bash
mctl run <name> args... mctl command add
Command alias: seed
Command: seed
``` ```
### Saving and running example The command saved can then be run with the following command, this will run the 'seed' command on the activated
server, or a different server using the -s flag
```bash ```bash
#saving command named "test" to run "tp %s 0 0 0" mctl command run seed
mctl save test
Command: tp %s 0 0 0
#run command on user "jake"
mctl run test jake
#will run: tp jake 0 0 0 on remote server
``` ```
### Delete saved command Commands can contain placeholders to be filled in at runtime
Commands can be deleted with:
```bash ```bash
mctl delete <name> #created the 'placeholder' command which runs the 'kill' command
mctl command add
Command alias: placeholder
Command: kill %s
```
```bash
#runs the 'kill' command on 'player'
mctl command run placeholder player
```
Running the placeholder command with the arg 'player' will run 'kill player' on the server
<br />
### Clearing database
The mctl database can be cleared with the destoy command. WARNING, this command will delete any saved servers or commands completely resetting mctl
```bash
mctl destroy
``` ```
## Documentation ### CRUD Commands
### Commands Other create, read, update, and delete versions of subcommands are available. Use the -h flag for more information
|Command|Description|
|---|---|
|config|used to update the config file|
|login|makes connection request to the server using saved configuration and enters command loop|
|save \<name>|saves specific command for reuse|
|view \<name>|displays saved command|
|delete \<name>|deletes saved command|
|run \<name> args...|runs saved command filling placeholders with supplied args|
### Flags <br />
#### config
|Flag|Shorthand|Required|Description|
|---|---|---|---|
|port|p|yes|RCon port|
|server|s|yes|RCon address|
### Configuration file # Security
All configuration data will be kept in /home/.mctl.yaml or C:\\Users\\username\\.mctl.yaml, passwords are encrypted for an added layer of security RCon is an inherently insecure protocol, passwords are sent in plaintext and, if possible, the port should not be exposed to the internet. It is best to keep these connections local or over a VPN.
## Security mctl utilizes [govulncheck](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck) and [gosec](https://github.com/securego/gosec) in workflows to ensure quality, secure code is being pushed. These workflow steps must pass before a PR will be accepted
RCon is an inherently insecure protocol, passwords are sent in plaintext and, if possible, the port should not be exposed to the internet. It is best to keep these connections local or over a VPN
## Development <br />
this repo is currently in heavy development and may encounter breaking changes, use a tag to prevent any surprises
# Development
this repo is currently in development and may encounter breaking changes, use a tag to prevent any surprises

View File

@ -1,20 +1,20 @@
package client package client
import ( import (
"encoding/base64"
"fmt" "fmt"
"code.jakeyoungdev.com/jake/mctl/cryptography" "code.jakeyoungdev.com/jake/mctl/database"
"code.jakeyoungdev.com/jake/mctl/model"
"github.com/jake-young-dev/mcr" "github.com/jake-young-dev/mcr"
"github.com/spf13/viper"
) )
/* /*
This is a simple wrapper for the MCR client to provide easy use of mcr without having to manually This is a simple wrapper for the MCR client to provide easy use of rcon
decrypt the password/hit viper each time.
*/ */
type Client struct { type Client struct {
cli *mcr.Client cli mcr.Client
} }
type IClient interface { type IClient interface {
@ -23,22 +23,35 @@ type IClient interface {
} }
// creates a new mcr client using saved credentials and decrypted password // creates a new mcr client using saved credentials and decrypted password
func New() (*Client, error) { func New(name string) (*Client, error) {
//grab saved credentials db, err := database.New()
server := viper.Get("server").(string) if err != nil {
password := viper.Get("password").(string) return nil, err
port := viper.Get("port").(int) }
fmt.Printf("Logging into %s on port %d\n", server, port) defer db.Close()
//decrypt password var srv model.Server
pt, err := cryptography.DecryptPassword([]byte(password)) if name != "" {
srv, err = db.GetServer(name)
if err != nil {
return nil, err
}
} else {
srv, err = db.GetActiveServer()
if err != nil {
return nil, err
}
}
fmt.Printf("Logging into %s on port %d\n", srv.Server, srv.Port)
p, err := base64.StdEncoding.DecodeString(srv.Password)
if err != nil { if err != nil {
return nil, err return nil, err
} }
//connect to game server //connect to game server
cli := mcr.NewClient(server, mcr.WithPort(port)) cli := mcr.NewClient(srv.Server, mcr.WithPort(srv.Port))
err = cli.Connect(string(pt)) err = cli.Connect(string(p))
if err != nil { if err != nil {
return nil, err return nil, err
} }

58
cmd/command/add.go Normal file
View File

@ -0,0 +1,58 @@
/*
Copyright © 2025 Jake jake.young.dev@gmail.com
*/
package command
import (
"bufio"
"fmt"
"os"
"code.jakeyoungdev.com/jake/mctl/database"
"github.com/spf13/cobra"
)
var addCmd = &cobra.Command{
Use: "add",
Example: "mctl command add",
Short: "Saves a new command to the database",
SilenceUsage: true,
Run: func(cmd *cobra.Command, args []string) {
scanner := bufio.NewScanner(os.Stdin)
//get server information
var cfgname string
fmt.Printf("Command alias: ")
if scanner.Scan() {
cfgname = scanner.Text()
}
var cfgcmd string
fmt.Printf("Command: ")
if scanner.Scan() {
cfgcmd = scanner.Text()
}
db, err := database.New()
cobra.CheckErr(err)
defer db.Close()
err = db.Init()
cobra.CheckErr(err)
err = db.SaveCmd(cfgname, cfgcmd)
if err != nil {
if err.Error() == ErrInit {
fmt.Println(ErrInitRsp)
return
}
}
cobra.CheckErr(err)
fmt.Println("Command saved")
},
}
func init() {
CommandCmd.AddCommand(addCmd)
}

23
cmd/command/command.go Normal file
View File

@ -0,0 +1,23 @@
/*
Copyright © 2025 Jake jake.young.dev@gmail.com
*/
package command
import (
"github.com/spf13/cobra"
)
const (
//sqlite doesn't have an error type for this error, but we want to catch when this error is thrown
//and provide the proper response. It should not be treated as an error if the db isn't setup.
ErrInit = "sqlite3: SQL logic error: no such table: commands"
ErrInitRsp = "The 'init' command must be run before mctl can be used"
)
// CommandCmd is such a cool name lol
var CommandCmd = &cobra.Command{
Use: "command",
Example: "mctl command <subcommand>",
}
func init() {}

45
cmd/command/delete.go Normal file
View File

@ -0,0 +1,45 @@
/*
Copyright © 2025 Jake jake.young.dev@gmail.com
*/
package command
import (
"errors"
"fmt"
"code.jakeyoungdev.com/jake/mctl/database"
"github.com/spf13/cobra"
)
var deleteCmd = &cobra.Command{
Use: "delete",
Example: "mctl command delete <name>",
Short: "Deletes a command from the database",
SilenceUsage: true,
Run: func(cmd *cobra.Command, args []string) {
db, err := database.New()
cobra.CheckErr(err)
defer db.Close()
err = db.DeleteCmd(args[0])
if err != nil {
if err.Error() == ErrInit {
fmt.Println(ErrInitRsp)
return
}
}
cobra.CheckErr(err)
fmt.Println("Command deleted")
},
PreRunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New("name parameter missing")
}
return nil
},
}
func init() {
CommandCmd.AddCommand(deleteCmd)
}

80
cmd/command/run.go Normal file
View File

@ -0,0 +1,80 @@
/*
Copyright © 2025 Jake jake.young.dev@gmail.com
*/
package command
import (
"errors"
"fmt"
"strings"
"code.jakeyoungdev.com/jake/mctl/client"
"code.jakeyoungdev.com/jake/mctl/database"
"github.com/spf13/cobra"
)
var (
cfgserver string
)
var runCmd = &cobra.Command{
Use: "run",
Example: "mctl command run -s <server> <command> args...",
Short: "Runs a saved command on a server",
Long: `Runs the named command with the provided args on the default/active server unless -s is specified`,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
cname := args[0]
db, err := database.New()
if err != nil {
return err
}
defer db.Close()
crun, err := db.GetCmd(cname)
if err != nil {
if err.Error() == ErrInit {
fmt.Println(ErrInitRsp)
return nil
}
return err
}
count := strings.Count(crun, "%s")
l := len(args)
if l-1 != count {
return fmt.Errorf("not enough arguments to fill all command placeholders, required: %d - have: %d", count, l-1)
}
cli, err := client.New(cfgserver)
cobra.CheckErr(err)
defer cli.Close()
var fargs []any
for _, x := range args[1:] {
fargs = append(fargs, x)
}
res, err := cli.Command(fmt.Sprintf(crun, fargs...))
if err != nil {
return err
}
fmt.Println(res)
return nil
},
PreRunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New("name parameter required")
}
return nil
},
}
func init() {
runCmd.Flags().StringVarP(&cfgserver, "server", "s", "", "server name")
CommandCmd.AddCommand(runCmd)
}

42
cmd/command/view.go Normal file
View File

@ -0,0 +1,42 @@
/*
Copyright © 2025 Jake jake.young.dev@gmail.com
*/
package command
import (
"fmt"
"code.jakeyoungdev.com/jake/mctl/database"
"github.com/spf13/cobra"
)
var viewCmd = &cobra.Command{
Use: "view",
Example: "mctl command view",
Short: "view all saved commands",
SilenceUsage: true,
Run: func(cmd *cobra.Command, args []string) {
db, err := database.New()
cobra.CheckErr(err)
defer db.Close()
ts, err := db.GetAllCmds()
if err != nil {
if err.Error() == ErrInit {
fmt.Println(ErrInitRsp)
return
}
}
cobra.CheckErr(err)
for _, s := range ts {
fmt.Println("-----")
fmt.Printf("Name: %s\n", s.Name)
fmt.Printf("Command: %s\n", s.Command)
}
},
}
func init() {
CommandCmd.AddCommand(viewCmd)
}

View File

@ -1,97 +0,0 @@
/*
Copyright © 2025 Jake jake.young.dev@gmail.com
*/
package cmd
import (
"crypto/rand"
"fmt"
"io"
"os"
"code.jakeyoungdev.com/jake/mctl/cryptography"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/term"
)
var (
cfgserver string
cfgport int
)
// configCmd represents the config command
var configCmd = &cobra.Command{
Use: "config",
Example: "mctl config -s x.x.x.x -p 61695",
Short: "Create and populate config file",
Long: `Creates the .mctl file in the user home directory
populating it with the server address, rcon password, and
rcon port to be pulled when using Login command`,
Run: func(cmd *cobra.Command, args []string) {
//read in password using term to keep it secure/hidden from bash history
fmt.Printf("Password: ")
ps, err := term.ReadPassword(int(os.Stdin.Fd()))
cobra.CheckErr(err)
//generate and apply random nonce
nonce := make([]byte, 12)
_, err = io.ReadFull(rand.Reader, nonce)
cobra.CheckErr(err)
viper.Set("nonce", string(nonce))
//encrypt password
ciphert, err := cryptography.EncryptPassword(ps)
cobra.CheckErr(err)
//update config file with new values
viper.Set("server", cfgserver)
viper.Set("password", string(ciphert))
viper.Set("port", cfgport)
viper.WriteConfig()
fmt.Println()
fmt.Println("Config file updated!")
},
}
func init() {
initConfig()
configCmd.Flags().StringVarP(&cfgserver, "server", "s", "", "server address")
configCmd.MarkFlagRequired("server")
configCmd.Flags().IntVarP(&cfgport, "port", "p", 0, "server rcon port")
configCmd.MarkFlagRequired("port")
rootCmd.AddCommand(configCmd)
}
// init config sets viper config and checks for config file, creating it if it doesn't exist
func initConfig() {
home, err := os.UserHomeDir()
cobra.CheckErr(err)
viper.AddConfigPath(home)
viper.SetConfigType("yaml")
viper.SetConfigName(".mctl")
viper.AutomaticEnv()
viper.ReadInConfig()
if err := viper.ReadInConfig(); err != nil {
//file does not exist, create it
viper.Set("server", cfgserver)
viper.Set("password", "")
viper.Set("port", cfgport)
viper.Set("nonce", "")
//generate psuedo-random key
uu := make([]byte, 32)
_, err := rand.Read(uu)
cobra.CheckErr(err)
//create custom command map
cmdMap := make(map[string]any, 0)
//write config
viper.Set("customcmd", cmdMap)
viper.Set("device", string(uu))
viper.SafeWriteConfig()
}
}

View File

@ -1,42 +0,0 @@
/*
Copyright © 2025 Jake jake.young.dev@gmail.com
*/
package cmd
import (
"errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// deleteCmd represents the delete command
var deleteCmd = &cobra.Command{
Use: "delete <name>",
Example: "mctl delete newcmd",
Short: "Delete a saved command",
Long: `Deletes a command stored using the save command`,
Run: func(cmd *cobra.Command, args []string) {
var cm map[string]any
cmdMap := viper.Get("customcmd")
if cmdMap == nil {
cm = make(map[string]any, 0)
} else {
cm = cmdMap.(map[string]any)
}
delete(cm, args[0])
viper.Set("customcmd", cmdMap)
viper.WriteConfig()
},
PreRunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New("name argument is required")
}
return nil
},
}
func init() {
rootCmd.AddCommand(deleteCmd)
}

52
cmd/destroy.go Normal file
View File

@ -0,0 +1,52 @@
/*
Copyright © 2025 Jake jake.young.dev@gmail.com
*/
package cmd
import (
"bufio"
"fmt"
"os"
"strings"
"code.jakeyoungdev.com/jake/mctl/cmd/command"
"code.jakeyoungdev.com/jake/mctl/database"
"github.com/spf13/cobra"
)
// destroyCmd represents the destroy command
var destroyCmd = &cobra.Command{
Use: "destroy",
Short: "clears all configuration",
Long: `clear all data and drop database tables, this will delete all previously saved data and the 'inti' command
must be run again before use`,
SilenceUsage: true,
Run: func(cmd *cobra.Command, args []string) {
scanner := bufio.NewScanner(os.Stdin)
fmt.Printf("Are you sure you want to destroy your config? (yes|no): ")
if scanner.Scan() {
if strings.EqualFold(scanner.Text(), "yes") {
db, err := database.New()
cobra.CheckErr(err)
defer db.Close()
err = db.Destroy()
if err != nil {
if err.Error() == command.ErrInit {
fmt.Println(command.ErrInitRsp)
return
}
}
cobra.CheckErr(err)
fmt.Println("Configuration is cleared, the 'init' command must be run again.")
}
} else {
return
}
},
}
func init() {
rootCmd.AddCommand(destroyCmd)
}

28
cmd/init.go Normal file
View File

@ -0,0 +1,28 @@
/*
Copyright © 2025 Jake jake.young.dev@gmail.com
*/
package cmd
import (
"code.jakeyoungdev.com/jake/mctl/database"
"github.com/spf13/cobra"
)
var initCmd = &cobra.Command{
Use: "init",
Example: "mctl init",
Short: "initializes the database tables, must be run before mctl can be used",
SilenceUsage: true,
Run: func(cmd *cobra.Command, args []string) {
db, err := database.New()
cobra.CheckErr(err)
defer db.Close()
err = db.Init()
cobra.CheckErr(err)
},
}
func init() {
rootCmd.AddCommand(initCmd)
}

View File

@ -5,36 +5,37 @@ package cmd
import ( import (
"bufio" "bufio"
"errors"
"fmt" "fmt"
"os" "os"
"code.jakeyoungdev.com/jake/mctl/client" "code.jakeyoungdev.com/jake/mctl/client"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" )
var (
server string
) )
// loginCmd represents the login command // loginCmd represents the login command
var loginCmd = &cobra.Command{ var loginCmd = &cobra.Command{
Use: "login", Use: "login",
Example: "mctl login", Example: "mctl login -s <server>",
SilenceUsage: true, SilenceUsage: true,
Short: "Login to server and send commands", Short: "Login to server and send commands",
Long: `Login to server using saved config and enter command loop Long: `Login to default server and enter command loop. The default server
sending commands to server and printing the response.`, is used unless the server flag is set`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
//grab saved credentials cli, err := client.New(server)
server := viper.Get("server").(string)
cli, err := client.New()
cobra.CheckErr(err) cobra.CheckErr(err)
defer cli.Close() defer cli.Close()
//start command loop //start command loop
fmt.Println("Connected! Type 'mctl' to close") fmt.Println("Connected! Type 'mctl' to close")
scanner := bufio.NewScanner(os.Stdin) scanner := bufio.NewScanner(os.Stdin)
var runningCmd string var runningCmd string
for runningCmd != "mctl" { for runningCmd != "mctl" {
fmt.Printf("RCON@%s /> ", server) fmt.Printf("RCON /> ")
if scanner.Scan() { if scanner.Scan() {
runningCmd = scanner.Text() runningCmd = scanner.Text()
@ -54,17 +55,11 @@ var loginCmd = &cobra.Command{
} }
} }
fmt.Printf("Disconnected from %s\n", server) fmt.Println("Disconnected")
},
PreRunE: func(cmd *cobra.Command, args []string) error {
//ensure config command has been run
if viper.Get("server") == "" || viper.Get("password") == "" || viper.Get("port") == 0 {
return errors.New("the 'config' command must be run before you can interact with servers")
}
return nil
}, },
} }
func init() { func init() {
loginCmd.Flags().StringVarP(&server, "server", "s", "", "server alias")
rootCmd.AddCommand(loginCmd) rootCmd.AddCommand(loginCmd)
} }

View File

@ -6,6 +6,8 @@ package cmd
import ( import (
"os" "os"
"code.jakeyoungdev.com/jake/mctl/cmd/command"
srv "code.jakeyoungdev.com/jake/mctl/cmd/server"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -14,8 +16,7 @@ var rootCmd = &cobra.Command{
Use: "mctl", Use: "mctl",
Short: "A remote console client", Short: "A remote console client",
Long: `mctl is a terminal-friendly remote console client made to manage game servers.`, Long: `mctl is a terminal-friendly remote console client made to manage game servers.`,
Version: "v0.3.2", Version: "v0.5.1",
// Run: func(cmd *cobra.Command, args []string) { },
} }
// Execute adds all child commands to the root command and sets flags appropriately. // Execute adds all child commands to the root command and sets flags appropriately.
@ -28,5 +29,9 @@ func Execute() {
} }
func init() { func init() {
rootCmd.CompletionOptions = cobra.CompletionOptions{
DisableDefaultCmd: true,
}
rootCmd.AddCommand(srv.ServerCmd)
rootCmd.AddCommand(command.CommandCmd) //the word command is in this four times, that can't be good.
} }

View File

@ -1,85 +0,0 @@
/*
Copyright © 2025 Jake jake.young.dev@gmail.com
*/
package cmd
import (
"errors"
"fmt"
"strings"
"code.jakeyoungdev.com/jake/mctl/client"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// runCmd represents the run command
var runCmd = &cobra.Command{
Use: "run <name> args...",
Example: "mctl run savedcmd 63 jake",
SilenceUsage: true,
Short: "Runs a previously saved command with supplied arguments on remote server",
Long: `Loads a saved command, injects the supplied arguments into the command, and sends the command to the remove server
printing the response`,
Run: func(cmd *cobra.Command, args []string) {
//check for command map
var cm map[string]any
cmdMap := viper.Get("customcmd")
if cmdMap == nil {
cm = make(map[string]any, 0)
} else {
cm = cmdMap.(map[string]any)
}
//is this an existing command
cmdRun, ok := cm[args[0]]
if !ok {
fmt.Printf("command %s not found", args[0])
return
}
//convert arguments to interface
var nargs []any
for _, a := range args[1:] {
nargs = append(nargs, a)
}
//inject arguments
fixed := fmt.Sprintf(cmdRun.(string), nargs...)
fmt.Printf("Running saved command %s\n", fixed)
//create client and send command
cli, err := client.New()
cobra.CheckErr(err)
defer cli.Close()
res, err := cli.Command(fixed)
cobra.CheckErr(err)
fmt.Println(res)
},
PreRunE: func(cmd *cobra.Command, args []string) error {
//ensure configuration has been setup
if viper.Get("server") == "" || viper.Get("password") == "" || viper.Get("port") == 0 {
return errors.New("the 'config' command must be run before you can interact with servers")
}
//ensure we have a command name
al := len(args)
if al == 0 {
return errors.New("name argument is required")
}
cmdMap := viper.Get("customcmd").(map[string]any)
count := strings.Count(cmdMap[args[0]].(string), "%s")
//make sure enough arguments are sent to fill command placeholders
if al < count+1 {
return fmt.Errorf("not enough arguments to populate command. Supplied: %d, Needed: %d", al-1, count)
}
return nil
},
}
func init() {
rootCmd.AddCommand(runCmd)
}

View File

@ -1,55 +0,0 @@
/*
Copyright © 2025 Jake jake.young.dev@gmail.com
*/
package cmd
import (
"bufio"
"errors"
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// saveCmd represents the save command
var saveCmd = &cobra.Command{
Use: "save <name>",
Example: "mctl save newcmd",
Short: "Saves a server command under an alias for quick execution",
Long: `Saves supplied command using alias <name> to allow the command to be executed using the run command. The %s placeholder can be
used as a wildcard to be injected when running the command`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Use %s as a wildcard in your command\n", "%s") //this is ugly, have to use printf to stop issues with compiler
fmt.Printf("Command: ")
sc := bufio.NewScanner(os.Stdin)
if sc.Scan() {
txt := sc.Text()
if txt != "" {
var cmdMap map[string]any
cm := viper.Get("customcmd")
if cmdMap == nil {
cmdMap = make(map[string]any, 0)
} else {
cmdMap = cm.(map[string]any)
}
cmdMap[args[0]] = txt
viper.Set("customcmd", cmdMap)
viper.WriteConfig()
fmt.Println("\nSaved!")
}
}
},
PreRunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New("name argument is required")
}
return nil
},
}
func init() {
rootCmd.AddCommand(saveCmd)
}

45
cmd/server/active.go Normal file
View File

@ -0,0 +1,45 @@
/*
Copyright © 2025 Jake jake.young.dev@gmail.com
*/
package server
import (
"errors"
"fmt"
"code.jakeyoungdev.com/jake/mctl/database"
"github.com/spf13/cobra"
)
var activeCmd = &cobra.Command{
Use: "activate",
Example: "mctl server active <server>",
Short: "sets the active server to run commands on",
SilenceUsage: true,
Run: func(cmd *cobra.Command, args []string) {
db, err := database.New()
cobra.CheckErr(err)
defer db.Close()
err = db.SetActiveServer(args[0])
if err != nil {
if err.Error() == ErrInit {
fmt.Println(ErrInitRsp)
return
}
}
cobra.CheckErr(err)
fmt.Println("Active server updated")
},
PreRunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New("server parameter missing")
}
return nil
},
}
func init() {
ServerCmd.AddCommand(activeCmd)
}

80
cmd/server/add.go Normal file
View File

@ -0,0 +1,80 @@
/*
Copyright © 2025 Jake jake.young.dev@gmail.com
*/
package server
import (
"bufio"
"encoding/base64"
"fmt"
"os"
"strconv"
"code.jakeyoungdev.com/jake/mctl/database"
"code.jakeyoungdev.com/jake/mctl/model"
"github.com/spf13/cobra"
"golang.org/x/term"
)
var addCmd = &cobra.Command{
Use: "add",
Example: "mctl server add",
Short: "Saves a new server configuration",
Long: `Saves server address, alias, port, and password to the database.`,
SilenceUsage: true,
Run: func(cmd *cobra.Command, args []string) {
scanner := bufio.NewScanner(os.Stdin)
//get server information
var cfgname string
fmt.Printf("Server alias: ")
if scanner.Scan() {
cfgname = scanner.Text()
}
var cfgaddress string
fmt.Printf("Server address: ")
if scanner.Scan() {
cfgaddress = scanner.Text()
}
var cfgport string
fmt.Printf("Server port: ")
if scanner.Scan() {
cfgport = scanner.Text()
}
//read in password using term to keep it secure/hidden from bash history
fmt.Printf("Password: ")
ps, err := term.ReadPassword(int(os.Stdin.Fd()))
cobra.CheckErr(err)
fmt.Println()
db, err := database.New()
cobra.CheckErr(err)
defer db.Close()
fp, err := strconv.Atoi(cfgport)
cobra.CheckErr(err)
err = db.SaveServer(model.Server{
Name: cfgname,
Server: cfgaddress,
Port: fp,
Password: base64.StdEncoding.EncodeToString(ps),
})
if err != nil {
if err.Error() == ErrInit {
fmt.Println(ErrInitRsp)
return
}
}
cobra.CheckErr(err)
fmt.Println("Server saved")
},
}
func init() {
ServerCmd.AddCommand(addCmd)
}

45
cmd/server/delete.go Normal file
View File

@ -0,0 +1,45 @@
/*
Copyright © 2025 Jake jake.young.dev@gmail.com
*/
package server
import (
"errors"
"fmt"
"code.jakeyoungdev.com/jake/mctl/database"
"github.com/spf13/cobra"
)
var deleteCmd = &cobra.Command{
Use: "delete",
Example: "mctl server delete <server>",
Short: "deletes a server from the database",
SilenceUsage: true,
Run: func(cmd *cobra.Command, args []string) {
db, err := database.New()
cobra.CheckErr(err)
defer db.Close()
err = db.DeleteServer(args[0])
if err != nil {
if err.Error() == ErrInit {
fmt.Println(ErrInitRsp)
return
}
}
cobra.CheckErr(err)
fmt.Println("Server deleted")
},
PreRunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New("server parameter missing")
}
return nil
},
}
func init() {
ServerCmd.AddCommand(deleteCmd)
}

22
cmd/server/server.go Normal file
View File

@ -0,0 +1,22 @@
/*
Copyright © 2025 Jake jake.young.dev@gmail.com
*/
package server
import (
"github.com/spf13/cobra"
)
const (
//sqlite doesn't have an error type for this error, but we want to catch when this error is thrown
//and provide the proper response. It should not be treated as an error if the db isn't setup.
ErrInit = "sqlite3: SQL logic error: no such table: servers"
ErrInitRsp = "The 'init' command must be run before mctl can be used"
)
var ServerCmd = &cobra.Command{
Use: "server",
Example: "mctl server <subcommand>",
}
func init() {}

54
cmd/server/update.go Normal file
View File

@ -0,0 +1,54 @@
/*
Copyright © 2025 Jake jake.young.dev@gmail.com
*/
package server
import (
"encoding/base64"
"errors"
"fmt"
"os"
"code.jakeyoungdev.com/jake/mctl/database"
"github.com/spf13/cobra"
"golang.org/x/term"
)
var updateCmd = &cobra.Command{
Use: "update",
Example: "mctl server update <name>",
Short: "updates a saved servers password in the database",
SilenceUsage: true,
Run: func(cmd *cobra.Command, args []string) {
//read in password using term to keep it secure/hidden from bash history
fmt.Printf("Password: ")
ps, err := term.ReadPassword(int(os.Stdin.Fd()))
cobra.CheckErr(err)
db, err := database.New()
cobra.CheckErr(err)
defer db.Close()
err = db.UpdateServer(args[0], base64.StdEncoding.EncodeToString(ps))
if err != nil {
if err.Error() == ErrInit {
fmt.Println(ErrInitRsp)
return
}
}
cobra.CheckErr(err)
fmt.Printf("%s password updated!", args[0])
},
PreRunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New("name command required")
}
return nil
},
}
func init() {
ServerCmd.AddCommand(updateCmd)
}

45
cmd/server/view.go Normal file
View File

@ -0,0 +1,45 @@
/*
Copyright © 2025 Jake jake.young.dev@gmail.com
*/
package server
import (
"fmt"
"code.jakeyoungdev.com/jake/mctl/database"
"github.com/spf13/cobra"
)
var viewCmd = &cobra.Command{
Use: "view",
Example: "mctl server view",
Short: "view all saved servers",
SilenceUsage: true,
Run: func(cmd *cobra.Command, args []string) {
db, err := database.New()
cobra.CheckErr(err)
defer db.Close()
ts, err := db.GetAllServers()
if err != nil {
if err.Error() == ErrInit {
fmt.Println(ErrInitRsp)
return
}
}
cobra.CheckErr(err)
for _, s := range ts {
fmt.Println("-----")
fmt.Printf("Name: %s\n", s.Name)
fmt.Printf("Address: %s\n", s.Server)
fmt.Printf("Port: %d\n", s.Port)
fmt.Println("Password: [REDACTED]")
fmt.Printf("Default: %t\n", s.Active)
}
},
}
func init() {
ServerCmd.AddCommand(viewCmd)
}

View File

@ -1,48 +0,0 @@
/*
Copyright © 2025 Jake jake.young.dev@gmail.com
*/
package cmd
import (
"errors"
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// viewCmd represents the view command
var viewCmd = &cobra.Command{
Use: "view <name>",
Example: "mctl view test",
Short: "View saved commands",
Long: `Load command using the supplied name and displays it in the terminal`,
Run: func(cmd *cobra.Command, args []string) {
var cm map[string]any
cmdMap := viper.Get("customcmd")
if cmdMap == nil {
fmt.Println("no custom commands found")
return
}
cm = cmdMap.(map[string]any)
custom, ok := cm[args[0]]
if !ok {
fmt.Println("command not found")
return
}
fmt.Printf("Command: %s\n", custom.(string))
},
PreRunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New("name argument is required")
}
return nil
},
}
func init() {
rootCmd.AddCommand(viewCmd)
}

View File

@ -1,49 +0,0 @@
package cryptography
import (
"crypto/aes"
"crypto/cipher"
"github.com/spf13/viper"
)
func EncryptPassword(b []byte) ([]byte, error) {
nonce := viper.Get("nonce").(string)
dev := viper.Get("device").(string)
block, err := aes.NewCipher([]byte(dev))
if err != nil {
return nil, err
}
aesg, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
ct := aesg.Seal(nil, []byte(nonce), []byte(b), nil)
return ct, nil
}
func DecryptPassword(b []byte) (string, error) {
nonce := viper.Get("nonce").(string)
password := viper.Get("password").(string)
dev := viper.Get("device").(string)
block, err := aes.NewCipher([]byte(dev))
if err != nil {
return "", err
}
aesg, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
op, err := aesg.Open(nil, []byte(nonce), []byte(password), nil)
if err != nil {
return "", err
}
return string(op), nil
}

365
database/sqlx.go Normal file
View File

@ -0,0 +1,365 @@
package database
import (
"context"
"database/sql"
"errors"
"fmt"
"os"
"time"
"code.jakeyoungdev.com/jake/mctl/model"
"github.com/jmoiron/sqlx"
_ "github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
)
/*
all sqlx methods for CRUD functionalities of commands and servers.
*/
const (
DB_TIMEOUT = time.Second * 10
)
type database struct {
*sqlx.DB
}
type Database interface {
Init() error
Destroy() error
Close() error
//internals
timeout() (context.Context, context.CancelFunc)
//command methods
GetCmd(name string) (string, error)
GetAllCmds() ([]model.Command, error)
SaveCmd(name, cmd string) error
UpdateCmd(name, cmd string) error
DeleteCmd(name string) error
//server methods
GetServer(name string) (model.Server, error)
GetActiveServer() (model.Server, error)
GetAllServers() ([]model.Server, error)
SetActiveServer(name string) error
SaveServer(srv model.Server) error
UpdateServer(name, password string) error
DeleteServer(name string) error
}
// creates a new sqlite connection to the mctl database. Database files are
// kept in the the user home directory
func New() (Database, error) {
home, err := os.UserHomeDir()
if err != nil {
return nil, err
}
db, err := sqlx.Open("sqlite3", fmt.Sprintf("file:%s/.mctl", home))
if err != nil {
return nil, err
}
return &database{
db,
}, nil
}
// intitial database setup, creates commands and servers tables
func (d *database) Init() error {
query := `
CREATE TABLE IF NOT EXISTS commands(
name TEXT PRIMARY KEY,
command TEXT
);
CREATE TABLE IF NOT EXISTS servers(
name TEXT PRIMARY KEY,
server TEXT,
password TEXT,
port INTEGER,
active INTEGER NOT NULL DEFAULT 0
);
`
// _, err := d.exec(query)
ctx, cl := d.timeout()
defer cl()
_, err := d.ExecContext(ctx, query)
return err
}
// drops commands and servers tables
func (d *database) Destroy() error {
query := `
DROP TABLE commands;
DROP TABLE servers;
`
ctx, cl := d.timeout()
defer cl()
_, err := d.ExecContext(ctx, query)
return err
}
func (d *database) Close() error {
return d.DB.Close()
}
func (d *database) timeout() (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), DB_TIMEOUT)
}
// gets command using name
func (d *database) GetCmd(name string) (string, error) {
query := `
SELECT
command
FROM
commands
WHERE
name = ?
`
var cmd string
ctx, cl := d.timeout()
defer cl()
err := d.QueryRowxContext(ctx, query, name).Scan(&cmd)
if errors.Is(err, sql.ErrNoRows) {
return "", errors.New("Command not found")
}
if err != nil {
return "", err
}
return cmd, nil
}
// gets all saved commands
func (d *database) GetAllCmds() ([]model.Command, error) {
query := `
SELECT
name,
command
FROM
commands
`
ctx, cancel := d.timeout()
defer cancel()
rows, err := d.QueryxContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var res []model.Command
for rows.Next() {
var r model.Command
err := rows.StructScan(&r)
if err != nil {
return nil, err
}
res = append(res, r)
}
return res, nil
}
// save a new command
func (d *database) SaveCmd(name, cmd string) error {
query := `
INSERT INTO commands(name, command)
VALUES(?, ?)
`
ctx, cancel := d.timeout()
defer cancel()
_, err := d.ExecContext(ctx, query, name, cmd)
return err
}
// DO WE NEED THIS?
func (d *database) UpdateCmd(name, cmd string) error {
query := `
UPDATE
commands
SET
cmd = ?
WHERE
name = ?
`
ctx, cancel := d.timeout()
defer cancel()
_, err := d.ExecContext(ctx, query, cmd, name)
return err
}
func (d *database) DeleteCmd(name string) error {
query := `
DELETE FROM commands
WHERE name = ?
`
ctx, cancel := d.timeout()
defer cancel()
_, err := d.ExecContext(ctx, query, name)
return err
}
func (d *database) GetServer(name string) (model.Server, error) {
query := `
SELECT
name,
server,
password,
port,
active
FROM
servers
WHERE
name = ?
`
ctx, cancel := d.timeout()
defer cancel()
var s model.Server
err := d.QueryRowxContext(ctx, query, name).StructScan(&s)
if errors.Is(err, sql.ErrNoRows) {
return model.Server{}, errors.New("Server not found")
}
if err != nil {
return model.Server{}, err
}
return s, nil
}
func (d *database) GetActiveServer() (model.Server, error) {
query := `
SELECT
name,
server,
password,
port,
active
FROM
servers
WHERE
active = 1
`
ctx, cancel := d.timeout()
defer cancel()
var s model.Server
err := d.QueryRowxContext(ctx, query).StructScan(&s)
if errors.Is(err, sql.ErrNoRows) {
return s, errors.New("No active server set")
}
return s, err
}
func (d *database) GetAllServers() ([]model.Server, error) {
query := `
SELECT
name,
server,
password,
port,
active
FROM
servers
`
ctx, cancel := d.timeout()
defer cancel()
rows, err := d.QueryxContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var res []model.Server
for rows.Next() {
var r model.Server
err := rows.StructScan(&r)
if err != nil {
return nil, err
}
res = append(res, r)
}
return res, nil
}
func (d *database) SaveServer(srv model.Server) error {
query := `
INSERT INTO servers(name, server, password, port)
VALUES(?, ?, ?, ?)
`
ctx, cancel := d.timeout()
defer cancel()
_, err := d.ExecContext(ctx, query, srv.Name, srv.Server, srv.Password, srv.Port)
return err
}
func (d *database) SetActiveServer(name string) error {
clear := `
UPDATE
servers
SET
active = 0
`
clrctx, clrcancel := d.timeout()
_, err := d.ExecContext(clrctx, clear)
if err != nil {
return err
}
defer clrcancel()
update := `
UPDATE
servers
SET
active = 1
WHERE
name = ?
`
ctx, cancel := d.timeout()
defer cancel()
_, err = d.ExecContext(ctx, update, name)
return err
}
// updates server password, if anymore fields need updated the entry should be deleted and recreated
func (d *database) UpdateServer(name, password string) error {
query := `
UPDATE servers
SET
password = ?
WHERE
name = ?
`
ctx, cancel := d.timeout()
defer cancel()
_, err := d.ExecContext(ctx, query, password, name)
return err
}
func (d *database) DeleteServer(name string) error {
query := `
DELETE FROM servers
WHERE
name = ?
`
ctx, cancel := d.timeout()
defer cancel()
_, err := d.ExecContext(ctx, query, name)
return err
}

23
go.mod
View File

@ -1,28 +1,19 @@
module code.jakeyoungdev.com/jake/mctl module code.jakeyoungdev.com/jake/mctl
go 1.24.0 go 1.24.4
require ( require (
github.com/jake-young-dev/mcr v1.3.1 github.com/jake-young-dev/mcr v1.4.0
github.com/jmoiron/sqlx v1.4.0
github.com/ncruces/go-sqlite3 v0.26.1
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1
github.com/spf13/viper v1.20.1
golang.org/x/term v0.31.0 golang.org/x/term v0.31.0
) )
require ( require (
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/ncruces/julianday v1.0.0 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect github.com/spf13/pflag v1.0.6 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/tetratelabs/wazero v1.9.0 // indirect
go.uber.org/atomic v1.9.0 // indirect golang.org/x/sys v0.33.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.21.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

72
go.sum
View File

@ -1,62 +1,34 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jake-young-dev/mcr v1.3.1 h1:ELJsrJHwQsMiM09o+q8auUaiGXXX3DWIgh/TfZQc0B0= github.com/jake-young-dev/mcr v1.4.0 h1:cXZImkfI8aNIiVPrONE6qP+nfblTGsD2iXpPKTcA25U=
github.com/jake-young-dev/mcr v1.3.1/go.mod h1:74yZHGf9h3tLUDUpInA17grKLrNp9lVesWvisCFCXKY= github.com/jake-young-dev/mcr v1.4.0/go.mod h1:74yZHGf9h3tLUDUpInA17grKLrNp9lVesWvisCFCXKY=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/ncruces/go-sqlite3 v0.26.1 h1:lBXmbmucH1Bsj57NUQR6T84UoMN7jnNImhF+ibEITJU=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/ncruces/go-sqlite3 v0.26.1/go.mod h1:XFTPtFIo1DmGCh+XVP8KGn9b/o2f+z0WZuT09x2N6eo=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

14
model/data.go Normal file
View File

@ -0,0 +1,14 @@
package model
type Command struct {
Name string `db:"name"`
Command string `db:"command"`
}
type Server struct {
Name string `db:"name"`
Server string `db:"server"`
Password string `db:"password"`
Port int `db:"port"`
Active bool `db:"active"`
}