# SSH Tunnel Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add a `--tunnel` flag that forwards port 22 of the selected EC2 instance to a random local port over SSM, then prints the SFTP connection URL for the user's file manager.

**Architecture:** Refactor `main.rs` to extract a shared `run_aws_command` helper for signal-handled process execution. Add a `tunnel()` function that picks a free local port using the OS port-0 trick, then invokes the SSM port-forwarding document. The `--tunnel` flag threads through `Args` and the post-selection dispatch.

**Tech Stack:** Rust, clap (derive), std::net::TcpListener (port selection), std::process::Command, signal-hook

---

## File Map

- **Modify:** `src/main.rs` — add `--tunnel` arg, extract `run_aws_command`, add `tunnel()` fn, update dispatch
- **Modify:** `README.md` — document the new flag and tunnel mode usage

---

### Task 1: Extract `run_aws_command` helper

**Files:**
- Modify: `src/main.rs`

- [ ] **Step 1: Extract the helper**

The signal-handling + spawn pattern in `connect()` is duplicated in the upcoming `tunnel()`. Extract it now. Replace the body of `connect()` and add a new `run_aws_command` function:

```rust
fn run_aws_command(args: &[&str]) -> Result<()> {
    let mut child = Command::new("aws")
        .args(args)
        .spawn()?;

    let mut _signals = Signals::new([SIGINT, SIGTSTP])?;

    child.wait()?;
    Ok(())
}

fn connect(instance: InstanceInfo) -> Result<()> {
    let entry = HistoryEntry::new(instance.get_instance_id());
    History::save(entry)?;
    run_aws_command(&[
        "--region",
        instance.get_region().as_ref(),
        "ssm",
        "start-session",
        "--target",
        instance.get_instance_id(),
    ])
}
```

- [ ] **Step 2: Verify the build**

```bash
cargo build 2>&1
```

Expected: no errors, no warnings.

- [ ] **Step 3: Commit**

```bash
git add src/main.rs
git commit -m "refactor: extract run_aws_command helper from connect()"
```

---

### Task 2: Add `--tunnel` flag and `pick_local_port` helper

**Files:**
- Modify: `src/main.rs`

- [ ] **Step 1: Add `use std::net::TcpListener;` to imports**

At the top of `src/main.rs`, add alongside the existing `use` statements:

```rust
use std::net::TcpListener;
```

- [ ] **Step 2: Add the `--tunnel` field to `Args`**

```rust
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
    /// AWS region (e.g., us-east-1, us-west-2)
    #[arg(short, long)]
    region: Option<String>,

    /// AWS EC2 instance ID (e.g., i-ad53d5e3831ea)
    #[arg(short, long)]
    instance: Option<String>,

    /// Forward SSH port over SSM instead of opening an interactive session
    #[arg(short, long)]
    tunnel: bool,
}
```

- [ ] **Step 3: Add `pick_local_port`**

Add this function after the imports, before `main`:

```rust
fn pick_local_port() -> Result<u16> {
    let listener = TcpListener::bind("127.0.0.1:0")?;
    let port = listener.local_addr()?.port();
    Ok(port)
    // listener dropped here, releasing the port
}
```

- [ ] **Step 4: Verify the build**

```bash
cargo build 2>&1
```

Expected: no errors. There may be an "unused" warning for `tunnel` field — that's fine, it gets wired up next task.

- [ ] **Step 5: Commit**

```bash
git add src/main.rs
git commit -m "feat: add --tunnel flag and pick_local_port helper"
```

---

### Task 3: Implement `tunnel()` and wire up dispatch

**Files:**
- Modify: `src/main.rs`

- [ ] **Step 1: Implement `tunnel()`**

Add after `connect()`:

```rust
fn tunnel(instance: InstanceInfo) -> Result<()> {
    let local_port = pick_local_port()?;
    let parameters = format!(
        r#"{{"portNumber":["22"],"localPortNumber":["{}"]}}"#,
        local_port
    );
    println!(
        "SSH tunnel ready — connect your file manager to sftp://localhost:{}\nPress Ctrl+C to close the tunnel.",
        local_port
    );
    run_aws_command(&[
        "--region",
        instance.get_region().as_ref(),
        "ssm",
        "start-session",
        "--target",
        instance.get_instance_id(),
        "--document-name",
        "AWS-StartPortForwardingSession",
        "--parameters",
        &parameters,
    ])
}
```

- [ ] **Step 2: Update `main()` to dispatch based on `args.tunnel`**

Replace the existing `main()` body with:

```rust
#[tokio::main]
async fn main() -> Result<()> {
    let args = Args::parse();

    if let (Some(region_str), Some(instance_id)) = (args.region, args.instance) {
        let region = Region::new(region_str);
        let instance = InstanceInfo::new(region, instance_id);
        if args.tunnel {
            tunnel(instance)?;
        } else {
            connect(instance)?;
        }
        return Ok(());
    }

    let mut app = App::new()?;
    let selected = app.run().await;
    drop(app);
    match selected {
        Err(e) => {
            eprintln!("Error: {}", e);
            for cause in e.chain().skip(1) {
                eprintln!("  caused by: {}", cause);
            }
        }
        Ok(Some(instance)) => {
            if args.tunnel {
                tunnel(instance)?;
            } else {
                connect(instance)?;
            }
        }
        Ok(None) => {}
    }
    Ok(())
}
```

- [ ] **Step 3: Build and verify help output**

```bash
cargo build --release 2>&1
./target/release/sm_connect --help
```

Expected: help output includes:
```
  -t, --tunnel  Forward SSH port over SSM instead of opening an interactive session
```

- [ ] **Step 4: Commit**

```bash
git add src/main.rs
git commit -m "feat: implement tunnel() and wire --tunnel flag to dispatch"
```

---

### Task 4: Update README

**Files:**
- Modify: `README.md`

- [ ] **Step 1: Add `--tunnel` to the Available Options section**

In `README.md`, find the `### Available Options` section and add:

```markdown
- `-t, --tunnel`: Forward the instance's SSH port (22) over SSM to a random local port, then print the SFTP URL for use with a file manager. Requires SSH running on the instance; authentication is handled by your file manager.
```

- [ ] **Step 2: Add a Tunnel Mode usage section**

After the `## Command-line Arguments` section, add:

```markdown
## Tunnel Mode

To connect a file manager to an instance without a VPN:

```sh
sm_connect --tunnel
```

Or directly, skipping instance selection:

```sh
sm_connect -r us-east-1 -i i-ad53d5e3831ea --tunnel
```

`sm_connect` prints an SFTP URL (e.g. `sftp://localhost:52341`) that you can open in any file manager (Cyberduck, Transmit, Finder, Midnight Commander, etc.). Press Ctrl+C when done to close the tunnel.

> **Prerequisites:** SSH must be running on the instance (port 22). Your file manager handles authentication using your existing SSH credentials.
```

- [ ] **Step 3: Commit**

```bash
git add README.md
git commit -m "docs: document --tunnel flag and tunnel mode"
```
