From 60c0c50acb7c90f1f5410149badf5e805f223ece Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Tue, 24 Mar 2026 22:53:07 -0700 Subject: [PATCH] Step 30: Targeting CLI commands. tag add/remove/list for machine-local tags. identity prints full label set. --only/--never flags on add. target command to set/clear targeting on existing entries. SetTargeting garden method. Co-Authored-By: Claude Opus 4.6 (1M context) --- cmd/sgard/add.go | 10 ++++++ cmd/sgard/identity.go | 27 +++++++++++++++ cmd/sgard/tag.go | 76 +++++++++++++++++++++++++++++++++++++++++++ cmd/sgard/target.go | 48 +++++++++++++++++++++++++++ garden/target.go | 34 +++++++++++++++++++ 5 files changed, 195 insertions(+) create mode 100644 cmd/sgard/identity.go create mode 100644 cmd/sgard/tag.go create mode 100644 cmd/sgard/target.go create mode 100644 garden/target.go diff --git a/cmd/sgard/add.go b/cmd/sgard/add.go index 68609c0..5992f70 100644 --- a/cmd/sgard/add.go +++ b/cmd/sgard/add.go @@ -14,6 +14,8 @@ var ( encryptFlag bool lockFlag bool dirOnlyFlag bool + onlyFlag []string + neverFlag []string ) var addCmd = &cobra.Command{ @@ -35,10 +37,16 @@ var addCmd = &cobra.Command{ } } + if len(onlyFlag) > 0 && len(neverFlag) > 0 { + return fmt.Errorf("--only and --never are mutually exclusive") + } + opts := garden.AddOptions{ Encrypt: encryptFlag, Lock: lockFlag, DirOnly: dirOnlyFlag, + Only: onlyFlag, + Never: neverFlag, } if err := g.Add(args, opts); err != nil { @@ -63,5 +71,7 @@ func init() { addCmd.Flags().BoolVar(&encryptFlag, "encrypt", false, "encrypt file contents before storing") addCmd.Flags().BoolVar(&lockFlag, "lock", false, "mark as locked (repo-authoritative, restore always overwrites)") addCmd.Flags().BoolVar(&dirOnlyFlag, "dir", false, "track directory itself without recursing into contents") + addCmd.Flags().StringSliceVar(&onlyFlag, "only", nil, "only apply on machines matching these labels (comma-separated)") + addCmd.Flags().StringSliceVar(&neverFlag, "never", nil, "never apply on machines matching these labels (comma-separated)") rootCmd.AddCommand(addCmd) } diff --git a/cmd/sgard/identity.go b/cmd/sgard/identity.go new file mode 100644 index 0000000..ba59dd4 --- /dev/null +++ b/cmd/sgard/identity.go @@ -0,0 +1,27 @@ +package main + +import ( + "fmt" + + "github.com/kisom/sgard/garden" + "github.com/spf13/cobra" +) + +var identityCmd = &cobra.Command{ + Use: "identity", + Short: "Show this machine's identity labels", + RunE: func(cmd *cobra.Command, args []string) error { + g, err := garden.Open(repoFlag) + if err != nil { + return err + } + for _, label := range g.Identity() { + fmt.Println(label) + } + return nil + }, +} + +func init() { + rootCmd.AddCommand(identityCmd) +} diff --git a/cmd/sgard/tag.go b/cmd/sgard/tag.go new file mode 100644 index 0000000..3a16ffa --- /dev/null +++ b/cmd/sgard/tag.go @@ -0,0 +1,76 @@ +package main + +import ( + "fmt" + "sort" + + "github.com/kisom/sgard/garden" + "github.com/spf13/cobra" +) + +var tagCmd = &cobra.Command{ + Use: "tag", + Short: "Manage machine tags for per-machine targeting", +} + +var tagAddCmd = &cobra.Command{ + Use: "add ", + Short: "Add a tag to this machine", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + g, err := garden.Open(repoFlag) + if err != nil { + return err + } + if err := g.SaveTag(args[0]); err != nil { + return err + } + fmt.Printf("Tag %q added.\n", args[0]) + return nil + }, +} + +var tagRemoveCmd = &cobra.Command{ + Use: "remove ", + Short: "Remove a tag from this machine", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + g, err := garden.Open(repoFlag) + if err != nil { + return err + } + if err := g.RemoveTag(args[0]); err != nil { + return err + } + fmt.Printf("Tag %q removed.\n", args[0]) + return nil + }, +} + +var tagListCmd = &cobra.Command{ + Use: "list", + Short: "List tags on this machine", + RunE: func(cmd *cobra.Command, args []string) error { + g, err := garden.Open(repoFlag) + if err != nil { + return err + } + tags := g.LoadTags() + if len(tags) == 0 { + fmt.Println("No tags set.") + return nil + } + sort.Strings(tags) + for _, tag := range tags { + fmt.Println(tag) + } + return nil + }, +} + +func init() { + tagCmd.AddCommand(tagAddCmd) + tagCmd.AddCommand(tagRemoveCmd) + tagCmd.AddCommand(tagListCmd) + rootCmd.AddCommand(tagCmd) +} diff --git a/cmd/sgard/target.go b/cmd/sgard/target.go new file mode 100644 index 0000000..9106bc4 --- /dev/null +++ b/cmd/sgard/target.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + + "github.com/kisom/sgard/garden" + "github.com/spf13/cobra" +) + +var ( + targetOnlyFlag []string + targetNeverFlag []string + targetClearFlag bool +) + +var targetCmd = &cobra.Command{ + Use: "target ", + Short: "Set or clear targeting labels on a tracked entry", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + g, err := garden.Open(repoFlag) + if err != nil { + return err + } + + if len(targetOnlyFlag) > 0 && len(targetNeverFlag) > 0 { + return fmt.Errorf("--only and --never are mutually exclusive") + } + + if err := g.SetTargeting(args[0], targetOnlyFlag, targetNeverFlag, targetClearFlag); err != nil { + return err + } + + if targetClearFlag { + fmt.Printf("Cleared targeting for %s.\n", args[0]) + } else { + fmt.Printf("Updated targeting for %s.\n", args[0]) + } + return nil + }, +} + +func init() { + targetCmd.Flags().StringSliceVar(&targetOnlyFlag, "only", nil, "only apply on matching machines") + targetCmd.Flags().StringSliceVar(&targetNeverFlag, "never", nil, "never apply on matching machines") + targetCmd.Flags().BoolVar(&targetClearFlag, "clear", false, "remove all targeting labels") + rootCmd.AddCommand(targetCmd) +} diff --git a/garden/target.go b/garden/target.go new file mode 100644 index 0000000..dff020b --- /dev/null +++ b/garden/target.go @@ -0,0 +1,34 @@ +package garden + +import "fmt" + +// SetTargeting updates the Only/Never fields on an existing manifest entry. +// If clear is true, both fields are reset to nil. +func (g *Garden) SetTargeting(path string, only, never []string, clear bool) error { + abs, err := ExpandTildePath(path) + if err != nil { + return fmt.Errorf("expanding path: %w", err) + } + tilded := toTildePath(abs) + + entry := g.findEntry(tilded) + if entry == nil { + return fmt.Errorf("not tracking %s", tilded) + } + + if clear { + entry.Only = nil + entry.Never = nil + } else { + if len(only) > 0 { + entry.Only = only + entry.Never = nil + } + if len(never) > 0 { + entry.Never = never + entry.Only = nil + } + } + + return g.manifest.Save(g.manifestPath) +}