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
44 changes: 35 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ env:

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand All @@ -22,13 +25,20 @@ jobs:
with:
go-version: ${{ env.GO_VERSION }}

- name: Run unit tests
- name: Run unit tests (Linux)
if: runner.os != 'Windows'
run: make test

- name: Run unit tests (Windows)
if: runner.os == 'Windows'
shell: bash
run: go test -tags mcp ./cmd/... ./internal/... -v

- name: Run go vet
run: make vet
run: go vet ./...

- name: Run go fmt check
- name: Run go fmt check (Linux only)
if: runner.os != 'Windows'
run: |
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
echo "The following files are not formatted:"
Expand All @@ -37,7 +47,16 @@ jobs:
fi

build-test:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- os: ubuntu-latest
binary: ./bin/vers
- os: macos-latest
binary: ./bin/vers
- os: windows-latest
binary: ./bin/vers.exe
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand All @@ -49,13 +68,20 @@ jobs:
with:
go-version: ${{ env.GO_VERSION }}

- name: Build with make
run: make build
- name: Build binary
shell: bash
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
CGO_ENABLED=0 go build -ldflags "-s -w" -o bin/vers.exe ./cmd/vers
else
make build
fi

- name: Test binary
shell: bash
run: |
./bin/vers --version
./bin/vers --VVersion
${{ matrix.binary }} --version
${{ matrix.binary }} --help

integration-tests:
if: github.event_name == 'pull_request'
Expand Down
20 changes: 18 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,26 @@ LDFLAGS = -s -w \
-X 'github.com/hdresearch/vers-cli/cmd.Repository=$(REPOSITORY)' \
-X 'github.com/hdresearch/vers-cli/cmd.License=$(LICENSE)'

# Build the vers binary
# Build the vers binary (static, no libc dependency)
.PHONY: build
build:
go build -ldflags "$(LDFLAGS)" -o bin/vers ./cmd/vers
CGO_ENABLED=0 go build -ldflags "$(LDFLAGS) -extldflags '-static'" -o bin/vers ./cmd/vers

# Build static Linux binaries for distribution
.PHONY: build-linux
build-linux:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS) -extldflags '-static'" -o bin/vers-linux-amd64 ./cmd/vers
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS) -extldflags '-static'" -o bin/vers-linux-arm64 ./cmd/vers

# Build Windows binaries for distribution
.PHONY: build-windows
build-windows:
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin/vers-windows-amd64.exe ./cmd/vers
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o bin/vers-windows-arm64.exe ./cmd/vers

# Build all platforms
.PHONY: build-all
build-all: build build-linux build-windows

# Clean build artifacts
.PHONY: clean
Expand Down
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,43 @@ If a request field needs a `param.Field[T]`, wrap with `vers.F(value)`. See the

## Installation

### Quick Install (Recommended)

Install the latest version using our installation script:

```bash
curl -fsSL https://raw.githubusercontent.com/hdresearch/vers-cli/main/install.sh | sh
```

This script will:
- Detect your OS and architecture automatically
- Download the appropriate prebuilt binary
- Verify the checksum for security
- Install to `~/.local/bin` (or use `INSTALL_DIR` to customize)
- Make the binary executable

**Custom installation directory:**
```bash
INSTALL_DIR=/usr/local/bin curl -fsSL https://raw.githubusercontent.com/hdresearch/vers-cli/main/install.sh | sh
```

**Install a specific version:**
```bash
VERS_VERSION=v0.5.0 curl -fsSL https://raw.githubusercontent.com/hdresearch/vers-cli/main/install.sh | sh
```

### Install from Source

If you have Go installed, you can build from source:

```bash
go install github.com/hdresearch/vers-cli/cmd/vers@latest
```

### Manual Installation

Download prebuilt binaries from the [releases page](https://github.com/hdresearch/vers-cli/releases).


## Usage

Expand Down
11 changes: 7 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,25 @@ require (
github.com/joho/godotenv v1.5.1
github.com/modelcontextprotocol/go-sdk v0.6.0
github.com/spf13/cobra v1.9.1
golang.org/x/term v0.32.0
golang.org/x/term v0.38.0
)

require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/charmbracelet/x/exp/golden v0.0.0-20241212170349-ad4b7ae0f25f // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/google/jsonschema-go v0.2.3 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/pkg/sftp v1.13.10 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/text v0.32.0 // indirect
)

require (
Expand All @@ -49,5 +52,5 @@ require (
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/sys v0.39.0 // indirect
)
14 changes: 14 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
Expand All @@ -53,6 +55,8 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=
github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
Expand All @@ -78,18 +82,28 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
45 changes: 34 additions & 11 deletions internal/handlers/connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ package handlers
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"strings"

"github.com/hdresearch/vers-cli/internal/app"
"github.com/hdresearch/vers-cli/internal/presenters"
runrt "github.com/hdresearch/vers-cli/internal/runtime"
vmSvc "github.com/hdresearch/vers-cli/internal/services/vm"
sshutil "github.com/hdresearch/vers-cli/internal/ssh"
"github.com/hdresearch/vers-cli/internal/utils"
Expand Down Expand Up @@ -51,23 +50,47 @@ func HandleConnect(ctx context.Context, a *app.App, r ConnectReq) (presenters.Co
// Render connection info BEFORE running SSH so it displays before connecting
presenters.RenderConnect(a, view)

// Save terminal state before SSH so we can restore it if the connection
// exits abnormally (network drop, server crash, etc.)
fd := int(os.Stdin.Fd())
oldState, termErr := term.GetState(fd)
// Get stdin as io.Reader
var stdin io.Reader
if a.IO.In != nil {
stdin = a.IO.In
} else {
stdin = os.Stdin
}

// Check if stdin is a terminal
var fd int
var oldState *term.State
if f, ok := stdin.(*os.File); ok {
fd = int(f.Fd())
if term.IsTerminal(fd) {
// Save terminal state before SSH so we can restore it if the connection
// exits abnormally (network drop, server crash, etc.)
oldState, _ = term.GetState(fd)

// Put terminal in raw mode for interactive session
rawState, err := term.MakeRaw(fd)
if err == nil {
defer term.Restore(fd, rawState)
}
}
}

args := sshutil.SSHArgs(sshHost, sshPort, info.KeyPath)
err = a.Runner.Run(ctx, "ssh", args, runrt.Stdio{In: a.IO.In, Out: a.IO.Out, Err: a.IO.Err})
// Use native SSH client
client := sshutil.NewClient(sshHost, info.KeyPath)
err = client.Interactive(ctx, stdin, a.IO.Out, a.IO.Err)

// Always restore terminal state after SSH exits, regardless of how it exited
if termErr == nil && oldState != nil {
if oldState != nil {
_ = term.Restore(fd, oldState)
}

if err != nil {
if _, ok := err.(*exec.ExitError); !ok {
return view, fmt.Errorf("failed to run SSH command: %w", err)
// Context cancellation is not an error for interactive sessions
if ctx.Err() != nil {
return view, nil
}
return view, fmt.Errorf("SSH session failed: %w", err)
}
return view, nil
}
Loading