From 778bef5ee6941056e06326d1eaaa6956d7307a85 Mon Sep 17 00:00:00 2001 From: Clawd Date: Sat, 18 Apr 2026 14:40:17 -0700 Subject: Remove Go implementation — ship is skills-only now MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The skills/ directory fully replaces the old Go CLI. Drop all Go source, build files, planning docs, and the stale SECURITY.md (which described the old git-user push-deploy model that no longer exists). Trim .gitignore to match the new tree. --- cmd/ship/host.go | 445 ------------------------------------------------------- 1 file changed, 445 deletions(-) delete mode 100644 cmd/ship/host.go (limited to 'cmd/ship/host.go') diff --git a/cmd/ship/host.go b/cmd/ship/host.go deleted file mode 100644 index b19c376..0000000 --- a/cmd/ship/host.go +++ /dev/null @@ -1,445 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/bdw/ship/internal/output" - "github.com/bdw/ship/internal/ssh" - "github.com/bdw/ship/internal/state" - "github.com/bdw/ship/internal/templates" - "github.com/spf13/cobra" -) - -func initHostV2() { - hostInitV2Cmd.Flags().String("domain", "", "Base domain for deployments (required)") - hostInitV2Cmd.MarkFlagRequired("domain") - - hostV2Cmd.AddCommand(hostInitV2Cmd) - hostV2Cmd.AddCommand(hostStatusV2Cmd) -} - -var hostInitV2Cmd = &cobra.Command{ - Use: "init USER@HOST --domain DOMAIN", - Short: "Initialize a VPS for deployments", - Long: `Set up a fresh VPS with Caddy, Docker, and required directories. - -Example: - ship host init user@my-vps --domain example.com`, - Args: cobra.ExactArgs(1), - RunE: runHostInitV2, -} - -func runHostInitV2(cmd *cobra.Command, args []string) error { - host := args[0] - domain, _ := cmd.Flags().GetString("domain") - - if domain == "" { - output.PrintAndExit(output.Err(output.ErrInvalidArgs, "--domain is required")) - } - - // Ensure SSH key exists - keyPath, pubkey, err := ensureSSHKey() - if err != nil { - output.PrintAndExit(output.Err(output.ErrSSHAuthFailed, "failed to setup SSH key: "+err.Error())) - } - - // Try to connect first (to verify key is authorized) - client, err := ssh.Connect(host) - if err != nil { - // Connection failed - provide helpful error with pubkey - resp := map[string]interface{}{ - "status": "error", - "code": "SSH_AUTH_FAILED", - "message": "SSH connection failed. Add this public key to your VPS authorized_keys:", - "public_key": pubkey, - "key_path": keyPath, - "host": host, - "setup_command": fmt.Sprintf("ssh-copy-id -i %s %s", keyPath, host), - } - printJSON(resp) - os.Exit(output.ExitSSHFailed) - } - defer client.Close() - - // Detect OS - osRelease, err := client.Run("cat /etc/os-release") - if err != nil { - output.PrintAndExit(output.Err(output.ErrSSHConnectFailed, "failed to detect OS: "+err.Error())) - } - - if !strings.Contains(osRelease, "Ubuntu") && !strings.Contains(osRelease, "Debian") { - output.PrintAndExit(output.Err(output.ErrInvalidArgs, "unsupported OS (only Ubuntu and Debian are supported)")) - } - - var installed []string - - // Install Caddy if needed - if _, err := client.Run("which caddy"); err != nil { - if err := installCaddyV2(client); err != nil { - output.PrintAndExit(output.Err(output.ErrServiceFailed, "failed to install Caddy: "+err.Error())) - } - installed = append(installed, "caddy") - } - - // Configure Caddy - caddyfile := `{ -} - -import /etc/caddy/sites-enabled/* -` - if err := client.WriteSudoFile("/etc/caddy/Caddyfile", caddyfile); err != nil { - output.PrintAndExit(output.Err(output.ErrCaddyFailed, "failed to write Caddyfile: "+err.Error())) - } - - // Create directories - dirs := []string{ - "/etc/ship/env", - "/etc/ship/ports", - "/etc/ship/ttl", - "/etc/caddy/sites-enabled", - "/var/www", - } - for _, dir := range dirs { - if _, err := client.RunSudo(fmt.Sprintf("mkdir -p %s", dir)); err != nil { - output.PrintAndExit(output.Err(output.ErrServiceFailed, "failed to create directory: "+err.Error())) - } - } - - // Install Docker - if _, err := client.Run("which docker"); err != nil { - if err := installDockerV2(client); err != nil { - output.PrintAndExit(output.Err(output.ErrServiceFailed, "failed to install Docker: "+err.Error())) - } - installed = append(installed, "docker") - } - - // Install cleanup timer for TTL - if err := installCleanupTimer(client); err != nil { - // Non-fatal - } - - // Enable and start services - if _, err := client.RunSudo("systemctl enable --now caddy docker"); err != nil { - output.PrintAndExit(output.Err(output.ErrServiceFailed, "failed to enable services: "+err.Error())) - } - - // Save state - st, err := state.Load() - if err != nil { - output.PrintAndExit(output.Err(output.ErrServiceFailed, "failed to load state: "+err.Error())) - } - - hostState := st.GetHost(host) - hostState.BaseDomain = domain - - if st.GetDefaultHost() == "" { - st.SetDefaultHost(host) - } - - if err := st.Save(); err != nil { - output.PrintAndExit(output.Err(output.ErrServiceFailed, "failed to save state: "+err.Error())) - } - - // Success - output.PrintAndExit(&output.HostInitResponse{ - Status: "ok", - Host: host, - Domain: domain, - Installed: installed, - }) - - return nil -} - -func installCaddyV2(client *ssh.Client) error { - commands := []string{ - "apt-get update", - "apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl gnupg", - "curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' -o /tmp/caddy.gpg", - "gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg < /tmp/caddy.gpg", - "curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' -o /etc/apt/sources.list.d/caddy-stable.list", - "apt-get update", - "apt-get install -y caddy", - } - - for _, cmd := range commands { - if _, err := client.RunSudo(cmd); err != nil { - return fmt.Errorf("command failed: %s: %w", cmd, err) - } - } - return nil -} - -func installDockerV2(client *ssh.Client) error { - commands := []string{ - "apt-get install -y ca-certificates curl gnupg", - "install -m 0755 -d /etc/apt/keyrings", - "curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc", - "chmod a+r /etc/apt/keyrings/docker.asc", - `sh -c 'echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo ${VERSION_CODENAME}) stable" > /etc/apt/sources.list.d/docker.list'`, - "apt-get update", - "apt-get install -y docker-ce docker-ce-cli containerd.io", - } - - for _, cmd := range commands { - if _, err := client.RunSudo(cmd); err != nil { - return fmt.Errorf("command failed: %s: %w", cmd, err) - } - } - return nil -} - -func installCleanupTimer(client *ssh.Client) error { - // Cleanup script - script := `#!/bin/bash -now=$(date +%s) -for f in /etc/ship/ttl/*; do - [ -f "$f" ] || continue - name=$(basename "$f") - expires=$(cat "$f") - if [ "$now" -gt "$expires" ]; then - systemctl stop "$name" 2>/dev/null || true - systemctl disable "$name" 2>/dev/null || true - rm -f "/etc/systemd/system/${name}.service" - rm -f "/etc/caddy/sites-enabled/${name}.caddy" - rm -rf "/var/www/${name}" - rm -rf "/var/lib/${name}" - rm -f "/usr/local/bin/${name}" - rm -f "/etc/ship/env/${name}.env" - rm -f "/etc/ship/ports/${name}" - rm -f "/etc/ship/ttl/${name}" - docker rm -f "$name" 2>/dev/null || true - docker rmi "$name" 2>/dev/null || true - fi -done -systemctl daemon-reload -systemctl reload caddy -` - if err := client.WriteSudoFile("/usr/local/bin/ship-cleanup", script); err != nil { - return err - } - if _, err := client.RunSudo("chmod +x /usr/local/bin/ship-cleanup"); err != nil { - return err - } - - // Timer unit - timer := `[Unit] -Description=Ship TTL cleanup timer - -[Timer] -OnCalendar=hourly -Persistent=true - -[Install] -WantedBy=timers.target -` - if err := client.WriteSudoFile("/etc/systemd/system/ship-cleanup.timer", timer); err != nil { - return err - } - - // Service unit - service := `[Unit] -Description=Ship TTL cleanup - -[Service] -Type=oneshot -ExecStart=/usr/local/bin/ship-cleanup -` - if err := client.WriteSudoFile("/etc/systemd/system/ship-cleanup.service", service); err != nil { - return err - } - - // Enable timer - if _, err := client.RunSudo("systemctl daemon-reload"); err != nil { - return err - } - if _, err := client.RunSudo("systemctl enable --now ship-cleanup.timer"); err != nil { - return err - } - - return nil -} - -var hostStatusV2Cmd = &cobra.Command{ - Use: "status", - Short: "Check host status", - RunE: func(cmd *cobra.Command, args []string) error { - st, err := state.Load() - if err != nil { - output.PrintAndExit(output.Err(output.ErrHostNotConfigured, err.Error())) - } - - hostName := hostFlag - if hostName == "" { - hostName = st.DefaultHost - } - if hostName == "" { - output.PrintAndExit(output.Err(output.ErrHostNotConfigured, "no host specified")) - } - - hostConfig := st.GetHost(hostName) - - client, err := ssh.Connect(hostName) - if err != nil { - output.PrintAndExit(output.Err(output.ErrSSHConnectFailed, err.Error())) - } - defer client.Close() - - // Check services - caddyStatus, _ := client.RunSudo("systemctl is-active caddy") - dockerStatus, _ := client.RunSudo("systemctl is-active docker") - - // Print as JSON directly (custom response type) - fmt.Printf(`{"status":"ok","host":%q,"domain":%q,"caddy":%t,"docker":%t}`+"\n", - hostName, - hostConfig.BaseDomain, - strings.TrimSpace(caddyStatus) == "active", - strings.TrimSpace(dockerStatus) == "active", - ) - return nil - }, -} - -// Preserve git setup functionality from v1 for advanced users -func setupGitDeployV2(client *ssh.Client, baseDomain string, hostState *state.Host) *output.ErrorResponse { - // Install git, fcgiwrap, cgit - if _, err := client.RunSudo("apt-get install -y git fcgiwrap cgit"); err != nil { - return output.Err(output.ErrServiceFailed, "failed to install git tools: "+err.Error()) - } - - // Create git user - client.RunSudo("useradd -r -m -d /home/git -s $(which git-shell) git") - client.RunSudo("usermod -aG docker git") - client.RunSudo("usermod -aG git www-data") - client.RunSudo("usermod -aG www-data caddy") - - // Copy SSH keys - copyKeysCommands := []string{ - "mkdir -p /home/git/.ssh", - "cp ~/.ssh/authorized_keys /home/git/.ssh/authorized_keys", - "chown -R git:git /home/git/.ssh", - "chmod 700 /home/git/.ssh", - "chmod 600 /home/git/.ssh/authorized_keys", - } - for _, cmd := range copyKeysCommands { - if _, err := client.RunSudo(cmd); err != nil { - return output.Err(output.ErrServiceFailed, "failed to setup git SSH: "+err.Error()) - } - } - - // Create /srv/git - client.RunSudo("mkdir -p /srv/git") - client.RunSudo("chown git:git /srv/git") - - // Sudoers - sudoersContent := `git ALL=(ALL) NOPASSWD: \ - /bin/systemctl daemon-reload, \ - /bin/systemctl reload caddy, \ - /bin/systemctl restart [a-z]*, \ - /bin/systemctl enable [a-z]*, \ - /bin/cp /var/lib/*/src/.ship/service /etc/systemd/system/*.service, \ - /bin/cp /var/lib/*/src/.ship/Caddyfile /etc/caddy/sites-enabled/*.caddy, \ - /bin/cp /var/www/*/.ship/Caddyfile /etc/caddy/sites-enabled/*.caddy, \ - /bin/mkdir -p /var/lib/*, \ - /bin/mkdir -p /var/www/*, \ - /bin/chown -R git\:git /var/lib/*, \ - /bin/chown git\:git /var/www/* -` - if err := client.WriteSudoFile("/etc/sudoers.d/ship-git", sudoersContent); err != nil { - return output.Err(output.ErrServiceFailed, "failed to write sudoers: "+err.Error()) - } - client.RunSudo("chmod 440 /etc/sudoers.d/ship-git") - - // Vanity import template - vanityHTML := ` - -{{$path := trimPrefix "/" (placeholder "http.request.orig_uri.path")}} -{{$parts := splitList "/" $path}} -{{$module := first $parts}} - - -go get {{.Host}}/{{$module}} - -` - client.RunSudo("mkdir -p /opt/ship/vanity") - client.WriteSudoFile("/opt/ship/vanity/index.html", vanityHTML) - - // cgit config - codeCaddyContent, _ := templates.CodeCaddy(map[string]string{"BaseDomain": baseDomain}) - client.WriteSudoFile("/etc/caddy/sites-enabled/ship-code.caddy", codeCaddyContent) - - cgitrcContent, _ := templates.CgitRC(map[string]string{"BaseDomain": baseDomain}) - client.WriteSudoFile("/etc/cgitrc", cgitrcContent) - client.WriteSudoFile("/opt/ship/cgit-header.html", templates.CgitHeader()) - - // Start services - client.RunSudo("systemctl enable --now fcgiwrap") - client.RunSudo("systemctl restart caddy") - - hostState.GitSetup = true - return nil -} - -// ensureSSHKey checks for an existing SSH key or generates a new one. -// Returns the key path, public key contents, and any error. -func ensureSSHKey() (keyPath string, pubkey string, err error) { - home, err := os.UserHomeDir() - if err != nil { - return "", "", err - } - - // Check common key locations - keyPaths := []string{ - filepath.Join(home, ".ssh", "id_ed25519"), - filepath.Join(home, ".ssh", "id_rsa"), - filepath.Join(home, ".ssh", "id_ecdsa"), - } - - for _, kp := range keyPaths { - pubPath := kp + ".pub" - if _, err := os.Stat(kp); err == nil { - if _, err := os.Stat(pubPath); err == nil { - // Key exists, read public key - pub, err := os.ReadFile(pubPath) - if err != nil { - continue - } - return kp, strings.TrimSpace(string(pub)), nil - } - } - } - - // No key found, generate one - keyPath = filepath.Join(home, ".ssh", "id_ed25519") - sshDir := filepath.Dir(keyPath) - - // Ensure .ssh directory exists - if err := os.MkdirAll(sshDir, 0700); err != nil { - return "", "", fmt.Errorf("failed to create .ssh directory: %w", err) - } - - // Generate key - cmd := exec.Command("ssh-keygen", "-t", "ed25519", "-f", keyPath, "-N", "", "-C", "ship") - if err := cmd.Run(); err != nil { - return "", "", fmt.Errorf("failed to generate SSH key: %w", err) - } - - // Read public key - pub, err := os.ReadFile(keyPath + ".pub") - if err != nil { - return "", "", fmt.Errorf("failed to read public key: %w", err) - } - - return keyPath, strings.TrimSpace(string(pub)), nil -} - -// printJSON outputs a value as JSON to stdout -func printJSON(v interface{}) { - enc := json.NewEncoder(os.Stdout) - enc.Encode(v) -} -- cgit v1.2.3