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
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,22 @@ Get this running locally in a few minutes.
curl -fsSL https://raw.githubusercontent.com/Robdel12/OrbitDock/main/orbitdock-server/install.sh | bash
```

The installer sets up the binary, shell `PATH`, data directory, database, Claude hooks, and background service.
The installer sets up the binary, shell `PATH`, data directory, and database. It then asks whether
you want to install Claude hooks and whether you want OrbitDock running as a background service.

### 2. Verify it started
### 2. Start or verify the server

```bash
orbitdock status
orbitdock doctor
```

If you skipped the background service during install, start OrbitDock manually with:

```bash
orbitdock start
```

`doctor` runs a full diagnostic — database, hooks, encryption key, disk space, port availability,
and more. If something's wrong, it'll tell you.

Expand All @@ -79,7 +86,7 @@ The app auto-connects to the local server.

**Codex** — Open Settings → CODEX CLI → Sign in with ChatGPT (or use API key auth mode).

**Claude Code** — Install the [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code/overview) and log in. The installer already set up hooks, but if you skipped that step: `orbitdock install-hooks`.
**Claude Code** — Install the [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code/overview) and log in. If you skipped hook setup during install, run `orbitdock install-hooks`.

### 5. Create a session

Expand All @@ -96,14 +103,14 @@ Running on a VPS, Raspberry Pi, NAS, or another machine:

```bash
# On the server
orbitdock setup --remote --server-url https://your-server.example.com:4000
orbitdock remote-setup

# On your developer machine
orbitdock install-hooks --server-url https://your-server.example.com:4000
```

`install-hooks` prompts for the token that `setup --remote` prints and stores it encrypted for local hook forwarding.
For the app, add the same server URL and token in Settings → Servers.
`remote-setup` guides secure exposure, creates a fresh auth token, and tells you the exact next commands
for pairing clients and forwarding hooks. For the app, add the same server URL and token in Settings → Servers.

See [DEPLOYMENT.md](docs/DEPLOYMENT.md) for Cloudflare tunnels, TLS, reverse proxies, and Raspberry Pi notes.

Expand Down
9 changes: 5 additions & 4 deletions docs/DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ curl -fsSL https://raw.githubusercontent.com/Robdel12/OrbitDock/main/orbitdock-s

```bash
orbitdock setup --local # localhost only
orbitdock setup --remote # generates auth token, binds 0.0.0.0
orbitdock remote-setup # secure remote exposure onboarding
```

If you're exposing the server through a tunnel, reverse proxy, or public hostname, pass `--server-url https://...` in remote mode so the printed instructions use the URL your other machines will actually reach.
`setup` is for local machine bootstrap. Use `remote-setup` when you want to expose an existing install
securely to other machines.

## Deployment Topologies

Expand All @@ -42,8 +43,7 @@ Run the server on a VPS, connect from your dev machine.
**On the server:**

```bash
orbitdock setup --remote --server-url https://your-server.example.com:4000
# Copy the auth token when it is printed. OrbitDock only shows it once.
orbitdock remote-setup
```

**On your dev machine** (hooks only — no local server):
Expand All @@ -53,6 +53,7 @@ orbitdock install-hooks \
--server-url https://your-server.example.com:4000
```

`remote-setup` prints the exact client URL and auth token instructions after it configures the server side.
`install-hooks` will prompt for the token and store it encrypted in `~/.orbitdock/hook-forward.json`.
For non-interactive setup, pass `--auth-token <token>` or set `ORBITDOCK_AUTH_TOKEN`.

Expand Down
22 changes: 14 additions & 8 deletions orbitdock-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,23 @@ The installer downloads a prebuilt binary for macOS, Linux x86_64, and Linux aar
- Installs `orbitdock` to `~/.orbitdock/bin/`
- Ensures `~/.orbitdock/bin` is on shell `PATH`
- Runs `orbitdock init`
- Runs `orbitdock install-hooks`
- Runs `orbitdock install-service --enable`
- Prompts before installing Claude Code hooks
- Prompts before installing the background service

Optional flags:

- `--skip-hooks` skip Claude hook setup
- `--skip-service` skip launchd/systemd install
- `--enable-service` install and start the background service without prompting
- `--server-url <url>` hooks-only mode for a remote server (skips service install)
- `--auth-token <token>` remote server auth token for non-interactive hooks-only install
- `--version <tag>` install a specific release tag (for example `v1.2.3`)
- `--force-source` skip prebuilt download and build from source with Cargo
- `-y, --yes` accept installer defaults without prompting

On interactive runs, the installer asks before editing `~/.claude/settings.json` and before
installing a launchd/systemd service. On non-interactive runs, it keeps the install lightweight by
default and only installs the service when you pass `--enable-service`.

### Standalone Setup

Expand All @@ -53,14 +59,13 @@ That's it. Codex direct sessions work immediately — the server embeds codex-co
For a dev server or headless machine:

```bash
# Interactive setup (generates token, binds 0.0.0.0)
orbitdock setup --remote --server-url https://your-server.example.com:4000

# Or manually:
orbitdock generate-token
orbitdock start --bind 0.0.0.0:4000
# Secure remote onboarding
orbitdock remote-setup
```

`remote-setup` guides exposure mode, creates a fresh auth token, optionally reconfigures the background
service, and prints the exact next commands for pairing clients and forwarding hooks.

Connect a remote developer machine (hooks only — no local server needed):

```bash
Expand Down Expand Up @@ -147,6 +152,7 @@ orbitdock [--data-dir PATH] <command>
|---------|-------------|
| `start` | Start the server (also the default when you omit the subcommand) |
| `setup` | Interactive wizard (init + hooks + token + service) |
| `remote-setup` | Guide secure remote exposure for an existing install |
| `init` | Create data directory and run migrations |
| `ensure-path` | Persist the server binary directory on your shell `PATH` |
| `install-hooks` | Merge OrbitDock hooks into `~/.claude/settings.json` |
Expand Down
12 changes: 11 additions & 1 deletion orbitdock-server/crates/server/src/cmd_ensure_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ enum ShellKind {
}

pub fn run() -> anyhow::Result<()> {
let installer_mode = installer_mode();
let binary_path = std::env::current_exe().context("failed to resolve current executable")?;
let bin_dir = binary_path.parent().ok_or_else(|| {
anyhow!(
Expand Down Expand Up @@ -53,7 +54,12 @@ pub fn run() -> anyhow::Result<()> {
bin_dir.display(),
profile_path.display()
);
println!(" Restart your terminal, or run:");
if installer_mode {
println!(" New terminals will pick it up automatically.");
println!(" For this shell only, run:");
} else {
println!(" Restart your terminal, or run:");
}
match shell_kind {
ShellKind::Fish => println!(" fish_add_path {}", quote_for_shell(bin_dir)),
_ => println!(
Expand All @@ -66,6 +72,10 @@ pub fn run() -> anyhow::Result<()> {
Ok(())
}

fn installer_mode() -> bool {
std::env::var_os("ORBITDOCK_INSTALLER_MODE").is_some()
}

fn detect_shell_kind(shell_env: Option<&str>) -> ShellKind {
let raw = shell_env.unwrap_or("/bin/bash");
let name = Path::new(raw)
Expand Down
31 changes: 19 additions & 12 deletions orbitdock-server/crates/server/src/cmd_init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::migration_runner;
use crate::paths;

pub fn run(data_dir: &Path, _server_url: &str) -> anyhow::Result<()> {
let installer_mode = installer_mode();
println!();

// 1. Create directory structure
Expand All @@ -31,24 +32,30 @@ pub fn run(data_dir: &Path, _server_url: &str) -> anyhow::Result<()> {
// 3. Detect Tailscale
let ts_ip = detect_tailscale_ip();

println!();
if let Some(ip) = &ts_ip {
println!(" Tailscale detected! Your IP: {}", ip);
println!(" For remote access (secure by default):");
println!(" orbitdock generate-token");
println!(" orbitdock start --bind 0.0.0.0:4000");
if !installer_mode {
println!();
}
if let Some(ip) = &ts_ip {
println!(" Tailscale detected! Your IP: {}", ip);
println!(" For remote access (secure by default):");
println!(" orbitdock generate-token");
println!(" orbitdock start --bind 0.0.0.0:4000");
println!();
}

println!(" Next steps:");
println!(" 1. Install Claude Code hooks: orbitdock install-hooks");
println!(" 2. Start the server: orbitdock start");
println!(" 3. Install as a service: orbitdock install-service --enable");
println!();
println!(" Next steps:");
println!(" 1. Install Claude Code hooks: orbitdock install-hooks");
println!(" 2. Start the server: orbitdock start");
println!(" 3. Install as a service: orbitdock install-service --enable");
println!();
}

Ok(())
}

fn installer_mode() -> bool {
std::env::var_os("ORBITDOCK_INSTALLER_MODE").is_some()
}

fn detect_tailscale_ip() -> Option<String> {
let output = std::process::Command::new("tailscale")
.args(["status", "--json"])
Expand Down
54 changes: 34 additions & 20 deletions orbitdock-server/crates/server/src/cmd_install_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pub fn run(
server_url: Option<&str>,
auth_token: Option<&str>,
) -> anyhow::Result<()> {
let installer_mode = installer_mode();
let settings_file = settings_path.map(PathBuf::from).unwrap_or_else(|| {
dirs::home_dir()
.expect("HOME not found")
Expand Down Expand Up @@ -144,11 +145,13 @@ pub fn run(
if settings_file.exists() {
let backup = settings_file.with_extension("json.bak");
std::fs::copy(&settings_file, &backup)?;
println!(
" Backed up {} → {}",
settings_file.display(),
backup.display()
);
if !installer_mode {
println!(
" Backed up {} → {}",
settings_file.display(),
backup.display()
);
}
}

// Ensure parent dir exists
Expand All @@ -161,41 +164,52 @@ pub fn run(
std::fs::write(&settings_file, formatted)?;

println!();
if !added.is_empty() {
println!(" Added {} hook(s):", added.len());
for h in &added {
println!(" + {}", h);
if installer_mode {
println!(" Claude Code hooks ready in {}", settings_file.display());
} else {
if !added.is_empty() {
println!(" Added {} hook(s):", added.len());
for h in &added {
println!(" + {}", h);
}
}
}
if !updated.is_empty() {
println!(" Updated {} hook(s):", updated.len());
for h in &updated {
println!(" ~ {}", h);
if !updated.is_empty() {
println!(" Updated {} hook(s):", updated.len());
for h in &updated {
println!(" ~ {}", h);
}
}
println!();
println!(" Settings written to {}", settings_file.display());
}
println!();
println!(" Settings written to {}", settings_file.display());
println!(
" Hook transport config: {}",
transport_config_path.display()
);
match resolved_auth_token {
match resolved_auth_token.as_deref() {
Some(_) => println!(" Hook auth token: configured"),
None if should_prompt_for_auth_token(target_url) => {
println!(" Hook auth token: not configured");
println!(
" Remote requests may be rejected until you rerun `orbitdock install-hooks` with a token."
);
}
None => println!(" Hook auth token: not configured"),
None if !installer_mode => println!(" Hook auth token: not configured"),
None => {}
}
if !installer_mode {
println!(" Hook forward binary: {}", resolve_hook_binary_path());
println!(" Spool directory: {}", paths::spool_dir().display());
}
println!(" Hook forward binary: {}", resolve_hook_binary_path());
println!(" Spool directory: {}", paths::spool_dir().display());
println!();

Ok(())
}

fn installer_mode() -> bool {
std::env::var_os("ORBITDOCK_INSTALLER_MODE").is_some()
}

fn resolve_auth_token(
server_url: &str,
auth_token: Option<&str>,
Expand Down
Loading