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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Test and coverage

on: [push, pull_request]

jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
with:
fetch-depth: 2
- uses: actions/setup-go@v6.2.0
with:
go-version: '1.25'
- name: Run coverage
run: go env -w GOTOOLCHAIN=go1.25.0+auto && go test -coverpkg=./... ./... -race -coverprofile=coverage.out -covermode=atomic
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
verbose: true
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
74 changes: 74 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"

on:
push:
branches: [ "main" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "main" ]
schedule:
- cron: '35 15 * * 3'

jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write

strategy:
fail-fast: false
matrix:
language: [ 'go' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support

steps:
- name: Checkout repository
uses: actions/checkout@v4

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.

# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality


# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3

# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun

# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.

# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
28 changes: 28 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go

name: Go

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2

- name: Set up Go
uses: actions/setup-go@v6.2.0
with:
go-version: '1.25'

- name: Build
run: go build -v ./...

- name: Test
run: go test -v ./...
181 changes: 119 additions & 62 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,97 +1,154 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/ncode/pretty)](https://goreportcard.com/report/github.com/ncode/pretty)
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![codecov](https://codecov.io/gh/ncode/pretty/graph/badge.svg?token=BCUQ77HCLY)](https://codecov.io/gh/ncode/pretty)

# pretty
Parallel remote execution tty - (Yet another parallel ssh/shell)

## Installation:
go get -u github.com/ncode/pretty
`Parallel remote execution tty` - (Yet another parallel ssh/shell)

- Run commands across many hosts with colored, prefixed output.
- Keep an interactive prompt with a per-host shell session.
- Run async jobs in separate SSH sessions and track status.
- Load hosts from args, config groups, or a hosts file.

## Config:
By default it lives in ~/.pretty.yaml
## Installation
Requires Go 1.25.

```
username: ncode
history_file: ~/.pretty.history
ssh_private_key: ~/.ssh/id_rsa
groups:
hosts:
- host1
- host2
- host3
- host4
go install github.com/ncode/pretty@latest
```

## Usage:
## Quick start
```
pretty host1 host2 host3
pretty -G prod
pretty -H /tmp/hosts.txt
```
Parallel remote execution tty - (Yet another parallel ssh/shell)

usage:
pretty <host1> <host2> <host3>...
## Configuration
`pretty` looks for a config file named `.pretty` in your home directory with a supported extension:
`$HOME/.pretty.yaml`, `$HOME/.pretty.yml`, `$HOME/.pretty.json`, or `$HOME/.pretty.toml`.
Use `--config` to point at an explicit path.

Usage:
pretty [flags]
Optional keys:
- `username`: SSH username override (falls back to SSH config, then current shell user).
- `known_hosts`: path to a known_hosts file for host key verification.
- `groups.<name>`: host groups as wrapper objects with `hosts` and optional `user`.
- `prompt`: interactive prompt string (UTF-8 supported). `--prompt` overrides config.

Flags:
--config string config file (default is $HOME/.pretty.yaml)
-h, --help help for pretty
-G, --hostGroup string group of hosts to be loaded from the config file
-H, --hostsFile string hosts file to be used instead of the args via stdout (one host per line)
Example:
```
known_hosts: /Users/me/.ssh/known_hosts
prompt: "pretty> "
groups:
web:
user: deploy
hosts:
- web1.example.com
- web2.example.com:2222
```

Host key verification:
- If `known_hosts` is set and loads successfully, it is used.
- Otherwise `~/.ssh/known_hosts` is used if it loads successfully.
- If neither can be loaded, host keys are not verified.
- A loaded known_hosts file must contain each host key or connections will fail.

Connecting to hosts:
Notes:
- Group entries must use the wrapper schema with a `hosts` list.
- Auth uses your SSH agent (`SSH_AUTH_SOCK`) and IdentityFile entries from SSH config. Load keys with `ssh-add`.

## Host specs
Accepted formats:
- `host` (defaults to port 22)
- `host:port`
- `user@host`
- `user@host:port`
- `[ipv6]:port` (required to specify a port with IPv6)
- `user@[ipv6]:port`

Hosts files (`-H`) accept one entry per line in the same formats. Blank lines are ignored.

## Flags
- `--config <path>`: config file path.
- `--prompt <string>`: prompt to display in the interactive shell.
- `-G`, `--hostGroup <name>`: load `groups.<name>` from config.
- `-H`, `--hostsFile <path>`: read hosts from a file (one host per line).
- `-h`, `--help`: help for pretty.

Host selection behavior:
- At least one of positional hosts, `--hostGroup`, or `--hostsFile` is required.
- With no positional hosts, `--hostGroup` loads only the group.
- With more than one positional host, `--hostGroup` appends the group.
- With exactly one positional host, `--hostGroup` is currently ignored.
- `--hostsFile` always appends its hosts.

## Interactive commands
```
pretty host1 host2 host3 host4
pretty(2)>>
Error connection to host host3: Failed to dial: dial tcp: lookup host3: no such host
Error connection to host host4: Failed to dial: dial tcp: lookup host4: no such host
:help
:list
:status [id]
:async <command>
:scroll
:bye
exit
```

Connecting to hostGroups:
Notes:
- `:list` shows connection status per host.
- `:status` shows the last normal job plus the last two async jobs; `:status <id>` targets a single job.
- `:async` runs a command in a new SSH session per host and returns to the prompt immediately.
- `:scroll` enters scroll mode for the output viewport (output scrolling is disabled otherwise); press `esc` to return to the prompt.
- Use Up/Down arrows to navigate command history (persisted in `history_file`).
- `Ctrl+C` forwards to remote sessions; press twice within 500ms to quit locally.
- `Ctrl+Z` forwards to remote sessions (suspend).

## How it works
- Starts one persistent SSH shell session per host for interactive commands.
- Wraps each command with a sentinel to capture per-host exit codes.
- Runs async commands in fresh SSH sessions and updates job status as they finish.
- Prefixes output with `host:port` and assigns a stable color per host.
- Keeps the last 10,000 output lines in the UI buffer.

## Local SSHD testbed
Use the local SSHD testbed to exercise `pretty` against three localhost targets.

Generate keys, password, and a ready-to-use config file:
```
pretty -G hosts
pretty(2)>>
Error connection to host host3: Failed to dial: dial tcp: lookup host3: no such host
Error connection to host host4: Failed to dial: dial tcp: lookup host4: no such host
export PRETTY_AUTHORIZED_KEY="$(ssh-add -L | grep 'my-key' | head -n1)"
./scripts/ssh-testbed-setup.sh
```

Connecting to hostsFile:
Start the testbed:
```
pretty -H /tmp/hosts.txt
pretty(2)>>
Error connection to host host3: Failed to dial: dial tcp: lookup host3: no such host
Error connection to host host4: Failed to dial: dial tcp: lookup host4: no such host
docker compose -f docker-compose.sshd.yml up -d --build
```

List connection status:
Re-run setup after the containers are running to populate `.pretty-test/known_hosts`:
```
pretty(2)>> :status
Connected hosts (2)
Failed hosts (2)
./scripts/ssh-testbed-setup.sh
```

List hosts:
If you want to use the generated test key instead of an existing agent key:
```
pretty(2)>> :list
host1: Connected(true)
host2: Connected(true)
host3: Connected(false)
host4: Connected(false)
ssh-add .pretty-test/id_ed25519
```

Running commands:
Example run:
```
pretty(2)>> whoami
host1: ncode
host2: ncode
pretty --config .pretty-test/pretty.yaml -G testbed
```
Then run `whoami` to confirm each host responds as `pretty`.

The generated password is stored at `.pretty-test/password.txt` for manual `ssh` testing if needed.

## Why do I need it?
pretty is a tool to control interactive shells across multiple hosts from
a single point.
## Why pretty?
`pretty` is a tool to control interactive shells across multiple hosts from a single point.

### Motivation
After using [polysh](http://guichaz.free.fr/polysh) for a long time. It came with
the motivation to try to write my own parallel shell in GO. In the end the tool worked
After using [polysh](http://guichaz.free.fr/polysh) for a long time, it came with
the motivation to try to write my own parallel shell in Go. In the end the tool worked
so well and I decided to open source the code.

### TODO:
Forward Control+C and Control+Z requests to the destination terminal
Support for encrypted ssh keys
## Limitations
- SSH authentication uses the local agent and SSH config IdentityFile entries; there is no keyfile flag.
Loading