mirror of
https://github.com/Gu1llaum-3/sshm.git
synced 2026-03-14 03:41:27 +01:00
feat: add info command for JSON host details
Adds a jq-friendly `sshm info` subcommand with host completion and documentation, and makes home directory resolution testable for backup path tests.
This commit is contained in:
199
cmd/info.go
Normal file
199
cmd/info.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Gu1llaum-3/sshm/internal/config"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type infoResponse struct {
|
||||
Schema string `json:"schema"`
|
||||
OK bool `json:"ok"`
|
||||
Hostname string `json:"hostname"`
|
||||
Result *infoResult `json:"result"`
|
||||
Error *infoError `json:"error"`
|
||||
}
|
||||
|
||||
type infoResult struct {
|
||||
CanonicalName string `json:"canonical_name"`
|
||||
Target infoTarget `json:"target"`
|
||||
IdentityFile *string `json:"identity_file"`
|
||||
ProxyJump *string `json:"proxy_jump"`
|
||||
ProxyCommand *string `json:"proxy_command"`
|
||||
Options *string `json:"options"`
|
||||
Tags []string `json:"tags"`
|
||||
RemoteCommand *string `json:"remote_command"`
|
||||
RequestTTY *string `json:"request_tty"`
|
||||
Source *infoSource `json:"source"`
|
||||
}
|
||||
|
||||
type infoTarget struct {
|
||||
Host string `json:"host"`
|
||||
Hostname *string `json:"hostname"`
|
||||
User *string `json:"user"`
|
||||
Port *int `json:"port"`
|
||||
}
|
||||
|
||||
type infoSource struct {
|
||||
File string `json:"file"`
|
||||
Line int `json:"line"`
|
||||
}
|
||||
|
||||
type infoError struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Details json.RawMessage `json:"details"`
|
||||
}
|
||||
|
||||
func maybeString(v string) *string {
|
||||
trimmed := strings.TrimSpace(v)
|
||||
if trimmed == "" {
|
||||
return nil
|
||||
}
|
||||
return &trimmed
|
||||
}
|
||||
|
||||
func maybePort(v string) (*int, error) {
|
||||
trimmed := strings.TrimSpace(v)
|
||||
if trimmed == "" {
|
||||
return nil, nil
|
||||
}
|
||||
port, err := strconv.Atoi(trimmed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &port, nil
|
||||
}
|
||||
|
||||
func writeInfoJSON(out io.Writer, pretty bool, resp infoResponse) {
|
||||
var b []byte
|
||||
var err error
|
||||
if pretty {
|
||||
b, err = json.MarshalIndent(resp, "", " ")
|
||||
} else {
|
||||
b, err = json.Marshal(resp)
|
||||
}
|
||||
if err != nil {
|
||||
_, _ = io.WriteString(out, `{"schema":"sshm.info.v1","ok":false,"hostname":"","result":null,"error":{"code":"INTERNAL","message":"failed to marshal JSON","details":null}}\n`)
|
||||
return
|
||||
}
|
||||
_, _ = out.Write(append(b, '\n'))
|
||||
}
|
||||
|
||||
func runInfo(out io.Writer, hostnameArg string, cfgFile string, pretty bool) int {
|
||||
resp := infoResponse{
|
||||
Schema: "sshm.info.v1",
|
||||
OK: false,
|
||||
Hostname: hostnameArg,
|
||||
Result: nil,
|
||||
Error: nil,
|
||||
}
|
||||
|
||||
var host *config.SSHHost
|
||||
var err error
|
||||
if cfgFile != "" {
|
||||
host, err = config.GetSSHHostFromFile(hostnameArg, cfgFile)
|
||||
} else {
|
||||
host, err = config.GetSSHHost(hostnameArg)
|
||||
}
|
||||
if err != nil {
|
||||
code := 1
|
||||
errCode := "CONFIG_ERROR"
|
||||
msg := err.Error()
|
||||
if strings.Contains(msg, "not found") {
|
||||
code = 2
|
||||
errCode = "NOT_FOUND"
|
||||
}
|
||||
|
||||
resp.Error = &infoError{Code: errCode, Message: msg, Details: nil}
|
||||
writeInfoJSON(out, pretty, resp)
|
||||
return code
|
||||
}
|
||||
|
||||
port, portErr := maybePort(host.Port)
|
||||
if portErr != nil {
|
||||
resp.Error = &infoError{Code: "CONFIG_ERROR", Message: "invalid port in host configuration", Details: nil}
|
||||
writeInfoJSON(out, pretty, resp)
|
||||
return 1
|
||||
}
|
||||
|
||||
res := infoResult{
|
||||
CanonicalName: host.Name,
|
||||
Target: infoTarget{
|
||||
Host: hostnameArg,
|
||||
Hostname: maybeString(host.Hostname),
|
||||
User: maybeString(host.User),
|
||||
Port: port,
|
||||
},
|
||||
IdentityFile: maybeString(host.Identity),
|
||||
ProxyJump: maybeString(host.ProxyJump),
|
||||
ProxyCommand: maybeString(host.ProxyCommand),
|
||||
Options: maybeString(host.Options),
|
||||
Tags: host.Tags,
|
||||
RemoteCommand: maybeString(host.RemoteCommand),
|
||||
RequestTTY: maybeString(host.RequestTTY),
|
||||
Source: &infoSource{
|
||||
File: host.SourceFile,
|
||||
Line: host.LineNumber,
|
||||
},
|
||||
}
|
||||
|
||||
resp.OK = true
|
||||
resp.Result = &res
|
||||
writeInfoJSON(out, pretty, resp)
|
||||
return 0
|
||||
}
|
||||
|
||||
var infoPretty bool
|
||||
|
||||
var infoCmd = &cobra.Command{
|
||||
Use: "info <hostname>",
|
||||
Short: "Print machine-readable information about a host",
|
||||
Long: "Print machine-readable information (JSON) about a configured SSH host.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) != 0 {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
var hosts []config.SSHHost
|
||||
var err error
|
||||
if configFile != "" {
|
||||
hosts, err = config.ParseSSHConfigFile(configFile)
|
||||
} else {
|
||||
hosts, err = config.ParseSSHConfig()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
|
||||
var completions []string
|
||||
toCompleteLower := strings.ToLower(toComplete)
|
||||
for _, host := range hosts {
|
||||
if strings.HasPrefix(strings.ToLower(host.Name), toCompleteLower) {
|
||||
completions = append(completions, host.Name)
|
||||
}
|
||||
}
|
||||
return completions, cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
exitCode := runInfo(cmd.OutOrStdout(), args[0], configFile, infoPretty)
|
||||
if exitCode != 0 {
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
infoCmd.Flags().BoolVar(&infoPretty, "pretty", false, "Pretty-print JSON output")
|
||||
RootCmd.AddCommand(infoCmd)
|
||||
}
|
||||
Reference in New Issue
Block a user