# Wraith Desktop — Phase 2: SSH + SFTP Implementation Plan

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

**Goal:** Connect to remote hosts via SSH with a real terminal (xterm.js), SFTP sidebar with file operations and CWD following, multi-tab sessions, and CodeMirror 6 file editor.

**Architecture:** Go SSH service wraps `x/crypto/ssh` for connections, PTY requests, and shell I/O. SFTP service wraps `pkg/sftp` riding the same SSH connection. Data flows: xterm.js ↔ Wails bindings ↔ Go SSH pipes. CWD tracking via OSC 7 shell injection.

**Tech Stack:** `golang.org/x/crypto/ssh`, `github.com/pkg/sftp`, xterm.js 5.x + WebGL addon + fit addon + search addon, CodeMirror 6

**Spec:** `docs/superpowers/specs/2026-03-17-wraith-desktop-design.md` (Sections 6, 10, 11)

---

## File Structure (New/Modified)

```
internal/
  ssh/
    service.go              # SSH dial, PTY, shell, I/O goroutines
    service_test.go         # Connection config tests (unit, no real SSH)
    hostkey.go              # Host key verification + storage
    hostkey_test.go
    cwd.go                  # OSC 7 parser for CWD tracking
    cwd_test.go
  sftp/
    service.go              # SFTP operations (list, upload, download, etc.)
    service_test.go
  credentials/
    service.go              # Credential CRUD (encrypted passwords + SSH keys)
    service_test.go
  app/
    app.go                  # Add SSH/SFTP/Credential services
frontend/
  src/
    components/
      terminal/
        TerminalView.vue    # xterm.js instance wrapper
      sftp/
        FileTree.vue        # Remote filesystem tree
        TransferProgress.vue # Upload/download progress
      editor/
        EditorWindow.vue    # CodeMirror 6 (placeholder for multi-window)
    composables/
      useTerminal.ts        # xterm.js lifecycle + Wails binding bridge
      useSftp.ts            # SFTP operations via Wails bindings
    stores/
      session.store.ts      # Update with real session management
    assets/
      css/
        terminal.css        # xterm.js overrides
  package.json              # Add xterm.js, codemirror deps
```

---

## Task 1: SSH Service — Connect, PTY, Shell I/O

**Files:**
- Create: `internal/ssh/service.go`
- Create: `internal/ssh/service_test.go`

The SSH service manages connections and exposes methods to Wails:

```go
// SSHService methods (exposed to frontend via Wails bindings):
// Connect(connectionID int64) (string, error)     → returns sessionID
// Write(sessionID string, data string) error       → write to stdin
// Resize(sessionID string, cols, rows int) error   → window change
// Disconnect(sessionID string) error               → close session
//
// Events emitted to frontend via Wails events:
// "ssh:data:{sessionID}"    → terminal output (stdout)
// "ssh:connected:{sessionID}" → connection established
// "ssh:disconnected:{sessionID}" → connection closed
// "ssh:error:{sessionID}"   → error message
```

Key implementation details:
- Each SSH session runs two goroutines: one reading stdout→Wails events, one for keepalive
- Sessions stored in a `map[string]*SSHSession` with mutex
- `SSHSession` holds: `*ssh.Client`, `*ssh.Session`, stdin `io.WriteCloser`, connection metadata
- PTY requested as `xterm-256color` with initial size from frontend
- Auth method determined by credential type (password, SSH key, keyboard-interactive)
- Host key verification delegates to `hostkey.go`

Tests (unit only — no real SSH server):
- TestSSHServiceCreation
- TestBuildAuthMethods (password → ssh.Password, key → ssh.PublicKeys)
- TestSessionTracking (create, get, remove)

- [ ] **Step 1:** Write tests
- [ ] **Step 2:** Implement service
- [ ] **Step 3:** Run tests, verify pass
- [ ] **Step 4:** Commit: `feat: SSH service — connect, PTY, shell I/O with goroutine pipes`

---

## Task 2: Host Key Verification

**Files:**
- Create: `internal/ssh/hostkey.go`
- Create: `internal/ssh/hostkey_test.go`

Host key verification stores/checks fingerprints in the `host_keys` SQLite table:
- New host → emit `ssh:hostkey-verify` event to frontend with fingerprint, wait for accept/reject
- Known host, matching fingerprint → proceed silently
- Known host, CHANGED fingerprint → emit warning event, block connection

For Phase 2, implement the storage and verification logic. The frontend prompt (accept/reject dialog) will be wired in the frontend task.

Tests:
- TestStoreHostKey
- TestVerifyKnownHost (match → ok)
- TestVerifyChangedHost (mismatch → error)
- TestVerifyNewHost (not found → returns "new")

- [ ] **Step 1:** Write tests
- [ ] **Step 2:** Implement hostkey.go
- [ ] **Step 3:** Run tests, verify pass
- [ ] **Step 4:** Commit: `feat: SSH host key verification — store, verify, detect changes`

---

## Task 3: CWD Tracker (OSC 7 Parser)

**Files:**
- Create: `internal/ssh/cwd.go`
- Create: `internal/ssh/cwd_test.go`

Parses OSC 7 escape sequences from terminal output to track the remote working directory:

```
Input:  "some output\033]7;file://hostname/home/user\033\\more output"
Output: stripped="some output more output", cwd="/home/user"
```

The CWD tracker:
1. Scans byte stream for `\033]7;` prefix
2. Extracts URL between prefix and `\033\\` (or `\007`) terminator
3. Parses `file://hostname/path` to extract just the path
4. Strips the OSC 7 sequence from the output before forwarding to xterm.js
5. Returns the new CWD path when detected

Shell injection command (injected after PTY is established):
```bash
# bash
PROMPT_COMMAND='printf "\033]7;file://%s%s\033\\" "$(hostname)" "$PWD"'
# zsh
precmd() { printf "\033]7;file://%s%s\033\\" "$(hostname)" "$PWD" }
# fish
function fish_prompt; printf "\033]7;file://%s%s\033\\" (hostname) "$PWD"; end
```

Tests:
- TestParseOSC7Basic
- TestParseOSC7WithBEL (terminated by \007 instead of ST)
- TestParseOSC7NoMatch (no OSC 7 in output)
- TestParseOSC7MultipleInStream
- TestStripOSC7FromOutput

- [ ] **Step 1:** Write tests
- [ ] **Step 2:** Implement cwd.go
- [ ] **Step 3:** Run tests, verify pass
- [ ] **Step 4:** Commit: `feat: OSC 7 CWD tracker — parse and strip directory change sequences`

---

## Task 4: SFTP Service

**Files:**
- Create: `internal/sftp/service.go`
- Create: `internal/sftp/service_test.go`

SFTP service wraps `pkg/sftp` and exposes file operations to the frontend:

```go
// SFTPService methods (exposed via Wails bindings):
// OpenSFTP(sessionID string) error                              → start SFTP on existing SSH connection
// List(sessionID string, path string) ([]FileEntry, error)      → directory listing
// ReadFile(sessionID string, path string) (string, error)       → read file content (max 5MB)
// WriteFile(sessionID string, path string, content string) error → write file
// Upload(sessionID string, remotePath string, localPath string) error
// Download(sessionID string, remotePath string) (string, error) → returns local temp path
// Mkdir(sessionID string, path string) error
// Delete(sessionID string, path string) error
// Rename(sessionID string, oldPath, newPath string) error
// Stat(sessionID string, path string) (*FileEntry, error)
```

`FileEntry` type:
```go
type FileEntry struct {
    Name        string `json:"name"`
    Path        string `json:"path"`
    Size        int64  `json:"size"`
    IsDir       bool   `json:"isDir"`
    Permissions string `json:"permissions"`
    ModTime     string `json:"modTime"`
    Owner       string `json:"owner"`
}
```

SFTP client is created from the existing `*ssh.Client` (same connection, separate channel). Stored alongside the SSH session.

Tests (unit — mock the sftp.Client interface):
- TestFileEntryFromFileInfo
- TestListSortsDirectoriesFirst
- TestReadFileRejectsLargeFiles (>5MB)

- [ ] **Step 1:** Write tests
- [ ] **Step 2:** Implement service
- [ ] **Step 3:** Add `pkg/sftp` dependency
- [ ] **Step 4:** Run tests, verify pass
- [ ] **Step 5:** Commit: `feat: SFTP service — list, read, write, upload, download, mkdir, delete`

---

## Task 5: Credential Service (Encrypted SSH Keys + Passwords)

**Files:**
- Create: `internal/credentials/service.go`
- Create: `internal/credentials/service_test.go`

CRUD for credentials and SSH keys with vault encryption:

```go
// CredentialService methods:
// CreatePassword(name, username, password, domain string) (*Credential, error)
// CreateSSHKey(name string, privateKey, passphrase []byte) (*SSHKey, error)
// GetCredential(id int64) (*Credential, error)
// ListCredentials() ([]Credential, error)
// DecryptPassword(id int64) (string, error)          → decrypt for connection use only
// DecryptSSHKey(id int64) ([]byte, string, error)    → returns (privateKey, passphrase, error)
// DeleteCredential(id int64) error
// ImportSSHKeyFile(name, filePath string) (*SSHKey, error) → read .pem file, detect type, store
```

All sensitive data encrypted via VaultService before storage. Decryption only happens at connection time.

Tests:
- TestCreatePasswordCredential
- TestCreateSSHKeyCredential
- TestDecryptPassword (round-trip through vault)
- TestDecryptSSHKey (round-trip)
- TestListCredentialsExcludesEncryptedValues
- TestDetectKeyType (RSA, Ed25519, ECDSA)

- [ ] **Step 1:** Write tests
- [ ] **Step 2:** Implement service
- [ ] **Step 3:** Run tests, verify pass
- [ ] **Step 4:** Commit: `feat: credential service — encrypted password and SSH key storage`

---

## Task 6: Wire SSH/SFTP/Credentials into App

**Files:**
- Modify: `internal/app/app.go`
- Modify: `main.go`

Add SSHService, SFTPService, and CredentialService to WraithApp. Register as Wails services.

- [ ] **Step 1:** Update app.go to create and expose new services
- [ ] **Step 2:** Update main.go to register them
- [ ] **Step 3:** Verify compilation: `go vet ./...`
- [ ] **Step 4:** Run all tests: `go test ./... -count=1`
- [ ] **Step 5:** Commit: `feat: wire SSH, SFTP, and credential services into Wails app`

---

## Task 7: Frontend — xterm.js Terminal

**Files:**
- Modify: `frontend/package.json` — add xterm.js + addons
- Create: `frontend/src/components/terminal/TerminalView.vue`
- Create: `frontend/src/composables/useTerminal.ts`
- Create: `frontend/src/assets/css/terminal.css`
- Modify: `frontend/src/components/session/SessionContainer.vue`
- Modify: `frontend/src/stores/session.store.ts`

Install xterm.js dependencies:
```
@xterm/xterm
@xterm/addon-fit
@xterm/addon-webgl
@xterm/addon-search
@xterm/addon-web-links
```

`useTerminal` composable:
- Creates xterm.js Terminal instance with theme from connection settings
- Attaches fit, WebGL, search, web-links addons
- Binds `terminal.onData` → Wails `SSHService.Write(sessionId, data)`
- Listens for Wails events `ssh:data:{sessionId}` → `terminal.write(data)`
- Handles resize via fit addon → Wails `SSHService.Resize(sessionId, cols, rows)`
- Cleanup on unmount

`TerminalView.vue`:
- Receives `sessionId` prop
- Mounts xterm.js into a div ref
- Applies theme colors from the active theme
- Handles focus management

`SessionContainer.vue` update:
- Replace placeholder with real TerminalView for SSH sessions
- Use `v-show` (not `v-if`) to keep terminals alive across tab switches

- [ ] **Step 1:** Install xterm.js deps: `cd frontend && npm install @xterm/xterm @xterm/addon-fit @xterm/addon-webgl @xterm/addon-search @xterm/addon-web-links`
- [ ] **Step 2:** Create terminal.css (xterm.js container styling)
- [ ] **Step 3:** Create useTerminal.ts composable
- [ ] **Step 4:** Create TerminalView.vue component
- [ ] **Step 5:** Update SessionContainer.vue to render TerminalView
- [ ] **Step 6:** Update session.store.ts with real Wails binding calls
- [ ] **Step 7:** Build frontend: `npm run build`
- [ ] **Step 8:** Commit: `feat: xterm.js terminal with WebGL rendering and Wails binding bridge`

---

## Task 8: Frontend — SFTP Sidebar

**Files:**
- Create: `frontend/src/components/sftp/FileTree.vue`
- Create: `frontend/src/components/sftp/TransferProgress.vue`
- Create: `frontend/src/composables/useSftp.ts`
- Modify: `frontend/src/layouts/MainLayout.vue` — SFTP sidebar rendering
- Modify: `frontend/src/components/sidebar/SidebarToggle.vue` — enable SFTP tab

`useSftp` composable:
- `listDirectory(sessionId, path)` → calls Wails SFTPService.List
- `uploadFile(sessionId, remotePath, file)` → chunked upload with progress
- `downloadFile(sessionId, remotePath)` → triggers browser download
- `deleteFile(sessionId, path)` → with confirmation
- `createDirectory(sessionId, path)`
- `renameFile(sessionId, old, new)`
- Tracks current path, file list, loading state, transfer progress

`FileTree.vue`:
- Renders file/directory tree (lazy-loaded on expand)
- Path bar at top showing current directory
- Toolbar: upload, download, new file, new folder, refresh, delete
- File entries show: icon (folder/file), name, size, modified date
- Double-click file → open in editor (Task 9)
- Drag-and-drop upload zone
- "Follow terminal folder" toggle at bottom

`TransferProgress.vue`:
- Shows active uploads/downloads with progress bars
- File name, percentage, speed, ETA

- [ ] **Step 1:** Create useSftp.ts composable
- [ ] **Step 2:** Create FileTree.vue component
- [ ] **Step 3:** Create TransferProgress.vue component
- [ ] **Step 4:** Update MainLayout.vue to render SFTP sidebar when toggled
- [ ] **Step 5:** Enable SFTP toggle in SidebarToggle.vue
- [ ] **Step 6:** Build frontend: `npm run build`
- [ ] **Step 7:** Commit: `feat: SFTP sidebar — file tree, upload/download, CWD following`

---

## Task 9: Frontend — Host Key Dialog + Connection Flow

**Files:**
- Create: `frontend/src/components/common/HostKeyDialog.vue`
- Modify: `frontend/src/components/sidebar/ConnectionTree.vue` — double-click to connect
- Modify: `frontend/src/stores/session.store.ts` — real connection flow

Wire up the full connection flow:
1. User double-clicks connection in sidebar
2. Session store calls Wails `SSHService.Connect(connectionId)`
3. If host key verification needed → show HostKeyDialog
4. On success → create tab, mount TerminalView, open SFTP sidebar
5. On error → show error toast

`HostKeyDialog.vue`:
- Modal showing: hostname, key type, fingerprint
- "New host" vs "CHANGED host key (WARNING)" modes
- Accept / Reject buttons
- "Always accept for this host" checkbox

- [ ] **Step 1:** Create HostKeyDialog.vue
- [ ] **Step 2:** Update ConnectionTree.vue with double-click handler
- [ ] **Step 3:** Update session.store.ts with connection flow
- [ ] **Step 4:** Build frontend: `npm run build`
- [ ] **Step 5:** Commit: `feat: connection flow — host key dialog, double-click to connect`

---

## Task 10: Frontend — CodeMirror 6 Editor (Placeholder)

**Files:**
- Modify: `frontend/package.json` — add CodeMirror deps
- Create: `frontend/src/components/editor/EditorWindow.vue`

Install CodeMirror 6:
```
codemirror
@codemirror/lang-javascript
@codemirror/lang-json
@codemirror/lang-html
@codemirror/lang-css
@codemirror/lang-python
@codemirror/lang-markdown
@codemirror/theme-one-dark
```

`EditorWindow.vue`:
- Renders CodeMirror 6 editor with dark theme
- Receives file content, path, and sessionId as props
- Syntax highlighting based on file extension
- Save button → calls Wails SFTPService.WriteFile
- Unsaved changes detection
- For Phase 2: renders inline (not separate window — multi-window is Phase 4)

- [ ] **Step 1:** Install CodeMirror deps
- [ ] **Step 2:** Create EditorWindow.vue
- [ ] **Step 3:** Wire file click in FileTree.vue to open EditorWindow
- [ ] **Step 4:** Build frontend: `npm run build`
- [ ] **Step 5:** Commit: `feat: CodeMirror 6 editor — syntax highlighting, dark theme, SFTP save`

---

## Task 11: Workspace Snapshot Persistence

**Files:**
- Create: `internal/app/workspace.go`
- Create: `internal/app/workspace_test.go`

Implements workspace snapshot saving/restoring per the spec:

```go
// SaveWorkspace() error — serialize current tab layout to settings
// LoadWorkspace() (*WorkspaceSnapshot, error) — read last saved layout
// Auto-save every 30 seconds via goroutine
// Save on clean shutdown
```

WorkspaceSnapshot JSON:
```json
{
  "tabs": [
    {"connectionId": 1, "protocol": "ssh", "position": 0},
    {"connectionId": 5, "protocol": "rdp", "position": 1}
  ],
  "sidebarWidth": 240,
  "sidebarMode": "connections",
  "activeTab": 0
}
```

Tests:
- TestSaveAndLoadWorkspace
- TestEmptyWorkspace

- [ ] **Step 1:** Write tests
- [ ] **Step 2:** Implement workspace.go
- [ ] **Step 3:** Run tests, verify pass
- [ ] **Step 4:** Commit: `feat: workspace snapshot persistence — auto-save layout every 30s`

---

## Task 12: Integration Test + Final Verification

- [ ] **Step 1:** Run all Go tests: `go test ./... -count=1`
- [ ] **Step 2:** Build frontend: `cd frontend && npm run build`
- [ ] **Step 3:** Verify Go compiles with embedded frontend: `go vet ./...`
- [ ] **Step 4:** Count tests and lines of code
- [ ] **Step 5:** Commit any fixes: `chore: Phase 2 complete — SSH + SFTP with terminal and file operations`

---

## Phase 2 Completion Checklist

- [ ] SSH service: connect, PTY, shell I/O with goroutine pipes
- [ ] Host key verification: store, verify, detect changes
- [ ] OSC 7 CWD tracker: parse and strip directory change sequences
- [ ] SFTP service: list, read, write, upload, download, mkdir, delete
- [ ] Credential service: encrypted password + SSH key storage
- [ ] All new services wired into Wails app
- [ ] xterm.js terminal with WebGL rendering
- [ ] SFTP file tree sidebar with upload/download
- [ ] Host key verification dialog
- [ ] Double-click connection to connect flow
- [ ] CodeMirror 6 inline editor with SFTP save
- [ ] Workspace snapshot persistence
- [ ] All Go tests passing
- [ ] Frontend builds clean
