CLI and desktop app share the same database:
~/.rssh/rssh.db # SQLite (profiles, credentials, forwards, settings)
~/.rssh/snippets.json # command snippets
~/.ssh/known_hosts # OpenSSH-standard host key store (shared with `ssh`)
All platforms need:
- Node.js >= 20
- Rust stable (install via rustup)
- npm (comes with Node)
No extra dependencies. Xcode Command Line Tools must be installed:
xcode-select --installsudo apt-get update
sudo apt-get install -y \
libgtk-3-dev libwebkit2gtk-4.1-dev \
libayatana-appindicator3-dev librsvg2-devNo extra dependencies. Visual Studio Build Tools with C++ workload must be installed.
- JDK 17 (recommend Eclipse Temurin)
- Android SDK with NDK installed
- Rust targets:
rustup target add aarch64-linux-android armv7-linux-androideabi
# install frontend deps
npm install
# start dev server (hot-reload frontend + Rust backend)
npm run tauri devnpm run tauri buildOutput lands in src-tauri/target/release/bundle/.
The desktop app bundles a CLI binary via resources. To include it:
# 1. build CLI
cd src-tauri
cargo build --release --features cli --bin rssh-cli
# 2. stage it
mkdir -p bin
cp target/release/rssh-cli bin/ # or rssh-cli.exe on Windows
# 3. build app
cd ..
npx tauri build# add the target
rustup target add x86_64-apple-darwin
# build
npx tauri build --target x86_64-apple-darwin# init (first time only)
npx tauri android init
# dev
npx tauri android dev
# release APK
npx tauri android build --apkThe release APK requires a signing keystore. See src-tauri/gen/android/key.properties.
For a local device build, add an Apple ID under Xcode > Settings > Accounts, connect and trust the iPhone, and enable Developer Mode. For a new account or device, initialize the generated project once:
APPLE_DEVELOPMENT_TEAM=XXXXXXXXXX \
./build-ios.sh --openIn Xcode, select the app target, enable Automatically manage signing, choose the Personal Team and connected iPhone, then Run once. This one-time step is required because Tauri 2.11 can update provisioning assets but cannot register a new device from its CLI build.
After the device is registered, build the IPA with the Personal Team ID:
APPLE_DEVELOPMENT_TEAM=XXXXXXXXXX ./build-ios.shThe script uses Xcode automatic signing and the debugging export method by
default. A free Personal Team profile normally expires after seven days, so the
app must then be rebuilt and reinstalled. The Apple ID and password are read by
Xcode from the local account and must not be passed to the script. Development
builds use com.rssh.app.dev.<TEAM_ID> to avoid colliding with the release App
ID. Override it only when necessary:
APPLE_DEVELOPMENT_TEAM=XXXXXXXXXX \
IOS_BUNDLE_IDENTIFIER=com.example.rssh.dev \
./build-ios.shiOS release packaging runs on GitHub Actions. Before enabling it, register the
identifier from src-tauri/tauri.conf.json (com.rssh.app by default) as an
explicit App ID. The registered Bundle ID and the Tauri identifier must match
exactly.
Configure these GitHub Actions secrets with the manual signing material:
APPLE_DEVELOPMENT_TEAM: the 10-character Apple Developer Team IDIOS_CERTIFICATE: base64-encoded Apple Distribution.p12IOS_CERTIFICATE_PASSWORD: password used when exporting that.p12IOS_MOBILE_PROVISION: base64-encoded App Store Connect provisioning profile
Run iOS Release manually from the Actions tab to test the setup. Its optional
build_number input defaults to the monotonically increasing GitHub Actions run
number. To package iOS automatically for v* tags, set the repository variable
IOS_BUILD_ENABLED to true; without that opt-in, tag-triggered iOS jobs are
skipped and the existing desktop/Android release workflow is unchanged.
The workflow builds with --export-method app-store-connect, uploads the IPA as
a workflow artifact, and, for tags, appends it to the draft GitHub release. The
IPA is not uploaded to App Store Connect automatically. The CI helper copies the
required privacy manifest into the generated Xcode project before packaging.
iOS may suspend network sessions when RSSH enters the background; this project does not claim a background execution mode it cannot guarantee.
The frontend can run outside the Tauri desktop shell — in a plain browser, or inside IntelliJ's embedded Chromium (JCEF). This is additive; the desktop app is unchanged.
It is not an "IPC → WebSocket" rewrite. The frontend always talks to the
backend through one seam: Tauri's injected window.__TAURI_INTERNALS__
(invoke / listen). There are two implementations of that one contract:
- Desktop (unchanged): Tauri injects the global; IPC is in-process via the WebView.
- Outside Tauri:
src/lib/ipc-shim.tsdetects the global is absent and installs a compatible one that routesinvoke/listenover a localhost WebSocket torssh-server. Inside the real app the shim is a no-op.
So the 90+ invoke / 25+ listen call sites are untouched and the desktop
transport stays byte-identical. On the Rust side the same engine is reused via a
Host enum (src-tauri/src/emitter.rs): Tauri commands pass Host::Tauri
(→ app.emit / app.state), the headless server passes Host::Headless (→ ws push).
src-tauri/src/server.rs # headless adapter: HTTP (embedded UI) + ws (IPC) on one port
src-tauri/src/emitter.rs # Host enum (Tauri | headless sink)
src-tauri/src/server_main.rs # the `rssh-server` binary entry (--features server; kept out of src/bin/ so the Tauri bundler ignores it)
src/lib/ipc-shim.ts # Tauri-IPC shim over ws (no-op inside Tauri)
idea-plugin/ # IntelliJ plugin (JCEF tool window)
rssh-server is self-contained: npm run build's output is embedded into the binary
(include_dir!), and a single loopback port serves both the UI (HTTP) and the IPC
(WebSocket, guarded by a per-launch token).
npm run build
node scripts/dev-browser.mjs # builds + runs rssh-server, prints a localhost URL to opennpm run build
cargo build --release --manifest-path src-tauri/Cargo.toml --features server --bin rssh-server
export RSSH_SERVER_BIN="$PWD/src-tauri/target/release/rssh-server"
# open idea-plugin/ in IDEA → run the `runIde` Gradle task → open the "RSSH" tool windowPackage: cd idea-plugin && ./gradlew buildPlugin → a zip under build/distributions/.
Ship it on a GitHub release; install via Settings → Plugins → ⚙ → Install Plugin from
Disk. Details in idea-plugin/README.md.
src/ # frontend (Svelte 5)
lib/
stores/app.svelte.ts # reactive state
components/ # UI components
styles/global.css # theme tokens
src-tauri/ # backend (Rust)
src/
main.rs # entry point
lib.rs # Tauri app builder, IPC commands
models.rs # domain types
error.rs # error types
state.rs # AppState (DB + sessions)
db/ # SQLite CRUD
ssh/ # russh client, config parser, forwarding, SFTP
commands/ # Tauri command handlers
terminal/ # PTY + asciicast recorder
crypto.rs # config encryption
sync/ # Remote sync (GitHub + WebDAV)
src/bin/rssh.rs # CLI binary (behind `cli` feature flag)
gen/android/ # Android build files
gen/apple/ # iOS Xcode project and privacy manifest
Push a git tag to trigger CI:
git tag v0.2.0
git push origin v0.2.0GitHub Actions builds desktop and Android artifacts and creates a draft release.
The separate iOS Release workflow adds a signed IPA only when explicitly run
or when IOS_BUILD_ENABLED=true enables tag builds.
Desktop/Android artifact naming:
rssh-{version}-{os}-{arch}.{ext}
The iOS Actions artifact is named rssh-ios-{run_id}-build-{build_number};
the signed file inside it, and on a tagged draft release, is RSSH.ipa.
Version is derived from the git tag. No need to manually update tauri.conf.json or Cargo.toml -- CI syncs them automatically.
- Rust:
cargo fmt+cargo clippy - Frontend: default Svelte/TypeScript conventions
- Commits: explain why, not what