Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 22 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,20 +184,20 @@ For managing NixOS systems defined in `systems.nixos`.
```sh
nilla os build <system_name>
# Build on remote host:
# nilla os build <system_name> --build-on user@hostname
# nilla os build <system_name> --build-on <user@target>
# Build on same host as target:
# nilla os build <system_name> --target user@hostname --build-on-target
# nilla os build <system_name> --target <user@target> --build-on-target
```

* **Build and switch to a configuration:**
```sh
nilla os switch <system_name>
# For remote targets:
# nilla os switch <system_name> --target user@hostname
# nilla os switch <system_name> --target <user@target>
# Build on remote, deploy to different host:
# nilla os switch <system_name> --build-on user@builder --target user@hostname
# nilla os switch <system_name> --build-on user@builder --target <user@target>
# Build and deploy to same host:
# nilla os switch <system_name> --target user@hostname --build-on-target
# nilla os switch <system_name> --target <user@target> --build-on-target
```
* **Test a configuration:**
```sh
Expand All @@ -215,6 +215,9 @@ For managing NixOS systems defined in `systems.nixos`.
```sh
nilla os generations list
nilla os generations clean --keep 3 # Keeps the last 3 generations
# For remote targets:
nilla os --target <user@target> generations list
nilla os --target <user@target> generations clean --keep 3
```
Use `nilla os --help` or `nilla os <subcommand> --help` for more details.

Expand All @@ -226,19 +229,19 @@ For managing Home Manager configurations defined in `systems.home`.
```sh
nilla home build <user@system_name>
# Build on remote host:
# nilla home build <user@system_name> --build-on user@hostname
nilla home build <user@system_name> --build-on <user@build-target>
# Build on same host as target:
# nilla home build <user@system_name> --target user@hostname --build-on-target
nilla home build <user@system_name> --target <user@target> --build-on-target
```
* **Build and switch to a configuration:**
```sh
nilla home switch <user@system_name>
# For remote targets:
# nilla home switch <user@system_name> --target user@hostname
nilla home switch <user@system_name> --target <user@target>
# Build on remote, deploy to different host:
# nilla home switch <user@system_name> --build-on user@builder --target user@hostname
nilla home switch <user@system_name> --build-on <user@build-target> --target <user@target>
# Build and deploy to same host:
# nilla home switch <user@system_name> --target user@hostname --build-on-target
nilla home switch <user@system_name> --target <user@target> --build-on-target
```
* **List available Home Manager configurations:**
```sh
Expand All @@ -248,6 +251,15 @@ For managing Home Manager configurations defined in `systems.home`.
```sh
nilla home generations list
nilla home generations clean --keep 3 # Keeps the last 3 generations
# For remote host - connect as current user:
nilla home --target <target> generations list
nilla home --target <target> generations clean --keep 3
# For remote host - connect as specific user:
nilla home --target <user@target> generations list
nilla home --target <user@target> generations clean --keep 3
# For remote host - connect as specific user and use specific Home Manager configuration (configuration must exist in "nilla home list" and hostname must match target):
nilla home --target <user@target> generations list <user@system_name>
nilla home --target <user@target> generations clean --keep 3 <user@system_name>
```
Use `nilla home --help` or `nilla home <subcommand> --help` for more details.

Expand Down
174 changes: 163 additions & 11 deletions cmd/nilla-home/generations.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import (
"context"
"fmt"
"os"
"os/exec"
osexec "os/exec"
"slices"
"strconv"
"time"

"github.com/arnarg/nilla-utils/internal/exec"
"github.com/arnarg/nilla-utils/internal/generation"
"github.com/arnarg/nilla-utils/internal/nix"
"github.com/arnarg/nilla-utils/internal/project"
"github.com/arnarg/nilla-utils/internal/tui"
"github.com/arnarg/nilla-utils/internal/util"
"github.com/charmbracelet/lipgloss"
Expand All @@ -23,15 +26,91 @@ func sortGenerationsDesc(generations []*generation.HomeGeneration) {
})
}

func setupExecutor(targetStr string) (exec.Executor, string, error) {
if targetStr == "" {
return nil, "", nil
}

executor, err := exec.NewSSHExecutor(targetStr)
if err != nil {
return nil, "", fmt.Errorf("failed to setup SSH executor for target %s: %w", targetStr, err)
}

// Extract username from target using util.ParseTarget
username, _ := util.ParseTarget(targetStr)
return executor, username, nil
}

func listGenerations(ctx context.Context, cmd *cli.Command) error {
// Allow 0 or 1 argument: optional Home Manager configuration name (e.g., "root@host1")
if err := util.ValidateArgs(cmd, 1); err != nil {
return err
}

targetStr := cmd.String("target")
configName := cmd.Args().First()

var username string
if configName != "" {
// Configuration name provided - validate before connecting
if targetStr == "" {
return fmt.Errorf("--target is required when listing generations for a specific configuration")
}

// Validate configuration exists in project (before SSH connection)
source, err := project.Resolve(cmd.String("project"))
if err != nil {
return err
}
systems, err := nix.ListAttrsInProject(source.NillaPath, source.FixedOutputStoreEntry(), "systems.home")
if err != nil {
return err
}
found := false
for _, system := range systems {
if system == configName {
found = true
break
}
}
if !found {
return fmt.Errorf("configuration '%s' not found in project. Available configurations: %v", configName, systems)
}

// Extract hostname from target
_, targetHostname := util.ParseTarget(targetStr)

// Get configuration name from argument
configUser, configHostname := util.ParseTarget(configName)

// Validate hostname matches target
if configHostname != targetHostname {
return fmt.Errorf("hostname mismatch: configuration '%s' has hostname '%s' but --target has hostname '%s'", configName, configHostname, targetHostname)
}

// Use username from configuration name
username = configUser
}

// Setup executor after validation (to avoid connecting if validation fails)
executor, targetUsername, err := setupExecutor(targetStr)
if err != nil {
return err
}

if username == "" {
// No configuration name provided - use username from target
username = targetUsername
}

// Get current generation
current, err := generation.CurrentHomeGeneration()
current, err := generation.CurrentHomeGeneration(executor, username)
if err != nil {
return err
}

// List all generations
generations, err := generation.ListHomeGenerations()
generations, err := generation.ListHomeGenerations(executor, username)
if err != nil {
return err
}
Expand Down Expand Up @@ -70,18 +149,79 @@ type genAction struct {
}

func cleanGenerations(ctx context.Context, cmd *cli.Command) error {
// Allow 0 or 1 argument: optional Home Manager configuration name (e.g., "root@host1")
if err := util.ValidateArgs(cmd, 1); err != nil {
return err
}

targetStr := cmd.String("target")
configName := cmd.Args().First()

var username string
if configName != "" {
// Configuration name provided - validate before connecting
if targetStr == "" {
return fmt.Errorf("--target is required when cleaning generations for a specific configuration")
}

// Validate configuration exists in project (before SSH connection)
source, err := project.Resolve(cmd.String("project"))
if err != nil {
return err
}
systems, err := nix.ListAttrsInProject(source.NillaPath, source.FixedOutputStoreEntry(), "systems.home")
if err != nil {
return err
}
found := false
for _, system := range systems {
if system == configName {
found = true
break
}
}
if !found {
return fmt.Errorf("configuration '%s' not found in project. Available configurations: %v", configName, systems)
}

// Extract hostname from target
_, targetHostname := util.ParseTarget(targetStr)

// Get configuration name from argument
configUser, configHostname := util.ParseTarget(configName)

// Validate hostname matches target
if configHostname != targetHostname {
return fmt.Errorf("hostname mismatch: configuration '%s' has hostname '%s' but --target has hostname '%s'", configName, configHostname, targetHostname)
}

// Use username from configuration name
username = configUser
}

// Setup executor after validation (to avoid connecting if validation fails)
executor, targetUsername, err := setupExecutor(targetStr)
if err != nil {
return err
}

if username == "" {
// No configuration name provided - use username from target
username = targetUsername
}

// Parse parameters
keep := cmd.Uint("keep")
foundCurrent := false

// Get current generation
current, err := generation.CurrentHomeGeneration()
current, err := generation.CurrentHomeGeneration(executor, username)
if err != nil {
return err
}

// List all generations
generations, err := generation.ListHomeGenerations()
generations, err := generation.ListHomeGenerations(executor, username)
if err != nil {
return err
}
Expand Down Expand Up @@ -167,7 +307,7 @@ func cleanGenerations(ctx context.Context, cmd *cli.Command) error {
//
for _, action := range actions {
if !action.keep {
if err := action.generation.Delete(); err != nil {
if err := action.generation.DeleteWithExecutor(executor); err != nil {
return err
}
}
Expand All @@ -179,9 +319,21 @@ func cleanGenerations(ctx context.Context, cmd *cli.Command) error {
fmt.Fprintln(os.Stderr)
printSection("Collecting garbage from nix store")

gc := exec.CommandContext(ctx, "nix", "store", "gc", "-v")
gc.Stdout = os.Stderr
gc.Stderr = os.Stderr

return gc.Run()
if executor != nil && !executor.IsLocal() {
// Run garbage collection on remote
// Note: Home Manager GC doesn't require sudo (user-level operation)
gcCmd, err := executor.Command("nix", "store", "gc", "-v")
if err != nil {
return err
}
gcCmd.SetStdout(os.Stderr)
gcCmd.SetStderr(os.Stderr)
return gcCmd.Run()
} else {
// Run garbage collection locally
gc := osexec.CommandContext(ctx, "nix", "store", "gc", "-v")
gc.Stdout = os.Stderr
gc.Stderr = os.Stderr
return gc.Run()
}
}
Loading