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:
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
Reference in New Issue
Block a user