Initial commit: sshkeeper v0.1.0

Console SSH connection manager for Linux.

Features:
- TUI (Bubble Tea) with server list, add/edit form, test/save
- CLI commands: add, list, show, edit, delete, connect, test, search, import, export, run, group, template, vault, ssh-config
- Encrypted vault (Argon2id + XChaCha20-Poly1305) for passwords
- PTY-wrapper for password auth
- SQLite (modernc, no CGO) for server profiles
- XDG-compatible paths
- OpenSSH config generation
- Import from ~/.ssh/config
This commit is contained in:
2026-05-26 09:11:55 +08:00
commit 883a1e66a7
34 changed files with 5244 additions and 0 deletions
+106
View File
@@ -0,0 +1,106 @@
package config
import (
"os"
"path/filepath"
"github.com/BurntSushi/toml"
)
type Config struct {
SSH SSHConfig `toml:"ssh"`
Vault VaultConfig `toml:"vault"`
UI UIConfig `toml:"ui"`
// resolved paths
ConfigDir string `toml:"-"`
DataDir string `toml:"-"`
}
type SSHConfig struct {
Binary string `toml:"binary"`
ConnectTimeoutSec int `toml:"connect_timeout_seconds"`
TestCommand string `toml:"test_command"`
}
type VaultConfig struct {
AutoLockMinutes int `toml:"auto_lock_minutes"`
}
type UIConfig struct {
ShowSecurityHints bool `toml:"show_security_hints"`
}
func defaultConfig() *Config {
return &Config{
SSH: SSHConfig{
Binary: "/usr/bin/ssh",
ConnectTimeoutSec: 10,
TestCommand: "echo SSHKEEPER_OK",
},
Vault: VaultConfig{
AutoLockMinutes: 15,
},
UI: UIConfig{
ShowSecurityHints: false,
},
}
}
func Load() (*Config, error) {
cfg := defaultConfig()
// XDG paths
configDir := os.Getenv("XDG_CONFIG_HOME")
if configDir == "" {
home, err := os.UserHomeDir()
if err != nil {
return nil, err
}
configDir = filepath.Join(home, ".config", "sshkeeper")
}
cfg.ConfigDir = configDir
dataDir := os.Getenv("XDG_DATA_HOME")
if dataDir == "" {
home, err := os.UserHomeDir()
if err != nil {
return nil, err
}
dataDir = filepath.Join(home, ".local", "share", "sshkeeper")
}
cfg.DataDir = dataDir
// Ensure dirs exist
if err := os.MkdirAll(configDir, 0700); err != nil {
return nil, err
}
if err := os.MkdirAll(dataDir, 0700); err != nil {
return nil, err
}
configFile := filepath.Join(configDir, "config.toml")
// Write default config if not exists
if _, err := os.Stat(configFile); os.IsNotExist(err) {
f, err := os.OpenFile(configFile, os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
return nil, err
}
defer f.Close()
if err := toml.NewEncoder(f).Encode(cfg); err != nil {
return nil, err
}
}
// Parse existing config
if _, err := toml.DecodeFile(configFile, cfg); err != nil {
return nil, err
}
// Re-apply paths since toml decode might overwrite
cfg.ConfigDir = configDir
cfg.DataDir = dataDir
return cfg, nil
}
+11
View File
@@ -0,0 +1,11 @@
package config
import "path/filepath"
func DBPath(dataDir string) string {
return filepath.Join(dataDir, "sshkeeper.db")
}
func VaultPath(dataDir string) string {
return filepath.Join(dataDir, "vault.bin")
}