Skip to content
Closed
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
18 changes: 14 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ resolver = "2"
members = ["reachpad", "crates/proto", "crates/authz", "crates/runtime"]

[workspace.package]
version = "0.1.1"
version = "0.2.0"
edition = "2021"
rust-version = "1.85"
license = "UNLICENSED"
Expand All @@ -24,6 +24,7 @@ biscuit-auth = "5"
blake3 = "1"
bytes = "1"
clap = { version = "4", features = ["derive", "env"] }
clap_complete = "4"
futures = "0.3"
hex = "0.4"
proptest = "1"
Expand Down
40 changes: 31 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,36 @@ machine underneath, so a paused workspace resumes mid-session.

## Install

With Homebrew on Apple silicon macOS or x86_64/arm64 Linux:

```sh
brew install --cask reachpad/tap/reachpad
```

Or with the checksum-verifying installer:

```sh
curl -fsSL https://raw.githubusercontent.com/Reachpad/reachpad-cli/main/install.sh | sh
curl -fsSL https://reachpad.dev/install | sh
```

Linux x86_64/arm64 (musl, static) and macOS arm64. The script fetches the
latest release from this repository, verifies its checksum against
SHA256SUMS, and installs to `~/.local/bin/reachpad` (override with
`REACHPAD_INSTALL_DIR`).

## Sign in
## Get started

Run the browser-based WorkOS login:
Run Reachpad:

```sh
reachpad auth login
reachpad
```

The CLI shows a short code and opens WorkOS hosted sign-in. WorkOS handles the
account login and any required MFA or SSO. After approval, Reachpad exchanges
the short-lived WorkOS token once and saves a user-scoped Reachpad credential
and the production endpoint with mode 0600. No password or authentication
factor is entered into Reachpad.
On first use, the CLI shows a short code, opens WorkOS hosted sign-in, and then
lists your workspaces. WorkOS handles the account login and any required MFA or
SSO. After approval, Reachpad exchanges the short-lived WorkOS token once and
saves a user-scoped Reachpad credential and the production endpoint with mode
0600. No password or authentication factor is entered into Reachpad.

On a remote machine without a usable browser, run `reachpad auth login
--no-browser` and open the displayed URL on another device. The manual
Expand All @@ -41,6 +49,20 @@ reachpad ws create --name scratch
reachpad attach <workspace-id>
```

Useful maintenance commands:

```sh
reachpad doctor
reachpad update
reachpad completions bash
reachpad completions zsh
reachpad completions fish
```

`reachpad update` respects how Reachpad was installed: Homebrew installs are
directed to `brew upgrade --cask reachpad`, while installer-managed binaries
are updated in place after the release checksum is verified.

Docs: [reachpad.dev/docs/cli](https://reachpad.dev/docs/cli)

## Source and provenance
Expand Down
14 changes: 14 additions & 0 deletions greentree.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
version: 1
checks:
format:
run: cargo fmt --all -- --check
required_for_publish: true
watch: false
test:
run: cargo test --workspace
required_for_publish: true
watch: false
installer:
run: shellcheck install.sh && sh -n install.sh
required_for_publish: true
watch: false
8 changes: 4 additions & 4 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# $REACHPAD_INSTALL_DIR). POSIX sh: this runs on machines we know nothing
# about, so no bashisms.
#
# curl -fsSL https://raw.githubusercontent.com/Reachpad/reachpad-cli/main/install.sh | sh
# curl -fsSL https://reachpad.dev/install | sh
#
# Nothing here needs root, and the script refuses to guess: an unsupported
# platform is an error naming the platform, never a wrong binary.
Expand Down Expand Up @@ -60,6 +60,6 @@ case ":$PATH:" in
echo " export PATH=\"$INSTALL_DIR:\$PATH\""
;;
esac
echo "next: sign in through WorkOS:"
echo " reachpad auth login"
echo "on a remote machine, use 'reachpad auth login --no-browser' and open the displayed URL elsewhere"
echo "next: start Reachpad:"
echo " reachpad"
echo "the guided first run signs in through WorkOS and lists your workspaces"
1 change: 1 addition & 0 deletions reachpad/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ thiserror.workspace = true
tokio.workspace = true
tracing.workspace = true
clap.workspace = true
clap_complete.workspace = true
serde.workspace = true
serde_json.workspace = true
base64.workspace = true
Expand Down
7 changes: 4 additions & 3 deletions reachpad/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -814,9 +814,10 @@ impl Client {
))
}

/// POST /v1/workspaces/:id/archive. Owner-only, and it destroys nothing:
/// the chain and the log stay, the workspace stops counting against
/// `max_workspaces` (I13). Returns when it was archived.
/// POST /v1/workspaces/:id/archive. Owner-only. The workspace stops
/// counting against `max_workspaces` and nothing is deleted immediately;
/// archived state follows ADR-0070's managed-retention boundary. Returns
/// when it was archived.
pub async fn archive(&self, workspace: &str, biscuit_b64: &str) -> Result<u64, ApiError> {
let body = self
.post(
Expand Down
67 changes: 64 additions & 3 deletions reachpad/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,16 @@ impl Cli {

#[derive(Subcommand, Debug)]
pub enum Command {
/// Check the installation, saved login, endpoints, and connectivity.
Doctor,
/// Install the latest release, or print the package-manager upgrade command.
Update,
/// Generate a shell completion script on stdout.
Completions {
/// Shell whose completion syntax should be generated.
#[arg(value_enum)]
shell: CompletionShell,
},
/// Show the account's compute-credit balance.
Credits,
/// Workspace lifecycle (create / attach / release).
Expand Down Expand Up @@ -185,6 +195,13 @@ pub enum Command {
Token(TokenCommand),
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
pub enum CompletionShell {
Bash,
Zsh,
Fish,
}

#[derive(Subcommand, Debug)]
pub enum KeyCommand {
/// Mint an API key (POST /v1/api-keys). The value is shown ONCE and is
Expand Down Expand Up @@ -391,9 +408,9 @@ pub enum WsCommand {
},
/// Archive a workspace (POST /v1/workspaces/:id/archive).
///
/// Frees the entitlement slot it holds. Nothing is deleted: the snapshot
/// chain and the event log are untouched (I4, I5) — the workspace simply
/// stops counting as live and can no longer be attached.
/// Frees the entitlement slot it holds and stops compute use. Nothing is
/// deleted immediately, but archived state follows managed retention
/// rather than a permanent-backup promise (ADR-0070).
Archive {
/// Workspace id.
id: String,
Expand Down Expand Up @@ -570,6 +587,50 @@ mod tests {
assert_eq!(operator_token.as_deref(), Some("-"));
}

#[test]
fn bare_reachpad_is_reserved_for_first_run_onboarding() {
let cli = parse(&["reachpad"]);
assert!(cli.command.is_none());
}

#[test]
fn maintenance_commands_and_completion_shells_parse() {
assert!(matches!(
parse(&["reachpad", "doctor"]).command,
Some(Command::Doctor)
));
assert!(matches!(
parse(&["reachpad", "update"]).command,
Some(Command::Update)
));
assert!(matches!(
parse(&["reachpad", "completions", "zsh"]).command,
Some(Command::Completions {
shell: CompletionShell::Zsh
})
));
assert!(Cli::try_parse_from(["reachpad", "completions", "nushell"]).is_err());
}

#[test]
fn every_supported_shell_generates_a_reachpad_script() {
use clap::CommandFactory as _;

for generator in [
clap_complete::Shell::Bash,
clap_complete::Shell::Zsh,
clap_complete::Shell::Fish,
] {
let mut output = Vec::new();
clap_complete::generate(generator, &mut Cli::command(), "reachpad", &mut output);
let script = String::from_utf8(output).unwrap();
assert!(
script.contains("reachpad"),
"empty script for {generator:?}"
);
}
}

#[test]
fn quic_hub_url_and_dev_pin_parse() {
let cli = parse(&[
Expand Down
19 changes: 17 additions & 2 deletions reachpad/src/cli_auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,8 +306,23 @@ pub async fn complete_device_authorization(
/// flow, so a remote machine with no desktop works without port forwarding.
pub fn open_browser(url: &str) -> bool {
#[cfg(target_os = "linux")]
if std::env::var_os("DISPLAY").is_none() && std::env::var_os("WAYLAND_DISPLAY").is_none() {
return false;
{
// Reachpad workspaces expose the owner's real browser through this
// host command even though the remote shell has no desktop display.
// Other Linux machines fall through to their ordinary desktop opener.
if std::process::Command::new("devbox-browser-open")
.arg(url)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.is_ok()
{
return true;
}
if std::env::var_os("DISPLAY").is_none() && std::env::var_os("WAYLAND_DISPLAY").is_none() {
return false;
}
}
#[cfg(target_os = "linux")]
let mut command = std::process::Command::new("xdg-open");
Expand Down
Loading