From e9d717ca4a8184a50072fc58ec80d5f59ef7bab9 Mon Sep 17 00:00:00 2001 From: Florian Kinder Date: Thu, 16 Apr 2026 11:32:53 +0200 Subject: [PATCH 1/3] feat(api): add project scope to Variable struct, ListVariables, and CreateVariable --- internal/api/client.go | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/internal/api/client.go b/internal/api/client.go index 0ec49f3..dd466d3 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -115,10 +115,11 @@ type Credential struct { // Variable represents an n8n variable type Variable struct { - ID string `json:"id,omitempty"` - Key string `json:"key"` - Value string `json:"value"` - Type string `json:"type,omitempty"` + ID string `json:"id,omitempty"` + Key string `json:"key"` + Value string `json:"value"` + Type string `json:"type,omitempty"` + ProjectID string `json:"projectId,omitempty"` } // ListResult contains paginated list results @@ -683,8 +684,8 @@ func (c *Client) TriggerWebhook(path, method string) ([]byte, error) { // --- Variables --- -// ListVariables returns all variables -func (c *Client) ListVariables(limit int, cursor string) ([]Variable, error) { +// ListVariables returns all variables, optionally filtered by project. +func (c *Client) ListVariables(limit int, cursor string, projectID string) ([]Variable, error) { params := url.Values{} if limit > 0 { params.Set("limit", strconv.Itoa(limit)) @@ -692,6 +693,9 @@ func (c *Client) ListVariables(limit int, cursor string) ([]Variable, error) { if cursor != "" { params.Set("cursor", cursor) } + if projectID != "" { + params.Set("projectId", projectID) + } path := "/variables" if len(params) > 0 { @@ -713,9 +717,12 @@ func (c *Client) ListVariables(limit int, cursor string) ([]Variable, error) { return resp.Data, nil } -// CreateVariable creates a new variable -func (c *Client) CreateVariable(key, value string) error { +// CreateVariable creates a new variable, optionally scoped to a project. +func (c *Client) CreateVariable(key, value, projectID string) error { body := map[string]string{"key": key, "value": value} + if projectID != "" { + body["projectId"] = projectID + } _, err := c.request(http.MethodPost, "/variables", body) return err } From 495e548d80aaf56704a04f45be726cbc35c34bec Mon Sep 17 00:00:00 2001 From: Florian Kinder Date: Thu, 16 Apr 2026 11:34:40 +0200 Subject: [PATCH 2/3] feat(variable): add --project flag for project-scoped variables --- internal/cmd/variable/variable.go | 58 +++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/internal/cmd/variable/variable.go b/internal/cmd/variable/variable.go index e4d26c9..a7884e5 100644 --- a/internal/cmd/variable/variable.go +++ b/internal/cmd/variable/variable.go @@ -44,6 +44,8 @@ func getClient() (*api.Client, error) { } func newListCmd() *cobra.Command { + var projectID string + cmd := &cobra.Command{ Use: "list", Short: "List all variables", @@ -53,7 +55,7 @@ func newListCmd() *cobra.Command { return err } - vars, err := client.ListVariables(0, "") + vars, err := client.ListVariables(0, "", projectID) if err != nil { return fmt.Errorf("failed to list variables: %w", err) } @@ -68,24 +70,48 @@ func newListCmd() *cobra.Command { return nil } - fmt.Printf("%-8s %-40s %s\n", "ID", "KEY", "VALUE") - fmt.Printf("%-8s %-40s %s\n", strings.Repeat("-", 8), strings.Repeat("-", 40), strings.Repeat("-", 50)) + hasProject := false for _, v := range vars { - value := v.Value - if len(value) > 50 { - value = value[:47] + "..." + if v.ProjectID != "" { + hasProject = true + break + } + } + + if hasProject { + fmt.Printf("%-8s %-18s %-40s %s\n", "ID", "PROJECT", "KEY", "VALUE") + fmt.Printf("%-8s %-18s %-40s %s\n", strings.Repeat("-", 8), strings.Repeat("-", 18), strings.Repeat("-", 40), strings.Repeat("-", 50)) + for _, v := range vars { + value := v.Value + if len(value) > 50 { + value = value[:47] + "..." + } + fmt.Printf("%-8s %-18s %-40s %s\n", v.ID, v.ProjectID, v.Key, value) + } + } else { + fmt.Printf("%-8s %-40s %s\n", "ID", "KEY", "VALUE") + fmt.Printf("%-8s %-40s %s\n", strings.Repeat("-", 8), strings.Repeat("-", 40), strings.Repeat("-", 50)) + for _, v := range vars { + value := v.Value + if len(value) > 50 { + value = value[:47] + "..." + } + fmt.Printf("%-8s %-40s %s\n", v.ID, v.Key, value) } - fmt.Printf("%-8s %-40s %s\n", v.ID, v.Key, value) } return nil }, } + cmd.Flags().StringVar(&projectID, "project", "", "Project ID (omit for global variables)") + return cmd } func newGetCmd() *cobra.Command { + var projectID string + cmd := &cobra.Command{ Use: "get ", Short: "Get a variable by key", @@ -96,7 +122,7 @@ func newGetCmd() *cobra.Command { return err } - vars, err := client.ListVariables(0, "") + vars, err := client.ListVariables(0, "", projectID) if err != nil { return fmt.Errorf("failed to list variables: %w", err) } @@ -117,11 +143,14 @@ func newGetCmd() *cobra.Command { }, } + cmd.Flags().StringVar(&projectID, "project", "", "Project ID (omit for global variables)") + return cmd } func newCreateCmd() *cobra.Command { var valueFlag string + var projectID string cmd := &cobra.Command{ Use: "create [value]", @@ -147,7 +176,7 @@ special shell characters (e.g. n8nctl var create key --value 'b!xyz').`, return fmt.Errorf("value is required: pass as second argument or use --value") } - if err := client.CreateVariable(key, value); err != nil { + if err := client.CreateVariable(key, value, projectID); err != nil { return fmt.Errorf("failed to create variable: %w", err) } @@ -157,12 +186,14 @@ special shell characters (e.g. n8nctl var create key --value 'b!xyz').`, } cmd.Flags().StringVar(&valueFlag, "value", "", "Variable value (alternative to positional argument)") + cmd.Flags().StringVar(&projectID, "project", "", "Project ID (omit for global variables)") return cmd } func newUpdateCmd() *cobra.Command { var valueFlag string + var projectID string cmd := &cobra.Command{ Use: "update [value]", @@ -189,7 +220,7 @@ special shell characters (e.g. n8nctl var update key --value 'b!xyz').`, } // Resolve key to ID - vars, err := client.ListVariables(0, "") + vars, err := client.ListVariables(0, "", projectID) if err != nil { return fmt.Errorf("failed to list variables: %w", err) } @@ -209,11 +240,14 @@ special shell characters (e.g. n8nctl var update key --value 'b!xyz').`, } cmd.Flags().StringVar(&valueFlag, "value", "", "Variable value (alternative to positional argument)") + cmd.Flags().StringVar(&projectID, "project", "", "Project ID (omit for global variables)") return cmd } func newDeleteCmd() *cobra.Command { + var projectID string + cmd := &cobra.Command{ Use: "delete ", Short: "Delete a variable by key", @@ -225,7 +259,7 @@ func newDeleteCmd() *cobra.Command { } // Resolve key to ID - vars, err := client.ListVariables(0, "") + vars, err := client.ListVariables(0, "", projectID) if err != nil { return fmt.Errorf("failed to list variables: %w", err) } @@ -245,6 +279,8 @@ func newDeleteCmd() *cobra.Command { }, } + cmd.Flags().StringVar(&projectID, "project", "", "Project ID (omit for global variables)") + return cmd } From 992b227f6b5ef2f2e62723818a4cbd83f1ce4f7b Mon Sep 17 00:00:00 2001 From: Florian Kinder Date: Thu, 16 Apr 2026 14:55:16 +0200 Subject: [PATCH 3/3] refactor(variable): use persistent flag and deduplicate table rendering Address PR review feedback: - Move --project flag to parent command as persistent flag - Deduplicate table rendering into single loop with conditional format --- internal/cmd/variable/variable.go | 53 +++++++++++++------------------ 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/internal/cmd/variable/variable.go b/internal/cmd/variable/variable.go index a7884e5..7a76f4a 100644 --- a/internal/cmd/variable/variable.go +++ b/internal/cmd/variable/variable.go @@ -20,6 +20,8 @@ func NewVariableCmd() *cobra.Command { Long: `List, create, update, and delete n8n environment variables.`, } + cmd.PersistentFlags().String("project", "", "Project ID (omit for global variables)") + cmd.AddCommand(newListCmd()) cmd.AddCommand(newGetCmd()) cmd.AddCommand(newCreateCmd()) @@ -44,8 +46,6 @@ func getClient() (*api.Client, error) { } func newListCmd() *cobra.Command { - var projectID string - cmd := &cobra.Command{ Use: "list", Short: "List all variables", @@ -55,6 +55,7 @@ func newListCmd() *cobra.Command { return err } + projectID, _ := cmd.Flags().GetString("project") vars, err := client.ListVariables(0, "", projectID) if err != nil { return fmt.Errorf("failed to list variables: %w", err) @@ -80,22 +81,22 @@ func newListCmd() *cobra.Command { if hasProject { fmt.Printf("%-8s %-18s %-40s %s\n", "ID", "PROJECT", "KEY", "VALUE") - fmt.Printf("%-8s %-18s %-40s %s\n", strings.Repeat("-", 8), strings.Repeat("-", 18), strings.Repeat("-", 40), strings.Repeat("-", 50)) - for _, v := range vars { - value := v.Value - if len(value) > 50 { - value = value[:47] + "..." - } - fmt.Printf("%-8s %-18s %-40s %s\n", v.ID, v.ProjectID, v.Key, value) - } + fmt.Printf("%-8s %-18s %-40s %s\n", + strings.Repeat("-", 8), strings.Repeat("-", 18), + strings.Repeat("-", 40), strings.Repeat("-", 50)) } else { fmt.Printf("%-8s %-40s %s\n", "ID", "KEY", "VALUE") fmt.Printf("%-8s %-40s %s\n", strings.Repeat("-", 8), strings.Repeat("-", 40), strings.Repeat("-", 50)) - for _, v := range vars { - value := v.Value - if len(value) > 50 { - value = value[:47] + "..." - } + } + + for _, v := range vars { + value := v.Value + if len(value) > 50 { + value = value[:47] + "..." + } + if hasProject { + fmt.Printf("%-8s %-18s %-40s %s\n", v.ID, v.ProjectID, v.Key, value) + } else { fmt.Printf("%-8s %-40s %s\n", v.ID, v.Key, value) } } @@ -104,14 +105,10 @@ func newListCmd() *cobra.Command { }, } - cmd.Flags().StringVar(&projectID, "project", "", "Project ID (omit for global variables)") - return cmd } func newGetCmd() *cobra.Command { - var projectID string - cmd := &cobra.Command{ Use: "get ", Short: "Get a variable by key", @@ -122,6 +119,7 @@ func newGetCmd() *cobra.Command { return err } + projectID, _ := cmd.Flags().GetString("project") vars, err := client.ListVariables(0, "", projectID) if err != nil { return fmt.Errorf("failed to list variables: %w", err) @@ -143,14 +141,11 @@ func newGetCmd() *cobra.Command { }, } - cmd.Flags().StringVar(&projectID, "project", "", "Project ID (omit for global variables)") - return cmd } func newCreateCmd() *cobra.Command { var valueFlag string - var projectID string cmd := &cobra.Command{ Use: "create [value]", @@ -176,6 +171,7 @@ special shell characters (e.g. n8nctl var create key --value 'b!xyz').`, return fmt.Errorf("value is required: pass as second argument or use --value") } + projectID, _ := cmd.Flags().GetString("project") if err := client.CreateVariable(key, value, projectID); err != nil { return fmt.Errorf("failed to create variable: %w", err) } @@ -186,14 +182,12 @@ special shell characters (e.g. n8nctl var create key --value 'b!xyz').`, } cmd.Flags().StringVar(&valueFlag, "value", "", "Variable value (alternative to positional argument)") - cmd.Flags().StringVar(&projectID, "project", "", "Project ID (omit for global variables)") return cmd } func newUpdateCmd() *cobra.Command { var valueFlag string - var projectID string cmd := &cobra.Command{ Use: "update [value]", @@ -219,7 +213,8 @@ special shell characters (e.g. n8nctl var update key --value 'b!xyz').`, return fmt.Errorf("value is required: pass as second argument or use --value") } - // Resolve key to ID + // Resolve key to ID within the given project scope. + projectID, _ := cmd.Flags().GetString("project") vars, err := client.ListVariables(0, "", projectID) if err != nil { return fmt.Errorf("failed to list variables: %w", err) @@ -240,14 +235,11 @@ special shell characters (e.g. n8nctl var update key --value 'b!xyz').`, } cmd.Flags().StringVar(&valueFlag, "value", "", "Variable value (alternative to positional argument)") - cmd.Flags().StringVar(&projectID, "project", "", "Project ID (omit for global variables)") return cmd } func newDeleteCmd() *cobra.Command { - var projectID string - cmd := &cobra.Command{ Use: "delete ", Short: "Delete a variable by key", @@ -258,7 +250,8 @@ func newDeleteCmd() *cobra.Command { return err } - // Resolve key to ID + // Resolve key to ID within the given project scope. + projectID, _ := cmd.Flags().GetString("project") vars, err := client.ListVariables(0, "", projectID) if err != nil { return fmt.Errorf("failed to list variables: %w", err) @@ -279,8 +272,6 @@ func newDeleteCmd() *cobra.Command { }, } - cmd.Flags().StringVar(&projectID, "project", "", "Project ID (omit for global variables)") - return cmd }