Go + Wails v3 + Vue 3 + SQLite + FreeRDP3 (purego) 183 tests, 76 source files, 9,910 lines of code Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
18 KiB
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:
// 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]*SSHSessionwith mutex SSHSessionholds:*ssh.Client,*ssh.Session, stdinio.WriteCloser, connection metadata- PTY requested as
xterm-256colorwith 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-verifyevent 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:
- Scans byte stream for
\033]7;prefix - Extracts URL between prefix and
\033\\(or\007) terminator - Parses
file://hostname/pathto extract just the path - Strips the OSC 7 sequence from the output before forwarding to xterm.js
- Returns the new CWD path when detected
Shell injection command (injected after PTY is established):
# 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:
// 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:
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/sftpdependency -
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:
// 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→ WailsSSHService.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
sessionIdprop - 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(notv-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.ListuploadFile(sessionId, remotePath, file)→ chunked upload with progressdownloadFile(sessionId, remotePath)→ triggers browser downloaddeleteFile(sessionId, path)→ with confirmationcreateDirectory(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:
- User double-clicks connection in sidebar
- Session store calls Wails
SSHService.Connect(connectionId) - If host key verification needed → show HostKeyDialog
- On success → create tab, mount TerminalView, open SFTP sidebar
- 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:
// 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:
{
"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