-
# sess
-tmux sessions that remember where they were and reconnect over SSH when the link drops.
-
-[](LICENSE)
-
-> One tool. No worktrees. No containers.
-
-
+**A persistent SSH terminal. Powered by zmx.**
-## Why
-
-Close the laptop and your SSH session dies. tmux keeps the shell alive on the server, but getting back means typing `ssh`, then `tmux attach`, then remembering which session was in which directory. mosh and Eternal Terminal fix the transport, but each needs its own server daemon on every box.
-
-`sess` is one bash script on top of `ssh` and `tmux`. It keeps a small state dir per session (cwd, branch, logs), copies itself to your VM with `sess init`, and when you attach to a remote session it re-runs `ssh -t host "sess "` every 3 seconds until you stop it. Nothing else to install, nothing listening on a port.
-
-## Demo
+Give a terminal on your VM a name. Leave it running. Come back to the same shell, directory, and programs after you detach or lose your connection.
+```sh
+sess init user@host
+sess set --host user@host
+sess new work
+# Ctrl+\ to detach
+sess attach work
```
-sess new feature-auth # create + auto-attach (cwd = where you ran it)
-...work in session...
-Ctrl+b d # detach (session persists in tmux)
-sess feature-auth # reattach (loops on SSH drop if a remote is set)
-sess rm feature-auth # destroy session
-```
+Run `sess` to browse your sessions. Your terminal application handles tabs and windows; sess handles session management and reconnection.
-Inside a session the tmux status bar shows the session name on the left and, on the right, the git branch of the current pane directory, the directory name, and the time. It refreshes every 3 seconds and follows wherever you `cd`.
+## Build and install
-```
- session-name main rfq-modular 22:18
-```
-
-
-
-## Quickstart
+Requires Go 1.26.5+ to build. The installed client needs OpenSSH. Linux and macOS on AMD64 and ARM64 are supported build targets.
-Prerequisites: `tmux`, `git`, `bash`, and `ssh` for remote sessions. `sess doctor` checks all four. Runs on macOS and Linux (`package.json` `os`).
-
-Install from source (the `sess-sh` npm package is not published yet). This installs `bin/sess` plus bash and zsh completions under `PREFIX`, default `/usr/local`):
-
-```bash
-git clone https://github.com/deepaksilaych/sess.git
+```sh
+git clone https://github.com/DeepakSilaych/sess.git
cd sess
-sudo make install # or: PREFIX=$HOME/.local make install
+make build
+./dist/sess --help
+make install # default: ~/.local/bin
+# or: make install PREFIX=/usr/local
```
-Or just put `bin/` on your PATH:
+Ensure the installation's `bin` directory is in your PATH. `make build` embeds the remote helpers for all four targets, so the remote VM needs neither Go nor a compiler. `go install` alone does not generate those helpers; use `make build` or a release archive.
-```bash
-chmod +x bin/sess
-export PATH="$PWD/bin:$PATH"
-```
+This is the zmx-based 0.6 development version. Existing tmux sessions from 0.5 continue to belong to tmux; they cannot be converted into live zmx sessions. Attach to them with tmux while finishing that work. The old `~/.sess` state is left untouched.
-Local sessions:
+## Prepare a host
-```bash
-cd ~/code/my-repo
-sess new feature-auth # creates ~/.sess/sessions/feature-auth, starts tmux, attaches
-# Ctrl+b d to detach
-sess ls # SESSION / BRANCH / STATUS / CREATED
-sess feature-auth # reattach
+```sh
+sess init dev # existing SSH alias, or user@host
+sess set --host dev # choose the default separately
+sess doctor
```
-Remote sessions (sessions live on the VM, the laptop is just a terminal):
+`init` verifies SSH, detects the VM's platform, installs the remote helper and pinned zmx 0.8.1 under `~/.local/share/sess/bin`, and checks the result. No sudo, extra network port, or laptop daemon. The zmx archive is downloaded over HTTPS and verified against its pinned SHA-256 digest before installation. A different existing managed zmx version is not silently replaced.
-```bash
-sess init user@dev-vm # one time: installs tmux+git, copies sess to ~/bin/sess, registers "default"
-sess new feature-auth # runs "sess new feature-auth --local" on the VM over ssh -t, attaches
-sess feature-auth # ssh -t + attach, retried every 3s after any disconnect
-sess ssh # plain ssh to the VM
-```
+Use SSH keys available to `ssh-agent` for session operations and reconnecting. Provisioning can prompt through SSH; subsequent operations use OpenSSH's batch mode so a background refresh cannot ask for a password. Configure ports, jump hosts, and identities in `~/.ssh/config`.
-Verify:
+`init` does not change your default host. It also does not edit your shell startup files.
-```bash
-sess status # version, state dir, session count, previously active sessions
-sess connections feature-auth
-```
+## Commands
-## How it works
+| Command | Shortcut | Purpose |
+| --- | --- | --- |
+| `sess init ` | | Prepare the VM |
+| `sess set --host ` | `sess set -h ` | Save the default SSH destination |
+| `sess new ` | `sess n ` | Create and attach |
+| `sess attach ` | `sess a ` | Attach to an existing session |
+| `sess remove ` | `sess rm ` | End the session and its programs |
+| `sess ls` | | List live sessions |
+| `sess detach` | | Inside a session, detach all attached terminals |
+| `sess` | | Open the interactive session browser |
+| `sess doctor` | | Check SSH and remote dependencies |
+| `sess version` | | Print version |
+| `sess completion bash` | | Generate bash, zsh, or fish completion |
-Everything is in `bin/sess` (one bash script, ~970 lines). The main `case` at the bottom dispatches subcommands; any other word is treated as a session name and goes to `cmd_attach`.
+Use `--host` or `-h` to override the host for a session command without changing the default:
-```
-laptop dev VM (same script at ~/bin/sess)
------- ----------------------------------
-sess feature-auth
- |
- | ~/.sess/remote has default.host?
- |
- no ---> cmd_attach (local)
- | _tmux_start: tmux has-session? else new-session -d -c $SESS_CWD tmux-init.sh
- | _apply_tmux_status; tmux attach-session
- | on return: connections += detach | exit
- |
- yes --> cmd_remote_attach
- while true:
- ssh -t user@vm "sess feature-auth" -----> cmd_attach (local, on the VM)
- ^ tmux attach ... returns on
- | ssh exits (drop / Ctrl+b d / exit) detach, exit, or SIGHUP
- |
- print "[sess] disconnected. reconnecting in 3s... (Ctrl+C to stop)"
- sleep 3
-```
-
-1. `sess new ` writes `~/.sess/sessions//state` (`SESS_SESSION`, `SESS_BRANCH`, `SESS_CWD=$(pwd)`, `SESS_CREATED`), appends to `log`, adds the name to `~/.sess/active-sessions`, then calls `cmd_attach`. If a remote is selected it instead `exec`s `ssh -t host "sess new --local"`, so the state lives on the VM.
-2. `cmd_attach` sources `state`, and `_tmux_start` creates the tmux session on first use with `tmux new-session -d -s -c tmux-init.sh`. That init script exports `SESS_SESSION`, `SESS_BRANCH`, `SESS_DIR`, `cd`s to the saved cwd and `exec`s `$SHELL`. If the tmux session already exists it is reused.
-3. `_apply_tmux_status` runs on every attach and sets session-scoped tmux options: `status-interval 3`, session name on the left, `git rev-parse --abbrev-ref HEAD` in `#{pane_current_path}` plus `#{b:pane_current_path}` and `%H:%M` on the right. No `.tmux.conf` changes.
-4. When `tmux attach-session` returns, `cmd_attach` checks `tmux has-session`. Still there means you detached (`detach` is written to `connections`); gone means the shell exited (`exit`, and the name is dropped from `active-sessions`).
-5. The reconnect loop is `cmd_remote_attach`. It only runs when `~/.sess/remote` has a `default.host=` line. It does not look at the ssh exit code: any return, including an intentional `Ctrl+b d`, prints the disconnected message, sleeps 3 seconds and runs ssh again. `Ctrl+C` during that 3 second wait ends the loop. No `ServerAliveInterval` is set, so how fast a dead link is noticed depends on your ssh config.
-6. `sess init` (`cmd_init`) installs tmux and git with whichever of `apt-get`, `dnf`, `yum`, `apk`, `pacman`, `brew` it finds, `scp`s the running script (symlinks resolved by `_self_path`) to `~/bin/sess`, appends `~/bin` to PATH in `.bashrc`, `.zshrc` and `.profile`, checks `~/bin/sess version`, and writes `name.host=user@host` to `~/.sess/remote`.
-7. `sess up` (`cmd_up`) reads `~/.sess/active-sessions`. Locally it reattaches the last one, or if you are already inside tmux it starts all of them and `switch-client`s to the first. With a default remote it checks each name with `ssh host "test -d ~/.sess/sessions/"`, then on macOS opens one Terminal.app tab per session via `osascript`, and on Linux runs `ssh -t host "sess "` one after another.
-
-### Compared with
-
-| | ssh + tmux by hand | mosh | Eternal Terminal | sess |
-|---|---|---|---|---|
-| Server side | tmux | mosh-server | etserver | tmux + `~/bin/sess` (copied by `sess init`) |
-| Transport | ssh | UDP, own protocol | own TCP protocol | ssh |
-| After a drop | you re-run `ssh` and `tmux attach` | link resumes by itself | link resumes by itself | `sess` re-runs `ssh -t host "sess "` every 3s |
-| Remembers cwd, branch and how each attach ended | no | no | no | yes, `~/.sess/sessions//` |
-
-## Features
-
-| Feature | Where |
-|---|---|
-| Per-session state dir: `state`, `log`, `connections`, `tmux-init.sh` | `cmd_new`, `_tmux_start`, `_log_event`, `_conn_log` |
-| `SESS_SESSION`, `SESS_BRANCH`, `SESS_DIR` exported inside every session shell | `tmux-init.sh` written by `_tmux_start` |
-| tmux status bar: session name, branch of current pane dir, dir name, HH:MM, 3s refresh | `_apply_tmux_status` |
-| Connection log with `detach` and `exit` events | `cmd_attach`, `cmd_connections` |
-| Activity log: created, attached, detached, exited, removed, diff, code | `_log_event`, `cmd_log` |
-| Remote reconnect loop, 3s retry, stop with Ctrl+C | `cmd_remote_attach` |
-| `sess init` provisions a host: package install, copy script, PATH, register | `cmd_init` |
-| Multiple remotes: `--remote `, `--local`, interactive pick, `default` first when non-interactive | `_select_remote` |
-| `sess up` reattaches previously active sessions; macOS opens a Terminal tab per session | `cmd_up`, `_local_up`, `_remote_up` |
-| `sess code` opens `$SESS_EDITOR`, else `cursor`, else `code`; remote via `--remote host` then a `vscode-remote://` URI | `cmd_code` |
-| bash and zsh completion for commands, session names, git branches, remote names | `etc/bash-completion/sess`, `etc/zsh-completion/_sess` |
-| npm wrapper so `npx sess-sh` works once published; zero npm dependencies | `bin/sess-cli.js`, `package.json` |
-
-All commands (`sess help`):
-
-```
-sess new [branch] Create session + auto-attach
- --remote use a specific remote
- --local force local, skip remotes
-sess Attach to existing session (also: sess attach )
- Ctrl+b d to detach
-
-sess ls List sessions
-sess rm Remove session (kills tmux)
-
-sess diff [path] Git diff in session's directory
-sess log [N] Activity log (default: 20)
-sess connections [N] Connection log (detach/exit)
-sess path Print session's cwd (for scripts)
-sess code Open Cursor/VS Code for session
-sess status [name] Session info or overall status
-
-sess ssh [args] SSH to configured remote VM
-sess up Reconnect to previously active sessions
-sess init [name] Provision a host (tmux+git+sess) + register it
-sess remote add [name] Register an already-provisioned remote
-sess remote ls List configured remotes
-sess remote rm [name] Remove a remote
-
-sess doctor Check prerequisites
-sess help Show help
-sess version Show version
+```sh
+sess n build -h staging
+sess a build -h staging
+sess ls -h staging
+sess rm build -h staging
```
-Aliases: `ls`/`list`, `rm`/`remove`, `connections`/`conn`. `[branch]` is a label stored in `state` and exported as `SESS_BRANCH`; it defaults to the current branch of the cwd (else `main`) and is never checked out.
+Host resolution is explicit flag → saved default → actionable error. There is no local fallback. `-h` means host; help is `--help`.
-Which commands go to the VM when a `default` remote is configured:
+Names are 1–48 letters, numbers, hyphens, or underscores, starting with a letter or number. Names are scoped to the remote account. Aliases for the same account and VM see the same sessions.
-| Proxied over ssh | Run against the laptop's `~/.sess` only |
-|---|---|
-| `sess new` (unless `--local`), `sess `, `sess ls`, `sess rm`, `sess up`, `sess ssh` | `sess diff`, `sess log`, `sess connections`, `sess path`, `sess status `, `sess code` |
+New shells start in the remote home directory. Use `cd` normally. Sessions share the VM's filesystem and Git checkout; creating a session does not copy a repository.
-## Configuration
+### Scripts
-Environment variables read by `bin/sess`:
+```sh
+sess new build --detach # also: -d; does not need a terminal
+sess ls --json # {"host": "dev", "sessions": [...]}
+sess ls --quiet # names only; also: -q
+```
-| Variable | Default | Purpose |
-|---|---|---|
-| `SESS_DIR` | `~/.sess` | State directory (sessions, remotes, active list) |
-| `SESS_EDITOR` | `cursor`, then `code` | Editor command for `sess code` |
-| `SHELL` | `bash` | Shell exec'd inside a new tmux session |
+Redirected bare `sess` prints a list. `new` without `--detach` and `attach` require an interactive terminal. Errors go to stderr with a nonzero exit status. JSON list output includes stable session IDs, client counts, creation time, initial directory, PID, and attached/detached state.
-Set inside every session shell by `tmux-init.sh`: `SESS_SESSION`, `SESS_BRANCH`, `SESS_DIR`.
+## Detach and reconnect
-Files under `SESS_DIR`:
+- **Ctrl+\** detaches only your current terminal. Work stays on the VM.
+- **Close the terminal tab** to disconnect; attach from another terminal later.
+- **`sess detach` inside the remote shell** detaches every client of that session.
+- **Ctrl+C while attached** interrupts the foreground program as usual.
+- **`exit` at the main shell prompt** ends the session.
+- **`sess rm `** terminates the session; files written to the VM remain.
-| Path | Contents |
-|---|---|
-| `remote` | `name.host=user@host` lines; `default` is the one attach/ls/rm/up/ssh use |
-| `active-sessions` | One session name per line, most recent last |
-| `sessions//state` | `SESS_SESSION`, `SESS_BRANCH`, `SESS_CWD`, `SESS_CREATED` (sourced by the script) |
-| `sessions//log` | Timestamped activity log |
-| `sessions//connections` | Timestamped `detach` / `exit` events |
-| `sessions//tmux-init.sh` | Generated init script for the tmux session |
+When SSH reports a transient connection failure, sess retries with delays of 1, 2, 4, 8, then at most 15 seconds. Ctrl+C during reconnection cancels the attempt. Intentional detach and normal shell exit do not reconnect. Authentication, host-key, configuration, and unknown SSH errors are reported instead of blindly retried.
-Makefile: `PREFIX` (default `/usr/local`) and `DESTDIR` control where `make install` puts the script and completions.
+A reconnect request carries the original session ID. If somebody removed the session and reused its name, reconnect stops instead of knowingly attaching you to the replacement. Missing sessions are never recreated by the sess attach command.
-Optional macOS hook: `etc/wakeup` is a SleepWatcher script that runs `sess up` in the background 3 seconds after wake. Install with `brew install sleepwatcher`, `cp etc/wakeup ~/.wakeup`, `chmod +x ~/.wakeup`. An already-attached remote session reconnects on its own through the retry loop; the hook is only for reopening sessions you were not attached to.
+No reconnect process remains after you close the client terminal. Run `sess a ` to return. The VM and zmx must stay alive: live sessions do not survive a VM reboot, backend crash, or operating-system process cleanup.
-## Design decisions
+## Session browser
-- One script, copied as-is. `sess init` scps `bin/sess` to the VM, so every remote command is just `ssh host "sess ..."` running the same code. No daemon, no port, no protocol of its own.
-- Reconnect is a blind retry. `cmd_remote_attach` ignores the ssh exit status and reattaches after 3 seconds no matter why ssh returned. Simple and hard to break, but it means `Ctrl+b d` on a remote session comes back after 3 seconds; `Ctrl+C` during the wait is the way out.
-- State is plain text. `state` is a shell file that gets `source`d, `remote` is `key=value`, logs are one line per event. Easy to `cat`, `grep` and `scp`, and easy to hand-edit if something goes wrong.
-- tmux options are set per session with `tmux set-option -t `, applied on every attach. Your `.tmux.conf` is untouched, but the status line of a sess session is always sess's.
-- The branch argument is metadata, not a checkout. Sessions are meant to be cheap labels over one working tree, which is the "no worktrees" part of the tagline.
-- Only the remote named `default` drives attach, ls, rm, up and ssh. Other remote names exist for `sess new --remote ` and the interactive picker.
+The browser refreshes asynchronously, shows host context and client counts, and has distinct loading, empty, filtered, and unreachable-host states.
-## Project layout
+| Key | Action |
+| --- | --- |
+| `↑` / `↓`, `k` / `j` | Select a session |
+| `Enter` | Attach; return to the browser on detach |
+| `n` | Create and attach |
+| `x` | Remove; type the session name to confirm in the browser |
+| `/` | Filter by name |
+| `h` | Browse another host without changing your saved default |
+| `r` | Refresh |
+| `?` | Keyboard help |
+| `q`, `Esc` | Quit or cancel the current form |
-```
-bin/sess The tool. Single bash script, VERSION at the top.
-bin/sess-cli.js npm shim: execFileSync(bin/sess, argv)
-etc/bash-completion/sess bash completion
-etc/zsh-completion/_sess zsh completion
-etc/wakeup Optional macOS SleepWatcher hook that runs `sess up`
-docs/index.html Static landing page
-test/test_sess.sh Smoke test (uses a temp SESS_DIR, needs git)
-Makefile install / uninstall / test / clean
-package.json npm package `sess-sh`, bin `sess`
-```
+The terminal is handed directly to SSH while attached; sess does not wrap the shell in another TUI. `NO_COLOR` is supported. Start attachments from an ordinary laptop terminal, rather than nesting persistent terminals across SSH.
-## Development
+## Under the hood
-```bash
-make test # bash -n on bin/sess, loads the bash completion
-bash test/test_sess.sh # smoke test: version, help, doctor, ls, status, path, log, rm
+```text
+laptop VM
+sess CLI / browser ── SSH ──> sess agent
+ │
+ zmx client
+ │ Unix socket
+ zmx daemon
+ │ PTY
+ shell + programs
```
-`make test` does not run `test/test_sess.sh`; run both. The smoke test creates a throwaway git repo and a session state dir under `/tmp` and cleans them up on exit. It never attaches (that needs a TTY).
+The agent speaks a versioned JSON command protocol over ordinary SSH. Requests are encoded as a single argument; names and hosts are validated rather than interpolated into shell commands. Interactive attachment uses SSH's PTY. The client never reimplements SSH authentication or terminal emulation.
-There is no linter or CI workflow in the repo. The version string lives in two places, `VERSION=` in `bin/sess` and `"version"` in `package.json`; bump both before `npm publish`. The published files are the `files` list in `package.json`.
+zmx owns the terminal process and restores its display when a client returns. sess uses a private, account-specific runtime directory, separate from ordinary zmx sessions. A random ID is stored on each session; zmx supplies live session state, so a local cache cannot claim that an unreachable VM is empty.
-## Limitations
+The remote agent is installed at an absolute path relative to `$HOME`; SSH startup PATH is not required. Session shells receive the managed binary directory on PATH so `sess detach` works there. User shell configuration can still override PATH. The reported directory is the directory at creation, not a continually tracked shell `pwd`.
-Things you can see in the code today:
+Local configuration is `~/.config/sess/config.json` (`XDG_CONFIG_HOME` supported), written atomically with private permissions. `SESS_CONFIG` overrides its path. Runtime and backend overrides `SESS_RUNTIME_DIR` and `SESS_ZMX` are intended for development/testing. `SESS_SSH` selects a local SSH executable or wrapper.
-- No `drop` event. The connection log only ever gets `detach` or `exit`; the help text and `docs/index.html` still mention `drop`. A network drop usually kills the remote script with the SSH session, so nothing is written.
-- `sess up` with a default remote reads the laptop's `~/.sess/active-sessions`, but the remote paths of `sess new` and `sess ` hand off to ssh before writing it. In a pure remote workflow it reports "No previously active sessions".
-- The Terminal tabs that `sess up` opens on macOS run a plain `ssh -t host 'sess '`, without the retry loop. On Linux the remote `sess up` attaches one session at a time.
-- `sess diff`, `log`, `connections`, `path`, `status ` and `code` only read the laptop's state dir, so with a default remote they fail for sessions that exist only on the VM. Run them on the VM through `sess ssh` instead.
-- A session created with `sess new --remote dev2` can only be reattached with `sess ` if `dev2` is also the `default` remote.
+## Development
-## Contributing
+```sh
+make build # builds client and embeds remote helpers
+make test # unit tests with race detector + go vet
+make integration # real SSH and zmx in disposable Docker
+```
-Open an issue or PR at [deepaksilaych/sess](https://github.com/deepaksilaych/sess). Run `make test` and `bash test/test_sess.sh` before pushing.
+Integration testing requires Docker, Python 3, and OpenSSH. It generates temporary keys/configuration, binds SSH only on loopback, and removes its test container on exit. It does not operate on your configured VMs.
-## License
+```text
+cmd/sess/ Client entry point
+cmd/sess-agent/ Small remote helper entry point
+internal/api/ Shared data types and validation
+internal/cli/ Commands and completion
+internal/tui/ Interactive session browser
+internal/transport/ System SSH and reconnect policy
+internal/backend/ zmx lifecycle and namespace
+internal/agent/ Remote request handling
+internal/provision/ Embedded helpers and verified zmx installer
+internal/store/ Local configuration
+scripts/ Cross-build and release packaging
+test/integration/ Isolated SSH lifecycle tests
+```
-MIT. See [LICENSE](LICENSE).
+[Full user guide](docs/user-guide.md) · [zmx](https://github.com/neurosnap/zmx) · [MIT license](LICENSE)
diff --git a/bin/sess b/bin/sess
index b4970a1..373f8bd 100755
--- a/bin/sess
+++ b/bin/sess
@@ -1,969 +1,9 @@
-#!/usr/bin/env bash
-# sess — tmux session manager
-# Single binary, zero dependencies beyond tmux+git.
-# Auto-reconnect: sess up reconnects to previously active sessions.
-set -euo pipefail
-
-VERSION="0.5.0"
-PROG="$(basename "$0")"
-SESS_DIR="${SESS_DIR:-$HOME/.sess}"
-
-# ─── Output ─────────────────────────────────────────────────────────────────
-die() { printf '%s\n' "$*" >&2; exit 1; }
-info() { printf '%s\n' "$*"; }
-
-# ─── Helpers ─────────────────────────────────────────────────────────────────
-_sess_dir() { echo "$SESS_DIR/sessions/$1"; }
-_state() { echo "$SESS_DIR/sessions/$1/state"; }
-_log_file() { echo "$SESS_DIR/sessions/$1/log"; }
-_is_macos() { [[ "$(uname -s)" == "Darwin" ]]; }
-_is_linux() { [[ "$(uname -s)" == "Linux" ]]; }
-
-# Resolve the real path to this script, following symlinks (for scp'ing to remotes)
-_self_path() {
- local src="${BASH_SOURCE[0]}"
- while [[ -L "$src" ]]; do
- local dir
- dir="$(cd -P "$(dirname "$src")" && pwd)"
- src="$(readlink "$src")"
- [[ "$src" = /* ]] || src="$dir/$src"
- done
- printf '%s\n' "$src"
-}
-
-# ─── Remote config ──────────────────────────────────────────────────────────
-_remote_config() { echo "$SESS_DIR/remote"; }
-
-_remote_get() {
- local name="$1" key="$2"
- local cfg
- cfg="$(_remote_config)"
- [[ -f "$cfg" ]] || return 1
- grep "^${name}.${key}=" "$cfg" | head -1 | cut -d= -f2-
-}
-
-_remote_set() {
- local name="$1" key="$2" value="$3"
- local cfg
- cfg="$(_remote_config)"
- mkdir -p "$(dirname "$cfg")"
- if [[ -f "$cfg" ]]; then
- local tmp
- tmp="$(mktemp)"
- grep -v "^${name}.${key}=" "$cfg" > "$tmp" 2>/dev/null || true
- echo "${name}.${key}=${value}" >> "$tmp"
- mv "$tmp" "$cfg"
- else
- echo "${name}.${key}=${value}" > "$cfg"
- fi
-}
-
-# List configured remote names (unique, in file order)
-_remote_names() {
- local cfg
- cfg="$(_remote_config)"
- [[ -f "$cfg" ]] || return 0
- sed -n 's/^\([^.]*\)\.host=.*/\1/p' "$cfg" | awk '!seen[$0]++'
-}
-
-# Resolve which remote host to use for a new session.
-# Order: explicit --remote flag > only remote configured > interactive pick > none (local).
-_select_remote() {
- local explicit="${1:-}"
- if [[ -n "$explicit" ]]; then
- local host
- host="$(_remote_get "$explicit" "host" 2>/dev/null || true)"
- [[ -n "$host" ]] || die "Remote '$explicit' not configured. Use: sess remote ls"
- echo "$host"
- return 0
- fi
-
- local names=()
- while IFS= read -r n; do
- [[ -n "$n" ]] && names+=("$n")
- done < <(_remote_names)
-
- case "${#names[@]}" in
- 0) return 0 ;;
- 1) _remote_get "${names[0]}" "host"; return 0 ;;
- *)
- if [[ ! -t 0 ]]; then
- # Non-interactive: prefer "default" if present, else the first configured remote.
- local host
- host="$(_remote_get "default" "host" 2>/dev/null || true)"
- if [[ -n "$host" ]]; then
- echo "$host"
- else
- _remote_get "${names[0]}" "host"
- fi
- return 0
- fi
-
- printf 'Multiple remotes configured:\n' >&2
- local i=1 n h
- for n in "${names[@]}"; do
- h="$(_remote_get "$n" "host")"
- printf ' %d) %s (%s)\n' "$i" "$n" "$h" >&2
- i=$((i + 1))
- done
- printf ' %d) local (no remote)\n' "$i" >&2
- printf 'Select remote [1-%d]: ' "$i" >&2
- local choice
- read -r choice
-
- if [[ "$choice" == "$i" ]]; then
- echo ""
- return 0
- fi
- if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#names[@]} ]]; then
- _remote_get "${names[$((choice - 1))]}" "host"
- else
- die "Invalid selection."
- fi
- ;;
- esac
-}
-
-# ─── Connection log ──────────────────────────────────────────────────────────
-_conn_log() {
- local session="$1" event="$2" detail="${3:-}"
- local conn_log
- conn_log="$(_sess_dir "$session")/connections"
- mkdir -p "$(dirname "$conn_log")"
- printf '%s %s%s\n' "$(date -Iseconds)" "$event" "${detail:+ ($detail)}" >> "$conn_log"
-}
-
-# ─── Log an event ───────────────────────────────────────────────────────────
-_log_event() {
- local session="$1" event="$2"
- local log
- log="$(_log_file "$session")"
- mkdir -p "$(dirname "$log")"
- printf '%s %s\n' "$(date -Iseconds)" "$event" >> "$log"
-}
-
-# ─── Record which sessions are "active" (last attached) ────────────────────
-_mark_active() {
- local session="$1"
- local active_file="$SESS_DIR/active-sessions"
- mkdir -p "$(dirname "$active_file")"
- local tmp
- tmp="$(mktemp)"
- grep -v "^${session}$" "$active_file" 2>/dev/null > "$tmp" || true
- echo "$session" >> "$tmp"
- mv "$tmp" "$active_file"
-}
-
-_mark_inactive() {
- local session="$1"
- local active_file="$SESS_DIR/active-sessions"
- [[ -f "$active_file" ]] || return 0
- local tmp
- tmp="$(mktemp)"
- grep -v "^${session}$" "$active_file" 2>/dev/null > "$tmp" || true
- mv "$tmp" "$active_file"
-}
-
-# ─── Apply tmux status bar for a session ────────────────────────────────────
-_apply_tmux_status() {
- local session="$1"
-
- # Status bar styling
- tmux set-option -t "$session" status on
- tmux set-option -t "$session" status-interval 3
- tmux set-option -t "$session" status-style "bg=colour235,fg=colour250"
- tmux set-option -t "$session" status-left-length 50
- tmux set-option -t "$session" status-right-length 100
-
- # Left: session name
- tmux set-option -t "$session" status-left \
- "#[fg=colour39,bold] ${session} #[default]"
-
- # Right: git branch | short path | time
- tmux set-option -t "$session" status-right \
- "#[fg=colour245]#(cd '#{pane_current_path}' && git rev-parse --abbrev-ref HEAD 2>/dev/null || echo ?) #[fg=colour39] #{b:pane_current_path} #[fg=colour250] %H:%M "
-
- # Window list styling
- tmux set-option -t "$session" window-status-current-style "fg=colour39,bold"
- tmux set-option -t "$session" window-status-current-format " #W "
- tmux set-option -t "$session" window-status-format " #W "
-
- # Status bar separator
- tmux set-option -t "$session" status-left-style "bg=colour237"
- tmux set-option -t "$session" status-right-style "bg=colour237"
-}
-
-# ─── Launch or reuse tmux session ───────────────────────────────────────────
-_tmux_start() {
- local session="$1" cwd="$2" branch="${3:-main}"
- local sdir
- sdir="$(_sess_dir "$session")"
-
- if ! tmux has-session -t "$session" 2>/dev/null; then
- # Write init script that sets sess env vars then execs the user's shell
- local init_path="$sdir/tmux-init.sh"
- local user_shell="${SHELL:-bash}"
- cat > "$init_path" </dev/null || cd
-exec '${user_shell}'
-INIT
- chmod +x "$init_path"
-
- tmux new-session -d -s "$session" -c "$cwd" "$init_path"
- fi
- # Always refresh status bar on attach (covers existing sessions too)
- _apply_tmux_status "$session"
-}
-
-# ─── cmd: new ────────────────────────────────────────────────────────────────
-cmd_new() {
- local remote_name="" force_local=false
- local args=()
- while [[ $# -gt 0 ]]; do
- case "$1" in
- -r|--remote) remote_name="${2:?Usage: sess new [branch] --remote }"; shift 2 ;;
- --local) force_local=true; shift ;;
- *) args+=("$1"); shift ;;
- esac
- done
- set -- "${args[@]}"
-
- local session="${1:?Usage: sess new [branch] [--remote |--local]}"
- local branch="${2:-}"
-
- # If a remote is configured (or selected), create the session on the VM and attach
- local remote_host=""
- if ! $force_local; then
- remote_host="$(_select_remote "$remote_name")"
- fi
- if [[ -n "$remote_host" ]]; then
- info "Creating session '$session' on $remote_host..."
- exec ssh -t "$remote_host" "sess new $session${branch:+ $branch} --local"
- fi
-
- local sdir
- sdir="$(_sess_dir "$session")"
-
- [[ -d "$sdir" ]] && die "Session '$session' already exists. Use: sess $session"
-
- if [[ -z "$branch" ]] && git rev-parse --abbrev-ref HEAD &>/dev/null; then
- branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo main)"
- fi
- branch="${branch:-main}"
-
- mkdir -p "$sdir"
-
- cat > "$(_state "$session")" </dev/null || true)"
- if [[ -n "$remote" ]]; then
- cmd_remote_attach "$session"
- return
- fi
-
- local sdir
- sdir="$(_sess_dir "$session")"
- [[ -d "$sdir" ]] || die "Session '$session' does not exist. Use: sess new $session"
-
- source "$(_state "$session")"
-
- local cwd="${SESS_CWD:-$(pwd)}"
-
- _log_event "$session" "attached"
- _mark_active "$session"
-
- _tmux_start "$session" "$cwd" "${SESS_BRANCH:-main}"
-
- # Attach — returns on detach (Ctrl+b d) or when session is killed
- tmux attach-session -t "$session" || true
-
- # Classify how this ended
- if tmux has-session -t "$session" 2>/dev/null; then
- _conn_log "$session" "detach" "Ctrl+b d"
- _log_event "$session" "detached (Ctrl+b d)"
- else
- _conn_log "$session" "exit"
- _log_event "$session" "exited"
- _mark_inactive "$session"
- fi
-}
-
-# ─── cmd: ls ─────────────────────────────────────────────────────────────────
-cmd_ls() {
- local remote_host
- remote_host="$(_remote_get "default" "host" 2>/dev/null || true)"
- if [[ -n "$remote_host" ]]; then
- ssh "$remote_host" "sess ls"
- return
- fi
-
- local sbase="$SESS_DIR/sessions"
- [[ -d "$sbase" ]] || { info "No sessions. Use: sess new "; return 0; }
-
- local count=0
- for d in "$sbase"/*/; do
- [[ -d "$d" ]] || continue
- count=$((count + 1))
- done
-
- [[ $count -eq 0 ]] && { info "No sessions. Use: sess new "; return 0; }
-
- printf '%-20s %-15s %-10s %-20s\n' "SESSION" "BRANCH" "STATUS" "CREATED"
- printf '%-20s %-15s %-10s %-20s\n' "--------------------" "---------------" "----------" "--------------------"
-
- for d in "$sbase"/*/; do
- [[ -d "$d" ]] || continue
- local name
- name="$(basename "$d")"
- local state="$d/state"
- local branch="?" created="?"
-
- if [[ -f "$state" ]]; then
- branch="$(grep '^SESS_BRANCH=' "$state" | cut -d= -f2)"
- created="$(grep '^SESS_CREATED=' "$state" | cut -d= -f2)"
- created="${created%%+*}"
- fi
-
- local status="inactive"
- tmux has-session -t "$name" 2>/dev/null && status="active"
-
- printf '%-20s %-15s %-10s %-20s\n' "$name" "$branch" "$status" "$created"
- done
-}
-
-# ─── cmd: rm ─────────────────────────────────────────────────────────────────
-cmd_rm() {
- local session="${1:?Usage: sess rm }"
-
- local remote_host
- remote_host="$(_remote_get "default" "host" 2>/dev/null || true)"
- if [[ -n "$remote_host" ]]; then
- ssh "$remote_host" "sess rm $session"
- return
- fi
-
- local sdir
- sdir="$(_sess_dir "$session")"
-
- [[ -d "$sdir" ]] || die "Session '$session' does not exist."
-
- # Kill tmux session if running
- if tmux has-session -t "$session" 2>/dev/null; then
- tmux kill-session -t "$session"
- fi
-
- _log_event "$session" "removed"
- _mark_inactive "$session"
- rm -rf "$sdir"
- info "Session '$session' removed."
-}
-
-# ─── cmd: diff ───────────────────────────────────────────────────────────────
-cmd_diff() {
- local session="${1:?Usage: sess diff [path]}"
- local path="${2:-}"
-
- local sdir
- sdir="$(_sess_dir "$session")"
- [[ -d "$sdir" ]] || die "Session '$session' does not exist."
-
- source "$(_state "$session")"
-
- local cwd="${SESS_CWD:-$(pwd)}"
- git -C "$cwd" rev-parse --is-inside-work-tree &>/dev/null || die "Session '$session' cwd is not a git repository."
-
- _log_event "$session" "diff${path:+ (path: $path)}"
-
- if [[ -n "$path" ]]; then
- git -C "$cwd" diff -- "$path"
- else
- git -C "$cwd" diff
- fi
-}
-
-# ─── cmd: log ────────────────────────────────────────────────────────────────
-cmd_log() {
- local session="${1:?Usage: sess log [N]}"
- local lines="${2:-20}"
-
- local sdir
- sdir="$(_sess_dir "$session")"
- [[ -d "$sdir" ]] || die "Session '$session' does not exist."
-
- local log
- log="$(_log_file "$session")"
-
- if [[ ! -f "$log" ]]; then
- info "No activity log for session '$session'."
- return 0
- fi
-
- info "Activity log for '$session':"
- tail -n "$lines" "$log"
-}
-
-# ─── cmd: path ───────────────────────────────────────────────────────────────
-cmd_path() {
- local session="${1:?Usage: sess path }"
-
- local sdir
- sdir="$(_sess_dir "$session")"
- [[ -d "$sdir" ]] || die "Session '$session' does not exist."
-
- source "$(_state "$session")"
-
- echo "${SESS_CWD:-$(pwd)}"
-}
-
-# ─── cmd: status ─────────────────────────────────────────────────────────────
-cmd_status() {
- local session="${1:-}"
-
- if [[ -z "$session" ]]; then
- info "sess $VERSION"
- info "State dir: $SESS_DIR"
- info "Platform: $(uname -s)"
-
- local count=0
- for _ in "$SESS_DIR/sessions"/*/; do
- [[ -d "$_" ]] && count=$((count + 1)) || true
- done
- info "Sessions: $count"
-
- local active_file="$SESS_DIR/active-sessions"
- if [[ -f "$active_file" ]] && [[ -s "$active_file" ]]; then
- info "Previously active:"
- while IFS= read -r name; do
- info " $name"
- done < "$active_file"
- fi
- return 0
- fi
-
- local sdir
- sdir="$(_sess_dir "$session")"
- [[ -d "$sdir" ]] || die "Session '$session' does not exist."
-
- source "$(_state "$session")"
-
- info "Session: $session"
- info " Branch: ${SESS_BRANCH:-?}"
- info " CWD: ${SESS_CWD:-?}"
- info " Created: ${SESS_CREATED:-?}"
-
- if tmux has-session -t "$session" 2>/dev/null; then
- info " Status: active (tmux session running)"
- else
- info " Status: inactive"
- fi
-
- local log
- log="$(_log_file "$session")"
- if [[ -f "$log" ]]; then
- info ""
- info "Last 5 events:"
- tail -n 5 "$log" | while IFS= read -r line; do
- info " $line"
- done
- fi
-}
-
-# ─── cmd: up ─────────────────────────────────────────────────────────────────
-cmd_up() {
- local active_file="$SESS_DIR/active-sessions"
-
- if [[ ! -f "$active_file" ]] || [[ ! -s "$active_file" ]]; then
- info "No previously active sessions."
- info "Start one with: sess new "
- return 0
- fi
-
- local remote_host
- remote_host="$(_remote_get "default" "host" 2>/dev/null || true)"
-
- if [[ -n "$remote_host" ]]; then
- _remote_up "$remote_host"
- else
- _local_up
- fi
-}
-
-_local_up() {
- local active_file="$SESS_DIR/active-sessions"
- local sessions=()
- while IFS= read -r name; do
- [[ -d "$(_sess_dir "$name")" ]] || continue
- sessions+=("$name")
- done < "$active_file"
-
- if [[ ${#sessions[@]} -eq 0 ]]; then
- info "No active sessions to reconnect to."
- return 0
- fi
-
- info "Reconnecting to ${#sessions[@]} session(s)..."
-
- # If inside tmux already, switch-client; otherwise attach
- if [[ -n "${TMUX:-}" ]]; then
- # Already in a tmux session — switch to the first one
- local first="${sessions[0]}"
- for session in "${sessions[@]}"; do
- source "$(_state "$session")"
- local cwd="${SESS_CWD:-$(pwd)}"
- _tmux_start "$session" "$cwd" "${SESS_BRANCH:-main}"
- done
- tmux switch-client -t "${first}"
- else
- # Attach to the last active session
- local last="${sessions[${#sessions[@]}-1]}"
- cmd_attach "$last"
- fi
-}
-
-# ─── cmd: ssh ────────────────────────────────────────────────────────────────
-cmd_ssh() {
- local remote_host
- remote_host="$(_remote_get "default" "host" 2>/dev/null || true)"
- [[ -n "$remote_host" ]] || die "No remote configured. Use: sess remote add "
-
- exec ssh "$remote_host" "$@"
-}
-
-# ─── cmd: code ────────────────────────────────────────────────────────────────
-cmd_code() {
- local session="${1:?Usage: sess code }"
- local sdir
- sdir="$(_sess_dir "$session")"
- [[ -d "$sdir" ]] || die "Session '$session' does not exist."
-
- source "$(_state "$session")"
- local cwd="${SESS_CWD:-$(pwd)}"
-
- local remote_host
- remote_host="$(_remote_get "default" "host" 2>/dev/null || true)"
-
- local editor_cmd="${SESS_EDITOR:-}"
- if [[ -z "$editor_cmd" ]]; then
- if command -v cursor &>/dev/null; then
- editor_cmd="cursor"
- elif command -v code &>/dev/null; then
- editor_cmd="code"
- else
- die "Neither 'cursor' nor 'code' found. Install one or set SESS_EDITOR."
- fi
- fi
-
- if [[ -n "$remote_host" ]]; then
- local uri="vscode-remote://ssh-remote+${remote_host}${cwd}"
- _log_event "$session" "code (remote: $editor_cmd)"
- info "Opening $editor_cmd on $remote_host:$cwd"
- "$editor_cmd" --remote "$remote_host" "$cwd" 2>/dev/null || \
- "$editor_cmd" "$uri" 2>/dev/null || \
- "$editor_cmd" "$cwd"
- else
- _log_event "$session" "code (local: $editor_cmd)"
- info "Opening $editor_cmd at $cwd"
- "$editor_cmd" "$cwd"
- fi
-}
-
-# ─── cmd: remote ─────────────────────────────────────────────────────────────
-cmd_remote() {
- local subcmd="${1:?Usage: sess remote ...}"
- shift
-
- case "$subcmd" in
- add) remote_add "$@" ;;
- rm|remove) remote_rm "$@" ;;
- ls) remote_ls "$@" ;;
- *) die "Unknown remote subcommand: $subcmd. Use: add, ls, rm" ;;
- esac
-}
-
-remote_add() {
- local name="${1:-default}"
- local host="${2:?Usage: sess remote add [name] }"
-
- _remote_set "$name" "host" "$host"
-
- info "Testing SSH connection to $host..."
- if ssh -o ConnectTimeout=5 -o BatchMode=yes "$host" "echo ok" &>/dev/null; then
- info "SSH connection to $host works."
- else
- info "SSH connection to $host failed. Make sure you have key-based auth set up."
- info " ssh-copy-id $host"
- fi
-
- info "Checking if sess is installed on remote..."
- if ssh "$host" "command -v sess" &>/dev/null; then
- info "sess is available on $host."
- else
- info "sess is not installed on $host."
- info "Install it with: sess init $host"
- fi
-
- info "Remote '$name' configured: $host"
-}
-
-# ─── cmd: init ───────────────────────────────────────────────────────────────
-# Provisions a remote host: installs tmux + git, ships this sess script over,
-# puts it on PATH, and registers the remote.
-cmd_init() {
- local host="${1:?Usage: sess init [name]}"
- local name="${2:-default}"
-
- info "Testing SSH connection to $host..."
- ssh -o ConnectTimeout=8 "$host" "echo ok" &>/dev/null \
- || die "Could not connect to $host. Check the host/credentials, e.g.: ssh $host"
- info "SSH connection to $host works."
-
- info "Installing tmux + git on $host (may prompt for sudo password)..."
- ssh -t "$host" '
- set -e
- if command -v tmux >/dev/null 2>&1 && command -v git >/dev/null 2>&1; then
- echo "tmux + git already installed."
- exit 0
- fi
- if command -v apt-get >/dev/null 2>&1; then
- sudo apt-get update -y && sudo apt-get install -y tmux git
- elif command -v dnf >/dev/null 2>&1; then
- sudo dnf install -y tmux git
- elif command -v yum >/dev/null 2>&1; then
- sudo yum install -y tmux git
- elif command -v apk >/dev/null 2>&1; then
- sudo apk add --no-cache tmux git
- elif command -v pacman >/dev/null 2>&1; then
- sudo pacman -Sy --noconfirm tmux git
- elif command -v brew >/dev/null 2>&1; then
- brew install tmux git
- else
- echo "No supported package manager found. Install tmux and git manually." >&2
- exit 1
- fi
- ' || die "Failed to install tmux/git on $host."
-
- info "Installing sess on $host..."
- local self
- self="$(_self_path)"
- ssh "$host" "mkdir -p ~/bin"
- scp -q "$self" "$host:~/bin/sess"
- ssh "$host" "chmod +x ~/bin/sess"
-
- ssh "$host" '
- for rc in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.profile"; do
- [ -f "$rc" ] || continue
- grep -qs "HOME/bin" "$rc" || printf "export PATH=\"\$HOME/bin:\$PATH\"\n" >> "$rc"
- done
- '
-
- info "Verifying install..."
- ssh "$host" '~/bin/sess version' || die "sess did not install correctly on $host."
-
- _remote_set "$name" "host" "$host"
- info ""
- info "Remote '$name' ready: $host"
- info "Start a session with: sess new "
-}
-
-remote_rm() {
- local name="${1:-default}"
- local cfg
- cfg="$(_remote_config)"
- [[ -f "$cfg" ]] || { info "No remotes configured."; return 0; }
-
- local tmp
- tmp="$(mktemp)"
- grep -v "^${name}\." "$cfg" > "$tmp" 2>/dev/null || true
- mv "$tmp" "$cfg"
-
- info "Remote '$name' removed."
-}
-
-remote_ls() {
- local cfg
- cfg="$(_remote_config)"
- if [[ ! -f "$cfg" ]] || [[ ! -s "$cfg" ]]; then
- info "No remotes configured. Use: sess remote add "
- return 0
- fi
-
- info "Configured remotes:"
- while IFS='=' read -r key value; do
- local name="${key%%.*}"
- local prop="${key#*.}"
- info " $name.$prop = $value"
- done < "$cfg"
-}
-
-# ─── Remote attach (SSH wrapper) ────────────────────────────────────────────
-cmd_remote_attach() {
- local session="$1"
- local remote_host
- remote_host="$(_remote_get "default" "host" 2>/dev/null || true)"
- [[ -n "$remote_host" ]] || die "No remote configured. Use: sess remote add "
-
- while true; do
- ssh -t "$remote_host" "sess $session" || true
- printf '\n[sess] disconnected. reconnecting in 3s... (Ctrl+C to stop)\n'
- sleep 3
- done
-}
-
-# ─── Remote up: reconnect to all active sessions on VM ──────────────────────
-_remote_up() {
- local remote_host="$1"
- local active_file="$SESS_DIR/active-sessions"
-
- [[ -f "$active_file" ]] || { info "No previously active sessions."; return 0; }
-
- local sessions=()
- while IFS= read -r name; do
- sessions+=("$name")
- done < "$active_file"
-
- if [[ ${#sessions[@]} -eq 0 ]]; then
- info "No active sessions to reconnect to."
- return 0
- fi
-
- local alive_sessions=()
- for session in "${sessions[@]}"; do
- if ssh "$remote_host" "test -d ~/.sess/sessions/$session" 2>/dev/null; then
- alive_sessions+=("$session")
- fi
- done
-
- if [[ ${#alive_sessions[@]} -eq 0 ]]; then
- info "No active sessions found on $remote_host."
- return 0
- fi
-
- info "Reconnecting to ${#alive_sessions[@]} session(s) on $remote_host..."
-
- for session in "${alive_sessions[@]}"; do
- if _is_macos; then
- osascript -e "tell application \"Terminal\"" \
- -e "activate" \
- -e "set currentTab to do script \"ssh -t $remote_host 'sess $session'\"" \
- -e "end tell" 2>/dev/null || {
- info "Could not open Terminal tab for '$session'."
- }
- else
- ssh -t "$remote_host" "sess $session"
- fi
- done
-}
-
-# ─── cmd: doctor ─────────────────────────────────────────────────────────────
-cmd_doctor() {
- local ok=true
-
- info "Platform: $(uname -s)"
-
- # tmux
- if command -v tmux &>/dev/null; then
- info "tmux: $(tmux -V)"
- else
- info "tmux: NOT installed"
- info " Install: apt install tmux / brew install tmux"
- ok=false
- fi
-
- # git
- if command -v git &>/dev/null; then
- info "git: $(git --version)"
- else
- info "git: NOT installed"
- ok=false
- fi
-
- # bash
- if command -v bash &>/dev/null; then
- info "bash: $(bash --version | head -1)"
- else
- info "bash: NOT installed"
- ok=false
- fi
-
- # SSH
- if command -v ssh &>/dev/null; then
- info "ssh: $(ssh -V 2>&1 | head -1 || echo 'installed')"
- else
- info "ssh: NOT installed (needed for remote sessions)"
- ok=false
- fi
-
- local remote_host
- remote_host="$(_remote_get "default" "host" 2>/dev/null || true)"
- if [[ -n "$remote_host" ]]; then
- info "Remote: $remote_host"
- if ssh -o ConnectTimeout=5 -o BatchMode=yes "$remote_host" "echo ok" &>/dev/null; then
- info "Remote SSH: working"
- else
- info "Remote SSH: connection failed (key-based auth required)"
- ok=false
- fi
- else
- info "Remote: not configured (use: sess remote add )"
- fi
-
- info ""
- if $ok; then
- info "All prerequisites met."
- else
- info "Some prerequisites missing. See above."
- fi
-}
-
-# ─── cmd: connections ────────────────────────────────────────────────────────
-cmd_connections() {
- local session="${1:?Usage: sess connections }"
- local lines="${2:-20}"
-
- local sdir
- sdir="$(_sess_dir "$session")"
- [[ -d "$sdir" ]] || die "Session '$session' does not exist."
-
- local conn_log="$sdir/connections"
-
- if [[ ! -f "$conn_log" ]]; then
- info "No connection log for session '$session'."
- return 0
- fi
-
- info "Connection log for '$session':"
- tail -n "$lines" "$conn_log"
-}
-
-# ─── cmd: help ───────────────────────────────────────────────────────────────
-cmd_help() {
- cat <<'HELP'
-sess — tmux session manager
-
-USAGE:
- sess new [branch] Create session + auto-attach
- --remote use a specific remote
- --local force local, skip remotes
- sess Attach to existing session
- Ctrl+b d to detach
-
- sess ls List sessions
- sess rm Remove session (kills tmux)
-
- sess diff [path] Git diff in session's directory
- sess log [N] Activity log (default: last 20)
- sess connections [N] Connection log (detach/exit/drop)
- sess path Print session's cwd (for scripts)
- sess code Open Cursor/VS Code for session
- sess status [name] Session info or overall status
-
- sess ssh [args] SSH to configured remote VM
- sess up Reconnect to previously active sessions
- sess init [name] Provision a host: install tmux+git+sess,
- register it as a remote
- sess remote add [name] Register an already-provisioned remote
- sess remote ls List configured remotes
- sess remote rm [name] Remove a remote
-
- sess doctor Check prerequisites
- sess help This help
- sess version Show version
-
-TMUX STATUS BAR:
- Inside a session the bottom bar shows:
- [left] session-name
- [right] git-branch dir-name HH:MM
- Updates every 3 seconds. No configuration needed.
-
-DETACH / REATTACH:
- Ctrl+b d detach from session (session persists in tmux)
- sess reattach
- sess up reconnect all previously active sessions
-
- Standard tmux keybindings work inside sessions:
- Ctrl+b c new window
- Ctrl+b " split horizontal
- Ctrl+b % split vertical
- Ctrl+b [ scroll mode
-
-CONNECTION LOG:
- Every time a session ends, sess records how it ended:
- detach — Ctrl+b d (intentional detach)
- exit — shell exited (typed exit or Ctrl+d)
-
- View with: sess connections
-
-AUTO-RECONNECT:
- When you close your laptop, SSH drops but sessions survive on the VM (tmux keeps them).
- When you open it, run: sess up
- This reconnects to all sessions you had active before.
-
-REMOTE SESSIONS:
- sess init user@dev-vm # one-time: install + register
- sess ssh # just SSH to the VM
- sess new feature-auth # creates on remote
- sess feature-auth # SSHes to remote, attaches
- sess code feature-auth # opens Cursor/VS Code on laptop
- sess up # reconnects all active on wake
-
- Multiple remotes configured? sess new prompts you to pick one,
- or skip the prompt with: sess new --remote
- Only one remote configured — it's used automatically, no prompt.
-
-ENVIRONMENT:
- SESS_DIR State directory (default: ~/.sess)
- SESS_SESSION Set inside sessions (session name)
- SESS_BRANCH Set inside sessions (branch)
- SESS_EDITOR Editor for 'sess code' (default: cursor or code)
-HELP
-}
-
-# ─── Main dispatch ───────────────────────────────────────────────────────────
-case "${1:-help}" in
- new) shift; cmd_new "$@" ;;
- rm|remove) shift; cmd_rm "$@" ;;
- ls|list) shift; cmd_ls "$@" ;;
- attach) shift; cmd_attach "$@" ;;
- up) shift; cmd_up "$@" ;;
- ssh) shift; cmd_ssh "$@" ;;
- code) shift; cmd_code "$@" ;;
- diff) shift; cmd_diff "$@" ;;
- log) shift; cmd_log "$@" ;;
- connections|conn) shift; cmd_connections "$@" ;;
- path) shift; cmd_path "$@" ;;
- status) shift; cmd_status "$@" ;;
- remote) shift; cmd_remote "$@" ;;
- init) shift; cmd_init "$@" ;;
- doctor) shift; cmd_doctor "$@" ;;
- help|--help|-h) cmd_help ;;
- version|--version|-v) info "sess $VERSION" ;;
- *)
- # Check local dir OR remote configured (sessions live on VM)
- if [[ -d "$(_sess_dir "$1")" ]] || [[ -n "$(_remote_get "default" "host" 2>/dev/null || true)" ]]; then
- cmd_attach "$1"
- else
- die "Unknown command: $1. Run 'sess help' for usage."
- fi
- ;;
-esac
+#!/bin/sh
+# Source-checkout convenience launcher. Releases install the compiled executable.
+set -eu
+base=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
+if [ ! -x "$base/dist/sess" ]; then
+ printf 'sess has moved to Go + zmx. Run make build in %s first.\n' "$base" >&2
+ exit 1
+fi
+exec "$base/dist/sess" "$@"
diff --git a/bin/sess-cli.js b/bin/sess-cli.js
deleted file mode 100755
index 4f02e20..0000000
--- a/bin/sess-cli.js
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/usr/bin/env node
-// npx wrapper — delegates to the real bash script
-const { execFileSync } = require('child_process');
-const path = require('path');
-
-// The bash script is next to this file
-const script = path.join(__dirname, 'sess');
-try {
- execFileSync(script, process.argv.slice(2), {
- stdio: 'inherit',
- env: { ...process.env }
- });
-} catch (e) {
- process.exit(e.status || 1);
-}
\ No newline at end of file
diff --git a/cmd/sess-agent/main.go b/cmd/sess-agent/main.go
new file mode 100644
index 0000000..bcc0c1e
--- /dev/null
+++ b/cmd/sess-agent/main.go
@@ -0,0 +1,8 @@
+package main
+
+import (
+ "github.com/DeepakSilaych/sess/internal/agent"
+ "os"
+)
+
+func main() { os.Exit(agent.Run(os.Args[1:])) }
diff --git a/cmd/sess/main.go b/cmd/sess/main.go
new file mode 100644
index 0000000..095474f
--- /dev/null
+++ b/cmd/sess/main.go
@@ -0,0 +1,8 @@
+package main
+
+import (
+ "github.com/DeepakSilaych/sess/internal/cli"
+ "os"
+)
+
+func main() { os.Exit(cli.Main()) }
diff --git a/docs/index.html b/docs/index.html
index d74e64d..e8276fa 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -1,547 +1,33 @@
-
+
-
-
-sess — tmux session manager
-
+
+
+
+sess — A persistent SSH terminal
-
-
-
-
-
-
-
-
-
tmux session manager
-
- One tool. No worktrees. No containers. Sessions that survive everything.
-
- Persistent terminal sessions backed by tmux. Create a session, work inside it, detach with Ctrl+b d. The session survives SSH drops, laptop sleep, and network failures.
-
-
-
-
-
tmux persistence
-
Native tmux sessions. Full tmux keybindings available. Sessions survive anything.
-
-
-
Native status bar
-
Session name, git branch, current dir, and time — right in the tmux status line.
-
-
-
Connection log
-
Every session end classified: detach, exit, or drop. Know what happened.
-
-
-
Remote sessions
-
sess init user@host once. Then sess new, sess ssh all go through SSH automatically.
-
-
-
Auto-reconnect on wake
-
sess up reconnects to all previously active sessions. Zero commands to type.
-
-
-
-
-
-
Every command
-
- One tool. No subcommands to memorize. Just sess <name>.
-
-
-
-
-
-
Command
-
Description
-
-
-
-
sess new <name>
Create session + auto-attach
-
sess <name>
Reattach (Ctrl+b d to detach)
-
sess ls
List sessions
-
sess rm <name>
Destroy session
-
sess diff <name>
Git diff in session directory
-
sess log <name>
Activity log
-
sess connections <name>
Connection log (detach/exit/drop)
-
sess path <name>
Print session's cwd
-
sess code <name>
Open Cursor / VS Code
-
sess ssh
SSH to configured VM
-
sess up
Reconnect all active sessions
-
sess init <user@host> [name]
Install tmux+git+sess on a host, register it
-
sess remote add [name] <host>
Register an already-provisioned remote
-
sess doctor
Check prerequisites
-
-
-
-
-
-
Why not just use...?
-
-
-
-
-
-
sess
-
git worktree
-
tmux
-
Docker
-
-
-
-
-
Creation time
-
instant
-
~1s
-
instant
-
seconds
-
-
-
SSH persistence
-
auto-reconnect
-
manual
-
yes
-
manual
-
-
-
Status bar
-
tmux native
-
no
-
yes
-
no
-
-
-
Connection log
-
detach/exit/drop
-
no
-
no
-
no
-
-
-
-
-
-
-
Install
-
- One command. Requires tmux, git, and bash.
-
-
-
- npx sess-sh
-
-
-
-
- npm install -g sess-sh
-
-
-
-
- git clone https://github.com/DeepakSilaych/sess.git && cd sess && sudo make install
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
+
+
Powered by zmx
A persistent SSH terminal.
Your work stays on the VM. Name a terminal, disconnect, and come back to the same running shell.
The zmx-based 0.6 version is in development. Build instructions are in the repository.
+
YOUR LAPTOP → DEV VM
$ sess init dev
+$ sess set --host dev
+$ sess new work
+
+# Work on the VM. Press Ctrl+\ to detach.
+
+$ sess attach work
+
Pick up where you left off.
Detach intentionally or lose your network connection. zmx holds your terminal on the VM; sess gets you back into it. Automatic reconnection stops when you intentionally detach.
Your terminal, your layout.
Use your terminal app for tabs and windows. Run sess for a searchable session browser, then attach directly to your remote shell.
+
A small command set.
Command
What it does
+
sess init <host>
Prepare sess and zmx on your VM
+
sess set --host <host>
Save your default SSH destination
+
sess new <name>
Create and attach · shortcut n
+
sess attach <name>
Return to a session · shortcut a
+
sess ls
List sessions · --json for scripts
+
sess remove <name>
Terminate a session · shortcut rm
+
Use --host or -h to override the host for one command. SSH aliases and user@host work throughout. Use --help for help.
+
SSH connects. zmx persists.
sess uses system SSH and installs a small helper on the VM. Each persistent terminal lives under zmx, separately from its SSH connection. No extra network port or laptop background service is needed.
Live sessions depend on the VM and backend staying alive. A VM reboot ends its processes; persistence is not a backup or automatic job recovery.