M6: shell completions with dynamic alias lookup
Add ValidArgsFunction to mount and unmount commands that reads config aliases for tab completion. Install zsh, bash, and fish completion scripts via flake postInstall. Update PLAN.md with post-1.0 roadmap. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
264
PLAN.md
264
PLAN.md
@@ -1,158 +1,154 @@
|
|||||||
# arca — v1.0.0 Plan
|
# arca — Post-1.0 Plan
|
||||||
|
|
||||||
## Current State (v0.1.0)
|
## Current State (v1.0.0)
|
||||||
|
|
||||||
Working proof of concept. FIDO2 unlock via cryptsetup with passphrase
|
Released. FIDO2/passphrase/keyfile/TPM2 unlock with method sequencing,
|
||||||
fallback, privileged mount/unmount, config aliases with method
|
idempotent mount/unmount, config aliases, init command, status command,
|
||||||
sequencing, init and status commands.
|
NixOS LD_LIBRARY_PATH workaround for cryptsetup token plugins, unit
|
||||||
|
tests, nix flake packaging.
|
||||||
## M1: Idempotent mount/unmount
|
|
||||||
|
|
||||||
`mount` and `unmount` should be safe to run at any point in a device's
|
|
||||||
lifecycle without producing confusing errors.
|
|
||||||
|
|
||||||
### Files changed
|
|
||||||
|
|
||||||
- `cmd/mount.go`
|
|
||||||
- `cmd/unmount.go`
|
|
||||||
- `internal/udisks/client.go`
|
|
||||||
|
|
||||||
### Work
|
|
||||||
|
|
||||||
1. In `runMount`, after `FindDevice`, check if the device is already
|
|
||||||
unlocked (`CleartextDevice` succeeds). If the cleartext device is
|
|
||||||
already mounted, print the existing mount point and return success.
|
|
||||||
If unlocked but not mounted, skip unlock and go straight to mount.
|
|
||||||
|
|
||||||
2. In `runUnmount`, handle each failure case:
|
|
||||||
- `CleartextDevice` fails (already locked): print "already locked"
|
|
||||||
and return success.
|
|
||||||
- `Unmount` fails because not mounted: proceed to lock anyway.
|
|
||||||
- `Lock` fails because already locked: return success.
|
|
||||||
|
|
||||||
3. Add `client.IsMounted(dev)` helper that returns `(mountpoint, bool)`
|
|
||||||
to reduce duplicated mount-point checking logic.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## M2: Error messages
|
## v1.1: Shell completions and verbose mode
|
||||||
|
|
||||||
Replace generic D-Bus errors with actionable messages.
|
### M6: Shell completions
|
||||||
|
|
||||||
### Files changed
|
Cobra generates completion scripts. Wire them up so alias names
|
||||||
|
complete in the shell.
|
||||||
|
|
||||||
- `internal/udisks/client.go`
|
#### Files changed
|
||||||
- `cmd/mount.go`, `cmd/unmount.go`, `cmd/init.go`, `cmd/status.go`
|
|
||||||
|
|
||||||
### Work
|
|
||||||
|
|
||||||
1. Wrap the `dbus.SystemBus()` error in `NewClient` to detect
|
|
||||||
"connection refused" or "no such file" and print:
|
|
||||||
`"cannot connect to udisks2 — is the udisks2 service running?"`
|
|
||||||
|
|
||||||
2. In `FindDevice`, when no device matches, include what was searched
|
|
||||||
and suggest `arca status` or `arca init`:
|
|
||||||
`"device /dev/sda1 not found (run 'arca status' to list devices)"`
|
|
||||||
|
|
||||||
3. In the unlock sequencer, prefix each method error with context:
|
|
||||||
`"fido2: cryptsetup open --token-only: exit status 5 (is the FIDO2 key plugged in?)"`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## M3: Unit tests
|
|
||||||
|
|
||||||
Cover pure logic that doesn't need D-Bus or real devices.
|
|
||||||
|
|
||||||
### Files changed
|
|
||||||
|
|
||||||
- `internal/config/config_test.go` (new)
|
|
||||||
- `internal/cryptsetup/cryptsetup_test.go` (new)
|
|
||||||
|
|
||||||
### Work
|
|
||||||
|
|
||||||
1. **config_test.go** — test cases:
|
|
||||||
- `ResolveDevice` with exact alias match
|
|
||||||
- `ResolveDevice` with device path match (`/dev/sda1` -> `sda1`)
|
|
||||||
- `ResolveDevice` with unknown name returns default methods
|
|
||||||
- `ResolveDevice` with empty methods list defaults to `[passphrase]`
|
|
||||||
- `AliasFor` finds alias by UUID
|
|
||||||
- `AliasFor` returns "" for unknown UUID
|
|
||||||
- `Load` with nonexistent file returns empty config
|
|
||||||
- `Load` with valid YAML parses correctly
|
|
||||||
|
|
||||||
2. **cryptsetup_test.go** — test cases:
|
|
||||||
- `MapperName("/dev/sda1")` == `"arca-sda1"`
|
|
||||||
- `MapperName("/dev/nvme0n1p2")` == `"arca-nvme0n1p2"`
|
|
||||||
- `hasTokenPlugins` with a temp dir containing a matching .so
|
|
||||||
- `hasTokenPlugins` with an empty temp dir
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## M4: CLI polish
|
|
||||||
|
|
||||||
Small usability improvements.
|
|
||||||
|
|
||||||
### Files changed
|
|
||||||
|
|
||||||
- `cmd/root.go`
|
- `cmd/root.go`
|
||||||
- `cmd/mount.go`
|
|
||||||
- `cmd/init.go`
|
|
||||||
- `main.go`
|
|
||||||
- `.gitignore` (new)
|
|
||||||
|
|
||||||
### Work
|
|
||||||
|
|
||||||
1. Add `var version = "dev"` to `main.go`. Set `rootCmd.Version` in
|
|
||||||
`cmd/root.go`. Build with `-ldflags "-X main.version=..."` in
|
|
||||||
`flake.nix`.
|
|
||||||
|
|
||||||
2. Add `--mountpoint` / `-m` flag to `mount` subcommand. When set,
|
|
||||||
pass it to `cryptsetup.Mount` (privileged path) or log a warning
|
|
||||||
that udisks2 doesn't support custom mount points.
|
|
||||||
|
|
||||||
3. In `init`, use first 8 chars of UUID as alias instead of device
|
|
||||||
path basename. Example: `b8b2f8e3` instead of `sda1`. UUIDs are
|
|
||||||
stable across boots; device paths are not.
|
|
||||||
|
|
||||||
4. Create `.gitignore` containing `/arca`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## M5: Documentation and packaging
|
|
||||||
|
|
||||||
Make the project installable and the README trustworthy.
|
|
||||||
|
|
||||||
### Files changed
|
|
||||||
|
|
||||||
- `README.md`
|
|
||||||
- `flake.nix`
|
- `flake.nix`
|
||||||
|
|
||||||
### Work
|
#### Work
|
||||||
|
|
||||||
1. Update `README.md`:
|
1. Cobra provides `completion` subcommand by default (`arca completion
|
||||||
- Replace placeholder UUIDs with realistic examples from actual
|
zsh`, `arca completion bash`, etc.). Verify it works.
|
||||||
tested usage.
|
|
||||||
- Add "NixOS notes" section documenting the `LD_LIBRARY_PATH`
|
|
||||||
requirement for FIDO2/TPM2 and why `--external-tokens-path`
|
|
||||||
doesn't work.
|
|
||||||
- Add "Troubleshooting" section: no FIDO2 token enrolled, udisks2
|
|
||||||
not running, permission denied on mount.
|
|
||||||
- Document `init` subcommand.
|
|
||||||
|
|
||||||
2. Verify `flake.nix`:
|
2. Add a custom `ValidArgsFunction` to `mountCmd` and `unmountCmd`
|
||||||
- Run `nix build` and confirm it produces a working binary.
|
that reads the config and returns alias names + device paths from
|
||||||
- Add `-ldflags` for version injection from `self.rev` or a
|
`arca status`. This gives dynamic completion for:
|
||||||
`version` variable.
|
```
|
||||||
- Test the flake output: `./result/bin/arca --version`.
|
arca mount <TAB> -> backup media /dev/sda1
|
||||||
|
arca unmount <TAB> -> backup media
|
||||||
|
```
|
||||||
|
|
||||||
3. Tag `v1.0.0`.
|
3. In `flake.nix`, install the zsh completion file to
|
||||||
|
`$out/share/zsh/site-functions/_arca` so NixOS picks it up
|
||||||
|
automatically.
|
||||||
|
|
||||||
|
### M7: Verbose mode
|
||||||
|
|
||||||
|
Add `-v` / `--verbose` flag to aid debugging (e.g., the FIDO2 plugin
|
||||||
|
discovery issue we hit during development).
|
||||||
|
|
||||||
|
#### Files changed
|
||||||
|
|
||||||
|
- `cmd/root.go`
|
||||||
|
- `internal/cryptsetup/cryptsetup.go`
|
||||||
|
- `internal/unlock/unlock.go`
|
||||||
|
- `internal/udisks/client.go`
|
||||||
|
|
||||||
|
#### Work
|
||||||
|
|
||||||
|
1. Add a `--verbose` / `-v` persistent flag on the root command. Store
|
||||||
|
in a package-level `Verbose bool`.
|
||||||
|
|
||||||
|
2. In `cryptsetup.Open`: print the full command being executed
|
||||||
|
(including `LD_LIBRARY_PATH` and privilege wrapper).
|
||||||
|
|
||||||
|
3. In `findTokenPluginDir`: print which directory was found (or that
|
||||||
|
none was found).
|
||||||
|
|
||||||
|
4. In `unlock.Unlock`: print which method is being attempted before
|
||||||
|
each try.
|
||||||
|
|
||||||
|
5. In `udisks.NewClient`: print the D-Bus connection status.
|
||||||
|
|
||||||
|
Use `fmt.Fprintf(os.Stderr, "arca: ...")` for verbose output — no
|
||||||
|
logging framework needed.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Non-goals for v1.0.0
|
## v1.2: Config management and cleanup
|
||||||
|
|
||||||
|
### M8: `arca add <device>`
|
||||||
|
|
||||||
|
Add a single device to an existing config without regenerating
|
||||||
|
everything.
|
||||||
|
|
||||||
|
#### Files changed
|
||||||
|
|
||||||
|
- `cmd/add.go` (new)
|
||||||
|
- `internal/config/config.go`
|
||||||
|
|
||||||
|
#### Work
|
||||||
|
|
||||||
|
1. New subcommand: `arca add /dev/sda1` or `arca add --uuid <uuid>`.
|
||||||
|
|
||||||
|
2. Resolve the device via udisks2 to get UUID.
|
||||||
|
|
||||||
|
3. Generate a UUID-prefix alias (same as `init`).
|
||||||
|
|
||||||
|
4. Prompt for alias name (default: UUID prefix), methods (default:
|
||||||
|
passphrase), and optional mountpoint.
|
||||||
|
|
||||||
|
5. Append to existing config. If the UUID is already configured, print
|
||||||
|
the existing alias and exit.
|
||||||
|
|
||||||
|
6. Add `config.Save(*Config)` to write the config file (currently
|
||||||
|
only `init` writes, with inline marshaling).
|
||||||
|
|
||||||
|
### M9: `init --merge`
|
||||||
|
|
||||||
|
Merge newly discovered devices into an existing config without
|
||||||
|
overwriting existing entries.
|
||||||
|
|
||||||
|
#### Files changed
|
||||||
|
|
||||||
|
- `cmd/init.go`
|
||||||
|
- `internal/config/config.go`
|
||||||
|
|
||||||
|
#### Work
|
||||||
|
|
||||||
|
1. Add `--merge` flag to `init`. When set, load existing config
|
||||||
|
first.
|
||||||
|
|
||||||
|
2. For each discovered device, skip if its UUID already exists in
|
||||||
|
config (regardless of alias name).
|
||||||
|
|
||||||
|
3. Add new devices with UUID-prefix aliases.
|
||||||
|
|
||||||
|
4. Write the merged config.
|
||||||
|
|
||||||
|
This replaces the current `--force` behavior for the common case of
|
||||||
|
"I plugged in a new drive."
|
||||||
|
|
||||||
|
### M10: Mount cleanup
|
||||||
|
|
||||||
|
Privileged unmount leaves empty directories under `/mnt/`.
|
||||||
|
|
||||||
|
#### Files changed
|
||||||
|
|
||||||
|
- `internal/cryptsetup/cryptsetup.go`
|
||||||
|
- `cmd/unmount.go`
|
||||||
|
|
||||||
|
#### Work
|
||||||
|
|
||||||
|
1. After `Unmount`, attempt `rmdir` on the mount point. Only remove
|
||||||
|
if empty (not `rm -rf`).
|
||||||
|
|
||||||
|
2. Use privileged `rmdir` since the directory was created with
|
||||||
|
privilege.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
- `--dry-run` flag
|
- `--dry-run` flag
|
||||||
- `--json` output for `status`
|
- `--json` output for `status`
|
||||||
- udev auto-mount on plug
|
- udev auto-mount on plug
|
||||||
- Keyfile creation/management
|
- Keyfile creation/management
|
||||||
- Multiple config files or config includes
|
- Multiple config files or config includes
|
||||||
|
- Systemd mount unit integration
|
||||||
|
|||||||
25
cmd/completion.go
Normal file
25
cmd/completion.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/arca/internal/config"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// completeDeviceOrAlias provides dynamic completion for device aliases
|
||||||
|
// and device paths from the config file.
|
||||||
|
func completeDeviceOrAlias(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) > 0 {
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := config.Load()
|
||||||
|
var completions []string
|
||||||
|
for alias := range cfg.Devices {
|
||||||
|
completions = append(completions, alias)
|
||||||
|
}
|
||||||
|
sort.Strings(completions)
|
||||||
|
|
||||||
|
return completions, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
@@ -15,10 +15,11 @@ import (
|
|||||||
var mountpoint string
|
var mountpoint string
|
||||||
|
|
||||||
var mountCmd = &cobra.Command{
|
var mountCmd = &cobra.Command{
|
||||||
Use: "mount <device|alias>",
|
Use: "mount <device|alias>",
|
||||||
Short: "Unlock and mount a LUKS volume",
|
Short: "Unlock and mount a LUKS volume",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: runMount,
|
RunE: runMount,
|
||||||
|
ValidArgsFunction: completeDeviceOrAlias,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -10,11 +10,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var unmountCmd = &cobra.Command{
|
var unmountCmd = &cobra.Command{
|
||||||
Use: "unmount <device|alias>",
|
Use: "unmount <device|alias>",
|
||||||
Aliases: []string{"umount"},
|
Aliases: []string{"umount"},
|
||||||
Short: "Unmount and lock a LUKS volume",
|
Short: "Unmount and lock a LUKS volume",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: runUnmount,
|
RunE: runUnmount,
|
||||||
|
ValidArgsFunction: completeDeviceOrAlias,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
10
flake.nix
10
flake.nix
@@ -10,7 +10,7 @@
|
|||||||
let
|
let
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
version = "1.0.0";
|
version = "1.1.0";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
packages.${system}.default = pkgs.buildGoModule {
|
packages.${system}.default = pkgs.buildGoModule {
|
||||||
@@ -21,6 +21,14 @@
|
|||||||
ldflags = [
|
ldflags = [
|
||||||
"-X main.version=${version}"
|
"-X main.version=${version}"
|
||||||
];
|
];
|
||||||
|
postInstall = ''
|
||||||
|
mkdir -p $out/share/zsh/site-functions
|
||||||
|
mkdir -p $out/share/bash-completion/completions
|
||||||
|
mkdir -p $out/share/fish/vendor_completions.d
|
||||||
|
$out/bin/arca completion zsh > $out/share/zsh/site-functions/_arca
|
||||||
|
$out/bin/arca completion bash > $out/share/bash-completion/completions/arca
|
||||||
|
$out/bin/arca completion fish > $out/share/fish/vendor_completions.d/arca.fish
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user