From feb22db03983ceb9e56af336ad50ee4ed5bbbefb Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Tue, 24 Mar 2026 08:38:32 -0700 Subject: [PATCH] M8: add command to append a single device to config New 'arca add ' subcommand detects a LUKS device via udisks2 and appends it to the config with passphrase as default method. Supports --alias/-a to override the generated name. Skips if UUID already configured. Adds Config.Save() and Config.HasUUID() to config package. Co-Authored-By: Claude Opus 4.6 (1M context) --- cmd/add.go | 87 +++++++++++++++++++++++++++++++++++++++ cmd/init.go | 11 ----- internal/config/config.go | 26 ++++++++++++ 3 files changed, 113 insertions(+), 11 deletions(-) create mode 100644 cmd/add.go diff --git a/cmd/add.go b/cmd/add.go new file mode 100644 index 0000000..1096948 --- /dev/null +++ b/cmd/add.go @@ -0,0 +1,87 @@ +package cmd + +import ( + "fmt" + "strings" + + "git.wntrmute.dev/kyle/arca/internal/config" + "git.wntrmute.dev/kyle/arca/internal/udisks" + "github.com/spf13/cobra" +) + +var addAlias string + +var addCmd = &cobra.Command{ + Use: "add ", + Short: "Add a device to the config", + Long: "Detects a LUKS device via udisks2 and adds it to the config file with a default passphrase method.", + Args: cobra.ExactArgs(1), + RunE: runAdd, +} + +func init() { + addCmd.Flags().StringVarP(&addAlias, "alias", "a", "", "alias name (default: first 8 chars of UUID)") + rootCmd.AddCommand(addCmd) +} + +func runAdd(cmd *cobra.Command, args []string) error { + target := args[0] + + client, err := udisks.NewClient() + if err != nil { + return fmt.Errorf("connecting to udisks2: %w", err) + } + defer client.Close() + + // Find the device to get its UUID. + dev, err := client.FindDevice("", target) + if err != nil { + return err + } + + if !dev.HasEncrypted { + return fmt.Errorf("%s is not a LUKS-encrypted device", target) + } + + if dev.UUID == "" { + return fmt.Errorf("%s has no UUID", target) + } + + cfg := config.Load() + + // Check if already configured. + if existing := cfg.AliasFor(dev.UUID); existing != "" { + fmt.Printf("Device %s (UUID %s) already configured as %q\n", dev.DevicePath, dev.UUID, existing) + return nil + } + + alias := addAlias + if alias == "" { + alias = aliasFromUUID(dev.UUID) + } + + // Check for alias collision. + if _, exists := cfg.Devices[alias]; exists { + return fmt.Errorf("alias %q already in use — choose a different name with --alias", alias) + } + + cfg.Devices[alias] = config.DeviceConfig{ + UUID: dev.UUID, + Methods: []string{"passphrase"}, + } + + if err := cfg.Save(); err != nil { + return fmt.Errorf("saving config: %w", err) + } + + fmt.Printf("Added %s (UUID %s) as %q\n", dev.DevicePath, dev.UUID, alias) + return nil +} + +func aliasFromUUID(uuid string) string { + clean := strings.ReplaceAll(uuid, "-", "") + if len(clean) > 8 { + clean = clean[:8] + } + return clean +} diff --git a/cmd/init.go b/cmd/init.go index b7bc116..a0f7976 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -4,8 +4,6 @@ import ( "fmt" "os" "path/filepath" - "strings" - "git.wntrmute.dev/kyle/arca/internal/config" "git.wntrmute.dev/kyle/arca/internal/udisks" @@ -102,12 +100,3 @@ func isRootBacking(path dbus.ObjectPath, rootDevices []dbus.ObjectPath) bool { return false } -func aliasFromUUID(uuid string) string { - // Use first 8 chars of UUID as a stable alias. - // "b8b2f8e3-4cde-4aca-a96e-df9274019f9f" -> "b8b2f8e3" - clean := strings.ReplaceAll(uuid, "-", "") - if len(clean) > 8 { - clean = clean[:8] - } - return clean -} diff --git a/internal/config/config.go b/internal/config/config.go index 26f8be7..857649a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,6 +1,7 @@ package config import ( + "fmt" "os" "path/filepath" @@ -76,6 +77,31 @@ func resolvedFrom(dev DeviceConfig) ResolvedDevice { } } +// HasUUID returns true if a device with the given UUID is already configured. +func (c *Config) HasUUID(uuid string) bool { + for _, dev := range c.Devices { + if dev.UUID == uuid { + return true + } + } + return false +} + +// Save writes the config to the config file. +func (c *Config) Save() error { + path := configPath() + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return fmt.Errorf("creating config directory: %w", err) + } + + data, err := yaml.Marshal(c) + if err != nil { + return fmt.Errorf("marshaling config: %w", err) + } + + return os.WriteFile(path, data, 0o644) +} + // AliasFor returns the config alias for a given UUID, or "" if none. func (c *Config) AliasFor(uuid string) string { for name, dev := range c.Devices {