Skip to content

Commit 0c30607

Browse files
committed
bugfix:重新提交部分文件
1 parent a91cf6e commit 0c30607

File tree

8 files changed

+919
-1
lines changed

8 files changed

+919
-1
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,3 @@
55
*.sock
66
.claude
77
CLAUDE.md
8-
fssh

cmd/fssh/setup.go

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"runtime"
8+
9+
"fssh/internal/config"
10+
"fssh/internal/keychain"
11+
"fssh/internal/otp"
12+
)
13+
14+
// runInteractiveSetup orchestrates the complete interactive setup wizard
15+
func runInteractiveSetup(force bool, seedTTL int, algorithm string, digits int) {
16+
printWelcome()
17+
18+
// Step 1: Check if already initialized
19+
if err := checkInitialization(force); err != nil {
20+
fatal(err)
21+
}
22+
23+
// Step 2: Choose authentication mode
24+
authMode, err := promptAuthMode()
25+
if err != nil {
26+
fatal(err)
27+
}
28+
29+
// Step 3: Execute authentication initialization
30+
fmt.Println()
31+
printStepHeader(3, 8, "Initialize Authentication")
32+
if authMode == "touchid" {
33+
initTouchIDMode(force)
34+
} else {
35+
initOTPMode(force, seedTTL, algorithm, digits)
36+
}
37+
38+
// Step 4: Binary installation
39+
fmt.Println()
40+
printStepHeader(4, 8, "Binary Installation")
41+
if err := ensureBinaryInstalled(); err != nil {
42+
fmt.Printf("⚠️ Warning: Binary installation failed: %v\n", err)
43+
fmt.Println("You can install manually with:")
44+
fmt.Println(" sudo cp fssh /usr/local/bin/")
45+
fmt.Println()
46+
}
47+
48+
// Step 5: Import SSH keys
49+
fmt.Println()
50+
printStepHeader(5, 8, "Import SSH Keys")
51+
if err := importSSHKeys(); err != nil {
52+
fmt.Printf("⚠️ Warning: SSH key import failed: %v\n", err)
53+
fmt.Println("You can import keys later with: fssh import")
54+
fmt.Println()
55+
}
56+
57+
// Step 6: Configure LaunchAgent
58+
fmt.Println()
59+
printStepHeader(6, 8, "Configure LaunchAgent (Auto-start)")
60+
if err := setupLaunchAgent(); err != nil {
61+
fmt.Printf("⚠️ Warning: LaunchAgent setup failed: %v\n", err)
62+
fmt.Println("You can configure manually later")
63+
fmt.Println()
64+
}
65+
66+
// Step 7: Start Agent
67+
fmt.Println()
68+
printStepHeader(7, 8, "Start SSH Agent")
69+
if err := startAgent(); err != nil {
70+
fmt.Printf("⚠️ Warning: Agent startup failed: %v\n", err)
71+
fmt.Println("You can start manually with: fssh agent")
72+
fmt.Println()
73+
}
74+
75+
// Step 8: Configure SSH config
76+
fmt.Println()
77+
printStepHeader(8, 8, "Configure SSH Client")
78+
if err := addToSSHConfig(); err != nil {
79+
fmt.Printf("⚠️ Warning: SSH config update failed: %v\n", err)
80+
fmt.Println()
81+
}
82+
83+
// Print completion message
84+
printSetupComplete()
85+
}
86+
87+
// printWelcome displays the welcome banner
88+
func printWelcome() {
89+
fmt.Println()
90+
fmt.Println("╔══════════════════════════════════════════════════════════╗")
91+
fmt.Println("║ Welcome to fssh Interactive Setup Wizard ║")
92+
fmt.Println("╚══════════════════════════════════════════════════════════╝")
93+
fmt.Println()
94+
fmt.Println("This wizard will help you:")
95+
fmt.Println(" 1. Initialize authentication (Touch ID or OTP)")
96+
fmt.Println(" 2. Install fssh binary to /usr/local/bin/")
97+
fmt.Println(" 3. Import your SSH keys from ~/.ssh/")
98+
fmt.Println(" 4. Configure LaunchAgent for auto-start")
99+
fmt.Println(" 5. Start the SSH agent")
100+
fmt.Println(" 6. Configure SSH client")
101+
fmt.Println()
102+
}
103+
104+
// checkInitialization checks if fssh is already initialized
105+
func checkInitialization(force bool) error {
106+
exists, err := keychain.MasterKeyExists()
107+
if err != nil {
108+
return fmt.Errorf("failed to check initialization status: %w", err)
109+
}
110+
111+
if exists && !force {
112+
fmt.Println("⚠️ fssh is already initialized!")
113+
fmt.Println()
114+
if !otp.PromptConfirm("Do you want to reinitialize? This will require re-importing all keys") {
115+
fmt.Println("Setup cancelled.")
116+
os.Exit(0)
117+
}
118+
}
119+
120+
return nil
121+
}
122+
123+
// promptAuthMode prompts the user to select authentication mode
124+
func promptAuthMode() (string, error) {
125+
printStepHeader(2, 8, "Choose Authentication Mode")
126+
127+
// Check Touch ID availability (macOS only)
128+
touchIDAvailable := runtime.GOOS == "darwin"
129+
130+
if touchIDAvailable {
131+
fmt.Println("✓ Your Mac supports Touch ID!")
132+
fmt.Println()
133+
}
134+
135+
fmt.Println("Available modes:")
136+
fmt.Println(" 1) Touch ID (recommended) - Use your fingerprint")
137+
fmt.Println(" 2) OTP - Use password + authenticator app")
138+
fmt.Println()
139+
140+
for {
141+
choice, err := otp.PromptInput("Choose authentication mode [1]: ")
142+
if err != nil {
143+
return "", err
144+
}
145+
146+
// Default to Touch ID
147+
if choice == "" {
148+
choice = "1"
149+
}
150+
151+
switch choice {
152+
case "1", "touchid", "TouchID":
153+
if !touchIDAvailable {
154+
fmt.Println("❌ Touch ID is only available on macOS")
155+
continue
156+
}
157+
return "touchid", nil
158+
case "2", "otp", "OTP":
159+
return "otp", nil
160+
default:
161+
fmt.Println("Invalid choice. Please enter 1 or 2.")
162+
}
163+
}
164+
}
165+
166+
// printStepHeader prints a step header
167+
func printStepHeader(step, total int, title string) {
168+
fmt.Printf("Step %d/%d: %s\n", step, total, title)
169+
fmt.Println("──────────────────────────────────────")
170+
fmt.Println()
171+
}
172+
173+
// printSetupComplete prints the completion message
174+
func printSetupComplete() {
175+
// Get socket path
176+
home, _ := os.UserHomeDir()
177+
socketPath := filepath.Join(home, ".fssh", "agent.sock")
178+
179+
// Try to load config to get actual socket path
180+
if cfg, err := config.Load(); err == nil && cfg.Socket != "" {
181+
socketPath = cfg.Socket
182+
}
183+
184+
fmt.Println()
185+
fmt.Println("╔══════════════════════════════════════════════════════════╗")
186+
fmt.Println("║ Setup Complete! 🎉 ║")
187+
fmt.Println("╚══════════════════════════════════════════════════════════╝")
188+
fmt.Println()
189+
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
190+
fmt.Println("⚠️ IMPORTANT: Set environment variable in your shell")
191+
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
192+
fmt.Println()
193+
fmt.Println("Add this line to your shell configuration file:")
194+
fmt.Println()
195+
fmt.Printf(" export SSH_AUTH_SOCK=%s\n", socketPath)
196+
fmt.Println()
197+
fmt.Println("Shell configuration files:")
198+
fmt.Println(" • bash: ~/.bashrc or ~/.bash_profile")
199+
fmt.Println(" • zsh: ~/.zshrc")
200+
fmt.Println(" • fish: ~/.config/fish/config.fish")
201+
fmt.Println()
202+
fmt.Println("Then reload your shell:")
203+
fmt.Println(" source ~/.zshrc # or your shell config file")
204+
fmt.Println()
205+
fmt.Println("Or for current session only:")
206+
fmt.Printf(" export SSH_AUTH_SOCK=%s\n", socketPath)
207+
fmt.Println()
208+
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
209+
fmt.Println()
210+
fmt.Println("Next Steps:")
211+
fmt.Println()
212+
fmt.Println("1. Set the environment variable (see above)")
213+
fmt.Println()
214+
fmt.Println("2. Test your setup:")
215+
fmt.Println(" ssh your-server # Should prompt for Touch ID/OTP")
216+
fmt.Println()
217+
fmt.Println("3. Manage your keys:")
218+
fmt.Println(" fssh list # List imported keys")
219+
fmt.Println(" fssh import # Import more keys")
220+
fmt.Println(" fssh shell # Interactive SSH management")
221+
fmt.Println()
222+
fmt.Println("4. Agent will auto-start on login via LaunchAgent")
223+
fmt.Println()
224+
}

cmd/fssh/setup_agent.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"net"
6+
"os"
7+
"path/filepath"
8+
"time"
9+
)
10+
11+
// startAgent starts the fssh agent and verifies it's running
12+
func startAgent() error {
13+
home, err := os.UserHomeDir()
14+
if err != nil {
15+
return fmt.Errorf("failed to get home directory: %w", err)
16+
}
17+
18+
socketPath := filepath.Join(home, ".fssh", "agent.sock")
19+
20+
// Check if agent is already running
21+
if _, err := os.Stat(socketPath); err == nil {
22+
// Try to connect
23+
if conn, err := net.Dial("unix", socketPath); err == nil {
24+
conn.Close()
25+
fmt.Println("✓ fssh agent is already running")
26+
return nil
27+
}
28+
29+
// Socket exists but not responding, remove it
30+
_ = os.Remove(socketPath)
31+
}
32+
33+
// Agent will be started by LaunchAgent automatically
34+
// We just need to wait for it to start
35+
fmt.Println("Waiting for agent to start...")
36+
37+
// Wait up to 10 seconds for socket to appear and be ready
38+
maxAttempts := 20
39+
for i := 0; i < maxAttempts; i++ {
40+
time.Sleep(500 * time.Millisecond)
41+
42+
if _, err := os.Stat(socketPath); err == nil {
43+
// Socket exists, try to connect
44+
if conn, err := net.Dial("unix", socketPath); err == nil {
45+
conn.Close()
46+
fmt.Println("✓ fssh agent started successfully")
47+
return nil
48+
}
49+
}
50+
51+
// Show progress
52+
if (i+1)%4 == 0 {
53+
fmt.Print(".")
54+
}
55+
}
56+
57+
fmt.Println()
58+
return fmt.Errorf("agent did not start within 10 seconds")
59+
}

cmd/fssh/setup_binary.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"path/filepath"
8+
9+
"fssh/internal/otp"
10+
)
11+
12+
// ensureBinaryInstalled installs fssh to /usr/local/bin if not already installed
13+
func ensureBinaryInstalled() error {
14+
targetPath := "/usr/local/bin/fssh"
15+
16+
// Get current executable path
17+
currentPath, err := os.Executable()
18+
if err != nil {
19+
return fmt.Errorf("failed to get executable path: %w", err)
20+
}
21+
22+
// Resolve symlinks
23+
currentPath, err = filepath.EvalSymlinks(currentPath)
24+
if err != nil {
25+
return fmt.Errorf("failed to resolve symlinks: %w", err)
26+
}
27+
28+
// Check if already installed at target location
29+
if _, err := os.Stat(targetPath); err == nil {
30+
// Target exists, check if it's the same file
31+
existingPath, err := filepath.EvalSymlinks(targetPath)
32+
if err == nil && existingPath == currentPath {
33+
fmt.Printf("✓ fssh is already installed at %s\n", targetPath)
34+
return nil
35+
}
36+
37+
// Different file, ask if user wants to replace
38+
fmt.Printf("⚠️ fssh already exists at %s\n", targetPath)
39+
if !otp.PromptConfirm("Do you want to replace it?") {
40+
fmt.Println("Skipped binary installation")
41+
return nil
42+
}
43+
}
44+
45+
// If we're already running from /usr/local/bin, no need to install
46+
if currentPath == targetPath {
47+
fmt.Printf("✓ Already running from %s\n", targetPath)
48+
return nil
49+
}
50+
51+
// Inform user about sudo requirement
52+
fmt.Printf("Installing fssh to %s (requires sudo)...\n", targetPath)
53+
54+
// Ensure /usr/local/bin exists
55+
if err := runSudoCommand("mkdir", "-p", "/usr/local/bin"); err != nil {
56+
return fmt.Errorf("failed to create /usr/local/bin: %w", err)
57+
}
58+
59+
// Copy binary
60+
if err := runSudoCommand("cp", currentPath, targetPath); err != nil {
61+
return fmt.Errorf("failed to copy binary: %w", err)
62+
}
63+
64+
// Set permissions
65+
if err := runSudoCommand("chmod", "755", targetPath); err != nil {
66+
return fmt.Errorf("failed to set permissions: %w", err)
67+
}
68+
69+
fmt.Printf("✓ Successfully installed fssh to %s\n", targetPath)
70+
return nil
71+
}
72+
73+
// runSudoCommand runs a command with sudo
74+
func runSudoCommand(name string, args ...string) error {
75+
cmdArgs := append([]string{name}, args...)
76+
cmd := exec.Command("sudo", cmdArgs...)
77+
cmd.Stdin = os.Stdin
78+
cmd.Stdout = os.Stdout
79+
cmd.Stderr = os.Stderr
80+
81+
return cmd.Run()
82+
}

0 commit comments

Comments
 (0)