Skip to content

Commit b3da3f2

Browse files
feat: add Windsurf IDE installation and automatic session backups to create code command
- Add Windsurf IDE installation (x86_64 only) via .deb package download from codeiumdata.com - Add automatic hourly backups of Claude Code conversations and Codex sessions - Add --skip-windsurf, --skip-session-backups, and --backup-interval flags - Add parseBackupInterval helper to convert user-friendly intervals (30min, hourly, 6hours, daily) to cron expressions - Update Config struct with SkipWindsurf, Setup
1 parent 50aa9f4 commit b3da3f2

4 files changed

Lines changed: 957 additions & 7 deletions

File tree

cmd/create/code.go

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ var CreateCodeCmd = &cobra.Command{
2222
Aliases: []string{"remotecode", "remote-code", "ide"},
2323
Short: "Configure SSH for remote IDE development (Windsurf, Claude Code, VS Code)",
2424
Long: `Configure your server for remote IDE development with:
25-
- Windsurf (Codeium's AI IDE)
25+
- Windsurf (Codeium's AI IDE) - x86_64 only
2626
- Claude Code (Anthropic's AI coding assistant)
27+
- OpenAI Codex CLI
2728
- VS Code Remote SSH
2829
- Cursor
2930
- JetBrains Gateway
@@ -37,17 +38,27 @@ This command:
3738
3839
2. Configures firewall rules to allow SSH from trusted networks
3940
40-
3. Installs AI coding tools:
41-
- Claude Code (via curl -fsSL https://claude.ai/install.sh | bash)
41+
3. Installs AI coding tools (idempotent - skips if already installed):
42+
- Claude Code (via official installer)
4243
- OpenAI Codex CLI (via npm install -g @openai/codex)
44+
- Windsurf IDE (x86_64 only, via .deb package)
45+
46+
4. Sets up automatic hourly backups of coding sessions:
47+
- Claude Code conversations (~/.claude/projects/)
48+
- Codex sessions (~/.codex/sessions/)
49+
- Backup scripts installed to ~/bin/
50+
- Cron job configured for periodic backups
4351
4452
Example:
4553
sudo eos create code
4654
sudo eos create code --user henry
4755
sudo eos create code --max-sessions 30 --dry-run
4856
sudo eos create code --skip-ai-tools # Skip AI tools installation
49-
sudo eos create code --skip-claude # Only install Codex
50-
sudo eos create code --skip-codex # Only install Claude Code`,
57+
sudo eos create code --skip-claude # Only install Codex/Windsurf
58+
sudo eos create code --skip-codex # Only install Claude/Windsurf
59+
sudo eos create code --skip-windsurf # Skip Windsurf (or on ARM)
60+
sudo eos create code --skip-session-backups # Skip session backup setup
61+
sudo eos create code --backup-interval 30min # Backup every 30 minutes`,
5162
RunE: eos_cli.Wrap(runCreateCode),
5263
}
5364

@@ -69,9 +80,14 @@ func init() {
6980
CreateCodeCmd.Flags().Bool("dry-run", false, "Show what would be done without making changes")
7081

7182
// AI tools flags
72-
CreateCodeCmd.Flags().Bool("skip-ai-tools", false, "Skip installation of AI coding tools (Claude Code, Codex)")
83+
CreateCodeCmd.Flags().Bool("skip-ai-tools", false, "Skip installation of AI coding tools (Claude Code, Codex, Windsurf)")
7384
CreateCodeCmd.Flags().Bool("skip-claude", false, "Skip Claude Code installation")
7485
CreateCodeCmd.Flags().Bool("skip-codex", false, "Skip OpenAI Codex CLI installation")
86+
CreateCodeCmd.Flags().Bool("skip-windsurf", false, "Skip Windsurf IDE installation (x86_64 only)")
87+
88+
// Session backup flags
89+
CreateCodeCmd.Flags().Bool("skip-session-backups", false, "Skip setting up automatic session backups")
90+
CreateCodeCmd.Flags().String("backup-interval", "hourly", "Session backup frequency: 30min, hourly, 6hours, daily")
7591

7692
// Network flags
7793
CreateCodeCmd.Flags().StringSlice("allowed-networks", []string{},
@@ -142,6 +158,19 @@ func runCreateCode(rc *eos_io.RuntimeContext, cmd *cobra.Command, args []string)
142158
config.SkipCodex = skipCodex
143159
}
144160

161+
if skipWindsurf, err := cmd.Flags().GetBool("skip-windsurf"); err == nil {
162+
config.SkipWindsurf = skipWindsurf
163+
}
164+
165+
// Session backup flags
166+
if skipSessionBackups, err := cmd.Flags().GetBool("skip-session-backups"); err == nil {
167+
config.SkipSessionBackups = skipSessionBackups
168+
}
169+
170+
if backupInterval, err := cmd.Flags().GetString("backup-interval"); err == nil {
171+
config.SessionBackupInterval = parseBackupInterval(backupInterval)
172+
}
173+
145174
// Windsurf-specific flags
146175
if skipConnCheck, err := cmd.Flags().GetBool("skip-connectivity-check"); err == nil {
147176
config.SkipConnectivityCheck = skipConnCheck
@@ -197,3 +226,24 @@ func runCreateCode(rc *eos_io.RuntimeContext, cmd *cobra.Command, args []string)
197226
logger.Info("Remote IDE development setup completed")
198227
return nil
199228
}
229+
230+
// parseBackupInterval converts user-friendly interval names to cron expressions
231+
func parseBackupInterval(interval string) string {
232+
switch interval {
233+
case "30min", "30m", "30minutes":
234+
return "*/30 * * * *"
235+
case "hourly", "1h", "hour":
236+
return "0 * * * *"
237+
case "6hours", "6h":
238+
return "0 */6 * * *"
239+
case "daily", "1d", "day":
240+
return "0 0 * * *"
241+
default:
242+
// If it looks like a cron expression, use it directly
243+
if strings.Contains(interval, "*") || strings.Contains(interval, "/") {
244+
return interval
245+
}
246+
// Default to hourly
247+
return "0 * * * *"
248+
}
249+
}

pkg/remotecode/install.go

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,28 @@ func Install(rc *eos_io.RuntimeContext, config *Config) (*InstallResult, error)
9191
}
9292
}
9393

94+
// INTERVENE - Set up session backups
95+
if config.SetupSessionBackups && !config.SkipSessionBackups {
96+
logger.Info("Setting up coding session backups")
97+
backupConfig := &SessionBackupConfig{
98+
User: config.User,
99+
CronInterval: config.SessionBackupInterval,
100+
DryRun: config.DryRun,
101+
}
102+
backupResult, err := SetupSessionBackups(rc, backupConfig)
103+
if err != nil {
104+
result.Warnings = append(result.Warnings,
105+
fmt.Sprintf("Session backup setup had issues: %v", err))
106+
logger.Warn("Session backup setup had issues", zap.Error(err))
107+
} else {
108+
result.SessionBackupsConfigured = true
109+
result.SessionBackupResult = backupResult
110+
logger.Info("Session backups configured",
111+
zap.Bool("cron_configured", backupResult.CronConfigured),
112+
zap.Int("scripts_installed", len(backupResult.ScriptsInstalled)))
113+
}
114+
}
115+
94116
// INTERVENE - Cleanup old IDE servers if requested
95117
if config.CleanupIDEServers {
96118
logger.Info("Cleaning up old IDE server versions")
@@ -367,6 +389,26 @@ func GenerateAccessInstructions(rc *eos_io.RuntimeContext, config *Config, resul
367389
sb.WriteString("\n")
368390
}
369391

392+
// Session backups configured
393+
if result.SessionBackupsConfigured && result.SessionBackupResult != nil {
394+
sb.WriteString("Session Backups:\n")
395+
sb.WriteString(strings.Repeat("-", 30) + "\n")
396+
if result.SessionBackupResult.CronConfigured {
397+
sb.WriteString(fmt.Sprintf(" Schedule: %s\n", result.SessionBackupResult.CronInterval))
398+
}
399+
sb.WriteString(fmt.Sprintf(" Backup dir: %s\n", result.SessionBackupResult.BackupDir))
400+
if result.SessionBackupResult.ClaudeDataFound {
401+
sb.WriteString(" Claude Code data: found\n")
402+
}
403+
if result.SessionBackupResult.CodexDataFound {
404+
sb.WriteString(" Codex data: found\n")
405+
}
406+
sb.WriteString("\n")
407+
sb.WriteString(" Manage backups: ~/bin/setup-coding-session-backups.sh\n")
408+
sb.WriteString(" Export to Markdown: ~/bin/export-coding-sessions.sh --today\n")
409+
sb.WriteString("\n")
410+
}
411+
370412
// Supported IDEs
371413
sb.WriteString("Supported IDEs:\n")
372414
sb.WriteString(strings.Repeat("-", 30) + "\n")
@@ -423,12 +465,13 @@ func formatBytes(bytes int64) string {
423465
}
424466
}
425467

426-
// InstallAITools installs AI coding assistants (Claude Code, OpenAI Codex CLI)
468+
// InstallAITools installs AI coding assistants (Claude Code, OpenAI Codex CLI, Windsurf)
427469
func InstallAITools(rc *eos_io.RuntimeContext, config *Config, result *InstallResult) error {
428470
logger := otelzap.Ctx(rc.Ctx)
429471
logger.Info("Installing AI coding tools",
430472
zap.Bool("skip_claude", config.SkipClaudeCode),
431473
zap.Bool("skip_codex", config.SkipCodex),
474+
zap.Bool("skip_windsurf", config.SkipWindsurf),
432475
zap.Bool("dry_run", config.DryRun))
433476

434477
result.AIToolsInstalled = []string{}
@@ -450,6 +493,14 @@ func InstallAITools(rc *eos_io.RuntimeContext, config *Config, result *InstallRe
450493
}
451494
}
452495

496+
// Install Windsurf (x86_64 only)
497+
if !config.SkipWindsurf {
498+
if err := installWindsurf(rc, config, result); err != nil {
499+
logger.Warn("Windsurf installation skipped or failed", zap.Error(err))
500+
// Don't set lastErr - Windsurf skip is informational on non-x86
501+
}
502+
}
503+
453504
return lastErr
454505
}
455506

@@ -552,6 +603,113 @@ func installCodexCLI(rc *eos_io.RuntimeContext, config *Config, result *InstallR
552603
return nil
553604
}
554605

606+
// installWindsurf installs Windsurf IDE (x86_64 only)
607+
// NOTE: Windsurf is a desktop IDE that runs on the CLIENT machine, not the server.
608+
// This function installs it on the server for local development or if the server
609+
// has a desktop environment. For remote SSH use, Windsurf should be installed on
610+
// the local machine and will connect via SSH.
611+
func installWindsurf(rc *eos_io.RuntimeContext, config *Config, result *InstallResult) error {
612+
logger := otelzap.Ctx(rc.Ctx)
613+
logger.Info("Checking Windsurf installation requirements")
614+
615+
// Check architecture - Windsurf only supports x86_64
616+
arch := runtime.GOARCH
617+
if arch != "amd64" {
618+
logger.Info("Windsurf not available on this architecture",
619+
zap.String("arch", arch),
620+
zap.String("required", "amd64"))
621+
result.AIToolsInstalled = append(result.AIToolsInstalled,
622+
fmt.Sprintf("Windsurf (skipped - requires x86_64, got %s)", arch))
623+
return fmt.Errorf("windsurf requires x86_64 architecture, this system is %s", arch)
624+
}
625+
626+
// Check if windsurf is already installed
627+
if _, err := exec.LookPath("windsurf"); err == nil {
628+
logger.Info("Windsurf already installed")
629+
result.AIToolsInstalled = append(result.AIToolsInstalled, "Windsurf (already installed)")
630+
result.WindsurfInstalled = true
631+
return nil
632+
}
633+
634+
// Check if the .deb package is available in standard locations
635+
windsurfDebPath := "/tmp/windsurf.deb"
636+
637+
if config.DryRun {
638+
logger.Info("DRY RUN: Would install Windsurf IDE",
639+
zap.String("arch", arch))
640+
result.AIToolsInstalled = append(result.AIToolsInstalled, "Windsurf (would install)")
641+
return nil
642+
}
643+
644+
// Try to download and install Windsurf
645+
// Windsurf provides a .deb package for Ubuntu/Debian
646+
windsurfDownloadURL := "https://windsurf-stable.codeiumdata.com/linux-x64/stable/latest/Windsurf.deb"
647+
648+
logger.Info("Downloading Windsurf", zap.String("url", windsurfDownloadURL))
649+
650+
// Download the .deb file
651+
resp, err := http.Get(windsurfDownloadURL)
652+
if err != nil {
653+
result.Warnings = append(result.Warnings,
654+
fmt.Sprintf("Failed to download Windsurf: %v. Install manually from https://codeium.com/windsurf", err))
655+
return fmt.Errorf("failed to download windsurf: %w", err)
656+
}
657+
defer resp.Body.Close()
658+
659+
if resp.StatusCode != http.StatusOK {
660+
result.Warnings = append(result.Warnings,
661+
fmt.Sprintf("Failed to download Windsurf: HTTP %d. Install manually from https://codeium.com/windsurf", resp.StatusCode))
662+
return fmt.Errorf("failed to download windsurf: HTTP %d", resp.StatusCode)
663+
}
664+
665+
// Save to temp file
666+
debFile, err := os.Create(windsurfDebPath)
667+
if err != nil {
668+
return fmt.Errorf("failed to create temp file for windsurf: %w", err)
669+
}
670+
defer os.Remove(windsurfDebPath)
671+
672+
if _, err := io.Copy(debFile, resp.Body); err != nil {
673+
debFile.Close()
674+
return fmt.Errorf("failed to save windsurf package: %w", err)
675+
}
676+
debFile.Close()
677+
678+
logger.Info("Installing Windsurf package")
679+
680+
// Install using dpkg
681+
installCmd := exec.Command("dpkg", "-i", windsurfDebPath)
682+
output, err := installCmd.CombinedOutput()
683+
if err != nil {
684+
// Try to fix dependencies
685+
logger.Warn("dpkg install had issues, attempting to fix dependencies",
686+
zap.String("output", string(output)))
687+
688+
fixCmd := exec.Command("apt-get", "install", "-f", "-y")
689+
fixOutput, fixErr := fixCmd.CombinedOutput()
690+
if fixErr != nil {
691+
logger.Error("Failed to fix Windsurf dependencies",
692+
zap.Error(fixErr),
693+
zap.String("output", string(fixOutput)))
694+
result.Warnings = append(result.Warnings,
695+
fmt.Sprintf("Windsurf installation had dependency issues. Run: sudo apt-get install -f"))
696+
return fmt.Errorf("windsurf dependency fix failed: %w", fixErr)
697+
}
698+
}
699+
700+
// Verify installation
701+
if _, err := exec.LookPath("windsurf"); err != nil {
702+
result.Warnings = append(result.Warnings,
703+
"Windsurf package installed but 'windsurf' command not found in PATH")
704+
return fmt.Errorf("windsurf installed but not in PATH")
705+
}
706+
707+
logger.Info("Windsurf installed successfully")
708+
result.AIToolsInstalled = append(result.AIToolsInstalled, "Windsurf")
709+
result.WindsurfInstalled = true
710+
return nil
711+
}
712+
555713
func downloadInstallerWithChecksum(url, expectedChecksum string) (string, error) {
556714
resp, err := http.Get(url)
557715
if err != nil {

0 commit comments

Comments
 (0)