mirror of
https://github.com/ivuorinen/a.git
synced 2026-01-26 11:34:07 +00:00
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
144 lines
3.6 KiB
Go
144 lines
3.6 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// Config represents the application's YAML configuration.
|
|
type Config struct {
|
|
SSHKeyPath string `yaml:"ssh_key_path"`
|
|
GitHubUser string `yaml:"github_user"`
|
|
DefaultRecipients []string `yaml:"default_recipients"`
|
|
CacheTTLMinutes int `yaml:"cache_ttl_minutes"`
|
|
LogFilePath string `yaml:"log_file_path"`
|
|
}
|
|
|
|
// ConfigPaths holds config and cache file paths.
|
|
type ConfigPaths struct {
|
|
ConfigDir string
|
|
ConfigFile string
|
|
CacheDir string
|
|
}
|
|
|
|
// InitConfigPaths initializes configuration and cache directories and returns their paths.
|
|
func InitConfigPaths() (ConfigPaths, error) {
|
|
var configDir string
|
|
var err error
|
|
|
|
// Personal preference, I don't like the "$HOME/Library/Application Support/" path
|
|
if runtime.GOOS == "darwin" {
|
|
configDir = filepath.Join(os.Getenv("HOME"), ".config")
|
|
} else {
|
|
configDir, err = os.UserConfigDir()
|
|
if err != nil {
|
|
return ConfigPaths{}, err
|
|
}
|
|
}
|
|
|
|
cfgDir := filepath.Join(configDir, "a")
|
|
cfgFile := filepath.Join(cfgDir, "config.yaml")
|
|
if err := os.MkdirAll(cfgDir, 0o700); err != nil {
|
|
return ConfigPaths{}, err
|
|
}
|
|
|
|
cacheBase, err := os.UserCacheDir()
|
|
if err != nil {
|
|
return ConfigPaths{}, err
|
|
}
|
|
cacheDir := filepath.Join(cacheBase, "a")
|
|
if err := os.MkdirAll(cacheDir, 0o700); err != nil {
|
|
return ConfigPaths{}, err
|
|
}
|
|
|
|
return ConfigPaths{
|
|
ConfigDir: cfgDir,
|
|
ConfigFile: cfgFile,
|
|
CacheDir: cacheDir,
|
|
}, nil
|
|
}
|
|
|
|
// LoadConfig loads configuration from the YAML file.
|
|
// gosec G304: cfgFile is always set by InitConfigPaths and not user-controlled.
|
|
func LoadConfig(cfgFile string) (*Config, error) {
|
|
// gosec G304 mitigation: Ensure cfgFile is within the expected config directory
|
|
configDir, err := os.UserConfigDir()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
expectedDir := filepath.Join(configDir, "a")
|
|
absCfgFile, err := filepath.Abs(cfgFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !strings.HasPrefix(absCfgFile, expectedDir) {
|
|
return nil, fmt.Errorf(
|
|
"config file path %s is not within expected config directory %s",
|
|
absCfgFile,
|
|
expectedDir,
|
|
)
|
|
}
|
|
if _, err := os.Stat(cfgFile); err != nil {
|
|
return nil, fmt.Errorf("config file does not exist: %w", err)
|
|
}
|
|
|
|
info, err := os.Stat(cfgFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("config file does not exist: %w", err)
|
|
}
|
|
if info.Mode().Perm() != 0o600 {
|
|
return nil, fmt.Errorf("config file must have 0600 permissions, got %o", info.Mode().Perm())
|
|
}
|
|
// #nosec G304 -- cfgFile is validated to be within the config directory
|
|
data, err := os.ReadFile(cfgFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var cfg Config
|
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
if cfg.LogFilePath == "" {
|
|
stateDir := filepath.Join(os.Getenv("HOME"), ".state", "a")
|
|
if err := os.MkdirAll(stateDir, 0o700); err != nil {
|
|
return nil, err
|
|
}
|
|
cfg.LogFilePath = filepath.Join(stateDir, "cli.log")
|
|
}
|
|
return &cfg, nil
|
|
}
|
|
|
|
// SaveConfig saves configuration to the YAML file.
|
|
func SaveConfig(cfgFile string, cfg *Config) error {
|
|
data, err := yaml.Marshal(cfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(cfgFile, data, 0o600)
|
|
}
|
|
|
|
// ScanSSHPrivateKeys scans ~/.ssh for private keys matching id_* (excluding .pub).
|
|
func ScanSSHPrivateKeys() ([]string, error) {
|
|
sshDir := filepath.Join(os.Getenv("HOME"), ".ssh")
|
|
files, err := os.ReadDir(sshDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var keys []string
|
|
for _, f := range files {
|
|
if f.IsDir() {
|
|
continue
|
|
}
|
|
name := f.Name()
|
|
if strings.HasPrefix(name, "id_") && !strings.HasSuffix(name, ".pub") {
|
|
keys = append(keys, filepath.Join(sshDir, name))
|
|
}
|
|
}
|
|
return keys, nil
|
|
}
|