Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev
build:
go build -ldflags "-s -w -X github.com/jerryagbesi/skipper/cmd.version=$(VERSION)" -o skipper

test:
go test ./...

run:
go run .

Expand Down
33 changes: 31 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@

# Skipper

A CLI tool for managing SSH connections with an interactive terminal UI. Skipper reads your `~/.ssh/config` file and lets you browse, search, and connect to hosts without memorizing aliases.
A CLI tool for managing SSH connections with an interactive terminal UI. Skipper reads your `~/.ssh/config` file and lets you browse, search, and connect to hosts without memorizing aliases.You can fuzzy-search aliases and connection details, then connect immediately from the same screen — or skip the UI entirely when there's only one match.

<img width="657" height="147" alt="image" src="https://github.com/user-attachments/assets/eb6edc3e-d238-470a-8fea-3deae369970b" />


## Features

Expand All @@ -13,6 +16,14 @@ A CLI tool for managing SSH connections with an interactive terminal UI. Skipper
- **Seamless connection** -- selects a host and drops you straight into an SSH session

## Installation
### Quick Install

Easily install Skipper using the provided install script, or download a release binary for your platform.

```bash
curl -fsSL https://raw.githubusercontent.com/JerryAgbesi/skipper/main/install.sh | sh
```
Comment thread
JerryAgbesi marked this conversation as resolved.


### Download a release binary

Expand Down Expand Up @@ -52,6 +63,14 @@ cd skipper
make run
```

Run the test suite with:

```bash
make test
```

Unit tests live alongside the Go packages they exercise under `cmd/` and `internal/`.

## Usage

```
Expand All @@ -60,10 +79,19 @@ skipper [flags]

| Flag | Description |
|------|-------------|
| `-a, --add <alias> <target>` | Add a host entry to the SSH config using a target like `user@host[:port]` |
| `-c, --config <path>` | Path to SSH config file (default: `~/.ssh/config`) |
| `-f, --find [term]` | Open directly in find mode, or pre-filter hosts when a search term is provided |
| `-v, --version` | Print version |
| `-h, --help` | Show help |

Examples:

```bash
skipper --add devone user@ipaddress:9000
skipper --add bastion admin@10.0.0.5
```

### Keyboard Controls

| Key | Action |
Expand All @@ -78,7 +106,8 @@ skipper [flags]
| Target | Description |
|--------|-------------|
| `make build` | Compile the `skipper` binary |
| `make test` | Run the full Go test suite |
| `make run` | Build and run |
| `make lint` | Run golangci-lint |
| `make fmt` | Format code |
| `make all` | Format + Build + Run |
| `make all` | Format + Build + Run |
168 changes: 128 additions & 40 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"os/exec"
"strings"

"github.com/jerryagbesi/skipper/internal/connect"
"github.com/jerryagbesi/skipper/internal/sshconfig"
Expand All @@ -13,55 +14,143 @@ import (
)

var configPath string
var addAlias string
var findQuery string

var version = "dev"

var rootCmd = &cobra.Command{
Use: "skipper <command> [flags]",
Version: version,
Short: "skipper is a cli tool for managing ssh connections",
Example: "skipper --version",
Run: func(cmd *cobra.Command, args []string) {
if configPath == "" {
var err error
configPath, err = sshconfig.DefaultConfigPath()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
hosts, err := sshconfig.ParseHosts(configPath)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
Use: "skipper <command> [flags]",
Version: version,
Short: "skipper is a cli tool for managing ssh connections",
Example: "skipper --version",
RunE: runRoot,
SilenceErrors: true,
Long: `skipper is a cli tool for managing ssh connections, It allows you to select your preferred ssh host alias, connect to it, and execute commands.`,
}

if len(hosts) == 0 {
fmt.Println("no hosts found in config file")
os.Exit(1)
}
func runRoot(cmd *cobra.Command, args []string) error {
path, err := resolveConfigPath(configPath)
if err != nil {
return err
}

// Render bubbletea UI and get selected host
result, err := ui.Run(hosts)
if cmd.Flags().Changed("add") {
host, err := addHost(path, addAlias, args)
if err != nil {
fmt.Println(err)
os.Exit(1)
return err
}

if result.Cancelled {
return
fmt.Printf("added host %q for %s\n", host.Alias, hostTarget(host))
return nil
}

hosts, err := sshconfig.ParseHosts(path)
if err != nil {
return err
}

if len(hosts) == 0 {
return fmt.Errorf("no hosts found in config file")
}

options, hosts, err := prepareHostSelection(cmd, hosts)
if err != nil {
return err
}

result, err := ui.Run(hosts, options)
if err != nil {
return err
}

if result.Cancelled {
return nil
}

return connect.Connect(result.Host, exec.Command)
}

func resolveConfigPath(path string) (string, error) {
if path != "" {
return path, nil
}

return sshconfig.DefaultConfigPath()
}

func prepareHostSelection(cmd *cobra.Command, hosts []sshconfig.Host) (ui.RunOptions, []sshconfig.Host, error) {
options := ui.RunOptions{}
if !cmd.Flags().Changed("find") {
return options, hosts, nil
}

options.StartFiltering = findQuery == ""
if findQuery == "" {
return options, hosts, nil
}

filtered := filterHosts(hosts, findQuery)
if len(filtered) == 0 {
return ui.RunOptions{}, nil, fmt.Errorf("no hosts found matching %q", findQuery)
}

return options, filtered, nil
}

func addHost(path, alias string, args []string) (*sshconfig.Host, error) {
alias = strings.TrimSpace(alias)
if alias == "" {
return nil, fmt.Errorf("--add requires an alias")
}

if len(args) != 1 {
return nil, fmt.Errorf("--add requires exactly one target in the format user@host[:port]")
}

host, err := connect.ParseTarget(args[0])
if err != nil {
return nil, err
}

host.Alias = alias
return sshconfig.AddHost(path, *host)
}

func filterHosts(hosts []sshconfig.Host, query string) []sshconfig.Host {
query = strings.TrimSpace(strings.ToLower(query))
if query == "" {
return hosts
}

filtered := make([]sshconfig.Host, 0, len(hosts))
for _, host := range hosts {
if hostMatchesQuery(host, query) {
filtered = append(filtered, host)
}
}

// Connect to selected host
err = connect.Connect(result.Host, exec.Command)
if err != nil {
fmt.Println(err)
os.Exit(1)
return filtered
}

func hostMatchesQuery(host sshconfig.Host, query string) bool {
fields := []string{host.Alias, host.Hostname, host.User, host.IdentityFile}
for _, field := range fields {
if strings.Contains(strings.ToLower(field), query) {
return true
}
}

},
SilenceErrors: true,
Long: `skipper is a cli tool for managing ssh connections, It allows you to select your preferred ssh host alias, connect to it, and execute commands.`,
return host.Port != 0 && strings.Contains(fmt.Sprintf("%d", host.Port), query)
}

func hostTarget(host *sshconfig.Host) string {
target := host.User + "@" + host.Hostname
if host.Port > 0 {
return fmt.Sprintf("%s:%d", target, host.Port)
}

return target
}

func Execute() {
Expand All @@ -72,10 +161,9 @@ func Execute() {
}

func init() {
// rootCmd.SetFlagErrorFunc(func(c *cobra.Command, err error) error {
// return fmt.Errorf("invalid flag: %w \n please run 'skipper --help' for usage information", err)
// })

rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "path to ssh config file, defaults to ~/.ssh/config")
rootCmd.Flags().StringVarP(&addAlias, "add", "a", "", "add a host alias using a target like user@host[:port]")
rootCmd.Flags().StringVarP(&findQuery, "find", "f", "", "start in find mode or pre-filter hosts by a search term")
rootCmd.Flags().Lookup("find").NoOptDefVal = ""
rootCmd.Flags().BoolP("version", "v", false, "print version information")
}
Loading