From 4e19a2cafde2d6e8e703063b285bd9384135d65c Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Tue, 29 Sep 2026 21:15:24 -0400 Subject: [PATCH 01/52] fix(role): align role commands with the v3 Roles API - role get: look up by --id or --name via GET /roles?q=roleId==/roleName== (no /roles/{id} endpoint exists); --privileges adds expand=privileges - Role.Privileges is now []RolePrivilege (API returns objects) - role get: interactive role picker when --id/--name omitted, optional "Display role privileges? [y/Q]" prompt in table output, privileges sorted by service and name, status color-coded, CSV output support - role create: --name/--description/--privilege (names or IDs resolved to IDs) and --from-file using the API request shape - replace role update (non-existent endpoint) with add-privileges and remove-privileges (PUT /roles/{id|name/}/add|removePrivileges) - role delete: support --name - docs, README, completions; bug record in docs/issues/pending --- README.md | 2 +- cmd/role.go | 274 ++++++++++++++---- cmd/role_prompt.go | 100 +++++++ completions/iics.bash | 92 +++++- .../comparison-informatica-cli.md | 2 +- docs/documentation/role.md | 222 +++++++++++--- ...026-09-29-role-get-unsupported-endpoint.md | 106 +++++++ internal/client/roles.go | 168 +++++++++-- internal/client/roles_test.go | 213 ++++++++++++++ 9 files changed, 1039 insertions(+), 140 deletions(-) create mode 100644 cmd/role_prompt.go create mode 100644 docs/issues/pending/2026-09-29-role-get-unsupported-endpoint.md create mode 100644 internal/client/roles_test.go diff --git a/README.md b/README.md index 59d8879..7aa0da6 100644 --- a/README.md +++ b/README.md @@ -269,7 +269,7 @@ it easy to override credentials in CI pipelines without touching the config file | [project](docs/documentation/project.md) | | `create`, `update`, `delete` | Manage projects | | [publish](docs/documentation/publish.md) | | `start`, `status`, `run` | Publish CAI assets to the runtime | | [release](docs/documentation/release.md) | | `manifest`, `validate`, `plan` | Generate multi-format release manifests and per-environment plans | -| [role](docs/documentation/role.md) | | `list`, `get`, `create`, `update`, `delete` | Manage roles | +| [role](docs/documentation/role.md) | | `list`, `get`, `create`, `add-privileges`, `remove-privileges`, `delete` | Manage roles | | [environment](docs/documentation/environment.md) | `runtime`, `rt`, `env` | `list`, `get`, `create`, `update`, `delete`, `configs` | Manage runtime environments (Secure Agent groups) | | [schedule](docs/documentation/schedule.md) | | `list`, `get`, `create`, `update`, `delete` | Manage schedules | | [securitylog](docs/documentation/securitylog.md) | | `list` | Query security audit log | diff --git a/cmd/role.go b/cmd/role.go index bc66e77..35e0c65 100644 --- a/cmd/role.go +++ b/cmd/role.go @@ -4,7 +4,10 @@ import ( "context" "encoding/json" "fmt" + "io" "os" + "strconv" + "strings" "github.com/jbrazda/iics-cli/internal/client" "github.com/jbrazda/iics-cli/internal/output" @@ -19,7 +22,8 @@ func newRoleCmd() *cobra.Command { cmd.AddCommand(newRoleListCmd()) cmd.AddCommand(newRoleGetCmd()) cmd.AddCommand(newRoleCreateCmd()) - cmd.AddCommand(newRoleUpdateCmd()) + cmd.AddCommand(newRolePrivilegesCmd("add-privileges", "Add privileges to a custom role", "Added")) + cmd.AddCommand(newRolePrivilegesCmd("remove-privileges", "Remove privileges from a custom role", "Removed")) cmd.AddCommand(newRoleDeleteCmd()) return cmd } @@ -57,62 +61,180 @@ func newRoleListCmd() *cobra.Command { } func newRoleGetCmd() *cobra.Command { - var id string + var opts client.RoleGetOptions cmd := &cobra.Command{ Use: "get", - Short: "Get role details", + Short: "Get role details (prompts for a role when --id and --name are omitted)", + Example: ` iics role get --id + iics role get --name "Business Manager" + iics role get --name "Business Manager" --privileges + iics role get --privileges # pick a role interactively`, RunE: func(cmd *cobra.Command, args []string) error { - if id == "" { - return fmt.Errorf("--id is required") - } c, err := getClient(cmd) if err != nil { return err } - role, err := c.GetRole(context.Background(), id) + ctx := context.Background() + if opts.ID == "" && opts.Name == "" { + picked, perr := pickRole(ctx, c, "view") + if perr != nil { + return perr + } + opts.ID = picked.ID + } + role, err := c.GetRole(ctx, opts) if err != nil { return err } - f, err := getFormatter() - if err != nil { + sortRolePrivileges(role.Privileges) + if outputFmt == "csv" { + f, ferr := getFormatter() + if ferr != nil { + return ferr + } + if opts.ExpandPrivileges { + // Plain status column: CSV keeps ANSI codes on a TTY. + cols := append([]output.Column(nil), rolePrivilegeColumns...) + cols[2].Func = nil + return f.Format(role.Privileges, cols) + } + return f.Format(role, roleDetailColumns) + } + if outputFmt != "" && outputFmt != "table" { + f, ferr := getFormatter() + if ferr != nil { + return ferr + } + return f.Format(role, nil) + } + w := cmd.OutOrStdout() + if err = printRoleDetails(w, role); err != nil { return err } - columns := []output.Column{ - {Header: "ID", Field: "id", Width: 24}, - {Header: "NAME", Field: "roleName", Width: 30}, - {Header: "SYSTEM", Field: "systemRole", Width: 8}, - {Header: "DESCRIPTION", Field: "description"}, + if !opts.ExpandPrivileges && !cmd.Flags().Changed("privileges") && isInteractiveTTY() { + if !promptShowPrivileges() { + return nil + } + opts.ID, opts.Name, opts.ExpandPrivileges = role.ID, "", true + if role, err = c.GetRole(ctx, opts); err != nil { + return err + } + sortRolePrivileges(role.Privileges) } - return f.Format(role, columns) + if !opts.ExpandPrivileges { + return nil + } + return printRolePrivileges(w, role) }, } - cmd.Flags().StringVar(&id, "id", "", "role ID (required)") + cmd.Flags().StringVar(&opts.ID, "id", "", "role ID (omit with --name to pick interactively)") + cmd.Flags().StringVar(&opts.Name, "name", "", "role name") + cmd.Flags().BoolVar(&opts.ExpandPrivileges, "privileges", false, "include privileges assigned to the role (table output on a terminal prompts when omitted)") + cmd.MarkFlagsMutuallyExclusive("id", "name") return cmd } +// printRoleDetails renders a role as a vertical detail table. +func printRoleDetails(w io.Writer, r *client.Role) error { + cfg, _ := loadConfig() + tf := output.New(output.FormatTable, w, resolveTableStyle(cfg)) + + _, _ = fmt.Fprintf(w, "Role: %s\n\n", r.RoleName) + rows := []output.KVRow{ + output.KV("id", r.ID), + output.KV("orgId", r.OrgID), + output.KV("displayName", r.DisplayName), + output.KV("description", r.Description), + output.KV("systemRole", strconv.FormatBool(r.SystemRole)), + output.KV("status", r.Status), + output.KV("createdBy", r.CreatedBy), + output.KV("createTime", r.CreateTime), + output.KV("updatedBy", r.UpdatedBy), + output.KV("updateTime", r.UpdateTime), + } + return tf.Format(rows, output.KVCols) +} + +// printRolePrivileges renders the privileges assigned to a role. +func printRolePrivileges(w io.Writer, r *client.Role) error { + cfg, _ := loadConfig() + tf := output.New(output.FormatTable, w, resolveTableStyle(cfg)) + + _, _ = fmt.Fprintf(w, "\nPrivileges (%d):\n", len(r.Privileges)) + if len(r.Privileges) == 0 { + return nil + } + return tf.Format(r.Privileges, rolePrivilegeColumns) +} + +var rolePrivilegeColumns = []output.Column{ + {Header: "NAME", Field: "name", Width: 36}, + {Header: "SERVICE", Field: "service", Width: 24}, + {Header: "STATUS", Field: "status", Width: 10, Func: privilegeStatusFunc}, + {Header: "ID", Field: "id", Width: 24}, + {Header: "DESCRIPTION", Field: "description"}, +} + +var roleDetailColumns = []output.Column{ + {Header: "ID", Field: "id"}, + {Header: "NAME", Field: "roleName"}, + {Header: "DISPLAY NAME", Field: "displayName"}, + {Header: "SYSTEM", Field: "systemRole"}, + {Header: "STATUS", Field: "status"}, + {Header: "DESCRIPTION", Field: "description"}, + {Header: "UPDATED BY", Field: "updatedBy"}, + {Header: "UPDATED", Field: "updateTime"}, +} + func newRoleCreateCmd() *cobra.Command { - var fromFile string + var ( + fromFile string + req client.CreateRoleRequest + privileges []string + ) cmd := &cobra.Command{ Use: "create", - Short: "Create a role", + Short: "Create a custom role", + Example: ` iics role create --name "CAI Viewer" --description "View CAI assets" \ + --privilege view.ai.designer --privilege view.ai.assets + iics role create --from-file cai-viewer-role.json`, RunE: func(cmd *cobra.Command, args []string) error { - if fromFile == "" { - return fmt.Errorf("--from-file is required") + var body client.CreateRoleRequest + if fromFile != "" { + data, err := os.ReadFile(fromFile) + if err != nil { + return fmt.Errorf("reading file: %w", err) + } + if err := json.Unmarshal(data, &body); err != nil { + return fmt.Errorf("parsing JSON: %w", err) + } } - data, err := os.ReadFile(fromFile) - if err != nil { - return fmt.Errorf("reading file: %w", err) + if req.Name != "" { + body.Name = req.Name } - var role client.Role - err = json.Unmarshal(data, &role) - if err != nil { - return fmt.Errorf("parsing JSON: %w", err) + if req.Description != "" { + body.Description = req.Description + } + refs := append(body.Privileges, privileges...) + if body.Name == "" { + return fmt.Errorf("--name is required") + } + if len(refs) == 0 { + return fmt.Errorf("at least one --privilege is required") } c, err := getClient(cmd) if err != nil { return err } - created, err := c.CreateRole(context.Background(), &role) + resolved, err := c.ResolvePrivileges(context.Background(), refs) + if err != nil { + return err + } + body.Privileges = make([]string, len(resolved)) + for i, p := range resolved { + body.Privileges[i] = p.ID + } + created, err := c.CreateRole(context.Background(), &body) if err != nil { return err } @@ -120,65 +242,95 @@ func newRoleCreateCmd() *cobra.Command { return nil }, } - cmd.Flags().StringVar(&fromFile, "from-file", "", "JSON file with role definition (required)") + cmd.Flags().StringVar(&req.Name, "name", "", "role name (required unless set in --from-file)") + cmd.Flags().StringVar(&req.Description, "description", "", "role description") + cmd.Flags().StringSliceVar(&privileges, "privilege", nil, "privilege name or ID to assign (repeatable or comma-separated)") + cmd.Flags().StringVar(&fromFile, "from-file", "", "JSON file with role definition (name, description, privileges)") return cmd } -func newRoleUpdateCmd() *cobra.Command { +// newRolePrivilegesCmd builds add-privileges / remove-privileges. The API has +// no general role update endpoint; privileges are the only mutable part. +func newRolePrivilegesCmd(use, short, verb string) *cobra.Command { var ( - id string - fromFile string + ref client.RoleRef + privileges []string ) cmd := &cobra.Command{ - Use: "update", - Short: "Update a role", + Use: use, + Short: short, + Example: fmt.Sprintf(` iics role %[1]s --name "CAI Viewer" --privilege view.ai.console + iics role %[1]s --id --privilege create.data.transfer.task,delete.data.transfer.task`, use), RunE: func(cmd *cobra.Command, args []string) error { - if id == "" { - return fmt.Errorf("--id is required") + if ref.ID == "" && ref.Name == "" { + return fmt.Errorf("--id or --name is required") } - if fromFile == "" { - return fmt.Errorf("--from-file is required") + if len(privileges) == 0 { + return fmt.Errorf("at least one --privilege is required") } - data, err := os.ReadFile(fromFile) - if err != nil { - return fmt.Errorf("reading file: %w", err) - } - var role client.Role - err = json.Unmarshal(data, &role) + c, err := getClient(cmd) if err != nil { - return fmt.Errorf("parsing JSON: %w", err) + return err } - c, err := getClient(cmd) + resolved, err := c.ResolvePrivileges(context.Background(), privileges) if err != nil { return err } - updated, err := c.UpdateRole(context.Background(), id, &role) + names := make([]string, len(resolved)) + for i, p := range resolved { + names[i] = p.Name + } + if use == "add-privileges" { + err = c.AddRolePrivileges(context.Background(), ref, names) + } else { + err = c.RemoveRolePrivileges(context.Background(), ref, names) + } if err != nil { return err } - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Role updated: %s (ID: %s)\n", updated.RoleName, updated.ID) + target := ref.Name + if target == "" { + target = ref.ID + } + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s %d privilege(s) for role %s: %s\n", verb, len(names), target, strings.Join(names, ", ")) return nil }, } - cmd.Flags().StringVar(&id, "id", "", "role ID (required)") - cmd.Flags().StringVar(&fromFile, "from-file", "", "JSON file with role updates (required)") + cmd.Flags().StringVar(&ref.ID, "id", "", "role ID") + cmd.Flags().StringVar(&ref.Name, "name", "", "role name") + cmd.Flags().StringSliceVar(&privileges, "privilege", nil, "privilege name or ID (repeatable or comma-separated)") + cmd.MarkFlagsMutuallyExclusive("id", "name") return cmd } func newRoleDeleteCmd() *cobra.Command { var ( - id string - yes bool + id string + name string + yes bool ) cmd := &cobra.Command{ Use: "delete", - Short: "Delete a role", + Short: "Delete a custom role", RunE: func(cmd *cobra.Command, args []string) error { - if id == "" { - return fmt.Errorf("--id is required") + if id == "" && name == "" { + return fmt.Errorf("--id or --name is required") + } + c, err := getClient(cmd) + if err != nil { + return err + } + label := id + if name != "" { + role, err := c.GetRole(context.Background(), client.RoleGetOptions{Name: name}) + if err != nil { + return err + } + id = role.ID + label = fmt.Sprintf("%s (ID: %s)", role.RoleName, role.ID) } if !yes { - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Are you sure you want to delete role %s? [y/N]: ", id) + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Are you sure you want to delete role %s? [y/N]: ", label) var confirm string _, _ = fmt.Scanln(&confirm) if confirm != "y" && confirm != "Y" { @@ -186,18 +338,16 @@ func newRoleDeleteCmd() *cobra.Command { return nil } } - c, err := getClient(cmd) - if err != nil { - return err - } if err := c.DeleteRole(context.Background(), id); err != nil { return err } - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Role deleted: %s\n", id) + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Role deleted: %s\n", label) return nil }, } - cmd.Flags().StringVar(&id, "id", "", "role ID (required)") + cmd.Flags().StringVar(&id, "id", "", "role ID") + cmd.Flags().StringVar(&name, "name", "", "role name") cmd.Flags().BoolVarP(&yes, "yes", "y", false, "skip confirmation prompt") + cmd.MarkFlagsMutuallyExclusive("id", "name") return cmd } diff --git a/cmd/role_prompt.go b/cmd/role_prompt.go new file mode 100644 index 0000000..164a6eb --- /dev/null +++ b/cmd/role_prompt.go @@ -0,0 +1,100 @@ +package cmd + +import ( + "bufio" + "context" + "fmt" + "os" + "sort" + "strings" + + "github.com/charmbracelet/lipgloss" + "github.com/jbrazda/iics-cli/internal/client" + "github.com/jbrazda/iics-cli/internal/config" + "golang.org/x/term" +) + +// isInteractiveTTY reports whether both stdin and stdout are terminals, so a +// prompt neither blocks a script nor ends up in piped output. +func isInteractiveTTY() bool { + return config.IsTerminal() && term.IsTerminal(int(os.Stdout.Fd())) +} + +// promptShowPrivileges asks whether to display the role's privileges. Only +// "y" or "yes" answers yes; anything else (including q or Enter) quits. +func promptShowPrivileges() bool { + _, _ = fmt.Fprint(os.Stderr, "\nDisplay role privileges? [y/Q]: ") + // A read error (e.g. Ctrl-D) leaves line empty, which is treated as quit. + line, _ := bufio.NewReader(os.Stdin).ReadString('\n') + line = strings.ToLower(strings.TrimSpace(line)) + return line == "y" || line == "yes" +} + +// pickRole lists the roles and lets the operator choose one. The roles API +// documents no paging parameters, so a single unpaged request returns them all. +func pickRole(ctx context.Context, c *client.Client, action string) (*client.Role, error) { + if !config.IsTerminal() { + return nil, fmt.Errorf("provide --id or --name (stdin is not a terminal)") + } + roles, err := c.ListRoles(ctx, client.RoleListOptions{}) + if err != nil { + return nil, err + } + if len(roles) == 0 { + return nil, fmt.Errorf("no roles found") + } + sort.SliceStable(roles, func(i, j int) bool { + return strings.ToLower(roles[i].RoleName) < strings.ToLower(roles[j].RoleName) + }) + labels := make([]string, len(roles)) + for i, r := range roles { + kind := "custom" + if r.SystemRole { + kind = "system" + } + labels[i] = fmt.Sprintf("%s (%s)", r.RoleName, kind) + } + idx, err := promptSelect(fmt.Sprintf("Select a role to %s", action), labels) + if err != nil { + return nil, err + } + if idx < 0 { + return nil, fmt.Errorf("canceled") + } + return &roles[idx], nil +} + +// sortRolePrivileges orders privileges by service, then privilege name. +func sortRolePrivileges(privs []client.RolePrivilege) { + sort.SliceStable(privs, func(i, j int) bool { + si, sj := strings.ToLower(privs[i].Service), strings.ToLower(privs[j].Service) + if si != sj { + return si < sj + } + return strings.ToLower(privs[i].Name) < strings.ToLower(privs[j].Name) + }) +} + +// privilegeStatusFunc colors a privilege status: green Enabled, orange +// Unassigned, red Disabled, blue Default. +func privilegeStatusFunc(v interface{}) string { + row, _ := v.(map[string]interface{}) + status, _ := row["status"].(string) + if noColor { + return status + } + var color string + switch status { + case "Enabled": + color = "2" + case "Unassigned": + color = "208" + case "Disabled": + color = "1" + case "Default": + color = "4" + default: + return status + } + return lipgloss.NewStyle().Foreground(lipgloss.Color(color)).Render(status) +} diff --git a/completions/iics.bash b/completions/iics.bash index 7e56f20..5a2cfbe 100644 --- a/completions/iics.bash +++ b/completions/iics.bash @@ -5260,6 +5260,57 @@ _iics_release() noun_aliases=() } +_iics_role_add-privileges() +{ + last_command="iics_role_add-privileges" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--id=") + two_word_flags+=("--id") + local_nonpersistent_flags+=("--id") + local_nonpersistent_flags+=("--id=") + flags+=("--name=") + two_word_flags+=("--name") + local_nonpersistent_flags+=("--name") + local_nonpersistent_flags+=("--name=") + flags+=("--privilege=") + two_word_flags+=("--privilege") + local_nonpersistent_flags+=("--privilege") + local_nonpersistent_flags+=("--privilege=") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--http-timeout=") + two_word_flags+=("--http-timeout") + flags+=("--no-color") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + flags+=("--profile=") + two_word_flags+=("--profile") + two_word_flags+=("-p") + flags+=("--theme=") + two_word_flags+=("--theme") + flags+=("--verbose") + flags+=("-v") + flags+=("--wide") + flags+=("--width=") + two_word_flags+=("--width") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _iics_role_create() { last_command="iics_role_create" @@ -5274,10 +5325,22 @@ _iics_role_create() flags_with_completion=() flags_completion=() + flags+=("--description=") + two_word_flags+=("--description") + local_nonpersistent_flags+=("--description") + local_nonpersistent_flags+=("--description=") flags+=("--from-file=") two_word_flags+=("--from-file") local_nonpersistent_flags+=("--from-file") local_nonpersistent_flags+=("--from-file=") + flags+=("--name=") + two_word_flags+=("--name") + local_nonpersistent_flags+=("--name") + local_nonpersistent_flags+=("--name=") + flags+=("--privilege=") + two_word_flags+=("--privilege") + local_nonpersistent_flags+=("--privilege") + local_nonpersistent_flags+=("--privilege=") flags+=("--config=") two_word_flags+=("--config") flags+=("--debug") @@ -5321,6 +5384,10 @@ _iics_role_delete() two_word_flags+=("--id") local_nonpersistent_flags+=("--id") local_nonpersistent_flags+=("--id=") + flags+=("--name=") + two_word_flags+=("--name") + local_nonpersistent_flags+=("--name") + local_nonpersistent_flags+=("--name=") flags+=("--yes") flags+=("-y") local_nonpersistent_flags+=("--yes") @@ -5368,6 +5435,12 @@ _iics_role_get() two_word_flags+=("--id") local_nonpersistent_flags+=("--id") local_nonpersistent_flags+=("--id=") + flags+=("--name=") + two_word_flags+=("--name") + local_nonpersistent_flags+=("--name") + local_nonpersistent_flags+=("--name=") + flags+=("--privileges") + local_nonpersistent_flags+=("--privileges") flags+=("--config=") two_word_flags+=("--config") flags+=("--debug") @@ -5440,9 +5513,9 @@ _iics_role_list() noun_aliases=() } -_iics_role_update() +_iics_role_remove-privileges() { - last_command="iics_role_update" + last_command="iics_role_remove-privileges" command_aliases=() @@ -5454,14 +5527,18 @@ _iics_role_update() flags_with_completion=() flags_completion=() - flags+=("--from-file=") - two_word_flags+=("--from-file") - local_nonpersistent_flags+=("--from-file") - local_nonpersistent_flags+=("--from-file=") flags+=("--id=") two_word_flags+=("--id") local_nonpersistent_flags+=("--id") local_nonpersistent_flags+=("--id=") + flags+=("--name=") + two_word_flags+=("--name") + local_nonpersistent_flags+=("--name") + local_nonpersistent_flags+=("--name=") + flags+=("--privilege=") + two_word_flags+=("--privilege") + local_nonpersistent_flags+=("--privilege") + local_nonpersistent_flags+=("--privilege=") flags+=("--config=") two_word_flags+=("--config") flags+=("--debug") @@ -5494,11 +5571,12 @@ _iics_role() command_aliases=() commands=() + commands+=("add-privileges") commands+=("create") commands+=("delete") commands+=("get") commands+=("list") - commands+=("update") + commands+=("remove-privileges") flags=() two_word_flags=() diff --git a/docs/documentation/comparison-informatica-cli.md b/docs/documentation/comparison-informatica-cli.md index b3b0aa9..57851a4 100644 --- a/docs/documentation/comparison-informatica-cli.md +++ b/docs/documentation/comparison-informatica-cli.md @@ -181,7 +181,7 @@ It cannot manage users, connections, schedules, roles, or any other administrati | `folder` | create, update, delete | | `user` | list, get, create, update, delete | | `usergroup` | list, get, create, update, delete | -| `role` | list, get, create, update, delete | +| `role` | list, get, create, add-privileges, remove-privileges, delete | | `privilege` | list | | `runtime` | list, get, create, update | | `agent` | list, start, stop | diff --git a/docs/documentation/role.md b/docs/documentation/role.md index 70ea287..cd8e03a 100644 --- a/docs/documentation/role.md +++ b/docs/documentation/role.md @@ -10,13 +10,17 @@ iics role [flags] ## Subcommands -| Subcommand | Description | -| ---------- | ------------------ | -| `list` | List roles | -| `get` | Get a single role | -| `create` | Create a role | -| `update` | Update a role | -| `delete` | Delete a role | +| Subcommand | Description | +| ------------------- | ------------------------------------ | +| `list` | List roles | +| `get` | Get a single role by ID or name, or pick one interactively | +| `create` | Create a custom role | +| `add-privileges` | Add privileges to a custom role | +| `remove-privileges` | Remove privileges from a custom role | +| `delete` | Delete a custom role | + +The IICS v3 Roles API has no general update endpoint. The only change you can +make to an existing custom role is adding or removing privileges. --- @@ -67,104 +71,239 @@ $roles | Where-Object { $_.systemRole -eq $false } ## role get +Get the details of a single role, looked up by ID or by name. The IICS v3 API +has no `/roles/{id}` endpoint, so the command queries +`GET /public/core/v3/roles?q=roleId==""` or `q=roleName==""`. +With `--privileges`, `expand=privileges` is added to the request and the +privileges assigned to the role are included in the output. + +When neither `--id` nor `--name` is given and stdin is a terminal, the command +lists all roles alphabetically and prompts you to pick one. Without a terminal +(for example in CI), `--id` or `--name` is required. + +In `table` output on a terminal, when `--privileges` is not given, the command +prints the role details and then asks `Display role privileges? [y/Q]`. +Answering `y` fetches and prints the privileges; any other answer (`q`, +Enter) quits. No prompt is shown when `--privileges` is set explicitly +(including `--privileges=false`), for other output formats, or when stdin or +stdout is not a terminal. + ### Flags -| Flag | Type | Required | Description | -| ------ | ------ | -------- | ----------- | -| `--id` | string | yes | Role ID | +| Flag | Type | Required | Description | +| -------------- | ------ | -------- | ----------------------------------------- | +| `--id` | string | | Role ID | +| `--name` | string | | Role name (for example `Business Manager`) | +| `--privileges` | bool | | Include privileges assigned to the role; prompts when omitted (table output on a terminal) | + +`--id` and `--name` are mutually exclusive. If both are omitted, you are +prompted to select a role. All [global flags](../../README.md#global-flags) apply. +### Output + +In `table` format the role is shown as a vertical detail table (`id`, `orgId`, +`displayName`, `description`, `systemRole`, `status`, `createdBy`, +`createTime`, `updatedBy`, `updateTime`). With `--privileges`, a second table +lists the privileges, sorted by service and then privilege name: + +| Column | Description | +| ------------- | ------------------------------------------------ | +| `NAME` | Privilege name | +| `SERVICE` | IDMC service that uses the privilege | +| `STATUS` | Privilege status (color-coded, see below) | +| `ID` | Privilege ID | +| `DESCRIPTION` | Privilege description | + +On colored themes the `STATUS` column is color-coded: + +| Status | Color | +| ------------ | ------ | +| `Enabled` | Green | +| `Unassigned` | Orange | +| `Disabled` | Red | +| `Default` | Blue | + +Colors are omitted with `--no-color`, `NO_COLOR`, the `plain`, `markdown` and +`gh` themes, and when output is not a terminal. + +`json` and `yaml` formats return the full role object, including the +`privileges` array (in the same sort order) when `--privileges` is set. + +`csv` output returns one row per privilege (`NAME`, `SERVICE`, `STATUS`, `ID`, +`DESCRIPTION`) when `--privileges` is set, otherwise a single role row (`ID`, +`NAME`, `DISPLAY NAME`, `SYSTEM`, `STATUS`, `DESCRIPTION`, `UPDATED BY`, +`UPDATED`). + ### Examples ```bash +# Pick a role from a list +iics role get --privileges + iics role get --id -# JSON to see all role fields including privileges -iics role get --id --output json +iics role get --name "Business Manager" + +# Include privileges assigned to the role +iics role get --name "Business Manager" --privileges + +# Privilege names only +iics role get --name "Business Manager" --privileges --output json | jq -r '.privileges[].name' ``` ```powershell +# Pick a role from a list +iics role get --privileges + iics role get --id -# JSON to see all role fields including privileges -iics role get --id --output json +iics role get --name "Business Manager" + +# Include privileges assigned to the role +iics role get --name "Business Manager" --privileges + +# Privilege names only +(iics role get --name "Business Manager" --privileges --output json | ConvertFrom-Json).privileges.name ``` --- ## role create -Create a role from a JSON definition file. +Create a custom role. The API requires a name and at least one privilege. + +Privileges can be given as names (for example `view.ai.assets`) or IDs. The +command looks them up with `iics privilege list` data and sends privilege IDs, +as the create endpoint requires. Unknown privileges are reported before the +role is created. ### Flags -| Flag | Type | Required | Description | -| ------------- | ------ | -------- | ------------------------------ | -| `--from-file` | string | yes | JSON file with role definition | +| Flag | Type | Required | Description | +| --------------- | -------- | -------- | ------------------------------------------------------------ | +| `--name` | string | yes* | Role name | +| `--description` | string | | Role description | +| `--privilege` | string[] | yes* | Privilege name or ID; repeatable or comma-separated | +| `--from-file` | string | | JSON file with `name`, `description` and `privileges` | + +\* Can be supplied through `--from-file` instead. Flags override `name` and +`description` from the file; `--privilege` values are added to the file's +`privileges`. All [global flags](../../README.md#global-flags) apply. ### JSON definition example +The file uses the same shape as the API request body. `privileges` may hold +privilege names or IDs. + ```json { - "name": "ETL Developer", - "description": "Can create and run mappings", - "privileges": [ - { "id": "", "name": "dataIntegration.mapping.create" }, - { "id": "", "name": "dataIntegration.mapping.run" } - ] + "name": "CAIviewer", + "description": "A role to view Application Integration designer and assets", + "privileges": ["view.ai.designer", "view.ai.assets"] } ``` -Use `iics privilege list` to find available privilege IDs. +### Examples + +```bash +iics role create --name "CAI Viewer" --description "View CAI assets" \ + --privilege view.ai.designer --privilege view.ai.assets + +iics role create --from-file cai-viewer-role.json +``` + +```powershell +iics role create --name "CAI Viewer" --description "View CAI assets" ` + --privilege view.ai.designer --privilege view.ai.assets + +iics role create --from-file cai-viewer-role.json +``` + +--- + +## role add-privileges + +Add privileges to a custom role. Sends +`PUT /public/core/v3/roles//addPrivileges`, or +`PUT /public/core/v3/roles/name//addPrivileges` when `--name` is used. + +### Flags + +| Flag | Type | Required | Description | +| ------------- | -------- | -------- | --------------------------------------------------- | +| `--id` | string | one of | Role ID | +| `--name` | string | one of | Role name | +| `--privilege` | string[] | yes | Privilege name or ID; repeatable or comma-separated | + +`--id` and `--name` are mutually exclusive. Privilege IDs are converted to +names, which is what the endpoint expects. + +All [global flags](../../README.md#global-flags) apply. ### Examples ```bash -iics role create --from-file etl-developer-role.json +iics role add-privileges --name "CAI Viewer" --privilege view.ai.console + +iics role add-privileges --id \ + --privilege create.data.transfer.task,delete.data.transfer.task ``` ```powershell -iics role create --from-file etl-developer-role.json +iics role add-privileges --name "CAI Viewer" --privilege view.ai.console + +iics role add-privileges --id ` + --privilege create.data.transfer.task,delete.data.transfer.task ``` --- -## role update +## role remove-privileges + +Remove privileges from a custom role. Sends +`PUT /public/core/v3/roles//removePrivileges`, or +`PUT /public/core/v3/roles/name//removePrivileges` when `--name` is +used. A role must keep at least one privilege. ### Flags -| Flag | Type | Required | Description | -| ------------- | ------ | -------- | ------------------------------- | -| `--id` | string | yes | Role ID | -| `--from-file` | string | yes | JSON file with updated fields | +| Flag | Type | Required | Description | +| ------------- | -------- | -------- | --------------------------------------------------- | +| `--id` | string | one of | Role ID | +| `--name` | string | one of | Role name | +| `--privilege` | string[] | yes | Privilege name or ID; repeatable or comma-separated | All [global flags](../../README.md#global-flags) apply. ### Examples ```bash -iics role update --id --from-file updated-role.json +iics role remove-privileges --name "CAI Viewer" --privilege view.ai.console ``` ```powershell -iics role update --id --from-file updated-role.json +iics role remove-privileges --name "CAI Viewer" --privilege view.ai.console ``` --- ## role delete -Delete a role. Prompts for confirmation unless `--yes` is given. +Delete a custom role. Sends `DELETE /public/core/v3/roles/`. With +`--name`, the role ID is looked up first. Prompts for confirmation unless +`--yes` is given. ### Flags -| Flag | Short | Type | Required | Description | -| ------- | ----- | ------ | -------- | ------------------------ | -| `--id` | | string | yes | Role ID | -| `--yes` | `-y` | bool | | Skip confirmation prompt | +| Flag | Short | Type | Required | Description | +| -------- | ----- | ------ | -------- | ------------------------ | +| `--id` | | string | one of | Role ID | +| `--name` | | string | one of | Role name | +| `--yes` | `-y` | bool | | Skip confirmation prompt | All [global flags](../../README.md#global-flags) apply. @@ -173,14 +312,13 @@ All [global flags](../../README.md#global-flags) apply. ```bash iics role delete --id -iics role delete --id --yes +iics role delete --name "CAI Viewer" --yes ``` ```powershell iics role delete --id -iics role delete --id --yes -``` +iics role delete --name "CAI Viewer" --yes ``` ## See also diff --git a/docs/issues/pending/2026-09-29-role-get-unsupported-endpoint.md b/docs/issues/pending/2026-09-29-role-get-unsupported-endpoint.md new file mode 100644 index 0000000..ec01360 --- /dev/null +++ b/docs/issues/pending/2026-09-29-role-get-unsupported-endpoint.md @@ -0,0 +1,106 @@ +# BUG: role commands do not match the v3 Roles API + +--- + +## Symptoms + +`iics role get --id ` calls `GET /public/core/v3/roles/{id}`, which is not part of the +IICS v3 Roles API. The command cannot look up a role by name and cannot return the privileges +assigned to a role. + +Follow-up review of the other role commands against the +[Roles](https://docs.informatica.com/cloud-common-services/administrator/current-version/rest-api-reference/platform-rest-api-version-3-resources/roles.html) +documentation found: + +- `role create` sent the `Role` struct (`roleName`, privilege objects), but the API expects + `name`, `description` and `privileges` as an array of privilege IDs. +- `role update` sent `PUT /roles/{id}`, which does not exist. The API only supports + `PUT /roles/{id}/addPrivileges` and `PUT /roles/{id}/removePrivileges` (or the + `/roles/name/{name}/...` variants), with privilege names in the body. +- `role delete` (`DELETE /roles/{id}`) matched the API but could not target a role by name. + +--- + +## Command / Reproduction Steps + +```bash +iics role get --id --verbose +``` + +--- + +## Expected Behaviour + +Per the [Getting role details](https://docs.informatica.com/cloud-common-services/administrator/current-version/rest-api-reference/platform-rest-api-version-3-resources/roles/getting-role-details.html) +documentation, a single role is retrieved with a query filter on `roleId` or `roleName`, and +privileges are included with `expand=privileges`: + +```text +GET /public/core/v3/roles?q=roleName=="Business Manager"&expand=privileges +``` + +- `iics role get --id ` queries `q=roleId==""` +- `iics role get --name ""` queries `q=roleName==""` +- `--privileges` adds `expand=privileges` and shows the privileges + +--- + +## Architecture Layer + +- [x] **`cmd/`** - flag parsing, command wiring, output formatting +- [x] **`internal/client/`** - HTTP logic, API structs, request/response handling +- [ ] **`internal/config/`** - config file loading, session cache +- [ ] **`internal/output/`** - table / JSON / CSV renderer + +--- + +## API Details + +| Field | Value | +| -------------- | --------------------------- | +| API version | V3 (`public/core/v3`) | +| HTTP method | GET | +| Endpoint path | `public/core/v3/roles` | +| Session header | `INFA-SESSION-ID` | + +The `privileges` array contains objects (`id`, `name`, `description`, `service`, `status`), but +`Role.Privileges` was typed `[]string`, so decoding an expanded response would fail. + +--- + +## Fix (filled in after resolution) + +**Root cause:** + +`GetRole` built the URL `/roles/{id}`, which the v3 Roles API does not provide. The role struct +also typed `privileges` as `[]string` instead of an array of objects. Create reused the response +struct as the request body, and update targeted a non-existent endpoint. + +**Files changed:** + +```text +internal/client/roles.go - Added RolePrivilege struct; Role.Privileges is now []RolePrivilege +internal/client/roles.go - Added RoleGetOptions (ID, Name, ExpandPrivileges) +internal/client/roles.go - GetRole queries /roles with q=roleId==/roleName== and optional expand=privileges +cmd/role.go - role get: --id / --name (mutually exclusive), --privileges flag +cmd/role.go - Table output renders role details plus a privileges table +cmd/role.go - role get prompts for a role when --id and --name are omitted (terminal only) +cmd/role.go - role get table output asks "Display role privileges? [y/Q]" when --privileges is omitted +cmd/role_prompt.go - pickRole picker, privilege prompt, privilege sort (service, name), status colors +internal/client/roles.go - Added CreateRoleRequest (name, description, privilege IDs); CreateRole uses it +internal/client/roles.go - Removed UpdateRole; added RoleRef, AddRolePrivileges, RemoveRolePrivileges +internal/client/roles.go - Added ResolvePrivileges (privilege name or ID to Privilege) +cmd/role.go - role create: --name, --description, --privilege (name or ID), --from-file +cmd/role.go - Replaced role update with role add-privileges / remove-privileges +cmd/role.go - role delete: added --name +docs/documentation/role.md - Updated get, create, delete; added add-privileges, remove-privileges +README.md - Updated role subcommands +``` + +**Test added / updated:** + +```text +internal/client/roles_test.go - TestGetRoleByName, TestGetRoleByID, TestGetRoleNotFound, TestGetRoleRequiresSelector +internal/client/roles_test.go - TestCreateRole, TestAddRolePrivileges, TestAddRolePrivilegesValidation +internal/client/roles_test.go - TestResolvePrivileges, TestDeleteRole +``` diff --git a/internal/client/roles.go b/internal/client/roles.go index f08e704..1f9b738 100644 --- a/internal/client/roles.go +++ b/internal/client/roles.go @@ -4,24 +4,44 @@ import ( "context" "fmt" "net/http" + "net/url" "strconv" + "strings" ) // Role represents an IICS role. type Role struct { - ID string `json:"id,omitempty"` - OrgID string `json:"orgId,omitempty"` - RoleName string `json:"roleName"` - Description string `json:"description,omitempty"` - DisplayName string `json:"displayName,omitempty"` - DisplayDescription string `json:"displayDescription,omitempty"` - Status string `json:"status,omitempty"` - CreateTime string `json:"createTime,omitempty"` - UpdateTime string `json:"updateTime,omitempty"` - CreatedBy string `json:"createdBy,omitempty"` - UpdatedBy string `json:"updatedBy,omitempty"` - SystemRole bool `json:"systemRole,omitempty"` - Privileges []string `json:"privileges,omitempty"` + ID string `json:"id,omitempty"` + OrgID string `json:"orgId,omitempty"` + RoleName string `json:"roleName"` + Description string `json:"description,omitempty"` + DisplayName string `json:"displayName,omitempty"` + DisplayDescription string `json:"displayDescription,omitempty"` + Status string `json:"status,omitempty"` + CreateTime string `json:"createTime,omitempty"` + UpdateTime string `json:"updateTime,omitempty"` + CreatedBy string `json:"createdBy,omitempty"` + UpdatedBy string `json:"updatedBy,omitempty"` + SystemRole bool `json:"systemRole,omitempty"` + Privileges []RolePrivilege `json:"privileges,omitempty"` +} + +// RolePrivilege is a privilege assigned to a role, returned only when the +// request includes expand=privileges. +type RolePrivilege struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Service string `json:"service,omitempty"` + Status string `json:"status,omitempty"` +} + +// RoleGetOptions selects a single role by ID or name. Exactly one of ID or +// Name must be set. +type RoleGetOptions struct { + ID string + Name string + ExpandPrivileges bool } // RoleListOptions holds query parameters for listing roles. @@ -47,34 +67,128 @@ func (c *Client) ListRoles(ctx context.Context, opts RoleListOptions) ([]Role, e return resp, nil } -// GetRole retrieves a single role by ID. -func (c *Client) GetRole(ctx context.Context, id string) (*Role, error) { - var resp Role - if err := c.doJSON(ctx, http.MethodGet, fmt.Sprintf("%s/roles/%s", BaseAPIPathV3, id), nil, &resp); err != nil { +// GetRole retrieves a single role by ID or name. The v3 API has no +// /roles/{id} endpoint, so the role is looked up with a q filter on roleId +// or roleName. +func (c *Client) GetRole(ctx context.Context, opts RoleGetOptions) (*Role, error) { + query := make(map[string]string) + var key string + switch { + case opts.ID != "" && opts.Name != "": + return nil, fmt.Errorf("specify either role ID or role name, not both") + case opts.ID != "": + key = opts.ID + query["q"] = fmt.Sprintf("roleId==%q", opts.ID) + case opts.Name != "": + key = opts.Name + query["q"] = fmt.Sprintf("roleName==%q", opts.Name) + default: + return nil, fmt.Errorf("role ID or role name is required") + } + if opts.ExpandPrivileges { + query["expand"] = "privileges" + } + + var resp []Role + if err := c.doJSONWithQuery(ctx, http.MethodGet, BaseAPIPathV3+"/roles", query, nil, &resp); err != nil { return nil, err } - return &resp, nil + if len(resp) == 0 { + return nil, fmt.Errorf("role %q not found", key) + } + return &resp[0], nil +} + +// CreateRoleRequest is the POST body for creating a custom role. Privileges +// holds privilege IDs; the API requires at least one. +type CreateRoleRequest struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + Privileges []string `json:"privileges"` } -// CreateRole creates a new role. -func (c *Client) CreateRole(ctx context.Context, role *Role) (*Role, error) { +// CreateRole creates a new custom role. +func (c *Client) CreateRole(ctx context.Context, req *CreateRoleRequest) (*Role, error) { var resp Role - if err := c.doJSON(ctx, http.MethodPost, BaseAPIPathV3+"/roles", role, &resp); err != nil { + if err := c.doJSON(ctx, http.MethodPost, BaseAPIPathV3+"/roles", req, &resp); err != nil { return nil, err } return &resp, nil } -// UpdateRole updates an existing role. -func (c *Client) UpdateRole(ctx context.Context, id string, role *Role) (*Role, error) { - var resp Role - if err := c.doJSON(ctx, http.MethodPut, fmt.Sprintf("%s/roles/%s", BaseAPIPathV3, id), role, &resp); err != nil { +// RoleRef identifies a role by ID or name. Exactly one field must be set. +type RoleRef struct { + ID string + Name string +} + +// rolePrivilegesRequest is the PUT body for addPrivileges / removePrivileges. +type rolePrivilegesRequest struct { + Privileges []string `json:"privileges"` +} + +// AddRolePrivileges adds privileges (by name) to a custom role. +func (c *Client) AddRolePrivileges(ctx context.Context, role RoleRef, privileges []string) error { + return c.updateRolePrivileges(ctx, role, "addPrivileges", privileges) +} + +// RemoveRolePrivileges removes privileges (by name) from a custom role. A +// role must keep at least one privilege. +func (c *Client) RemoveRolePrivileges(ctx context.Context, role RoleRef, privileges []string) error { + return c.updateRolePrivileges(ctx, role, "removePrivileges", privileges) +} + +func (c *Client) updateRolePrivileges(ctx context.Context, role RoleRef, action string, privileges []string) error { + var path string + switch { + case role.ID != "" && role.Name != "": + return fmt.Errorf("specify either role ID or role name, not both") + case role.ID != "": + path = fmt.Sprintf("%s/roles/%s/%s", BaseAPIPathV3, url.PathEscape(role.ID), action) + case role.Name != "": + path = fmt.Sprintf("%s/roles/name/%s/%s", BaseAPIPathV3, url.PathEscape(role.Name), action) + default: + return fmt.Errorf("role ID or role name is required") + } + if len(privileges) == 0 { + return fmt.Errorf("at least one privilege is required") + } + return c.doJSON(ctx, http.MethodPut, path, &rolePrivilegesRequest{Privileges: privileges}, nil) +} + +// ResolvePrivileges maps each reference (privilege ID or name) to a known +// privilege, dropping duplicates. Unknown references are reported together in +// one error. +func (c *Client) ResolvePrivileges(ctx context.Context, refs []string) ([]Privilege, error) { + all, err := c.ListPrivileges(ctx) + if err != nil { return nil, err } - return &resp, nil + byKey := make(map[string]Privilege, len(all)*2) + for _, p := range all { + byKey[p.ID] = p + byKey[p.Name] = p + } + var resolved []Privilege + var unknown []string + seen := make(map[string]bool, len(refs)) + for _, ref := range refs { + if p, ok := byKey[ref]; ok { + if !seen[p.ID] { + seen[p.ID] = true + resolved = append(resolved, p) + } + } else { + unknown = append(unknown, ref) + } + } + if len(unknown) > 0 { + return nil, fmt.Errorf("unknown privilege(s): %s", strings.Join(unknown, ", ")) + } + return resolved, nil } // DeleteRole deletes a role by ID. func (c *Client) DeleteRole(ctx context.Context, id string) error { - return c.doJSON(ctx, http.MethodDelete, fmt.Sprintf("%s/roles/%s", BaseAPIPathV3, id), nil, nil) + return c.doJSON(ctx, http.MethodDelete, fmt.Sprintf("%s/roles/%s", BaseAPIPathV3, url.PathEscape(id)), nil, nil) } diff --git a/internal/client/roles_test.go b/internal/client/roles_test.go new file mode 100644 index 0000000..f46e192 --- /dev/null +++ b/internal/client/roles_test.go @@ -0,0 +1,213 @@ +package client + +import ( + "context" + "encoding/json" + "io" + "net/http" + "strings" + "testing" +) + +func TestGetRoleByName(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/public/core/v3/roles" { + t.Errorf("expected /public/core/v3/roles, got %s", r.URL.Path) + } + if q := r.URL.Query().Get("q"); q != `roleName=="Business Manager"` { + t.Errorf("unexpected q: %s", q) + } + if e := r.URL.Query().Get("expand"); e != "privileges" { + t.Errorf("expected expand=privileges, got %q", e) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`[{ + "id": "7EjAMAHsiOTcg8v29z0Gsl", + "orgId": "52ZSTB0IDK6dXxaEQLUaQu", + "roleName": "Business Manager", + "systemRole": true, + "status": "Disabled", + "privileges": [ + {"id": "5Cgp0GcsmRejyxIgV4eXy1", "name": "view.ai.console", "description": "View application integration console", "service": "ApplicationIntegration", "status": "Disabled"} + ] + }]`)) + }) + c := newTestClient(handler) + + role, err := c.GetRole(context.Background(), RoleGetOptions{Name: "Business Manager", ExpandPrivileges: true}) + if err != nil { + t.Fatalf("GetRole() error: %v", err) + } + if role.ID != "7EjAMAHsiOTcg8v29z0Gsl" || role.RoleName != "Business Manager" || !role.SystemRole { + t.Errorf("unexpected role: %+v", role) + } + if len(role.Privileges) != 1 { + t.Fatalf("expected 1 privilege, got %d", len(role.Privileges)) + } + p := role.Privileges[0] + if p.Name != "view.ai.console" || p.Service != "ApplicationIntegration" || p.Status != "Disabled" { + t.Errorf("unexpected privilege: %+v", p) + } +} + +func TestGetRoleByID(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if q := r.URL.Query().Get("q"); q != `roleId=="abc123"` { + t.Errorf("unexpected q: %s", q) + } + if r.URL.Query().Has("expand") { + t.Errorf("expand should not be set") + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`[{"id": "abc123", "roleName": "Designer"}]`)) + }) + c := newTestClient(handler) + + role, err := c.GetRole(context.Background(), RoleGetOptions{ID: "abc123"}) + if err != nil { + t.Fatalf("GetRole() error: %v", err) + } + if role.RoleName != "Designer" { + t.Errorf("expected Designer, got %s", role.RoleName) + } +} + +func TestGetRoleNotFound(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`[]`)) + }) + c := newTestClient(handler) + + _, err := c.GetRole(context.Background(), RoleGetOptions{Name: "Missing"}) + if err == nil || !strings.Contains(err.Error(), "not found") { + t.Errorf("expected not found error, got %v", err) + } +} + +func TestGetRoleRequiresSelector(t *testing.T) { + c := newTestClient(http.NotFoundHandler()) + if _, err := c.GetRole(context.Background(), RoleGetOptions{}); err == nil { + t.Error("expected error when neither ID nor name is set") + } + if _, err := c.GetRole(context.Background(), RoleGetOptions{ID: "a", Name: "b"}); err == nil { + t.Error("expected error when both ID and name are set") + } +} + +func TestCreateRole(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost || r.URL.Path != "/public/core/v3/roles" { + t.Errorf("unexpected request %s %s", r.Method, r.URL.Path) + } + body, _ := io.ReadAll(r.Body) + var got map[string]interface{} + if err := json.Unmarshal(body, &got); err != nil { + t.Fatalf("invalid body: %v", err) + } + if got["name"] != "CAIviewer" || got["description"] != "View CAI" { + t.Errorf("unexpected body: %s", body) + } + privs, _ := got["privileges"].([]interface{}) + if len(privs) != 2 || privs[0] != "aQwUdcM8RcQewA1yWphZ4F" { + t.Errorf("unexpected privileges: %v", got["privileges"]) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"id": "8j2MPlr8ubZgteIOwleSCk", "roleName": "CAIviewer", "systemRole": false, + "privileges": [{"id": "aQwUdcM8RcQewA1yWphZ4F", "name": "view.ai.assets", "description": "View assets"}]}`)) + }) + c := newTestClient(handler) + + role, err := c.CreateRole(context.Background(), &CreateRoleRequest{ + Name: "CAIviewer", + Description: "View CAI", + Privileges: []string{"aQwUdcM8RcQewA1yWphZ4F", "0nTOXl8dzEwlSFoM0cO8gI"}, + }) + if err != nil { + t.Fatalf("CreateRole() error: %v", err) + } + if role.ID != "8j2MPlr8ubZgteIOwleSCk" || len(role.Privileges) != 1 { + t.Errorf("unexpected role: %+v", role) + } +} + +func TestAddRolePrivileges(t *testing.T) { + tests := []struct { + name string + ref RoleRef + call func(*Client, context.Context, RoleRef, []string) error + wantPath string + }{ + {"add by id", RoleRef{ID: "abc123"}, (*Client).AddRolePrivileges, "/public/core/v3/roles/abc123/addPrivileges"}, + {"add by name", RoleRef{Name: "CAI Viewer"}, (*Client).AddRolePrivileges, "/public/core/v3/roles/name/CAI%20Viewer/addPrivileges"}, + {"remove by id", RoleRef{ID: "abc123"}, (*Client).RemoveRolePrivileges, "/public/core/v3/roles/abc123/removePrivileges"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut { + t.Errorf("expected PUT, got %s", r.Method) + } + if r.URL.EscapedPath() != tt.wantPath { + t.Errorf("expected path %s, got %s", tt.wantPath, r.URL.EscapedPath()) + } + body, _ := io.ReadAll(r.Body) + if string(body) != `{"privileges":["create.data.transfer.task"]}` { + t.Errorf("unexpected body: %s", body) + } + w.WriteHeader(http.StatusOK) + }) + c := newTestClient(handler) + if err := tt.call(c, context.Background(), tt.ref, []string{"create.data.transfer.task"}); err != nil { + t.Fatalf("error: %v", err) + } + }) + } +} + +func TestAddRolePrivilegesValidation(t *testing.T) { + c := newTestClient(http.NotFoundHandler()) + if err := c.AddRolePrivileges(context.Background(), RoleRef{}, []string{"x"}); err == nil { + t.Error("expected error without role reference") + } + if err := c.AddRolePrivileges(context.Background(), RoleRef{ID: "a"}, nil); err == nil { + t.Error("expected error without privileges") + } +} + +func TestResolvePrivileges(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`[ + {"id": "id1", "name": "view.ai.assets"}, + {"id": "id2", "name": "view.ai.designer"} + ]`)) + }) + c := newTestClient(handler) + + got, err := c.ResolvePrivileges(context.Background(), []string{"view.ai.assets", "id2", "id1"}) + if err != nil { + t.Fatalf("ResolvePrivileges() error: %v", err) + } + if len(got) != 2 || got[0].ID != "id1" || got[1].Name != "view.ai.designer" { + t.Errorf("unexpected result: %+v", got) + } + + _, err = c.ResolvePrivileges(context.Background(), []string{"view.ai.assets", "bogus"}) + if err == nil || !strings.Contains(err.Error(), "bogus") { + t.Errorf("expected unknown privilege error, got %v", err) + } +} + +func TestDeleteRole(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodDelete || r.URL.Path != "/public/core/v3/roles/abc123" { + t.Errorf("unexpected request %s %s", r.Method, r.URL.Path) + } + w.WriteHeader(http.StatusNoContent) + }) + c := newTestClient(handler) + if err := c.DeleteRole(context.Background(), "abc123"); err != nil { + t.Fatalf("DeleteRole() error: %v", err) + } +} From 27ed61de23fdcd5956cd6a1f5a010754ba72c187 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Tue, 29 Sep 2026 21:45:06 -0400 Subject: [PATCH 02/52] feat(role): interactive privilege editor and role cloning (CR-0040) - role edit: TUI to add/remove privileges, grouped by service, then by object with one column per action (view/create/update/delete/execute/ changeperm); non-matching names listed under Other; review step with add-before-remove apply and a guard against removing all privileges - role add-privileges / remove-privileges open the editor in add-only / remove-only mode when --privilege is omitted on a terminal - role create: interactive wizard (name, description, optional clone source, editor) and non-interactive --from-role - role picker is now a filterable huh select - client: GroupPrivileges, DiffPrivileges, FindRole with tests; internal/tui/privmatrix Bubble Tea model with tests - deps: charmbracelet/huh, bubbletea, bubbles - docs: role.md editor/edit/--from-role, README, docs/CLAUDE.md deps, completions; CR-0040 moved to pending --- README.md | 2 +- cmd/role.go | 120 +++- cmd/role_prompt.go | 222 +++++++- completions/iics.bash | 52 ++ docs/CLAUDE.md | 3 + ...-0040-role-interactive-privilege-editor.md | 110 ++++ .../comparison-informatica-cli.md | 2 +- docs/documentation/role.md | 176 +++++- go.mod | 25 +- go.sum | 71 ++- internal/client/privilegegroups.go | 141 +++++ internal/client/privilegegroups_test.go | 70 +++ internal/client/roles.go | 14 + internal/client/roles_test.go | 24 + internal/tui/privmatrix/model.go | 536 ++++++++++++++++++ internal/tui/privmatrix/model_test.go | 162 ++++++ internal/tui/roleeditor.go | 296 ++++++++++ 17 files changed, 1959 insertions(+), 67 deletions(-) create mode 100644 docs/ChangeRequests/pending/CR-0040-role-interactive-privilege-editor.md create mode 100644 internal/client/privilegegroups.go create mode 100644 internal/client/privilegegroups_test.go create mode 100644 internal/tui/privmatrix/model.go create mode 100644 internal/tui/privmatrix/model_test.go create mode 100644 internal/tui/roleeditor.go diff --git a/README.md b/README.md index 7aa0da6..d34b9a4 100644 --- a/README.md +++ b/README.md @@ -269,7 +269,7 @@ it easy to override credentials in CI pipelines without touching the config file | [project](docs/documentation/project.md) | | `create`, `update`, `delete` | Manage projects | | [publish](docs/documentation/publish.md) | | `start`, `status`, `run` | Publish CAI assets to the runtime | | [release](docs/documentation/release.md) | | `manifest`, `validate`, `plan` | Generate multi-format release manifests and per-environment plans | -| [role](docs/documentation/role.md) | | `list`, `get`, `create`, `add-privileges`, `remove-privileges`, `delete` | Manage roles | +| [role](docs/documentation/role.md) | | `list`, `get`, `create`, `edit`, `add-privileges`, `remove-privileges`, `delete` | Manage roles | | [environment](docs/documentation/environment.md) | `runtime`, `rt`, `env` | `list`, `get`, `create`, `update`, `delete`, `configs` | Manage runtime environments (Secure Agent groups) | | [schedule](docs/documentation/schedule.md) | | `list`, `get`, `create`, `update`, `delete` | Manage schedules | | [securitylog](docs/documentation/securitylog.md) | | `list` | Query security audit log | diff --git a/cmd/role.go b/cmd/role.go index 35e0c65..8a2d7fe 100644 --- a/cmd/role.go +++ b/cmd/role.go @@ -11,6 +11,7 @@ import ( "github.com/jbrazda/iics-cli/internal/client" "github.com/jbrazda/iics-cli/internal/output" + "github.com/jbrazda/iics-cli/internal/tui/privmatrix" "github.com/spf13/cobra" ) @@ -22,6 +23,7 @@ func newRoleCmd() *cobra.Command { cmd.AddCommand(newRoleListCmd()) cmd.AddCommand(newRoleGetCmd()) cmd.AddCommand(newRoleCreateCmd()) + cmd.AddCommand(newRoleEditCmd()) cmd.AddCommand(newRolePrivilegesCmd("add-privileges", "Add privileges to a custom role", "Added")) cmd.AddCommand(newRolePrivilegesCmd("remove-privileges", "Remove privileges from a custom role", "Removed")) cmd.AddCommand(newRoleDeleteCmd()) @@ -189,16 +191,26 @@ var roleDetailColumns = []output.Column{ func newRoleCreateCmd() *cobra.Command { var ( fromFile string + fromRole string req client.CreateRoleRequest privileges []string ) cmd := &cobra.Command{ Use: "create", Short: "Create a custom role", - Example: ` iics role create --name "CAI Viewer" --description "View CAI assets" \ + Long: `Create a custom role. + +On a terminal, when --name or privileges are missing (no --privilege, +--from-role or --from-file), an interactive wizard asks for the name, +description and an optional role to copy privileges from, then opens the +privilege editor.`, + Example: ` iics role create # interactive wizard + iics role create --name "CAI Viewer" --description "View CAI assets" \ --privilege view.ai.designer --privilege view.ai.assets + iics role create --name "Designer Copy" --from-role Designer iics role create --from-file cai-viewer-role.json`, RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() var body client.CreateRoleRequest if fromFile != "" { data, err := os.ReadFile(fromFile) @@ -215,40 +227,94 @@ func newRoleCreateCmd() *cobra.Command { if req.Description != "" { body.Description = req.Description } - refs := append(body.Privileges, privileges...) - if body.Name == "" { - return fmt.Errorf("--name is required") - } - if len(refs) == 0 { - return fmt.Errorf("at least one --privilege is required") - } c, err := getClient(cmd) if err != nil { return err } - resolved, err := c.ResolvePrivileges(context.Background(), refs) - if err != nil { - return err - } - body.Privileges = make([]string, len(resolved)) - for i, p := range resolved { - body.Privileges[i] = p.ID + + wizard := fromFile == "" && isInteractiveTTY() && + (body.Name == "" || (len(privileges) == 0 && fromRole == "")) + if wizard { + ok, werr := runRoleCreateWizard(ctx, c, &body, fromRole, privileges) + if werr != nil { + return werr + } + if !ok { + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Canceled.") + return nil + } + } else { + refs := append(body.Privileges, privileges...) + if fromRole != "" { + src, ferr := c.FindRole(ctx, fromRole, true) + if ferr != nil { + return ferr + } + for _, p := range src.Privileges { + refs = append(refs, p.ID) + } + if body.Description == "" { + body.Description = src.Description + } + } + if body.Name == "" { + return fmt.Errorf("--name is required") + } + if len(refs) == 0 { + return fmt.Errorf("at least one --privilege or --from-role is required") + } + resolved, rerr := c.ResolvePrivileges(ctx, refs) + if rerr != nil { + return rerr + } + body.Privileges = make([]string, len(resolved)) + for i, p := range resolved { + body.Privileges[i] = p.ID + } } - created, err := c.CreateRole(context.Background(), &body) + + created, err := c.CreateRole(ctx, &body) if err != nil { return err } - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Role created: %s (ID: %s)\n", created.RoleName, created.ID) + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Role created: %s (ID: %s, %d privileges)\n", created.RoleName, created.ID, len(body.Privileges)) return nil }, } - cmd.Flags().StringVar(&req.Name, "name", "", "role name (required unless set in --from-file)") + cmd.Flags().StringVar(&req.Name, "name", "", "role name (prompted on a terminal when omitted)") cmd.Flags().StringVar(&req.Description, "description", "", "role description") cmd.Flags().StringSliceVar(&privileges, "privilege", nil, "privilege name or ID to assign (repeatable or comma-separated)") + cmd.Flags().StringVar(&fromRole, "from-role", "", "copy privileges (and description) from an existing role, by name or ID") cmd.Flags().StringVar(&fromFile, "from-file", "", "JSON file with role definition (name, description, privileges)") return cmd } +// newRoleEditCmd opens the interactive privilege editor for a custom role. +func newRoleEditCmd() *cobra.Command { + var ref client.RoleRef + cmd := &cobra.Command{ + Use: "edit", + Short: "Interactively add and remove privileges of a custom role", + Long: `Open the interactive privilege editor for a custom role. Privileges are +grouped by service, then by object with one column per action (view, create, +update, delete, execute, change permission). Changes are applied after a +review step. Requires a terminal.`, + Example: ` iics role edit # pick a role from a list + iics role edit --name "CAI Viewer"`, + RunE: func(cmd *cobra.Command, args []string) error { + c, err := getClient(cmd) + if err != nil { + return err + } + return editRolePrivilegesInteractive(context.Background(), c, cmd.OutOrStdout(), ref, privmatrix.ModeEdit) + }, + } + cmd.Flags().StringVar(&ref.ID, "id", "", "role ID") + cmd.Flags().StringVar(&ref.Name, "name", "", "role name") + cmd.MarkFlagsMutuallyExclusive("id", "name") + return cmd +} + // newRolePrivilegesCmd builds add-privileges / remove-privileges. The API has // no general role update endpoint; privileges are the only mutable part. func newRolePrivilegesCmd(use, short, verb string) *cobra.Command { @@ -259,9 +325,25 @@ func newRolePrivilegesCmd(use, short, verb string) *cobra.Command { cmd := &cobra.Command{ Use: use, Short: short, - Example: fmt.Sprintf(` iics role %[1]s --name "CAI Viewer" --privilege view.ai.console + Long: fmt.Sprintf(`%s. + +Without --privilege on a terminal, the interactive privilege editor opens +(and a role picker when --id and --name are omitted).`, short), + Example: fmt.Sprintf(` iics role %[1]s # interactive editor + iics role %[1]s --name "CAI Viewer" --privilege view.ai.console iics role %[1]s --id --privilege create.data.transfer.task,delete.data.transfer.task`, use), RunE: func(cmd *cobra.Command, args []string) error { + if len(privileges) == 0 && isInteractiveTTY() { + c, err := getClient(cmd) + if err != nil { + return err + } + mode := privmatrix.ModeAdd + if use == "remove-privileges" { + mode = privmatrix.ModeRemove + } + return editRolePrivilegesInteractive(context.Background(), c, cmd.OutOrStdout(), ref, mode) + } if ref.ID == "" && ref.Name == "" { return fmt.Errorf("--id or --name is required") } diff --git a/cmd/role_prompt.go b/cmd/role_prompt.go index 164a6eb..4a0b2ad 100644 --- a/cmd/role_prompt.go +++ b/cmd/role_prompt.go @@ -3,14 +3,19 @@ package cmd import ( "bufio" "context" + "errors" "fmt" + "io" "os" "sort" "strings" + "github.com/charmbracelet/huh" "github.com/charmbracelet/lipgloss" "github.com/jbrazda/iics-cli/internal/client" "github.com/jbrazda/iics-cli/internal/config" + "github.com/jbrazda/iics-cli/internal/tui" + "github.com/jbrazda/iics-cli/internal/tui/privmatrix" "golang.org/x/term" ) @@ -30,8 +35,9 @@ func promptShowPrivileges() bool { return line == "y" || line == "yes" } -// pickRole lists the roles and lets the operator choose one. The roles API -// documents no paging parameters, so a single unpaged request returns them all. +// pickRole lists the roles in a filterable menu and lets the operator choose +// one. The roles API documents no paging parameters, so a single unpaged +// request returns them all. func pickRole(ctx context.Context, c *client.Client, action string) (*client.Role, error) { if !config.IsTerminal() { return nil, fmt.Errorf("provide --id or --name (stdin is not a terminal)") @@ -43,25 +49,223 @@ func pickRole(ctx context.Context, c *client.Client, action string) (*client.Rol if len(roles) == 0 { return nil, fmt.Errorf("no roles found") } + sortRoles(roles) + idx := 0 + sel := huh.NewSelect[int](). + Title(fmt.Sprintf("Select a role to %s (/ to filter)", action)). + Options(roleOptions(roles)...). + Height(min(len(roles)+2, 22)). + Value(&idx) + if err := huh.NewForm(huh.NewGroup(sel)).WithOutput(os.Stderr).Run(); err != nil { + if errors.Is(err, huh.ErrUserAborted) { + return nil, fmt.Errorf("canceled") + } + return nil, err + } + return &roles[idx], nil +} + +func sortRoles(roles []client.Role) { sort.SliceStable(roles, func(i, j int) bool { return strings.ToLower(roles[i].RoleName) < strings.ToLower(roles[j].RoleName) }) - labels := make([]string, len(roles)) +} + +func roleOptions(roles []client.Role) []huh.Option[int] { + opts := make([]huh.Option[int], len(roles)) for i, r := range roles { kind := "custom" if r.SystemRole { kind = "system" } - labels[i] = fmt.Sprintf("%s (%s)", r.RoleName, kind) + opts[i] = huh.NewOption(fmt.Sprintf("%s (%s)", r.RoleName, kind), i) } - idx, err := promptSelect(fmt.Sprintf("Select a role to %s", action), labels) + return opts +} + +// editorNoColor reports whether interactive editors should render without color. +func editorNoColor() bool { + cfg, _ := loadConfig() + return resolveTableStyle(cfg).NoColor +} + +// editRolePrivilegesInteractive opens the privilege editor for a role and +// applies the resulting changes (add first, then remove, so the role never +// drops to zero privileges in between). +func editRolePrivilegesInteractive(ctx context.Context, c *client.Client, w io.Writer, ref client.RoleRef, mode privmatrix.Mode) error { + if !isInteractiveTTY() { + return fmt.Errorf("interactive editing requires a terminal; use role add-privileges / remove-privileges with --privilege") + } + opts := client.RoleGetOptions{ID: ref.ID, Name: ref.Name, ExpandPrivileges: true} + if opts.ID == "" && opts.Name == "" { + picked, err := pickRole(ctx, c, "edit") + if err != nil { + return err + } + opts.ID = picked.ID + } + role, err := c.GetRole(ctx, opts) if err != nil { - return nil, err + return err } - if idx < 0 { - return nil, fmt.Errorf("canceled") + if role.SystemRole { + return fmt.Errorf("role %q is a system role; only custom roles can be changed", role.RoleName) } - return &roles[idx], nil + all, err := c.ListPrivileges(ctx) + if err != nil { + return err + } + assigned := make([]string, len(role.Privileges)) + for i, p := range role.Privileges { + assigned[i] = p.Name + } + + desired, err := tui.EditPrivileges(all, assigned, assigned, tui.PrivilegeEditorOptions{ + Title: "Role: " + role.RoleName, + Mode: mode, + NoColor: editorNoColor(), + }) + if errors.Is(err, tui.ErrCanceled) { + _, _ = fmt.Fprintln(w, "Canceled.") + return nil + } + if err != nil { + return err + } + + add, remove := client.DiffPrivileges(assigned, desired) + if len(add) == 0 && len(remove) == 0 { + _, _ = fmt.Fprintln(w, "No changes.") + return nil + } + target := client.RoleRef{ID: role.ID} + if len(add) > 0 { + if err := c.AddRolePrivileges(ctx, target, add); err != nil { + return err + } + printPrivilegeChanges(w, "Added", role.RoleName, "+", add) + } + if len(remove) > 0 { + if err := c.RemoveRolePrivileges(ctx, target, remove); err != nil { + return err + } + printPrivilegeChanges(w, "Removed", role.RoleName, "-", remove) + } + return nil +} + +func printPrivilegeChanges(w io.Writer, verb, roleName, sign string, names []string) { + _, _ = fmt.Fprintf(w, "%s %d privilege(s) for role %s:\n", verb, len(names), roleName) + for _, n := range names { + _, _ = fmt.Fprintf(w, " %s %s\n", sign, n) + } +} + +// runRoleCreateWizard collects the name, description and optional clone +// source for a new role, then opens the privilege editor. It fills body with +// privilege IDs. ok is false when the user canceled. +func runRoleCreateWizard(ctx context.Context, c *client.Client, body *client.CreateRoleRequest, fromRole string, extra []string) (bool, error) { + roles, err := c.ListRoles(ctx, client.RoleListOptions{}) + if err != nil { + return false, err + } + sortRoles(roles) + existing := make(map[string]bool, len(roles)) + for _, r := range roles { + existing[strings.ToLower(r.RoleName)] = true + } + + cloneIdx := -1 + if fromRole != "" { + src, ferr := c.FindRole(ctx, fromRole, false) + if ferr != nil { + return false, ferr + } + for i := range roles { + if roles[i].ID == src.ID { + cloneIdx = i + } + } + } + cloneOpts := append([]huh.Option[int]{huh.NewOption("None - start empty", -1)}, roleOptions(roles)...) + + form := huh.NewForm( + huh.NewGroup( + huh.NewInput().Title("Role name").Value(&body.Name).Validate(func(v string) error { + v = strings.TrimSpace(v) + if v == "" { + return fmt.Errorf("name is required") + } + if existing[strings.ToLower(v)] { + return fmt.Errorf("a role named %q already exists", v) + } + return nil + }), + huh.NewInput().Title("Description (optional)").Value(&body.Description), + huh.NewSelect[int](). + Title("Copy privileges from an existing role? (/ to filter)"). + Options(cloneOpts...). + Height(12). + Value(&cloneIdx), + ), + ).WithOutput(os.Stderr) + if err = form.Run(); err != nil { + if errors.Is(err, huh.ErrUserAborted) { + return false, nil + } + return false, err + } + body.Name = strings.TrimSpace(body.Name) + + var initial []string + if cloneIdx >= 0 { + src, gerr := c.GetRole(ctx, client.RoleGetOptions{ID: roles[cloneIdx].ID, ExpandPrivileges: true}) + if gerr != nil { + return false, gerr + } + for _, p := range src.Privileges { + initial = append(initial, p.Name) + } + if body.Description == "" { + body.Description = src.Description + } + } + if len(extra) > 0 { + resolved, rerr := c.ResolvePrivileges(ctx, extra) + if rerr != nil { + return false, rerr + } + for _, p := range resolved { + initial = append(initial, p.Name) + } + } + + all, err := c.ListPrivileges(ctx) + if err != nil { + return false, err + } + desired, err := tui.EditPrivileges(all, nil, initial, tui.PrivilegeEditorOptions{ + Title: "New role: " + body.Name, + ApplyLabel: "Create role", + NoColor: editorNoColor(), + }) + if errors.Is(err, tui.ErrCanceled) { + return false, nil + } + if err != nil { + return false, err + } + idByName := make(map[string]string, len(all)) + for _, p := range all { + idByName[p.Name] = p.ID + } + body.Privileges = body.Privileges[:0] + for _, n := range desired { + if id, ok := idByName[n]; ok { + body.Privileges = append(body.Privileges, id) + } + } + return true, nil } // sortRolePrivileges orders privileges by service, then privilege name. diff --git a/completions/iics.bash b/completions/iics.bash index 5a2cfbe..8bd9be5 100644 --- a/completions/iics.bash +++ b/completions/iics.bash @@ -5333,6 +5333,10 @@ _iics_role_create() two_word_flags+=("--from-file") local_nonpersistent_flags+=("--from-file") local_nonpersistent_flags+=("--from-file=") + flags+=("--from-role=") + two_word_flags+=("--from-role") + local_nonpersistent_flags+=("--from-role") + local_nonpersistent_flags+=("--from-role=") flags+=("--name=") two_word_flags+=("--name") local_nonpersistent_flags+=("--name") @@ -5417,6 +5421,53 @@ _iics_role_delete() noun_aliases=() } +_iics_role_edit() +{ + last_command="iics_role_edit" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--id=") + two_word_flags+=("--id") + local_nonpersistent_flags+=("--id") + local_nonpersistent_flags+=("--id=") + flags+=("--name=") + two_word_flags+=("--name") + local_nonpersistent_flags+=("--name") + local_nonpersistent_flags+=("--name=") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--http-timeout=") + two_word_flags+=("--http-timeout") + flags+=("--no-color") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + flags+=("--profile=") + two_word_flags+=("--profile") + two_word_flags+=("-p") + flags+=("--theme=") + two_word_flags+=("--theme") + flags+=("--verbose") + flags+=("-v") + flags+=("--wide") + flags+=("--width=") + two_word_flags+=("--width") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _iics_role_get() { last_command="iics_role_get" @@ -5574,6 +5625,7 @@ _iics_role() commands+=("add-privileges") commands+=("create") commands+=("delete") + commands+=("edit") commands+=("get") commands+=("list") commands+=("remove-privileges") diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md index c9e21f1..2c2941d 100644 --- a/docs/CLAUDE.md +++ b/docs/CLAUDE.md @@ -104,6 +104,9 @@ iics_cli/ | `github.com/spf13/cobra` | v1.10.2 | CLI framework | | `github.com/spf13/viper` | v1.21.0 | Config file + env var management | | `github.com/charmbracelet/lipgloss` | v1.1.0 | Table output and terminal styles | +| `github.com/charmbracelet/huh` | v1.0.0 | Interactive forms and selects | +| `github.com/charmbracelet/bubbletea` | v1.3.10 | TUI models (`internal/tui/`) | +| `github.com/charmbracelet/bubbles` | v1.0.0 | TUI components (text input) | | `github.com/mattn/go-isatty` | v0.0.20 | TTY detection for color fallback | | `gopkg.in/yaml.v3` | v3.0.1 | Session cache serialization | diff --git a/docs/ChangeRequests/pending/CR-0040-role-interactive-privilege-editor.md b/docs/ChangeRequests/pending/CR-0040-role-interactive-privilege-editor.md new file mode 100644 index 0000000..6483f47 --- /dev/null +++ b/docs/ChangeRequests/pending/CR-0040-role-interactive-privilege-editor.md @@ -0,0 +1,110 @@ +# CR-0040: Interactive role privilege editor and role cloning + +## CR Type + +- [x] Enhancement to existing command +- [x] New command + +## Problem + +`role add-privileges`, `role remove-privileges` and `role create` require +privileges to be typed with `--privilege` by name or ID. An organization has +600+ privileges (dev org: 625 across 19 services), so users must look up exact +privilege names with `iics privilege list` first. The existing numbered +multi-select prompt (`promptMultiSelect` in `cmd/user_prompt.go`) does not +scale to a list of this size. + +There is also no way to create a role from an existing one; users must copy +every privilege manually. + +## Proposed Solution + +### Privilege grouping + +Privilege names mostly follow `.` with action one of `view`, +`create`, `update`, `delete`, `execute`, `changeperm` (about 475 of 625 in the +dev org, for example `create.data.transfer.task`). Privileges are grouped: + +1. By `service` (empty service grouped as `(no service)`). +2. Within a service, by object (`data.transfer.task`) with one column per + action. +3. Names that do not match the pattern (`feature.mcp.*`, `PROFILE.*`, + `oi.*`, ...) are listed individually in an "Other" section per service. + +### Interactive editor (TUI) + +Built with `charmbracelet/huh` for forms and a custom Bubble Tea model for the +privilege matrix. + +1. Role picker (when `--id` / `--name` omitted) - filterable select. +2. Service picker - filterable select showing privilege count, assigned + count and pending changes per service, plus `Review and apply` and + `Cancel`. +3. Privilege matrix per service: + + ```text + Role: CAI Viewer > DI (157) filter: /task + OBJECT VIEW CREATE UPDATE DELETE EXEC PERM + data.transfer.task [x] [+] [ ] [ ] [ ] [ ] + file.listener [x] [ ] [-] [ ] [x] [ ] + mapping [ ] [ ] [ ] [ ] . . + -- Other -- + edc.iics.discovery [ ] + create.data.transfer.task - Create Data Transfer Task (Enabled) + arrows move space toggle a row c column / filter esc back enter done + ``` + + - `[x]` assigned, `[+]` pending add, `[-]` pending remove, `.` action not + available for the object. + - `a` toggles all actions in the row; `c` toggles the column for all + visible (filtered) rows. + - Footer shows the focused privilege's full name, description and status. +4. Review - pending additions and removals grouped by service, then confirm. + Applying is blocked if the role would be left with no privileges (API + error `V3API_IDSError_088`). + +### Commands + +| Command | Interactive when | Behavior | +| ------- | ---------------- | -------- | +| `role edit [--id\|--name]` (new) | always (terminal required) | Full editor; applies the diff with add first, then remove | +| `role add-privileges` | `--privilege` omitted on a terminal | Editor in add-only mode (assigned cells locked) | +| `role remove-privileges` | `--privilege` omitted on a terminal | Editor in remove-only mode (only assigned privileges shown) | +| `role create` | `--name` or privileges missing on a terminal | Form: name, description, optional clone source role, then editor pre-checked with the source privileges | +| `role create --from-role ` | never | Copies the source role's privileges, and description unless `--description` is given; `--privilege` values are added on top | + +Without a terminal, behavior is unchanged: missing flags return an error. + +## Implementation + +- Dependencies: `github.com/charmbracelet/huh`, + `github.com/charmbracelet/bubbletea`, `github.com/charmbracelet/bubbles`. +- `internal/client/privilegegroups.go` - `GroupPrivileges` (service, object, + action grouping) and `DiffPrivileges` (add/remove sets); pure functions. +- `internal/tui/privmatrix/` - Bubble Tea privilege matrix model (navigation, + toggles, row/column toggles, filter, add-only / remove-only modes). +- `internal/tui/` - editor orchestration: service picker, matrix, review and + confirm; returns the desired privilege set. +- `cmd/role.go`, `cmd/role_prompt.go` - `role edit`, interactive branches for + `add-privileges`, `remove-privileges`, `create`; `--from-role` flag; + `pickRole` moved to a filterable select. +- No new API endpoints; reuses `GetRole`, `ListPrivileges`, + `AddRolePrivileges`, `RemoveRolePrivileges`, `CreateRole`, + `ResolvePrivileges`. +- Tests: `internal/client/privilegegroups_test.go`, + `internal/tui/privmatrix/*_test.go` (model `Update` key handling). +- Docs: `docs/documentation/role.md` (`role edit`, interactive notes, key + bindings, `--from-role`), `README.md` role row, `make completions`. + +## Acceptance Criteria + +- [ ] `role edit` adds and removes privileges across services and applies + them in one confirmed step +- [ ] `role add-privileges` / `role remove-privileges` without `--privilege` + open the editor in the matching mode +- [ ] `role create --from-role` creates a role with the source role's + privileges +- [ ] Interactive `role create` supports cloning and editing before create +- [ ] Removing all privileges is blocked before any API call +- [ ] Non-terminal usage is unchanged +- [ ] `go build`, `go vet`, `golangci-lint`, `go test ./...` pass diff --git a/docs/documentation/comparison-informatica-cli.md b/docs/documentation/comparison-informatica-cli.md index 57851a4..08fa7ed 100644 --- a/docs/documentation/comparison-informatica-cli.md +++ b/docs/documentation/comparison-informatica-cli.md @@ -181,7 +181,7 @@ It cannot manage users, connections, schedules, roles, or any other administrati | `folder` | create, update, delete | | `user` | list, get, create, update, delete | | `usergroup` | list, get, create, update, delete | -| `role` | list, get, create, add-privileges, remove-privileges, delete | +| `role` | list, get, create, edit, add-privileges, remove-privileges, delete | | `privilege` | list | | `runtime` | list, get, create, update | | `agent` | list, start, stop | diff --git a/docs/documentation/role.md b/docs/documentation/role.md index cd8e03a..9faf0f3 100644 --- a/docs/documentation/role.md +++ b/docs/documentation/role.md @@ -10,14 +10,15 @@ iics role [flags] ## Subcommands -| Subcommand | Description | -| ------------------- | ------------------------------------ | -| `list` | List roles | -| `get` | Get a single role by ID or name, or pick one interactively | -| `create` | Create a custom role | -| `add-privileges` | Add privileges to a custom role | -| `remove-privileges` | Remove privileges from a custom role | -| `delete` | Delete a custom role | +| Subcommand | Description | +| ------------------- | ------------------------------------------------------------- | +| `list` | List roles | +| `get` | Get a single role by ID or name, or pick one interactively | +| `create` | Create a custom role (interactive wizard on a terminal) | +| `edit` | Add and remove privileges in the interactive editor | +| `add-privileges` | Add privileges to a custom role | +| `remove-privileges` | Remove privileges from a custom role | +| `delete` | Delete a custom role | The IICS v3 Roles API has no general update endpoint. The only change you can make to an existing custom role is adding or removing privileges. @@ -170,10 +171,118 @@ iics role get --name "Business Manager" --privileges --- +## Interactive privilege editor + +`role edit`, `role create` (wizard), and `role add-privileges` / +`role remove-privileges` without `--privilege` open an interactive editor on a +terminal. Privileges are grouped in two levels: + +1. **Service** - a filterable list with the number of privileges, how many are + selected, and pending changes (`+added -removed`) per service. +2. **Object and action grid** - privilege names of the form + `.` (for example `create.data.transfer.task`) become one row + per object with a column per action. Names that do not follow this pattern + (for example `feature.mcp.SuperAdmin`, `PROFILE.viewResults`) are listed + individually under **Other**. Privileges without a service are grouped as + `(no service)`. + +```text +Role: CAI Viewer > ApplicationIntegration (14) + + OBJECT VIEW CREATE UPDATE DELETE EXEC PERM +> ai.assets [-] [ ] [ ] [ ] [ ] [ ] + ai.console [x] . . . . . + ai.designer [x] . . . . . + -- Other -- + ai.admin [+] + +ai.admin - Administration (Enabled) +arrows move space toggle a row c column / filter ctrl+u clear filter enter/esc back +``` + +| Mark | Meaning | +| ----- | ---------------------------------------- | +| `[x]` | Assigned, unchanged | +| `[+]` | Will be added | +| `[-]` | Will be removed | +| `[ ]` | Not assigned | +| `.` | The action does not exist for the object | + +Grid keys: + +| Key | Action | +| --------------------- | --------------------------------------------------- | +| Arrows or `h j k l` | Move | +| `Space` or `x` | Toggle the privilege under the cursor | +| `a` | Toggle every action in the row | +| `c` | Toggle the column for all visible (filtered) rows | +| `/` | Filter by object, privilege name or description | +| `Ctrl+U` | Clear the filter | +| `PgUp` / `PgDn`, `g` / `G` | Page, jump to top / bottom | +| `Enter`, `Esc`, `q` | Back to the service list | + +In the service list, choose **Review and apply** to see the pending changes +grouped by service and confirm them, or **Cancel** to leave without changes. +Additions are applied before removals. A change that would leave the role with +no privileges is blocked at review. + +Modes: + +- `role edit` - add and remove. +- `role add-privileges` - add only; assigned privileges are locked. +- `role remove-privileges` - remove only; only assigned privileges are shown. + +System roles cannot be edited. Without a terminal, the commands require +`--privilege` (or `--name` and privileges for `create`). + +--- + +## role edit + +Open the interactive privilege editor for a custom role. Without `--id` or +`--name`, a filterable role list is shown first. Requires a terminal. + +### Flags + +| Flag | Type | Required | Description | +| -------- | ------ | -------- | ----------- | +| `--id` | string | | Role ID | +| `--name` | string | | Role name | + +All [global flags](../../README.md#global-flags) apply. + +### Examples + +```bash +# Pick a role, then edit its privileges +iics role edit + +iics role edit --name "CAI Viewer" +``` + +```powershell +iics role edit + +iics role edit --name "CAI Viewer" +``` + +--- + ## role create Create a custom role. The API requires a name and at least one privilege. +On a terminal, when `--name` is missing or no privileges are given (no +`--privilege`, `--from-role` or `--from-file`), a wizard asks for the role +name (duplicate names are rejected), description, and an optional existing +role to copy privileges from, then opens the +[interactive privilege editor](#interactive-privilege-editor). `--from-role` +and `--privilege` values pre-select privileges in the wizard. + +`--from-role` copies the privileges of an existing role, looked up by name +first and then by ID. The source description is used unless `--description` +is given. `--privilege` values are added on top. + Privileges can be given as names (for example `view.ai.assets`) or IDs. The command looks them up with `iics privilege list` data and sends privilege IDs, as the create endpoint requires. Unknown privileges are reported before the @@ -186,11 +295,13 @@ role is created. | `--name` | string | yes* | Role name | | `--description` | string | | Role description | | `--privilege` | string[] | yes* | Privilege name or ID; repeatable or comma-separated | +| `--from-role` | string | | Copy privileges and description from a role (name or ID) | | `--from-file` | string | | JSON file with `name`, `description` and `privileges` | -\* Can be supplied through `--from-file` instead. Flags override `name` and -`description` from the file; `--privilege` values are added to the file's -`privileges`. +\* Can be supplied through `--from-file` instead, and privileges through +`--from-role`. On a terminal, missing values are collected by the wizard. +Flags override `name` and `description` from the file; `--privilege` values +are added to the file's `privileges`. All [global flags](../../README.md#global-flags) apply. @@ -210,16 +321,30 @@ privilege names or IDs. ### Examples ```bash +# Interactive wizard +iics role create + iics role create --name "CAI Viewer" --description "View CAI assets" \ --privilege view.ai.designer --privilege view.ai.assets +# Clone an existing role and add one privilege +iics role create --name "Designer Plus" --from-role Designer \ + --privilege view.ai.console + iics role create --from-file cai-viewer-role.json ``` ```powershell +# Interactive wizard +iics role create + iics role create --name "CAI Viewer" --description "View CAI assets" ` --privilege view.ai.designer --privilege view.ai.assets +# Clone an existing role and add one privilege +iics role create --name "Designer Plus" --from-role Designer ` + --privilege view.ai.console + iics role create --from-file cai-viewer-role.json ``` @@ -231,14 +356,19 @@ Add privileges to a custom role. Sends `PUT /public/core/v3/roles//addPrivileges`, or `PUT /public/core/v3/roles/name//addPrivileges` when `--name` is used. +Without `--privilege` on a terminal, the +[interactive privilege editor](#interactive-privilege-editor) opens in +add-only mode (with a role picker when `--id` and `--name` are omitted). + ### Flags | Flag | Type | Required | Description | | ------------- | -------- | -------- | --------------------------------------------------- | -| `--id` | string | one of | Role ID | -| `--name` | string | one of | Role name | -| `--privilege` | string[] | yes | Privilege name or ID; repeatable or comma-separated | +| `--id` | string | one of* | Role ID | +| `--name` | string | one of* | Role name | +| `--privilege` | string[] | yes* | Privilege name or ID; repeatable or comma-separated | +\* Optional on a terminal, where the interactive editor is used instead. `--id` and `--name` are mutually exclusive. Privilege IDs are converted to names, which is what the endpoint expects. @@ -247,6 +377,9 @@ All [global flags](../../README.md#global-flags) apply. ### Examples ```bash +# Interactive editor (add-only) +iics role add-privileges --name "CAI Viewer" + iics role add-privileges --name "CAI Viewer" --privilege view.ai.console iics role add-privileges --id \ @@ -269,19 +402,28 @@ Remove privileges from a custom role. Sends `PUT /public/core/v3/roles/name//removePrivileges` when `--name` is used. A role must keep at least one privilege. +Without `--privilege` on a terminal, the +[interactive privilege editor](#interactive-privilege-editor) opens in +remove-only mode. + ### Flags | Flag | Type | Required | Description | | ------------- | -------- | -------- | --------------------------------------------------- | -| `--id` | string | one of | Role ID | -| `--name` | string | one of | Role name | -| `--privilege` | string[] | yes | Privilege name or ID; repeatable or comma-separated | +| `--id` | string | one of* | Role ID | +| `--name` | string | one of* | Role name | +| `--privilege` | string[] | yes* | Privilege name or ID; repeatable or comma-separated | + +\* Optional on a terminal, where the interactive editor is used instead. All [global flags](../../README.md#global-flags) apply. ### Examples ```bash +# Interactive editor (remove-only) +iics role remove-privileges --name "CAI Viewer" + iics role remove-privileges --name "CAI Viewer" --privilege view.ai.console ``` diff --git a/go.mod b/go.mod index 7f24ea7..d1815f9 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,9 @@ module github.com/jbrazda/iics-cli go 1.25.0 require ( + github.com/charmbracelet/bubbles v1.0.0 + github.com/charmbracelet/bubbletea v1.3.10 + github.com/charmbracelet/huh v1.0.0 github.com/charmbracelet/lipgloss v1.1.0 github.com/mattn/go-isatty v0.0.24 github.com/spf13/cobra v1.10.2 @@ -13,20 +16,30 @@ require ( ) require ( + github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect - github.com/charmbracelet/x/ansi v0.8.0 // indirect - github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect - github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/catppuccin/go v0.3.0 // indirect + github.com/charmbracelet/colorprofile v0.4.1 // indirect + github.com/charmbracelet/x/ansi v0.11.6 // indirect + github.com/charmbracelet/x/cellbuf v0.0.15 // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.9.0 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect - github.com/clipperhouse/uax29/v2 v2.3.0 // indirect + github.com/clipperhouse/uax29/v2 v2.5.0 // indirect github.com/danieljoos/wincred v1.2.3 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/rivo/uniseg v0.4.7 // indirect diff --git a/go.sum b/go.sum index 261860a..e7bd0aa 100644 --- a/go.sum +++ b/go.sum @@ -1,24 +1,58 @@ +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= +github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= +github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= +github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= +github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= +github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= +github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= +github.com/charmbracelet/huh v1.0.0 h1:wOnedH8G4qzJbmhftTqrpppyqHakl/zbbNdXIWJyIxw= +github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= -github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= -github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= -github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= -github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= -github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= -github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= +github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= +github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= +github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= +github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= +github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI= +github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4= +github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA= +github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA= github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= -github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= -github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U= +github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= @@ -35,12 +69,20 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= @@ -79,8 +121,9 @@ github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPc github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= -golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= diff --git a/internal/client/privilegegroups.go b/internal/client/privilegegroups.go new file mode 100644 index 0000000..030274e --- /dev/null +++ b/internal/client/privilegegroups.go @@ -0,0 +1,141 @@ +package client + +import ( + "sort" + "strings" +) + +// PrivilegeAction is the leading verb of an "." privilege name. +type PrivilegeAction string + +// Privilege actions recognized when grouping privileges by object. +const ( + ActionView PrivilegeAction = "view" + ActionCreate PrivilegeAction = "create" + ActionUpdate PrivilegeAction = "update" + ActionDelete PrivilegeAction = "delete" + ActionExecute PrivilegeAction = "execute" + ActionChangePerm PrivilegeAction = "changeperm" +) + +// PrivilegeActions lists the recognized actions in display order. +var PrivilegeActions = []PrivilegeAction{ + ActionView, ActionCreate, ActionUpdate, ActionDelete, ActionExecute, ActionChangePerm, +} + +// NoServiceGroup is the service name used for privileges with an empty service. +const NoServiceGroup = "(no service)" + +// PrivilegeObject groups the action privileges that apply to one object type, +// e.g. "data.transfer.task" with its view/create/update/delete privileges. +type PrivilegeObject struct { + Key string + Actions map[PrivilegeAction]Privilege +} + +// PrivilegeService groups the privileges of one service. Privileges whose +// names do not follow "." are listed in Other. +type PrivilegeService struct { + Name string + Objects []PrivilegeObject + Other []Privilege +} + +// Count returns the number of privileges in the service. +func (s PrivilegeService) Count() int { + n := len(s.Other) + for _, o := range s.Objects { + n += len(o.Actions) + } + return n +} + +// GroupPrivileges groups privileges by service, then by object and action. +// Services, objects and Other entries are sorted case-insensitively by name. +func GroupPrivileges(all []Privilege) []PrivilegeService { + actions := make(map[string]PrivilegeAction, len(PrivilegeActions)) + for _, a := range PrivilegeActions { + actions[string(a)] = a + } + + type svcAcc struct { + objects map[string]*PrivilegeObject + other []Privilege + } + services := make(map[string]*svcAcc) + + for _, p := range all { + name := p.Service + if name == "" { + name = NoServiceGroup + } + acc, ok := services[name] + if !ok { + acc = &svcAcc{objects: make(map[string]*PrivilegeObject)} + services[name] = acc + } + verb, object, found := strings.Cut(p.Name, ".") + action, known := actions[verb] + if !found || !known || object == "" { + acc.other = append(acc.other, p) + continue + } + obj, ok := acc.objects[object] + if !ok { + obj = &PrivilegeObject{Key: object, Actions: make(map[PrivilegeAction]Privilege)} + acc.objects[object] = obj + } + obj.Actions[action] = p + } + + result := make([]PrivilegeService, 0, len(services)) + for name, acc := range services { + svc := PrivilegeService{Name: name, Other: acc.other} + for _, o := range acc.objects { + svc.Objects = append(svc.Objects, *o) + } + sort.Slice(svc.Objects, func(i, j int) bool { + return lessFold(svc.Objects[i].Key, svc.Objects[j].Key) + }) + sort.Slice(svc.Other, func(i, j int) bool { + return lessFold(svc.Other[i].Name, svc.Other[j].Name) + }) + result = append(result, svc) + } + sort.Slice(result, func(i, j int) bool { + return lessFold(result[i].Name, result[j].Name) + }) + return result +} + +// DiffPrivileges returns the privilege names to add and remove to turn +// current into desired. Both results are sorted. +func DiffPrivileges(current, desired []string) (add, remove []string) { + cur := make(map[string]bool, len(current)) + for _, n := range current { + cur[n] = true + } + want := make(map[string]bool, len(desired)) + for _, n := range desired { + want[n] = true + if !cur[n] { + add = append(add, n) + } + } + for _, n := range current { + if !want[n] { + remove = append(remove, n) + } + } + sort.Strings(add) + sort.Strings(remove) + return add, remove +} + +func lessFold(a, b string) bool { + la, lb := strings.ToLower(a), strings.ToLower(b) + if la != lb { + return la < lb + } + return a < b +} diff --git a/internal/client/privilegegroups_test.go b/internal/client/privilegegroups_test.go new file mode 100644 index 0000000..58d7ff0 --- /dev/null +++ b/internal/client/privilegegroups_test.go @@ -0,0 +1,70 @@ +package client + +import ( + "reflect" + "testing" +) + +func TestGroupPrivileges(t *testing.T) { + all := []Privilege{ + {ID: "1", Name: "create.data.transfer.task", Service: "DI"}, + {ID: "2", Name: "view.data.transfer.task", Service: "DI"}, + {ID: "3", Name: "delete.mapping", Service: "DI"}, + {ID: "4", Name: "edc.iics.discovery", Service: "DI"}, + {ID: "5", Name: "feature.mcp.SuperAdmin", Service: "MetadataControlPlane"}, + {ID: "6", Name: "changeperm.test_case", Service: ""}, + {ID: "7", Name: "view.", Service: "DI"}, + {ID: "8", Name: "PROFILE.viewResults", Service: "Profile"}, + } + + got := GroupPrivileges(all) + + names := make([]string, len(got)) + for i, s := range got { + names[i] = s.Name + } + wantNames := []string{NoServiceGroup, "DI", "MetadataControlPlane", "Profile"} + if !reflect.DeepEqual(names, wantNames) { + t.Fatalf("services = %v, want %v", names, wantNames) + } + + di := got[1] + if len(di.Objects) != 2 || di.Objects[0].Key != "data.transfer.task" || di.Objects[1].Key != "mapping" { + t.Fatalf("DI objects = %+v", di.Objects) + } + dtt := di.Objects[0].Actions + if dtt[ActionCreate].ID != "1" || dtt[ActionView].ID != "2" || len(dtt) != 2 { + t.Errorf("data.transfer.task actions = %+v", dtt) + } + if len(di.Other) != 2 || di.Other[0].Name != "edc.iics.discovery" || di.Other[1].Name != "view." { + t.Errorf("DI other = %+v", di.Other) + } + if di.Count() != 5 { + t.Errorf("DI count = %d, want 5", di.Count()) + } + + if got[0].Objects[0].Key != "test_case" || got[0].Objects[0].Actions[ActionChangePerm].ID != "6" { + t.Errorf("no-service group = %+v", got[0]) + } + if len(got[2].Other) != 1 || len(got[3].Other) != 1 { + t.Errorf("unrecognized names should go to Other: %+v %+v", got[2], got[3]) + } +} + +func TestDiffPrivileges(t *testing.T) { + add, remove := DiffPrivileges( + []string{"view.a", "create.a", "delete.a"}, + []string{"view.a", "update.a", "execute.a"}, + ) + if !reflect.DeepEqual(add, []string{"execute.a", "update.a"}) { + t.Errorf("add = %v", add) + } + if !reflect.DeepEqual(remove, []string{"create.a", "delete.a"}) { + t.Errorf("remove = %v", remove) + } + + add, remove = DiffPrivileges([]string{"x"}, []string{"x"}) + if add != nil || remove != nil { + t.Errorf("expected no changes, got add=%v remove=%v", add, remove) + } +} diff --git a/internal/client/roles.go b/internal/client/roles.go index 1f9b738..e5c5c67 100644 --- a/internal/client/roles.go +++ b/internal/client/roles.go @@ -99,6 +99,20 @@ func (c *Client) GetRole(ctx context.Context, opts RoleGetOptions) (*Role, error return &resp[0], nil } +// FindRole looks up a role by name, falling back to role ID when no role has +// that name. +func (c *Client) FindRole(ctx context.Context, nameOrID string, expandPrivileges bool) (*Role, error) { + role, err := c.GetRole(ctx, RoleGetOptions{Name: nameOrID, ExpandPrivileges: expandPrivileges}) + if err == nil { + return role, nil + } + byID, idErr := c.GetRole(ctx, RoleGetOptions{ID: nameOrID, ExpandPrivileges: expandPrivileges}) + if idErr == nil { + return byID, nil + } + return nil, err +} + // CreateRoleRequest is the POST body for creating a custom role. Privileges // holds privilege IDs; the API requires at least one. type CreateRoleRequest struct { diff --git a/internal/client/roles_test.go b/internal/client/roles_test.go index f46e192..d53ba55 100644 --- a/internal/client/roles_test.go +++ b/internal/client/roles_test.go @@ -211,3 +211,27 @@ func TestDeleteRole(t *testing.T) { t.Fatalf("DeleteRole() error: %v", err) } } + +func TestFindRoleFallsBackToID(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch r.URL.Query().Get("q") { + case `roleId=="abc123"`: + _, _ = w.Write([]byte(`[{"id": "abc123", "roleName": "Designer"}]`)) + default: + _, _ = w.Write([]byte(`[]`)) + } + }) + c := newTestClient(handler) + + role, err := c.FindRole(context.Background(), "abc123", false) + if err != nil { + t.Fatalf("FindRole() error: %v", err) + } + if role.RoleName != "Designer" { + t.Errorf("expected Designer, got %s", role.RoleName) + } + if _, err := c.FindRole(context.Background(), "missing", false); err == nil || !strings.Contains(err.Error(), "not found") { + t.Errorf("expected not found error, got %v", err) + } +} diff --git a/internal/tui/privmatrix/model.go b/internal/tui/privmatrix/model.go new file mode 100644 index 0000000..fc58bce --- /dev/null +++ b/internal/tui/privmatrix/model.go @@ -0,0 +1,536 @@ +// Package privmatrix provides a Bubble Tea model for toggling the privileges +// of one service, laid out as a grid of objects (rows) by actions (columns). +package privmatrix + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/jbrazda/iics-cli/internal/client" +) + +// Mode restricts which cells can be toggled. +type Mode int + +const ( + // ModeEdit allows adding and removing privileges. + ModeEdit Mode = iota + // ModeAdd only allows adding; assigned privileges are locked. + ModeAdd + // ModeRemove only shows assigned privileges and only allows removing them. + ModeRemove +) + +// Options configures the model. +type Options struct { + Title string + Mode Mode + NoColor bool +} + +// rowKind distinguishes grid rows. +type rowKind int + +const ( + rowObject rowKind = iota + rowHeader + rowOther +) + +type row struct { + kind rowKind + object *client.PrivilegeObject + other *client.Privilege +} + +var columnHeaders = []string{"VIEW", "CREATE", "UPDATE", "DELETE", "EXEC", "PERM"} + +const ( + cellWidth = 8 + chromeLines = 6 // title, blank, column header, blank, detail, help +) + +// Model is the Bubble Tea model for one service's privilege grid. +type Model struct { + svc client.PrivilegeService + assigned map[string]bool + selected map[string]bool + opts Options + + rows []row + cursorRow int + cursorCol int + offset int + width int + height int + + filter textinput.Model + filtering bool + done bool + + styles styles +} + +type styles struct { + title, header, dim, add, remove, keep, cursor, detail lipgloss.Style +} + +func newStyles(noColor bool) styles { + if noColor { + plain := lipgloss.NewStyle() + return styles{ + title: plain.Bold(true), + header: plain.Bold(true), + dim: plain, + add: plain, + remove: plain, + keep: plain.Bold(true), + cursor: plain.Reverse(true), + detail: plain, + } + } + return styles{ + title: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("6")), + header: lipgloss.NewStyle().Bold(true), + dim: lipgloss.NewStyle().Foreground(lipgloss.Color("244")), + add: lipgloss.NewStyle().Foreground(lipgloss.Color("2")).Bold(true), + remove: lipgloss.NewStyle().Foreground(lipgloss.Color("1")).Bold(true), + keep: lipgloss.NewStyle().Bold(true), + cursor: lipgloss.NewStyle().Reverse(true), + detail: lipgloss.NewStyle().Foreground(lipgloss.Color("250")), + } +} + +// New creates a model. assigned holds the privilege names the role has now; +// selected holds the desired state and is updated in place. +func New(svc client.PrivilegeService, assigned, selected map[string]bool, opts Options) *Model { + ti := textinput.New() + ti.Prompt = "/" + ti.Placeholder = "filter" + m := &Model{ + svc: svc, + assigned: assigned, + selected: selected, + opts: opts, + filter: ti, + width: 100, + height: 30, + styles: newStyles(opts.NoColor), + } + m.rebuildRows() + return m +} + +// Init implements tea.Model. +func (m *Model) Init() tea.Cmd { return nil } + +// Done reports whether the user left the grid. +func (m *Model) Done() bool { return m.done } + +// visible reports whether a privilege is shown in the current mode. +func (m *Model) visible(p client.Privilege) bool { + return m.opts.Mode != ModeRemove || m.assigned[p.Name] +} + +// locked reports whether a privilege cannot be toggled in the current mode. +func (m *Model) locked(p client.Privilege) bool { + switch m.opts.Mode { + case ModeAdd: + return m.assigned[p.Name] + case ModeRemove: + return !m.assigned[p.Name] + } + return false +} + +func (m *Model) matches(p client.Privilege, q string) bool { + return strings.Contains(strings.ToLower(p.Name), q) || strings.Contains(strings.ToLower(p.Description), q) +} + +func (m *Model) rebuildRows() { + q := strings.ToLower(strings.TrimSpace(m.filter.Value())) + m.rows = m.rows[:0] + for i := range m.svc.Objects { + obj := &m.svc.Objects[i] + show := false + for _, p := range obj.Actions { + if m.visible(p) && (q == "" || strings.Contains(strings.ToLower(obj.Key), q) || m.matches(p, q)) { + show = true + break + } + } + if show { + m.rows = append(m.rows, row{kind: rowObject, object: obj}) + } + } + var others []row + for i := range m.svc.Other { + p := &m.svc.Other[i] + if m.visible(*p) && (q == "" || m.matches(*p, q)) { + others = append(others, row{kind: rowOther, other: p}) + } + } + if len(others) > 0 { + m.rows = append(m.rows, row{kind: rowHeader}) + m.rows = append(m.rows, others...) + } + if m.cursorRow >= len(m.rows) { + m.cursorRow = len(m.rows) - 1 + } + if m.cursorRow < 0 { + m.cursorRow = 0 + } + m.skipHeader(1) + m.clampCol() +} + +// cell returns the privilege at a grid position, if any. +func (m *Model) cell(r, c int) (client.Privilege, bool) { + if r < 0 || r >= len(m.rows) { + return client.Privilege{}, false + } + switch m.rows[r].kind { + case rowObject: + if c < 0 || c >= len(client.PrivilegeActions) { + return client.Privilege{}, false + } + p, ok := m.rows[r].object.Actions[client.PrivilegeActions[c]] + if !ok || !m.visible(p) { + return client.Privilege{}, false + } + return p, true + case rowOther: + if c != 0 { + return client.Privilege{}, false + } + return *m.rows[r].other, true + } + return client.Privilege{}, false +} + +func (m *Model) skipHeader(dir int) { + if len(m.rows) == 0 { + return + } + if m.rows[m.cursorRow].kind == rowHeader { + next := m.cursorRow + dir + if next < 0 || next >= len(m.rows) { + next = m.cursorRow - dir + } + if next >= 0 && next < len(m.rows) { + m.cursorRow = next + } + } +} + +func (m *Model) clampCol() { + if len(m.rows) == 0 { + m.cursorCol = 0 + return + } + if m.rows[m.cursorRow].kind == rowOther { + m.cursorCol = 0 + return + } + if m.cursorCol >= len(client.PrivilegeActions) { + m.cursorCol = len(client.PrivilegeActions) - 1 + } +} + +func (m *Model) toggle(p client.Privilege) { + if m.locked(p) { + return + } + m.selected[p.Name] = !m.selected[p.Name] +} + +// setAll selects all toggleable privileges in ps, or clears them when every +// toggleable one is already selected. +func (m *Model) setAll(ps []client.Privilege) { + allOn := true + for _, p := range ps { + if !m.locked(p) && !m.selected[p.Name] { + allOn = false + break + } + } + for _, p := range ps { + if !m.locked(p) { + m.selected[p.Name] = !allOn + } + } +} + +func (m *Model) rowPrivileges(r int) []client.Privilege { + var ps []client.Privilege + for c := range client.PrivilegeActions { + if p, ok := m.cell(r, c); ok { + ps = append(ps, p) + } + } + if len(ps) == 0 { + if p, ok := m.cell(r, 0); ok { + ps = append(ps, p) + } + } + return ps +} + +func (m *Model) columnPrivileges(c int) []client.Privilege { + var ps []client.Privilege + for r, rw := range m.rows { + if rw.kind != rowObject { + continue + } + if p, ok := m.cell(r, c); ok { + ps = append(ps, p) + } + } + return ps +} + +func (m *Model) pageSize() int { + n := m.height - chromeLines + if n < 3 { + n = 3 + } + return n +} + +func (m *Model) moveRow(delta int) { + if len(m.rows) == 0 { + return + } + dir := 1 + if delta < 0 { + dir = -1 + } + m.cursorRow += delta + if m.cursorRow < 0 { + m.cursorRow = 0 + } + if m.cursorRow >= len(m.rows) { + m.cursorRow = len(m.rows) - 1 + } + m.skipHeader(dir) + m.clampCol() +} + +func (m *Model) ensureVisible() { + page := m.pageSize() + if m.cursorRow < m.offset { + m.offset = m.cursorRow + } + if m.cursorRow >= m.offset+page { + m.offset = m.cursorRow - page + 1 + } + if m.offset < 0 { + m.offset = 0 + } +} + +// Update implements tea.Model. +func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width, m.height = msg.Width, msg.Height + m.ensureVisible() + return m, nil + case tea.KeyMsg: + if m.filtering { + switch msg.String() { + case "enter", "esc", "down", "up": + m.filtering = false + m.filter.Blur() + return m, nil + } + var cmd tea.Cmd + m.filter, cmd = m.filter.Update(msg) + m.offset = 0 + m.rebuildRows() + return m, cmd + } + switch msg.String() { + case "ctrl+c", "esc", "enter", "q": + m.done = true + return m, tea.Quit + case "up", "k": + m.moveRow(-1) + case "down", "j": + m.moveRow(1) + case "pgup": + m.moveRow(-m.pageSize()) + case "pgdown": + m.moveRow(m.pageSize()) + case "home", "g": + m.cursorRow = 0 + m.skipHeader(1) + m.clampCol() + case "end", "G": + m.cursorRow = len(m.rows) - 1 + m.skipHeader(-1) + m.clampCol() + case "left", "h": + if m.cursorCol > 0 { + m.cursorCol-- + } + case "right", "l": + m.cursorCol++ + m.clampCol() + case " ", "x": + if p, ok := m.cell(m.cursorRow, m.cursorCol); ok { + m.toggle(p) + } + case "a": + m.setAll(m.rowPrivileges(m.cursorRow)) + case "c": + if len(m.rows) > 0 && m.rows[m.cursorRow].kind == rowObject { + m.setAll(m.columnPrivileges(m.cursorCol)) + } + case "/": + m.filtering = true + m.filter.Focus() + return m, textinput.Blink + case "ctrl+u": + m.filter.SetValue("") + m.rebuildRows() + } + m.ensureVisible() + } + return m, nil +} + +func (m *Model) mark(p client.Privilege) string { + orig, want := m.assigned[p.Name], m.selected[p.Name] + switch { + case orig && want: + if m.locked(p) { + return m.styles.dim.Render("[x]") + } + return m.styles.keep.Render("[x]") + case !orig && want: + return m.styles.add.Render("[+]") + case orig && !want: + return m.styles.remove.Render("[-]") + } + if m.locked(p) { + return m.styles.dim.Render("[ ]") + } + return "[ ]" +} + +func truncate(s string, n int) string { + if n <= 1 { + return "" + } + r := []rune(s) + if len(r) <= n { + return s + } + return string(r[:n-1]) + "…" +} + +func (m *Model) keyWidth() int { + w := m.width - cellWidth*len(columnHeaders) - 3 + if w > 48 { + w = 48 + } + if w < 16 { + w = 16 + } + return w +} + +// View implements tea.Model. +func (m *Model) View() string { + var b strings.Builder + kw := m.keyWidth() + + title := fmt.Sprintf("%s > %s (%d)", m.opts.Title, m.svc.Name, m.svc.Count()) + b.WriteString(m.styles.title.Render(title)) + if m.filtering || m.filter.Value() != "" { + b.WriteString(" " + m.filter.View()) + } + b.WriteString("\n\n") + + hdr := fmt.Sprintf(" %-*s ", kw, "OBJECT") + for _, h := range columnHeaders { + hdr += fmt.Sprintf("%-*s", cellWidth, h) + } + b.WriteString(m.styles.header.Render(strings.TrimRight(hdr, " ")) + "\n") + + if len(m.rows) == 0 { + b.WriteString(m.styles.dim.Render(" no matching privileges") + "\n") + } + end := m.offset + m.pageSize() + if end > len(m.rows) { + end = len(m.rows) + } + for r := m.offset; r < end; r++ { + rw := m.rows[r] + pointer := " " + if r == m.cursorRow { + pointer = "> " + } + switch rw.kind { + case rowHeader: + b.WriteString(m.styles.dim.Render(" -- Other --") + "\n") + case rowObject: + b.WriteString(pointer + fmt.Sprintf("%-*s ", kw, truncate(rw.object.Key, kw))) + for c := range client.PrivilegeActions { + cellText := " . " + if p, ok := m.cell(r, c); ok { + cellText = m.mark(p) + } else { + cellText = m.styles.dim.Render(cellText) + } + if r == m.cursorRow && c == m.cursorCol { + cellText = m.styles.cursor.Render(stripStyle(cellText)) + } + b.WriteString(cellText + strings.Repeat(" ", cellWidth-3)) + } + b.WriteString("\n") + case rowOther: + cellText := m.mark(*rw.other) + if r == m.cursorRow { + cellText = m.styles.cursor.Render(stripStyle(cellText)) + } + b.WriteString(pointer + fmt.Sprintf("%-*s ", kw, truncate(rw.other.Name, kw)) + cellText + "\n") + } + } + if len(m.rows) > m.pageSize() { + b.WriteString(m.styles.dim.Render(fmt.Sprintf(" rows %d-%d of %d", m.offset+1, end, len(m.rows))) + "\n") + } else { + b.WriteString("\n") + } + + if p, ok := m.cell(m.cursorRow, m.cursorCol); ok { + detail := p.Name + if p.Description != "" { + detail += " - " + p.Description + } + if p.Status != "" { + detail += " (" + p.Status + ")" + } + b.WriteString(m.styles.detail.Render(truncate(detail, m.width-1)) + "\n") + } else { + b.WriteString("\n") + } + help := "arrows move space toggle a row c column / filter ctrl+u clear filter enter/esc back" + b.WriteString(m.styles.dim.Render(truncate(help, m.width-1))) + return b.String() +} + +// stripStyle returns the three-character mark without ANSI styling so the +// cursor style can be applied cleanly. +func stripStyle(s string) string { + for _, mk := range []string{"[x]", "[+]", "[-]", "[ ]", " . "} { + if strings.Contains(s, mk) { + return mk + } + } + return s +} diff --git a/internal/tui/privmatrix/model_test.go b/internal/tui/privmatrix/model_test.go new file mode 100644 index 0000000..0082775 --- /dev/null +++ b/internal/tui/privmatrix/model_test.go @@ -0,0 +1,162 @@ +package privmatrix + +import ( + "strings" + "testing" + + tea "github.com/charmbracelet/bubbletea" + "github.com/jbrazda/iics-cli/internal/client" +) + +func testService() client.PrivilegeService { + return client.GroupPrivileges([]client.Privilege{ + {ID: "1", Name: "view.data.transfer.task", Service: "DI"}, + {ID: "2", Name: "create.data.transfer.task", Service: "DI"}, + {ID: "3", Name: "delete.data.transfer.task", Service: "DI"}, + {ID: "4", Name: "view.mapping", Service: "DI"}, + {ID: "5", Name: "update.mapping", Service: "DI"}, + {ID: "6", Name: "edc.iics.discovery", Service: "DI", Description: "EDC discovery"}, + })[0] +} + +func key(s string) tea.KeyMsg { + switch s { + case "down": + return tea.KeyMsg{Type: tea.KeyDown} + case "up": + return tea.KeyMsg{Type: tea.KeyUp} + case "right": + return tea.KeyMsg{Type: tea.KeyRight} + case "left": + return tea.KeyMsg{Type: tea.KeyLeft} + case "space": + return tea.KeyMsg{Type: tea.KeySpace, Runes: []rune{' '}} + case "enter": + return tea.KeyMsg{Type: tea.KeyEnter} + case "esc": + return tea.KeyMsg{Type: tea.KeyEsc} + } + return tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(s)} +} + +func press(m *Model, keys ...string) { + for _, k := range keys { + m.Update(key(k)) + } +} + +func TestToggleCell(t *testing.T) { + assigned := map[string]bool{"view.data.transfer.task": true} + selected := map[string]bool{"view.data.transfer.task": true} + m := New(testService(), assigned, selected, Options{Title: "R", NoColor: true}) + + // Row 0 is data.transfer.task; column 1 is create. + press(m, "right", "space") + if !selected["create.data.transfer.task"] { + t.Error("expected create.data.transfer.task selected") + } + // Column 0 (view) is assigned; toggling marks it for removal. + press(m, "left", "space") + if selected["view.data.transfer.task"] { + t.Error("expected view.data.transfer.task deselected") + } + view := m.View() + if !strings.Contains(view, "[+]") || !strings.Contains(view, "[-]") { + t.Errorf("view should show pending add and remove marks:\n%s", view) + } +} + +func TestRowAndColumnToggle(t *testing.T) { + selected := map[string]bool{} + m := New(testService(), map[string]bool{}, selected, Options{NoColor: true}) + + press(m, "a") + for _, n := range []string{"view.data.transfer.task", "create.data.transfer.task", "delete.data.transfer.task"} { + if !selected[n] { + t.Errorf("row toggle should select %s", n) + } + } + press(m, "a") + if selected["view.data.transfer.task"] { + t.Error("second row toggle should clear the row") + } + + press(m, "c") // column 0: view on both objects + if !selected["view.data.transfer.task"] || !selected["view.mapping"] { + t.Errorf("column toggle should select all views: %v", selected) + } +} + +func TestOtherRowsSkipHeader(t *testing.T) { + selected := map[string]bool{} + m := New(testService(), map[string]bool{}, selected, Options{NoColor: true}) + + press(m, "right", "right", "down", "down") + if m.rows[m.cursorRow].kind != rowOther { + t.Fatalf("cursor should skip the header and land on an Other row, got kind %v", m.rows[m.cursorRow].kind) + } + if m.cursorCol != 0 { + t.Errorf("Other rows have one column, cursorCol=%d", m.cursorCol) + } + press(m, "space") + if !selected["edc.iics.discovery"] { + t.Error("expected edc.iics.discovery selected") + } +} + +func TestFilter(t *testing.T) { + m := New(testService(), map[string]bool{}, map[string]bool{}, Options{NoColor: true}) + press(m, "/", "m", "a", "p", "enter") + if len(m.rows) != 1 || m.rows[0].object.Key != "mapping" { + t.Fatalf("filter should leave only mapping, got %d rows", len(m.rows)) + } + press(m, "/", "e", "d", "c") + if len(m.rows) != 0 { + // "mapedc" matches nothing + t.Fatalf("expected no rows, got %d", len(m.rows)) + } + press(m, "esc", "ctrl+u") + m.filter.SetValue("discovery") + m.rebuildRows() + if len(m.rows) != 2 || m.rows[1].other.Name != "edc.iics.discovery" { + t.Errorf("description filter should match the Other row, got %+v", m.rows) + } +} + +func TestAddModeLocksAssigned(t *testing.T) { + assigned := map[string]bool{"view.data.transfer.task": true} + selected := map[string]bool{"view.data.transfer.task": true} + m := New(testService(), assigned, selected, Options{Mode: ModeAdd, NoColor: true}) + press(m, "space") + if !selected["view.data.transfer.task"] { + t.Error("assigned privilege must stay selected in add mode") + } + press(m, "a") + if !selected["create.data.transfer.task"] || !selected["view.data.transfer.task"] { + t.Errorf("row toggle in add mode should add unassigned only: %v", selected) + } +} + +func TestRemoveModeShowsOnlyAssigned(t *testing.T) { + assigned := map[string]bool{"update.mapping": true} + selected := map[string]bool{"update.mapping": true} + m := New(testService(), assigned, selected, Options{Mode: ModeRemove, NoColor: true}) + if len(m.rows) != 1 || m.rows[0].object.Key != "mapping" { + t.Fatalf("remove mode should show only objects with assigned privileges, got %d rows", len(m.rows)) + } + press(m, "a") + if selected["update.mapping"] { + t.Error("row toggle in remove mode should deselect the assigned privilege") + } + if selected["view.mapping"] { + t.Error("unassigned privilege must not be selectable in remove mode") + } +} + +func TestQuitKeys(t *testing.T) { + m := New(testService(), map[string]bool{}, map[string]bool{}, Options{NoColor: true}) + _, cmd := m.Update(key("enter")) + if !m.Done() || cmd == nil { + t.Error("enter should finish the grid") + } +} diff --git a/internal/tui/roleeditor.go b/internal/tui/roleeditor.go new file mode 100644 index 0000000..b42dd37 --- /dev/null +++ b/internal/tui/roleeditor.go @@ -0,0 +1,296 @@ +// Package tui holds interactive terminal UIs built on huh and Bubble Tea. +package tui + +import ( + "errors" + "fmt" + "os" + "sort" + "strings" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/huh" + "github.com/jbrazda/iics-cli/internal/client" + "github.com/jbrazda/iics-cli/internal/tui/privmatrix" +) + +// ErrCanceled is returned when the user cancels an interactive flow. +var ErrCanceled = errors.New("canceled") + +// PrivilegeEditorOptions configures EditPrivileges. +type PrivilegeEditorOptions struct { + // Title is shown above each screen, e.g. "Role: CAI Viewer". + Title string + // Mode restricts the edit to adding or removing privileges. + Mode privmatrix.Mode + // ApplyLabel is the verb on the final confirmation, e.g. "Apply" or "Create role". + ApplyLabel string + NoColor bool +} + +const ( + choiceReview = "\x00review" + choiceCancel = "\x00cancel" +) + +// EditPrivileges runs the interactive privilege editor. assigned holds the +// privilege names the role has now; initial holds the starting selection +// (usually equal to assigned, or a cloned role's privileges on create). +// It returns the desired set of privilege names, sorted. +func EditPrivileges(all []client.Privilege, assigned, initial []string, opts PrivilegeEditorOptions) ([]string, error) { + services := client.GroupPrivileges(all) + assignedSet := toSet(assigned) + selected := toSet(initial) + if opts.ApplyLabel == "" { + opts.ApplyLabel = "Apply" + } + + if opts.Mode == privmatrix.ModeRemove { + services = filterServices(services, assignedSet) + if len(services) == 0 { + return nil, fmt.Errorf("role has no privileges to remove") + } + } + + lastService := "" + for { + choice := lastService + options := serviceOptions(services, assignedSet, selected) + sel := huh.NewSelect[string](). + Title(opts.Title). + Description(summaryLine(assignedSet, selected)). + Options(options...). + Height(min(len(options)+2, 22)). + Value(&choice) + if err := runForm(huh.NewForm(huh.NewGroup(sel))); err != nil { + return nil, err + } + + switch choice { + case choiceCancel: + return nil, ErrCanceled + case choiceReview: + desired, ok, err := review(services, assignedSet, selected, opts) + if err != nil { + return nil, err + } + if ok { + return desired, nil + } + continue + } + + lastService = choice + for i := range services { + if services[i].Name != choice { + continue + } + m := privmatrix.New(services[i], assignedSet, selected, privmatrix.Options{ + Title: opts.Title, + Mode: opts.Mode, + NoColor: opts.NoColor, + }) + p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithOutput(os.Stderr)) + if _, err := p.Run(); err != nil { + return nil, fmt.Errorf("privilege editor: %w", err) + } + } + } +} + +// review prints the pending changes and asks whether to apply them. ok is +// false when the user chose to keep editing. +func review(services []client.PrivilegeService, assigned, selected map[string]bool, opts PrivilegeEditorOptions) ([]string, bool, error) { + desired := setToSorted(selected) + add, remove := client.DiffPrivileges(setToSorted(assigned), desired) + + var b strings.Builder + if len(add) == 0 && len(remove) == 0 { + b.WriteString("No changes.") + } + serviceOf := make(map[string]string) + for _, s := range services { + for _, o := range s.Objects { + for _, p := range o.Actions { + serviceOf[p.Name] = s.Name + } + } + for _, p := range s.Other { + serviceOf[p.Name] = s.Name + } + } + // Keep the review compact: huh descriptions do not scroll. + const maxLines = 24 + lines := 0 + writeGroup := func(sign string, names []string) { + byService := make(map[string][]string) + for _, n := range names { + byService[serviceOf[n]] = append(byService[serviceOf[n]], n) + } + keys := make([]string, 0, len(byService)) + for k := range byService { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + if lines >= maxLines { + fmt.Fprintf(&b, " ... and %d more in %s\n", len(byService[k]), k) + continue + } + fmt.Fprintf(&b, "%s\n", k) + lines++ + for i, n := range byService[k] { + if lines >= maxLines { + fmt.Fprintf(&b, " ... and %d more\n", len(byService[k])-i) + break + } + fmt.Fprintf(&b, " %s %s\n", sign, n) + lines++ + } + } + } + if len(add) > 0 { + fmt.Fprintf(&b, "Add (%d):\n", len(add)) + writeGroup("+", add) + } + if len(remove) > 0 { + fmt.Fprintf(&b, "Remove (%d):\n", len(remove)) + writeGroup("-", remove) + } + + options := []huh.Option[string]{huh.NewOption("Back to editing", "back")} + if len(desired) == 0 { + b.WriteString("\nA role must keep at least one privilege.") + } else if len(add) > 0 || len(remove) > 0 || opts.ApplyLabel != "Apply" { + options = append([]huh.Option[string]{huh.NewOption(opts.ApplyLabel, "apply")}, options...) + } + options = append(options, huh.NewOption("Cancel", "cancel")) + + choice := options[0].Value + sel := huh.NewSelect[string](). + Title(opts.Title + " - review"). + Description(strings.TrimRight(b.String(), "\n")). + Options(options...). + Value(&choice) + if err := runForm(huh.NewForm(huh.NewGroup(sel))); err != nil { + return nil, false, err + } + switch choice { + case "apply": + return desired, true, nil + case "cancel": + return nil, false, ErrCanceled + } + return nil, false, nil +} + +func serviceOptions(services []client.PrivilegeService, assigned, selected map[string]bool) []huh.Option[string] { + width := 0 + for _, s := range services { + if len(s.Name) > width { + width = len(s.Name) + } + } + opts := make([]huh.Option[string], 0, len(services)+2) + for _, s := range services { + total, have, add, remove := 0, 0, 0, 0 + count := func(p client.Privilege) { + total++ + if selected[p.Name] { + have++ + } + if selected[p.Name] && !assigned[p.Name] { + add++ + } + if !selected[p.Name] && assigned[p.Name] { + remove++ + } + } + for _, o := range s.Objects { + for _, p := range o.Actions { + count(p) + } + } + for _, p := range s.Other { + count(p) + } + label := fmt.Sprintf("%-*s %4d privileges %4d selected", width, s.Name, total, have) + if add > 0 || remove > 0 { + label += fmt.Sprintf(" +%d -%d", add, remove) + } + opts = append(opts, huh.NewOption(label, s.Name)) + } + opts = append(opts, + huh.NewOption("> Review and apply", choiceReview), + huh.NewOption("x Cancel", choiceCancel), + ) + return opts +} + +func summaryLine(assigned, selected map[string]bool) string { + add, remove := client.DiffPrivileges(setToSorted(assigned), setToSorted(selected)) + return fmt.Sprintf("Select a service (/ to filter). %d selected, pending +%d -%d.", + countTrue(selected), len(add), len(remove)) +} + +// filterServices keeps only services (and their privileges) that have at +// least one assigned privilege. +func filterServices(services []client.PrivilegeService, assigned map[string]bool) []client.PrivilegeService { + var out []client.PrivilegeService + for _, s := range services { + keep := false + for _, o := range s.Objects { + for _, p := range o.Actions { + if assigned[p.Name] { + keep = true + } + } + } + for _, p := range s.Other { + if assigned[p.Name] { + keep = true + } + } + if keep { + out = append(out, s) + } + } + return out +} + +func runForm(f *huh.Form) error { + err := f.WithOutput(os.Stderr).Run() + if errors.Is(err, huh.ErrUserAborted) { + return ErrCanceled + } + return err +} + +func toSet(names []string) map[string]bool { + s := make(map[string]bool, len(names)) + for _, n := range names { + s[n] = true + } + return s +} + +func setToSorted(s map[string]bool) []string { + out := make([]string, 0, len(s)) + for n, ok := range s { + if ok { + out = append(out, n) + } + } + sort.Strings(out) + return out +} + +func countTrue(s map[string]bool) int { + n := 0 + for _, ok := range s { + if ok { + n++ + } + } + return n +} From ef41cc59ad5e38d72f0b02bf4bc31b67a3289792 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Tue, 29 Sep 2026 21:47:38 -0400 Subject: [PATCH 03/52] chore: move CR-0040 and role API bug to completed --- .../CR-0040-role-interactive-privilege-editor.md | 14 +++++++------- .../2026-09-29-role-get-unsupported-endpoint.md | 0 2 files changed, 7 insertions(+), 7 deletions(-) rename docs/ChangeRequests/{pending => completed}/CR-0040-role-interactive-privilege-editor.md (92%) rename docs/issues/{pending => completed}/2026-09-29-role-get-unsupported-endpoint.md (100%) diff --git a/docs/ChangeRequests/pending/CR-0040-role-interactive-privilege-editor.md b/docs/ChangeRequests/completed/CR-0040-role-interactive-privilege-editor.md similarity index 92% rename from docs/ChangeRequests/pending/CR-0040-role-interactive-privilege-editor.md rename to docs/ChangeRequests/completed/CR-0040-role-interactive-privilege-editor.md index 6483f47..26bc3d4 100644 --- a/docs/ChangeRequests/pending/CR-0040-role-interactive-privilege-editor.md +++ b/docs/ChangeRequests/completed/CR-0040-role-interactive-privilege-editor.md @@ -98,13 +98,13 @@ Without a terminal, behavior is unchanged: missing flags return an error. ## Acceptance Criteria -- [ ] `role edit` adds and removes privileges across services and applies +- [x] `role edit` adds and removes privileges across services and applies them in one confirmed step -- [ ] `role add-privileges` / `role remove-privileges` without `--privilege` +- [x] `role add-privileges` / `role remove-privileges` without `--privilege` open the editor in the matching mode -- [ ] `role create --from-role` creates a role with the source role's +- [x] `role create --from-role` creates a role with the source role's privileges -- [ ] Interactive `role create` supports cloning and editing before create -- [ ] Removing all privileges is blocked before any API call -- [ ] Non-terminal usage is unchanged -- [ ] `go build`, `go vet`, `golangci-lint`, `go test ./...` pass +- [x] Interactive `role create` supports cloning and editing before create +- [x] Removing all privileges is blocked before any API call +- [x] Non-terminal usage is unchanged +- [x] `go build`, `go vet`, `golangci-lint`, `go test ./...` pass diff --git a/docs/issues/pending/2026-09-29-role-get-unsupported-endpoint.md b/docs/issues/completed/2026-09-29-role-get-unsupported-endpoint.md similarity index 100% rename from docs/issues/pending/2026-09-29-role-get-unsupported-endpoint.md rename to docs/issues/completed/2026-09-29-role-get-unsupported-endpoint.md From 5e21c5dd00da4c5c4be905739b2fb0f987814ffa Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Tue, 29 Sep 2026 21:55:55 -0400 Subject: [PATCH 04/52] docs: add CR-0041, CR-0042 and permission API bug --- .../CR-0041-tui-shared-prompts-and-wizards.md | 121 ++++++++++++++++++ .../CR-0042-permission-interactive-editor.md | 88 +++++++++++++ ...09-30-permission-client-wrong-api-shape.md | 107 ++++++++++++++++ 3 files changed, 316 insertions(+) create mode 100644 docs/ChangeRequests/new/CR-0041-tui-shared-prompts-and-wizards.md create mode 100644 docs/ChangeRequests/new/CR-0042-permission-interactive-editor.md create mode 100644 docs/issues/new/2026-09-30-permission-client-wrong-api-shape.md diff --git a/docs/ChangeRequests/new/CR-0041-tui-shared-prompts-and-wizards.md b/docs/ChangeRequests/new/CR-0041-tui-shared-prompts-and-wizards.md new file mode 100644 index 0000000..67e037a --- /dev/null +++ b/docs/ChangeRequests/new/CR-0041-tui-shared-prompts-and-wizards.md @@ -0,0 +1,121 @@ +# CR-0041: Shared TUI prompts and interactive wizard refactor + +## CR Type + +- [x] Enhancement to existing command +- [x] Refactor + +## Problem + +Interactive mode across the CLI uses line-based helpers in `cmd/user_prompt.go` +(`promptSelect`, `promptMultiSelect`, `promptText`, `promptYesNo`). They print +numbered menus and read typed numbers, which does not scale to real org data: + +- Group and role checklists list every group or role and require typing + comma-separated numbers. +- The time zone prompt is a type-a-query-then-pick loop over ~600 zones. +- Pickers (`pickUserGroup`, `pickAgent`, `pickAgentService`, + `promptUserSearch`) are numbered menus with no search. +- The runtime wizard loops through "Add agents / Remove agents / Done" menus. + +CR-0040 introduced `charmbracelet/huh` and Bubble Tea for the role editor. +The rest of the CLI should use the same style. + +## Proposed Solution + +### Step 0 - shared prompt layer on huh + +Reimplement the shared helpers on huh, keeping their signatures so existing +call sites change behavior without code changes: + +| Helper | New behavior | +| ------ | ------------ | +| `promptSelect(label, options)` | `huh.Select`, `/` to filter, cursor keys; returns `-1` on Esc/Ctrl-C | +| `promptMultiSelect(label, options, defaults)` | `huh.MultiSelect` with defaults pre-checked, `/` to filter, `space` toggle, `ctrl+a` select all | +| `promptText(label, default)` | `huh.Input` with the default pre-filled | +| `promptYesNo(label, defaultYes)` | `huh.Confirm` | +| `promptPassword` / `promptPasswordConfirm` | `huh.Input` with password echo mode | + +- Move the implementations to `internal/tui/prompt.go`; keep thin wrappers in + `cmd/user_prompt.go` so call sites are unchanged. +- Add generic `tui.PickOne[T]` / `tui.PickMany[T]` helpers that take items and + a label function, used by the pickers below. +- Non-terminal stdin (answers piped in by scripts): run huh in accessible mode + (`WithAccessible(true)`), which reads line-based answers. Existing scripted + usage keeps working. Also honor an `IICS_ACCESSIBLE=1` environment variable + for screen readers. +- Output goes to stderr, as today. + +### 1. User wizard (`user create`, `user update`) + +`runUserWizard` (18 prompts) becomes one huh form with pages: + +1. **Identity** - authentication type (Native/SSO), first name, last name, + user name, email (validated), phone, title, description. +2. **Account** - state, force password change, time zone (filterable select + over the zone list, current value pre-selected). +3. **Membership** - groups and roles as filterable multi-selects with current + membership pre-checked. +4. **Review** (update only) - changed fields and `+/-` group and role changes, + then Apply / Back / Cancel. + +### 2. User group wizard (`usergroup create`, `usergroup update`) + +`runGroupWizard`: name and description inputs (create only, as today), roles +as a filterable multi-select with current roles pre-checked, and a review of +`+/-` role changes on update. Shares the role option builder with the user +wizard. + +### 3. Runtime environment wizard (`runtime create`, `runtime update`) + +`runRuntimeCreateWizard`: replace the add/remove menu loop with one filterable +multi-select of agents (assigned agents pre-checked; agents assigned to +another environment shown with a note and excluded), then a review of +`+/-` agents, then apply. + +### 4. Pickers + +`pickUserGroup`, `pickAgent`, `pickAgentService`, `promptUserSearch`, +`pickRole` use `tui.PickOne` with filtering. `promptUserSearch` lists users +directly (filterable) instead of the query loop. + +### Out of scope + +- Delete confirmations (`fmt.Scanln` in 7 commands) stay plain `[y/N]` so + scripts can pipe answers; they are consolidated into one `confirmDelete` + helper as a cleanup. +- `profile add` / `profile edit` (prompting lives in `internal/config`) - + separate CR. +- `permission set` grid - CR-0042. + +## Implementation + +- `internal/tui/prompt.go` - huh-based `Select`, `MultiSelect`, `Input`, + `Confirm`, `Password`, `PickOne[T]`, `PickMany[T]`, accessible mode + detection. +- `cmd/user_prompt.go` - wrappers delegate to `internal/tui`; `promptTimezone` + becomes a filterable select. +- `cmd/user.go` - `runUserWizard` rewritten as a paged form plus review. +- `cmd/usergroup_prompt.go` - `runGroupWizard`, `pickUserGroup`. +- `cmd/runtime_prompt.go` - agent multi-select plus review. +- `cmd/agent_service.go` - `pickAgent`, `pickAgentService`. +- `cmd/role_prompt.go` - `pickRole` uses `tui.PickOne`. +- New helper `confirmDelete` replacing duplicated `fmt.Scanln` blocks. +- Tests: `internal/tui/prompt_test.go` (accessible-mode line input for each + helper, defaults, cancel), diff helpers for group/role/agent review. +- Docs: `docs/documentation/user.md`, `usergroup.md`, `runtime.md`, + `agent.md` interactive sections (key bindings, accessible mode, + `IICS_ACCESSIBLE`); `make completions`. + +## Acceptance Criteria + +- [ ] Existing interactive commands work with arrow keys and `/` filtering +- [ ] Piped answers on non-terminal stdin still work (accessible mode) +- [ ] User wizard: paged form, filterable time zone, group and role + multi-selects pre-checked, review on update +- [ ] User group and runtime wizards: pre-checked multi-select plus review +- [ ] All pickers are filterable +- [ ] Delete confirmations unchanged in behavior, deduplicated +- [ ] Live test on `dev`: create/update/delete a throwaway user, group and + runtime environment through the wizards +- [ ] `go build`, `go vet`, `golangci-lint`, `go test ./...` pass diff --git a/docs/ChangeRequests/new/CR-0042-permission-interactive-editor.md b/docs/ChangeRequests/new/CR-0042-permission-interactive-editor.md new file mode 100644 index 0000000..b150cdf --- /dev/null +++ b/docs/ChangeRequests/new/CR-0042-permission-interactive-editor.md @@ -0,0 +1,88 @@ +# CR-0042: Interactive object permission editor + +## CR Type + +- [x] Enhancement to existing command +- [x] New command + +## Dependencies + +- Bug `docs/issues/new/2026-09-30-permission-client-wrong-api-shape.md` must + be fixed first (correct ACL structs and per-ACL endpoints). +- Reuses the TUI foundation from CR-0040 and CR-0041. + +## Problem + +Object permissions (ACLs) can only be changed by writing JSON. An ACL grants a +user or group five permissions on one object (read, update, delete, execute, +change permission), which is naturally a grid of principals by permission +types - the same shape as the role privilege matrix from CR-0040. Sharing an +object with several groups currently takes several hand-written requests. + +## Proposed Solution + +### `permission edit [--object-id | --path --type ]` + +1. **Object selection** - `--object-id`, or `--path` and `--type` resolved + with the existing lookup client. Without either on a terminal, a filterable + picker over projects and folders, then assets in the chosen location. +2. **ACL grid** - Bubble Tea model, generalized from + `internal/tui/privmatrix`: + + ```text + Object: Default/Sales/m_load_orders (DTEMPLATE) + PRINCIPAL TYPE READ UPDATE DELETE EXEC PERM + > Everyone GROUP [x] [x] [ ] [x] [ ] + Data Engineering GROUP [x] [+] [ ] [x] [ ] + larry@infa.com USER [x] [x] [x] [x] [x] + analyst@infa.com (new) USER [+] [ ] [ ] [ ] [ ] + n add principal d remove principal space toggle a row c column enter done + ``` + + - `n` opens a filterable picker of users and user groups not yet in the + ACL list; the new row starts with `read` checked. + - `d` marks a principal's ACL for deletion (row shown struck or `[-]`). + - Same marks as CR-0040: `[x]` unchanged, `[+]` grant, `[-]` revoke. +3. **Review** - per principal: create, update (changed permissions), or + delete; then Apply / Back / Cancel. +4. **Apply** - `POST` for new principals, `PUT ` for changed ones, + `DELETE ` for removed ones. Stops at the first error and reports + which changes were applied. + +### Guardrails + +- Before applying, call `checkAccess` for the object; if the current user + lacks `changePermission`, stop with an error before any change. +- Warn when the change removes the current user's own `changePermission`. + +### Non-interactive equivalent + +`permission set --object-id --from-file acls.json` (from the bug fix) +applies the same diff declaratively, so the TUI and scripts share one +`DiffACLs` function. + +## Implementation + +- `internal/client/permissions.go` - `DiffACLs(current, desired)` returning + create/update/delete sets (pure, tested). +- `internal/tui/grid/` - extract the reusable grid core (cursor, toggles, + row/column toggle, filter, marks, scrolling) from `privmatrix`; `privmatrix` + and the new ACL grid become thin adapters. Existing privmatrix tests keep + passing. +- `internal/tui/acleditor.go` - object picker, grid, add-principal picker, + review. +- `cmd/permission.go` - `permission edit`. +- Tests: `DiffACLs`, grid core, ACL adapter (add/remove principal rows). +- Docs: `docs/documentation/permission.md` (`permission edit`, keys, + guardrails), `README.md` row, `make completions`. + +## Acceptance Criteria + +- [ ] `permission edit` shows current ACLs as a grid and applies + create/update/delete changes after review +- [ ] Principals can be added from a filterable user/group list and removed +- [ ] Missing `changePermission` is detected before any change +- [ ] `permission set --from-file` and the editor share `DiffACLs` +- [ ] Role privilege editor behavior unchanged after the grid extraction +- [ ] Live test on `dev` against a throwaway object, restoring ACLs afterwards +- [ ] `go build`, `go vet`, `golangci-lint`, `go test ./...` pass diff --git a/docs/issues/new/2026-09-30-permission-client-wrong-api-shape.md b/docs/issues/new/2026-09-30-permission-client-wrong-api-shape.md new file mode 100644 index 0000000..a65781a --- /dev/null +++ b/docs/issues/new/2026-09-30-permission-client-wrong-api-shape.md @@ -0,0 +1,107 @@ +# BUG: permission commands do not match the v3 object permissions API + +--- + +## Symptoms + +`iics permission get`, `permission set` and `permission delete` use request +and response structs that do not match the +[Object permissions](https://docs.informatica.com/cloud-common-services/administrator/current-version/rest-api-reference/platform-rest-api-version-3-resources/object-permissions.html) +API. `permission set` calls an endpoint that does not exist. + +--- + +## Command / Reproduction Steps + +```bash +iics permission get --object-id --verbose +iics permission set --object-id --from-file perms.json --verbose +``` + +--- + +## Expected Behaviour + +Per the documentation: + +| Operation | Method and URI | +| --------- | -------------- | +| List ACLs of an object | `GET /public/core/v3/objects//permissions` | +| Get one ACL | `GET /public/core/v3/objects//permissions/` | +| Create an ACL (one principal) | `POST /public/core/v3/objects//permissions` | +| Update an ACL | `PUT /public/core/v3/objects//permissions/` | +| Delete one ACL | `DELETE /public/core/v3/objects//permissions/` | +| Delete all ACLs | `DELETE /public/core/v3/objects//permissions` | +| Check own access | `GET /public/core/v3/objects//permissions/checkAccess[?type=]` | + +`GET` returns an array of ACLs: + +```json +[ + { + "id": "4D6ER3yic8cjjE1GmxEKEi", + "principal": { + "type": "USER", + "name": "saki@infa.com" + }, + "permissions": { + "read": true, + "update": true, + "delete": true, + "execute": true, + "changePermission": true + } + } +] +``` + +POST and PUT bodies use the same `principal` (`type` is `USER` or `GROUP`, +`name`) and `permissions` objects. + +--- + +## Actual Behaviour + +`internal/client/permissions.go` models the response as a single +`ObjectPermission` object with `objectId`, `objectType` and a +`permissions` array of `{principalId, principalType, principalName, +permission}` entries, which does not match the API. `SetObjectPermissions` +sends `PUT /objects//permissions`, which the API does not define. + +--- + +## Architecture Layer + +- [x] **`cmd/`** - flag parsing, command wiring, output formatting +- [x] **`internal/client/`** - HTTP logic, API structs, request/response handling +- [ ] **`internal/config/`** - config file loading, session cache +- [ ] **`internal/output/`** - table / JSON / CSV renderer + +--- + +## Likely Affected Files + +```text +internal/client/permissions.go +cmd/permission.go +docs/documentation/permission.md +``` + +--- + +## Fix Instructions + +1. Replace the structs with `ObjectACL {id, principal {type, name}, + permissions {read, update, delete, execute, changePermission}}`. +2. Client methods: `ListObjectACLs`, `GetObjectACL`, `CreateObjectACL`, + `UpdateObjectACL`, `DeleteObjectACL`, `DeleteAllObjectACLs`, + `CheckObjectAccess`. +3. Commands: `permission get` (table: principal type, name, R/U/D/X/P), + `permission add`, `permission update`, `permission delete` (`--acl-id` or + `--all`), `permission check`. Keep `permission set --from-file` as a + declarative apply (diff file against current ACLs; create/update/delete). +4. Verify against a live object on `dev` before committing. +5. Tests with `newTestClient` for every method; update docs and completions. + +The interactive editor for permissions is tracked separately in CR-0042 and +builds on this fix. From 3af25c34ae17e50e5b4e02ea43b1fafb8cfb9b14 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Tue, 29 Sep 2026 22:01:19 -0400 Subject: [PATCH 05/52] feat(tui): shared huh-based prompts (CR-0041 step 0) - internal/tui/prompt.go: Prompter with Text, Secret, Select, MultiSelect, Confirm, Timezone and generic PickOne; huh forms on a terminal (arrows, / filter, checkboxes with defaults pre-checked), line-based fallback with the historical input format when stdin/stderr is not a terminal; one shared buffered reader so piped answers are not lost between prompts; IICS_ACCESSIBLE=1 enables huh accessible mode - cmd/user_prompt.go: promptText/Password/Select/MultiSelect/YesNo and promptTimezone delegate to the Prompter (signatures unchanged); promptUserSearch uses the shared helpers - cmd/role_prompt.go: pickRole uses tui.PickOne - tests for the line-based path, including sequential piped prompts - docs: interactive.md (keys, accessible mode, piped format), links from user/group/environment/agent/role docs, IICS_ACCESSIBLE in README; CR-0041 updated for the line-based fallback decision --- README.md | 1 + cmd/role_prompt.go | 29 +- cmd/user_prompt.go | 231 ++--------- .../CR-0041-tui-shared-prompts-and-wizards.md | 22 +- docs/documentation/agent.md | 6 +- docs/documentation/environment.md | 3 + docs/documentation/group.md | 1 + docs/documentation/interactive.md | 52 +++ docs/documentation/role.md | 1 + docs/documentation/user.md | 1 + internal/tui/prompt.go | 360 ++++++++++++++++++ internal/tui/prompt_test.go | 161 ++++++++ 12 files changed, 637 insertions(+), 231 deletions(-) create mode 100644 docs/documentation/interactive.md create mode 100644 internal/tui/prompt.go create mode 100644 internal/tui/prompt_test.go diff --git a/README.md b/README.md index d34b9a4..e3cc255 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,7 @@ env > `80`. | `IICS_OUTPUT` | Override default output format | | `IICS_THEME` | Override table theme (same values as `--theme` flag) | | `IICS_WIDTH` | Override auto-detected terminal width for responsive table output | +| `IICS_ACCESSIBLE` | Set to `1` for screen-reader friendly line-by-line prompts ([interactive prompts](docs/documentation/interactive.md)) | | `IICS_HTTP_TIMEOUT` | Override per-HTTP-request timeout in seconds (default `120`; `--http-timeout` flag wins if set) | | `IICS_VALID_DEPLOY_TARGETS` | Override valid target allowlist for `iics release` commands (comma-separated) | | `IICS_TARGET_PROFILE_MAP` | Override target to profile mapping for `iics release plan` (format `TARGET=profile,...`) | diff --git a/cmd/role_prompt.go b/cmd/role_prompt.go index 4a0b2ad..87fe8c2 100644 --- a/cmd/role_prompt.go +++ b/cmd/role_prompt.go @@ -50,18 +50,13 @@ func pickRole(ctx context.Context, c *client.Client, action string) (*client.Rol return nil, fmt.Errorf("no roles found") } sortRoles(roles) - idx := 0 - sel := huh.NewSelect[int](). - Title(fmt.Sprintf("Select a role to %s (/ to filter)", action)). - Options(roleOptions(roles)...). - Height(min(len(roles)+2, 22)). - Value(&idx) - if err := huh.NewForm(huh.NewGroup(sel)).WithOutput(os.Stderr).Run(); err != nil { - if errors.Is(err, huh.ErrUserAborted) { - return nil, fmt.Errorf("canceled") - } + idx, err := tui.PickOne(prompter, fmt.Sprintf("Select a role to %s", action), roles, roleLabel) + if err != nil { return nil, err } + if idx < 0 { + return nil, fmt.Errorf("canceled") + } return &roles[idx], nil } @@ -71,14 +66,18 @@ func sortRoles(roles []client.Role) { }) } +func roleLabel(r client.Role) string { + kind := "custom" + if r.SystemRole { + kind = "system" + } + return fmt.Sprintf("%s (%s)", r.RoleName, kind) +} + func roleOptions(roles []client.Role) []huh.Option[int] { opts := make([]huh.Option[int], len(roles)) for i, r := range roles { - kind := "custom" - if r.SystemRole { - kind = "system" - } - opts[i] = huh.NewOption(fmt.Sprintf("%s (%s)", r.RoleName, kind), i) + opts[i] = huh.NewOption(roleLabel(r), i) } return opts } diff --git a/cmd/user_prompt.go b/cmd/user_prompt.go index e12668a..e293cf8 100644 --- a/cmd/user_prompt.go +++ b/cmd/user_prompt.go @@ -1,46 +1,27 @@ package cmd import ( - "bufio" "context" "fmt" "os" - "strconv" - "strings" "github.com/jbrazda/iics-cli/internal/client" "github.com/jbrazda/iics-cli/internal/config" - "golang.org/x/term" + "github.com/jbrazda/iics-cli/internal/tui" ) +// prompter renders interactive prompts: huh forms on a terminal, line-based +// prompts (the historical input format) when stdin is not a terminal. +var prompter = tui.Default() + // promptText prompts for a line of text. Returns defaultVal if the user presses Enter. func promptText(label, defaultVal string) (string, error) { - if defaultVal != "" { - _, _ = fmt.Fprintf(os.Stderr, "%s [%s]: ", label, defaultVal) - } else { - _, _ = fmt.Fprintf(os.Stderr, "%s: ", label) - } - r := bufio.NewReader(os.Stdin) - line, err := r.ReadString('\n') - if err != nil { - return "", fmt.Errorf("reading input: %w", err) - } - line = strings.TrimSpace(line) - if line == "" { - return defaultVal, nil - } - return line, nil + return prompter.Text(label, defaultVal) } // promptPassword reads a password without echoing it to the terminal. func promptPassword(label string) (string, error) { - _, _ = fmt.Fprint(os.Stderr, label) - pw, err := term.ReadPassword(int(os.Stdin.Fd())) - _, _ = fmt.Fprintln(os.Stderr) - if err != nil { - return "", fmt.Errorf("reading password: %w", err) - } - return string(pw), nil + return prompter.Secret(label) } // promptPasswordConfirm reads a new password and a confirmation. Repeats until they @@ -65,110 +46,22 @@ func promptPasswordConfirm(label string) (string, error) { } } -// promptSelect presents a numbered menu and returns the 0-based index of the chosen -// option, or -1 if the user cancels (selects 0, presses Enter, or types 'q'). +// promptSelect presents a menu and returns the 0-based index of the chosen +// option, or -1 if the user cancels. func promptSelect(label string, options []string) (int, error) { - _, _ = fmt.Fprintf(os.Stderr, "%s:\n", label) - for i, opt := range options { - _, _ = fmt.Fprintf(os.Stderr, " [%d] %s\n", i+1, opt) - } - _, _ = fmt.Fprintln(os.Stderr, " [0] Cancel") - - r := bufio.NewReader(os.Stdin) - for { - _, _ = fmt.Fprint(os.Stderr, "Selection (q to cancel): ") - line, err := r.ReadString('\n') - if err != nil { - return -1, fmt.Errorf("reading selection: %w", err) - } - line = strings.TrimSpace(line) - if line == "0" || line == "" || strings.EqualFold(line, "q") { - return -1, nil - } - n, convErr := strconv.Atoi(line) - if convErr != nil || n < 1 || n > len(options) { - _, _ = fmt.Fprintf(os.Stderr, "Enter a number between 0 and %d, or 'q' to cancel.\n", len(options)) - continue - } - return n - 1, nil - } + return prompter.Select(label, options) } -// promptMultiSelect presents a numbered checklist where items marked with * are the -// current defaults. Returns the 0-based indices of selected items, or nil to skip. +// promptMultiSelect presents a checklist with defaults pre-selected. Returns the +// 0-based indices of selected items, or nil for none. func promptMultiSelect(label string, options []string, defaults []int) ([]int, error) { - defaultSet := make(map[int]bool, len(defaults)) - for _, d := range defaults { - defaultSet[d] = true - } - - _, _ = fmt.Fprintf(os.Stderr, "%s (comma-separated numbers, 0 = none):\n", label) - for i, opt := range options { - mark := " " - if defaultSet[i] { - mark = "*" - } - _, _ = fmt.Fprintf(os.Stderr, " [%d]%s %s\n", i+1, mark, opt) - } - - if len(defaults) > 0 { - defaultNums := make([]string, len(defaults)) - for i, d := range defaults { - defaultNums[i] = strconv.Itoa(d + 1) - } - _, _ = fmt.Fprintf(os.Stderr, "Selections [%s]: ", strings.Join(defaultNums, ",")) - } else { - _, _ = fmt.Fprint(os.Stderr, "Selections: ") - } - - r := bufio.NewReader(os.Stdin) - line, err := r.ReadString('\n') - if err != nil { - return nil, fmt.Errorf("reading selections: %w", err) - } - line = strings.TrimSpace(line) - - if line == "" && len(defaults) > 0 { - return defaults, nil - } - if line == "0" || line == "" { - return nil, nil - } - - parts := strings.Split(line, ",") - selected := make([]int, 0, len(parts)) - for _, p := range parts { - p = strings.TrimSpace(p) - n, convErr := strconv.Atoi(p) - if convErr != nil || n < 0 || n > len(options) { - return nil, fmt.Errorf("invalid selection %q; enter numbers between 0 and %d", p, len(options)) - } - if n == 0 { - return nil, nil - } - selected = append(selected, n-1) - } - return selected, nil + return prompter.MultiSelect(label, options, defaults) } // promptYesNo prompts for a yes/no answer. defaultYes controls what pressing Enter // returns. Returns true for yes, false for no. func promptYesNo(label string, defaultYes bool) (bool, error) { - hint := "[Y/n]" - if !defaultYes { - hint = "[y/N]" - } - _, _ = fmt.Fprintf(os.Stderr, "%s %s: ", label, hint) - r := bufio.NewReader(os.Stdin) - line, err := r.ReadString('\n') - if err != nil { - return false, fmt.Errorf("reading input: %w", err) - } - line = strings.TrimSpace(strings.ToLower(line)) - if line == "" { - return defaultYes, nil - } - return line == "y" || line == "yes", nil + return prompter.Confirm(label, defaultYes) } // promptUserSearch interactively finds a user by searching by name or exact ID. @@ -178,31 +71,20 @@ func promptUserSearch(ctx context.Context, c *client.Client) (*client.User, erro return nil, fmt.Errorf("stdin is not a terminal; provide --id or --username flag") } - r := bufio.NewReader(os.Stdin) for { - _, _ = fmt.Fprintln(os.Stderr, "\nSearch user by:") - _, _ = fmt.Fprintln(os.Stderr, " [1] User Name (partial match)") - _, _ = fmt.Fprintln(os.Stderr, " [2] ID (exact match)") - _, _ = fmt.Fprintln(os.Stderr, " [0] Exit") - _, _ = fmt.Fprint(os.Stderr, "Selection: ") - - line, err := r.ReadString('\n') + mode, err := promptSelect("Search user by", []string{"User Name (partial match)", "ID (exact match)"}) if err != nil { - return nil, fmt.Errorf("reading selection: %w", err) + return nil, err } - line = strings.TrimSpace(line) - - switch line { - case "0", "": + switch mode { + case -1: return nil, nil - case "1": - _, _ = fmt.Fprint(os.Stderr, "User Name (partial match): ") - query, qErr := r.ReadString('\n') + case 0: + query, qErr := promptText("User Name (partial match)", "") if qErr != nil { return nil, fmt.Errorf("reading user name: %w", qErr) } - query = strings.TrimSpace(query) if query == "" { continue } @@ -230,13 +112,11 @@ func promptUserSearch(ctx context.Context, c *client.Client) (*client.User, erro } return &users[idx], nil - case "2": - _, _ = fmt.Fprint(os.Stderr, "User ID (exact match): ") - id, iErr := r.ReadString('\n') + case 1: + id, iErr := promptText("User ID (exact match)", "") if iErr != nil { return nil, fmt.Errorf("reading user ID: %w", iErr) } - id = strings.TrimSpace(id) if id == "" { continue } @@ -246,9 +126,6 @@ func promptUserSearch(ctx context.Context, c *client.Client) (*client.User, erro continue } return u, nil - - default: - _, _ = fmt.Fprintln(os.Stderr, "Enter 0, 1, or 2.") } } } @@ -279,68 +156,10 @@ var iicsTimezones = []string{ "Pacific/Kiritimati", "Pacific/Norfolk", "Pacific/Tahiti", "UTC", "VST", } -// promptTimezone prompts for a timezone with substring search over iicsTimezones. -// The user types a search term; matching entries are shown as a numbered list. -// Enter with no input keeps the current value; "0" clears it. +// promptTimezone picks a timezone from iicsTimezones. The current value is +// pre-selected (terminal) or kept on Enter (line mode, where "0" clears it). func promptTimezone(current string) (string, error) { - r := bufio.NewReader(os.Stdin) - for { - if current != "" { - _, _ = fmt.Fprintf(os.Stderr, "Time Zone ID [%s] (type to search, Enter to keep, 0 to clear): ", current) - } else { - _, _ = fmt.Fprint(os.Stderr, "Time Zone ID (type to search, e.g. New_York, Europe, or 0 to skip): ") - } - - line, err := r.ReadString('\n') - if err != nil { - return "", fmt.Errorf("reading input: %w", err) - } - line = strings.TrimSpace(line) - - if line == "" { - return current, nil - } - if line == "0" { - return "", nil - } - - lower := strings.ToLower(line) - var matches []string - for _, tz := range iicsTimezones { - if strings.Contains(strings.ToLower(tz), lower) { - matches = append(matches, tz) - } - } - - if len(matches) == 0 { - _, _ = fmt.Fprintf(os.Stderr, "No matching timezone for %q. Try again.\n", line) - continue - } - if len(matches) == 1 { - _, _ = fmt.Fprintf(os.Stderr, "Selected: %s\n", matches[0]) - return matches[0], nil - } - - _, _ = fmt.Fprintf(os.Stderr, "Matches for %q:\n", line) - for i, tz := range matches { - _, _ = fmt.Fprintf(os.Stderr, " [%d] %s\n", i+1, tz) - } - _, _ = fmt.Fprint(os.Stderr, "Selection (0 to search again): ") - selLine, selErr := r.ReadString('\n') - if selErr != nil { - return "", fmt.Errorf("reading selection: %w", selErr) - } - selLine = strings.TrimSpace(selLine) - if selLine == "0" || selLine == "" { - continue - } - n, convErr := strconv.Atoi(selLine) - if convErr != nil || n < 1 || n > len(matches) { - _, _ = fmt.Fprintln(os.Stderr, "Invalid selection. Try again.") - continue - } - return matches[n-1], nil - } + return prompter.Timezone(iicsTimezones, current) } // resolveUser returns the user identified by id or userName flags, falling back to diff --git a/docs/ChangeRequests/new/CR-0041-tui-shared-prompts-and-wizards.md b/docs/ChangeRequests/new/CR-0041-tui-shared-prompts-and-wizards.md index 67e037a..0659029 100644 --- a/docs/ChangeRequests/new/CR-0041-tui-shared-prompts-and-wizards.md +++ b/docs/ChangeRequests/new/CR-0041-tui-shared-prompts-and-wizards.md @@ -40,9 +40,12 @@ call sites change behavior without code changes: `cmd/user_prompt.go` so call sites are unchanged. - Add generic `tui.PickOne[T]` / `tui.PickMany[T]` helpers that take items and a label function, used by the pickers below. -- Non-terminal stdin (answers piped in by scripts): run huh in accessible mode - (`WithAccessible(true)`), which reads line-based answers. Existing scripted - usage keeps working. Also honor an `IICS_ACCESSIBLE=1` environment variable +- Non-terminal stdin or stderr (answers piped in by scripts, stderr + redirected): keep the existing line-based prompts, moved to + `internal/tui` with one shared buffered reader. huh accessible mode is not + used here because its line protocol differs (for example a multi-select + toggles one number per line), which would break scripted answers. +- `IICS_ACCESSIBLE=1` runs the huh forms in accessible mode on a terminal, for screen readers. - Output goes to stderr, as today. @@ -103,14 +106,17 @@ directly (filterable) instead of the query loop. - New helper `confirmDelete` replacing duplicated `fmt.Scanln` blocks. - Tests: `internal/tui/prompt_test.go` (accessible-mode line input for each helper, defaults, cancel), diff helpers for group/role/agent review. -- Docs: `docs/documentation/user.md`, `usergroup.md`, `runtime.md`, - `agent.md` interactive sections (key bindings, accessible mode, - `IICS_ACCESSIBLE`); `make completions`. +- Docs: shared `docs/documentation/interactive.md` (keys, accessible mode, + piped input format) linked from `user.md`, `group.md`, `environment.md`, + `agent.md`, `role.md`; `IICS_ACCESSIBLE` in the README environment table; + `make completions`. ## Acceptance Criteria -- [ ] Existing interactive commands work with arrow keys and `/` filtering -- [ ] Piped answers on non-terminal stdin still work (accessible mode) +- [x] Existing interactive commands work with arrow keys and `/` filtering + (Step 0) +- [x] Piped answers on non-terminal stdin still work (line-based fallback, + Step 0) - [ ] User wizard: paged form, filterable time zone, group and role multi-selects pre-checked, review on update - [ ] User group and runtime wizards: pre-checked multi-select plus review diff --git a/docs/documentation/agent.md b/docs/documentation/agent.md index 1e17b3b..0c5a18c 100644 --- a/docs/documentation/agent.md +++ b/docs/documentation/agent.md @@ -322,8 +322,9 @@ for a platform. | ------ | ------ | -------- | ------------------------------------ | | `--os` | string | yes | Operating system: `win64` or `linux64` | -When `--os` is omitted and the session is interactive, the command shows a numbered -selection menu (`win64`/`linux64`); enter `0`, press Enter, or type `q` to cancel. +When `--os` is omitted and the session is interactive, the command shows a +selection list (`win64`/`linux64`); press `Esc` to cancel. See +[Interactive prompts](interactive.md). All [global flags](../../README.md#global-flags) apply. @@ -438,4 +439,5 @@ iics agent installer-download --installer-info info.json --target C:\Temp\agent. ## See also +- [Interactive prompts](interactive.md) - keys, accessible mode and piped input for wizards and pickers - [environment](environment.md) - manage runtime environments (Secure Agent groups) that contain agents diff --git a/docs/documentation/environment.md b/docs/documentation/environment.md index 6c66cb8..565a4ec 100644 --- a/docs/documentation/environment.md +++ b/docs/documentation/environment.md @@ -150,6 +150,8 @@ prompts. Without a file and without a terminal, `--from-file` is required. Agents to pick from; `Remove agents` removes from the working selection; `Done` creates the environment with the selected agents. +See [Interactive prompts](interactive.md) for keys and piped input. + All [global flags](../../README.md#global-flags) apply. ### Examples @@ -265,4 +267,5 @@ iics environment configs set --id --from-file props.json --yes ## See also +- [Interactive prompts](interactive.md) - keys, accessible mode and piped input for wizards and pickers - [agent](agent.md) - manage Secure Agents within runtime environments diff --git a/docs/documentation/group.md b/docs/documentation/group.md index 9b64fe4..d8ae46a 100644 --- a/docs/documentation/group.md +++ b/docs/documentation/group.md @@ -200,5 +200,6 @@ iics group delete # interactive picker ## See also +- [Interactive prompts](interactive.md) - keys, accessible mode and piped input for wizards and pickers - [user](user.md) - manage individual users - [role](role.md) - manage roles assignable to groups diff --git a/docs/documentation/interactive.md b/docs/documentation/interactive.md new file mode 100644 index 0000000..c476505 --- /dev/null +++ b/docs/documentation/interactive.md @@ -0,0 +1,52 @@ +# Interactive prompts + +Commands that run interactively (wizards such as `user create --interactive`, +`group create -i`, `environment create`, and pickers shown when `--id` or +`--name` is omitted) render terminal forms when stdin and stderr are both a +terminal. + +## Keys + +| Prompt | Keys | +| ------------ | -------------------------------------------------------------------- | +| Text input | Type, `Enter` to accept (an empty value keeps the shown default) | +| Single list | Arrows or `j` / `k` to move, `/` to filter, `Enter` to choose, `Esc` to cancel | +| Checklist | `Space` or `x` to toggle, `/` to filter, `Ctrl+A` to select all, `Enter` to confirm | +| Yes / No | Left / right or `y` / `n`, `Enter` to confirm | +| Any prompt | `Ctrl+C` cancels the command | + +Checklists open with the current values already checked (for example a user's +current groups and roles), so confirming without changes keeps them. + +The role privilege editor (`role edit`) has its own grid keys; see +[role](role.md#interactive-privilege-editor). + +## Accessible mode + +Set `IICS_ACCESSIBLE=1` to render prompts as plain, line-by-line questions that +work with screen readers: + +```bash +IICS_ACCESSIBLE=1 iics user create --interactive +``` + +```powershell +$env:IICS_ACCESSIBLE = "1"; iics user create --interactive +``` + +## Piped input + +When stdin or stderr is not a terminal (for example answers piped in by a +script, or stderr redirected to a file), prompts fall back to the line-based +format: + +| Prompt | Answer format | +| ----------- | --------------------------------------------------------------- | +| Text input | One line; empty keeps the default shown in brackets | +| Single list | Option number; `0`, `q` or empty cancels | +| Checklist | Comma-separated numbers (`1,3,5`); empty keeps defaults (marked `*`); `0` selects none | +| Yes / No | `y`, `yes`, `n`, `no`; empty uses the default | +| Time zone | Search text, then the number of a match; empty keeps the current value; `0` clears | + +Commands that require a terminal (for example `role edit`) report an error +instead of prompting. diff --git a/docs/documentation/role.md b/docs/documentation/role.md index 9faf0f3..993eb85 100644 --- a/docs/documentation/role.md +++ b/docs/documentation/role.md @@ -465,6 +465,7 @@ iics role delete --name "CAI Viewer" --yes ## See also +- [Interactive prompts](interactive.md) - keys, accessible mode and piped input for wizards and pickers - [privilege](privilege.md) - list available privileges to assign to roles - [user](user.md) - assign roles to users - [group](group.md) - assign roles to user groups diff --git a/docs/documentation/user.md b/docs/documentation/user.md index 47f24b1..85f058a 100644 --- a/docs/documentation/user.md +++ b/docs/documentation/user.md @@ -281,6 +281,7 @@ iics user reset-password --id --security-answer --new-passwor ## See also +- [Interactive prompts](interactive.md) - keys, accessible mode and piped input for wizards and pickers - [group](group.md) - manage user groups - [role](role.md) - manage roles assignable to users - [privilege](privilege.md) - list available privileges diff --git a/internal/tui/prompt.go b/internal/tui/prompt.go new file mode 100644 index 0000000..596aa04 --- /dev/null +++ b/internal/tui/prompt.go @@ -0,0 +1,360 @@ +package tui + +import ( + "bufio" + "errors" + "fmt" + "io" + "os" + "strconv" + "strings" + + "github.com/charmbracelet/huh" + "golang.org/x/term" +) + +// Prompter asks the user for input. On a terminal it renders huh forms +// (arrow keys, "/" filtering, checkboxes). Otherwise it falls back to +// line-based prompts whose input format scripts may rely on, e.g. +// "1,3,5" for a multi-select. +type Prompter struct { + In io.Reader + Out io.Writer + // Interactive selects huh forms; false selects line-based prompts. + Interactive bool + // Accessible runs huh forms in accessible (screen reader) mode. + Accessible bool + // Password reads a secret without echo on the line path. Nil uses + // golang.org/x/term on os.Stdin. + Password func() (string, error) + + reader *bufio.Reader +} + +// Default returns a Prompter on stdin/stderr. Forms are used when both are +// terminals; IICS_ACCESSIBLE=1 enables accessible mode. +func Default() *Prompter { + accessible, _ := strconv.ParseBool(os.Getenv("IICS_ACCESSIBLE")) + return &Prompter{ + In: os.Stdin, + Out: os.Stderr, + Interactive: term.IsTerminal(int(os.Stdin.Fd())) && term.IsTerminal(int(os.Stderr.Fd())), + Accessible: accessible, + } +} + +// line returns the shared buffered reader. One reader per Prompter keeps +// piped answers for later prompts from being lost in a discarded buffer. +func (p *Prompter) line() (string, error) { + if p.reader == nil { + p.reader = bufio.NewReader(p.In) + } + s, err := p.reader.ReadString('\n') + if err != nil && (s == "" || !errors.Is(err, io.EOF)) { + return "", err + } + return strings.TrimSpace(s), nil +} + +func (p *Prompter) run(fields ...huh.Field) error { + err := huh.NewForm(huh.NewGroup(fields...)). + WithOutput(p.Out). + WithAccessible(p.Accessible). + Run() + if errors.Is(err, huh.ErrUserAborted) { + return ErrCanceled + } + return err +} + +func listHeight(n int) int { + return min(n+2, 20) +} + +// Text prompts for a line of text. An empty answer returns def. +func (p *Prompter) Text(label, def string) (string, error) { + if p.Interactive { + v := def + if err := p.run(huh.NewInput().Title(label).Value(&v)); err != nil { + return "", err + } + v = strings.TrimSpace(v) + if v == "" { + return def, nil + } + return v, nil + } + if def != "" { + _, _ = fmt.Fprintf(p.Out, "%s [%s]: ", label, def) + } else { + _, _ = fmt.Fprintf(p.Out, "%s: ", label) + } + s, err := p.line() + if err != nil { + return "", fmt.Errorf("reading input: %w", err) + } + if s == "" { + return def, nil + } + return s, nil +} + +// Secret prompts for a password without echoing it. +func (p *Prompter) Secret(label string) (string, error) { + if p.Interactive { + var v string + label = strings.TrimSuffix(strings.TrimSpace(label), ":") + if err := p.run(huh.NewInput().Title(label).EchoMode(huh.EchoModePassword).Value(&v)); err != nil { + return "", err + } + return v, nil + } + _, _ = fmt.Fprint(p.Out, label) + read := p.Password + if read == nil { + read = func() (string, error) { + pw, err := term.ReadPassword(int(os.Stdin.Fd())) + return string(pw), err + } + } + pw, err := read() + _, _ = fmt.Fprintln(p.Out) + if err != nil { + return "", fmt.Errorf("reading password: %w", err) + } + return pw, nil +} + +// Select presents a menu and returns the 0-based index of the chosen option, +// or -1 if the user cancels. +func (p *Prompter) Select(label string, options []string) (int, error) { + if p.Interactive { + idx := 0 + opts := make([]huh.Option[int], len(options)) + for i, o := range options { + opts[i] = huh.NewOption(o, i) + } + sel := huh.NewSelect[int](). + Title(label). + Description("/ to filter, esc to cancel"). + Options(opts...). + Height(listHeight(len(options)) + 1). + Value(&idx) + if err := p.run(sel); err != nil { + if errors.Is(err, ErrCanceled) { + return -1, nil + } + return -1, err + } + return idx, nil + } + + _, _ = fmt.Fprintf(p.Out, "%s:\n", label) + for i, opt := range options { + _, _ = fmt.Fprintf(p.Out, " [%d] %s\n", i+1, opt) + } + _, _ = fmt.Fprintln(p.Out, " [0] Cancel") + for { + _, _ = fmt.Fprint(p.Out, "Selection (q to cancel): ") + s, err := p.line() + if err != nil { + return -1, fmt.Errorf("reading selection: %w", err) + } + if s == "0" || s == "" || strings.EqualFold(s, "q") { + return -1, nil + } + n, convErr := strconv.Atoi(s) + if convErr != nil || n < 1 || n > len(options) { + _, _ = fmt.Fprintf(p.Out, "Enter a number between 0 and %d, or 'q' to cancel.\n", len(options)) + continue + } + return n - 1, nil + } +} + +// MultiSelect presents a checklist with defaults pre-selected and returns +// the 0-based indices of the selected options (nil for none). Canceling +// returns ErrCanceled. +func (p *Prompter) MultiSelect(label string, options []string, defaults []int) ([]int, error) { + defaultSet := make(map[int]bool, len(defaults)) + for _, d := range defaults { + defaultSet[d] = true + } + + if p.Interactive { + opts := make([]huh.Option[int], len(options)) + for i, o := range options { + opts[i] = huh.NewOption(o, i).Selected(defaultSet[i]) + } + var picked []int + ms := huh.NewMultiSelect[int](). + Title(label). + Description("space to toggle, / to filter, ctrl+a all, enter to confirm"). + Options(opts...). + Filterable(true). + Height(listHeight(len(options)) + 1). + Value(&picked) + if err := p.run(ms); err != nil { + return nil, err + } + if len(picked) == 0 { + return nil, nil + } + return picked, nil + } + + _, _ = fmt.Fprintf(p.Out, "%s (comma-separated numbers, 0 = none):\n", label) + for i, opt := range options { + mark := " " + if defaultSet[i] { + mark = "*" + } + _, _ = fmt.Fprintf(p.Out, " [%d]%s %s\n", i+1, mark, opt) + } + if len(defaults) > 0 { + nums := make([]string, len(defaults)) + for i, d := range defaults { + nums[i] = strconv.Itoa(d + 1) + } + _, _ = fmt.Fprintf(p.Out, "Selections [%s]: ", strings.Join(nums, ",")) + } else { + _, _ = fmt.Fprint(p.Out, "Selections: ") + } + + s, err := p.line() + if err != nil { + return nil, fmt.Errorf("reading selections: %w", err) + } + if s == "" && len(defaults) > 0 { + return defaults, nil + } + if s == "0" || s == "" { + return nil, nil + } + + parts := strings.Split(s, ",") + selected := make([]int, 0, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + n, convErr := strconv.Atoi(part) + if convErr != nil || n < 0 || n > len(options) { + return nil, fmt.Errorf("invalid selection %q; enter numbers between 0 and %d", part, len(options)) + } + if n == 0 { + return nil, nil + } + selected = append(selected, n-1) + } + return selected, nil +} + +// Confirm asks a yes/no question. defaultYes is returned for an empty answer. +func (p *Prompter) Confirm(label string, defaultYes bool) (bool, error) { + if p.Interactive { + v := defaultYes + if err := p.run(huh.NewConfirm().Title(label).Affirmative("Yes").Negative("No").Value(&v)); err != nil { + return false, err + } + return v, nil + } + hint := "[Y/n]" + if !defaultYes { + hint = "[y/N]" + } + _, _ = fmt.Fprintf(p.Out, "%s %s: ", label, hint) + s, err := p.line() + if err != nil { + return false, fmt.Errorf("reading input: %w", err) + } + s = strings.ToLower(s) + if s == "" { + return defaultYes, nil + } + return s == "y" || s == "yes", nil +} + +// Timezone picks a time zone from zones. current is pre-selected; choosing +// "(none)" returns "". +func (p *Prompter) Timezone(zones []string, current string) (string, error) { + if p.Interactive { + v := current + opts := make([]huh.Option[string], 0, len(zones)+1) + opts = append(opts, huh.NewOption("(none)", "")) + for _, z := range zones { + opts = append(opts, huh.NewOption(z, z)) + } + sel := huh.NewSelect[string](). + Title("Time Zone"). + Description("/ to filter, e.g. New_York or Europe"). + Options(opts...). + Height(12). + Value(&v) + if err := p.run(sel); err != nil { + return "", err + } + return v, nil + } + + for { + if current != "" { + _, _ = fmt.Fprintf(p.Out, "Time Zone ID [%s] (type to search, Enter to keep, 0 to clear): ", current) + } else { + _, _ = fmt.Fprint(p.Out, "Time Zone ID (type to search, e.g. New_York, Europe, or 0 to skip): ") + } + s, err := p.line() + if err != nil { + return "", fmt.Errorf("reading input: %w", err) + } + if s == "" { + return current, nil + } + if s == "0" { + return "", nil + } + + lower := strings.ToLower(s) + var matches []string + for _, tz := range zones { + if strings.Contains(strings.ToLower(tz), lower) { + matches = append(matches, tz) + } + } + if len(matches) == 0 { + _, _ = fmt.Fprintf(p.Out, "No matching timezone for %q. Try again.\n", s) + continue + } + if len(matches) == 1 { + _, _ = fmt.Fprintf(p.Out, "Selected: %s\n", matches[0]) + return matches[0], nil + } + + _, _ = fmt.Fprintf(p.Out, "Matches for %q:\n", s) + for i, tz := range matches { + _, _ = fmt.Fprintf(p.Out, " [%d] %s\n", i+1, tz) + } + _, _ = fmt.Fprint(p.Out, "Selection (0 to search again): ") + sel, selErr := p.line() + if selErr != nil { + return "", fmt.Errorf("reading selection: %w", selErr) + } + if sel == "0" || sel == "" { + continue + } + n, convErr := strconv.Atoi(sel) + if convErr != nil || n < 1 || n > len(matches) { + _, _ = fmt.Fprintln(p.Out, "Invalid selection. Try again.") + continue + } + return matches[n-1], nil + } +} + +// PickOne lets the user pick one item, labeled by labelFn. It returns the +// index, or -1 if the user cancels. +func PickOne[T any](p *Prompter, label string, items []T, labelFn func(T) string) (int, error) { + labels := make([]string, len(items)) + for i, it := range items { + labels[i] = labelFn(it) + } + return p.Select(label, labels) +} diff --git a/internal/tui/prompt_test.go b/internal/tui/prompt_test.go new file mode 100644 index 0000000..01b11b1 --- /dev/null +++ b/internal/tui/prompt_test.go @@ -0,0 +1,161 @@ +package tui + +import ( + "bytes" + "reflect" + "strings" + "testing" +) + +func linePrompter(input string) (*Prompter, *bytes.Buffer) { + out := &bytes.Buffer{} + return &Prompter{In: strings.NewReader(input), Out: out}, out +} + +func TestLineSelect(t *testing.T) { + tests := []struct { + input string + want int + }{ + {"2\n", 1}, + {"q\n", -1}, + {"\n", -1}, + {"0\n", -1}, + {"9\n1\n", 0}, // invalid, then valid + } + for _, tt := range tests { + p, _ := linePrompter(tt.input) + got, err := p.Select("Pick", []string{"a", "b", "c"}) + if err != nil { + t.Fatalf("Select(%q) error: %v", tt.input, err) + } + if got != tt.want { + t.Errorf("Select(%q) = %d, want %d", tt.input, got, tt.want) + } + } +} + +func TestLineMultiSelect(t *testing.T) { + opts := []string{"a", "b", "c"} + tests := []struct { + input string + defaults []int + want []int + }{ + {"1,3\n", nil, []int{0, 2}}, + {" 2 , 1 \n", nil, []int{1, 0}}, + {"\n", []int{1}, []int{1}}, + {"0\n", []int{1}, nil}, + {"\n", nil, nil}, + } + for _, tt := range tests { + p, out := linePrompter(tt.input) + got, err := p.MultiSelect("Pick", opts, tt.defaults) + if err != nil { + t.Fatalf("MultiSelect(%q) error: %v", tt.input, err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("MultiSelect(%q, %v) = %v, want %v", tt.input, tt.defaults, got, tt.want) + } + if tt.defaults != nil && !strings.Contains(out.String(), "[2]* b") { + t.Errorf("defaults should be marked with *:\n%s", out.String()) + } + } + + p, _ := linePrompter("1,x\n") + if _, err := p.MultiSelect("Pick", opts, nil); err == nil { + t.Error("expected error for invalid selection") + } +} + +func TestLineTextAndConfirm(t *testing.T) { + p, out := linePrompter("\nAlice\n\nyes\nn\n") + if v, _ := p.Text("Name", "Bob"); v != "Bob" { + t.Errorf("empty Text should return default, got %q", v) + } + if !strings.Contains(out.String(), "Name [Bob]: ") { + t.Errorf("default should be shown in the prompt: %q", out.String()) + } + if v, _ := p.Text("Name", "Bob"); v != "Alice" { + t.Errorf("Text = %q, want Alice", v) + } + if v, _ := p.Confirm("Ok?", true); !v { + t.Error("empty Confirm should return default true") + } + if v, _ := p.Confirm("Ok?", false); !v { + t.Error("Confirm(yes) should be true") + } + if v, _ := p.Confirm("Ok?", true); v { + t.Error("Confirm(n) should be false") + } +} + +func TestLineTimezone(t *testing.T) { + zones := []string{"America/New_York", "Europe/Paris", "Europe/Prague"} + tests := []struct { + input, current, want string + }{ + {"\n", "UTC", "UTC"}, + {"0\n", "UTC", ""}, + {"york\n", "", "America/New_York"}, + {"europe\n2\n", "", "Europe/Prague"}, + {"mars\nparis\n", "", "Europe/Paris"}, + } + for _, tt := range tests { + p, _ := linePrompter(tt.input) + got, err := p.Timezone(zones, tt.current) + if err != nil { + t.Fatalf("Timezone(%q) error: %v", tt.input, err) + } + if got != tt.want { + t.Errorf("Timezone(%q) = %q, want %q", tt.input, got, tt.want) + } + } +} + +// Piped answers for consecutive prompts must all be read from one buffer. +func TestLineSequentialPrompts(t *testing.T) { + p, _ := linePrompter("Data Team\nDescription here\n2,3\ny\n") + name, _ := p.Text("Name", "") + desc, _ := p.Text("Description", "") + picks, _ := p.MultiSelect("Roles", []string{"a", "b", "c"}, nil) + ok, _ := p.Confirm("Create?", false) + if name != "Data Team" || desc != "Description here" || !reflect.DeepEqual(picks, []int{1, 2}) || !ok { + t.Errorf("got name=%q desc=%q picks=%v ok=%v", name, desc, picks, ok) + } +} + +func TestLineEOF(t *testing.T) { + p, _ := linePrompter("") + if _, err := p.Text("Name", ""); err == nil { + t.Error("expected error on EOF without input") + } + p, _ = linePrompter("last") + if v, err := p.Text("Name", ""); err != nil || v != "last" { + t.Errorf("final line without newline: got %q, %v", v, err) + } +} + +func TestLineSecret(t *testing.T) { + p, out := linePrompter("") + p.Password = func() (string, error) { return "s3cret", nil } + v, err := p.Secret("Password: ") + if err != nil || v != "s3cret" { + t.Errorf("Secret = %q, %v", v, err) + } + if !strings.HasPrefix(out.String(), "Password: ") { + t.Errorf("prompt not written: %q", out.String()) + } +} + +func TestPickOne(t *testing.T) { + type item struct{ name string } + p, out := linePrompter("2\n") + idx, err := PickOne(p, "Pick", []item{{"x"}, {"y"}}, func(i item) string { return "item " + i.name }) + if err != nil || idx != 1 { + t.Errorf("PickOne = %d, %v", idx, err) + } + if !strings.Contains(out.String(), "[2] item y") { + t.Errorf("labels not rendered: %s", out.String()) + } +} From 980dada6c257eccb59168a442e652292de1c282d Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Tue, 29 Sep 2026 22:14:08 -0400 Subject: [PATCH 06/52] feat(user): paged user wizard with review (CR-0041 #1) - internal/tui/userwizard.go: huh form with Identity, Single sign-on (SSO only), Details and Membership pages; user name suggestion, required-field and email validation, filterable group and role checklists pre-checked with current membership, time zone select on update; review step (create summary / update diff) with apply, back to editing, or cancel; create requires at least one group or role - update shows user name, authentication and state read-only instead of prompting for values the update API ignores - client: aliasName on create (documented as required for SSO) - cmd/user.go: runUserWizard delegates to tui.RunUserWizard; cancel prints "Canceled." instead of failing - tests for change/summary builders, user name suggestion, email check - docs: user.md wizard section; CR-0041 progress; new bug record for the existing V2 user update failure (403 REPO_10704) --- cmd/user.go | 191 ++------ cmd/user_prompt.go | 6 - .../CR-0041-tui-shared-prompts-and-wizards.md | 7 +- docs/documentation/user.md | 36 +- .../new/2026-09-30-user-update-v2-403.md | 87 ++++ internal/client/users.go | 3 + internal/tui/userwizard.go | 418 ++++++++++++++++++ internal/tui/userwizard_test.go | 106 +++++ 8 files changed, 683 insertions(+), 171 deletions(-) create mode 100644 docs/issues/new/2026-09-30-user-update-v2-403.md create mode 100644 internal/tui/userwizard.go create mode 100644 internal/tui/userwizard_test.go diff --git a/cmd/user.go b/cmd/user.go index d356cb4..527429b 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -15,6 +15,7 @@ import ( "github.com/jbrazda/iics-cli/internal/client" "github.com/jbrazda/iics-cli/internal/config" "github.com/jbrazda/iics-cli/internal/output" + "github.com/jbrazda/iics-cli/internal/tui" "github.com/spf13/cobra" "gopkg.in/yaml.v3" ) @@ -553,8 +554,9 @@ func resolveUserGroupsAndRoles(ctx context.Context, c *client.Client, users []cl return nil } -// runUserWizard interactively collects user fields. When existing is non-nil, current -// values are shown as defaults. Returns the completed User. +// runUserWizard interactively collects user fields in a paged form with a +// review step. When existing is non-nil, its values are pre-filled and the +// review shows the changes. Returns nil when the user cancels. func runUserWizard(ctx context.Context, c *client.Client, existing *client.User) (*client.User, error) { if !config.IsTerminal() { return nil, fmt.Errorf("--interactive requires an interactive terminal") @@ -565,172 +567,35 @@ func runUserWizard(ctx context.Context, c *client.Client, existing *client.User) *u = *existing } - _, _ = fmt.Fprintln(os.Stderr) - - // Authentication type - authIdx, err := promptSelect("Authentication type", []string{"Native", "SSO"}) + groups, err := listAllUserGroups(ctx, c) if err != nil { - return nil, err - } - if authIdx < 0 { - return nil, fmt.Errorf("canceled") - } - authTypes := []string{"Native", "SSO"} - u.Authentication = authTypes[authIdx] - - // Scalar fields - if u.FirstName, err = promptText("First Name", u.FirstName); err != nil { - return nil, err - } - if u.LastName, err = promptText("Last Name", u.LastName); err != nil { - return nil, err - } - - // Username default: firstname.lastname@domain - if u.UserName == "" && u.FirstName != "" && u.LastName != "" { - _, p, _, _ := resolveProfile() - domain := "" - if p != nil && strings.Contains(p.Username, "@") { - domain = p.Username[strings.Index(p.Username, "@")+1:] - } - if domain != "" { - u.UserName = fmt.Sprintf("%s.%s@%s", - strings.ToLower(u.FirstName), - strings.ToLower(u.LastName), - domain) - } - } - if u.UserName, err = promptText("User Name", u.UserName); err != nil { - return nil, err - } - if u.UserName == "" { - return nil, fmt.Errorf("userName is required") - } - - if u.Email, err = promptText("Email", u.Email); err != nil { - return nil, err - } - if u.Phone, err = promptText("Phone (optional)", u.Phone); err != nil { - return nil, err - } - if u.Title, err = promptText("Title (optional)", u.Title); err != nil { - return nil, err - } - if u.Description, err = promptText("Description (optional)", u.Description); err != nil { - return nil, err - } - if u.TimeZoneID, err = promptTimezone(u.TimeZoneID); err != nil { - return nil, err - } - - // State is only settable on update, not create - if existing != nil { - stateDefault := 0 - if u.State == "Disabled" { - stateDefault = 1 - } - stateOptions := []string{"Active", "Disabled"} - var sIdx int - sIdx, err = promptSelect("State", stateOptions) - if err != nil { - return nil, err - } - if sIdx >= 0 { - u.State = stateOptions[sIdx] - } else if u.State == "" { - u.State = stateOptions[stateDefault] - } + return nil, fmt.Errorf("fetching groups: %w", err) } - - // Force password change - forceChange, err := promptYesNo("Force password change on next login?", u.ForcePasswordChange) + roles, err := c.ListRoles(ctx, client.RoleListOptions{}) if err != nil { - return nil, err + return nil, fmt.Errorf("fetching roles: %w", err) } - u.ForcePasswordChange = forceChange - // Groups - var allGroups []client.UserGroup - grpOpts := client.UserGroupListOptions{Limit: 200} - for { - batch, err := c.ListUserGroups(ctx, grpOpts) - if err != nil { - return nil, fmt.Errorf("fetching groups: %w", err) - } - allGroups = append(allGroups, batch...) - if len(batch) < grpOpts.Limit { - break - } - grpOpts.Skip += grpOpts.Limit - } - if len(allGroups) > 0 { - groupOpts := make([]string, len(allGroups)) - groupDefaults := make([]int, 0) - currentGroupIDs := make(map[string]bool) - for _, g := range u.Groups { - currentGroupIDs[g.ID] = true - } - for i, g := range allGroups { - groupOpts[i] = fmt.Sprintf("%s - %s", g.UserGroupName, truncate(g.Description, 100)) - if currentGroupIDs[g.ID] { - groupDefaults = append(groupDefaults, i) - } - } - selected, sErr := promptMultiSelect("Groups", groupOpts, groupDefaults) - if sErr != nil { - return nil, sErr - } - u.Groups = nil - for _, idx := range selected { - g := allGroups[idx] - u.Groups = append(u.Groups, client.UserGroupRef{ - ID: g.ID, - UserGroupName: g.UserGroupName, - }) + domain := "" + if _, p, _, _ := resolveProfile(); p != nil { + if i := strings.Index(p.Username, "@"); i >= 0 { + domain = p.Username[i+1:] } } - // Roles - var allRoles []client.Role - roleOpts := client.RoleListOptions{Limit: 200} - for { - batch, err := c.ListRoles(ctx, roleOpts) - if err != nil { - return nil, fmt.Errorf("fetching roles: %w", err) - } - allRoles = append(allRoles, batch...) - if len(batch) < roleOpts.Limit { - break - } - roleOpts.Skip += roleOpts.Limit - } - if len(allRoles) > 0 { - roleOpts := make([]string, len(allRoles)) - roleDefaults := make([]int, 0) - currentRoleIDs := make(map[string]bool) - for _, r := range u.Roles { - currentRoleIDs[r.ID] = true - } - for i, r := range allRoles { - roleOpts[i] = fmt.Sprintf("%s - %s", r.RoleName, truncate(r.Description, 100)) - if currentRoleIDs[r.ID] { - roleDefaults = append(roleDefaults, i) - } - } - selected, sErr := promptMultiSelect("Roles", roleOpts, roleDefaults) - if sErr != nil { - return nil, sErr - } - u.Roles = nil - for _, idx := range selected { - r := allRoles[idx] - u.Roles = append(u.Roles, client.UserRole{ - ID: r.ID, - RoleName: r.RoleName, - }) - } + ok, err := tui.RunUserWizard(tui.UserWizardInput{ + User: u, + Update: existing != nil, + Groups: groups, + Roles: roles, + Timezones: iicsTimezones, + UserNameDomain: domain, + Out: os.Stderr, + Accessible: prompter.Accessible, + }) + if err != nil || !ok { + return nil, err } - return u, nil } @@ -771,6 +636,10 @@ func newUserCreateCmd() *cobra.Command { if wErr != nil { return wErr } + if u == nil { + _, _ = fmt.Fprintln(os.Stderr, "Canceled.") + return nil + } created, cErr := c.CreateUser(ctx, u) if cErr != nil { return cErr @@ -910,6 +779,10 @@ func newUserUpdateCmd() *cobra.Command { if wErr != nil { return wErr } + if updated == nil { + _, _ = fmt.Fprintln(os.Stderr, "Canceled.") + return nil + } result, uErr := c.UpdateUser(ctx, target.ID, updated) if uErr != nil { return uErr diff --git a/cmd/user_prompt.go b/cmd/user_prompt.go index e293cf8..a66fd51 100644 --- a/cmd/user_prompt.go +++ b/cmd/user_prompt.go @@ -156,12 +156,6 @@ var iicsTimezones = []string{ "Pacific/Kiritimati", "Pacific/Norfolk", "Pacific/Tahiti", "UTC", "VST", } -// promptTimezone picks a timezone from iicsTimezones. The current value is -// pre-selected (terminal) or kept on Enter (line mode, where "0" clears it). -func promptTimezone(current string) (string, error) { - return prompter.Timezone(iicsTimezones, current) -} - // resolveUser returns the user identified by id or userName flags, falling back to // interactive search when both are empty and stdin is a terminal. func resolveUser(ctx context.Context, c *client.Client, id, userName string) (*client.User, error) { diff --git a/docs/ChangeRequests/new/CR-0041-tui-shared-prompts-and-wizards.md b/docs/ChangeRequests/new/CR-0041-tui-shared-prompts-and-wizards.md index 0659029..d95d5c9 100644 --- a/docs/ChangeRequests/new/CR-0041-tui-shared-prompts-and-wizards.md +++ b/docs/ChangeRequests/new/CR-0041-tui-shared-prompts-and-wizards.md @@ -117,8 +117,11 @@ directly (filterable) instead of the query loop. (Step 0) - [x] Piped answers on non-terminal stdin still work (line-based fallback, Step 0) -- [ ] User wizard: paged form, filterable time zone, group and role - multi-selects pre-checked, review on update +- [x] User wizard: paged form, filterable time zone (update only; the + create API has no time zone), group and role multi-selects + pre-checked, review on update. Applying updates is blocked by the + existing V2 update failure + (`docs/issues/new/2026-09-30-user-update-v2-403.md`). - [ ] User group and runtime wizards: pre-checked multi-select plus review - [ ] All pickers are filterable - [ ] Delete confirmations unchanged in behavior, deduplicated diff --git a/docs/documentation/user.md b/docs/documentation/user.md index 85f058a..b9e6cb1 100644 --- a/docs/documentation/user.md +++ b/docs/documentation/user.md @@ -94,13 +94,32 @@ iics user get --id --output json ## user create -Create a user from a JSON definition file. +Create a user from a definition file, or interactively with `--interactive`. ### Flags -| Flag | Type | Required | Description | -| ------------- | ------ | -------- | -------------------------------- | -| `--from-file` | string | yes | JSON file with user definition | +| Flag | Type | Required | Description | +| --------------- | ------ | -------- | ------------------------------------------------------ | +| `--from-file` | string | one of | JSON, YAML or CSV file with user(s); `-` for stdin | +| `--interactive` | bool | one of | Launch the interactive creation wizard (terminal only) | + +### Interactive wizard + +`--interactive` opens a paged form (`Enter` next field, `Shift+Tab` back): + +1. **Identity** - authentication (Native or SSO), first name, last name, user + name (empty uses the suggestion `first.last@`), email + (validated). +2. **Single sign-on** - alias name in the identity provider; shown only for + SSO, where the API requires it. +3. **Details** - phone, title, description, force password change. +4. **Membership** - user groups and roles as filterable checklists + (`/` filter, `Space` toggle). +5. **Review** - summary with **Create user**, **Back to editing** or + **Cancel**. At least one user group or role is required. + +The create API does not accept a time zone, so the wizard does not ask for +one. See [Interactive prompts](interactive.md) for keys. All [global flags](../../README.md#global-flags) apply. @@ -127,10 +146,14 @@ All [global flags](../../README.md#global-flags) apply. ```bash iics user create --from-file new-user.json + +iics user create --interactive ``` ```powershell iics user create --from-file new-user.json + +iics user create --interactive ``` --- @@ -153,6 +176,11 @@ iics user create --from-file new-user.json > > To update a user's group or role membership in the meantime, use the IICS > Administrator UI. +> +> The `--interactive` wizard pre-fills the current values, shows user name, +> authentication and state read-only, and ends with a review of the changes +> (`field: old -> new`, `+ Group`, `- Role`). Applying the changes is subject +> to the limitation above. ### Flags diff --git a/docs/issues/new/2026-09-30-user-update-v2-403.md b/docs/issues/new/2026-09-30-user-update-v2-403.md new file mode 100644 index 0000000..7356af1 --- /dev/null +++ b/docs/issues/new/2026-09-30-user-update-v2-403.md @@ -0,0 +1,87 @@ +# BUG: user update fails with HTTP 403 REPO_10704 + +--- + +## Symptoms + +`iics user update` (both `--interactive` and `--from-file`) fails with HTTP 403 +before any group or role change is applied: + +```text +Error: IICS API error (HTTP 403): REPO_10704Internal +error. An error occurred while loading the user. Contact Informatica Global Customer +Support.403 +``` + +--- + +## Command / Reproduction Steps + +Observed on the `dev` profile on 2026-09-30 with a freshly created user +(state `Provisioned`, not yet activated): + +```bash +iics user create --interactive # create iics.cli.test.wizard +cat > upd.json <<'JSON' +{"userName":"iics.cli.test.wizard","firstName":"Test","lastName":"Wizardry","email":"iics-cli-test@example.com"} +JSON +iics user update --from-file upd.json # fails with 403 REPO_10704 +``` + +--- + +## Expected Behaviour + +The user's last name (and in the interactive run, time zone and roles) are +updated. + +--- + +## Actual Behaviour + +`Client.UpdateUser` (`internal/client/users.go`) first sends +`POST /api/v2/user/` (XML, legacy V2 update) using the V3 user ID. That +call fails with 403 `REPO_10704`; the following V3 `addGroups` / +`removeGroups` / `addRoles` / `removeRoles` calls are never reached. + +This limitation is already noted in `docs/documentation/user.md` (user update +section), which reports HTTP 400 from the same V2 call in earlier testing. The +error has since changed to 403 `REPO_10704`. The CR-0041 update wizard builds +and reviews the change correctly but cannot apply it until this is fixed. + +Not yet determined: + +- Whether the failure is specific to users in `Provisioned` state (never + logged in), or happens for all users. +- Whether the V2 endpoint expects a V2 user ID or requires fields the request + does not send. + +--- + +## Architecture Layer + +- [ ] **`cmd/`** - flag parsing, command wiring, output formatting +- [x] **`internal/client/`** - HTTP logic, API structs, request/response handling +- [ ] **`internal/config/`** - config file loading, session cache +- [ ] **`internal/output/`** - table / JSON / CSV renderer + +--- + +## Likely Affected Files + +```text +internal/client/users.go # UpdateUser, updateUserV2Request +``` + +--- + +## Fix Instructions + +1. Check the V2 user update docs (`platform-rest-api-version-2-resources/user`) + for the ID format and required fields, and compare with a working update of + an active user on `dev`. +2. Check whether the V3 API offers an update for the scalar fields used here. +3. Consider applying group/role changes even when the scalar update is not + needed (no scalar field changed), so membership-only updates do not depend + on the V2 call. +4. Add a test with `newTestClient` for the fixed request. diff --git a/internal/client/users.go b/internal/client/users.go index ce0da00..308561b 100644 --- a/internal/client/users.go +++ b/internal/client/users.go @@ -20,6 +20,7 @@ type createUserRequest struct { Title string `json:"title,omitempty"` Description string `json:"description,omitempty"` Authentication int `json:"authentication"` + AliasName string `json:"aliasName,omitempty"` ForcePasswordChange bool `json:"forcePasswordChange,omitempty"` Roles []string `json:"roles,omitempty"` Groups []string `json:"groups,omitempty"` @@ -40,6 +41,7 @@ func userToCreateRequest(u *User) *createUserRequest { } if strings.EqualFold(u.Authentication, "SSO") { req.Authentication = 1 // 0 = Native (default) + req.AliasName = u.AliasName } for _, g := range u.Groups { req.Groups = append(req.Groups, g.ID) @@ -83,6 +85,7 @@ type User struct { Title string `json:"title,omitempty"` State string `json:"state,omitempty"` Authentication string `json:"authentication,omitempty"` + AliasName string `json:"aliasName,omitempty"` TimeZoneID string `json:"timeZoneId,omitempty"` ForcePasswordChange bool `json:"forcePasswordChange,omitempty"` LastLoginTime string `json:"lastLoginTime,omitempty"` diff --git a/internal/tui/userwizard.go b/internal/tui/userwizard.go new file mode 100644 index 0000000..a2d7b0d --- /dev/null +++ b/internal/tui/userwizard.go @@ -0,0 +1,418 @@ +package tui + +import ( + "errors" + "fmt" + "io" + "net/mail" + "sort" + "strings" + + "github.com/charmbracelet/huh" + "github.com/jbrazda/iics-cli/internal/client" +) + +// UserWizardInput configures RunUserWizard. +type UserWizardInput struct { + // User holds the starting values and receives the result. + User *client.User + // Update is true when editing an existing user. User name, + // authentication and state are then shown read-only because the update + // API does not change them. + Update bool + // Groups and Roles are all groups and roles in the organization. + Groups []client.UserGroup + Roles []client.Role + // Timezones lists the accepted time zone IDs. + Timezones []string + // UserNameDomain suggests "first.last@" as the user name. + UserNameDomain string + + Out io.Writer + Accessible bool +} + +// RunUserWizard collects user fields in a paged form (Identity, Details, +// Membership) followed by a review. It returns false if the user canceled. +func RunUserWizard(in UserWizardInput) (bool, error) { + before := cloneUser(in.User) + u := in.User + + auth := u.Authentication + if auth == "" { + auth = "Native" + } + groupIDs := make([]string, 0, len(u.Groups)) + for _, g := range u.Groups { + groupIDs = append(groupIDs, g.ID) + } + roleIDs := make([]string, 0, len(u.Roles)) + for _, r := range u.Roles { + roleIDs = append(roleIDs, r.ID) + } + + for { + form := huh.NewForm( + identityGroup(in, u, &auth), + ssoGroup(in, u, &auth), + detailsGroup(in, u), + membershipGroup(in, &groupIDs, &roleIDs), + ).WithOutput(in.Out).WithAccessible(in.Accessible) + if err := form.Run(); err != nil { + if errors.Is(err, huh.ErrUserAborted) { + return false, nil + } + return false, err + } + + if !in.Update { + u.Authentication = auth + if auth != "SSO" { + u.AliasName = "" + } + if strings.TrimSpace(u.UserName) == "" { + u.UserName = suggestUserName(u.FirstName, u.LastName, in.UserNameDomain) + } + } + trimUser(u) + u.Groups = groupsByID(in.Groups, groupIDs) + u.Roles = rolesByID(in.Roles, roleIDs) + + choice, err := reviewUser(in, before, u) + if err != nil { + return false, err + } + switch choice { + case "apply": + return true, nil + case "cancel": + return false, nil + } + } +} + +func identityGroup(in UserWizardInput, u *client.User, auth *string) *huh.Group { + var fields []huh.Field + if in.Update { + fields = append(fields, huh.NewNote(). + Title("User: "+u.UserName). + Description(fmt.Sprintf("Authentication: %s State: %s\nUser name, authentication and state cannot be changed here.", + orDash(u.Authentication), orDash(u.State)))) + } else { + fields = append(fields, huh.NewSelect[string](). + Title("Authentication"). + Options(huh.NewOption("Native", "Native"), huh.NewOption("SSO", "SSO")). + Value(auth)) + } + fields = append(fields, + huh.NewInput().Title("First name").Value(&u.FirstName).Validate(required("first name")), + huh.NewInput().Title("Last name").Value(&u.LastName).Validate(required("last name")), + ) + if !in.Update { + fields = append(fields, huh.NewInput(). + Title("User name"). + Description("Leave empty to use the suggestion"). + PlaceholderFunc(func() string { + return suggestUserName(u.FirstName, u.LastName, in.UserNameDomain) + }, []any{&u.FirstName, &u.LastName}). + Value(&u.UserName). + Validate(func(v string) error { + if strings.TrimSpace(v) == "" && suggestUserName(u.FirstName, u.LastName, in.UserNameDomain) == "" { + return errors.New("user name is required") + } + return nil + })) + } + fields = append(fields, huh.NewInput().Title("Email").Value(&u.Email).Validate(validEmail)) + return huh.NewGroup(fields...).Title("Identity") +} + +// ssoGroup asks for the SSO alias name on create; the API requires it when +// authentication is SSO. Hidden otherwise. +func ssoGroup(in UserWizardInput, u *client.User, auth *string) *huh.Group { + return huh.NewGroup( + huh.NewInput(). + Title("SSO alias name"). + Description("User identifier in the identity provider"). + Value(&u.AliasName). + Validate(required("alias name")), + ).Title("Single sign-on").WithHideFunc(func() bool { + return in.Update || *auth != "SSO" + }) +} + +func detailsGroup(in UserWizardInput, u *client.User) *huh.Group { + fields := []huh.Field{ + huh.NewInput().Title("Phone (optional)").Value(&u.Phone), + huh.NewInput().Title("Title (optional)").Value(&u.Title), + huh.NewInput().Title("Description (optional)").Value(&u.Description), + } + // The create API does not accept a time zone; only update applies it. + if in.Update { + tzOpts := make([]huh.Option[string], 0, len(in.Timezones)+1) + tzOpts = append(tzOpts, huh.NewOption("(none)", "")) + for _, z := range in.Timezones { + tzOpts = append(tzOpts, huh.NewOption(z, z)) + } + fields = append(fields, huh.NewSelect[string](). + Title("Time zone"). + Description("/ to filter, e.g. New_York or Europe"). + Options(tzOpts...). + Height(8). + Value(&u.TimeZoneID)) + } + fields = append(fields, huh.NewConfirm().Title("Force password change on next login?").Value(&u.ForcePasswordChange)) + return huh.NewGroup(fields...).Title("Details") +} + +func membershipGroup(in UserWizardInput, groupIDs, roleIDs *[]string) *huh.Group { + groups := append([]client.UserGroup(nil), in.Groups...) + sort.SliceStable(groups, func(i, j int) bool { + return strings.ToLower(groups[i].UserGroupName) < strings.ToLower(groups[j].UserGroupName) + }) + gOpts := make([]huh.Option[string], len(groups)) + for i, g := range groups { + gOpts[i] = huh.NewOption(optionLabel(g.UserGroupName, g.Description), g.ID) + } + roles := append([]client.Role(nil), in.Roles...) + sort.SliceStable(roles, func(i, j int) bool { + return strings.ToLower(roles[i].RoleName) < strings.ToLower(roles[j].RoleName) + }) + rOpts := make([]huh.Option[string], len(roles)) + for i, r := range roles { + rOpts[i] = huh.NewOption(optionLabel(r.RoleName, r.Description), r.ID) + } + + var fields []huh.Field + if len(gOpts) > 0 { + fields = append(fields, huh.NewMultiSelect[string](). + Title("User groups"). + Description("space toggle, / filter, ctrl+a all"). + Options(gOpts...). + Filterable(true). + Height(min(len(gOpts)+3, 12)). + Value(groupIDs)) + } + if len(rOpts) > 0 { + fields = append(fields, huh.NewMultiSelect[string](). + Title("Roles"). + Description("space toggle, / filter, ctrl+a all"). + Options(rOpts...). + Filterable(true). + Height(min(len(rOpts)+3, 12)). + Value(roleIDs)) + } + if len(fields) == 0 { + fields = append(fields, huh.NewNote().Title("No user groups or roles available")) + } + return huh.NewGroup(fields...).Title("Membership") +} + +// reviewUser shows the result (create) or the changes (update) and returns +// "apply", "back" or "cancel". +func reviewUser(in UserWizardInput, before, after *client.User) (string, error) { + var desc string + title := "Create user " + after.UserName + applyLabel := "Create user" + if in.Update { + title = "Update user " + after.UserName + applyLabel = "Apply changes" + changes := UserChanges(before, after) + if len(changes) == 0 { + desc = "No changes." + } else { + desc = strings.Join(changes, "\n") + } + } else { + desc = strings.Join(UserSummary(after), "\n") + } + + options := []huh.Option[string]{ + huh.NewOption(applyLabel, "apply"), + huh.NewOption("Back to editing", "back"), + huh.NewOption("Cancel", "cancel"), + } + // The create API requires at least one role or user group. + if !in.Update && len(after.Groups) == 0 && len(after.Roles) == 0 { + desc += "\n\nSelect at least one user group or role." + options = options[1:] + } + choice := options[0].Value + sel := huh.NewSelect[string]().Title(title).Description(desc).Options(options...).Value(&choice) + if err := huh.NewForm(huh.NewGroup(sel)).WithOutput(in.Out).WithAccessible(in.Accessible).Run(); err != nil { + if errors.Is(err, huh.ErrUserAborted) { + return "cancel", nil + } + return "", err + } + return choice, nil +} + +// UserSummary lists the fields of a user to be created. +func UserSummary(u *client.User) []string { + lines := []string{ + "User name: " + u.UserName, + "Name: " + strings.TrimSpace(u.FirstName+" "+u.LastName), + "Email: " + orDash(u.Email), + "Authentication: " + orDash(u.Authentication), + } + if u.AliasName != "" { + lines = append(lines, "SSO alias: "+u.AliasName) + } + for _, f := range []struct{ label, v string }{ + {"Phone: ", u.Phone}, + {"Title: ", u.Title}, + {"Description: ", u.Description}, + {"Time zone: ", u.TimeZoneID}, + } { + if f.v != "" { + lines = append(lines, f.label+f.v) + } + } + if u.ForcePasswordChange { + lines = append(lines, "Force password change on next login") + } + lines = append(lines, + "Groups: "+orDash(strings.Join(groupNames(u.Groups), ", ")), + "Roles: "+orDash(strings.Join(roleNames(u.Roles), ", "))) + return lines +} + +// UserChanges describes the differences between two versions of a user in +// the fields the update applies: scalar fields as "field: old -> new", +// groups and roles as "+ name" / "- name". +func UserChanges(before, after *client.User) []string { + var out []string + for _, f := range []struct{ name, old, new string }{ + {"First name", before.FirstName, after.FirstName}, + {"Last name", before.LastName, after.LastName}, + {"Email", before.Email, after.Email}, + {"Phone", before.Phone, after.Phone}, + {"Title", before.Title, after.Title}, + {"Description", before.Description, after.Description}, + {"Time zone", before.TimeZoneID, after.TimeZoneID}, + } { + if f.old != f.new { + out = append(out, fmt.Sprintf("%s: %s -> %s", f.name, orDash(f.old), orDash(f.new))) + } + } + if before.ForcePasswordChange != after.ForcePasswordChange { + out = append(out, fmt.Sprintf("Force password change: %t -> %t", before.ForcePasswordChange, after.ForcePasswordChange)) + } + add, remove := client.DiffPrivileges(groupNames(before.Groups), groupNames(after.Groups)) + out = append(out, signed("Group", add, remove)...) + add, remove = client.DiffPrivileges(roleNames(before.Roles), roleNames(after.Roles)) + out = append(out, signed("Role", add, remove)...) + return out +} + +func signed(kind string, add, remove []string) []string { + var out []string + for _, n := range add { + out = append(out, fmt.Sprintf("+ %s: %s", kind, n)) + } + for _, n := range remove { + out = append(out, fmt.Sprintf("- %s: %s", kind, n)) + } + return out +} + +func suggestUserName(first, last, domain string) string { + first, last = strings.TrimSpace(first), strings.TrimSpace(last) + if first == "" || last == "" || domain == "" { + return "" + } + return fmt.Sprintf("%s.%s@%s", strings.ToLower(first), strings.ToLower(last), domain) +} + +func required(what string) func(string) error { + return func(v string) error { + if strings.TrimSpace(v) == "" { + return fmt.Errorf("%s is required", what) + } + return nil + } +} + +func validEmail(v string) error { + v = strings.TrimSpace(v) + if v == "" { + return errors.New("email is required") + } + if _, err := mail.ParseAddress(v); err != nil { + return fmt.Errorf("invalid email address") + } + return nil +} + +func optionLabel(name, desc string) string { + desc = strings.Join(strings.Fields(desc), " ") + if desc == "" { + return name + } + const maxDesc = 50 + if r := []rune(desc); len(r) > maxDesc { + desc = string(r[:maxDesc-1]) + "…" + } + return name + " - " + desc +} + +func trimUser(u *client.User) { + for _, s := range []*string{&u.FirstName, &u.LastName, &u.UserName, &u.Email, &u.Phone, &u.Title, &u.Description} { + *s = strings.TrimSpace(*s) + } +} + +func groupsByID(all []client.UserGroup, ids []string) []client.UserGroupRef { + want := toSet(ids) + var out []client.UserGroupRef + for _, g := range all { + if want[g.ID] { + out = append(out, client.UserGroupRef{ID: g.ID, UserGroupName: g.UserGroupName}) + } + } + return out +} + +func rolesByID(all []client.Role, ids []string) []client.UserRole { + want := toSet(ids) + var out []client.UserRole + for _, r := range all { + if want[r.ID] { + out = append(out, client.UserRole{ID: r.ID, RoleName: r.RoleName}) + } + } + return out +} + +func groupNames(gs []client.UserGroupRef) []string { + out := make([]string, len(gs)) + for i, g := range gs { + out[i] = g.UserGroupName + } + sort.Strings(out) + return out +} + +func roleNames(rs []client.UserRole) []string { + out := make([]string, len(rs)) + for i, r := range rs { + out[i] = r.RoleName + } + sort.Strings(out) + return out +} + +func cloneUser(u *client.User) *client.User { + c := *u + c.Groups = append([]client.UserGroupRef(nil), u.Groups...) + c.Roles = append([]client.UserRole(nil), u.Roles...) + return &c +} + +func orDash(s string) string { + if s == "" { + return "-" + } + return s +} diff --git a/internal/tui/userwizard_test.go b/internal/tui/userwizard_test.go new file mode 100644 index 0000000..2103f81 --- /dev/null +++ b/internal/tui/userwizard_test.go @@ -0,0 +1,106 @@ +package tui + +import ( + "reflect" + "strings" + "testing" + + "github.com/jbrazda/iics-cli/internal/client" +) + +func TestUserChanges(t *testing.T) { + before := &client.User{ + FirstName: "Ann", LastName: "Lee", Email: "ann@x.com", TimeZoneID: "UTC", + Groups: []client.UserGroupRef{{ID: "g1", UserGroupName: "Dev"}, {ID: "g2", UserGroupName: "Ops"}}, + Roles: []client.UserRole{{ID: "r1", RoleName: "Designer"}}, + } + after := cloneUser(before) + after.LastName = "Smith" + after.TimeZoneID = "" + after.ForcePasswordChange = true + after.Groups = []client.UserGroupRef{{ID: "g2", UserGroupName: "Ops"}, {ID: "g3", UserGroupName: "QA"}} + after.Roles = append(after.Roles, client.UserRole{ID: "r2", RoleName: "Monitor"}) + + got := UserChanges(before, after) + want := []string{ + "Last name: Lee -> Smith", + "Time zone: UTC -> -", + "Force password change: false -> true", + "+ Group: QA", + "- Group: Dev", + "+ Role: Monitor", + } + if !reflect.DeepEqual(got, want) { + t.Errorf("UserChanges =\n%s\nwant\n%s", strings.Join(got, "\n"), strings.Join(want, "\n")) + } + if n := UserChanges(before, cloneUser(before)); len(n) != 0 { + t.Errorf("expected no changes, got %v", n) + } +} + +func TestUserSummary(t *testing.T) { + u := &client.User{ + UserName: "ann.lee@x.com", FirstName: "Ann", LastName: "Lee", Email: "ann@x.com", + Authentication: "SSO", AliasName: "alee", + Roles: []client.UserRole{{RoleName: "Designer"}}, + } + s := strings.Join(UserSummary(u), "\n") + for _, want := range []string{"User name: ann.lee@x.com", "Name: Ann Lee", "SSO alias: alee", "Groups: -", "Roles: Designer"} { + if !strings.Contains(s, want) { + t.Errorf("summary missing %q:\n%s", want, s) + } + } + if strings.Contains(s, "Phone") { + t.Errorf("empty optional fields should be omitted:\n%s", s) + } +} + +func TestSuggestUserName(t *testing.T) { + if got := suggestUserName(" Ann ", "Lee", "x.com"); got != "ann.lee@x.com" { + t.Errorf("got %q", got) + } + if got := suggestUserName("Ann", "", "x.com"); got != "" { + t.Errorf("missing last name should give no suggestion, got %q", got) + } + if got := suggestUserName("Ann", "Lee", ""); got != "" { + t.Errorf("missing domain should give no suggestion, got %q", got) + } +} + +func TestValidEmail(t *testing.T) { + for _, ok := range []string{"a@b.com", " a.b+c@d.org "} { + if err := validEmail(ok); err != nil { + t.Errorf("validEmail(%q) = %v", ok, err) + } + } + for _, bad := range []string{"", " ", "not-an-email", "a@"} { + if err := validEmail(bad); err == nil { + t.Errorf("validEmail(%q) should fail", bad) + } + } +} + +func TestOptionLabel(t *testing.T) { + if got := optionLabel("Designer", ""); got != "Designer" { + t.Errorf("got %q", got) + } + long := optionLabel("Admin", strings.Repeat("word ", 30)) + if len([]rune(long)) != len("Admin - ")+50 || !strings.HasSuffix(long, "…") { + t.Errorf("long description should be cut to 50 runes: %q", long) + } + if got := optionLabel("X", "a\n b"); got != "X - a b" { + t.Errorf("whitespace should be collapsed, got %q", got) + } +} + +func TestMembershipByID(t *testing.T) { + groups := []client.UserGroup{{ID: "g1", UserGroupName: "Dev"}, {ID: "g2", UserGroupName: "Ops"}} + got := groupsByID(groups, []string{"g2"}) + if len(got) != 1 || got[0].UserGroupName != "Ops" { + t.Errorf("groupsByID = %+v", got) + } + roles := []client.Role{{ID: "r1", RoleName: "Designer"}} + if r := rolesByID(roles, nil); r != nil { + t.Errorf("rolesByID(nil) = %+v", r) + } +} From deea7e94fa9190abe2f76b8a0dd3e86f36cc3cb6 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Tue, 29 Sep 2026 22:21:57 -0400 Subject: [PATCH 07/52] feat(group,environment): checklist wizards with review (CR-0041 #2, #3) - internal/tui/review.go: shared confirmReview (apply / back / cancel, apply hidden when nothing to apply), SetChanges (+/- lines), requiredUnique and atLeastOne validators; user wizard reuses them - internal/tui/groupwizard.go: create asks name (duplicates rejected), description and roles (filterable checklist, at least one); update shows name/description read-only, roles pre-checked, review of +/- role changes, no-change review offers only back/cancel - internal/tui/runtimewizard.go: name (duplicates rejected), description, shared flag, filterable checklist of unassigned agents (file-seeded agents pre-checked), review; replaces the add/remove/done menu loop - cmd: group and environment wizards delegate to internal/tui; cancel prints "Canceled." instead of failing; remove unused promptMultiSelect wrapper and truncate helper - tests for review helpers and agent list building - docs: group.md and environment.md wizard descriptions; CR-0041 progress --- cmd/runtime.go | 7 +- cmd/runtime_prompt.go | 127 +++------------ cmd/user.go | 8 - cmd/user_prompt.go | 6 - cmd/usergroup.go | 14 +- cmd/usergroup_prompt.go | 82 +++------- .../CR-0041-tui-shared-prompts-and-wizards.md | 4 +- docs/documentation/environment.md | 12 +- docs/documentation/group.md | 10 ++ internal/tui/groupwizard.go | 120 ++++++++++++++ internal/tui/review.go | 102 ++++++++++++ internal/tui/review_test.go | 61 ++++++++ internal/tui/runtimewizard.go | 148 ++++++++++++++++++ internal/tui/userwizard.go | 63 ++------ 14 files changed, 532 insertions(+), 232 deletions(-) create mode 100644 internal/tui/groupwizard.go create mode 100644 internal/tui/review.go create mode 100644 internal/tui/review_test.go create mode 100644 internal/tui/runtimewizard.go diff --git a/cmd/runtime.go b/cmd/runtime.go index cc56ec5..ca27183 100644 --- a/cmd/runtime.go +++ b/cmd/runtime.go @@ -275,9 +275,14 @@ shared flag, and member agents.`, } if useWizard { - if werr := runRuntimeCreateWizard(ctx, c, &rt); werr != nil { + ok, werr := runRuntimeCreateWizard(ctx, c, &rt) + if werr != nil { return werr } + if !ok { + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Canceled.") + return nil + } } created, err := c.CreateRuntimeEnvironment(ctx, &rt) diff --git a/cmd/runtime_prompt.go b/cmd/runtime_prompt.go index 432e771..06e3734 100644 --- a/cmd/runtime_prompt.go +++ b/cmd/runtime_prompt.go @@ -7,117 +7,40 @@ import ( "github.com/jbrazda/iics-cli/internal/client" "github.com/jbrazda/iics-cli/internal/config" + "github.com/jbrazda/iics-cli/internal/tui" ) // runRuntimeCreateWizard interactively collects the fields for a new runtime // environment. rt is used both as the seed (from --from-file) and the result. -func runRuntimeCreateWizard(ctx context.Context, c *client.Client, rt *client.RuntimeEnvironment) error { +// It returns false if the user canceled. +func runRuntimeCreateWizard(ctx context.Context, c *client.Client, rt *client.RuntimeEnvironment) (bool, error) { if !config.IsTerminal() { - return fmt.Errorf("--interactive requires a terminal; use --from-file") + return false, fmt.Errorf("--interactive requires a terminal; use --from-file") } - - // Name (required). - for { - name, err := promptText("Environment Name", rt.Name) - if err != nil { - return err - } - if name != "" { - rt.Name = name - break - } - _, _ = fmt.Fprintln(os.Stderr, "Environment name is required.") - } - - desc, err := promptText("Description (optional)", rt.Description) + agents, err := c.ListAgents(ctx, client.AgentListOptions{IncludeUnassignedOnly: true}) if err != nil { - return err + return false, err } - rt.Description = desc - - shared, err := promptYesNo("Is shared", rt.IsShared) + envs, err := c.ListRuntimeEnvironments(ctx, client.RuntimeListOptions{}) if err != nil { - return err - } - rt.IsShared = shared - - // Working set of member agents, keyed by ID (deduped). Seeded from rt.Agents. - selected := make([]client.RuntimeEnvironmentAgent, 0, len(rt.Agents)) - inSet := make(map[string]bool) - for _, a := range rt.Agents { - if a.ID != "" && !inSet[a.ID] { - selected = append(selected, a) - inSet[a.ID] = true - } - } - - for { - choice, err := promptSelect("Manage agents", []string{"Add agents", "Remove agents", "Done"}) - if err != nil { - return err - } - switch choice { - case 0: // Add - agents, err := c.ListAgents(ctx, client.AgentListOptions{IncludeUnassignedOnly: true}) - if err != nil { - return err - } - var avail []client.Agent - for _, a := range agents { - if !inSet[a.ID] { - avail = append(avail, a) - } - } - if len(avail) == 0 { - _, _ = fmt.Fprintln(os.Stderr, "No unassigned agents available.") - continue - } - labels := make([]string, len(avail)) - for i, a := range avail { - labels[i] = fmt.Sprintf("%s (%s)", a.Name, a.AgentHost) - } - picks, err := promptMultiSelect("Unassigned agents to add", labels, nil) - if err != nil { - return err - } - for _, idx := range picks { - a := avail[idx] - selected = append(selected, client.RuntimeEnvironmentAgent{ID: a.ID, Name: a.Name}) - inSet[a.ID] = true - } - case 1: // Remove - if len(selected) == 0 { - _, _ = fmt.Fprintln(os.Stderr, "No agents selected.") - continue - } - labels := make([]string, len(selected)) - for i, a := range selected { - labels[i] = fmt.Sprintf("%s (%s)", a.Name, a.ID) - } - picks, err := promptMultiSelect("Agents to remove", labels, nil) - if err != nil { - return err - } - drop := make(map[int]bool, len(picks)) - for _, idx := range picks { - drop[idx] = true - } - kept := selected[:0] - for i, a := range selected { - if drop[i] { - delete(inSet, a.ID) - continue - } - kept = append(kept, a) - } - selected = kept - default: // 2 Done, or Cancel (-1) - rt.Agents = trimAgentRefs(selected) - _, _ = fmt.Fprintf(os.Stderr, "%d agent(s) selected.\n", len(rt.Agents)) - return nil - } - _, _ = fmt.Fprintf(os.Stderr, "%d agent(s) currently selected.\n", len(selected)) - } + return false, err + } + names := make([]string, len(envs)) + for i, e := range envs { + names[i] = e.Name + } + ok, err := tui.RunRuntimeWizard(tui.RuntimeWizardInput{ + Env: rt, + Agents: agents, + ExistingNames: names, + Out: os.Stderr, + Accessible: prompter.Accessible, + }) + if err != nil || !ok { + return false, err + } + rt.Agents = trimAgentRefs(rt.Agents) + return true, nil } // trimAgentRefs reduces each agent to the fields the create request needs. diff --git a/cmd/user.go b/cmd/user.go index 527429b..2f81cf1 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -349,14 +349,6 @@ func buildRoleMap(ctx context.Context, c *client.Client) (map[string]string, err return m, nil } -// truncate shortens s to at most max bytes, appending "..." if truncated. -func truncate(s string, max int) string { - if len(s) <= max { - return s - } - return s[:max] + "..." -} - // detectInputFormat infers the file format from extension or content. func detectInputFormat(filename string, data []byte) string { if filename != "-" && filename != "" { diff --git a/cmd/user_prompt.go b/cmd/user_prompt.go index a66fd51..91f77fb 100644 --- a/cmd/user_prompt.go +++ b/cmd/user_prompt.go @@ -52,12 +52,6 @@ func promptSelect(label string, options []string) (int, error) { return prompter.Select(label, options) } -// promptMultiSelect presents a checklist with defaults pre-selected. Returns the -// 0-based indices of selected items, or nil for none. -func promptMultiSelect(label string, options []string, defaults []int) ([]int, error) { - return prompter.MultiSelect(label, options, defaults) -} - // promptYesNo prompts for a yes/no answer. defaultYes controls what pressing Enter // returns. Returns true for yes, false for no. func promptYesNo(label string, defaultYes bool) (bool, error) { diff --git a/cmd/usergroup.go b/cmd/usergroup.go index 58544f7..e708458 100644 --- a/cmd/usergroup.go +++ b/cmd/usergroup.go @@ -290,9 +290,14 @@ input on a terminal) to be prompted for the name, description, and roles.`, return fmt.Errorf("provide --from-file, pipe JSON to stdin, or use --interactive") } var g client.UserGroup - if werr := runGroupWizard(ctx, c, &g, true); werr != nil { + ok, werr := runGroupWizard(ctx, c, &g, true) + if werr != nil { return werr } + if !ok { + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Canceled.") + return nil + } in.groups = []client.UserGroup{g} } @@ -372,9 +377,14 @@ group by its "id", or by "userGroupName" when no id is present.`, case in.present: applyGroupPatch(target, &in.groups[0]) case interactive || config.IsTerminal(): - if werr := runGroupWizard(ctx, c, target, false); werr != nil { + ok, werr := runGroupWizard(ctx, c, target, false) + if werr != nil { return werr } + if !ok { + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Canceled.") + return nil + } default: return fmt.Errorf("provide --from-file, pipe JSON to stdin, or use --interactive") } diff --git a/cmd/usergroup_prompt.go b/cmd/usergroup_prompt.go index 1ef37a3..3d430d2 100644 --- a/cmd/usergroup_prompt.go +++ b/cmd/usergroup_prompt.go @@ -7,6 +7,7 @@ import ( "github.com/jbrazda/iics-cli/internal/client" "github.com/jbrazda/iics-cli/internal/config" + "github.com/jbrazda/iics-cli/internal/tui" ) // listAllUserGroups fetches every user group, following pagination. @@ -66,74 +67,39 @@ func resolveUserGroup(ctx context.Context, c *client.Client, id, name, action st } } -// runGroupWizard interactively edits a user group. On create it prompts for -// name, description, and roles. On update (forCreate=false) only roles can -// change - the v3 API cannot rename a group or edit its description - so the -// name/description are shown for context and only the role selection is prompted. -func runGroupWizard(ctx context.Context, c *client.Client, g *client.UserGroup, forCreate bool) error { +// runGroupWizard interactively edits a user group. On create it asks for the +// name, description and roles; on update (forCreate=false) only roles can +// change - the v3 API cannot rename a group or edit its description. It +// returns false if the user canceled. +func runGroupWizard(ctx context.Context, c *client.Client, g *client.UserGroup, forCreate bool) (bool, error) { if !config.IsTerminal() { - return fmt.Errorf("--interactive requires a terminal") + return false, fmt.Errorf("--interactive requires a terminal") } - - if forCreate { - for { - name, err := promptText("Group Name", g.UserGroupName) - if err != nil { - return err - } - if name != "" { - g.UserGroupName = name - break - } - _, _ = fmt.Fprintln(os.Stderr, "Group name is required.") - } - desc, err := promptText("Description (optional)", g.Description) - if err != nil { - return err - } - g.Description = desc - } else { - _, _ = fmt.Fprintf(os.Stderr, "Editing group %q", g.UserGroupName) - if g.Description != "" { - _, _ = fmt.Fprintf(os.Stderr, " - %s", g.Description) - } - _, _ = fmt.Fprintln(os.Stderr, " (name and description cannot be changed via the API)") - } - roles, err := listAllRoles(ctx, c) if err != nil { - return err + return false, err } if len(roles) == 0 { - return fmt.Errorf("no roles available; a user group requires at least one role") - } - current := make(map[string]bool) - for _, r := range g.Roles { - current[r.ID] = true - } - labels := make([]string, len(roles)) - defaults := make([]int, 0) - for i, r := range roles { - labels[i] = fmt.Sprintf("%s - %s", r.RoleName, truncate(r.Description, 80)) - if current[r.ID] { - defaults = append(defaults, i) - } + return false, fmt.Errorf("no roles available; a user group requires at least one role") } - for { - picks, perr := promptMultiSelect("Roles (at least one required)", labels, defaults) - if perr != nil { - return perr - } - if len(picks) == 0 { - _, _ = fmt.Fprintln(os.Stderr, "Select at least one role.") - continue + var existing []string + if forCreate { + groups, gerr := listAllUserGroups(ctx, c) + if gerr != nil { + return false, gerr } - g.Roles = nil - for _, idx := range picks { - g.Roles = append(g.Roles, client.UserRole{ID: roles[idx].ID, RoleName: roles[idx].RoleName}) + for _, eg := range groups { + existing = append(existing, eg.UserGroupName) } - return nil } + return tui.RunGroupWizard(tui.GroupWizardInput{ + Group: g, + Update: !forCreate, + Roles: roles, + ExistingNames: existing, + Out: os.Stderr, + Accessible: prompter.Accessible, + }) } // listAllRoles fetches every role, following pagination. diff --git a/docs/ChangeRequests/new/CR-0041-tui-shared-prompts-and-wizards.md b/docs/ChangeRequests/new/CR-0041-tui-shared-prompts-and-wizards.md index d95d5c9..9accdb3 100644 --- a/docs/ChangeRequests/new/CR-0041-tui-shared-prompts-and-wizards.md +++ b/docs/ChangeRequests/new/CR-0041-tui-shared-prompts-and-wizards.md @@ -122,7 +122,9 @@ directly (filterable) instead of the query loop. pre-checked, review on update. Applying updates is blocked by the existing V2 update failure (`docs/issues/new/2026-09-30-user-update-v2-403.md`). -- [ ] User group and runtime wizards: pre-checked multi-select plus review +- [x] User group and runtime wizards: pre-checked multi-select plus review + (runtime wizard is create-only, as before; agent checklist not + exercised live because `dev` has no unassigned agents) - [ ] All pickers are filterable - [ ] Delete confirmations unchanged in behavior, deduplicated - [ ] Live test on `dev`: create/update/delete a throwaway user, group and diff --git a/docs/documentation/environment.md b/docs/documentation/environment.md index 565a4ec..ca35aa8 100644 --- a/docs/documentation/environment.md +++ b/docs/documentation/environment.md @@ -143,12 +143,12 @@ prompts. Without a file and without a terminal, `--from-file` is required. ### Interactive prompts -1. `Environment Name` (required). -2. `Description` (optional). -3. `Is shared` (y/N). -4. Agent management menu - `Add agents` lists the currently unassigned Secure - Agents to pick from; `Remove agents` removes from the working selection; - `Done` creates the environment with the selected agents. +1. **Runtime environment** - name (required; existing names are rejected), + description, and whether the environment is shared. +2. **Agents** - a filterable checklist of unassigned Secure Agents (name, host, + active or inactive). Agents from `--from-file` are pre-checked. Selecting + agents is optional. +3. **Review** - **Create environment**, **Back to editing** or **Cancel**. See [Interactive prompts](interactive.md) for keys and piped input. diff --git a/docs/documentation/group.md b/docs/documentation/group.md index d8ae46a..05157b0 100644 --- a/docs/documentation/group.md +++ b/docs/documentation/group.md @@ -109,6 +109,10 @@ A JSON **array** creates groups in bulk and prints a per-group result table | `--interactive`, `-i` | bool | Prompt for name, description, and roles | The wizard runs with `-i`, or when no input source is present on a terminal. +It asks for the group name (existing names are rejected) and description, then +shows the roles as a filterable checklist (at least one role is required), and +ends with a review: **Create group**, **Back to editing** or **Cancel**. See +[Interactive prompts](interactive.md) for keys. ### JSON definition @@ -161,6 +165,12 @@ the identifiers the add/remove endpoints use). A file produced by | `--from-file` | string | JSON file (object or array); omit to read piped stdin | | `--interactive`, `-i` | bool | Edit the role selection interactively | +The interactive editor shows the group's current roles pre-checked in a +filterable checklist. Name and description are shown read-only because the +API cannot change them. The review lists the changes (`+ Role: ...`, +`- Role: ...`) before they are applied; with no changes only **Back to +editing** and **Cancel** are offered. + ### Examples ```bash diff --git a/internal/tui/groupwizard.go b/internal/tui/groupwizard.go new file mode 100644 index 0000000..71f5626 --- /dev/null +++ b/internal/tui/groupwizard.go @@ -0,0 +1,120 @@ +package tui + +import ( + "errors" + "io" + "sort" + "strings" + + "github.com/charmbracelet/huh" + "github.com/jbrazda/iics-cli/internal/client" +) + +// GroupWizardInput configures RunGroupWizard. +type GroupWizardInput struct { + // Group holds the starting values and receives the result. + Group *client.UserGroup + // Update is true when editing an existing group. Only roles can change; + // the API cannot rename a group or edit its description. + Update bool + // Roles are all roles in the organization. + Roles []client.Role + // ExistingNames are the names of existing groups, rejected on create. + ExistingNames []string + + Out io.Writer + Accessible bool +} + +// RunGroupWizard collects a user group's name, description (create only) and +// roles, then shows a review. It returns false if the user canceled. +func RunGroupWizard(in GroupWizardInput) (bool, error) { + g := in.Group + before := roleNames(g.Roles) + roleIDs := make([]string, 0, len(g.Roles)) + for _, r := range g.Roles { + roleIDs = append(roleIDs, r.ID) + } + + for { + var groups []*huh.Group + if in.Update { + desc := g.Description + if desc == "" { + desc = "-" + } + groups = append(groups, huh.NewGroup( + huh.NewNote().Title("User group: "+g.UserGroupName). + Description("Description: "+desc+"\nName and description cannot be changed through the API."), + rolesField(in.Roles, &roleIDs), + )) + } else { + groups = append(groups, + huh.NewGroup( + huh.NewInput().Title("Group name").Value(&g.UserGroupName). + Validate(requiredUnique("group name", in.ExistingNames)), + huh.NewInput().Title("Description (optional)").Value(&g.Description), + ).Title("User group"), + huh.NewGroup(rolesField(in.Roles, &roleIDs)).Title("Roles"), + ) + } + err := huh.NewForm(groups...).WithOutput(in.Out).WithAccessible(in.Accessible).Run() + if errors.Is(err, huh.ErrUserAborted) { + return false, nil + } + if err != nil { + return false, err + } + + g.UserGroupName = strings.TrimSpace(g.UserGroupName) + g.Description = strings.TrimSpace(g.Description) + g.Roles = rolesByID(in.Roles, roleIDs) + + var title, applyLabel string + var lines []string + canApply := true + if in.Update { + title, applyLabel = "Update user group "+g.UserGroupName, "Apply changes" + lines = SetChanges("Role", before, roleNames(g.Roles)) + if len(lines) == 0 { + lines, canApply = []string{"No changes."}, false + } + } else { + title, applyLabel = "Create user group "+g.UserGroupName, "Create group" + lines = []string{ + "Name: " + g.UserGroupName, + "Description: " + orDash(g.Description), + "Roles: " + strings.Join(roleNames(g.Roles), ", "), + } + } + choice, err := confirmReview(in.Out, in.Accessible, title, lines, applyLabel, canApply) + if err != nil { + return false, err + } + switch choice { + case reviewApply: + return true, nil + case reviewCancel: + return false, nil + } + } +} + +func rolesField(roles []client.Role, roleIDs *[]string) huh.Field { + sorted := append([]client.Role(nil), roles...) + sort.SliceStable(sorted, func(i, j int) bool { + return strings.ToLower(sorted[i].RoleName) < strings.ToLower(sorted[j].RoleName) + }) + opts := make([]huh.Option[string], len(sorted)) + for i, r := range sorted { + opts[i] = huh.NewOption(optionLabel(r.RoleName, r.Description), r.ID) + } + return huh.NewMultiSelect[string](). + Title("Roles (at least one)"). + Description("space toggle, / filter, ctrl+a all"). + Options(opts...). + Filterable(true). + Height(min(len(opts)+3, 14)). + Validate(atLeastOne("role")). + Value(roleIDs) +} diff --git a/internal/tui/review.go b/internal/tui/review.go new file mode 100644 index 0000000..2cd9ea1 --- /dev/null +++ b/internal/tui/review.go @@ -0,0 +1,102 @@ +package tui + +import ( + "errors" + "fmt" + "io" + "sort" + "strings" + + "github.com/charmbracelet/huh" +) + +// Review choices returned by confirmReview. +const ( + reviewApply = "apply" + reviewBack = "back" + reviewCancel = "cancel" +) + +// confirmReview shows lines under title and asks whether to apply, go back to +// editing, or cancel. When canApply is false the apply option is omitted +// (for example when nothing changed or a required value is missing). +func confirmReview(out io.Writer, accessible bool, title string, lines []string, applyLabel string, canApply bool) (string, error) { + var options []huh.Option[string] + if canApply { + options = append(options, huh.NewOption(applyLabel, reviewApply)) + } + options = append(options, + huh.NewOption("Back to editing", reviewBack), + huh.NewOption("Cancel", reviewCancel), + ) + choice := options[0].Value + sel := huh.NewSelect[string](). + Title(title). + Description(strings.Join(lines, "\n")). + Options(options...). + Value(&choice) + err := huh.NewForm(huh.NewGroup(sel)).WithOutput(out).WithAccessible(accessible).Run() + if errors.Is(err, huh.ErrUserAborted) { + return reviewCancel, nil + } + if err != nil { + return "", err + } + return choice, nil +} + +// SetChanges describes membership changes between two sets of names as +// "+ : name" and "- : name" lines, additions first, each sorted. +func SetChanges(kind string, before, after []string) []string { + cur := toSet(before) + want := toSet(after) + var add, remove []string + for n := range want { + if !cur[n] { + add = append(add, n) + } + } + for n := range cur { + if !want[n] { + remove = append(remove, n) + } + } + sort.Strings(add) + sort.Strings(remove) + var out []string + for _, n := range add { + out = append(out, fmt.Sprintf("+ %s: %s", kind, n)) + } + for _, n := range remove { + out = append(out, fmt.Sprintf("- %s: %s", kind, n)) + } + return out +} + +// requiredUnique validates a non-empty name that is not in existing +// (case-insensitive). +func requiredUnique(what string, existing []string) func(string) error { + taken := make(map[string]bool, len(existing)) + for _, n := range existing { + taken[strings.ToLower(n)] = true + } + return func(v string) error { + v = strings.TrimSpace(v) + if v == "" { + return fmt.Errorf("%s is required", what) + } + if taken[strings.ToLower(v)] { + return fmt.Errorf("%q already exists", v) + } + return nil + } +} + +func atLeastOne(what string) func([]string) error { + return func(v []string) error { + if len(v) == 0 { + return fmt.Errorf("select at least one %s", what) + } + return nil + } +} diff --git a/internal/tui/review_test.go b/internal/tui/review_test.go new file mode 100644 index 0000000..18bf6ae --- /dev/null +++ b/internal/tui/review_test.go @@ -0,0 +1,61 @@ +package tui + +import ( + "reflect" + "testing" + + "github.com/jbrazda/iics-cli/internal/client" +) + +func TestSetChanges(t *testing.T) { + got := SetChanges("Role", []string{"B", "A", "C"}, []string{"C", "D", "B", "E"}) + want := []string{"+ Role: D", "+ Role: E", "- Role: A"} + if !reflect.DeepEqual(got, want) { + t.Errorf("SetChanges = %v, want %v", got, want) + } + if got := SetChanges("Role", []string{"A"}, []string{"A"}); got != nil { + t.Errorf("expected no changes, got %v", got) + } +} + +func TestRequiredUnique(t *testing.T) { + v := requiredUnique("group name", []string{"Data Engineering"}) + if err := v(" "); err == nil { + t.Error("empty name should fail") + } + if err := v("data engineering"); err == nil { + t.Error("existing name (case-insensitive) should fail") + } + if err := v("Data Science"); err != nil { + t.Errorf("new name should pass: %v", err) + } +} + +func TestAtLeastOne(t *testing.T) { + v := atLeastOne("role") + if err := v(nil); err == nil { + t.Error("empty selection should fail") + } + if err := v([]string{"r1"}); err != nil { + t.Errorf("one selection should pass: %v", err) + } +} + +func TestAgentItems(t *testing.T) { + available := []client.Agent{ + {ID: "a2", Name: "zeta", AgentHost: "host2", Active: true}, + {ID: "a1", Name: "Alpha"}, + {ID: "a1", Name: "Alpha"}, // duplicate + } + seeded := []client.RuntimeEnvironmentAgent{{ID: "a2"}, {ID: "a3", Name: "Mid"}, {ID: "a4"}} + got := agentItems(available, seeded) + + var labels []string + for _, it := range got { + labels = append(labels, it.label) + } + want := []string{"a4 (from file)", "Alpha - inactive", "Mid (from file)", "zeta (host2) - active"} + if !reflect.DeepEqual(labels, want) { + t.Errorf("labels = %v, want %v", labels, want) + } +} diff --git a/internal/tui/runtimewizard.go b/internal/tui/runtimewizard.go new file mode 100644 index 0000000..af3c384 --- /dev/null +++ b/internal/tui/runtimewizard.go @@ -0,0 +1,148 @@ +package tui + +import ( + "errors" + "fmt" + "io" + "sort" + "strings" + + "github.com/charmbracelet/huh" + "github.com/jbrazda/iics-cli/internal/client" +) + +// RuntimeWizardInput configures RunRuntimeWizard. +type RuntimeWizardInput struct { + // Env holds the starting values (e.g. from --from-file) and receives the + // result. + Env *client.RuntimeEnvironment + // Agents are the Secure Agents that can be added (not assigned to + // another environment). + Agents []client.Agent + // ExistingNames are the names of existing environments, rejected. + ExistingNames []string + + Out io.Writer + Accessible bool +} + +// RunRuntimeWizard collects a new runtime environment's name, description, +// sharing and member agents, then shows a review. It returns false if the +// user canceled. +func RunRuntimeWizard(in RuntimeWizardInput) (bool, error) { + env := in.Env + items := agentItems(in.Agents, env.Agents) + agentIDs := make([]string, 0, len(env.Agents)) + for _, a := range env.Agents { + if a.ID != "" { + agentIDs = append(agentIDs, a.ID) + } + } + + for { + fields := []*huh.Group{ + huh.NewGroup( + huh.NewInput().Title("Environment name").Value(&env.Name). + Validate(requiredUnique("environment name", in.ExistingNames)), + huh.NewInput().Title("Description (optional)").Value(&env.Description), + huh.NewConfirm().Title("Shared environment?").Value(&env.IsShared), + ).Title("Runtime environment"), + } + if len(items) > 0 { + opts := make([]huh.Option[string], len(items)) + for i, it := range items { + opts[i] = huh.NewOption(it.label, it.id) + } + fields = append(fields, huh.NewGroup( + huh.NewMultiSelect[string](). + Title("Secure Agents (optional)"). + Description("Unassigned agents. space toggle, / filter, ctrl+a all"). + Options(opts...). + Filterable(true). + Height(min(len(opts)+3, 14)). + Value(&agentIDs), + ).Title("Agents")) + } else { + fields = append(fields, huh.NewGroup( + huh.NewNote().Title("Agents").Description("No unassigned Secure Agents are available. Agents can be added later."), + )) + } + err := huh.NewForm(fields...).WithOutput(in.Out).WithAccessible(in.Accessible).Run() + if errors.Is(err, huh.ErrUserAborted) { + return false, nil + } + if err != nil { + return false, err + } + + env.Name = strings.TrimSpace(env.Name) + env.Description = strings.TrimSpace(env.Description) + want := toSet(agentIDs) + env.Agents = nil + var names []string + for _, it := range items { + if want[it.id] { + env.Agents = append(env.Agents, client.RuntimeEnvironmentAgent{ID: it.id}) + names = append(names, it.name) + } + } + + lines := []string{ + "Name: " + env.Name, + "Description: " + orDash(env.Description), + fmt.Sprintf("Shared: %t", env.IsShared), + "Agents: " + orDash(strings.Join(names, ", ")), + } + choice, err := confirmReview(in.Out, in.Accessible, "Create runtime environment "+env.Name, lines, "Create environment", true) + if err != nil { + return false, err + } + switch choice { + case reviewApply: + return true, nil + case reviewCancel: + return false, nil + } + } +} + +type agentItem struct { + id, name, label string +} + +// agentItems builds checklist entries from the available agents plus any +// seeded agents not in that list (e.g. from --from-file), sorted by name. +func agentItems(available []client.Agent, seeded []client.RuntimeEnvironmentAgent) []agentItem { + seen := make(map[string]bool) + var items []agentItem + for _, a := range available { + if a.ID == "" || seen[a.ID] { + continue + } + seen[a.ID] = true + status := "inactive" + if a.Active { + status = "active" + } + label := a.Name + if a.AgentHost != "" { + label += " (" + a.AgentHost + ")" + } + items = append(items, agentItem{id: a.ID, name: a.Name, label: label + " - " + status}) + } + for _, a := range seeded { + if a.ID == "" || seen[a.ID] { + continue + } + seen[a.ID] = true + name := a.Name + if name == "" { + name = a.ID + } + items = append(items, agentItem{id: a.ID, name: name, label: name + " (from file)"}) + } + sort.SliceStable(items, func(i, j int) bool { + return strings.ToLower(items[i].name) < strings.ToLower(items[j].name) + }) + return items +} diff --git a/internal/tui/userwizard.go b/internal/tui/userwizard.go index a2d7b0d..1b3cb0b 100644 --- a/internal/tui/userwizard.go +++ b/internal/tui/userwizard.go @@ -83,9 +83,9 @@ func RunUserWizard(in UserWizardInput) (bool, error) { return false, err } switch choice { - case "apply": + case reviewApply: return true, nil - case "cancel": + case reviewCancel: return false, nil } } @@ -209,43 +209,23 @@ func membershipGroup(in UserWizardInput, groupIDs, roleIDs *[]string) *huh.Group } // reviewUser shows the result (create) or the changes (update) and returns -// "apply", "back" or "cancel". +// reviewApply, reviewBack or reviewCancel. func reviewUser(in UserWizardInput, before, after *client.User) (string, error) { - var desc string - title := "Create user " + after.UserName - applyLabel := "Create user" if in.Update { - title = "Update user " + after.UserName - applyLabel = "Apply changes" - changes := UserChanges(before, after) - if len(changes) == 0 { - desc = "No changes." - } else { - desc = strings.Join(changes, "\n") + lines := UserChanges(before, after) + canApply := len(lines) > 0 + if !canApply { + lines = []string{"No changes."} } - } else { - desc = strings.Join(UserSummary(after), "\n") - } - - options := []huh.Option[string]{ - huh.NewOption(applyLabel, "apply"), - huh.NewOption("Back to editing", "back"), - huh.NewOption("Cancel", "cancel"), + return confirmReview(in.Out, in.Accessible, "Update user "+after.UserName, lines, "Apply changes", canApply) } + lines := UserSummary(after) // The create API requires at least one role or user group. - if !in.Update && len(after.Groups) == 0 && len(after.Roles) == 0 { - desc += "\n\nSelect at least one user group or role." - options = options[1:] - } - choice := options[0].Value - sel := huh.NewSelect[string]().Title(title).Description(desc).Options(options...).Value(&choice) - if err := huh.NewForm(huh.NewGroup(sel)).WithOutput(in.Out).WithAccessible(in.Accessible).Run(); err != nil { - if errors.Is(err, huh.ErrUserAborted) { - return "cancel", nil - } - return "", err + canApply := len(after.Groups) > 0 || len(after.Roles) > 0 + if !canApply { + lines = append(lines, "", "Select at least one user group or role.") } - return choice, nil + return confirmReview(in.Out, in.Accessible, "Create user "+after.UserName, lines, "Create user", canApply) } // UserSummary lists the fields of a user to be created. @@ -299,21 +279,8 @@ func UserChanges(before, after *client.User) []string { if before.ForcePasswordChange != after.ForcePasswordChange { out = append(out, fmt.Sprintf("Force password change: %t -> %t", before.ForcePasswordChange, after.ForcePasswordChange)) } - add, remove := client.DiffPrivileges(groupNames(before.Groups), groupNames(after.Groups)) - out = append(out, signed("Group", add, remove)...) - add, remove = client.DiffPrivileges(roleNames(before.Roles), roleNames(after.Roles)) - out = append(out, signed("Role", add, remove)...) - return out -} - -func signed(kind string, add, remove []string) []string { - var out []string - for _, n := range add { - out = append(out, fmt.Sprintf("+ %s: %s", kind, n)) - } - for _, n := range remove { - out = append(out, fmt.Sprintf("- %s: %s", kind, n)) - } + out = append(out, SetChanges("Group", groupNames(before.Groups), groupNames(after.Groups))...) + out = append(out, SetChanges("Role", roleNames(before.Roles), roleNames(after.Roles))...) return out } From b9941b6fd723119101d4fb1a77efb0d42e536061 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Tue, 29 Sep 2026 22:23:54 -0400 Subject: [PATCH 08/52] refactor(cmd): shared pickers and confirmAction (CR-0041 #4) - pickUserGroup, pickAgent, pickAgentService use tui.PickOne - confirmAction replaces 12 duplicated [y/N] confirmation blocks with identical prompt text and behavior (plain line input kept for scripts) - docs: interactive.md confirmations section - CR-0041 acceptance criteria complete; moved to pending --- cmd/agent.go | 10 ++------- cmd/agent_service.go | 18 ++++++--------- cmd/confirm.go | 22 +++++++++++++++++++ cmd/connection.go | 10 ++------- cmd/folder.go | 10 ++------- cmd/package.go | 10 ++------- cmd/permission.go | 10 ++------- cmd/profile.go | 10 ++------- cmd/project.go | 10 ++------- cmd/role.go | 10 ++------- cmd/runtime.go | 10 ++------- cmd/runtime_configs.go | 10 ++------- cmd/schedule.go | 10 ++------- cmd/usergroup.go | 10 ++------- cmd/usergroup_prompt.go | 8 +++---- .../CR-0041-tui-shared-prompts-and-wizards.md | 14 +++++++----- docs/documentation/interactive.md | 14 ++++++++---- 17 files changed, 75 insertions(+), 121 deletions(-) create mode 100644 cmd/confirm.go rename docs/ChangeRequests/{new => pending}/CR-0041-tui-shared-prompts-and-wizards.md (91%) diff --git a/cmd/agent.go b/cmd/agent.go index b24cc6f..ca0bc85 100644 --- a/cmd/agent.go +++ b/cmd/agent.go @@ -426,14 +426,8 @@ func newAgentDeleteCmd() *cobra.Command { if err != nil { return err } - if !yes { - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Are you sure you want to delete agent %s? [y/N]: ", agentID) - var confirm string - _, _ = fmt.Scanln(&confirm) - if confirm != "y" && confirm != "Y" { - _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Canceled.") - return nil - } + if !yes && !confirmAction(cmd, fmt.Sprintf("Are you sure you want to delete agent %s?", agentID)) { + return nil } if err := c.DeleteAgent(ctx, agentID); err != nil { return err diff --git a/cmd/agent_service.go b/cmd/agent_service.go index 6b151c4..938df81 100644 --- a/cmd/agent_service.go +++ b/cmd/agent_service.go @@ -10,6 +10,7 @@ import ( "github.com/jbrazda/iics-cli/internal/client" "github.com/jbrazda/iics-cli/internal/config" + "github.com/jbrazda/iics-cli/internal/tui" "github.com/spf13/cobra" ) @@ -239,11 +240,9 @@ func pickAgent(ctx context.Context, c *client.Client) (*client.Agent, error) { if len(agents) == 0 { return nil, fmt.Errorf("no agents found") } - labels := make([]string, len(agents)) - for i, a := range agents { - labels[i] = fmt.Sprintf("%s (%s) - %s", a.Name, a.AgentHost, a.ID) - } - idx, err := promptSelect("Select an agent", labels) + idx, err := tui.PickOne(prompter, "Select an agent", agents, func(a client.Agent) string { + return fmt.Sprintf("%s (%s) - %s", a.Name, a.AgentHost, a.ID) + }) if err != nil { return nil, err } @@ -263,12 +262,9 @@ func pickAgentService(ctx context.Context, c *client.Client, agentID, verb strin if len(details.AgentEngines) == 0 { return "", fmt.Errorf("no running services found on agent %s", agentID) } - labels := make([]string, len(details.AgentEngines)) - for i, e := range details.AgentEngines { - st := e.AgentEngineStatus - labels[i] = fmt.Sprintf("%s (%s)", st.AppDisplayName, st.Status) - } - idx, err := promptSelect(fmt.Sprintf("Select a service to %s", verb), labels) + idx, err := tui.PickOne(prompter, fmt.Sprintf("Select a service to %s", verb), details.AgentEngines, func(e client.AgentEngine) string { + return fmt.Sprintf("%s (%s)", e.AgentEngineStatus.AppDisplayName, e.AgentEngineStatus.Status) + }) if err != nil { return "", err } diff --git a/cmd/confirm.go b/cmd/confirm.go new file mode 100644 index 0000000..7101f19 --- /dev/null +++ b/cmd/confirm.go @@ -0,0 +1,22 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// confirmAction asks question followed by " [y/N]: " on the command's output +// and reads one line from stdin. It returns true only for "y" or "Y"; any other +// answer prints "Canceled." and returns false. The plain line format is kept +// deliberately so scripts can pipe an answer. +func confirmAction(cmd *cobra.Command, question string) bool { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s [y/N]: ", question) + var confirm string + _, _ = fmt.Scanln(&confirm) + if confirm != "y" && confirm != "Y" { + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Canceled.") + return false + } + return true +} diff --git a/cmd/connection.go b/cmd/connection.go index 3d41937..d3871e1 100644 --- a/cmd/connection.go +++ b/cmd/connection.go @@ -221,14 +221,8 @@ func newConnectionDeleteCmd() *cobra.Command { return fmt.Errorf("--id is required") } - if !yes { - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Are you sure you want to delete connection %s? [y/N]: ", id) - var confirm string - _, _ = fmt.Scanln(&confirm) - if confirm != "y" && confirm != "Y" { - _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Canceled.") - return nil - } + if !yes && !confirmAction(cmd, fmt.Sprintf("Are you sure you want to delete connection %s?", id)) { + return nil } c, err := getClient(cmd) diff --git a/cmd/folder.go b/cmd/folder.go index 6b3842e..e58e4ee 100644 --- a/cmd/folder.go +++ b/cmd/folder.go @@ -115,14 +115,8 @@ func newFolderDeleteCmd() *cobra.Command { return fmt.Errorf("--id is required") } - if !yes { - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Are you sure you want to delete folder %s? [y/N]: ", id) - var confirm string - _, _ = fmt.Scanln(&confirm) - if confirm != "y" && confirm != "Y" { - _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Canceled.") - return nil - } + if !yes && !confirmAction(cmd, fmt.Sprintf("Are you sure you want to delete folder %s?", id)) { + return nil } c, err := getClient(cmd) diff --git a/cmd/package.go b/cmd/package.go index a43550a..0eee2fd 100644 --- a/cmd/package.go +++ b/cmd/package.go @@ -82,14 +82,8 @@ func newPackageExpandCmd() *cobra.Command { if !clean { return fmt.Errorf("target directory is not empty; use --clean to overwrite") } - if !yes { - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "About to delete %d entries from %s. Continue? [y/N]: ", len(entries), target) - var confirm string - _, _ = fmt.Scanln(&confirm) - if confirm != "y" && confirm != "Y" { - _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Canceled.") - return nil - } + if !yes && !confirmAction(cmd, fmt.Sprintf("About to delete %d entries from %s. Continue?", len(entries), target)) { + return nil } for _, e := range entries { if err := os.RemoveAll(filepath.Join(target, e.Name())); err != nil { diff --git a/cmd/permission.go b/cmd/permission.go index 765e551..451b363 100644 --- a/cmd/permission.go +++ b/cmd/permission.go @@ -110,14 +110,8 @@ func newPermissionDeleteCmd() *cobra.Command { if objectID == "" { return fmt.Errorf("--object-id is required") } - if !yes { - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Are you sure you want to delete permissions for object %s? [y/N]: ", objectID) - var confirm string - _, _ = fmt.Scanln(&confirm) - if confirm != "y" && confirm != "Y" { - _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Canceled.") - return nil - } + if !yes && !confirmAction(cmd, fmt.Sprintf("Are you sure you want to delete permissions for object %s?", objectID)) { + return nil } c, err := getClient(cmd) if err != nil { diff --git a/cmd/profile.go b/cmd/profile.go index 640ec69..da11eae 100644 --- a/cmd/profile.go +++ b/cmd/profile.go @@ -362,14 +362,8 @@ func newProfileDeleteCmd() *cobra.Command { return fmt.Errorf("profile %q not found", name) } - if !yes { - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Delete profile %q? [y/N]: ", name) - var confirm string - _, _ = fmt.Scanln(&confirm) - if confirm != "y" && confirm != "Y" { - _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Canceled.") - return nil - } + if !yes && !confirmAction(cmd, fmt.Sprintf("Delete profile %q?", name)) { + return nil } delete(cfg.Profiles, name) diff --git a/cmd/project.go b/cmd/project.go index 4678831..553a07e 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -107,14 +107,8 @@ func newProjectDeleteCmd() *cobra.Command { return fmt.Errorf("--id is required") } - if !yes { - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Are you sure you want to delete project %s? [y/N]: ", id) - var confirm string - _, _ = fmt.Scanln(&confirm) - if confirm != "y" && confirm != "Y" { - _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Canceled.") - return nil - } + if !yes && !confirmAction(cmd, fmt.Sprintf("Are you sure you want to delete project %s?", id)) { + return nil } c, err := getClient(cmd) diff --git a/cmd/role.go b/cmd/role.go index 8a2d7fe..972b7b4 100644 --- a/cmd/role.go +++ b/cmd/role.go @@ -411,14 +411,8 @@ func newRoleDeleteCmd() *cobra.Command { id = role.ID label = fmt.Sprintf("%s (ID: %s)", role.RoleName, role.ID) } - if !yes { - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Are you sure you want to delete role %s? [y/N]: ", label) - var confirm string - _, _ = fmt.Scanln(&confirm) - if confirm != "y" && confirm != "Y" { - _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Canceled.") - return nil - } + if !yes && !confirmAction(cmd, fmt.Sprintf("Are you sure you want to delete role %s?", label)) { + return nil } if err := c.DeleteRole(context.Background(), id); err != nil { return err diff --git a/cmd/runtime.go b/cmd/runtime.go index ca27183..d6e1952 100644 --- a/cmd/runtime.go +++ b/cmd/runtime.go @@ -364,14 +364,8 @@ func newRuntimeDeleteCmd() *cobra.Command { if err != nil { return err } - if !yes { - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Are you sure you want to delete runtime environment %s? [y/N]: ", envID) - var confirm string - _, _ = fmt.Scanln(&confirm) - if confirm != "y" && confirm != "Y" { - _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Canceled.") - return nil - } + if !yes && !confirmAction(cmd, fmt.Sprintf("Are you sure you want to delete runtime environment %s?", envID)) { + return nil } if err := c.DeleteRuntimeEnvironment(ctx, envID); err != nil { return err diff --git a/cmd/runtime_configs.go b/cmd/runtime_configs.go index b16827e..9a7248f 100644 --- a/cmd/runtime_configs.go +++ b/cmd/runtime_configs.go @@ -131,14 +131,8 @@ func newRuntimeConfigsSetCmd() *cobra.Command { if err != nil { return err } - if !yes { - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Replace service property overrides for group %s? [y/N]: ", groupID) - var confirm string - _, _ = fmt.Scanln(&confirm) - if confirm != "y" && confirm != "Y" { - _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Canceled.") - return nil - } + if !yes && !confirmAction(cmd, fmt.Sprintf("Replace service property overrides for group %s?", groupID)) { + return nil } if _, err := c.UpdateAgentGroupConfigs(ctx, groupID, cfg); err != nil { return err diff --git a/cmd/schedule.go b/cmd/schedule.go index 5a4fbe8..ab53531 100644 --- a/cmd/schedule.go +++ b/cmd/schedule.go @@ -209,14 +209,8 @@ func newScheduleDeleteCmd() *cobra.Command { return fmt.Errorf("--id is required") } - if !yes { - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Are you sure you want to delete schedule %s? [y/N]: ", id) - var confirm string - _, _ = fmt.Scanln(&confirm) - if confirm != "y" && confirm != "Y" { - _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Canceled.") - return nil - } + if !yes && !confirmAction(cmd, fmt.Sprintf("Are you sure you want to delete schedule %s?", id)) { + return nil } c, err := getClient(cmd) diff --git a/cmd/usergroup.go b/cmd/usergroup.go index e708458..b6f78b5 100644 --- a/cmd/usergroup.go +++ b/cmd/usergroup.go @@ -482,14 +482,8 @@ func newUsergroupDeleteCmd() *cobra.Command { if err != nil { return err } - if !yes { - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Are you sure you want to delete user group %s (%s)? [y/N]: ", group.UserGroupName, group.ID) - var confirm string - _, _ = fmt.Scanln(&confirm) - if confirm != "y" && confirm != "Y" { - _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Canceled.") - return nil - } + if !yes && !confirmAction(cmd, fmt.Sprintf("Are you sure you want to delete user group %s (%s)?", group.UserGroupName, group.ID)) { + return nil } if err := c.DeleteUserGroup(ctx, group.ID); err != nil { return err diff --git a/cmd/usergroup_prompt.go b/cmd/usergroup_prompt.go index 3d430d2..e9514b5 100644 --- a/cmd/usergroup_prompt.go +++ b/cmd/usergroup_prompt.go @@ -40,11 +40,9 @@ func pickUserGroup(ctx context.Context, c *client.Client, action string) (*clien if len(groups) == 0 { return nil, fmt.Errorf("no user groups found") } - labels := make([]string, len(groups)) - for i, g := range groups { - labels[i] = fmt.Sprintf("%s (%d members, %d roles)", g.UserGroupName, g.CountMembers, g.CountRoles) - } - idx, err := promptSelect(fmt.Sprintf("Select a user group to %s", action), labels) + idx, err := tui.PickOne(prompter, fmt.Sprintf("Select a user group to %s", action), groups, func(g client.UserGroup) string { + return fmt.Sprintf("%s (%d members, %d roles)", g.UserGroupName, g.CountMembers, g.CountRoles) + }) if err != nil { return nil, err } diff --git a/docs/ChangeRequests/new/CR-0041-tui-shared-prompts-and-wizards.md b/docs/ChangeRequests/pending/CR-0041-tui-shared-prompts-and-wizards.md similarity index 91% rename from docs/ChangeRequests/new/CR-0041-tui-shared-prompts-and-wizards.md rename to docs/ChangeRequests/pending/CR-0041-tui-shared-prompts-and-wizards.md index 9accdb3..b69684c 100644 --- a/docs/ChangeRequests/new/CR-0041-tui-shared-prompts-and-wizards.md +++ b/docs/ChangeRequests/pending/CR-0041-tui-shared-prompts-and-wizards.md @@ -125,8 +125,12 @@ directly (filterable) instead of the query loop. - [x] User group and runtime wizards: pre-checked multi-select plus review (runtime wizard is create-only, as before; agent checklist not exercised live because `dev` has no unassigned agents) -- [ ] All pickers are filterable -- [ ] Delete confirmations unchanged in behavior, deduplicated -- [ ] Live test on `dev`: create/update/delete a throwaway user, group and - runtime environment through the wizards -- [ ] `go build`, `go vet`, `golangci-lint`, `go test ./...` pass +- [x] All pickers are filterable (`pickRole`, `pickUserGroup`, `pickAgent`, + `pickAgentService` use `tui.PickOne`) +- [x] Delete confirmations unchanged in behavior, deduplicated into + `confirmAction` (12 call sites; `user delete` writes to stderr and + `login` asks `[Y/n]`, so both keep their own prompt) +- [x] Live test on `dev`: create/update/delete a throwaway user, group and + runtime environment through the wizards (user update blocked by + `docs/issues/new/2026-09-30-user-update-v2-403.md`) +- [x] `go build`, `go vet`, `golangci-lint`, `go test ./...` pass diff --git a/docs/documentation/interactive.md b/docs/documentation/interactive.md index c476505..dfb26a9 100644 --- a/docs/documentation/interactive.md +++ b/docs/documentation/interactive.md @@ -44,9 +44,15 @@ format: | ----------- | --------------------------------------------------------------- | | Text input | One line; empty keeps the default shown in brackets | | Single list | Option number; `0`, `q` or empty cancels | -| Checklist | Comma-separated numbers (`1,3,5`); empty keeps defaults (marked `*`); `0` selects none | | Yes / No | `y`, `yes`, `n`, `no`; empty uses the default | -| Time zone | Search text, then the number of a match; empty keeps the current value; `0` clears | -Commands that require a terminal (for example `role edit`) report an error -instead of prompting. +Wizards with checklists (`user create`/`update --interactive`, `group +create`/`update -i`, `environment create -i`, `role edit`) require a terminal +and report an error instead of prompting; use `--from-file` in scripts. + +## Confirmations + +Destructive commands (`delete`, `package expand --clean`, +`environment configs set`) ask a plain `[y/N]` question even on a terminal, +so an answer can always be piped (`echo y | iics ...`). Only `y` or `Y` +proceeds. Use `--yes` / `-y` to skip the question. From 800a3aaf0dd6d90ce4039b21ce2dbaaef54c2266 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Tue, 29 Sep 2026 23:13:28 -0400 Subject: [PATCH 09/52] feat(user): update-roles and update-groups commands (CR-0043) - user update-roles / update-groups: --id|--username (aliases --uid, --uname), --add and --remove (combinable) or --replace (exclusive, empty removes all); de-duplicated, case-insensitive names validated against the org before any change; no-ops reported and skipped; additions before removals; resulting user printed in the --output format - role assignment endpoints match the role display name (verified on dev); commands send the display name and accept role name or display name - user update --interactive edits only group and role assignments and applies them through the V3 assignment endpoints - client: Add/RemoveUserRoles, Add/RemoveUserGroups, PlanMembership with tests - shared printUser used by user get and the new commands - docs: user.md, README, completions; bug record updated with mitigation --- README.md | 2 +- cmd/user.go | 37 +-- cmd/user_membership.go | 210 ++++++++++++++++++ cmd/user_prompt.go | 26 --- completions/iics.bash | 128 +++++++++++ ...043-user-role-group-assignment-commands.md | 54 +++++ docs/documentation/user.md | 133 ++++++++--- .../new/2026-09-30-user-update-v2-403.md | 15 ++ internal/client/usermembership.go | 190 ++++++++++++++++ internal/client/usermembership_test.go | 115 ++++++++++ internal/tui/userwizard.go | 81 +++---- 11 files changed, 863 insertions(+), 128 deletions(-) create mode 100644 cmd/user_membership.go create mode 100644 docs/ChangeRequests/pending/CR-0043-user-role-group-assignment-commands.md create mode 100644 internal/client/usermembership.go create mode 100644 internal/client/usermembership_test.go diff --git a/README.md b/README.md index e3cc255..12245b9 100644 --- a/README.md +++ b/README.md @@ -278,7 +278,7 @@ it easy to override credentials in CI pipelines without touching the config file | [state](docs/documentation/state.md) | | `fetch`, `load` | Fetch/load object state snapshots | | [tag](docs/documentation/tag.md) | | `assign`, `remove` | Assign/remove tags on objects | | [unpublish](docs/documentation/unpublish.md) | | `start`, `status`, `run` | Unpublish CAI assets from the runtime | -| [user](docs/documentation/user.md) | | `list`, `get`, `create`, `update`, `delete` | Manage users | +| [user](docs/documentation/user.md) | | `list`, `get`, `create`, `update`, `update-roles`, `update-groups`, `delete` | Manage users | | [group](docs/documentation/group.md) | `usergroup`, `ug` | `list`, `get`, `create`, `update`, `delete` | Manage user groups | > **Keeping completions up to date:** After adding or changing any command or flag, regenerate diff --git a/cmd/user.go b/cmd/user.go index 2f81cf1..1187438 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -30,6 +30,8 @@ func newUserCmd() *cobra.Command { cmd.AddCommand(newUserGetCmd()) cmd.AddCommand(newUserCreateCmd()) cmd.AddCommand(newUserUpdateCmd()) + cmd.AddCommand(newUserMembershipCmd(userRoleMembership)) + cmd.AddCommand(newUserMembershipCmd(userGroupMembership)) cmd.AddCommand(newUserDeleteCmd()) cmd.AddCommand(newUserChangePasswordCmd()) cmd.AddCommand(newUserResetPasswordCmd()) @@ -275,27 +277,7 @@ func newUserGetCmd() *cobra.Command { if err != nil { return err } - - f, err := output.ParseFormat(outputFmt) - if err != nil { - return err - } - - cfg, _ := loadConfig() - style := resolveTableStyle(cfg) - - switch f { - case output.FormatCSV: - cols := buildUserCSVColumns(csvFields) - csvFmt := output.New(output.FormatCSV, os.Stdout, style) - return csvFmt.Format([]*client.User{user}, cols) - case output.FormatTable: - return printUserSections(os.Stdout, user, style) - default: - // JSON / YAML: output the full struct - fmtr := output.New(f, os.Stdout, style) - return fmtr.Format(user, nil) - } + return printUser(user, csvFields) }, } @@ -580,7 +562,6 @@ func runUserWizard(ctx context.Context, c *client.Client, existing *client.User) Update: existing != nil, Groups: groups, Roles: roles, - Timezones: iicsTimezones, UserNameDomain: domain, Out: os.Stderr, Accessible: prompter.Accessible, @@ -775,12 +756,14 @@ func newUserUpdateCmd() *cobra.Command { _, _ = fmt.Fprintln(os.Stderr, "Canceled.") return nil } - result, uErr := c.UpdateUser(ctx, target.ID, updated) - if uErr != nil { - return uErr + if aErr := applyUserMembership(ctx, c, cmd.ErrOrStderr(), target, updated); aErr != nil { + return aErr } - cfg, _ := loadConfig() - return printUserSections(os.Stdout, result, resolveTableStyle(cfg)) + result, gErr := c.GetUser(ctx, target.ID) + if gErr != nil { + return gErr + } + return printUser(result, defaultCSVFields) } if fromFile == "" { diff --git a/cmd/user_membership.go b/cmd/user_membership.go new file mode 100644 index 0000000..f3985c0 --- /dev/null +++ b/cmd/user_membership.go @@ -0,0 +1,210 @@ +package cmd + +import ( + "context" + "fmt" + "io" + "os" + "strings" + + "github.com/jbrazda/iics-cli/internal/client" + "github.com/jbrazda/iics-cli/internal/output" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// membershipKind describes one of the two user membership types. +type membershipKind struct { + use, noun, plural string + // available returns every entry that can be assigned. + available func(ctx context.Context, c *client.Client) ([]client.MemberName, error) + // current returns the assignment names (MemberName.Name form) of the user. + current func(u *client.User) []string + add func(c *client.Client, ctx context.Context, userID string, names []string) error + remove func(c *client.Client, ctx context.Context, userID string, names []string) error +} + +var userRoleMembership = membershipKind{ + use: "update-roles", noun: "role", plural: "roles", + // The role assignment endpoints match the role's display name. + available: func(ctx context.Context, c *client.Client) ([]client.MemberName, error) { + roles, err := c.ListRoles(ctx, client.RoleListOptions{}) + if err != nil { + return nil, err + } + names := make([]client.MemberName, len(roles)) + for i, r := range roles { + names[i] = client.MemberName{Name: client.RoleMemberName(r.RoleName, r.DisplayName), Aliases: []string{r.RoleName}} + } + return names, nil + }, + current: func(u *client.User) []string { + names := make([]string, len(u.Roles)) + for i, r := range u.Roles { + names[i] = client.RoleMemberName(r.RoleName, r.DisplayName) + } + return names + }, + add: (*client.Client).AddUserRoles, + remove: (*client.Client).RemoveUserRoles, +} + +var userGroupMembership = membershipKind{ + use: "update-groups", noun: "user group", plural: "user groups", + available: func(ctx context.Context, c *client.Client) ([]client.MemberName, error) { + groups, err := listAllUserGroups(ctx, c) + if err != nil { + return nil, err + } + names := make([]client.MemberName, len(groups)) + for i, g := range groups { + names[i] = client.MemberName{Name: g.UserGroupName} + } + return names, nil + }, + current: func(u *client.User) []string { + names := make([]string, len(u.Groups)) + for i, g := range u.Groups { + names[i] = g.UserGroupName + } + return names + }, + add: (*client.Client).AddUserGroups, + remove: (*client.Client).RemoveUserGroups, +} + +// newUserMembershipCmd builds user update-roles / update-groups. +func newUserMembershipCmd(k membershipKind) *cobra.Command { + var ( + id, userName, csvFields string + req client.MembershipRequest + ) + cmd := &cobra.Command{ + Use: k.use, + Short: fmt.Sprintf("Add, remove or replace a user's %s", k.plural), + Long: fmt.Sprintf(`Add, remove or replace the %[1]s assigned to a user. + +--add and --remove can be combined; --replace sets the exact list (an empty +value removes all %[1]s) and cannot be combined with them. Names match +case-insensitively and duplicates are ignored. Unknown names, or a name in +both --add and --remove, fail before any change is made. Additions are +applied before removals. The resulting user is printed in the --output format.`, k.plural), + Example: fmt.Sprintf(` iics user %[1]s --username jdoe@example.com --add "Designer,Monitor" + iics user %[1]s --id --add Designer --remove Monitor + iics user %[1]s --username jdoe@example.com --replace "Designer" -o json`, k.use), + RunE: func(cmd *cobra.Command, args []string) error { + req.ReplaceSet = cmd.Flags().Changed("replace") + c, err := getClient(cmd) + if err != nil { + return err + } + ctx := context.Background() + + user, err := resolveUser(ctx, c, id, userName) + if err != nil { + return err + } + available, err := k.available(ctx, c) + if err != nil { + return err + } + plan, err := client.PlanMembership(k.noun, available, k.current(user), req) + if err != nil { + return err + } + + w := cmd.ErrOrStderr() + reportSkipped(w, "Already assigned", plan.AlreadyAssigned) + reportSkipped(w, "Not assigned", plan.NotAssigned) + if len(plan.Add) == 0 && len(plan.Remove) == 0 { + _, _ = fmt.Fprintf(w, "No %s changes for %s.\n", k.noun, user.UserName) + } + if len(plan.Add) > 0 { + if err = k.add(c, ctx, user.ID, plan.Add); err != nil { + return err + } + _, _ = fmt.Fprintf(w, "Added %s(s): %s\n", k.noun, strings.Join(plan.Add, ", ")) + } + if len(plan.Remove) > 0 { + if err = k.remove(c, ctx, user.ID, plan.Remove); err != nil { + return err + } + _, _ = fmt.Fprintf(w, "Removed %s(s): %s\n", k.noun, strings.Join(plan.Remove, ", ")) + } + + updated, err := c.GetUser(ctx, user.ID) + if err != nil { + return err + } + return printUser(updated, csvFields) + }, + } + cmd.Flags().StringVar(&id, "id", "", "user ID") + cmd.Flags().StringVar(&userName, "username", "", "user name (exact match)") + cmd.Flags().StringSliceVar(&req.Add, "add", nil, fmt.Sprintf("%s to assign (comma-separated or repeated)", k.plural)) + cmd.Flags().StringSliceVar(&req.Remove, "remove", nil, fmt.Sprintf("%s to unassign (comma-separated or repeated)", k.plural)) + cmd.Flags().StringSliceVar(&req.Replace, "replace", nil, fmt.Sprintf("exact list of %s to keep; all others are removed", k.plural)) + cmd.Flags().StringVar(&csvFields, "fields", defaultCSVFields, "comma-separated fields for CSV output") + cmd.MarkFlagsOneRequired("id", "username") + cmd.MarkFlagsMutuallyExclusive("id", "username") + cmd.MarkFlagsMutuallyExclusive("replace", "add") + cmd.MarkFlagsMutuallyExclusive("replace", "remove") + // Accept --uid / --uname as aliases for --id / --username. + cmd.Flags().SetNormalizeFunc(func(_ *pflag.FlagSet, name string) pflag.NormalizedName { + switch name { + case "uid": + name = "id" + case "uname": + name = "username" + } + return pflag.NormalizedName(name) + }) + return cmd +} + +// applyUserMembership applies the user group and role differences between +// before and after through the add/remove assignment endpoints (additions +// first). Other user fields are not changed. +func applyUserMembership(ctx context.Context, c *client.Client, w io.Writer, before, after *client.User) error { + for _, k := range []membershipKind{userGroupMembership, userRoleMembership} { + add, remove := client.DiffPrivileges(k.current(before), k.current(after)) + if len(add) > 0 { + if err := k.add(c, ctx, before.ID, add); err != nil { + return err + } + _, _ = fmt.Fprintf(w, "Added %s(s): %s\n", k.noun, strings.Join(add, ", ")) + } + if len(remove) > 0 { + if err := k.remove(c, ctx, before.ID, remove); err != nil { + return err + } + _, _ = fmt.Fprintf(w, "Removed %s(s): %s\n", k.noun, strings.Join(remove, ", ")) + } + } + return nil +} + +func reportSkipped(w io.Writer, label string, names []string) { + if len(names) > 0 { + _, _ = fmt.Fprintf(w, "%s (skipped): %s\n", label, strings.Join(names, ", ")) + } +} + +// printUser writes a user in the --output format: sections for table, the +// selected fields for CSV, the full object for JSON and YAML. +func printUser(user *client.User, csvFields string) error { + f, err := output.ParseFormat(outputFmt) + if err != nil { + return err + } + cfg, _ := loadConfig() + style := resolveTableStyle(cfg) + switch f { + case output.FormatCSV: + return output.New(output.FormatCSV, os.Stdout, style).Format([]*client.User{user}, buildUserCSVColumns(csvFields)) + case output.FormatTable: + return printUserSections(os.Stdout, user, style) + default: + return output.New(f, os.Stdout, style).Format(user, nil) + } +} diff --git a/cmd/user_prompt.go b/cmd/user_prompt.go index 91f77fb..b3e7a50 100644 --- a/cmd/user_prompt.go +++ b/cmd/user_prompt.go @@ -124,32 +124,6 @@ func promptUserSearch(ctx context.Context, c *client.Client) (*client.User, erro } } -// iicsTimezones is the complete list of timezone IDs accepted by the IICS v3 API. -// Source: https://docs.informatica.com/cloud-common-services/administrator/current-version/rest-api-reference/rest-api-codes/time-zone-codes.html -var iicsTimezones = []string{ - "ACT", "AET", "Africa/Cairo", "Africa/Casablanca", "Africa/Johannesburg", - "Africa/Nairobi", "America/Barbados", "America/Bogota", "America/Buenos_Aires", - "America/Caracas", "America/Chicago", "America/Costa_Rica", "America/Dawson_Creek", - "America/Denver", "America/Dominica", "America/El_Salvador", "America/Guadeloupe", - "America/Halifax", "America/Havana", "America/Jamaica", "America/La_Paz", - "America/Los_Angeles", "America/Mexico_City", "America/Montreal", "America/New_York", - "America/Panama", "America/Phoenix", "America/Puerto_Rico", "America/Santiago", - "America/Tijuana", "America/Vancouver", "Asia/Baghdad", "Asia/Bahrain", "Asia/Dubai", - "Asia/Hong_Kong", "Asia/Jerusalem", "Asia/Karachi", "Asia/Katmandu", - "Asia/Kuala_Lumpur", "Asia/Kuwait", "Asia/Magadan", "Asia/Muscat", "Asia/Qatar", - "Asia/Rangoon", "Asia/Riyadh", "Asia/Seoul", "Asia/Singapore", "AST", - "Atlantic/Cape_Verde", "Atlantic/South_Georgia", "Australia/Lord_Howe", - "Australia/Perth", "Brazil/Acre", "Brazil/DeNoronha", "Brazil/East", "Brazil/West", - "BST", "CNT", "CTT", "Europe/Amsterdam", "Europe/Athens", "Europe/Belgrade", - "Europe/Berlin", "Europe/Brussels", "Europe/Bucharest", "Europe/Budapest", - "Europe/Copenhagen", "Europe/Istanbul", "Europe/London", "Europe/Luxembourg", - "Europe/Madrid", "Europe/Moscow", "Europe/Paris", "Europe/Prague", "Europe/Rome", - "Europe/Stockholm", "Europe/Vienna", "Europe/Warsaw", "Europe/Zurich", - "GMT", "HST", "Indian/Mauritius", "IST", "JST", "Pacific/Apia", "Pacific/Auckland", - "Pacific/Chatham", "Pacific/Enderbury", "Pacific/Fiji", "Pacific/Gambier", - "Pacific/Kiritimati", "Pacific/Norfolk", "Pacific/Tahiti", "UTC", "VST", -} - // resolveUser returns the user identified by id or userName flags, falling back to // interactive search when both are empty and stdin is a terminal. func resolveUser(ctx context.Context, c *client.Client, id, userName string) (*client.User, error) { diff --git a/completions/iics.bash b/completions/iics.bash index 8bd9be5..cc0fcd9 100644 --- a/completions/iics.bash +++ b/completions/iics.bash @@ -7088,6 +7088,132 @@ _iics_user_update() noun_aliases=() } +_iics_user_update-groups() +{ + last_command="iics_user_update-groups" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--add=") + two_word_flags+=("--add") + local_nonpersistent_flags+=("--add") + local_nonpersistent_flags+=("--add=") + flags+=("--fields=") + two_word_flags+=("--fields") + local_nonpersistent_flags+=("--fields") + local_nonpersistent_flags+=("--fields=") + flags+=("--id=") + two_word_flags+=("--id") + local_nonpersistent_flags+=("--id") + local_nonpersistent_flags+=("--id=") + flags+=("--remove=") + two_word_flags+=("--remove") + local_nonpersistent_flags+=("--remove") + local_nonpersistent_flags+=("--remove=") + flags+=("--replace=") + two_word_flags+=("--replace") + local_nonpersistent_flags+=("--replace") + local_nonpersistent_flags+=("--replace=") + flags+=("--username=") + two_word_flags+=("--username") + local_nonpersistent_flags+=("--username") + local_nonpersistent_flags+=("--username=") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--http-timeout=") + two_word_flags+=("--http-timeout") + flags+=("--no-color") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + flags+=("--profile=") + two_word_flags+=("--profile") + two_word_flags+=("-p") + flags+=("--theme=") + two_word_flags+=("--theme") + flags+=("--verbose") + flags+=("-v") + flags+=("--wide") + flags+=("--width=") + two_word_flags+=("--width") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_iics_user_update-roles() +{ + last_command="iics_user_update-roles" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--add=") + two_word_flags+=("--add") + local_nonpersistent_flags+=("--add") + local_nonpersistent_flags+=("--add=") + flags+=("--fields=") + two_word_flags+=("--fields") + local_nonpersistent_flags+=("--fields") + local_nonpersistent_flags+=("--fields=") + flags+=("--id=") + two_word_flags+=("--id") + local_nonpersistent_flags+=("--id") + local_nonpersistent_flags+=("--id=") + flags+=("--remove=") + two_word_flags+=("--remove") + local_nonpersistent_flags+=("--remove") + local_nonpersistent_flags+=("--remove=") + flags+=("--replace=") + two_word_flags+=("--replace") + local_nonpersistent_flags+=("--replace") + local_nonpersistent_flags+=("--replace=") + flags+=("--username=") + two_word_flags+=("--username") + local_nonpersistent_flags+=("--username") + local_nonpersistent_flags+=("--username=") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--http-timeout=") + two_word_flags+=("--http-timeout") + flags+=("--no-color") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + flags+=("--profile=") + two_word_flags+=("--profile") + two_word_flags+=("-p") + flags+=("--theme=") + two_word_flags+=("--theme") + flags+=("--verbose") + flags+=("-v") + flags+=("--wide") + flags+=("--width=") + two_word_flags+=("--width") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _iics_user() { last_command="iics_user" @@ -7102,6 +7228,8 @@ _iics_user() commands+=("list") commands+=("reset-password") commands+=("update") + commands+=("update-groups") + commands+=("update-roles") flags=() two_word_flags=() diff --git a/docs/ChangeRequests/pending/CR-0043-user-role-group-assignment-commands.md b/docs/ChangeRequests/pending/CR-0043-user-role-group-assignment-commands.md new file mode 100644 index 0000000..8d45620 --- /dev/null +++ b/docs/ChangeRequests/pending/CR-0043-user-role-group-assignment-commands.md @@ -0,0 +1,54 @@ +# CR-0043: User role and user group assignment commands + +## CR Type + +- [x] New command +- [x] Enhancement to existing command + +## Problem + +`user update` cannot change user properties: the V3 API has no update +endpoint and the V2 update fails (see +`docs/issues/new/2026-09-30-user-update-v2-403.md`). Role and user group +assignments can be changed through documented V3 endpoints, but the CLI only +reached them through the failing `UpdateUser` flow. + +References: + +- [Updating role assignments](https://docs.informatica.com/cloud-common-services/administrator/current-version/rest-api-reference/platform-rest-api-version-3-resources/users/updating-role-assignments.html) +- [Updating user group assignments](https://docs.informatica.com/cloud-common-services/administrator/current-version/rest-api-reference/platform-rest-api-version-3-resources/users/updating-user-group-assignments.html) + +## Proposed Solution + +- `user update-roles` and `user update-groups`: + - `--id` / `--username` (aliases `--uid` / `--uname`), one required. + - `--add`, `--remove` (combinable), `--replace` (exclusive; empty removes + all). + - Inputs are de-duplicated and matched case-insensitively; unknown names + and names in both `--add` and `--remove` fail before any change. + - Additions before removals; no-op requests reported and skipped. + - Resulting user printed in the `--output` format. +- `user update --interactive` edits only group and role assignments and + applies them through the same endpoints. + +## Implementation + +- `internal/client/usermembership.go` - `AddUserRoles`, `RemoveUserRoles`, + `AddUserGroups`, `RemoveUserGroups`, `PlanMembership`, `MemberName`, + `RoleMemberName`. +- `cmd/user_membership.go` - both commands, `applyUserMembership`, shared + `printUser` (also used by `user get`). +- `internal/tui/userwizard.go` - update mode shows only the Membership page. +- Finding verified on `dev`: role assignment endpoints match the role + display name, not `roleName`; the commands send the display name and + accept either as input. + +## Acceptance Criteria + +- [x] Add, remove, combined add/remove and replace work on `dev` for roles + and user groups +- [x] Validation: duplicates, case, unknown names, add/remove conflict, + replace exclusivity, missing user flag +- [x] Output honors `-o table|json|yaml|csv` +- [x] Interactive update applies group and role changes only +- [x] `go build`, `go vet`, `golangci-lint`, `go test ./...` pass diff --git a/docs/documentation/user.md b/docs/documentation/user.md index b9e6cb1..d7cabdf 100644 --- a/docs/documentation/user.md +++ b/docs/documentation/user.md @@ -15,7 +15,9 @@ iics user [flags] | `list` | List users | | `get` | Get a single user | | `create` | Create a user | -| `update` | Update a user | +| `update` | Update a user (`--interactive`: groups and roles) | +| `update-roles` | Add, remove or replace a user's roles | +| `update-groups` | Add, remove or replace a user's user groups | | `delete` | Delete a user | | `change-password` | Change a user password | | `reset-password` | Reset a user password using the security answer | @@ -160,27 +162,22 @@ iics user create --interactive ## user update -> **NOTE: This command is currently not supported and will fail at runtime.** -> -> The IICS REST API does not provide a working endpoint for updating user scalar -> properties (name, email, title, timezone, etc.): -> -> - The V3 API (`PUT /public/core/v3/users/{id}`) returns HTTP 405 - only `DELETE` -> is allowed on that path. -> - The V2 API (`POST /api/v2/user/{id}`) uses XML and returns HTTP 400 "Internal -> error" in testing, with no public documentation confirming the request format. -> -> Group and role assignment via the V3 `addGroups`/`removeGroups`/`addRoles`/`removeRoles` -> endpoints may work, but scalar property update does not. This command will be -> re-enabled once a working API endpoint is confirmed. -> -> To update a user's group or role membership in the meantime, use the IICS -> Administrator UI. -> -> The `--interactive` wizard pre-fills the current values, shows user name, -> authentication and state read-only, and ends with a review of the changes -> (`field: old -> new`, `+ Group`, `- Role`). Applying the changes is subject -> to the limitation above. +Update a user's user group and role assignments interactively. For scripts, +use [user update-roles](#user-update-roles) and +[user update-groups](#user-update-groups). + +`--interactive` shows the user's name, email, authentication and state +read-only, then the user groups and roles as filterable checklists with the +current assignments pre-checked, and ends with a review (`+ Group: ...`, +`- Role: ...`). Changes are applied through the V3 `addGroups` / +`removeGroups` / `addRoles` / `removeRoles` endpoints, and the resulting user +is printed. See [Interactive prompts](interactive.md) for keys. + +> **`--from-file` is not supported and fails at runtime.** The IICS REST API +> has no working endpoint for updating user properties (name, email, title, +> time zone): the V3 `PUT /public/core/v3/users/{id}` returns HTTP 405 and the +> V2 `POST /api/v2/user/{id}` fails (HTTP 400 in earlier testing, HTTP 403 +> `REPO_10704` on 2026-09-30). ### Flags @@ -188,19 +185,103 @@ iics user create --interactive | --------------- | ----- | ------ | -------- | ---------------------------------------- | | `--id` | | string | | User ID | | `--username` | | string | | User name (exact match) | -| `--from-file` | | string | | JSON, YAML, or CSV file with user fields | -| `--interactive` | | bool | | Launch interactive update wizard | +| `--interactive` | | bool | | Edit group and role assignments | +| `--from-file` | | string | | Not supported (see note above) | + +Without `--id` or `--username`, the interactive mode lets you search for the +user. All [global flags](../../README.md#global-flags) apply. ### Examples ```bash -iics user update --username user@example.com --from-file updated-user.json +iics user update --interactive --username user@example.com +``` + +```powershell +iics user update --interactive --username user@example.com +``` + +--- + +## user update-roles + +Add, remove or replace the roles assigned to a user. Uses +`PUT /public/core/v3/users//addRoles` and `.../removeRoles`. + +### Flags + +| Flag | Type | Required | Description | +| ---------------------- | -------- | -------- | ---------------------------------------------------------- | +| `--id` (`--uid`) | string | one of | User ID | +| `--username` (`--uname`) | string | one of | User name (exact match) | +| `--add` | string[] | | Roles to assign; comma-separated or repeated | +| `--remove` | string[] | | Roles to unassign; comma-separated or repeated | +| `--replace` | string[] | | Exact list of roles; all other roles are removed | +| `--fields` | string | | Fields for CSV output (same as `user get`) | + +Rules: + +- `--id` and `--username` are mutually exclusive; one is required. +- `--add` and `--remove` can be combined. `--replace` cannot be combined with + either. `--replace=` (empty) removes all roles. +- Names match case-insensitively, by role name or display name. Duplicates are + ignored. Unknown names, or a name in both `--add` and `--remove`, fail + before any change is made. +- Roles already assigned (for `--add`) or not assigned (for `--remove`) are + skipped and reported on stderr. +- Additions are applied before removals. +- The role assignment endpoints match the role's **display name** (for + example `Data Integration Data Previewer` for the `Data Preview` role); the + command sends the display name, so either name can be typed. +- The API rejects changes when the organization maps SAML groups and roles. + +After the change the resulting user is printed in the `--output` format +(table sections, JSON, YAML, or CSV with `--fields`). Progress messages go to +stderr, so JSON output can be piped. + +### Examples + +```bash +iics user update-roles --username jdoe@example.com --add "Designer,Monitor" + +iics user update-roles --id --add Designer --remove "Data Preview" + +# Set the exact role list and print the user as JSON +iics user update-roles --username jdoe@example.com --replace Designer -o json +``` + +```powershell +iics user update-roles --username jdoe@example.com --add "Designer,Monitor" + +iics user update-roles --id --add Designer --remove "Data Preview" + +iics user update-roles --username jdoe@example.com --replace Designer -o json +``` + +--- + +## user update-groups + +Add, remove or replace the user groups assigned to a user. Uses +`PUT /public/core/v3/users//addGroups` and `.../removeGroups`. Flags and +rules are the same as [user update-roles](#user-update-roles), with user group +names instead of role names. + +### Examples + +```bash +iics user update-groups --username jdoe@example.com --add "Data Engineering" + +# Remove the user from all groups +iics user update-groups --uid --replace= ``` ```powershell -iics user update --username user@example.com --from-file updated-user.json +iics user update-groups --username jdoe@example.com --add "Data Engineering" + +iics user update-groups --uid --replace= ``` --- diff --git a/docs/issues/new/2026-09-30-user-update-v2-403.md b/docs/issues/new/2026-09-30-user-update-v2-403.md index 7356af1..97f2fc4 100644 --- a/docs/issues/new/2026-09-30-user-update-v2-403.md +++ b/docs/issues/new/2026-09-30-user-update-v2-403.md @@ -85,3 +85,18 @@ internal/client/users.go # UpdateUser, updateUserV2Request needed (no scalar field changed), so membership-only updates do not depend on the V2 call. 4. Add a test with `newTestClient` for the fixed request. + +--- + +## Mitigation (2026-09-30) + +`user update --interactive` now edits only user group and role assignments +and applies them through the V3 assignment endpoints, which work. New +commands `user update-roles` and `user update-groups` cover the same changes +for scripts. Scalar property updates (`user update --from-file`) remain +unsupported until a working endpoint is confirmed. + +Finding: the role assignment endpoints match the role **display name**, not +`roleName` (for example `Data Integration Data Previewer`, not +`Data Preview`). `UpdateUser` sends `roleName`, so its role sync would also +fail for roles whose display name differs. diff --git a/internal/client/usermembership.go b/internal/client/usermembership.go new file mode 100644 index 0000000..30d440b --- /dev/null +++ b/internal/client/usermembership.go @@ -0,0 +1,190 @@ +package client + +import ( + "context" + "fmt" + "net/http" + "net/url" + "sort" + "strings" +) + +// AddUserRoles assigns roles (by name) to a user. +func (c *Client) AddUserRoles(ctx context.Context, userID string, roles []string) error { + return c.putUserMembership(ctx, userID, "addRoles", roleNamesRequest{Roles: roles}) +} + +// RemoveUserRoles removes role assignments (by name) from a user. +func (c *Client) RemoveUserRoles(ctx context.Context, userID string, roles []string) error { + return c.putUserMembership(ctx, userID, "removeRoles", roleNamesRequest{Roles: roles}) +} + +// AddUserGroups assigns user groups (by name) to a user. +func (c *Client) AddUserGroups(ctx context.Context, userID string, groups []string) error { + return c.putUserMembership(ctx, userID, "addGroups", groupNamesRequest{Groups: groups}) +} + +// RemoveUserGroups removes user group assignments (by name) from a user. +func (c *Client) RemoveUserGroups(ctx context.Context, userID string, groups []string) error { + return c.putUserMembership(ctx, userID, "removeGroups", groupNamesRequest{Groups: groups}) +} + +func (c *Client) putUserMembership(ctx context.Context, userID, action string, body interface{}) error { + path := fmt.Sprintf("%s/users/%s/%s", BaseAPIPathV3, url.PathEscape(userID), action) + return c.doJSON(ctx, http.MethodPut, path, body, nil) +} + +// MembershipPlan is the result of PlanMembership. +type MembershipPlan struct { + // Add and Remove are the names to send to the API. + Add []string + Remove []string + // AlreadyAssigned and NotAssigned are requested changes that are no-ops. + AlreadyAssigned []string + NotAssigned []string +} + +// MembershipRequest describes requested role or user group changes. +type MembershipRequest struct { + Add []string + Remove []string + Replace []string + // ReplaceSet is true when Replace was given (an empty Replace removes all). + ReplaceSet bool +} + +// MemberName is an assignable role or user group. Name is the value the +// assignment endpoints expect; Aliases are other names users may type (for +// roles the endpoints match the display name, so the role name is an alias). +type MemberName struct { + Name string + Aliases []string +} + +// RoleMemberName returns the assignment name for a role: its display name, +// or the role name when no display name is set. +func RoleMemberName(roleName, displayName string) string { + if displayName != "" { + return displayName + } + return roleName +} + +// PlanMembership validates a membership change and computes the names to add +// and remove. kind is used in error messages ("role", "user group"). +// available lists every assignable entry in the organization and current the +// names (MemberName.Name form) assigned now. Requested names match a Name or +// alias case-insensitively and are returned as Name; duplicates are ignored. +// Unknown names, Replace combined with Add/Remove, and a name in both Add and +// Remove are errors. +func PlanMembership(kind string, available []MemberName, current []string, req MembershipRequest) (MembershipPlan, error) { + var plan MembershipPlan + if req.ReplaceSet && (len(req.Add) > 0 || len(req.Remove) > 0) { + return plan, fmt.Errorf("--replace cannot be combined with --add or --remove") + } + if !req.ReplaceSet && len(req.Add) == 0 && len(req.Remove) == 0 { + return plan, fmt.Errorf("provide --add, --remove or --replace") + } + + canonical := make(map[string]string, len(available)*2) + for _, m := range available { + for _, a := range m.Aliases { + if a != "" { + canonical[strings.ToLower(a)] = m.Name + } + } + } + // Names take precedence over aliases of other entries. + for _, m := range available { + canonical[strings.ToLower(m.Name)] = m.Name + } + resolve := func(names []string) ([]string, error) { + seen := make(map[string]bool) + var out, unknown []string + for _, n := range names { + n = strings.TrimSpace(n) + if n == "" { + continue + } + name, ok := canonical[strings.ToLower(n)] + if !ok { + unknown = append(unknown, n) + continue + } + if !seen[name] { + seen[name] = true + out = append(out, name) + } + } + if len(unknown) > 0 { + return nil, fmt.Errorf("unknown %s(s): %s", kind, strings.Join(unknown, ", ")) + } + return out, nil + } + + add, err := resolve(req.Add) + if err != nil { + return plan, err + } + remove, err := resolve(req.Remove) + if err != nil { + return plan, err + } + replace, err := resolve(req.Replace) + if err != nil { + return plan, err + } + + cur := make(map[string]bool, len(current)) + for _, n := range current { + cur[n] = true + } + + if req.ReplaceSet { + want := make(map[string]bool, len(replace)) + for _, n := range replace { + want[n] = true + if !cur[n] { + plan.Add = append(plan.Add, n) + } + } + for _, n := range current { + if !want[n] { + plan.Remove = append(plan.Remove, n) + } + } + } else { + addSet := make(map[string]bool, len(add)) + for _, n := range add { + addSet[n] = true + } + var conflict []string + for _, n := range remove { + if addSet[n] { + conflict = append(conflict, n) + } + } + if len(conflict) > 0 { + return plan, fmt.Errorf("%s(s) in both --add and --remove: %s", kind, strings.Join(conflict, ", ")) + } + for _, n := range add { + if cur[n] { + plan.AlreadyAssigned = append(plan.AlreadyAssigned, n) + } else { + plan.Add = append(plan.Add, n) + } + } + for _, n := range remove { + if cur[n] { + plan.Remove = append(plan.Remove, n) + } else { + plan.NotAssigned = append(plan.NotAssigned, n) + } + } + } + + for _, s := range [][]string{plan.Add, plan.Remove, plan.AlreadyAssigned, plan.NotAssigned} { + sort.Strings(s) + } + return plan, nil +} diff --git a/internal/client/usermembership_test.go b/internal/client/usermembership_test.go new file mode 100644 index 0000000..f29accb --- /dev/null +++ b/internal/client/usermembership_test.go @@ -0,0 +1,115 @@ +package client + +import ( + "context" + "io" + "net/http" + "reflect" + "strings" + "testing" +) + +func TestUserMembershipRequests(t *testing.T) { + tests := []struct { + name string + call func(*Client) error + wantPath string + wantBody string + }{ + {"add roles", func(c *Client) error { + return c.AddUserRoles(context.Background(), "u1", []string{"Admin", "Business Manager"}) + }, + "/public/core/v3/users/u1/addRoles", `{"roles":["Admin","Business Manager"]}`}, + {"remove roles", func(c *Client) error { return c.RemoveUserRoles(context.Background(), "u1", []string{"Designer"}) }, + "/public/core/v3/users/u1/removeRoles", `{"roles":["Designer"]}`}, + {"add groups", func(c *Client) error { return c.AddUserGroups(context.Background(), "u1", []string{"MDM Admin"}) }, + "/public/core/v3/users/u1/addGroups", `{"groups":["MDM Admin"]}`}, + {"remove groups", func(c *Client) error { return c.RemoveUserGroups(context.Background(), "u1", []string{"MDM Admin"}) }, + "/public/core/v3/users/u1/removeGroups", `{"groups":["MDM Admin"]}`}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut || r.URL.Path != tt.wantPath { + t.Errorf("got %s %s, want PUT %s", r.Method, r.URL.Path, tt.wantPath) + } + body, _ := io.ReadAll(r.Body) + if string(body) != tt.wantBody { + t.Errorf("body = %s, want %s", body, tt.wantBody) + } + w.WriteHeader(http.StatusOK) + }) + if err := tt.call(newTestClient(handler)); err != nil { + t.Fatalf("error: %v", err) + } + }) + } +} + +func TestPlanMembership(t *testing.T) { + available := []MemberName{ + {Name: "Admin"}, + {Name: "Designer"}, + {Name: "Monitor"}, + {Name: "Application Integration Business Manager", Aliases: []string{"Business Manager"}}, + } + current := []string{"Designer", "Monitor"} + + tests := []struct { + name string + req MembershipRequest + want MembershipPlan + wantErr string + }{ + { + name: "add and remove with case-insensitive names and duplicates", + req: MembershipRequest{Add: []string{"admin", "Admin", " designer "}, Remove: []string{"monitor", "business manager"}}, + want: MembershipPlan{Add: []string{"Admin"}, Remove: []string{"Monitor"}, AlreadyAssigned: []string{"Designer"}, NotAssigned: []string{"Application Integration Business Manager"}}, + }, + { + name: "alias resolves to display name", + req: MembershipRequest{Add: []string{"business manager", "Application Integration Business Manager"}}, + want: MembershipPlan{Add: []string{"Application Integration Business Manager"}}, + }, + { + name: "replace", + req: MembershipRequest{Replace: []string{"Admin", "Designer"}, ReplaceSet: true}, + want: MembershipPlan{Add: []string{"Admin"}, Remove: []string{"Monitor"}}, + }, + { + name: "replace with empty list removes all", + req: MembershipRequest{ReplaceSet: true}, + want: MembershipPlan{Remove: []string{"Designer", "Monitor"}}, + }, + {name: "unknown", req: MembershipRequest{Add: []string{"Admin", "Nope", "Zip"}}, wantErr: "unknown role(s): Nope, Zip"}, + {name: "conflict", req: MembershipRequest{Add: []string{"Admin"}, Remove: []string{"ADMIN"}}, wantErr: "in both --add and --remove: Admin"}, + {name: "replace with add", req: MembershipRequest{Add: []string{"Admin"}, ReplaceSet: true}, wantErr: "cannot be combined"}, + {name: "nothing requested", req: MembershipRequest{}, wantErr: "provide --add"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := PlanMembership("role", available, current, tt.req) + if tt.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("error = %v, want %q", err, tt.wantErr) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("plan = %+v, want %+v", got, tt.want) + } + }) + } +} + +func TestRoleMemberName(t *testing.T) { + if got := RoleMemberName("Data Preview", "Data Integration Data Previewer"); got != "Data Integration Data Previewer" { + t.Errorf("got %q", got) + } + if got := RoleMemberName("Designer", ""); got != "Designer" { + t.Errorf("got %q", got) + } +} diff --git a/internal/tui/userwizard.go b/internal/tui/userwizard.go index 1b3cb0b..35f62a6 100644 --- a/internal/tui/userwizard.go +++ b/internal/tui/userwizard.go @@ -16,15 +16,13 @@ import ( type UserWizardInput struct { // User holds the starting values and receives the result. User *client.User - // Update is true when editing an existing user. User name, - // authentication and state are then shown read-only because the update - // API does not change them. + // Update is true when editing an existing user. Only user group and role + // assignments are edited then; other fields are shown read-only because + // the API cannot update them. Update bool // Groups and Roles are all groups and roles in the organization. Groups []client.UserGroup Roles []client.Role - // Timezones lists the accepted time zone IDs. - Timezones []string // UserNameDomain suggests "first.last@" as the user name. UserNameDomain string @@ -33,7 +31,8 @@ type UserWizardInput struct { } // RunUserWizard collects user fields in a paged form (Identity, Details, -// Membership) followed by a review. It returns false if the user canceled. +// Membership) followed by a review. On update only the Membership page is +// shown. It returns false if the user canceled. func RunUserWizard(in UserWizardInput) (bool, error) { before := cloneUser(in.User) u := in.User @@ -52,12 +51,17 @@ func RunUserWizard(in UserWizardInput) (bool, error) { } for { - form := huh.NewForm( + pages := []*huh.Group{ identityGroup(in, u, &auth), ssoGroup(in, u, &auth), - detailsGroup(in, u), + detailsGroup(u), membershipGroup(in, &groupIDs, &roleIDs), - ).WithOutput(in.Out).WithAccessible(in.Accessible) + } + if in.Update { + // Only role and user group assignments can be updated. + pages = []*huh.Group{membershipGroup(in, &groupIDs, &roleIDs)} + } + form := huh.NewForm(pages...).WithOutput(in.Out).WithAccessible(in.Accessible) if err := form.Run(); err != nil { if errors.Is(err, huh.ErrUserAborted) { return false, nil @@ -92,24 +96,14 @@ func RunUserWizard(in UserWizardInput) (bool, error) { } func identityGroup(in UserWizardInput, u *client.User, auth *string) *huh.Group { - var fields []huh.Field - if in.Update { - fields = append(fields, huh.NewNote(). - Title("User: "+u.UserName). - Description(fmt.Sprintf("Authentication: %s State: %s\nUser name, authentication and state cannot be changed here.", - orDash(u.Authentication), orDash(u.State)))) - } else { - fields = append(fields, huh.NewSelect[string](). + return huh.NewGroup( + huh.NewSelect[string](). Title("Authentication"). Options(huh.NewOption("Native", "Native"), huh.NewOption("SSO", "SSO")). - Value(auth)) - } - fields = append(fields, + Value(auth), huh.NewInput().Title("First name").Value(&u.FirstName).Validate(required("first name")), huh.NewInput().Title("Last name").Value(&u.LastName).Validate(required("last name")), - ) - if !in.Update { - fields = append(fields, huh.NewInput(). + huh.NewInput(). Title("User name"). Description("Leave empty to use the suggestion"). PlaceholderFunc(func() string { @@ -121,13 +115,12 @@ func identityGroup(in UserWizardInput, u *client.User, auth *string) *huh.Group return errors.New("user name is required") } return nil - })) - } - fields = append(fields, huh.NewInput().Title("Email").Value(&u.Email).Validate(validEmail)) - return huh.NewGroup(fields...).Title("Identity") + }), + huh.NewInput().Title("Email").Value(&u.Email).Validate(validEmail), + ).Title("Identity") } -// ssoGroup asks for the SSO alias name on create; the API requires it when +// ssoGroup asks for the SSO alias name; the create API requires it when // authentication is SSO. Hidden otherwise. func ssoGroup(in UserWizardInput, u *client.User, auth *string) *huh.Group { return huh.NewGroup( @@ -141,28 +134,13 @@ func ssoGroup(in UserWizardInput, u *client.User, auth *string) *huh.Group { }) } -func detailsGroup(in UserWizardInput, u *client.User) *huh.Group { - fields := []huh.Field{ +func detailsGroup(u *client.User) *huh.Group { + return huh.NewGroup( huh.NewInput().Title("Phone (optional)").Value(&u.Phone), huh.NewInput().Title("Title (optional)").Value(&u.Title), huh.NewInput().Title("Description (optional)").Value(&u.Description), - } - // The create API does not accept a time zone; only update applies it. - if in.Update { - tzOpts := make([]huh.Option[string], 0, len(in.Timezones)+1) - tzOpts = append(tzOpts, huh.NewOption("(none)", "")) - for _, z := range in.Timezones { - tzOpts = append(tzOpts, huh.NewOption(z, z)) - } - fields = append(fields, huh.NewSelect[string](). - Title("Time zone"). - Description("/ to filter, e.g. New_York or Europe"). - Options(tzOpts...). - Height(8). - Value(&u.TimeZoneID)) - } - fields = append(fields, huh.NewConfirm().Title("Force password change on next login?").Value(&u.ForcePasswordChange)) - return huh.NewGroup(fields...).Title("Details") + huh.NewConfirm().Title("Force password change on next login?").Value(&u.ForcePasswordChange), + ).Title("Details") } func membershipGroup(in UserWizardInput, groupIDs, roleIDs *[]string) *huh.Group { @@ -184,6 +162,13 @@ func membershipGroup(in UserWizardInput, groupIDs, roleIDs *[]string) *huh.Group } var fields []huh.Field + if in.Update { + u := in.User + fields = append(fields, huh.NewNote(). + Title("User: "+u.UserName). + Description(fmt.Sprintf("%s %s Authentication: %s State: %s\nOnly user group and role assignments can be changed.", + strings.TrimSpace(u.FirstName+" "+u.LastName), orDash(u.Email), orDash(u.Authentication), orDash(u.State)))) + } if len(gOpts) > 0 { fields = append(fields, huh.NewMultiSelect[string](). Title("User groups"). @@ -346,7 +331,7 @@ func rolesByID(all []client.Role, ids []string) []client.UserRole { var out []client.UserRole for _, r := range all { if want[r.ID] { - out = append(out, client.UserRole{ID: r.ID, RoleName: r.RoleName}) + out = append(out, client.UserRole{ID: r.ID, RoleName: r.RoleName, DisplayName: r.DisplayName}) } } return out From d28c18d47eb04570f9cb973efc010f96855ed634 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Tue, 29 Sep 2026 23:26:24 -0400 Subject: [PATCH 10/52] refactor(user): replace user update with interactive user edit - user edit: terminal-only form for user group and role assignments (no --interactive switch needed); applies changes via the V3 add/remove assignment endpoints and prints the resulting user - user update kept as a hidden, deprecated alias of user edit; --interactive accepted and ignored - remove user update --from-file and the V2 scalar update (Client.UpdateUser, updateUserV2Request, doXML, TestUpdateUser): it never worked against the API (HTTP 405 on V3, 400/403 on V2) - docs: user.md "user edit" section, README, interactive.md, completions; CR-0043 updated; user update bug moved to pending --- README.md | 2 +- cmd/user.go | 197 +++++------------- completions/iics.bash | 80 ++++--- ...043-user-role-group-assignment-commands.md | 7 + docs/documentation/interactive.md | 6 +- docs/documentation/user.md | 55 ++--- .../2026-09-30-user-update-v2-403.md | 8 + internal/client/client.go | 56 ----- internal/client/users.go | 118 ----------- internal/client/users_test.go | 31 --- 10 files changed, 135 insertions(+), 425 deletions(-) rename docs/issues/{new => pending}/2026-09-30-user-update-v2-403.md (89%) diff --git a/README.md b/README.md index 12245b9..9eaae18 100644 --- a/README.md +++ b/README.md @@ -278,7 +278,7 @@ it easy to override credentials in CI pipelines without touching the config file | [state](docs/documentation/state.md) | | `fetch`, `load` | Fetch/load object state snapshots | | [tag](docs/documentation/tag.md) | | `assign`, `remove` | Assign/remove tags on objects | | [unpublish](docs/documentation/unpublish.md) | | `start`, `status`, `run` | Unpublish CAI assets from the runtime | -| [user](docs/documentation/user.md) | | `list`, `get`, `create`, `update`, `update-roles`, `update-groups`, `delete` | Manage users | +| [user](docs/documentation/user.md) | | `list`, `get`, `create`, `edit`, `update-roles`, `update-groups`, `delete` | Manage users | | [group](docs/documentation/group.md) | `usergroup`, `ug` | `list`, `get`, `create`, `update`, `delete` | Manage user groups | > **Keeping completions up to date:** After adding or changing any command or flag, regenerate diff --git a/cmd/user.go b/cmd/user.go index 1187438..22de8c7 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -29,7 +29,8 @@ func newUserCmd() *cobra.Command { cmd.AddCommand(newUserListCmd()) cmd.AddCommand(newUserGetCmd()) cmd.AddCommand(newUserCreateCmd()) - cmd.AddCommand(newUserUpdateCmd()) + cmd.AddCommand(newUserEditCmd("")) + cmd.AddCommand(newUserEditCmd("update")) cmd.AddCommand(newUserMembershipCmd(userRoleMembership)) cmd.AddCommand(newUserMembershipCmd(userGroupMembership)) cmd.AddCommand(newUserDeleteCmd()) @@ -718,182 +719,78 @@ func newUserCreateCmd() *cobra.Command { } // --------------------------------------------------------------------------- -// user update +// user edit // --------------------------------------------------------------------------- -func newUserUpdateCmd() *cobra.Command { +// newUserEditCmd builds "user edit". With deprecatedUse set it builds the +// hidden, deprecated alias of that name (formerly "user update"). +func newUserEditCmd(deprecatedUse string) *cobra.Command { var ( id string userName string - fromFile string interactive bool ) cmd := &cobra.Command{ - Use: "update", - Short: "Update one or more users", + Use: "edit", + Short: "Edit a user's group and role assignments (interactive)", + Long: `Edit a user's user group and role assignments in an interactive form. + +The IICS API cannot update user properties (name, email, title, time zone), +so only group and role assignments can be changed. The form shows the user's +current assignments pre-checked and ends with a review before any change is +applied. Requires a terminal; for scripts use "user update-roles" and +"user update-groups".`, + Example: ` iics user edit --username jdoe@example.com + iics user edit # search for the user first`, RunE: func(cmd *cobra.Command, args []string) error { + if !isInteractiveTTY() { + return fmt.Errorf("user edit requires a terminal; use 'iics user update-roles' or 'iics user update-groups' in scripts") + } c, err := getClient(cmd) if err != nil { return err } ctx := context.Background() - // Interactive update - if interactive { - target, rErr := resolveUser(ctx, c, id, userName) - if rErr != nil { - return rErr - } - if target == nil { - return nil // user canceled - } - updated, wErr := runUserWizard(ctx, c, target) - if wErr != nil { - return wErr - } - if updated == nil { - _, _ = fmt.Fprintln(os.Stderr, "Canceled.") - return nil - } - if aErr := applyUserMembership(ctx, c, cmd.ErrOrStderr(), target, updated); aErr != nil { - return aErr - } - result, gErr := c.GetUser(ctx, target.ID) - if gErr != nil { - return gErr - } - return printUser(result, defaultCSVFields) - } - - if fromFile == "" { - return fmt.Errorf("--from-file or --interactive is required") - } - - var data []byte - if fromFile == "-" { - data, err = io.ReadAll(os.Stdin) - } else { - data, err = os.ReadFile(fromFile) - } - if err != nil { - return fmt.Errorf("reading input: %w", err) - } - - format := detectInputFormat(fromFile, data) - users, err := parseUsersFromBytes(data, format) + target, err := resolveUser(ctx, c, id, userName) if err != nil { return err } - if err := resolveUserGroupsAndRoles(ctx, c, users); err != nil { - return err + if target == nil { + return nil // canceled } - - // Single user: resolve target by --id/--username or by userName in file - if len(users) == 1 { - targetID := id - if targetID == "" && users[0].ID != "" { - targetID = users[0].ID - } - if targetID == "" && users[0].UserName != "" { - target, lErr := c.GetUserByName(ctx, users[0].UserName) - if lErr != nil { - return lErr - } - targetID = target.ID - } - if targetID == "" { - return fmt.Errorf("cannot determine user ID; provide --id or include id/userName in the file") - } - result, uErr := c.UpdateUser(ctx, targetID, &users[0]) - if uErr != nil { - return uErr - } - cfg, _ := loadConfig() - style := resolveTableStyle(cfg) - f, fErr := output.ParseFormat(outputFmt) - if fErr != nil { - return fErr - } - if f == output.FormatTable { - return printUserSections(os.Stdout, result, style) - } - fmtr := output.New(f, os.Stdout, style) - return fmtr.Format(result, nil) + updated, err := runUserWizard(ctx, c, target) + if err != nil { + return err } - - // Bulk update - results := make([]userCreateResult, 0, len(users)) - for i := range users { - start := time.Now() - targetID := users[i].ID - if targetID == "" && users[i].UserName != "" { - target, lErr := c.GetUserByName(ctx, users[i].UserName) - if lErr != nil { - elapsed := time.Since(start).Round(time.Millisecond).String() - results = append(results, userCreateResult{ - UserName: users[i].UserName, - Status: "Error: " + lErr.Error(), - Elapsed: elapsed, - }) - _, _ = fmt.Fprintf(os.Stderr, "Error looking up %s: %v\n", users[i].UserName, lErr) - continue - } - targetID = target.ID - } - if targetID == "" { - results = append(results, userCreateResult{ - UserName: users[i].UserName, - Status: "Error: no id or userName to identify user", - }) - continue - } - - result, uErr := c.UpdateUser(ctx, targetID, &users[i]) - elapsed := time.Since(start).Round(time.Millisecond).String() - - res := userCreateResult{ - UserName: users[i].UserName, - FirstName: users[i].FirstName, - LastName: users[i].LastName, - Email: users[i].Email, - Elapsed: elapsed, - } - if uErr != nil { - res.Status = "Error: " + uErr.Error() - _, _ = fmt.Fprintf(os.Stderr, "Error updating %s: %v\n", users[i].UserName, uErr) - } else { - res.ID = result.ID - res.State = result.State - res.Status = "Success" - if verbose { - slog.Info("user updated", - "userName", result.UserName, - "id", result.ID, - "elapsed", elapsed) - } - } - results = append(results, res) + if updated == nil { + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Canceled.") + return nil } - - f, fErr := getFormatter() - if fErr != nil { - return fErr + if err = applyUserMembership(ctx, c, cmd.ErrOrStderr(), target, updated); err != nil { + return err } - cols := []output.Column{ - {Header: "ID", Field: "id", Width: 24}, - {Header: "USERNAME", Field: "userName", Width: 30}, - {Header: "STATE", Field: "state", Width: 10}, - {Header: "STATUS", Field: "status", Width: 20}, + result, err := c.GetUser(ctx, target.ID) + if err != nil { + return err } - return f.Format(results, cols) + return printUser(result, defaultCSVFields) }, } + if deprecatedUse != "" { + cmd.Use = deprecatedUse + cmd.Deprecated = "use 'iics user edit' (interactive) or 'iics user update-roles' / 'update-groups' (scripts)" + cmd.Hidden = true + } - cmd.Flags().StringVar(&id, "id", "", "user ID of the user to update") - cmd.Flags().StringVar(&userName, "username", "", "user name of the user to update") - cmd.Flags().StringVar(&fromFile, "from-file", "", "JSON, YAML or CSV file with updated user(s); use - for stdin") - cmd.Flags().BoolVar(&interactive, "interactive", false, "interactively edit user fields") + cmd.Flags().StringVar(&id, "id", "", "user ID") + cmd.Flags().StringVar(&userName, "username", "", "user name (exact match)") + cmd.MarkFlagsMutuallyExclusive("id", "username") + // Kept so older invocations ("--interactive") still parse; edit is always + // interactive. + cmd.Flags().BoolVar(&interactive, "interactive", false, "no longer needed; always interactive") + _ = cmd.Flags().MarkHidden("interactive") return cmd } diff --git a/completions/iics.bash b/completions/iics.bash index cc0fcd9..a4c09f4 100644 --- a/completions/iics.bash +++ b/completions/iics.bash @@ -6882,9 +6882,9 @@ _iics_user_delete() noun_aliases=() } -_iics_user_get() +_iics_user_edit() { - last_command="iics_user_get" + last_command="iics_user_edit" command_aliases=() @@ -6896,10 +6896,6 @@ _iics_user_get() flags_with_completion=() flags_completion=() - flags+=("--fields=") - two_word_flags+=("--fields") - local_nonpersistent_flags+=("--fields") - local_nonpersistent_flags+=("--fields=") flags+=("--id=") two_word_flags+=("--id") local_nonpersistent_flags+=("--id") @@ -6933,9 +6929,9 @@ _iics_user_get() noun_aliases=() } -_iics_user_list() +_iics_user_get() { - last_command="iics_user_list" + last_command="iics_user_get" command_aliases=() @@ -6947,14 +6943,18 @@ _iics_user_list() flags_with_completion=() flags_completion=() - flags+=("--limit=") - two_word_flags+=("--limit") - local_nonpersistent_flags+=("--limit") - local_nonpersistent_flags+=("--limit=") - flags+=("--skip=") - two_word_flags+=("--skip") - local_nonpersistent_flags+=("--skip") - local_nonpersistent_flags+=("--skip=") + flags+=("--fields=") + two_word_flags+=("--fields") + local_nonpersistent_flags+=("--fields") + local_nonpersistent_flags+=("--fields=") + flags+=("--id=") + two_word_flags+=("--id") + local_nonpersistent_flags+=("--id") + local_nonpersistent_flags+=("--id=") + flags+=("--username=") + two_word_flags+=("--username") + local_nonpersistent_flags+=("--username") + local_nonpersistent_flags+=("--username=") flags+=("--config=") two_word_flags+=("--config") flags+=("--debug") @@ -6980,9 +6980,9 @@ _iics_user_list() noun_aliases=() } -_iics_user_reset-password() +_iics_user_list() { - last_command="iics_user_reset-password" + last_command="iics_user_list" command_aliases=() @@ -6994,22 +6994,14 @@ _iics_user_reset-password() flags_with_completion=() flags_completion=() - flags+=("--id=") - two_word_flags+=("--id") - local_nonpersistent_flags+=("--id") - local_nonpersistent_flags+=("--id=") - flags+=("--new-password=") - two_word_flags+=("--new-password") - local_nonpersistent_flags+=("--new-password") - local_nonpersistent_flags+=("--new-password=") - flags+=("--security-answer=") - two_word_flags+=("--security-answer") - local_nonpersistent_flags+=("--security-answer") - local_nonpersistent_flags+=("--security-answer=") - flags+=("--username=") - two_word_flags+=("--username") - local_nonpersistent_flags+=("--username") - local_nonpersistent_flags+=("--username=") + flags+=("--limit=") + two_word_flags+=("--limit") + local_nonpersistent_flags+=("--limit") + local_nonpersistent_flags+=("--limit=") + flags+=("--skip=") + two_word_flags+=("--skip") + local_nonpersistent_flags+=("--skip") + local_nonpersistent_flags+=("--skip=") flags+=("--config=") two_word_flags+=("--config") flags+=("--debug") @@ -7035,9 +7027,9 @@ _iics_user_reset-password() noun_aliases=() } -_iics_user_update() +_iics_user_reset-password() { - last_command="iics_user_update" + last_command="iics_user_reset-password" command_aliases=() @@ -7049,16 +7041,18 @@ _iics_user_update() flags_with_completion=() flags_completion=() - flags+=("--from-file=") - two_word_flags+=("--from-file") - local_nonpersistent_flags+=("--from-file") - local_nonpersistent_flags+=("--from-file=") flags+=("--id=") two_word_flags+=("--id") local_nonpersistent_flags+=("--id") local_nonpersistent_flags+=("--id=") - flags+=("--interactive") - local_nonpersistent_flags+=("--interactive") + flags+=("--new-password=") + two_word_flags+=("--new-password") + local_nonpersistent_flags+=("--new-password") + local_nonpersistent_flags+=("--new-password=") + flags+=("--security-answer=") + two_word_flags+=("--security-answer") + local_nonpersistent_flags+=("--security-answer") + local_nonpersistent_flags+=("--security-answer=") flags+=("--username=") two_word_flags+=("--username") local_nonpersistent_flags+=("--username") @@ -7224,10 +7218,10 @@ _iics_user() commands+=("change-password") commands+=("create") commands+=("delete") + commands+=("edit") commands+=("get") commands+=("list") commands+=("reset-password") - commands+=("update") commands+=("update-groups") commands+=("update-roles") diff --git a/docs/ChangeRequests/pending/CR-0043-user-role-group-assignment-commands.md b/docs/ChangeRequests/pending/CR-0043-user-role-group-assignment-commands.md index 8d45620..86bae79 100644 --- a/docs/ChangeRequests/pending/CR-0043-user-role-group-assignment-commands.md +++ b/docs/ChangeRequests/pending/CR-0043-user-role-group-assignment-commands.md @@ -30,6 +30,11 @@ References: - Resulting user printed in the `--output` format. - `user update --interactive` edits only group and role assignments and applies them through the same endpoints. +- Follow-up: `user update` is renamed to `user edit` (terminal only, no + `--interactive` switch needed). `user update` stays as a hidden, + deprecated alias. `--from-file` is removed together with the V2 scalar + update (`Client.UpdateUser`, `updateUserV2Request`, `doXML`), which never + worked against the API. ## Implementation @@ -51,4 +56,6 @@ References: replace exclusivity, missing user flag - [x] Output honors `-o table|json|yaml|csv` - [x] Interactive update applies group and role changes only +- [x] `user edit` replaces `user update`; `update` kept as deprecated alias; + `--from-file` update and V2 client code removed - [x] `go build`, `go vet`, `golangci-lint`, `go test ./...` pass diff --git a/docs/documentation/interactive.md b/docs/documentation/interactive.md index dfb26a9..72b92c9 100644 --- a/docs/documentation/interactive.md +++ b/docs/documentation/interactive.md @@ -1,6 +1,7 @@ # Interactive prompts Commands that run interactively (wizards such as `user create --interactive`, +`user edit`, `group create -i`, `environment create`, and pickers shown when `--id` or `--name` is omitted) render terminal forms when stdin and stderr are both a terminal. @@ -46,9 +47,10 @@ format: | Single list | Option number; `0`, `q` or empty cancels | | Yes / No | `y`, `yes`, `n`, `no`; empty uses the default | -Wizards with checklists (`user create`/`update --interactive`, `group +Wizards with checklists (`user create --interactive`, `user edit`, `group create`/`update -i`, `environment create -i`, `role edit`) require a terminal -and report an error instead of prompting; use `--from-file` in scripts. +and report an error instead of prompting; use `--from-file` or the +non-interactive commands (for example `user update-roles`) in scripts. ## Confirmations diff --git a/docs/documentation/user.md b/docs/documentation/user.md index d7cabdf..bda31f8 100644 --- a/docs/documentation/user.md +++ b/docs/documentation/user.md @@ -15,7 +15,7 @@ iics user [flags] | `list` | List users | | `get` | Get a single user | | `create` | Create a user | -| `update` | Update a user (`--interactive`: groups and roles) | +| `edit` | Edit group and role assignments (interactive) | | `update-roles` | Add, remove or replace a user's roles | | `update-groups` | Add, remove or replace a user's user groups | | `delete` | Delete a user | @@ -160,47 +160,54 @@ iics user create --interactive --- -## user update +## user edit -Update a user's user group and role assignments interactively. For scripts, -use [user update-roles](#user-update-roles) and +Edit a user's user group and role assignments in an interactive form. Requires +a terminal; for scripts use [user update-roles](#user-update-roles) and [user update-groups](#user-update-groups). -`--interactive` shows the user's name, email, authentication and state -read-only, then the user groups and roles as filterable checklists with the -current assignments pre-checked, and ends with a review (`+ Group: ...`, +The form shows the user's name, email, authentication and state read-only, +then the user groups and roles as filterable checklists with the current +assignments pre-checked, and ends with a review (`+ Group: ...`, `- Role: ...`). Changes are applied through the V3 `addGroups` / -`removeGroups` / `addRoles` / `removeRoles` endpoints, and the resulting user -is printed. See [Interactive prompts](interactive.md) for keys. +`removeGroups` / `addRoles` / `removeRoles` endpoints (additions first), and +the resulting user is printed. See [Interactive prompts](interactive.md) for +keys. -> **`--from-file` is not supported and fails at runtime.** The IICS REST API -> has no working endpoint for updating user properties (name, email, title, -> time zone): the V3 `PUT /public/core/v3/users/{id}` returns HTTP 405 and the -> V2 `POST /api/v2/user/{id}` fails (HTTP 400 in earlier testing, HTTP 403 -> `REPO_10704` on 2026-09-30). +User properties (name, email, phone, title, description, time zone) cannot be +changed: the IICS REST API has no working update endpoint for them. The V3 +`PUT /public/core/v3/users/{id}` returns HTTP 405, and the V2 +`POST /api/v2/user/{id}` never succeeded in testing (HTTP 400, later HTTP 403 +`REPO_10704`). + +> `user update` is a deprecated, hidden alias of `user edit`. Its former +> `--from-file` option was removed because it never worked against the API. +> `--interactive` is accepted and ignored. ### Flags -| Flag | Short | Type | Required | Description | -| --------------- | ----- | ------ | -------- | ---------------------------------------- | -| `--id` | | string | | User ID | -| `--username` | | string | | User name (exact match) | -| `--interactive` | | bool | | Edit group and role assignments | -| `--from-file` | | string | | Not supported (see note above) | +| Flag | Type | Required | Description | +| ------------ | ------ | -------- | ----------------------- | +| `--id` | string | | User ID | +| `--username` | string | | User name (exact match) | -Without `--id` or `--username`, the interactive mode lets you search for the -user. +Without `--id` or `--username`, you are asked to search for the user. All [global flags](../../README.md#global-flags) apply. ### Examples ```bash -iics user update --interactive --username user@example.com +iics user edit --username user@example.com + +# Search for the user first +iics user edit ``` ```powershell -iics user update --interactive --username user@example.com +iics user edit --username user@example.com + +iics user edit ``` --- diff --git a/docs/issues/new/2026-09-30-user-update-v2-403.md b/docs/issues/pending/2026-09-30-user-update-v2-403.md similarity index 89% rename from docs/issues/new/2026-09-30-user-update-v2-403.md rename to docs/issues/pending/2026-09-30-user-update-v2-403.md index 97f2fc4..762174e 100644 --- a/docs/issues/new/2026-09-30-user-update-v2-403.md +++ b/docs/issues/pending/2026-09-30-user-update-v2-403.md @@ -100,3 +100,11 @@ Finding: the role assignment endpoints match the role **display name**, not `roleName` (for example `Data Integration Data Previewer`, not `Data Preview`). `UpdateUser` sends `roleName`, so its role sync would also fail for roles whose display name differs. + +## Resolution (2026-09-30) + +No evidence exists that the V2 update ever succeeded (the command was +documented as failing since `79d16a7`). `user update --from-file` and the V2 +client code (`Client.UpdateUser`, `updateUserV2Request`, `doXML`) were +removed. `user update` is now a deprecated alias of the interactive +`user edit`, which only changes group and role assignments. See CR-0043. diff --git a/internal/client/client.go b/internal/client/client.go index babacab..6032a88 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "encoding/json" - "encoding/xml" "fmt" "io" "log/slog" @@ -322,61 +321,6 @@ func (c *Client) doJSON(ctx context.Context, method, path string, reqBody, respB return nil } -// doXML sends an XML-encoded request and optionally decodes an XML response. -// Used for legacy V2 endpoints that require application/xml (e.g. user update). -func (c *Client) doXML(ctx context.Context, method, path string, reqBody, respBody interface{}) error { - if err := c.ensureSession(ctx); err != nil { - return err - } - - var body io.Reader - var reqData []byte - if reqBody != nil { - var err error - reqData, err = xml.Marshal(reqBody) - if err != nil { - return fmt.Errorf("marshaling request: %w", err) - } - reqData = append([]byte(xml.Header), reqData...) - body = bytes.NewReader(reqData) - } - - url := c.apiURL(path) - req, err := http.NewRequestWithContext(ctx, method, url, body) - if err != nil { - return fmt.Errorf("creating request: %w", err) - } - req.Header.Set("Content-Type", "application/xml") - req.Header.Set("Accept", "application/xml") - - resp, err := c.do(ctx, req) - if err != nil { - return err - } - defer func() { _ = resp.Body.Close() }() - - respData, err := io.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("reading response: %w", err) - } - - if c.debug { - debugPrintHTTP(req, reqData, resp, respData) - } - - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return newAPIError(resp, respData) - } - - if respBody != nil && len(respData) > 0 { - if err := xml.Unmarshal(respData, respBody); err != nil { - return fmt.Errorf("parsing response: %w", err) - } - } - - return nil -} - // doJSONWithQuery is like doJSON but with query parameters. func (c *Client) doJSONWithQuery(ctx context.Context, method, path string, query map[string]string, reqBody, respBody interface{}) error { if err := c.ensureSession(ctx); err != nil { diff --git a/internal/client/users.go b/internal/client/users.go index 308561b..d4bc1f7 100644 --- a/internal/client/users.go +++ b/internal/client/users.go @@ -2,7 +2,6 @@ package client import ( "context" - "encoding/xml" "fmt" "net/http" "strconv" @@ -194,23 +193,6 @@ func (c *Client) CreateUser(ctx context.Context, user *User) (*User, error) { return &resp, nil } -// updateUserV2Request is the XML body for updating a user via the V2 API. -// V2 uses different field names than V3 (timezone vs timeZoneId, emails vs email) -// and requires application/xml content type. -type updateUserV2Request struct { - XMLName xml.Name `xml:"user" json:"-"` - OrgID string `xml:"orgId"` - Name string `xml:"name"` - FirstName string `xml:"firstName"` - LastName string `xml:"lastName"` - Title string `xml:"title,omitempty"` - Phone string `xml:"phone,omitempty"` - Emails string `xml:"emails,omitempty"` - Description string `xml:"description,omitempty"` - Timezone string `xml:"timezone,omitempty"` - ForceChangePassword bool `xml:"forceChangePassword,omitempty"` -} - type groupNamesRequest struct { Groups []string `json:"groups"` } @@ -219,106 +201,6 @@ type roleNamesRequest struct { Roles []string `json:"roles"` } -// UpdateUser updates a user's scalar properties via the V2 API, and syncs -// group and role membership via the V3 addGroups/removeGroups/addRoles/removeRoles -// endpoints. It fetches the current user state first to compute diffs. -func (c *Client) UpdateUser(ctx context.Context, id string, user *User) (*User, error) { - // Fetch current state to know orgId, userName, and existing groups/roles. - current, err := c.GetUser(ctx, id) - if err != nil { - return nil, fmt.Errorf("fetching current user: %w", err) - } - - // Update scalar properties via V2. - v2req := &updateUserV2Request{ - OrgID: current.OrgID, - Name: current.UserName, - FirstName: user.FirstName, - LastName: user.LastName, - Title: user.Title, - Phone: user.Phone, - Emails: user.Email, - Description: user.Description, - Timezone: user.TimeZoneID, - ForceChangePassword: user.ForcePasswordChange, - } - if err := c.doXML(ctx, http.MethodPost, fmt.Sprintf("%s/user/%s", BaseAPIPathV2, id), v2req, nil); err != nil { - return nil, fmt.Errorf("updating user properties: %w", err) - } - - // Sync groups via V3 addGroups/removeGroups. - currentGroups := make(map[string]bool, len(current.Groups)) - for _, g := range current.Groups { - currentGroups[g.UserGroupName] = true - } - desiredGroups := make(map[string]bool, len(user.Groups)) - for _, g := range user.Groups { - desiredGroups[g.UserGroupName] = true - } - var groupsToAdd, groupsToRemove []string - for n := range desiredGroups { - if !currentGroups[n] { - groupsToAdd = append(groupsToAdd, n) - } - } - for n := range currentGroups { - if !desiredGroups[n] { - groupsToRemove = append(groupsToRemove, n) - } - } - if len(groupsToAdd) > 0 { - if err := c.doJSON(ctx, http.MethodPut, - fmt.Sprintf("%s/users/%s/addGroups", BaseAPIPathV3, id), - groupNamesRequest{Groups: groupsToAdd}, nil); err != nil { - return nil, fmt.Errorf("adding groups: %w", err) - } - } - if len(groupsToRemove) > 0 { - if err := c.doJSON(ctx, http.MethodPut, - fmt.Sprintf("%s/users/%s/removeGroups", BaseAPIPathV3, id), - groupNamesRequest{Groups: groupsToRemove}, nil); err != nil { - return nil, fmt.Errorf("removing groups: %w", err) - } - } - - // Sync roles via V3 addRoles/removeRoles. - currentRoles := make(map[string]bool, len(current.Roles)) - for _, r := range current.Roles { - currentRoles[r.RoleName] = true - } - desiredRoles := make(map[string]bool, len(user.Roles)) - for _, r := range user.Roles { - desiredRoles[r.RoleName] = true - } - var rolesToAdd, rolesToRemove []string - for n := range desiredRoles { - if !currentRoles[n] { - rolesToAdd = append(rolesToAdd, n) - } - } - for n := range currentRoles { - if !desiredRoles[n] { - rolesToRemove = append(rolesToRemove, n) - } - } - if len(rolesToAdd) > 0 { - if err := c.doJSON(ctx, http.MethodPut, - fmt.Sprintf("%s/users/%s/addRoles", BaseAPIPathV3, id), - roleNamesRequest{Roles: rolesToAdd}, nil); err != nil { - return nil, fmt.Errorf("adding roles: %w", err) - } - } - if len(rolesToRemove) > 0 { - if err := c.doJSON(ctx, http.MethodPut, - fmt.Sprintf("%s/users/%s/removeRoles", BaseAPIPathV3, id), - roleNamesRequest{Roles: rolesToRemove}, nil); err != nil { - return nil, fmt.Errorf("removing roles: %w", err) - } - } - - return c.GetUser(ctx, id) -} - // DeleteUser deletes a user by ID. func (c *Client) DeleteUser(ctx context.Context, id string) error { return c.doJSON(ctx, http.MethodDelete, fmt.Sprintf("%s/users/%s", BaseAPIPathV3, id), nil, nil) diff --git a/internal/client/users_test.go b/internal/client/users_test.go index eb1fc3f..1b92f64 100644 --- a/internal/client/users_test.go +++ b/internal/client/users_test.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "net/http" - "strings" "testing" ) @@ -156,36 +155,6 @@ func TestCreateUser(t *testing.T) { } } -func TestUpdateUser(t *testing.T) { - // UpdateUser makes multiple requests: GET (current state), POST v2 (scalar update), - // optional PUT (group/role diff), GET (final state). The handler tracks call order. - callCount := 0 - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - callCount++ - switch { - case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/v3/users"): - // GET list for GetUser (called twice: fetch current + fetch final) - json.NewEncoder(w).Encode([]User{{ID: "u123", OrgID: "org1", UserName: "test@example.com", Email: "old@example.com"}}) - case r.Method == http.MethodPost && strings.Contains(r.URL.Path, "/v2/user"): - // V2 scalar update - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]string{"id": "u123"}) - default: - w.WriteHeader(http.StatusNoContent) - } - }) - - c := newTestClient(handler) - user, err := c.UpdateUser(context.Background(), "u123", &User{Email: "updated@example.com"}) - if err != nil { - t.Fatalf("UpdateUser() error: %v", err) - } - if user.ID != "u123" { - t.Errorf("expected ID u123, got %s", user.ID) - } -} - func TestDeleteUser(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete { From 089f26c7d1403fe1761cb895d46b141258bcbcf2 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Tue, 29 Sep 2026 23:34:15 -0400 Subject: [PATCH 11/52] fix(permission): align permission commands with the object permissions API - client: ObjectACL model (principal {type, name}, permissions {read, update, delete, execute, changePermission}); list, get, create (POST per principal), update (PUT per ACL ID), delete one / all, checkAccess; DiffACLs and ParseACLPermissions with tests. Removes the previous guessed structs and the non-existent PUT /objects//permissions - commands: get, add, update, delete (--acl-id | --user | --group | --all), set (--from-file, --prune, --dry-run), check (--asset-type); objects by --object-id or --path/--type (lookup); changes print the resulting ACLs - verified against dev on a throwaway project - docs: permission.md rewritten, README, completions; bug moved to pending --- README.md | 2 +- cmd/permission.go | 490 ++++++++++++++++-- completions/iics.bash | 237 +++++++++ docs/documentation/permission.md | 245 ++++++--- ...09-30-permission-client-wrong-api-shape.md | 24 + internal/client/permissions.go | 251 ++++++++- internal/client/permissions_test.go | 158 ++++++ 7 files changed, 1279 insertions(+), 128 deletions(-) rename docs/issues/{new => pending}/2026-09-30-permission-client-wrong-api-shape.md (76%) create mode 100644 internal/client/permissions_test.go diff --git a/README.md b/README.md index 9eaae18..e06b7f2 100644 --- a/README.md +++ b/README.md @@ -264,7 +264,7 @@ it easy to override credentials in CI pipelines without touching the config file | [metering](docs/documentation/metering.md) | | `get`, `download` | Query usage and metering data | | [objects](docs/documentation/objects.md) | | `list`, `dependencies` | List/search assets, find dependencies | | [package](docs/documentation/package.md) | | `expand`, `create`, `dependencies` | Extract, assemble, or inspect dependencies of IICS export packages | -| [permission](docs/documentation/permission.md) | `perm` | `get`, `set`, `delete` | Manage object-level permissions | +| [permission](docs/documentation/permission.md) | `perm` | `get`, `add`, `update`, `delete`, `set`, `check` | Manage object-level permissions | | [privilege](docs/documentation/privilege.md) | | `list` | List available privileges | | [profile](docs/documentation/profile.md) | | `add`, `edit`, `list`, `delete`, `set-default`, `set-password`, `show` | Manage connection profiles | | [project](docs/documentation/project.md) | | `create`, `update`, `delete` | Manage projects | diff --git a/cmd/permission.go b/cmd/permission.go index 451b363..af84551 100644 --- a/cmd/permission.go +++ b/cmd/permission.go @@ -4,7 +4,9 @@ import ( "context" "encoding/json" "fmt" + "io" "os" + "strings" "github.com/jbrazda/iics-cli/internal/client" "github.com/jbrazda/iics-cli/internal/output" @@ -15,116 +17,512 @@ func newPermissionCmd() *cobra.Command { cmd := &cobra.Command{ Use: "permission", Aliases: []string{"perm"}, - Short: "Manage object permissions", + Short: "Manage object permissions (ACLs)", + Long: `Manage object permissions. Each access control list (ACL) entry grants one +user or user group read, update, delete, execute and change-permission rights +on one object (project, folder or asset). + +Select the object with --object-id, or with --path and --type.`, } cmd.AddCommand(newPermissionGetCmd()) - cmd.AddCommand(newPermissionSetCmd()) + cmd.AddCommand(newPermissionAddCmd()) + cmd.AddCommand(newPermissionUpdateCmd()) cmd.AddCommand(newPermissionDeleteCmd()) + cmd.AddCommand(newPermissionSetCmd()) + cmd.AddCommand(newPermissionCheckCmd()) return cmd } +// objectRef holds the flags that select an object. +type objectRef struct { + id, path, typ string +} + +func (o *objectRef) register(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.id, "object-id", "", "object ID") + cmd.Flags().StringVar(&o.path, "path", "", `object path, e.g. "Default/Sales/m_load_orders" (with --type)`) + cmd.Flags().StringVar(&o.typ, "type", "", "object type for --path, e.g. Project, Folder, DTEMPLATE, MTT, TASKFLOW") + cmd.MarkFlagsMutuallyExclusive("object-id", "path") + cmd.MarkFlagsRequiredTogether("path", "type") +} + +// resolve returns the object ID and a display label. +func (o *objectRef) resolve(ctx context.Context, c *client.Client) (string, string, error) { + if o.id != "" { + return o.id, o.id, nil + } + if o.path == "" { + return "", "", fmt.Errorf("--object-id or --path with --type is required") + } + resp, err := c.Lookup(ctx, []client.LookupObject{{Path: o.path, Type: o.typ}}) + if err != nil { + return "", "", err + } + if len(resp.Objects) == 0 || resp.Objects[0].ID == "" { + return "", "", fmt.Errorf("object %q of type %s not found", o.path, o.typ) + } + r := resp.Objects[0] + return r.ID, fmt.Sprintf("%s (%s)", r.Path, r.Type), nil +} + +// principalFlags hold --user / --group. +type principalFlags struct { + user, group string +} + +func (p *principalFlags) register(cmd *cobra.Command) { + cmd.Flags().StringVar(&p.user, "user", "", "user name") + cmd.Flags().StringVar(&p.group, "group", "", "user group name") + cmd.MarkFlagsMutuallyExclusive("user", "group") +} + +func (p *principalFlags) principal() (client.ACLPrincipal, bool) { + switch { + case p.user != "": + return client.ACLPrincipal{Type: client.PrincipalUser, Name: p.user}, true + case p.group != "": + return client.ACLPrincipal{Type: client.PrincipalGroup, Name: p.group}, true + } + return client.ACLPrincipal{}, false +} + +var aclColumns = []output.Column{ + {Header: "TYPE", Field: "principal.type", Width: 6}, + {Header: "NAME", Field: "principal.name", Width: 36}, + {Header: "READ", Field: "permissions.read", Width: 5}, + {Header: "UPDATE", Field: "permissions.update", Width: 6}, + {Header: "DELETE", Field: "permissions.delete", Width: 6}, + {Header: "EXECUTE", Field: "permissions.execute", Width: 7}, + {Header: "CHANGE PERM", Field: "permissions.changePermission", Width: 11}, + {Header: "ACL ID", Field: "id", Width: 24}, +} + +// printACLs lists an object's ACLs in the --output format. +func printACLs(ctx context.Context, c *client.Client, objectID string) error { + acls, err := c.ListObjectACLs(ctx, objectID) + if err != nil { + return err + } + f, err := getFormatter() + if err != nil { + return err + } + if acls == nil { + acls = []client.ObjectACL{} + } + return f.Format(acls, aclColumns) +} + +func findACL(acls []client.ObjectACL, p client.ACLPrincipal) (client.ObjectACL, bool) { + key := client.PrincipalKey(p) + for _, a := range acls { + if client.PrincipalKey(a.Principal) == key { + return a, true + } + } + return client.ObjectACL{}, false +} + func newPermissionGetCmd() *cobra.Command { - var objectID string + var obj objectRef cmd := &cobra.Command{ Use: "get", - Short: "Get object permissions", + Short: "List the ACLs of an object", + Example: ` iics permission get --object-id + iics permission get --path "Default/Sales" --type Folder -o json`, + RunE: func(cmd *cobra.Command, args []string) error { + c, err := getClient(cmd) + if err != nil { + return err + } + ctx := context.Background() + id, _, err := obj.resolve(ctx, c) + if err != nil { + return err + } + return printACLs(ctx, c, id) + }, + } + obj.register(cmd) + return cmd +} + +func newPermissionAddCmd() *cobra.Command { + var ( + obj objectRef + who principalFlags + grant []string + ) + cmd := &cobra.Command{ + Use: "add", + Short: "Grant permissions to a user or user group", + Long: `Create an ACL for a user or user group on an object. Fails if the principal +already has an ACL on the object; use "permission update" to change it.`, + Example: ` iics permission add --object-id --group "Data Engineering" --grant read,execute + iics permission add --path "Default/Sales" --type Folder --user jdoe@example.com --grant all`, RunE: func(cmd *cobra.Command, args []string) error { - if objectID == "" { - return fmt.Errorf("--object-id is required") + p, ok := who.principal() + if !ok { + return fmt.Errorf("--user or --group is required") + } + perms, err := client.ParseACLPermissions(grant) + if err != nil { + return err } c, err := getClient(cmd) if err != nil { return err } - perms, err := c.GetObjectPermissions(context.Background(), objectID) + ctx := context.Background() + id, label, err := obj.resolve(ctx, c) if err != nil { return err } - f, err := getFormatter() + acls, err := c.ListObjectACLs(ctx, id) if err != nil { return err } - columns := []output.Column{ - {Header: "PRINCIPAL ID", Field: "principalId", Width: 24}, - {Header: "PRINCIPAL TYPE", Field: "principalType", Width: 15}, - {Header: "PRINCIPAL NAME", Field: "principalName", Width: 20}, - {Header: "PERMISSION", Field: "permission", Width: 15}, + if existing, found := findACL(acls, p); found { + return fmt.Errorf("%s %q already has an ACL (%s) on %s; use 'permission update'", strings.ToLower(p.Type), existing.Principal.Name, existing.ID, label) + } + if _, err = c.CreateObjectACL(ctx, id, client.ObjectACL{Principal: p, Permissions: perms}); err != nil { + return err } - return f.Format(perms.Permissions, columns) + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Granted %s to %s %s on %s\n", perms, strings.ToLower(p.Type), p.Name, label) + return printACLs(ctx, c, id) }, } - cmd.Flags().StringVar(&objectID, "object-id", "", "object ID (required)") + obj.register(cmd) + who.register(cmd) + cmd.Flags().StringSliceVar(&grant, "grant", nil, "permissions: read, update, delete, execute, changePermission, all (comma-separated)") + _ = cmd.MarkFlagRequired("grant") return cmd } -func newPermissionSetCmd() *cobra.Command { +func newPermissionUpdateCmd() *cobra.Command { var ( - objectID string - fromFile string + obj objectRef + who principalFlags + aclID string + grant []string ) cmd := &cobra.Command{ - Use: "set", - Short: "Set object permissions", + Use: "update", + Short: "Set the permissions of an existing ACL", + Long: `Replace the permissions of an existing ACL, selected by --acl-id or by +--user / --group. --grant is the complete new permission set.`, + Example: ` iics permission update --object-id --group "Data Engineering" --grant read + iics permission update --object-id --acl-id --grant read,update,execute`, RunE: func(cmd *cobra.Command, args []string) error { - if objectID == "" { - return fmt.Errorf("--object-id is required") + p, byPrincipal := who.principal() + if !byPrincipal && aclID == "" { + return fmt.Errorf("--acl-id, --user or --group is required") } - if fromFile == "" { - return fmt.Errorf("--from-file is required") + perms, err := client.ParseACLPermissions(grant) + if err != nil { + return err } - data, err := os.ReadFile(fromFile) + c, err := getClient(cmd) if err != nil { - return fmt.Errorf("reading file: %w", err) + return err } - var perms client.ObjectPermission - err = json.Unmarshal(data, &perms) + ctx := context.Background() + id, label, err := obj.resolve(ctx, c) if err != nil { - return fmt.Errorf("parsing JSON: %w", err) + return err + } + acls, err := c.ListObjectACLs(ctx, id) + if err != nil { + return err + } + var target client.ObjectACL + found := false + if byPrincipal { + target, found = findACL(acls, p) + } else { + for _, a := range acls { + if a.ID == aclID { + target, found = a, true + } + } + } + if !found { + return fmt.Errorf("no matching ACL on %s; use 'permission add' to create one", label) + } + target.Permissions = perms + if err = c.UpdateObjectACL(ctx, id, target.ID, target); err != nil { + return err + } + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Set %s %s on %s to %s\n", strings.ToLower(target.Principal.Type), target.Principal.Name, label, perms) + return printACLs(ctx, c, id) + }, + } + obj.register(cmd) + who.register(cmd) + cmd.Flags().StringVar(&aclID, "acl-id", "", "ACL ID") + cmd.Flags().StringSliceVar(&grant, "grant", nil, "complete permission set: read, update, delete, execute, changePermission, all, none") + _ = cmd.MarkFlagRequired("grant") + cmd.MarkFlagsMutuallyExclusive("acl-id", "user", "group") + return cmd +} + +func newPermissionDeleteCmd() *cobra.Command { + var ( + obj objectRef + who principalFlags + aclID string + all bool + yes bool + ) + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete an ACL, or all ACLs of an object", + Example: ` iics permission delete --object-id --group "Data Engineering" + iics permission delete --object-id --acl-id + iics permission delete --object-id --all --yes`, + RunE: func(cmd *cobra.Command, args []string) error { + p, byPrincipal := who.principal() + if !byPrincipal && aclID == "" && !all { + return fmt.Errorf("--acl-id, --user, --group or --all is required") } c, err := getClient(cmd) if err != nil { return err } - _, err = c.SetObjectPermissions(context.Background(), objectID, &perms) + ctx := context.Background() + id, label, err := obj.resolve(ctx, c) if err != nil { return err } - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Permissions set on object %s\n", objectID) - return nil + if all { + if !yes && !confirmAction(cmd, fmt.Sprintf("Delete ALL permissions on %s?", label)) { + return nil + } + if err = c.DeleteAllObjectACLs(ctx, id); err != nil { + return err + } + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Deleted all ACLs on %s\n", label) + return printACLs(ctx, c, id) + } + target := client.ObjectACL{ID: aclID} + if byPrincipal { + acls, lerr := c.ListObjectACLs(ctx, id) + if lerr != nil { + return lerr + } + var found bool + if target, found = findACL(acls, p); !found { + return fmt.Errorf("%s %q has no ACL on %s", strings.ToLower(p.Type), p.Name, label) + } + } + desc := target.ID + if target.Principal.Name != "" { + desc = fmt.Sprintf("%s %s", strings.ToLower(target.Principal.Type), target.Principal.Name) + } + if !yes && !confirmAction(cmd, fmt.Sprintf("Delete the ACL of %s on %s?", desc, label)) { + return nil + } + if err = c.DeleteObjectACL(ctx, id, target.ID); err != nil { + return err + } + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Deleted ACL of %s on %s\n", desc, label) + return printACLs(ctx, c, id) }, } - cmd.Flags().StringVar(&objectID, "object-id", "", "object ID (required)") - cmd.Flags().StringVar(&fromFile, "from-file", "", "JSON file with permissions (required)") + obj.register(cmd) + who.register(cmd) + cmd.Flags().StringVar(&aclID, "acl-id", "", "ACL ID") + cmd.Flags().BoolVar(&all, "all", false, "delete every ACL of the object") + cmd.Flags().BoolVarP(&yes, "yes", "y", false, "skip confirmation prompt") + cmd.MarkFlagsMutuallyExclusive("acl-id", "user", "group", "all") return cmd } -func newPermissionDeleteCmd() *cobra.Command { +func newPermissionSetCmd() *cobra.Command { var ( - objectID string + obj objectRef + fromFile string + prune bool + dryRun bool yes bool ) cmd := &cobra.Command{ - Use: "delete", - Short: "Delete object permissions", + Use: "set", + Short: "Apply ACLs from a JSON file", + Long: `Make an object's ACLs match a JSON file: principals in the file that have no +ACL are added, and principals whose permissions differ are updated. With +--prune, ACLs of principals not in the file are deleted (asks for +confirmation unless --yes). --dry-run shows the changes without applying them. + +The file is a JSON array in the same shape as "permission get -o json"; +"id" values are ignored: + + [ + {"principal": {"type": "GROUP", "name": "Data Engineering"}, + "permissions": {"read": true, "update": true, "delete": false, + "execute": true, "changePermission": false}} + ]`, + Example: ` iics permission set --object-id --from-file acls.json --dry-run + iics permission set --path "Default/Sales" --type Folder --from-file acls.json --prune`, RunE: func(cmd *cobra.Command, args []string) error { - if objectID == "" { - return fmt.Errorf("--object-id is required") + data, err := readInputFile(fromFile) + if err != nil { + return err + } + var desired []client.ObjectACL + if err = json.Unmarshal(data, &desired); err != nil { + return fmt.Errorf("parsing JSON (expected an array of ACLs): %w", err) + } + for i, d := range desired { + t := strings.ToUpper(d.Principal.Type) + if (t != client.PrincipalUser && t != client.PrincipalGroup) || strings.TrimSpace(d.Principal.Name) == "" { + return fmt.Errorf("entry %d: principal needs type USER or GROUP and a name", i+1) + } + desired[i].Principal.Type = t + } + c, err := getClient(cmd) + if err != nil { + return err + } + ctx := context.Background() + id, label, err := obj.resolve(ctx, c) + if err != nil { + return err + } + current, err := c.ListObjectACLs(ctx, id) + if err != nil { + return err + } + plan := client.DiffACLs(current, desired, prune) + w := cmd.ErrOrStderr() + writeACLPlan(w, plan) + if plan.Empty() { + _, _ = fmt.Fprintf(w, "No changes for %s.\n", label) + return printACLs(ctx, c, id) + } + if dryRun { + _, _ = fmt.Fprintln(w, "Dry run: no changes applied.") + return nil } - if !yes && !confirmAction(cmd, fmt.Sprintf("Are you sure you want to delete permissions for object %s?", objectID)) { + if len(plan.Delete) > 0 && !yes && !confirmAction(cmd, fmt.Sprintf("Delete %d ACL(s) on %s?", len(plan.Delete), label)) { return nil } + if err = applyACLPlan(ctx, c, id, plan); err != nil { + return err + } + return printACLs(ctx, c, id) + }, + } + obj.register(cmd) + cmd.Flags().StringVar(&fromFile, "from-file", "", "JSON file with the desired ACLs; - for stdin (required)") + cmd.Flags().BoolVar(&prune, "prune", false, "delete ACLs of principals not in the file") + cmd.Flags().BoolVar(&dryRun, "dry-run", false, "show the changes without applying them") + cmd.Flags().BoolVarP(&yes, "yes", "y", false, "skip confirmation prompt for --prune deletions") + _ = cmd.MarkFlagRequired("from-file") + return cmd +} + +func newPermissionCheckCmd() *cobra.Command { + var ( + obj objectRef + assetType string + ) + cmd := &cobra.Command{ + Use: "check", + Short: "Show your own access to an object", + Long: `Show the current user's access to an object. With --asset-type on a project +or folder, CREATE reports whether that asset type can be created there.`, + Example: ` iics permission check --object-id + iics permission check --path "Default" --type Project --asset-type DTEMPLATE`, + RunE: func(cmd *cobra.Command, args []string) error { c, err := getClient(cmd) if err != nil { return err } - if err := c.DeleteObjectPermissions(context.Background(), objectID); err != nil { + ctx := context.Background() + id, _, err := obj.resolve(ctx, c) + if err != nil { + return err + } + access, err := c.CheckObjectAccess(ctx, id, assetType) + if err != nil { + return err + } + f, err := getFormatter() + if err != nil { return err } - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Permissions deleted for object %s\n", objectID) - return nil + return f.Format(access, []output.Column{ + {Header: "CREATE", Field: "permissions.create"}, + {Header: "READ", Field: "permissions.read"}, + {Header: "UPDATE", Field: "permissions.update"}, + {Header: "DELETE", Field: "permissions.delete"}, + {Header: "EXECUTE", Field: "permissions.execute"}, + {Header: "CHANGE PERM", Field: "permissions.changePermission"}, + }) }, } - cmd.Flags().StringVar(&objectID, "object-id", "", "object ID (required)") - cmd.Flags().BoolVarP(&yes, "yes", "y", false, "skip confirmation prompt") + obj.register(cmd) + cmd.Flags().StringVar(&assetType, "asset-type", "", "asset type to check create access for, e.g. DTEMPLATE") return cmd } + +// writeACLPlan describes an ACLPlan, one line per change. +func writeACLPlan(w io.Writer, plan client.ACLPlan) { + for _, a := range plan.Create { + _, _ = fmt.Fprintf(w, "+ %s %s: %s\n", strings.ToLower(a.Principal.Type), a.Principal.Name, a.Permissions) + } + for _, a := range plan.Update { + _, _ = fmt.Fprintf(w, "~ %s %s: %s\n", strings.ToLower(a.Principal.Type), a.Principal.Name, a.Permissions) + } + for _, a := range plan.Delete { + _, _ = fmt.Fprintf(w, "- %s %s\n", strings.ToLower(a.Principal.Type), a.Principal.Name) + } +} + +// applyACLPlan creates, then updates, then deletes ACLs. It stops at the +// first error and reports how many changes were applied. +func applyACLPlan(ctx context.Context, c *client.Client, objectID string, plan client.ACLPlan) error { + done := 0 + total := len(plan.Create) + len(plan.Update) + len(plan.Delete) + fail := func(action string, a client.ObjectACL, err error) error { + return fmt.Errorf("%s ACL of %s %s failed after %d of %d change(s): %w", + action, strings.ToLower(a.Principal.Type), a.Principal.Name, done, total, err) + } + for _, a := range plan.Create { + if _, err := c.CreateObjectACL(ctx, objectID, a); err != nil { + return fail("creating", a, err) + } + done++ + } + for _, a := range plan.Update { + if err := c.UpdateObjectACL(ctx, objectID, a.ID, a); err != nil { + return fail("updating", a, err) + } + done++ + } + for _, a := range plan.Delete { + if err := c.DeleteObjectACL(ctx, objectID, a.ID); err != nil { + return fail("deleting", a, err) + } + done++ + } + return nil +} + +// readInputFile reads a file, or stdin for "-". +func readInputFile(path string) ([]byte, error) { + var ( + data []byte + err error + ) + if path == "-" { + data, err = io.ReadAll(os.Stdin) + } else { + data, err = os.ReadFile(path) + } + if err != nil { + return nil, fmt.Errorf("reading input: %w", err) + } + return data, nil +} diff --git a/completions/iics.bash b/completions/iics.bash index a4c09f4..470ea7b 100644 --- a/completions/iics.bash +++ b/completions/iics.bash @@ -4028,6 +4028,125 @@ _iics_package() noun_aliases=() } +_iics_permission_add() +{ + last_command="iics_permission_add" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--grant=") + two_word_flags+=("--grant") + local_nonpersistent_flags+=("--grant") + local_nonpersistent_flags+=("--grant=") + flags+=("--group=") + two_word_flags+=("--group") + local_nonpersistent_flags+=("--group") + local_nonpersistent_flags+=("--group=") + flags+=("--object-id=") + two_word_flags+=("--object-id") + local_nonpersistent_flags+=("--object-id") + local_nonpersistent_flags+=("--object-id=") + flags+=("--path=") + two_word_flags+=("--path") + local_nonpersistent_flags+=("--path") + local_nonpersistent_flags+=("--path=") + flags+=("--type=") + two_word_flags+=("--type") + local_nonpersistent_flags+=("--type") + local_nonpersistent_flags+=("--type=") + flags+=("--user=") + two_word_flags+=("--user") + local_nonpersistent_flags+=("--user") + local_nonpersistent_flags+=("--user=") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--http-timeout=") + two_word_flags+=("--http-timeout") + flags+=("--no-color") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + flags+=("--profile=") + two_word_flags+=("--profile") + two_word_flags+=("-p") + flags+=("--theme=") + two_word_flags+=("--theme") + flags+=("--verbose") + flags+=("-v") + flags+=("--wide") + flags+=("--width=") + two_word_flags+=("--width") + + must_have_one_flag=() + must_have_one_flag+=("--grant=") + must_have_one_noun=() + noun_aliases=() +} + +_iics_permission_check() +{ + last_command="iics_permission_check" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--asset-type=") + two_word_flags+=("--asset-type") + local_nonpersistent_flags+=("--asset-type") + local_nonpersistent_flags+=("--asset-type=") + flags+=("--object-id=") + two_word_flags+=("--object-id") + local_nonpersistent_flags+=("--object-id") + local_nonpersistent_flags+=("--object-id=") + flags+=("--path=") + two_word_flags+=("--path") + local_nonpersistent_flags+=("--path") + local_nonpersistent_flags+=("--path=") + flags+=("--type=") + two_word_flags+=("--type") + local_nonpersistent_flags+=("--type") + local_nonpersistent_flags+=("--type=") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--http-timeout=") + two_word_flags+=("--http-timeout") + flags+=("--no-color") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + flags+=("--profile=") + two_word_flags+=("--profile") + two_word_flags+=("-p") + flags+=("--theme=") + two_word_flags+=("--theme") + flags+=("--verbose") + flags+=("-v") + flags+=("--wide") + flags+=("--width=") + two_word_flags+=("--width") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _iics_permission_delete() { last_command="iics_permission_delete" @@ -4042,10 +4161,32 @@ _iics_permission_delete() flags_with_completion=() flags_completion=() + flags+=("--acl-id=") + two_word_flags+=("--acl-id") + local_nonpersistent_flags+=("--acl-id") + local_nonpersistent_flags+=("--acl-id=") + flags+=("--all") + local_nonpersistent_flags+=("--all") + flags+=("--group=") + two_word_flags+=("--group") + local_nonpersistent_flags+=("--group") + local_nonpersistent_flags+=("--group=") flags+=("--object-id=") two_word_flags+=("--object-id") local_nonpersistent_flags+=("--object-id") local_nonpersistent_flags+=("--object-id=") + flags+=("--path=") + two_word_flags+=("--path") + local_nonpersistent_flags+=("--path") + local_nonpersistent_flags+=("--path=") + flags+=("--type=") + two_word_flags+=("--type") + local_nonpersistent_flags+=("--type") + local_nonpersistent_flags+=("--type=") + flags+=("--user=") + two_word_flags+=("--user") + local_nonpersistent_flags+=("--user") + local_nonpersistent_flags+=("--user=") flags+=("--yes") flags+=("-y") local_nonpersistent_flags+=("--yes") @@ -4093,6 +4234,14 @@ _iics_permission_get() two_word_flags+=("--object-id") local_nonpersistent_flags+=("--object-id") local_nonpersistent_flags+=("--object-id=") + flags+=("--path=") + two_word_flags+=("--path") + local_nonpersistent_flags+=("--path") + local_nonpersistent_flags+=("--path=") + flags+=("--type=") + two_word_flags+=("--type") + local_nonpersistent_flags+=("--type") + local_nonpersistent_flags+=("--type=") flags+=("--config=") two_word_flags+=("--config") flags+=("--debug") @@ -4132,6 +4281,8 @@ _iics_permission_set() flags_with_completion=() flags_completion=() + flags+=("--dry-run") + local_nonpersistent_flags+=("--dry-run") flags+=("--from-file=") two_word_flags+=("--from-file") local_nonpersistent_flags+=("--from-file") @@ -4140,6 +4291,88 @@ _iics_permission_set() two_word_flags+=("--object-id") local_nonpersistent_flags+=("--object-id") local_nonpersistent_flags+=("--object-id=") + flags+=("--path=") + two_word_flags+=("--path") + local_nonpersistent_flags+=("--path") + local_nonpersistent_flags+=("--path=") + flags+=("--prune") + local_nonpersistent_flags+=("--prune") + flags+=("--type=") + two_word_flags+=("--type") + local_nonpersistent_flags+=("--type") + local_nonpersistent_flags+=("--type=") + flags+=("--yes") + flags+=("-y") + local_nonpersistent_flags+=("--yes") + local_nonpersistent_flags+=("-y") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--http-timeout=") + two_word_flags+=("--http-timeout") + flags+=("--no-color") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + flags+=("--profile=") + two_word_flags+=("--profile") + two_word_flags+=("-p") + flags+=("--theme=") + two_word_flags+=("--theme") + flags+=("--verbose") + flags+=("-v") + flags+=("--wide") + flags+=("--width=") + two_word_flags+=("--width") + + must_have_one_flag=() + must_have_one_flag+=("--from-file=") + must_have_one_noun=() + noun_aliases=() +} + +_iics_permission_update() +{ + last_command="iics_permission_update" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--acl-id=") + two_word_flags+=("--acl-id") + local_nonpersistent_flags+=("--acl-id") + local_nonpersistent_flags+=("--acl-id=") + flags+=("--grant=") + two_word_flags+=("--grant") + local_nonpersistent_flags+=("--grant") + local_nonpersistent_flags+=("--grant=") + flags+=("--group=") + two_word_flags+=("--group") + local_nonpersistent_flags+=("--group") + local_nonpersistent_flags+=("--group=") + flags+=("--object-id=") + two_word_flags+=("--object-id") + local_nonpersistent_flags+=("--object-id") + local_nonpersistent_flags+=("--object-id=") + flags+=("--path=") + two_word_flags+=("--path") + local_nonpersistent_flags+=("--path") + local_nonpersistent_flags+=("--path=") + flags+=("--type=") + two_word_flags+=("--type") + local_nonpersistent_flags+=("--type") + local_nonpersistent_flags+=("--type=") + flags+=("--user=") + two_word_flags+=("--user") + local_nonpersistent_flags+=("--user") + local_nonpersistent_flags+=("--user=") flags+=("--config=") two_word_flags+=("--config") flags+=("--debug") @@ -4161,6 +4394,7 @@ _iics_permission_set() two_word_flags+=("--width") must_have_one_flag=() + must_have_one_flag+=("--grant=") must_have_one_noun=() noun_aliases=() } @@ -4172,9 +4406,12 @@ _iics_permission() command_aliases=() commands=() + commands+=("add") + commands+=("check") commands+=("delete") commands+=("get") commands+=("set") + commands+=("update") flags=() two_word_flags=() diff --git a/docs/documentation/permission.md b/docs/documentation/permission.md index 4893517..49d80f4 100644 --- a/docs/documentation/permission.md +++ b/docs/documentation/permission.md @@ -1,8 +1,15 @@ # permission -Manage object-level permissions in IICS. Alias: `perm`. +Manage object permissions in IICS. Alias: `perm`. -Permissions control which users and groups can read, write, or execute a specific object. +Each access control list (ACL) entry grants one user or user group a set of +permissions on one object (project, folder or asset): `read`, `update`, +`delete`, `execute` and `changePermission`. When an object has no ACLs, access +follows the users' roles; once ACLs exist, they restrict access to the listed +principals (administrators keep the right to change permissions). + +Uses the [Object permissions](https://docs.informatica.com/cloud-common-services/administrator/current-version/rest-api-reference/platform-rest-api-version-3-resources/object-permissions.html) +API (`/public/core/v3/objects//permissions`). ## Synopsis @@ -13,85 +20,201 @@ iics perm [flags] ## Subcommands -| Subcommand | Description | -| ---------- | ------------------------------- | -| `get` | Get permissions for an object | -| `set` | Set permissions on an object | -| `delete` | Delete permissions from an object | +| Subcommand | Description | +| ---------- | ---------------------------------------------------- | +| `get` | List the ACLs of an object | +| `add` | Grant permissions to a user or user group | +| `update` | Set the permissions of an existing ACL | +| `delete` | Delete one ACL, or all ACLs of an object | +| `set` | Make an object's ACLs match a JSON file | +| `check` | Show your own access to an object | ---- +## Selecting the object -## permission get +Every subcommand takes the object as either: -Retrieve the current permission assignments for an object. +| Flag | Description | +| ------------------ | ------------------------------------------------------------------ | +| `--object-id` | Object ID | +| `--path`, `--type` | Object path and type, resolved with [lookup](lookup.md), e.g. `--path "Default/Sales" --type Folder` | -### Flags +Common types: `Project`, `Folder`, `DTEMPLATE` (mapping), `MTT` (mapping task), +`TASKFLOW`, `PROCESS`. -| Flag | Type | Required | Description | -| ------------- | ------ | -------- | ------------- | -| `--object-id` | string | yes | Object ID | +## Permission names -All [global flags](../../README.md#global-flags) apply. +`--grant` takes a comma-separated list (or the flag repeated): `read`, +`update`, `delete`, `execute`, `changePermission` (also `perm`), `all`, +`none`. Names are case-insensitive. + +--- + +## permission get ### Output columns -| Column | Description | -| --------------- | ------------------------------------------ | -| `principalId` | User or group ID | -| `principalType` | Type: `USER` or `GROUP` | -| `principalName` | User or group name | -| `permission` | Permission level: `READ`, `WRITE`, `EXECUTE` | +| Column | Description | +| ------------- | ----------------------------------------- | +| `TYPE` | `USER` or `GROUP` | +| `NAME` | User name or user group name | +| `READ` ... `CHANGE PERM` | Granted permissions (`true`/`false`) | +| `ACL ID` | ACL ID, used by `update` / `delete --acl-id` | + +`-o json` returns the API shape (see [permission set](#permission-set)). ### Examples ```bash iics permission get --object-id -iics perm get --object-id --output json - -# Resolve object ID from path first -ID=$(iics lookup --path "My Project/ETL/LoadOrders" --type MTT --output json | jq -r '.id') -iics permission get --object-id "$ID" +iics perm get --path "Default/Sales" --type Folder -o json ``` ```powershell iics permission get --object-id -iics perm get --object-id --output json +iics perm get --path "Default/Sales" --type Folder -o json +``` + +--- + +## permission add + +Create an ACL for a user or user group. Fails if the principal already has an +ACL on the object; use `permission update` instead. + +### Flags -# Resolve object ID from path first -$obj = iics lookup --path "My Project/ETL/LoadOrders" --type MTT --output json | ConvertFrom-Json -iics permission get --object-id $obj.id +| Flag | Type | Required | Description | +| --------- | -------- | -------- | ----------------------------------- | +| `--user` | string | one of | User name | +| `--group` | string | one of | User group name | +| `--grant` | string[] | yes | Permissions to grant | + +### Examples + +```bash +iics permission add --object-id --group "Data Engineering" --grant read,execute + +iics perm add --path "Default/Sales" --type Folder --user jdoe@example.com --grant all +``` + +```powershell +iics permission add --object-id --group "Data Engineering" --grant read,execute + +iics perm add --path "Default/Sales" --type Folder --user jdoe@example.com --grant all +``` + +--- + +## permission update + +Replace the permissions of an existing ACL. `--grant` is the complete new set. + +### Flags + +| Flag | Type | Required | Description | +| ---------- | -------- | -------- | ------------------------------- | +| `--acl-id` | string | one of | ACL ID | +| `--user` | string | one of | User name | +| `--group` | string | one of | User group name | +| `--grant` | string[] | yes | Complete permission set | + +### Examples + +```bash +iics permission update --object-id --group "Data Engineering" --grant read + +iics permission update --object-id --acl-id --grant read,update,execute +``` + +```powershell +iics permission update --object-id --group "Data Engineering" --grant read + +iics permission update --object-id --acl-id --grant read,update,execute +``` + +--- + +## permission delete + +Delete one ACL (by `--acl-id`, `--user` or `--group`) or every ACL of the +object (`--all`). Prompts for confirmation unless `--yes` is given. + +### Flags + +| Flag | Short | Type | Required | Description | +| ------------ | ----- | ------ | -------- | --------------------------- | +| `--acl-id` | | string | one of | ACL ID | +| `--user` | | string | one of | User name | +| `--group` | | string | one of | User group name | +| `--all` | | bool | one of | Delete every ACL | +| `--yes` | `-y` | bool | | Skip confirmation prompt | + +### Examples + +```bash +iics permission delete --object-id --group "Data Engineering" + +iics perm delete --object-id --all --yes +``` + +```powershell +iics permission delete --object-id --group "Data Engineering" + +iics perm delete --object-id --all --yes ``` --- ## permission set -Set or replace permissions on an object from a JSON file. +Make an object's ACLs match a JSON file: + +- principals in the file without an ACL are added (`+`), +- principals whose permissions differ are updated (`~`), +- with `--prune`, ACLs of principals not in the file are deleted (`-`); this + asks for confirmation unless `--yes` is given. + +Principals match by type and case-insensitive name. The planned changes are +printed to stderr; `--dry-run` stops there. Changes are applied in the order +add, update, delete; on error the command stops and reports how many changes +were applied. ### Flags -| Flag | Type | Required | Description | -| ------------- | ------ | -------- | ---------------------------------- | -| `--object-id` | string | yes | Object ID | -| `--from-file` | string | yes | JSON file with permission definitions | +| Flag | Short | Type | Required | Description | +| ------------- | ----- | ------ | -------- | --------------------------------------------- | +| `--from-file` | | string | yes | JSON file with the desired ACLs; `-` for stdin | +| `--prune` | | bool | | Delete ACLs of principals not in the file | +| `--dry-run` | | bool | | Show changes without applying them | +| `--yes` | `-y` | bool | | Skip the confirmation for `--prune` | -All [global flags](../../README.md#global-flags) apply. +### JSON file -### JSON definition example +The same shape as `permission get -o json`; `id` values are ignored. ```json [ { - "principalId": "", - "principalType": "USER", - "permission": "READ" + "principal": { "type": "GROUP", "name": "Data Engineering" }, + "permissions": { + "read": true, + "update": true, + "delete": false, + "execute": true, + "changePermission": false + } }, { - "principalId": "", - "principalType": "GROUP", - "permission": "WRITE" + "principal": { "type": "USER", "name": "jdoe@example.com" }, + "permissions": { + "read": true, + "update": false, + "delete": false, + "execute": false, + "changePermission": false + } } ] ``` @@ -99,44 +222,48 @@ All [global flags](../../README.md#global-flags) apply. ### Examples ```bash -iics permission set --object-id --from-file permissions.json +# Preview, then apply +iics permission set --object-id --from-file acls.json --dry-run +iics permission set --object-id --from-file acls.json -iics perm set --object-id --from-file permissions.json +# Copy the ACLs of one folder to another +iics perm get --path "Default/Sales" --type Folder -o json \ + | iics perm set --path "Default/Marketing" --type Folder --from-file - --prune --yes ``` ```powershell -iics permission set --object-id --from-file permissions.json +iics permission set --object-id --from-file acls.json --dry-run +iics permission set --object-id --from-file acls.json -iics perm set --object-id --from-file permissions.json +iics perm get --path "Default/Sales" --type Folder -o json | + iics perm set --path "Default/Marketing" --type Folder --from-file - --prune --yes ``` --- -## permission delete +## permission check -Remove all permissions from an object. Prompts for confirmation unless `--yes` is given. +Show the current user's access to an object. With `--asset-type` on a project +or folder, `CREATE` reports whether that asset type can be created there. ### Flags -| Flag | Short | Type | Required | Description | -| ------------- | ----- | ------ | -------- | ------------------------ | -| `--object-id` | | string | yes | Object ID | -| `--yes` | `-y` | bool | | Skip confirmation prompt | - -All [global flags](../../README.md#global-flags) apply. +| Flag | Type | Required | Description | +| -------------- | ------ | -------- | -------------------------------------------- | +| `--asset-type` | string | | Asset type to check create access for | ### Examples ```bash -iics permission delete --object-id +iics permission check --object-id -iics perm delete --object-id --yes +iics perm check --path "Default" --type Project --asset-type DTEMPLATE ``` ```powershell -iics permission delete --object-id +iics permission check --object-id -iics perm delete --object-id --yes +iics perm check --path "Default" --type Project --asset-type DTEMPLATE ``` ## See also diff --git a/docs/issues/new/2026-09-30-permission-client-wrong-api-shape.md b/docs/issues/pending/2026-09-30-permission-client-wrong-api-shape.md similarity index 76% rename from docs/issues/new/2026-09-30-permission-client-wrong-api-shape.md rename to docs/issues/pending/2026-09-30-permission-client-wrong-api-shape.md index a65781a..f2d4085 100644 --- a/docs/issues/new/2026-09-30-permission-client-wrong-api-shape.md +++ b/docs/issues/pending/2026-09-30-permission-client-wrong-api-shape.md @@ -105,3 +105,27 @@ docs/documentation/permission.md The interactive editor for permissions is tracked separately in CR-0042 and builds on this fix. + +--- + +## Fix (filled in after resolution) + +**Root cause:** the client modeled a response shape and a `PUT +/objects//permissions` endpoint that the API does not have. + +**Files changed:** + +```text +internal/client/permissions.go - ObjectACL/ACLPrincipal/ACLPermissions, ObjectAccess; List/Get/Create/ + Update/Delete/DeleteAll ObjectACL(s), CheckObjectAccess; DiffACLs, + ParseACLPermissions, PrincipalKey +internal/client/permissions_test.go - tests for every method, DiffACLs and permission parsing +cmd/permission.go - get, add, update, delete (--acl-id/--user/--group/--all), set + (--from-file, --prune, --dry-run), check; --object-id or --path/--type +docs/documentation/permission.md - rewritten +``` + +**Verified on `dev`** (throwaway project): get on an empty object, add, +duplicate add rejected, update, check, set dry-run/apply/no-change, +prune (declined and confirmed), delete by group, delete --all, and a +`get -o json | set --from-file -` round-trip. diff --git a/internal/client/permissions.go b/internal/client/permissions.go index a75de4e..f962a72 100644 --- a/internal/client/permissions.go +++ b/internal/client/permissions.go @@ -1,45 +1,252 @@ package client import ( + "bytes" "context" + "encoding/json" "fmt" "net/http" + "net/url" + "sort" + "strings" ) -// ObjectPermission represents permissions on an IICS object. -type ObjectPermission struct { - ObjectID string `json:"objectId"` - ObjectType string `json:"objectType,omitempty"` - Permissions []Permission `json:"permissions,omitempty"` +// Principal types accepted by the object permissions API. +const ( + PrincipalUser = "USER" + PrincipalGroup = "GROUP" +) + +// ACLPrincipal identifies the user or user group an ACL applies to. +type ACLPrincipal struct { + Type string `json:"type"` + Name string `json:"name"` +} + +// ACLPermissions are the permissions an ACL grants. +type ACLPermissions struct { + Read bool `json:"read"` + Update bool `json:"update"` + Delete bool `json:"delete"` + Execute bool `json:"execute"` + ChangePermission bool `json:"changePermission"` +} + +// ObjectACL is an access control list entry for one principal on an object. +type ObjectACL struct { + ID string `json:"id,omitempty"` + Principal ACLPrincipal `json:"principal"` + Permissions ACLPermissions `json:"permissions"` } -// Permission represents a single permission entry. -type Permission struct { - PrincipalID string `json:"principalId"` - PrincipalType string `json:"principalType"` - PrincipalName string `json:"principalName,omitempty"` - Permission string `json:"permission"` +// ObjectAccess is the current user's access to an object (checkAccess). +type ObjectAccess struct { + Permissions struct { + Create bool `json:"create"` + Read bool `json:"read"` + Update bool `json:"update"` + Delete bool `json:"delete"` + Execute bool `json:"execute"` + ChangePermission bool `json:"changePermission"` + } `json:"permissions"` } -// GetObjectPermissions retrieves permissions for an object. -func (c *Client) GetObjectPermissions(ctx context.Context, objectID string) (*ObjectPermission, error) { - var resp ObjectPermission - if err := c.doJSON(ctx, http.MethodGet, fmt.Sprintf("%s/objects/%s/permissions", BaseAPIPathV3, objectID), nil, &resp); err != nil { +// aclRequest is the POST/PUT body; the ACL ID is only part of the URI. +type aclRequest struct { + Principal ACLPrincipal `json:"principal"` + Permissions ACLPermissions `json:"permissions"` +} + +func permissionsPath(objectID string, parts ...string) string { + p := fmt.Sprintf("%s/objects/%s/permissions", BaseAPIPathV3, url.PathEscape(objectID)) + for _, part := range parts { + p += "/" + url.PathEscape(part) + } + return p +} + +// ListObjectACLs returns all ACLs defined on an object. +func (c *Client) ListObjectACLs(ctx context.Context, objectID string) ([]ObjectACL, error) { + var resp []ObjectACL + if err := c.doJSON(ctx, http.MethodGet, permissionsPath(objectID), nil, &resp); err != nil { + return nil, err + } + return resp, nil +} + +// GetObjectACL returns one ACL of an object. +func (c *Client) GetObjectACL(ctx context.Context, objectID, aclID string) (*ObjectACL, error) { + var resp ObjectACL + if err := c.doJSON(ctx, http.MethodGet, permissionsPath(objectID, aclID), nil, &resp); err != nil { return nil, err } return &resp, nil } -// SetObjectPermissions sets permissions on an object. -func (c *Client) SetObjectPermissions(ctx context.Context, objectID string, perms *ObjectPermission) (*ObjectPermission, error) { - var resp ObjectPermission - if err := c.doJSON(ctx, http.MethodPut, fmt.Sprintf("%s/objects/%s/permissions", BaseAPIPathV3, objectID), perms, &resp); err != nil { +// CreateObjectACL creates an ACL for one principal and returns it with its ID. +func (c *Client) CreateObjectACL(ctx context.Context, objectID string, acl ObjectACL) (*ObjectACL, error) { + var raw json.RawMessage + body := aclRequest{Principal: acl.Principal, Permissions: acl.Permissions} + if err := c.doJSON(ctx, http.MethodPost, permissionsPath(objectID), body, &raw); err != nil { + return nil, err + } + // The documented response is an array with the created ACL; accept a + // single object too. + raw = bytes.TrimSpace(raw) + if len(raw) == 0 { + return &acl, nil + } + if raw[0] == '[' { + var list []ObjectACL + if err := json.Unmarshal(raw, &list); err != nil { + return nil, fmt.Errorf("parsing response: %w", err) + } + if len(list) == 0 { + return &acl, nil + } + return &list[0], nil + } + var one ObjectACL + if err := json.Unmarshal(raw, &one); err != nil { + return nil, fmt.Errorf("parsing response: %w", err) + } + return &one, nil +} + +// UpdateObjectACL replaces the permissions of an existing ACL. +func (c *Client) UpdateObjectACL(ctx context.Context, objectID, aclID string, acl ObjectACL) error { + body := aclRequest{Principal: acl.Principal, Permissions: acl.Permissions} + return c.doJSON(ctx, http.MethodPut, permissionsPath(objectID, aclID), body, nil) +} + +// DeleteObjectACL deletes one ACL of an object. +func (c *Client) DeleteObjectACL(ctx context.Context, objectID, aclID string) error { + return c.doJSON(ctx, http.MethodDelete, permissionsPath(objectID, aclID), nil, nil) +} + +// DeleteAllObjectACLs deletes every ACL of an object. +func (c *Client) DeleteAllObjectACLs(ctx context.Context, objectID string) error { + return c.doJSON(ctx, http.MethodDelete, permissionsPath(objectID), nil, nil) +} + +// CheckObjectAccess returns the current user's access to an object. With +// assetType set, Create reports whether that asset type can be created in +// the object (a project or folder). +func (c *Client) CheckObjectAccess(ctx context.Context, objectID, assetType string) (*ObjectAccess, error) { + query := map[string]string{} + if assetType != "" { + query["type"] = assetType + } + var resp ObjectAccess + if err := c.doJSONWithQuery(ctx, http.MethodGet, permissionsPath(objectID, "checkAccess"), query, nil, &resp); err != nil { return nil, err } return &resp, nil } -// DeleteObjectPermissions deletes permissions from an object. -func (c *Client) DeleteObjectPermissions(ctx context.Context, objectID string) error { - return c.doJSON(ctx, http.MethodDelete, fmt.Sprintf("%s/objects/%s/permissions", BaseAPIPathV3, objectID), nil, nil) +// PrincipalKey identifies a principal regardless of name case. +func PrincipalKey(p ACLPrincipal) string { + return strings.ToUpper(p.Type) + "\x1f" + strings.ToLower(p.Name) +} + +// ACLPlan lists the ACL changes needed to reach a desired state. +type ACLPlan struct { + Create []ObjectACL + // Update entries carry the existing ACL ID and the new permissions. + Update []ObjectACL + // Delete entries are existing ACLs (with IDs). + Delete []ObjectACL +} + +// Empty reports whether the plan has no changes. +func (p ACLPlan) Empty() bool { + return len(p.Create) == 0 && len(p.Update) == 0 && len(p.Delete) == 0 +} + +// DiffACLs compares current ACLs with desired ones, matched by principal +// type and case-insensitive name. Principals only in desired are created, +// those with different permissions are updated. Principals only in current +// are deleted when prune is true and left alone otherwise. Results are +// sorted by principal. +func DiffACLs(current, desired []ObjectACL, prune bool) ACLPlan { + var plan ACLPlan + cur := make(map[string]ObjectACL, len(current)) + for _, a := range current { + cur[PrincipalKey(a.Principal)] = a + } + want := make(map[string]bool, len(desired)) + for _, d := range desired { + k := PrincipalKey(d.Principal) + if want[k] { + continue + } + want[k] = true + existing, ok := cur[k] + switch { + case !ok: + plan.Create = append(plan.Create, ObjectACL{Principal: d.Principal, Permissions: d.Permissions}) + case existing.Permissions != d.Permissions: + plan.Update = append(plan.Update, ObjectACL{ID: existing.ID, Principal: existing.Principal, Permissions: d.Permissions}) + } + } + if prune { + for _, a := range current { + if !want[PrincipalKey(a.Principal)] { + plan.Delete = append(plan.Delete, a) + } + } + } + for _, s := range [][]ObjectACL{plan.Create, plan.Update, plan.Delete} { + sort.Slice(s, func(i, j int) bool { return PrincipalKey(s[i].Principal) < PrincipalKey(s[j].Principal) }) + } + return plan +} + +// ParseACLPermissions parses a comma-separated permission list such as +// "read,execute". "all" grants every permission and "none" none. Names are +// case-insensitive; "changePermission", "change-permission" and "perm" are +// accepted for the change permission right. +func ParseACLPermissions(list []string) (ACLPermissions, error) { + var p ACLPermissions + for _, item := range list { + for _, name := range strings.Split(item, ",") { + switch strings.ToLower(strings.TrimSpace(name)) { + case "": + case "read": + p.Read = true + case "update": + p.Update = true + case "delete": + p.Delete = true + case "execute": + p.Execute = true + case "changepermission", "change-permission", "perm": + p.ChangePermission = true + case "all": + p = ACLPermissions{Read: true, Update: true, Delete: true, Execute: true, ChangePermission: true} + case "none": + p = ACLPermissions{} + default: + return p, fmt.Errorf("unknown permission %q (use read, update, delete, execute, changePermission, all, none)", strings.TrimSpace(name)) + } + } + } + return p, nil +} + +// String lists the granted permissions, e.g. "read,execute", or "none". +func (p ACLPermissions) String() string { + var out []string + for _, f := range []struct { + on bool + name string + }{{p.Read, "read"}, {p.Update, "update"}, {p.Delete, "delete"}, {p.Execute, "execute"}, {p.ChangePermission, "changePermission"}} { + if f.on { + out = append(out, f.name) + } + } + if len(out) == 0 { + return "none" + } + return strings.Join(out, ",") } diff --git a/internal/client/permissions_test.go b/internal/client/permissions_test.go new file mode 100644 index 0000000..47b26e8 --- /dev/null +++ b/internal/client/permissions_test.go @@ -0,0 +1,158 @@ +package client + +import ( + "context" + "encoding/json" + "io" + "net/http" + "reflect" + "strings" + "testing" +) + +const aclListJSON = `[ + {"id": "a1", "principal": {"type": "GROUP", "name": "Developer"}, + "permissions": {"read": true, "update": true, "delete": true, "execute": true, "changePermission": false}}, + {"id": "a2", "principal": {"type": "USER", "name": "larry@infa.com"}, + "permissions": {"read": true, "update": false, "delete": false, "execute": false, "changePermission": false}} +]` + +func TestListObjectACLs(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet || r.URL.Path != "/public/core/v3/objects/obj1/permissions" { + t.Errorf("unexpected request %s %s", r.Method, r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(aclListJSON)) + }) + acls, err := newTestClient(handler).ListObjectACLs(context.Background(), "obj1") + if err != nil { + t.Fatalf("ListObjectACLs() error: %v", err) + } + if len(acls) != 2 || acls[0].Principal.Name != "Developer" || !acls[0].Permissions.Execute || acls[1].Principal.Type != "USER" { + t.Errorf("unexpected ACLs: %+v", acls) + } +} + +func TestCreateObjectACL(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost || r.URL.Path != "/public/core/v3/objects/obj1/permissions" { + t.Errorf("unexpected request %s %s", r.Method, r.URL.Path) + } + body, _ := io.ReadAll(r.Body) + want := `{"principal":{"type":"USER","name":"larry@infa.com"},"permissions":{"read":true,"update":false,"delete":false,"execute":true,"changePermission":false}}` + if string(body) != want { + t.Errorf("body = %s\nwant %s", body, want) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`[{"id": "new1", "principal": {"type": "USER", "name": "larry@infa.com"}, "permissions": {"read": true, "execute": true}}]`)) + }) + acl, err := newTestClient(handler).CreateObjectACL(context.Background(), "obj1", ObjectACL{ + Principal: ACLPrincipal{Type: PrincipalUser, Name: "larry@infa.com"}, + Permissions: ACLPermissions{Read: true, Execute: true}, + }) + if err != nil { + t.Fatalf("CreateObjectACL() error: %v", err) + } + if acl.ID != "new1" { + t.Errorf("expected ID new1, got %q", acl.ID) + } +} + +func TestUpdateAndDeleteObjectACL(t *testing.T) { + var calls []string + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls = append(calls, r.Method+" "+r.URL.Path) + if r.Method == http.MethodPut { + var body map[string]json.RawMessage + _ = json.NewDecoder(r.Body).Decode(&body) + if _, hasID := body["id"]; hasID { + t.Error("PUT body must not contain the ACL id") + } + } + w.WriteHeader(http.StatusNoContent) + }) + c := newTestClient(handler) + ctx := context.Background() + acl := ObjectACL{ID: "a1", Principal: ACLPrincipal{Type: PrincipalGroup, Name: "Developer"}, Permissions: ACLPermissions{Read: true}} + if err := c.UpdateObjectACL(ctx, "obj1", "a1", acl); err != nil { + t.Fatal(err) + } + if err := c.DeleteObjectACL(ctx, "obj1", "a1"); err != nil { + t.Fatal(err) + } + if err := c.DeleteAllObjectACLs(ctx, "obj1"); err != nil { + t.Fatal(err) + } + want := []string{ + "PUT /public/core/v3/objects/obj1/permissions/a1", + "DELETE /public/core/v3/objects/obj1/permissions/a1", + "DELETE /public/core/v3/objects/obj1/permissions", + } + if !reflect.DeepEqual(calls, want) { + t.Errorf("calls = %v, want %v", calls, want) + } +} + +func TestCheckObjectAccess(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/public/core/v3/objects/obj1/permissions/checkAccess" || r.URL.Query().Get("type") != "DTEMPLATE" { + t.Errorf("unexpected request %s?%s", r.URL.Path, r.URL.RawQuery) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"permissions":{"create":true,"read":true,"update":true,"delete":true,"execute":false,"changePermission":true}}`)) + }) + a, err := newTestClient(handler).CheckObjectAccess(context.Background(), "obj1", "DTEMPLATE") + if err != nil { + t.Fatal(err) + } + if !a.Permissions.Create || !a.Permissions.ChangePermission || a.Permissions.Execute { + t.Errorf("unexpected access: %+v", a.Permissions) + } +} + +func TestDiffACLs(t *testing.T) { + var current []ObjectACL + _ = json.Unmarshal([]byte(aclListJSON), ¤t) + + desired := []ObjectACL{ + {Principal: ACLPrincipal{Type: "GROUP", Name: "developer"}, Permissions: ACLPermissions{Read: true}}, // update (case-insensitive match) + {Principal: ACLPrincipal{Type: "GROUP", Name: "Everyone"}, Permissions: ACLPermissions{Read: true}}, // create + {Principal: ACLPrincipal{Type: "GROUP", Name: "Everyone"}, Permissions: ACLPermissions{Read: true, Update: true}}, // duplicate, ignored + } + + plan := DiffACLs(current, desired, false) + if len(plan.Create) != 1 || plan.Create[0].Principal.Name != "Everyone" || plan.Create[0].Permissions.Update { + t.Errorf("create = %+v", plan.Create) + } + if len(plan.Update) != 1 || plan.Update[0].ID != "a1" || plan.Update[0].Permissions != (ACLPermissions{Read: true}) || plan.Update[0].Principal.Name != "Developer" { + t.Errorf("update = %+v", plan.Update) + } + if len(plan.Delete) != 0 { + t.Errorf("without prune nothing is deleted, got %+v", plan.Delete) + } + + plan = DiffACLs(current, desired, true) + if len(plan.Delete) != 1 || plan.Delete[0].ID != "a2" { + t.Errorf("prune should delete a2, got %+v", plan.Delete) + } + if !DiffACLs(current, current, true).Empty() { + t.Error("identical ACLs should give an empty plan") + } +} + +func TestParseACLPermissions(t *testing.T) { + p, err := ParseACLPermissions([]string{"Read, execute", "PERM"}) + if err != nil || p != (ACLPermissions{Read: true, Execute: true, ChangePermission: true}) { + t.Errorf("got %+v, %v", p, err) + } + if p, _ := ParseACLPermissions([]string{"all"}); p.String() != "read,update,delete,execute,changePermission" { + t.Errorf("all = %s", p) + } + if p, _ := ParseACLPermissions([]string{"all", "none"}); p.String() != "none" { + t.Errorf("none = %s", p) + } + if _, err := ParseACLPermissions([]string{"write"}); err == nil || !strings.Contains(err.Error(), "write") { + t.Errorf("expected unknown permission error, got %v", err) + } +} From 0d9c87c1cfba75ab5e97aaa18979d2236d9672a0 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Tue, 29 Sep 2026 23:43:27 -0400 Subject: [PATCH 12/52] feat(permission): interactive ACL editor (CR-0042) - internal/tui/grid: generic grid core extracted from privmatrix (cells, pending-change marks, cursor, row/column toggles, filter with section headers, scrolling, detail footer, clear-row key, action keys, empty text) - privmatrix is now a thin adapter over the grid; role edit unchanged - internal/tui/acleditor.go: EditACLs grid of principals x read/update/ delete/execute/changePermission; n adds a user group or user, d clears a row (ACL deleted); review with +/~/- lines and a warning when your own change permission is removed; returns a DiffACLs plan - permission edit: --object-id, --path/--type, or a project -> folder -> asset picker; checkAccess guard; applies via applyACLPlan (shared with permission set) and prints the resulting ACLs - fix huh cancel hints: Ctrl+C cancels lists (Esc clears the filter) - docs: permission.md edit section, interactive.md, agent.md, README, comparison, completions; CR-0042 moved to pending --- README.md | 2 +- cmd/permission.go | 1 + cmd/permission_edit.go | 197 +++++++ completions/iics.bash | 52 ++ .../CR-0042-permission-interactive-editor.md | 41 +- docs/documentation/agent.md | 2 +- .../comparison-informatica-cli.md | 2 +- docs/documentation/interactive.md | 2 +- docs/documentation/permission.md | 68 +++ internal/tui/acleditor.go | 251 ++++++++ internal/tui/acleditor_test.go | 78 +++ internal/tui/grid/grid.go | 537 +++++++++++++++++ internal/tui/grid/grid_test.go | 80 +++ internal/tui/privmatrix/model.go | 545 ++---------------- internal/tui/privmatrix/model_test.go | 40 +- internal/tui/prompt.go | 2 +- 16 files changed, 1379 insertions(+), 521 deletions(-) create mode 100644 cmd/permission_edit.go rename docs/ChangeRequests/{new => pending}/CR-0042-permission-interactive-editor.md (63%) create mode 100644 internal/tui/acleditor.go create mode 100644 internal/tui/acleditor_test.go create mode 100644 internal/tui/grid/grid.go create mode 100644 internal/tui/grid/grid_test.go diff --git a/README.md b/README.md index e06b7f2..41055dd 100644 --- a/README.md +++ b/README.md @@ -264,7 +264,7 @@ it easy to override credentials in CI pipelines without touching the config file | [metering](docs/documentation/metering.md) | | `get`, `download` | Query usage and metering data | | [objects](docs/documentation/objects.md) | | `list`, `dependencies` | List/search assets, find dependencies | | [package](docs/documentation/package.md) | | `expand`, `create`, `dependencies` | Extract, assemble, or inspect dependencies of IICS export packages | -| [permission](docs/documentation/permission.md) | `perm` | `get`, `add`, `update`, `delete`, `set`, `check` | Manage object-level permissions | +| [permission](docs/documentation/permission.md) | `perm` | `get`, `add`, `update`, `delete`, `set`, `check`, `edit` | Manage object-level permissions | | [privilege](docs/documentation/privilege.md) | | `list` | List available privileges | | [profile](docs/documentation/profile.md) | | `add`, `edit`, `list`, `delete`, `set-default`, `set-password`, `show` | Manage connection profiles | | [project](docs/documentation/project.md) | | `create`, `update`, `delete` | Manage projects | diff --git a/cmd/permission.go b/cmd/permission.go index af84551..96420b6 100644 --- a/cmd/permission.go +++ b/cmd/permission.go @@ -30,6 +30,7 @@ Select the object with --object-id, or with --path and --type.`, cmd.AddCommand(newPermissionDeleteCmd()) cmd.AddCommand(newPermissionSetCmd()) cmd.AddCommand(newPermissionCheckCmd()) + cmd.AddCommand(newPermissionEditCmd()) return cmd } diff --git a/cmd/permission_edit.go b/cmd/permission_edit.go new file mode 100644 index 0000000..c4a4695 --- /dev/null +++ b/cmd/permission_edit.go @@ -0,0 +1,197 @@ +package cmd + +import ( + "context" + "fmt" + "os" + "sort" + "strings" + + "github.com/jbrazda/iics-cli/internal/client" + "github.com/jbrazda/iics-cli/internal/tui" + "github.com/spf13/cobra" +) + +func newPermissionEditCmd() *cobra.Command { + var obj objectRef + cmd := &cobra.Command{ + Use: "edit", + Short: "Edit an object's permissions in an interactive grid", + Long: `Edit an object's ACLs in an interactive grid: user groups and users are rows, +read / update / delete / execute / change permission are columns. + +Keys: space toggles a permission, a toggles a row, c a column, d clears a +row (the ACL is deleted), n adds a user group or user, / filters. Enter ends +editing and shows a review of the changes (+ add, ~ update, - delete) before +anything is applied. + +Without --object-id or --path, pick a project, then optionally a folder or +asset inside it. Requires a terminal and change permission on the object.`, + Example: ` iics permission edit --path "Default/Sales" --type Folder + iics permission edit --object-id + iics permission edit # pick the object`, + RunE: func(cmd *cobra.Command, args []string) error { + if !isInteractiveTTY() { + return fmt.Errorf("permission edit requires a terminal; use 'permission add/update/delete/set' in scripts") + } + c, err := getClient(cmd) + if err != nil { + return err + } + ctx := context.Background() + + var id, label string + if obj.id == "" && obj.path == "" { + picked, perr := pickObject(ctx, c) + if perr != nil || picked == nil { + return perr + } + id, label = picked.ID, fmt.Sprintf("%s (%s)", picked.Path, picked.Type) + } else if id, label, err = obj.resolve(ctx, c); err != nil { + return err + } + + access, err := c.CheckObjectAccess(ctx, id, "") + if err != nil { + return err + } + if !access.Permissions.ChangePermission { + return fmt.Errorf("you do not have change permission on %s", label) + } + current, err := c.ListObjectACLs(ctx, id) + if err != nil { + return err + } + users, groups, err := listPrincipalNames(ctx, c) + if err != nil { + return err + } + currentUser := "" + if _, p, _, perr := resolveProfile(); perr == nil && p != nil { + currentUser = p.Username + } + + plan, ok, err := tui.EditACLs(tui.ACLEditorInput{ + ObjectLabel: label, + Current: current, + Users: users, + Groups: groups, + CurrentUser: currentUser, + Out: os.Stderr, + Accessible: prompter.Accessible, + NoColor: editorNoColor(), + }) + if err != nil { + return err + } + if !ok { + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Canceled.") + return nil + } + if err = applyACLPlan(ctx, c, id, plan); err != nil { + return err + } + for _, l := range tui.ACLPlanLines(plan) { + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), l) + } + return printACLs(ctx, c, id) + }, + } + obj.register(cmd) + return cmd +} + +// listPrincipalNames returns all user names and user group names. +func listPrincipalNames(ctx context.Context, c *client.Client) ([]string, []string, error) { + var users []string + opts := client.UserListOptions{Limit: 200} + for { + batch, err := c.ListUsers(ctx, opts) + if err != nil { + return nil, nil, fmt.Errorf("listing users: %w", err) + } + for _, u := range batch { + users = append(users, u.UserName) + } + if len(batch) < opts.Limit { + break + } + opts.Skip += opts.Limit + } + groups, err := listAllUserGroups(ctx, c) + if err != nil { + return nil, nil, fmt.Errorf("listing user groups: %w", err) + } + names := make([]string, len(groups)) + for i, g := range groups { + names[i] = g.UserGroupName + } + return users, names, nil +} + +// pickObject lets the user choose a project, then drill into folders, and +// pick the project, a folder, or an asset. Returns nil when canceled. +func pickObject(ctx context.Context, c *client.Client) (*client.Object, error) { + projects, err := c.ListAllObjects(ctx, client.ObjectsListOptions{Type: "Project"}, nil) + if err != nil { + return nil, err + } + if len(projects.Objects) == 0 { + return nil, fmt.Errorf("no projects found") + } + items := sortedObjects(projects.Objects) + idx, err := tui.PickOne(prompter, "Select a project", items, func(o client.Object) string { return o.Path }) + if err != nil || idx < 0 { + return nil, err + } + current := items[idx] + + for { + children, lerr := c.ListAllObjects(ctx, client.ObjectsListOptions{Query: fmt.Sprintf("location=='%s'", current.Path)}, nil) + if lerr != nil { + return nil, lerr + } + // The location query also returns the location object itself. + var kids []client.Object + for _, o := range sortedObjects(children.Objects) { + if o.ID != current.ID { + kids = append(kids, o) + } + } + if len(kids) == 0 { + return ¤t, nil + } + options := append([]client.Object{current}, kids...) + idx, err = tui.PickOne(prompter, "Select the object", options, func(o client.Object) string { + if o.ID == current.ID { + return fmt.Sprintf("(this %s) %s", strings.ToLower(o.Type), o.Path) + } + name := o.Path[strings.LastIndex(o.Path, "/")+1:] + if o.Type == "Folder" { + return name + "/" + } + return fmt.Sprintf("%s [%s]", name, o.Type) + }) + if err != nil || idx < 0 { + return nil, err + } + chosen := options[idx] + if chosen.ID == current.ID || chosen.Type != "Folder" { + return &chosen, nil + } + current = chosen + } +} + +// sortedObjects orders folders first, then by path. +func sortedObjects(objs []client.Object) []client.Object { + out := append([]client.Object(nil), objs...) + sort.SliceStable(out, func(i, j int) bool { + fi, fj := out[i].Type == "Folder", out[j].Type == "Folder" + if fi != fj { + return fi + } + return strings.ToLower(out[i].Path) < strings.ToLower(out[j].Path) + }) + return out +} diff --git a/completions/iics.bash b/completions/iics.bash index 470ea7b..0193912 100644 --- a/completions/iics.bash +++ b/completions/iics.bash @@ -4216,6 +4216,57 @@ _iics_permission_delete() noun_aliases=() } +_iics_permission_edit() +{ + last_command="iics_permission_edit" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--object-id=") + two_word_flags+=("--object-id") + local_nonpersistent_flags+=("--object-id") + local_nonpersistent_flags+=("--object-id=") + flags+=("--path=") + two_word_flags+=("--path") + local_nonpersistent_flags+=("--path") + local_nonpersistent_flags+=("--path=") + flags+=("--type=") + two_word_flags+=("--type") + local_nonpersistent_flags+=("--type") + local_nonpersistent_flags+=("--type=") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--http-timeout=") + two_word_flags+=("--http-timeout") + flags+=("--no-color") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + flags+=("--profile=") + two_word_flags+=("--profile") + two_word_flags+=("-p") + flags+=("--theme=") + two_word_flags+=("--theme") + flags+=("--verbose") + flags+=("-v") + flags+=("--wide") + flags+=("--width=") + two_word_flags+=("--width") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _iics_permission_get() { last_command="iics_permission_get" @@ -4409,6 +4460,7 @@ _iics_permission() commands+=("add") commands+=("check") commands+=("delete") + commands+=("edit") commands+=("get") commands+=("set") commands+=("update") diff --git a/docs/ChangeRequests/new/CR-0042-permission-interactive-editor.md b/docs/ChangeRequests/pending/CR-0042-permission-interactive-editor.md similarity index 63% rename from docs/ChangeRequests/new/CR-0042-permission-interactive-editor.md rename to docs/ChangeRequests/pending/CR-0042-permission-interactive-editor.md index b150cdf..53f91fd 100644 --- a/docs/ChangeRequests/new/CR-0042-permission-interactive-editor.md +++ b/docs/ChangeRequests/pending/CR-0042-permission-interactive-editor.md @@ -7,8 +7,8 @@ ## Dependencies -- Bug `docs/issues/new/2026-09-30-permission-client-wrong-api-shape.md` must - be fixed first (correct ACL structs and per-ACL endpoints). +- Bug `docs/issues/pending/2026-09-30-permission-client-wrong-api-shape.md` + (fixed in `089f26c`: correct ACL structs and per-ACL endpoints). - Reuses the TUI foundation from CR-0040 and CR-0041. ## Problem @@ -41,7 +41,8 @@ object with several groups currently takes several hand-written requests. - `n` opens a filterable picker of users and user groups not yet in the ACL list; the new row starts with `read` checked. - - `d` marks a principal's ACL for deletion (row shown struck or `[-]`). + - `d` clears a principal's row; a principal with no permissions has its + ACL deleted (shown as `[-]` cells). - Same marks as CR-0040: `[x]` unchanged, `[+]` grant, `[-]` revoke. 3. **Review** - per principal: create, update (changed permissions), or delete; then Apply / Back / Cancel. @@ -78,11 +79,31 @@ applies the same diff declaratively, so the TUI and scripts share one ## Acceptance Criteria -- [ ] `permission edit` shows current ACLs as a grid and applies +- [x] `permission edit` shows current ACLs as a grid and applies create/update/delete changes after review -- [ ] Principals can be added from a filterable user/group list and removed -- [ ] Missing `changePermission` is detected before any change -- [ ] `permission set --from-file` and the editor share `DiffACLs` -- [ ] Role privilege editor behavior unchanged after the grid extraction -- [ ] Live test on `dev` against a throwaway object, restoring ACLs afterwards -- [ ] `go build`, `go vet`, `golangci-lint`, `go test ./...` pass +- [x] Principals can be added from a filterable user/group list and removed +- [x] Missing `changePermission` is detected before any change +- [x] `permission set --from-file` and the editor share `DiffACLs` +- [x] Role privilege editor behavior unchanged after the grid extraction +- [x] Live test on `dev` against a throwaway object, restoring ACLs afterwards +- [x] `go build`, `go vet`, `golangci-lint`, `go test ./...` pass + +## Implementation Notes + +- `internal/tui/grid/` - generic grid core (cells, marks, cursor, row/column + toggles, filter with section headers, scrolling, footer detail, clear-row + key, extra action keys that end the grid, empty-state text). +- `internal/tui/privmatrix/` - now a thin adapter building grid rows from a + privilege service; `role edit` behavior unchanged (tests rewritten against + the public model API, same scenarios). +- `internal/tui/acleditor.go` - `EditACLs`: grid, `n` add-principal picker, + review with `ACLPlanLines` and self-lockout warning; returns a + `client.ACLPlan` from `client.DiffACLs(current, desired, prune=true)`. +- `cmd/permission_edit.go` - object resolution (`--object-id`, + `--path/--type`, or project/folder/asset picker via + `q=location==''`), `checkAccess` guard, apply with + `applyACLPlan` (shared with `permission set`). +- Verified on `dev` with a throwaway project and folder: add principal, + toggle, clear row, review, apply; object picker drill-down; no-change + review. The location query also returns the location object itself, so + it is filtered out of its own child list. diff --git a/docs/documentation/agent.md b/docs/documentation/agent.md index 0c5a18c..b491985 100644 --- a/docs/documentation/agent.md +++ b/docs/documentation/agent.md @@ -323,7 +323,7 @@ for a platform. | `--os` | string | yes | Operating system: `win64` or `linux64` | When `--os` is omitted and the session is interactive, the command shows a -selection list (`win64`/`linux64`); press `Esc` to cancel. See +selection list (`win64`/`linux64`); press `Ctrl+C` to cancel. See [Interactive prompts](interactive.md). All [global flags](../../README.md#global-flags) apply. diff --git a/docs/documentation/comparison-informatica-cli.md b/docs/documentation/comparison-informatica-cli.md index 08fa7ed..ee9382c 100644 --- a/docs/documentation/comparison-informatica-cli.md +++ b/docs/documentation/comparison-informatica-cli.md @@ -186,7 +186,7 @@ It cannot manage users, connections, schedules, roles, or any other administrati | `runtime` | list, get, create, update | | `agent` | list, start, stop | | `tag` | assign, remove | -| `permission` | get, set, delete | +| `permission` | get, add, update, delete, set, check, edit | | `securitylog` | list | | `metering` | get, download | | `sourcecontrol` | checkout, checkin, pull, commit | diff --git a/docs/documentation/interactive.md b/docs/documentation/interactive.md index 72b92c9..30b7e5c 100644 --- a/docs/documentation/interactive.md +++ b/docs/documentation/interactive.md @@ -11,7 +11,7 @@ terminal. | Prompt | Keys | | ------------ | -------------------------------------------------------------------- | | Text input | Type, `Enter` to accept (an empty value keeps the shown default) | -| Single list | Arrows or `j` / `k` to move, `/` to filter, `Enter` to choose, `Esc` to cancel | +| Single list | Arrows or `j` / `k` to move, `/` to filter (`Esc` clears the filter), `Enter` to choose, `Ctrl+C` to cancel | | Checklist | `Space` or `x` to toggle, `/` to filter, `Ctrl+A` to select all, `Enter` to confirm | | Yes / No | Left / right or `y` / `n`, `Enter` to confirm | | Any prompt | `Ctrl+C` cancels the command | diff --git a/docs/documentation/permission.md b/docs/documentation/permission.md index 49d80f4..30c7be1 100644 --- a/docs/documentation/permission.md +++ b/docs/documentation/permission.md @@ -28,6 +28,7 @@ iics perm [flags] | `delete` | Delete one ACL, or all ACLs of an object | | `set` | Make an object's ACLs match a JSON file | | `check` | Show your own access to an object | +| `edit` | Edit an object's ACLs in an interactive grid | ## Selecting the object @@ -266,6 +267,73 @@ iics permission check --object-id iics perm check --path "Default" --type Project --asset-type DTEMPLATE ``` +## permission edit + +Edit an object's ACLs in an interactive grid (terminal only). User groups and +users are rows; `READ`, `UPDATE`, `DELETE`, `EXEC` and `PERM` (change +permission) are columns. + +```text +Permissions: Default/Sales (Folder) + + PRINCIPAL READ UPDATE DELETE EXEC PERM + Data Engineering group [x] [+] [ ] [x] [ ] +> Developer_ReadOnly group [-] [ ] [ ] [ ] [ ] + jdoe@example.com user(new) [+] [ ] [ ] [ ] [ ] +``` + +| Mark | Meaning | +| ----- | ------------------------------ | +| `[x]` | Granted, unchanged | +| `[+]` | Will be granted | +| `[-]` | Will be revoked | +| `[ ]` | Not granted | + +| Key | Action | +| ------------------- | ------------------------------------------------------- | +| Arrows or `h j k l` | Move | +| `Space` or `x` | Toggle the permission under the cursor | +| `a` | Toggle every permission in the row | +| `c` | Toggle the column for all visible rows | +| `d` | Clear the row; the principal's ACL is deleted | +| `n` | Add a user group or user (filterable list) | +| `/`, `Ctrl+U` | Filter rows, clear the filter | +| `Enter`, `Esc`, `q` | Finish editing and review | + +A principal left with no permissions has its ACL deleted. The review lists the +changes (`+` add, `~` update, `-` delete) with **Apply changes**, **Back to +editing** or **Cancel**, and warns when the change removes change permission +from your own user. Changes are applied in the order add, update, delete, and +the resulting ACLs are printed. + +Without `--object-id` or `--path`, pick a project from a filterable list, then +the project itself, a folder (to open it), or an asset. + +The command checks first that you have change permission on the object. + +> Once an object has ACLs, only the listed principals have access. Removing +> your own groups can take away even your create access; administrators keep +> change permission and can delete the ACLs again. + +### Examples + +```bash +iics permission edit --path "Default/Sales" --type Folder + +iics perm edit --object-id + +# Pick the object interactively +iics perm edit +``` + +```powershell +iics permission edit --path "Default/Sales" --type Folder + +iics perm edit +``` + +See [Interactive prompts](interactive.md) for general keys. + ## See also - [lookup](lookup.md) - resolve object paths to IDs diff --git a/internal/tui/acleditor.go b/internal/tui/acleditor.go new file mode 100644 index 0000000..705d3b4 --- /dev/null +++ b/internal/tui/acleditor.go @@ -0,0 +1,251 @@ +package tui + +import ( + "errors" + "fmt" + "io" + "os" + "sort" + "strings" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/huh" + "github.com/jbrazda/iics-cli/internal/client" + "github.com/jbrazda/iics-cli/internal/tui/grid" +) + +// ACLEditorInput configures EditACLs. +type ACLEditorInput struct { + // ObjectLabel names the object in titles, e.g. "Default/Sales (Folder)". + ObjectLabel string + // Current are the object's ACLs now. + Current []client.ObjectACL + // Users and Groups are the principals that can be added. + Users, Groups []string + // CurrentUser is the signed-in user name, used to warn when the change + // removes that user's own change permission. + CurrentUser string + + Out io.Writer + Accessible bool + NoColor bool +} + +var aclColumns = []string{"READ", "UPDATE", "DELETE", "EXEC", "PERM"} + +// aclPermissionKeys names the permission of each column, in order. +var aclPermissionKeys = []string{"read", "update", "delete", "execute", "changePermission"} + +type aclRow struct { + principal client.ACLPrincipal + added bool +} + +func aclCellKey(p client.ACLPrincipal, perm string) string { + return client.PrincipalKey(p) + "\x1e" + perm +} + +func permsToSet(set map[string]bool, p client.ACLPrincipal, perms client.ACLPermissions) { + set[aclCellKey(p, "read")] = perms.Read + set[aclCellKey(p, "update")] = perms.Update + set[aclCellKey(p, "delete")] = perms.Delete + set[aclCellKey(p, "execute")] = perms.Execute + set[aclCellKey(p, "changePermission")] = perms.ChangePermission +} + +func permsFromSet(set map[string]bool, p client.ACLPrincipal) client.ACLPermissions { + return client.ACLPermissions{ + Read: set[aclCellKey(p, "read")], + Update: set[aclCellKey(p, "update")], + Delete: set[aclCellKey(p, "delete")], + Execute: set[aclCellKey(p, "execute")], + ChangePermission: set[aclCellKey(p, "changePermission")], + } +} + +// EditACLs runs the ACL grid for one object and returns the change plan. ok +// is false when the user canceled. A principal left with no permissions has +// its ACL deleted. +func EditACLs(in ACLEditorInput) (client.ACLPlan, bool, error) { + original := map[string]bool{} + selected := map[string]bool{} + var rows []aclRow + for _, a := range in.Current { + rows = append(rows, aclRow{principal: a.Principal}) + permsToSet(original, a.Principal, a.Permissions) + permsToSet(selected, a.Principal, a.Permissions) + } + + for { + m := grid.New(aclGridRows(rows), original, selected, grid.Options{ + Title: "Permissions: " + in.ObjectLabel, + Columns: aclColumns, + KeyHeader: "PRINCIPAL", + NoColor: in.NoColor, + ClearKey: "d", + EmptyText: "No ACLs: access follows roles. Press n to add a user group or user.", + Actions: map[string]string{"n": "add principal"}, + ActionOrder: []string{"n"}, + }) + out := in.Out + if out == nil { + out = os.Stderr + } + if _, err := tea.NewProgram(m, tea.WithAltScreen(), tea.WithOutput(out)).Run(); err != nil { + return client.ACLPlan{}, false, fmt.Errorf("permission editor: %w", err) + } + + if m.Action() == "n" { + p, ok, err := pickPrincipal(in, rows) + if err != nil { + return client.ACLPlan{}, false, err + } + if ok { + rows = append(rows, aclRow{principal: p, added: true}) + selected[aclCellKey(p, "read")] = true + } + continue + } + + desired := desiredACLs(rows, selected) + plan := client.DiffACLs(in.Current, desired, true) + lines := ACLPlanLines(plan) + canApply := !plan.Empty() + if !canApply { + lines = []string{"No changes."} + } + if w := selfLockoutWarning(in.CurrentUser, in.Current, desired); w != "" { + lines = append(lines, "", w) + } + choice, err := confirmReview(in.Out, in.Accessible, "Permissions: "+in.ObjectLabel, lines, "Apply changes", canApply) + if err != nil { + return client.ACLPlan{}, false, err + } + switch choice { + case reviewApply: + return plan, true, nil + case reviewCancel: + return client.ACLPlan{}, false, nil + } + } +} + +func aclGridRows(rows []aclRow) []grid.Row { + sorted := append([]aclRow(nil), rows...) + sort.SliceStable(sorted, func(i, j int) bool { + a, b := sorted[i].principal, sorted[j].principal + if a.Type != b.Type { + return a.Type == client.PrincipalGroup // groups first + } + return strings.ToLower(a.Name) < strings.ToLower(b.Name) + }) + out := make([]grid.Row, len(sorted)) + for i, r := range sorted { + tag := strings.ToLower(r.principal.Type) + if r.added { + tag += " (new)" + } + cells := make([]grid.Cell, len(aclPermissionKeys)) + for j, perm := range aclPermissionKeys { + cells[j] = grid.Cell{ + Key: aclCellKey(r.principal, perm), + Detail: fmt.Sprintf("%s %s - %s", strings.ToLower(r.principal.Type), r.principal.Name, perm), + } + } + out[i] = grid.Row{Label: r.principal.Name, Tag: tag, Cells: cells, Match: r.principal.Type} + } + return out +} + +// desiredACLs returns the ACLs for principals with at least one permission. +func desiredACLs(rows []aclRow, selected map[string]bool) []client.ObjectACL { + var out []client.ObjectACL + for _, r := range rows { + perms := permsFromSet(selected, r.principal) + if perms != (client.ACLPermissions{}) { + out = append(out, client.ObjectACL{Principal: r.principal, Permissions: perms}) + } + } + return out +} + +// ACLPlanLines describes a plan: "+ group X: read", "~ user Y: read,update", +// "- group Z". +func ACLPlanLines(plan client.ACLPlan) []string { + var lines []string + for _, a := range plan.Create { + lines = append(lines, fmt.Sprintf("+ %s %s: %s", strings.ToLower(a.Principal.Type), a.Principal.Name, a.Permissions)) + } + for _, a := range plan.Update { + lines = append(lines, fmt.Sprintf("~ %s %s: %s", strings.ToLower(a.Principal.Type), a.Principal.Name, a.Permissions)) + } + for _, a := range plan.Delete { + lines = append(lines, fmt.Sprintf("- %s %s", strings.ToLower(a.Principal.Type), a.Principal.Name)) + } + return lines +} + +// selfLockoutWarning returns a warning when the signed-in user had change +// permission through a user ACL and the desired ACLs drop it. +func selfLockoutWarning(user string, current, desired []client.ObjectACL) string { + if user == "" { + return "" + } + me := client.ACLPrincipal{Type: client.PrincipalUser, Name: user} + key := client.PrincipalKey(me) + had := false + for _, a := range current { + if client.PrincipalKey(a.Principal) == key && a.Permissions.ChangePermission { + had = true + } + } + if !had { + return "" + } + for _, a := range desired { + if client.PrincipalKey(a.Principal) == key && a.Permissions.ChangePermission { + return "" + } + } + return fmt.Sprintf("Warning: this removes change permission from your own user (%s).", user) +} + +// pickPrincipal asks for a user or group that has no row yet. +func pickPrincipal(in ACLEditorInput, rows []aclRow) (client.ACLPrincipal, bool, error) { + present := make(map[string]bool, len(rows)) + for _, r := range rows { + present[client.PrincipalKey(r.principal)] = true + } + var opts []huh.Option[string] + add := func(typ string, names []string) { + sorted := append([]string(nil), names...) + sort.Slice(sorted, func(i, j int) bool { return strings.ToLower(sorted[i]) < strings.ToLower(sorted[j]) }) + for _, n := range sorted { + p := client.ACLPrincipal{Type: typ, Name: n} + if !present[client.PrincipalKey(p)] { + opts = append(opts, huh.NewOption(fmt.Sprintf("%-5s %s", strings.ToLower(typ), n), typ+"\x1f"+n)) + } + } + } + add(client.PrincipalGroup, in.Groups) + add(client.PrincipalUser, in.Users) + if len(opts) == 0 { + return client.ACLPrincipal{}, false, nil + } + var choice string + sel := huh.NewSelect[string](). + Title("Add a user group or user"). + Description("/ to filter, ctrl+c to go back"). + Options(opts...). + Height(min(len(opts)+3, 18)). + Value(&choice) + err := huh.NewForm(huh.NewGroup(sel)).WithOutput(in.Out).WithAccessible(in.Accessible).Run() + if errors.Is(err, huh.ErrUserAborted) { + return client.ACLPrincipal{}, false, nil + } + if err != nil { + return client.ACLPrincipal{}, false, err + } + typ, name, _ := strings.Cut(choice, "\x1f") + return client.ACLPrincipal{Type: typ, Name: name}, true, nil +} diff --git a/internal/tui/acleditor_test.go b/internal/tui/acleditor_test.go new file mode 100644 index 0000000..d23f5b3 --- /dev/null +++ b/internal/tui/acleditor_test.go @@ -0,0 +1,78 @@ +package tui + +import ( + "reflect" + "strings" + "testing" + + "github.com/jbrazda/iics-cli/internal/client" +) + +func TestDesiredACLsAndPlan(t *testing.T) { + dev := client.ACLPrincipal{Type: client.PrincipalGroup, Name: "Developer"} + ops := client.ACLPrincipal{Type: client.PrincipalGroup, Name: "Ops"} + ann := client.ACLPrincipal{Type: client.PrincipalUser, Name: "ann@x.com"} + current := []client.ObjectACL{ + {ID: "a1", Principal: dev, Permissions: client.ACLPermissions{Read: true}}, + {ID: "a2", Principal: ops, Permissions: client.ACLPermissions{Read: true, Execute: true}}, + } + rows := []aclRow{{principal: dev}, {principal: ops}, {principal: ann, added: true}} + + selected := map[string]bool{} + permsToSet(selected, dev, client.ACLPermissions{Read: true, Update: true}) // update + permsToSet(selected, ops, client.ACLPermissions{}) // cleared -> delete + permsToSet(selected, ann, client.ACLPermissions{Read: true}) // new -> create + + desired := desiredACLs(rows, selected) + if len(desired) != 2 { + t.Fatalf("principals without permissions must be dropped, got %+v", desired) + } + got := ACLPlanLines(client.DiffACLs(current, desired, true)) + want := []string{ + "+ user ann@x.com: read", + "~ group Developer: read,update", + "- group Ops", + } + if !reflect.DeepEqual(got, want) { + t.Errorf("plan lines = %v, want %v", got, want) + } +} + +func TestACLGridRowsOrder(t *testing.T) { + rows := aclGridRows([]aclRow{ + {principal: client.ACLPrincipal{Type: client.PrincipalUser, Name: "zed@x.com"}}, + {principal: client.ACLPrincipal{Type: client.PrincipalGroup, Name: "ops"}, added: true}, + {principal: client.ACLPrincipal{Type: client.PrincipalGroup, Name: "Admins"}}, + }) + var labels []string + for _, r := range rows { + labels = append(labels, r.Label+" "+r.Tag) + } + want := []string{"Admins group", "ops group (new)", "zed@x.com user"} + if !reflect.DeepEqual(labels, want) { + t.Errorf("rows = %v, want %v", labels, want) + } + if len(rows[0].Cells) != 5 { + t.Errorf("each row needs 5 permission cells, got %d", len(rows[0].Cells)) + } +} + +func TestSelfLockoutWarning(t *testing.T) { + me := client.ACLPrincipal{Type: client.PrincipalUser, Name: "Admin@X.com"} + current := []client.ObjectACL{{Principal: me, Permissions: client.ACLPermissions{Read: true, ChangePermission: true}}} + kept := []client.ObjectACL{{Principal: me, Permissions: client.ACLPermissions{ChangePermission: true}}} + dropped := []client.ObjectACL{{Principal: me, Permissions: client.ACLPermissions{Read: true}}} + + if w := selfLockoutWarning("admin@x.com", current, kept); w != "" { + t.Errorf("no warning expected, got %q", w) + } + if w := selfLockoutWarning("admin@x.com", current, dropped); !strings.Contains(w, "your own user") { + t.Errorf("expected warning, got %q", w) + } + if w := selfLockoutWarning("admin@x.com", current, nil); w == "" { + t.Error("deleting the own ACL should warn") + } + if w := selfLockoutWarning("", current, nil); w != "" { + t.Error("unknown current user should not warn") + } +} diff --git a/internal/tui/grid/grid.go b/internal/tui/grid/grid.go new file mode 100644 index 0000000..aa24e64 --- /dev/null +++ b/internal/tui/grid/grid.go @@ -0,0 +1,537 @@ +// Package grid provides a Bubble Tea model for toggling a grid of boolean +// cells: rows are items (objects, principals), columns are actions or +// permissions. It renders pending changes against an original state, supports +// filtering, row and column toggles, locked cells, and scrolling. +package grid + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +// Cell is one toggleable value. Cells with an empty Key do not exist for +// the row and render as " . ". +type Cell struct { + Key string + Locked bool + // Detail is shown in the footer when the cell is focused. + Detail string +} + +// RowKind distinguishes item rows from section headers. +type RowKind int + +const ( + // RowItem is a row of cells. + RowItem RowKind = iota + // RowHeader is a non-selectable section label, e.g. "-- Other --". + RowHeader +) + +// Row is one grid row. +type Row struct { + Kind RowKind + Label string + // Tag is appended to the label, e.g. "(new)". + Tag string + Cells []Cell + // Match is extra text matched by the filter besides Label and cell details. + Match string +} + +// Options configures a Model. +type Options struct { + Title string + Columns []string + // KeyHeader is the header of the label column. + KeyHeader string + NoColor bool + // Actions are extra keys that end the grid with that action, e.g. + // {"n": "add principal"}. They are listed in the help line. + Actions map[string]string + // ActionOrder fixes the order of Actions in the help line. + ActionOrder []string + // ClearKey, when set, clears every unlocked cell of the focused row. + ClearKey string + // EmptyText is shown when the grid has no rows at all. + EmptyText string +} + +const ( + cellWidth = 8 + chromeLines = 6 // title, blank, column header, scroll info, detail, help +) + +// Model is the Bubble Tea model. +type Model struct { + opts Options + all []Row + rows []Row // visible rows + original map[string]bool + selected map[string]bool + + cursorRow, cursorCol int + offset int + width, height int + + filter textinput.Model + filtering bool + done bool + action string + + styles styles +} + +type styles struct { + title, header, dim, add, remove, keep, cursor, detail lipgloss.Style +} + +func newStyles(noColor bool) styles { + if noColor { + plain := lipgloss.NewStyle() + return styles{ + title: plain.Bold(true), header: plain.Bold(true), dim: plain, add: plain, + remove: plain, keep: plain.Bold(true), cursor: plain.Reverse(true), detail: plain, + } + } + return styles{ + title: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("6")), + header: lipgloss.NewStyle().Bold(true), + dim: lipgloss.NewStyle().Foreground(lipgloss.Color("244")), + add: lipgloss.NewStyle().Foreground(lipgloss.Color("2")).Bold(true), + remove: lipgloss.NewStyle().Foreground(lipgloss.Color("1")).Bold(true), + keep: lipgloss.NewStyle().Bold(true), + cursor: lipgloss.NewStyle().Reverse(true), + detail: lipgloss.NewStyle().Foreground(lipgloss.Color("250")), + } +} + +// New creates a model. original holds the keys that are set now; selected +// holds the desired state and is updated in place. +func New(rows []Row, original, selected map[string]bool, opts Options) *Model { + ti := textinput.New() + ti.Prompt = "/" + ti.Placeholder = "filter" + if opts.KeyHeader == "" { + opts.KeyHeader = "NAME" + } + m := &Model{ + opts: opts, all: rows, original: original, selected: selected, + filter: ti, width: 100, height: 30, styles: newStyles(opts.NoColor), + } + m.refilter() + return m +} + +// Init implements tea.Model. +func (m *Model) Init() tea.Cmd { return nil } + +// Done reports whether the user left the grid. +func (m *Model) Done() bool { return m.done } + +// Action returns the extra action key that ended the grid, or "". +func (m *Model) Action() string { return m.action } + +// Filter returns the current filter text. +func (m *Model) Filter() string { return m.filter.Value() } + +// SetFilter sets the filter text. +func (m *Model) SetFilter(v string) { + m.filter.SetValue(v) + m.offset = 0 + m.refilter() +} + +// VisibleRows returns the rows currently shown (after filtering). +func (m *Model) VisibleRows() []Row { return m.rows } + +// Cursor returns the focused row and column. +func (m *Model) Cursor() (row, col int) { return m.cursorRow, m.cursorCol } + +func (m *Model) matches(r Row, q string) bool { + if q == "" { + return true + } + if strings.Contains(strings.ToLower(r.Label), q) || strings.Contains(strings.ToLower(r.Match), q) { + return true + } + for _, c := range r.Cells { + if c.Key != "" && (strings.Contains(strings.ToLower(c.Key), q) || strings.Contains(strings.ToLower(c.Detail), q)) { + return true + } + } + return false +} + +// refilter rebuilds the visible rows. A header is shown when at least one +// row of its section is visible. +func (m *Model) refilter() { + q := strings.ToLower(strings.TrimSpace(m.filter.Value())) + m.rows = m.rows[:0] + var pending *Row + for i := range m.all { + r := m.all[i] + if r.Kind == RowHeader { + pending = &m.all[i] + continue + } + if !m.matches(r, q) { + continue + } + if pending != nil { + m.rows = append(m.rows, *pending) + pending = nil + } + m.rows = append(m.rows, r) + } + if m.cursorRow >= len(m.rows) { + m.cursorRow = len(m.rows) - 1 + } + if m.cursorRow < 0 { + m.cursorRow = 0 + } + m.skipHeader(1) + m.clampCol() +} + +func (m *Model) cell(r, c int) (Cell, bool) { + if r < 0 || r >= len(m.rows) || m.rows[r].Kind != RowItem { + return Cell{}, false + } + cells := m.rows[r].Cells + if c < 0 || c >= len(cells) || cells[c].Key == "" { + return Cell{}, false + } + return cells[c], true +} + +func (m *Model) skipHeader(dir int) { + if len(m.rows) == 0 || m.rows[m.cursorRow].Kind != RowHeader { + return + } + next := m.cursorRow + dir + if next < 0 || next >= len(m.rows) { + next = m.cursorRow - dir + } + if next >= 0 && next < len(m.rows) { + m.cursorRow = next + } +} + +func (m *Model) clampCol() { + if len(m.rows) == 0 { + m.cursorCol = 0 + return + } + n := len(m.rows[m.cursorRow].Cells) + if m.cursorCol >= n { + m.cursorCol = max(n-1, 0) + } +} + +func (m *Model) toggle(c Cell) { + if c.Locked { + return + } + m.selected[c.Key] = !m.selected[c.Key] +} + +// setAll sets all unlocked cells, or clears them when all are already set. +func (m *Model) setAll(cells []Cell) { + allOn := true + for _, c := range cells { + if !c.Locked && !m.selected[c.Key] { + allOn = false + break + } + } + for _, c := range cells { + if !c.Locked { + m.selected[c.Key] = !allOn + } + } +} + +func (m *Model) rowCells(r int) []Cell { + var out []Cell + for c := range m.opts.Columns { + if cell, ok := m.cell(r, c); ok { + out = append(out, cell) + } + } + return out +} + +func (m *Model) columnCells(c int) []Cell { + var out []Cell + for r := range m.rows { + // Only rows with a cell per column take part in column toggles. + if len(m.rows[r].Cells) < len(m.opts.Columns) { + continue + } + if cell, ok := m.cell(r, c); ok { + out = append(out, cell) + } + } + return out +} + +// ClearRow unsets every unlocked cell of the focused row. +func (m *Model) ClearRow() { + for _, c := range m.rowCells(m.cursorRow) { + if !c.Locked { + m.selected[c.Key] = false + } + } +} + +func (m *Model) pageSize() int { + return max(m.height-chromeLines, 3) +} + +func (m *Model) moveRow(delta int) { + if len(m.rows) == 0 { + return + } + dir := 1 + if delta < 0 { + dir = -1 + } + m.cursorRow = min(max(m.cursorRow+delta, 0), len(m.rows)-1) + m.skipHeader(dir) + m.clampCol() +} + +func (m *Model) ensureVisible() { + page := m.pageSize() + if m.cursorRow < m.offset { + m.offset = m.cursorRow + } + if m.cursorRow >= m.offset+page { + m.offset = m.cursorRow - page + 1 + } + m.offset = max(m.offset, 0) +} + +// Update implements tea.Model. +func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width, m.height = msg.Width, msg.Height + m.ensureVisible() + return m, nil + case tea.KeyMsg: + if m.filtering { + switch msg.String() { + case "enter", "esc", "down", "up": + m.filtering = false + m.filter.Blur() + return m, nil + } + var cmd tea.Cmd + m.filter, cmd = m.filter.Update(msg) + m.offset = 0 + m.refilter() + return m, cmd + } + key := msg.String() + if m.opts.ClearKey != "" && key == m.opts.ClearKey { + m.ClearRow() + return m, nil + } + if _, ok := m.opts.Actions[key]; ok { + m.action = key + m.done = true + return m, tea.Quit + } + switch key { + case "ctrl+c", "esc", "enter", "q": + m.done = true + return m, tea.Quit + case "up", "k": + m.moveRow(-1) + case "down", "j": + m.moveRow(1) + case "pgup": + m.moveRow(-m.pageSize()) + case "pgdown": + m.moveRow(m.pageSize()) + case "home", "g": + m.cursorRow = 0 + m.skipHeader(1) + m.clampCol() + case "end", "G": + m.cursorRow = len(m.rows) - 1 + m.skipHeader(-1) + m.clampCol() + case "left", "h": + if m.cursorCol > 0 { + m.cursorCol-- + } + case "right", "l": + m.cursorCol++ + m.clampCol() + case " ", "x": + if c, ok := m.cell(m.cursorRow, m.cursorCol); ok { + m.toggle(c) + } + case "a": + m.setAll(m.rowCells(m.cursorRow)) + case "c": + if len(m.rows) > 0 && len(m.rows[m.cursorRow].Cells) >= len(m.opts.Columns) { + m.setAll(m.columnCells(m.cursorCol)) + } + case "/": + m.filtering = true + m.filter.Focus() + return m, textinput.Blink + case "ctrl+u": + m.SetFilter("") + } + m.ensureVisible() + } + return m, nil +} + +func (m *Model) mark(c Cell) string { + orig, want := m.original[c.Key], m.selected[c.Key] + switch { + case orig && want: + if c.Locked { + return m.styles.dim.Render("[x]") + } + return m.styles.keep.Render("[x]") + case !orig && want: + return m.styles.add.Render("[+]") + case orig && !want: + return m.styles.remove.Render("[-]") + } + if c.Locked { + return m.styles.dim.Render("[ ]") + } + return "[ ]" +} + +func plainMark(orig, want bool) string { + switch { + case orig && want: + return "[x]" + case !orig && want: + return "[+]" + case orig && !want: + return "[-]" + } + return "[ ]" +} + +func truncate(s string, n int) string { + if n <= 1 { + return "" + } + r := []rune(s) + if len(r) <= n { + return s + } + return string(r[:n-1]) + "…" +} + +func (m *Model) keyWidth() int { + return min(max(m.width-cellWidth*len(m.opts.Columns)-3, 16), 48) +} + +func (m *Model) helpLine() string { + parts := []string{"arrows move", "space toggle", "a row", "c column", "/ filter", "ctrl+u clear filter"} + if m.opts.ClearKey != "" { + parts = append(parts, m.opts.ClearKey+" clear row") + } + for _, k := range m.opts.ActionOrder { + if desc, ok := m.opts.Actions[k]; ok { + parts = append(parts, k+" "+desc) + } + } + parts = append(parts, "enter/esc done") + return strings.Join(parts, " ") +} + +// View implements tea.Model. +func (m *Model) View() string { + var b strings.Builder + kw := m.keyWidth() + + b.WriteString(m.styles.title.Render(m.opts.Title)) + if m.filtering || m.filter.Value() != "" { + b.WriteString(" ") + b.WriteString(m.filter.View()) + } + b.WriteString("\n\n") + + var hdr strings.Builder + _, _ = fmt.Fprintf(&hdr, " %-*s ", kw, m.opts.KeyHeader) + for _, h := range m.opts.Columns { + _, _ = fmt.Fprintf(&hdr, "%-*s", cellWidth, h) + } + b.WriteString(m.styles.header.Render(strings.TrimRight(hdr.String(), " "))) + b.WriteString("\n") + + if len(m.rows) == 0 { + msg := "no matching rows" + if len(m.all) == 0 && m.opts.EmptyText != "" { + msg = m.opts.EmptyText + } + b.WriteString(m.styles.dim.Render(" " + msg)) + b.WriteString("\n") + } + end := min(m.offset+m.pageSize(), len(m.rows)) + for r := m.offset; r < end; r++ { + row := m.rows[r] + if row.Kind == RowHeader { + b.WriteString(m.styles.dim.Render(" " + row.Label)) + b.WriteString("\n") + continue + } + pointer := " " + if r == m.cursorRow { + pointer = "> " + } + label := row.Label + if row.Tag != "" { + label += " " + row.Tag + } + b.WriteString(pointer) + _, _ = fmt.Fprintf(&b, "%-*s ", kw, truncate(label, kw)) + for c := range row.Cells { + var text string + if cell, ok := m.cell(r, c); ok { + text = m.mark(cell) + if r == m.cursorRow && c == m.cursorCol { + text = m.styles.cursor.Render(plainMark(m.original[cell.Key], m.selected[cell.Key])) + } + } else { + text = m.styles.dim.Render(" . ") + if r == m.cursorRow && c == m.cursorCol { + text = m.styles.cursor.Render(" . ") + } + } + b.WriteString(text) + if c < len(row.Cells)-1 { + b.WriteString(strings.Repeat(" ", cellWidth-3)) + } + } + b.WriteString("\n") + } + if len(m.rows) > m.pageSize() { + b.WriteString(m.styles.dim.Render(fmt.Sprintf(" rows %d-%d of %d", m.offset+1, end, len(m.rows)))) + } + b.WriteString("\n") + + if c, ok := m.cell(m.cursorRow, m.cursorCol); ok && c.Detail != "" { + b.WriteString(m.styles.detail.Render(truncate(c.Detail, m.width-1))) + } + b.WriteString("\n") + b.WriteString(m.styles.dim.Render(truncate(m.helpLine(), m.width-1))) + return b.String() +} diff --git a/internal/tui/grid/grid_test.go b/internal/tui/grid/grid_test.go new file mode 100644 index 0000000..f1e8f53 --- /dev/null +++ b/internal/tui/grid/grid_test.go @@ -0,0 +1,80 @@ +package grid + +import ( + "strings" + "testing" + + tea "github.com/charmbracelet/bubbletea" +) + +func runes(s string) tea.KeyMsg { return tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(s)} } + +func testRows() []Row { + return []Row{ + {Label: "Developer", Tag: "group", Cells: []Cell{{Key: "dev|read"}, {Key: "dev|update"}, {Key: "dev|delete", Locked: true}}}, + {Label: "Ops", Cells: []Cell{{Key: "ops|read"}, {}, {Key: "ops|delete"}}}, + {Kind: RowHeader, Label: "-- Other --"}, + {Label: "misc", Match: "extra words", Cells: []Cell{{Key: "misc|x", Detail: "misc detail"}}}, + } +} + +func TestClearKeyAndActions(t *testing.T) { + original := map[string]bool{"dev|read": true, "dev|update": true, "dev|delete": true} + selected := map[string]bool{"dev|read": true, "dev|update": true, "dev|delete": true} + m := New(testRows(), original, selected, Options{ + Columns: []string{"R", "U", "D"}, NoColor: true, ClearKey: "d", + Actions: map[string]string{"n": "add"}, ActionOrder: []string{"n"}, + }) + + m.Update(runes("d")) + if selected["dev|read"] || selected["dev|update"] { + t.Errorf("clear should unset unlocked cells: %v", selected) + } + if !selected["dev|delete"] { + t.Error("clear must keep locked cells") + } + if m.Done() { + t.Error("clear must not end the grid") + } + view := m.View() + if !strings.Contains(view, "Developer group") || !strings.Contains(view, "[-]") || !strings.Contains(view, "d clear row") || !strings.Contains(view, "n add") { + t.Errorf("unexpected view:\n%s", view) + } + + _, cmd := m.Update(runes("n")) + if !m.Done() || m.Action() != "n" || cmd == nil { + t.Errorf("action key should end the grid with action n, got done=%v action=%q", m.Done(), m.Action()) + } +} + +func TestMissingCellsAndColumnToggle(t *testing.T) { + selected := map[string]bool{} + m := New(testRows(), map[string]bool{}, selected, Options{Columns: []string{"R", "U", "D"}, NoColor: true}) + + m.Update(tea.KeyMsg{Type: tea.KeyDown}) + m.Update(tea.KeyMsg{Type: tea.KeyRight}) + m.Update(tea.KeyMsg{Type: tea.KeySpace, Runes: []rune{' '}}) + if len(selected) != 0 { + t.Errorf("a missing cell must not toggle: %v", selected) + } + m.Update(tea.KeyMsg{Type: tea.KeyLeft}) + m.Update(runes("c")) + if !selected["dev|read"] || !selected["ops|read"] || selected["misc|x"] { + t.Errorf("column toggle should cover full rows only: %v", selected) + } +} + +func TestFilterMatchesLabelMatchAndDetail(t *testing.T) { + m := New(testRows(), map[string]bool{}, map[string]bool{}, Options{Columns: []string{"R", "U", "D"}, NoColor: true}) + for _, q := range []string{"extra", "misc detail"} { + m.SetFilter(q) + rows := m.VisibleRows() + if len(rows) != 2 || rows[0].Kind != RowHeader || rows[1].Label != "misc" { + t.Errorf("filter %q: got %+v", q, rows) + } + } + m.SetFilter("ops") + if rows := m.VisibleRows(); len(rows) != 1 || rows[0].Label != "Ops" { + t.Errorf("filter ops: got %+v", rows) + } +} diff --git a/internal/tui/privmatrix/model.go b/internal/tui/privmatrix/model.go index fc58bce..95e03fe 100644 --- a/internal/tui/privmatrix/model.go +++ b/internal/tui/privmatrix/model.go @@ -1,15 +1,14 @@ -// Package privmatrix provides a Bubble Tea model for toggling the privileges -// of one service, laid out as a grid of objects (rows) by actions (columns). +// Package privmatrix adapts one service's privileges to the grid model: +// objects are rows, actions (view, create, update, delete, execute, change +// permission) are columns, and privileges that do not follow the +// "." pattern are listed under "-- Other --". package privmatrix import ( "fmt" - "strings" - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" "github.com/jbrazda/iics-cli/internal/client" + "github.com/jbrazda/iics-cli/internal/tui/grid" ) // Mode restricts which cells can be toggled. @@ -31,506 +30,70 @@ type Options struct { NoColor bool } -// rowKind distinguishes grid rows. -type rowKind int - -const ( - rowObject rowKind = iota - rowHeader - rowOther -) - -type row struct { - kind rowKind - object *client.PrivilegeObject - other *client.Privilege -} +// Model is the grid model used for a service's privileges. +type Model = grid.Model var columnHeaders = []string{"VIEW", "CREATE", "UPDATE", "DELETE", "EXEC", "PERM"} -const ( - cellWidth = 8 - chromeLines = 6 // title, blank, column header, blank, detail, help -) - -// Model is the Bubble Tea model for one service's privilege grid. -type Model struct { - svc client.PrivilegeService - assigned map[string]bool - selected map[string]bool - opts Options - - rows []row - cursorRow int - cursorCol int - offset int - width int - height int - - filter textinput.Model - filtering bool - done bool - - styles styles -} - -type styles struct { - title, header, dim, add, remove, keep, cursor, detail lipgloss.Style -} - -func newStyles(noColor bool) styles { - if noColor { - plain := lipgloss.NewStyle() - return styles{ - title: plain.Bold(true), - header: plain.Bold(true), - dim: plain, - add: plain, - remove: plain, - keep: plain.Bold(true), - cursor: plain.Reverse(true), - detail: plain, - } - } - return styles{ - title: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("6")), - header: lipgloss.NewStyle().Bold(true), - dim: lipgloss.NewStyle().Foreground(lipgloss.Color("244")), - add: lipgloss.NewStyle().Foreground(lipgloss.Color("2")).Bold(true), - remove: lipgloss.NewStyle().Foreground(lipgloss.Color("1")).Bold(true), - keep: lipgloss.NewStyle().Bold(true), - cursor: lipgloss.NewStyle().Reverse(true), - detail: lipgloss.NewStyle().Foreground(lipgloss.Color("250")), - } -} - -// New creates a model. assigned holds the privilege names the role has now; -// selected holds the desired state and is updated in place. +// New creates a model for one service. assigned holds the privilege names +// the role has now; selected holds the desired state and is updated in place. func New(svc client.PrivilegeService, assigned, selected map[string]bool, opts Options) *Model { - ti := textinput.New() - ti.Prompt = "/" - ti.Placeholder = "filter" - m := &Model{ - svc: svc, - assigned: assigned, - selected: selected, - opts: opts, - filter: ti, - width: 100, - height: 30, - styles: newStyles(opts.NoColor), - } - m.rebuildRows() - return m -} - -// Init implements tea.Model. -func (m *Model) Init() tea.Cmd { return nil } - -// Done reports whether the user left the grid. -func (m *Model) Done() bool { return m.done } - -// visible reports whether a privilege is shown in the current mode. -func (m *Model) visible(p client.Privilege) bool { - return m.opts.Mode != ModeRemove || m.assigned[p.Name] -} - -// locked reports whether a privilege cannot be toggled in the current mode. -func (m *Model) locked(p client.Privilege) bool { - switch m.opts.Mode { - case ModeAdd: - return m.assigned[p.Name] - case ModeRemove: - return !m.assigned[p.Name] - } - return false -} - -func (m *Model) matches(p client.Privilege, q string) bool { - return strings.Contains(strings.ToLower(p.Name), q) || strings.Contains(strings.ToLower(p.Description), q) -} - -func (m *Model) rebuildRows() { - q := strings.ToLower(strings.TrimSpace(m.filter.Value())) - m.rows = m.rows[:0] - for i := range m.svc.Objects { - obj := &m.svc.Objects[i] - show := false - for _, p := range obj.Actions { - if m.visible(p) && (q == "" || strings.Contains(strings.ToLower(obj.Key), q) || m.matches(p, q)) { - show = true - break + visible := func(p client.Privilege) bool { + return opts.Mode != ModeRemove || assigned[p.Name] + } + locked := func(p client.Privilege) bool { + switch opts.Mode { + case ModeAdd: + return assigned[p.Name] + case ModeRemove: + return !assigned[p.Name] + } + return false + } + cellFor := func(p client.Privilege) grid.Cell { + return grid.Cell{Key: p.Name, Locked: locked(p), Detail: detail(p)} + } + + var rows []grid.Row + for _, obj := range svc.Objects { + cells := make([]grid.Cell, len(client.PrivilegeActions)) + hasCell := false + for i, a := range client.PrivilegeActions { + if p, ok := obj.Actions[a]; ok && visible(p) { + cells[i] = cellFor(p) + hasCell = true } } - if show { - m.rows = append(m.rows, row{kind: rowObject, object: obj}) + if hasCell { + rows = append(rows, grid.Row{Label: obj.Key, Cells: cells}) } } - var others []row - for i := range m.svc.Other { - p := &m.svc.Other[i] - if m.visible(*p) && (q == "" || m.matches(*p, q)) { - others = append(others, row{kind: rowOther, other: p}) + var others []grid.Row + for _, p := range svc.Other { + if visible(p) { + others = append(others, grid.Row{Label: p.Name, Cells: []grid.Cell{cellFor(p)}}) } } if len(others) > 0 { - m.rows = append(m.rows, row{kind: rowHeader}) - m.rows = append(m.rows, others...) - } - if m.cursorRow >= len(m.rows) { - m.cursorRow = len(m.rows) - 1 - } - if m.cursorRow < 0 { - m.cursorRow = 0 - } - m.skipHeader(1) - m.clampCol() -} - -// cell returns the privilege at a grid position, if any. -func (m *Model) cell(r, c int) (client.Privilege, bool) { - if r < 0 || r >= len(m.rows) { - return client.Privilege{}, false - } - switch m.rows[r].kind { - case rowObject: - if c < 0 || c >= len(client.PrivilegeActions) { - return client.Privilege{}, false - } - p, ok := m.rows[r].object.Actions[client.PrivilegeActions[c]] - if !ok || !m.visible(p) { - return client.Privilege{}, false - } - return p, true - case rowOther: - if c != 0 { - return client.Privilege{}, false - } - return *m.rows[r].other, true - } - return client.Privilege{}, false -} - -func (m *Model) skipHeader(dir int) { - if len(m.rows) == 0 { - return - } - if m.rows[m.cursorRow].kind == rowHeader { - next := m.cursorRow + dir - if next < 0 || next >= len(m.rows) { - next = m.cursorRow - dir - } - if next >= 0 && next < len(m.rows) { - m.cursorRow = next - } - } -} - -func (m *Model) clampCol() { - if len(m.rows) == 0 { - m.cursorCol = 0 - return - } - if m.rows[m.cursorRow].kind == rowOther { - m.cursorCol = 0 - return - } - if m.cursorCol >= len(client.PrivilegeActions) { - m.cursorCol = len(client.PrivilegeActions) - 1 - } -} - -func (m *Model) toggle(p client.Privilege) { - if m.locked(p) { - return - } - m.selected[p.Name] = !m.selected[p.Name] -} - -// setAll selects all toggleable privileges in ps, or clears them when every -// toggleable one is already selected. -func (m *Model) setAll(ps []client.Privilege) { - allOn := true - for _, p := range ps { - if !m.locked(p) && !m.selected[p.Name] { - allOn = false - break - } - } - for _, p := range ps { - if !m.locked(p) { - m.selected[p.Name] = !allOn - } - } -} - -func (m *Model) rowPrivileges(r int) []client.Privilege { - var ps []client.Privilege - for c := range client.PrivilegeActions { - if p, ok := m.cell(r, c); ok { - ps = append(ps, p) - } - } - if len(ps) == 0 { - if p, ok := m.cell(r, 0); ok { - ps = append(ps, p) - } - } - return ps -} - -func (m *Model) columnPrivileges(c int) []client.Privilege { - var ps []client.Privilege - for r, rw := range m.rows { - if rw.kind != rowObject { - continue - } - if p, ok := m.cell(r, c); ok { - ps = append(ps, p) - } - } - return ps -} - -func (m *Model) pageSize() int { - n := m.height - chromeLines - if n < 3 { - n = 3 - } - return n -} - -func (m *Model) moveRow(delta int) { - if len(m.rows) == 0 { - return - } - dir := 1 - if delta < 0 { - dir = -1 + rows = append(rows, grid.Row{Kind: grid.RowHeader, Label: "-- Other --"}) + rows = append(rows, others...) } - m.cursorRow += delta - if m.cursorRow < 0 { - m.cursorRow = 0 - } - if m.cursorRow >= len(m.rows) { - m.cursorRow = len(m.rows) - 1 - } - m.skipHeader(dir) - m.clampCol() -} - -func (m *Model) ensureVisible() { - page := m.pageSize() - if m.cursorRow < m.offset { - m.offset = m.cursorRow - } - if m.cursorRow >= m.offset+page { - m.offset = m.cursorRow - page + 1 - } - if m.offset < 0 { - m.offset = 0 - } -} -// Update implements tea.Model. -func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.WindowSizeMsg: - m.width, m.height = msg.Width, msg.Height - m.ensureVisible() - return m, nil - case tea.KeyMsg: - if m.filtering { - switch msg.String() { - case "enter", "esc", "down", "up": - m.filtering = false - m.filter.Blur() - return m, nil - } - var cmd tea.Cmd - m.filter, cmd = m.filter.Update(msg) - m.offset = 0 - m.rebuildRows() - return m, cmd - } - switch msg.String() { - case "ctrl+c", "esc", "enter", "q": - m.done = true - return m, tea.Quit - case "up", "k": - m.moveRow(-1) - case "down", "j": - m.moveRow(1) - case "pgup": - m.moveRow(-m.pageSize()) - case "pgdown": - m.moveRow(m.pageSize()) - case "home", "g": - m.cursorRow = 0 - m.skipHeader(1) - m.clampCol() - case "end", "G": - m.cursorRow = len(m.rows) - 1 - m.skipHeader(-1) - m.clampCol() - case "left", "h": - if m.cursorCol > 0 { - m.cursorCol-- - } - case "right", "l": - m.cursorCol++ - m.clampCol() - case " ", "x": - if p, ok := m.cell(m.cursorRow, m.cursorCol); ok { - m.toggle(p) - } - case "a": - m.setAll(m.rowPrivileges(m.cursorRow)) - case "c": - if len(m.rows) > 0 && m.rows[m.cursorRow].kind == rowObject { - m.setAll(m.columnPrivileges(m.cursorCol)) - } - case "/": - m.filtering = true - m.filter.Focus() - return m, textinput.Blink - case "ctrl+u": - m.filter.SetValue("") - m.rebuildRows() - } - m.ensureVisible() - } - return m, nil + return grid.New(rows, assigned, selected, grid.Options{ + Title: fmt.Sprintf("%s > %s (%d)", opts.Title, svc.Name, svc.Count()), + Columns: columnHeaders, + KeyHeader: "OBJECT", + NoColor: opts.NoColor, + }) } -func (m *Model) mark(p client.Privilege) string { - orig, want := m.assigned[p.Name], m.selected[p.Name] - switch { - case orig && want: - if m.locked(p) { - return m.styles.dim.Render("[x]") - } - return m.styles.keep.Render("[x]") - case !orig && want: - return m.styles.add.Render("[+]") - case orig && !want: - return m.styles.remove.Render("[-]") +func detail(p client.Privilege) string { + d := p.Name + if p.Description != "" { + d += " - " + p.Description } - if m.locked(p) { - return m.styles.dim.Render("[ ]") - } - return "[ ]" -} - -func truncate(s string, n int) string { - if n <= 1 { - return "" - } - r := []rune(s) - if len(r) <= n { - return s - } - return string(r[:n-1]) + "…" -} - -func (m *Model) keyWidth() int { - w := m.width - cellWidth*len(columnHeaders) - 3 - if w > 48 { - w = 48 - } - if w < 16 { - w = 16 - } - return w -} - -// View implements tea.Model. -func (m *Model) View() string { - var b strings.Builder - kw := m.keyWidth() - - title := fmt.Sprintf("%s > %s (%d)", m.opts.Title, m.svc.Name, m.svc.Count()) - b.WriteString(m.styles.title.Render(title)) - if m.filtering || m.filter.Value() != "" { - b.WriteString(" " + m.filter.View()) - } - b.WriteString("\n\n") - - hdr := fmt.Sprintf(" %-*s ", kw, "OBJECT") - for _, h := range columnHeaders { - hdr += fmt.Sprintf("%-*s", cellWidth, h) - } - b.WriteString(m.styles.header.Render(strings.TrimRight(hdr, " ")) + "\n") - - if len(m.rows) == 0 { - b.WriteString(m.styles.dim.Render(" no matching privileges") + "\n") - } - end := m.offset + m.pageSize() - if end > len(m.rows) { - end = len(m.rows) - } - for r := m.offset; r < end; r++ { - rw := m.rows[r] - pointer := " " - if r == m.cursorRow { - pointer = "> " - } - switch rw.kind { - case rowHeader: - b.WriteString(m.styles.dim.Render(" -- Other --") + "\n") - case rowObject: - b.WriteString(pointer + fmt.Sprintf("%-*s ", kw, truncate(rw.object.Key, kw))) - for c := range client.PrivilegeActions { - cellText := " . " - if p, ok := m.cell(r, c); ok { - cellText = m.mark(p) - } else { - cellText = m.styles.dim.Render(cellText) - } - if r == m.cursorRow && c == m.cursorCol { - cellText = m.styles.cursor.Render(stripStyle(cellText)) - } - b.WriteString(cellText + strings.Repeat(" ", cellWidth-3)) - } - b.WriteString("\n") - case rowOther: - cellText := m.mark(*rw.other) - if r == m.cursorRow { - cellText = m.styles.cursor.Render(stripStyle(cellText)) - } - b.WriteString(pointer + fmt.Sprintf("%-*s ", kw, truncate(rw.other.Name, kw)) + cellText + "\n") - } - } - if len(m.rows) > m.pageSize() { - b.WriteString(m.styles.dim.Render(fmt.Sprintf(" rows %d-%d of %d", m.offset+1, end, len(m.rows))) + "\n") - } else { - b.WriteString("\n") - } - - if p, ok := m.cell(m.cursorRow, m.cursorCol); ok { - detail := p.Name - if p.Description != "" { - detail += " - " + p.Description - } - if p.Status != "" { - detail += " (" + p.Status + ")" - } - b.WriteString(m.styles.detail.Render(truncate(detail, m.width-1)) + "\n") - } else { - b.WriteString("\n") - } - help := "arrows move space toggle a row c column / filter ctrl+u clear filter enter/esc back" - b.WriteString(m.styles.dim.Render(truncate(help, m.width-1))) - return b.String() -} - -// stripStyle returns the three-character mark without ANSI styling so the -// cursor style can be applied cleanly. -func stripStyle(s string) string { - for _, mk := range []string{"[x]", "[+]", "[-]", "[ ]", " . "} { - if strings.Contains(s, mk) { - return mk - } + if p.Status != "" { + d += " (" + p.Status + ")" } - return s + return d } diff --git a/internal/tui/privmatrix/model_test.go b/internal/tui/privmatrix/model_test.go index 0082775..d00ce9e 100644 --- a/internal/tui/privmatrix/model_test.go +++ b/internal/tui/privmatrix/model_test.go @@ -6,6 +6,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/jbrazda/iics-cli/internal/client" + "github.com/jbrazda/iics-cli/internal/tui/grid" ) func testService() client.PrivilegeService { @@ -64,6 +65,9 @@ func TestToggleCell(t *testing.T) { if !strings.Contains(view, "[+]") || !strings.Contains(view, "[-]") { t.Errorf("view should show pending add and remove marks:\n%s", view) } + if !strings.Contains(view, "R > DI (6)") { + t.Errorf("title should name the service and count:\n%s", view) + } } func TestRowAndColumnToggle(t *testing.T) { @@ -85,6 +89,9 @@ func TestRowAndColumnToggle(t *testing.T) { if !selected["view.data.transfer.task"] || !selected["view.mapping"] { t.Errorf("column toggle should select all views: %v", selected) } + if selected["edc.iics.discovery"] { + t.Error("column toggle must not touch Other rows") + } } func TestOtherRowsSkipHeader(t *testing.T) { @@ -92,11 +99,13 @@ func TestOtherRowsSkipHeader(t *testing.T) { m := New(testService(), map[string]bool{}, selected, Options{NoColor: true}) press(m, "right", "right", "down", "down") - if m.rows[m.cursorRow].kind != rowOther { - t.Fatalf("cursor should skip the header and land on an Other row, got kind %v", m.rows[m.cursorRow].kind) + row, col := m.Cursor() + rows := m.VisibleRows() + if rows[row].Kind != grid.RowItem || rows[row].Label != "edc.iics.discovery" { + t.Fatalf("cursor should skip the header and land on the Other row, got %+v", rows[row]) } - if m.cursorCol != 0 { - t.Errorf("Other rows have one column, cursorCol=%d", m.cursorCol) + if col != 0 { + t.Errorf("Other rows have one column, cursor col=%d", col) } press(m, "space") if !selected["edc.iics.discovery"] { @@ -107,19 +116,19 @@ func TestOtherRowsSkipHeader(t *testing.T) { func TestFilter(t *testing.T) { m := New(testService(), map[string]bool{}, map[string]bool{}, Options{NoColor: true}) press(m, "/", "m", "a", "p", "enter") - if len(m.rows) != 1 || m.rows[0].object.Key != "mapping" { - t.Fatalf("filter should leave only mapping, got %d rows", len(m.rows)) + rows := m.VisibleRows() + if len(rows) != 1 || rows[0].Label != "mapping" { + t.Fatalf("filter should leave only mapping, got %+v", rows) } press(m, "/", "e", "d", "c") - if len(m.rows) != 0 { - // "mapedc" matches nothing - t.Fatalf("expected no rows, got %d", len(m.rows)) + if len(m.VisibleRows()) != 0 { + t.Fatalf("expected no rows, got %+v", m.VisibleRows()) } press(m, "esc", "ctrl+u") - m.filter.SetValue("discovery") - m.rebuildRows() - if len(m.rows) != 2 || m.rows[1].other.Name != "edc.iics.discovery" { - t.Errorf("description filter should match the Other row, got %+v", m.rows) + m.SetFilter("discovery") + rows = m.VisibleRows() + if len(rows) != 2 || rows[0].Kind != grid.RowHeader || rows[1].Label != "edc.iics.discovery" { + t.Errorf("description filter should match the Other row with its header, got %+v", rows) } } @@ -141,8 +150,9 @@ func TestRemoveModeShowsOnlyAssigned(t *testing.T) { assigned := map[string]bool{"update.mapping": true} selected := map[string]bool{"update.mapping": true} m := New(testService(), assigned, selected, Options{Mode: ModeRemove, NoColor: true}) - if len(m.rows) != 1 || m.rows[0].object.Key != "mapping" { - t.Fatalf("remove mode should show only objects with assigned privileges, got %d rows", len(m.rows)) + rows := m.VisibleRows() + if len(rows) != 1 || rows[0].Label != "mapping" { + t.Fatalf("remove mode should show only objects with assigned privileges, got %+v", rows) } press(m, "a") if selected["update.mapping"] { diff --git a/internal/tui/prompt.go b/internal/tui/prompt.go index 596aa04..e184de3 100644 --- a/internal/tui/prompt.go +++ b/internal/tui/prompt.go @@ -136,7 +136,7 @@ func (p *Prompter) Select(label string, options []string) (int, error) { } sel := huh.NewSelect[int](). Title(label). - Description("/ to filter, esc to cancel"). + Description("/ to filter, ctrl+c to cancel"). Options(opts...). Height(listHeight(len(options)) + 1). Value(&idx) From 76a8123a4cc961c9c655ef8063cd0d406a4a1be6 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Wed, 30 Sep 2026 00:11:31 -0400 Subject: [PATCH 13/52] feat(menu): interactive main menu for bare iics (CR-0044) - bare "iics" opens a main menu when stdin and stdout are terminals; otherwise prints help as before; opt out with IICS_NO_MENU=1, config ui.menu: false, or CI; "iics menu" (alias tui) opens it explicitly - entries run the real command as a subprocess (terminal passed through, --profile and explicitly set global flags appended), print the command line, show a done/failed status, pause, and return to the last entry; Ctrl+C cancels only the running command - header with profile, org, user and session time from the session cache; session-only profile switch (p) and "Set as default profile" - production profiles (config production: true, or prd/prod name hint) get a red marker and a confirmation before entries that change data; profile add/edit ask for the production flag - first run without profiles starts profile add; accessible mode and small terminals use a numbered line menu - internal/menu with tests (open decision, entries, model, loop with fake runner); config production/UI settings with tests - docs: menu.md, README, interactive.md, profile.md, completions --- README.md | 10 + cmd/menu.go | 163 +++++++ cmd/root.go | 2 + completions/iics.bash | 44 ++ .../pending/CR-0044-interactive-main-menu.md | 138 ++++++ docs/documentation/interactive.md | 3 + docs/documentation/menu.md | 132 ++++++ docs/documentation/profile.md | 16 + internal/config/config.go | 26 ++ internal/config/production_test.go | 41 ++ internal/config/prompt.go | 18 + internal/config/prompt_test.go | 45 +- internal/config/session.go | 6 + internal/menu/entries.go | 105 +++++ internal/menu/loop.go | 315 +++++++++++++ internal/menu/menu_test.go | 328 ++++++++++++++ internal/menu/model.go | 414 ++++++++++++++++++ internal/menu/open.go | 38 ++ 18 files changed, 1834 insertions(+), 10 deletions(-) create mode 100644 cmd/menu.go create mode 100644 docs/ChangeRequests/pending/CR-0044-interactive-main-menu.md create mode 100644 docs/documentation/menu.md create mode 100644 internal/config/production_test.go create mode 100644 internal/menu/entries.go create mode 100644 internal/menu/loop.go create mode 100644 internal/menu/menu_test.go create mode 100644 internal/menu/model.go create mode 100644 internal/menu/open.go diff --git a/README.md b/README.md index 41055dd..2b685cf 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,11 @@ make build ## Quick Start +Run `iics` without arguments in a terminal to open the interactive +[main menu](docs/documentation/menu.md): pick a task (edit a role, create a +user, change permissions, ...), and the menu runs the matching command and +returns. Scripts and pipes still get the plain CLI. + ### 1. Set up a profile ```bash @@ -111,6 +116,8 @@ iics logout ```yaml defaultProfile: dev httpTimeout: 120 # seconds; per-HTTP-request timeout (default 120) +ui: + menu: true # false = bare "iics" prints help instead of opening the menu style: theme: default # default | minimal | compact | plain | markdown | gh noColor: false # true = disable color permanently (same as --no-color) @@ -127,6 +134,7 @@ profiles: caiUrl: "https://use4-cai.dm-us.informaticacloud.com" prod: name: "Production Org" + production: true # menu marks it and asks before changes region: "EMEA" username: "admin@company.com" password: "@keyring" # real password stored in OS keychain @@ -194,6 +202,7 @@ env > `80`. | `IICS_THEME` | Override table theme (same values as `--theme` flag) | | `IICS_WIDTH` | Override auto-detected terminal width for responsive table output | | `IICS_ACCESSIBLE` | Set to `1` for screen-reader friendly line-by-line prompts ([interactive prompts](docs/documentation/interactive.md)) | +| `IICS_NO_MENU` | Set to `1` so bare `iics` prints help instead of opening the [menu](docs/documentation/menu.md) | | `IICS_HTTP_TIMEOUT` | Override per-HTTP-request timeout in seconds (default `120`; `--http-timeout` flag wins if set) | | `IICS_VALID_DEPLOY_TARGETS` | Override valid target allowlist for `iics release` commands (comma-separated) | | `IICS_TARGET_PROFILE_MAP` | Override target to profile mapping for `iics release plan` (format `TARGET=profile,...`) | @@ -264,6 +273,7 @@ it easy to override credentials in CI pipelines without touching the config file | [metering](docs/documentation/metering.md) | | `get`, `download` | Query usage and metering data | | [objects](docs/documentation/objects.md) | | `list`, `dependencies` | List/search assets, find dependencies | | [package](docs/documentation/package.md) | | `expand`, `create`, `dependencies` | Extract, assemble, or inspect dependencies of IICS export packages | +| [menu](docs/documentation/menu.md) | `tui` | - | Interactive main menu (bare `iics` in a terminal) | | [permission](docs/documentation/permission.md) | `perm` | `get`, `add`, `update`, `delete`, `set`, `check`, `edit` | Manage object-level permissions | | [privilege](docs/documentation/privilege.md) | | `list` | List available privileges | | [profile](docs/documentation/profile.md) | | `add`, `edit`, `list`, `delete`, `set-default`, `set-password`, `show` | Manage connection profiles | diff --git a/cmd/menu.go b/cmd/menu.go new file mode 100644 index 0000000..f966f24 --- /dev/null +++ b/cmd/menu.go @@ -0,0 +1,163 @@ +package cmd + +import ( + "fmt" + "os" + "path/filepath" + "sort" + + "github.com/jbrazda/iics-cli/internal/config" + "github.com/jbrazda/iics-cli/internal/menu" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "golang.org/x/term" +) + +func newMenuCmd() *cobra.Command { + return &cobra.Command{ + Use: "menu", + Aliases: []string{"tui"}, + Short: "Open the interactive main menu", + Long: `Open the interactive main menu: pick an entry to run the matching command, +then return to the menu. Running "iics" without arguments in a terminal opens +the same menu (disable with IICS_NO_MENU=1 or "ui: {menu: false}" in the +config file). Requires a terminal.`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if !isInteractiveTTY() { + return fmt.Errorf("the menu requires a terminal; run 'iics --help' for commands") + } + return runMenu(cmd) + }, + } +} + +// rootRun opens the menu for bare "iics" on a terminal, otherwise prints help. +func rootRun(cmd *cobra.Command, args []string) error { + cfg, _ := loadConfig() + var cfgMenu *bool + if cfg != nil { + cfgMenu = cfg.UI.Menu + } + open := menu.ShouldOpen(menu.OpenInput{ + StdinTTY: term.IsTerminal(int(os.Stdin.Fd())), + StdoutTTY: term.IsTerminal(int(os.Stdout.Fd())), + Env: os.Getenv, + ConfigMenu: cfgMenu, + }) + if len(args) > 0 || !open { + return cmd.Help() + } + return runMenu(cmd) +} + +func runMenu(cmd *cobra.Command) error { + binary, err := os.Executable() + if err != nil { + return fmt.Errorf("locating the iics binary: %w", err) + } + display := filepath.Base(os.Args[0]) + passthrough := globalFlagArgs(cmd.Root()) + + _, _, start, _ := resolveProfile() + + line := prompter.Accessible + if w, h, serr := term.GetSize(int(os.Stdout.Fd())); serr == nil && (w < 60 || h < 15) { + line = true + } + + return menu.Run(menu.Deps{ + Runner: passthroughRunner{inner: menu.ExecRunner{Binary: binary}, extra: passthrough}, + Prompter: prompter, + Out: os.Stderr, + Binary: display, + NoColor: editorNoColor(), + Line: line, + LoadState: loadMenuState, + SetDefault: setDefaultProfile, + ShowMenu: menu.DefaultShowMenu, + Pause: menu.DefaultPause(os.Stderr), + ClearScreen: menu.DefaultClearScreen(os.Stderr), + }, start) +} + +// passthroughRunner appends the global flags the menu was started with. +type passthroughRunner struct { + inner menu.Runner + extra []string +} + +func (r passthroughRunner) Run(args []string) int { + return r.inner.Run(append(append([]string(nil), args...), r.extra...)) +} + +// globalFlagArgs returns explicitly set persistent flags (except --profile, +// which the menu manages, and --output) as "--name=value" arguments. +func globalFlagArgs(root *cobra.Command) []string { + var out []string + root.PersistentFlags().Visit(func(f *pflag.Flag) { + switch f.Name { + case "profile", "output", "help": + return + } + out = append(out, fmt.Sprintf("--%s=%s", f.Name, f.Value.String())) + }) + return out +} + +func loadMenuState(profileName string) (menu.State, error) { + var st menu.State + cfg, err := loadConfig() + if err != nil { + return st, err + } + if profileName == "" { + profileName = cfg.DefaultProfile + } + names := make([]string, 0, len(cfg.Profiles)) + for n := range cfg.Profiles { + names = append(names, n) + } + sort.Strings(names) + for _, n := range names { + p := cfg.Profiles[n] + st.Profiles = append(st.Profiles, menu.ProfileInfo{ + Name: n, + Region: p.Region, + Production: config.IsProductionProfile(n, p), + Default: n == cfg.DefaultProfile, + }) + } + + p := cfg.Profiles[profileName] + st.HasProfile = p != nil || os.Getenv("IICS_USERNAME") != "" + st.Header = menu.Header{ + Profile: profileName, + Production: config.IsProductionProfile(profileName, p), + Session: "not logged in - signs in on first command", + } + if cache, cerr := config.LoadSessionCache(""); cerr == nil && cache != nil { + if e := cache.Sessions[profileName]; e != nil { + st.Header.Org = e.OrgName + st.Header.User = e.UserName + if rem := e.Remaining(); rem > 0 { + st.Header.Session = fmt.Sprintf("session ok (%d min left)", int(rem.Minutes())+1) + } else { + st.Header.Session = "session expired - signs in on first command" + } + } + } + return st, nil +} + +func setDefaultProfile(name string) error { + cfg, err := loadConfig() + if err != nil { + return err + } + if _, ok := cfg.Profiles[name]; !ok { + return fmt.Errorf("profile %q is not in the config file", name) + } + cfg.DefaultProfile = name + return cfg.Save(cfgFile) +} diff --git a/cmd/root.go b/cmd/root.go index 72d701d..31116de 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -219,6 +219,8 @@ func init() { rootCmd.AddCommand(newAgentCmd()) rootCmd.AddCommand(newTagCmd()) rootCmd.AddCommand(newPermissionCmd()) + rootCmd.AddCommand(newMenuCmd()) + rootCmd.RunE = rootRun rootCmd.AddCommand(newSecuritylogCmd()) rootCmd.AddCommand(newMeteringCmd()) rootCmd.AddCommand(newSourcecontrolCmd()) diff --git a/completions/iics.bash b/completions/iics.bash index 0193912..949ff3a 100644 --- a/completions/iics.bash +++ b/completions/iics.bash @@ -3405,6 +3405,45 @@ _iics_lookup() noun_aliases=() } +_iics_menu() +{ + last_command="iics_menu" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--http-timeout=") + two_word_flags+=("--http-timeout") + flags+=("--no-color") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + flags+=("--profile=") + two_word_flags+=("--profile") + two_word_flags+=("-p") + flags+=("--theme=") + two_word_flags+=("--theme") + flags+=("--verbose") + flags+=("-v") + flags+=("--wide") + flags+=("--width=") + two_word_flags+=("--width") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _iics_metering_download() { last_command="iics_metering_download" @@ -7588,6 +7627,11 @@ _iics_root_command() commands+=("login") commands+=("logout") commands+=("lookup") + commands+=("menu") + if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then + command_aliases+=("tui") + aliashash["tui"]="menu" + fi commands+=("metering") commands+=("objects") commands+=("package") diff --git a/docs/ChangeRequests/pending/CR-0044-interactive-main-menu.md b/docs/ChangeRequests/pending/CR-0044-interactive-main-menu.md new file mode 100644 index 0000000..b905bc7 --- /dev/null +++ b/docs/ChangeRequests/pending/CR-0044-interactive-main-menu.md @@ -0,0 +1,138 @@ +# CR-0044: Interactive main menu + +## CR Type + +- [x] New command +- [x] Enhancement to existing command + +## Problem + +Many commands now have interactive modes (role, user, group, permission, +environment and agent wizards and editors), but users must know each command +name and flag to reach them. Running `iics` without arguments only prints the +help text. + +## Proposed Solution + +A hybrid CLI main menu: interactive when a person runs `iics` in a terminal, +unchanged for scripts. + +### When the menu opens + +- `iics` with no arguments opens the menu when stdin **and** stdout are + terminals. Otherwise the help text is printed as today. +- `iics menu` (alias `iics tui`) always opens it (terminal required). +- `iics --help` / `iics help` always print help. +- Opt-out: `IICS_NO_MENU=1`, config `ui.menu: false`, or `CI` set in the + environment - bare `iics` then prints help. + +### Running commands + +- Each entry runs the real command as a subprocess of the same binary with + the terminal passed through and `--profile ` appended. No flag or + global state leaks between runs. +- The equivalent command line is printed (dimmed) before it runs, e.g. + `$ iics role edit --profile dev`. +- Output stays on the normal screen. After the command exits, a status line + (`✓ done` or `✗ failed (exit N)`) is shown; `Enter` returns to the menu + (screen cleared), `q` quits. +- `Ctrl+C` while a command runs cancels that command only (the menu ignores + the signal while the child runs). In the menu, `Ctrl+C` or `q` quits with + exit code 0. +- The cursor stays on the last entry for the session (not persisted). + +### Profiles and production safety + +- The active profile starts from the normal resolution order (`--profile`, + `IICS_PROFILE`, config default). +- Switching profile (`p` or the Session entry) applies to the menu session; + a separate "Set as default profile" entry writes `defaultProfile`. +- Header (from the local session cache, no network call): profile, org, + user, session status ("session ok (12 min left)" or "not logged in - signs + in on first command"), and a red `PRODUCTION` marker. +- Production detection: per-profile config `production: true|false`; when + unset, names `prd`, `prod`, `production` (or starting with `prod-` / `prd-`) + are treated as production. `profile add` / `profile edit` ask for the flag. +- On a production profile, entries that change data ask for confirmation + (default No) before running. Read-only entries never ask. +- With no profiles configured, `profile add` runs first; canceling it opens + the menu with only the Session group usable. + +### Menu content + +A curated list of entries (group, label, description, arguments, writes +flag), grouped by resource: + +| Group | Entries | +| ----- | ------- | +| Session | Switch profile, Set as default profile, Login, List profiles, Add profile ✎, Edit active profile ✎ | +| Users | List, Create ✎, Edit groups and roles ✎, Delete ✎, Change password ✎ | +| User groups | List, Show, Create ✎, Edit roles ✎, Delete ✎ | +| Roles | List, Show with privileges, Create ✎, Edit privileges ✎ | +| Privileges | List | +| Permissions | Edit object permissions ✎ | +| Environments and agents | List environments, Create environment ✎, List agents, Start / Stop / Restart agent service ✎, Download agent installer | +| Help | Show CLI help | + +✎ = changes data (production confirmation). + +### Look and fallback + +- Full-screen Bubble Tea menu: header bar, section headers, `/` filter + across groups, description and equivalent command for the focused entry, + `p` switch profile, `?` key help, `q` quit. +- `IICS_ACCESSIBLE=1` or a terminal smaller than 60x15: a numbered, + line-based menu with the same entries and the same run/return loop. + +## Implementation + +- `internal/config`: `Profile.Production *bool`, `Config.UI.Menu *bool`, + `IsProductionProfile`, `SessionEntry.Remaining`; production question in the + profile prompt. +- `internal/menu`: entries, menu model, `ShouldOpen` decision, `Runner` + interface with an exec-based implementation, the run/return loop and the + line-based fallback. +- `cmd/menu.go`: `menu` command and root `RunE` (bare `iics`). +- Tests: `ShouldOpen`, production detection, session remaining time, menu + model (navigation, filter, sections, selection, profile switch action), + loop with a fake runner (production confirmation, status, return). +- Docs: `docs/documentation/menu.md`, README quick start and commands table, + link from `interactive.md`, `profile.md` production flag; + `make completions`. + +## Acceptance Criteria + +- [x] Bare `iics` on a terminal opens the menu; piped or `CI`/opt-out prints help +- [x] `iics menu` / `iics tui` open the menu +- [x] Entries run as subprocesses, show the command line, pause with status, + and return to the menu +- [x] Ctrl+C cancels only the running command +- [x] Profile switch (session) and set-default work; header shows session info +- [x] Production profiles are marked and write entries ask for confirmation +- [x] First run without profiles starts `profile add` +- [x] Accessible / small-terminal line menu works +- [x] `go build`, `go vet`, `golangci-lint`, `go test ./...` pass + +## Implementation Notes + +- `internal/menu/entries.go` - curated `DefaultEntries`, `CommandArgs`, + `CommandLine` (shell-quoted display). +- `internal/menu/open.go` - `ShouldOpen` (TTY on stdin and stdout, `CI`, + `IICS_NO_MENU`, `ui.menu`). +- `internal/menu/model.go` - full-screen menu (header, sections, filter, + disabled entries without a profile, key help, equivalent command). +- `internal/menu/loop.go` - `Run` loop with `Deps` (runner, prompter, state + loader, set-default, show-menu, pause, clear-screen) so it is testable; + `ExecRunner` ignores SIGINT in the menu while the child runs; line-based + menu for accessible mode and small terminals. +- `cmd/menu.go` - `menu` / `tui` command, root `RunE`, state from config and + session cache, global flag pass-through (all explicitly set persistent + flags except `--profile` and `--output`). +- `internal/config` - `Profile.Production`, `UIConfig.Menu`, + `IsProductionProfile`, `SessionEntry.Remaining`, production question in the + profile prompt. +- Verified on `dev` through a pty: menu opens on bare `iics`, filter, run + with command line / status / pause / return to the last entry, production + header and declined confirmation on `prd`, profile picker, Ctrl+C in a + running command returns to the menu, accessible line menu. Non-TTY `iics` + still prints help; unknown commands still fail. diff --git a/docs/documentation/interactive.md b/docs/documentation/interactive.md index 30b7e5c..d952834 100644 --- a/docs/documentation/interactive.md +++ b/docs/documentation/interactive.md @@ -1,5 +1,8 @@ # Interactive prompts +> Looking for a starting point? Run `iics` without arguments to open the +> [main menu](menu.md), which lists the interactive commands. + Commands that run interactively (wizards such as `user create --interactive`, `user edit`, `group create -i`, `environment create`, and pickers shown when `--id` or diff --git a/docs/documentation/menu.md b/docs/documentation/menu.md new file mode 100644 index 0000000..d37633f --- /dev/null +++ b/docs/documentation/menu.md @@ -0,0 +1,132 @@ +# menu + +Interactive main menu. Alias: `tui`. + +Run `iics` without arguments in a terminal (or `iics menu`) to browse the +interactive commands, pick one, and return to the menu when it finishes. Every +menu entry runs a real command line, which is shown before it runs, so the +menu also teaches the scriptable form. + +## Synopsis + +```bash +iics # opens the menu in a terminal, otherwise prints help +iics menu # always opens the menu (terminal required) +iics tui +iics -p prd # opens the menu on profile prd +``` + +## When the menu opens + +`iics` without arguments opens the menu only when **stdin and stdout are both +terminals**. Otherwise it prints the help text, as before. Scripts, pipes +(`iics | less`) and CI are not affected. + +| Setting | Effect on bare `iics` | +| ------------------------------- | --------------------------------------------- | +| `IICS_NO_MENU=1` | Print help instead of opening the menu | +| `ui.menu: false` in the config | Print help instead of opening the menu | +| `CI` set (any value) | Print help instead of opening the menu | + +`iics --help` and `iics help` always print help. `iics menu` opens the menu +regardless of these settings, but still requires a terminal. + +```yaml +# ~/.iics/config.yaml +ui: + menu: false +``` + +## Screen + +```text +iics main menu + PRODUCTION profile prd · org Acme Prod · admin@acme.com · session ok (12 min left) + +Session +> Switch profile + Set as default profile ✎ + Login +Users + List users + Create user ✎ + ... + +Use another org profile for this menu session (key p) +enter run · / filter · p profile · ? help · q quit +``` + +- The header shows the active profile, and the org, user and session time + from the local session cache (no network call). Without a valid session it + shows "not logged in - signs in on first command". +- Entries are grouped by resource. `✎` marks entries that change data. +- The bottom lines describe the focused entry and show its command line. + +## Keys + +| Key | Action | +| ------------------- | --------------------------------------------- | +| `↑` / `↓`, `k` / `j` | Move | +| `PgUp` / `PgDn`, `g` / `G` | Page, first / last entry | +| `Enter` | Run the focused entry | +| `/` | Filter by group, label, description or command | +| `Esc` | Clear the filter | +| `p` | Switch profile | +| `?` | Show or hide key help | +| `q`, `Ctrl+C` | Quit the menu | + +## Running a command + +1. The command line is printed, for example `$ iics role edit --profile dev`. +2. The command runs as its own process in the terminal, exactly as if you had + typed it. Global flags the menu was started with (for example `--config`, + `--theme`, `--no-color`) are passed on. +3. When it finishes, `✓ done` or `✗ failed (exit N)` is shown. Press `Enter` + to return to the menu (on the last used entry) or `q` to quit. + +`Ctrl+C` while a command runs cancels only that command; the menu stays open. + +## Profiles + +- The menu starts with the usual profile: `--profile`, then `IICS_PROFILE`, + then `defaultProfile` from the config. +- **Switch profile** (`p`) changes the profile for this menu session only. +- **Set as default profile** writes `defaultProfile` to the config file. +- With no profiles configured, the menu starts `profile add` first. If that is + canceled, only the Session entries are available. + +## Production profiles + +A profile is treated as production when its config has `production: true`. +Without that setting, profile names `prd`, `prod`, `production`, or names +starting with `prd-` / `prod-` count as production; `production: false` turns +this off. `iics profile add` and `iics profile edit` ask for the setting. + +On a production profile: + +- the header shows a red `PRODUCTION` marker, +- entries marked `✎` ask for confirmation (default **No**) before they run. + +## Entries + +| Group | Entries | +| ----------------------- | ----------------------------------------------------------------------- | +| Session | Switch profile, Set as default profile ✎, Login, List profiles, Add profile ✎, Edit active profile ✎ | +| Users | List users, Create user ✎, Edit user groups and roles ✎, Delete user ✎, Change password ✎ | +| User groups | List, Show, Create ✎, Edit roles ✎, Delete ✎ | +| Roles | List, Show (with privileges), Create ✎, Edit privileges ✎ | +| Privileges | List privileges | +| Permissions | Edit object permissions ✎ | +| Environments and agents | List runtime environments, Create runtime environment ✎, List agents, Start / Stop / Restart agent service ✎, Download agent installer | +| Help | Show CLI help | + +## Accessible mode and small terminals + +With `IICS_ACCESSIBLE=1`, or when the terminal is smaller than 60 columns or +15 rows, the menu is a numbered list with the same entries and the same +run-and-return behavior. See [Interactive prompts](interactive.md). + +## See also + +- [Interactive prompts](interactive.md) - keys and accessible mode for wizards +- [profile](profile.md) - manage profiles and the production flag diff --git a/docs/documentation/profile.md b/docs/documentation/profile.md index 4c9a041..660ef35 100644 --- a/docs/documentation/profile.md +++ b/docs/documentation/profile.md @@ -53,6 +53,22 @@ specifies a `username`, `password`, and either a `region` code (resolved to a lo the built-in POD registry) or an explicit `loginUrl`. The `baseApiUrl` and `caiUrl` fields are populated automatically after the first successful `iics login`. +The optional `production` field marks a production org. The interactive +[main menu](menu.md) then shows a red `PRODUCTION` marker and asks for +confirmation before running entries that change data. `profile add` and +`profile edit` ask "Production org?"; the default is the current setting, or +yes for profile names `prd`, `prod`, `production` and names starting with +`prd-` / `prod-`. + +```yaml +profiles: + prd: + region: "USE4" + username: "admin@company.com" + password: "@keyring" + production: true +``` + The optional top-level `style` key controls table output appearance and is shared across all profiles: diff --git a/internal/config/config.go b/internal/config/config.go index d5f0618..c1a0a81 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -35,6 +35,14 @@ type Config struct { Profiles map[string]*Profile `yaml:"profiles" mapstructure:"profiles"` Style StyleConfig `yaml:"style,omitempty" mapstructure:"style"` HTTPTimeout int `yaml:"httpTimeout,omitempty" mapstructure:"httpTimeout"` + UI UIConfig `yaml:"ui,omitempty" mapstructure:"ui"` +} + +// UIConfig holds interactive UI preferences. +type UIConfig struct { + // Menu enables the interactive main menu for bare "iics" on a terminal. + // Defaults to true when unset. + Menu *bool `yaml:"menu,omitempty" mapstructure:"menu"` } // DefaultHTTPTimeoutSeconds is the per-HTTP-request timeout used when no @@ -77,6 +85,24 @@ type Profile struct { LoginURL string `yaml:"loginUrl,omitempty" mapstructure:"loginUrl"` BaseAPIURL string `yaml:"baseApiUrl,omitempty" mapstructure:"baseApiUrl"` CaiURL string `yaml:"caiUrl,omitempty" mapstructure:"caiUrl"` + // Production marks a production org. When unset, the profile name is + // used as a hint (see IsProductionProfile). + Production *bool `yaml:"production,omitempty" mapstructure:"production"` +} + +// IsProductionProfile reports whether a profile is a production org: the +// profile's explicit production setting when present, otherwise a name of +// "prd", "prod" or "production", or one starting with "prd-" / "prod-". +func IsProductionProfile(name string, p *Profile) bool { + if p != nil && p.Production != nil { + return *p.Production + } + n := strings.ToLower(strings.TrimSpace(name)) + switch n { + case "prd", "prod", "production": + return true + } + return strings.HasPrefix(n, "prd-") || strings.HasPrefix(n, "prod-") } // DeriveCaiURL derives the CAI base URL from an IICS product baseApiUrl. diff --git a/internal/config/production_test.go b/internal/config/production_test.go new file mode 100644 index 0000000..bf4dc38 --- /dev/null +++ b/internal/config/production_test.go @@ -0,0 +1,41 @@ +package config + +import ( + "testing" + "time" +) + +func TestIsProductionProfile(t *testing.T) { + yes, no := true, false + tests := []struct { + name string + p *Profile + want bool + }{ + {"prd", nil, true}, + {"PROD", &Profile{}, true}, + {"production", nil, true}, + {"prod-eu", nil, true}, + {"prd-us", nil, true}, + {"dev", nil, false}, + {"preprod", nil, false}, + {"dev", &Profile{Production: &yes}, true}, + {"prd", &Profile{Production: &no}, false}, + } + for _, tt := range tests { + if got := IsProductionProfile(tt.name, tt.p); got != tt.want { + t.Errorf("IsProductionProfile(%q, %+v) = %v, want %v", tt.name, tt.p, got, tt.want) + } + } +} + +func TestSessionRemaining(t *testing.T) { + e := &SessionEntry{CreatedAt: time.Now().Add(-10 * time.Minute)} + if r := e.Remaining(); r < 19*time.Minute || r > 20*time.Minute { + t.Errorf("Remaining = %v, want about 20m", r) + } + old := &SessionEntry{CreatedAt: time.Now().Add(-time.Hour)} + if old.Remaining() > 0 || !old.IsExpired() { + t.Error("an hour-old session should be expired") + } +} diff --git a/internal/config/prompt.go b/internal/config/prompt.go index d1ae87f..e2186bc 100644 --- a/internal/config/prompt.go +++ b/internal/config/prompt.go @@ -152,6 +152,24 @@ func promptProfileInternal(existing *Profile, profileName string) (*Profile, boo p.CaiURL = caiDefault } + // Production flag (default: the current setting, else the name hint). + prodDefault := IsProductionProfile(profileName, p) + prodHint := "[y/N]" + if prodDefault { + prodHint = "[Y/n]" + } + _, _ = fmt.Fprintf(os.Stderr, "Production org (menu asks before changes)? %s: ", prodHint) + line, err = r.ReadString('\n') + if err != nil { + return nil, false, false, fmt.Errorf("reading production choice: %w", err) + } + line = strings.TrimSpace(strings.ToLower(line)) + production := prodDefault + if line != "" { + production = line == "y" || line == "yes" + } + p.Production = &production + // Default profile prompt (default: yes) _, _ = fmt.Fprint(os.Stderr, "Set as default profile? [Y/n]: ") line, err = r.ReadString('\n') diff --git a/internal/config/prompt_test.go b/internal/config/prompt_test.go index 64b73df..275e44e 100644 --- a/internal/config/prompt_test.go +++ b/internal/config/prompt_test.go @@ -73,8 +73,8 @@ func TestPromptProfile_KeepsExistingOnEmptyInput(t *testing.T) { Region: "USW3", } stubReadPassword(t, "") // empty — should keep existing password - // inputs: username(keep), region(keep), caiUrl(keep derived), default(y), keyring(y) - withFakeStdin(t, "\n\n\n\n\n", func() { + // inputs: username(keep), region(keep), caiUrl(keep derived), production(keep), default(y), keyring(y) + withFakeStdin(t, "\n\n\n\n\n\n", func() { p, makeDefault, _, err := promptProfileInternal(existing, "test") if err != nil { t.Fatalf("unexpected error: %v", err) @@ -102,9 +102,9 @@ func TestPromptProfile_URLTreatedAsLoginURL(t *testing.T) { } customURL := "https://custom.example.com/saas/public/core/v3/login" stubReadPassword(t, "newpass") - // stdin needs: username(keep=\n), region line, caiUrl line, default line, keyring line. + // stdin needs: username(keep=\n), region line, caiUrl line, production line, default line, keyring line. // password is handled by stubReadPassword (no stdin line needed). - withFakeStdin(t, "\n"+customURL+"\n\n\n\n", func() { + withFakeStdin(t, "\n"+customURL+"\n\n\n\n\n", func() { p, _, _, err := promptProfileInternal(existing, "test") if err != nil { t.Fatalf("unexpected error: %v", err) @@ -121,9 +121,9 @@ func TestPromptProfile_URLTreatedAsLoginURL(t *testing.T) { func TestPromptProfile_RegionUppercased(t *testing.T) { existing := &Profile{Username: "u@example.com", Password: "pass"} stubReadPassword(t, "pass") - // stdin needs: username(keep=\n), region, caiUrl, default, keyring. + // stdin needs: username(keep=\n), region, caiUrl, production, default, keyring. // password is handled by stubReadPassword (no stdin line needed). - withFakeStdin(t, "\nusw3\n\n\n\n", func() { + withFakeStdin(t, "\nusw3\n\n\n\n\n", func() { p, _, _, err := promptProfileInternal(existing, "test") if err != nil { t.Fatalf("unexpected error: %v", err) @@ -143,9 +143,9 @@ func TestPromptProfile_KeepsExistingKeyringUnchanged(t *testing.T) { Region: "USW3", } stubReadPassword(t, "") // empty - keep existing (which is sentinel) - // inputs: username(keep), region(keep), caiUrl(keep), default(y) + // inputs: username(keep), region(keep), caiUrl(keep), production(keep), default(y) // NO keyring line - the prompt is skipped when password kept as sentinel - withFakeStdin(t, "\n\n\n\n", func() { + withFakeStdin(t, "\n\n\n\n\n", func() { p, _, storeInKeyring, err := promptProfileInternal(existing, "test") if err != nil { t.Fatalf("unexpected error: %v", err) @@ -163,7 +163,7 @@ func TestPromptProfile_KeyringDefaultYes(t *testing.T) { existing := &Profile{Username: "u@example.com", Password: "pass"} stubReadPassword(t, "pass") // Empty answer to keyring prompt means yes (default). - withFakeStdin(t, "\nusw3\n\n\n\n", func() { + withFakeStdin(t, "\nusw3\n\n\n\n\n", func() { _, _, storeInKeyring, err := promptProfileInternal(existing, "test") if err != nil { t.Fatalf("unexpected error: %v", err) @@ -178,7 +178,7 @@ func TestPromptProfile_KeyringExplicitNo(t *testing.T) { existing := &Profile{Username: "u@example.com", Password: "pass"} stubReadPassword(t, "pass") // Explicit "n" answer to keyring prompt means no. - withFakeStdin(t, "\nusw3\n\n\nn\n", func() { + withFakeStdin(t, "\nusw3\n\n\n\nn\n", func() { _, _, storeInKeyring, err := promptProfileInternal(existing, "test") if err != nil { t.Fatalf("unexpected error: %v", err) @@ -188,3 +188,28 @@ func TestPromptProfile_KeyringExplicitNo(t *testing.T) { } }) } + +func TestPromptProfile_Production(t *testing.T) { + existing := &Profile{Username: "u@example.com", Password: "secret", Region: "USW3"} + stubReadPassword(t, "") + // username, region, caiUrl, production (empty -> name hint "prd" = yes), default, keyring + withFakeStdin(t, "\n\n\n\n\n\n", func() { + p, _, _, err := promptProfileInternal(existing, "prd") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if p.Production == nil || !*p.Production { + t.Errorf("empty answer for profile prd should keep the production hint, got %v", p.Production) + } + }) + stubReadPassword(t, "") + withFakeStdin(t, "\n\n\ny\n\n\n", func() { + p, _, _, err := promptProfileInternal(existing, "dev") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if p.Production == nil || !*p.Production { + t.Errorf("answer y should mark production, got %v", p.Production) + } + }) +} diff --git a/internal/config/session.go b/internal/config/session.go index 4169e6f..75939d2 100644 --- a/internal/config/session.go +++ b/internal/config/session.go @@ -107,6 +107,12 @@ func (sc *SessionCache) Delete(profileName string) { delete(sc.Sessions, profileName) } +// Remaining returns how long the session stays valid; zero or negative +// means it has expired. +func (e *SessionEntry) Remaining() time.Duration { + return sessionTimeout - time.Since(e.CreatedAt) +} + // IsExpired returns true if the session has expired. func (e *SessionEntry) IsExpired() bool { return time.Since(e.CreatedAt) >= sessionTimeout diff --git a/internal/menu/entries.go b/internal/menu/entries.go new file mode 100644 index 0000000..76c9fec --- /dev/null +++ b/internal/menu/entries.go @@ -0,0 +1,105 @@ +// Package menu implements the interactive main menu shown when "iics" runs +// without arguments in a terminal. Each entry maps to a real command line, +// which the menu runs as a subprocess and then returns to the menu. +package menu + +import "strings" + +// Special entry actions handled by the menu itself instead of a command. +const ( + ActionSwitchProfile = "switch-profile" + ActionSetDefault = "set-default" +) + +// Entry is one menu item. +type Entry struct { + Group string + Label string + Description string + // Args are the command arguments, e.g. ["role", "edit"]. + Args []string + // Writes marks entries that change data (confirmation on production). + Writes bool + // Action is set for entries handled by the menu (no command). + Action string + // NoProfileFlag omits "--profile " (profile management commands). + NoProfileFlag bool + // ActiveProfileArg appends the active profile name as an argument. + ActiveProfileArg bool + // AskArg prompts for a text argument appended to Args (label shown). + AskArg string + // SessionOnly entries stay usable when no profile is configured. + SessionOnly bool +} + +// CommandArgs returns the arguments to run for the entry with the active +// profile, and the text argument from AskArg when given. +func (e Entry) CommandArgs(profile, asked string) []string { + args := append([]string(nil), e.Args...) + if e.ActiveProfileArg && profile != "" { + args = append(args, profile) + } + if e.AskArg != "" && asked != "" { + args = append(args, asked) + } + if !e.NoProfileFlag && profile != "" { + args = append(args, "--profile", profile) + } + return args +} + +// CommandLine formats a command line for display, quoting arguments that +// contain spaces. +func CommandLine(binary string, args []string) string { + parts := []string{binary} + for _, a := range args { + if a == "" || strings.ContainsAny(a, " \t\"'") { + a = "'" + strings.ReplaceAll(a, "'", `'\''`) + "'" + } + parts = append(parts, a) + } + return strings.Join(parts, " ") +} + +// DefaultEntries is the curated menu. +func DefaultEntries() []Entry { + return []Entry{ + {Group: "Session", Label: "Switch profile", Description: "Use another org profile for this menu session (key p)", Action: ActionSwitchProfile, SessionOnly: true}, + {Group: "Session", Label: "Set as default profile", Description: "Make the active profile the default in ~/.iics/config.yaml", Action: ActionSetDefault, Writes: true}, + {Group: "Session", Label: "Login", Description: "Sign in and refresh the session for the active profile", Args: []string{"login"}}, + {Group: "Session", Label: "List profiles", Description: "Show configured profiles", Args: []string{"profile", "list"}, NoProfileFlag: true, SessionOnly: true}, + {Group: "Session", Label: "Add profile", Description: "Set up a new org profile", Args: []string{"profile", "add"}, AskArg: "Profile name", NoProfileFlag: true, Writes: true, SessionOnly: true}, + {Group: "Session", Label: "Edit active profile", Description: "Update credentials, region and production flag of the active profile", Args: []string{"profile", "edit"}, ActiveProfileArg: true, NoProfileFlag: true, Writes: true}, + + {Group: "Users", Label: "List users", Description: "Show all users", Args: []string{"user", "list"}}, + {Group: "Users", Label: "Create user", Description: "Create a user with the paged wizard", Args: []string{"user", "create", "--interactive"}, Writes: true}, + {Group: "Users", Label: "Edit user groups and roles", Description: "Change a user's group and role assignments", Args: []string{"user", "edit"}, Writes: true}, + {Group: "Users", Label: "Delete user", Description: "Search for a user and delete it", Args: []string{"user", "delete"}, Writes: true}, + {Group: "Users", Label: "Change password", Description: "Change your own or another user's password", Args: []string{"user", "change-password"}, Writes: true}, + + {Group: "User groups", Label: "List user groups", Description: "Show all user groups", Args: []string{"group", "list"}}, + {Group: "User groups", Label: "Show user group", Description: "Pick a user group and show its roles and members", Args: []string{"group", "get"}}, + {Group: "User groups", Label: "Create user group", Description: "Create a user group with roles", Args: []string{"group", "create", "-i"}, Writes: true}, + {Group: "User groups", Label: "Edit user group roles", Description: "Change the roles of a user group", Args: []string{"group", "update", "-i"}, Writes: true}, + {Group: "User groups", Label: "Delete user group", Description: "Pick a user group and delete it", Args: []string{"group", "delete"}, Writes: true}, + + {Group: "Roles", Label: "List roles", Description: "Show all roles", Args: []string{"role", "list"}}, + {Group: "Roles", Label: "Show role", Description: "Pick a role and show its details and privileges", Args: []string{"role", "get"}}, + {Group: "Roles", Label: "Create role", Description: "Create a custom role, optionally copying another role", Args: []string{"role", "create"}, Writes: true}, + {Group: "Roles", Label: "Edit role privileges", Description: "Edit a custom role's privileges in the grid editor", Args: []string{"role", "edit"}, Writes: true}, + + {Group: "Privileges", Label: "List privileges", Description: "Show all privileges", Args: []string{"privilege", "list"}}, + + {Group: "Permissions", Label: "Edit object permissions", Description: "Pick a project, folder or asset and edit its ACLs", Args: []string{"permission", "edit"}, Writes: true}, + + {Group: "Environments and agents", Label: "List runtime environments", Description: "Show runtime environments (Secure Agent groups)", Args: []string{"environment", "list"}}, + {Group: "Environments and agents", Label: "Create runtime environment", Description: "Create a runtime environment and add agents", Args: []string{"environment", "create", "-i"}, Writes: true}, + {Group: "Environments and agents", Label: "List agents", Description: "Show Secure Agents", Args: []string{"agent", "list"}}, + {Group: "Environments and agents", Label: "Start agent service", Description: "Pick an agent and start a service", Args: []string{"agent", "start", "-i"}, Writes: true}, + {Group: "Environments and agents", Label: "Stop agent service", Description: "Pick an agent and stop a service", Args: []string{"agent", "stop", "-i"}, Writes: true}, + {Group: "Environments and agents", Label: "Restart agent service", Description: "Pick an agent and restart a service", Args: []string{"agent", "restart", "-i"}, Writes: true}, + {Group: "Environments and agents", Label: "Download agent installer", Description: "Download the Secure Agent installer", Args: []string{"agent", "installer-download"}}, + + {Group: "Help", Label: "Show CLI help", Description: "All commands and global flags", Args: []string{"--help"}, NoProfileFlag: true, SessionOnly: true}, + } +} diff --git a/internal/menu/loop.go b/internal/menu/loop.go new file mode 100644 index 0000000..a087b1a --- /dev/null +++ b/internal/menu/loop.go @@ -0,0 +1,315 @@ +package menu + +import ( + "bufio" + "errors" + "fmt" + "io" + "os" + "os/exec" + "os/signal" + "strings" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/jbrazda/iics-cli/internal/tui" +) + +// Runner runs a command line and returns its exit code. +type Runner interface { + Run(args []string) int +} + +// ExecRunner runs the CLI binary as a subprocess attached to the terminal. +// While the child runs, the menu ignores interrupts so Ctrl+C only reaches +// the child. +type ExecRunner struct { + Binary string +} + +// Run implements Runner. +func (r ExecRunner) Run(args []string) int { + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt) + defer signal.Stop(sig) + + c := exec.Command(r.Binary, args...) + c.Stdin, c.Stdout, c.Stderr = os.Stdin, os.Stdout, os.Stderr + err := c.Run() + var exitErr *exec.ExitError + switch { + case err == nil: + return 0 + case errors.As(err, &exitErr): + return exitErr.ExitCode() + default: + _, _ = fmt.Fprintf(os.Stderr, "Error: %v\n", err) + return -1 + } +} + +// ProfileInfo describes a configured profile for the profile picker. +type ProfileInfo struct { + Name string + Region string + Production bool + Default bool +} + +// State is what the menu shows for the active profile. +type State struct { + Profiles []ProfileInfo + Header Header + HasProfile bool +} + +// Deps are the menu's collaborators; tests replace them with fakes. +type Deps struct { + Runner Runner + Prompter *tui.Prompter + Out io.Writer + Binary string + NoColor bool + // Line selects the numbered, line-based menu (accessible / small terminal). + Line bool + // LoadState reads config and session information for a profile. + LoadState func(profile string) (State, error) + // SetDefault makes a profile the config default. + SetDefault func(profile string) error + // ShowMenu displays the full-screen menu and returns the choice. + ShowMenu func(m *Model) (Result, error) + // Pause waits after a command; it returns true when the user quits. + Pause func() (bool, error) + // ClearScreen clears the terminal before the menu is shown again. + ClearScreen func() + // Entries overrides DefaultEntries (tests). + Entries []Entry +} + +// DefaultShowMenu runs the model as a full-screen Bubble Tea program. +func DefaultShowMenu(m *Model) (Result, error) { + if _, err := tea.NewProgram(m, tea.WithAltScreen(), tea.WithOutput(os.Stderr)).Run(); err != nil { + return Result{}, err + } + return m.Result(), nil +} + +// DefaultPause asks the user to press Enter (menu) or q (quit). +func DefaultPause(out io.Writer) func() (bool, error) { + return func() (bool, error) { + _, _ = fmt.Fprint(out, "Press Enter to return to the menu, q to quit: ") + line, err := bufio.NewReader(os.Stdin).ReadString('\n') + if err != nil && line == "" { + return true, nil + } + return strings.EqualFold(strings.TrimSpace(line), "q"), nil + } +} + +// DefaultClearScreen clears the terminal. +func DefaultClearScreen(out io.Writer) func() { + return func() { _, _ = fmt.Fprint(out, "\x1b[H\x1b[2J") } +} + +// Run shows the menu for startProfile until the user quits. +func Run(d Deps, startProfile string) error { + entries := d.Entries + if entries == nil { + entries = DefaultEntries() + } + dim := lipgloss.NewStyle().Foreground(lipgloss.Color("244")) + ok := lipgloss.NewStyle().Foreground(lipgloss.Color("2")).Bold(true) + bad := lipgloss.NewStyle().Foreground(lipgloss.Color("1")).Bold(true) + if d.NoColor { + dim, ok, bad = lipgloss.NewStyle(), lipgloss.NewStyle(), lipgloss.NewStyle() + } + + profile := startProfile + st, err := d.LoadState(profile) + if err != nil { + return err + } + + // First run: set up a profile before showing the menu. + if !st.HasProfile && len(st.Profiles) == 0 { + _, _ = fmt.Fprintln(d.Out, "No profile is configured yet. Let's add one.") + name, perr := d.Prompter.Text("Profile name", "default") + if perr != nil { + return perr + } + if code := d.run(dim, []string{"profile", "add", name}); code == 0 { + profile = name + } + if d.ClearScreen != nil { + d.ClearScreen() + } + } + + last := "" + for { + st, err = d.LoadState(profile) + if err != nil { + return err + } + if !st.HasProfile && len(st.Profiles) == 0 { + st.Header.Notice = "No profile configured - choose Session > Add profile." + } else if !st.HasProfile { + st.Header.Notice = fmt.Sprintf("Profile %q is not configured - switch profile (p) or add it.", profile) + } + st.Header.Binary = d.Binary + + res, err := d.choose(entries, st, last) + if err != nil { + return err + } + switch res.Kind { + case ResultQuit: + return nil + case ResultSwitchProfile: + if picked, perr := d.pickProfile(st.Profiles, profile); perr != nil { + return perr + } else if picked != "" { + profile = picked + } + last = "Switch profile" + continue + } + + e := res.Entry + last = e.Label + if e.Writes && st.Header.Production { + yes, cerr := d.Prompter.Confirm(fmt.Sprintf("Run %q against PRODUCTION (%s)?", e.Label, profile), false) + if cerr != nil { + return cerr + } + if !yes { + continue + } + } + + var code int + switch e.Action { + case ActionSetDefault: + if serr := d.SetDefault(profile); serr != nil { + _, _ = fmt.Fprintf(d.Out, "Error: %v\n", serr) + code = 1 + } else { + _, _ = fmt.Fprintf(d.Out, "Default profile set to %q.\n", profile) + } + default: + asked := "" + if e.AskArg != "" { + asked, err = d.Prompter.Text(e.AskArg, "") + if err != nil { + return err + } + if asked == "" { + continue + } + } + code = d.run(dim, e.CommandArgs(profile, asked)) + if code == 0 && e.AskArg != "" && !st.HasProfile { + profile = asked // first profile just added + } + } + + if code == 0 { + _, _ = fmt.Fprintln(d.Out, ok.Render("✓ done")) + } else { + _, _ = fmt.Fprintln(d.Out, bad.Render(fmt.Sprintf("✗ failed (exit %d)", code))) + } + quit, perr := d.Pause() + if perr != nil { + return perr + } + if quit { + return nil + } + if d.ClearScreen != nil { + d.ClearScreen() + } + } +} + +func (d Deps) run(dim lipgloss.Style, args []string) int { + _, _ = fmt.Fprintln(d.Out, dim.Render("$ "+CommandLine(d.Binary, args))) + return d.Runner.Run(args) +} + +func (d Deps) choose(entries []Entry, st State, last string) (Result, error) { + if !d.Line { + return d.ShowMenu(NewModel(entries, st.Header, st.HasProfile, last, d.NoColor)) + } + return lineMenu(d, entries, st) +} + +// lineMenu is the numbered, line-based menu. +func lineMenu(d Deps, entries []Entry, st State) (Result, error) { + h := st.Header + head := "iics main menu - profile " + orDash(h.Profile) + if h.Production { + head += " [PRODUCTION]" + } + if h.Org != "" { + head += " - org " + h.Org + } + if h.Session != "" { + head += " - " + h.Session + } + _, _ = fmt.Fprintln(d.Out, head) + if h.Notice != "" { + _, _ = fmt.Fprintln(d.Out, h.Notice) + } + var usable []Entry + var labels []string + for _, e := range entries { + if !st.HasProfile && !e.SessionOnly { + continue + } + label := e.Group + " > " + e.Label + if e.Writes { + label += " (changes data)" + } + usable = append(usable, e) + labels = append(labels, label) + } + idx, err := d.Prompter.Select("Choose an entry (0 or q quits)", labels) + if err != nil { + return Result{}, err + } + if idx < 0 { + return Result{Kind: ResultQuit}, nil + } + e := usable[idx] + if e.Action == ActionSwitchProfile { + return Result{Kind: ResultSwitchProfile}, nil + } + return Result{Kind: ResultRun, Entry: e}, nil +} + +func (d Deps) pickProfile(profiles []ProfileInfo, current string) (string, error) { + if len(profiles) == 0 { + _, _ = fmt.Fprintln(d.Out, "No profiles configured.") + return "", nil + } + idx, err := tui.PickOne(d.Prompter, "Switch profile", profiles, func(p ProfileInfo) string { + label := p.Name + if p.Region != "" { + label += " " + p.Region + } + if p.Production { + label += " PRODUCTION" + } + if p.Default { + label += " (default)" + } + if p.Name == current { + label += " (active)" + } + return label + }) + if err != nil || idx < 0 { + return "", err + } + return profiles[idx].Name, nil +} diff --git a/internal/menu/menu_test.go b/internal/menu/menu_test.go new file mode 100644 index 0000000..0f8fc06 --- /dev/null +++ b/internal/menu/menu_test.go @@ -0,0 +1,328 @@ +package menu + +import ( + "bytes" + "reflect" + "strings" + "testing" + + tea "github.com/charmbracelet/bubbletea" + "github.com/jbrazda/iics-cli/internal/tui" +) + +func env(vars map[string]string) func(string) string { + return func(k string) string { return vars[k] } +} + +func TestShouldOpen(t *testing.T) { + off := false + on := true + tests := []struct { + name string + in OpenInput + want bool + }{ + {"terminal", OpenInput{StdinTTY: true, StdoutTTY: true}, true}, + {"stdout piped", OpenInput{StdinTTY: true}, false}, + {"stdin piped", OpenInput{StdoutTTY: true}, false}, + {"CI", OpenInput{StdinTTY: true, StdoutTTY: true, Env: env(map[string]string{"CI": "true"})}, false}, + {"opt-out env", OpenInput{StdinTTY: true, StdoutTTY: true, Env: env(map[string]string{"IICS_NO_MENU": "1"})}, false}, + {"opt-out env false", OpenInput{StdinTTY: true, StdoutTTY: true, Env: env(map[string]string{"IICS_NO_MENU": "0"})}, true}, + {"config off", OpenInput{StdinTTY: true, StdoutTTY: true, ConfigMenu: &off}, false}, + {"config on", OpenInput{StdinTTY: true, StdoutTTY: true, ConfigMenu: &on}, true}, + } + for _, tt := range tests { + if got := ShouldOpen(tt.in); got != tt.want { + t.Errorf("%s: ShouldOpen = %v, want %v", tt.name, got, tt.want) + } + } +} + +func TestCommandArgsAndLine(t *testing.T) { + e := Entry{Args: []string{"role", "edit"}} + if got := e.CommandArgs("dev", ""); !reflect.DeepEqual(got, []string{"role", "edit", "--profile", "dev"}) { + t.Errorf("CommandArgs = %v", got) + } + add := Entry{Args: []string{"profile", "add"}, AskArg: "Profile name", NoProfileFlag: true} + if got := add.CommandArgs("dev", "qa eu"); !reflect.DeepEqual(got, []string{"profile", "add", "qa eu"}) { + t.Errorf("CommandArgs = %v", got) + } + edit := Entry{Args: []string{"profile", "edit"}, ActiveProfileArg: true, NoProfileFlag: true} + if got := edit.CommandArgs("dev", ""); !reflect.DeepEqual(got, []string{"profile", "edit", "dev"}) { + t.Errorf("CommandArgs = %v", got) + } + if got := CommandLine("iics", []string{"profile", "add", "qa eu", "it's"}); got != `iics profile add 'qa eu' 'it'\''s'` { + t.Errorf("CommandLine = %s", got) + } +} + +func TestDefaultEntriesAreRunnable(t *testing.T) { + seen := map[string]bool{} + for _, e := range DefaultEntries() { + if e.Group == "" || e.Label == "" || e.Description == "" { + t.Errorf("entry missing text: %+v", e) + } + if e.Action == "" && len(e.Args) == 0 { + t.Errorf("entry %q has neither args nor action", e.Label) + } + if seen[e.Label] { + t.Errorf("duplicate label %q", e.Label) + } + seen[e.Label] = true + } +} + +func key(s string) tea.KeyMsg { + switch s { + case "down": + return tea.KeyMsg{Type: tea.KeyDown} + case "up": + return tea.KeyMsg{Type: tea.KeyUp} + case "enter": + return tea.KeyMsg{Type: tea.KeyEnter} + case "esc": + return tea.KeyMsg{Type: tea.KeyEsc} + } + return tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(s)} +} + +func press(m *Model, keys ...string) { + for _, k := range keys { + m.Update(key(k)) + } +} + +var testEntries = []Entry{ + {Group: "Session", Label: "Switch profile", Description: "d", Action: ActionSwitchProfile, SessionOnly: true}, + {Group: "Session", Label: "Add profile", Description: "d", Args: []string{"profile", "add"}, AskArg: "Profile name", NoProfileFlag: true, SessionOnly: true, Writes: true}, + {Group: "Roles", Label: "List roles", Description: "d", Args: []string{"role", "list"}}, + {Group: "Roles", Label: "Edit role privileges", Description: "privilege grid", Args: []string{"role", "edit"}, Writes: true}, +} + +func TestModelNavigationAndRun(t *testing.T) { + m := NewModel(testEntries, Header{Profile: "dev", Production: true}, true, "", true) + view := m.View() + for _, want := range []string{"PRODUCTION", "profile dev", "Session", "Roles", "Edit role privileges ✎", "$ iics --profile dev"} { + if want == "$ iics --profile dev" { + continue // first entry is an action without a command line + } + if !strings.Contains(view, want) { + t.Errorf("view missing %q:\n%s", want, view) + } + } + // Cursor starts on the first entry (headers are skipped). + press(m, "down", "down", "down") // Add profile, List roles, Edit role privileges + press(m, "enter") + if !m.Done() || m.Result().Kind != ResultRun || m.Result().Entry.Label != "Edit role privileges" { + t.Fatalf("expected run of Edit role privileges, got %+v", m.Result()) + } + if !strings.Contains(m.View(), "$ iics role edit --profile dev") { + t.Errorf("equivalent command not shown:\n%s", m.View()) + } +} + +func TestModelFilterLastAndKeys(t *testing.T) { + m := NewModel(testEntries, Header{Profile: "dev"}, true, "List roles", true) + press(m, "enter") + if m.Result().Entry.Label != "List roles" { + t.Errorf("cursor should start on the last entry, got %+v", m.Result()) + } + + m = NewModel(testEntries, Header{Profile: "dev"}, true, "", true) + press(m, "/", "g", "r", "i", "d", "enter", "enter") + if m.Result().Entry.Label != "Edit role privileges" { + t.Errorf("filter on description should select Edit role privileges, got %+v", m.Result()) + } + + m = NewModel(testEntries, Header{}, true, "", true) + press(m, "p") + if m.Result().Kind != ResultSwitchProfile { + t.Errorf("p should switch profile, got %+v", m.Result()) + } + + m = NewModel(testEntries, Header{}, true, "", true) + press(m, "q") + if m.Result().Kind != ResultQuit { + t.Errorf("q should quit, got %+v", m.Result()) + } +} + +func TestModelDisabledWithoutProfile(t *testing.T) { + m := NewModel(testEntries, Header{Notice: "No profile configured"}, false, "List roles", true) + press(m, "enter") + if m.Done() { + t.Fatal("entries needing a profile must not run without one") + } + if !strings.Contains(m.View(), "Add a profile first") || !strings.Contains(m.View(), "No profile configured") { + t.Errorf("expected notice and message:\n%s", m.View()) + } +} + +// fakeRunner records command lines and returns scripted exit codes. +type fakeRunner struct { + calls [][]string + codes []int +} + +func (f *fakeRunner) Run(args []string) int { + f.calls = append(f.calls, args) + if len(f.codes) == 0 { + return 0 + } + c := f.codes[0] + f.codes = f.codes[1:] + return c +} + +type loopHarness struct { + deps Deps + runner *fakeRunner + out *bytes.Buffer + results []Result + states map[string]State + def string +} + +func newHarness(input string, results []Result, states map[string]State) *loopHarness { + h := &loopHarness{runner: &fakeRunner{}, out: &bytes.Buffer{}, results: results, states: states} + h.deps = Deps{ + Runner: h.runner, + Prompter: &tui.Prompter{In: strings.NewReader(input), Out: h.out}, + Out: h.out, + Binary: "iics", + NoColor: true, + Entries: testEntries, + LoadState: func(p string) (State, error) { + st, ok := h.states[p] + if !ok { + st = h.states[""] + } + st.Header.Profile = p + return st, nil + }, + SetDefault: func(p string) error { h.def = p; return nil }, + ShowMenu: func(m *Model) (Result, error) { + if len(h.results) == 0 { + return Result{Kind: ResultQuit}, nil + } + r := h.results[0] + h.results = h.results[1:] + return r, nil + }, + Pause: func() (bool, error) { return false, nil }, + } + return h +} + +var twoProfiles = []ProfileInfo{{Name: "dev"}, {Name: "prd", Production: true}} + +func TestLoopRunsAndReturns(t *testing.T) { + st := State{HasProfile: true, Profiles: twoProfiles} + h := newHarness("", []Result{ + {Kind: ResultRun, Entry: testEntries[2]}, + {Kind: ResultRun, Entry: testEntries[3]}, + }, map[string]State{"dev": st}) + h.runner.codes = []int{0, 3} + if err := Run(h.deps, "dev"); err != nil { + t.Fatal(err) + } + want := [][]string{{"role", "list", "--profile", "dev"}, {"role", "edit", "--profile", "dev"}} + if !reflect.DeepEqual(h.runner.calls, want) { + t.Errorf("calls = %v, want %v", h.runner.calls, want) + } + out := h.out.String() + for _, s := range []string{"$ iics role list --profile dev", "✓ done", "✗ failed (exit 3)"} { + if !strings.Contains(out, s) { + t.Errorf("output missing %q:\n%s", s, out) + } + } +} + +func TestLoopProductionConfirmation(t *testing.T) { + prod := State{HasProfile: true, Profiles: twoProfiles, Header: Header{Production: true}} + // Declined write, then a read (never asks), then an accepted write. + h := newHarness("n\ny\n", []Result{ + {Kind: ResultRun, Entry: testEntries[3]}, + {Kind: ResultRun, Entry: testEntries[2]}, + {Kind: ResultRun, Entry: testEntries[3]}, + }, map[string]State{"prd": prod}) + if err := Run(h.deps, "prd"); err != nil { + t.Fatal(err) + } + want := [][]string{{"role", "list", "--profile", "prd"}, {"role", "edit", "--profile", "prd"}} + if !reflect.DeepEqual(h.runner.calls, want) { + t.Errorf("calls = %v, want %v", h.runner.calls, want) + } + if n := strings.Count(h.out.String(), "against PRODUCTION"); n != 2 { + t.Errorf("expected 2 production confirmations, got %d:\n%s", n, h.out.String()) + } +} + +func TestLoopSwitchProfileAndSetDefault(t *testing.T) { + states := map[string]State{ + "dev": {HasProfile: true, Profiles: twoProfiles}, + "prd": {HasProfile: true, Profiles: twoProfiles, Header: Header{Production: true}}, + } + setDefault := Entry{Group: "Session", Label: "Set as default profile", Description: "d", Action: ActionSetDefault, Writes: true} + // Switch to profile 2 (prd), run a read, set default (confirm y on prd). + h := newHarness("2\ny\n", []Result{ + {Kind: ResultSwitchProfile}, + {Kind: ResultRun, Entry: testEntries[2]}, + {Kind: ResultRun, Entry: setDefault}, + }, states) + if err := Run(h.deps, "dev"); err != nil { + t.Fatal(err) + } + if len(h.runner.calls) != 1 || h.runner.calls[0][3] != "prd" { + t.Errorf("command should run with the switched profile, got %v", h.runner.calls) + } + if h.def != "prd" { + t.Errorf("default profile = %q, want prd", h.def) + } +} + +func TestLoopFirstRunAddsProfile(t *testing.T) { + states := map[string]State{ + "": {}, + "qa": {HasProfile: true, Profiles: []ProfileInfo{{Name: "qa"}}}, + "dev": {}, + } + h := newHarness("qa\n", nil, states) + if err := Run(h.deps, "dev"); err != nil { + t.Fatal(err) + } + if len(h.runner.calls) != 1 || !reflect.DeepEqual(h.runner.calls[0], []string{"profile", "add", "qa"}) { + t.Errorf("first run should add a profile, got %v", h.runner.calls) + } +} + +func TestLoopQuitFromPause(t *testing.T) { + h := newHarness("", []Result{{Kind: ResultRun, Entry: testEntries[2]}, {Kind: ResultRun, Entry: testEntries[2]}}, + map[string]State{"dev": {HasProfile: true}}) + h.deps.Pause = func() (bool, error) { return true, nil } + if err := Run(h.deps, "dev"); err != nil { + t.Fatal(err) + } + if len(h.runner.calls) != 1 { + t.Errorf("q at the pause should quit after one command, got %d calls", len(h.runner.calls)) + } +} + +func TestLineMenu(t *testing.T) { + // Numbered menu: without a profile only session entries (1, 2) are listed. + h := newHarness("2\nqa\n0\n", nil, map[string]State{"dev": {Profiles: twoProfiles}, "qa": {HasProfile: true}}) + h.deps.Line = true + if err := Run(h.deps, "dev"); err != nil { + t.Fatal(err) + } + if len(h.runner.calls) != 1 || !reflect.DeepEqual(h.runner.calls[0], []string{"profile", "add", "qa"}) { + t.Errorf("calls = %v", h.runner.calls) + } + first, _, _ := strings.Cut(h.out.String(), "$ iics profile add") + if strings.Contains(first, "List roles") { + t.Errorf("entries needing a profile must be hidden without one:\n%s", first) + } + if !strings.Contains(h.out.String()[len(first):], "List roles") { + t.Error("after adding a profile all entries should be listed") + } +} diff --git a/internal/menu/model.go b/internal/menu/model.go new file mode 100644 index 0000000..2290a86 --- /dev/null +++ b/internal/menu/model.go @@ -0,0 +1,414 @@ +package menu + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +// Header describes the active profile and session shown above the menu. +type Header struct { + Profile string + Org string + User string + Session string + Production bool + // Notice is an extra line, e.g. "No profile configured". + Notice string + // Binary is the command name used for the equivalent command line. + Binary string +} + +// ResultKind is what the user chose in the menu. +type ResultKind int + +const ( + // ResultQuit ends the menu. + ResultQuit ResultKind = iota + // ResultRun runs Result.Entry. + ResultRun + // ResultSwitchProfile opens the profile picker. + ResultSwitchProfile +) + +// Result is returned by the model when it finishes. +type Result struct { + Kind ResultKind + Entry Entry +} + +type row struct { + header bool + group string + entry Entry + index int // index in entries +} + +// Model is the Bubble Tea main menu. +type Model struct { + entries []Entry + header Header + // hasProfile disables non-session entries when false. + hasProfile bool + + rows []row + cursor int + offset int + width int + height int + + filter textinput.Model + filtering bool + showHelp bool + message string + + result Result + done bool + styles menuStyles +} + +type menuStyles struct { + title, prod, header, group, selected, dim, desc, cmd, warn lipgloss.Style +} + +func newMenuStyles(noColor bool) menuStyles { + if noColor { + p := lipgloss.NewStyle() + return menuStyles{ + title: p.Bold(true), prod: p.Bold(true).Reverse(true), header: p, group: p.Bold(true), + selected: p.Reverse(true), dim: p, desc: p, cmd: p, warn: p.Bold(true), + } + } + return menuStyles{ + title: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("6")), + prod: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("15")).Background(lipgloss.Color("1")).Padding(0, 1), + header: lipgloss.NewStyle().Foreground(lipgloss.Color("250")), + group: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("4")), + selected: lipgloss.NewStyle().Bold(true).Reverse(true), + dim: lipgloss.NewStyle().Foreground(lipgloss.Color("244")), + desc: lipgloss.NewStyle().Foreground(lipgloss.Color("252")), + cmd: lipgloss.NewStyle().Foreground(lipgloss.Color("244")).Italic(true), + warn: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("1")), + } +} + +// NewModel creates the menu. last is the label of the entry to focus. +func NewModel(entries []Entry, header Header, hasProfile bool, last string, noColor bool) *Model { + ti := textinput.New() + ti.Prompt = "/" + ti.Placeholder = "filter" + m := &Model{ + entries: entries, header: header, hasProfile: hasProfile, + filter: ti, width: 100, height: 30, styles: newMenuStyles(noColor), + } + m.rebuild() + for i, r := range m.rows { + if !r.header && r.entry.Label == last { + m.cursor = i + } + } + m.skipHeader(1) + return m +} + +// Result returns the user's choice once the model is done. +func (m *Model) Result() Result { return m.result } + +// Done reports whether the model finished. +func (m *Model) Done() bool { return m.done } + +func (m *Model) enabled(e Entry) bool { + return m.hasProfile || e.SessionOnly +} + +func (m *Model) rebuild() { + q := strings.ToLower(strings.TrimSpace(m.filter.Value())) + m.rows = m.rows[:0] + lastGroup := "" + for i, e := range m.entries { + if q != "" { + hay := strings.ToLower(e.Group + " " + e.Label + " " + e.Description + " " + strings.Join(e.Args, " ")) + if !strings.Contains(hay, q) { + continue + } + } + if e.Group != lastGroup { + m.rows = append(m.rows, row{header: true, group: e.Group}) + lastGroup = e.Group + } + m.rows = append(m.rows, row{group: e.Group, entry: e, index: i}) + } + if m.cursor >= len(m.rows) { + m.cursor = len(m.rows) - 1 + } + if m.cursor < 0 { + m.cursor = 0 + } + m.skipHeader(1) +} + +func (m *Model) skipHeader(dir int) { + for len(m.rows) > 0 && m.rows[m.cursor].header { + next := m.cursor + dir + if next < 0 || next >= len(m.rows) { + dir = -dir + next = m.cursor + dir + if next < 0 || next >= len(m.rows) { + return + } + } + m.cursor = next + } +} + +func (m *Model) move(delta int) { + if len(m.rows) == 0 { + return + } + dir := 1 + if delta < 0 { + dir = -1 + } + m.cursor = min(max(m.cursor+delta, 0), len(m.rows)-1) + m.skipHeader(dir) +} + +func (m *Model) headerLines() int { + n := 3 // title, profile line, blank + if m.header.Notice != "" { + n++ + } + return n +} + +func (m *Model) pageSize() int { + footer := 4 // blank, description, command, key line + if m.showHelp { + footer += 6 + } + return max(m.height-m.headerLines()-footer, 3) +} + +func (m *Model) ensureVisible() { + page := m.pageSize() + if m.cursor < m.offset { + m.offset = m.cursor + } + if m.cursor >= m.offset+page { + m.offset = m.cursor - page + 1 + } + m.offset = max(m.offset, 0) +} + +// Init implements tea.Model. +func (m *Model) Init() tea.Cmd { return nil } + +// Update implements tea.Model. +func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width, m.height = msg.Width, msg.Height + m.ensureVisible() + return m, nil + case tea.KeyMsg: + m.message = "" + if m.filtering { + switch msg.String() { + case "enter", "down", "up": + m.filtering = false + m.filter.Blur() + return m, nil + case "esc": + m.filtering = false + m.filter.Blur() + m.filter.SetValue("") + m.rebuild() + return m, nil + case "ctrl+c": + return m.finish(Result{Kind: ResultQuit}) + } + var cmd tea.Cmd + m.filter, cmd = m.filter.Update(msg) + m.offset = 0 + m.rebuild() + return m, cmd + } + switch msg.String() { + case "ctrl+c", "q": + return m.finish(Result{Kind: ResultQuit}) + case "esc": + if m.filter.Value() != "" { + m.filter.SetValue("") + m.rebuild() + } + case "up", "k": + m.move(-1) + case "down", "j": + m.move(1) + case "pgup": + m.move(-m.pageSize()) + case "pgdown": + m.move(m.pageSize()) + case "home", "g": + m.cursor = 0 + m.skipHeader(1) + case "end", "G": + m.cursor = len(m.rows) - 1 + m.skipHeader(-1) + case "/": + m.filtering = true + m.filter.Focus() + return m, textinput.Blink + case "?": + m.showHelp = !m.showHelp + case "p": + return m.finish(Result{Kind: ResultSwitchProfile}) + case "enter": + if len(m.rows) == 0 { + return m, nil + } + e := m.rows[m.cursor].entry + if !m.enabled(e) { + m.message = "Add a profile first (Session > Add profile)." + return m, nil + } + if e.Action == ActionSwitchProfile { + return m.finish(Result{Kind: ResultSwitchProfile}) + } + return m.finish(Result{Kind: ResultRun, Entry: e}) + } + m.ensureVisible() + } + return m, nil +} + +func (m *Model) finish(r Result) (tea.Model, tea.Cmd) { + m.result = r + m.done = true + return m, tea.Quit +} + +func truncate(s string, n int) string { + if n <= 1 { + return "" + } + r := []rune(s) + if len(r) <= n { + return s + } + return string(r[:n-1]) + "…" +} + +// View implements tea.Model. +func (m *Model) View() string { + var b strings.Builder + w := m.width - 1 + h := m.header + + b.WriteString(m.styles.title.Render("iics")) + b.WriteString(" ") + b.WriteString(m.styles.dim.Render("main menu")) + if m.filtering || m.filter.Value() != "" { + b.WriteString(" ") + b.WriteString(m.filter.View()) + } + b.WriteString("\n") + + profile := "profile " + orDash(h.Profile) + var parts []string + parts = append(parts, profile) + if h.Org != "" { + parts = append(parts, "org "+h.Org) + } + if h.User != "" { + parts = append(parts, h.User) + } + if h.Session != "" { + parts = append(parts, h.Session) + } + line := m.styles.header.Render(truncate(strings.Join(parts, " · "), w-14)) + if h.Production { + line = m.styles.prod.Render("PRODUCTION") + " " + line + } + b.WriteString(line) + b.WriteString("\n") + if h.Notice != "" { + b.WriteString(m.styles.warn.Render(truncate(h.Notice, w))) + b.WriteString("\n") + } + b.WriteString("\n") + + if len(m.rows) == 0 { + b.WriteString(m.styles.dim.Render(" no matching entries")) + b.WriteString("\n") + } + end := min(m.offset+m.pageSize(), len(m.rows)) + for i := m.offset; i < end; i++ { + r := m.rows[i] + if r.header { + b.WriteString(m.styles.group.Render(r.group)) + b.WriteString("\n") + continue + } + label := r.entry.Label + if r.entry.Writes { + label += " ✎" + } + text := " " + truncate(label, w-4) + switch { + case i == m.cursor: + b.WriteString(m.styles.selected.Render("> " + truncate(label, w-4))) + case !m.enabled(r.entry): + b.WriteString(m.styles.dim.Render(text)) + default: + b.WriteString(text) + } + b.WriteString("\n") + } + if len(m.rows) > m.pageSize() { + b.WriteString(m.styles.dim.Render(fmt.Sprintf(" %d-%d of %d", m.offset+1, end, len(m.rows)))) + } + b.WriteString("\n") + + if len(m.rows) > 0 { + e := m.rows[m.cursor].entry + b.WriteString(m.styles.desc.Render(truncate(e.Description, w))) + b.WriteString("\n") + if e.Action == "" { + binary := h.Binary + if binary == "" { + binary = "iics" + } + b.WriteString(m.styles.cmd.Render(truncate("$ "+CommandLine(binary, e.CommandArgs(h.Profile, "<"+strings.ToLower(e.AskArg)+">")), w))) + } + b.WriteString("\n") + } + if m.message != "" { + b.WriteString(m.styles.warn.Render(truncate(m.message, w))) + b.WriteString("\n") + } + if m.showHelp { + for _, l := range []string{ + "↑/↓ k/j move pgup/pgdn page g/G first/last", + "enter run entry / filter esc clear filter", + "p switch profile ? hide help", + "q quit ctrl+c (in a command) cancels only that command", + "✎ marks entries that change data; on a PRODUCTION profile they ask first.", + } { + b.WriteString(m.styles.dim.Render(truncate(l, w))) + b.WriteString("\n") + } + } + b.WriteString(m.styles.dim.Render(truncate("enter run · / filter · p profile · ? help · q quit", w))) + return b.String() +} + +func orDash(s string) string { + if s == "" { + return "-" + } + return s +} diff --git a/internal/menu/open.go b/internal/menu/open.go new file mode 100644 index 0000000..2183972 --- /dev/null +++ b/internal/menu/open.go @@ -0,0 +1,38 @@ +package menu + +import ( + "strconv" + "strings" +) + +// OpenInput holds what decides whether bare "iics" opens the menu. +type OpenInput struct { + StdinTTY, StdoutTTY bool + // Env returns an environment variable (os.Getenv). + Env func(string) string + // ConfigMenu is the config ui.menu setting (nil when unset). + ConfigMenu *bool +} + +// ShouldOpen reports whether bare "iics" should open the menu instead of +// printing help: stdin and stdout are terminals, CI is not set, +// IICS_NO_MENU is not true, and config ui.menu is not false. +func ShouldOpen(in OpenInput) bool { + if !in.StdinTTY || !in.StdoutTTY { + return false + } + env := in.Env + if env == nil { + env = func(string) string { return "" } + } + if strings.TrimSpace(env("CI")) != "" { + return false + } + if v, err := strconv.ParseBool(strings.TrimSpace(env("IICS_NO_MENU"))); err == nil && v { + return false + } + if in.ConfigMenu != nil && !*in.ConfigMenu { + return false + } + return true +} From 2d5b88bf6b126fca5c8880631bc731bf468522da Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Wed, 30 Sep 2026 00:49:21 -0400 Subject: [PATCH 14/52] feat(agent): interactive installer download and installer info menu entry - agent installer-download --interactive: OS picker, "Download to" prompt pre-filled with the default temp path (directories and ~/ supported), and an existing-file prompt - existing destination file: SHA-256 compared with the installer checksum and reported; scripts skip a matching file (download otherwise), interactive mode asks (default No when it matches, Yes otherwise), --force always downloads; fetched checksum is reused for --verify - client: SHA256File, CompareChecksum, DownloadOverExisting with tests - menu: "Show agent installer info" entry; download entry runs installer-download --interactive --progress - docs: agent.md, menu.md, CR-0044 follow-up; completions --- cmd/agent_installer.go | 116 ++++++++++++++++-- completions/iics.bash | 6 + .../pending/CR-0044-interactive-main-menu.md | 18 ++- docs/documentation/agent.md | 33 ++++- docs/documentation/menu.md | 2 +- internal/client/installer.go | 63 ++++++++++ internal/client/installer_test.go | 66 ++++++++++ internal/menu/entries.go | 3 +- 8 files changed, 295 insertions(+), 12 deletions(-) create mode 100644 internal/client/installer.go create mode 100644 internal/client/installer_test.go diff --git a/cmd/agent_installer.go b/cmd/agent_installer.go index 83bbaf8..75282c1 100644 --- a/cmd/agent_installer.go +++ b/cmd/agent_installer.go @@ -245,6 +245,8 @@ func newAgentInstallerDownloadCmd() *cobra.Command { target string verify bool progressFlag bool + interactive bool + force bool ) cmd := &cobra.Command{ Use: "installer-download", @@ -260,12 +262,23 @@ name from the installer metadata; otherwise --target is treated as the full file path. Without --target the file is written to the system temp directory. --progress prints a live progress line to stderr (bytes transferred, percentage -when the server reports a content length, and transfer rate).`, +when the server reports a content length, and transfer rate). + +When the destination file already exists, its SHA-256 is compared with the +installer checksum and reported. Without --force, a file that already matches +is not downloaded again; a different or unverifiable file is replaced. With +--interactive (terminal only) you are asked for the destination ("Download +to", default the system temp directory) and, when the file exists, whether to +download again. --force always downloads.`, Example: ` iics agent installer-download --os linux64 --target ./downloads/ iics agent installer-info --os win64 --output json | iics agent installer-download --verify iics agent installer-download --installer-info info.json --target /tmp/agent.exe --verify - iics agent installer-download --os linux64 --target ./downloads/ --progress`, + iics agent installer-download --os linux64 --target ./downloads/ --progress + iics agent installer-download --interactive --progress`, RunE: func(cmd *cobra.Command, args []string) error { + if interactive && !isInteractiveTTY() { + return fmt.Errorf("--interactive requires a terminal") + } c, err := getClient(cmd) if err != nil { return err @@ -292,7 +305,47 @@ when the server reports a content length, and transfer rate).`, } fileName := installerFileName(info.DownloadURL) + if interactive { + answer, perr := prompter.Text("Download to", resolveTargetPath(target, fileName)) + if perr != nil { + return perr + } + target = expandHome(answer) + } destPath := resolveTargetPath(target, fileName) + + // Existing file: report its checksum state and decide whether to download. + expected := "" + expectedFetched := false + if st, serr := os.Stat(destPath); serr == nil && !st.IsDir() { + expected, expectedFetched = fetchExpectedChecksum(ctx, c, info, cmd.ErrOrStderr()) + actual, size, herr := client.SHA256File(destPath) + if herr != nil { + return herr + } + state := client.CompareChecksum(actual, expected) + reportExistingInstaller(cmd.ErrOrStderr(), destPath, size, st.ModTime(), state) + download, derr := client.DownloadOverExisting(state, force, interactive, func(def bool) (bool, error) { + return prompter.Confirm("Download again and overwrite it?", def) + }) + if derr != nil { + return derr + } + if !download { + res := &installerDownloadResult{ + File: destPath, FileName: fileName, Size: size, + DownloadURL: info.DownloadURL, ChecksumDownloadURL: info.ChecksumDownloadURL, + InstallToken: info.InstallToken, + } + if state != client.ChecksumUnknown { + match := state == client.ChecksumMatch + res.ChecksumAlgorithm, res.ExpectedChecksum, res.ActualChecksum, res.Verified = "sha256", expected, actual, &match + } + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Kept the existing file; download skipped.") + return printInstallerDownloadResult(res) + } + } + if dir := filepath.Dir(destPath); dir != "" { if mkErr := os.MkdirAll(dir, 0o755); mkErr != nil { return fmt.Errorf("creating target directory: %w", mkErr) @@ -339,12 +392,14 @@ when the server reports a content length, and transfer rate).`, _ = os.Remove(destPath) return fmt.Errorf("--verify requested but installer info has no checksumDownloadUrl") } - checksumText, cerr := c.FetchText(ctx, info.ChecksumDownloadURL) - if cerr != nil { - _ = os.Remove(destPath) - return cerr + if !expectedFetched { + checksumText, cerr := c.FetchText(ctx, info.ChecksumDownloadURL) + if cerr != nil { + _ = os.Remove(destPath) + return cerr + } + expected = parseChecksum(checksumText) } - expected := parseChecksum(checksumText) actual := hex.EncodeToString(hasher.Sum(nil)) match := expected != "" && expected == actual res.ChecksumAlgorithm = "sha256" @@ -368,9 +423,56 @@ when the server reports a content length, and transfer rate).`, cmd.Flags().StringVar(&target, "target", "", "destination file or directory (default: system temp directory)") cmd.Flags().BoolVar(&verify, "verify", false, "download the checksum and verify the installer") cmd.Flags().BoolVar(&progressFlag, "progress", false, "print download progress to stderr") + cmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "prompt for the OS, the destination and whether to replace an existing file") + cmd.Flags().BoolVar(&force, "force", false, "download even when the destination file already matches") return cmd } +// fetchExpectedChecksum downloads the installer checksum. Failures are +// reported and treated as unknown. The bool reports whether the fetch +// succeeded (so --verify can reuse the value). +func fetchExpectedChecksum(ctx context.Context, c *client.Client, info *client.AgentInstallerInfo, w io.Writer) (string, bool) { + if info.ChecksumDownloadURL == "" { + return "", false + } + text, err := c.FetchText(ctx, info.ChecksumDownloadURL) + if err != nil { + _, _ = fmt.Fprintf(w, "Could not fetch the installer checksum: %v\n", err) + return "", false + } + return parseChecksum(text), true +} + +// reportExistingInstaller describes an existing destination file. +func reportExistingInstaller(w io.Writer, path string, size int64, mod time.Time, state client.ChecksumState) { + _, _ = fmt.Fprintf(w, "File already exists: %s (%s, modified %s)\n", path, formatBytes(size), mod.Format("2006-01-02 15:04")) + switch state { + case client.ChecksumMatch: + _, _ = fmt.Fprintln(w, " It matches the checksum of the current installer.") + case client.ChecksumMismatch: + _, _ = fmt.Fprintln(w, " It does NOT match the checksum of the current installer.") + default: + _, _ = fmt.Fprintln(w, " Its checksum could not be verified (no installer checksum available).") + } +} + +// expandHome replaces a leading "~" with the user's home directory, keeping +// a trailing separator (which marks a directory for resolveTargetPath). +func expandHome(p string) string { + if p != "~" && !strings.HasPrefix(p, "~/") && !strings.HasPrefix(p, `~\`) { + return p + } + home, err := os.UserHomeDir() + if err != nil { + return p + } + out := filepath.Join(home, p[1:]) + if strings.HasSuffix(p, "/") || strings.HasSuffix(p, `\`) { + out += string(os.PathSeparator) + } + return out +} + // newDownloadProgressPrinter returns a DownloadProgressFunc that prints a // single, carriage-return-updated progress line to w, throttled to at most // once every 200ms plus a final call at completion. diff --git a/completions/iics.bash b/completions/iics.bash index 949ff3a..c5f01ce 100644 --- a/completions/iics.bash +++ b/completions/iics.bash @@ -678,10 +678,16 @@ _iics_agent_installer-download() flags_with_completion=() flags_completion=() + flags+=("--force") + local_nonpersistent_flags+=("--force") flags+=("--installer-info=") two_word_flags+=("--installer-info") local_nonpersistent_flags+=("--installer-info") local_nonpersistent_flags+=("--installer-info=") + flags+=("--interactive") + flags+=("-i") + local_nonpersistent_flags+=("--interactive") + local_nonpersistent_flags+=("-i") flags+=("--os=") two_word_flags+=("--os") local_nonpersistent_flags+=("--os") diff --git a/docs/ChangeRequests/pending/CR-0044-interactive-main-menu.md b/docs/ChangeRequests/pending/CR-0044-interactive-main-menu.md index b905bc7..b176f3a 100644 --- a/docs/ChangeRequests/pending/CR-0044-interactive-main-menu.md +++ b/docs/ChangeRequests/pending/CR-0044-interactive-main-menu.md @@ -71,7 +71,7 @@ flag), grouped by resource: | Roles | List, Show with privileges, Create ✎, Edit privileges ✎ | | Privileges | List | | Permissions | Edit object permissions ✎ | -| Environments and agents | List environments, Create environment ✎, List agents, Start / Stop / Restart agent service ✎, Download agent installer | +| Environments and agents | List environments, Create environment ✎, List agents, Start / Stop / Restart agent service ✎, Show agent installer info, Download agent installer | | Help | Show CLI help | ✎ = changes data (production confirmation). @@ -136,3 +136,19 @@ flag), grouped by resource: header and declined confirmation on `prd`, profile picker, Ctrl+C in a running command returns to the menu, accessible line menu. Non-TTY `iics` still prints help; unknown commands still fail. + +## Follow-up: agent installer entries (2026-09-30) + +- New entry "Show agent installer info" (`agent installer-info`). +- "Download agent installer" runs `agent installer-download --interactive + --progress`. +- `agent installer-download` gains `--interactive` (OS, "Download to" with + the default temp path, existing-file prompt) and `--force`. An existing + destination file is compared with the installer checksum and reported; + scripts skip a matching file (download otherwise), interactive mode asks + (default No when it matches, Yes otherwise), `--force` always downloads. + Helpers `client.SHA256File`, `CompareChecksum`, `DownloadOverExisting` with + tests. +- Verified on `dev`: fresh download with `--progress --verify` (365 MB, + checksum verified), script rerun skipped in 1s, interactive prompt with + matching file (kept), mismatching file (declined, kept). diff --git a/docs/documentation/agent.md b/docs/documentation/agent.md index b491985..fabf964 100644 --- a/docs/documentation/agent.md +++ b/docs/documentation/agent.md @@ -366,8 +366,8 @@ Installer info can be supplied three ways: 1. Piped as JSON on stdin (for example from `iics agent installer-info --output json`). 2. From a file via `--installer-info `. 3. Omitted entirely, in which case the info is requested for the default or - `--profile` account using `--os` (shown as a selection menu when interactive; - `0`, Enter, or `q` cancels). + `--profile` account using `--os` (shown as a selection list when interactive; + `Ctrl+C` cancels). ### Flags @@ -378,6 +378,8 @@ Installer info can be supplied three ways: | `--target` | string | system temp directory | Destination file or directory | | `--verify` | bool | false | Download the checksum file and verify the installer | | `--progress` | bool | false | Print live download progress to stderr | +| `--interactive`, `-i` | bool | false | Prompt for the OS, the destination and whether to replace an existing file (terminal only) | +| `--force` | bool | false | Download even when the destination file already matches | `--target` behavior: @@ -387,6 +389,33 @@ Installer info can be supplied three ways: name is appended. - Any other value: treated as the full destination file path. +### Existing destination file + +When the destination file already exists, the command compares its SHA-256 +with the installer checksum (`checksumDownloadUrl`) and reports the result: + +```text +File already exists: /tmp/agent64_install_ng_ext.7617.bin (364.6MB, modified 2026-09-30 00:41) + It matches the checksum of the current installer. +``` + +| Mode | File matches | File differs or cannot be checked | +| ----------------------- | ----------------------------- | --------------------------------- | +| Default (scripts) | Kept, download skipped | Downloaded again (replaced) | +| `--interactive` | Asks "Download again and overwrite it?" (default **No**) | Asks (default **Yes**) | +| `--force` | Downloaded again | Downloaded again | + +When the download is skipped, the result describes the existing file +(including `verified` when the checksum could be checked). + +### Interactive mode + +`--interactive` asks for the operating system (unless `--os` is given), then +"Download to", pre-filled with the default destination (the system temp +directory plus the installer file name, or `--target`). Enter a directory +(existing, or ending with `/`) or a full file path; `~/` is expanded. The +[main menu](menu.md) runs `iics agent installer-download --interactive --progress`. + All [global flags](../../README.md#global-flags) apply. ### Output diff --git a/docs/documentation/menu.md b/docs/documentation/menu.md index d37633f..bb0023c 100644 --- a/docs/documentation/menu.md +++ b/docs/documentation/menu.md @@ -117,7 +117,7 @@ On a production profile: | Roles | List, Show (with privileges), Create ✎, Edit privileges ✎ | | Privileges | List privileges | | Permissions | Edit object permissions ✎ | -| Environments and agents | List runtime environments, Create runtime environment ✎, List agents, Start / Stop / Restart agent service ✎, Download agent installer | +| Environments and agents | List runtime environments, Create runtime environment ✎, List agents, Start / Stop / Restart agent service ✎, Show agent installer info, Download agent installer (`--interactive --progress`) | | Help | Show CLI help | ## Accessible mode and small terminals diff --git a/internal/client/installer.go b/internal/client/installer.go new file mode 100644 index 0000000..2c702a7 --- /dev/null +++ b/internal/client/installer.go @@ -0,0 +1,63 @@ +package client + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "os" +) + +// SHA256File returns the lowercase hex SHA-256 digest and size of a file. +func SHA256File(path string) (string, int64, error) { + f, err := os.Open(path) + if err != nil { + return "", 0, err + } + defer func() { _ = f.Close() }() + h := sha256.New() + n, err := io.Copy(h, f) + if err != nil { + return "", 0, fmt.Errorf("reading %s: %w", path, err) + } + return hex.EncodeToString(h.Sum(nil)), n, nil +} + +// ChecksumState describes how an existing file compares to the expected +// installer checksum. +type ChecksumState int + +const ( + // ChecksumUnknown means the expected checksum could not be determined. + ChecksumUnknown ChecksumState = iota + // ChecksumMatch means the file matches the expected checksum. + ChecksumMatch + // ChecksumMismatch means the file differs from the expected checksum. + ChecksumMismatch +) + +// CompareChecksum compares an actual and an expected hex digest. +func CompareChecksum(actual, expected string) ChecksumState { + switch { + case expected == "" || actual == "": + return ChecksumUnknown + case actual == expected: + return ChecksumMatch + default: + return ChecksumMismatch + } +} + +// DownloadOverExisting decides whether to download over an existing file. +// With force it always downloads. Interactively it asks confirm, defaulting +// to yes unless the file already matches. Otherwise it skips only a file +// that matches the expected checksum. +func DownloadOverExisting(state ChecksumState, force, interactive bool, confirm func(defaultYes bool) (bool, error)) (bool, error) { + if force { + return true, nil + } + if interactive && confirm != nil { + return confirm(state != ChecksumMatch) + } + return state != ChecksumMatch, nil +} diff --git a/internal/client/installer_test.go b/internal/client/installer_test.go new file mode 100644 index 0000000..b732aba --- /dev/null +++ b/internal/client/installer_test.go @@ -0,0 +1,66 @@ +package client + +import ( + "os" + "path/filepath" + "testing" +) + +func TestSHA256File(t *testing.T) { + p := filepath.Join(t.TempDir(), "f.bin") + if err := os.WriteFile(p, []byte("abc"), 0o600); err != nil { + t.Fatal(err) + } + sum, n, err := SHA256File(p) + if err != nil { + t.Fatal(err) + } + if sum != "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" || n != 3 { + t.Errorf("got %s, %d", sum, n) + } + if _, _, err := SHA256File(filepath.Join(t.TempDir(), "missing")); err == nil { + t.Error("expected error for a missing file") + } +} + +func TestCompareChecksum(t *testing.T) { + if CompareChecksum("aa", "aa") != ChecksumMatch || CompareChecksum("aa", "bb") != ChecksumMismatch || + CompareChecksum("aa", "") != ChecksumUnknown { + t.Error("unexpected CompareChecksum result") + } +} + +func TestDownloadOverExisting(t *testing.T) { + var gotDefault *bool + confirm := func(answer bool) func(bool) (bool, error) { + return func(def bool) (bool, error) { gotDefault = &def; return answer, nil } + } + tests := []struct { + name string + state ChecksumState + force bool + interactive bool + answer bool + want bool + wantDefault *bool + }{ + {"force", ChecksumMatch, true, true, false, true, nil}, + {"script match skips", ChecksumMatch, false, false, false, false, nil}, + {"script mismatch downloads", ChecksumMismatch, false, false, false, true, nil}, + {"script unknown downloads", ChecksumUnknown, false, false, false, true, nil}, + {"interactive match defaults no", ChecksumMatch, false, true, false, false, ptr(false)}, + {"interactive mismatch defaults yes", ChecksumMismatch, false, true, true, true, ptr(true)}, + } + for _, tt := range tests { + gotDefault = nil + got, err := DownloadOverExisting(tt.state, tt.force, tt.interactive, confirm(tt.answer)) + if err != nil || got != tt.want { + t.Errorf("%s: got %v, %v; want %v", tt.name, got, err, tt.want) + } + if (tt.wantDefault == nil) != (gotDefault == nil) || (tt.wantDefault != nil && *tt.wantDefault != *gotDefault) { + t.Errorf("%s: confirm default = %v, want %v", tt.name, gotDefault, tt.wantDefault) + } + } +} + +func ptr(b bool) *bool { return &b } diff --git a/internal/menu/entries.go b/internal/menu/entries.go index 76c9fec..a0d52d6 100644 --- a/internal/menu/entries.go +++ b/internal/menu/entries.go @@ -98,7 +98,8 @@ func DefaultEntries() []Entry { {Group: "Environments and agents", Label: "Start agent service", Description: "Pick an agent and start a service", Args: []string{"agent", "start", "-i"}, Writes: true}, {Group: "Environments and agents", Label: "Stop agent service", Description: "Pick an agent and stop a service", Args: []string{"agent", "stop", "-i"}, Writes: true}, {Group: "Environments and agents", Label: "Restart agent service", Description: "Pick an agent and restart a service", Args: []string{"agent", "restart", "-i"}, Writes: true}, - {Group: "Environments and agents", Label: "Download agent installer", Description: "Download the Secure Agent installer", Args: []string{"agent", "installer-download"}}, + {Group: "Environments and agents", Label: "Show agent installer info", Description: "Show the installer download URL, checksum URL and install token", Args: []string{"agent", "installer-info"}}, + {Group: "Environments and agents", Label: "Download agent installer", Description: "Download the Secure Agent installer; choose the folder, reuse a matching file", Args: []string{"agent", "installer-download", "--interactive", "--progress"}}, {Group: "Help", Label: "Show CLI help", Description: "All commands and global flags", Args: []string{"--help"}, NoProfileFlag: true, SessionOnly: true}, } From 84045fba43eb0824b852d0624a343e7e9c280d95 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Wed, 30 Sep 2026 08:27:24 -0400 Subject: [PATCH 15/52] feat(user): configurable user name and email patterns for new users (CR-0045) - profile config newUser: domain (default: domain of the profile username), userNamePattern (default {firstName}.{lastName}.{profileName}@{domain}), emailPattern (default {firstName}.{lastName}@{domain}); placeholders firstName, lastName, firstInitial, lastInitial, profileName, domain - config.ExpandUserPattern validates placeholders and missing values - user create --interactive: user name and email show the generated value in gray; right arrow or ctrl+e fills it in for editing, empty uses it; notes under the field explain the pattern or why there is no suggestion - profile show lists Production and the effective new-user settings - tests for pattern defaults/expansion and wizard suggestions - docs: user.md, profile.md, README; CR-0045 in pending --- README.md | 4 + cmd/profile.go | 13 +- cmd/user.go | 25 ++-- .../pending/CR-0045-new-user-name-patterns.md | 83 ++++++++++++ docs/documentation/profile.md | 16 +++ docs/documentation/user.md | 38 +++++- internal/config/config.go | 2 + internal/config/userpattern.go | 94 +++++++++++++ internal/config/userpattern_test.go | 47 +++++++ internal/tui/userwizard.go | 128 +++++++++++++++--- internal/tui/userwizard_test.go | 34 ++++- 11 files changed, 441 insertions(+), 43 deletions(-) create mode 100644 docs/ChangeRequests/pending/CR-0045-new-user-name-patterns.md create mode 100644 internal/config/userpattern.go create mode 100644 internal/config/userpattern_test.go diff --git a/README.md b/README.md index 2b685cf..b4ffde3 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,10 @@ profiles: loginUrl: "https://use4.dm-us.informaticacloud.com/saas/public/core/v3/login" baseApiUrl: "https://use4.dm-us.informaticacloud.com/saas" caiUrl: "https://use4-cai.dm-us.informaticacloud.com" + newUser: # suggestions in "user create --interactive" (optional) + domain: "company.com" + userNamePattern: "{firstName}.{lastName}.{profileName}@{domain}" + emailPattern: "{firstName}.{lastName}@{domain}" prod: name: "Production Org" production: true # menu marks it and asks before changes diff --git a/cmd/profile.go b/cmd/profile.go index da11eae..6af9049 100644 --- a/cmd/profile.go +++ b/cmd/profile.go @@ -466,6 +466,17 @@ func newProfileShowCmd() *cobra.Command { {"field": "Username", "value": p.Username}, {"field": "Password", "value": maskedPassword}, } + production := "no" + if config.IsProductionProfile(name, p) { + production = "yes" + } + nu := config.NewUserPatterns(p) + rows = append(rows, + map[string]interface{}{"field": "Production", "value": production}, + map[string]interface{}{"field": "New User Domain", "value": nu.Domain}, + map[string]interface{}{"field": "User Name Pattern", "value": nu.UserNamePattern}, + map[string]interface{}{"field": "Email Pattern", "value": nu.EmailPattern}, + ) // Append session-derived fields from the cache. const noSession = "(no active session)" @@ -503,7 +514,7 @@ func newProfileShowCmd() *cobra.Command { return err } columns := []output.Column{ - {Header: "FIELD", Field: "field", Width: 15}, + {Header: "FIELD", Field: "field", Width: 17}, {Header: "VALUE", Field: "value"}, } return f.Format(rows, columns) diff --git a/cmd/user.go b/cmd/user.go index 22de8c7..2d60bf0 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -551,21 +551,22 @@ func runUserWizard(ctx context.Context, c *client.Client, existing *client.User) return nil, fmt.Errorf("fetching roles: %w", err) } - domain := "" - if _, p, _, _ := resolveProfile(); p != nil { - if i := strings.Index(p.Username, "@"); i >= 0 { - domain = p.Username[i+1:] - } + cfg, _ := loadConfig() + _, _, profileName, _ := resolveProfile() + var prof *config.Profile + if cfg != nil { + prof = cfg.Profiles[profileName] } ok, err := tui.RunUserWizard(tui.UserWizardInput{ - User: u, - Update: existing != nil, - Groups: groups, - Roles: roles, - UserNameDomain: domain, - Out: os.Stderr, - Accessible: prompter.Accessible, + User: u, + Update: existing != nil, + Groups: groups, + Roles: roles, + ProfileName: profileName, + Patterns: config.NewUserPatterns(prof), + Out: os.Stderr, + Accessible: prompter.Accessible, }) if err != nil || !ok { return nil, err diff --git a/docs/ChangeRequests/pending/CR-0045-new-user-name-patterns.md b/docs/ChangeRequests/pending/CR-0045-new-user-name-patterns.md new file mode 100644 index 0000000..44219fd --- /dev/null +++ b/docs/ChangeRequests/pending/CR-0045-new-user-name-patterns.md @@ -0,0 +1,83 @@ +# CR-0045: Configurable user name and email patterns for new users + +## CR Type + +- [x] Enhancement to existing command + +## Problem + +The `user create --interactive` wizard suggests a user name +`first.last@`, where the domain is taken from the active profile's +login user name. The format is fixed, the email is not suggested at all, and +the suggestion can only be used by leaving the field empty. + +Organizations use naming conventions per environment, for example +`jane.doe.dev@acme.com` for a user in the dev org and `jane.doe@acme.com` as +the email address. + +## Proposed Solution + +### Profile settings + +```yaml +profiles: + dev: + username: admin@acme.com + newUser: + domain: acme.com # default: domain of the profile username + userNamePattern: "{firstName}.{lastName}.{profileName}@{domain}" # default + emailPattern: "{firstName}.{lastName}@{domain}" # default +``` + +Placeholders: + +| Placeholder | Value | +| ---------------- | ------------------------------------------------------- | +| `{firstName}` | First name, lowercase, spaces removed | +| `{lastName}` | Last name, lowercase, spaces removed | +| `{firstInitial}` | First letter of the first name, lowercase | +| `{lastInitial}` | First letter of the last name, lowercase | +| `{profileName}` | Name of the active profile | +| `{domain}` | `newUser.domain`, or the domain of the profile username | + +An unknown placeholder, or a pattern that needs a missing value (for example +`{domain}` without a domain), produces no suggestion and a note in the +wizard. + +### Wizard + +- User name and email fields show the generated value as a suggestion + (ghost text). +- `→` (right arrow) or `Ctrl+E` completes the suggestion into the field for + editing; submitting an empty field uses the suggestion. +- `profile show` lists the effective patterns and domain. + +## Implementation + +- `internal/config`: `Profile.NewUser` (`NewUserConfig`), `NewUserPatterns` + (effective values with defaults), `ExpandUserPattern` with placeholder + validation; tests. +- `internal/tui/userwizard.go`: pattern-based suggestions for user name and + email, right-arrow accept key; tests. +- `cmd/user.go`: pass the effective patterns; `cmd/profile.go`: show them. +- Docs: `user.md`, `profile.md`, README config sample. + +## Acceptance Criteria + +- [x] Defaults produce `{firstName}.{lastName}.{profileName}@{domain}` and + `{firstName}.{lastName}@{domain}` with the domain of the profile user +- [x] Custom patterns and domain from the profile are used +- [x] Unknown placeholders are reported, no suggestion is made +- [x] `→` / `Ctrl+E` completes; empty submit uses the suggestion +- [x] `profile show` lists the effective settings +- [x] `go build`, `go vet`, `golangci-lint`, `go test ./...` pass + +## Implementation Notes + +- huh's built-in input suggestions only complete text that was already + typed, so the wizard wraps the input (`suggestField`): the generated value + is the placeholder (gray), and `→` / `Ctrl+E` on an empty field fills it in. + Typed prefixes still get the built-in completion. +- Verified in a pty on `dev`: placeholders after entering names, `→` fills + user name and email, value editable afterwards; wizard canceled (no user + created). diff --git a/docs/documentation/profile.md b/docs/documentation/profile.md index 660ef35..e0224ef 100644 --- a/docs/documentation/profile.md +++ b/docs/documentation/profile.md @@ -69,6 +69,22 @@ profiles: production: true ``` +The optional `newUser` block sets the user name and email suggested by +`iics user create --interactive` (see +[user - New user patterns](user.md#new-user-patterns)): + +```yaml +profiles: + dev: + newUser: + domain: company.com + userNamePattern: "{firstName}.{lastName}.{profileName}@{domain}" + emailPattern: "{firstName}.{lastName}@{domain}" +``` + +`profile show` lists `Production`, `New User Domain`, `User Name Pattern` and +`Email Pattern` with defaults applied. + The optional top-level `style` key controls table output appearance and is shared across all profiles: diff --git a/docs/documentation/user.md b/docs/documentation/user.md index bda31f8..71e8342 100644 --- a/docs/documentation/user.md +++ b/docs/documentation/user.md @@ -109,9 +109,11 @@ Create a user from a definition file, or interactively with `--interactive`. `--interactive` opens a paged form (`Enter` next field, `Shift+Tab` back): -1. **Identity** - authentication (Native or SSO), first name, last name, user - name (empty uses the suggestion `first.last@`), email - (validated). +1. **Identity** - authentication (Native or SSO), first name, last name, + user name and email (validated). User name and email show a generated + value in gray once first and last name are entered; `→` or `Ctrl+E` + fills it in for editing, and leaving the field empty uses it. The values + come from the active profile's [new user patterns](#new-user-patterns). 2. **Single sign-on** - alias name in the identity provider; shown only for SSO, where the API requires it. 3. **Details** - phone, title, description, force password change. @@ -123,6 +125,36 @@ Create a user from a definition file, or interactively with `--interactive`. The create API does not accept a time zone, so the wizard does not ask for one. See [Interactive prompts](interactive.md) for keys. +### New user patterns + +The suggested user name and email are built from patterns in the active +profile (`~/.iics/config.yaml`): + +```yaml +profiles: + dev: + username: admin@acme.com + newUser: + domain: acme.com # default: domain of the profile username + userNamePattern: "{firstName}.{lastName}.{profileName}@{domain}" # default + emailPattern: "{firstName}.{lastName}@{domain}" # default +``` + +| Placeholder | Value | +| ---------------- | ------------------------------------------------------- | +| `{firstName}` | First name, lowercase, spaces removed | +| `{lastName}` | Last name, lowercase, spaces removed | +| `{firstInitial}` | First letter of the first name, lowercase | +| `{lastInitial}` | First letter of the last name, lowercase | +| `{profileName}` | Name of the active profile | +| `{domain}` | `newUser.domain`, or the domain of the profile username | + +With the defaults, Jane Doe in profile `dev` (login `admin@acme.com`) gets +user name `jane.doe.dev@acme.com` and email `jane.doe@acme.com`. An unknown +placeholder or a missing value (for example no `@` in the profile username +and no `newUser.domain`) shows a note under the field instead of a +suggestion. `iics profile show ` lists the effective patterns. + All [global flags](../../README.md#global-flags) apply. ### JSON definition example diff --git a/internal/config/config.go b/internal/config/config.go index c1a0a81..770515a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -88,6 +88,8 @@ type Profile struct { // Production marks a production org. When unset, the profile name is // used as a hint (see IsProductionProfile). Production *bool `yaml:"production,omitempty" mapstructure:"production"` + // NewUser configures suggested user names and emails for new users. + NewUser *NewUserConfig `yaml:"newUser,omitempty" mapstructure:"newUser"` } // IsProductionProfile reports whether a profile is a production org: the diff --git a/internal/config/userpattern.go b/internal/config/userpattern.go new file mode 100644 index 0000000..4bed1ec --- /dev/null +++ b/internal/config/userpattern.go @@ -0,0 +1,94 @@ +package config + +import ( + "fmt" + "regexp" + "strings" +) + +// Default patterns for new user names and emails. +const ( + DefaultUserNamePattern = "{firstName}.{lastName}.{profileName}@{domain}" + DefaultEmailPattern = "{firstName}.{lastName}@{domain}" +) + +// UserPatternPlaceholders lists the supported placeholders. +var UserPatternPlaceholders = []string{"firstName", "lastName", "firstInitial", "lastInitial", "profileName", "domain"} + +// NewUserConfig holds the per-profile settings for new users. +type NewUserConfig struct { + // Domain replaces {domain}; defaults to the domain of the profile username. + Domain string `yaml:"domain,omitempty" mapstructure:"domain"` + UserNamePattern string `yaml:"userNamePattern,omitempty" mapstructure:"userNamePattern"` + EmailPattern string `yaml:"emailPattern,omitempty" mapstructure:"emailPattern"` +} + +// NewUserPatterns returns the effective new-user settings of a profile, +// applying defaults for unset values. +func NewUserPatterns(p *Profile) NewUserConfig { + out := NewUserConfig{UserNamePattern: DefaultUserNamePattern, EmailPattern: DefaultEmailPattern} + if p == nil { + return out + } + if i := strings.LastIndex(p.Username, "@"); i >= 0 { + out.Domain = p.Username[i+1:] + } + if p.NewUser != nil { + if p.NewUser.Domain != "" { + out.Domain = p.NewUser.Domain + } + if p.NewUser.UserNamePattern != "" { + out.UserNamePattern = p.NewUser.UserNamePattern + } + if p.NewUser.EmailPattern != "" { + out.EmailPattern = p.NewUser.EmailPattern + } + } + return out +} + +// UserPatternValues are the inputs for ExpandUserPattern. +type UserPatternValues struct { + FirstName, LastName, ProfileName, Domain string +} + +var placeholderRe = regexp.MustCompile(`\{([^{}]*)\}`) + +// ExpandUserPattern replaces the placeholders of pattern. Name values are +// lowercased with whitespace removed. It returns an error for unknown +// placeholders or when a used placeholder has no value. +func ExpandUserPattern(pattern string, v UserPatternValues) (string, error) { + clean := func(s string) string { + return strings.ToLower(strings.Join(strings.Fields(s), "")) + } + initial := func(s string) string { + if r := []rune(clean(s)); len(r) > 0 { + return string(r[0]) + } + return "" + } + values := map[string]string{ + "firstName": clean(v.FirstName), + "lastName": clean(v.LastName), + "firstInitial": initial(v.FirstName), + "lastInitial": initial(v.LastName), + "profileName": clean(v.ProfileName), + "domain": strings.ToLower(strings.TrimSpace(v.Domain)), + } + var errs []string + out := placeholderRe.ReplaceAllStringFunc(pattern, func(m string) string { + name := m[1 : len(m)-1] + val, known := values[name] + switch { + case !known: + errs = append(errs, fmt.Sprintf("unknown placeholder %s", m)) + case val == "": + errs = append(errs, fmt.Sprintf("no value for %s", m)) + } + return val + }) + if len(errs) > 0 { + return "", fmt.Errorf("pattern %q: %s", pattern, strings.Join(errs, "; ")) + } + return out, nil +} diff --git a/internal/config/userpattern_test.go b/internal/config/userpattern_test.go new file mode 100644 index 0000000..6177189 --- /dev/null +++ b/internal/config/userpattern_test.go @@ -0,0 +1,47 @@ +package config + +import ( + "strings" + "testing" +) + +func TestNewUserPatterns(t *testing.T) { + d := NewUserPatterns(&Profile{Username: "admin@Acme.com"}) + if d.Domain != "Acme.com" || d.UserNamePattern != DefaultUserNamePattern || d.EmailPattern != DefaultEmailPattern { + t.Errorf("defaults = %+v", d) + } + c := NewUserPatterns(&Profile{Username: "admin@acme.com", NewUser: &NewUserConfig{Domain: "corp.example", EmailPattern: "{firstInitial}{lastName}@{domain}"}}) + if c.Domain != "corp.example" || c.UserNamePattern != DefaultUserNamePattern || c.EmailPattern != "{firstInitial}{lastName}@{domain}" { + t.Errorf("custom = %+v", c) + } + if n := NewUserPatterns(&Profile{Username: "admin"}); n.Domain != "" { + t.Errorf("no @ in username should give no domain, got %q", n.Domain) + } +} + +func TestExpandUserPattern(t *testing.T) { + v := UserPatternValues{FirstName: "Mary Ann", LastName: "O'Neil", ProfileName: "Dev", Domain: "Acme.com"} + tests := []struct { + pattern, want, wantErr string + }{ + {DefaultUserNamePattern, "maryann.o'neil.dev@acme.com", ""}, + {DefaultEmailPattern, "maryann.o'neil@acme.com", ""}, + {"{firstInitial}{lastInitial}-{profileName}", "mo-dev", ""}, + {"{first}.{lastName}", "", "unknown placeholder {first}"}, + } + for _, tt := range tests { + got, err := ExpandUserPattern(tt.pattern, v) + if tt.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Errorf("%s: error = %v, want %q", tt.pattern, err, tt.wantErr) + } + continue + } + if err != nil || got != tt.want { + t.Errorf("%s: got %q, %v; want %q", tt.pattern, got, err, tt.want) + } + } + if _, err := ExpandUserPattern(DefaultEmailPattern, UserPatternValues{FirstName: "a", LastName: "b"}); err == nil || !strings.Contains(err.Error(), "no value for {domain}") { + t.Errorf("missing domain should fail, got %v", err) + } +} diff --git a/internal/tui/userwizard.go b/internal/tui/userwizard.go index 35f62a6..9ffc47c 100644 --- a/internal/tui/userwizard.go +++ b/internal/tui/userwizard.go @@ -8,8 +8,11 @@ import ( "sort" "strings" + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/huh" "github.com/jbrazda/iics-cli/internal/client" + "github.com/jbrazda/iics-cli/internal/config" ) // UserWizardInput configures RunUserWizard. @@ -23,8 +26,10 @@ type UserWizardInput struct { // Groups and Roles are all groups and roles in the organization. Groups []client.UserGroup Roles []client.Role - // UserNameDomain suggests "first.last@" as the user name. - UserNameDomain string + // ProfileName and Patterns build the suggested user name and email + // (see config.ExpandUserPattern). + ProfileName string + Patterns config.NewUserConfig Out io.Writer Accessible bool @@ -61,7 +66,7 @@ func RunUserWizard(in UserWizardInput) (bool, error) { // Only role and user group assignments can be updated. pages = []*huh.Group{membershipGroup(in, &groupIDs, &roleIDs)} } - form := huh.NewForm(pages...).WithOutput(in.Out).WithAccessible(in.Accessible) + form := huh.NewForm(pages...).WithOutput(in.Out).WithAccessible(in.Accessible).WithKeyMap(suggestionKeyMap()) if err := form.Run(); err != nil { if errors.Is(err, huh.ErrUserAborted) { return false, nil @@ -75,7 +80,10 @@ func RunUserWizard(in UserWizardInput) (bool, error) { u.AliasName = "" } if strings.TrimSpace(u.UserName) == "" { - u.UserName = suggestUserName(u.FirstName, u.LastName, in.UserNameDomain) + u.UserName, _ = in.suggest(in.Patterns.UserNamePattern, u) + } + if strings.TrimSpace(u.Email) == "" { + u.Email, _ = in.suggest(in.Patterns.EmailPattern, u) } } trimUser(u) @@ -96,6 +104,7 @@ func RunUserWizard(in UserWizardInput) (bool, error) { } func identityGroup(in UserWizardInput, u *client.User, auth *string) *huh.Group { + names := []any{&u.FirstName, &u.LastName} return huh.NewGroup( huh.NewSelect[string](). Title("Authentication"). @@ -103,23 +112,110 @@ func identityGroup(in UserWizardInput, u *client.User, auth *string) *huh.Group Value(auth), huh.NewInput().Title("First name").Value(&u.FirstName).Validate(required("first name")), huh.NewInput().Title("Last name").Value(&u.LastName).Validate(required("last name")), - huh.NewInput(). + in.suggestInput(in.Patterns.UserNamePattern, u, names). Title("User name"). - Description("Leave empty to use the suggestion"). - PlaceholderFunc(func() string { - return suggestUserName(u.FirstName, u.LastName, in.UserNameDomain) - }, []any{&u.FirstName, &u.LastName}). Value(&u.UserName). Validate(func(v string) error { - if strings.TrimSpace(v) == "" && suggestUserName(u.FirstName, u.LastName, in.UserNameDomain) == "" { - return errors.New("user name is required") + if strings.TrimSpace(v) == "" { + if _, err := in.suggest(in.Patterns.UserNamePattern, u); err != nil { + return errors.New("user name is required") + } } return nil }), - huh.NewInput().Title("Email").Value(&u.Email).Validate(validEmail), + in.suggestInput(in.Patterns.EmailPattern, u, names). + Title("Email"). + Value(&u.Email). + Validate(func(v string) error { + if strings.TrimSpace(v) == "" { + if s, err := in.suggest(in.Patterns.EmailPattern, u); err == nil { + return validEmail(s) + } + } + return validEmail(v) + }), ).Title("Identity") } +// suggest expands a pattern for the user's current first and last name. +func (in UserWizardInput) suggest(pattern string, u *client.User) (string, error) { + return config.ExpandUserPattern(pattern, config.UserPatternValues{ + FirstName: u.FirstName, + LastName: u.LastName, + ProfileName: in.ProfileName, + Domain: in.Patterns.Domain, + }) +} + +func (in UserWizardInput) suggestions(pattern string, u *client.User) []string { + if s, err := in.suggest(pattern, u); err == nil { + return []string{s} + } + return nil +} + +// suggestionHint describes the suggestion under a field. +func (in UserWizardInput) suggestionHint(pattern string, u *client.User) string { + if _, err := in.suggest(pattern, u); err != nil { + if strings.TrimSpace(u.FirstName) == "" || strings.TrimSpace(u.LastName) == "" { + return "Pattern " + pattern + } + return "No suggestion: " + err.Error() + } + return "Pattern " + pattern + " (→ or ctrl+e completes, empty uses it)" +} + +// suggestField is a huh input that shows a generated value as placeholder +// and fills it in when → or ctrl+e is pressed on an empty field. (The +// built-in suggestion support only completes text that was already typed.) +type suggestField struct { + *huh.Input + suggest func() string +} + +// Update implements tea.Model. +func (f *suggestField) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + if k, ok := msg.(tea.KeyMsg); ok && (k.String() == "right" || k.String() == "ctrl+e") { + if v, _ := f.GetValue().(string); v == "" { + if s := f.suggest(); s != "" { + _, cmd := f.Input.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(s)}) + return f, cmd + } + } + } + _, cmd := f.Input.Update(msg) + return f, cmd +} + +// Title sets the title and keeps the wrapper for chaining. +func (f *suggestField) Title(t string) *suggestField { f.Input.Title(t); return f } + +// Value binds the value and keeps the wrapper for chaining. +func (f *suggestField) Value(v *string) *suggestField { f.Input.Value(v); return f } + +// Validate sets the validator and keeps the wrapper for chaining. +func (f *suggestField) Validate(fn func(string) error) *suggestField { f.Input.Validate(fn); return f } + +// suggestInput builds a suggestField for pattern. +func (in UserWizardInput) suggestInput(pattern string, u *client.User, bindings []any) *suggestField { + sugg := func() string { + s, _ := in.suggest(pattern, u) + return s + } + input := huh.NewInput(). + DescriptionFunc(func() string { return in.suggestionHint(pattern, u) }, bindings). + PlaceholderFunc(sugg, bindings). + SuggestionsFunc(func() []string { return in.suggestions(pattern, u) }, bindings) + return &suggestField{Input: input, suggest: sugg} +} + +// suggestionKeyMap lets the right arrow (and ctrl+e) complete suggestions. +func suggestionKeyMap() *huh.KeyMap { + km := huh.NewDefaultKeyMap() + km.Input.AcceptSuggestion = key.NewBinding(key.WithKeys("right", "ctrl+e"), key.WithHelp("→", "complete")) + return km +} + // ssoGroup asks for the SSO alias name; the create API requires it when // authentication is SSO. Hidden otherwise. func ssoGroup(in UserWizardInput, u *client.User, auth *string) *huh.Group { @@ -269,14 +365,6 @@ func UserChanges(before, after *client.User) []string { return out } -func suggestUserName(first, last, domain string) string { - first, last = strings.TrimSpace(first), strings.TrimSpace(last) - if first == "" || last == "" || domain == "" { - return "" - } - return fmt.Sprintf("%s.%s@%s", strings.ToLower(first), strings.ToLower(last), domain) -} - func required(what string) func(string) error { return func(v string) error { if strings.TrimSpace(v) == "" { diff --git a/internal/tui/userwizard_test.go b/internal/tui/userwizard_test.go index 2103f81..dc344b9 100644 --- a/internal/tui/userwizard_test.go +++ b/internal/tui/userwizard_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/jbrazda/iics-cli/internal/client" + "github.com/jbrazda/iics-cli/internal/config" ) func TestUserChanges(t *testing.T) { @@ -55,15 +56,34 @@ func TestUserSummary(t *testing.T) { } } -func TestSuggestUserName(t *testing.T) { - if got := suggestUserName(" Ann ", "Lee", "x.com"); got != "ann.lee@x.com" { - t.Errorf("got %q", got) +func TestWizardSuggestions(t *testing.T) { + in := UserWizardInput{ProfileName: "dev", Patterns: config.NewUserPatterns(&config.Profile{Username: "admin@acme.com"})} + u := &client.User{FirstName: " Ann ", LastName: "Lee"} + if got := in.suggestions(in.Patterns.UserNamePattern, u); len(got) != 1 || got[0] != "ann.lee.dev@acme.com" { + t.Errorf("user name suggestion = %v", got) + } + if got, _ := in.suggest(in.Patterns.EmailPattern, u); got != "ann.lee@acme.com" { + t.Errorf("email suggestion = %q", got) + } + if hint := in.suggestionHint(in.Patterns.EmailPattern, u); !strings.Contains(hint, "→") { + t.Errorf("hint should mention the complete key: %q", hint) + } + + missing := &client.User{FirstName: "Ann"} + if got := in.suggestions(in.Patterns.EmailPattern, missing); got != nil { + t.Errorf("no suggestion expected without a last name, got %v", got) } - if got := suggestUserName("Ann", "", "x.com"); got != "" { - t.Errorf("missing last name should give no suggestion, got %q", got) + + bad := UserWizardInput{ProfileName: "dev", Patterns: config.NewUserConfig{UserNamePattern: "{nick}@{domain}", Domain: "acme.com"}} + if hint := bad.suggestionHint(bad.Patterns.UserNamePattern, u); !strings.Contains(hint, "unknown placeholder {nick}") { + t.Errorf("unknown placeholder should be reported, got %q", hint) } - if got := suggestUserName("Ann", "Lee", ""); got != "" { - t.Errorf("missing domain should give no suggestion, got %q", got) +} + +func TestSuggestionKeyMap(t *testing.T) { + keys := suggestionKeyMap().Input.AcceptSuggestion.Keys() + if len(keys) != 2 || keys[0] != "right" || keys[1] != "ctrl+e" { + t.Errorf("accept keys = %v", keys) } } From d9d6e4249e0560a5864f8ef0cfe56c6e1bfe2131 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Wed, 30 Sep 2026 08:40:38 -0400 Subject: [PATCH 16/52] feat(user): Show user menu entry with user search; server-side lookups - menu: "Users > Show user" runs user get - user get without --id/--username on a terminal asks for a user name, ID, or part of a name/email; same prompt replaces the two-step search menu in user edit / user delete - GetUser / GetUserByName use the q=userId== / q=userName== filters instead of listing every user (the API supports exact matches only) - SearchUsers also matches first, last and full name and email; several matches open a filterable picker (user name, name, email, state) - tests updated; docs: user.md (finding a user), menu.md, CR-0044 follow-up --- cmd/user.go | 8 +- cmd/user_prompt.go | 100 +++++++++--------- .../pending/CR-0044-interactive-main-menu.md | 14 +++ docs/documentation/menu.md | 2 +- docs/documentation/user.md | 55 +++++++++- internal/client/users.go | 65 ++++++------ internal/client/users_test.go | 23 ++-- internal/menu/entries.go | 1 + 8 files changed, 171 insertions(+), 97 deletions(-) diff --git a/cmd/user.go b/cmd/user.go index 2d60bf0..2581c57 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -259,7 +259,7 @@ func newUserGetCmd() *cobra.Command { cmd := &cobra.Command{ Use: "get", - Short: "Get user details", + Short: "Get user details (searches interactively without --id/--username)", RunE: func(cmd *cobra.Command, args []string) error { c, err := getClient(cmd) if err != nil { @@ -272,6 +272,12 @@ func newUserGetCmd() *cobra.Command { user, err = c.GetUser(context.Background(), id) case userName != "": user, err = c.GetUserByName(context.Background(), userName) + case isInteractiveTTY(): + user, err = promptUserSearch(context.Background(), c) + if err == nil && user == nil { + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Canceled.") + return nil + } default: return fmt.Errorf("--id or --username is required") } diff --git a/cmd/user_prompt.go b/cmd/user_prompt.go index b3e7a50..56ed1ad 100644 --- a/cmd/user_prompt.go +++ b/cmd/user_prompt.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "os" + "sort" + "strings" "github.com/jbrazda/iics-cli/internal/client" "github.com/jbrazda/iics-cli/internal/config" @@ -58,70 +60,66 @@ func promptYesNo(label string, defaultYes bool) (bool, error) { return prompter.Confirm(label, defaultYes) } -// promptUserSearch interactively finds a user by searching by name or exact ID. -// Returns nil if the user cancels. Returns an error if stdin is not a terminal. +// promptUserSearch asks for a user name, user ID, or part of a name or +// email. Exact user names and IDs are looked up on the server; other text +// is matched against user name, first/last name and email, and several +// matches are offered in a filterable list. Returns nil if the user cancels +// (empty input). Returns an error if stdin is not a terminal. func promptUserSearch(ctx context.Context, c *client.Client) (*client.User, error) { if !config.IsTerminal() { return nil, fmt.Errorf("stdin is not a terminal; provide --id or --username flag") } - for { - mode, err := promptSelect("Search user by", []string{"User Name (partial match)", "ID (exact match)"}) + text, err := promptText("User name, ID, or part of a name/email (empty to cancel)", "") if err != nil { return nil, err } - switch mode { - case -1: + if text == "" { return nil, nil - - case 0: - query, qErr := promptText("User Name (partial match)", "") - if qErr != nil { - return nil, fmt.Errorf("reading user name: %w", qErr) - } - if query == "" { - continue - } - users, sErr := c.SearchUsers(ctx, query) - if sErr != nil { - return nil, sErr - } - if len(users) == 0 { - _, _ = fmt.Fprintf(os.Stderr, "No users found matching %q.\n", query) - continue - } - if len(users) == 1 { - return &users[0], nil - } - opts := make([]string, len(users)) - for i, u := range users { - opts[i] = fmt.Sprintf("%s (%s)", u.UserName, u.ID) - } - idx, sErr := promptSelect("Select user", opts) - if sErr != nil { - return nil, sErr - } - if idx < 0 { - continue + } + if u, lerr := c.GetUserByName(ctx, text); lerr == nil { + return u, nil + } + if !strings.ContainsAny(text, " @") { + if u, lerr := c.GetUser(ctx, text); lerr == nil { + return u, nil } - return &users[idx], nil - + } + users, err := c.SearchUsers(ctx, text) + if err != nil { + return nil, err + } + switch len(users) { + case 0: + _, _ = fmt.Fprintf(os.Stderr, "No users match %q.\n", text) + continue case 1: - id, iErr := promptText("User ID (exact match)", "") - if iErr != nil { - return nil, fmt.Errorf("reading user ID: %w", iErr) - } - if id == "" { - continue - } - u, gErr := c.GetUser(ctx, id) - if gErr != nil { - _, _ = fmt.Fprintf(os.Stderr, "Error: %v\n", gErr) - continue - } - return u, nil + return &users[0], nil } + sort.Slice(users, func(i, j int) bool { return strings.ToLower(users[i].UserName) < strings.ToLower(users[j].UserName) }) + idx, err := tui.PickOne(prompter, fmt.Sprintf("%d users match %q", len(users), text), users, userLabel) + if err != nil { + return nil, err + } + if idx >= 0 { + return &users[idx], nil + } + } +} + +// userLabel describes a user in pickers. +func userLabel(u client.User) string { + label := u.UserName + if name := strings.TrimSpace(u.FirstName + " " + u.LastName); name != "" { + label += " " + name + } + if u.Email != "" && !strings.EqualFold(u.Email, u.UserName) { + label += " <" + u.Email + ">" + } + if u.State != "" { + label += " (" + u.State + ")" } + return label } // resolveUser returns the user identified by id or userName flags, falling back to diff --git a/docs/ChangeRequests/pending/CR-0044-interactive-main-menu.md b/docs/ChangeRequests/pending/CR-0044-interactive-main-menu.md index b176f3a..d060322 100644 --- a/docs/ChangeRequests/pending/CR-0044-interactive-main-menu.md +++ b/docs/ChangeRequests/pending/CR-0044-interactive-main-menu.md @@ -152,3 +152,17 @@ flag), grouped by resource: - Verified on `dev`: fresh download with `--progress --verify` (365 MB, checksum verified), script rerun skipped in 1s, interactive prompt with matching file (kept), mismatching file (declined, kept). + +## Follow-up: Show user (2026-09-30) + +- New entry "Users > Show user" (`user get`). Without `--id` / + `--username` on a terminal, `user get` asks for a user name, ID, or part + of a name/email (same prompt now used by `user edit` / `user delete`). +- `GetUser` / `GetUserByName` use the server-side `q=userId==` / + `q=userName==` filters instead of listing every user. Probed on `dev`: the + API supports exact matches only (no wildcard; `=like=` is rejected), so + partial search still lists users page by page and matches user name, + first/last/full name and email. Org limit: 1000 users, groups and roles + combined. +- Verified on `dev`: exact user name and ID lookups, missing ID error, + interactive no-match and partial-match picker. diff --git a/docs/documentation/menu.md b/docs/documentation/menu.md index bb0023c..022f592 100644 --- a/docs/documentation/menu.md +++ b/docs/documentation/menu.md @@ -112,7 +112,7 @@ On a production profile: | Group | Entries | | ----------------------- | ----------------------------------------------------------------------- | | Session | Switch profile, Set as default profile ✎, Login, List profiles, Add profile ✎, Edit active profile ✎ | -| Users | List users, Create user ✎, Edit user groups and roles ✎, Delete user ✎, Change password ✎ | +| Users | List users, Show user (search by user name, ID, or part of a name/email), Create user ✎, Edit user groups and roles ✎, Delete user ✎, Change password ✎ | | User groups | List, Show, Create ✎, Edit roles ✎, Delete ✎ | | Roles | List, Show (with privileges), Create ✎, Edit privileges ✎ | | Privileges | List privileges | diff --git a/docs/documentation/user.md b/docs/documentation/user.md index 71e8342..5bdf895 100644 --- a/docs/documentation/user.md +++ b/docs/documentation/user.md @@ -70,10 +70,59 @@ $users | Where-Object { $_.userName -match "john" } ## user get +Show a user's details (profile fields, groups and roles). + ### Flags -| Flag | Type | Required | Description | -| ------ | ------ | -------- | ----------- | +| Flag | Type | Required | Description | +| ------------ | ------ | -------- | -------------------------------------------- | +| `--id` | string | one of* | User ID | +| `--username` | string | one of* | User name (exact match, case-insensitive) | +| `--fields` | string | | Fields for CSV output | + +\* On a terminal both can be omitted: you are asked for a +[user search](#finding-a-user). + +All [global flags](../../README.md#global-flags) apply. + +### Finding a user + +Without `--id` / `--username` on a terminal (also used by `user edit` and +`user delete`), one prompt accepts a user name, a user ID, or part of a name +or email: + +1. An exact user name or user ID is looked up on the server + (`q=userName==` / `q=userId==`). +2. Otherwise the text is matched (case-insensitive) against user name, first + name, last name, full name and email. One match is used directly; several + are shown in a filterable list with user name, name, email and state. +3. No match asks again; an empty answer cancels. + +The IICS API only filters exact user names and IDs, so partial matching +lists the users page by page (200 per request). An organization has at most +1000 users, user groups and roles combined, so this stays within a few +requests. + +### Examples + +```bash +iics user get --id + +iics user get --username jdoe@example.com --output json + +# Search interactively +iics user get +``` + +```powershell +iics user get --id + +iics user get --username jdoe@example.com --output json + +iics user get +``` + +------ | ------ | -------- | ----------- | | `--id` | string | yes | User ID | All [global flags](../../README.md#global-flags) apply. @@ -223,7 +272,7 @@ changed: the IICS REST API has no working update endpoint for them. The V3 | `--id` | string | | User ID | | `--username` | string | | User name (exact match) | -Without `--id` or `--username`, you are asked to search for the user. +Without `--id` or `--username`, you are asked to [find the user](#finding-a-user). All [global flags](../../README.md#global-flags) apply. diff --git a/internal/client/users.go b/internal/client/users.go index d4bc1f7..681465c 100644 --- a/internal/client/users.go +++ b/internal/client/users.go @@ -98,6 +98,9 @@ type User struct { type UserListOptions struct { Limit int Skip int + // Query is the q filter, e.g. userName==jdoe@acme.com. The API supports + // exact matches on userId and userName only. + Query string } // ListUsers retrieves users. @@ -109,6 +112,9 @@ func (c *Client) ListUsers(ctx context.Context, opts UserListOptions) ([]User, e if opts.Skip > 0 { query["skip"] = strconv.Itoa(opts.Skip) } + if opts.Query != "" { + query["q"] = opts.Query + } var resp []User if err := c.doJSONWithQuery(ctx, http.MethodGet, BaseAPIPathV3+"/users", query, nil, &resp); err != nil { @@ -117,52 +123,42 @@ func (c *Client) ListUsers(ctx context.Context, opts UserListOptions) ([]User, e return resp, nil } -// GetUser retrieves a single user by ID by scanning the users list. +// GetUser retrieves a single user by ID with the q=userId== filter. // The IICS v3 API does not support GET /users/{id}; only DELETE is allowed on that path. func (c *Client) GetUser(ctx context.Context, id string) (*User, error) { - opts := UserListOptions{Limit: 200} - for { - users, err := c.ListUsers(ctx, opts) - if err != nil { - return nil, err - } - for i := range users { - if users[i].ID == id { - return &users[i], nil - } - } - if len(users) < opts.Limit { - break + users, err := c.ListUsers(ctx, UserListOptions{Query: "userId==" + id, Limit: 1}) + if err != nil { + return nil, err + } + for i := range users { + if users[i].ID == id { + return &users[i], nil } - opts.Skip += opts.Limit } return nil, fmt.Errorf("user %q not found", id) } -// GetUserByName finds a user by exact userName match. +// GetUserByName finds a user by exact userName (case-insensitive) with the +// q=userName== filter. func (c *Client) GetUserByName(ctx context.Context, userName string) (*User, error) { - opts := UserListOptions{Limit: 200} - for { - users, err := c.ListUsers(ctx, opts) - if err != nil { - return nil, err - } - for i := range users { - if users[i].UserName == userName { - return &users[i], nil - } - } - if len(users) < opts.Limit { - break + users, err := c.ListUsers(ctx, UserListOptions{Query: "userName==" + userName, Limit: 1}) + if err != nil { + return nil, err + } + for i := range users { + if strings.EqualFold(users[i].UserName, userName) { + return &users[i], nil } - opts.Skip += opts.Limit } return nil, fmt.Errorf("user %q not found", userName) } -// SearchUsers returns all users whose userName contains the given substring (case-insensitive). -func (c *Client) SearchUsers(ctx context.Context, substring string) ([]User, error) { - lower := strings.ToLower(substring) +// SearchUsers returns users whose user name, first name, last name, full +// name or email contains text (case-insensitive). The API only filters +// exact matches, so the users are listed page by page and filtered here; an +// organization has at most 1000 users, groups and roles combined. +func (c *Client) SearchUsers(ctx context.Context, text string) ([]User, error) { + lower := strings.ToLower(strings.TrimSpace(text)) opts := UserListOptions{Limit: 200} var matches []User for { @@ -171,7 +167,8 @@ func (c *Client) SearchUsers(ctx context.Context, substring string) ([]User, err return nil, err } for _, u := range users { - if strings.Contains(strings.ToLower(u.UserName), lower) { + hay := strings.ToLower(strings.Join([]string{u.UserName, u.FirstName, u.LastName, u.FirstName + " " + u.LastName, u.Email}, "\x1f")) + if strings.Contains(hay, lower) { matches = append(matches, u) } } diff --git a/internal/client/users_test.go b/internal/client/users_test.go index 1b92f64..fcd2afd 100644 --- a/internal/client/users_test.go +++ b/internal/client/users_test.go @@ -77,16 +77,18 @@ func TestGetUserNotFound(t *testing.T) { func TestGetUserByName(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if q := r.URL.Query().Get("q"); q != "userName==Alice@example.com" { + t.Errorf("expected q=userName==Alice@example.com, got %q", q) + } users := []User{ {ID: "u1", UserName: "alice@example.com"}, - {ID: "u2", UserName: "bob@example.com"}, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(users) }) c := newTestClient(handler) - user, err := c.GetUserByName(context.Background(), "alice@example.com") + user, err := c.GetUserByName(context.Background(), "Alice@example.com") if err != nil { t.Fatalf("GetUserByName() error: %v", err) } @@ -112,20 +114,27 @@ func TestSearchUsers(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { users := []User{ {ID: "u1", UserName: "alice@example.com"}, - {ID: "u2", UserName: "bob@example.com"}, - {ID: "u3", UserName: "alice.smith@example.com"}, + {ID: "u2", UserName: "bob@example.com", FirstName: "Bob", LastName: "Alison"}, + {ID: "u3", UserName: "a.smith@example.com", FirstName: "Alice", LastName: "Smith"}, + {ID: "u4", UserName: "carol", Email: "Carol.White@example.com"}, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(users) }) c := newTestClient(handler) - results, err := c.SearchUsers(context.Background(), "alice") + results, err := c.SearchUsers(context.Background(), "ALI") if err != nil { t.Fatalf("SearchUsers() error: %v", err) } - if len(results) != 2 { - t.Errorf("expected 2 results for 'alice', got %d", len(results)) + if len(results) != 3 { + t.Errorf("expected 3 results for 'ALI' (user name, last name, first name), got %d", len(results)) + } + for q, want := range map[string]int{"alice smith": 1, "white@": 1, "zzz": 0} { + got, _ := c.SearchUsers(context.Background(), q) + if len(got) != want { + t.Errorf("SearchUsers(%q) = %d results, want %d", q, len(got), want) + } } } diff --git a/internal/menu/entries.go b/internal/menu/entries.go index a0d52d6..7edf993 100644 --- a/internal/menu/entries.go +++ b/internal/menu/entries.go @@ -72,6 +72,7 @@ func DefaultEntries() []Entry { {Group: "Session", Label: "Edit active profile", Description: "Update credentials, region and production flag of the active profile", Args: []string{"profile", "edit"}, ActiveProfileArg: true, NoProfileFlag: true, Writes: true}, {Group: "Users", Label: "List users", Description: "Show all users", Args: []string{"user", "list"}}, + {Group: "Users", Label: "Show user", Description: "Find a user by user name, ID, or part of a name or email and show the details", Args: []string{"user", "get"}}, {Group: "Users", Label: "Create user", Description: "Create a user with the paged wizard", Args: []string{"user", "create", "--interactive"}, Writes: true}, {Group: "Users", Label: "Edit user groups and roles", Description: "Change a user's group and role assignments", Args: []string{"user", "edit"}, Writes: true}, {Group: "Users", Label: "Delete user", Description: "Search for a user and delete it", Args: []string{"user", "delete"}, Writes: true}, From cca4e9ee0b3302c4b1f6446327b40a777e2a8b31 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Wed, 30 Sep 2026 08:51:27 -0400 Subject: [PATCH 17/52] feat(user): reject existing user names in the create wizard - user create --interactive: leaving the User name field looks up the effective name (typed or suggested) server-side and shows 'user "" already exists (ID )'; cached per name, lookup errors do not block - client: FindUserByName (nil when not found); GetUserByName uses it - tests; docs: user.md, CR-0045 follow-up --- cmd/user.go | 11 +++++- .../pending/CR-0045-new-user-name-patterns.md | 9 +++++ docs/documentation/user.md | 3 ++ internal/client/users.go | 15 +++++++- internal/client/users_test.go | 20 ++++++++++ internal/tui/userwizard.go | 37 +++++++++++++++++-- internal/tui/userwizard_test.go | 33 +++++++++++++++++ 7 files changed, 122 insertions(+), 6 deletions(-) diff --git a/cmd/user.go b/cmd/user.go index 2581c57..bff3b52 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -571,8 +571,15 @@ func runUserWizard(ctx context.Context, c *client.Client, existing *client.User) Roles: roles, ProfileName: profileName, Patterns: config.NewUserPatterns(prof), - Out: os.Stderr, - Accessible: prompter.Accessible, + UserExists: func(name string) (string, error) { + existing, lerr := c.FindUserByName(ctx, name) + if lerr != nil || existing == nil { + return "", lerr + } + return existing.ID, nil + }, + Out: os.Stderr, + Accessible: prompter.Accessible, }) if err != nil || !ok { return nil, err diff --git a/docs/ChangeRequests/pending/CR-0045-new-user-name-patterns.md b/docs/ChangeRequests/pending/CR-0045-new-user-name-patterns.md index 44219fd..f279877 100644 --- a/docs/ChangeRequests/pending/CR-0045-new-user-name-patterns.md +++ b/docs/ChangeRequests/pending/CR-0045-new-user-name-patterns.md @@ -81,3 +81,12 @@ wizard. - Verified in a pty on `dev`: placeholders after entering names, `→` fills user name and email, value editable afterwards; wizard canceled (no user created). + +## Follow-up: existing user check (2026-09-30) + +- The User name field of `user create --interactive` checks the effective + name (typed or suggested) with `client.FindUserByName` (server-side + `q=userName==`) when the field is left, and shows + `user "" already exists (ID )`. Results are cached per name; + lookup errors do not block (the create call reports conflicts). +- Verified on `dev`: existing login rejected with its ID, free name accepted. diff --git a/docs/documentation/user.md b/docs/documentation/user.md index 5bdf895..76c7dad 100644 --- a/docs/documentation/user.md +++ b/docs/documentation/user.md @@ -163,6 +163,9 @@ Create a user from a definition file, or interactively with `--interactive`. value in gray once first and last name are entered; `→` or `Ctrl+E` fills it in for editing, and leaving the field empty uses it. The values come from the active profile's [new user patterns](#new-user-patterns). + When you leave the User name field, the name (typed or suggested) is + looked up on the server; an existing name shows + `user "" already exists (ID )` and keeps you on the field. 2. **Single sign-on** - alias name in the identity provider; shown only for SSO, where the API requires it. 3. **Details** - phone, title, description, force password change. diff --git a/internal/client/users.go b/internal/client/users.go index 681465c..7eb3e08 100644 --- a/internal/client/users.go +++ b/internal/client/users.go @@ -141,6 +141,19 @@ func (c *Client) GetUser(ctx context.Context, id string) (*User, error) { // GetUserByName finds a user by exact userName (case-insensitive) with the // q=userName== filter. func (c *Client) GetUserByName(ctx context.Context, userName string) (*User, error) { + u, err := c.FindUserByName(ctx, userName) + if err != nil { + return nil, err + } + if u == nil { + return nil, fmt.Errorf("user %q not found", userName) + } + return u, nil +} + +// FindUserByName is GetUserByName but returns nil without an error when no +// user has that name. +func (c *Client) FindUserByName(ctx context.Context, userName string) (*User, error) { users, err := c.ListUsers(ctx, UserListOptions{Query: "userName==" + userName, Limit: 1}) if err != nil { return nil, err @@ -150,7 +163,7 @@ func (c *Client) GetUserByName(ctx context.Context, userName string) (*User, err return &users[i], nil } } - return nil, fmt.Errorf("user %q not found", userName) + return nil, nil } // SearchUsers returns users whose user name, first name, last name, full diff --git a/internal/client/users_test.go b/internal/client/users_test.go index fcd2afd..c2c3ad2 100644 --- a/internal/client/users_test.go +++ b/internal/client/users_test.go @@ -110,6 +110,26 @@ func TestGetUserByNameNotFound(t *testing.T) { } } +func TestFindUserByName(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + if r.URL.Query().Get("q") == "userName==taken@example.com" { + json.NewEncoder(w).Encode([]User{{ID: "u7", UserName: "taken@example.com"}}) + return + } + json.NewEncoder(w).Encode([]User{}) + }) + c := newTestClient(handler) + u, err := c.FindUserByName(context.Background(), "taken@example.com") + if err != nil || u == nil || u.ID != "u7" { + t.Errorf("expected u7, got %+v, %v", u, err) + } + u, err = c.FindUserByName(context.Background(), "free@example.com") + if err != nil || u != nil { + t.Errorf("expected nil, nil for a free name, got %+v, %v", u, err) + } +} + func TestSearchUsers(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { users := []User{ diff --git a/internal/tui/userwizard.go b/internal/tui/userwizard.go index 9ffc47c..f460106 100644 --- a/internal/tui/userwizard.go +++ b/internal/tui/userwizard.go @@ -30,15 +30,21 @@ type UserWizardInput struct { // (see config.ExpandUserPattern). ProfileName string Patterns config.NewUserConfig + // UserExists returns the ID of an existing user with that user name, or + // "" when there is none (create only). Lookup errors do not block. + UserExists func(userName string) (string, error) Out io.Writer Accessible bool + + existsCache map[string]string } // RunUserWizard collects user fields in a paged form (Identity, Details, // Membership) followed by a review. On update only the Membership page is // shown. It returns false if the user canceled. func RunUserWizard(in UserWizardInput) (bool, error) { + in.existsCache = map[string]string{} before := cloneUser(in.User) u := in.User @@ -116,12 +122,15 @@ func identityGroup(in UserWizardInput, u *client.User, auth *string) *huh.Group Title("User name"). Value(&u.UserName). Validate(func(v string) error { - if strings.TrimSpace(v) == "" { - if _, err := in.suggest(in.Patterns.UserNamePattern, u); err != nil { + name := strings.TrimSpace(v) + if name == "" { + s, err := in.suggest(in.Patterns.UserNamePattern, u) + if err != nil { return errors.New("user name is required") } + name = s } - return nil + return in.checkNewUserName(name) }), in.suggestInput(in.Patterns.EmailPattern, u, names). Title("Email"). @@ -137,6 +146,28 @@ func identityGroup(in UserWizardInput, u *client.User, auth *string) *huh.Group ).Title("Identity") } +// checkNewUserName reports an error when a user with that name exists. +// Results are cached per name; lookup errors are ignored (the create call +// reports a real conflict). +func (in UserWizardInput) checkNewUserName(name string) error { + if in.Update || in.UserExists == nil || name == "" { + return nil + } + key := strings.ToLower(name) + id, ok := in.existsCache[key] + if !ok { + found, err := in.UserExists(name) + if err == nil { + in.existsCache[key] = found + id = found + } + } + if id != "" { + return fmt.Errorf("user %q already exists (ID %s)", name, id) + } + return nil +} + // suggest expands a pattern for the user's current first and last name. func (in UserWizardInput) suggest(pattern string, u *client.User) (string, error) { return config.ExpandUserPattern(pattern, config.UserPatternValues{ diff --git a/internal/tui/userwizard_test.go b/internal/tui/userwizard_test.go index dc344b9..9762fc4 100644 --- a/internal/tui/userwizard_test.go +++ b/internal/tui/userwizard_test.go @@ -1,6 +1,7 @@ package tui import ( + "errors" "reflect" "strings" "testing" @@ -80,6 +81,38 @@ func TestWizardSuggestions(t *testing.T) { } } +func TestCheckNewUserName(t *testing.T) { + calls := 0 + in := UserWizardInput{existsCache: map[string]string{}, UserExists: func(n string) (string, error) { + calls++ + switch n { + case "taken@acme.com", "Taken@acme.com": + return "u7", nil + case "broken@acme.com": + return "", errors.New("network down") + } + return "", nil + }} + if err := in.checkNewUserName("taken@acme.com"); err == nil || !strings.Contains(err.Error(), `user "taken@acme.com" already exists (ID u7)`) { + t.Errorf("expected exists error, got %v", err) + } + _ = in.checkNewUserName("Taken@acme.com") // cached (case-insensitive) + if calls != 1 { + t.Errorf("lookups should be cached per name, got %d calls", calls) + } + if err := in.checkNewUserName("free@acme.com"); err != nil { + t.Errorf("free name should pass, got %v", err) + } + if err := in.checkNewUserName("broken@acme.com"); err != nil { + t.Errorf("lookup errors must not block, got %v", err) + } + upd := in + upd.Update = true + if err := upd.checkNewUserName("taken@acme.com"); err != nil { + t.Errorf("no check on update, got %v", err) + } +} + func TestSuggestionKeyMap(t *testing.T) { keys := suggestionKeyMap().Input.AcceptSuggestion.Keys() if len(keys) != 2 || keys[0] != "right" || keys[1] != "ctrl+e" { From ae4ef8549b0216703cb3e2e109c6bfd00fbbc324 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Thu, 1 Oct 2026 19:59:22 -0400 Subject: [PATCH 18/52] fix(user): keep last direct role; skip roles inherited from groups IICS rejects removing a user's last directly assigned role with V3API_IDSError_044 ("The group must have at least one role"), even when user groups grant other roles. - user update-roles and user edit reject such a change before any API call; the edit wizard review cannot be applied in that case - user update-roles skips additions of roles granted by the user's groups and reports them on stderr - the user wizard hides roles granted by the selected groups (except current direct roles) and lists them in the field description - document both rules in docs/documentation/user.md --- cmd/user_membership.go | 60 +++++++++- docs/documentation/user.md | 24 +++- docs/issues/pending/Bug_edit_user_roles.md | 124 +++++++++++++++++++++ internal/client/usermembership.go | 59 +++++++++- internal/client/usermembership_test.go | 56 ++++++++++ internal/tui/userwizard.go | 74 ++++++++++-- internal/tui/userwizard_test.go | 20 ++++ 7 files changed, 395 insertions(+), 22 deletions(-) create mode 100644 docs/issues/pending/Bug_edit_user_roles.md diff --git a/cmd/user_membership.go b/cmd/user_membership.go index f3985c0..fdb3622 100644 --- a/cmd/user_membership.go +++ b/cmd/user_membership.go @@ -16,16 +16,29 @@ import ( // membershipKind describes one of the two user membership types. type membershipKind struct { use, noun, plural string + // longNote is appended to the command's long help. + longNote string // available returns every entry that can be assigned. available func(ctx context.Context, c *client.Client) ([]client.MemberName, error) // current returns the assignment names (MemberName.Name form) of the user. current func(u *client.User) []string - add func(c *client.Client, ctx context.Context, userID string, names []string) error - remove func(c *client.Client, ctx context.Context, userID string, names []string) error + // inherited, when set, returns the names the user already has through + // user groups; requested additions of these are skipped. + inherited func(ctx context.Context, c *client.Client, u *client.User) ([]string, error) + // check, when set, validates a planned change before it is applied. + check func(u *client.User, current, add, remove []string) error + add func(c *client.Client, ctx context.Context, userID string, names []string) error + remove func(c *client.Client, ctx context.Context, userID string, names []string) error } var userRoleMembership = membershipKind{ use: "update-roles", noun: "role", plural: "roles", + longNote: ` + +Roles the user already has through a user group are not assigned directly +(reported as skipped). IICS requires a user to keep at least one directly +assigned role, so a change that would remove the last one fails before any +change is made; add a replacement role in the same command.`, // The role assignment endpoints match the role's display name. available: func(ctx context.Context, c *client.Client) ([]client.MemberName, error) { roles, err := c.ListRoles(ctx, client.RoleListOptions{}) @@ -45,6 +58,28 @@ var userRoleMembership = membershipKind{ } return names }, + inherited: func(ctx context.Context, c *client.Client, u *client.User) ([]string, error) { + if len(u.Groups) == 0 { + return nil, nil + } + groups, err := listAllUserGroups(ctx, c) + if err != nil { + return nil, err + } + ids := make([]string, len(u.Groups)) + for i, g := range u.Groups { + ids[i] = g.ID + } + var names []string + for _, r := range client.InheritedRoles(groups, ids) { + names = append(names, client.RoleMemberName(r.RoleName, r.DisplayName)) + } + return names, nil + }, + // IICS rejects removing a user's last direct role. + check: func(u *client.User, current, add, remove []string) error { + return client.CheckKeepsDirectRole(u.UserName, current, add, remove) + }, add: (*client.Client).AddUserRoles, remove: (*client.Client).RemoveUserRoles, } @@ -88,7 +123,7 @@ func newUserMembershipCmd(k membershipKind) *cobra.Command { value removes all %[1]s) and cannot be combined with them. Names match case-insensitively and duplicates are ignored. Unknown names, or a name in both --add and --remove, fail before any change is made. Additions are -applied before removals. The resulting user is printed in the --output format.`, k.plural), +applied before removals. The resulting user is printed in the --output format.%[2]s`, k.plural, k.longNote), Example: fmt.Sprintf(` iics user %[1]s --username jdoe@example.com --add "Designer,Monitor" iics user %[1]s --id --add Designer --remove Monitor iics user %[1]s --username jdoe@example.com --replace "Designer" -o json`, k.use), @@ -108,14 +143,26 @@ applied before removals. The resulting user is printed in the --output format.`, if err != nil { return err } - plan, err := client.PlanMembership(k.noun, available, k.current(user), req) + if k.inherited != nil { + if req.Inherited, err = k.inherited(ctx, c, user); err != nil { + return err + } + } + current := k.current(user) + plan, err := client.PlanMembership(k.noun, available, current, req) if err != nil { return err } + if k.check != nil { + if err = k.check(user, current, plan.Add, plan.Remove); err != nil { + return err + } + } w := cmd.ErrOrStderr() reportSkipped(w, "Already assigned", plan.AlreadyAssigned) reportSkipped(w, "Not assigned", plan.NotAssigned) + reportSkipped(w, "Inherited from user group", plan.Inherited) if len(plan.Add) == 0 && len(plan.Remove) == 0 { _, _ = fmt.Fprintf(w, "No %s changes for %s.\n", k.noun, user.UserName) } @@ -168,6 +215,11 @@ applied before removals. The resulting user is printed in the --output format.`, func applyUserMembership(ctx context.Context, c *client.Client, w io.Writer, before, after *client.User) error { for _, k := range []membershipKind{userGroupMembership, userRoleMembership} { add, remove := client.DiffPrivileges(k.current(before), k.current(after)) + if k.check != nil { + if err := k.check(before, k.current(before), add, remove); err != nil { + return err + } + } if len(add) > 0 { if err := k.add(c, ctx, before.ID, add); err != nil { return err diff --git a/docs/documentation/user.md b/docs/documentation/user.md index 76c7dad..d7c8438 100644 --- a/docs/documentation/user.md +++ b/docs/documentation/user.md @@ -170,7 +170,9 @@ Create a user from a definition file, or interactively with `--interactive`. SSO, where the API requires it. 3. **Details** - phone, title, description, force password change. 4. **Membership** - user groups and roles as filterable checklists - (`/` filter, `Space` toggle). + (`/` filter, `Space` toggle). Roles granted by the selected user groups + are left out of the role list and named in its description; the list + follows the group selection. 5. **Review** - summary with **Create user**, **Back to editing** or **Cancel**. At least one user group or role is required. @@ -253,7 +255,11 @@ a terminal; for scripts use [user update-roles](#user-update-roles) and The form shows the user's name, email, authentication and state read-only, then the user groups and roles as filterable checklists with the current assignments pre-checked, and ends with a review (`+ Group: ...`, -`- Role: ...`). Changes are applied through the V3 `addGroups` / +`- Role: ...`). Roles granted by the selected user groups are left out of the +role list and named in its description, except roles the user already has +directly, which stay listed so they can be removed. The review cannot be +applied when it would remove the user's last directly assigned role (see +[user update-roles](#user-update-roles)). Changes are applied through the V3 `addGroups` / `removeGroups` / `addRoles` / `removeRoles` endpoints (additions first), and the resulting user is printed. See [Interactive prompts](interactive.md) for keys. @@ -316,12 +322,21 @@ Rules: - `--id` and `--username` are mutually exclusive; one is required. - `--add` and `--remove` can be combined. `--replace` cannot be combined with - either. `--replace=` (empty) removes all roles. + either. `--replace=` (empty) would remove all roles, which IICS does not + allow (see below). - Names match case-insensitively, by role name or display name. Duplicates are ignored. Unknown names, or a name in both `--add` and `--remove`, fail before any change is made. - Roles already assigned (for `--add`) or not assigned (for `--remove`) are skipped and reported on stderr. +- Roles the user already has through a user group are not assigned directly; + they are skipped and reported on stderr as + `Inherited from user group (skipped)`. +- IICS requires a user to keep at least one directly assigned role, even + when user groups grant other roles. Removing the last one fails in the API + with `V3API_IDSError_044` ("The group must have at least one role"), so the + command rejects such a change before calling the API. Add a replacement + role in the same command (`--add New --remove Old`, or `--replace New`). - Additions are applied before removals. - The role assignment endpoints match the role's **display name** (for example `Data Integration Data Previewer` for the `Data Preview` role); the @@ -358,7 +373,8 @@ iics user update-roles --username jdoe@example.com --replace Designer -o json Add, remove or replace the user groups assigned to a user. Uses `PUT /public/core/v3/users//addGroups` and `.../removeGroups`. Flags and rules are the same as [user update-roles](#user-update-roles), with user group -names instead of role names. +names instead of role names; the inherited-role and last-direct-role rules +apply to roles only. ### Examples diff --git a/docs/issues/pending/Bug_edit_user_roles.md b/docs/issues/pending/Bug_edit_user_roles.md new file mode 100644 index 0000000..4a1dd31 --- /dev/null +++ b/docs/issues/pending/Bug_edit_user_roles.md @@ -0,0 +1,124 @@ +# BUG: removing a user's last direct role fails with V3API_IDSError_044 + +--- + +## Symptoms + +Removing a role from a user fails with HTTP 400: + +```text +V3API_IDSError_048: Failed to remove role(s) from the user 3PH7U8cFuoxbnZov19KWE0 + V3API_IDSError_044: The group must have at least one role.Selected operation + will result in Group without any role +``` + +--- + +## Command / Reproduction Steps + +Observed on the `dev` profile on 2026-09-30. The user had one direct role +(`CommandTask`) and was a member of the `Developer` user group, which also +grants `CommandTask`. + +```bash +iics user update-roles --username test.user.dev@natl.com --remove CommandTask +``` + +The same happens in `iics user edit` when the last role is unchecked. + +--- + +## Analysis + +The error is about the user's direct roles, not the `Developer` group. +Once the user had a second direct role (`DummyTest`), removing `CommandTask` +succeeded, although `Developer` still grants it. Adding and then removing +`CustomerAPI` next to `DummyTest` also succeeded. + +IICS appears to keep a user's direct roles in a hidden per-user group that +must keep at least one role, so `removeRoles` cannot leave a user with no +direct role, whatever the user's groups grant. + +Assigning a role directly that a user group already grants has no effect on +access and only adds to the confusion. + +--- + +## Fix + +- `client.CheckKeepsDirectRole` rejects a change that would leave no direct + role. `user update-roles` and `user edit` (`applyUserMembership`) call it + before any API call; the edit wizard review cannot be applied in that case. +- `client.InheritedRoles` computes the roles granted by user groups. + `user update-roles` skips additions of inherited roles (reported on stderr); + the user wizard hides roles granted by the selected groups (except roles + assigned directly now) and lists them in the field description. +- Docs: `docs/documentation/user.md` (`user create`, `user edit`, + `user update-roles`, `user update-groups`). + +--- + +## Verification + +```bash +iics user update-roles --id 3PH7U8cFuoxbnZov19KWE0 --remove DummyTest +# Error: cannot remove every role assigned directly to test.user.dev@natl.com ... +iics user update-roles --id 3PH7U8cFuoxbnZov19KWE0 --add CommandTask +# Inherited from user group (skipped): CommandTask +``` + +The wizard role list was covered by unit tests only; it was not driven in a +terminal. + +--- + +## Initial Analysis (superseded) + +The report as first filed. Its explanation (removing a role inherited from a +group) did not hold up; see Analysis above. + +```text +That error indicates you're not removing a role directly from the user. You're attempting to remove a role assignment that comes from a group membership, and the operation would leave that group with zero roles assigned. The IICS API is blocking the change. + +Key error: + +JSON +{ +"code": "V3API_IDSError_044", +"message": "The group must have at least one role. Selected operation will result in Group without any role" +} +Show more lines + +This means: + +The user is a member of one or more groups. +The role you're removing is likely assigned through that group. +IICS enforces a rule that a group must always have at least one role. +Removing the last role from the group is not allowed. + +The error is returned while processing the user request: + +JSON +V3API_IDSError_048 +Failed to remove role(s) from the user +`` +Show more lines + +but the underlying cause is the group validation failure. The platform exposes user, group, and role management through the same API area. + +Things to check + +Get the user's group memberships. + +For each group, inspect the roles assigned to the group. + +Determine whether the role you're removing is: + +Directly assigned to the user, or +Inherited through a group. + +If it's the only role on that group, you must either: + +Assign another role to the group first, or +Remove the user from the group instead of removing the group's role. +``` diff --git a/internal/client/usermembership.go b/internal/client/usermembership.go index 30d440b..b3637a6 100644 --- a/internal/client/usermembership.go +++ b/internal/client/usermembership.go @@ -42,6 +42,9 @@ type MembershipPlan struct { // AlreadyAssigned and NotAssigned are requested changes that are no-ops. AlreadyAssigned []string NotAssigned []string + // Inherited are requested additions skipped because a user group of the + // user already grants them. + Inherited []string } // MembershipRequest describes requested role or user group changes. @@ -51,6 +54,9 @@ type MembershipRequest struct { Replace []string // ReplaceSet is true when Replace was given (an empty Replace removes all). ReplaceSet bool + // Inherited lists names (MemberName.Name form) the user already has + // through user groups; they are not added. + Inherited []string } // MemberName is an assignable role or user group. Name is the value the @@ -139,12 +145,20 @@ func PlanMembership(kind string, available []MemberName, current []string, req M for _, n := range current { cur[n] = true } + inherited := make(map[string]bool, len(req.Inherited)) + for _, n := range req.Inherited { + inherited[n] = true + } if req.ReplaceSet { want := make(map[string]bool, len(replace)) for _, n := range replace { want[n] = true - if !cur[n] { + switch { + case cur[n]: + case inherited[n]: + plan.Inherited = append(plan.Inherited, n) + default: plan.Add = append(plan.Add, n) } } @@ -168,9 +182,12 @@ func PlanMembership(kind string, available []MemberName, current []string, req M return plan, fmt.Errorf("%s(s) in both --add and --remove: %s", kind, strings.Join(conflict, ", ")) } for _, n := range add { - if cur[n] { + switch { + case cur[n]: plan.AlreadyAssigned = append(plan.AlreadyAssigned, n) - } else { + case inherited[n]: + plan.Inherited = append(plan.Inherited, n) + default: plan.Add = append(plan.Add, n) } } @@ -183,8 +200,42 @@ func PlanMembership(kind string, available []MemberName, current []string, req M } } - for _, s := range [][]string{plan.Add, plan.Remove, plan.AlreadyAssigned, plan.NotAssigned} { + for _, s := range [][]string{plan.Add, plan.Remove, plan.AlreadyAssigned, plan.NotAssigned, plan.Inherited} { sort.Strings(s) } return plan, nil } + +// InheritedRoles returns the roles granted by the user groups with the given +// IDs, keyed by role ID. +func InheritedRoles(groups []UserGroup, groupIDs []string) map[string]UserRole { + want := make(map[string]bool, len(groupIDs)) + for _, id := range groupIDs { + want[id] = true + } + out := make(map[string]UserRole) + for _, g := range groups { + if !want[g.ID] { + continue + } + for _, r := range g.Roles { + if _, ok := out[r.ID]; !ok { + out[r.ID] = r + } + } + } + return out +} + +// CheckKeepsDirectRole returns an error when removing roles (after adding +// roles) would leave a user with no directly assigned role. IICS rejects that +// with V3API_IDSError_044 ("The group must have at least one role") even when +// the user's groups grant roles. current, add and remove are role names as +// planned by PlanMembership (add not assigned, remove assigned). +func CheckKeepsDirectRole(userName string, current, add, remove []string) error { + if len(remove) == 0 || len(current)+len(add)-len(remove) > 0 { + return nil + } + return fmt.Errorf("cannot remove every role assigned directly to %s: IICS requires a user to keep at least one direct role (roles from user groups do not count); add a replacement role in the same change or keep one of: %s", + userName, strings.Join(remove, ", ")) +} diff --git a/internal/client/usermembership_test.go b/internal/client/usermembership_test.go index f29accb..168fd75 100644 --- a/internal/client/usermembership_test.go +++ b/internal/client/usermembership_test.go @@ -113,3 +113,59 @@ func TestRoleMemberName(t *testing.T) { t.Errorf("got %q", got) } } + +func TestPlanMembershipInherited(t *testing.T) { + avail := []MemberName{{Name: "Designer"}, {Name: "Monitor"}, {Name: "Admin"}} + req := MembershipRequest{Add: []string{"designer", "Monitor"}, Inherited: []string{"Designer", "Admin"}} + plan, err := PlanMembership("role", avail, nil, req) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(plan.Add, []string{"Monitor"}) || !reflect.DeepEqual(plan.Inherited, []string{"Designer"}) { + t.Errorf("add=%v inherited=%v", plan.Add, plan.Inherited) + } + + // A direct role that is also inherited stays assigned under --replace. + req = MembershipRequest{Replace: []string{"Admin", "Designer"}, ReplaceSet: true, Inherited: []string{"Admin", "Designer"}} + plan, err = PlanMembership("role", avail, []string{"Admin", "Monitor"}, req) + if err != nil { + t.Fatal(err) + } + if len(plan.Add) != 0 || !reflect.DeepEqual(plan.Remove, []string{"Monitor"}) || !reflect.DeepEqual(plan.Inherited, []string{"Designer"}) { + t.Errorf("add=%v remove=%v inherited=%v", plan.Add, plan.Remove, plan.Inherited) + } +} + +func TestInheritedRoles(t *testing.T) { + groups := []UserGroup{ + {ID: "g1", Roles: []UserRole{{ID: "r1", RoleName: "Designer"}, {ID: "r2", RoleName: "Monitor"}}}, + {ID: "g2", Roles: []UserRole{{ID: "r2", RoleName: "Monitor"}, {ID: "r3", RoleName: "Admin"}}}, + } + got := InheritedRoles(groups, []string{"g1"}) + if len(got) != 2 || got["r1"].RoleName != "Designer" || got["r2"].RoleName != "Monitor" { + t.Errorf("InheritedRoles(g1) = %v", got) + } + if got := InheritedRoles(groups, nil); len(got) != 0 { + t.Errorf("InheritedRoles(nil) = %v", got) + } +} + +func TestCheckKeepsDirectRole(t *testing.T) { + tests := []struct { + name string + current, add, remove []string + wantErr bool + }{ + {"remove last", []string{"A"}, nil, []string{"A"}, true}, + {"remove all", []string{"A", "B"}, nil, []string{"A", "B"}, true}, + {"swap", []string{"A"}, []string{"B"}, []string{"A"}, false}, + {"keep one", []string{"A", "B"}, nil, []string{"A"}, false}, + {"no removal", nil, nil, nil, false}, + } + for _, tt := range tests { + err := CheckKeepsDirectRole("u@x.com", tt.current, tt.add, tt.remove) + if (err != nil) != tt.wantErr { + t.Errorf("%s: err = %v, wantErr %v", tt.name, err, tt.wantErr) + } + } +} diff --git a/internal/tui/userwizard.go b/internal/tui/userwizard.go index f460106..f66bb55 100644 --- a/internal/tui/userwizard.go +++ b/internal/tui/userwizard.go @@ -60,17 +60,20 @@ func RunUserWizard(in UserWizardInput) (bool, error) { for _, r := range u.Roles { roleIDs = append(roleIDs, r.ID) } + // Roles assigned directly now stay selectable even when a group grants + // them, so they can still be removed. + direct := toSet(roleIDs) for { pages := []*huh.Group{ identityGroup(in, u, &auth), ssoGroup(in, u, &auth), detailsGroup(u), - membershipGroup(in, &groupIDs, &roleIDs), + membershipGroup(in, direct, &groupIDs, &roleIDs), } if in.Update { // Only role and user group assignments can be updated. - pages = []*huh.Group{membershipGroup(in, &groupIDs, &roleIDs)} + pages = []*huh.Group{membershipGroup(in, direct, &groupIDs, &roleIDs)} } form := huh.NewForm(pages...).WithOutput(in.Out).WithAccessible(in.Accessible).WithKeyMap(suggestionKeyMap()) if err := form.Run(); err != nil { @@ -93,6 +96,7 @@ func RunUserWizard(in UserWizardInput) (bool, error) { } } trimUser(u) + roleIDs = dropInherited(roleIDs, client.InheritedRoles(in.Groups, groupIDs), direct) u.Groups = groupsByID(in.Groups, groupIDs) u.Roles = rolesByID(in.Roles, roleIDs) @@ -270,7 +274,7 @@ func detailsGroup(u *client.User) *huh.Group { ).Title("Details") } -func membershipGroup(in UserWizardInput, groupIDs, roleIDs *[]string) *huh.Group { +func membershipGroup(in UserWizardInput, direct map[string]bool, groupIDs, roleIDs *[]string) *huh.Group { groups := append([]client.UserGroup(nil), in.Groups...) sort.SliceStable(groups, func(i, j int) bool { return strings.ToLower(groups[i].UserGroupName) < strings.ToLower(groups[j].UserGroupName) @@ -283,9 +287,14 @@ func membershipGroup(in UserWizardInput, groupIDs, roleIDs *[]string) *huh.Group sort.SliceStable(roles, func(i, j int) bool { return strings.ToLower(roles[i].RoleName) < strings.ToLower(roles[j].RoleName) }) - rOpts := make([]huh.Option[string], len(roles)) - for i, r := range roles { - rOpts[i] = huh.NewOption(optionLabel(r.RoleName, r.Description), r.ID) + // Roles granted by the selected groups are hidden (unless assigned + // directly now) and listed in the description; both follow the group + // selection. + rOpts := func() []huh.Option[string] { + return roleOptions(roles, client.InheritedRoles(in.Groups, *groupIDs), direct) + } + rDesc := func() string { + return inheritedDescription(client.InheritedRoles(in.Groups, *groupIDs)) } var fields []huh.Field @@ -305,13 +314,13 @@ func membershipGroup(in UserWizardInput, groupIDs, roleIDs *[]string) *huh.Group Height(min(len(gOpts)+3, 12)). Value(groupIDs)) } - if len(rOpts) > 0 { + if len(roles) > 0 { fields = append(fields, huh.NewMultiSelect[string](). Title("Roles"). - Description("space toggle, / filter, ctrl+a all"). - Options(rOpts...). + DescriptionFunc(rDesc, groupIDs). + OptionsFunc(rOpts, groupIDs). Filterable(true). - Height(min(len(rOpts)+3, 12)). + Height(min(len(roles)+3, 12)). Value(roleIDs)) } if len(fields) == 0 { @@ -329,6 +338,12 @@ func reviewUser(in UserWizardInput, before, after *client.User) (string, error) if !canApply { lines = []string{"No changes."} } + // IICS rejects removing a user's last direct role. + if len(before.Roles) > 0 && len(after.Roles) == 0 { + canApply = false + lines = append(lines, "", "Keep at least one directly assigned role: IICS does not allow", + "removing the last one (roles from user groups do not count).") + } return confirmReview(in.Out, in.Accessible, "Update user "+after.UserName, lines, "Apply changes", canApply) } lines := UserSummary(after) @@ -428,6 +443,45 @@ func optionLabel(name, desc string) string { return name + " - " + desc } +// roleOptions builds the role options, leaving out roles in inherited that +// are not in direct. +func roleOptions(roles []client.Role, inherited map[string]client.UserRole, direct map[string]bool) []huh.Option[string] { + opts := make([]huh.Option[string], 0, len(roles)) + for _, r := range roles { + if _, ok := inherited[r.ID]; ok && !direct[r.ID] { + continue + } + opts = append(opts, huh.NewOption(optionLabel(r.RoleName, r.Description), r.ID)) + } + return opts +} + +func inheritedDescription(inherited map[string]client.UserRole) string { + const help = "space toggle, / filter, ctrl+a all" + if len(inherited) == 0 { + return help + } + names := make([]string, 0, len(inherited)) + for _, r := range inherited { + names = append(names, r.RoleName) + } + sort.Strings(names) + return help + "\nInherited from selected groups (not listed): " + strings.Join(names, ", ") +} + +// dropInherited removes from ids the roles in inherited that are not in +// direct (a role selected before a group granting it was selected). +func dropInherited(ids []string, inherited map[string]client.UserRole, direct map[string]bool) []string { + out := ids[:0:0] + for _, id := range ids { + if _, ok := inherited[id]; ok && !direct[id] { + continue + } + out = append(out, id) + } + return out +} + func trimUser(u *client.User) { for _, s := range []*string{&u.FirstName, &u.LastName, &u.UserName, &u.Email, &u.Phone, &u.Title, &u.Description} { *s = strings.TrimSpace(*s) diff --git a/internal/tui/userwizard_test.go b/internal/tui/userwizard_test.go index 9762fc4..645e5b4 100644 --- a/internal/tui/userwizard_test.go +++ b/internal/tui/userwizard_test.go @@ -157,3 +157,23 @@ func TestMembershipByID(t *testing.T) { t.Errorf("rolesByID(nil) = %+v", r) } } + +func TestRoleOptionsHideInherited(t *testing.T) { + roles := []client.Role{{ID: "r1", RoleName: "Designer"}, {ID: "r2", RoleName: "Monitor"}, {ID: "r3", RoleName: "Admin"}} + inherited := map[string]client.UserRole{"r1": {ID: "r1", RoleName: "Designer"}, "r2": {ID: "r2", RoleName: "Monitor"}} + direct := map[string]bool{"r2": true} + + var got []string + for _, o := range roleOptions(roles, inherited, direct) { + got = append(got, o.Value) + } + if !reflect.DeepEqual(got, []string{"r2", "r3"}) { + t.Errorf("roleOptions = %v, want [r2 r3]", got) + } + if ids := dropInherited([]string{"r1", "r2", "r3"}, inherited, direct); !reflect.DeepEqual(ids, []string{"r2", "r3"}) { + t.Errorf("dropInherited = %v, want [r2 r3]", ids) + } + if d := inheritedDescription(inherited); !strings.Contains(d, "not listed): Designer, Monitor") { + t.Errorf("description = %q", d) + } +} From e1d5aa93dff05eeedb5ce242a8cc5813b6ac3703 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Thu, 1 Oct 2026 20:49:51 -0400 Subject: [PATCH 19/52] fix(menu): clear the screen before each menu entry runs The menu runs in the alternate screen while commands run on the normal screen, which was only cleared after the "Press Enter" pause. The first command showed the shell output from before iics started, and entries that returned to the menu without a pause (profile switch, declined production confirmation, empty prompt) left their prompts above the next command. Clear the screen as soon as an entry is chosen. --- docs/documentation/menu.md | 3 ++- internal/menu/loop.go | 11 +++++++++-- internal/menu/menu_test.go | 25 +++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/docs/documentation/menu.md b/docs/documentation/menu.md index 022f592..74af5fb 100644 --- a/docs/documentation/menu.md +++ b/docs/documentation/menu.md @@ -77,7 +77,8 @@ enter run · / filter · p profile · ? help · q quit ## Running a command -1. The command line is printed, for example `$ iics role edit --profile dev`. +1. The screen is cleared and the command line is printed, for example + `$ iics role edit --profile dev`. 2. The command runs as its own process in the terminal, exactly as if you had typed it. Global flags the menu was started with (for example `--config`, `--theme`, `--no-color`) are passed on. diff --git a/internal/menu/loop.go b/internal/menu/loop.go index a087b1a..d3758de 100644 --- a/internal/menu/loop.go +++ b/internal/menu/loop.go @@ -162,9 +162,16 @@ func Run(d Deps, startProfile string) error { if err != nil { return err } - switch res.Kind { - case ResultQuit: + if res.Kind == ResultQuit { return nil + } + // Start every entry on a clean screen: the menu runs in the alternate + // screen, so the normal screen still shows the shell before iics or + // prompts from entries that returned to the menu without a pause. + if d.ClearScreen != nil { + d.ClearScreen() + } + switch res.Kind { case ResultSwitchProfile: if picked, perr := d.pickProfile(st.Profiles, profile); perr != nil { return perr diff --git a/internal/menu/menu_test.go b/internal/menu/menu_test.go index 0f8fc06..32a90c8 100644 --- a/internal/menu/menu_test.go +++ b/internal/menu/menu_test.go @@ -326,3 +326,28 @@ func TestLineMenu(t *testing.T) { t.Error("after adding a profile all entries should be listed") } } + +func TestLoopClearsBeforeEachEntry(t *testing.T) { + prod := State{HasProfile: true, Profiles: twoProfiles, Header: Header{Production: true}} + // A declined write returns straight to the menu; the next entry must + // still start on a clean screen. + h := newHarness("n\n", []Result{ + {Kind: ResultRun, Entry: testEntries[3]}, + {Kind: ResultRun, Entry: testEntries[2]}, + }, map[string]State{"prd": prod}) + h.deps.ClearScreen = func() { h.out.WriteString("") } + if err := Run(h.deps, "prd"); err != nil { + t.Fatal(err) + } + out := h.out.String() + cmd := strings.Index(out, "$ iics role list") + if cmd < 0 { + t.Fatalf("unexpected output:\n%s", out) + } + if !strings.HasPrefix(out, "Run \"Edit role privileges\" against PRODUCTION") { + t.Errorf("screen not cleared before the confirmation:\n%s", out) + } + if !strings.HasSuffix(out[:cmd], "") { + t.Errorf("screen not cleared before the command:\n%s", out) + } +} From bf246b87b1189025afba62faf74a602569e88be6 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Thu, 1 Oct 2026 21:38:35 -0400 Subject: [PATCH 20/52] feat(profile): profile wizard and global settings editor (CR-0046) - profile add, profile edit, login (missing profile) and first-run setup share one paged form: connection (region list or custom login URL), options, keychain, per-profile new user overrides, review - global newUser defaults with per-field profile overrides; profile show lists effective values and their source - new "iics config edit" for global settings: new user defaults, table theme with preview, header color, colors, responsive tables, HTTP timeout, main menu; menu entry under Session - Config.Save now writes httpTimeout, ui and newUser (profile saves used to drop httpTimeout and ui.menu) - remove the line-based profile prompts and the theme prompt from the profile commands --- README.md | 15 +- cmd/config.go | 75 ++++ cmd/login.go | 20 +- cmd/profile.go | 205 ++++------- cmd/root.go | 24 +- cmd/user.go | 2 +- completions/iics.bash | 80 +++++ ...0046-profile-wizard-and-global-settings.md | 96 +++++ docs/documentation/config.md | 85 +++++ docs/documentation/menu.md | 2 +- docs/documentation/profile.md | 92 ++--- docs/documentation/user.md | 24 +- internal/config/config.go | 19 + internal/config/config_test.go | 29 ++ internal/config/pods.go | 11 +- internal/config/prompt.go | 193 ---------- internal/config/prompt_test.go | 207 +---------- internal/config/userpattern.go | 73 +++- internal/config/userpattern_test.go | 32 +- internal/menu/entries.go | 3 +- internal/output/formatter.go | 3 + internal/output/table.go | 2 +- internal/tui/configwizard.go | 206 +++++++++++ internal/tui/configwizard_test.go | 42 +++ internal/tui/profilewizard.go | 339 ++++++++++++++++++ internal/tui/profilewizard_test.go | 68 ++++ internal/tui/userwizard_test.go | 2 +- 27 files changed, 1304 insertions(+), 645 deletions(-) create mode 100644 cmd/config.go create mode 100644 docs/ChangeRequests/pending/CR-0046-profile-wizard-and-global-settings.md create mode 100644 docs/documentation/config.md create mode 100644 internal/tui/configwizard.go create mode 100644 internal/tui/configwizard_test.go create mode 100644 internal/tui/profilewizard.go create mode 100644 internal/tui/profilewizard_test.go diff --git a/README.md b/README.md index b4ffde3..8fc69ee 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,10 @@ iics logout ```yaml defaultProfile: dev httpTimeout: 120 # seconds; per-HTTP-request timeout (default 120) +newUser: # global defaults for "user create --interactive" (optional; iics config edit) + domain: "company.com" + userNamePattern: "{firstName}.{lastName}.{profileName}@{domain}" + emailPattern: "{firstName}.{lastName}@{domain}" ui: menu: true # false = bare "iics" prints help instead of opening the menu style: @@ -132,10 +136,8 @@ profiles: loginUrl: "https://use4.dm-us.informaticacloud.com/saas/public/core/v3/login" baseApiUrl: "https://use4.dm-us.informaticacloud.com/saas" caiUrl: "https://use4-cai.dm-us.informaticacloud.com" - newUser: # suggestions in "user create --interactive" (optional) - domain: "company.com" - userNamePattern: "{firstName}.{lastName}.{profileName}@{domain}" - emailPattern: "{firstName}.{lastName}@{domain}" + newUser: # overrides the global newUser for this profile (optional) + emailPattern: "{firstInitial}{lastName}@{domain}" prod: name: "Production Org" production: true # menu marks it and asks before changes @@ -148,7 +150,9 @@ profiles: ``` The `loginUrl`, `baseApiUrl`, and `caiUrl` fields are populated automatically after the first -`iics login` - you do not need to set them manually. +`iics login` - you do not need to set them manually. Profiles are edited with +`iics profile add` / `iics profile edit`, global settings with `iics config edit` +(see [config](docs/documentation/config.md)). The `style` section controls table output appearance. Available themes: @@ -267,6 +271,7 @@ it easy to override credentials in CI pipelines without touching the config file | [agent](docs/documentation/agent.md) | | `list`, `get`, `details`, `delete`, `start`, `stop`, `restart`, `installer-info`, `installer-download` | Manage Secure Agents | | [auditlog](docs/documentation/auditlog.md) | | `list` | Query organization audit log (V2 API) | | [completion](docs/documentation/completion.md) | | `bash`, `zsh`, `fish`, `powershell` | Generate shell completion scripts | +| [config](docs/documentation/config.md) | | `edit` | Edit global settings (new user defaults, theme, timeout, menu) | | [connection](docs/documentation/connection.md) | `conn` | `list`, `get`, `create`, `update`, `delete` | Manage connections | | [export](docs/documentation/export.md) | | `run`, `start`, `status`, `download`, `create` | Export asset packages | | [folder](docs/documentation/folder.md) | | `create`, `update`, `delete` | Manage folders | diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 0000000..a415e5b --- /dev/null +++ b/cmd/config.go @@ -0,0 +1,75 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/jbrazda/iics-cli/internal/config" + "github.com/jbrazda/iics-cli/internal/tui" + "github.com/spf13/cobra" +) + +func newConfigCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "config", + Short: "Manage global settings", + Long: `Manage the global settings in the config file (~/.iics/config.yaml).`, + } + cmd.AddCommand(newConfigEditCmd()) + return cmd +} + +func newConfigEditCmd() *cobra.Command { + return &cobra.Command{ + Use: "edit", + Short: "Edit global settings interactively", + Long: `Edit the global settings in an interactive form, followed by a review +of the changes: + + New user domain, user name pattern and email pattern defaults for all + profiles (profiles can override each value) + Appearance table theme (with a preview), header color, no color, + responsive tables + Other HTTP timeout, main menu for bare "iics" + +Profile settings are edited with "iics profile edit". Requires a terminal.`, + Example: ` iics config edit`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if !isInteractiveTTY() { + return fmt.Errorf("config edit requires a terminal; edit %s directly in scripts", configFilePath()) + } + cfg, err := loadConfig() + if err != nil { + return err + } + saved, err := tui.RunConfigWizard(tui.ConfigWizardInput{ + Config: cfg, + Themes: themeNames, + ThemePreview: themePreview, + Out: os.Stderr, + Accessible: prompter.Accessible, + }) + if err != nil { + return err + } + if !saved { + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "No changes saved.") + return nil + } + if err := cfg.Save(cfgFile); err != nil { + return fmt.Errorf("saving config: %w", err) + } + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Global settings saved.") + return nil + }, + } +} + +// configFilePath returns the config file in use (--config or the default). +func configFilePath() string { + if cfgFile != "" { + return cfgFile + } + return config.DefaultConfigPath() +} diff --git a/cmd/login.go b/cmd/login.go index df5398b..8ccc89f 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -48,26 +48,14 @@ The session is cached locally so subsequent commands don't require re-authentica if answer != "" && answer != "y" && answer != "Y" { return fmt.Errorf("profile %q not found; run 'iics profile add %s' to create it", profileName, profileName) } - newProfile, makeDefault, storeInKeyring, promptErr := config.PromptProfile(nil, profileName) + res, promptErr := promptProfile(cfg, profileName) if promptErr != nil { return fmt.Errorf("profile setup: %w", promptErr) } - if storeInKeyring { - if keyErr := config.SetKeychainPassword(profileName, newProfile.Password); keyErr != nil { - _, _ = fmt.Fprintf(os.Stderr, - "Warning: could not store password in keychain: %v\n"+ - " Storing password in config file instead.\n", keyErr) - } else { - newProfile.Password = config.KeyringSentinel - } - } - if cfg.Profiles == nil { - cfg.Profiles = make(map[string]*config.Profile) - } - cfg.Profiles[profileName] = newProfile - if makeDefault { - cfg.DefaultProfile = profileName + if res == nil { + return fmt.Errorf("profile %q not created", profileName) } + storeProfile(cfg, profileName, res, os.Stderr) if saveErr := cfg.Save(cfgFile); saveErr != nil { return fmt.Errorf("saving profile: %w", saveErr) } diff --git a/cmd/profile.go b/cmd/profile.go index 6af9049..57767a2 100644 --- a/cmd/profile.go +++ b/cmd/profile.go @@ -1,9 +1,10 @@ package cmd import ( - "bufio" "context" + "errors" "fmt" + "io" "os" "sort" "strings" @@ -12,6 +13,7 @@ import ( "github.com/jbrazda/iics-cli/internal/client" "github.com/jbrazda/iics-cli/internal/config" "github.com/jbrazda/iics-cli/internal/output" + "github.com/jbrazda/iics-cli/internal/tui" "github.com/spf13/cobra" "golang.org/x/term" ) @@ -29,56 +31,56 @@ var sampleThemeData = []map[string]interface{}{ {"name": "prod-org", "region": "EMEA", "status": "active"}, } -// promptThemeSelection renders a live sample for each theme and presents a numbered -// menu. currentTheme is shown as the default; pressing Enter keeps it. -// All output goes to stderr so stdout piping is not contaminated. -// Returns the selected theme name, or currentTheme if the user pressed Enter. -func promptThemeSelection(currentTheme string) (string, error) { - themes := []string{"default", "minimal", "compact", "plain", "markdown", "gh"} +// themeNames are the table themes offered by config edit. +var themeNames = []string{"default", "minimal", "compact", "plain", "markdown", "gh"} - _, _ = fmt.Fprint(os.Stderr, "\nTable theme selection - preview of each theme:\n\n") +// themePreview renders the sample table in a theme as on a terminal. +func themePreview(theme, headerColor string) string { + var b strings.Builder + f := output.New(output.FormatTable, &b, output.TableStyle{Theme: theme, HeaderColor: headerColor, ForceTerminal: true}) + _ = f.Format(sampleThemeData, sampleThemeCols) + return strings.TrimRight(b.String(), "\n") +} - for i, theme := range themes { - _, _ = fmt.Fprintf(os.Stderr, " [%d] %s\n", i+1, theme) - f := output.New(output.FormatTable, os.Stderr, output.TableStyle{Theme: theme}) - _ = f.Format(sampleThemeData, sampleThemeCols) - _, _ = fmt.Fprintln(os.Stderr) - } - - currentIdx := 0 - for i, t := range themes { - if t == currentTheme { - currentIdx = i + 1 - break - } +// promptProfile runs the profile wizard for profile name with the stored +// profile (if any) as the starting values. It returns nil when the user +// canceled. +func promptProfile(cfg *config.Config, name string) (*tui.ProfileWizardResult, error) { + if !config.IsTerminal() { + return nil, errors.New("stdin is not a terminal; use --profile flag or IICS_* env vars") } + return tui.RunProfileWizard(tui.ProfileWizardInput{ + Name: name, + Existing: cfg.Profiles[name], + Global: cfg.GlobalNewUser(), + IsDefault: cfg.DefaultProfile == name, + Out: os.Stderr, + Accessible: prompter.Accessible, + }) +} - r := bufio.NewReader(os.Stdin) - for { - if currentIdx > 0 { - _, _ = fmt.Fprintf(os.Stderr, - "Select theme [1-%d, current: %d (%s), Enter to keep]: ", - len(themes), currentIdx, currentTheme) +// storeProfile puts the wizard result into cfg as profile name, moving a new +// password to the OS keychain when requested. It does not save cfg. +func storeProfile(cfg *config.Config, name string, res *tui.ProfileWizardResult, errOut io.Writer) { + p := res.Profile + if config.IsKeyringSentinel(p.Password) { + // The existing keychain entry is kept; never write the sentinel to it. + p.Password = config.KeyringSentinel + } else if res.StoreInKeychain { + if err := config.SetKeychainPassword(name, p.Password); err != nil { + _, _ = fmt.Fprintf(errOut, + "Warning: could not store password in keychain: %v\n"+ + " Storing password in config file instead.\n", err) } else { - _, _ = fmt.Fprintf(os.Stderr, "Select theme [1-%d]: ", len(themes)) - } - - line, err := r.ReadString('\n') - if err != nil { - return "", fmt.Errorf("reading theme selection: %w", err) - } - line = strings.TrimSpace(line) - - if line == "" { - return currentTheme, nil + p.Password = config.KeyringSentinel } - - var n int - if _, scanErr := fmt.Sscanf(line, "%d", &n); scanErr == nil && n >= 1 && n <= len(themes) { - return themes[n-1], nil - } - _, _ = fmt.Fprintf(os.Stderr, - "Invalid selection %q. Enter a number from 1 to %d.\n", line, len(themes)) + } + if cfg.Profiles == nil { + cfg.Profiles = make(map[string]*config.Profile) + } + cfg.Profiles[name] = p + if res.MakeDefault { + cfg.DefaultProfile = name } } @@ -163,8 +165,9 @@ func newProfileAddCmd() *cobra.Command { return &cobra.Command{ Use: "add [name]", Short: "Add or update a profile interactively", - Long: `Interactively prompt for credentials and save them as a named profile. -If name is omitted, the profile is saved as "default".`, + Long: `Add a profile in an interactive form (connection, options, new user +defaults), followed by a review. If name is omitted, the profile is saved as +"default". An existing profile of that name is edited instead.`, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { name := "default" @@ -177,48 +180,15 @@ If name is omitted, the profile is saved as "default".`, return err } - existing := cfg.Profiles[name] // nil if not found - - p, makeDefault, storeInKeyring, err := config.PromptProfile(existing, name) + res, err := promptProfile(cfg, name) if err != nil { return err } - - // Keyring storage: save password in OS keychain and write sentinel to config. - // Guard: if the user kept an existing @keyring sentinel unchanged, do not - // pass it to SetKeychainPassword - that would corrupt the keychain entry. - plainPassword := p.Password - if config.IsKeyringSentinel(plainPassword) { - // Existing keychain entry is intact; just preserve the sentinel. - p.Password = config.KeyringSentinel - } else if storeInKeyring { - if keyErr := config.SetKeychainPassword(name, plainPassword); keyErr != nil { - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), - "Warning: could not store password in keychain: %v\n"+ - " Storing password in config file instead.\n", keyErr) - } else { - p.Password = config.KeyringSentinel - } - } - - if cfg.Profiles == nil { - cfg.Profiles = make(map[string]*config.Profile) - } - cfg.Profiles[name] = p - if makeDefault { - cfg.DefaultProfile = name - } - - // Interactive theme selection (terminal only) - if config.IsTerminal() { - selected, styleErr := promptThemeSelection(cfg.Style.Theme) - if styleErr != nil { - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), - "Warning: theme selection skipped: %v\n", styleErr) - } else if selected != "" { - cfg.Style.Theme = selected - } + if res == nil { + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Canceled.") + return nil } + storeProfile(cfg, name, res, cmd.ErrOrStderr()) if err := cfg.Save(cfgFile); err != nil { return fmt.Errorf("saving config: %w", err) @@ -234,10 +204,11 @@ func newProfileEditCmd() *cobra.Command { return &cobra.Command{ Use: "edit [name]", Short: "Edit an existing profile interactively", - Long: `Interactively update credentials for an existing profile. -Current values are shown as defaults; press Enter to keep them. -After saving, validates the credentials by logging in and refreshes -the session cache with the org-specific API URLs discovered from the response.`, + Long: `Edit an existing profile in an interactive form (connection, options, +new user defaults) with the current values filled in, followed by a review. +After the review, validates the credentials by logging in and refreshes the +session cache with the org-specific API URLs discovered from the response. +The profile is not saved when the login fails.`, Example: ` iics profile edit iics profile edit qa`, Args: cobra.MaximumNArgs(1), @@ -256,28 +227,16 @@ the session cache with the org-specific API URLs discovered from the response.`, return fmt.Errorf("profile %q not found; use 'profile add' to create it", name) } - // Interactive prompt with existing values as defaults. - p, makeDefault, storeInKeyring, err := config.PromptProfile(existing, name) + res, err := promptProfile(cfg, name) if err != nil { return err } - - // Keyring storage: save password in OS keychain and write sentinel to config. - // Guard: if the user kept an existing @keyring sentinel unchanged, do not - // pass it to SetKeychainPassword - that would corrupt the keychain entry. - plainPassword := p.Password - if config.IsKeyringSentinel(plainPassword) { - // Existing keychain entry is intact; just preserve the sentinel. - p.Password = config.KeyringSentinel - } else if storeInKeyring { - if keyErr := config.SetKeychainPassword(name, plainPassword); keyErr != nil { - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), - "Warning: could not store password in keychain: %v\n"+ - " Storing password in config file instead.\n", keyErr) - } else { - p.Password = config.KeyringSentinel - } + if res == nil { + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Canceled.") + return nil } + p := res.Profile + plainPassword := p.Password // Validate credentials and discover org-specific URLs via login. // When the password is the sentinel (user kept existing keychain password), @@ -307,22 +266,8 @@ the session cache with the org-specific API URLs discovered from the response.`, p.CaiURL = config.DeriveCaiURL(baseAPIURL) } - // Persist updated profile. - cfg.Profiles[name] = p - if makeDefault { - cfg.DefaultProfile = name - } - - // Interactive theme selection (terminal only) - if config.IsTerminal() { - selected, styleErr := promptThemeSelection(cfg.Style.Theme) - if styleErr != nil { - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), - "Warning: theme selection skipped: %v\n", styleErr) - } else if selected != "" { - cfg.Style.Theme = selected - } - } + // Persist updated profile (moves a new password to the keychain). + storeProfile(cfg, name, res, cmd.ErrOrStderr()) if err := cfg.Save(cfgFile); err != nil { return fmt.Errorf("saving config: %w", err) @@ -470,12 +415,18 @@ func newProfileShowCmd() *cobra.Command { if config.IsProductionProfile(name, p) { production = "yes" } - nu := config.NewUserPatterns(p) + nu, src := config.ResolveNewUser(cfg.GlobalNewUser(), p) + withSource := func(v, source string) string { + if v == "" { + return "" + } + return v + " (" + source + ")" + } rows = append(rows, map[string]interface{}{"field": "Production", "value": production}, - map[string]interface{}{"field": "New User Domain", "value": nu.Domain}, - map[string]interface{}{"field": "User Name Pattern", "value": nu.UserNamePattern}, - map[string]interface{}{"field": "Email Pattern", "value": nu.EmailPattern}, + map[string]interface{}{"field": "New User Domain", "value": withSource(nu.Domain, src.Domain)}, + map[string]interface{}{"field": "User Name Pattern", "value": withSource(nu.UserNamePattern, src.UserNamePattern)}, + map[string]interface{}{"field": "Email Pattern", "value": withSource(nu.EmailPattern, src.EmailPattern)}, ) // Append session-derived fields from the cache. diff --git a/cmd/root.go b/cmd/root.go index 31116de..07e1029 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -226,6 +226,7 @@ func init() { rootCmd.AddCommand(newSourcecontrolCmd()) rootCmd.AddCommand(newStateCmd()) rootCmd.AddCommand(newProfileCmd()) + rootCmd.AddCommand(newConfigCmd()) rootCmd.AddCommand(newPackageCmd()) rootCmd.AddCommand(newActivitylogCmd()) rootCmd.AddCommand(newAuditlogCmd()) @@ -301,29 +302,14 @@ func getClient(cmd *cobra.Command) (*client.Client, error) { if loadErr != nil { return nil, err } - newProfile, makeDefault, storeInKeyring, promptErr := config.PromptProfile(loadedCfg.Profiles[profileName], profileName) + res, promptErr := promptProfile(loadedCfg, profileName) if promptErr != nil { return nil, fmt.Errorf("profile setup: %w", promptErr) } - - // Keyring storage: save password in OS keychain and write sentinel to config. - if storeInKeyring { - if keyErr := config.SetKeychainPassword(profileName, newProfile.Password); keyErr != nil { - _, _ = fmt.Fprintf(os.Stderr, - "Warning: could not store password in keychain: %v\n"+ - " Storing password in config file instead.\n", keyErr) - } else { - newProfile.Password = config.KeyringSentinel - } - } - - if loadedCfg.Profiles == nil { - loadedCfg.Profiles = make(map[string]*config.Profile) - } - loadedCfg.Profiles[profileName] = newProfile - if makeDefault { - loadedCfg.DefaultProfile = profileName + if res == nil { + return nil, err } + storeProfile(loadedCfg, profileName, res, os.Stderr) if saveErr := loadedCfg.Save(cfgFile); saveErr != nil { return nil, fmt.Errorf("saving profile: %w", saveErr) } diff --git a/cmd/user.go b/cmd/user.go index bff3b52..5c52628 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -570,7 +570,7 @@ func runUserWizard(ctx context.Context, c *client.Client, existing *client.User) Groups: groups, Roles: roles, ProfileName: profileName, - Patterns: config.NewUserPatterns(prof), + Patterns: config.NewUserPatterns(cfg.GlobalNewUser(), prof), UserExists: func(name string) (string, error) { existing, lerr := c.FindUserByName(ctx, name) if lerr != nil || existing == nil { diff --git a/completions/iics.bash b/completions/iics.bash index c5f01ce..aeffbe2 100644 --- a/completions/iics.bash +++ b/completions/iics.bash @@ -1376,6 +1376,85 @@ _iics_completion() noun_aliases=() } +_iics_config_edit() +{ + last_command="iics_config_edit" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--http-timeout=") + two_word_flags+=("--http-timeout") + flags+=("--no-color") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + flags+=("--profile=") + two_word_flags+=("--profile") + two_word_flags+=("-p") + flags+=("--theme=") + two_word_flags+=("--theme") + flags+=("--verbose") + flags+=("-v") + flags+=("--wide") + flags+=("--width=") + two_word_flags+=("--width") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_iics_config() +{ + last_command="iics_config" + + command_aliases=() + + commands=() + commands+=("edit") + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--http-timeout=") + two_word_flags+=("--http-timeout") + flags+=("--no-color") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + flags+=("--profile=") + two_word_flags+=("--profile") + two_word_flags+=("-p") + flags+=("--theme=") + two_word_flags+=("--theme") + flags+=("--verbose") + flags+=("-v") + flags+=("--wide") + flags+=("--width=") + two_word_flags+=("--width") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _iics_connection_create() { last_command="iics_connection_create" @@ -7601,6 +7680,7 @@ _iics_root_command() commands+=("agent") commands+=("auditlog") commands+=("completion") + commands+=("config") commands+=("connection") if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then command_aliases+=("conn") diff --git a/docs/ChangeRequests/pending/CR-0046-profile-wizard-and-global-settings.md b/docs/ChangeRequests/pending/CR-0046-profile-wizard-and-global-settings.md new file mode 100644 index 0000000..ea5f256 --- /dev/null +++ b/docs/ChangeRequests/pending/CR-0046-profile-wizard-and-global-settings.md @@ -0,0 +1,96 @@ +# CR-0046: Profile wizard and global settings editor + +## Status + +Implemented, pending confirmation. + +## Summary + +Replace the line-by-line profile prompts with a paged TUI form used by every +profile entry point, add global new-user defaults with per-profile +overrides, and add `iics config edit` to edit global settings. + +## Motivation + +- `profile add` / `profile edit` ask one line at a time, show the region list + as a long text line and cannot go back to a previous answer. +- The new user patterns (CR-0045) can only be set per profile by editing + `~/.iics/config.yaml`. The same patterns usually apply to every org, so + each profile needs a copy. +- Global settings (theme, colors, responsive tables, HTTP timeout, main menu) + are only partly editable: the theme is asked at the end of `profile add` / + `profile edit`, the rest only in the YAML file. + +## Desired Behaviour + +### Global new-user defaults + +A top-level `newUser` block holds the global defaults. A profile `newUser` +block overrides them field by field. + +```yaml +newUser: + domain: acme.com + userNamePattern: "{firstName}.{lastName}.{profileName}@{domain}" + emailPattern: "{firstName}.{lastName}@{domain}" +profiles: + dev: + username: admin@acme.com + newUser: + emailPattern: "{firstInitial}{lastName}@{domain}" # override for dev only +``` + +Resolution per field: profile `newUser` -> global `newUser` -> built-in +default. `domain` falls back to the domain of the profile username when +neither block sets it. + +### Profile wizard + +`profile add`, `profile edit`, `login` (profile missing) and first-run setup +use one form: + +1. **Connection** - user name, password (masked; empty keeps the current + password), region (filterable list) or custom login URL, CAI URL + (optional). +2. **Options** - production org, set as default profile, store password in + the OS keychain (not asked when an unchanged keychain password is kept). +3. **New user** - domain, user name pattern and email pattern overrides for + this profile. Empty inherits; each field shows the inherited value and + its source. Patterns are validated (unknown placeholders are rejected). +4. **Review** - summary (new profile) or changes (existing profile) with + **Save**, **Back to editing** or **Cancel**. + +The theme is no longer asked by the profile commands; it moves to +`iics config edit`. `profile edit` still validates the credentials by +logging in after the form. In accessible mode the form runs as huh +accessible prompts. + +### `iics config edit` + +Interactive form for global settings, followed by a review of the changes: + +- New user defaults: domain, user name pattern, email pattern (empty uses + the built-in default). +- Appearance: table theme, header color, no color, responsive tables. +- HTTP timeout in seconds (empty uses the built-in default). +- Main menu for bare `iics`. + +### Other + +- `profile show` lists the effective new-user values with their source. +- The main menu gets a **Session > Edit global settings** entry. +- `Config.Save` writes `httpTimeout`, `ui` and `newUser`. It previously wrote + only `defaultProfile`, `profiles` and `style`, so saving a profile dropped + `httpTimeout` and `ui.menu` from the config file. + +## Files to Modify + +- `internal/config/config.go`, `internal/config/userpattern.go` - global + `newUser`, merge, pattern validation, region list +- `internal/config/prompt.go` - remove the line-based profile prompts +- `internal/tui/profilewizard.go`, `internal/tui/configwizard.go` - new forms +- `cmd/profile.go`, `cmd/login.go`, `cmd/root.go` - use the wizard +- `cmd/config.go` - new `config edit` command +- `internal/menu/entries.go` - menu entry +- `docs/documentation/profile.md`, `docs/documentation/config.md`, + `docs/documentation/user.md`, `README.md`, `completions/` diff --git a/docs/documentation/config.md b/docs/documentation/config.md new file mode 100644 index 0000000..3c578dc --- /dev/null +++ b/docs/documentation/config.md @@ -0,0 +1,85 @@ +# config + +Manage the global settings in `~/.iics/config.yaml` (or the file given with +`--config`). Profile settings are managed with [profile](profile.md). + +## Synopsis + +```bash +iics config [flags] +``` + +## Subcommands + +| Subcommand | Description | +| ---------- | ----------------------------------- | +| `edit` | Edit global settings interactively | + +## config edit + +Edit the global settings in an interactive form, followed by a review of the +changes (**Save**, **Back to editing** or **Cancel**). Requires a terminal; +in scripts, edit the config file directly. In accessible mode +(`IICS_ACCESSIBLE=1`) the form runs as numbered line prompts. + +| Page | Setting | Config key | Empty value | +| ---------- | ----------------------- | ------------------------ | -------------------------------------------- | +| New user | Domain | `newUser.domain` | Domain of each profile's user name | +| New user | User name pattern | `newUser.userNamePattern` | `{firstName}.{lastName}.{profileName}@{domain}` | +| New user | Email pattern | `newUser.emailPattern` | `{firstName}.{lastName}@{domain}` | +| Appearance | Table theme | `style.theme` | `default` on a terminal, `markdown` when piped | +| Appearance | Header color | `style.headerColor` | Theme default | +| Appearance | Disable colors | `style.noColor` | Colors on | +| Appearance | Responsive tables | `style.responsiveTables` | On | +| Other | HTTP timeout (seconds) | `httpTimeout` | 120 | +| Other | Main menu for bare `iics` | `ui.menu` | On | + +The table theme list shows a sample table in the highlighted theme. Pattern +fields reject unknown placeholders; see +[user - New user patterns](user.md#new-user-patterns) for the placeholders. + +### New user defaults + +The `newUser` values apply to every profile. A profile's own `newUser` block +(set with [profile edit](profile.md#profile-form)) overrides them field by +field: + +```yaml +newUser: + domain: company.com + emailPattern: "{firstInitial}{lastName}@{domain}" +profiles: + dev: + username: admin@company.com + newUser: + domain: dev.company.com # dev only; the email pattern is inherited +``` + +Each value resolves as: profile `newUser`, then global `newUser`, then the +built-in default (for the domain: the domain of the profile user name). +`iics profile show ` lists the effective values and their sources. + +### Flags + +No command-specific flags. All [global flags](../../README.md#global-flags) apply. + +### Examples + +```bash +iics config edit + +# Edit another config file +iics config edit --config ./team-config.yaml +``` + +```powershell +iics config edit + +iics config edit --config .\team-config.yaml +``` + +## See also + +- [profile](profile.md) +- [user - New user patterns](user.md#new-user-patterns) +- [Main menu](menu.md) diff --git a/docs/documentation/menu.md b/docs/documentation/menu.md index 74af5fb..7de96db 100644 --- a/docs/documentation/menu.md +++ b/docs/documentation/menu.md @@ -112,7 +112,7 @@ On a production profile: | Group | Entries | | ----------------------- | ----------------------------------------------------------------------- | -| Session | Switch profile, Set as default profile ✎, Login, List profiles, Add profile ✎, Edit active profile ✎ | +| Session | Switch profile, Set as default profile ✎, Login, List profiles, Add profile ✎, Edit active profile ✎, Edit global settings | | Users | List users, Show user (search by user name, ID, or part of a name/email), Create user ✎, Edit user groups and roles ✎, Delete user ✎, Change password ✎ | | User groups | List, Show, Create ✎, Edit roles ✎, Delete ✎ | | Roles | List, Show (with privileges), Create ✎, Edit privileges ✎ | diff --git a/docs/documentation/profile.md b/docs/documentation/profile.md index e0224ef..b4b97b7 100644 --- a/docs/documentation/profile.md +++ b/docs/documentation/profile.md @@ -3,8 +3,9 @@ Manage IICS connection profiles stored in `~/.iics/config.yaml`. Profiles hold the credentials and region needed to connect to an IICS org. The interactive -`add` subcommand prompts for all required values and saves them, eliminating the need to edit -the config file manually. +`add` and `edit` subcommands collect all values in a form and save them, eliminating the need +to edit the config file manually. Global settings shared by all profiles are edited with +[config edit](config.md). ## Synopsis @@ -69,63 +70,66 @@ profiles: production: true ``` -The optional `newUser` block sets the user name and email suggested by -`iics user create --interactive` (see -[user - New user patterns](user.md#new-user-patterns)): +The optional `newUser` block overrides, for this profile, the user name and +email suggested by `iics user create --interactive`. Each field overrides the +global `newUser` default of the same name (set with [config edit](config.md)); +see [user - New user patterns](user.md#new-user-patterns): ```yaml +newUser: # global defaults (all profiles) + domain: company.com profiles: dev: - newUser: - domain: company.com - userNamePattern: "{firstName}.{lastName}.{profileName}@{domain}" - emailPattern: "{firstName}.{lastName}@{domain}" + newUser: # overrides for dev only + emailPattern: "{firstInitial}{lastName}@{domain}" ``` `profile show` lists `Production`, `New User Domain`, `User Name Pattern` and -`Email Pattern` with defaults applied. - -The optional top-level `style` key controls table output appearance and is shared across -all profiles: - -```yaml -style: - theme: default # default | minimal | compact | plain | markdown | gh - noColor: false # true = disable color permanently (same as --no-color flag) - headerColor: "" # lipgloss color: "6"=cyan, "244"=gray, "#FF0000"=hex -``` - -The `add` subcommand: - -- Prompts for `username`, `password` (input is masked), and `region` or custom login URL. -- Derives `loginUrl` from the region automatically (if a known region code is entered). -- Derives `caiUrl` from the login URL and shows it as the default for the CAI URL prompt; - the user can accept the derived value or type a custom URL. -- When invoked on an existing profile name, shows the current value in brackets so you can - press Enter to keep it. -- Asks whether to set the profile as the default. -- **Offers to store the password in the OS keychain** (default: yes). When accepted, writes - `password: "@keyring"` to the config file instead of the plaintext password, and stores - the real password in the native OS keychain (macOS Keychain, Windows Credential Manager, - or Linux D-Bus Secret Service). -- Presents a live preview of all available table themes and prompts you to select one. - The selected theme is saved to the global `style.theme` in `~/.iics/config.yaml`. - Press Enter to keep the current theme. -- Saves the result to `~/.iics/config.yaml`. -- Does not validate credentials. Run `iics login --profile ` afterwards to verify and - populate `baseApiUrl` and `caiUrl`. +`Email Pattern` with the effective value and its source (`profile`, `global`, +`profile username` or `built-in default`). + +Table appearance (`style`), the HTTP timeout and the main menu setting are +global; see [config edit](config.md). + +### Profile form + +`add`, `edit`, `login` (when the profile does not exist) and the automatic +setup on missing credentials use the same paged form (`Enter` next field, +`Shift+Tab` back). Stored values are filled in. + +1. **Connection** - user name, password (masked; leave empty to keep the + current password) and region. The region list filters as you type; + **Custom login URL** asks for a login URL on the next page instead. + `loginUrl` is derived from the region. +2. **Options** - CAI URL (optional; derived from the org URL when known), + production org, and whether to set the profile as the default. +3. **Keychain** - **store the password in the OS keychain** (default: yes). + When accepted, `password: "@keyring"` is written to the config file and the + real password goes to the native OS keychain (macOS Keychain, Windows + Credential Manager, or Linux D-Bus Secret Service). Skipped when an + existing keychain password is kept. +4. **New user** - domain, user name pattern and email pattern overrides for + this profile. Leave a field empty to inherit; each field shows the + inherited value and its source. Unknown placeholders are rejected. +5. **Review** - the new profile, or the changes to an existing one, with + **Save**, **Back to editing** or **Cancel**. + +In accessible mode (`IICS_ACCESSIBLE=1`) the form runs as numbered line +prompts. + +The `add` subcommand saves the profile without validating the credentials. +Run `iics login --profile ` afterwards to verify them and populate +`baseApiUrl` and `caiUrl`. When the name already exists, the stored values +are filled in and the profile is updated. The `edit` subcommand: - Requires the profile to already exist (use `profile add` to create new profiles). -- Behaves identically to `add` for prompting - shows current values as defaults. -- After the prompts, **validates credentials by logging in** immediately. If the login fails +- After the review, **validates credentials by logging in**. If the login fails the profile is not saved and the error is shown. - On success: saves the updated profile with org-specific `baseApiUrl` and `caiUrl` derived from the login response, and refreshes the session cache. You do not need to run `iics login` separately. -- Presents the same live theme preview and selection menu as `add`. -- Also offers keyring storage as the last prompt. On first `iics login` after creating a profile with `add`, the `baseApiUrl` (org-specific, not known before a real login) and any remaining derived URLs are written back to the profile diff --git a/docs/documentation/user.md b/docs/documentation/user.md index d7c8438..6ae1702 100644 --- a/docs/documentation/user.md +++ b/docs/documentation/user.md @@ -181,19 +181,26 @@ one. See [Interactive prompts](interactive.md) for keys. ### New user patterns -The suggested user name and email are built from patterns in the active -profile (`~/.iics/config.yaml`): +The suggested user name and email are built from patterns in +`~/.iics/config.yaml`. The top-level `newUser` block holds the defaults for +all profiles ([config edit](config.md)); a profile's `newUser` block +overrides them field by field ([profile edit](profile.md#profile-form)): ```yaml +newUser: # all profiles + domain: acme.com # default: domain of the profile username + userNamePattern: "{firstName}.{lastName}.{profileName}@{domain}" # default + emailPattern: "{firstName}.{lastName}@{domain}" # default profiles: dev: username: admin@acme.com - newUser: - domain: acme.com # default: domain of the profile username - userNamePattern: "{firstName}.{lastName}.{profileName}@{domain}" # default - emailPattern: "{firstName}.{lastName}@{domain}" # default + newUser: # dev only + emailPattern: "{firstInitial}{lastName}@{domain}" ``` +Each value resolves as: profile `newUser`, global `newUser`, built-in +default. + | Placeholder | Value | | ---------------- | ------------------------------------------------------- | | `{firstName}` | First name, lowercase, spaces removed | @@ -201,13 +208,14 @@ profiles: | `{firstInitial}` | First letter of the first name, lowercase | | `{lastInitial}` | First letter of the last name, lowercase | | `{profileName}` | Name of the active profile | -| `{domain}` | `newUser.domain`, or the domain of the profile username | +| `{domain}` | `newUser.domain` (profile, then global), or the domain of the profile username | With the defaults, Jane Doe in profile `dev` (login `admin@acme.com`) gets user name `jane.doe.dev@acme.com` and email `jane.doe@acme.com`. An unknown placeholder or a missing value (for example no `@` in the profile username and no `newUser.domain`) shows a note under the field instead of a -suggestion. `iics profile show ` lists the effective patterns. +suggestion. `iics profile show ` lists the effective patterns and +where each comes from. All [global flags](../../README.md#global-flags) apply. diff --git a/internal/config/config.go b/internal/config/config.go index 770515a..271dd48 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -36,6 +36,16 @@ type Config struct { Style StyleConfig `yaml:"style,omitempty" mapstructure:"style"` HTTPTimeout int `yaml:"httpTimeout,omitempty" mapstructure:"httpTimeout"` UI UIConfig `yaml:"ui,omitempty" mapstructure:"ui"` + // NewUser holds the global new-user defaults; profiles override them. + NewUser *NewUserConfig `yaml:"newUser,omitempty" mapstructure:"newUser"` +} + +// GlobalNewUser returns the global new-user defaults; nil-safe. +func (c *Config) GlobalNewUser() *NewUserConfig { + if c == nil { + return nil + } + return c.NewUser } // UIConfig holds interactive UI preferences. @@ -316,6 +326,15 @@ func (c *Config) Save(configPath string) error { if c.Style.Theme != "" || c.Style.NoColor || c.Style.HeaderColor != "" || c.Style.ResponsiveTables != nil { v.Set("style", c.Style) } + if c.HTTPTimeout > 0 { + v.Set("httpTimeout", c.HTTPTimeout) + } + if c.UI.Menu != nil { + v.Set("ui", c.UI) + } + if c.NewUser != nil && !c.NewUser.IsZero() { + v.Set("newUser", c.NewUser) + } v.SetConfigFile(configPath) v.SetConfigType("yaml") diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 032a4c4..4838bf1 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -260,3 +260,32 @@ func TestResolveHTTPTimeoutSeconds(t *testing.T) { } }) } + +func TestSaveLoadRoundTripGlobalSettings(t *testing.T) { + path := t.TempDir() + "/config.yaml" + menu := false + in := &Config{ + DefaultProfile: "dev", + Profiles: map[string]*Profile{"dev": {Name: "dev", Username: "a@b.c", Password: "x", + NewUser: &NewUserConfig{EmailPattern: "{lastName}@{domain}"}}}, + HTTPTimeout: 45, + UI: UIConfig{Menu: &menu}, + NewUser: &NewUserConfig{Domain: "acme.com"}, + } + if err := in.Save(path); err != nil { + t.Fatal(err) + } + out, err := Load(path) + if err != nil { + t.Fatal(err) + } + if out.DefaultProfile != "dev" || out.HTTPTimeout != 45 || out.UI.Menu == nil || *out.UI.Menu { + t.Errorf("global settings lost: %+v", out) + } + if out.NewUser == nil || out.NewUser.Domain != "acme.com" { + t.Errorf("global newUser = %+v", out.NewUser) + } + if nu := out.Profiles["dev"].NewUser; nu == nil || nu.EmailPattern != "{lastName}@{domain}" { + t.Errorf("profile newUser = %+v", nu) + } +} diff --git a/internal/config/pods.go b/internal/config/pods.go index 317e876..aa50717 100644 --- a/internal/config/pods.go +++ b/internal/config/pods.go @@ -45,12 +45,17 @@ func LoginURL(region string) (string, error) { return fmt.Sprintf("https://%s/saas/public/core/v3/login", host), nil } -// ValidRegions returns a sorted comma-separated list of valid region identifiers. -func ValidRegions() string { +// Regions returns the valid region identifiers, sorted. +func Regions() []string { regions := make([]string, 0, len(podURLs)) for r := range podURLs { regions = append(regions, r) } sort.Strings(regions) - return strings.Join(regions, ", ") + return regions +} + +// ValidRegions returns a sorted comma-separated list of valid region identifiers. +func ValidRegions() string { + return strings.Join(Regions(), ", ") } diff --git a/internal/config/prompt.go b/internal/config/prompt.go index e2186bc..b7ce6e9 100644 --- a/internal/config/prompt.go +++ b/internal/config/prompt.go @@ -1,205 +1,12 @@ package config import ( - "bufio" - "errors" - "fmt" "os" - "strings" "golang.org/x/term" ) -// readPassword is a package-level var so tests can stub it out without a real TTY. -var readPassword = func(fd int) ([]byte, error) { return term.ReadPassword(fd) } - // IsTerminal reports whether stdin is an interactive terminal. func IsTerminal() bool { return term.IsTerminal(int(os.Stdin.Fd())) } - -// PromptProfile interactively collects profile credentials from the user. -// existing may be nil when creating a new profile; its values are used as defaults. -// profileName is used only in display messages. -// Returns the filled profile, whether the user wants it as the default, whether to store -// the password in the OS keychain, and any error. -// Returns an error immediately if stdin is not a terminal. -func PromptProfile(existing *Profile, profileName string) (*Profile, bool, bool, error) { - if !IsTerminal() { - return nil, false, false, errors.New("stdin is not a terminal; use --profile flag or IICS_* env vars") - } - return promptProfileInternal(existing, profileName) -} - -// promptProfileInternal contains the prompting logic. Separated from PromptProfile -// so tests can exercise it via a pipe-based stdin without a real TTY. -func promptProfileInternal(existing *Profile, profileName string) (*Profile, bool, bool, error) { - p := &Profile{} - if existing != nil { - *p = *existing - } - - r := bufio.NewReader(os.Stdin) - - _, _ = fmt.Fprintf(os.Stderr, "Setting up profile %q\n\n", profileName) - - // Username - always prompt at least once so existing profiles can be updated. - for { - if existing != nil && existing.Username != "" { - _, _ = fmt.Fprintf(os.Stderr, "Username [%s]: ", existing.Username) - } else { - _, _ = fmt.Fprint(os.Stderr, "Username: ") - } - line, err := r.ReadString('\n') - if err != nil { - return nil, false, false, fmt.Errorf("reading username: %w", err) - } - line = strings.TrimSpace(line) - if line != "" { - p.Username = line - } else if existing != nil && existing.Username != "" { - p.Username = existing.Username - } - if p.Username != "" { - break - } - _, _ = fmt.Fprintln(os.Stderr, "Username is required.") - } - - // Password - always prompt at least once so existing profiles can be updated. - for { - if existing != nil && existing.Password != "" { - if IsKeyringSentinel(existing.Password) { - _, _ = fmt.Fprint(os.Stderr, "Password [current: keychain]: ") - } else { - _, _ = fmt.Fprint(os.Stderr, "Password [current: ***]: ") - } - } else { - _, _ = fmt.Fprint(os.Stderr, "Password: ") - } - pw, err := readPassword(int(os.Stdin.Fd())) - _, _ = fmt.Fprintln(os.Stderr) - if err != nil { - return nil, false, false, fmt.Errorf("reading password: %w", err) - } - if len(pw) > 0 { - p.Password = string(pw) - } else if existing != nil && existing.Password != "" { - p.Password = existing.Password - } - if p.Password != "" { - break - } - _, _ = fmt.Fprintln(os.Stderr, "Password is required.") - } - - // Region or custom login URL - regionCurrent := p.Region - if regionCurrent == "" && p.LoginURL != "" { - regionCurrent = p.LoginURL - } - if regionCurrent != "" { - _, _ = fmt.Fprintf(os.Stderr, "Region or login URL [%s]: ", regionCurrent) - } else { - _, _ = fmt.Fprintf(os.Stderr, "Region (%s)\n or custom login URL: ", ValidRegions()) - } - line, err := r.ReadString('\n') - if err != nil { - return nil, false, false, fmt.Errorf("reading region: %w", err) - } - line = strings.TrimSpace(line) - if line == "" { - // Keep existing values (already copied into p above) - } else if strings.HasPrefix(line, "https://") || strings.HasPrefix(line, "http://") { - p.LoginURL = line - p.Region = "" - } else { - p.Region = strings.ToUpper(line) - p.LoginURL = "" - } - - // Derive loginUrl from region if not set explicitly. - if p.LoginURL == "" && p.Region != "" { - if derivedLogin, lerr := LoginURL(p.Region); lerr == nil { - p.LoginURL = derivedLogin - } - } - - // CAI URL - derive from baseApiUrl when available. - // Do not derive from loginUrl (pod-level host), which can produce an - // unresolvable CAI hostname for region-specific org hosts. - derivedCaiURL := "" - if p.BaseAPIURL != "" { - derivedCaiURL = DeriveCaiURL(p.BaseAPIURL) - } - caiDefault := p.CaiURL - if caiDefault == "" { - caiDefault = derivedCaiURL - } - if caiDefault != "" { - _, _ = fmt.Fprintf(os.Stderr, "CAI URL [%s]: ", caiDefault) - } else { - _, _ = fmt.Fprint(os.Stderr, "CAI URL (leave blank to derive automatically on login): ") - } - line, err = r.ReadString('\n') - if err != nil { - return nil, false, false, fmt.Errorf("reading CAI URL: %w", err) - } - line = strings.TrimSpace(line) - if line != "" { - p.CaiURL = line - } else { - p.CaiURL = caiDefault - } - - // Production flag (default: the current setting, else the name hint). - prodDefault := IsProductionProfile(profileName, p) - prodHint := "[y/N]" - if prodDefault { - prodHint = "[Y/n]" - } - _, _ = fmt.Fprintf(os.Stderr, "Production org (menu asks before changes)? %s: ", prodHint) - line, err = r.ReadString('\n') - if err != nil { - return nil, false, false, fmt.Errorf("reading production choice: %w", err) - } - line = strings.TrimSpace(strings.ToLower(line)) - production := prodDefault - if line != "" { - production = line == "y" || line == "yes" - } - p.Production = &production - - // Default profile prompt (default: yes) - _, _ = fmt.Fprint(os.Stderr, "Set as default profile? [Y/n]: ") - line, err = r.ReadString('\n') - if err != nil { - return nil, false, false, fmt.Errorf("reading default choice: %w", err) - } - line = strings.TrimSpace(strings.ToLower(line)) - makeDefault := line == "" || line == "y" || line == "yes" - - // Keychain storage offer (default: yes). - // Skip the offer when the user kept an existing keychain-backed password unchanged - - // the keychain entry is already correct; re-offering would risk overwriting it with - // the sentinel literal string. - existingIsKeyring := existing != nil && IsKeyringSentinel(existing.Password) - passwordUnchanged := existingIsKeyring && IsKeyringSentinel(p.Password) - if passwordUnchanged { - return p, makeDefault, false, nil - } - - if existingIsKeyring { - _, _ = fmt.Fprint(os.Stderr, "Store password in OS keychain? [Y/n]: ") - } else { - _, _ = fmt.Fprint(os.Stderr, "Store password in OS keychain (recommended)? [Y/n]: ") - } - line, err = r.ReadString('\n') - if err != nil { - return nil, false, false, fmt.Errorf("reading keyring choice: %w", err) - } - line = strings.TrimSpace(strings.ToLower(line)) - storeInKeyring := line == "" || line == "y" || line == "yes" - - return p, makeDefault, storeInKeyring, nil -} diff --git a/internal/config/prompt_test.go b/internal/config/prompt_test.go index 275e44e..bf7bf0b 100644 --- a/internal/config/prompt_test.go +++ b/internal/config/prompt_test.go @@ -1,10 +1,6 @@ package config -import ( - "os" - "strings" - "testing" -) +import "testing" func TestIsTerminal(t *testing.T) { // In test environments stdin is a pipe, not a terminal. @@ -12,204 +8,3 @@ func TestIsTerminal(t *testing.T) { t.Skip("skipping: stdin is a real terminal (run in CI for proper coverage)") } } - -func TestPromptProfile_NonTerminalReturnsError(t *testing.T) { - // Ensure stdin is not a terminal (it won't be in tests). - if IsTerminal() { - t.Skip("test requires non-terminal stdin") - } - _, _, _, err := PromptProfile(nil, "test") - if err == nil { - t.Fatal("expected error when stdin is not a terminal") - } - if !strings.Contains(err.Error(), "stdin is not a terminal") { - t.Errorf("unexpected error: %v", err) - } -} - -// withFakeStdin replaces os.Stdin with a pipe fed by input, calls f, then restores. -func withFakeStdin(t *testing.T, input string, f func()) { - t.Helper() - r, w, err := os.Pipe() - if err != nil { - t.Fatalf("os.Pipe: %v", err) - } - old := os.Stdin - os.Stdin = r - t.Cleanup(func() { - os.Stdin = old - r.Close() - }) - if _, err := w.WriteString(input); err != nil { - t.Fatalf("writing to pipe: %v", err) - } - w.Close() - f() -} - -// stubReadPassword replaces the readPassword var so tests don't need a real TTY. -func stubReadPassword(t *testing.T, password string) { - t.Helper() - orig := readPassword - readPassword = func(_ int) ([]byte, error) { return []byte(password), nil } - t.Cleanup(func() { readPassword = orig }) -} - -// stubIsTerminal overrides IsTerminal by temporarily marking stdin as a pipe that -// reports true. We achieve this by monkey-patching readPassword AND feeding stdin via -// withFakeStdin. But IsTerminal() checks os.Stdin.Fd() which is a pipe — not a real -// terminal. We need to make the prompt proceed past the IsTerminal guard. -// Strategy: directly call the internal prompting after skipping the terminal check by -// temporarily replacing os.Stdin with a real TTY — but that's not portable. Instead, -// we test the individual path by checking that PromptProfile errors correctly, and we -// test the prompting logic via a helper that bypasses the terminal check. -// -// For portability the tests below work around the guard by calling promptProfile -// (the unexported implementation) directly. We expose it via a test-only shim. -func TestPromptProfile_KeepsExistingOnEmptyInput(t *testing.T) { - existing := &Profile{ - Username: "user@example.com", - Password: "secret", - Region: "USW3", - } - stubReadPassword(t, "") // empty — should keep existing password - // inputs: username(keep), region(keep), caiUrl(keep derived), production(keep), default(y), keyring(y) - withFakeStdin(t, "\n\n\n\n\n\n", func() { - p, makeDefault, _, err := promptProfileInternal(existing, "test") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if p.Username != "user@example.com" { - t.Errorf("username = %q; want %q", p.Username, "user@example.com") - } - if p.Password != "secret" { - t.Errorf("password should have been kept") - } - if p.Region != "USW3" { - t.Errorf("region = %q; want USW3", p.Region) - } - // Default answer is "y" (empty line → yes) - if !makeDefault { - t.Errorf("expected makeDefault=true for empty input") - } - }) -} - -func TestPromptProfile_URLTreatedAsLoginURL(t *testing.T) { - existing := &Profile{ - Username: "user@example.com", - Password: "secret", - } - customURL := "https://custom.example.com/saas/public/core/v3/login" - stubReadPassword(t, "newpass") - // stdin needs: username(keep=\n), region line, caiUrl line, production line, default line, keyring line. - // password is handled by stubReadPassword (no stdin line needed). - withFakeStdin(t, "\n"+customURL+"\n\n\n\n\n", func() { - p, _, _, err := promptProfileInternal(existing, "test") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if p.LoginURL != customURL { - t.Errorf("LoginURL = %q; want %q", p.LoginURL, customURL) - } - if p.Region != "" { - t.Errorf("Region should be empty when URL is provided, got %q", p.Region) - } - }) -} - -func TestPromptProfile_RegionUppercased(t *testing.T) { - existing := &Profile{Username: "u@example.com", Password: "pass"} - stubReadPassword(t, "pass") - // stdin needs: username(keep=\n), region, caiUrl, production, default, keyring. - // password is handled by stubReadPassword (no stdin line needed). - withFakeStdin(t, "\nusw3\n\n\n\n\n", func() { - p, _, _, err := promptProfileInternal(existing, "test") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if p.Region != "USW3" { - t.Errorf("Region = %q; want USW3", p.Region) - } - }) -} - -func TestPromptProfile_KeepsExistingKeyringUnchanged(t *testing.T) { - // When existing profile already uses @keyring and user keeps the password unchanged, - // the keyring re-offer prompt should be skipped and storeInKeyring must be false. - existing := &Profile{ - Username: "u@example.com", - Password: KeyringSentinel, - Region: "USW3", - } - stubReadPassword(t, "") // empty - keep existing (which is sentinel) - // inputs: username(keep), region(keep), caiUrl(keep), production(keep), default(y) - // NO keyring line - the prompt is skipped when password kept as sentinel - withFakeStdin(t, "\n\n\n\n\n", func() { - p, _, storeInKeyring, err := promptProfileInternal(existing, "test") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if p.Password != KeyringSentinel { - t.Errorf("password = %q; want %q", p.Password, KeyringSentinel) - } - if storeInKeyring { - t.Errorf("storeInKeyring should be false when sentinel kept unchanged") - } - }) -} - -func TestPromptProfile_KeyringDefaultYes(t *testing.T) { - existing := &Profile{Username: "u@example.com", Password: "pass"} - stubReadPassword(t, "pass") - // Empty answer to keyring prompt means yes (default). - withFakeStdin(t, "\nusw3\n\n\n\n\n", func() { - _, _, storeInKeyring, err := promptProfileInternal(existing, "test") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if !storeInKeyring { - t.Errorf("storeInKeyring should be true when user presses Enter (default=yes)") - } - }) -} - -func TestPromptProfile_KeyringExplicitNo(t *testing.T) { - existing := &Profile{Username: "u@example.com", Password: "pass"} - stubReadPassword(t, "pass") - // Explicit "n" answer to keyring prompt means no. - withFakeStdin(t, "\nusw3\n\n\n\nn\n", func() { - _, _, storeInKeyring, err := promptProfileInternal(existing, "test") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if storeInKeyring { - t.Errorf("storeInKeyring should be false when user answers 'n'") - } - }) -} - -func TestPromptProfile_Production(t *testing.T) { - existing := &Profile{Username: "u@example.com", Password: "secret", Region: "USW3"} - stubReadPassword(t, "") - // username, region, caiUrl, production (empty -> name hint "prd" = yes), default, keyring - withFakeStdin(t, "\n\n\n\n\n\n", func() { - p, _, _, err := promptProfileInternal(existing, "prd") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if p.Production == nil || !*p.Production { - t.Errorf("empty answer for profile prd should keep the production hint, got %v", p.Production) - } - }) - stubReadPassword(t, "") - withFakeStdin(t, "\n\n\ny\n\n\n", func() { - p, _, _, err := promptProfileInternal(existing, "dev") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if p.Production == nil || !*p.Production { - t.Errorf("answer y should mark production, got %v", p.Production) - } - }) -} diff --git a/internal/config/userpattern.go b/internal/config/userpattern.go index 4bed1ec..f1b886b 100644 --- a/internal/config/userpattern.go +++ b/internal/config/userpattern.go @@ -15,7 +15,8 @@ const ( // UserPatternPlaceholders lists the supported placeholders. var UserPatternPlaceholders = []string{"firstName", "lastName", "firstInitial", "lastInitial", "profileName", "domain"} -// NewUserConfig holds the per-profile settings for new users. +// NewUserConfig holds the settings for new users. The top-level config +// block holds global defaults; a profile block overrides them per field. type NewUserConfig struct { // Domain replaces {domain}; defaults to the domain of the profile username. Domain string `yaml:"domain,omitempty" mapstructure:"domain"` @@ -23,30 +24,70 @@ type NewUserConfig struct { EmailPattern string `yaml:"emailPattern,omitempty" mapstructure:"emailPattern"` } -// NewUserPatterns returns the effective new-user settings of a profile, -// applying defaults for unset values. -func NewUserPatterns(p *Profile) NewUserConfig { +// IsZero reports whether no field is set. +func (n NewUserConfig) IsZero() bool { + return n == NewUserConfig{} +} + +// Sources of an effective new-user value (see ResolveNewUser). +const ( + SourceProfile = "profile" + SourceGlobal = "global" + SourceUsername = "profile username" + SourceDefault = "built-in default" +) + +// NewUserSources names where each effective new-user value comes from. +type NewUserSources struct { + Domain, UserNamePattern, EmailPattern string +} + +// ResolveNewUser returns the effective new-user settings of a profile and +// their sources. Each field resolves as: profile newUser, global newUser, +// built-in default. The domain falls back to the domain of the profile +// username. global and p may be nil. +func ResolveNewUser(global *NewUserConfig, p *Profile) (NewUserConfig, NewUserSources) { out := NewUserConfig{UserNamePattern: DefaultUserNamePattern, EmailPattern: DefaultEmailPattern} - if p == nil { - return out - } - if i := strings.LastIndex(p.Username, "@"); i >= 0 { - out.Domain = p.Username[i+1:] + src := NewUserSources{UserNamePattern: SourceDefault, EmailPattern: SourceDefault} + if p != nil { + if i := strings.LastIndex(p.Username, "@"); i >= 0 { + out.Domain, src.Domain = p.Username[i+1:], SourceUsername + } } - if p.NewUser != nil { - if p.NewUser.Domain != "" { - out.Domain = p.NewUser.Domain + apply := func(n *NewUserConfig, source string) { + if n == nil { + return + } + if n.Domain != "" { + out.Domain, src.Domain = n.Domain, source } - if p.NewUser.UserNamePattern != "" { - out.UserNamePattern = p.NewUser.UserNamePattern + if n.UserNamePattern != "" { + out.UserNamePattern, src.UserNamePattern = n.UserNamePattern, source } - if p.NewUser.EmailPattern != "" { - out.EmailPattern = p.NewUser.EmailPattern + if n.EmailPattern != "" { + out.EmailPattern, src.EmailPattern = n.EmailPattern, source } } + apply(global, SourceGlobal) + if p != nil { + apply(p.NewUser, SourceProfile) + } + return out, src +} + +// NewUserPatterns returns the effective new-user settings of a profile +// (see ResolveNewUser). +func NewUserPatterns(global *NewUserConfig, p *Profile) NewUserConfig { + out, _ := ResolveNewUser(global, p) return out } +// ValidateUserPattern reports unknown placeholders in pattern. +func ValidateUserPattern(pattern string) error { + _, err := ExpandUserPattern(pattern, UserPatternValues{FirstName: "a", LastName: "b", ProfileName: "c", Domain: "d"}) + return err +} + // UserPatternValues are the inputs for ExpandUserPattern. type UserPatternValues struct { FirstName, LastName, ProfileName, Domain string diff --git a/internal/config/userpattern_test.go b/internal/config/userpattern_test.go index 6177189..7424177 100644 --- a/internal/config/userpattern_test.go +++ b/internal/config/userpattern_test.go @@ -6,19 +6,45 @@ import ( ) func TestNewUserPatterns(t *testing.T) { - d := NewUserPatterns(&Profile{Username: "admin@Acme.com"}) + d := NewUserPatterns(nil, &Profile{Username: "admin@Acme.com"}) if d.Domain != "Acme.com" || d.UserNamePattern != DefaultUserNamePattern || d.EmailPattern != DefaultEmailPattern { t.Errorf("defaults = %+v", d) } - c := NewUserPatterns(&Profile{Username: "admin@acme.com", NewUser: &NewUserConfig{Domain: "corp.example", EmailPattern: "{firstInitial}{lastName}@{domain}"}}) + c := NewUserPatterns(nil, &Profile{Username: "admin@acme.com", NewUser: &NewUserConfig{Domain: "corp.example", EmailPattern: "{firstInitial}{lastName}@{domain}"}}) if c.Domain != "corp.example" || c.UserNamePattern != DefaultUserNamePattern || c.EmailPattern != "{firstInitial}{lastName}@{domain}" { t.Errorf("custom = %+v", c) } - if n := NewUserPatterns(&Profile{Username: "admin"}); n.Domain != "" { + if n := NewUserPatterns(nil, &Profile{Username: "admin"}); n.Domain != "" { t.Errorf("no @ in username should give no domain, got %q", n.Domain) } } +func TestResolveNewUserGlobalAndOverride(t *testing.T) { + global := &NewUserConfig{Domain: "global.example", UserNamePattern: "{firstName}@{domain}"} + p := &Profile{Username: "admin@acme.com", NewUser: &NewUserConfig{UserNamePattern: "{lastName}@{domain}"}} + got, src := ResolveNewUser(global, p) + want := NewUserConfig{Domain: "global.example", UserNamePattern: "{lastName}@{domain}", EmailPattern: DefaultEmailPattern} + if got != want { + t.Errorf("ResolveNewUser = %+v, want %+v", got, want) + } + wantSrc := NewUserSources{Domain: SourceGlobal, UserNamePattern: SourceProfile, EmailPattern: SourceDefault} + if src != wantSrc { + t.Errorf("sources = %+v, want %+v", src, wantSrc) + } + if _, src = ResolveNewUser(nil, &Profile{Username: "a@b.c"}); src.Domain != SourceUsername { + t.Errorf("domain source = %q, want %q", src.Domain, SourceUsername) + } +} + +func TestValidateUserPattern(t *testing.T) { + if err := ValidateUserPattern(DefaultUserNamePattern); err != nil { + t.Errorf("default pattern: %v", err) + } + if err := ValidateUserPattern("{first}.{lastName}"); err == nil || !strings.Contains(err.Error(), "{first}") { + t.Errorf("err = %v, want unknown placeholder {first}", err) + } +} + func TestExpandUserPattern(t *testing.T) { v := UserPatternValues{FirstName: "Mary Ann", LastName: "O'Neil", ProfileName: "Dev", Domain: "Acme.com"} tests := []struct { diff --git a/internal/menu/entries.go b/internal/menu/entries.go index 7edf993..527532b 100644 --- a/internal/menu/entries.go +++ b/internal/menu/entries.go @@ -69,7 +69,8 @@ func DefaultEntries() []Entry { {Group: "Session", Label: "Login", Description: "Sign in and refresh the session for the active profile", Args: []string{"login"}}, {Group: "Session", Label: "List profiles", Description: "Show configured profiles", Args: []string{"profile", "list"}, NoProfileFlag: true, SessionOnly: true}, {Group: "Session", Label: "Add profile", Description: "Set up a new org profile", Args: []string{"profile", "add"}, AskArg: "Profile name", NoProfileFlag: true, Writes: true, SessionOnly: true}, - {Group: "Session", Label: "Edit active profile", Description: "Update credentials, region and production flag of the active profile", Args: []string{"profile", "edit"}, ActiveProfileArg: true, NoProfileFlag: true, Writes: true}, + {Group: "Session", Label: "Edit active profile", Description: "Update credentials, region, production flag and new user defaults of the active profile", Args: []string{"profile", "edit"}, ActiveProfileArg: true, NoProfileFlag: true, Writes: true}, + {Group: "Session", Label: "Edit global settings", Description: "New user defaults for all profiles, table theme, HTTP timeout, main menu", Args: []string{"config", "edit"}, NoProfileFlag: true, SessionOnly: true}, {Group: "Users", Label: "List users", Description: "Show all users", Args: []string{"user", "list"}}, {Group: "Users", Label: "Show user", Description: "Find a user by user name, ID, or part of a name or email and show the details", Args: []string{"user", "get"}}, diff --git a/internal/output/formatter.go b/internal/output/formatter.go index cd5a96b..6cc0775 100644 --- a/internal/output/formatter.go +++ b/internal/output/formatter.go @@ -69,6 +69,9 @@ type TableStyle struct { // false. Detected/overridden width, if any, is still used for layout // info but never causes a column to be dropped, truncated, or wrapped. SkipAdapt bool + // ForceTerminal renders the theme as on a terminal even when the writer + // is not one (theme previews rendered into a buffer). + ForceTerminal bool } // Formatter is the interface for rendering API results. diff --git a/internal/output/table.go b/internal/output/table.go index 7a9a91d..5ac9274 100644 --- a/internal/output/table.go +++ b/internal/output/table.go @@ -129,7 +129,7 @@ func effectiveTheme(w io.Writer, style TableStyle) string { if style.NoColor { return "plain" } - if !isTerminal(w) { + if !style.ForceTerminal && !isTerminal(w) { if style.Theme == "" { return "markdown" } diff --git a/internal/tui/configwizard.go b/internal/tui/configwizard.go new file mode 100644 index 0000000..1787358 --- /dev/null +++ b/internal/tui/configwizard.go @@ -0,0 +1,206 @@ +package tui + +import ( + "errors" + "fmt" + "io" + "strconv" + "strings" + + "github.com/charmbracelet/huh" + "github.com/jbrazda/iics-cli/internal/config" +) + +// ConfigWizardInput configures RunConfigWizard. +type ConfigWizardInput struct { + // Config is edited in place when the user saves. + Config *config.Config + // Themes are the table theme names. + Themes []string + // ThemePreview renders a sample table in a theme; nil shows no preview. + ThemePreview func(theme, headerColor string) string + + Out io.Writer + Accessible bool +} + +// configForm holds the form values of the config wizard. +type configForm struct { + domain, unPat, emPat string + theme, headerColor string + noColor, responsive bool + timeout string + menu bool +} + +// RunConfigWizard edits the global settings (new user defaults, +// appearance, HTTP timeout, main menu) followed by a review of the changes. +// It returns false when nothing was saved. +func RunConfigWizard(in ConfigWizardInput) (bool, error) { + f := newConfigForm(in.Config) + for { + form := huh.NewForm(configPages(in, f)...).WithOutput(in.Out).WithAccessible(in.Accessible) + if err := form.Run(); err != nil { + if errors.Is(err, huh.ErrUserAborted) { + return false, nil + } + return false, err + } + after := f.apply(in.Config) + lines := ConfigChanges(in.Config, after) + canApply := len(lines) > 0 + if !canApply { + lines = []string{"No changes."} + } + choice, err := confirmReview(in.Out, in.Accessible, "Global settings", lines, "Save", canApply) + if err != nil { + return false, err + } + switch choice { + case reviewApply: + *in.Config = *after + return true, nil + case reviewCancel: + return false, nil + } + } +} + +func newConfigForm(c *config.Config) *configForm { + f := &configForm{ + theme: c.Style.Theme, + headerColor: c.Style.HeaderColor, + noColor: c.Style.NoColor, + responsive: c.Style.ResponsiveTablesEnabled(), + menu: c.UI.Menu == nil || *c.UI.Menu, + } + if c.NewUser != nil { + f.domain, f.unPat, f.emPat = c.NewUser.Domain, c.NewUser.UserNamePattern, c.NewUser.EmailPattern + } + if c.HTTPTimeout > 0 { + f.timeout = strconv.Itoa(c.HTTPTimeout) + } + return f +} + +func configPages(in ConfigWizardInput, f *configForm) []*huh.Group { + newUser := huh.NewGroup( + huh.NewNote().Title("New user defaults (all profiles)"). + Description("Profiles can override each value (iics profile edit).\nPlaceholders: {"+strings.Join(config.UserPatternPlaceholders, "}, {")+"}."), + huh.NewInput().Title("Domain"). + Description("Leave empty to use the domain of each profile's user name."). + Value(&f.domain), + huh.NewInput().Title("User name pattern"). + Description("Leave empty for the built-in default "+config.DefaultUserNamePattern). + Value(&f.unPat).Validate(optionalPattern), + huh.NewInput().Title("Email pattern"). + Description("Leave empty for the built-in default "+config.DefaultEmailPattern). + Value(&f.emPat).Validate(optionalPattern), + ).Title("New user") + + themeOpts := []huh.Option[string]{huh.NewOption("(automatic) default on a terminal, markdown when piped", "")} + for _, t := range in.Themes { + themeOpts = append(themeOpts, huh.NewOption(t, t)) + } + theme := huh.NewSelect[string]().Title("Table theme").Options(themeOpts...).Value(&f.theme) + if in.ThemePreview != nil { + theme.DescriptionFunc(func() string { + t := f.theme + if t == "" { + t = "default" + } + return in.ThemePreview(t, f.headerColor) + }, &f.theme) + } + appearance := huh.NewGroup( + theme, + huh.NewInput().Title("Header color"). + Description(`Lipgloss color for default and minimal themes: "6" cyan, "244" gray, "#FF0000". Empty uses the theme default.`). + Value(&f.headerColor), + huh.NewConfirm().Title("Disable colors?").Value(&f.noColor), + huh.NewConfirm().Title("Responsive tables?"). + Description("Drop, truncate or wrap columns to fit the terminal width (off is the same as --wide)."). + Value(&f.responsive), + ).Title("Appearance") + + other := huh.NewGroup( + huh.NewInput().Title("HTTP timeout (seconds)"). + Description(fmt.Sprintf("Per request. Leave empty for the built-in default (%d).", config.DefaultHTTPTimeoutSeconds)). + Value(&f.timeout).Validate(func(v string) error { + v = strings.TrimSpace(v) + if v == "" { + return nil + } + if n, err := strconv.Atoi(v); err != nil || n <= 0 { + return errors.New("enter a whole number of seconds greater than 0") + } + return nil + }), + huh.NewConfirm().Title("Open the main menu when iics runs without a command?").Value(&f.menu), + ).Title("Other") + + return []*huh.Group{newUser, appearance, other} +} + +// apply returns a copy of c with the form values. +func (f *configForm) apply(c *config.Config) *config.Config { + out := *c + nu := config.NewUserConfig{ + Domain: strings.TrimSpace(f.domain), + UserNamePattern: strings.TrimSpace(f.unPat), + EmailPattern: strings.TrimSpace(f.emPat), + } + out.NewUser = nil + if !nu.IsZero() { + out.NewUser = &nu + } + out.Style.Theme = f.theme + out.Style.HeaderColor = strings.TrimSpace(f.headerColor) + out.Style.NoColor = f.noColor + // Keep "unset" when the value matches the default (true). + if f.responsive != c.Style.ResponsiveTablesEnabled() || c.Style.ResponsiveTables != nil { + r := f.responsive + out.Style.ResponsiveTables = &r + } + out.HTTPTimeout, _ = strconv.Atoi(strings.TrimSpace(f.timeout)) + if f.menu != (c.UI.Menu == nil || *c.UI.Menu) || c.UI.Menu != nil { + m := f.menu + out.UI.Menu = &m + } + return &out +} + +// ConfigChanges describes the differences in global settings between two +// configs as "setting: old -> new" lines. +func ConfigChanges(before, after *config.Config) []string { + nu := func(c *config.Config) config.NewUserConfig { + if c.NewUser == nil { + return config.NewUserConfig{} + } + return *c.NewUser + } + timeout := func(c *config.Config) string { + if c.HTTPTimeout <= 0 { + return "" + } + return strconv.Itoa(c.HTTPTimeout) + } + menu := func(c *config.Config) string { return fmt.Sprint(c.UI.Menu == nil || *c.UI.Menu) } + var out []string + for _, c := range []struct{ name, old, new string }{ + {"New user domain", nu(before).Domain, nu(after).Domain}, + {"User name pattern", nu(before).UserNamePattern, nu(after).UserNamePattern}, + {"Email pattern", nu(before).EmailPattern, nu(after).EmailPattern}, + {"Table theme", before.Style.Theme, after.Style.Theme}, + {"Header color", before.Style.HeaderColor, after.Style.HeaderColor}, + {"No color", fmt.Sprint(before.Style.NoColor), fmt.Sprint(after.Style.NoColor)}, + {"Responsive tables", fmt.Sprint(before.Style.ResponsiveTablesEnabled()), fmt.Sprint(after.Style.ResponsiveTablesEnabled())}, + {"HTTP timeout", timeout(before), timeout(after)}, + {"Main menu", menu(before), menu(after)}, + } { + if c.old != c.new { + out = append(out, fmt.Sprintf("%s: %s -> %s", c.name, orDash(c.old), orDash(c.new))) + } + } + return out +} diff --git a/internal/tui/configwizard_test.go b/internal/tui/configwizard_test.go new file mode 100644 index 0000000..00e736b --- /dev/null +++ b/internal/tui/configwizard_test.go @@ -0,0 +1,42 @@ +package tui + +import ( + "reflect" + "testing" + + "github.com/jbrazda/iics-cli/internal/config" +) + +func TestConfigFormApply(t *testing.T) { + before := &config.Config{HTTPTimeout: 60} + f := newConfigForm(before) + if f.timeout != "60" || !f.responsive || !f.menu { + t.Errorf("form = %+v", f) + } + + // Unchanged form: no changes, defaults stay unset. + after := f.apply(before) + if lines := ConfigChanges(before, after); len(lines) != 0 { + t.Errorf("unexpected changes %v", lines) + } + if after.Style.ResponsiveTables != nil || after.UI.Menu != nil || after.NewUser != nil { + t.Errorf("defaults were written: %+v", after) + } + + f.domain, f.emPat = " acme.com ", "{firstInitial}{lastName}@{domain}" + f.theme, f.timeout, f.menu = "minimal", "", false + after = f.apply(before) + want := []string{ + "New user domain: - -> acme.com", + "Email pattern: - -> {firstInitial}{lastName}@{domain}", + "Table theme: - -> minimal", + "HTTP timeout: 60 -> -", + "Main menu: true -> false", + } + if got := ConfigChanges(before, after); !reflect.DeepEqual(got, want) { + t.Errorf("ConfigChanges = %q\nwant %q", got, want) + } + if before.NewUser != nil || before.Style.Theme != "" { + t.Error("apply modified the input config") + } +} diff --git a/internal/tui/profilewizard.go b/internal/tui/profilewizard.go new file mode 100644 index 0000000..e69a732 --- /dev/null +++ b/internal/tui/profilewizard.go @@ -0,0 +1,339 @@ +package tui + +import ( + "errors" + "fmt" + "io" + "slices" + "strings" + + "github.com/charmbracelet/huh" + "github.com/jbrazda/iics-cli/internal/config" +) + +// customLoginURL is the region option that selects a custom login URL. +const customLoginURL = "custom" + +// ProfileWizardInput configures RunProfileWizard. +type ProfileWizardInput struct { + // Name is the profile name. + Name string + // Existing is the stored profile, or nil for a new one. It is not + // modified. + Existing *config.Profile + // Global holds the global new-user defaults, shown as inherited values. + Global *config.NewUserConfig + // IsDefault reports whether the profile is the default profile now. + IsDefault bool + + Out io.Writer + Accessible bool +} + +// ProfileWizardResult is the outcome of RunProfileWizard. +type ProfileWizardResult struct { + Profile *config.Profile + MakeDefault bool + // StoreInKeychain is true when Profile.Password should be moved to the + // OS keychain. It is false when an unchanged keychain password is kept. + StoreInKeychain bool +} + +// profileForm holds the form values of the profile wizard. +type profileForm struct { + username, password string + region, loginURL string + caiURL string + production, makeDflt bool + keychain bool + domain, unPat, emPat string + existingKeychain, hasPassword bool +} + +// RunProfileWizard collects a profile in a paged form (Connection, Login +// URL when custom, Options, Keychain, New user) followed by a review. It +// returns nil when the user canceled. +func RunProfileWizard(in ProfileWizardInput) (*ProfileWizardResult, error) { + f := newProfileForm(in) + for { + form := huh.NewForm(profilePages(in, f)...).WithOutput(in.Out).WithAccessible(in.Accessible) + if err := form.Run(); err != nil { + if errors.Is(err, huh.ErrUserAborted) { + return nil, nil + } + return nil, err + } + res := f.result(in) + var lines []string + title := "Save profile " + in.Name + if in.Existing == nil { + title = "Create profile " + in.Name + lines = ProfileSummary(res.Profile) + } else { + lines = ProfileChanges(in.Existing, res.Profile) + if len(lines) == 0 { + lines = []string{"No changes."} + } + } + if res.MakeDefault && !in.IsDefault { + lines = append(lines, "Set as default profile") + } + if res.StoreInKeychain { + lines = append(lines, "Store password in OS keychain") + } + choice, err := confirmReview(in.Out, in.Accessible, title, lines, "Save", true) + if err != nil { + return nil, err + } + switch choice { + case reviewApply: + return res, nil + case reviewCancel: + return nil, nil + } + } +} + +func newProfileForm(in ProfileWizardInput) *profileForm { + p := &config.Profile{} + if in.Existing != nil { + *p = *in.Existing + } + f := &profileForm{ + username: p.Username, + caiURL: p.CaiURL, + production: config.IsProductionProfile(in.Name, p), + makeDflt: in.Existing == nil || in.IsDefault, + keychain: true, + existingKeychain: config.IsKeyringSentinel(p.Password), + hasPassword: p.Password != "", + } + if f.caiURL == "" && p.BaseAPIURL != "" { + f.caiURL = config.DeriveCaiURL(p.BaseAPIURL) + } + region := strings.ToUpper(p.Region) + switch { + case region != "" && slices.Contains(config.Regions(), region): + f.region = region + case p.LoginURL != "": + f.region, f.loginURL = customLoginURL, p.LoginURL + } + if p.NewUser != nil { + f.domain, f.unPat, f.emPat = p.NewUser.Domain, p.NewUser.UserNamePattern, p.NewUser.EmailPattern + } + return f +} + +// keepsKeychainPassword reports whether the form keeps an existing keychain +// password unchanged (the keychain question is skipped then). +func (f *profileForm) keepsKeychainPassword() bool { + return f.password == "" && f.existingKeychain +} + +func profilePages(in ProfileWizardInput, f *profileForm) []*huh.Group { + title := "New profile " + in.Name + if in.Existing != nil { + title = "Profile " + in.Name + } + pwDesc := "Required." + switch { + case f.existingKeychain: + pwDesc = "Leave empty to keep the current password (OS keychain)." + case f.hasPassword: + pwDesc = "Leave empty to keep the current password." + } + regionOpts := make([]huh.Option[string], 0, len(config.Regions())+1) + for _, r := range config.Regions() { + regionOpts = append(regionOpts, huh.NewOption(r, r)) + } + regionOpts = append(regionOpts, huh.NewOption("Custom login URL", customLoginURL)) + + connection := huh.NewGroup( + huh.NewNote().Title(title), + huh.NewInput().Title("User name").Value(&f.username).Validate(required("user name")), + huh.NewInput().Title("Password").Description(pwDesc). + EchoMode(huh.EchoModePassword).Value(&f.password). + Validate(func(v string) error { + if v == "" && !f.hasPassword { + return errors.New("password is required") + } + return nil + }), + huh.NewSelect[string]().Title("Region"). + Description("/ filter; choose \"Custom login URL\" for a login URL"). + Options(regionOpts...).Height(8).Value(&f.region). + Validate(func(v string) error { + if v == "" { + return errors.New("choose a region") + } + return nil + }), + ).Title("Connection") + + login := huh.NewGroup( + huh.NewInput().Title("Login URL"). + Description("For example https://dm-us.informaticacloud.com/saas/public/core/v3/login"). + Value(&f.loginURL).Validate(validLoginURL), + ).Title("Login URL").WithHideFunc(func() bool { return f.region != customLoginURL }) + + options := huh.NewGroup( + huh.NewInput().Title("CAI URL (optional)"). + Description("Leave empty to derive it from the org URL on login."). + Value(&f.caiURL), + huh.NewConfirm().Title("Production org?"). + Description("The main menu asks before running changes against a production org."). + Value(&f.production), + huh.NewConfirm().Title("Set as default profile?").Value(&f.makeDflt), + ).Title("Options") + + keychain := huh.NewGroup( + huh.NewConfirm().Title("Store password in OS keychain?"). + Description("Recommended. Otherwise the password is stored in the config file."). + Value(&f.keychain), + ).Title("Keychain").WithHideFunc(f.keepsKeychainPassword) + + inherited := func(pick func(config.NewUserConfig, config.NewUserSources) (string, string)) func() string { + return func() string { + v, src := config.ResolveNewUser(in.Global, &config.Profile{Username: f.username}) + val, source := pick(v, src) + if val == "" { + return "Leave empty to inherit (no value: set a domain here or globally)." + } + return fmt.Sprintf("Leave empty to inherit %s (%s).", val, source) + } + } + newUser := huh.NewGroup( + huh.NewNote().Title("New user defaults for this profile"). + Description("Placeholders: {"+strings.Join(config.UserPatternPlaceholders, "}, {")+"}.\nGlobal defaults: iics config edit."), + huh.NewInput().Title("Domain").Value(&f.domain). + DescriptionFunc(inherited(func(v config.NewUserConfig, s config.NewUserSources) (string, string) { + return v.Domain, s.Domain + }), &f.username), + huh.NewInput().Title("User name pattern").Value(&f.unPat).Validate(optionalPattern). + DescriptionFunc(inherited(func(v config.NewUserConfig, s config.NewUserSources) (string, string) { + return v.UserNamePattern, s.UserNamePattern + }), &f.username), + huh.NewInput().Title("Email pattern").Value(&f.emPat).Validate(optionalPattern). + DescriptionFunc(inherited(func(v config.NewUserConfig, s config.NewUserSources) (string, string) { + return v.EmailPattern, s.EmailPattern + }), &f.username), + ).Title("New user") + + return []*huh.Group{connection, login, options, keychain, newUser} +} + +// result builds the profile from the form values. +func (f *profileForm) result(in ProfileWizardInput) *ProfileWizardResult { + p := &config.Profile{} + if in.Existing != nil { + *p = *in.Existing + } + p.Name = in.Name + p.Username = strings.TrimSpace(f.username) + if f.password != "" { + p.Password = f.password + } + if f.region == customLoginURL { + p.Region, p.LoginURL = "", strings.TrimSpace(f.loginURL) + } else { + p.Region = f.region + if u, err := config.LoginURL(f.region); err == nil { + p.LoginURL = u + } + } + p.CaiURL = strings.TrimSpace(f.caiURL) + production := f.production + p.Production = &production + nu := config.NewUserConfig{ + Domain: strings.TrimSpace(f.domain), + UserNamePattern: strings.TrimSpace(f.unPat), + EmailPattern: strings.TrimSpace(f.emPat), + } + p.NewUser = nil + if !nu.IsZero() { + p.NewUser = &nu + } + return &ProfileWizardResult{ + Profile: p, + MakeDefault: f.makeDflt, + StoreInKeychain: f.keychain && !f.keepsKeychainPassword(), + } +} + +// ProfileSummary lists the settings of a profile. +func ProfileSummary(p *config.Profile) []string { + lines := []string{ + "User name: " + p.Username, + "Password: " + passwordLabel(p.Password), + "Region: " + orDash(p.Region), + "Login URL: " + orDash(p.LoginURL), + "CAI URL: " + orDash(p.CaiURL), + fmt.Sprintf("Production: %t", p.Production != nil && *p.Production), + } + if nu := p.NewUser; nu != nil { + lines = append(lines, + "New user domain: "+orDash(nu.Domain), + "User name pattern: "+orDash(nu.UserNamePattern), + "Email pattern: "+orDash(nu.EmailPattern)) + } + return lines +} + +// ProfileChanges describes the differences between two versions of a +// profile as "field: old -> new" lines. Passwords are never shown. +func ProfileChanges(before, after *config.Profile) []string { + nu := func(p *config.Profile) config.NewUserConfig { + if p.NewUser == nil { + return config.NewUserConfig{} + } + return *p.NewUser + } + prod := func(p *config.Profile) string { + return fmt.Sprint(p.Production != nil && *p.Production) + } + var out []string + for _, c := range []struct{ name, old, new string }{ + {"User name", before.Username, after.Username}, + {"Region", before.Region, after.Region}, + {"Login URL", before.LoginURL, after.LoginURL}, + {"CAI URL", before.CaiURL, after.CaiURL}, + {"Production", prod(before), prod(after)}, + {"New user domain", nu(before).Domain, nu(after).Domain}, + {"User name pattern", nu(before).UserNamePattern, nu(after).UserNamePattern}, + {"Email pattern", nu(before).EmailPattern, nu(after).EmailPattern}, + } { + if c.old != c.new { + out = append(out, fmt.Sprintf("%s: %s -> %s", c.name, orDash(c.old), orDash(c.new))) + } + } + if before.Password != after.Password { + out = append(out, "Password: changed") + } + return out +} + +func passwordLabel(pw string) string { + switch { + case pw == "": + return "-" + case config.IsKeyringSentinel(pw): + return "*** (keychain)" + default: + return "***" + } +} + +func validLoginURL(v string) error { + v = strings.TrimSpace(v) + if !strings.HasPrefix(v, "https://") && !strings.HasPrefix(v, "http://") { + return errors.New("enter a URL starting with https://") + } + return nil +} + +func optionalPattern(v string) error { + if strings.TrimSpace(v) == "" { + return nil + } + return config.ValidateUserPattern(v) +} diff --git a/internal/tui/profilewizard_test.go b/internal/tui/profilewizard_test.go new file mode 100644 index 0000000..c4cb720 --- /dev/null +++ b/internal/tui/profilewizard_test.go @@ -0,0 +1,68 @@ +package tui + +import ( + "reflect" + "testing" + + "github.com/jbrazda/iics-cli/internal/config" +) + +func TestProfileFormResult(t *testing.T) { + existing := &config.Profile{ + Username: "admin@acme.com", Password: config.KeyringSentinel, Region: "usw3", + BaseAPIURL: "https://usw3.dm-us.informaticacloud.com/saas", + NewUser: &config.NewUserConfig{EmailPattern: "{lastName}@{domain}"}, + } + in := ProfileWizardInput{Name: "dev", Existing: existing} + f := newProfileForm(in) + if f.region != "USW3" || f.caiURL != "https://usw3-cai.dm-us.informaticacloud.com" || f.makeDflt { + t.Errorf("form = %+v", f) + } + + // Keeping the keychain password skips the keychain question. + res := f.result(in) + if res.Profile.Password != config.KeyringSentinel || res.StoreInKeychain { + t.Errorf("kept keychain password: password=%q store=%v", res.Profile.Password, res.StoreInKeychain) + } + if res.Profile.LoginURL != "https://dm-us.informaticacloud.com/saas/public/core/v3/login" { + t.Errorf("login URL = %q", res.Profile.LoginURL) + } + + // New password, custom login URL, cleared new user overrides. + f.password = "secret" + f.region, f.loginURL = customLoginURL, " https://example.com/login " + f.emPat = "" + res = f.result(in) + p := res.Profile + if p.Password != "secret" || !res.StoreInKeychain || p.Region != "" || p.LoginURL != "https://example.com/login" || p.NewUser != nil { + t.Errorf("result = %+v store=%v", p, res.StoreInKeychain) + } + if existing.Password != config.KeyringSentinel || existing.NewUser == nil { + t.Error("existing profile was modified") + } + + got := ProfileChanges(existing, p) + want := []string{ + "Region: usw3 -> -", + "Login URL: - -> https://example.com/login", + "CAI URL: - -> https://usw3-cai.dm-us.informaticacloud.com", + "Email pattern: {lastName}@{domain} -> -", + "Password: changed", + } + if !reflect.DeepEqual(got, want) { + t.Errorf("ProfileChanges = %q\nwant %q", got, want) + } +} + +func TestProfileFormNewProfile(t *testing.T) { + f := newProfileForm(ProfileWizardInput{Name: "prod"}) + if !f.production || !f.makeDflt || f.region != "" || f.hasPassword { + t.Errorf("new form = %+v", f) + } + if err := optionalPattern("{nope}"); err == nil { + t.Error("optionalPattern accepted an unknown placeholder") + } + if err := validLoginURL("example.com"); err == nil { + t.Error("validLoginURL accepted a URL without scheme") + } +} diff --git a/internal/tui/userwizard_test.go b/internal/tui/userwizard_test.go index 645e5b4..de806ba 100644 --- a/internal/tui/userwizard_test.go +++ b/internal/tui/userwizard_test.go @@ -58,7 +58,7 @@ func TestUserSummary(t *testing.T) { } func TestWizardSuggestions(t *testing.T) { - in := UserWizardInput{ProfileName: "dev", Patterns: config.NewUserPatterns(&config.Profile{Username: "admin@acme.com"})} + in := UserWizardInput{ProfileName: "dev", Patterns: config.NewUserPatterns(nil, &config.Profile{Username: "admin@acme.com"})} u := &client.User{FirstName: " Ann ", LastName: "Lee"} if got := in.suggestions(in.Patterns.UserNamePattern, u); len(got) != 1 || got[0] != "ann.lee.dev@acme.com" { t.Errorf("user name suggestion = %v", got) From 36823281fe0661f698e61012328c4cf7a04c61f4 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Mon, 5 Oct 2026 16:20:04 -0400 Subject: [PATCH 21/52] build: move main package to cmd/iics so go install produces iics go install names the binary after the main package directory, so installing from the module root produced iics-cli while release archives, make build and all documentation use iics. - move main.go to cmd/iics/main.go - point Makefile build, install and completions targets at ./cmd/iics - point goreleaser and the CI build job at ./cmd/iics - update the README install command and the directory layout docs - go mod tidy: mark github.com/spf13/pflag as a direct dependency The install command is now: go install github.com/jbrazda/iics-cli/cmd/iics@latest --- .github/workflows/ci.yml | 2 +- .goreleaser.yaml | 2 +- Makefile | 12 ++++++------ README.md | 2 +- main.go => cmd/iics/main.go | 0 docs/CLAUDE.md | 2 +- docs/DESIGN.md | 2 +- go.mod | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) rename main.go => cmd/iics/main.go (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b310ff8..588da4c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,7 +65,7 @@ jobs: EXT="" if [ "$GOOS" = "windows" ]; then EXT=".exe"; fi go build -ldflags="-s -w -X main.version=${{ github.sha }}" \ - -o iics-${{ matrix.goos }}-${{ matrix.goarch }}${EXT} . + -o iics-${{ matrix.goos }}-${{ matrix.goarch }}${EXT} ./cmd/iics - name: Upload binary uses: actions/upload-artifact@v7 diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 52cd243..cf11843 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -8,7 +8,7 @@ before: - go vet ./... builds: - - main: . + - main: ./cmd/iics binary: iics env: - CGO_ENABLED=0 diff --git a/Makefile b/Makefile index 04115ea..a97cbaa 100644 --- a/Makefile +++ b/Makefile @@ -5,10 +5,10 @@ LDFLAGS := -ldflags "-s -w -X main.version=$(VERSION)" .PHONY: build clean test vet lint fmt install completions build: - go build $(LDFLAGS) -o $(BINARY_NAME) . + go build $(LDFLAGS) -o $(BINARY_NAME) ./cmd/iics install: - go install $(LDFLAGS) . + go install $(LDFLAGS) ./cmd/iics clean: rm -f $(BINARY_NAME) @@ -27,9 +27,9 @@ lint: vet @which golangci-lint > /dev/null 2>&1 && golangci-lint run ./... || true completions: - go run . completion bash > completions/iics.bash - go run . completion zsh > completions/iics.zsh - go run . completion fish > completions/iics.fish - go run . completion powershell > completions/iics.ps1 + go run ./cmd/iics completion bash > completions/iics.bash + go run ./cmd/iics completion zsh > completions/iics.zsh + go run ./cmd/iics completion fish > completions/iics.fish + go run ./cmd/iics completion powershell > completions/iics.ps1 all: fmt vet test clean build completions diff --git a/README.md b/README.md index 8fc69ee..e6fa587 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ A comprehensive command-line interface for the [Informatica Intelligent Cloud Se Requires Go 1.25 or later. ```bash -go install github.com/jbrazda/iics-cli@latest +go install github.com/jbrazda/iics-cli/cmd/iics@latest ``` ### From releases diff --git a/main.go b/cmd/iics/main.go similarity index 100% rename from main.go rename to cmd/iics/main.go diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md index 2c2941d..5242146 100644 --- a/docs/CLAUDE.md +++ b/docs/CLAUDE.md @@ -21,10 +21,10 @@ imports, users, roles, schedules, agents, runtime environments, and more. ```text iics_cli/ -├── main.go # Entry point; injects version via ldflags ├── go.mod / go.sum ├── Makefile ├── cmd/ # Cobra command definitions (thin orchestration layer) +│ ├── iics/main.go # Entry point; injects version via ldflags │ ├── root.go # Root command, global flags, init(), helpers │ ├── login.go / logout.go │ ├── agent.go # Secure agents (v2 API) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index c1751a4..344586f 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -19,10 +19,10 @@ A comprehensive Go CLI tool (`iics`) to interact with the Informatica Intelligen ```text iics_cli/ -├── main.go # Entry point: cmd.Execute() ├── go.mod / go.sum ├── Makefile ├── cmd/ # Cobra command definitions (thin layer) +│ ├── iics/main.go # Entry point: cmd.Execute() │ ├── root.go # Root cmd, global flags, config/client init │ ├── login.go / logout.go │ ├── objects.go # objects list, dependencies diff --git a/go.mod b/go.mod index d1815f9..cb71daf 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/charmbracelet/lipgloss v1.1.0 github.com/mattn/go-isatty v0.0.24 github.com/spf13/cobra v1.10.2 + github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 github.com/zalando/go-keyring v0.2.8 golang.org/x/term v0.45.0 @@ -47,7 +48,6 @@ require ( github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect - github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect From 20052b476a4cb0ba4f72bf5f74a754a8426960e3 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Mon, 5 Oct 2026 16:20:25 -0400 Subject: [PATCH 22/52] docs: note cmd/iics install path change in CHANGELOG --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3da2ec..0becd0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- The main package moved to `cmd/iics`, so `go install` and `make install` + now produce a binary named `iics` instead of `iics-cli`. Install from + source with `go install github.com/jbrazda/iics-cli/cmd/iics@latest`; the + previous `go install github.com/jbrazda/iics-cli@latest` path no longer + works. Release archives are unaffected. + ## [0.5.6] - 2026-09-22 ### Fixed From 7de106c884893b785fde72e9ac7c0dddc015e5b7 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Mon, 5 Oct 2026 19:25:40 -0400 Subject: [PATCH 23/52] feat(menu): Delete profile entry with profile picker Add a Session entry that lists the configured profiles and runs 'profile delete ' for the picked one. When the active profile is deleted, the menu continues on the default profile, or on the first remaining profile when there is no default. --- CHANGELOG.md | 5 ++++ docs/documentation/menu.md | 6 +++- internal/menu/entries.go | 8 +++-- internal/menu/loop.go | 38 ++++++++++++++++++++++-- internal/menu/menu_test.go | 61 ++++++++++++++++++++++++++++++++++++++ internal/menu/model.go | 10 ++++++- 6 files changed, 121 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0becd0e..77e4f2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Main menu: **Delete profile** entry in the Session group picks a profile + from the list and runs `profile delete` for it + ### Changed - The main package moved to `cmd/iics`, so `go install` and `make install` diff --git a/docs/documentation/menu.md b/docs/documentation/menu.md index 7de96db..3623d84 100644 --- a/docs/documentation/menu.md +++ b/docs/documentation/menu.md @@ -93,6 +93,10 @@ enter run · / filter · p profile · ? help · q quit then `defaultProfile` from the config. - **Switch profile** (`p`) changes the profile for this menu session only. - **Set as default profile** writes `defaultProfile` to the config file. +- **Delete profile** lists the configured profiles, then runs + `profile delete ` for the one you pick, which asks for confirmation. + If the active profile is deleted, the menu continues on the default + profile, or on the first remaining profile when there is no default. - With no profiles configured, the menu starts `profile add` first. If that is canceled, only the Session entries are available. @@ -112,7 +116,7 @@ On a production profile: | Group | Entries | | ----------------------- | ----------------------------------------------------------------------- | -| Session | Switch profile, Set as default profile ✎, Login, List profiles, Add profile ✎, Edit active profile ✎, Edit global settings | +| Session | Switch profile, Set as default profile ✎, Login, List profiles, Add profile ✎, Edit active profile ✎, Delete profile ✎, Edit global settings | | Users | List users, Show user (search by user name, ID, or part of a name/email), Create user ✎, Edit user groups and roles ✎, Delete user ✎, Change password ✎ | | User groups | List, Show, Create ✎, Edit roles ✎, Delete ✎ | | Roles | List, Show (with privileges), Create ✎, Edit privileges ✎ | diff --git a/internal/menu/entries.go b/internal/menu/entries.go index 527532b..78c7440 100644 --- a/internal/menu/entries.go +++ b/internal/menu/entries.go @@ -28,18 +28,21 @@ type Entry struct { ActiveProfileArg bool // AskArg prompts for a text argument appended to Args (label shown). AskArg string + // PickProfile prompts for a configured profile whose name is appended to + // Args (picker title shown). + PickProfile string // SessionOnly entries stay usable when no profile is configured. SessionOnly bool } // CommandArgs returns the arguments to run for the entry with the active -// profile, and the text argument from AskArg when given. +// profile, and the argument from AskArg or PickProfile when given. func (e Entry) CommandArgs(profile, asked string) []string { args := append([]string(nil), e.Args...) if e.ActiveProfileArg && profile != "" { args = append(args, profile) } - if e.AskArg != "" && asked != "" { + if (e.AskArg != "" || e.PickProfile != "") && asked != "" { args = append(args, asked) } if !e.NoProfileFlag && profile != "" { @@ -70,6 +73,7 @@ func DefaultEntries() []Entry { {Group: "Session", Label: "List profiles", Description: "Show configured profiles", Args: []string{"profile", "list"}, NoProfileFlag: true, SessionOnly: true}, {Group: "Session", Label: "Add profile", Description: "Set up a new org profile", Args: []string{"profile", "add"}, AskArg: "Profile name", NoProfileFlag: true, Writes: true, SessionOnly: true}, {Group: "Session", Label: "Edit active profile", Description: "Update credentials, region, production flag and new user defaults of the active profile", Args: []string{"profile", "edit"}, ActiveProfileArg: true, NoProfileFlag: true, Writes: true}, + {Group: "Session", Label: "Delete profile", Description: "Pick a profile and delete it with its cached session and keychain entry", Args: []string{"profile", "delete"}, PickProfile: "Delete profile", NoProfileFlag: true, Writes: true, SessionOnly: true}, {Group: "Session", Label: "Edit global settings", Description: "New user defaults for all profiles, table theme, HTTP timeout, main menu", Args: []string{"config", "edit"}, NoProfileFlag: true, SessionOnly: true}, {Group: "Users", Label: "List users", Description: "Show all users", Args: []string{"user", "list"}}, diff --git a/internal/menu/loop.go b/internal/menu/loop.go index d3758de..5597e83 100644 --- a/internal/menu/loop.go +++ b/internal/menu/loop.go @@ -173,7 +173,7 @@ func Run(d Deps, startProfile string) error { } switch res.Kind { case ResultSwitchProfile: - if picked, perr := d.pickProfile(st.Profiles, profile); perr != nil { + if picked, perr := d.pickProfile("Switch profile", st.Profiles, profile); perr != nil { return perr } else if picked != "" { profile = picked @@ -214,10 +214,27 @@ func Run(d Deps, startProfile string) error { continue } } + if e.PickProfile != "" { + asked, err = d.pickProfile(e.PickProfile, st.Profiles, profile) + if err != nil { + return err + } + if asked == "" { + continue + } + } code = d.run(dim, e.CommandArgs(profile, asked)) if code == 0 && e.AskArg != "" && !st.HasProfile { profile = asked // first profile just added } + if e.PickProfile != "" && asked == profile { + // The active profile may be gone: fall back to another one. + after, lerr := d.LoadState(profile) + if lerr != nil { + return lerr + } + profile = remainingProfile(after.Profiles, profile) + } } if code == 0 { @@ -294,12 +311,12 @@ func lineMenu(d Deps, entries []Entry, st State) (Result, error) { return Result{Kind: ResultRun, Entry: e}, nil } -func (d Deps) pickProfile(profiles []ProfileInfo, current string) (string, error) { +func (d Deps) pickProfile(title string, profiles []ProfileInfo, current string) (string, error) { if len(profiles) == 0 { _, _ = fmt.Fprintln(d.Out, "No profiles configured.") return "", nil } - idx, err := tui.PickOne(d.Prompter, "Switch profile", profiles, func(p ProfileInfo) string { + idx, err := tui.PickOne(d.Prompter, title, profiles, func(p ProfileInfo) string { label := p.Name if p.Region != "" { label += " " + p.Region @@ -320,3 +337,18 @@ func (d Deps) pickProfile(profiles []ProfileInfo, current string) (string, error } return profiles[idx].Name, nil } + +// remainingProfile returns current while it is still configured, otherwise +// the default profile, the first profile, or "" when none is left. +func remainingProfile(profiles []ProfileInfo, current string) string { + next := "" + for i, p := range profiles { + if p.Name == current { + return current + } + if p.Default || i == 0 { + next = p.Name + } + } + return next +} diff --git a/internal/menu/menu_test.go b/internal/menu/menu_test.go index 32a90c8..23a5630 100644 --- a/internal/menu/menu_test.go +++ b/internal/menu/menu_test.go @@ -174,6 +174,11 @@ func (f *fakeRunner) Run(args []string) int { return c } +// runnerFunc adapts a function to Runner. +type runnerFunc func(args []string) int + +func (f runnerFunc) Run(args []string) int { return f(args) } + type loopHarness struct { deps Deps runner *fakeRunner @@ -281,6 +286,62 @@ func TestLoopSwitchProfileAndSetDefault(t *testing.T) { } } +func TestLoopDeleteProfile(t *testing.T) { + del := Entry{Group: "Session", Label: "Delete profile", Description: "d", Args: []string{"profile", "delete"}, PickProfile: "Delete profile", NoProfileFlag: true, SessionOnly: true} + if got := del.CommandArgs("dev", "qa"); !reflect.DeepEqual(got, []string{"profile", "delete", "qa"}) { + t.Errorf("CommandArgs = %v", got) + } + + three := []ProfileInfo{{Name: "dev"}, {Name: "prd", Production: true}, {Name: "qa", Default: true}} + states := map[string]State{ + "dev": {HasProfile: true, Profiles: three}, + "qa": {HasProfile: true, Profiles: three}, + } + // Cancel the picker, delete another profile (2 = prd), delete the active + // profile (1 = dev), then run a read on the profile the menu fell back to. + h := newHarness("0\n2\n1\n", []Result{ + {Kind: ResultRun, Entry: del}, + {Kind: ResultRun, Entry: del}, + {Kind: ResultRun, Entry: del}, + {Kind: ResultRun, Entry: testEntries[2]}, + }, states) + h.deps.Runner = runnerFunc(func(args []string) int { + if args[1] == "delete" && args[2] == "dev" { + gone := []ProfileInfo{{Name: "prd", Production: true}, {Name: "qa", Default: true}} + h.states["dev"] = State{Profiles: gone} + h.states["qa"] = State{HasProfile: true, Profiles: gone} + } + return h.runner.Run(args) + }) + if err := Run(h.deps, "dev"); err != nil { + t.Fatal(err) + } + want := [][]string{ + {"profile", "delete", "prd"}, + {"profile", "delete", "dev"}, + {"role", "list", "--profile", "qa"}, + } + if !reflect.DeepEqual(h.runner.calls, want) { + t.Errorf("calls = %v, want %v", h.runner.calls, want) + } +} + +func TestRemainingProfile(t *testing.T) { + ps := []ProfileInfo{{Name: "a"}, {Name: "b", Default: true}} + if got := remainingProfile(ps, "a"); got != "a" { + t.Errorf("kept profile = %q, want a", got) + } + if got := remainingProfile(ps, "gone"); got != "b" { + t.Errorf("fallback = %q, want default b", got) + } + if got := remainingProfile([]ProfileInfo{{Name: "a"}, {Name: "c"}}, "gone"); got != "a" { + t.Errorf("fallback = %q, want first a", got) + } + if got := remainingProfile(nil, "gone"); got != "" { + t.Errorf("fallback = %q, want empty", got) + } +} + func TestLoopFirstRunAddsProfile(t *testing.T) { states := map[string]State{ "": {}, diff --git a/internal/menu/model.go b/internal/menu/model.go index 2290a86..61bdf31 100644 --- a/internal/menu/model.go +++ b/internal/menu/model.go @@ -382,7 +382,7 @@ func (m *Model) View() string { if binary == "" { binary = "iics" } - b.WriteString(m.styles.cmd.Render(truncate("$ "+CommandLine(binary, e.CommandArgs(h.Profile, "<"+strings.ToLower(e.AskArg)+">")), w))) + b.WriteString(m.styles.cmd.Render(truncate("$ "+CommandLine(binary, e.CommandArgs(h.Profile, argPlaceholder(e))), w))) } b.WriteString("\n") } @@ -412,3 +412,11 @@ func orDash(s string) string { } return s } + +// argPlaceholder is the stand-in shown for an argument the entry asks for. +func argPlaceholder(e Entry) string { + if e.PickProfile != "" { + return "" + } + return "<" + strings.ToLower(e.AskArg) + ">" +} From ffac154e65cf1a279dcb4dc97a2c5d093c26f415 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Mon, 5 Oct 2026 19:25:40 -0400 Subject: [PATCH 24/52] fix(menu): pass global flags on to commands run from the menu globalFlagArgs used PersistentFlags().Visit on the root command, but cobra parses on the invoked command's own flag set, so the root set never recorded any flag as set and nothing was passed through. A menu started with --config listed profiles from that file while the commands it ran read ~/.iics/config.yaml. Check Flag.Changed with VisitAll instead. --- CHANGELOG.md | 6 ++++++ cmd/menu.go | 7 ++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77e4f2c..d1b0b4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 previous `go install github.com/jbrazda/iics-cli@latest` path no longer works. Release archives are unaffected. +### Fixed + +- Main menu: global flags given when starting the menu (such as `--config`, + `--theme` or `--http-timeout`) are now passed on to the commands the menu + runs; previously they were dropped, so commands read the default config + ## [0.5.6] - 2026-09-22 ### Fixed diff --git a/cmd/menu.go b/cmd/menu.go index f966f24..2106405 100644 --- a/cmd/menu.go +++ b/cmd/menu.go @@ -95,7 +95,12 @@ func (r passthroughRunner) Run(args []string) int { // which the menu manages, and --output) as "--name=value" arguments. func globalFlagArgs(root *cobra.Command) []string { var out []string - root.PersistentFlags().Visit(func(f *pflag.Flag) { + // Not Visit: cobra parses on the invoked command's own flag set, so the + // root set never records which of its flags were set. + root.PersistentFlags().VisitAll(func(f *pflag.Flag) { + if !f.Changed { + return + } switch f.Name { case "profile", "output", "help": return From 75024c8d621f455904823c294240f31d8f63522a Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Wed, 7 Oct 2026 20:22:40 -0400 Subject: [PATCH 25/52] chore: move CR-0041 to CR-0046 and pending bugs to completed --- .../CR-0041-tui-shared-prompts-and-wizards.md | 0 .../CR-0042-permission-interactive-editor.md | 0 .../CR-0043-user-role-group-assignment-commands.md | 0 .../{pending => completed}/CR-0044-interactive-main-menu.md | 0 .../{pending => completed}/CR-0045-new-user-name-patterns.md | 0 .../CR-0046-profile-wizard-and-global-settings.md | 2 +- .../2026-09-30-permission-client-wrong-api-shape.md | 0 .../{pending => completed}/2026-09-30-user-update-v2-403.md | 0 docs/issues/{pending => completed}/Bug_edit_user_roles.md | 0 9 files changed, 1 insertion(+), 1 deletion(-) rename docs/ChangeRequests/{pending => completed}/CR-0041-tui-shared-prompts-and-wizards.md (100%) rename docs/ChangeRequests/{pending => completed}/CR-0042-permission-interactive-editor.md (100%) rename docs/ChangeRequests/{pending => completed}/CR-0043-user-role-group-assignment-commands.md (100%) rename docs/ChangeRequests/{pending => completed}/CR-0044-interactive-main-menu.md (100%) rename docs/ChangeRequests/{pending => completed}/CR-0045-new-user-name-patterns.md (100%) rename docs/ChangeRequests/{pending => completed}/CR-0046-profile-wizard-and-global-settings.md (99%) rename docs/issues/{pending => completed}/2026-09-30-permission-client-wrong-api-shape.md (100%) rename docs/issues/{pending => completed}/2026-09-30-user-update-v2-403.md (100%) rename docs/issues/{pending => completed}/Bug_edit_user_roles.md (100%) diff --git a/docs/ChangeRequests/pending/CR-0041-tui-shared-prompts-and-wizards.md b/docs/ChangeRequests/completed/CR-0041-tui-shared-prompts-and-wizards.md similarity index 100% rename from docs/ChangeRequests/pending/CR-0041-tui-shared-prompts-and-wizards.md rename to docs/ChangeRequests/completed/CR-0041-tui-shared-prompts-and-wizards.md diff --git a/docs/ChangeRequests/pending/CR-0042-permission-interactive-editor.md b/docs/ChangeRequests/completed/CR-0042-permission-interactive-editor.md similarity index 100% rename from docs/ChangeRequests/pending/CR-0042-permission-interactive-editor.md rename to docs/ChangeRequests/completed/CR-0042-permission-interactive-editor.md diff --git a/docs/ChangeRequests/pending/CR-0043-user-role-group-assignment-commands.md b/docs/ChangeRequests/completed/CR-0043-user-role-group-assignment-commands.md similarity index 100% rename from docs/ChangeRequests/pending/CR-0043-user-role-group-assignment-commands.md rename to docs/ChangeRequests/completed/CR-0043-user-role-group-assignment-commands.md diff --git a/docs/ChangeRequests/pending/CR-0044-interactive-main-menu.md b/docs/ChangeRequests/completed/CR-0044-interactive-main-menu.md similarity index 100% rename from docs/ChangeRequests/pending/CR-0044-interactive-main-menu.md rename to docs/ChangeRequests/completed/CR-0044-interactive-main-menu.md diff --git a/docs/ChangeRequests/pending/CR-0045-new-user-name-patterns.md b/docs/ChangeRequests/completed/CR-0045-new-user-name-patterns.md similarity index 100% rename from docs/ChangeRequests/pending/CR-0045-new-user-name-patterns.md rename to docs/ChangeRequests/completed/CR-0045-new-user-name-patterns.md diff --git a/docs/ChangeRequests/pending/CR-0046-profile-wizard-and-global-settings.md b/docs/ChangeRequests/completed/CR-0046-profile-wizard-and-global-settings.md similarity index 99% rename from docs/ChangeRequests/pending/CR-0046-profile-wizard-and-global-settings.md rename to docs/ChangeRequests/completed/CR-0046-profile-wizard-and-global-settings.md index ea5f256..63dd856 100644 --- a/docs/ChangeRequests/pending/CR-0046-profile-wizard-and-global-settings.md +++ b/docs/ChangeRequests/completed/CR-0046-profile-wizard-and-global-settings.md @@ -2,7 +2,7 @@ ## Status -Implemented, pending confirmation. +Completed. ## Summary diff --git a/docs/issues/pending/2026-09-30-permission-client-wrong-api-shape.md b/docs/issues/completed/2026-09-30-permission-client-wrong-api-shape.md similarity index 100% rename from docs/issues/pending/2026-09-30-permission-client-wrong-api-shape.md rename to docs/issues/completed/2026-09-30-permission-client-wrong-api-shape.md diff --git a/docs/issues/pending/2026-09-30-user-update-v2-403.md b/docs/issues/completed/2026-09-30-user-update-v2-403.md similarity index 100% rename from docs/issues/pending/2026-09-30-user-update-v2-403.md rename to docs/issues/completed/2026-09-30-user-update-v2-403.md diff --git a/docs/issues/pending/Bug_edit_user_roles.md b/docs/issues/completed/Bug_edit_user_roles.md similarity index 100% rename from docs/issues/pending/Bug_edit_user_roles.md rename to docs/issues/completed/Bug_edit_user_roles.md From 02004e21244cc7b748237360193cff2dd4bee0f8 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Sun, 11 Oct 2026 18:56:33 -0400 Subject: [PATCH 26/52] docs: add package doc comments and AGENTS.md --- AGENTS.md | 152 +++++++++++++++++++++++++++++++++++ cmd/doc.go | 4 + cmd/iics/main.go | 3 + internal/client/doc.go | 5 ++ internal/config/doc.go | 4 + internal/dependencies/doc.go | 5 ++ internal/output/doc.go | 4 + internal/release/doc.go | 6 ++ 8 files changed, 183 insertions(+) create mode 100644 AGENTS.md create mode 100644 cmd/doc.go create mode 100644 internal/client/doc.go create mode 100644 internal/config/doc.go create mode 100644 internal/dependencies/doc.go create mode 100644 internal/output/doc.go create mode 100644 internal/release/doc.go diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..906d383 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,152 @@ +# AGENTS.md + +This file provides guidance to Codex (Codex.ai/code) when working with code in this repository. + +> For the full authoritative reference (patterns, struct rules, API versioning, examples), read **`docs/AGENTS.md`** first. + +--- + +## Project Structure + +This is a Go CLI project using Cobra. Key paths: + +- `cmd/` - command definitions (thin wiring only) +- `internal/` - core logic (HTTP client, config, output) +- `docs/` - documentation and Change Requests (CRs) + +Change Requests live in `docs/ChangeRequests/` as markdown files and must be referenced when implementing features. + +--- + +## Git Workflow + +- Always work on the `dev` branch. Never commit directly to `main`. +- Before making any commits, verify the current branch with `git branch --show-current`. + +--- + +## Build & Validation + +After implementing changes, run the following before committing: + +```bash +/opt/local/bin/go build ./... +/opt/local/bin/go vet ./... +``` + +Also verify that the Go version in `go.mod` matches the version used in `.github/workflows/` CI files to avoid version mismatch failures. + +--- + +## API Integration + +When modifying API request/response structs: + +- Always verify field names and types against actual API responses - never guess. +- Check for type mismatches (e.g., `string` vs `int`) by referencing existing working examples in `internal/client/`. +- Test with a real API call or review existing client code before committing struct changes. + +--- + +## Common Commands + +```bash +# Build +/opt/local/bin/go build ./... +make build # includes ldflags with version injection + +# Test +/opt/local/bin/go test ./... +/opt/local/bin/go test -v -race ./internal/client/... # single package, with race detector +/opt/local/bin/go test -run TestListWidgets ./internal/client/... # single test + +# Vet / Format / Lint +/opt/local/bin/go vet ./... +gofmt -s -w . +golangci-lint run ./... +``` + +--- + +## Architecture Overview + +**Two-layer design:** + +- **`cmd/`** - thin Cobra command files; only flag parsing, `getClient()`, one client call, `getFormatter()`, format output. No API logic, no URL construction. +- **`internal/client/`** - all HTTP logic, API structs, session management, request/response handling. No Cobra or output dependencies. +- **`internal/config/`** - config file (`~/.iics/config.yaml`) and session cache (`~/.iics/sessions.yaml`) with 30-minute TTL. +- **`internal/output/`** - `Formatter` interface with `table`, `json`, `csv`, `yaml` backends. Columns are specified by JSON tag name, supporting dot notation for nested fields. + +**HTTP client** (`internal/client/client.go`): single `*Client` struct with `do()` → auto-login on first request, 401 → re-login + retry once. Auto-detects V2 vs V3 API from URL path and sets the correct session header (`icSessionId` for `/v2/`, `INFA-SESSION-ID` for all others). + +**Config resolution order** (highest to lowest): env vars (`IICS_*`) → `--profile` flag → `defaultProfile` in config file. + +--- + +## Commit Style + +- Do **not** include `Co-Authored-By: Codex ...` trailers in commit messages. +- **Before every commit**, run the following and fix all reported issues: + + ```bash + gofmt -s -w . + /opt/local/bin/go vet ./... + golangci-lint run ./... + ``` + +--- + +## Critical Constraints + +- **Table output**: use `output.New(format, w, output.TableStyle{})` from `internal/output/`. Do not add new `tablewriter` imports - the dependency was removed. +- **JSON tags must exactly match API field names** - never guess; verify against API docs. +- **Nested API objects must use typed structs** - never `[]string` for arrays of objects. +- **Never call `os.Exit()`** - always return errors from `RunE`. +- **Tests in `internal/` only** - use `newTestClient(handler)` with `httptest.Server`. No `cmd/` tests. +- **Do not refactor unrelated code** when fixing a bug or implementing a change request. + +--- + +## Adding a New Resource + +1. `internal/client/s.go` - struct + CRUD methods using `doJSON` / `doJSONWithQuery` +2. `internal/client/s_test.go` - test each method with `newTestClient` +3. `cmd/.go` - thin command wiring, `getClient` → client method → `getFormatter` → `f.Format` +4. `cmd/root.go` `init()` - `rootCmd.AddCommand(newResourceCmd())` +5. `docs/documentation/.md` - command reference page (synopsis, flags, output columns, examples) +6. `README.md` Commands table - add a row linking to the new doc page + +**Documentation is mandatory.** Every time a command is added or updated, steps 5 and 6 must be completed before the work is considered done. + +After updating documentation, regenerate shell completion scripts: + +```bash +make completions +``` + +Commit the updated files in `completions/` together with the code change. + +## Change Request and Bug Lifecycle + +### Change Requests (`docs/ChangeRequests/`) + +- **New CRs** are created in `docs/ChangeRequests/new/`. +- **After implementing** a CR, move the file to `docs/ChangeRequests/pending/` and include that move in the same commit as the implementation. +- **After the developer confirms** the CR is complete and correct, move the file to `docs/ChangeRequests/completed/` and include that move in the commit. +- If the developer has **not yet confirmed**, keep the file in `docs/ChangeRequests/pending/`. + +### Bugs (`docs/issues/`) + +- **New bugs** are created in `docs/issues/new/`. +- **After fixing** a bug, move the file to `docs/issues/pending/` and include that move in the same commit as the fix. +- **After the developer confirms** the fix is correct, move the file to `docs/issues/completed/` and include that move in the commit. +- If the developer has **not yet confirmed**, keep the file in `docs/issues/pending/`. + +--- + +## Markdown Rules + +- Generate Markdown that follows the [Markdown Lint Rules](https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md) +- Do **not** use em dashes (`-`) in generated Markdown; use a regular hyphen (`-`) instead + +See [docs/AGENTS.md](docs/AGENTS.md) for complete code templates for all four files. diff --git a/cmd/doc.go b/cmd/doc.go new file mode 100644 index 0000000..758c510 --- /dev/null +++ b/cmd/doc.go @@ -0,0 +1,4 @@ +// Package cmd implements the iics command-line interface: the root command, +// its global flags, and the subcommands that manage Informatica Intelligent +// Cloud Services (IICS) resources through the REST API v3. +package cmd diff --git a/cmd/iics/main.go b/cmd/iics/main.go index e7ce186..5a0131f 100644 --- a/cmd/iics/main.go +++ b/cmd/iics/main.go @@ -1,3 +1,6 @@ +// Command iics is the command-line interface for Informatica Intelligent Cloud +// Services (IICS). It sets the build version and runs the root command from +// package cmd. package main import ( diff --git a/internal/client/doc.go b/internal/client/doc.go new file mode 100644 index 0000000..5158242 --- /dev/null +++ b/internal/client/doc.go @@ -0,0 +1,5 @@ +// Package client is the HTTP client for the Informatica Intelligent Cloud +// Services (IICS) REST API. It handles login and logout, API errors and exit +// codes, and provides typed calls for each resource the CLI manages, such as +// agents, connections, users, roles, exports, imports and publishing. +package client diff --git a/internal/config/doc.go b/internal/config/doc.go new file mode 100644 index 0000000..0ee0314 --- /dev/null +++ b/internal/config/doc.go @@ -0,0 +1,4 @@ +// Package config loads and resolves the CLI configuration: profiles and +// regions from the config file, passwords from the system keyring, the cached +// login session, and the pattern used to build names for new users. +package config diff --git a/internal/dependencies/doc.go b/internal/dependencies/doc.go new file mode 100644 index 0000000..0c18db6 --- /dev/null +++ b/internal/dependencies/doc.go @@ -0,0 +1,5 @@ +// Package dependencies works out which IICS objects belong together. It +// resolves seed assets from user input or a build manifest, walks object +// references to build a dependency graph, and expands a selection to the +// closure of everything it references. +package dependencies diff --git a/internal/output/doc.go b/internal/output/doc.go new file mode 100644 index 0000000..a8924ab --- /dev/null +++ b/internal/output/doc.go @@ -0,0 +1,4 @@ +// Package output renders command results as a table, JSON, YAML or CSV. It +// also provides the cell helpers the table formatter relies on: sanitizing, +// wrapping and truncating text to fit the terminal width. +package output diff --git a/internal/release/doc.go b/internal/release/doc.go new file mode 100644 index 0000000..117febc --- /dev/null +++ b/internal/release/doc.go @@ -0,0 +1,6 @@ +// Package release plans and records the promotion of IICS assets between +// environments. It resolves the assets of a release, applies include and +// exclude policies, validates them against the target profiles, reads and +// writes the release manifest, and renders the Markdown run logs for export, +// import and publish. +package release From 901c88cc37daaeb1242480a4f1a1e1a5f0997c37 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Sun, 11 Oct 2026 18:56:33 -0400 Subject: [PATCH 27/52] docs: fix markdownlint errors across all Markdown files Add a project markdownlint-cli2 config (no line length limit, repeated headings allowed under different parents, H1 allowed next to a front matter title) and one for .github templates, then fix what is left: table alignment and escaped pipes, code fence languages, hard tabs and blank lines. Also repair two damaged pages: a stray example in activitylog.md that sat outside its code blocks and turned the rest of the page into code, and a leftover fragment of an old user get section in user.md. Test fixtures under testdata/ are not touched. --- .github/.markdownlint-cli2.yaml | 3 + .github/ISSUE_TEMPLATE/bug_report.md | 20 +- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- .github/copilot-instructions.md | 1 - .markdownlint-cli2.yaml | 11 + README.md | 168 +++++------ docs/CLAUDE.md | 48 +-- .../2026-03-24-add-powershell-examples.md | 3 + .../completed/CR-0001-import.md | 2 +- .../completed/CR-0004-package-command.md | 7 +- ...R-0005-strip-server-timestamp-on-expand.md | 1 - .../CR-0009-lipgloss-table-styling.md | 6 +- .../CR-0010-login-session-reliability.md | 26 +- ...-package-dependencies-connection-lookup.md | 10 +- .../CR-0013-consistent-log-format.md | 24 +- .../CR-0014-package-dependencies-output.md | 6 +- .../completed/CR-0015-auditlog-command.md | 284 +++++++++--------- .../CR-0017-secure-credential-storage.md | 4 +- .../CR-0018-user-command-enhancements.md | 46 +-- ...-0019-objects-dependencies-enhancements.md | 2 +- .../CR-002-ExportCommandEnhancments.md | 1 + .../CR-0021-CI-piepline-integration.md | 3 +- .../CR-0022-relase-manifest-creds.md | 1 - .../completed/CR-0023-selective-packaging.md | 8 +- .../CR-0028-release-manifest-md-log.md | 9 +- .../CR-0037-agent-details-services-table.md | 2 +- .../completed/CR-profile-edit.md | 2 +- .../new/CR-0025-package-exclusion-manifest.md | 8 +- .../new/CR-0026-build-manifest-report.md | 4 +- .../CR-0027-package-manifest-diff-report.md | 6 +- docs/HOW-TO-Vibe.md | 2 +- docs/documentation/activitylog.md | 7 +- docs/documentation/agent.md | 134 ++++----- docs/documentation/auditlog.md | 36 +-- .../comparison-informatica-cli.md | 46 +-- docs/documentation/config.md | 22 +- docs/documentation/connection.md | 10 +- docs/documentation/environment.md | 76 ++--- docs/documentation/export.md | 60 ++-- docs/documentation/folder.md | 28 +- docs/documentation/group.md | 62 ++-- docs/documentation/import.md | 50 +-- docs/documentation/interactive.md | 14 +- docs/documentation/login.md | 2 +- docs/documentation/lookup.md | 8 +- docs/documentation/menu.md | 38 +-- docs/documentation/output.md | 20 +- docs/documentation/package.md | 8 +- docs/documentation/permission.md | 28 +- docs/documentation/privilege.md | 10 +- docs/documentation/profile.md | 18 +- docs/documentation/publish.md | 20 +- docs/documentation/release.md | 60 ++-- docs/documentation/role.md | 28 +- docs/documentation/securitylog.md | 24 +- docs/documentation/sourcecontrol.md | 18 +- docs/documentation/state.md | 14 +- docs/documentation/user.md | 63 ++-- ...ixed-http-timeout-ignores-max-wait-time.md | 10 +- ...UG-0001-package-dependencies-sort-order.md | 8 +- ...BUG-0002-table-ansi-column-misalignment.md | 6 +- .../BUG-0003-keyring-integration-issues.md | 5 +- .../BUG-0004-user-get-by-id-http-405.md | 4 +- ...er-create-interactive-limit-exceeds-max.md | 2 +- ...7-location-prefix-sys-for-system-assets.md | 1 - ...ntime-create-missing-type-discriminator.md | 2 +- ...ug-caiUrl-not-derived-and-not-persisted.md | 6 +- ...port-object-status-table-missing-fields.md | 6 +- .../Bug-objects-list-query-filter.md | 10 +- .../Bug-publish-api-missing-accept-header.md | 4 +- 70 files changed, 850 insertions(+), 838 deletions(-) create mode 100644 .github/.markdownlint-cli2.yaml create mode 100644 .markdownlint-cli2.yaml diff --git a/.github/.markdownlint-cli2.yaml b/.github/.markdownlint-cli2.yaml new file mode 100644 index 0000000..137649e --- /dev/null +++ b/.github/.markdownlint-cli2.yaml @@ -0,0 +1,3 @@ +# Templates are fragments: GitHub supplies the title. +config: + MD041: false diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index af15546..ec20349 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -6,24 +6,30 @@ labels: bug assignees: '' --- -**Describe the bug** +## Describe the bug + A clear and concise description of what the bug is. -**Command run** -``` +## Command run + +```bash iics ``` -**Expected behavior** +## Expected behavior + What you expected to happen. -**Actual behavior** +## Actual behavior + What actually happened. Include error messages and output. -**Environment** +## Environment + - OS: [e.g., macOS 15, Ubuntu 24.04] - iics version: [output of `iics --version`] - IICS region: [e.g., US, EMEA] -**Additional context** +## Additional context + Any other relevant information (verbose output with `--verbose`, config details, etc.). diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 0097b38..34cde03 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -12,7 +12,7 @@ A clear description of the problem. E.g., "I'm always frustrated when..." **Describe the solution you'd like** What you want to happen. Include example CLI usage if applicable: -``` +```bash iics ``` diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index e58682c..ac998ed 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -100,7 +100,6 @@ f.Format(rows, []output.Column{ - Before every commit: `gofmt -s -w .`, `go vet ./...`, `golangci-lint run ./...`. - Do **not** include `Co-Authored-By: ...` trailers in commit messages. - ### Change Requests and Bug Fixes Change Requests live in `docs/ChangeRequests/` as markdown files: diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml new file mode 100644 index 0000000..1fc5f9d --- /dev/null +++ b/.markdownlint-cli2.yaml @@ -0,0 +1,11 @@ +# Project rules for markdownlint-cli2 (https://github.com/DavidAnson/markdownlint-cli2). +config: + # Prose is not hard-wrapped. + MD013: false + # Command pages repeat "Flags" and "Examples" under each subcommand, and the + # changelog repeats "Added" and "Fixed" under each release. + MD024: + siblings_only: true + # CR and bug files carry a "title" in their front matter and an H1 as well. + MD025: + front_matter_title: "" diff --git a/README.md b/README.md index e6fa587..06a8b49 100644 --- a/README.md +++ b/README.md @@ -115,18 +115,18 @@ iics logout ```yaml defaultProfile: dev -httpTimeout: 120 # seconds; per-HTTP-request timeout (default 120) -newUser: # global defaults for "user create --interactive" (optional; iics config edit) +httpTimeout: 120 # seconds; per-HTTP-request timeout (default 120) +newUser: # global defaults for "user create --interactive" (optional; iics config edit) domain: "company.com" userNamePattern: "{firstName}.{lastName}.{profileName}@{domain}" emailPattern: "{firstName}.{lastName}@{domain}" ui: - menu: true # false = bare "iics" prints help instead of opening the menu + menu: true # false = bare "iics" prints help instead of opening the menu style: - theme: default # default | minimal | compact | plain | markdown | gh - noColor: false # true = disable color permanently (same as --no-color) - headerColor: "" # lipgloss color: "6"=cyan, "244"=gray, "#FF0000"=hex (empty = theme default) - responsiveTables: true # false = never drop/truncate/wrap columns to fit (same as --wide) + theme: default # default | minimal | compact | plain | markdown | gh + noColor: false # true = disable color permanently (same as --no-color) + headerColor: "" # lipgloss color: "6"=cyan, "244"=gray, "#FF0000"=hex (empty = theme default) + responsiveTables: true # false = never drop/truncate/wrap columns to fit (same as --wide) profiles: dev: name: "Development Org" @@ -136,14 +136,14 @@ profiles: loginUrl: "https://use4.dm-us.informaticacloud.com/saas/public/core/v3/login" baseApiUrl: "https://use4.dm-us.informaticacloud.com/saas" caiUrl: "https://use4-cai.dm-us.informaticacloud.com" - newUser: # overrides the global newUser for this profile (optional) + newUser: # overrides the global newUser for this profile (optional) emailPattern: "{firstInitial}{lastName}@{domain}" prod: name: "Production Org" - production: true # menu marks it and asks before changes + production: true # menu marks it and asks before changes region: "EMEA" username: "admin@company.com" - password: "@keyring" # real password stored in OS keychain + password: "@keyring" # real password stored in OS keychain loginUrl: "https://dm-em.informaticacloud.com/saas/public/core/v3/login" baseApiUrl: "https://dm-em.informaticacloud.com/saas" caiUrl: "https://dm-em-cai.informaticacloud.com" @@ -157,7 +157,7 @@ The `loginUrl`, `baseApiUrl`, and `caiUrl` fields are populated automatically af The `style` section controls table output appearance. Available themes: | Theme | Description | -|------------|--------------------------------------------------------------------------------------------------------| +| ---------- | ------------------------------------------------------------------------------------------------------ | | `default` | Unicode rounded borders, cyan bold headers (TTY only) | | `minimal` | No borders, colored bold headers with unicode underline | | `compact` | No borders, gray bold headers, 1-space column gap (TTY only) | @@ -186,45 +186,45 @@ order - before the essential columns are ever touched. If a hidden or truncated help, a hint is printed to stderr naming what was hidden. This never applies to non-TTY output, `--output csv|json|yaml`, or the `markdown`/`gh` themes, which always render in full. -| Flag/setting | Description | -|--------------------------------|-----------------------------------------------------------------------------| -| `--width` | Terminal width to adapt to; 0 (default) auto-detects | -| `--wide` | Never drop, truncate, or wrap columns, regardless of terminal width | -| `style.responsiveTables: false` | Config equivalent of `--wide` | -| `IICS_WIDTH` | Override the auto-detected width (same precedence as `--width`) | +| Flag/setting | Description | +| ------------------------------- | ------------------------------------------------------------------- | +| `--width` | Terminal width to adapt to; 0 (default) auto-detects | +| `--wide` | Never drop, truncate, or wrap columns, regardless of terminal width | +| `style.responsiveTables: false` | Config equivalent of `--wide` | +| `IICS_WIDTH` | Override the auto-detected width (same precedence as `--width`) | Width resolution order: `--width` flag > `IICS_WIDTH` env > detected terminal size > `$COLUMNS` env > `80`. ### Environment variable overrides -| Variable | Description | -|-----------------------------|------------------------------------------------------------------------------------------| -| `IICS_PROFILE` | Override default profile | -| `IICS_USERNAME` | Override profile username | -| `IICS_PASSWORD` | Override profile password (takes precedence over keychain) | -| `IICS_REGION` | Override profile region | -| `IICS_LOGIN_URL` | Override computed login URL | -| `IICS_CAI_URL` | Override profile `caiUrl` | -| `IICS_OUTPUT` | Override default output format | -| `IICS_THEME` | Override table theme (same values as `--theme` flag) | -| `IICS_WIDTH` | Override auto-detected terminal width for responsive table output | +| Variable | Description | +| --------------------------- | --------------------------------------------------------------------------------------------------------------------- | +| `IICS_PROFILE` | Override default profile | +| `IICS_USERNAME` | Override profile username | +| `IICS_PASSWORD` | Override profile password (takes precedence over keychain) | +| `IICS_REGION` | Override profile region | +| `IICS_LOGIN_URL` | Override computed login URL | +| `IICS_CAI_URL` | Override profile `caiUrl` | +| `IICS_OUTPUT` | Override default output format | +| `IICS_THEME` | Override table theme (same values as `--theme` flag) | +| `IICS_WIDTH` | Override auto-detected terminal width for responsive table output | | `IICS_ACCESSIBLE` | Set to `1` for screen-reader friendly line-by-line prompts ([interactive prompts](docs/documentation/interactive.md)) | -| `IICS_NO_MENU` | Set to `1` so bare `iics` prints help instead of opening the [menu](docs/documentation/menu.md) | -| `IICS_HTTP_TIMEOUT` | Override per-HTTP-request timeout in seconds (default `120`; `--http-timeout` flag wins if set) | -| `IICS_VALID_DEPLOY_TARGETS` | Override valid target allowlist for `iics release` commands (comma-separated) | -| `IICS_TARGET_PROFILE_MAP` | Override target to profile mapping for `iics release plan` (format `TARGET=profile,...`) | -| `IICS_USER_` | CI target username fallback for `iics release plan` when profile is not configured | -| `IICS_PWD_` | CI target password fallback for `iics release plan` when profile is not configured | -| `IICS_REGION_` | Optional CI target region for `iics release plan` | -| `IICS_LOGIN_URL_` | Optional CI target login URL for `iics release plan` | +| `IICS_NO_MENU` | Set to `1` so bare `iics` prints help instead of opening the [menu](docs/documentation/menu.md) | +| `IICS_HTTP_TIMEOUT` | Override per-HTTP-request timeout in seconds (default `120`; `--http-timeout` flag wins if set) | +| `IICS_VALID_DEPLOY_TARGETS` | Override valid target allowlist for `iics release` commands (comma-separated) | +| `IICS_TARGET_PROFILE_MAP` | Override target to profile mapping for `iics release plan` (format `TARGET=profile,...`) | +| `IICS_USER_` | CI target username fallback for `iics release plan` when profile is not configured | +| `IICS_PWD_` | CI target password fallback for `iics release plan` when profile is not configured | +| `IICS_REGION_` | Optional CI target region for `iics release plan` | +| `IICS_LOGIN_URL_` | Optional CI target login URL for `iics release plan` | Environment variables take precedence over config file values. ### Supported regions | Region | ssLogin Host | -|----------------------------------------|-----------------------------| +| -------------------------------------- | --------------------------- | | US, USW1, USE2, USW3, USE4, USW5, USE6 | dm-us.informaticacloud.com | | USW1-1, USW3-1 | dm1-us.informaticacloud.com | | USW1-2 | dm2-us.informaticacloud.com | @@ -243,7 +243,7 @@ file instead of the real password. Supported backends: | Platform | Backend | -|----------|----------------------------------------------------| +| -------- | -------------------------------------------------- | | macOS | macOS Keychain (`security` framework) | | Linux | D-Bus Secret Service (e.g. GNOME Keyring, KWallet) | | Windows | Windows Credential Manager | @@ -265,40 +265,40 @@ it easy to override credentials in CI pipelines without touching the config file ## Commands -| Command | Alias | Subcommands | Description | -|------------------------------------------------------|--------|------------------------------------------------------------------------|--------------------------------------------------------------------| -| [activitylog](docs/documentation/activitylog.md) | | `list`, `get` | Query activity logs for completed jobs | -| [agent](docs/documentation/agent.md) | | `list`, `get`, `details`, `delete`, `start`, `stop`, `restart`, `installer-info`, `installer-download` | Manage Secure Agents | -| [auditlog](docs/documentation/auditlog.md) | | `list` | Query organization audit log (V2 API) | -| [completion](docs/documentation/completion.md) | | `bash`, `zsh`, `fish`, `powershell` | Generate shell completion scripts | -| [config](docs/documentation/config.md) | | `edit` | Edit global settings (new user defaults, theme, timeout, menu) | -| [connection](docs/documentation/connection.md) | `conn` | `list`, `get`, `create`, `update`, `delete` | Manage connections | -| [export](docs/documentation/export.md) | | `run`, `start`, `status`, `download`, `create` | Export asset packages | -| [folder](docs/documentation/folder.md) | | `create`, `update`, `delete` | Manage folders | -| [import](docs/documentation/import.md) | `imp` | `run`, `upload`, `start`, `status`, `download-log` | Import asset packages | -| [login](docs/documentation/login.md) | | | Authenticate and cache session | -| [logout](docs/documentation/logout.md) | | | Invalidate session | -| [lookup](docs/documentation/lookup.md) | | | Resolve object IDs, names, and paths | -| [metering](docs/documentation/metering.md) | | `get`, `download` | Query usage and metering data | -| [objects](docs/documentation/objects.md) | | `list`, `dependencies` | List/search assets, find dependencies | -| [package](docs/documentation/package.md) | | `expand`, `create`, `dependencies` | Extract, assemble, or inspect dependencies of IICS export packages | -| [menu](docs/documentation/menu.md) | `tui` | - | Interactive main menu (bare `iics` in a terminal) | -| [permission](docs/documentation/permission.md) | `perm` | `get`, `add`, `update`, `delete`, `set`, `check`, `edit` | Manage object-level permissions | -| [privilege](docs/documentation/privilege.md) | | `list` | List available privileges | -| [profile](docs/documentation/profile.md) | | `add`, `edit`, `list`, `delete`, `set-default`, `set-password`, `show` | Manage connection profiles | -| [project](docs/documentation/project.md) | | `create`, `update`, `delete` | Manage projects | -| [publish](docs/documentation/publish.md) | | `start`, `status`, `run` | Publish CAI assets to the runtime | -| [release](docs/documentation/release.md) | | `manifest`, `validate`, `plan` | Generate multi-format release manifests and per-environment plans | -| [role](docs/documentation/role.md) | | `list`, `get`, `create`, `edit`, `add-privileges`, `remove-privileges`, `delete` | Manage roles | -| [environment](docs/documentation/environment.md) | `runtime`, `rt`, `env` | `list`, `get`, `create`, `update`, `delete`, `configs` | Manage runtime environments (Secure Agent groups) | -| [schedule](docs/documentation/schedule.md) | | `list`, `get`, `create`, `update`, `delete` | Manage schedules | -| [securitylog](docs/documentation/securitylog.md) | | `list` | Query security audit log | -| [sourcecontrol](docs/documentation/sourcecontrol.md) | `sc` | `checkout`, `checkin`, `pull`, `commit` | Source control operations | -| [state](docs/documentation/state.md) | | `fetch`, `load` | Fetch/load object state snapshots | -| [tag](docs/documentation/tag.md) | | `assign`, `remove` | Assign/remove tags on objects | -| [unpublish](docs/documentation/unpublish.md) | | `start`, `status`, `run` | Unpublish CAI assets from the runtime | -| [user](docs/documentation/user.md) | | `list`, `get`, `create`, `edit`, `update-roles`, `update-groups`, `delete` | Manage users | -| [group](docs/documentation/group.md) | `usergroup`, `ug` | `list`, `get`, `create`, `update`, `delete` | Manage user groups | +| Command | Alias | Subcommands | Description | +| ---------------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | +| [activitylog](docs/documentation/activitylog.md) | | `list`, `get` | Query activity logs for completed jobs | +| [agent](docs/documentation/agent.md) | | `list`, `get`, `details`, `delete`, `start`, `stop`, `restart`, `installer-info`, `installer-download` | Manage Secure Agents | +| [auditlog](docs/documentation/auditlog.md) | | `list` | Query organization audit log (V2 API) | +| [completion](docs/documentation/completion.md) | | `bash`, `zsh`, `fish`, `powershell` | Generate shell completion scripts | +| [config](docs/documentation/config.md) | | `edit` | Edit global settings (new user defaults, theme, timeout, menu) | +| [connection](docs/documentation/connection.md) | `conn` | `list`, `get`, `create`, `update`, `delete` | Manage connections | +| [export](docs/documentation/export.md) | | `run`, `start`, `status`, `download`, `create` | Export asset packages | +| [folder](docs/documentation/folder.md) | | `create`, `update`, `delete` | Manage folders | +| [import](docs/documentation/import.md) | `imp` | `run`, `upload`, `start`, `status`, `download-log` | Import asset packages | +| [login](docs/documentation/login.md) | | | Authenticate and cache session | +| [logout](docs/documentation/logout.md) | | | Invalidate session | +| [lookup](docs/documentation/lookup.md) | | | Resolve object IDs, names, and paths | +| [metering](docs/documentation/metering.md) | | `get`, `download` | Query usage and metering data | +| [objects](docs/documentation/objects.md) | | `list`, `dependencies` | List/search assets, find dependencies | +| [package](docs/documentation/package.md) | | `expand`, `create`, `dependencies` | Extract, assemble, or inspect dependencies of IICS export packages | +| [menu](docs/documentation/menu.md) | `tui` | - | Interactive main menu (bare `iics` in a terminal) | +| [permission](docs/documentation/permission.md) | `perm` | `get`, `add`, `update`, `delete`, `set`, `check`, `edit` | Manage object-level permissions | +| [privilege](docs/documentation/privilege.md) | | `list` | List available privileges | +| [profile](docs/documentation/profile.md) | | `add`, `edit`, `list`, `delete`, `set-default`, `set-password`, `show` | Manage connection profiles | +| [project](docs/documentation/project.md) | | `create`, `update`, `delete` | Manage projects | +| [publish](docs/documentation/publish.md) | | `start`, `status`, `run` | Publish CAI assets to the runtime | +| [release](docs/documentation/release.md) | | `manifest`, `validate`, `plan` | Generate multi-format release manifests and per-environment plans | +| [role](docs/documentation/role.md) | | `list`, `get`, `create`, `edit`, `add-privileges`, `remove-privileges`, `delete` | Manage roles | +| [environment](docs/documentation/environment.md) | `runtime`, `rt`, `env` | `list`, `get`, `create`, `update`, `delete`, `configs` | Manage runtime environments (Secure Agent groups) | +| [schedule](docs/documentation/schedule.md) | | `list`, `get`, `create`, `update`, `delete` | Manage schedules | +| [securitylog](docs/documentation/securitylog.md) | | `list` | Query security audit log | +| [sourcecontrol](docs/documentation/sourcecontrol.md) | `sc` | `checkout`, `checkin`, `pull`, `commit` | Source control operations | +| [state](docs/documentation/state.md) | | `fetch`, `load` | Fetch/load object state snapshots | +| [tag](docs/documentation/tag.md) | | `assign`, `remove` | Assign/remove tags on objects | +| [unpublish](docs/documentation/unpublish.md) | | `start`, `status`, `run` | Unpublish CAI assets from the runtime | +| [user](docs/documentation/user.md) | | `list`, `get`, `create`, `edit`, `update-roles`, `update-groups`, `delete` | Manage users | +| [group](docs/documentation/group.md) | `usergroup`, `ug` | `list`, `get`, `create`, `update`, `delete` | Manage user groups | > **Keeping completions up to date:** After adding or changing any command or flag, regenerate > the shell completion scripts by running `make completions`. The pre-generated scripts live in @@ -306,18 +306,18 @@ it easy to override credentials in CI pipelines without touching the config file ### Global flags -| Flag | Short | Description | -|--------------|-------|--------------------------------------------------------------------------------------------| -| `--profile` | `-p` | Profile to use (overrides default) | -| `--output` | `-o` | Output format: `table` (default), `json`, `csv`, `yaml` | -| `--verbose` | `-v` | Enable verbose output | -| `--no-color` | | Disable colored output and force `plain` table theme | -| `--theme` | | Table theme: `default`, `minimal`, `compact`, `plain`, `markdown`, `gh` (overrides config) | -| `--width` | | Terminal width to adapt table output to; 0 (default) auto-detects | -| `--wide` | | Never drop, truncate, or wrap table columns to fit the terminal width | -| `--config` | | Config file path (default `~/.iics/config.yaml`) | -| `--debug` | | Print full HTTP request/response trace to stderr | -| `--http-timeout` | | Per-HTTP-request timeout in seconds (default `120`; overrides config `httpTimeout` / `IICS_HTTP_TIMEOUT` env var). Independent of `export`/`import` `--max-wait-time`, which only bounds job polling. | +| Flag | Short | Description | +| ---------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--profile` | `-p` | Profile to use (overrides default) | +| `--output` | `-o` | Output format: `table` (default), `json`, `csv`, `yaml` | +| `--verbose` | `-v` | Enable verbose output | +| `--no-color` | | Disable colored output and force `plain` table theme | +| `--theme` | | Table theme: `default`, `minimal`, `compact`, `plain`, `markdown`, `gh` (overrides config) | +| `--width` | | Terminal width to adapt table output to; 0 (default) auto-detects | +| `--wide` | | Never drop, truncate, or wrap table columns to fit the terminal width | +| `--config` | | Config file path (default `~/.iics/config.yaml`) | +| `--debug` | | Print full HTTP request/response trace to stderr | +| `--http-timeout` | | Per-HTTP-request timeout in seconds (default `120`; overrides config `httpTimeout` / `IICS_HTTP_TIMEOUT` env var). Independent of `export`/`import` `--max-wait-time`, which only bounds job polling. | ## Development diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md index 5242146..5e40105 100644 --- a/docs/CLAUDE.md +++ b/docs/CLAUDE.md @@ -99,16 +99,16 @@ iics_cli/ ## Key Dependencies -| Package | Version | Purpose | -| ----------------------------------- | ------- | -------------------------------- | -| `github.com/spf13/cobra` | v1.10.2 | CLI framework | -| `github.com/spf13/viper` | v1.21.0 | Config file + env var management | -| `github.com/charmbracelet/lipgloss` | v1.1.0 | Table output and terminal styles | -| `github.com/charmbracelet/huh` | v1.0.0 | Interactive forms and selects | -| `github.com/charmbracelet/bubbletea` | v1.3.10 | TUI models (`internal/tui/`) | -| `github.com/charmbracelet/bubbles` | v1.0.0 | TUI components (text input) | -| `github.com/mattn/go-isatty` | v0.0.20 | TTY detection for color fallback | -| `gopkg.in/yaml.v3` | v3.0.1 | Session cache serialization | +| Package | Version | Purpose | +| ------------------------------------ | ------- | -------------------------------- | +| `github.com/spf13/cobra` | v1.10.2 | CLI framework | +| `github.com/spf13/viper` | v1.21.0 | Config file + env var management | +| `github.com/charmbracelet/lipgloss` | v1.1.0 | Table output and terminal styles | +| `github.com/charmbracelet/huh` | v1.0.0 | Interactive forms and selects | +| `github.com/charmbracelet/bubbletea` | v1.3.10 | TUI models (`internal/tui/`) | +| `github.com/charmbracelet/bubbles` | v1.0.0 | TUI components (text input) | +| `github.com/mattn/go-isatty` | v0.0.20 | TTY detection for color fallback | +| `gopkg.in/yaml.v3` | v3.0.1 | Session cache serialization | No external HTTP library - uses standard `net/http` only. @@ -491,13 +491,13 @@ Supports dot notation for nested fields: ## Global Flags (from `cmd/root.go`) -| Flag | Variable | Default | Description | | | -| ------------------ | ----------- | --------------------- | ---------------------- | ----- | ---- | -| `--config` | `cfgFile` | `~/.iics/config.yaml` | Config file path | | | -| `--profile` / `-p` | `profile` | from config | Profile name | | | -| `--output` / `-o` | `outputFmt` | `"table"` | Output format: `table\ | json\ | csv` | -| `--verbose` / `-v` | `verbose` | `false` | Verbose output | | | -| `--no-color` | `noColor` | `false` | Disable color | | | +| Flag | Variable | Default | Description | +| ------------------ | ----------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| `--config` | `cfgFile` | `~/.iics/config.yaml` | Config file path | +| `--profile` / `-p` | `profile` | from config | Profile name | +| `--output` / `-o` | `outputFmt` | `"table"` | Output format: `table\|json\|csv` | +| `--verbose` / `-v` | `verbose` | `false` | Verbose output | +| `--no-color` | `noColor` | `false` | Disable color | | `--http-timeout` | `httpTimeoutFlag` | `0` (resolves to `120`) | Per-HTTP-request timeout in seconds; overrides config `httpTimeout` / `IICS_HTTP_TIMEOUT`. Independent of `--max-wait-time` (job polling only). | `verbose` is a package-level `bool` in the `cmd` package - access it directly in `RunE` closures. @@ -522,13 +522,13 @@ profiles: **Environment variable overrides (highest precedence):** -| Env var | Overrides | -| ---------------- | ---------------- | -| `IICS_PROFILE` | `--profile` flag | -| `IICS_USERNAME` | profile username | -| `IICS_PASSWORD` | profile password | -| `IICS_REGION` | profile region | -| `IICS_LOGIN_URL` | profile loginUrl | +| Env var | Overrides | +| ------------------- | --------------------------------------------------------------------------------------------------------- | +| `IICS_PROFILE` | `--profile` flag | +| `IICS_USERNAME` | profile username | +| `IICS_PASSWORD` | profile password | +| `IICS_REGION` | profile region | +| `IICS_LOGIN_URL` | profile loginUrl | | `IICS_HTTP_TIMEOUT` | `httpTimeout` config value (per-request timeout in seconds; `--http-timeout` flag wins if explicitly set) | **Session cache:** `~/.iics/sessions.yaml` diff --git a/docs/ChangeRequests/completed/2026-03-24-add-powershell-examples.md b/docs/ChangeRequests/completed/2026-03-24-add-powershell-examples.md index 47b050e..9cc4254 100644 --- a/docs/ChangeRequests/completed/2026-03-24-add-powershell-examples.md +++ b/docs/ChangeRequests/completed/2026-03-24-add-powershell-examples.md @@ -36,6 +36,7 @@ Currently, all shell examples in the documentation use only bash syntax. Windows Add PowerShell code samples as variants to all bash examples in the documentation files under `docs/documentation/*.md`. Each bash example should be followed by an equivalent PowerShell example shown in a separate code block with `powershell` language tag. Examples of PowerShell equivalents: + - `iics user list` → `iics user list` (same, no translation needed) - `iics user list --output json | jq '...'` → `iics user list --output json | ConvertFrom-Json | ` - Other shell-specific features should be translated to PowerShell equivalents @@ -102,6 +103,7 @@ Not applicable - documentation update only, no API calls involved. ## Examples ### Current Format (bash only) + ```bash iics user list @@ -112,6 +114,7 @@ iics user list --output json | jq '.[] | select(.userName | test("john"))' ``` ### Desired Format (bash + PowerShell) + ```bash iics user list diff --git a/docs/ChangeRequests/completed/CR-0001-import.md b/docs/ChangeRequests/completed/CR-0001-import.md index 275a062..424f3cf 100644 --- a/docs/ChangeRequests/completed/CR-0001-import.md +++ b/docs/ChangeRequests/completed/CR-0001-import.md @@ -26,7 +26,7 @@ - print polling results including the timestamp and job status - print total job duration - add --detailedPolling Print list of imported objects object details on each poll and overall progress of imported objects otherwise print just timestamp status and duration - - add --printImportLog which the import job via /public/core/v3/import//log log and print to console + - add --printImportLog which the import job via `/public/core/v3/import//log` log and print to console - for table print job summary table and table with objects - Always download and print importLog in case of import errors - Always print final status of the job formatted according to --output and --expand flags Print job summary and object list table diff --git a/docs/ChangeRequests/completed/CR-0004-package-command.md b/docs/ChangeRequests/completed/CR-0004-package-command.md index e785e90..dacdce1 100644 --- a/docs/ChangeRequests/completed/CR-0004-package-command.md +++ b/docs/ChangeRequests/completed/CR-0004-package-command.md @@ -63,7 +63,7 @@ testdata/imports/ZZ_TEST_CLI_Unpacked # root of the package └── Telematics.Connection.zip ``` -In order to be able to implement the expand and create sub-commands, we need to reverse-engineer and replicate replicate the functionality expand and package command of the IICS management Utility available on github https://github.com/InformaticaCloudApplicationIntegration/Tools/tree/master/IICS%20Asset%20Management%20CLI/v2.0.1 +In order to be able to implement the expand and create sub-commands, we need to reverse-engineer and replicate replicate the functionality expand and package command of the IICS management Utility available on github >Note: Informatic asset organization has only three layer @@ -214,7 +214,7 @@ Flags: only include source directory contents 4. Close the writer (flushes the ZIP central directory). 5. Print a summary line to stdout: `Created (N files)`. -6. In `--verbose` mode print contents of content report cvs file contained in the package +6. In `--verbose` mode print contents of content report cvs file contained in the package 7. Return any error encountered. ### Step 2 - Register (`cmd/root.go`) @@ -266,7 +266,7 @@ Contents of the generated `ContentsofExportPackage_ZZ_TEST_CLI.csv`as a table --- -## Example Original package Command Output +## Example Original package Command Output ```txt ~/bin/iics package -z ~/workspace/claude/iics-cli/testdata/imports/ZZ_TEST_CLI_packaged.zip -w ~/workspace/claude/iics-cli/testdata/imports/ZZ_TEST_CLI_Extracted -a 'Explore/ZZ_TEST_CLI.Project' @@ -300,7 +300,6 @@ INFO[0000] Generated checksum for artifact Artifact="{Folder Explo INFO[0000] Generated checksum for artifact Artifact="{Folder Explore/ZZ_TEST_CLI/Tasks.Folder.json Explore/ZZ_TEST_CLI/.Tasks.Folder.json Explore/ZZ_TEST_CLI/Tasks.Folder }" ``` - ## Implementation Notes ### Files created / modified diff --git a/docs/ChangeRequests/completed/CR-0005-strip-server-timestamp-on-expand.md b/docs/ChangeRequests/completed/CR-0005-strip-server-timestamp-on-expand.md index 4e70a7d..77875cc 100644 --- a/docs/ChangeRequests/completed/CR-0005-strip-server-timestamp-on-expand.md +++ b/docs/ChangeRequests/completed/CR-0005-strip-server-timestamp-on-expand.md @@ -167,7 +167,6 @@ Search terms: - `avrepository.xsd CurrentServerDateTime` - `getResponse CurrentServerDateTime import` - Document findings in the **Research Findings** section below before implementation begins. ### R2 - Round-trip import test diff --git a/docs/ChangeRequests/completed/CR-0009-lipgloss-table-styling.md b/docs/ChangeRequests/completed/CR-0009-lipgloss-table-styling.md index 3198b58..a76a287 100644 --- a/docs/ChangeRequests/completed/CR-0009-lipgloss-table-styling.md +++ b/docs/ChangeRequests/completed/CR-0009-lipgloss-table-styling.md @@ -17,7 +17,7 @@ disabled. 3. **`NO_COLOR` env var is not respected.** The POSIX `NO_COLOR` convention - (https://no-color.org) is not checked anywhere in the CLI. + () is not checked anywhere in the CLI. 4. **No user-configurable themes.** There is no way to choose between compact, minimal, or plain output styles without editing source code. @@ -137,13 +137,13 @@ func getFormatter() (output.Formatter, error) { Add direct dependency: -``` +```text github.com/charmbracelet/lipgloss v1.x ``` Remove direct dependency (no other usages remain): -``` +```text github.com/olekukonko/tablewriter ``` diff --git a/docs/ChangeRequests/completed/CR-0010-login-session-reliability.md b/docs/ChangeRequests/completed/CR-0010-login-session-reliability.md index 17e274c..72c1398 100644 --- a/docs/ChangeRequests/completed/CR-0010-login-session-reliability.md +++ b/docs/ChangeRequests/completed/CR-0010-login-session-reliability.md @@ -1,7 +1,7 @@ # CR-0010 - Login, Session, and Profile Reliability | Field | Value | -|-------|-------| +| ------- | ------- | | ID | CR-0010 | | Status | New | | Priority | High | @@ -26,7 +26,7 @@ Several related issues make the login/session lifecycle unreliable: ### Observed behaviour -``` +```text ./iics login --profile ttt Error: username not configured for profile "ttt" ``` @@ -41,13 +41,13 @@ The interactive profile-creation wizard in `getClient()` (`cmd/root.go`) activat In `cmd/login.go`, before calling `resolveProfile`, check whether the named profile key exists in the loaded config. If it does not exist and the command is running on a TTY, prompt the user: -``` +```text Profile "ttt" does not exist. Create it now? [Y/n]: ``` If confirmed, invoke `config.PromptProfile(nil, name)`, save the new profile to config, then continue with login. If declined or not a TTY, return a clear error: -``` +```text profile "ttt" not found; run 'iics profile add ttt' to create it ``` @@ -99,7 +99,7 @@ The error message does not tell the user what to do next and does not distinguis When the 401-renewal `Login()` call fails, return a wrapped error with a clear message: -``` +```text session renewal failed: ; run 'iics login --profile ' to re-authenticate ``` @@ -125,10 +125,10 @@ Note: full benefit of logout cleanup requires Issue 2 to be fixed so that sessio Add a dedicated `REGION` column to `profile list`: -| NAME | DEFAULT | REGION | ENDPOINT | USERNAME | -|------|---------|--------|----------|----------| -| prod | yes | us1 | https://dm-us.informaticacloud.com/saas/api | user@example.com | -| dev | | eu1 | https://dm-em.informaticacloud.com/saas/api | dev@example.com | +| NAME | DEFAULT | REGION | ENDPOINT | USERNAME | +| ---- | ------- | ------ | --------------------------------------------- | ------------------ | +| prod | yes | us1 | | | +| dev | | eu1 | | | Both fields are shown independently. ENDPOINT continues to prefer `LoginURL` over `Region` as a resolved value. @@ -145,18 +145,18 @@ Both fields are shown independently. ENDPOINT continues to prefer `LoginURL` ove `profile show` should additionally load the session cache and append session-derived rows: | FIELD | VALUE | -|-------|-------| +| ------- | ------- | | Name | prod | | Default | yes | | Region | us1 | | Login URL | https://... | | Base API URL | https://... | | CAI URL | https://... | -| Username | user@example.com | +| Username | | | Password | *** | | Org Name | My Org | | Org ID | abc123 | -| Session User | user@example.com | +| Session User | | | Last Login | 2026-03-23 14:05:00 UTC | | Session Expires | 2026-03-23 14:35:00 UTC | @@ -209,7 +209,7 @@ Set `LastLoginTime = time.Now()` in `saveSession()` whenever a new session is wr ## Files to Modify | File | Change | -|------|--------| +| ------ | -------- | | `internal/config/session.go` | Add `LastLoginTime time.Time` field to `SessionEntry` | | `internal/client/client.go` | Add `OnLoginSuccess func(*LoginResponse)` callback field; call it in `Login()` after successful auth | | `cmd/root.go` | Wire `OnLoginSuccess` to `saveSession()` in `getClient()` | diff --git a/docs/ChangeRequests/completed/CR-0012-package-dependencies-connection-lookup.md b/docs/ChangeRequests/completed/CR-0012-package-dependencies-connection-lookup.md index 1060f11..8631389 100644 --- a/docs/ChangeRequests/completed/CR-0012-package-dependencies-connection-lookup.md +++ b/docs/ChangeRequests/completed/CR-0012-package-dependencies-connection-lookup.md @@ -36,13 +36,13 @@ Three targeted improvements to `iics package dependencies --target-profile`: Running `iics package dependencies -f pkg.zip --target-profile prod` with a package that contains a `Connection` type dependency (e.g., `/SYS/Telematics`) produces: -``` +```text SYS/Telematics Connection package unknown lookup error: ... ``` or -``` +```text SYS/Telematics Connection package missing asset not found in target org ``` @@ -96,7 +96,7 @@ imported. No new imports required. The dependencies table always renders with very wide columns regardless of actual content: -``` +```text PATH TYPE SOURCE Explore/ZZ_TEST_CLI/Connections/TestServiceConn1 AI_CONNECTION package ``` @@ -147,7 +147,7 @@ if targetProfile != "" { ### Observed behaviour -``` +```text level=DEBUG msg="looking up dependency in target org" path=Connections/LexisNexis/GW-Update-Police-Order-Api-v1 type=AI_CONNECTION level=DEBUG msg="dependency found in target org" path=Connections/LexisNexis/GW-Update-Police-Order-Api-v1 type=AI_CONNECTION ``` @@ -185,7 +185,7 @@ in the loop body. For the very first "looking up" log line the org name will be Expected output after the fix: -``` +```text level=INFO msg="validating dependencies against target org" profile=prod count=12 level=DEBUG msg="looking up dependency in target org" profile=prod org="" path=Connections/LexisNexis/GW-Update... type=AI_CONNECTION level=DEBUG msg="dependency found in target org" profile=prod org="National Interstate - PROD" path=Connections/LexisNexis/GW-Update... type=AI_CONNECTION diff --git a/docs/ChangeRequests/completed/CR-0013-consistent-log-format.md b/docs/ChangeRequests/completed/CR-0013-consistent-log-format.md index 3272e9c..5af11c9 100644 --- a/docs/ChangeRequests/completed/CR-0013-consistent-log-format.md +++ b/docs/ChangeRequests/completed/CR-0013-consistent-log-format.md @@ -1,10 +1,10 @@ # CR-0013: Consistent Log Format Across All Commands -| Field | Value | -| --------- | ------------------------------------------------------------- | -| ID | CR-0013 | -| Status | New | -| Priority | Medium | +| Field | Value | +| --------- | ------------------------------------------------------------------------------------------------------------------- | +| ID | CR-0013 | +| Status | New | +| Priority | Medium | | Component | `cmd/root.go`, `internal/client/client.go`, `cmd/publish.go`, `cmd/unpublish.go`, `cmd/export.go`, `cmd/import_.go` | --- @@ -25,6 +25,7 @@ Three independent logging mechanisms produce inconsistent output format: Desired unified format: `[timestamp][LEVEL] msg key=value` Additionally: + - Publish verbose output is overly chatty (5+ separate lines per run) with no key=value structure - Long-running commands lack elapsed-time measurements in verbose mode - Verbose progress in publish/export/import writes to stdout; in a piped scenario @@ -36,7 +37,7 @@ Additionally: ### Observed behaviour -``` +```text time="[2026-04-01 23:02:03.971]" level=INFO msg="resolving dependencies: phase 1 - scanning package objects" total=25 publishMode=true time="[2026-04-01 23:02:03.971]" level=INFO msg="resolving dependencies: phase 1 complete" packageObjects=6 externalGUIDs=0 time="[2026-04-01 23:02:03.971]" level=INFO msg="validating dependencies against target org" profile=dev count=6 @@ -54,7 +55,7 @@ using `ReplaceAttr` alone. Replace `slog.NewTextHandler` with a minimal custom handler `cliHandler` in `cmd/root.go` that produces: -``` +```text [2026-04-01 23:02:03.971][INFO] resolving dependencies: phase 1 - scanning package objects total=25 publishMode=true [2026-04-01 23:02:03.971][INFO] resolving dependencies: phase 1 complete packageObjects=6 externalGUIDs=0 [2026-04-01 23:02:03.971][INFO] validating dependencies against target org profile=dev count=6 @@ -117,7 +118,7 @@ Remove the now-unused `ReplaceAttr` closure and `time.Now().Format` call from `i ### Observed behaviour -``` +```text DEBUG < 200 OK Response Headers: Content-Type: application/json;charset=UTF-8 @@ -165,7 +166,7 @@ lines belonging to the same request/response pair. Every section header ("Reques ### Observed behaviour -``` +```text [2026-04-01 23:02:04.979] Publishing 6 assets in 1 batch(es)... [2026-04-01 23:02:04.979] Submitting batch 1/1 (6 assets)... [2026-04-01 23:02:05.681] Batch 1/1 job ID: 1224192035368853504 @@ -184,7 +185,7 @@ calls. Slog writes to stderr and uses the new `[timestamp][LEVEL]` format automa Desired output after fix: -``` +```text [2026-04-01 23:02:04.979][INFO] publishing assets count=6 batches=1 [2026-04-01 23:02:05.681][INFO] batch submitted batch=1/1 job=1224192035368853504 assets=6 [2026-04-01 23:02:15.718][INFO] publish progress processed=4 total=6 state=IN_PROGRESS elapsed=11s @@ -192,6 +193,7 @@ Desired output after fix: ``` Key changes: + - Combine the "Submitting batch N/M (K assets)..." and "Batch N/M job ID: ..." lines into one `slog.Info("batch submitted", "batch", batchLabel, "job", jobID, "assets", len(batch))` call - Use key=value pairs instead of positional text @@ -247,7 +249,7 @@ slog.Info("dependencies validated", Expected package.go verbose output after fix: -``` +```text [2026-04-01 23:02:03.971][INFO] resolving dependencies phase=1 total=25 publishMode=true [2026-04-01 23:02:03.971][INFO] dependencies resolved packageObjects=6 externalGUIDs=0 elapsed=0ms [2026-04-01 23:02:03.971][INFO] validating dependencies against target org profile=dev count=6 diff --git a/docs/ChangeRequests/completed/CR-0014-package-dependencies-output.md b/docs/ChangeRequests/completed/CR-0014-package-dependencies-output.md index 1455096..d9504a7 100644 --- a/docs/ChangeRequests/completed/CR-0014-package-dependencies-output.md +++ b/docs/ChangeRequests/completed/CR-0014-package-dependencies-output.md @@ -212,7 +212,7 @@ Columns: `id`, `path`, `type`, then for each profile in order: `status_ Add a `slog.Debug` tick inside `validateTargetDependencies` every 50 items to show progress during long runs: -``` +```text [DEBUG] validating dependencies progress profile=qa processed=50 total=279 [DEBUG] validating dependencies progress profile=qa processed=100 total=279 ``` @@ -221,7 +221,7 @@ progress during long runs: Log goroutine lifecycle and per-profile summary: -``` +```text [INFO] report: starting validation profile=dev total=279 [INFO] report: starting validation profile=qa total=279 [INFO] report: profile complete profile=dev found=279 missing=0 elapsed=34.2s @@ -351,12 +351,14 @@ var reportProfiles []string ``` Register: + ```go cmd.Flags().StringSliceVar(&reportProfiles, "report", nil, "compare dependencies across one or more target profiles (mutually exclusive with --target-profile)") ``` Validation in `RunE`: + ```go if targetProfile != "" && len(reportProfiles) > 0 { return fmt.Errorf("--target-profile and --report are mutually exclusive") diff --git a/docs/ChangeRequests/completed/CR-0015-auditlog-command.md b/docs/ChangeRequests/completed/CR-0015-auditlog-command.md index 07b78cb..37a1fdb 100644 --- a/docs/ChangeRequests/completed/CR-0015-auditlog-command.md +++ b/docs/ChangeRequests/completed/CR-0015-auditlog-command.md @@ -108,9 +108,9 @@ internal/output/ # output layer is correct as-is ### Query parameters -| API parameter | CLI flag | Description | -| ------------- | --------- | -------------------------------------------------- | -| `batchSize` | `--limit` | Number of entries per batch | +| API parameter | CLI flag | Description | +| ------------- | --------- | --------------------------------------------------- | +| `batchSize` | `--limit` | Number of entries per batch | | `batchId` | `--skip` | Batch number; 0 = most recent, 1 = next older batch | When neither parameter is supplied, the API returns the most recent 200 entries. @@ -148,49 +148,49 @@ When neither parameter is supplied, the API returns the most recent 200 entries. package client import ( - "context" - "fmt" - "net/http" - "strconv" + "context" + "fmt" + "net/http" + "strconv" ) // AuditLog represents a single audit log entry returned by GET /api/v2/auditlog. type AuditLog struct { - ID string `json:"id,omitempty"` - Version int `json:"version,omitempty"` - OrgID string `json:"orgId,omitempty"` - Username string `json:"username,omitempty"` - EntryTime string `json:"entryTime,omitempty"` - EntryTimeUTC string `json:"entryTimeUTC,omitempty"` - ObjectID string `json:"objectId,omitempty"` - ObjectName string `json:"objectName,omitempty"` - Category string `json:"category,omitempty"` - Event string `json:"event,omitempty"` - EventParam string `json:"eventParam,omitempty"` - Message string `json:"message,omitempty"` + ID string `json:"id,omitempty"` + Version int `json:"version,omitempty"` + OrgID string `json:"orgId,omitempty"` + Username string `json:"username,omitempty"` + EntryTime string `json:"entryTime,omitempty"` + EntryTimeUTC string `json:"entryTimeUTC,omitempty"` + ObjectID string `json:"objectId,omitempty"` + ObjectName string `json:"objectName,omitempty"` + Category string `json:"category,omitempty"` + Event string `json:"event,omitempty"` + EventParam string `json:"eventParam,omitempty"` + Message string `json:"message,omitempty"` } // AuditLogListOptions holds optional query parameters for listing audit logs. // Limit maps to the API batchSize parameter; Skip maps to the API batchId parameter. type AuditLogListOptions struct { - Limit int // 0 = not set; API returns most recent 200 when omitted - Skip int // 0-based batch number; only sent when Limit > 0 + Limit int // 0 = not set; API returns most recent 200 when omitted + Skip int // 0-based batch number; only sent when Limit > 0 } // ListAuditLogs retrieves audit log entries. // When opts.Limit is 0, no pagination parameters are sent and the API returns // the most recent 200 entries. func (c *Client) ListAuditLogs(ctx context.Context, opts AuditLogListOptions) ([]AuditLog, error) { - query := make(map[string]string) - if opts.Limit > 0 { - query["batchSize"] = strconv.Itoa(opts.Limit) - query["batchId"] = strconv.Itoa(opts.Skip) - } - var resp []AuditLog - if err := c.doJSONWithQuery(ctx, http.MethodGet, fmt.Sprintf("%s/auditlog", BaseAPIPathV2), query, nil, &resp); err != nil { - return nil, err - } - return resp, nil + query := make(map[string]string) + if opts.Limit > 0 { + query["batchSize"] = strconv.Itoa(opts.Limit) + query["batchId"] = strconv.Itoa(opts.Skip) + } + var resp []AuditLog + if err := c.doJSONWithQuery(ctx, http.MethodGet, fmt.Sprintf("%s/auditlog", BaseAPIPathV2), query, nil, &resp); err != nil { + return nil, err + } + return resp, nil } ``` @@ -204,66 +204,66 @@ and update the column map in `cmd/auditlog.go` to match. package client import ( - "context" - "encoding/json" - "net/http" - "testing" + "context" + "encoding/json" + "net/http" + "testing" ) func TestListAuditLogs(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - t.Errorf("expected GET, got %s", r.Method) - } - if r.URL.Path != "/api/v2/auditlog" { - t.Errorf("unexpected path: %s", r.URL.Path) - } - entries := []AuditLog{ - { - ID: "al1", - Username: "admin@example.com", - Category: "AUTH", - Event: "LOGIN", - EntryTimeUTC: "2025-01-15T10:00:00.000Z", - }, - } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(entries) - }) - - c := newTestClient(handler) - logs, err := c.ListAuditLogs(context.Background(), AuditLogListOptions{}) - if err != nil { - t.Fatalf("ListAuditLogs() error: %v", err) - } - if len(logs) != 1 { - t.Fatalf("expected 1 entry, got %d", len(logs)) - } - if logs[0].ID != "al1" { - t.Errorf("expected ID al1, got %s", logs[0].ID) - } - if logs[0].Category != "AUTH" { - t.Errorf("expected category AUTH, got %s", logs[0].Category) - } + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/api/v2/auditlog" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + entries := []AuditLog{ + { + ID: "al1", + Username: "admin@example.com", + Category: "AUTH", + Event: "LOGIN", + EntryTimeUTC: "2025-01-15T10:00:00.000Z", + }, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(entries) + }) + + c := newTestClient(handler) + logs, err := c.ListAuditLogs(context.Background(), AuditLogListOptions{}) + if err != nil { + t.Fatalf("ListAuditLogs() error: %v", err) + } + if len(logs) != 1 { + t.Fatalf("expected 1 entry, got %d", len(logs)) + } + if logs[0].ID != "al1" { + t.Errorf("expected ID al1, got %s", logs[0].ID) + } + if logs[0].Category != "AUTH" { + t.Errorf("expected category AUTH, got %s", logs[0].Category) + } } func TestListAuditLogsPaginated(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Query().Get("batchSize") != "50" { - t.Errorf("expected batchSize=50, got %s", r.URL.Query().Get("batchSize")) - } - if r.URL.Query().Get("batchId") != "1" { - t.Errorf("expected batchId=1, got %s", r.URL.Query().Get("batchId")) - } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode([]AuditLog{}) - }) - - c := newTestClient(handler) - _, err := c.ListAuditLogs(context.Background(), AuditLogListOptions{Limit: 50, Skip: 1}) - if err != nil { - t.Fatalf("ListAuditLogs() error: %v", err) - } + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("batchSize") != "50" { + t.Errorf("expected batchSize=50, got %s", r.URL.Query().Get("batchSize")) + } + if r.URL.Query().Get("batchId") != "1" { + t.Errorf("expected batchId=1, got %s", r.URL.Query().Get("batchId")) + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode([]AuditLog{}) + }) + + c := newTestClient(handler) + _, err := c.ListAuditLogs(context.Background(), AuditLogListOptions{Limit: 50, Skip: 1}) + if err != nil { + t.Fatalf("ListAuditLogs() error: %v", err) + } } ``` @@ -273,78 +273,78 @@ func TestListAuditLogsPaginated(t *testing.T) { package cmd import ( - "context" - "strings" + "context" + "strings" - "github.com/jbrazda/iics-cli/internal/client" - "github.com/jbrazda/iics-cli/internal/output" - "github.com/spf13/cobra" + "github.com/jbrazda/iics-cli/internal/client" + "github.com/jbrazda/iics-cli/internal/output" + "github.com/spf13/cobra" ) var auditLogColumnMap = map[string]output.Column{ - "id": {Header: "ID", Field: "id", Width: 24}, - "username": {Header: "USERNAME", Field: "username", Width: 30}, - "category": {Header: "CATEGORY", Field: "category", Width: 16}, - "event": {Header: "EVENT", Field: "event", Width: 16}, - "entryTimeUTC": {Header: "TIME (UTC)", Field: "entryTimeUTC", Width: 24}, - "entryTime": {Header: "TIME (ET)", Field: "entryTime", Width: 24}, - "objectId": {Header: "OBJECT ID", Field: "objectId", Width: 24}, - "objectName": {Header: "OBJECT NAME", Field: "objectName", Width: 30}, - "eventParam": {Header: "EVENT PARAM", Field: "eventParam", Width: 40}, - "message": {Header: "MESSAGE", Field: "message", Width: 40}, - "orgId": {Header: "ORG ID", Field: "orgId", Width: 24}, - "version": {Header: "VERSION", Field: "version", Width: 8}, + "id": {Header: "ID", Field: "id", Width: 24}, + "username": {Header: "USERNAME", Field: "username", Width: 30}, + "category": {Header: "CATEGORY", Field: "category", Width: 16}, + "event": {Header: "EVENT", Field: "event", Width: 16}, + "entryTimeUTC": {Header: "TIME (UTC)", Field: "entryTimeUTC", Width: 24}, + "entryTime": {Header: "TIME (ET)", Field: "entryTime", Width: 24}, + "objectId": {Header: "OBJECT ID", Field: "objectId", Width: 24}, + "objectName": {Header: "OBJECT NAME", Field: "objectName", Width: 30}, + "eventParam": {Header: "EVENT PARAM", Field: "eventParam", Width: 40}, + "message": {Header: "MESSAGE", Field: "message", Width: 40}, + "orgId": {Header: "ORG ID", Field: "orgId", Width: 24}, + "version": {Header: "VERSION", Field: "version", Width: 8}, } const auditLogDefaultFields = "id,username,category,event,entryTimeUTC,objectName" func buildAuditLogColumns(fields string) []output.Column { - names := strings.Split(fields, ",") - cols := make([]output.Column, 0, len(names)) - for _, name := range names { - name = strings.TrimSpace(name) - if col, ok := auditLogColumnMap[name]; ok { - cols = append(cols, col) - } - } - return cols + names := strings.Split(fields, ",") + cols := make([]output.Column, 0, len(names)) + for _, name := range names { + name = strings.TrimSpace(name) + if col, ok := auditLogColumnMap[name]; ok { + cols = append(cols, col) + } + } + return cols } func newAuditlogCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "auditlog", - Short: "Retrieve audit log entries", - } - cmd.AddCommand(newAuditlogListCmd()) - return cmd + cmd := &cobra.Command{ + Use: "auditlog", + Short: "Retrieve audit log entries", + } + cmd.AddCommand(newAuditlogListCmd()) + return cmd } func newAuditlogListCmd() *cobra.Command { - var opts client.AuditLogListOptions - var fields string - cmd := &cobra.Command{ - Use: "list", - Short: "List audit log entries", - RunE: func(cmd *cobra.Command, args []string) error { - c, err := getClient(cmd) - if err != nil { - return err - } - logs, err := c.ListAuditLogs(context.Background(), opts) - if err != nil { - return err - } - f, err := getFormatter() - if err != nil { - return err - } - return f.Format(logs, buildAuditLogColumns(fields)) - }, - } - cmd.Flags().IntVar(&opts.Limit, "limit", 0, "number of entries per page (maps to API batchSize; 0 = return most recent 200)") - cmd.Flags().IntVar(&opts.Skip, "skip", 0, "page number to retrieve, 0-based (maps to API batchId; only used when --limit > 0)") - cmd.Flags().StringVar(&fields, "fields", auditLogDefaultFields, "comma-separated list of fields to display (table/csv only)") - return cmd + var opts client.AuditLogListOptions + var fields string + cmd := &cobra.Command{ + Use: "list", + Short: "List audit log entries", + RunE: func(cmd *cobra.Command, args []string) error { + c, err := getClient(cmd) + if err != nil { + return err + } + logs, err := c.ListAuditLogs(context.Background(), opts) + if err != nil { + return err + } + f, err := getFormatter() + if err != nil { + return err + } + return f.Format(logs, buildAuditLogColumns(fields)) + }, + } + cmd.Flags().IntVar(&opts.Limit, "limit", 0, "number of entries per page (maps to API batchSize; 0 = return most recent 200)") + cmd.Flags().IntVar(&opts.Skip, "skip", 0, "page number to retrieve, 0-based (maps to API batchId; only used when --limit > 0)") + cmd.Flags().StringVar(&fields, "fields", auditLogDefaultFields, "comma-separated list of fields to display (table/csv only)") + return cmd } ``` diff --git a/docs/ChangeRequests/completed/CR-0017-secure-credential-storage.md b/docs/ChangeRequests/completed/CR-0017-secure-credential-storage.md index 66ddd18..e6aaec5 100644 --- a/docs/ChangeRequests/completed/CR-0017-secure-credential-storage.md +++ b/docs/ChangeRequests/completed/CR-0017-secure-credential-storage.md @@ -313,7 +313,7 @@ if err := config.DeleteKeychainPassword(name); err != nil { Lets users migrate an existing plaintext password to the keychain, or update a stored keychain password, without re-running the full profile setup. -``` +```text iics profile set-password [name] ``` @@ -411,7 +411,7 @@ profiles: ## `go.mod` Change -``` +```text require ( ... github.com/zalando/go-keyring v0.2.x diff --git a/docs/ChangeRequests/completed/CR-0018-user-command-enhancements.md b/docs/ChangeRequests/completed/CR-0018-user-command-enhancements.md index aa435b8..cd78666 100644 --- a/docs/ChangeRequests/completed/CR-0018-user-command-enhancements.md +++ b/docs/ChangeRequests/completed/CR-0018-user-command-enhancements.md @@ -35,26 +35,26 @@ For `--output table` (default), render three sections instead of a single flat r **Section 1 - User Details** (property/value pairs): -| Property | Value | -|-----------------|------------------------------| -| id | 9IB4JZitmQDhj2cLTgWS64 | -| orgId | iGefnn2sAmxbnCx8WdS5mb | -| userName | jaroslav.brazda.dev@natl.com | -| firstName | Jaroslav | -| lastName | Brazda | -| email | jaroslav.brazda@natl.com | -| phone | 1234567890 | -| state | Enabled | -| timeZoneId | America/New_York | -| title | App Analyst Dev Consultant | -| authentication | Native | -| lastLoginMode | API | -| lastLoginTime | 2026-04-08T20:37:55Z | -| maxLoginAttempts| 10 | -| createTime | 2021-05-03T18:11:59.000Z | -| createdBy | paul.luc@natl.com.dev | -| updateTime | 2026-04-08T11:45:51.000Z | -| updatedBy | System built-in user | +| Property | Value | +| ---------------- | ------------------------------ | +| id | 9IB4JZitmQDhj2cLTgWS64 | +| orgId | iGefnn2sAmxbnCx8WdS5mb | +| userName | | +| firstName | Jaroslav | +| lastName | Brazda | +| email | | +| phone | 1234567890 | +| state | Enabled | +| timeZoneId | America/New_York | +| title | App Analyst Dev Consultant | +| authentication | Native | +| lastLoginMode | API | +| lastLoginTime | 2026-04-08T20:37:55Z | +| maxLoginAttempts | 10 | +| createTime | 2021-05-03T18:11:59.000Z | +| createdBy | | +| updateTime | 2026-04-08T11:45:51.000Z | +| updatedBy | System built-in user | **Section 2 - Groups:** @@ -186,6 +186,7 @@ Bulk errors are printed to stderr. Stdout receives only the summary table. ### 5 - `user update`: interactive role/group management API docs: + - Update user: - Update role assignments: @@ -193,6 +194,7 @@ Apply the same lookup pattern from reset-password (sections --id / --username / search) to identify the user to update. **`--interactive` flag:** + 1. Resolve the target user (via `--id`, `--username`, or interactive search). 2. Display current field values; prompt to update each field (press Enter to keep current). 3. Show current groups; prompt to add or remove groups (multi-select diff). @@ -215,7 +217,7 @@ to IDs. Create sample input files in `testdata/user/create/`: | File | Description | -|------|-------------| +| ------ | ------------- | | `single_native.json` | Single Native user, JSON format | | `single_native.yaml` | Single Native user, YAML format | | `bulk_native.json` | Array of 3 Native users, JSON | @@ -262,7 +264,7 @@ Docs: - [x] Connectors and Connections (CAI assets that require manual intervention after deployment) > **Note:** Connectors and connections are typically not included in the default package configurations and require manual intervention after deployment (e.g. setting up credentials or environment-specific parameters). This connector Package is only built and staged for deployment but must be deployed manually to allow for review and control over when and how connectors are deployed based on the target environment and deployment strategy. -``` + +```text diff --git a/docs/ChangeRequests/completed/CR-0022-relase-manifest-creds.md b/docs/ChangeRequests/completed/CR-0022-relase-manifest-creds.md index 4d0c2a4..5e3ba6a 100644 --- a/docs/ChangeRequests/completed/CR-0022-relase-manifest-creds.md +++ b/docs/ChangeRequests/completed/CR-0022-relase-manifest-creds.md @@ -114,4 +114,3 @@ If no new map flag/env is set, current target-name matching behavior remains. 4. `IICS_TARGET_PROFILE_MAP` works when flag is not set. 5. Explicitly mapped missing profile produces a clear error. 6. Existing workflows without new settings continue to work. - diff --git a/docs/ChangeRequests/completed/CR-0023-selective-packaging.md b/docs/ChangeRequests/completed/CR-0023-selective-packaging.md index 0a85b66..492f6d3 100644 --- a/docs/ChangeRequests/completed/CR-0023-selective-packaging.md +++ b/docs/ChangeRequests/completed/CR-0023-selective-packaging.md @@ -9,10 +9,10 @@ See the following files - docs/ChangeRequests/completed/CR-0022-relase-manifest-creds.md - we need to add support for build descriptor (csv,json,yaml,txt) as an input file to drive package selection - - should we add support to release plan command to also output yaml and json or just keep it csv? - - the package manifest can be passed in as stdin input or --manifest-file (short -f) parameter - - stdin to package create should auto-detect format and apply the file list selection accordingly - - the .txt file content should have backward compatible mode for txt files used by the official IIC CLI v2 + - should we add support to release plan command to also output yaml and json or just keep it csv? + - the package manifest can be passed in as stdin input or --manifest-file (short -f) parameter + - stdin to package create should auto-detect format and apply the file list selection accordingly + - the .txt file content should have backward compatible mode for txt files used by the official IIC CLI v2 - the package create command should include all assets listed in the package manifest if the package manifest contains path or location pointing only to ProjectName.Project or ProjectName/FolderName.Folder it must include all assets within the folder - Package Create Command must generate the `xexportPackage.chksum` checksum file calculated after content of the package is is determined, Only include checksums of the files actually included in the final package x - Package must contain the ContentsofExportPackage_[package_name].csv ( the package_name name should default to filename part of the --target parameter and be possible to override using to --name parameter). this file should contain a matching structure to export file format (objectPath,objectName,objectType,id), listing the actual file assets included in the package diff --git a/docs/ChangeRequests/completed/CR-0028-release-manifest-md-log.md b/docs/ChangeRequests/completed/CR-0028-release-manifest-md-log.md index a47b4b5..71fbe99 100644 --- a/docs/ChangeRequests/completed/CR-0028-release-manifest-md-log.md +++ b/docs/ChangeRequests/completed/CR-0028-release-manifest-md-log.md @@ -53,8 +53,8 @@ print a warning to stderr and keep the original command result. Add the following flag to each command in scope: -| Flag | Type | Default | Description | -|------|------|---------|-------------| +| Flag | Type | Default | Description | +| ------------ | --------------- | ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | | `--log-file` | optional string | disabled when omitted, `target/iics/import/logs/release_manifest.md` when present without value | Append a Markdown release report section to the given file | Recommended Cobra setup: @@ -83,7 +83,7 @@ the sample file in `testdata/release/release-manifest.md`. `iics release plan --log-file` initializes or appends the planning sections: -```markdown +````markdown # Release Manifest - Schema Version: `v1` @@ -129,7 +129,8 @@ target | TYPE | COUNT (PROD) | COUNT (QA) | COUNT (TST) | |------|-------------:|-----------:|------------:| | ... | ... | ... | ... | -``` + +```` ### Package create diff --git a/docs/ChangeRequests/completed/CR-0037-agent-details-services-table.md b/docs/ChangeRequests/completed/CR-0037-agent-details-services-table.md index 51b22b9..4a15515 100644 --- a/docs/ChangeRequests/completed/CR-0037-agent-details-services-table.md +++ b/docs/ChangeRequests/completed/CR-0037-agent-details-services-table.md @@ -25,5 +25,5 @@ document). No API change - the data is already in the details response. - `cmd/agent.go` - `agentServiceCols` var; `--services` flag on `newAgentDetailsCmd`; early return in the table branch that prints the summary - + the one services table; `MarkFlagsMutuallyExclusive("full", "services")`. + - the one services table; `MarkFlagsMutuallyExclusive("full", "services")`. - Docs: `docs/documentation/agent.md`. diff --git a/docs/ChangeRequests/completed/CR-profile-edit.md b/docs/ChangeRequests/completed/CR-profile-edit.md index b8063ad..04aa5d5 100644 --- a/docs/ChangeRequests/completed/CR-profile-edit.md +++ b/docs/ChangeRequests/completed/CR-profile-edit.md @@ -25,7 +25,7 @@ derive `baseApiUrl`/`caiUrl` from the login response - those URLs are only updat ## Desired Behaviour -``` +```text $ iics profile edit qa Setting up profile "qa" diff --git a/docs/ChangeRequests/new/CR-0025-package-exclusion-manifest.md b/docs/ChangeRequests/new/CR-0025-package-exclusion-manifest.md index 29bfba3..3524275 100644 --- a/docs/ChangeRequests/new/CR-0025-package-exclusion-manifest.md +++ b/docs/ChangeRequests/new/CR-0025-package-exclusion-manifest.md @@ -56,10 +56,10 @@ iics package create \ Flag details: -| Flag | Short | Type | Default | Description | -|-------------------------|-------|--------|---------|-------------| -| `--exclude-manifest-file` | - | string | `""` | Exclusion manifest path; same formats as `--manifest-file` | -| `--exclude` | `-e` | string | `""` | Go regex matched against `path/name.type`; matching assets are excluded | +| Flag | Short | Type | Default | Description | +| ------------------------- | ----- | ------ | ------- | ----------------------------------------------------------------------- | +| `--exclude-manifest-file` | - | string | `""` | Exclusion manifest path; same formats as `--manifest-file` | +| `--exclude` | `-e` | string | `""` | Go regex matched against `path/name.type`; matching assets are excluded | The short alias `-e` is not currently used by `package create`. It mirrors the same flag on `package dependencies --exclude / -e` for consistency. diff --git a/docs/ChangeRequests/new/CR-0026-build-manifest-report.md b/docs/ChangeRequests/new/CR-0026-build-manifest-report.md index 8182121..5f2fc9c 100644 --- a/docs/ChangeRequests/new/CR-0026-build-manifest-report.md +++ b/docs/ChangeRequests/new/CR-0026-build-manifest-report.md @@ -35,8 +35,8 @@ generation entirely, so callers that do not want a report do not have to opt out ### New flag on `iics package create` -| Flag | Type | Default | Description | -|------|------|---------|-------------| +| Flag | Type | Default | Description | +| ------------------------- | -------- | --------------------------------------------- | ----------------------------------------------------------------------------------- | | `--build-manifest-report` | `string` | `target/iics/import/logs/release-manifest.md` | Path to the build manifest report file; append if it exists; leave empty to disable | ### Behavior diff --git a/docs/ChangeRequests/new/CR-0027-package-manifest-diff-report.md b/docs/ChangeRequests/new/CR-0027-package-manifest-diff-report.md index 60bc10a..ce709e9 100644 --- a/docs/ChangeRequests/new/CR-0027-package-manifest-diff-report.md +++ b/docs/ChangeRequests/new/CR-0027-package-manifest-diff-report.md @@ -44,7 +44,7 @@ iics package create \ ``` | Flag | Short | Default | Description | -|---|---|---|---| +| --- | --- | --- | --- | | `--diff-report` | - | - | Path to write the diff report; if omitted, no report is generated | | `--diff-report-format` | - | `markdown` | Report format: `markdown`, `json`, `csv` | @@ -83,7 +83,7 @@ Objects in the final `selectedObjects` slice whose inclusion was triggered by lo the literal manifest rows: | Reason | Source | -|---|---| +| --- | --- | | `parent-container` | Added by `IncludeParentContainers` | | `transitive-closure` | Added by `IncludeReferencedClosure` | | `sys-implied` | SYS-rooted objects added when the manifest references only containers | @@ -94,7 +94,7 @@ Columns: `objectName`, `objectType`, `path`, `objectGuid`, `reason`. ### 4. Summary counters | Counter | Description | -|---|---| +| --- | --- | | `manifestEntries` | Number of rows in the input manifest after parsing | | `excludedTransitiveFound` | Rows dropped by `--exclude-found-transitive` (from `BuildManifestStats`) | | `directMatches` | Objects matched directly from manifest rows | diff --git a/docs/HOW-TO-Vibe.md b/docs/HOW-TO-Vibe.md index 7a77f4b..70e9212 100644 --- a/docs/HOW-TO-Vibe.md +++ b/docs/HOW-TO-Vibe.md @@ -10,7 +10,7 @@ One session per logical task works best - one bug fix, one feature, one refactor ## 3. Use subagents for research** -Delegate research with "use subagents to investigate X." They explore in a separate context, keeping your main conversation clean for implementation. When Claude researches a codebase it reads lots of files, all of which consume your context. Subagents run in separate context windows and report back summaries. +Delegate research with "use subagents to investigate X." They explore in a separate context, keeping your main conversation clean for implementation. When Claude researches a codebase it reads lots of files, all of which consume your context. Subagents run in separate context windows and report back summaries. ## 4. Use `/compact` and plan mode** diff --git a/docs/documentation/activitylog.md b/docs/documentation/activitylog.md index c8b3f93..171316f 100644 --- a/docs/documentation/activitylog.md +++ b/docs/documentation/activitylog.md @@ -180,6 +180,10 @@ iics activitylog get abc123 # Full JSON including all nested objects iics activitylog get abc123 --output json + +# Table with additional row count fields +iics activitylog get abc123 \ + --fields id,objectName,state,runId,successTargetRows,failedTargetRows,errorMsg ``` ```powershell @@ -188,10 +192,9 @@ iics activitylog get abc123 # Full JSON including all nested objects iics activitylog get abc123 --output json -``` # Table with additional row count fields -iics activitylog get abc123 \ +iics activitylog get abc123 ` --fields id,objectName,state,runId,successTargetRows,failedTargetRows,errorMsg ``` diff --git a/docs/documentation/agent.md b/docs/documentation/agent.md index fabf964..02d2899 100644 --- a/docs/documentation/agent.md +++ b/docs/documentation/agent.md @@ -10,15 +10,15 @@ iics agent [flags] ## Subcommands -| Subcommand | Description | -| ---------- | ------------------------------------ | -| `list` | List Secure Agents | -| `get` | Get a single agent | -| `details` | Get agent service engine details | -| `delete` | Delete a Secure Agent | -| `start` | Start an agent service | -| `stop` | Stop an agent service | -| `installer-info` | Get Secure Agent installer download information | +| Subcommand | Description | +| -------------------- | -------------------------------------------------------------- | +| `list` | List Secure Agents | +| `get` | Get a single agent | +| `details` | Get agent service engine details | +| `delete` | Delete a Secure Agent | +| `start` | Start an agent service | +| `stop` | Stop an agent service | +| `installer-info` | Get Secure Agent installer download information | | `installer-download` | Download the Secure Agent installer (optional checksum verify) | --- @@ -27,14 +27,14 @@ iics agent [flags] ### Flags -| Flag | Type | Default | Description | -| -------------- | ------------ | ------- | ---------------------------------------------------------------------- | -| `--unassigned` | bool | false | Include only agents not in a group | -| `--basic-info` | bool | false | Include package and configuration details in the response | -| `--fields` | string | | Comma-separated columns to display, by technical field name | +| Flag | Type | Default | Description | +| -------------- | ------------ | ------- | ------------------------------------------------------------------------------------------------- | +| `--unassigned` | bool | false | Include only agents not in a group | +| `--basic-info` | bool | false | Include package and configuration details in the response | +| `--fields` | string | | Comma-separated columns to display, by technical field name | | `--filter` | string array | | Client-side filter, e.g. `agentHost==host01` or `active!=true`. Repeatable; conditions are AND-ed | -| `--limit` | int | 200 | Accepted for compatibility; the API currently ignores it | -| `--skip` | int | 0 | Accepted for compatibility; the API currently ignores it | +| `--limit` | int | 200 | Accepted for compatibility; the API currently ignores it | +| `--skip` | int | 0 | Accepted for compatibility; the API currently ignores it | All [global flags](../../README.md#global-flags) apply. @@ -95,9 +95,9 @@ Get a single Secure Agent by ID or name. ### Flags -| Flag | Type | Description | -| -------- | ------ | ----------- | -| `--id` | string | Agent ID | +| Flag | Type | Description | +| -------- | ------ | ------------------------------------------------- | +| `--id` | string | Agent ID | | `--name` | string | Agent name (uses `GET /api/v2/agent/name/`) | Exactly one of `--id` / `--name` is required; they are mutually exclusive. @@ -136,13 +136,13 @@ service (engine) with its status and, optionally, its configuration properties. ### Flags -| Flag | Type | Description | -| ------------ | ------ | ----------- | -| `--id` | string | Agent ID | -| `--fid` | string | Agent federated ID | -| `--name` | string | Agent name | -| `--hostname` | string | Agent host name | -| `--full` | bool | Include agent-level and per-service configuration properties (adds `onlyStatus=false`) | +| Flag | Type | Description | +| ------------ | ------ | --------------------------------------------------------------------------------------------------- | +| `--id` | string | Agent ID | +| `--fid` | string | Agent federated ID | +| `--name` | string | Agent name | +| `--hostname` | string | Agent host name | +| `--full` | bool | Include agent-level and per-service configuration properties (adds `onlyStatus=false`) | | `--services` | bool | Show only the agent summary and one horizontal table of services (mutually exclusive with `--full`) | Exactly one of `--id` / `--fid` / `--name` / `--hostname` is required; they are @@ -199,12 +199,12 @@ Delete a Secure Agent. ### Flags -| Flag | Type | Description | -| ------------ | ------ | ----------- | -| `--id` | string | Agent ID | -| `--name` | string | Agent name | -| `--hostname` | string | Agent host name | -| `--yes` / `-y` | bool | Skip the confirmation prompt | +| Flag | Type | Description | +| -------------- | ------ | ---------------------------- | +| `--id` | string | Agent ID | +| `--name` | string | Agent name | +| `--hostname` | string | Agent host name | +| `--yes` / `-y` | bool | Skip the confirmation prompt | Exactly one of `--id` / `--name` / `--hostname` is required; they are mutually exclusive. @@ -234,16 +234,16 @@ Start or stop a service on a Secure Agent. Uses ### Flags -| Flag | Type | Default | Description | -| --------------------- | ------ | ------- | ----------- | -| `--id` | string | | Agent ID | -| `--name` | string | | Agent name | -| `--hostname` | string | | Agent host name | +| Flag | Type | Default | Description | +| --------------------- | ------ | ------- | ---------------------------------------------------------------------------------------- | +| `--id` | string | | Agent ID | +| `--name` | string | | Agent name | +| `--hostname` | string | | Agent host name | | `--service` | string | | Service display name, e.g. `"Data Integration Server"` (required unless `--interactive`) | -| `--interactive`, `-i` | bool | false | Select the agent and service interactively | -| `--blocking` | bool | false | Poll the service status until it has started/stopped, printing each poll | -| `--poll-interval` | int | 10 | Seconds between status polls (with `--blocking`) | -| `--max-wait-time` | int | 300 | Maximum seconds to wait (with `--blocking`) | +| `--interactive`, `-i` | bool | false | Select the agent and service interactively | +| `--blocking` | bool | false | Poll the service status until it has started/stopped, printing each poll | +| `--poll-interval` | int | 10 | Seconds between status polls (with `--blocking`) | +| `--max-wait-time` | int | 300 | Maximum seconds to wait (with `--blocking`) | Exactly one of `--id` / `--name` / `--hostname` is required, unless `--interactive` is used. `--interactive` lists the Secure Agents to choose from, @@ -318,8 +318,8 @@ for a platform. ### Flags -| Flag | Type | Required | Description | -| ------ | ------ | -------- | ------------------------------------ | +| Flag | Type | Required | Description | +| ------ | ------ | -------- | -------------------------------------- | | `--os` | string | yes | Operating system: `win64` or `linux64` | When `--os` is omitted and the session is interactive, the command shows a @@ -371,15 +371,15 @@ Installer info can be supplied three ways: ### Flags -| Flag | Type | Default | Description | -| ------------------ | ------ | -------------------- | ------------------------------------------------------------------ | -| `--os` | string | | Operating system: `win64` or `linux64` (used when no info supplied) | -| `--installer-info` | string | | Path to installer info JSON (omit to read piped stdin) | -| `--target` | string | system temp directory | Destination file or directory | -| `--verify` | bool | false | Download the checksum file and verify the installer | -| `--progress` | bool | false | Print live download progress to stderr | -| `--interactive`, `-i` | bool | false | Prompt for the OS, the destination and whether to replace an existing file (terminal only) | -| `--force` | bool | false | Download even when the destination file already matches | +| Flag | Type | Default | Description | +| --------------------- | ------ | --------------------- | ------------------------------------------------------------------------------------------ | +| `--os` | string | | Operating system: `win64` or `linux64` (used when no info supplied) | +| `--installer-info` | string | | Path to installer info JSON (omit to read piped stdin) | +| `--target` | string | system temp directory | Destination file or directory | +| `--verify` | bool | false | Download the checksum file and verify the installer | +| `--progress` | bool | false | Print live download progress to stderr | +| `--interactive`, `-i` | bool | false | Prompt for the OS, the destination and whether to replace an existing file (terminal only) | +| `--force` | bool | false | Download even when the destination file already matches | `--target` behavior: @@ -399,11 +399,11 @@ File already exists: /tmp/agent64_install_ng_ext.7617.bin (364.6MB, modified 202 It matches the checksum of the current installer. ``` -| Mode | File matches | File differs or cannot be checked | -| ----------------------- | ----------------------------- | --------------------------------- | -| Default (scripts) | Kept, download skipped | Downloaded again (replaced) | -| `--interactive` | Asks "Download again and overwrite it?" (default **No**) | Asks (default **Yes**) | -| `--force` | Downloaded again | Downloaded again | +| Mode | File matches | File differs or cannot be checked | +| ----------------- | -------------------------------------------------------- | --------------------------------- | +| Default (scripts) | Kept, download skipped | Downloaded again (replaced) | +| `--interactive` | Asks "Download again and overwrite it?" (default **No**) | Asks (default **Yes**) | +| `--force` | Downloaded again | Downloaded again | When the download is skipped, the result describes the existing file (including `verified` when the checksum could be checked). @@ -427,17 +427,17 @@ for a live, updating transfer line (bytes transferred, percentage when the serve reports a content length, and transfer rate) written to stderr. `--output json`, `yaml`, and `csv` render the structured result. -| Field | Description | -| --------------------- | ------------------------------------------------- | -| `file` | Full path of the downloaded file | -| `fileName` | Installer file name | -| `size` | Downloaded size in bytes | -| `downloadUrl` | Source URL | -| `checksumDownloadUrl` | Checksum file URL (when known) | +| Field | Description | +| --------------------- | ----------------------------------------------------------- | +| `file` | Full path of the downloaded file | +| `fileName` | Installer file name | +| `size` | Downloaded size in bytes | +| `downloadUrl` | Source URL | +| `checksumDownloadUrl` | Checksum file URL (when known) | | `installToken` | Token used to register the agent after install (when known) | -| `expectedChecksum` | SHA-256 digest from the checksum file (`--verify`) | -| `actualChecksum` | SHA-256 digest of the downloaded file (`--verify`) | -| `verified` | Whether the checksums matched (`--verify`) | +| `expectedChecksum` | SHA-256 digest from the checksum file (`--verify`) | +| `actualChecksum` | SHA-256 digest of the downloaded file (`--verify`) | +| `verified` | Whether the checksums matched (`--verify`) | If the checksums do not match, the partial download is removed and the command exits with an error. diff --git a/docs/documentation/auditlog.md b/docs/documentation/auditlog.md index 7bdb265..8411f05 100644 --- a/docs/documentation/auditlog.md +++ b/docs/documentation/auditlog.md @@ -25,11 +25,11 @@ iics auditlog [flags] ### Flags -| Flag | Type | Default | Description | -| ---------- | ------ | ------------------------------------------------ | ----------- | -| `--limit` | int | 0 | Entries per page (maps to API `batchSize`). When 0, the API returns the most recent 200 entries. | -| `--skip` | int | 0 | Page number to retrieve, 0-based (maps to API `batchId`). Only used when `--limit > 0`. | -| `--fields` | string | `id,username,category,event,entryTimeUTC,objectName` | Comma-separated list of fields to display (table and CSV output only). | +| Flag | Type | Default | Description | +| ---------- | ------ | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------ | +| `--limit` | int | 0 | Entries per page (maps to API `batchSize`). When 0, the API returns the most recent 200 entries. | +| `--skip` | int | 0 | Page number to retrieve, 0-based (maps to API `batchId`). Only used when `--limit > 0`. | +| `--fields` | string | `id,username,category,event,entryTimeUTC,objectName` | Comma-separated list of fields to display (table and CSV output only). | All [global flags](../../README.md#global-flags) apply. @@ -37,20 +37,20 @@ All [global flags](../../README.md#global-flags) apply. All available fields (selectable via `--fields`): -| Field (JSON tag) | Header | Description | -| ---------------- | ----------- | --------------------------------------------------- | -| `id` | ID | Audit log entry identifier (default) | -| `username` | USERNAME | User who performed the action (default) | -| `category` | CATEGORY | Audit category: AGENT, AUTH, CONNECTION, USER, etc. (default) | +| Field (JSON tag) | Header | Description | +| ---------------- | ----------- | ----------------------------------------------------------------- | +| `id` | ID | Audit log entry identifier (default) | +| `username` | USERNAME | User who performed the action (default) | +| `category` | CATEGORY | Audit category: AGENT, AUTH, CONNECTION, USER, etc. (default) | | `event` | EVENT | Action type: CREATE, DELETE, UPDATE, RUN, PUBLISH, etc. (default) | -| `entryTimeUTC` | TIME (UTC) | Timestamp of the action in UTC (default) | -| `objectName` | OBJECT NAME | Name of the affected object (default) | -| `entryTime` | TIME (ET) | Timestamp in Eastern Time (optional) | -| `objectId` | OBJECT ID | Identifier of the affected object (optional) | -| `eventParam` | EVENT PARAM | Related objects, max 1024 characters (optional) | -| `message` | MESSAGE | Additional context information (optional) | -| `orgId` | ORG ID | Organization identifier (optional) | -| `version` | VERSION | Entry version number (optional) | +| `entryTimeUTC` | TIME (UTC) | Timestamp of the action in UTC (default) | +| `objectName` | OBJECT NAME | Name of the affected object (default) | +| `entryTime` | TIME (ET) | Timestamp in Eastern Time (optional) | +| `objectId` | OBJECT ID | Identifier of the affected object (optional) | +| `eventParam` | EVENT PARAM | Related objects, max 1024 characters (optional) | +| `message` | MESSAGE | Additional context information (optional) | +| `orgId` | ORG ID | Organization identifier (optional) | +| `version` | VERSION | Entry version number (optional) | ### Pagination diff --git a/docs/documentation/comparison-informatica-cli.md b/docs/documentation/comparison-informatica-cli.md index ee9382c..c8447d4 100644 --- a/docs/documentation/comparison-informatica-cli.md +++ b/docs/documentation/comparison-informatica-cli.md @@ -169,28 +169,28 @@ It cannot manage users, connections, schedules, roles, or any other administrati `iics-cli` covers the full IICS REST API v3 surface: -| Resource | Operations | -| --------------- | --------------------------------- | -| `objects` | list, dependencies | -| `lookup` | resolve IDs, names, paths | -| `connection` | list, get, create, update, delete | -| `export` | create, status, download | -| `import` | upload, start, status, log | -| `schedule` | list, get, create, update, delete | -| `project` | create, update, delete | -| `folder` | create, update, delete | -| `user` | list, get, create, update, delete | -| `usergroup` | list, get, create, update, delete | +| Resource | Operations | +| --------------- | ------------------------------------------------------------------ | +| `objects` | list, dependencies | +| `lookup` | resolve IDs, names, paths | +| `connection` | list, get, create, update, delete | +| `export` | create, status, download | +| `import` | upload, start, status, log | +| `schedule` | list, get, create, update, delete | +| `project` | create, update, delete | +| `folder` | create, update, delete | +| `user` | list, get, create, update, delete | +| `usergroup` | list, get, create, update, delete | | `role` | list, get, create, edit, add-privileges, remove-privileges, delete | -| `privilege` | list | -| `runtime` | list, get, create, update | -| `agent` | list, start, stop | -| `tag` | assign, remove | -| `permission` | get, add, update, delete, set, check, edit | -| `securitylog` | list | -| `metering` | get, download | -| `sourcecontrol` | checkout, checkin, pull, commit | -| `state` | fetch, load | +| `privilege` | list | +| `runtime` | list, get, create, update | +| `agent` | list, start, stop | +| `tag` | assign, remove | +| `permission` | get, add, update, delete, set, check, edit | +| `securitylog` | list | +| `metering` | get, download | +| `sourcecontrol` | checkout, checkin, pull, commit | +| `state` | fetch, load | --- @@ -213,13 +213,13 @@ a generic message with little context. `iics-cli` reports structured API errors to `stderr`, including the HTTP status code, the Informatica error code, and the full response body when `--debug` is set: -``` +```text Error: API error 400: invalid artifact type 'PROCESS_OBJECT' ``` With `--debug`: -``` +```text Error: API error 400: invalid artifact type 'PROCESS_OBJECT' HTTP 400 Bad Request { diff --git a/docs/documentation/config.md b/docs/documentation/config.md index 3c578dc..152d13f 100644 --- a/docs/documentation/config.md +++ b/docs/documentation/config.md @@ -22,17 +22,17 @@ changes (**Save**, **Back to editing** or **Cancel**). Requires a terminal; in scripts, edit the config file directly. In accessible mode (`IICS_ACCESSIBLE=1`) the form runs as numbered line prompts. -| Page | Setting | Config key | Empty value | -| ---------- | ----------------------- | ------------------------ | -------------------------------------------- | -| New user | Domain | `newUser.domain` | Domain of each profile's user name | -| New user | User name pattern | `newUser.userNamePattern` | `{firstName}.{lastName}.{profileName}@{domain}` | -| New user | Email pattern | `newUser.emailPattern` | `{firstName}.{lastName}@{domain}` | -| Appearance | Table theme | `style.theme` | `default` on a terminal, `markdown` when piped | -| Appearance | Header color | `style.headerColor` | Theme default | -| Appearance | Disable colors | `style.noColor` | Colors on | -| Appearance | Responsive tables | `style.responsiveTables` | On | -| Other | HTTP timeout (seconds) | `httpTimeout` | 120 | -| Other | Main menu for bare `iics` | `ui.menu` | On | +| Page | Setting | Config key | Empty value | +| ---------- | ------------------------- | ------------------------- | ----------------------------------------------- | +| New user | Domain | `newUser.domain` | Domain of each profile's user name | +| New user | User name pattern | `newUser.userNamePattern` | `{firstName}.{lastName}.{profileName}@{domain}` | +| New user | Email pattern | `newUser.emailPattern` | `{firstName}.{lastName}@{domain}` | +| Appearance | Table theme | `style.theme` | `default` on a terminal, `markdown` when piped | +| Appearance | Header color | `style.headerColor` | Theme default | +| Appearance | Disable colors | `style.noColor` | Colors on | +| Appearance | Responsive tables | `style.responsiveTables` | On | +| Other | HTTP timeout (seconds) | `httpTimeout` | 120 | +| Other | Main menu for bare `iics` | `ui.menu` | On | The table theme list shows a sample table in the highlighted theme. Pattern fields reject unknown placeholders; see diff --git a/docs/documentation/connection.md b/docs/documentation/connection.md index 7b962a1..df45a70 100644 --- a/docs/documentation/connection.md +++ b/docs/documentation/connection.md @@ -119,8 +119,8 @@ Create a new connection from a JSON definition file. ### Flags -| Flag | Type | Required | Description | -| ------------- | ------ | -------- | ------------------------------------ | +| Flag | Type | Required | Description | +| ------------- | ------ | -------- | -------------------------------------------------- | | `--from-file` | string | yes | Path to a JSON file with the connection definition | All [global flags](../../README.md#global-flags) apply. @@ -161,9 +161,9 @@ Update an existing connection from a JSON file. Only fields present in the file ### Flags -| Flag | Type | Required | Description | -| ------------- | ------ | -------- | ------------------------------------ | -| `--id` | string | yes | Connection ID to update | +| Flag | Type | Required | Description | +| ------------- | ------ | -------- | ------------------------------------------- | +| `--id` | string | yes | Connection ID to update | | `--from-file` | string | yes | Path to a JSON file with the updated fields | All [global flags](../../README.md#global-flags) apply. diff --git a/docs/documentation/environment.md b/docs/documentation/environment.md index ca35aa8..2379526 100644 --- a/docs/documentation/environment.md +++ b/docs/documentation/environment.md @@ -15,13 +15,13 @@ iics rt [flags] ## Subcommands -| Subcommand | Description | -| ---------- | -------------------------------- | -| `list` | List runtime environments | -| `get` | Get a single runtime environment | -| `create` | Create a runtime environment | -| `update` | Update a runtime environment | -| `delete` | Delete a runtime environment | +| Subcommand | Description | +| ---------- | -------------------------------------------- | +| `list` | List runtime environments | +| `get` | Get a single runtime environment | +| `create` | Create a runtime environment | +| `update` | Update a runtime environment | +| `delete` | Delete a runtime environment | | `configs` | Manage Secure Agent group service properties | --- @@ -30,10 +30,10 @@ iics rt [flags] ### Flags -| Flag | Type | Default | Description | -| ---------- | ------------ | ------- | ------------------------------------------------------- | -| `--limit` | int | 200 | Max results | -| `--skip` | int | 0 | Results to skip | +| Flag | Type | Default | Description | +| ---------- | ------------ | ------- | ------------------------------------------------------------- | +| `--limit` | int | 200 | Max results | +| `--skip` | int | 0 | Results to skip | | `--filter` | string array | | Client-side filter, e.g. `isShared==true`. Repeatable; AND-ed | `--filter` supports `==` and `!=` on the raw API field names. Note that boolean @@ -45,13 +45,13 @@ All [global flags](../../README.md#global-flags) apply. ### Output columns | Column | Description | -| ------------- | ----------------------------------- | -| `id` | Runtime environment ID | -| `name` | Runtime environment name | -| `federatedId` | Federated ID | -| `isShared` | Whether the environment is shared | +| ------------- | ------------------------------------ | +| `id` | Runtime environment ID | +| `name` | Runtime environment name | +| `federatedId` | Federated ID | +| `isShared` | Whether the environment is shared | | `agents` | Number of Secure Agents in the group | -| `updateTime` | Last modification timestamp | +| `updateTime` | Last modification timestamp | ### Examples @@ -77,9 +77,9 @@ iics environment list --filter isShared==true ### Flags -| Flag | Type | Description | -| -------- | ------ | ----------------------- | -| `--id` | string | Runtime environment ID | +| Flag | Type | Description | +| -------- | ------ | ------------------------ | +| `--id` | string | Runtime environment ID | | `--name` | string | Runtime environment name | Exactly one of `--id` / `--name` is required; they are mutually exclusive. @@ -131,11 +131,11 @@ element) are added automatically. An explicit `@type` in the file is honored. ### Flags -| Flag | Type | Description | -| --------------------- | ------ | ----------------------------------------------------- | -| `--from-file` | string | JSON file with the runtime environment definition | -| `--description` | string | Environment description (overrides the file value) | -| `--interactive`, `-i` | bool | Prompt for the fields interactively | +| Flag | Type | Description | +| --------------------- | ------ | -------------------------------------------------- | +| `--from-file` | string | JSON file with the runtime environment definition | +| `--description` | string | Environment description (overrides the file value) | +| `--interactive`, `-i` | bool | Prompt for the fields interactively | The interactive wizard runs when `-i` is given, or when `--from-file` is omitted and stdin is a terminal. With both `--from-file` and `-i`, the file pre-fills the @@ -196,10 +196,10 @@ Delete a runtime environment (Secure Agent group). Uses ### Flags -| Flag | Type | Description | -| -------------- | ------ | ----------- | -| `--id` | string | Runtime environment ID | -| `--name` | string | Runtime environment name | +| Flag | Type | Description | +| -------------- | ------ | ---------------------------- | +| `--id` | string | Runtime environment ID | +| `--name` | string | Runtime environment name | | `--yes` / `-y` | bool | Skip the confirmation prompt | Exactly one of `--id` / `--name` is required; they are mutually exclusive. @@ -230,11 +230,11 @@ is the runtime environment ID. Show the service property overrides for a group. -| Flag | Type | Description | -| ----------- | ------ | ----------- | -| `--id` | string | Secure Agent group (runtime environment) ID | +| Flag | Type | Description | +| ----------- | ------ | --------------------------------------------- | +| `--id` | string | Secure Agent group (runtime environment) ID | | `--name` | string | Secure Agent group (runtime environment) name | -| `--service` | string | Show only this service's properties | +| `--service` | string | Show only this service's properties | Exactly one of `--id` / `--name` is required. @@ -252,12 +252,12 @@ iics environment configs get --name "My Group" --service Data_Integration_Server Replace the service property overrides for a group from a JSON file shaped as `{"":[{ ...settings... }]}`. -| Flag | Type | Description | -| -------------- | ------ | ----------- | -| `--id` | string | Secure Agent group (runtime environment) ID | +| Flag | Type | Description | +| -------------- | ------ | --------------------------------------------- | +| `--id` | string | Secure Agent group (runtime environment) ID | | `--name` | string | Secure Agent group (runtime environment) name | -| `--from-file` | string | JSON file with the overrides (required) | -| `--yes` / `-y` | bool | Skip the confirmation prompt | +| `--from-file` | string | JSON file with the overrides (required) | +| `--yes` / `-y` | bool | Skip the confirmation prompt | Exactly one of `--id` / `--name` is required. diff --git a/docs/documentation/export.md b/docs/documentation/export.md index 631eb59..7d26f71 100644 --- a/docs/documentation/export.md +++ b/docs/documentation/export.md @@ -10,13 +10,13 @@ iics export [flags] ## Subcommands -| Subcommand | Description | -| ----------- | -------------------------------------------------------- | -| `run` | Full pipeline: resolve IDs, start job, poll, download | -| `start` | Resolve IDs and start job, return job ID only | -| `status` | Check export job status | -| `download` | Download completed export ZIP | -| `create` | Start export with explicit object IDs | +| Subcommand | Description | +| ---------- | ----------------------------------------------------- | +| `run` | Full pipeline: resolve IDs, start job, poll, download | +| `start` | Resolve IDs and start job, return job ID only | +| `status` | Check export job status | +| `download` | Download completed export ZIP | +| `create` | Start export with explicit object IDs | --- @@ -26,19 +26,19 @@ The recommended command for most use cases. Reads an artifacts list, resolves pa ### Flags -| Flag | Short | Type | Required | Default | Description | -| ----------------------- | ----- | ------ | -------- | ---------- | ------------------------------------------------------------------ | -| `--artifacts-file` | | string | | stdin | File listing objects to export (txt/json/yaml/csv); omit for stdin | -| `--export-file-path` | | string | yes | | Output ZIP file path | -| `-n, --name` | `-n` | string | | timestamp | Export job name | -| `--include-tags` | | bool | | false | Include tag data in the export | -| `--exclude-dependencies`| | bool | | false | Do not include dependent objects | -| `--polling-interval` | | int | | 10 | Seconds between status polls | -| `--max-wait-time` | | int | | 300 | Maximum seconds to wait for the job | -| `--expand-status` | | bool | | false | Print the per-object export status table | -| `--print-file-contents` | | bool | | false | List ZIP file contents after download | -| `--download-export-log` | | string | | | Download export log to this path (default: `.log`) | -| `--log-file` | | string | | | Append a Markdown release report section; use `--log-file=PATH` to override default path | +| Flag | Short | Type | Required | Default | Description | +| ------------------------ | ----- | ------ | -------- | --------- | ---------------------------------------------------------------------------------------- | +| `--artifacts-file` | | string | | stdin | File listing objects to export (txt/json/yaml/csv); omit for stdin | +| `--export-file-path` | | string | yes | | Output ZIP file path | +| `-n, --name` | `-n` | string | | timestamp | Export job name | +| `--include-tags` | | bool | | false | Include tag data in the export | +| `--exclude-dependencies` | | bool | | false | Do not include dependent objects | +| `--polling-interval` | | int | | 10 | Seconds between status polls | +| `--max-wait-time` | | int | | 300 | Maximum seconds to wait for the job | +| `--expand-status` | | bool | | false | Print the per-object export status table | +| `--print-file-contents` | | bool | | false | List ZIP file contents after download | +| `--download-export-log` | | string | | | Download export log to this path (default: `.log`) | +| `--log-file` | | string | | | Append a Markdown release report section; use `--log-file=PATH` to override default path | All [global flags](../../README.md#global-flags) apply. @@ -195,12 +195,12 @@ All [global flags](../../README.md#global-flags) apply. ### Output columns -| Column | Description | -| --------------- | -------------------------------------------- | -| `id` | Job ID | -| `name` | Job name | -| `status.state` | Current state: `SUCCESSFUL`, `FAILED`, `IN_PROGRESS`, etc. | -| `status.message`| Status detail message | +| Column | Description | +| ---------------- | ---------------------------------------------------------- | +| `id` | Job ID | +| `name` | Job name | +| `status.state` | Current state: `SUCCESSFUL`, `FAILED`, `IN_PROGRESS`, etc. | +| `status.message` | Status detail message | ### Examples @@ -243,10 +243,10 @@ Download the ZIP package for a completed export job. ### Flags -| Flag | Short | Type | Required | Default | Description | -| ----------------- | ----- | ------ | -------- | -------------------- | -------------------------- | -| `--id` | | string | yes | | Export job ID | -| `--output-file` | `-f` | string | | `export_.zip` | Output file path | +| Flag | Short | Type | Required | Default | Description | +| --------------- | ----- | ------ | -------- | ----------------- | ---------------- | +| `--id` | | string | yes | | Export job ID | +| `--output-file` | `-f` | string | | `export_.zip` | Output file path | All [global flags](../../README.md#global-flags) apply. diff --git a/docs/documentation/folder.md b/docs/documentation/folder.md index cd68f93..3e1bdd7 100644 --- a/docs/documentation/folder.md +++ b/docs/documentation/folder.md @@ -22,12 +22,12 @@ iics folder [flags] ### Flags -| Flag | Type | Required | Description | -| ----------------- | ------ | -------- | -------------------------------------------------------- | -| `--name` | string | yes | Folder name | -| `--description` | string | | Folder description | -| `--project-id` | string | | Parent project ID (mutually exclusive with `--project-name`) | -| `--project-name` | string | | Parent project name (mutually exclusive with `--project-id`) | +| Flag | Type | Required | Description | +| ---------------- | ------ | -------- | ------------------------------------------------------------ | +| `--name` | string | yes | Folder name | +| `--description` | string | | Folder description | +| `--project-id` | string | | Parent project ID (mutually exclusive with `--project-name`) | +| `--project-name` | string | | Parent project name (mutually exclusive with `--project-id`) | All [global flags](../../README.md#global-flags) apply. @@ -63,14 +63,14 @@ iics folder create --name "Staging" --project-name "Finance ETL" ` ### Flags -| Flag | Type | Required | Description | -| ----------------- | ------ | -------- | ---------------------------------------------------------- | -| `--id` | string | | Folder ID | -| `--name` | string | | New folder name | -| `--description` | string | | New description | -| `--project-id` | string | | Parent project ID | -| `--project-name` | string | | Parent project name | -| `--folder-name` | string | | Current folder name (use with `--project-name` to identify) | +| Flag | Type | Required | Description | +| ---------------- | ------ | -------- | ----------------------------------------------------------- | +| `--id` | string | | Folder ID | +| `--name` | string | | New folder name | +| `--description` | string | | New description | +| `--project-id` | string | | Parent project ID | +| `--project-name` | string | | Parent project name | +| `--folder-name` | string | | Current folder name (use with `--project-name` to identify) | All [global flags](../../README.md#global-flags) apply. diff --git a/docs/documentation/group.md b/docs/documentation/group.md index 05157b0..9ff97c3 100644 --- a/docs/documentation/group.md +++ b/docs/documentation/group.md @@ -12,13 +12,13 @@ iics ug [flags] ## Subcommands -| Subcommand | Description | -| ---------- | ---------------------------------- | -| `list` | List user groups | -| `get` | Get a single user group | -| `create` | Create one or more user groups | -| `update` | Update one or more user groups | -| `delete` | Delete a user group | +| Subcommand | Description | +| ---------- | ------------------------------ | +| `list` | List user groups | +| `get` | Get a single user group | +| `create` | Create one or more user groups | +| `update` | Update one or more user groups | +| `delete` | Delete a user group | ## API constraints @@ -39,12 +39,12 @@ The v3 `userGroups` resource is limited: ### Flags -| Flag | Short | Type | Default | Description | -| ----------- | ----- | ------ | ------- | ------------------------------------------------ | -| `--limit` | | int | 200 | Max results | -| `--skip` | | int | 0 | Results to skip | -| `--query` | `-q` | string | | Server-side filter (e.g. `userGroupName=="Admins"`) | -| `--fields` | | string | (note) | Comma-separated list of fields to display | +| Flag | Short | Type | Default | Description | +| ---------- | ----- | ------ | ------- | --------------------------------------------------- | +| `--limit` | | int | 200 | Max results | +| `--skip` | | int | 0 | Results to skip | +| `--query` | `-q` | string | | Server-side filter (e.g. `userGroupName=="Admins"`) | +| `--fields` | | string | (note) | Comma-separated list of fields to display | Default fields (table): `id,userGroupName,updatedBy,updateTime,countMembers,countRoles` @@ -72,10 +72,10 @@ prompted to pick one from a list. ### Flags -| Flag | Type | Description | -| -------- | ------ | ------------------ | -| `--id` | string | User group ID | -| `--name` | string | User group name | +| Flag | Type | Description | +| -------- | ------ | --------------- | +| `--id` | string | User group ID | +| `--name` | string | User group name | ### Output @@ -103,10 +103,10 @@ A JSON **array** creates groups in bulk and prints a per-group result table ### Flags -| Flag | Type | Description | -| --------------------- | ------ | ------------------------------------------------------- | -| `--from-file` | string | JSON file (object or array); omit to read piped stdin | -| `--interactive`, `-i` | bool | Prompt for name, description, and roles | +| Flag | Type | Description | +| --------------------- | ------ | ----------------------------------------------------- | +| `--from-file` | string | JSON file (object or array); omit to read piped stdin | +| `--interactive`, `-i` | bool | Prompt for name, description, and roles | The wizard runs with `-i`, or when no input source is present on a terminal. It asks for the group name (existing names are rejected) and description, then @@ -158,12 +158,12 @@ the identifiers the add/remove endpoints use). A file produced by ### Flags -| Flag | Type | Description | -| --------------------- | ------ | -------------------------------------------------- | -| `--id` | string | User group ID | -| `--name` | string | User group name | +| Flag | Type | Description | +| --------------------- | ------ | ----------------------------------------------------- | +| `--id` | string | User group ID | +| `--name` | string | User group name | | `--from-file` | string | JSON file (object or array); omit to read piped stdin | -| `--interactive`, `-i` | bool | Edit the role selection interactively | +| `--interactive`, `-i` | bool | Edit the role selection interactively | The interactive editor shows the group's current roles pre-checked in a filterable checklist. Name and description are shown read-only because the @@ -194,11 +194,11 @@ when neither is given on a terminal. Prompts for confirmation unless `--yes`. ### Flags -| Flag | Short | Type | Description | -| ------- | ----- | ------ | ------------------------ | -| `--id` | | string | User group ID | -| `--name`| | string | User group name | -| `--yes` | `-y` | bool | Skip the confirmation | +| Flag | Short | Type | Description | +| -------- | ----- | ------ | --------------------- | +| `--id` | | string | User group ID | +| `--name` | | string | User group name | +| `--yes` | `-y` | bool | Skip the confirmation | ### Examples diff --git a/docs/documentation/import.md b/docs/documentation/import.md index 070e170..d48b7b6 100644 --- a/docs/documentation/import.md +++ b/docs/documentation/import.md @@ -11,13 +11,13 @@ iics imp [flags] ## Subcommands -| Subcommand | Description | -| ---------------- | ---------------------------------------------------------- | -| `run` | Full pipeline: upload, start, poll, download log | -| `upload` | Upload a ZIP package and get a job ID | -| `start` | Start an import job from an uploaded package | -| `status` | Check import job status | -| `download-log` | Download the import log to a file | +| Subcommand | Description | +| -------------- | ------------------------------------------------ | +| `run` | Full pipeline: upload, start, poll, download log | +| `upload` | Upload a ZIP package and get a job ID | +| `start` | Start an import job from an uploaded package | +| `status` | Check import job status | +| `download-log` | Download the import log to a file | --- @@ -27,19 +27,19 @@ The recommended command for most use cases. Uploads the ZIP, starts the import, ### Flags -| Flag | Short | Type | Required | Default | Description | -| ----------------------- | ----- | ------ | -------- | --------------- | ------------------------------------------------------------ | -| `--zip-file` | `-z` | string | yes | | ZIP package to import | -| `--name` | | string | | zip filename | Import job name | -| `--conflict-resolution` | | string | | `REUSE` | How to handle conflicts: `REUSE` or `OVERWRITE` | -| `--from-file` | | string | | | JSON file with full import specification (overrides other flags) | -| `--polling-interval` | | int | | 10 | Seconds between status polls | -| `--max-wait-time` | | int | | 300 | Maximum seconds to wait | -| `--detailed-polling` | | bool | | false | Print per-object status on every poll | -| `--print-import-log` | | bool | | false | Print the import log after completion | -| `--expand` | | bool | | false | Expand object list in final output | -| `--object-status-fields`| | string | | see below | Comma-separated list of object fields to display | -| `--log-file` | | string | | | Append a Markdown release report section; use `--log-file=PATH` to override default path | +| Flag | Short | Type | Required | Default | Description | +| ------------------------ | ----- | ------ | -------- | ------------ | ---------------------------------------------------------------------------------------- | +| `--zip-file` | `-z` | string | yes | | ZIP package to import | +| `--name` | | string | | zip filename | Import job name | +| `--conflict-resolution` | | string | | `REUSE` | How to handle conflicts: `REUSE` or `OVERWRITE` | +| `--from-file` | | string | | | JSON file with full import specification (overrides other flags) | +| `--polling-interval` | | int | | 10 | Seconds between status polls | +| `--max-wait-time` | | int | | 300 | Maximum seconds to wait | +| `--detailed-polling` | | bool | | false | Print per-object status on every poll | +| `--print-import-log` | | bool | | false | Print the import log after completion | +| `--expand` | | bool | | false | Expand object list in final output | +| `--object-status-fields` | | string | | see below | Comma-separated list of object fields to display | +| `--log-file` | | string | | | Append a Markdown release report section; use `--log-file=PATH` to override default path | All [global flags](../../README.md#global-flags) apply. @@ -272,11 +272,11 @@ Download the import log file for a completed job. ### Flags -| Flag | Type | Required | Default | Description | -| ------------- | ------ | -------- | ------------------------------ | ---------------------------------------- | -| `--id` | string | yes | | Import job ID | -| `--log-path` | string | yes | | Directory to save the log file | -| `--log-name` | string | | `__.log` | Log filename | +| Flag | Type | Required | Default | Description | +| ------------ | ------ | -------- | -------------------------- | ------------------------------ | +| `--id` | string | yes | | Import job ID | +| `--log-path` | string | yes | | Directory to save the log file | +| `--log-name` | string | | `__.log` | Log filename | All [global flags](../../README.md#global-flags) apply. diff --git a/docs/documentation/interactive.md b/docs/documentation/interactive.md index d952834..f098a1c 100644 --- a/docs/documentation/interactive.md +++ b/docs/documentation/interactive.md @@ -11,13 +11,13 @@ terminal. ## Keys -| Prompt | Keys | -| ------------ | -------------------------------------------------------------------- | -| Text input | Type, `Enter` to accept (an empty value keeps the shown default) | -| Single list | Arrows or `j` / `k` to move, `/` to filter (`Esc` clears the filter), `Enter` to choose, `Ctrl+C` to cancel | -| Checklist | `Space` or `x` to toggle, `/` to filter, `Ctrl+A` to select all, `Enter` to confirm | -| Yes / No | Left / right or `y` / `n`, `Enter` to confirm | -| Any prompt | `Ctrl+C` cancels the command | +| Prompt | Keys | +| ----------- | ----------------------------------------------------------------------------------------------------------- | +| Text input | Type, `Enter` to accept (an empty value keeps the shown default) | +| Single list | Arrows or `j` / `k` to move, `/` to filter (`Esc` clears the filter), `Enter` to choose, `Ctrl+C` to cancel | +| Checklist | `Space` or `x` to toggle, `/` to filter, `Ctrl+A` to select all, `Enter` to confirm | +| Yes / No | Left / right or `y` / `n`, `Enter` to confirm | +| Any prompt | `Ctrl+C` cancels the command | Checklists open with the current values already checked (for example a user's current groups and roles), so confirming without changes keeps them. diff --git a/docs/documentation/login.md b/docs/documentation/login.md index 28192d8..d22e297 100644 --- a/docs/documentation/login.md +++ b/docs/documentation/login.md @@ -121,7 +121,7 @@ iics login --profile prod On success the command prints the authenticated user, org info, base API URL, and derived CAI URL: -``` +```text Logging in as jane.smith@company.com... Logged in successfully. User: Jane Smith diff --git a/docs/documentation/lookup.md b/docs/documentation/lookup.md index 2aa3b56..8cfa2c1 100644 --- a/docs/documentation/lookup.md +++ b/docs/documentation/lookup.md @@ -10,10 +10,10 @@ iics lookup [flags] ## Flags -| Flag | Type | Required | Description | -| -------- | ------ | -------- | ------------------------------------------------------------ | -| `--id` | string | * | Object ID to look up | -| `--path` | string | * | Object path (requires `--type`) | +| Flag | Type | Required | Description | +| -------- | ------ | -------- | ------------------------------------------------------------------------------- | +| `--id` | string | * | Object ID to look up | +| `--path` | string | * | Object path (requires `--type`) | | `--type` | string | * | Object type: `PROJECT`, `FOLDER`, `DTEMPLATE`, `MTT`, `DSS`, `CONNECTION`, etc. | \* Provide exactly one of `--id` or `--path`+`--type`. diff --git a/docs/documentation/menu.md b/docs/documentation/menu.md index 3623d84..d5fee49 100644 --- a/docs/documentation/menu.md +++ b/docs/documentation/menu.md @@ -64,16 +64,16 @@ enter run · / filter · p profile · ? help · q quit ## Keys -| Key | Action | -| ------------------- | --------------------------------------------- | -| `↑` / `↓`, `k` / `j` | Move | -| `PgUp` / `PgDn`, `g` / `G` | Page, first / last entry | -| `Enter` | Run the focused entry | -| `/` | Filter by group, label, description or command | -| `Esc` | Clear the filter | -| `p` | Switch profile | -| `?` | Show or hide key help | -| `q`, `Ctrl+C` | Quit the menu | +| Key | Action | +| -------------------------- | ---------------------------------------------- | +| `↑` / `↓`, `k` / `j` | Move | +| `PgUp` / `PgDn`, `g` / `G` | Page, first / last entry | +| `Enter` | Run the focused entry | +| `/` | Filter by group, label, description or command | +| `Esc` | Clear the filter | +| `p` | Switch profile | +| `?` | Show or hide key help | +| `q`, `Ctrl+C` | Quit the menu | ## Running a command @@ -114,16 +114,16 @@ On a production profile: ## Entries -| Group | Entries | -| ----------------------- | ----------------------------------------------------------------------- | -| Session | Switch profile, Set as default profile ✎, Login, List profiles, Add profile ✎, Edit active profile ✎, Delete profile ✎, Edit global settings | -| Users | List users, Show user (search by user name, ID, or part of a name/email), Create user ✎, Edit user groups and roles ✎, Delete user ✎, Change password ✎ | -| User groups | List, Show, Create ✎, Edit roles ✎, Delete ✎ | -| Roles | List, Show (with privileges), Create ✎, Edit privileges ✎ | -| Privileges | List privileges | -| Permissions | Edit object permissions ✎ | +| Group | Entries | +| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Session | Switch profile, Set as default profile ✎, Login, List profiles, Add profile ✎, Edit active profile ✎, Delete profile ✎, Edit global settings | +| Users | List users, Show user (search by user name, ID, or part of a name/email), Create user ✎, Edit user groups and roles ✎, Delete user ✎, Change password ✎ | +| User groups | List, Show, Create ✎, Edit roles ✎, Delete ✎ | +| Roles | List, Show (with privileges), Create ✎, Edit privileges ✎ | +| Privileges | List privileges | +| Permissions | Edit object permissions ✎ | | Environments and agents | List runtime environments, Create runtime environment ✎, List agents, Start / Stop / Restart agent service ✎, Show agent installer info, Download agent installer (`--interactive --progress`) | -| Help | Show CLI help | +| Help | Show CLI help | ## Accessible mode and small terminals diff --git a/docs/documentation/output.md b/docs/documentation/output.md index d7a024c..59c83fa 100644 --- a/docs/documentation/output.md +++ b/docs/documentation/output.md @@ -5,9 +5,9 @@ See [README.md](../../README.md#configuration) for the full config file layout. ## Formats -| Flag | Values | Default | -|-----------------|----------------------------------|---------| -| `--output`, `-o` | `table`, `json`, `csv`, `yaml` | `table` | +| Flag | Values | Default | +| ---------------- | ------------------------------ | ------- | +| `--output`, `-o` | `table`, `json`, `csv`, `yaml` | `table` | `json`, `csv`, and `yaml` always render every field in full - responsive width adaptation (below) applies only to `table` output. @@ -44,13 +44,13 @@ When columns are dropped, a one-line hint is printed to **stderr** naming what w ### Disabling adaptation -| Setting | Effect | -|-----------------------------------|----------------------------------------------------------------| -| `--wide` | Never drop, truncate, or wrap columns for this invocation | -| `style.responsiveTables: false` | Same, set permanently in config | -| non-TTY output (piped/redirected) | Adaptation never applies - full, untruncated output | -| `--output csv\|json\|yaml` | Not affected by adaptation - these formats always render in full | -| `--theme markdown` / `--theme gh` | Always render in full, regardless of TTY | +| Setting | Effect | +| --------------------------------- | ---------------------------------------------------------------- | +| `--wide` | Never drop, truncate, or wrap columns for this invocation | +| `style.responsiveTables: false` | Same, set permanently in config | +| non-TTY output (piped/redirected) | Adaptation never applies - full, untruncated output | +| `--output csv\|json\|yaml` | Not affected by adaptation - these formats always render in full | +| `--theme markdown` / `--theme gh` | Always render in full, regardless of TTY | ### Width detection diff --git a/docs/documentation/package.md b/docs/documentation/package.md index 752ee51..4ced4ab 100644 --- a/docs/documentation/package.md +++ b/docs/documentation/package.md @@ -402,7 +402,7 @@ Use `--order-by` to override the default sort in either mode. | `--order-by` | | string | | | Sort output by field: `path`, `type`, `status`, `warning`, `location`, `dependency` | | `--exclude` | `-e` | string | | | Regex matched against `path.type` - excludes from BFS resolution | | `--exclude-file` | | string | | | Regex policy file; each matching `path.type` is excluded from BFS resolution | -| `--filter` | | string | | | Regex matched against `location` (`(Explore|SYS)/path.type`) - filters final output only | +| `--filter` | | string | | | Regex matched against `location` (`(Explore\|SYS)/path.type`) - filters final output only | | `--output-file` | | string | | | Write output to a file | | `--output-file-format` | | string | | `yaml` | Output file format: `table`, `json`, `csv`, `yaml` | | `--output-file-fields` | | string | | `location,dependency,type,path,status,warning` | Comma-separated fields for output file rows | @@ -425,7 +425,7 @@ API-resolved during traversal. | Column | JSON field | Description | |--------------|--------------|------------------------------------------------------------------------------------------------------| -| `LOCATION` | `location` | Asset identifier as `(Explore|SYS)/path.type` | +| `LOCATION` | `location` | Asset identifier as `(Explore\|SYS)/path.type` | | `DEPENDENCY` | `dependency` | `explicit` for checksum-backed package assets, `transitive` for metadata-only or API-resolved assets | | `TYPE` | `type` | Asset type | | `PATH` | `path` | Asset path without `Explore/` prefix | @@ -434,7 +434,7 @@ API-resolved during traversal. | Column | JSON field | Description | |-----------------|--------------|------------------------------------------------------------------------------------------------------| -| `LOCATION` | `location` | Asset identifier as `(Explore|SYS)/path.type` | +| `LOCATION` | `location` | Asset identifier as `(Explore\|SYS)/path.type` | | `DEPENDENCY` | `dependency` | `explicit` for checksum-backed package assets, `transitive` for metadata-only or API-resolved assets | | `TYPE` | `type` | Asset type | | `PATH` | `path` | Asset path without `Explore/` prefix | @@ -445,7 +445,7 @@ API-resolved during traversal. | Column | JSON field | Description | |-----------------|---------------|------------------------------------------------------------------------------------------------------| -| `LOCATION` | `location` | Asset identifier as `(Explore|SYS)/path.type` | +| `LOCATION` | `location` | Asset identifier as `(Explore\|SYS)/path.type` | | `DEPENDENCY` | `dependency` | `explicit` for checksum-backed package assets, `transitive` for metadata-only or API-resolved assets | | `STATUS (dev)` | `status_dev` | Per-profile status; color-coded | | `STATUS (qa)` | `status_qa` | Per-profile status; color-coded | diff --git a/docs/documentation/permission.md b/docs/documentation/permission.md index 30c7be1..88f74d7 100644 --- a/docs/documentation/permission.md +++ b/docs/documentation/permission.md @@ -34,9 +34,9 @@ iics perm [flags] Every subcommand takes the object as either: -| Flag | Description | -| ------------------ | ------------------------------------------------------------------ | -| `--object-id` | Object ID | +| Flag | Description | +| ------------------ | ---------------------------------------------------------------------------------------------------- | +| `--object-id` | Object ID | | `--path`, `--type` | Object path and type, resolved with [lookup](lookup.md), e.g. `--path "Default/Sales" --type Folder` | Common types: `Project`, `Folder`, `DTEMPLATE` (mapping), `MTT` (mapping task), @@ -54,12 +54,12 @@ Common types: `Project`, `Folder`, `DTEMPLATE` (mapping), `MTT` (mapping task), ### Output columns -| Column | Description | -| ------------- | ----------------------------------------- | -| `TYPE` | `USER` or `GROUP` | -| `NAME` | User name or user group name | -| `READ` ... `CHANGE PERM` | Granted permissions (`true`/`false`) | -| `ACL ID` | ACL ID, used by `update` / `delete --acl-id` | +| Column | Description | +| ------------------------ | -------------------------------------------- | +| `TYPE` | `USER` or `GROUP` | +| `NAME` | User name or user group name | +| `READ` ... `CHANGE PERM` | Granted permissions (`true`/`false`) | +| `ACL ID` | ACL ID, used by `update` / `delete --acl-id` | `-o json` returns the API shape (see [permission set](#permission-set)). @@ -184,12 +184,12 @@ were applied. ### Flags -| Flag | Short | Type | Required | Description | -| ------------- | ----- | ------ | -------- | --------------------------------------------- | +| Flag | Short | Type | Required | Description | +| ------------- | ----- | ------ | -------- | ---------------------------------------------- | | `--from-file` | | string | yes | JSON file with the desired ACLs; `-` for stdin | -| `--prune` | | bool | | Delete ACLs of principals not in the file | -| `--dry-run` | | bool | | Show changes without applying them | -| `--yes` | `-y` | bool | | Skip the confirmation for `--prune` | +| `--prune` | | bool | | Delete ACLs of principals not in the file | +| `--dry-run` | | bool | | Show changes without applying them | +| `--yes` | `-y` | bool | | Skip the confirmation for `--prune` | ### JSON file diff --git a/docs/documentation/privilege.md b/docs/documentation/privilege.md index b887a66..d032464 100644 --- a/docs/documentation/privilege.md +++ b/docs/documentation/privilege.md @@ -22,11 +22,11 @@ No command-specific flags. All [global flags](../../README.md#global-flags) appl ### Output columns -| Column | Description | -| ------------- | ------------------------------------ | -| `id` | Privilege ID | -| `name` | Privilege name | -| `service` | Service the privilege applies to | +| Column | Description | +| ------------- | ---------------------------------------- | +| `id` | Privilege ID | +| `name` | Privilege name | +| `service` | Service the privilege applies to | | `description` | Description of what the privilege grants | ### Examples diff --git a/docs/documentation/profile.md b/docs/documentation/profile.md index b4b97b7..6473702 100644 --- a/docs/documentation/profile.md +++ b/docs/documentation/profile.md @@ -15,15 +15,15 @@ iics profile [flags] ## Subcommands -| Subcommand | Description | -| --------------- | ---------------------------------------------- | -| `add` | Add or update a profile interactively | -| `edit` | Edit an existing profile interactively | -| `list` | List all configured profiles | -| `delete` | Delete a profile | -| `set-default` | Set the default profile used when no `--profile` flag is given | -| `set-password` | Store a profile's password in the OS keychain | -| `show` | Show the details of a single profile | +| Subcommand | Description | +| -------------- | -------------------------------------------------------------- | +| `add` | Add or update a profile interactively | +| `edit` | Edit an existing profile interactively | +| `list` | List all configured profiles | +| `delete` | Delete a profile | +| `set-default` | Set the default profile used when no `--profile` flag is given | +| `set-password` | Store a profile's password in the OS keychain | +| `show` | Show the details of a single profile | ## Flags diff --git a/docs/documentation/publish.md b/docs/documentation/publish.md index ff1db55..9ebb0af 100644 --- a/docs/documentation/publish.md +++ b/docs/documentation/publish.md @@ -221,16 +221,16 @@ and print a detailed summary. Mirrors the `import run` pattern. ### Flags -| Flag | Type | Default | Description | Required | -|----------------------|---------------------|-----------|------------------------------------------------------------------------|--------------------| -| `--asset` | string (repeatable) | | Asset path(s) to publish | one input required | -| `--from-file` | string | | File with asset paths (.txt/.csv/.json/.yaml); omit to read from stdin | | -| `--cai-url` | string | | CAI base URL override | | -| `--name` | string | | Optional job label | | -| `--polling-interval` | int | 10 | Seconds between status polls | | -| `--max-wait-time` | int | 300 | Maximum seconds to wait for completion | | -| `--detailed-polling` | bool | false | Print per-asset item detail table on each poll (requires `--verbose`) | | -| `--item-fields` | string | see below | Comma-separated item detail fields to display | | +| Flag | Type | Default | Description | Required | +| -------------------- | ------------------- | --------- | ---------------------------------------------------------------------------------------- | ------------------ | +| `--asset` | string (repeatable) | | Asset path(s) to publish | one input required | +| `--from-file` | string | | File with asset paths (.txt/.csv/.json/.yaml); omit to read from stdin | | +| `--cai-url` | string | | CAI base URL override | | +| `--name` | string | | Optional job label | | +| `--polling-interval` | int | 10 | Seconds between status polls | | +| `--max-wait-time` | int | 300 | Maximum seconds to wait for completion | | +| `--detailed-polling` | bool | false | Print per-asset item detail table on each poll (requires `--verbose`) | | +| `--item-fields` | string | see below | Comma-separated item detail fields to display | | | `--log-file` | string | | Append a Markdown release report section; use `--log-file=PATH` to override default path | | ### Input format diff --git a/docs/documentation/release.md b/docs/documentation/release.md index 68e7294..4028da4 100644 --- a/docs/documentation/release.md +++ b/docs/documentation/release.md @@ -24,19 +24,19 @@ Generates release manifest files from either a PR description markdown file (Dep ### Flags -| Flag | Type | Default | Description | -|------------------------|-------------|----------------------|-------------------------------------------------------------------------------------| -| `--from-file` | string | | PR description markdown file to parse | -| `--output-root` | string | `target/iics/import` | Root output directory | -| `--output` | string | `yaml` | Manifest output format: `yaml`, `json`, `properties` | -| `--mode` | string | | Override mode: `full` or `tag-based` | -| `--tag` | string | | Deployment tag when mode is `tag-based` | -| `--targets` | string list | | Target environments (e.g. `tst,qa`) | -| `--valid-targets` | string | | Comma-separated allowlist for valid targets (overrides `IICS_VALID_DEPLOY_TARGETS`) | -| `--include-connectors` | bool | `false` | Include connector assets | -| `--include-connections` | bool | `false` | Include connection assets | -| `--exclude-file` | string | | Regex policy file path | -| `--source` | string | | Optional source identifier stored in manifest | +| Flag | Type | Default | Description | +| ----------------------- | ----------- | -------------------- | ----------------------------------------------------------------------------------- | +| `--from-file` | string | | PR description markdown file to parse | +| `--output-root` | string | `target/iics/import` | Root output directory | +| `--output` | string | `yaml` | Manifest output format: `yaml`, `json`, `properties` | +| `--mode` | string | | Override mode: `full` or `tag-based` | +| `--tag` | string | | Deployment tag when mode is `tag-based` | +| `--targets` | string list | | Target environments (e.g. `tst,qa`) | +| `--valid-targets` | string | | Comma-separated allowlist for valid targets (overrides `IICS_VALID_DEPLOY_TARGETS`) | +| `--include-connectors` | bool | `false` | Include connector assets | +| `--include-connections` | bool | `false` | Include connection assets | +| `--exclude-file` | string | | Regex policy file path | +| `--source` | string | | Optional source identifier stored in manifest | ### Example @@ -65,8 +65,8 @@ Validates manifest schema version and normalized options. ### Flags | Flag | Type | Default | Description | -|-------------------|--------|-------------------------------------------------|-------------------------------------------------------------------------------------| -| `--manifest` | string | `target/iics/import/conf/release_manifest.yaml` | Manifest path (`.yaml`, `.json`, `.properties`), or `-` for stdin | +| ----------------- | ------ | ----------------------------------------------- | ----------------------------------------------------------------------------------- | +| `--manifest` | string | `target/iics/import/conf/release_manifest.yaml` | Manifest path (`.yaml`, `.json`, `.properties`), or `-` for stdin | | `--valid-targets` | string | | Comma-separated allowlist for valid targets (overrides `IICS_VALID_DEPLOY_TARGETS`) | ### Example @@ -204,21 +204,21 @@ Excludes always apply to the publish file regardless of whether ### Flags -| Flag | Type | Default | Description | -|---------------------------------|--------|-------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------| -| `--manifest` | string | `target/iics/import/conf/release_manifest.yaml` | Manifest path | -| `--output-root` | string | `target/iics/import` | Root output directory | -| `--full-package-config` | string | `./conf/full_build.package.csv` | Full mode seed config (`id`, `location`, or `path+type`) used to resolve package assets | -| `--valid-targets` | string | | Comma-separated allowlist for valid targets (overrides `IICS_VALID_DEPLOY_TARGETS`) | -| `--target-profile-map` | string | | Comma-separated mapping `TARGET=profile` used for target org credential resolution (overrides `IICS_TARGET_PROFILE_MAP`) | -| `--include-found-transitive` | bool | `false` | Keep all resolved transitive dependencies, even when they already exist in the target environment | -| `--publish-include-found-transitive` | bool | `false` | Include transitive dependencies already found in the target in the generated publish file only (package file unaffected; independent of `--include-found-transitive`) | -| `--publish-exclude-regex` | string | | Regex matched against an asset's `location` to exclude it from the generated publish file only | -| `--publish-exclude-file` | string | | Path to regex patterns file (same format as `excludeFile`) to exclude assets from the generated publish file only | -| `--output` | string | `csv` | Plan file output format: `csv`, `json`, `yaml` | -| `--package-fields` | string | `location,type,path,dependency` | Fields for package files; `STATUS ()` is auto-added per environment file | -| `--publish-fields` | string | `location,type,path,dependency` | Fields for publish files | -| `--log-file` | string | disabled; default path when flag has no value | Append a Markdown release report section; use `--log-file=PATH` to override `target/iics/import/logs/release_manifest.md` | +| Flag | Type | Default | Description | +| ------------------------------------ | ------ | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--manifest` | string | `target/iics/import/conf/release_manifest.yaml` | Manifest path | +| `--output-root` | string | `target/iics/import` | Root output directory | +| `--full-package-config` | string | `./conf/full_build.package.csv` | Full mode seed config (`id`, `location`, or `path+type`) used to resolve package assets | +| `--valid-targets` | string | | Comma-separated allowlist for valid targets (overrides `IICS_VALID_DEPLOY_TARGETS`) | +| `--target-profile-map` | string | | Comma-separated mapping `TARGET=profile` used for target org credential resolution (overrides `IICS_TARGET_PROFILE_MAP`) | +| `--include-found-transitive` | bool | `false` | Keep all resolved transitive dependencies, even when they already exist in the target environment | +| `--publish-include-found-transitive` | bool | `false` | Include transitive dependencies already found in the target in the generated publish file only (package file unaffected; independent of `--include-found-transitive`) | +| `--publish-exclude-regex` | string | | Regex matched against an asset's `location` to exclude it from the generated publish file only | +| `--publish-exclude-file` | string | | Path to regex patterns file (same format as `excludeFile`) to exclude assets from the generated publish file only | +| `--output` | string | `csv` | Plan file output format: `csv`, `json`, `yaml` | +| `--package-fields` | string | `location,type,path,dependency` | Fields for package files; `STATUS ()` is auto-added per environment file | +| `--publish-fields` | string | `location,type,path,dependency` | Fields for publish files | +| `--log-file` | string | disabled; default path when flag has no value | Append a Markdown release report section; use `--log-file=PATH` to override `target/iics/import/logs/release_manifest.md` | ### Example diff --git a/docs/documentation/role.md b/docs/documentation/role.md index 993eb85..01bdc64 100644 --- a/docs/documentation/role.md +++ b/docs/documentation/role.md @@ -91,10 +91,10 @@ stdout is not a terminal. ### Flags -| Flag | Type | Required | Description | -| -------------- | ------ | -------- | ----------------------------------------- | -| `--id` | string | | Role ID | -| `--name` | string | | Role name (for example `Business Manager`) | +| Flag | Type | Required | Description | +| -------------- | ------ | -------- | ------------------------------------------------------------------------------------------ | +| `--id` | string | | Role ID | +| `--name` | string | | Role name (for example `Business Manager`) | | `--privileges` | bool | | Include privileges assigned to the role; prompts when omitted (table output on a terminal) | `--id` and `--name` are mutually exclusive. If both are omitted, you are @@ -210,16 +210,16 @@ arrows move space toggle a row c column / filter ctrl+u clear filter enter Grid keys: -| Key | Action | -| --------------------- | --------------------------------------------------- | -| Arrows or `h j k l` | Move | -| `Space` or `x` | Toggle the privilege under the cursor | -| `a` | Toggle every action in the row | -| `c` | Toggle the column for all visible (filtered) rows | -| `/` | Filter by object, privilege name or description | -| `Ctrl+U` | Clear the filter | -| `PgUp` / `PgDn`, `g` / `G` | Page, jump to top / bottom | -| `Enter`, `Esc`, `q` | Back to the service list | +| Key | Action | +| -------------------------- | ------------------------------------------------- | +| Arrows or `h j k l` | Move | +| `Space` or `x` | Toggle the privilege under the cursor | +| `a` | Toggle every action in the row | +| `c` | Toggle the column for all visible (filtered) rows | +| `/` | Filter by object, privilege name or description | +| `Ctrl+U` | Clear the filter | +| `PgUp` / `PgDn`, `g` / `G` | Page, jump to top / bottom | +| `Enter`, `Esc`, `q` | Back to the service list | In the service list, choose **Review and apply** to see the pending changes grouped by service and confirm them, or **Cancel** to leave without changes. diff --git a/docs/documentation/securitylog.md b/docs/documentation/securitylog.md index 92948d2..c137b34 100644 --- a/docs/documentation/securitylog.md +++ b/docs/documentation/securitylog.md @@ -22,25 +22,25 @@ iics securitylog [flags] ### Flags -| Flag | Type | Default | Description | -| --------- | ------ | ------- | ------------------------------------------------- | +| Flag | Type | Default | Description | +| --------- | ------ | ------- | ---------------------------------------------------------- | | `--start` | string | | Start time in ISO 8601 format, e.g. `2026-01-01T00:00:00Z` | -| `--end` | string | | End time in ISO 8601 format | -| `--limit` | int | 200 | Max results | -| `--skip` | int | 0 | Results to skip | +| `--end` | string | | End time in ISO 8601 format | +| `--limit` | int | 200 | Max results | +| `--skip` | int | 0 | Results to skip | All [global flags](../../README.md#global-flags) apply. ### Output columns -| Column | Description | -| ------------ | ---------------------------------------- | -| `entryTime` | Timestamp of the audit event | -| `userName` | User who performed the action | +| Column | Description | +| ------------ | -------------------------------------------------- | +| `entryTime` | Timestamp of the audit event | +| `userName` | User who performed the action | | `action` | Action taken (LOGIN, CREATE, UPDATE, DELETE, etc.) | -| `objectType` | Type of object affected | -| `status` | Outcome: SUCCESS or FAILED | -| `sourceIp` | IP address of the client | +| `objectType` | Type of object affected | +| `status` | Outcome: SUCCESS or FAILED | +| `sourceIp` | IP address of the client | ### Examples diff --git a/docs/documentation/sourcecontrol.md b/docs/documentation/sourcecontrol.md index 9abdcde..72a0023 100644 --- a/docs/documentation/sourcecontrol.md +++ b/docs/documentation/sourcecontrol.md @@ -13,12 +13,12 @@ iics sc [flags] ## Subcommands -| Subcommand | Description | -| ---------- | -------------------------------------------------------- | -| `checkout` | Check out an object for editing | -| `checkin` | Check in an object with a commit comment | -| `pull` | Pull latest changes from the source control repository | -| `commit` | Commit pending changes to the repository | +| Subcommand | Description | +| ---------- | ------------------------------------------------------ | +| `checkout` | Check out an object for editing | +| `checkin` | Check in an object with a commit comment | +| `pull` | Pull latest changes from the source control repository | +| `commit` | Commit pending changes to the repository | --- @@ -72,9 +72,9 @@ Check in an object after editing, adding a commit comment. ### Flags -| Flag | Type | Required | Description | -| ------------- | ------ | -------- | ------------------------------- | -| `--object-id` | string | yes | Object ID to check in | +| Flag | Type | Required | Description | +| ------------- | ------ | -------- | --------------------------------- | +| `--object-id` | string | yes | Object ID to check in | | `--comment` | string | | Commit comment for source control | All [global flags](../../README.md#global-flags) apply. diff --git a/docs/documentation/state.md b/docs/documentation/state.md index 39aee1d..821fa37 100644 --- a/docs/documentation/state.md +++ b/docs/documentation/state.md @@ -10,9 +10,9 @@ iics state [flags] ## Subcommands -| Subcommand | Description | -| ---------- | ---------------------------------------- | -| `fetch` | Fetch the current state of an object | +| Subcommand | Description | +| ---------- | -------------------------------------------- | +| `fetch` | Fetch the current state of an object | | `load` | Load a previously saved state into an object | --- @@ -23,10 +23,10 @@ Retrieve the current state of an object as JSON. Output is written to a file or ### Flags -| Flag | Short | Type | Required | Description | -| ----------------- | ----- | ------ | -------- | --------------------------------------- | -| `--object-id` | | string | yes | Object ID | -| `--output-file` | `-f` | string | | File path to save state JSON (default: stdout) | +| Flag | Short | Type | Required | Description | +| --------------- | ----- | ------ | -------- | ---------------------------------------------- | +| `--object-id` | | string | yes | Object ID | +| `--output-file` | `-f` | string | | File path to save state JSON (default: stdout) | All [global flags](../../README.md#global-flags) apply. diff --git a/docs/documentation/user.md b/docs/documentation/user.md index 6ae1702..14fafdf 100644 --- a/docs/documentation/user.md +++ b/docs/documentation/user.md @@ -37,13 +37,13 @@ All [global flags](../../README.md#global-flags) apply. ### Output columns -| Column | Description | -| ----------- | ---------------------- | -| `id` | User ID | -| `userName` | Username / login name | -| `email` | Email address | -| `state` | Account state (Active/Inactive) | -| `updateTime`| Last modification time | +| Column | Description | +| ------------ | ------------------------------- | +| `id` | User ID | +| `userName` | Username / login name | +| `email` | Email address | +| `state` | Account state (Active/Inactive) | +| `updateTime` | Last modification time | ### Examples @@ -122,25 +122,6 @@ iics user get --username jdoe@example.com --output json iics user get ``` ------- | ------ | -------- | ----------- | -| `--id` | string | yes | User ID | - -All [global flags](../../README.md#global-flags) apply. - -### Examples - -```bash -iics user get --id - -iics user get --id --output json -``` - -```powershell -iics user get --id - -iics user get --id --output json -``` - --- ## user create @@ -201,13 +182,13 @@ profiles: Each value resolves as: profile `newUser`, global `newUser`, built-in default. -| Placeholder | Value | -| ---------------- | ------------------------------------------------------- | -| `{firstName}` | First name, lowercase, spaces removed | -| `{lastName}` | Last name, lowercase, spaces removed | -| `{firstInitial}` | First letter of the first name, lowercase | -| `{lastInitial}` | First letter of the last name, lowercase | -| `{profileName}` | Name of the active profile | +| Placeholder | Value | +| ---------------- | ------------------------------------------------------------------------------ | +| `{firstName}` | First name, lowercase, spaces removed | +| `{lastName}` | Last name, lowercase, spaces removed | +| `{firstInitial}` | First letter of the first name, lowercase | +| `{lastInitial}` | First letter of the last name, lowercase | +| `{profileName}` | Name of the active profile | | `{domain}` | `newUser.domain` (profile, then global), or the domain of the profile username | With the defaults, Jane Doe in profile `dev` (login `admin@acme.com`) gets @@ -317,14 +298,14 @@ Add, remove or replace the roles assigned to a user. Uses ### Flags -| Flag | Type | Required | Description | -| ---------------------- | -------- | -------- | ---------------------------------------------------------- | -| `--id` (`--uid`) | string | one of | User ID | -| `--username` (`--uname`) | string | one of | User name (exact match) | -| `--add` | string[] | | Roles to assign; comma-separated or repeated | -| `--remove` | string[] | | Roles to unassign; comma-separated or repeated | -| `--replace` | string[] | | Exact list of roles; all other roles are removed | -| `--fields` | string | | Fields for CSV output (same as `user get`) | +| Flag | Type | Required | Description | +| ------------------------ | -------- | -------- | ------------------------------------------------ | +| `--id` (`--uid`) | string | one of | User ID | +| `--username` (`--uname`) | string | one of | User name (exact match) | +| `--add` | string[] | | Roles to assign; comma-separated or repeated | +| `--remove` | string[] | | Roles to unassign; comma-separated or repeated | +| `--replace` | string[] | | Exact list of roles; all other roles are removed | +| `--fields` | string | | Fields for CSV output (same as `user get`) | Rules: diff --git a/docs/issues/completed/2026-09-01-export-fixed-http-timeout-ignores-max-wait-time.md b/docs/issues/completed/2026-09-01-export-fixed-http-timeout-ignores-max-wait-time.md index 6ede500..b635fb7 100644 --- a/docs/issues/completed/2026-09-01-export-fixed-http-timeout-ignores-max-wait-time.md +++ b/docs/issues/completed/2026-09-01-export-fixed-http-timeout-ignores-max-wait-time.md @@ -6,7 +6,7 @@ `iics export run` fails immediately (before job polling ever starts) with: -``` +```text Error: starting export: Post "https://use4.dm-us.informaticacloud.com/saas/public/core/v3/export?includeTagInformation=true": context deadline exceeded (Client.Timeout exceeded while awaiting headers) ``` @@ -55,10 +55,10 @@ This timeout applies to **every** HTTP request made by the client (login, `Start ## Environment -| Field | Value | -| -------------------------- | ---------------------------------------------------- | -| Command | `iics export run` | -| Region | USE4 | +| Field | Value | +| ------- | ----------------- | +| Command | `iics export run` | +| Region | USE4 | --- diff --git a/docs/issues/completed/BUG-0001-package-dependencies-sort-order.md b/docs/issues/completed/BUG-0001-package-dependencies-sort-order.md index 2b8262c..90a2ac2 100644 --- a/docs/issues/completed/BUG-0001-package-dependencies-sort-order.md +++ b/docs/issues/completed/BUG-0001-package-dependencies-sort-order.md @@ -67,8 +67,8 @@ any other `dependencyItem` field. When `--publish` is set, assets must sort in the following order (primary), with `Path` as the secondary (tiebreaker) sort key: -| Priority | Type | -|----------|-----------------------| +| Priority | Type | +| -------- | ---------------------- | | 1 | `AI_SERVICE_CONNECTOR` | | 2 | `AI_CONNECTION` | | 3 | `PROCESS` | @@ -87,10 +87,10 @@ Add an `--order-by ` flag to `package dependencies` that accepts any field name present in the dependency output: | Field | Notes | -|----------------|-------------------------------------| +| -------------- | ----------------------------------- | | `path` | Default when `--publish` is not set | | `type` | Sort by asset type string | -| `source` | `package` vs `external` | +| `source` | `package` vs `external` | | `targetStatus` | `found`, `missing`, `unknown` | | `warning` | Warning message text | diff --git a/docs/issues/completed/BUG-0002-table-ansi-column-misalignment.md b/docs/issues/completed/BUG-0002-table-ansi-column-misalignment.md index 6aba1fc..929880d 100644 --- a/docs/issues/completed/BUG-0002-table-ansi-column-misalignment.md +++ b/docs/issues/completed/BUG-0002-table-ansi-column-misalignment.md @@ -17,7 +17,7 @@ without proper column separation. ## Observed output (`--report dev,qa,tst`) -``` +```text PATH STATUS (dev) STATUS (qa) STATUS (tst) ───────────────────────────────────────────────────────────────────────────────────────── ──────────── ─────────── ─────────── Corvel_v1/Corvel-GW-SubmitClaimFeedService.AI_SERVICE_CONNECTOR found found found @@ -27,7 +27,7 @@ Corvel_v1/GuideWire-CreateInvoice-v1.AI_SERVICE_CONNECTOR ## Expected output -``` +```text PATH STATUS (dev) STATUS (qa) STATUS (tst) ───────────────────────────────────────────────────────────────────────────────────────── ──────────── ─────────── ─────────── Corvel_v1/Corvel-GW-SubmitClaimFeedService.AI_SERVICE_CONNECTOR found found found @@ -49,7 +49,7 @@ l := utf8.RuneCountInString(extractField(row, col)) `extractField` calls `col.Func(row)` when a `Func` is set. `makeProfileStatusFunc` and `targetStatusFunc` return ANSI-escaped strings such as: -``` +```text \x1b[32;1mfound\x1b[0m (visible width: 5, rune count: 15) \x1b[38;5;208mmissing\x1b[0m (visible width: 7, rune count: 18) ``` diff --git a/docs/issues/completed/BUG-0003-keyring-integration-issues.md b/docs/issues/completed/BUG-0003-keyring-integration-issues.md index 1e80f45..aa0d81d 100644 --- a/docs/issues/completed/BUG-0003-keyring-integration-issues.md +++ b/docs/issues/completed/BUG-0003-keyring-integration-issues.md @@ -52,7 +52,8 @@ sends `"@keyring"` as the IICS password. ### Impact All commands that require authentication fail for the affected profile with: -``` + +```text [WARN] dependency lookup failed profile=dev error=auto-login failed: IICS API error (HTTP 400): ``` @@ -152,7 +153,7 @@ stored in the config file or the OS keychain. ### Observed behavior -``` +```text ./iics package dependencies -f file.zip --report --target-profile 'dev' --verbose [INFO] report: starting validation profile=--target-profile total=10 Error: profile "--target-profile": username not configured for target profile "--target-profile" diff --git a/docs/issues/completed/BUG-0004-user-get-by-id-http-405.md b/docs/issues/completed/BUG-0004-user-get-by-id-http-405.md index b0696e1..70e77a7 100644 --- a/docs/issues/completed/BUG-0004-user-get-by-id-http-405.md +++ b/docs/issues/completed/BUG-0004-user-get-by-id-http-405.md @@ -17,7 +17,7 @@ HTTP method or URL for retrieving a single user by ID. ## Observed Behavior -``` +```text $ ./iics user get --id '9IB4JZitmQDhj2cLTgWS64' Error: IICS API error (HTTP 405): HTTP 405 Method Not Allowed @@ -44,7 +44,7 @@ depending on `--output`). `GetUser()` in `internal/client/users.go:76-82` issues: -``` +```text GET /public/core/v3/users/{id} ``` diff --git a/docs/issues/completed/BUG-0005-user-create-interactive-limit-exceeds-max.md b/docs/issues/completed/BUG-0005-user-create-interactive-limit-exceeds-max.md index 8f03336..2bf2fc5 100644 --- a/docs/issues/completed/BUG-0005-user-create-interactive-limit-exceeds-max.md +++ b/docs/issues/completed/BUG-0005-user-create-interactive-limit-exceeds-max.md @@ -18,7 +18,7 @@ API maximum of 200. ## Observed Behavior -``` +```text Force password change on next login? [y/N]: n Error: IICS API error (HTTP 400): "code": "V3API_IDSError_003", diff --git a/docs/issues/completed/BUG-0007-location-prefix-sys-for-system-assets.md b/docs/issues/completed/BUG-0007-location-prefix-sys-for-system-assets.md index af40ca0..4ee6033 100644 --- a/docs/issues/completed/BUG-0007-location-prefix-sys-for-system-assets.md +++ b/docs/issues/completed/BUG-0007-location-prefix-sys-for-system-assets.md @@ -43,4 +43,3 @@ Multiple code paths independently constructed location values with dependency flows. - Updated parsing and documentation to support both `Explore/` and `SYS/` location roots. - diff --git a/docs/issues/completed/BUG-0015-runtime-create-missing-type-discriminator.md b/docs/issues/completed/BUG-0015-runtime-create-missing-type-discriminator.md index 6687508..d23503f 100644 --- a/docs/issues/completed/BUG-0015-runtime-create-missing-type-discriminator.md +++ b/docs/issues/completed/BUG-0015-runtime-create-missing-type-discriminator.md @@ -7,7 +7,7 @@ ## Symptom -``` +```text $ iics runtime create --from-file TEST_ENV.json Error: IICS API error APP_20149 (HTTP 400): "description": "Missing required request body:Malformed or missing request body." diff --git a/docs/issues/completed/Bug-caiUrl-not-derived-and-not-persisted.md b/docs/issues/completed/Bug-caiUrl-not-derived-and-not-persisted.md index c369cc4..395a00f 100644 --- a/docs/issues/completed/Bug-caiUrl-not-derived-and-not-persisted.md +++ b/docs/issues/completed/Bug-caiUrl-not-derived-and-not-persisted.md @@ -4,7 +4,7 @@ Running any `publish` or `state` command that requires the CAI endpoint fails with: -``` +```text Error: batch 1: submitting: CAI URL not configured; set caiUrl in profile config, IICS_CAI_URL env var, or --cai-url flag ``` @@ -30,7 +30,7 @@ c.caiURL = fmt.Sprintf("%s://%s", u.Scheme, u.Host) The correct CAI URL inserts `-cai` into the first DNS label of the hostname: -``` +```text baseApiUrl: https://use4.dm-us.informaticacloud.com/saas caiUrl: https://use4-cai.dm-us.informaticacloud.com ``` @@ -206,7 +206,7 @@ In `PromptProfile` (`internal/config/prompt.go`), after the user provides a regi URL, derive `baseApiUrl` and `caiUrl` and show them for confirmation: - If a known region is entered, look up the pod hostname and construct both URLs. -- Show the computed values and prompt: `CAI URL []: ` so the user can override. +- Show the computed values and prompt: `CAI URL []:` so the user can override. - Store all three fields (`loginUrl`, `baseApiUrl` if deterministic, `caiUrl`) in the returned `Profile`. diff --git a/docs/issues/completed/Bug-import-object-status-table-missing-fields.md b/docs/issues/completed/Bug-import-object-status-table-missing-fields.md index 1d34c92..d34dfba 100644 --- a/docs/issues/completed/Bug-import-object-status-table-missing-fields.md +++ b/docs/issues/completed/Bug-import-object-status-table-missing-fields.md @@ -5,7 +5,7 @@ When running `iics import run` with `--expand` or `--detailed-polling`, the "Imported Objects" table prints rows with blank ID, NAME, and TYPE columns. Only STATUS and MESSAGE are populated: -``` +```text Imported Objects: ┌────┬──────┬──────┬────────────┬───────────────────────────┐ │ ID │ NAME │ TYPE │ STATUS │ MESSAGE │ @@ -17,7 +17,7 @@ Imported Objects: Expected output (illustrative): -``` +```text Imported Objects: ┌────────────────────────┬──────────────────────────────┬───────────────────────────────┬──────────────────────┬────────────┬───────────────────────────┐ │ SOURCE ID │ SOURCE PATH │ SOURCE NAME │ TARGET NAME │ STATE │ MESSAGE │ @@ -171,7 +171,7 @@ type ImportJob struct { Default columns (field names match `FlatImportObject` JSON tags): -``` +```text sourceId, sourcePath, sourceName, targetName, state, message ``` diff --git a/docs/issues/completed/Bug-objects-list-query-filter.md b/docs/issues/completed/Bug-objects-list-query-filter.md index 738b4e7..51eb860 100644 --- a/docs/issues/completed/Bug-objects-list-query-filter.md +++ b/docs/issues/completed/Bug-objects-list-query-filter.md @@ -84,11 +84,11 @@ cmd/objects.go - flag definitions, --query example string ### API query parameters -| Parameter | Type | Notes | -| --------- | ---- | ----- | +| Parameter | Type | Notes | +| --------- | ------ | ----------------------------------------------------------- | | `q` | String | Only filter parameter. Multiple conditions separated by `;` | -| `limit` | Int | Max 200 per page | -| `skip` | Int | Pagination offset | +| `limit` | Int | Max 200 per page | +| `skip` | Int | Pagination offset | ### Supported `q` filter fields and operators @@ -107,7 +107,7 @@ cmd/objects.go - flag definitions, --query example string **Sample API request (correct separator):** -``` +```text GET /public/core/v3/objects?q=location=='Default/Sales';type=='MTT'&limit=200 ``` diff --git a/docs/issues/completed/Bug-publish-api-missing-accept-header.md b/docs/issues/completed/Bug-publish-api-missing-accept-header.md index 53d3fa8..5193677 100644 --- a/docs/issues/completed/Bug-publish-api-missing-accept-header.md +++ b/docs/issues/completed/Bug-publish-api-missing-accept-header.md @@ -29,13 +29,13 @@ HTTP 500 Internal Server Error `internal/client/client.go` - `doCAIJSON` method (line 338) only sets: -``` +```text Content-Type: application/vnd.api+json ``` The CAI REST API requires both: -``` +```text Content-Type: application/vnd.api+json Accept: application/vnd.api+json ``` From 0a34081716058c90d967b8ea1399c2b22e9f32c4 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Sun, 11 Oct 2026 18:57:29 -0400 Subject: [PATCH 28/52] docs: point AGENTS.md at docs/CLAUDE.md --- AGENTS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 906d383..3a52640 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ This file provides guidance to Codex (Codex.ai/code) when working with code in this repository. -> For the full authoritative reference (patterns, struct rules, API versioning, examples), read **`docs/AGENTS.md`** first. +> For the full authoritative reference (patterns, struct rules, API versioning, examples), read **`docs/CLAUDE.md`** first. --- @@ -149,4 +149,4 @@ Commit the updated files in `completions/` together with the code change. - Generate Markdown that follows the [Markdown Lint Rules](https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md) - Do **not** use em dashes (`-`) in generated Markdown; use a regular hyphen (`-`) instead -See [docs/AGENTS.md](docs/AGENTS.md) for complete code templates for all four files. +See [docs/CLAUDE.md](docs/CLAUDE.md) for complete code templates for all four files. From a800412767a4ad288fc9d66d04887dd00672e539 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Sun, 11 Oct 2026 18:58:15 -0400 Subject: [PATCH 29/52] docs: use go from PATH instead of /opt/local/bin/go The hard-coded path only exists on the MacPorts machine. Completed change requests and bug reports keep the old path as historical records. --- .github/copilot-instructions.md | 10 +++++----- AGENTS.md | 16 ++++++++-------- CLAUDE.md | 16 ++++++++-------- docs/CLAUDE.md | 6 +++--- docs/templates/Bug-Template.md | 6 +++--- docs/templates/CR-Template.md | 6 +++--- 6 files changed, 30 insertions(+), 30 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index ac998ed..899843a 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -4,20 +4,20 @@ ```bash # Build -/opt/local/bin/go build ./... +go build ./... make build # with version/commit ldflags injected # Test (full suite with race detector) -/opt/local/bin/go test -v -race ./... +go test -v -race ./... # Single test -/opt/local/bin/go test -v -run TestFunctionName ./internal/client/... +go test -v -run TestFunctionName ./internal/client/... # Single package -/opt/local/bin/go test -v -race ./internal/client/... +go test -v -race ./internal/client/... # Vet / Format / Lint -/opt/local/bin/go vet ./... +go vet ./... gofmt -s -w . golangci-lint run ./... diff --git a/AGENTS.md b/AGENTS.md index 3a52640..8b9bfd0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,8 +30,8 @@ Change Requests live in `docs/ChangeRequests/` as markdown files and must be ref After implementing changes, run the following before committing: ```bash -/opt/local/bin/go build ./... -/opt/local/bin/go vet ./... +go build ./... +go vet ./... ``` Also verify that the Go version in `go.mod` matches the version used in `.github/workflows/` CI files to avoid version mismatch failures. @@ -52,16 +52,16 @@ When modifying API request/response structs: ```bash # Build -/opt/local/bin/go build ./... +go build ./... make build # includes ldflags with version injection # Test -/opt/local/bin/go test ./... -/opt/local/bin/go test -v -race ./internal/client/... # single package, with race detector -/opt/local/bin/go test -run TestListWidgets ./internal/client/... # single test +go test ./... +go test -v -race ./internal/client/... # single package, with race detector +go test -run TestListWidgets ./internal/client/... # single test # Vet / Format / Lint -/opt/local/bin/go vet ./... +go vet ./... gofmt -s -w . golangci-lint run ./... ``` @@ -90,7 +90,7 @@ golangci-lint run ./... ```bash gofmt -s -w . - /opt/local/bin/go vet ./... + go vet ./... golangci-lint run ./... ``` diff --git a/CLAUDE.md b/CLAUDE.md index 6a7cf47..65525d7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,8 +30,8 @@ Change Requests live in `docs/ChangeRequests/` as markdown files and must be ref After implementing changes, run the following before committing: ```bash -/opt/local/bin/go build ./... -/opt/local/bin/go vet ./... +go build ./... +go vet ./... ``` Also verify that the Go version in `go.mod` matches the version used in `.github/workflows/` CI files to avoid version mismatch failures. @@ -52,16 +52,16 @@ When modifying API request/response structs: ```bash # Build -/opt/local/bin/go build ./... +go build ./... make build # includes ldflags with version injection # Test -/opt/local/bin/go test ./... -/opt/local/bin/go test -v -race ./internal/client/... # single package, with race detector -/opt/local/bin/go test -run TestListWidgets ./internal/client/... # single test +go test ./... +go test -v -race ./internal/client/... # single package, with race detector +go test -run TestListWidgets ./internal/client/... # single test # Vet / Format / Lint -/opt/local/bin/go vet ./... +go vet ./... gofmt -s -w . golangci-lint run ./... ``` @@ -90,7 +90,7 @@ golangci-lint run ./... ```bash gofmt -s -w . - /opt/local/bin/go vet ./... + go vet ./... golangci-lint run ./... ``` diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md index 5e40105..13a93af 100644 --- a/docs/CLAUDE.md +++ b/docs/CLAUDE.md @@ -13,7 +13,7 @@ imports, users, roles, schedules, agents, runtime environments, and more. **Module:** `github.com/jbrazda/iics-cli` **Go version:** 1.25.0 -**Go binary on this machine:** `/opt/local/bin/go` +**Go binary on this machine:** `go` --- @@ -643,8 +643,8 @@ func TestGetWidget(t *testing.T) { } ``` -Run tests with: `/opt/local/bin/go test ./...` -Build with: `/opt/local/bin/go build ./...` +Run tests with: `go test ./...` +Build with: `go build ./...` --- diff --git a/docs/templates/Bug-Template.md b/docs/templates/Bug-Template.md index 7b4cc8f..791f738 100644 --- a/docs/templates/Bug-Template.md +++ b/docs/templates/Bug-Template.md @@ -137,15 +137,15 @@ internal/client/.go: - In `internal/client/users.go`, the `Email` field has JSON tag `"email"` but the API returns `"emails"` - change the tag. - Do **not** touch the `cmd/` layer unless a flag name or output column must change. 3. Add or update the test in `internal/client/_test.go` to cover the fixed case. -4. Run `/opt/local/bin/go test ./internal/client/...` and verify it passes. -5. Run `/opt/local/bin/go build ./...` to confirm no compilation errors. +4. Run `go test ./internal/client/...` and verify it passes. +5. Run `go build ./...` to confirm no compilation errors. --- ## Acceptance Criteria - [ ] The reproduction command now produces the expected output -- [ ] All existing tests still pass (`/opt/local/bin/go test ./...`) +- [ ] All existing tests still pass (`go test ./...`) - [ ] No unrelated code is refactored - [ ] No new dependencies are introduced - [ ] `go vet ./...` and `golangci-lint run ./...` report no new issues diff --git a/docs/templates/CR-Template.md b/docs/templates/CR-Template.md index e09942c..94aa208 100644 --- a/docs/templates/CR-Template.md +++ b/docs/templates/CR-Template.md @@ -185,9 +185,9 @@ rootCmd.AddCommand(newCmd()) ### Step 5 - Verify ```bash -/opt/local/bin/go build ./... -/opt/local/bin/go test ./... -/opt/local/bin/go vet ./... +go build ./... +go test ./... +go vet ./... golangci-lint run ./... ``` From 867f2f90cd6f07f40de53b4b5cc462e20327aa5b Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Mon, 19 Oct 2026 17:30:15 -0400 Subject: [PATCH 30/52] feat: connection management, connector metadata, project/folder PATCH (CR-0047) Connections (V2, JSON): - update uses POST /connection/; --partial sends Update-Mode: PARTIAL - create/update send the file as-is and add "@type":"connection" - get --name; new test, search, objects and metadata subcommands - list --type/--name filter client-side (the API ignores them) - Connection struct extended with verified fields Connectors (new): connector list and connector metadata, including attributes parsed from the CCI definition (cciMeta). Projects and folders: - project update uses PATCH by --id or --project-name - folder update percent-encodes names, validates flags and updates by ID via /folders/; --project-id is deprecated (documented /projects//folders/ returns 404) - --interactive pickers driven by the objects API with name filter, plus "Projects and folders" menu entries Response shapes verified against a DEV org; CR moved to pending. --- README.md | 3 +- cmd/connection.go | 307 +++++++++++++-- cmd/connector.go | 162 ++++++++ cmd/folder.go | 68 +++- cmd/project.go | 79 +++- cmd/projectfolder_prompt.go | 201 ++++++++++ cmd/root.go | 1 + completions/iics.bash | 352 +++++++++++++++++- ...nnection-connector-project-folder-patch.md | 108 ++++++ docs/documentation/connection.md | 313 +++++++++++++--- docs/documentation/connector.md | 124 ++++++ docs/documentation/folder.md | 46 ++- docs/documentation/menu.md | 1 + docs/documentation/project.md | 34 +- internal/client/client.go | 8 + internal/client/connections.go | 227 +++++++++-- internal/client/connections_test.go | 195 +++++++++- internal/client/connectors.go | 182 +++++++++ internal/client/connectors_test.go | 104 ++++++ internal/client/folders.go | 31 +- internal/client/folders_test.go | 82 ++++ internal/client/projects.go | 32 +- internal/client/projects_test.go | 65 ++++ internal/menu/entries.go | 3 + 24 files changed, 2563 insertions(+), 165 deletions(-) create mode 100644 cmd/connector.go create mode 100644 cmd/projectfolder_prompt.go create mode 100644 docs/ChangeRequests/pending/CR-0047-connection-connector-project-folder-patch.md create mode 100644 docs/documentation/connector.md create mode 100644 internal/client/connectors.go create mode 100644 internal/client/connectors_test.go create mode 100644 internal/client/folders_test.go create mode 100644 internal/client/projects_test.go diff --git a/README.md b/README.md index 06a8b49..028447e 100644 --- a/README.md +++ b/README.md @@ -272,7 +272,8 @@ it easy to override credentials in CI pipelines without touching the config file | [auditlog](docs/documentation/auditlog.md) | | `list` | Query organization audit log (V2 API) | | [completion](docs/documentation/completion.md) | | `bash`, `zsh`, `fish`, `powershell` | Generate shell completion scripts | | [config](docs/documentation/config.md) | | `edit` | Edit global settings (new user defaults, theme, timeout, menu) | -| [connection](docs/documentation/connection.md) | `conn` | `list`, `get`, `create`, `update`, `delete` | Manage connections | +| [connection](docs/documentation/connection.md) | `conn` | `list`, `get`, `create`, `update`, `delete`, `test`, `search`, `objects`, `metadata` | Manage connections | +| [connector](docs/documentation/connector.md) | | `list`, `metadata` | List connectors and their connection attributes | | [export](docs/documentation/export.md) | | `run`, `start`, `status`, `download`, `create` | Export asset packages | | [folder](docs/documentation/folder.md) | | `create`, `update`, `delete` | Manage folders | | [import](docs/documentation/import.md) | `imp` | `run`, `upload`, `start`, `status`, `download-log` | Import asset packages | diff --git a/cmd/connection.go b/cmd/connection.go index d3871e1..49bdfad 100644 --- a/cmd/connection.go +++ b/cmd/connection.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "os" + "strings" "github.com/jbrazda/iics-cli/internal/client" "github.com/jbrazda/iics-cli/internal/output" @@ -23,6 +24,10 @@ func newConnectionCmd() *cobra.Command { cmd.AddCommand(newConnectionCreateCmd()) cmd.AddCommand(newConnectionUpdateCmd()) cmd.AddCommand(newConnectionDeleteCmd()) + cmd.AddCommand(newConnectionTestCmd()) + cmd.AddCommand(newConnectionSearchCmd()) + cmd.AddCommand(newConnectionObjectsCmd()) + cmd.AddCommand(newConnectionMetadataCmd()) return cmd } @@ -63,8 +68,8 @@ func newConnectionListCmd() *cobra.Command { }, } - cmd.Flags().StringVar(&opts.Type, "type", "", "filter by connection type") - cmd.Flags().StringVar(&opts.Name, "name", "", "filter by name") + cmd.Flags().StringVar(&opts.Type, "type", "", "filter by connection type (exact, case-insensitive)") + cmd.Flags().StringVar(&opts.Name, "name", "", "filter by name substring (case-insensitive)") cmd.Flags().IntVar(&opts.Limit, "limit", 200, "max results") cmd.Flags().IntVar(&opts.Skip, "skip", 0, "number of results to skip") @@ -72,16 +77,16 @@ func newConnectionListCmd() *cobra.Command { } func newConnectionGetCmd() *cobra.Command { - var id string + var id, name string cmd := &cobra.Command{ Use: "get", Short: "Get connection details", Example: ` iics connection get --id - iics connection get --id --output json`, + iics connection get --name "Salesforce Prod" --output json`, RunE: func(cmd *cobra.Command, args []string) error { - if id == "" { - return fmt.Errorf("--id is required") + if id == "" && name == "" { + return fmt.Errorf("--id or --name is required") } c, err := getClient(cmd) @@ -89,7 +94,7 @@ func newConnectionGetCmd() *cobra.Command { return err } - conn, err := c.GetConnection(context.Background(), id) + conn, err := getConnectionByIDOrName(context.Background(), c, id, name) if err != nil { return err } @@ -103,6 +108,9 @@ func newConnectionGetCmd() *cobra.Command { {Header: "ID", Field: "id", Width: 24}, {Header: "NAME", Field: "name", Width: 30}, {Header: "TYPE", Field: "type", Width: 15}, + {Header: "HOST", Field: "host", Width: 20}, + {Header: "RUNTIME ENV", Field: "runtimeEnvironmentId", Width: 24}, + {Header: "UPDATED", Field: "updateTime", Width: 20}, {Header: "DESCRIPTION", Field: "description"}, } @@ -110,10 +118,49 @@ func newConnectionGetCmd() *cobra.Command { }, } - cmd.Flags().StringVar(&id, "id", "", "connection ID (required)") + cmd.Flags().StringVar(&id, "id", "", "connection ID") + cmd.Flags().StringVar(&name, "name", "", "connection name") + cmd.MarkFlagsMutuallyExclusive("id", "name") return cmd } +// getConnectionByIDOrName fetches a connection by ID, or by name when id is empty. +func getConnectionByIDOrName(ctx context.Context, c *client.Client, id, name string) (*client.Connection, error) { + if id != "" { + return c.GetConnection(ctx, id) + } + return c.GetConnectionByName(ctx, name) +} + +// resolveConnectionID returns id, or looks up the ID of the named connection. +func resolveConnectionID(ctx context.Context, c *client.Client, id, name string) (string, error) { + if id != "" { + return id, nil + } + if name == "" { + return "", fmt.Errorf("--id or --name is required") + } + conn, err := c.GetConnectionByName(ctx, name) + if err != nil { + return "", err + } + return conn.ID, nil +} + +// readConnectionDef reads a JSON connection definition as a generic map so +// connector-specific attributes are sent unchanged. +func readConnectionDef(path string) (map[string]interface{}, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("reading file %s: %w", path, err) + } + var def map[string]interface{} + if err := json.Unmarshal(data, &def); err != nil { + return nil, fmt.Errorf("parsing connection JSON: %w", err) + } + return def, nil +} + func newConnectionCreateCmd() *cobra.Command { var fromFile string @@ -126,15 +173,9 @@ func newConnectionCreateCmd() *cobra.Command { return fmt.Errorf("--from-file is required") } - data, err := os.ReadFile(fromFile) - if err != nil { - return fmt.Errorf("reading file %s: %w", fromFile, err) - } - - var conn client.Connection - err = json.Unmarshal(data, &conn) + def, err := readConnectionDef(fromFile) if err != nil { - return fmt.Errorf("parsing connection JSON: %w", err) + return err } c, err := getClient(cmd) @@ -142,7 +183,7 @@ func newConnectionCreateCmd() *cobra.Command { return err } - created, err := c.CreateConnection(context.Background(), &conn) + created, err := c.CreateConnection(context.Background(), def) if err != nil { return err } @@ -160,12 +201,20 @@ func newConnectionUpdateCmd() *cobra.Command { var ( id string fromFile string + partial bool ) cmd := &cobra.Command{ - Use: "update", - Short: "Update a connection", - Example: ` iics connection update --id --from-file connection.json`, + Use: "update", + Short: "Update a connection", + Long: `Update a connection from a JSON file. + +Without --partial the definition replaces the connection: every attribute +must be present. With --partial only the attributes in the file change. +"connection get" returns the password masked, so do not send its output back +as a full update; use --partial and leave the password out.`, + Example: ` iics connection update --id --from-file connection.json + iics connection update --id --partial --from-file changes.json`, RunE: func(cmd *cobra.Command, args []string) error { if id == "" { return fmt.Errorf("--id is required") @@ -174,15 +223,9 @@ func newConnectionUpdateCmd() *cobra.Command { return fmt.Errorf("--from-file is required") } - data, err := os.ReadFile(fromFile) + def, err := readConnectionDef(fromFile) if err != nil { - return fmt.Errorf("reading file %s: %w", fromFile, err) - } - - var conn client.Connection - err = json.Unmarshal(data, &conn) - if err != nil { - return fmt.Errorf("parsing connection JSON: %w", err) + return err } c, err := getClient(cmd) @@ -190,7 +233,7 @@ func newConnectionUpdateCmd() *cobra.Command { return err } - updated, err := c.UpdateConnection(context.Background(), id, &conn) + updated, err := c.UpdateConnection(context.Background(), id, def, partial) if err != nil { return err } @@ -202,6 +245,7 @@ func newConnectionUpdateCmd() *cobra.Command { cmd.Flags().StringVar(&id, "id", "", "connection ID (required)") cmd.Flags().StringVar(&fromFile, "from-file", "", "JSON file with connection updates (required)") + cmd.Flags().BoolVar(&partial, "partial", false, "change only the attributes in the file (Update-Mode: PARTIAL)") return cmd } @@ -243,3 +287,208 @@ func newConnectionDeleteCmd() *cobra.Command { cmd.Flags().BoolVarP(&yes, "yes", "y", false, "skip confirmation prompt") return cmd } + +func newConnectionTestCmd() *cobra.Command { + var id, name string + + cmd := &cobra.Command{ + Use: "test", + Short: "Test a connection", + Example: ` iics connection test --id + iics connection test --name "Salesforce Prod"`, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + c, err := getClient(cmd) + if err != nil { + return err + } + + connID, err := resolveConnectionID(ctx, c, id, name) + if err != nil { + return err + } + + res, err := c.TestConnection(ctx, connID) + if err != nil { + return err + } + + if outputFmt != "" && outputFmt != "table" { + f, ferr := getFormatter() + if ferr != nil { + return ferr + } + return f.Format(res, nil) + } + _, _ = fmt.Fprintln(cmd.OutOrStdout(), res.Description) + return nil + }, + } + + cmd.Flags().StringVar(&id, "id", "", "connection ID") + cmd.Flags().StringVar(&name, "name", "", "connection name") + cmd.MarkFlagsMutuallyExclusive("id", "name") + return cmd +} + +func newConnectionSearchCmd() *cobra.Command { + var opts client.ConnectionSearchOptions + + cmd := &cobra.Command{ + Use: "search", + Short: "List connections of a type available to a runtime environment or agent", + Example: ` iics connection search --type Salesforce --runtime-id + iics connection search --type TOOLKIT --agent-id --output json`, + RunE: func(cmd *cobra.Command, args []string) error { + if opts.UIType == "" { + return fmt.Errorf("--type is required") + } + if opts.RuntimeEnvironmentID == "" && opts.AgentID == "" { + return fmt.Errorf("--runtime-id or --agent-id is required") + } + + c, err := getClient(cmd) + if err != nil { + return err + } + + conns, err := c.SearchConnections(context.Background(), opts) + if err != nil { + return err + } + + f, err := getFormatter() + if err != nil { + return err + } + + columns := []output.Column{ + {Header: "ID", Field: "id", Width: 24, Priority: 5, Shrink: output.ShrinkNever}, + {Header: "NAME", Field: "name", Width: 30, Priority: 1}, + {Header: "TYPE", Field: "type", Width: 15, Priority: 2, Shrink: output.ShrinkNever}, + {Header: "RUNTIME ENV", Field: "runtimeEnvironmentId", Width: 24, Priority: 4, Shrink: output.ShrinkNever}, + {Header: "UPDATED", Field: "updateTime", Width: 20, Priority: 3, Shrink: output.ShrinkNever}, + } + + return f.Format(conns, columns) + }, + } + + cmd.Flags().StringVar(&opts.UIType, "type", "", "connection type, e.g. Salesforce, CSVFile, TOOLKIT (required)") + cmd.Flags().StringVar(&opts.RuntimeEnvironmentID, "runtime-id", "", "runtime environment ID (takes precedence over --agent-id)") + cmd.Flags().StringVar(&opts.AgentID, "agent-id", "", "Secure Agent ID") + return cmd +} + +func newConnectionObjectsCmd() *cobra.Command { + var opts client.ConnectionObjectsOptions + + cmd := &cobra.Command{ + Use: "objects", + Short: "List the source or target objects of a connection", + Long: `List the source objects (or target objects with --target) available +through a connection, such as tables or Salesforce objects. The API returns +at most 200 objects; use --search-pattern to narrow the list.`, + Example: ` iics connection objects --id + iics connection objects --name "Salesforce Prod" --target --search-pattern Account`, + RunE: func(cmd *cobra.Command, args []string) error { + if opts.ID == "" && opts.Name == "" { + return fmt.Errorf("--id or --name is required") + } + + c, err := getClient(cmd) + if err != nil { + return err + } + + items, err := c.ListConnectionObjects(context.Background(), opts) + if err != nil { + return err + } + + f, err := getFormatter() + if err != nil { + return err + } + + columns := []output.Column{ + {Header: "ID", Field: "id", Width: 6}, + {Header: "NAME", Field: "name"}, + } + + return f.Format(items, columns) + }, + } + + cmd.Flags().StringVar(&opts.ID, "id", "", "connection ID") + cmd.Flags().StringVar(&opts.Name, "name", "", "connection name") + cmd.Flags().BoolVar(&opts.Target, "target", false, "list target objects instead of source objects") + cmd.Flags().StringVar(&opts.SearchPattern, "search-pattern", "", "case-insensitive substring filter on object names") + cmd.MarkFlagsMutuallyExclusive("id", "name") + return cmd +} + +func newConnectionMetadataCmd() *cobra.Command { + var ( + id, name string + target bool + ) + + cmd := &cobra.Command{ + Use: "metadata", + Short: "Show the source or target runtime attributes of a connection", + Example: ` iics connection metadata --id + iics connection metadata --name "Salesforce Prod" --target`, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + c, err := getClient(cmd) + if err != nil { + return err + } + + connID, err := resolveConnectionID(ctx, c, id, name) + if err != nil { + return err + } + + attrs, err := c.GetConnectionMetadata(ctx, connID, target) + if err != nil { + return err + } + + f, err := getFormatter() + if err != nil { + return err + } + + columns := []output.Column{ + {Header: "NAME", Field: "name", Width: 30, Priority: 1}, + {Header: "DATA TYPE", Field: "dataType", Width: 10, Priority: 2, Shrink: output.ShrinkNever}, + {Header: "MANDATORY", Field: "mandatory", Width: 9, Priority: 3, Shrink: output.ShrinkNever}, + {Header: "DEFAULT", Field: "defaultValue", Width: 15, Priority: 2}, + {Header: "VALUES", Field: "possibleValues", MaxWidth: 60, Priority: 4, Shrink: output.ShrinkWrap, Func: joinedListFunc("possibleValues")}, + } + + return f.Format(attrs, columns) + }, + } + + cmd.Flags().StringVar(&id, "id", "", "connection ID") + cmd.Flags().StringVar(&name, "name", "", "connection name") + cmd.Flags().BoolVar(&target, "target", false, "show target attributes instead of source attributes") + cmd.MarkFlagsMutuallyExclusive("id", "name") + return cmd +} + +// joinedListFunc renders a list field of a table row as comma-separated text. +func joinedListFunc(field string) func(row interface{}) string { + return func(row interface{}) string { + m, _ := row.(map[string]interface{}) + items, _ := m[field].([]interface{}) + parts := make([]string, 0, len(items)) + for _, it := range items { + parts = append(parts, fmt.Sprint(it)) + } + return strings.Join(parts, ", ") + } +} diff --git a/cmd/connector.go b/cmd/connector.go new file mode 100644 index 0000000..5ef69df --- /dev/null +++ b/cmd/connector.go @@ -0,0 +1,162 @@ +package cmd + +import ( + "context" + "fmt" + "strings" + + "github.com/jbrazda/iics-cli/internal/client" + "github.com/jbrazda/iics-cli/internal/output" + "github.com/spf13/cobra" +) + +func newConnectorCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "connector", + Short: "List connectors and their connection attributes", + } + + cmd.AddCommand(newConnectorListCmd()) + cmd.AddCommand(newConnectorMetadataCmd()) + return cmd +} + +func newConnectorListCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List the connectors available to the organization", + Example: ` iics connector list + iics connector list --output json`, + RunE: func(cmd *cobra.Command, args []string) error { + c, err := getClient(cmd) + if err != nil { + return err + } + + connectors, err := c.ListConnectors(context.Background()) + if err != nil { + return err + } + + f, err := getFormatter() + if err != nil { + return err + } + + columns := []output.Column{ + {Header: "NAME", Field: "name", Width: 30, Priority: 1}, + {Header: "SHORT NAME", Field: "shortName", Width: 20, Priority: 3}, + {Header: "SDK", Field: "sdkType", Width: 5, Priority: 2, Shrink: output.ShrinkNever}, + {Header: "VERSION", Field: "connectorVersion", Width: 10, Priority: 4, Shrink: output.ShrinkNever}, + {Header: "PUBLIC", Field: "isPublic", Width: 6, Priority: 5, Shrink: output.ShrinkNever}, + {Header: "UPDATED", Field: "updateTime", Width: 20, Priority: 4, Shrink: output.ShrinkNever}, + } + + return f.Format(connectors, columns) + }, + } + return cmd +} + +func newConnectorMetadataCmd() *cobra.Command { + var ( + name string + exact bool + ) + + cmd := &cobra.Command{ + Use: "metadata", + Short: "Show the connection attributes of a connector", + Long: `Show the connection attributes of a connector, the fields a connection of +that type takes. + +--name is the connector name from "connector list" (not the short name). +The API matches it as a prefix, so "Oracle" also returns "Oracle Autonomous +Database". When one connector matches (or --exact selects one), its attributes +are listed; otherwise the matching connectors are summarized. + +For CCI connectors the attributes are read from the connector's CCI definition.`, + Example: ` iics connector metadata --name SqlServer + iics connector metadata --name Oracle --exact + iics connector metadata --name "Amazon S3 v2" --output json`, + RunE: func(cmd *cobra.Command, args []string) error { + if name == "" { + return fmt.Errorf("--name is required") + } + + c, err := getClient(cmd) + if err != nil { + return err + } + + metas, err := c.GetConnectorMetadata(context.Background(), name) + if err != nil { + return err + } + if exact { + var matched []client.ConnectorMetadata + for _, m := range metas { + if strings.EqualFold(m.Type, name) { + matched = append(matched, m) + } + } + metas = matched + } + if len(metas) == 0 { + return fmt.Errorf("no connector named %q", name) + } + + f, err := getFormatter() + if err != nil { + return err + } + + if outputFmt != "" && outputFmt != "table" { + return f.Format(metas, nil) + } + + if len(metas) > 1 { + type summary struct { + Type string `json:"type"` + Standard bool `json:"isStandardConnType"` + CCI bool `json:"cci"` + Attributes int `json:"attributes"` + } + rows := make([]summary, 0, len(metas)) + for _, m := range metas { + fields, ferr := m.Fields() + if ferr != nil { + return ferr + } + rows = append(rows, summary{Type: m.Type, Standard: m.IsStandardConnType, CCI: m.CCIMeta != "", Attributes: len(fields)}) + } + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%d connectors match %q; use the full name with --exact to list attributes.\n", len(metas), name) + return f.Format(rows, []output.Column{ + {Header: "CONNECTOR", Field: "type", Width: 40}, + {Header: "STANDARD", Field: "isStandardConnType", Width: 8}, + {Header: "CCI", Field: "cci", Width: 5}, + {Header: "ATTRIBUTES", Field: "attributes", Width: 10}, + }) + } + + fields, err := metas[0].Fields() + if err != nil { + return err + } + columns := []output.Column{ + {Header: "NAME", Field: "name", Width: 30, Priority: 1}, + {Header: "LABEL", Field: "label", Width: 25, Priority: 4}, + {Header: "DATA TYPE", Field: "dataType", Width: 10, Priority: 2, Shrink: output.ShrinkNever}, + {Header: "MANDATORY", Field: "mandatory", Width: 9, Priority: 2, Shrink: output.ShrinkNever}, + {Header: "ENCRYPTED", Field: "encrypted", Width: 9, Priority: 5, Shrink: output.ShrinkNever}, + {Header: "DEFAULT", Field: "defaultValue", Width: 15, Priority: 3}, + {Header: "VALUES", Field: "values", MaxWidth: 60, Priority: 4, Shrink: output.ShrinkWrap}, + } + return f.Format(fields, columns) + }, + } + + cmd.Flags().StringVar(&name, "name", "", "connector name, e.g. SqlServer, Salesforce, \"Amazon S3 v2\" (required)") + cmd.Flags().BoolVar(&exact, "exact", false, "only the connector whose name matches exactly (case-insensitive)") + return cmd +} diff --git a/cmd/folder.go b/cmd/folder.go index e58e4ee..f398d52 100644 --- a/cmd/folder.go +++ b/cmd/folder.go @@ -4,6 +4,8 @@ import ( "context" "fmt" + "github.com/jbrazda/iics-cli/internal/client" + "github.com/jbrazda/iics-cli/internal/config" "github.com/spf13/cobra" ) @@ -68,26 +70,78 @@ func newFolderUpdateCmd() *cobra.Command { projectID string projectName string folderName string + interactive bool ) cmd := &cobra.Command{ Use: "update", - Short: "Update a folder", + Short: "Update a folder's name or description", + Long: `Change the name and/or description of a folder (PATCH). + +Identify the folder by ID (--id, works for folders in any project) or by name +(--project-name and --folder-name); IDs and names cannot be mixed. With --interactive (or with no identifier on a terminal) pick +a project, then one of its folders, filtered by name, and edit the folder's +name and description. A description cannot be cleared.`, Example: ` iics folder update --id --name "New Name" - iics folder update --id --project-id --name "New Name" - iics folder update --project-name "Orders" --folder-name "Northeast Orders" --name "Northeast Requests"`, + iics folder update --project-name "Orders" --folder-name "Northeast Orders" --name "Northeast Requests" + iics folder update -i`, RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + noTarget := id == "" && projectID == "" && projectName == "" && folderName == "" + if noTarget && !interactive && config.IsTerminal() { + interactive = true + } + c, err := getClient(cmd) if err != nil { return err } - updated, err := c.UpdateFolder(context.Background(), id, name, description, projectID, projectName, folderName) + if interactive { + if !config.IsTerminal() { + return fmt.Errorf("--interactive requires a terminal") + } + var current *client.Object + switch { + case noTarget: + project, folder, perr := pickFolder(ctx, c) + if perr != nil { + return perr + } + if folder == nil { + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Canceled.") + return nil + } + current = folder + projectName, folderName = project.Path, objectName(*folder) + case projectName != "" && folderName != "": + folders, lerr := listFolders(ctx, c, projectName) + if lerr != nil { + return lerr + } + current = findObject(folders, "", folderName) + } + curName, curDesc := folderName, "" + if current != nil { + curName, curDesc = objectName(*current), current.Description + } + target := fmt.Sprintf("%q", firstNonEmpty(curName, id)) + if projectName != "" { + target = fmt.Sprintf("%q in project %q", curName, projectName) + } + edit, ok, perr := promptProjectFolderEdit("folder", target, curName, curDesc, name, description) + if perr != nil || !ok { + return perr + } + name, description = edit.Name, edit.Description + } + + updated, err := c.UpdateFolder(ctx, id, name, description, projectID, projectName, folderName) if err != nil { return err } - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Folder updated: %s (ID: %s)\n", updated.Name, updated.ID) + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Folder updated: %s\n", firstNonEmpty(updated.Name, name, folderName, id)) return nil }, } @@ -95,9 +149,11 @@ func newFolderUpdateCmd() *cobra.Command { cmd.Flags().StringVar(&id, "id", "", "folder ID") cmd.Flags().StringVar(&name, "name", "", "new folder name") cmd.Flags().StringVar(&description, "description", "", "new folder description") - cmd.Flags().StringVar(&projectID, "project-id", "", "parent project ID") + cmd.Flags().StringVar(&projectID, "project-id", "", "parent project ID (not needed; --id identifies the folder)") + _ = cmd.Flags().MarkDeprecated("project-id", "folder IDs are unique in the org; --id alone identifies the folder") cmd.Flags().StringVar(&projectName, "project-name", "", "parent project name (use with --folder-name)") cmd.Flags().StringVar(&folderName, "folder-name", "", "current folder name (use with --project-name)") + cmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "pick the folder and edit it interactively") return cmd } diff --git a/cmd/project.go b/cmd/project.go index 553a07e..60e2b14 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/jbrazda/iics-cli/internal/client" + "github.com/jbrazda/iics-cli/internal/config" "github.com/spf13/cobra" ) @@ -59,16 +60,31 @@ func newProjectCreateCmd() *cobra.Command { func newProjectUpdateCmd() *cobra.Command { var ( id string + projectName string name string description string + interactive bool ) cmd := &cobra.Command{ Use: "update", - Short: "Update a project", + Short: "Update a project's name or description", + Long: `Change the name and/or description of a project (PATCH). + +Identify the project with --id or its current name (--project-name). With +--interactive (or with neither flag on a terminal) pick the project from a +list, filtered by name, then edit its name and description. A description +cannot be cleared.`, + Example: ` iics project update --id --name "Finance Data Engineering" + iics project update --project-name "Orders" --description "Orders from all regions" + iics project update -i`, RunE: func(cmd *cobra.Command, args []string) error { - if id == "" { - return fmt.Errorf("--id is required") + ctx := context.Background() + if id == "" && projectName == "" && !interactive { + if !config.IsTerminal() { + return fmt.Errorf("--id or --project-name is required") + } + interactive = true } c, err := getClient(cmd) @@ -76,23 +92,68 @@ func newProjectUpdateCmd() *cobra.Command { return err } - project := &client.Project{Name: name, Description: description} - updated, err := c.UpdateProject(context.Background(), id, project) + if interactive { + if !config.IsTerminal() { + return fmt.Errorf("--interactive requires a terminal") + } + var current *client.Object + if id == "" && projectName == "" { + current, err = pickProject(ctx, c) + if err != nil { + return err + } + if current == nil { + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Canceled.") + return nil + } + projectName = current.Path + } else { + projects, lerr := listProjects(ctx, c) + if lerr != nil { + return lerr + } + current = findObject(projects, id, projectName) + } + curName, curDesc := projectName, "" + if current != nil { + curName, curDesc = current.Path, current.Description + } + edit, ok, perr := promptProjectFolderEdit("project", fmt.Sprintf("%q", firstNonEmpty(curName, id)), curName, curDesc, name, description) + if perr != nil || !ok { + return perr + } + name, description = edit.Name, edit.Description + } + + updated, err := c.UpdateProject(ctx, id, projectName, name, description) if err != nil { return err } - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Project updated: %s (ID: %s)\n", updated.Name, updated.ID) + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Project updated: %s\n", firstNonEmpty(updated.Name, name, projectName, id)) return nil }, } - cmd.Flags().StringVar(&id, "id", "", "project ID (required)") - cmd.Flags().StringVar(&name, "name", "", "project name") - cmd.Flags().StringVar(&description, "description", "", "project description") + cmd.Flags().StringVar(&id, "id", "", "project ID") + cmd.Flags().StringVar(&projectName, "project-name", "", "current project name") + cmd.Flags().StringVar(&name, "name", "", "new project name") + cmd.Flags().StringVar(&description, "description", "", "new project description") + cmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "pick the project and edit it interactively") + cmd.MarkFlagsMutuallyExclusive("id", "project-name") return cmd } +// firstNonEmpty returns the first non-empty string. +func firstNonEmpty(vals ...string) string { + for _, v := range vals { + if v != "" { + return v + } + } + return "" +} + func newProjectDeleteCmd() *cobra.Command { var ( id string diff --git a/cmd/projectfolder_prompt.go b/cmd/projectfolder_prompt.go new file mode 100644 index 0000000..8c1ad51 --- /dev/null +++ b/cmd/projectfolder_prompt.go @@ -0,0 +1,201 @@ +package cmd + +import ( + "context" + "errors" + "fmt" + "os" + "strings" + + "github.com/jbrazda/iics-cli/internal/client" + "github.com/jbrazda/iics-cli/internal/config" + "github.com/jbrazda/iics-cli/internal/tui" +) + +// pickFilterThreshold is the list size above which the pickers first ask for +// a name filter. +const pickFilterThreshold = 10 + +// listProjects returns all projects from the objects API. +func listProjects(ctx context.Context, c *client.Client) ([]client.Object, error) { + res, err := c.ListAllObjects(ctx, client.ObjectsListOptions{Type: "Project"}, nil) + if err != nil { + return nil, err + } + return sortedObjects(res.Objects), nil +} + +// listFolders returns the folders of a project from the objects API. +func listFolders(ctx context.Context, c *client.Client, projectName string) ([]client.Object, error) { + res, err := c.ListAllObjects(ctx, client.ObjectsListOptions{Query: fmt.Sprintf("location=='%s'", projectName)}, nil) + if err != nil { + return nil, err + } + var folders []client.Object + for _, o := range sortedObjects(res.Objects) { + if o.Type == "Folder" { + folders = append(folders, o) + } + } + return folders, nil +} + +// objectName returns the last path segment of an object path. +func objectName(o client.Object) string { + return o.Path[strings.LastIndex(o.Path, "/")+1:] +} + +// pickByName lets the operator choose one object. Long lists first ask for a +// case-insensitive name filter (empty lists everything). Returns nil when +// canceled. +func pickByName(kind string, objs []client.Object) (*client.Object, error) { + if len(objs) == 0 { + return nil, fmt.Errorf("no %ss found", kind) + } + for { + matches := objs + filter := "" + if len(objs) > pickFilterThreshold { + text, err := promptText(fmt.Sprintf("Filter %ss by name (empty to list all %d)", kind, len(objs)), "") + if err != nil { + return nil, cancelAsNil(err) + } + filter = strings.TrimSpace(text) + if filter != "" { + matches = nil + needle := strings.ToLower(filter) + for _, o := range objs { + if strings.Contains(strings.ToLower(objectName(o)), needle) { + matches = append(matches, o) + } + } + } + } + switch len(matches) { + case 0: + _, _ = fmt.Fprintf(os.Stderr, "No %ss match %q.\n", kind, filter) + continue + case 1: + if filter != "" { + return &matches[0], nil + } + } + idx, err := tui.PickOne(prompter, fmt.Sprintf("Select a %s", kind), matches, objectName) + if err != nil { + return nil, cancelAsNil(err) + } + if idx < 0 { + return nil, nil + } + return &matches[idx], nil + } +} + +// cancelAsNil turns a prompt cancellation into a nil error. +func cancelAsNil(err error) error { + if errors.Is(err, tui.ErrCanceled) { + return nil + } + return err +} + +// pickProject lists the projects and lets the operator choose one. +// Returns nil when canceled. +func pickProject(ctx context.Context, c *client.Client) (*client.Object, error) { + projects, err := listProjects(ctx, c) + if err != nil { + return nil, err + } + return pickByName("project", projects) +} + +// pickFolder lets the operator choose a project, then one of its folders. +// Returns nil objects when canceled. +func pickFolder(ctx context.Context, c *client.Client) (project, folder *client.Object, err error) { + project, err = pickProject(ctx, c) + if err != nil || project == nil { + return nil, nil, err + } + folders, err := listFolders(ctx, c, project.Path) + if err != nil { + return nil, nil, err + } + if len(folders) == 0 { + return nil, nil, fmt.Errorf("project %q has no folders", project.Path) + } + folder, err = pickByName("folder", folders) + if err != nil || folder == nil { + return nil, nil, err + } + return project, folder, nil +} + +// findObject returns the object in objs whose ID is id, or whose name is +// name; nil when not found. +func findObject(objs []client.Object, id, name string) *client.Object { + for i, o := range objs { + if (id != "" && o.ID == id) || (name != "" && strings.EqualFold(objectName(o), name)) { + return &objs[i] + } + } + return nil +} + +// projectFolderEdit holds the values entered in the interactive editor. +// Only fields that differ from the current values are set. +type projectFolderEdit struct { + Name string + Description string +} + +// promptProjectFolderEdit asks for a new name and description, pre-filled +// with the current values (flag values win when given). ok is false when +// nothing changed or the operator declined. +func promptProjectFolderEdit(kind, target, curName, curDesc, flagName, flagDesc string) (edit projectFolderEdit, ok bool, err error) { + if !config.IsTerminal() { + return edit, false, fmt.Errorf("--interactive requires a terminal") + } + defName, defDesc := curName, curDesc + if flagName != "" { + defName = flagName + } + if flagDesc != "" { + defDesc = flagDesc + } + newName, err := promptText(fmt.Sprintf("New %s name", kind), defName) + if err != nil { + return edit, false, cancelAsNil(err) + } + newDesc, err := promptText("Description (cannot be cleared)", defDesc) + if err != nil { + return edit, false, cancelAsNil(err) + } + newName, newDesc = strings.TrimSpace(newName), strings.TrimSpace(newDesc) + if newName != "" && newName != curName { + edit.Name = newName + } + if newDesc != "" && newDesc != curDesc { + edit.Description = newDesc + } + if edit.Name == "" && edit.Description == "" { + _, _ = fmt.Fprintln(os.Stderr, "Nothing to update.") + return edit, false, nil + } + + var changes []string + if edit.Name != "" { + changes = append(changes, fmt.Sprintf("name %q -> %q", curName, edit.Name)) + } + if edit.Description != "" { + changes = append(changes, fmt.Sprintf("description -> %q", edit.Description)) + } + yes, err := promptYesNo(fmt.Sprintf("Update %s %s: %s?", kind, target, strings.Join(changes, ", ")), true) + if err != nil { + return edit, false, cancelAsNil(err) + } + if !yes { + _, _ = fmt.Fprintln(os.Stderr, "Canceled.") + return edit, false, nil + } + return edit, true, nil +} diff --git a/cmd/root.go b/cmd/root.go index 07e1029..b5fdae7 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -206,6 +206,7 @@ func init() { rootCmd.AddCommand(newObjectsCmd()) rootCmd.AddCommand(newLookupCmd()) rootCmd.AddCommand(newConnectionCmd()) + rootCmd.AddCommand(newConnectorCmd()) rootCmd.AddCommand(newExportCmd()) rootCmd.AddCommand(newImportCmd()) rootCmd.AddCommand(newScheduleCmd()) diff --git a/completions/iics.bash b/completions/iics.bash index aeffbe2..4005036 100644 --- a/completions/iics.bash +++ b/completions/iics.bash @@ -1563,6 +1563,10 @@ _iics_connection_get() two_word_flags+=("--id") local_nonpersistent_flags+=("--id") local_nonpersistent_flags+=("--id=") + flags+=("--name=") + two_word_flags+=("--name") + local_nonpersistent_flags+=("--name") + local_nonpersistent_flags+=("--name=") flags+=("--config=") two_word_flags+=("--config") flags+=("--debug") @@ -1643,6 +1647,206 @@ _iics_connection_list() noun_aliases=() } +_iics_connection_metadata() +{ + last_command="iics_connection_metadata" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--id=") + two_word_flags+=("--id") + local_nonpersistent_flags+=("--id") + local_nonpersistent_flags+=("--id=") + flags+=("--name=") + two_word_flags+=("--name") + local_nonpersistent_flags+=("--name") + local_nonpersistent_flags+=("--name=") + flags+=("--target") + local_nonpersistent_flags+=("--target") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--http-timeout=") + two_word_flags+=("--http-timeout") + flags+=("--no-color") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + flags+=("--profile=") + two_word_flags+=("--profile") + two_word_flags+=("-p") + flags+=("--theme=") + two_word_flags+=("--theme") + flags+=("--verbose") + flags+=("-v") + flags+=("--wide") + flags+=("--width=") + two_word_flags+=("--width") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_iics_connection_objects() +{ + last_command="iics_connection_objects" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--id=") + two_word_flags+=("--id") + local_nonpersistent_flags+=("--id") + local_nonpersistent_flags+=("--id=") + flags+=("--name=") + two_word_flags+=("--name") + local_nonpersistent_flags+=("--name") + local_nonpersistent_flags+=("--name=") + flags+=("--search-pattern=") + two_word_flags+=("--search-pattern") + local_nonpersistent_flags+=("--search-pattern") + local_nonpersistent_flags+=("--search-pattern=") + flags+=("--target") + local_nonpersistent_flags+=("--target") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--http-timeout=") + two_word_flags+=("--http-timeout") + flags+=("--no-color") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + flags+=("--profile=") + two_word_flags+=("--profile") + two_word_flags+=("-p") + flags+=("--theme=") + two_word_flags+=("--theme") + flags+=("--verbose") + flags+=("-v") + flags+=("--wide") + flags+=("--width=") + two_word_flags+=("--width") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_iics_connection_search() +{ + last_command="iics_connection_search" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--agent-id=") + two_word_flags+=("--agent-id") + local_nonpersistent_flags+=("--agent-id") + local_nonpersistent_flags+=("--agent-id=") + flags+=("--runtime-id=") + two_word_flags+=("--runtime-id") + local_nonpersistent_flags+=("--runtime-id") + local_nonpersistent_flags+=("--runtime-id=") + flags+=("--type=") + two_word_flags+=("--type") + local_nonpersistent_flags+=("--type") + local_nonpersistent_flags+=("--type=") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--http-timeout=") + two_word_flags+=("--http-timeout") + flags+=("--no-color") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + flags+=("--profile=") + two_word_flags+=("--profile") + two_word_flags+=("-p") + flags+=("--theme=") + two_word_flags+=("--theme") + flags+=("--verbose") + flags+=("-v") + flags+=("--wide") + flags+=("--width=") + two_word_flags+=("--width") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_iics_connection_test() +{ + last_command="iics_connection_test" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--id=") + two_word_flags+=("--id") + local_nonpersistent_flags+=("--id") + local_nonpersistent_flags+=("--id=") + flags+=("--name=") + two_word_flags+=("--name") + local_nonpersistent_flags+=("--name") + local_nonpersistent_flags+=("--name=") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--http-timeout=") + two_word_flags+=("--http-timeout") + flags+=("--no-color") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + flags+=("--profile=") + two_word_flags+=("--profile") + two_word_flags+=("-p") + flags+=("--theme=") + two_word_flags+=("--theme") + flags+=("--verbose") + flags+=("-v") + flags+=("--wide") + flags+=("--width=") + two_word_flags+=("--width") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _iics_connection_update() { last_command="iics_connection_update" @@ -1665,6 +1869,8 @@ _iics_connection_update() two_word_flags+=("--id") local_nonpersistent_flags+=("--id") local_nonpersistent_flags+=("--id=") + flags+=("--partial") + local_nonpersistent_flags+=("--partial") flags+=("--config=") two_word_flags+=("--config") flags+=("--debug") @@ -1701,6 +1907,10 @@ _iics_connection() commands+=("delete") commands+=("get") commands+=("list") + commands+=("metadata") + commands+=("objects") + commands+=("search") + commands+=("test") commands+=("update") flags=() @@ -1734,6 +1944,131 @@ _iics_connection() noun_aliases=() } +_iics_connector_list() +{ + last_command="iics_connector_list" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--http-timeout=") + two_word_flags+=("--http-timeout") + flags+=("--no-color") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + flags+=("--profile=") + two_word_flags+=("--profile") + two_word_flags+=("-p") + flags+=("--theme=") + two_word_flags+=("--theme") + flags+=("--verbose") + flags+=("-v") + flags+=("--wide") + flags+=("--width=") + two_word_flags+=("--width") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_iics_connector_metadata() +{ + last_command="iics_connector_metadata" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--exact") + local_nonpersistent_flags+=("--exact") + flags+=("--name=") + two_word_flags+=("--name") + local_nonpersistent_flags+=("--name") + local_nonpersistent_flags+=("--name=") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--http-timeout=") + two_word_flags+=("--http-timeout") + flags+=("--no-color") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + flags+=("--profile=") + two_word_flags+=("--profile") + two_word_flags+=("-p") + flags+=("--theme=") + two_word_flags+=("--theme") + flags+=("--verbose") + flags+=("-v") + flags+=("--wide") + flags+=("--width=") + two_word_flags+=("--width") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_iics_connector() +{ + last_command="iics_connector" + + command_aliases=() + + commands=() + commands+=("list") + commands+=("metadata") + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--http-timeout=") + two_word_flags+=("--http-timeout") + flags+=("--no-color") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + flags+=("--profile=") + two_word_flags+=("--profile") + two_word_flags+=("-p") + flags+=("--theme=") + two_word_flags+=("--theme") + flags+=("--verbose") + flags+=("-v") + flags+=("--wide") + flags+=("--width=") + two_word_flags+=("--width") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _iics_environment_configs_get() { last_command="iics_environment_configs_get" @@ -2618,14 +2953,14 @@ _iics_folder_update() two_word_flags+=("--id") local_nonpersistent_flags+=("--id") local_nonpersistent_flags+=("--id=") + flags+=("--interactive") + flags+=("-i") + local_nonpersistent_flags+=("--interactive") + local_nonpersistent_flags+=("-i") flags+=("--name=") two_word_flags+=("--name") local_nonpersistent_flags+=("--name") local_nonpersistent_flags+=("--name=") - flags+=("--project-id=") - two_word_flags+=("--project-id") - local_nonpersistent_flags+=("--project-id") - local_nonpersistent_flags+=("--project-id=") flags+=("--project-name=") two_word_flags+=("--project-name") local_nonpersistent_flags+=("--project-name") @@ -5138,10 +5473,18 @@ _iics_project_update() two_word_flags+=("--id") local_nonpersistent_flags+=("--id") local_nonpersistent_flags+=("--id=") + flags+=("--interactive") + flags+=("-i") + local_nonpersistent_flags+=("--interactive") + local_nonpersistent_flags+=("-i") flags+=("--name=") two_word_flags+=("--name") local_nonpersistent_flags+=("--name") local_nonpersistent_flags+=("--name=") + flags+=("--project-name=") + two_word_flags+=("--project-name") + local_nonpersistent_flags+=("--project-name") + local_nonpersistent_flags+=("--project-name=") flags+=("--config=") two_word_flags+=("--config") flags+=("--debug") @@ -7686,6 +8029,7 @@ _iics_root_command() command_aliases+=("conn") aliashash["conn"]="connection" fi + commands+=("connector") commands+=("environment") if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then command_aliases+=("env") diff --git a/docs/ChangeRequests/pending/CR-0047-connection-connector-project-folder-patch.md b/docs/ChangeRequests/pending/CR-0047-connection-connector-project-folder-patch.md new file mode 100644 index 0000000..95f7442 --- /dev/null +++ b/docs/ChangeRequests/pending/CR-0047-connection-connector-project-folder-patch.md @@ -0,0 +1,108 @@ +# CR-0047: Connection management, connector metadata, project and folder PATCH + +## CR Type + +- [x] New resource (`connector`) +- [x] New subcommand (`connection test`, `search`, `objects`, `metadata`) +- [x] Enhancement (`connection get`, `connection update`, `project update`, `folder update`, menu) + +## Problem + +- `connection update` sends PUT; the V2 API updates a connection with + `POST /api/v2/connection/`. JSON bodies lack the V2 `@type` + discriminator (same cause as BUG-0015). +- The `Connection` struct misses most documented fields, and there is no way + to test a connection, list its source/target objects, read runtime + attribute metadata, or search connections by runtime environment. +- The V2 `connector` resource (available connectors and their attribute + metadata) is not covered. +- `project update` sends PUT; the V3 API uses PATCH. Folder update already + uses PATCH, but name-based URIs are not percent-encoded and an empty + success body prints an empty name and ID. +- There is no interactive way to rename a project or folder or change its + description. + +References: + +- [Connections (V2)](https://developer.informatica.com/tpn/v2.0/reference/connectivity) +- [Connectors (V2)](https://developer.informatica.com/tpn/v2.0/reference/connector) +- [Updating a project](https://docs.informatica.com/cloud-common-services/administrator/current-version/rest-api-reference/platform-rest-api-version-3-resources/projects-and-folders/updating-a-project.html) +- [Updating a folder](https://docs.informatica.com/cloud-common-services/administrator/current-version/rest-api-reference/platform-rest-api-version-3-resources/projects-and-folders/updating-a-folder.html) + +## JSON vs XML + +The TPN pages only show `application/xml`. Live calls against a DEV org on +2026-10-19 with `Accept: application/json` returned +`application/json;charset=UTF-8` for every endpoint below, so the CLI uses +JSON throughout. Findings that differ from the TPN docs: + +- `GET /connector` returns extra fields: `orgId`, `description`, + `updateTime`, `pluginId`, `guid`, `sdkType`, `displayName`. `type` and + `publisher` are often absent. +- `GET /connector/metadata?connectorName=` takes the connector + **name** (for example `SqlServer`, `Amazon S3 v2`), matches by prefix and + returns an **array** of `connectorMetadata`. `attributes[].list` is an + array of strings, not a string. CCI connectors return a single + `agentGroupId` attribute plus `cciMeta`, a JSON-encoded string that holds + the real attribute definitions. +- `GET /connection/test/` returns `{"@type":"success","description":"..."}`. +- `GET /connection/{source|target}/` returns `connListItem` objects; + `searchPattern` is a case-insensitive substring match. +- `GET /connection/{source|target}//metadata` returns + `runtimeAttribute` objects (`maxLength` int, `mandatory` and + `sessionVarAllowed` bool, `possibleValues` string array, extra + `uniqueName`). Connectors without runtime attributes return + `403 APP_13405 The connection has no object.` +- `GET /connection` ignores the `type` and `name` query parameters; the + filters are applied client-side. + +## Project and folder PATCH findings + +Verified against the DEV org on 2026-10-19: + +- PATCH by name and by ID returns `204 No Content`; the CLI reports the + identifier it used. +- `PATCH /public/core/v3/projects//folders/` returns + `404 No static resource`, although it is documented. `/folders/` + works for folders in any project, so `folder update --project-id` is + deprecated and ignored. +- Object IDs from the objects API equal project and folder IDs. + +## Desired Change + +### connection + +- `get --id | --name`. +- `update --partial` sends `Update-Mode: PARTIAL`; update uses POST. +- create and update set `"@type":"connection"`. +- `test --id | --name`. +- `search --type --runtime-id | --agent-id`. +- `objects --id | --name [--target] [--search-pattern]`. +- `metadata --id | --name [--target]`. +- `list --type` and `--name` filter client-side. + +### connector (new) + +- `list`. +- `metadata --name [--exact]`: one match shows its + attributes (CCI attributes parsed from `cciMeta`); several matches show a + summary. + +### project and folder + +- `project update` uses PATCH by `--id` or by current `--project-name`. +- `folder update` percent-encodes names, validates flag combinations and + updates by ID through `/folders/`. +- Both accept `--interactive/-i`: pick a project (and folder) from the + objects API with a name filter, edit name and description, confirm. +- Menu group "Projects and folders" with "Edit project" and "Edit folder". + +## Verification + +- `go test ./...` covers each new client method. +- Live smoke test against the DEV org for each command: connector list and + metadata, connection get/test/objects/metadata/search/list filters, + connection create and partial update, project and folder PATCH by name + and ID. Throwaway objects were deleted afterwards. +- Not run live: the interactive pickers (`-i`, menu entries) need a manual + check in a terminal. diff --git a/docs/documentation/connection.md b/docs/documentation/connection.md index df45a70..00579c0 100644 --- a/docs/documentation/connection.md +++ b/docs/documentation/connection.md @@ -2,6 +2,9 @@ Manage IICS connections (data sources and targets). Alias: `conn`. +Connections use the V2 API (`/api/v2/connection`). The CLI sends and receives JSON; +JSON request bodies carry `"@type": "connection"`, which the CLI adds when missing. + ## Synopsis ```bash @@ -11,38 +14,47 @@ iics conn [flags] ## Subcommands -| Subcommand | Description | -| ---------- | ----------------------------- | -| `list` | List connections | -| `get` | Get a single connection | -| `create` | Create a new connection | -| `update` | Update an existing connection | -| `delete` | Delete a connection | +| Subcommand | Description | +| ---------- | ---------------------------------------------------------------------- | +| `list` | List connections | +| `get` | Get a single connection by ID or name | +| `create` | Create a new connection | +| `update` | Update an existing connection (full or partial) | +| `delete` | Delete a connection | +| `test` | Test a connection | +| `search` | List connections of a type available to a runtime environment or agent | +| `objects` | List the source or target objects of a connection | +| `metadata` | Show the source or target runtime attributes of a connection | + +See also [connector](connector.md) for the attributes each connection type takes. --- ## connection list +The API returns every connection in the organization; `--type` and `--name` are +applied by the CLI. + ### Flags -| Flag | Type | Default | Description | -| --------- | ------ | ------- | ------------------------------ | -| `--type` | string | | Filter by connection type | -| `--name` | string | | Filter by connection name | -| `--limit` | int | 200 | Max results | -| `--skip` | int | 0 | Results to skip | +| Flag | Type | Default | Description | +| --------- | ------ | ------- | -------------------------------------------------- | +| `--type` | string | | Filter by connection type (exact, ignores case) | +| `--name` | string | | Filter by connection name substring (ignores case) | +| `--limit` | int | 200 | Max results | +| `--skip` | int | 0 | Results to skip | All [global flags](../../README.md#global-flags) apply. ### Output columns -| Column | Description | -| ----------- | ------------------------ | -| `id` | Connection ID | -| `name` | Connection name | -| `type` | Connection type | -| `updatedBy` | Last modifier | -| `updateTime`| Last modification time | +| Column | Description | +| ------------ | ---------------------- | +| `id` | Connection ID | +| `name` | Connection name | +| `type` | Connection type | +| `updatedBy` | Last modifier | +| `updateTime` | Last modification time | ### Examples @@ -85,37 +97,56 @@ $conns | ForEach-Object { $_.id } ## connection get -Get the full details of a single connection. +Get the full details of a single connection. Use `--output json` to see every +attribute, including connector-specific `connParams`. The password is returned +masked (`********`). ### Flags -| Flag | Type | Required | Description | -| ------ | ------ | -------- | --------------- | -| `--id` | string | yes | Connection ID | +| Flag | Type | Required | Description | +| -------- | ------ | -------- | --------------- | +| `--id` | string | one of | Connection ID | +| `--name` | string | one of | Connection name | All [global flags](../../README.md#global-flags) apply. +### Output columns + +| Column | Description | +| ---------------------- | ----------------------- | +| `id` | Connection ID | +| `name` | Connection name | +| `type` | Connection type | +| `host` | Host name | +| `runtimeEnvironmentId` | Runtime environment ID | +| `updateTime` | Last modification time | +| `description` | Description | + ### Examples ```bash iics connection get --id abc123 -# View as JSON to see all fields -iics connection get --id abc123 --output json +# By name, as JSON to see all fields +iics connection get --name "Salesforce Prod" --output json ``` ```powershell iics connection get --id abc123 -# View as JSON to see all fields -iics connection get --id abc123 --output json +# By name, as JSON to see all fields +iics connection get --name "Salesforce Prod" --output json ``` --- ## connection create -Create a new connection from a JSON definition file. +Create a new connection from a JSON definition file. The file is sent as-is +(plus `@type`), so any connector-specific attribute can be included. Attributes +that are not top-level connection fields go in `connParams`. Use +[`connector metadata`](connector.md#connector-metadata) to list the attributes +of a connection type. ### Flags @@ -129,16 +160,21 @@ All [global flags](../../README.md#global-flags) apply. ```json { - "name": "My Snowflake Prod", - "type": "Snowflake", - "description": "Production Snowflake connection", + "@type": "connection", + "name": "Actgdata Dev", + "type": "SqlServer2019", + "description": "Accounting database in dev", "runtimeEnvironmentId": "", - "connectionParams": { - "account": "myorg.snowflakecomputing.com", - "database": "PROD_DB", - "warehouse": "COMPUTE_WH", - "username": "etl_user", - "password": "" + "host": "devsql01", + "port": 1433, + "database": "Actgdata", + "schema": "dbo", + "authenticationType": "SqlServer", + "username": "etl_user", + "password": "", + "codepage": "MS1252", + "connParams": { + "encryptionMethod": "None" } } ``` @@ -146,36 +182,55 @@ All [global flags](../../README.md#global-flags) apply. ### Examples ```bash -iics connection create --from-file snowflake_prod.json +iics connection create --from-file actgdata_dev.json ``` ```powershell -iics connection create --from-file snowflake_prod.json +iics connection create --from-file actgdata_dev.json ``` --- ## connection update -Update an existing connection from a JSON file. Only fields present in the file are changed. +Update an existing connection from a JSON file (`POST /api/v2/connection/`). + +- Without `--partial` the file replaces the connection definition, so it must + contain every attribute. +- With `--partial` (header `Update-Mode: PARTIAL`) only the attributes in the + file change. + +`connection get` returns the password masked. Do not send its output back as a +full update; use `--partial` and leave `password` out unless you are changing it. ### Flags -| Flag | Type | Required | Description | -| ------------- | ------ | -------- | ------------------------------------------- | -| `--id` | string | yes | Connection ID to update | -| `--from-file` | string | yes | Path to a JSON file with the updated fields | +| Flag | Type | Required | Description | +| ------------- | ------ | -------- | ----------------------------------------------------- | +| `--id` | string | yes | Connection ID to update | +| `--from-file` | string | yes | Path to a JSON file with the connection attributes | +| `--partial` | bool | | Change only the attributes in the file | All [global flags](../../README.md#global-flags) apply. ### Examples ```bash -iics connection update --id abc123 --from-file snowflake_prod_updated.json +# Change only the description +echo '{"description": "Accounting database (dev)"}' > desc.json +iics connection update --id abc123 --partial --from-file desc.json + +# Full replacement +iics connection update --id abc123 --from-file actgdata_dev.json ``` ```powershell -iics connection update --id abc123 --from-file snowflake_prod_updated.json +# Change only the description +'{"description": "Accounting database (dev)"}' | Set-Content desc.json +iics connection update --id abc123 --partial --from-file desc.json + +# Full replacement +iics connection update --id abc123 --from-file actgdata_dev.json ``` --- @@ -186,10 +241,10 @@ Delete a connection. Prompts for confirmation unless `--yes` is provided. ### Flags -| Flag | Short | Type | Required | Description | -| ---------- | ----- | ------ | -------- | -------------------------- | -| `--id` | | string | yes | Connection ID to delete | -| `--yes` | `-y` | bool | | Skip confirmation prompt | +| Flag | Short | Type | Required | Description | +| ------- | ----- | ------ | -------- | ------------------------ | +| `--id` | | string | yes | Connection ID to delete | +| `--yes` | `-y` | bool | | Skip confirmation prompt | All [global flags](../../README.md#global-flags) apply. @@ -203,7 +258,163 @@ iics connection delete --id abc123 iics connection delete --id abc123 --yes ``` +```powershell +# Interactive - will prompt "Are you sure?" +iics connection delete --id abc123 + +# Non-interactive (CI/CD) +iics connection delete --id abc123 --yes +``` + +--- + +## connection test + +Test a connection from its runtime environment. Prints the API's success +message; a failed test is returned as an error (exit code 1). + +### Flags + +| Flag | Type | Required | Description | +| -------- | ------ | -------- | --------------- | +| `--id` | string | one of | Connection ID | +| `--name` | string | one of | Connection name | + +All [global flags](../../README.md#global-flags) apply. + +### Examples + +```bash +iics connection test --name "Salesforce Prod" + +# In CI/CD: fail the step when the connection does not work +iics connection test --id abc123 || exit 1 +``` + +```powershell +iics connection test --name "Salesforce Prod" + +# In CI/CD: fail the step when the connection does not work +iics connection test --id abc123 +if ($LASTEXITCODE -ne 0) { exit 1 } +``` + +--- + +## connection search + +List the connections of a type that a runtime environment (or Secure Agent) can use. + +### Flags + +| Flag | Type | Required | Description | +| -------------- | ------ | -------- | --------------------------------------------------------------- | +| `--type` | string | yes | Connection type, for example `Salesforce`, `CSVFile`, `TOOLKIT` | +| `--runtime-id` | string | one of | Runtime environment ID (takes precedence over `--agent-id`) | +| `--agent-id` | string | one of | Secure Agent ID | + +All [global flags](../../README.md#global-flags) apply. + +### Output columns + +| Column | Description | +| ---------------------- | ---------------------- | +| `id` | Connection ID | +| `name` | Connection name | +| `type` | Connection type | +| `runtimeEnvironmentId` | Runtime environment ID | +| `updateTime` | Last modification time | + +### Examples + +```bash +iics connection search --type Salesforce --runtime-id +``` + +```powershell +iics connection search --type Salesforce --runtime-id +``` + +--- + +## connection objects + +List the source objects (or target objects with `--target`) available through a +connection, such as database tables or Salesforce objects. The API returns at +most 200 objects; use `--search-pattern` to narrow the list. + +### Flags + +| Flag | Type | Required | Description | +| ------------------ | ------ | -------- | ----------------------------------------------- | +| `--id` | string | one of | Connection ID | +| `--name` | string | one of | Connection name | +| `--target` | bool | | List target objects instead of source objects | +| `--search-pattern` | string | | Substring filter on object names (ignores case) | + +All [global flags](../../README.md#global-flags) apply. + +### Output columns + +| Column | Description | +| ------ | --------------------------------------------- | +| `id` | Position of the object in the result | +| `name` | Object name | + +### Examples + +```bash +iics connection objects --name "Actgdata Dev" --search-pattern billing + +iics connection objects --id abc123 --target --output json | jq -r '.[].name' +``` + +```powershell +iics connection objects --name "Actgdata Dev" --search-pattern billing + +(iics connection objects --id abc123 --target --output json | ConvertFrom-Json).name +``` + +--- + +## connection metadata + +Show the source (or target with `--target`) runtime attributes of a connection, +such as Salesforce batch size or upsert options. Connection types without +runtime attributes return the API error `APP_13405 The connection has no object.` + +### Flags + +| Flag | Type | Required | Description | +| ---------- | ------ | -------- | --------------------------------------------------- | +| `--id` | string | one of | Connection ID | +| `--name` | string | one of | Connection name | +| `--target` | bool | | Show target attributes instead of source attributes | + +All [global flags](../../README.md#global-flags) apply. + +### Output columns + +| Column | Description | +| ---------------- | ------------------------------------ | +| `name` | Attribute name | +| `dataType` | `STRING`, `NUMBER` or `BOOLEAN` | +| `mandatory` | Whether the attribute is required | +| `defaultValue` | Default value | +| `possibleValues` | Allowed values, when restricted | + +### Examples + +```bash +iics connection metadata --name "Salesforce Prod" --target +``` + +```powershell +iics connection metadata --name "Salesforce Prod" --target +``` + ## See also +- [connector](connector.md) - list connectors and the attributes a connection type takes - [lookup](lookup.md) - resolve connection paths to IDs - [objects](objects.md) - list assets that use a connection diff --git a/docs/documentation/connector.md b/docs/documentation/connector.md new file mode 100644 index 0000000..d408d36 --- /dev/null +++ b/docs/documentation/connector.md @@ -0,0 +1,124 @@ +# connector + +List the connectors available to the organization and the connection attributes +each connector takes. Use it to build the JSON definition for +[`connection create`](connection.md#connection-create). + +Connectors use the V2 API (`/api/v2/connector`) with JSON. + +## Synopsis + +```bash +iics connector [flags] +``` + +## Subcommands + +| Subcommand | Description | +| ---------- | --------------------------------------------- | +| `list` | List the connectors available to the org | +| `metadata` | Show the connection attributes of a connector | + +--- + +## connector list + +### Flags + +No command-specific flags. All [global flags](../../README.md#global-flags) apply. + +### Output columns + +| Column | Description | +| ------------------ | ---------------------------------------------- | +| `name` | Connector name (use with `connector metadata`) | +| `shortName` | Connector short name | +| `sdkType` | Connector SDK, for example `CTK` or `CCI` | +| `connectorVersion` | Connector version | +| `isPublic` | Whether the connector is public | +| `updateTime` | Last update time | + +`--output json` also returns `displayName`, `description`, `publisher`, `type`, +`pluginId`, `guid` and `orgId` when the API provides them. + +### Examples + +```bash +iics connector list + +# Names of all CCI connectors +iics connector list --output json | jq -r '.[] | select(.sdkType == "CCI") | .name' +``` + +```powershell +iics connector list + +# Names of all CCI connectors +(iics connector list --output json | ConvertFrom-Json) | Where-Object sdkType -eq 'CCI' | ForEach-Object name +``` + +--- + +## connector metadata + +Show the connection attributes of a connector. + +`--name` is the connector name from `connector list` (for example `SqlServer`, +`Salesforce`, `Amazon S3 v2`), not the short name. The API matches it as a +prefix, so `Oracle` also returns `Oracle Autonomous Database` and others: + +- One match (or one exact match with `--exact`): the attributes are listed. +- Several matches: a summary of the matching connectors is shown; repeat with + the full name and `--exact`. + +CCI connectors describe their attributes in a CCI definition (`cciMeta` in the +JSON output); the table lists the attributes from that definition, including +their labels and whether they are encrypted. + +### Flags + +| Flag | Type | Required | Description | +| --------- | ------ | -------- | ----------------------------------------------------------------- | +| `--name` | string | yes | Connector name | +| `--exact` | bool | | Keep only the connector whose name matches exactly (ignores case) | + +All [global flags](../../README.md#global-flags) apply. + +### Output columns + +| Column | Description | +| -------------- | ----------------------------------------------------------------------------- | +| `name` | Attribute name | +| `label` | Display label (CCI connectors) | +| `dataType` | Numeric connector data type code, or the CCI type such as `STRING`, `BOOLEAN` | +| `mandatory` | Whether the attribute is required | +| `encrypted` | Whether the attribute is stored encrypted (CCI connectors) | +| `defaultValue` | Default value | +| `values` | Allowed values, when restricted | + +`--output json` returns the raw API response: an array of objects with `type`, +`isStandardConnType`, `attributes` and, for CCI connectors, `cciMeta`. + +### Examples + +```bash +iics connector metadata --name SqlServer + +# Several connectors start with "Oracle"; pick the exact one +iics connector metadata --name Oracle --exact + +iics connector metadata --name "Amazon S3 v2" --output json +``` + +```powershell +iics connector metadata --name SqlServer + +# Several connectors start with "Oracle"; pick the exact one +iics connector metadata --name Oracle --exact + +iics connector metadata --name "Amazon S3 v2" --output json +``` + +## See also + +- [connection](connection.md) - create, update and test connections diff --git a/docs/documentation/folder.md b/docs/documentation/folder.md index 3e1bdd7..6efe4c9 100644 --- a/docs/documentation/folder.md +++ b/docs/documentation/folder.md @@ -61,16 +61,40 @@ iics folder create --name "Staging" --project-name "Finance ETL" ` ## folder update +Change the name and/or description of a folder (`PATCH`). Identify the folder +in one of these ways; IDs and names cannot be mixed: + +| Identify by | Flags | URI | +| ----------- | ------------------------------------ | ------------------------------------------------ | +| ID | `--id` | `/folders/` | +| Names | `--project-name` and `--folder-name` | `/projects/name//folders/name/` | + +Folder IDs are unique in the organization, so `--id` works for folders in any +project. `--project-id` is deprecated and ignored: the documented +`/projects//folders/` URI returns HTTP 404. + +Names are percent-encoded, so names with spaces work as typed. A description +cannot be cleared. + +With `--interactive` (or with no identifier when run in a terminal), pick a +project, then one of its folders. Lists with more than 10 entries first ask for +a name filter (substring, ignores case; empty lists all). Then edit the name and +description, pre-filled with the current values, and confirm. The +[menu](menu.md) entry "Edit folder" runs this flow. + ### Flags -| Flag | Type | Required | Description | -| ---------------- | ------ | -------- | ----------------------------------------------------------- | -| `--id` | string | | Folder ID | -| `--name` | string | | New folder name | -| `--description` | string | | New description | -| `--project-id` | string | | Parent project ID | -| `--project-name` | string | | Parent project name | -| `--folder-name` | string | | Current folder name (use with `--project-name` to identify) | +| Flag | Short | Type | Required | Description | +| ---------------- | ----- | ------ | -------- | ----------------------------------------------- | +| `--id` | | string | | Folder ID | +| `--project-id` | | string | | Deprecated and ignored; `--id` is enough | +| `--project-name` | | string | | Parent project name (use with `--folder-name`) | +| `--folder-name` | | string | | Current folder name (use with `--project-name`) | +| `--name` | | string | | New folder name | +| `--description` | | string | | New description | +| `--interactive` | `-i` | bool | | Pick the folder and edit it interactively | + +Without `--interactive`, at least one of `--name` and `--description` is required. All [global flags](../../README.md#global-flags) apply. @@ -83,6 +107,9 @@ iics folder update --id --name "Production" # Update by project and folder name iics folder update --project-name "Finance ETL" --folder-name "Staging" \ --name "Prod" --description "Production-ready pipelines" + +# Pick the project and folder, then edit +iics folder update -i ``` ```powershell @@ -92,6 +119,9 @@ iics folder update --id --name "Production" # Update by project and folder name iics folder update --project-name "Finance ETL" --folder-name "Staging" ` --name "Prod" --description "Production-ready pipelines" + +# Pick the project and folder, then edit +iics folder update -i ``` --- diff --git a/docs/documentation/menu.md b/docs/documentation/menu.md index d5fee49..f500d92 100644 --- a/docs/documentation/menu.md +++ b/docs/documentation/menu.md @@ -121,6 +121,7 @@ On a production profile: | User groups | List, Show, Create ✎, Edit roles ✎, Delete ✎ | | Roles | List, Show (with privileges), Create ✎, Edit privileges ✎ | | Privileges | List privileges | +| Projects and folders | Edit project ✎, Edit folder ✎ | | Permissions | Edit object permissions ✎ | | Environments and agents | List runtime environments, Create runtime environment ✎, List agents, Start / Stop / Restart agent service ✎, Show agent installer info, Download agent installer (`--interactive --progress`) | | Help | Show CLI help | diff --git a/docs/documentation/project.md b/docs/documentation/project.md index ace5b4b..4abfba0 100644 --- a/docs/documentation/project.md +++ b/docs/documentation/project.md @@ -47,13 +47,27 @@ iics project create --name "Finance ETL" --description "Finance team data pipeli ## project update +Change the name and/or description of a project (`PATCH /public/core/v3/projects/` +or `/projects/name/`). Identify the project by `--id` or by its current +name with `--project-name`. A description cannot be cleared. + +With `--interactive` (or with no identifier when run in a terminal), pick the +project from a list. When there are more than 10 projects you are first asked +for a name filter (substring, ignores case; empty lists all). Then edit the +name and description, pre-filled with the current values, and confirm. +The [menu](menu.md) entry "Edit project" runs this flow. + ### Flags -| Flag | Type | Required | Description | -| --------------- | ------ | -------- | ------------------ | -| `--id` | string | yes | Project ID | -| `--name` | string | | New project name | -| `--description` | string | | New description | +| Flag | Short | Type | Required | Description | +| ---------------- | ----- | ------ | -------- | ------------------------------------------ | +| `--id` | | string | one of | Project ID | +| `--project-name` | | string | one of | Current project name | +| `--name` | | string | | New project name | +| `--description` | | string | | New description | +| `--interactive` | `-i` | bool | | Pick the project and edit it interactively | + +Without `--interactive`, at least one of `--name` and `--description` is required. All [global flags](../../README.md#global-flags) apply. @@ -62,13 +76,19 @@ All [global flags](../../README.md#global-flags) apply. ```bash iics project update --id --name "Finance Data Engineering" -iics project update --id --description "Updated description" +iics project update --project-name "Finance ETL" --description "Finance team data pipelines" + +# Pick the project and edit it +iics project update -i ``` ```powershell iics project update --id --name "Finance Data Engineering" -iics project update --id --description "Updated description" +iics project update --project-name "Finance ETL" --description "Finance team data pipelines" + +# Pick the project and edit it +iics project update -i ``` --- diff --git a/internal/client/client.go b/internal/client/client.go index 6032a88..f78ba01 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -272,6 +272,11 @@ func debugPrintHTTP(req *http.Request, reqData []byte, resp *http.Response, resp // doJSON is a convenience method that sends a JSON request and decodes the response. func (c *Client) doJSON(ctx context.Context, method, path string, reqBody, respBody interface{}) error { + return c.doJSONWithHeaders(ctx, method, path, nil, reqBody, respBody) +} + +// doJSONWithHeaders is like doJSON but sets extra request headers. +func (c *Client) doJSONWithHeaders(ctx context.Context, method, path string, headers map[string]string, reqBody, respBody interface{}) error { if err := c.ensureSession(ctx); err != nil { return err } @@ -292,6 +297,9 @@ func (c *Client) doJSON(ctx context.Context, method, path string, reqBody, respB if err != nil { return fmt.Errorf("creating request: %w", err) } + for k, v := range headers { + req.Header.Set(k, v) + } resp, err := c.do(ctx, req) if err != nil { diff --git a/internal/client/connections.go b/internal/client/connections.go index 2c72258..43eb13f 100644 --- a/internal/client/connections.go +++ b/internal/client/connections.go @@ -6,25 +6,54 @@ import ( "net/http" "net/url" "strconv" + "strings" ) -// Connection represents an IICS connection. +// connectionAtType is the V2 type discriminator for connection request bodies. +const connectionAtType = "connection" + +// Connection represents an IICS connection (V2 API). Connector-specific +// attributes not listed here are returned in ConnParams. type Connection struct { - ID string `json:"id,omitempty"` - OrgID string `json:"orgId,omitempty"` - Name string `json:"name"` - Description string `json:"description,omitempty"` - Type string `json:"type"` - CreateTime string `json:"createTime,omitempty"` - UpdateTime string `json:"updateTime,omitempty"` - CreatedBy string `json:"createdBy,omitempty"` - UpdatedBy string `json:"updatedBy,omitempty"` - AgentID string `json:"agentId,omitempty"` - RuntimeEnvID string `json:"runtimeEnvironmentId,omitempty"` - ConnParams map[string]interface{} `json:"connParams,omitempty"` -} - -// ConnectionListOptions holds query parameters for listing connections. + AtType string `json:"@type,omitempty"` + ID string `json:"id,omitempty"` + OrgID string `json:"orgId,omitempty"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + ShortDescription string `json:"shortDescription,omitempty"` + Type string `json:"type"` + BaseType string `json:"baseType,omitempty"` + SubType string `json:"subType,omitempty"` + InstanceDisplayName string `json:"instanceDisplayName,omitempty"` + CreateTime string `json:"createTime,omitempty"` + UpdateTime string `json:"updateTime,omitempty"` + MajorUpdateTime string `json:"majorUpdateTime,omitempty"` + CreatedBy string `json:"createdBy,omitempty"` + UpdatedBy string `json:"updatedBy,omitempty"` + AgentID string `json:"agentId,omitempty"` + RuntimeEnvID string `json:"runtimeEnvironmentId,omitempty"` + FederatedID string `json:"federatedId,omitempty"` + ConnectorStatus string `json:"connectorStatus,omitempty"` + Host string `json:"host,omitempty"` + Port int `json:"port,omitempty"` + Domain string `json:"domain,omitempty"` + Database string `json:"database,omitempty"` + Schema string `json:"schema,omitempty"` + InstanceName string `json:"instanceName,omitempty"` + Codepage string `json:"codepage,omitempty"` + AuthenticationType string `json:"authenticationType,omitempty"` + AdjustedJdbcHostName string `json:"adjustedJdbcHostName,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + SecurityToken string `json:"securityToken,omitempty"` + ServiceURL string `json:"serviceUrl,omitempty"` + StsURL string `json:"stsUrl,omitempty"` + Timeout int `json:"timeout,omitempty"` + ConnParams map[string]interface{} `json:"connParams,omitempty"` +} + +// ConnectionListOptions holds filters for listing connections. The API +// ignores type and name, so they are applied client-side. type ConnectionListOptions struct { Type string Name string @@ -33,15 +62,10 @@ type ConnectionListOptions struct { } // ListConnections retrieves connections with optional filtering. +// Type matches exactly (case-insensitive); Name matches a case-insensitive substring. func (c *Client) ListConnections(ctx context.Context, opts ConnectionListOptions) ([]Connection, error) { query := make(map[string]string) - if opts.Type != "" { - query["type"] = opts.Type - } - if opts.Name != "" { - query["name"] = opts.Name - } if opts.Limit > 0 { query["limit"] = strconv.Itoa(opts.Limit) } @@ -53,7 +77,21 @@ func (c *Client) ListConnections(ctx context.Context, opts ConnectionListOptions if err := c.doJSONWithQuery(ctx, http.MethodGet, fmt.Sprintf("%s/connection", BaseAPIPathV2), query, nil, &resp); err != nil { return nil, err } - return resp, nil + if opts.Type == "" && opts.Name == "" { + return resp, nil + } + name := strings.ToLower(opts.Name) + filtered := make([]Connection, 0, len(resp)) + for _, conn := range resp { + if opts.Type != "" && !strings.EqualFold(conn.Type, opts.Type) { + continue + } + if name != "" && !strings.Contains(strings.ToLower(conn.Name), name) { + continue + } + filtered = append(filtered, conn) + } + return filtered, nil } // GetConnection retrieves a single connection by ID. @@ -78,25 +116,156 @@ func (c *Client) GetConnectionByName(ctx context.Context, name string) (*Connect return &resp, nil } -// CreateConnection creates a new connection. -func (c *Client) CreateConnection(ctx context.Context, conn *Connection) (*Connection, error) { +// CreateConnection creates a new connection from a raw JSON definition, so +// connector-specific attributes are sent unchanged. "@type" is set to +// "connection" when missing. +func (c *Client) CreateConnection(ctx context.Context, def map[string]interface{}) (*Connection, error) { + setConnectionAtType(def) var resp Connection - if err := c.doJSON(ctx, http.MethodPost, fmt.Sprintf("%s/connection", BaseAPIPathV2), conn, &resp); err != nil { + if err := c.doJSON(ctx, http.MethodPost, fmt.Sprintf("%s/connection", BaseAPIPathV2), def, &resp); err != nil { return nil, err } return &resp, nil } -// UpdateConnection updates an existing connection. -func (c *Client) UpdateConnection(ctx context.Context, id string, conn *Connection) (*Connection, error) { +// UpdateConnection updates an existing connection with POST /connection/. +// A full update replaces all attributes; partial sends "Update-Mode: PARTIAL" +// so only the attributes in def change. +func (c *Client) UpdateConnection(ctx context.Context, id string, def map[string]interface{}, partial bool) (*Connection, error) { + setConnectionAtType(def) + var headers map[string]string + if partial { + headers = map[string]string{"Update-Mode": "PARTIAL"} + } var resp Connection - if err := c.doJSON(ctx, http.MethodPut, fmt.Sprintf("%s/connection/%s", BaseAPIPathV2, id), conn, &resp); err != nil { + if err := c.doJSONWithHeaders(ctx, http.MethodPost, fmt.Sprintf("%s/connection/%s", BaseAPIPathV2, id), headers, def, &resp); err != nil { return nil, err } return &resp, nil } +func setConnectionAtType(def map[string]interface{}) { + if _, ok := def["@type"]; !ok { + def["@type"] = connectionAtType + } +} + // DeleteConnection deletes a connection by ID. func (c *Client) DeleteConnection(ctx context.Context, id string) error { return c.doJSON(ctx, http.MethodDelete, fmt.Sprintf("%s/connection/%s", BaseAPIPathV2, id), nil, nil) } + +// ConnectionTestResult is the success object returned by a connection test. +type ConnectionTestResult struct { + AtType string `json:"@type,omitempty"` + Description string `json:"description,omitempty"` +} + +// TestConnection tests a connection. A failed test is returned as an *APIError. +func (c *Client) TestConnection(ctx context.Context, id string) (*ConnectionTestResult, error) { + var resp ConnectionTestResult + if err := c.doJSON(ctx, http.MethodGet, fmt.Sprintf("%s/connection/test/%s", BaseAPIPathV2, id), nil, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +// ConnectionSearchOptions holds query parameters for searching connections. +// UIType is required. RuntimeEnvironmentID takes precedence over AgentID. +type ConnectionSearchOptions struct { + UIType string + AgentID string + RuntimeEnvironmentID string +} + +// SearchConnections lists the connections of a type available to a Secure +// Agent or runtime environment. +func (c *Client) SearchConnections(ctx context.Context, opts ConnectionSearchOptions) ([]Connection, error) { + if opts.UIType == "" { + return nil, fmt.Errorf("connection type is required") + } + query := map[string]string{"uiType": opts.UIType} + if opts.AgentID != "" { + query["agentId"] = opts.AgentID + } + if opts.RuntimeEnvironmentID != "" { + query["runtimeEnvironmentId"] = opts.RuntimeEnvironmentID + } + var resp []Connection + if err := c.doJSONWithQuery(ctx, http.MethodGet, fmt.Sprintf("%s/connection/search", BaseAPIPathV2), query, nil, &resp); err != nil { + return nil, err + } + return resp, nil +} + +// ConnListItem is a source or target object available through a connection. +type ConnListItem struct { + AtType string `json:"@type,omitempty"` + ID string `json:"id"` + Name string `json:"name"` +} + +// ConnectionObjectsOptions selects the connection and the objects to list. +// Exactly one of ID and Name is used (ID wins). SearchPattern is a +// case-insensitive substring filter applied by the API. +type ConnectionObjectsOptions struct { + ID string + Name string + Target bool + SearchPattern string +} + +// ListConnectionObjects lists the source (or target) objects of a connection. +// The API returns at most 200 objects per request. +func (c *Client) ListConnectionObjects(ctx context.Context, opts ConnectionObjectsOptions) ([]ConnListItem, error) { + path := fmt.Sprintf("%s/connection/%s/", BaseAPIPathV2, sourceOrTarget(opts.Target)) + switch { + case opts.ID != "": + path += opts.ID + case opts.Name != "": + path += "name/" + url.PathEscape(opts.Name) + default: + return nil, fmt.Errorf("connection ID or name is required") + } + query := map[string]string{} + if opts.SearchPattern != "" { + query["searchPattern"] = opts.SearchPattern + } + var resp []ConnListItem + if err := c.doJSONWithQuery(ctx, http.MethodGet, path, query, nil, &resp); err != nil { + return nil, err + } + return resp, nil +} + +// RuntimeAttribute is a source or target runtime attribute of a connection. +type RuntimeAttribute struct { + AtType string `json:"@type,omitempty"` + Name string `json:"name"` + UniqueName string `json:"uniqueName,omitempty"` + DataType string `json:"dataType"` + DefaultValue string `json:"defaultValue"` + Label string `json:"label"` + Mandatory bool `json:"mandatory"` + MaxLength int `json:"maxLength"` + SessionVarAllowed bool `json:"sessionVarAllowed"` + PossibleValues []string `json:"possibleValues"` +} + +// GetConnectionMetadata returns the source (or target) runtime attributes of +// a connection. Connectors without runtime attributes return an APIError +// (APP_13405 "The connection has no object."). +func (c *Client) GetConnectionMetadata(ctx context.Context, id string, target bool) ([]RuntimeAttribute, error) { + var resp []RuntimeAttribute + if err := c.doJSON(ctx, http.MethodGet, fmt.Sprintf("%s/connection/%s/%s/metadata", BaseAPIPathV2, sourceOrTarget(target), id), nil, &resp); err != nil { + return nil, err + } + return resp, nil +} + +func sourceOrTarget(target bool) string { + if target { + return "target" + } + return "source" +} diff --git a/internal/client/connections_test.go b/internal/client/connections_test.go index bcb675b..06ee89c 100644 --- a/internal/client/connections_test.go +++ b/internal/client/connections_test.go @@ -10,13 +10,14 @@ import ( func TestListConnections(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Query().Get("type") != "TOOLKIT" { - t.Errorf("expected type=TOOLKIT, got %s", r.URL.Query().Get("type")) + if r.URL.Query().Has("type") { + t.Errorf("type is filtered client-side and should not be sent") } conns := []Connection{ {ID: "c1", Name: "My Conn", Type: "TOOLKIT"}, {ID: "c2", Name: "Other Conn", Type: "TOOLKIT"}, + {ID: "c3", Name: "File Conn", Type: "CSVFile"}, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(conns) @@ -83,27 +84,203 @@ func TestCreateConnection(t *testing.T) { if r.Method != http.MethodPost { t.Errorf("expected POST, got %s", r.Method) } + if r.URL.Path != "/api/v2/connection" { + t.Errorf("unexpected path: %s", r.URL.Path) + } - var conn Connection - json.NewDecoder(r.Body).Decode(&conn) - if conn.Name != "New Conn" { - t.Errorf("expected name 'New Conn', got %s", conn.Name) + var body map[string]interface{} + _ = json.NewDecoder(r.Body).Decode(&body) + if body["@type"] != "connection" { + t.Errorf("expected @type connection, got %v", body["@type"]) + } + if body["name"] != "New Conn" { + t.Errorf("expected name 'New Conn', got %v", body["name"]) + } + if body["customAttr"] != "kept" { + t.Errorf("expected connector-specific attribute to be sent, got %v", body["customAttr"]) } - conn.ID = "new123" w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(conn) + _, _ = w.Write([]byte(`{"@type":"connection","id":"new123","name":"New Conn","type":"JDBC","port":1433}`)) }) c := newTestClient(handler) - conn, err := c.CreateConnection(context.Background(), &Connection{Name: "New Conn", Type: "JDBC"}) + conn, err := c.CreateConnection(context.Background(), map[string]interface{}{"name": "New Conn", "type": "JDBC", "customAttr": "kept"}) if err != nil { t.Fatalf("CreateConnection() error: %v", err) } if conn.ID != "new123" { t.Errorf("expected ID new123, got %s", conn.ID) } + if conn.Port != 1433 { + t.Errorf("expected port 1433, got %d", conn.Port) + } +} + +func TestUpdateConnection(t *testing.T) { + for _, partial := range []bool{false, true} { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Errorf("expected POST, got %s", r.Method) + } + if r.URL.Path != "/api/v2/connection/conn123" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + mode := r.Header.Get("Update-Mode") + if partial && mode != "PARTIAL" { + t.Errorf("expected Update-Mode PARTIAL, got %q", mode) + } + if !partial && mode != "" { + t.Errorf("expected no Update-Mode header, got %q", mode) + } + var body map[string]interface{} + _ = json.NewDecoder(r.Body).Decode(&body) + if body["@type"] != "connection" { + t.Errorf("expected @type connection, got %v", body["@type"]) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"id":"conn123","name":"C","type":"JDBC"}`)) + }) + + c := newTestClient(handler) + conn, err := c.UpdateConnection(context.Background(), "conn123", map[string]interface{}{"description": "d"}, partial) + if err != nil { + t.Fatalf("UpdateConnection(partial=%v) error: %v", partial, err) + } + if conn.ID != "conn123" { + t.Errorf("expected ID conn123, got %s", conn.ID) + } + } +} + +func TestListConnectionsClientSideFilter(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`[ + {"id":"1","name":"Sales Prod","type":"Salesforce"}, + {"id":"2","name":"Sales Dev","type":"Salesforce"}, + {"id":"3","name":"Sales DB","type":"SqlServer2019"}]`)) + }) + + c := newTestClient(handler) + conns, err := c.ListConnections(context.Background(), ConnectionListOptions{Type: "salesforce", Name: "prod"}) + if err != nil { + t.Fatalf("ListConnections() error: %v", err) + } + if len(conns) != 1 || conns[0].ID != "1" { + t.Errorf("expected only connection 1, got %+v", conns) + } +} + +func TestTestConnection(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/api/v2/connection/test/conn123" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + if r.Header.Get("icSessionId") != "test-session" { + t.Errorf("expected V2 session header") + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"@type":"success","description":"The test for this connection was successful."}`)) + }) + + c := newTestClient(handler) + res, err := c.TestConnection(context.Background(), "conn123") + if err != nil { + t.Fatalf("TestConnection() error: %v", err) + } + if res.AtType != "success" || res.Description == "" { + t.Errorf("unexpected result: %+v", res) + } +} + +func TestSearchConnections(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v2/connection/search" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + q := r.URL.Query() + if q.Get("uiType") != "Salesforce" || q.Get("runtimeEnvironmentId") != "rt1" { + t.Errorf("unexpected query: %s", r.URL.RawQuery) + } + if q.Has("agentId") { + t.Errorf("agentId should not be sent when empty") + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`[{"id":"1","name":"SF","type":"Salesforce"}]`)) + }) + + c := newTestClient(handler) + conns, err := c.SearchConnections(context.Background(), ConnectionSearchOptions{UIType: "Salesforce", RuntimeEnvironmentID: "rt1"}) + if err != nil { + t.Fatalf("SearchConnections() error: %v", err) + } + if len(conns) != 1 { + t.Errorf("expected 1 connection, got %d", len(conns)) + } + + if _, err := c.SearchConnections(context.Background(), ConnectionSearchOptions{}); err == nil { + t.Error("expected error when type is missing") + } +} + +func TestListConnectionObjects(t *testing.T) { + tests := []struct { + opts ConnectionObjectsOptions + wantPath string + }{ + {ConnectionObjectsOptions{ID: "c1"}, "/api/v2/connection/source/c1"}, + {ConnectionObjectsOptions{ID: "c1", Target: true}, "/api/v2/connection/target/c1"}, + {ConnectionObjectsOptions{Name: "My Conn", SearchPattern: "Acc"}, "/api/v2/connection/source/name/My%20Conn"}, + } + for _, tt := range tests { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.EscapedPath() != tt.wantPath { + t.Errorf("path = %s, want %s", r.URL.EscapedPath(), tt.wantPath) + } + if got := r.URL.Query().Get("searchPattern"); got != tt.opts.SearchPattern { + t.Errorf("searchPattern = %q, want %q", got, tt.opts.SearchPattern) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`[{"@type":"connListItem","id":"0","name":"Account"}]`)) + }) + c := newTestClient(handler) + items, err := c.ListConnectionObjects(context.Background(), tt.opts) + if err != nil { + t.Fatalf("ListConnectionObjects(%+v) error: %v", tt.opts, err) + } + if len(items) != 1 || items[0].Name != "Account" { + t.Errorf("unexpected items: %+v", items) + } + } + + c := newTestClient(http.NotFoundHandler()) + if _, err := c.ListConnectionObjects(context.Background(), ConnectionObjectsOptions{}); err == nil { + t.Error("expected error without ID or name") + } +} + +func TestGetConnectionMetadata(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v2/connection/target/c1/metadata" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`[{"@type":"runtimeAttribute","name":"UPDATE","dataType":"STRING","defaultValue":"None","label":"","mandatory":false,"maxLength":0,"sessionVarAllowed":false,"uniqueName":"UPDATE","possibleValues":["Update as Update","None"]}]`)) + }) + + c := newTestClient(handler) + attrs, err := c.GetConnectionMetadata(context.Background(), "c1", true) + if err != nil { + t.Fatalf("GetConnectionMetadata() error: %v", err) + } + if len(attrs) != 1 || attrs[0].Name != "UPDATE" || len(attrs[0].PossibleValues) != 2 { + t.Errorf("unexpected attributes: %+v", attrs) + } } func TestAPIErrorNotFound(t *testing.T) { diff --git a/internal/client/connectors.go b/internal/client/connectors.go new file mode 100644 index 0000000..2a3973f --- /dev/null +++ b/internal/client/connectors.go @@ -0,0 +1,182 @@ +package client + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" +) + +// Connector is a connector available to the organization (V2 API). +type Connector struct { + AtType string `json:"@type,omitempty"` + OrgID string `json:"orgId,omitempty"` + Name string `json:"name"` + DisplayName string `json:"displayName,omitempty"` + Description string `json:"description,omitempty"` + Type string `json:"type,omitempty"` + Publisher string `json:"publisher,omitempty"` + ConnectorVersion int `json:"connectorVersion"` + ShortName string `json:"shortName,omitempty"` + IsPublic bool `json:"isPublic"` + SDKType string `json:"sdkType,omitempty"` + PluginID string `json:"pluginId,omitempty"` + GUID string `json:"guid,omitempty"` + UpdateTime string `json:"updateTime,omitempty"` +} + +// ConnectorAttribute is a connection attribute of a connector type. +// Type is the numeric connector data type code. +type ConnectorAttribute struct { + Name string `json:"name"` + Label string `json:"label"` + ID string `json:"id"` + Value string `json:"value"` + Type int `json:"type"` + IsMandatory bool `json:"isMandatory"` + Visible bool `json:"visible"` + List []string `json:"list"` +} + +// ConnectorMetadata describes the attributes of a connector type. CCI +// connectors carry their attribute definitions in CCIMeta, a JSON-encoded +// string; see CCIAttributes. +type ConnectorMetadata struct { + AtType string `json:"@type,omitempty"` + Type string `json:"type"` + IsStandardConnType bool `json:"isStandardConnType"` + CCIMeta string `json:"cciMeta,omitempty"` + Attributes []ConnectorAttribute `json:"attributes"` +} + +// ListConnectors returns the connectors available to the organization. +func (c *Client) ListConnectors(ctx context.Context) ([]Connector, error) { + var resp []Connector + if err := c.doJSON(ctx, http.MethodGet, BaseAPIPathV2+"/connector", nil, &resp); err != nil { + return nil, err + } + return resp, nil +} + +// GetConnectorMetadata returns metadata for the connectors whose name starts +// with connectorName (the API matches by prefix, e.g. "Oracle" also returns +// "Oracle Autonomous Database"). +func (c *Client) GetConnectorMetadata(ctx context.Context, connectorName string) ([]ConnectorMetadata, error) { + if connectorName == "" { + return nil, fmt.Errorf("connector name is required") + } + var resp []ConnectorMetadata + if err := c.doJSONWithQuery(ctx, http.MethodGet, BaseAPIPathV2+"/connector/metadata", + map[string]string{"connectorName": connectorName}, nil, &resp); err != nil { + return nil, err + } + return resp, nil +} + +// ConnectorField is one connection attribute of a connector in a flat form +// that covers both standard attributes and CCI attribute definitions. +// Encrypted is reported for CCI attributes only. +type ConnectorField struct { + Name string `json:"name"` + Label string `json:"label,omitempty"` + DataType string `json:"dataType"` + Mandatory bool `json:"mandatory"` + Encrypted bool `json:"encrypted"` + DefaultValue string `json:"defaultValue,omitempty"` + Values string `json:"values,omitempty"` +} + +// cciMeta mirrors the parts of the cciMeta document that hold the +// connection attribute definitions. +type cciMeta struct { + Connections []struct { + Pages []struct { + Groups []struct { + Group struct { + Controls []struct { + AttributeType cciAttributeType `json:"attributeType"` + } `json:"connInfoPresentationControls"` + } `json:"group"` + } `json:"groups"` + } `json:"pages"` + } `json:"connections"` +} + +type cciAttributeType struct { + AttributeName string `json:"attributeName"` + AttrLocalisedName string `json:"attrLocalisedName"` + AttributeType string `json:"attributeType"` + Mandatory bool `json:"mandatory"` + Encrypted bool `json:"encrypted"` + DefaultValue json.RawMessage `json:"defaultValue"` + ValidList []struct { + Name string `json:"name"` + } `json:"validList"` +} + +// Fields returns the connection attributes of the connector. For CCI +// connectors (CCIMeta set) the attributes come from the CCI definition; +// otherwise from Attributes, with DataType set to the numeric type code. +func (m ConnectorMetadata) Fields() ([]ConnectorField, error) { + if m.CCIMeta == "" { + out := make([]ConnectorField, 0, len(m.Attributes)) + for _, a := range m.Attributes { + out = append(out, ConnectorField{ + Name: a.Name, + Label: a.Label, + DataType: fmt.Sprintf("%d", a.Type), + Mandatory: a.IsMandatory, + DefaultValue: a.Value, + Values: strings.Join(a.List, ", "), + }) + } + return out, nil + } + + var meta cciMeta + if err := json.Unmarshal([]byte(m.CCIMeta), &meta); err != nil { + return nil, fmt.Errorf("parsing cciMeta for %s: %w", m.Type, err) + } + var out []ConnectorField + seen := map[string]bool{} + for _, conn := range meta.Connections { + for _, page := range conn.Pages { + for _, g := range page.Groups { + for _, ctl := range g.Group.Controls { + at := ctl.AttributeType + if at.AttributeName == "" || seen[at.AttributeName] { + continue + } + seen[at.AttributeName] = true + values := make([]string, 0, len(at.ValidList)) + for _, v := range at.ValidList { + values = append(values, v.Name) + } + out = append(out, ConnectorField{ + Name: at.AttributeName, + Label: at.AttrLocalisedName, + DataType: at.AttributeType, + Mandatory: at.Mandatory, + Encrypted: at.Encrypted, + DefaultValue: rawScalar(at.DefaultValue), + Values: strings.Join(values, ", "), + }) + } + } + } + } + return out, nil +} + +// rawScalar renders a JSON scalar (string, number, bool) as text; null is "". +func rawScalar(raw json.RawMessage) string { + if len(raw) == 0 || string(raw) == "null" { + return "" + } + var s string + if json.Unmarshal(raw, &s) == nil { + return s + } + return string(raw) +} diff --git a/internal/client/connectors_test.go b/internal/client/connectors_test.go new file mode 100644 index 0000000..2469902 --- /dev/null +++ b/internal/client/connectors_test.go @@ -0,0 +1,104 @@ +package client + +import ( + "context" + "net/http" + "testing" +) + +func TestListConnectors(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/api/v2/connector" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`[{"@type":"connector","orgId":"0","name":"Amazon S3 v2","connectorVersion":53000414,"pluginId":"451500","shortName":"AmazonS3","isPublic":true,"sdkType":"CCI","displayName":"Amazon S3 v2"}]`)) + }) + + c := newTestClient(handler) + connectors, err := c.ListConnectors(context.Background()) + if err != nil { + t.Fatalf("ListConnectors() error: %v", err) + } + if len(connectors) != 1 { + t.Fatalf("expected 1 connector, got %d", len(connectors)) + } + got := connectors[0] + if got.Name != "Amazon S3 v2" || got.ConnectorVersion != 53000414 || !got.IsPublic || got.SDKType != "CCI" { + t.Errorf("unexpected connector: %+v", got) + } +} + +func TestGetConnectorMetadataStandard(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v2/connector/metadata" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + if got := r.URL.Query().Get("connectorName"); got != "SqlServer" { + t.Errorf("connectorName = %q", got) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`[{"@type":"connectorMetadata","type":"SqlServer","isStandardConnType":true,"attributes":[ + {"name":"port","label":"","id":"","value":"1433","type":2,"isMandatory":true,"visible":false,"list":[]}, + {"name":"subType","label":"","id":"","value":"","type":8,"isMandatory":true,"visible":false,"list":["SqlServer2017","SqlServer2019"]}]}]`)) + }) + + c := newTestClient(handler) + metas, err := c.GetConnectorMetadata(context.Background(), "SqlServer") + if err != nil { + t.Fatalf("GetConnectorMetadata() error: %v", err) + } + if len(metas) != 1 || !metas[0].IsStandardConnType || len(metas[0].Attributes) != 2 { + t.Fatalf("unexpected metadata: %+v", metas) + } + fields, err := metas[0].Fields() + if err != nil { + t.Fatalf("Fields() error: %v", err) + } + if fields[0].DefaultValue != "1433" || fields[0].DataType != "2" || !fields[0].Mandatory { + t.Errorf("unexpected port field: %+v", fields[0]) + } + if fields[1].Values != "SqlServer2017, SqlServer2019" { + t.Errorf("unexpected subType values: %q", fields[1].Values) + } + + if _, err := c.GetConnectorMetadata(context.Background(), ""); err == nil { + t.Error("expected error for empty connector name") + } +} + +func TestConnectorMetadataCCIFields(t *testing.T) { + cci := `{"connections":[{"pages":[{"groups":[{"group":{"connInfoPresentationControls":[ + {"attributeType":{"attributeName":"AccessKey","attrLocalisedName":"Access Key","attributeType":"STRING","mandatory":false,"encrypted":true,"defaultValue":""}}, + {"attributeType":{"attributeName":"UseEC2Role","attrLocalisedName":"Use EC2 Role","attributeType":"BOOLEAN","mandatory":false,"encrypted":false,"defaultValue":false}}, + {"attributeType":{"attributeName":"RegionName","attrLocalisedName":"Region","attributeType":"STRING","mandatory":true,"encrypted":false,"defaultValue":"US East","validList":[{"name":"US East"},{"name":"EU"}]}} + ]}}]}]},{"pages":[{"groups":[{"group":{"connInfoPresentationControls":[ + {"attributeType":{"attributeName":"AccessKey","attributeType":"STRING"}} + ]}}]}]}]}` + m := ConnectorMetadata{Type: "Amazon S3 v2", CCIMeta: cci, Attributes: []ConnectorAttribute{{Name: "agentGroupId"}}} + + fields, err := m.Fields() + if err != nil { + t.Fatalf("Fields() error: %v", err) + } + if len(fields) != 3 { + t.Fatalf("expected 3 de-duplicated fields, got %d: %+v", len(fields), fields) + } + if !fields[0].Encrypted || fields[0].Label != "Access Key" { + t.Errorf("unexpected AccessKey field: %+v", fields[0]) + } + if fields[1].DefaultValue != "false" { + t.Errorf("expected boolean default rendered as false, got %q", fields[1].DefaultValue) + } + if !fields[2].Mandatory || fields[2].DefaultValue != "US East" || fields[2].Values != "US East, EU" { + t.Errorf("unexpected RegionName field: %+v", fields[2]) + } + + bad := ConnectorMetadata{Type: "X", CCIMeta: "{"} + if _, err := bad.Fields(); err == nil { + t.Error("expected error for invalid cciMeta") + } +} diff --git a/internal/client/folders.go b/internal/client/folders.go index af5d2a0..7e7d467 100644 --- a/internal/client/folders.go +++ b/internal/client/folders.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "net/url" ) // Folder represents an IICS folder as returned by the v3 API. @@ -44,18 +45,30 @@ func (c *Client) CreateFolder(ctx context.Context, name, description, projectID, // UpdateFolder updates name and/or description of an existing folder using PATCH. // - When projectName and folderName are both set, uses the name-based URL path. -// - When projectID is set, uses project ID + folder ID path. -// - Otherwise uses the default /folders/ path. +// - Otherwise uses /folders/. Folder IDs are unique in the org, so +// this works for folders in any project. projectID is accepted for +// compatibility but not used: the documented +// /projects//folders/ path returns 404 (verified +// 2026-10-19). +// +// IDs and names cannot be mixed in one URI. The API may return an empty body +// on success, in which case the returned Folder is empty. func (c *Client) UpdateFolder(ctx context.Context, folderID, name, description, projectID, projectName, folderName string) (*Folder, error) { + byName := projectName != "" || folderName != "" + if byName && (folderID != "" || projectID != "") { + return nil, fmt.Errorf("use either --id/--project-id or --project-name/--folder-name, not both") + } + if name == "" && description == "" { + return nil, fmt.Errorf("nothing to update: provide --name and/or --description") + } + var path string switch { - case projectName != "" && folderName != "": - path = fmt.Sprintf("%s/projects/name/%s/folders/name/%s", BaseAPIPathV3, projectName, folderName) - case projectID != "": - if folderID == "" { - return nil, fmt.Errorf("--id is required when --project-id is set") + case byName: + if projectName == "" || folderName == "" { + return nil, fmt.Errorf("--project-name and --folder-name must be used together") } - path = fmt.Sprintf("%s/projects/%s/folders/%s", BaseAPIPathV3, projectID, folderID) + path = fmt.Sprintf("%s/projects/name/%s/folders/name/%s", BaseAPIPathV3, url.PathEscape(projectName), url.PathEscape(folderName)) default: if folderID == "" { return nil, fmt.Errorf("--id is required") @@ -85,7 +98,7 @@ func folderBasePath(projectID, projectName string) (string, error) { return fmt.Sprintf("%s/projects/%s/folders", BaseAPIPathV3, projectID), nil } if projectName != "" { - return fmt.Sprintf("%s/projects/name/%s/folders", BaseAPIPathV3, projectName), nil + return fmt.Sprintf("%s/projects/name/%s/folders", BaseAPIPathV3, url.PathEscape(projectName)), nil } return fmt.Sprintf("%s/folders", BaseAPIPathV3), nil } diff --git a/internal/client/folders_test.go b/internal/client/folders_test.go new file mode 100644 index 0000000..fa0f4fe --- /dev/null +++ b/internal/client/folders_test.go @@ -0,0 +1,82 @@ +package client + +import ( + "context" + "encoding/json" + "net/http" + "testing" +) + +func TestUpdateFolder(t *testing.T) { + tests := []struct { + folderID, projectID, projectName, folderName string + wantPath string + }{ + {"f1", "", "", "", "/public/core/v3/folders/f1"}, + {"f1", "p1", "", "", "/public/core/v3/folders/f1"}, + {"", "", "Orders", "Northeast Orders", "/public/core/v3/projects/name/Orders/folders/name/Northeast%20Orders"}, + } + for _, tt := range tests { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPatch { + t.Errorf("expected PATCH, got %s", r.Method) + } + if r.URL.EscapedPath() != tt.wantPath { + t.Errorf("path = %s, want %s", r.URL.EscapedPath(), tt.wantPath) + } + var body map[string]interface{} + _ = json.NewDecoder(r.Body).Decode(&body) + if body["name"] != "Northeast Requests" { + t.Errorf("unexpected body: %v", body) + } + w.WriteHeader(http.StatusNoContent) + }) + + c := newTestClient(handler) + if _, err := c.UpdateFolder(context.Background(), tt.folderID, "Northeast Requests", "", tt.projectID, tt.projectName, tt.folderName); err != nil { + t.Fatalf("UpdateFolder(%+v) error: %v", tt, err) + } + } +} + +func TestUpdateFolderValidation(t *testing.T) { + c := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Errorf("unexpected request %s %s", r.Method, r.URL.Path) + })) + cases := []struct{ folderID, name, projectID, projectName, folderName string }{ + {"", "n", "", "", ""}, // no identifier + {"f1", "", "", "", ""}, // nothing to update + {"f1", "n", "", "Orders", "F"}, // ID mixed with names + {"", "n", "p1", "Orders", "F"}, // project ID mixed with names + {"", "n", "", "", "F"}, // folder name without project name + {"", "n", "", "Orders", ""}, // project name without folder name + {"", "n", "p1", "", ""}, // project ID without folder ID + } + for _, tc := range cases { + if _, err := c.UpdateFolder(context.Background(), tc.folderID, tc.name, "", tc.projectID, tc.projectName, tc.folderName); err == nil { + t.Errorf("expected error for %+v", tc) + } + } +} + +func TestCreateFolderEscapesProjectName(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Errorf("expected POST, got %s", r.Method) + } + if want := "/public/core/v3/projects/name/Finance%20ETL/folders"; r.URL.EscapedPath() != want { + t.Errorf("path = %s, want %s", r.URL.EscapedPath(), want) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"id":"f1","name":"Staging"}`)) + }) + + c := newTestClient(handler) + f, err := c.CreateFolder(context.Background(), "Staging", "", "", "Finance ETL") + if err != nil { + t.Fatalf("CreateFolder() error: %v", err) + } + if f.ID != "f1" { + t.Errorf("expected ID f1, got %s", f.ID) + } +} diff --git a/internal/client/projects.go b/internal/client/projects.go index 58863aa..7a21af1 100644 --- a/internal/client/projects.go +++ b/internal/client/projects.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "net/url" ) // Project represents an IICS project. @@ -27,10 +28,35 @@ func (c *Client) CreateProject(ctx context.Context, project *Project) (*Project, return &resp, nil } -// UpdateProject updates an existing project. -func (c *Client) UpdateProject(ctx context.Context, id string, project *Project) (*Project, error) { +// projectRequest is the body sent to update a project. +type projectRequest struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` +} + +// UpdateProject changes the name and/or description of a project using PATCH. +// The project is identified by projectID or by its current projectName +// (mutually exclusive). The API may return an empty body on success, in which +// case the returned Project is empty. +func (c *Client) UpdateProject(ctx context.Context, projectID, projectName, name, description string) (*Project, error) { + var path string + switch { + case projectID != "" && projectName != "": + return nil, fmt.Errorf("--id and --project-name are mutually exclusive") + case projectID != "": + path = fmt.Sprintf("%s/projects/%s", BaseAPIPathV3, projectID) + case projectName != "": + path = fmt.Sprintf("%s/projects/name/%s", BaseAPIPathV3, url.PathEscape(projectName)) + default: + return nil, fmt.Errorf("--id or --project-name is required") + } + if name == "" && description == "" { + return nil, fmt.Errorf("nothing to update: provide --name and/or --description") + } + + body := projectRequest{Name: name, Description: description} var resp Project - if err := c.doJSON(ctx, http.MethodPut, fmt.Sprintf("%s/projects/%s", BaseAPIPathV3, id), project, &resp); err != nil { + if err := c.doJSON(ctx, http.MethodPatch, path, body, &resp); err != nil { return nil, err } return &resp, nil diff --git a/internal/client/projects_test.go b/internal/client/projects_test.go new file mode 100644 index 0000000..3045923 --- /dev/null +++ b/internal/client/projects_test.go @@ -0,0 +1,65 @@ +package client + +import ( + "context" + "encoding/json" + "net/http" + "testing" +) + +func TestUpdateProject(t *testing.T) { + tests := []struct { + id, projectName string + wantPath string + }{ + {"p1", "", "/public/core/v3/projects/p1"}, + {"", "Northeast Orders", "/public/core/v3/projects/name/Northeast%20Orders"}, + } + for _, tt := range tests { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPatch { + t.Errorf("expected PATCH, got %s", r.Method) + } + if r.URL.EscapedPath() != tt.wantPath { + t.Errorf("path = %s, want %s", r.URL.EscapedPath(), tt.wantPath) + } + if r.Header.Get("INFA-SESSION-ID") != "test-session" { + t.Errorf("expected V3 session header") + } + var body map[string]interface{} + _ = json.NewDecoder(r.Body).Decode(&body) + if body["description"] != "Orders from all regions" { + t.Errorf("unexpected body: %v", body) + } + if _, ok := body["name"]; ok { + t.Errorf("name should be omitted when empty: %v", body) + } + w.WriteHeader(http.StatusOK) + }) + + c := newTestClient(handler) + p, err := c.UpdateProject(context.Background(), tt.id, tt.projectName, "", "Orders from all regions") + if err != nil { + t.Fatalf("UpdateProject(%q, %q) error: %v", tt.id, tt.projectName, err) + } + if p == nil || p.ID != "" { + t.Errorf("expected empty project for empty response body, got %+v", p) + } + } +} + +func TestUpdateProjectValidation(t *testing.T) { + c := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Errorf("unexpected request %s %s", r.Method, r.URL.Path) + })) + cases := []struct{ id, projectName, name, desc string }{ + {"", "", "n", ""}, + {"p1", "P", "n", ""}, + {"p1", "", "", ""}, + } + for _, tc := range cases { + if _, err := c.UpdateProject(context.Background(), tc.id, tc.projectName, tc.name, tc.desc); err == nil { + t.Errorf("expected error for %+v", tc) + } + } +} diff --git a/internal/menu/entries.go b/internal/menu/entries.go index 78c7440..1dc9adc 100644 --- a/internal/menu/entries.go +++ b/internal/menu/entries.go @@ -96,6 +96,9 @@ func DefaultEntries() []Entry { {Group: "Privileges", Label: "List privileges", Description: "Show all privileges", Args: []string{"privilege", "list"}}, + {Group: "Projects and folders", Label: "Edit project", Description: "Pick a project, filtered by name, and change its name or description", Args: []string{"project", "update", "-i"}, Writes: true}, + {Group: "Projects and folders", Label: "Edit folder", Description: "Pick a project, then a folder, and change its name or description", Args: []string{"folder", "update", "-i"}, Writes: true}, + {Group: "Permissions", Label: "Edit object permissions", Description: "Pick a project, folder or asset and edit its ACLs", Args: []string{"permission", "edit"}, Writes: true}, {Group: "Environments and agents", Label: "List runtime environments", Description: "Show runtime environments (Secure Agent groups)", Args: []string{"environment", "list"}}, From a012dca79986026a20c8d05904e7b5bb561d3d67 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Mon, 19 Oct 2026 18:01:21 -0400 Subject: [PATCH 31/52] feat: project list and folder list with created/updated details (CR-0047) - project list [--name REGEX] [-i] - folder list [--name REGEX] [--project-name | --project-id | --pick-project] [-i] - regex matches the full project/folder path, case-insensitive - created time and created by come from the internal FRS API (/frs/api/v1, IDS-SESSION-ID), user IDs resolved via the users API; falls back to the objects API without the created columns when FRS fails - menu: List projects, List folders, List folders in a project --- README.md | 4 +- cmd/folder.go | 1 + cmd/project.go | 1 + cmd/projectfolder_list.go | 181 +++++++++++ completions/iics.bash | 106 +++++++ ...nnection-connector-project-folder-patch.md | 14 + docs/documentation/folder.md | 101 ++++++- docs/documentation/menu.md | 2 +- docs/documentation/project.md | 72 ++++- internal/client/client.go | 7 +- internal/client/containers.go | 281 ++++++++++++++++++ internal/client/containers_test.go | 148 +++++++++ internal/menu/entries.go | 3 + 13 files changed, 907 insertions(+), 14 deletions(-) create mode 100644 cmd/projectfolder_list.go create mode 100644 internal/client/containers.go create mode 100644 internal/client/containers_test.go diff --git a/README.md b/README.md index 028447e..92cf44f 100644 --- a/README.md +++ b/README.md @@ -275,7 +275,7 @@ it easy to override credentials in CI pipelines without touching the config file | [connection](docs/documentation/connection.md) | `conn` | `list`, `get`, `create`, `update`, `delete`, `test`, `search`, `objects`, `metadata` | Manage connections | | [connector](docs/documentation/connector.md) | | `list`, `metadata` | List connectors and their connection attributes | | [export](docs/documentation/export.md) | | `run`, `start`, `status`, `download`, `create` | Export asset packages | -| [folder](docs/documentation/folder.md) | | `create`, `update`, `delete` | Manage folders | +| [folder](docs/documentation/folder.md) | | `list`, `create`, `update`, `delete` | Manage folders | | [import](docs/documentation/import.md) | `imp` | `run`, `upload`, `start`, `status`, `download-log` | Import asset packages | | [login](docs/documentation/login.md) | | | Authenticate and cache session | | [logout](docs/documentation/logout.md) | | | Invalidate session | @@ -287,7 +287,7 @@ it easy to override credentials in CI pipelines without touching the config file | [permission](docs/documentation/permission.md) | `perm` | `get`, `add`, `update`, `delete`, `set`, `check`, `edit` | Manage object-level permissions | | [privilege](docs/documentation/privilege.md) | | `list` | List available privileges | | [profile](docs/documentation/profile.md) | | `add`, `edit`, `list`, `delete`, `set-default`, `set-password`, `show` | Manage connection profiles | -| [project](docs/documentation/project.md) | | `create`, `update`, `delete` | Manage projects | +| [project](docs/documentation/project.md) | | `list`, `create`, `update`, `delete` | Manage projects | | [publish](docs/documentation/publish.md) | | `start`, `status`, `run` | Publish CAI assets to the runtime | | [release](docs/documentation/release.md) | | `manifest`, `validate`, `plan` | Generate multi-format release manifests and per-environment plans | | [role](docs/documentation/role.md) | | `list`, `get`, `create`, `edit`, `add-privileges`, `remove-privileges`, `delete` | Manage roles | diff --git a/cmd/folder.go b/cmd/folder.go index f398d52..871b435 100644 --- a/cmd/folder.go +++ b/cmd/folder.go @@ -15,6 +15,7 @@ func newFolderCmd() *cobra.Command { Short: "Manage folders", } + cmd.AddCommand(newFolderListCmd()) cmd.AddCommand(newFolderCreateCmd()) cmd.AddCommand(newFolderUpdateCmd()) cmd.AddCommand(newFolderDeleteCmd()) diff --git a/cmd/project.go b/cmd/project.go index 60e2b14..0dc2ddc 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -15,6 +15,7 @@ func newProjectCmd() *cobra.Command { Short: "Manage projects", } + cmd.AddCommand(newProjectListCmd()) cmd.AddCommand(newProjectCreateCmd()) cmd.AddCommand(newProjectUpdateCmd()) cmd.AddCommand(newProjectDeleteCmd()) diff --git a/cmd/projectfolder_list.go b/cmd/projectfolder_list.go new file mode 100644 index 0000000..b9e0926 --- /dev/null +++ b/cmd/projectfolder_list.go @@ -0,0 +1,181 @@ +package cmd + +import ( + "context" + "fmt" + "sort" + "strings" + + "github.com/jbrazda/iics-cli/internal/client" + "github.com/jbrazda/iics-cli/internal/config" + "github.com/jbrazda/iics-cli/internal/output" + "github.com/spf13/cobra" +) + +// promptNamePattern asks for a name regex for the list commands. +func promptNamePattern(kind string) (string, error) { + text, err := promptText(fmt.Sprintf("Search %s by path (regular expression, empty for all)", kind), "") + if err != nil { + return "", cancelAsNil(err) + } + return strings.TrimSpace(text), nil +} + +// printContainerList filters a project or folder list by pattern and prints +// it. When the FRS API failed, a note goes to stderr and the created columns +// are left out. +func printContainerList(cmd *cobra.Command, res *client.ContainerList, pattern string) error { + items, err := client.FilterContainersByPath(res.Items, pattern) + if err != nil { + return err + } + sort.SliceStable(items, func(i, j int) bool { return strings.ToLower(items[i].Path) < strings.ToLower(items[j].Path) }) + if res.FRSError != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Note: created time and created by are not available (%v); showing objects API data.\n", res.FRSError) + } + + f, err := getFormatter() + if err != nil { + return err + } + + columns := []output.Column{ + {Header: "PATH", Field: "path", Width: 30, Priority: 1}, + {Header: "DESCRIPTION", Field: "description", Width: 20, MaxWidth: 50, Priority: 4, Shrink: output.ShrinkWrap}, + } + if res.FRSError == nil { + columns = append(columns, + output.Column{Header: "CREATED", Field: "createTime", Width: 20, Priority: 3, Shrink: output.ShrinkNever}, + output.Column{Header: "CREATED BY", Field: "createdBy", Width: 20, Priority: 4, Shrink: output.ShrinkTruncate}, + ) + } + columns = append(columns, + output.Column{Header: "UPDATED", Field: "updateTime", Width: 20, Priority: 2, Shrink: output.ShrinkNever}, + output.Column{Header: "UPDATED BY", Field: "updatedBy", Width: 20, Priority: 3, Shrink: output.ShrinkTruncate}, + output.Column{Header: "ID", Field: "id", Width: 22, Priority: 5, Shrink: output.ShrinkNever}, + ) + return f.Format(items, columns) +} + +func newProjectListCmd() *cobra.Command { + var ( + pattern string + interactive bool + ) + + cmd := &cobra.Command{ + Use: "list", + Short: "List projects with created and updated details", + Long: `List projects with description, created and updated time and user. + +--name filters by a regular expression matched against the project name +(case-insensitive); --interactive asks for it. Created time and created by come +from the internal FRS API used by the IICS web UI; when it is not available the +list falls back to the objects API without those columns.`, + Example: ` iics project list + iics project list --name '^fin' + iics project list -i`, + RunE: func(cmd *cobra.Command, args []string) error { + if interactive { + if !config.IsTerminal() { + return fmt.Errorf("--interactive requires a terminal") + } + p, err := promptNamePattern("projects") + if err != nil { + return err + } + pattern = p + } + + c, err := getClient(cmd) + if err != nil { + return err + } + + res, err := c.ListProjectInfo(context.Background()) + if err != nil { + return err + } + return printContainerList(cmd, res, pattern) + }, + } + + cmd.Flags().StringVar(&pattern, "name", "", "regular expression matched against the project name (case-insensitive)") + cmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "ask for the name pattern") + return cmd +} + +func newFolderListCmd() *cobra.Command { + var ( + pattern string + projectID string + projectName string + interactive bool + pickProj bool + ) + + cmd := &cobra.Command{ + Use: "list", + Short: "List folders with created and updated details", + Long: `List folders with their full path (project/folder), description, created +and updated time and user. + +Without a project all folders are listed. Limit the list to one project with +--project-name or --project-id, or pick the project from a list with +--pick-project. --name filters by a regular expression matched against the +full path (case-insensitive); --interactive asks for it. + +Created time and created by come from the internal FRS API used by the IICS +web UI; when it is not available the list falls back to the objects API +without those columns.`, + Example: ` iics folder list + iics folder list --name 'sales.*_v1$' + iics folder list --project-name "Finance ETL" + iics folder list --pick-project + iics folder list -i`, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + if (interactive || pickProj) && !config.IsTerminal() { + return fmt.Errorf("--interactive and --pick-project require a terminal") + } + + c, err := getClient(cmd) + if err != nil { + return err + } + + if pickProj { + project, perr := pickProject(ctx, c) + if perr != nil { + return perr + } + if project == nil { + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Canceled.") + return nil + } + projectID, projectName = project.ID, project.Path + } + if interactive { + p, perr := promptNamePattern("folders") + if perr != nil { + return perr + } + pattern = p + } + + res, err := c.ListFolderInfo(ctx, projectID, projectName) + if err != nil { + return err + } + return printContainerList(cmd, res, pattern) + }, + } + + cmd.Flags().StringVar(&pattern, "name", "", "regular expression matched against the full folder path (case-insensitive)") + cmd.Flags().StringVar(&projectID, "project-id", "", "list only the folders of this project ID") + cmd.Flags().StringVar(&projectName, "project-name", "", "list only the folders of this project") + cmd.Flags().BoolVar(&pickProj, "pick-project", false, "pick the project from a list") + cmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "ask for the name pattern") + cmd.MarkFlagsMutuallyExclusive("project-id", "project-name", "pick-project") + return cmd +} diff --git a/completions/iics.bash b/completions/iics.bash index 4005036..7f368b3 100644 --- a/completions/iics.bash +++ b/completions/iics.bash @@ -2927,6 +2927,63 @@ _iics_folder_delete() noun_aliases=() } +_iics_folder_list() +{ + last_command="iics_folder_list" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--interactive") + flags+=("-i") + local_nonpersistent_flags+=("--interactive") + local_nonpersistent_flags+=("-i") + flags+=("--name=") + two_word_flags+=("--name") + local_nonpersistent_flags+=("--name") + local_nonpersistent_flags+=("--name=") + flags+=("--pick-project") + local_nonpersistent_flags+=("--pick-project") + flags+=("--project-id=") + two_word_flags+=("--project-id") + local_nonpersistent_flags+=("--project-id") + local_nonpersistent_flags+=("--project-id=") + flags+=("--project-name=") + two_word_flags+=("--project-name") + local_nonpersistent_flags+=("--project-name") + local_nonpersistent_flags+=("--project-name=") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--http-timeout=") + two_word_flags+=("--http-timeout") + flags+=("--no-color") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + flags+=("--profile=") + two_word_flags+=("--profile") + two_word_flags+=("-p") + flags+=("--theme=") + two_word_flags+=("--theme") + flags+=("--verbose") + flags+=("-v") + flags+=("--wide") + flags+=("--width=") + two_word_flags+=("--width") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _iics_folder_update() { last_command="iics_folder_update" @@ -2999,6 +3056,7 @@ _iics_folder() commands=() commands+=("create") commands+=("delete") + commands+=("list") commands+=("update") flags=() @@ -5451,6 +5509,53 @@ _iics_project_delete() noun_aliases=() } +_iics_project_list() +{ + last_command="iics_project_list" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--interactive") + flags+=("-i") + local_nonpersistent_flags+=("--interactive") + local_nonpersistent_flags+=("-i") + flags+=("--name=") + two_word_flags+=("--name") + local_nonpersistent_flags+=("--name") + local_nonpersistent_flags+=("--name=") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--http-timeout=") + two_word_flags+=("--http-timeout") + flags+=("--no-color") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + flags+=("--profile=") + two_word_flags+=("--profile") + two_word_flags+=("-p") + flags+=("--theme=") + two_word_flags+=("--theme") + flags+=("--verbose") + flags+=("-v") + flags+=("--wide") + flags+=("--width=") + two_word_flags+=("--width") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _iics_project_update() { last_command="iics_project_update" @@ -5519,6 +5624,7 @@ _iics_project() commands=() commands+=("create") commands+=("delete") + commands+=("list") commands+=("update") flags=() diff --git a/docs/ChangeRequests/pending/CR-0047-connection-connector-project-folder-patch.md b/docs/ChangeRequests/pending/CR-0047-connection-connector-project-folder-patch.md index 95f7442..9634881 100644 --- a/docs/ChangeRequests/pending/CR-0047-connection-connector-project-folder-patch.md +++ b/docs/ChangeRequests/pending/CR-0047-connection-connector-project-folder-patch.md @@ -97,6 +97,20 @@ Verified against the DEV org on 2026-10-19: objects API with a name filter, edit name and description, confirm. - Menu group "Projects and folders" with "Edit project" and "Edit folder". +### project and folder list (added after review) + +- `project list [--name REGEX] [-i]` and + `folder list [--name REGEX] [--project-name | --project-id | --pick-project] [-i]`. +- Columns: path, description, created, created by, updated, updated by, ID. +- The regex matches the full path (`project/folder`), case-insensitive. +- Created time and created by are only available from the internal FRS API + (`/frs/api/v1/Projects`, `/Folders`, `/Projects('')/Folders`, header + `IDS-SESSION-ID`), which the IICS web UI uses and Informatica does not + document. FRS returns user IDs; they are resolved through the users API. + When FRS fails, the commands fall back to the objects API without the + created columns and print a note. The developer approved this dependency. +- Menu: "List projects", "List folders", "List folders in a project". + ## Verification - `go test ./...` covers each new client method. diff --git a/docs/documentation/folder.md b/docs/documentation/folder.md index 6efe4c9..8dabaab 100644 --- a/docs/documentation/folder.md +++ b/docs/documentation/folder.md @@ -10,11 +10,102 @@ iics folder [flags] ## Subcommands -| Subcommand | Description | -| ---------- | --------------------- | -| `create` | Create a new folder | -| `update` | Update a folder | -| `delete` | Delete a folder | +| Subcommand | Description | +| ---------- | --------------------------------------------- | +| `list` | List folders with created and updated details | +| `create` | Create a new folder | +| `update` | Update a folder | +| `delete` | Delete a folder | + +--- + +## folder list + +List folders with their full path (`project/folder`), description, created and +updated time and user, sorted by path. + +- Without a project, all folders in the org are listed. +- `--project-name` or `--project-id` lists the folders of one project. +- `--pick-project` picks the project from a list, the same picker as + `folder update -i` (more than 10 projects first ask for a name filter). + +`--name` filters by a regular expression matched against the full path +(case-insensitive), so it can match the project name, the folder name or both; +`--interactive` asks for it. + +The [menu](menu.md) entries "List folders" (`folder list -i`) and "List folders +in a project" (`folder list --pick-project`) run these flows. + +Created time and created by come from the internal FRS API that the IICS web +UI uses (`/frs/api/v1`). It is not part of the published REST API, so the CLI +falls back to the documented objects API when it fails: a note is printed to +stderr and the `createTime` and `createdBy` columns are left out. User IDs are +resolved to user names; users that are not in the org user list (such as +Informatica system users) are shown by ID. + +### Flags + +| Flag | Short | Type | Description | +| ---------------- | ----- | ------ | --------------------------------------------------------------- | +| `--name` | | string | Regular expression matched against the full path (ignores case) | +| `--project-name` | | string | List only the folders of this project | +| `--project-id` | | string | List only the folders of this project ID | +| `--pick-project` | | bool | Pick the project from a list | +| `--interactive` | `-i` | bool | Ask for the name pattern | + +`--project-name`, `--project-id` and `--pick-project` are mutually exclusive. + +All [global flags](../../README.md#global-flags) apply. + +### Output columns + +| Column | Description | +| ------------- | ------------------------------------------ | +| `path` | Full path: `project/folder` | +| `description` | Description | +| `createTime` | Created time (FRS only) | +| `createdBy` | User who created the folder (FRS only) | +| `updateTime` | Last update time | +| `updatedBy` | User who last updated the folder | +| `id` | Folder ID | + +`--output json` also returns `name` and `type`. + +### Examples + +```bash +# All folders +iics folder list + +# Folders in the Salesforce project whose name starts with "Acc" +iics folder list --name '^salesforce/acc' + +# Any folder ending in _v1 +iics folder list --name '_v1$' + +# Folders of one project +iics folder list --project-name "Finance ETL" + +# Pick the project, then show its folders +iics folder list --pick-project +``` + +```powershell +# All folders +iics folder list + +# Folders in the Salesforce project whose name starts with "Acc" +iics folder list --name '^salesforce/acc' + +# Any folder ending in _v1 +iics folder list --name '_v1$' + +# Folders of one project +iics folder list --project-name "Finance ETL" + +# Pick the project, then show its folders +iics folder list --pick-project +``` --- diff --git a/docs/documentation/menu.md b/docs/documentation/menu.md index f500d92..e03e27d 100644 --- a/docs/documentation/menu.md +++ b/docs/documentation/menu.md @@ -121,7 +121,7 @@ On a production profile: | User groups | List, Show, Create ✎, Edit roles ✎, Delete ✎ | | Roles | List, Show (with privileges), Create ✎, Edit privileges ✎ | | Privileges | List privileges | -| Projects and folders | Edit project ✎, Edit folder ✎ | +| Projects and folders | List projects, List folders, List folders in a project, Edit project ✎, Edit folder ✎ | | Permissions | Edit object permissions ✎ | | Environments and agents | List runtime environments, Create runtime environment ✎, List agents, Start / Stop / Restart agent service ✎, Show agent installer info, Download agent installer (`--interactive --progress`) | | Help | Show CLI help | diff --git a/docs/documentation/project.md b/docs/documentation/project.md index 4abfba0..824a507 100644 --- a/docs/documentation/project.md +++ b/docs/documentation/project.md @@ -10,11 +10,73 @@ iics project [flags] ## Subcommands -| Subcommand | Description | -| ---------- | ---------------------- | -| `create` | Create a new project | -| `update` | Update a project | -| `delete` | Delete a project | +| Subcommand | Description | +| ---------- | ---------------------------------------------- | +| `list` | List projects with created and updated details | +| `create` | Create a new project | +| `update` | Update a project | +| `delete` | Delete a project | + +--- + +## project list + +List projects with description, created and updated time and user, sorted by +name. `--name` filters by a regular expression matched against the project +name (case-insensitive); `--interactive` asks for it. The [menu](menu.md) +entry "List projects" runs `project list -i`. + +Created time and created by come from the internal FRS API that the IICS web +UI uses (`/frs/api/v1`). It is not part of the published REST API, so the CLI +falls back to the documented objects API when it fails: a note is printed to +stderr and the `createTime` and `createdBy` columns are left out. User IDs are +resolved to user names; users that are not in the org user list (such as +Informatica system users) are shown by ID. + +### Flags + +| Flag | Short | Type | Description | +| --------------- | ----- | ------ | ------------------------------------------------------------- | +| `--name` | | string | Regular expression matched against the name (ignores case) | +| `--interactive` | `-i` | bool | Ask for the name pattern | + +All [global flags](../../README.md#global-flags) apply. + +### Output columns + +| Column | Description | +| ------------- | ------------------------------------------- | +| `path` | Project name | +| `description` | Description | +| `createTime` | Created time (FRS only) | +| `createdBy` | User who created the project (FRS only) | +| `updateTime` | Last update time | +| `updatedBy` | User who last updated the project | +| `id` | Project ID | + +`--output json` also returns `name` and `type`. + +### Examples + +```bash +iics project list + +# Projects whose name starts with "fin" +iics project list --name '^fin' + +# Ask for the pattern +iics project list -i +``` + +```powershell +iics project list + +# Projects whose name starts with "fin" +iics project list --name '^fin' + +# Ask for the pattern +iics project list -i +``` --- diff --git a/internal/client/client.go b/internal/client/client.go index f78ba01..84ce7f0 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -19,6 +19,8 @@ const ( // Note: IICS v3 uses "INFA-SESSION-ID" while v2 used "icSessionId". sessionHeaderV3 = "INFA-SESSION-ID" sessionHeaderV2 = "icSessionId" + // sessionHeaderFRS is the session header of the internal FRS repository API. + sessionHeaderFRS = "IDS-SESSION-ID" // baseAPIPathV2 is the base path for v2 API endpoints. BaseAPIPathV2 = "api/v2" // baseAPIPathV3 is the base path for v3 API endpoints. @@ -150,6 +152,9 @@ func (c *Client) doWithSession(ctx context.Context, req *http.Request) (*http.Re } else { req.Header.Set(sessionHeaderV3, sessionID) } + if strings.HasPrefix(req.URL.Path, "/"+frsAPIPath+"/") { + req.Header.Set(sessionHeaderFRS, sessionID) + } if req.Header.Get("Accept") == "" { req.Header.Set("Accept", "application/json") } @@ -245,7 +250,7 @@ func debugPrintHTTP(req *http.Request, reqData []byte, resp *http.Response, resp _, _ = fmt.Fprintf(w, "%s[DEBUG] Request Headers:\n", now) for k, vs := range req.Header { for _, v := range vs { - if strings.EqualFold(k, sessionHeaderV3) || strings.EqualFold(k, sessionHeaderV2) { + if strings.EqualFold(k, sessionHeaderV3) || strings.EqualFold(k, sessionHeaderV2) || strings.EqualFold(k, sessionHeaderFRS) { _, _ = fmt.Fprintf(w, " %s: ***\n", k) } else { _, _ = fmt.Fprintf(w, " %s: %s\n", k, v) diff --git a/internal/client/containers.go b/internal/client/containers.go new file mode 100644 index 0000000..8aa2904 --- /dev/null +++ b/internal/client/containers.go @@ -0,0 +1,281 @@ +package client + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "regexp" + "strconv" + "strings" +) + +// frsAPIPath is the path of the internal FRS repository API used by the IICS +// web UI. It is undocumented; callers must fall back to the objects API. +const frsAPIPath = "frs/api/v1" + +// frsPageSize is the $top page size for FRS collection requests. +const frsPageSize = 1000 + +// ContainerInfo describes a project or folder. CreatedTime and CreatedBy are +// only available from the FRS API; they are empty when the objects API was +// used. +type ContainerInfo struct { + ID string `json:"id"` + Type string `json:"type"` + Name string `json:"name"` + Path string `json:"path"` + Description string `json:"description"` + CreatedTime string `json:"createTime,omitempty"` + CreatedBy string `json:"createdBy,omitempty"` + UpdateTime string `json:"updateTime,omitempty"` + UpdatedBy string `json:"updatedBy,omitempty"` +} + +// ContainerList is the result of a project or folder listing. FRSError is set +// when the FRS API failed and the objects API was used instead. +type ContainerList struct { + Items []ContainerInfo + FRSError error +} + +// frsContainer is a project or folder as returned by the FRS API. +type frsContainer struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + CreatedBy string `json:"createdBy"` + LastUpdatedBy string `json:"lastUpdatedBy"` + CreatedTime string `json:"createdTime"` + LastUpdatedTime string `json:"lastUpdatedTime"` + DocumentType string `json:"documentType"` + ParentInfo []struct { + ParentID string `json:"parentId"` + ParentName string `json:"parentName"` + ParentType string `json:"parentType"` + } `json:"parentInfo"` +} + +type frsCollection struct { + Value []frsContainer `json:"value"` +} + +// ListProjectInfo lists all projects with created and updated details. +func (c *Client) ListProjectInfo(ctx context.Context) (*ContainerList, error) { + items, frsErr := c.listFRSContainers(ctx, "Projects") + if frsErr == nil { + return &ContainerList{Items: items}, nil + } + res, err := c.ListAllObjects(ctx, ObjectsListOptions{Type: "Project"}, nil) + if err != nil { + return nil, err + } + return &ContainerList{Items: containersFromObjects(res.Objects), FRSError: frsErr}, nil +} + +// ListFolderInfo lists folders with created and updated details: all folders +// when projectID and projectName are empty, otherwise the folders of that +// project (projectID wins). +func (c *Client) ListFolderInfo(ctx context.Context, projectID, projectName string) (*ContainerList, error) { + collection := "Folders" + if projectID != "" { + collection = fmt.Sprintf("Projects('%s')/Folders", projectID) + } + items, frsErr := c.listFRSContainers(ctx, collection) + if frsErr == nil { + if projectID == "" && projectName != "" { + items = filterByProject(items, projectName) + } + return &ContainerList{Items: items}, nil + } + + opts := ObjectsListOptions{Type: "Folder"} + if projectID != "" && projectName == "" { + projects, err := c.ListAllObjects(ctx, ObjectsListOptions{Type: "Project"}, nil) + if err != nil { + return nil, err + } + for _, p := range projects.Objects { + if p.ID == projectID { + projectName = p.Path + } + } + if projectName == "" { + return nil, fmt.Errorf("project %s not found", projectID) + } + } + if projectName != "" { + opts = ObjectsListOptions{Query: fmt.Sprintf("location=='%s'", projectName)} + } + res, err := c.ListAllObjects(ctx, opts, nil) + if err != nil { + return nil, err + } + var folders []Object + for _, o := range res.Objects { + if o.Type == "Folder" { + folders = append(folders, o) + } + } + return &ContainerList{Items: containersFromObjects(folders), FRSError: frsErr}, nil +} + +// FilterContainersByPath keeps the items whose path matches the regular +// expression pattern (case-insensitive). An empty pattern keeps all items. +func FilterContainersByPath(items []ContainerInfo, pattern string) ([]ContainerInfo, error) { + if pattern == "" { + return items, nil + } + re, err := regexp.Compile("(?i)" + pattern) + if err != nil { + return nil, fmt.Errorf("invalid name pattern: %w", err) + } + var out []ContainerInfo + for _, it := range items { + if re.MatchString(it.Path) { + out = append(out, it) + } + } + return out, nil +} + +func filterByProject(items []ContainerInfo, projectName string) []ContainerInfo { + prefix := strings.ToLower(projectName) + "/" + var out []ContainerInfo + for _, it := range items { + if strings.HasPrefix(strings.ToLower(it.Path), prefix) { + out = append(out, it) + } + } + return out +} + +func containersFromObjects(objs []Object) []ContainerInfo { + out := make([]ContainerInfo, 0, len(objs)) + for _, o := range objs { + out = append(out, ContainerInfo{ + ID: o.ID, + Type: o.Type, + Name: o.Path[strings.LastIndex(o.Path, "/")+1:], + Path: o.Path, + Description: o.Description, + UpdateTime: o.UpdateTime, + UpdatedBy: o.UpdatedBy, + }) + } + return out +} + +// listFRSContainers reads an FRS collection page by page and resolves the +// created/updated user IDs to user names. +func (c *Client) listFRSContainers(ctx context.Context, collection string) ([]ContainerInfo, error) { + var all []frsContainer + for skip := 0; ; skip += frsPageSize { + var page frsCollection + query := map[string]string{"$top": strconv.Itoa(frsPageSize)} + if skip > 0 { + query["$skip"] = strconv.Itoa(skip) + } + if err := c.doFRSJSON(ctx, collection, query, &page); err != nil { + return nil, fmt.Errorf("FRS %s: %w", collection, err) + } + all = append(all, page.Value...) + if len(page.Value) < frsPageSize { + break + } + } + + // Without user read access the IDs are shown as returned. + names, _ := c.userNamesByID(ctx) + userName := func(id string) string { + if n, ok := names[id]; ok { + return n + } + return id + } + + out := make([]ContainerInfo, 0, len(all)) + for _, f := range all { + path := f.Name + for _, p := range f.ParentInfo { + if p.ParentType == "Project" { + path = p.ParentName + "/" + f.Name + } + } + out = append(out, ContainerInfo{ + ID: f.ID, + Type: f.DocumentType, + Name: f.Name, + Path: path, + Description: f.Description, + CreatedTime: f.CreatedTime, + CreatedBy: userName(f.CreatedBy), + UpdateTime: f.LastUpdatedTime, + UpdatedBy: userName(f.LastUpdatedBy), + }) + } + return out, nil +} + +// userNamesByID maps user IDs to user names for all users of the org. +func (c *Client) userNamesByID(ctx context.Context) (map[string]string, error) { + names := map[string]string{} + opts := UserListOptions{Limit: 200} + for { + users, err := c.ListUsers(ctx, opts) + if err != nil { + return nil, err + } + for _, u := range users { + names[u.ID] = u.UserName + } + if len(users) < opts.Limit { + return names, nil + } + opts.Skip += opts.Limit + } +} + +// doFRSJSON sends a GET to the FRS API on the host of the base API URL and +// decodes the JSON response. doWithSession adds the IDS-SESSION-ID header. +func (c *Client) doFRSJSON(ctx context.Context, collection string, query map[string]string, respBody interface{}) error { + if err := c.ensureSession(ctx); err != nil { + return err + } + base, err := url.Parse(c.BaseAPIURL()) + if err != nil { + return fmt.Errorf("parsing base API URL: %w", err) + } + u := url.URL{Scheme: base.Scheme, Host: base.Host, Path: "/" + frsAPIPath + "/" + collection} + q := url.Values{} + for k, v := range query { + q.Set(k, v) + } + u.RawQuery = q.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) + if err != nil { + return fmt.Errorf("creating request: %w", err) + } + resp, err := c.do(ctx, req) + if err != nil { + return err + } + defer func() { _ = resp.Body.Close() }() + respData, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("reading response: %w", err) + } + if c.debug { + debugPrintHTTP(req, nil, resp, respData) + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return newAPIError(resp, respData) + } + if err := json.Unmarshal(respData, respBody); err != nil { + return fmt.Errorf("parsing response: %w", err) + } + return nil +} diff --git a/internal/client/containers_test.go b/internal/client/containers_test.go new file mode 100644 index 0000000..489aad3 --- /dev/null +++ b/internal/client/containers_test.go @@ -0,0 +1,148 @@ +package client + +import ( + "context" + "net/http" + "strings" + "testing" +) + +const frsUsersJSON = `[{"id":"u1","userName":"jdoe@example.com"}]` + +func TestListProjectInfoFRS(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch r.URL.Path { + case "/frs/api/v1/Projects": + if r.Header.Get("IDS-SESSION-ID") != "test-session" { + t.Errorf("expected IDS-SESSION-ID header on FRS request") + } + if r.URL.Query().Get("$top") == "" { + t.Errorf("expected $top query parameter") + } + _, _ = w.Write([]byte(`{"value":[{"id":"p1","name":"Finance","description":"d","createdBy":"u1","lastUpdatedBy":"u2","createdTime":"2025-01-01T00:00:00Z","lastUpdatedTime":"2025-02-01T00:00:00Z","documentType":"Project","parentInfo":[{"parentId":"s","parentName":"REG","parentType":"Space"}]}]}`)) + case "/public/core/v3/users": + _, _ = w.Write([]byte(frsUsersJSON)) + default: + t.Errorf("unexpected request %s", r.URL.Path) + } + }) + + c := newTestClient(handler) + res, err := c.ListProjectInfo(context.Background()) + if err != nil { + t.Fatalf("ListProjectInfo() error: %v", err) + } + if res.FRSError != nil { + t.Fatalf("unexpected FRS fallback: %v", res.FRSError) + } + got := res.Items[0] + if got.Path != "Finance" || got.CreatedTime != "2025-01-01T00:00:00Z" || got.UpdateTime != "2025-02-01T00:00:00Z" { + t.Errorf("unexpected project: %+v", got) + } + if got.CreatedBy != "jdoe@example.com" { + t.Errorf("expected createdBy resolved to user name, got %q", got.CreatedBy) + } + if got.UpdatedBy != "u2" { + t.Errorf("expected unknown user ID kept, got %q", got.UpdatedBy) + } +} + +func TestListFolderInfoFRS(t *testing.T) { + folders := `{"value":[ + {"id":"f1","name":"Staging","documentType":"Folder","parentInfo":[{"parentType":"Space","parentName":"REG"},{"parentType":"Project","parentName":"Finance"}]}, + {"id":"f2","name":"Other","documentType":"Folder","parentInfo":[{"parentType":"Project","parentName":"Sales"}]}]}` + tests := []struct { + projectID, projectName, wantPath string + wantIDs []string + }{ + {"", "", "/frs/api/v1/Folders", []string{"f1", "f2"}}, + {"", "finance", "/frs/api/v1/Folders", []string{"f1"}}, + {"p1", "", "/frs/api/v1/Projects('p1')/Folders", []string{"f1", "f2"}}, + } + for _, tt := range tests { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + if r.URL.Path == "/public/core/v3/users" { + _, _ = w.Write([]byte(frsUsersJSON)) + return + } + if r.URL.Path != tt.wantPath { + t.Errorf("path = %s, want %s", r.URL.Path, tt.wantPath) + } + _, _ = w.Write([]byte(folders)) + }) + c := newTestClient(handler) + res, err := c.ListFolderInfo(context.Background(), tt.projectID, tt.projectName) + if err != nil { + t.Fatalf("ListFolderInfo(%q, %q) error: %v", tt.projectID, tt.projectName, err) + } + var ids []string + for _, it := range res.Items { + ids = append(ids, it.ID) + } + if strings.Join(ids, ",") != strings.Join(tt.wantIDs, ",") { + t.Errorf("ListFolderInfo(%q, %q) ids = %v, want %v", tt.projectID, tt.projectName, ids, tt.wantIDs) + } + if res.Items[0].Path != "Finance/Staging" { + t.Errorf("expected full path Finance/Staging, got %q", res.Items[0].Path) + } + } +} + +func TestListFolderInfoFallback(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch { + case strings.HasPrefix(r.URL.Path, "/frs/"): + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"error":{"code":"FRS_404","message":"gone"}}`)) + case r.URL.Path == "/public/core/v3/objects": + if q := r.URL.Query().Get("q"); q != "location=='Finance'" { + t.Errorf("unexpected objects query %q", q) + } + _, _ = w.Write([]byte(`{"count":2,"objects":[ + {"id":"p1","path":"Finance","type":"Project"}, + {"id":"f1","path":"Finance/Staging","type":"Folder","description":"d","updatedBy":"jdoe","updateTime":"2025-02-01T00:00:00Z"}]}`)) + default: + t.Errorf("unexpected request %s", r.URL.Path) + } + }) + + c := newTestClient(handler) + res, err := c.ListFolderInfo(context.Background(), "", "Finance") + if err != nil { + t.Fatalf("ListFolderInfo() error: %v", err) + } + if res.FRSError == nil { + t.Fatal("expected FRSError to be set on fallback") + } + if len(res.Items) != 1 { + t.Fatalf("expected only the folder, got %+v", res.Items) + } + got := res.Items[0] + if got.Name != "Staging" || got.UpdatedBy != "jdoe" || got.CreatedTime != "" { + t.Errorf("unexpected fallback folder: %+v", got) + } +} + +func TestFilterContainersByPath(t *testing.T) { + items := []ContainerInfo{{Path: "Finance/Staging"}, {Path: "Sales/AccountSearch_v1"}, {Path: "Sales/Orders"}} + + got, err := FilterContainersByPath(items, "sales/acc.*_V1$") + if err != nil { + t.Fatalf("FilterContainersByPath() error: %v", err) + } + if len(got) != 1 || got[0].Path != "Sales/AccountSearch_v1" { + t.Errorf("unexpected match: %+v", got) + } + + all, _ := FilterContainersByPath(items, "") + if len(all) != 3 { + t.Errorf("empty pattern should keep all items, got %d", len(all)) + } + + if _, err := FilterContainersByPath(items, "["); err == nil { + t.Error("expected error for invalid pattern") + } +} diff --git a/internal/menu/entries.go b/internal/menu/entries.go index 1dc9adc..2ca3dfe 100644 --- a/internal/menu/entries.go +++ b/internal/menu/entries.go @@ -96,6 +96,9 @@ func DefaultEntries() []Entry { {Group: "Privileges", Label: "List privileges", Description: "Show all privileges", Args: []string{"privilege", "list"}}, + {Group: "Projects and folders", Label: "List projects", Description: "Show projects with created and updated details; search by name (regex)", Args: []string{"project", "list", "-i"}}, + {Group: "Projects and folders", Label: "List folders", Description: "Show all folders with full paths; search by path (regex)", Args: []string{"folder", "list", "-i"}}, + {Group: "Projects and folders", Label: "List folders in a project", Description: "Pick a project, filtered by name, and show its folders", Args: []string{"folder", "list", "--pick-project"}}, {Group: "Projects and folders", Label: "Edit project", Description: "Pick a project, filtered by name, and change its name or description", Args: []string{"project", "update", "-i"}, Writes: true}, {Group: "Projects and folders", Label: "Edit folder", Description: "Pick a project, then a folder, and change its name or description", Args: []string{"folder", "update", "-i"}, Writes: true}, From e8c8d53437af03cf4de17d081eb4ca7269f7b31b Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Mon, 19 Oct 2026 18:11:09 -0400 Subject: [PATCH 32/52] chore: move CR-0047 to completed --- .../CR-0047-connection-connector-project-folder-patch.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/ChangeRequests/{pending => completed}/CR-0047-connection-connector-project-folder-patch.md (100%) diff --git a/docs/ChangeRequests/pending/CR-0047-connection-connector-project-folder-patch.md b/docs/ChangeRequests/completed/CR-0047-connection-connector-project-folder-patch.md similarity index 100% rename from docs/ChangeRequests/pending/CR-0047-connection-connector-project-folder-patch.md rename to docs/ChangeRequests/completed/CR-0047-connection-connector-project-folder-patch.md From 6d9e98abfe1e3a15af0393a5db2483650a81918b Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Mon, 19 Oct 2026 18:17:39 -0400 Subject: [PATCH 33/52] feat: Esc/q cancel prompts, single-key confirmations (CR-0048) - huh pickers, text and yes/no prompts: Esc cancels (closes/clears an open filter first), q cancels lists and yes/no prompts but is typed in text inputs and filters; multi-page wizards keep ctrl+c only - [y/N] confirmations, login profile creation, role privilege prompt and the menu pause read single keys in raw mode on a terminal, so arrow keys are ignored instead of echoed as ^[[A; piped answers still work - menu pause: Enter/Esc return to the menu, q/ctrl+c quit --- cmd/confirm.go | 32 +-- cmd/login.go | 7 +- cmd/role_prompt.go | 13 +- cmd/user.go | 8 +- ...-prompt-cancel-keys-and-raw-key-confirm.md | 53 +++++ docs/documentation/interactive.md | 34 +++- docs/documentation/menu.md | 6 +- internal/menu/loop.go | 22 ++- internal/menu/model.go | 2 +- internal/tui/acleditor.go | 6 +- internal/tui/keys.go | 182 ++++++++++++++++++ internal/tui/keys_test.go | 106 ++++++++++ internal/tui/prompt.go | 11 +- internal/tui/review.go | 4 +- internal/tui/roleeditor.go | 6 +- 15 files changed, 429 insertions(+), 63 deletions(-) create mode 100644 docs/ChangeRequests/pending/CR-0048-prompt-cancel-keys-and-raw-key-confirm.md create mode 100644 internal/tui/keys.go create mode 100644 internal/tui/keys_test.go diff --git a/cmd/confirm.go b/cmd/confirm.go index 7101f19..3386e0c 100644 --- a/cmd/confirm.go +++ b/cmd/confirm.go @@ -2,21 +2,31 @@ package cmd import ( "fmt" + "os" + "github.com/jbrazda/iics-cli/internal/config" + "github.com/jbrazda/iics-cli/internal/tui" "github.com/spf13/cobra" ) -// confirmAction asks question followed by " [y/N]: " on the command's output -// and reads one line from stdin. It returns true only for "y" or "Y"; any other -// answer prints "Canceled." and returns false. The plain line format is kept -// deliberately so scripts can pipe an answer. +// confirmAction asks question followed by " [y/N]: " on the command's output. +// On a terminal the answer is a single key (y yes; n, Enter, Esc, q or ctrl+c +// no; arrow keys are ignored). Otherwise one line is read from stdin, so +// scripts can pipe an answer. It returns true only for yes; any other answer +// prints "Canceled." and returns false. func confirmAction(cmd *cobra.Command, question string) bool { - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s [y/N]: ", question) - var confirm string - _, _ = fmt.Scanln(&confirm) - if confirm != "y" && confirm != "Y" { - _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Canceled.") - return false + out := cmd.OutOrStdout() + var yes bool + if config.IsTerminal() { + yes, _ = tui.KeyConfirm(os.Stdin, out, question, false) + } else { + _, _ = fmt.Fprintf(out, "%s [y/N]: ", question) + var confirm string + _, _ = fmt.Scanln(&confirm) + yes = confirm == "y" || confirm == "Y" } - return true + if !yes { + _, _ = fmt.Fprintln(out, "Canceled.") + } + return yes } diff --git a/cmd/login.go b/cmd/login.go index 8ccc89f..9fec671 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -7,6 +7,7 @@ import ( "github.com/jbrazda/iics-cli/internal/client" "github.com/jbrazda/iics-cli/internal/config" + "github.com/jbrazda/iics-cli/internal/tui" "github.com/spf13/cobra" ) @@ -42,10 +43,8 @@ The session is cached locally so subsequent commands don't require re-authentica if !config.IsTerminal() { return fmt.Errorf("profile %q not found; run 'iics profile add %s' to create it", profileName, profileName) } - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Profile %q does not exist. Create it now? [Y/n]: ", profileName) - var answer string - _, _ = fmt.Scanln(&answer) - if answer != "" && answer != "y" && answer != "Y" { + create, _ := tui.KeyConfirm(os.Stdin, cmd.OutOrStdout(), fmt.Sprintf("Profile %q does not exist. Create it now?", profileName), true) + if !create { return fmt.Errorf("profile %q not found; run 'iics profile add %s' to create it", profileName, profileName) } res, promptErr := promptProfile(cfg, profileName) diff --git a/cmd/role_prompt.go b/cmd/role_prompt.go index 87fe8c2..6b79e08 100644 --- a/cmd/role_prompt.go +++ b/cmd/role_prompt.go @@ -1,7 +1,6 @@ package cmd import ( - "bufio" "context" "errors" "fmt" @@ -25,14 +24,12 @@ func isInteractiveTTY() bool { return config.IsTerminal() && term.IsTerminal(int(os.Stdout.Fd())) } -// promptShowPrivileges asks whether to display the role's privileges. Only -// "y" or "yes" answers yes; anything else (including q or Enter) quits. +// promptShowPrivileges asks whether to display the role's privileges. Only y +// answers yes; anything else (n, Enter, Esc, q or ctrl+c) quits. func promptShowPrivileges() bool { - _, _ = fmt.Fprint(os.Stderr, "\nDisplay role privileges? [y/Q]: ") - // A read error (e.g. Ctrl-D) leaves line empty, which is treated as quit. - line, _ := bufio.NewReader(os.Stdin).ReadString('\n') - line = strings.ToLower(strings.TrimSpace(line)) - return line == "y" || line == "yes" + _, _ = fmt.Fprintln(os.Stderr) + yes, _ := tui.KeyConfirm(os.Stdin, os.Stderr, "Display role privileges?", false) + return yes } // pickRole lists the roles in a filterable menu and lets the operator choose diff --git a/cmd/user.go b/cmd/user.go index 5c52628..2aa37ce 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -838,13 +838,7 @@ func newUserDeleteCmd() *cobra.Command { } if !yes { - _, _ = fmt.Fprintf(os.Stderr, - "Are you sure you want to delete user %s (%s)? [y/N]: ", - target.UserName, target.ID) - var confirm string - _, _ = fmt.Scanln(&confirm) - if confirm != "y" && confirm != "Y" { - _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Canceled.") + if !confirmAction(cmd, fmt.Sprintf("Are you sure you want to delete user %s (%s)?", target.UserName, target.ID)) { return nil } } diff --git a/docs/ChangeRequests/pending/CR-0048-prompt-cancel-keys-and-raw-key-confirm.md b/docs/ChangeRequests/pending/CR-0048-prompt-cancel-keys-and-raw-key-confirm.md new file mode 100644 index 0000000..664f6e6 --- /dev/null +++ b/docs/ChangeRequests/pending/CR-0048-prompt-cancel-keys-and-raw-key-confirm.md @@ -0,0 +1,53 @@ +# CR-0048: Esc and q cancel prompts; single-key confirmations + +## CR Type + +- [x] Enhancement (interactive prompts, menu) + +## Problem + +- Prompts that read a line from the terminal (`[y/N]` confirmations, the + menu's "Press Enter to return" pause, `login` profile creation, + `role get` privilege prompt) echo arrow keys as `^[[A` / `^[[B`. +- In a command started from the menu, pickers and text prompts can only be + left with `Ctrl+C`; `Esc` and `q` do nothing. + +## Pattern + +The common terminal UI convention (fzf, lazygit, k9s, gh): + +| Key | List, select, yes/no | Text input | List filter open or applied | +| -------- | -------------------- | ------------ | --------------------------- | +| `Esc` | Cancel the prompt | Cancel | Close or clear the filter | +| `q` | Cancel the prompt | Typed letter | Typed letter | +| `Ctrl+C` | Cancel | Cancel | Cancel | + +Single-key questions read keys in raw mode: nothing is echoed, escape +sequences (arrows, function keys) are skipped, `Esc` / `q` mean no or back. + +## Desired Change + +- `internal/tui/keys.go`: + - `ReadKey` reads one key in raw mode and skips escape sequences, also when + they arrive in the same read as a key. + - `KeyConfirm` asks a `[y/N]` / `[Y/n]` question answered with one key. + - `RunForm` runs a huh form with a Bubble Tea message filter that turns + `Esc` and `q` into a cancel, unless the focused field has an enabled + binding for `Esc` (filter open or applied); `q` passes through in text + inputs. +- `Prompter` (text, select, confirm, multi-select), the review select, the + ACL editor principal picker and the role privilege editor use `RunForm`. + Multi-page wizards and the `role create` form keep `Ctrl+C` only, so a + stray `Esc` does not discard entered values. +- On a terminal, `confirmAction`, `user delete`, `login` (create missing + profile), `role get` (show privileges) and the menu pause use single keys. + Without a terminal the line format is kept so answers can be piped. +- Menu pause: `Enter` or `Esc` returns to the menu, `q` or `Ctrl+C` quits. + +## Verification + +- Unit tests: `parseKey` (arrows, CSI with parameters, SS3, alt+key, UTF-8), + `cancelKeysFilter` for select (including filter mode), input and confirm. +- Pseudo-terminal checks: arrow keys then `n` or `y` on a delete confirmation; + `Esc` on the `folder list --pick-project` filter prompt and picker. +- Manual check in a real terminal still recommended for the menu pause. diff --git a/docs/documentation/interactive.md b/docs/documentation/interactive.md index f098a1c..6e03702 100644 --- a/docs/documentation/interactive.md +++ b/docs/documentation/interactive.md @@ -11,13 +11,24 @@ terminal. ## Keys -| Prompt | Keys | -| ----------- | ----------------------------------------------------------------------------------------------------------- | -| Text input | Type, `Enter` to accept (an empty value keeps the shown default) | -| Single list | Arrows or `j` / `k` to move, `/` to filter (`Esc` clears the filter), `Enter` to choose, `Ctrl+C` to cancel | -| Checklist | `Space` or `x` to toggle, `/` to filter, `Ctrl+A` to select all, `Enter` to confirm | -| Yes / No | Left / right or `y` / `n`, `Enter` to confirm | -| Any prompt | `Ctrl+C` cancels the command | +| Prompt | Keys | +| ----------- | ------------------------------------------------------------------------------------- | +| Text input | Type, `Enter` to accept (an empty value keeps the shown default), `Esc` to cancel | +| Single list | Arrows or `j` / `k` to move, `/` to filter, `Enter` to choose, `Esc` or `q` to cancel | +| Checklist | `Space` or `x` to toggle, `/` to filter, `Ctrl+A` to select all, `Enter` to confirm | +| Yes / No | Left / right or `y` / `n`, `Enter` to confirm, `Esc` or `q` to cancel | +| Any prompt | `Ctrl+C` cancels the command | + +### Cancel keys + +- `Esc` cancels the prompt, like `Ctrl+C`. While a list filter is open or + applied, the first `Esc` closes or clears the filter instead. +- `q` cancels lists and yes/no prompts. In text inputs and while typing a + filter, `q` is typed as a letter. +- Multi-page wizards (`user create --interactive`, `user edit`, `profile add`, + `group create -i`, `environment create -i`, `config edit`) and the + `role create` form cancel only with `Ctrl+C`, so a stray `Esc` does not + discard what you have entered. Checklists open with the current values already checked (for example a user's current groups and roles), so confirming without changes keeps them. @@ -58,6 +69,9 @@ non-interactive commands (for example `user update-roles`) in scripts. ## Confirmations Destructive commands (`delete`, `package expand --clean`, -`environment configs set`) ask a plain `[y/N]` question even on a terminal, -so an answer can always be piped (`echo y | iics ...`). Only `y` or `Y` -proceeds. Use `--yes` / `-y` to skip the question. +`environment configs set`) ask a `[y/N]` question. On a terminal it takes a +single key: `y` proceeds; `n`, `Enter`, `Esc`, `q` or `Ctrl+C` cancels; arrow +and other special keys are ignored (they are no longer echoed as `^[[A`). +When stdin is not a terminal one line is read, so an answer can be piped +(`echo y | iics ...`); only `y` or `Y` proceeds. Use `--yes` / `-y` to skip +the question. diff --git a/docs/documentation/menu.md b/docs/documentation/menu.md index e03e27d..f0dcacc 100644 --- a/docs/documentation/menu.md +++ b/docs/documentation/menu.md @@ -83,9 +83,13 @@ enter run · / filter · p profile · ? help · q quit typed it. Global flags the menu was started with (for example `--config`, `--theme`, `--no-color`) are passed on. 3. When it finishes, `✓ done` or `✗ failed (exit N)` is shown. Press `Enter` - to return to the menu (on the last used entry) or `q` to quit. + or `Esc` to return to the menu (on the last used entry), or `q` to quit. + Other keys, such as arrows, are ignored. `Ctrl+C` while a command runs cancels only that command; the menu stays open. +In a command's pickers and yes/no prompts, `Esc` or `q` also cancel the +command and return to the menu; see +[Interactive prompts](interactive.md#cancel-keys). ## Profiles diff --git a/internal/menu/loop.go b/internal/menu/loop.go index 5597e83..17b7bae 100644 --- a/internal/menu/loop.go +++ b/internal/menu/loop.go @@ -13,6 +13,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/jbrazda/iics-cli/internal/tui" + "golang.org/x/term" ) // Runner runs a command line and returns its exit code. @@ -94,10 +95,27 @@ func DefaultShowMenu(m *Model) (Result, error) { return m.Result(), nil } -// DefaultPause asks the user to press Enter (menu) or q (quit). +// DefaultPause asks the user to press Enter or Esc (menu) or q (quit). On a +// terminal it reads single keys, ignoring arrow keys; otherwise one line. func DefaultPause(out io.Writer) func() (bool, error) { return func() (bool, error) { - _, _ = fmt.Fprint(out, "Press Enter to return to the menu, q to quit: ") + _, _ = fmt.Fprint(out, "Press Enter or Esc to return to the menu, q to quit: ") + if term.IsTerminal(int(os.Stdin.Fd())) { + for { + k, r, err := tui.ReadKey(os.Stdin) + if err != nil { + return true, err + } + switch { + case k == tui.KeyEnter || k == tui.KeyEsc: + _, _ = fmt.Fprintln(out) + return false, nil + case k == tui.KeyCancel || r == 'q' || r == 'Q': + _, _ = fmt.Fprintln(out) + return true, nil + } + } + } line, err := bufio.NewReader(os.Stdin).ReadString('\n') if err != nil && line == "" { return true, nil diff --git a/internal/menu/model.go b/internal/menu/model.go index 61bdf31..75fbbeb 100644 --- a/internal/menu/model.go +++ b/internal/menu/model.go @@ -395,7 +395,7 @@ func (m *Model) View() string { "↑/↓ k/j move pgup/pgdn page g/G first/last", "enter run entry / filter esc clear filter", "p switch profile ? hide help", - "q quit ctrl+c (in a command) cancels only that command", + "q quit esc/q/ctrl+c (in a command) cancel only that command", "✎ marks entries that change data; on a PRODUCTION profile they ask first.", } { b.WriteString(m.styles.dim.Render(truncate(l, w))) diff --git a/internal/tui/acleditor.go b/internal/tui/acleditor.go index 705d3b4..fcc645d 100644 --- a/internal/tui/acleditor.go +++ b/internal/tui/acleditor.go @@ -235,12 +235,12 @@ func pickPrincipal(in ACLEditorInput, rows []aclRow) (client.ACLPrincipal, bool, var choice string sel := huh.NewSelect[string](). Title("Add a user group or user"). - Description("/ to filter, ctrl+c to go back"). + Description("/ to filter, esc or q to go back"). Options(opts...). Height(min(len(opts)+3, 18)). Value(&choice) - err := huh.NewForm(huh.NewGroup(sel)).WithOutput(in.Out).WithAccessible(in.Accessible).Run() - if errors.Is(err, huh.ErrUserAborted) { + err := RunForm(huh.NewForm(huh.NewGroup(sel)), in.Out, in.Accessible) + if errors.Is(err, ErrCanceled) { return client.ACLPrincipal{}, false, nil } if err != nil { diff --git a/internal/tui/keys.go b/internal/tui/keys.go new file mode 100644 index 0000000..c63e9ff --- /dev/null +++ b/internal/tui/keys.go @@ -0,0 +1,182 @@ +package tui + +import ( + "errors" + "fmt" + "io" + "os" + "unicode/utf8" + + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/huh" + "golang.org/x/term" +) + +// Key is a single key press read by ReadKey. +type Key int + +// Keys returned by ReadKey. +const ( + KeyRune Key = iota // a printable character + KeyEnter // Enter + KeyEsc // Esc on its own + KeyCancel // ctrl+c or ctrl+d +) + +// parseKey reads the first key in b and returns the number of bytes it +// used. Escape sequences such as arrow and function keys are consumed and +// reported with ok false, as are other control characters. +func parseKey(b []byte) (k Key, r rune, ok bool, n int) { + if len(b) == 0 { + return 0, 0, false, 0 + } + switch b[0] { + case 0x1b: + if len(b) == 1 { + return KeyEsc, 0, true, 1 + } + switch b[1] { + case '[': // CSI: parameters, then a final byte in 0x40-0x7e + for i := 2; i < len(b); i++ { + if b[i] >= 0x40 && b[i] <= 0x7e { + return 0, 0, false, i + 1 + } + } + return 0, 0, false, len(b) + case 'O': // SS3: one more byte (F1-F4, keypad) + return 0, 0, false, min(3, len(b)) + } + return 0, 0, false, 2 // alt+key + case 0x03, 0x04: + return KeyCancel, 0, true, 1 + case '\r', '\n': + return KeyEnter, 0, true, 1 + } + r, size := utf8.DecodeRune(b) + if r == utf8.RuneError || r < 0x20 { + return 0, 0, false, size + } + return KeyRune, r, true, size +} + +// ReadKey reads one key press from a terminal in raw mode, so nothing is +// echoed. Arrow keys, function keys and other control input are skipped, +// also when they arrive in the same read as the key. +func ReadKey(in *os.File) (Key, rune, error) { + fd := int(in.Fd()) + state, err := term.MakeRaw(fd) + if err != nil { + return 0, 0, fmt.Errorf("reading key: %w", err) + } + defer func() { _ = term.Restore(fd, state) }() + + buf := make([]byte, 32) + for { + n, err := in.Read(buf) + if err != nil { + if err == io.EOF { + return KeyCancel, 0, nil + } + return 0, 0, fmt.Errorf("reading key: %w", err) + } + // Return the first real key; anything typed after it in the same + // read is dropped, which is fine for single-key prompts. + for b := buf[:n]; len(b) > 0; { + k, r, ok, used := parseKey(b) + if ok { + return k, r, nil + } + b = b[used:] + } + } +} + +// KeyConfirm asks a yes/no question answered with a single key on a +// terminal: y or n, Enter for the default, Esc, q or ctrl+c for no. Other +// keys, including arrows, are ignored. The answer is echoed as "yes"/"no". +func KeyConfirm(in *os.File, out io.Writer, label string, defaultYes bool) (bool, error) { + hint := "[y/N]" + if defaultYes { + hint = "[Y/n]" + } + _, _ = fmt.Fprintf(out, "%s %s: ", label, hint) + for { + k, r, err := ReadKey(in) + if err != nil { + return false, err + } + answer, done := false, true + switch { + case k == KeyEnter: + answer = defaultYes + case k == KeyEsc || k == KeyCancel: + case r == 'y' || r == 'Y': + answer = true + case r == 'n' || r == 'N' || r == 'q' || r == 'Q': + default: + done = false + } + if done { + word := "no" + if answer { + word = "yes" + } + _, _ = fmt.Fprintln(out, word) + return answer, nil + } + } +} + +// cancelKeysFilter makes Esc and q cancel a huh form the same way ctrl+c +// does, unless the focused field uses the key itself: +// - Esc is left to the field while it has an enabled binding for it, such +// as closing or clearing a list filter. +// - q cancels only list and confirm fields, and not while a filter is +// active; in text inputs it is typed. +func cancelKeysFilter(m tea.Model, msg tea.Msg) tea.Msg { + k, ok := msg.(tea.KeyMsg) + if !ok { + return msg + } + s := k.String() + if s != "esc" && s != "q" { + return msg + } + form, ok := m.(*huh.Form) + if !ok { + return msg + } + field := form.GetFocusedField() + if field == nil { + return msg + } + esc := tea.KeyMsg{Type: tea.KeyEscape} + for _, b := range field.KeyBinds() { + if b.Enabled() && key.Matches(esc, b) { + // The field handles Esc (filter active); leave both keys to it. + return msg + } + } + if s == "q" { + switch field.(type) { + case *huh.Input, *huh.Text: + return msg + } + } + return tea.KeyMsg{Type: tea.KeyCtrlC} +} + +// RunForm runs a single-step form with Esc and q as cancel keys (see +// cancelKeysFilter). Canceling returns ErrCanceled. +func RunForm(form *huh.Form, out io.Writer, accessible bool) error { + err := form. + WithProgramOptions(tea.WithReportFocus(), tea.WithFilter(cancelKeysFilter)). + WithOutput(out). + WithAccessible(accessible). + Run() + if errors.Is(err, huh.ErrUserAborted) { + return ErrCanceled + } + return err +} diff --git a/internal/tui/keys_test.go b/internal/tui/keys_test.go new file mode 100644 index 0000000..952bb92 --- /dev/null +++ b/internal/tui/keys_test.go @@ -0,0 +1,106 @@ +package tui + +import ( + "testing" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/huh" +) + +func TestParseKey(t *testing.T) { + tests := []struct { + in string + key Key + r rune + wantOK bool + used int + }{ + {"\r", KeyEnter, 0, true, 1}, + {"\n", KeyEnter, 0, true, 1}, + {"\x1b", KeyEsc, 0, true, 1}, + {"\x1b[A", 0, 0, false, 3}, // arrow up + {"\x1b[1;5Ay", 0, 0, false, 6}, // ctrl+arrow, then y + {"\x1bOP", 0, 0, false, 3}, // F1 + {"\x1bx", 0, 0, false, 2}, // alt+x + {"\x03", KeyCancel, 0, true, 1}, + {"\x04", KeyCancel, 0, true, 1}, + {"y", KeyRune, 'y', true, 1}, + {"Q", KeyRune, 'Q', true, 1}, + {"é", KeyRune, 'é', true, 2}, + {"\x01", 0, 0, false, 1}, + {"", 0, 0, false, 0}, + } + for _, tt := range tests { + k, r, ok, used := parseKey([]byte(tt.in)) + if ok != tt.wantOK || used != tt.used || (ok && (k != tt.key || r != tt.r)) { + t.Errorf("parseKey(%q) = (%v, %q, %v, %d), want (%v, %q, %v, %d)", tt.in, k, r, ok, used, tt.key, tt.r, tt.wantOK, tt.used) + } + } +} + +func keyMsg(s string) tea.KeyMsg { + switch s { + case "esc": + return tea.KeyMsg{Type: tea.KeyEscape} + case "enter": + return tea.KeyMsg{Type: tea.KeyEnter} + } + return tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(s)} +} + +func isCancel(msg tea.Msg) bool { + k, ok := msg.(tea.KeyMsg) + return ok && k.Type == tea.KeyCtrlC +} + +func newSelectForm() *huh.Form { + v := 0 + f := huh.NewForm(huh.NewGroup(huh.NewSelect[int](). + Options(huh.NewOption("alpha", 0), huh.NewOption("beta", 1)). + Value(&v))) + f.Init() + return f +} + +func TestCancelKeysFilterSelect(t *testing.T) { + f := newSelectForm() + if !isCancel(cancelKeysFilter(f, keyMsg("esc"))) { + t.Error("esc on a select should cancel") + } + if !isCancel(cancelKeysFilter(f, keyMsg("q"))) { + t.Error("q on a select should cancel") + } + if isCancel(cancelKeysFilter(f, keyMsg("j"))) { + t.Error("other keys must pass through") + } + + // Start filtering: esc and q belong to the filter now. + _, _ = f.Update(keyMsg("/")) + if isCancel(cancelKeysFilter(f, keyMsg("esc"))) { + t.Error("esc while filtering should close the filter, not cancel") + } + if isCancel(cancelKeysFilter(f, keyMsg("q"))) { + t.Error("q while filtering should be typed into the filter") + } +} + +func TestCancelKeysFilterInput(t *testing.T) { + v := "" + f := huh.NewForm(huh.NewGroup(huh.NewInput().Value(&v))) + f.Init() + if !isCancel(cancelKeysFilter(f, keyMsg("esc"))) { + t.Error("esc on an input should cancel") + } + if isCancel(cancelKeysFilter(f, keyMsg("q"))) { + t.Error("q on an input must be typed") + } +} + +func TestCancelKeysFilterConfirm(t *testing.T) { + v := false + f := huh.NewForm(huh.NewGroup(huh.NewConfirm().Value(&v))) + f.Init() + if !isCancel(cancelKeysFilter(f, keyMsg("q"))) { + t.Error("q on a confirm should cancel") + } +} diff --git a/internal/tui/prompt.go b/internal/tui/prompt.go index e184de3..b929070 100644 --- a/internal/tui/prompt.go +++ b/internal/tui/prompt.go @@ -57,14 +57,7 @@ func (p *Prompter) line() (string, error) { } func (p *Prompter) run(fields ...huh.Field) error { - err := huh.NewForm(huh.NewGroup(fields...)). - WithOutput(p.Out). - WithAccessible(p.Accessible). - Run() - if errors.Is(err, huh.ErrUserAborted) { - return ErrCanceled - } - return err + return RunForm(huh.NewForm(huh.NewGroup(fields...)), p.Out, p.Accessible) } func listHeight(n int) int { @@ -136,7 +129,7 @@ func (p *Prompter) Select(label string, options []string) (int, error) { } sel := huh.NewSelect[int](). Title(label). - Description("/ to filter, ctrl+c to cancel"). + Description("/ to filter, esc or q to cancel"). Options(opts...). Height(listHeight(len(options)) + 1). Value(&idx) diff --git a/internal/tui/review.go b/internal/tui/review.go index 2cd9ea1..c8367f8 100644 --- a/internal/tui/review.go +++ b/internal/tui/review.go @@ -35,8 +35,8 @@ func confirmReview(out io.Writer, accessible bool, title string, lines []string, Description(strings.Join(lines, "\n")). Options(options...). Value(&choice) - err := huh.NewForm(huh.NewGroup(sel)).WithOutput(out).WithAccessible(accessible).Run() - if errors.Is(err, huh.ErrUserAborted) { + err := RunForm(huh.NewForm(huh.NewGroup(sel)), out, accessible) + if errors.Is(err, ErrCanceled) { return reviewCancel, nil } if err != nil { diff --git a/internal/tui/roleeditor.go b/internal/tui/roleeditor.go index b42dd37..eb014d5 100644 --- a/internal/tui/roleeditor.go +++ b/internal/tui/roleeditor.go @@ -259,11 +259,7 @@ func filterServices(services []client.PrivilegeService, assigned map[string]bool } func runForm(f *huh.Form) error { - err := f.WithOutput(os.Stderr).Run() - if errors.Is(err, huh.ErrUserAborted) { - return ErrCanceled - } - return err + return RunForm(f, os.Stderr, false) } func toSet(names []string) map[string]bool { From 779e5deae52ec869149e66bba7f64ec9365d49a0 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Mon, 19 Oct 2026 18:29:59 -0400 Subject: [PATCH 34/52] feat: Esc cancels wizards; cancel-key hint under prompts (CR-0048) - wizards (user, profile, group, environment, config) and role create cancel with Esc or ctrl+c; q is always typed since they have text inputs - forms run inside a small Bubble Tea model that applies the cancel keys and renders a hint line: "esc, q (in lists) or ctrl+c to cancel" for pickers and questions, "esc or ctrl+c to cancel" for wizards --- cmd/role_prompt.go | 4 +- ...-prompt-cancel-keys-and-raw-key-confirm.md | 16 +++- docs/documentation/interactive.md | 11 ++- internal/tui/acleditor.go | 2 +- internal/tui/configwizard.go | 4 +- internal/tui/groupwizard.go | 2 +- internal/tui/keys.go | 81 +++++++++++++++---- internal/tui/keys_test.go | 32 +++++--- internal/tui/profilewizard.go | 4 +- internal/tui/prompt.go | 2 +- internal/tui/runtimewizard.go | 2 +- internal/tui/userwizard.go | 4 +- 12 files changed, 118 insertions(+), 46 deletions(-) diff --git a/cmd/role_prompt.go b/cmd/role_prompt.go index 6b79e08..ee22e2d 100644 --- a/cmd/role_prompt.go +++ b/cmd/role_prompt.go @@ -204,8 +204,8 @@ func runRoleCreateWizard(ctx context.Context, c *client.Client, body *client.Cre Height(12). Value(&cloneIdx), ), - ).WithOutput(os.Stderr) - if err = form.Run(); err != nil { + ) + if err = tui.RunWizard(form, os.Stderr, prompter.Accessible); err != nil { if errors.Is(err, huh.ErrUserAborted) { return false, nil } diff --git a/docs/ChangeRequests/pending/CR-0048-prompt-cancel-keys-and-raw-key-confirm.md b/docs/ChangeRequests/pending/CR-0048-prompt-cancel-keys-and-raw-key-confirm.md index 664f6e6..7ae44c0 100644 --- a/docs/ChangeRequests/pending/CR-0048-prompt-cancel-keys-and-raw-key-confirm.md +++ b/docs/ChangeRequests/pending/CR-0048-prompt-cancel-keys-and-raw-key-confirm.md @@ -36,9 +36,15 @@ sequences (arrows, function keys) are skipped, `Esc` / `q` mean no or back. binding for `Esc` (filter open or applied); `q` passes through in text inputs. - `Prompter` (text, select, confirm, multi-select), the review select, the - ACL editor principal picker and the role privilege editor use `RunForm`. - Multi-page wizards and the `role create` form keep `Ctrl+C` only, so a - stray `Esc` does not discard entered values. + ACL editor principal picker and the role privilege editor use `RunForm` + (`Esc`, `q` in lists, `Ctrl+C`). +- Wizards (user, profile, group, runtime environment, config) and the + `role create` form use `RunWizard`: `Esc` or `Ctrl+C` cancels; `q` is + always typed because these forms contain text inputs. +- Both run the huh form inside a small Bubble Tea model that applies the + cancel keys and renders a hint line under the form's key help: + `esc, q (in lists) or ctrl+c to cancel` or `esc or ctrl+c to cancel`. + Accessible mode keeps huh's line prompts. - On a terminal, `confirmAction`, `user delete`, `login` (create missing profile), `role get` (show privileges) and the menu pause use single keys. Without a terminal the line format is kept so answers can be piped. @@ -49,5 +55,7 @@ sequences (arrows, function keys) are skipped, `Esc` / `q` mean no or back. - Unit tests: `parseKey` (arrows, CSI with parameters, SS3, alt+key, UTF-8), `cancelKeysFilter` for select (including filter mode), input and confirm. - Pseudo-terminal checks: arrow keys then `n` or `y` on a delete confirmation; - `Esc` on the `folder list --pick-project` filter prompt and picker. + `Esc` on the `folder list --pick-project` filter prompt; `q` on its project + list; `profile add` with a scratch config: `q` typed into the text field, + `Esc` cancels, config unchanged; hint lines shown. - Manual check in a real terminal still recommended for the menu pause. diff --git a/docs/documentation/interactive.md b/docs/documentation/interactive.md index 6e03702..5dd8ace 100644 --- a/docs/documentation/interactive.md +++ b/docs/documentation/interactive.md @@ -25,10 +25,13 @@ terminal. applied, the first `Esc` closes or clears the filter instead. - `q` cancels lists and yes/no prompts. In text inputs and while typing a filter, `q` is typed as a letter. -- Multi-page wizards (`user create --interactive`, `user edit`, `profile add`, - `group create -i`, `environment create -i`, `config edit`) and the - `role create` form cancel only with `Ctrl+C`, so a stray `Esc` does not - discard what you have entered. +- Wizards and forms with text fields (`user create --interactive`, + `user edit`, `profile add`/`edit`, `group create -i`, `environment create -i`, + `config edit`, `role create`) cancel with `Esc` or `Ctrl+C`. `q` is always + typed there, also on their list pages. +- A hint under each prompt shows the cancel keys: `esc, q (in lists) or + ctrl+c to cancel` for pickers and questions, `esc or ctrl+c to cancel` for + wizards. Checklists open with the current values already checked (for example a user's current groups and roles), so confirming without changes keeps them. diff --git a/internal/tui/acleditor.go b/internal/tui/acleditor.go index fcc645d..8bf06df 100644 --- a/internal/tui/acleditor.go +++ b/internal/tui/acleditor.go @@ -235,7 +235,7 @@ func pickPrincipal(in ACLEditorInput, rows []aclRow) (client.ACLPrincipal, bool, var choice string sel := huh.NewSelect[string](). Title("Add a user group or user"). - Description("/ to filter, esc or q to go back"). + Description("/ to filter"). Options(opts...). Height(min(len(opts)+3, 18)). Value(&choice) diff --git a/internal/tui/configwizard.go b/internal/tui/configwizard.go index 1787358..daca691 100644 --- a/internal/tui/configwizard.go +++ b/internal/tui/configwizard.go @@ -39,8 +39,8 @@ type configForm struct { func RunConfigWizard(in ConfigWizardInput) (bool, error) { f := newConfigForm(in.Config) for { - form := huh.NewForm(configPages(in, f)...).WithOutput(in.Out).WithAccessible(in.Accessible) - if err := form.Run(); err != nil { + form := huh.NewForm(configPages(in, f)...) + if err := RunWizard(form, in.Out, in.Accessible); err != nil { if errors.Is(err, huh.ErrUserAborted) { return false, nil } diff --git a/internal/tui/groupwizard.go b/internal/tui/groupwizard.go index 71f5626..05a6f87 100644 --- a/internal/tui/groupwizard.go +++ b/internal/tui/groupwizard.go @@ -58,7 +58,7 @@ func RunGroupWizard(in GroupWizardInput) (bool, error) { huh.NewGroup(rolesField(in.Roles, &roleIDs)).Title("Roles"), ) } - err := huh.NewForm(groups...).WithOutput(in.Out).WithAccessible(in.Accessible).Run() + err := RunWizard(huh.NewForm(groups...), in.Out, in.Accessible) if errors.Is(err, huh.ErrUserAborted) { return false, nil } diff --git a/internal/tui/keys.go b/internal/tui/keys.go index c63e9ff..b391283 100644 --- a/internal/tui/keys.go +++ b/internal/tui/keys.go @@ -10,6 +10,7 @@ import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" "golang.org/x/term" ) @@ -128,23 +129,19 @@ func KeyConfirm(in *os.File, out io.Writer, label string, defaultYes bool) (bool } } -// cancelKeysFilter makes Esc and q cancel a huh form the same way ctrl+c -// does, unless the focused field uses the key itself: +// cancelKey turns Esc (and q when allowQ) into ctrl+c, which makes a huh +// form cancel, unless the focused field uses the key itself: // - Esc is left to the field while it has an enabled binding for it, such // as closing or clearing a list filter. // - q cancels only list and confirm fields, and not while a filter is // active; in text inputs it is typed. -func cancelKeysFilter(m tea.Model, msg tea.Msg) tea.Msg { +func cancelKey(form *huh.Form, msg tea.Msg, allowQ bool) tea.Msg { k, ok := msg.(tea.KeyMsg) if !ok { return msg } s := k.String() - if s != "esc" && s != "q" { - return msg - } - form, ok := m.(*huh.Form) - if !ok { + if s != "esc" && (s != "q" || !allowQ) { return msg } field := form.GetFocusedField() @@ -167,16 +164,70 @@ func cancelKeysFilter(m tea.Model, msg tea.Msg) tea.Msg { return tea.KeyMsg{Type: tea.KeyCtrlC} } -// RunForm runs a single-step form with Esc and q as cancel keys (see -// cancelKeysFilter). Canceling returns ErrCanceled. +var hintStyle = lipgloss.NewStyle().Faint(true) + +// cancelModel runs a huh form, applies the cancel keys and shows a hint +// below the form's own key help. +type cancelModel struct { + form *huh.Form + allowQ bool + hint string +} + +func (m cancelModel) Init() tea.Cmd { return m.form.Init() } + +func (m cancelModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + _, cmd := m.form.Update(cancelKey(m.form, msg, m.allowQ)) + if m.form.State != huh.StateNormal { + return m, tea.Quit + } + return m, cmd +} + +func (m cancelModel) View() string { + if m.form.State != huh.StateNormal { + return "" + } + return m.form.View() + "\n" + hintStyle.Render(m.hint) +} + +// runCancelable runs form with Esc (and q when allowQ) as cancel keys and +// returns huh.ErrUserAborted when canceled. Accessible mode uses huh's line +// prompts unchanged. +func runCancelable(form *huh.Form, out io.Writer, accessible, allowQ bool) error { + if accessible { + return form.WithOutput(out).WithAccessible(true).Run() + } + hint := "esc or ctrl+c to cancel" + if allowQ { + hint = "esc, q (in lists) or ctrl+c to cancel" + } + // Leave quitting to cancelModel, which watches the form state. + form.SubmitCmd = nil + form.CancelCmd = nil + m := cancelModel{form: form, allowQ: allowQ, hint: hint} + if _, err := tea.NewProgram(m, tea.WithOutput(out), tea.WithReportFocus()).Run(); err != nil { + return fmt.Errorf("running form: %w", err) + } + if form.State == huh.StateAborted { + return huh.ErrUserAborted + } + return nil +} + +// RunForm runs a single-step form (picker, question): Esc cancels, and q +// cancels lists and yes/no prompts. Canceling returns ErrCanceled. func RunForm(form *huh.Form, out io.Writer, accessible bool) error { - err := form. - WithProgramOptions(tea.WithReportFocus(), tea.WithFilter(cancelKeysFilter)). - WithOutput(out). - WithAccessible(accessible). - Run() + err := runCancelable(form, out, accessible, true) if errors.Is(err, huh.ErrUserAborted) { return ErrCanceled } return err } + +// RunWizard runs a multi-field wizard: Esc or ctrl+c cancels; q is always +// typed because wizards contain text inputs. Canceling returns +// huh.ErrUserAborted. +func RunWizard(form *huh.Form, out io.Writer, accessible bool) error { + return runCancelable(form, out, accessible, false) +} diff --git a/internal/tui/keys_test.go b/internal/tui/keys_test.go index 952bb92..9e07f02 100644 --- a/internal/tui/keys_test.go +++ b/internal/tui/keys_test.go @@ -62,45 +62,55 @@ func newSelectForm() *huh.Form { return f } -func TestCancelKeysFilterSelect(t *testing.T) { +func TestCancelKeySelect(t *testing.T) { f := newSelectForm() - if !isCancel(cancelKeysFilter(f, keyMsg("esc"))) { + if !isCancel(cancelKey(f, keyMsg("esc"), true)) { t.Error("esc on a select should cancel") } - if !isCancel(cancelKeysFilter(f, keyMsg("q"))) { + if !isCancel(cancelKey(f, keyMsg("q"), true)) { t.Error("q on a select should cancel") } - if isCancel(cancelKeysFilter(f, keyMsg("j"))) { + if isCancel(cancelKey(f, keyMsg("j"), true)) { t.Error("other keys must pass through") } // Start filtering: esc and q belong to the filter now. _, _ = f.Update(keyMsg("/")) - if isCancel(cancelKeysFilter(f, keyMsg("esc"))) { + if isCancel(cancelKey(f, keyMsg("esc"), true)) { t.Error("esc while filtering should close the filter, not cancel") } - if isCancel(cancelKeysFilter(f, keyMsg("q"))) { + if isCancel(cancelKey(f, keyMsg("q"), true)) { t.Error("q while filtering should be typed into the filter") } } -func TestCancelKeysFilterInput(t *testing.T) { +func TestCancelKeyInput(t *testing.T) { v := "" f := huh.NewForm(huh.NewGroup(huh.NewInput().Value(&v))) f.Init() - if !isCancel(cancelKeysFilter(f, keyMsg("esc"))) { + if !isCancel(cancelKey(f, keyMsg("esc"), true)) { t.Error("esc on an input should cancel") } - if isCancel(cancelKeysFilter(f, keyMsg("q"))) { + if isCancel(cancelKey(f, keyMsg("q"), true)) { t.Error("q on an input must be typed") } } -func TestCancelKeysFilterConfirm(t *testing.T) { +func TestCancelKeyConfirm(t *testing.T) { v := false f := huh.NewForm(huh.NewGroup(huh.NewConfirm().Value(&v))) f.Init() - if !isCancel(cancelKeysFilter(f, keyMsg("q"))) { + if !isCancel(cancelKey(f, keyMsg("q"), true)) { t.Error("q on a confirm should cancel") } } + +func TestCancelKeyWizardIgnoresQ(t *testing.T) { + f := newSelectForm() + if isCancel(cancelKey(f, keyMsg("q"), false)) { + t.Error("q must not cancel a wizard") + } + if !isCancel(cancelKey(f, keyMsg("esc"), false)) { + t.Error("esc should cancel a wizard") + } +} diff --git a/internal/tui/profilewizard.go b/internal/tui/profilewizard.go index e69a732..53db55e 100644 --- a/internal/tui/profilewizard.go +++ b/internal/tui/profilewizard.go @@ -56,8 +56,8 @@ type profileForm struct { func RunProfileWizard(in ProfileWizardInput) (*ProfileWizardResult, error) { f := newProfileForm(in) for { - form := huh.NewForm(profilePages(in, f)...).WithOutput(in.Out).WithAccessible(in.Accessible) - if err := form.Run(); err != nil { + form := huh.NewForm(profilePages(in, f)...) + if err := RunWizard(form, in.Out, in.Accessible); err != nil { if errors.Is(err, huh.ErrUserAborted) { return nil, nil } diff --git a/internal/tui/prompt.go b/internal/tui/prompt.go index b929070..f7dc4e4 100644 --- a/internal/tui/prompt.go +++ b/internal/tui/prompt.go @@ -129,7 +129,7 @@ func (p *Prompter) Select(label string, options []string) (int, error) { } sel := huh.NewSelect[int](). Title(label). - Description("/ to filter, esc or q to cancel"). + Description("/ to filter"). Options(opts...). Height(listHeight(len(options)) + 1). Value(&idx) diff --git a/internal/tui/runtimewizard.go b/internal/tui/runtimewizard.go index af3c384..7769fdb 100644 --- a/internal/tui/runtimewizard.go +++ b/internal/tui/runtimewizard.go @@ -67,7 +67,7 @@ func RunRuntimeWizard(in RuntimeWizardInput) (bool, error) { huh.NewNote().Title("Agents").Description("No unassigned Secure Agents are available. Agents can be added later."), )) } - err := huh.NewForm(fields...).WithOutput(in.Out).WithAccessible(in.Accessible).Run() + err := RunWizard(huh.NewForm(fields...), in.Out, in.Accessible) if errors.Is(err, huh.ErrUserAborted) { return false, nil } diff --git a/internal/tui/userwizard.go b/internal/tui/userwizard.go index f66bb55..aa3c3d8 100644 --- a/internal/tui/userwizard.go +++ b/internal/tui/userwizard.go @@ -75,8 +75,8 @@ func RunUserWizard(in UserWizardInput) (bool, error) { // Only role and user group assignments can be updated. pages = []*huh.Group{membershipGroup(in, direct, &groupIDs, &roleIDs)} } - form := huh.NewForm(pages...).WithOutput(in.Out).WithAccessible(in.Accessible).WithKeyMap(suggestionKeyMap()) - if err := form.Run(); err != nil { + form := huh.NewForm(pages...).WithKeyMap(suggestionKeyMap()) + if err := RunWizard(form, in.Out, in.Accessible); err != nil { if errors.Is(err, huh.ErrUserAborted) { return false, nil } From 5ca7773e485c364e35390eb2bed362cd59d287b6 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Mon, 19 Oct 2026 18:31:47 -0400 Subject: [PATCH 35/52] chore: move CR-0048 to completed --- .../CR-0048-prompt-cancel-keys-and-raw-key-confirm.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/ChangeRequests/{pending => completed}/CR-0048-prompt-cancel-keys-and-raw-key-confirm.md (100%) diff --git a/docs/ChangeRequests/pending/CR-0048-prompt-cancel-keys-and-raw-key-confirm.md b/docs/ChangeRequests/completed/CR-0048-prompt-cancel-keys-and-raw-key-confirm.md similarity index 100% rename from docs/ChangeRequests/pending/CR-0048-prompt-cancel-keys-and-raw-key-confirm.md rename to docs/ChangeRequests/completed/CR-0048-prompt-cancel-keys-and-raw-key-confirm.md From c0270cc9169632e50a9486c13d015e73cc5a441b Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Mon, 19 Oct 2026 23:08:42 -0400 Subject: [PATCH 36/52] feat: agent configs get/set for agent-level service properties (CR-0049) Read and change Secure Agent service properties of a single agent, such as the Process Server cluster and database settings. There is no REST API for this; the commands use the Administrator form /runtime/app/agent.do with the REST session as USER_SESSION cookie and a double-submit CSRF token, submit the whole form as the browser does, and verify the change by reading back. - agent configs get: filter by service, type, name regex, overridden - agent configs set: --set (value @file), --reset, --from-file, --dry-run, confirmation, refuses agents whose group has group-level properties unless --force, refuses agent system properties (server ignores them) - --debug masks Cookie and Set-Cookie headers --- README.md | 70 +- cmd/agent.go | 1 + cmd/agent_configs.go | 280 ++++++++ completions/iics.bash | 186 ++++++ .../CR-0049-agent-level-service-properties.md | 152 +++++ docs/documentation/agent.md | 147 ++++ docs/documentation/environment.md | 4 + internal/client/agentform.go | 628 ++++++++++++++++++ internal/client/agentform_test.go | 306 +++++++++ internal/client/client.go | 5 +- 10 files changed, 1743 insertions(+), 36 deletions(-) create mode 100644 cmd/agent_configs.go create mode 100644 docs/ChangeRequests/pending/CR-0049-agent-level-service-properties.md create mode 100644 internal/client/agentform.go create mode 100644 internal/client/agentform_test.go diff --git a/README.md b/README.md index 92cf44f..4bbdb43 100644 --- a/README.md +++ b/README.md @@ -265,41 +265,41 @@ it easy to override credentials in CI pipelines without touching the config file ## Commands -| Command | Alias | Subcommands | Description | -| ---------------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | -| [activitylog](docs/documentation/activitylog.md) | | `list`, `get` | Query activity logs for completed jobs | -| [agent](docs/documentation/agent.md) | | `list`, `get`, `details`, `delete`, `start`, `stop`, `restart`, `installer-info`, `installer-download` | Manage Secure Agents | -| [auditlog](docs/documentation/auditlog.md) | | `list` | Query organization audit log (V2 API) | -| [completion](docs/documentation/completion.md) | | `bash`, `zsh`, `fish`, `powershell` | Generate shell completion scripts | -| [config](docs/documentation/config.md) | | `edit` | Edit global settings (new user defaults, theme, timeout, menu) | -| [connection](docs/documentation/connection.md) | `conn` | `list`, `get`, `create`, `update`, `delete`, `test`, `search`, `objects`, `metadata` | Manage connections | -| [connector](docs/documentation/connector.md) | | `list`, `metadata` | List connectors and their connection attributes | -| [export](docs/documentation/export.md) | | `run`, `start`, `status`, `download`, `create` | Export asset packages | -| [folder](docs/documentation/folder.md) | | `list`, `create`, `update`, `delete` | Manage folders | -| [import](docs/documentation/import.md) | `imp` | `run`, `upload`, `start`, `status`, `download-log` | Import asset packages | -| [login](docs/documentation/login.md) | | | Authenticate and cache session | -| [logout](docs/documentation/logout.md) | | | Invalidate session | -| [lookup](docs/documentation/lookup.md) | | | Resolve object IDs, names, and paths | -| [metering](docs/documentation/metering.md) | | `get`, `download` | Query usage and metering data | -| [objects](docs/documentation/objects.md) | | `list`, `dependencies` | List/search assets, find dependencies | -| [package](docs/documentation/package.md) | | `expand`, `create`, `dependencies` | Extract, assemble, or inspect dependencies of IICS export packages | -| [menu](docs/documentation/menu.md) | `tui` | - | Interactive main menu (bare `iics` in a terminal) | -| [permission](docs/documentation/permission.md) | `perm` | `get`, `add`, `update`, `delete`, `set`, `check`, `edit` | Manage object-level permissions | -| [privilege](docs/documentation/privilege.md) | | `list` | List available privileges | -| [profile](docs/documentation/profile.md) | | `add`, `edit`, `list`, `delete`, `set-default`, `set-password`, `show` | Manage connection profiles | -| [project](docs/documentation/project.md) | | `list`, `create`, `update`, `delete` | Manage projects | -| [publish](docs/documentation/publish.md) | | `start`, `status`, `run` | Publish CAI assets to the runtime | -| [release](docs/documentation/release.md) | | `manifest`, `validate`, `plan` | Generate multi-format release manifests and per-environment plans | -| [role](docs/documentation/role.md) | | `list`, `get`, `create`, `edit`, `add-privileges`, `remove-privileges`, `delete` | Manage roles | -| [environment](docs/documentation/environment.md) | `runtime`, `rt`, `env` | `list`, `get`, `create`, `update`, `delete`, `configs` | Manage runtime environments (Secure Agent groups) | -| [schedule](docs/documentation/schedule.md) | | `list`, `get`, `create`, `update`, `delete` | Manage schedules | -| [securitylog](docs/documentation/securitylog.md) | | `list` | Query security audit log | -| [sourcecontrol](docs/documentation/sourcecontrol.md) | `sc` | `checkout`, `checkin`, `pull`, `commit` | Source control operations | -| [state](docs/documentation/state.md) | | `fetch`, `load` | Fetch/load object state snapshots | -| [tag](docs/documentation/tag.md) | | `assign`, `remove` | Assign/remove tags on objects | -| [unpublish](docs/documentation/unpublish.md) | | `start`, `status`, `run` | Unpublish CAI assets from the runtime | -| [user](docs/documentation/user.md) | | `list`, `get`, `create`, `edit`, `update-roles`, `update-groups`, `delete` | Manage users | -| [group](docs/documentation/group.md) | `usergroup`, `ug` | `list`, `get`, `create`, `update`, `delete` | Manage user groups | +| Command | Alias | Subcommands | Description | +| ---------------------------------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | +| [activitylog](docs/documentation/activitylog.md) | | `list`, `get` | Query activity logs for completed jobs | +| [agent](docs/documentation/agent.md) | | `list`, `get`, `details`, `delete`, `start`, `stop`, `restart`, `installer-info`, `installer-download`, `configs` | Manage Secure Agents | +| [auditlog](docs/documentation/auditlog.md) | | `list` | Query organization audit log (V2 API) | +| [completion](docs/documentation/completion.md) | | `bash`, `zsh`, `fish`, `powershell` | Generate shell completion scripts | +| [config](docs/documentation/config.md) | | `edit` | Edit global settings (new user defaults, theme, timeout, menu) | +| [connection](docs/documentation/connection.md) | `conn` | `list`, `get`, `create`, `update`, `delete`, `test`, `search`, `objects`, `metadata` | Manage connections | +| [connector](docs/documentation/connector.md) | | `list`, `metadata` | List connectors and their connection attributes | +| [export](docs/documentation/export.md) | | `run`, `start`, `status`, `download`, `create` | Export asset packages | +| [folder](docs/documentation/folder.md) | | `list`, `create`, `update`, `delete` | Manage folders | +| [import](docs/documentation/import.md) | `imp` | `run`, `upload`, `start`, `status`, `download-log` | Import asset packages | +| [login](docs/documentation/login.md) | | | Authenticate and cache session | +| [logout](docs/documentation/logout.md) | | | Invalidate session | +| [lookup](docs/documentation/lookup.md) | | | Resolve object IDs, names, and paths | +| [metering](docs/documentation/metering.md) | | `get`, `download` | Query usage and metering data | +| [objects](docs/documentation/objects.md) | | `list`, `dependencies` | List/search assets, find dependencies | +| [package](docs/documentation/package.md) | | `expand`, `create`, `dependencies` | Extract, assemble, or inspect dependencies of IICS export packages | +| [menu](docs/documentation/menu.md) | `tui` | - | Interactive main menu (bare `iics` in a terminal) | +| [permission](docs/documentation/permission.md) | `perm` | `get`, `add`, `update`, `delete`, `set`, `check`, `edit` | Manage object-level permissions | +| [privilege](docs/documentation/privilege.md) | | `list` | List available privileges | +| [profile](docs/documentation/profile.md) | | `add`, `edit`, `list`, `delete`, `set-default`, `set-password`, `show` | Manage connection profiles | +| [project](docs/documentation/project.md) | | `list`, `create`, `update`, `delete` | Manage projects | +| [publish](docs/documentation/publish.md) | | `start`, `status`, `run` | Publish CAI assets to the runtime | +| [release](docs/documentation/release.md) | | `manifest`, `validate`, `plan` | Generate multi-format release manifests and per-environment plans | +| [role](docs/documentation/role.md) | | `list`, `get`, `create`, `edit`, `add-privileges`, `remove-privileges`, `delete` | Manage roles | +| [environment](docs/documentation/environment.md) | `runtime`, `rt`, `env` | `list`, `get`, `create`, `update`, `delete`, `configs` | Manage runtime environments (Secure Agent groups) | +| [schedule](docs/documentation/schedule.md) | | `list`, `get`, `create`, `update`, `delete` | Manage schedules | +| [securitylog](docs/documentation/securitylog.md) | | `list` | Query security audit log | +| [sourcecontrol](docs/documentation/sourcecontrol.md) | `sc` | `checkout`, `checkin`, `pull`, `commit` | Source control operations | +| [state](docs/documentation/state.md) | | `fetch`, `load` | Fetch/load object state snapshots | +| [tag](docs/documentation/tag.md) | | `assign`, `remove` | Assign/remove tags on objects | +| [unpublish](docs/documentation/unpublish.md) | | `start`, `status`, `run` | Unpublish CAI assets from the runtime | +| [user](docs/documentation/user.md) | | `list`, `get`, `create`, `edit`, `update-roles`, `update-groups`, `delete` | Manage users | +| [group](docs/documentation/group.md) | `usergroup`, `ug` | `list`, `get`, `create`, `update`, `delete` | Manage user groups | > **Keeping completions up to date:** After adding or changing any command or flag, regenerate > the shell completion scripts by running `make completions`. The pre-generated scripts live in diff --git a/cmd/agent.go b/cmd/agent.go index ca0bc85..78807eb 100644 --- a/cmd/agent.go +++ b/cmd/agent.go @@ -26,6 +26,7 @@ func newAgentCmd() *cobra.Command { cmd.AddCommand(newAgentRestartCmd()) cmd.AddCommand(newAgentInstallerInfoCmd()) cmd.AddCommand(newAgentInstallerDownloadCmd()) + cmd.AddCommand(newAgentConfigsCmd()) return cmd } diff --git a/cmd/agent_configs.go b/cmd/agent_configs.go new file mode 100644 index 0000000..429d29a --- /dev/null +++ b/cmd/agent_configs.go @@ -0,0 +1,280 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/jbrazda/iics-cli/internal/client" + "github.com/jbrazda/iics-cli/internal/output" + "github.com/spf13/cobra" +) + +const agentConfigsExperimental = "Experimental: uses the undocumented Administrator form endpoint /runtime/app/agent.do." + +func newAgentConfigsCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "configs", + Short: "Show and change agent-level Secure Agent service properties (experimental)", + Long: `Show and change Secure Agent service properties of a single agent, such as +the Process Server cluster or database settings, and the agent's own +Agent Core, Agent Manager and Tomcat settings. + +` + agentConfigsExperimental + ` The public REST API only +configures properties for a whole Secure Agent group (see "iics environment +configs").`, + } + cmd.AddCommand(newAgentConfigsGetCmd()) + cmd.AddCommand(newAgentConfigsSetCmd()) + return cmd +} + +// agentPropertyCols is the table layout for `agent configs get`. +var agentPropertyCols = []output.Column{ + {Header: "SERVICE", Field: "service", Width: 14, Priority: 1, Shrink: output.ShrinkTruncate}, + {Header: "TYPE", Field: "type", Width: 12, Priority: 1, Shrink: output.ShrinkTruncate, Func: func(v interface{}) string { + row, _ := v.(map[string]interface{}) + t, _ := row["type"].(string) + if sub, _ := row["subType"].(string); sub != "" { + t += "/" + sub + } + return t + }}, + {Header: "NAME", Field: "name", Width: 24, Priority: 1, Shrink: output.ShrinkWrap}, + {Header: "VALUE", Field: "value", Width: 30, Priority: 1, Func: wrapConfigField("value")}, + {Header: "DEFAULT", Field: "defaultValue", Width: 30, Priority: 3, Func: wrapConfigField("defaultValue")}, + {Header: "OVERRIDDEN", Field: "overridden", Width: 10, Priority: 2, Shrink: output.ShrinkNever, Func: func(v interface{}) string { + row, _ := v.(map[string]interface{}) + if b, _ := row["overridden"].(bool); b { + return "yes" + } + return "" + }}, +} + +// agentConfigDiffCols is the table layout for the changes `agent configs set` +// makes. +var agentConfigDiffCols = []output.Column{ + {Header: "PROPERTY", Field: "property", Width: 30, Priority: 1, Shrink: output.ShrinkWrap}, + {Header: "CURRENT", Field: "oldValue", Width: 30, Priority: 1, Func: diffValueFunc("oldValue", "oldOverridden")}, + {Header: "NEW", Field: "newValue", Width: 30, Priority: 1, Func: diffValueFunc("newValue", "newOverridden")}, +} + +// diffValueFunc shows a value, "(default)" when it is not overridden, and +// hides sensitive values. +func diffValueFunc(valueField, overField string) func(v interface{}) string { + return func(v interface{}) string { + row, _ := v.(map[string]interface{}) + s, _ := row[valueField].(string) + if sens, _ := row["sensitive"].(bool); sens { + s = "***" + } + s = output.WrapCell(s, 44) + if over, _ := row[overField].(bool); !over { + s += " (default)" + } + return s + } +} + +func addAgentSelectorFlags(cmd *cobra.Command, id, fid, name, hostname *string) { + cmd.Flags().StringVar(id, "id", "", "agent ID") + cmd.Flags().StringVar(fid, "fid", "", "agent federated ID") + cmd.Flags().StringVar(name, "name", "", "agent name") + cmd.Flags().StringVar(hostname, "hostname", "", "agent host name") + cmd.MarkFlagsMutuallyExclusive("id", "fid", "name", "hostname") +} + +func newAgentConfigsGetCmd() *cobra.Command { + var ( + id, fid, name, hostname string + flt client.AgentConfigFilter + ) + cmd := &cobra.Command{ + Use: "get", + Short: "Show an agent's service properties", + Long: "Show an agent's service properties with their defaults and whether they are overridden.\n\n" + agentConfigsExperimental, + Example: ` iics agent configs get --name CAI-A03 + iics agent configs get --name CAI-A03 --service process-engine --overridden + iics agent configs get --hostname myhost --service "Process Server" --type db + iics agent configs get --id --service agent --type TOMCAT_JRE -o json`, + RunE: func(cmd *cobra.Command, args []string) error { + if id == "" && fid == "" && name == "" && hostname == "" { + return fmt.Errorf("one of --id, --fid, --name, or --hostname is required") + } + c, err := getClient(cmd) + if err != nil { + return err + } + ctx := context.Background() + agentID, err := resolveAgentID(ctx, c, id, name, hostname, fid) + if err != nil { + return err + } + form, err := c.GetAgentConfigForm(ctx, agentID) + if err != nil { + return err + } + props, err := form.Filter(flt) + if err != nil { + return err + } + f, err := getFormatter() + if err != nil { + return err + } + return f.Format(props, agentPropertyCols) + }, + } + addAgentSelectorFlags(cmd, &id, &fid, &name, &hostname) + cmd.Flags().StringVar(&flt.Service, "service", "", `service name, for example process-engine, "Process Server", or agent for the agent's own properties`) + cmd.Flags().StringVar(&flt.Type, "type", "", "property type, for example cluster, db or AGENTCORE_LOG4J/DEBUG") + cmd.Flags().StringVar(&flt.Name, "search", "", "regular expression on the property name (case-insensitive)") + cmd.Flags().BoolVar(&flt.Overridden, "overridden", false, "show only properties that differ from the defaults") + return cmd +} + +// parseAgentConfigChanges builds the change list from --set, --reset and +// --from-file. A --set value starting with @ is read from that file. +func parseAgentConfigChanges(sets, resets []string, fromFile string) ([]client.AgentConfigChange, error) { + var changes []client.AgentConfigChange + if fromFile != "" { + data, err := os.ReadFile(fromFile) + if err != nil { + return nil, fmt.Errorf("reading file: %w", err) + } + if err := json.Unmarshal(data, &changes); err != nil { + return nil, fmt.Errorf("parsing %s: %w", fromFile, err) + } + } + for _, s := range sets { + prop, value, ok := strings.Cut(s, "=") + if !ok { + return nil, fmt.Errorf("invalid --set %q: expected service:type:name=value", s) + } + if file, isFile := strings.CutPrefix(value, "@"); isFile { + data, err := os.ReadFile(file) + if err != nil { + return nil, fmt.Errorf("reading value for %s: %w", prop, err) + } + value = string(data) + } + changes = append(changes, client.AgentConfigChange{Property: prop, Value: value}) + } + for _, r := range resets { + changes = append(changes, client.AgentConfigChange{Property: r, Reset: true}) + } + return changes, nil +} + +func newAgentConfigsSetCmd() *cobra.Command { + var ( + id, fid, name, hostname string + sets, resets []string + fromFile string + dryRun, yes, force bool + ) + cmd := &cobra.Command{ + Use: "set", + Short: "Change an agent's service properties", + Long: `Change or reset service properties of one agent. + +Properties are named service:type:name, as shown by "iics agent configs get". +Use type/subType (for example AGENTCORE_LOG4J/DEBUG) when a name exists for +both subtypes. Values are stored as typed, including quotes: Process Server +values such as '1024M' keep their single quotes. + +The changes are shown first and applied after confirmation (or with --yes). +The agent's whole property form is submitted, as the Administrator UI does, +and read back to verify. The agent applies changed properties itself, which +may restart the affected service. + +Agents in a Secure Agent group with group-level properties get those settings +from the group; the command refuses to change them unless --force is given. + +` + agentConfigsExperimental, + Example: ` iics agent configs set --name CAI-A03 --set "process-engine:cluster:primary-node=false" --dry-run + iics agent configs set --name CAI-A03 --set "process-engine:jvm:max-heap='4G'" + iics agent configs set --name CAI-A03 --set "process-engine:db:connection-properties=@db-props.yaml" + iics agent configs set --name CAI-A03 --reset process-engine:jvm:max-heap --yes + iics agent configs set --name CAI-A03 --from-file changes.json`, + RunE: func(cmd *cobra.Command, args []string) error { + if id == "" && fid == "" && name == "" && hostname == "" { + return fmt.Errorf("one of --id, --fid, --name, or --hostname is required") + } + changes, err := parseAgentConfigChanges(sets, resets, fromFile) + if err != nil { + return err + } + if len(changes) == 0 { + return fmt.Errorf("nothing to change: use --set, --reset or --from-file") + } + c, err := getClient(cmd) + if err != nil { + return err + } + ctx := context.Background() + agent, err := resolveAgent(ctx, c, id, name, hostname, fid) + if err != nil { + return err + } + if agent.GroupID != "" && !force { + groupCfg, gErr := c.GetAgentGroupConfigs(ctx, agent.GroupID) + if gErr != nil { + return fmt.Errorf("checking group-level properties: %w", gErr) + } + if len(groupCfg) > 0 { + return fmt.Errorf("agent %s is in group %s, which has group-level properties for %v; "+ + "remove them first (iics environment configs) or use --force", agent.Name, agent.GroupID, sortedKeys(groupCfg)) + } + } + form, err := c.GetAgentConfigForm(ctx, agent.ID) + if err != nil { + return err + } + diffs, err := form.Apply(changes) + if err != nil { + return err + } + w := cmd.OutOrStdout() + if len(diffs) == 0 { + _, _ = fmt.Fprintln(w, "Nothing to change: the properties already have these values.") + return nil + } + if outputFmt == "" || outputFmt == "table" { + _, _ = fmt.Fprintf(w, "Changes for agent %s (%s):\n\n", agent.Name, agent.ID) + } + f, err := getFormatter() + if err != nil { + return err + } + if err := f.Format(diffs, agentConfigDiffCols); err != nil { + return err + } + if dryRun { + return nil + } + if !yes && !confirmAction(cmd, fmt.Sprintf("Apply %d change(s) to agent %s?", len(diffs), agent.Name)) { + return nil + } + if err := c.SaveAgentConfigForm(ctx, form); err != nil { + return err + } + if err := c.VerifyAgentConfigDiffs(ctx, agent.ID, diffs); err != nil { + return err + } + _, _ = fmt.Fprintf(w, "Updated %d propert%s on agent %s.\n", len(diffs), map[bool]string{true: "y", false: "ies"}[len(diffs) == 1], agent.Name) + return nil + }, + } + addAgentSelectorFlags(cmd, &id, &fid, &name, &hostname) + cmd.Flags().StringArrayVar(&sets, "set", nil, "set a property: service:type:name=value (value @file reads a file); repeatable") + cmd.Flags().StringArrayVar(&resets, "reset", nil, "reset a property to its default: service:type:name; repeatable") + cmd.Flags().StringVar(&fromFile, "from-file", "", `JSON file with [{"property":"service:type:name","value":"...","reset":false}]`) + cmd.Flags().BoolVar(&dryRun, "dry-run", false, "show the changes without applying them") + cmd.Flags().BoolVarP(&yes, "yes", "y", false, "apply without asking for confirmation") + cmd.Flags().BoolVar(&force, "force", false, "change the agent even if its group has group-level properties") + return cmd +} diff --git a/completions/iics.bash b/completions/iics.bash index 7f368b3..63a34ed 100644 --- a/completions/iics.bash +++ b/completions/iics.bash @@ -503,6 +503,191 @@ _iics_activitylog() noun_aliases=() } +_iics_agent_configs_get() +{ + last_command="iics_agent_configs_get" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--fid=") + two_word_flags+=("--fid") + local_nonpersistent_flags+=("--fid") + local_nonpersistent_flags+=("--fid=") + flags+=("--hostname=") + two_word_flags+=("--hostname") + local_nonpersistent_flags+=("--hostname") + local_nonpersistent_flags+=("--hostname=") + flags+=("--id=") + two_word_flags+=("--id") + local_nonpersistent_flags+=("--id") + local_nonpersistent_flags+=("--id=") + flags+=("--name=") + two_word_flags+=("--name") + local_nonpersistent_flags+=("--name") + local_nonpersistent_flags+=("--name=") + flags+=("--overridden") + local_nonpersistent_flags+=("--overridden") + flags+=("--search=") + two_word_flags+=("--search") + local_nonpersistent_flags+=("--search") + local_nonpersistent_flags+=("--search=") + flags+=("--service=") + two_word_flags+=("--service") + local_nonpersistent_flags+=("--service") + local_nonpersistent_flags+=("--service=") + flags+=("--type=") + two_word_flags+=("--type") + local_nonpersistent_flags+=("--type") + local_nonpersistent_flags+=("--type=") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--http-timeout=") + two_word_flags+=("--http-timeout") + flags+=("--no-color") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + flags+=("--profile=") + two_word_flags+=("--profile") + two_word_flags+=("-p") + flags+=("--theme=") + two_word_flags+=("--theme") + flags+=("--verbose") + flags+=("-v") + flags+=("--wide") + flags+=("--width=") + two_word_flags+=("--width") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_iics_agent_configs_set() +{ + last_command="iics_agent_configs_set" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--dry-run") + local_nonpersistent_flags+=("--dry-run") + flags+=("--fid=") + two_word_flags+=("--fid") + local_nonpersistent_flags+=("--fid") + local_nonpersistent_flags+=("--fid=") + flags+=("--force") + local_nonpersistent_flags+=("--force") + flags+=("--from-file=") + two_word_flags+=("--from-file") + local_nonpersistent_flags+=("--from-file") + local_nonpersistent_flags+=("--from-file=") + flags+=("--hostname=") + two_word_flags+=("--hostname") + local_nonpersistent_flags+=("--hostname") + local_nonpersistent_flags+=("--hostname=") + flags+=("--id=") + two_word_flags+=("--id") + local_nonpersistent_flags+=("--id") + local_nonpersistent_flags+=("--id=") + flags+=("--name=") + two_word_flags+=("--name") + local_nonpersistent_flags+=("--name") + local_nonpersistent_flags+=("--name=") + flags+=("--reset=") + two_word_flags+=("--reset") + local_nonpersistent_flags+=("--reset") + local_nonpersistent_flags+=("--reset=") + flags+=("--set=") + two_word_flags+=("--set") + local_nonpersistent_flags+=("--set") + local_nonpersistent_flags+=("--set=") + flags+=("--yes") + flags+=("-y") + local_nonpersistent_flags+=("--yes") + local_nonpersistent_flags+=("-y") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--http-timeout=") + two_word_flags+=("--http-timeout") + flags+=("--no-color") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + flags+=("--profile=") + two_word_flags+=("--profile") + two_word_flags+=("-p") + flags+=("--theme=") + two_word_flags+=("--theme") + flags+=("--verbose") + flags+=("-v") + flags+=("--wide") + flags+=("--width=") + two_word_flags+=("--width") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_iics_agent_configs() +{ + last_command="iics_agent_configs" + + command_aliases=() + + commands=() + commands+=("get") + commands+=("set") + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--http-timeout=") + two_word_flags+=("--http-timeout") + flags+=("--no-color") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + flags+=("--profile=") + two_word_flags+=("--profile") + two_word_flags+=("-p") + flags+=("--theme=") + two_word_flags+=("--theme") + flags+=("--verbose") + flags+=("-v") + flags+=("--wide") + flags+=("--width=") + two_word_flags+=("--width") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _iics_agent_delete() { last_command="iics_agent_delete" @@ -1041,6 +1226,7 @@ _iics_agent() command_aliases=() commands=() + commands+=("configs") commands+=("delete") commands+=("details") commands+=("get") diff --git a/docs/ChangeRequests/pending/CR-0049-agent-level-service-properties.md b/docs/ChangeRequests/pending/CR-0049-agent-level-service-properties.md new file mode 100644 index 0000000..8554431 --- /dev/null +++ b/docs/ChangeRequests/pending/CR-0049-agent-level-service-properties.md @@ -0,0 +1,152 @@ +# CR: Agent-level Secure Agent service properties (agent configs) + +--- + +## CR Type + +- [ ] **New resource** +- [x] **New subcommand** - `iics agent configs get` and `iics agent configs set` +- [ ] **Enhancement** +- [ ] **Output change** +- [ ] **Flag / config change** +- [ ] **Refactor** + +--- + +## Problem + +Some Secure Agent service properties must differ per agent, for example the +Process Server cluster `primary-node` flag, `host-name` / `key-alias`, and the +Process Server database URLs and connection properties. The public REST API +only configures service properties for a whole Secure Agent group +(`/api/v2/runtimeEnvironment//configs`, already covered by +`iics environment configs`). Passing an agent ID to those endpoints fails with +HTTP 403 `REPO_54014` "Access denied to perform operation on Agent". The `agent` +resource documents only GET and DELETE, so agent-level properties could not be +changed from the CLI. + +--- + +## Desired Change + +- `iics agent configs get --id|--fid|--name|--hostname` lists the agent's + properties with value, default and overridden flag. Filters: `--service`, + `--type` (`type/subType`), `--search` (regex on the name), `--overridden`. +- `iics agent configs set` changes properties with `--set + service:type:name=value` (value `@file` reads a file), `--reset + service:type:name` and `--from-file` (JSON list of `{property, value, reset}`). + It prints a CURRENT / NEW table, asks for confirmation (`--yes` skips, + `--dry-run` only shows), saves, and reads back to verify. +- `set` refuses agents whose group has group-level properties unless `--force`, + because the docs say group settings replace agent settings. +- `set` refuses the agent's own properties (`agent:` Agent Core, Agent Manager, + Tomcat), because the server ignores changes to them (see notes). +- Both commands are marked experimental. + +**Informatica docs:** + +- Group-level API: `https://docs.informatica.com/cloud-common-services/administrator/current-version/rest-api-reference/platform-rest-api-version-2-resources/runtime-environments/configuring-secure-agent-service-properties-for-secure-agent-gro.html` +- Agent resource (GET/DELETE only): `https://onlinehelp.informatica.com/IICS/prod/CDI/en/tt-cloud-rest-api/Secure_Agents_and_services.html` + +--- + +## Scope + +### Files to CREATE + +```text +internal/client/agentform.go # form read/parse/apply/save/verify +internal/client/agentform_test.go # parser, apply, filter, GET/POST, re-login tests +cmd/agent_configs.go # agent configs get / set +``` + +### Files to MODIFY + +```text +cmd/agent.go # register `agent configs` +internal/client/client.go # mask Cookie and Set-Cookie in --debug output +docs/documentation/agent.md # agent configs section +docs/documentation/environment.md # pointer to agent configs, REPO_54014 note +README.md # agent row lists `configs` +completions/ # regenerated +``` + +--- + +## API Details + +There is no REST API. The Administrator UI uses a server-rendered form, +verified with a browser HAR capture and live calls on the tst org. + +| Field | Value | +| -------------- | ------------------------------------------------------------------------------------- | +| Read | `GET https:///runtime/app/agent.do?actionMethod=displayUpdate&id=` | +| Write | `POST https:///runtime/app/agent.do`, `application/x-www-form-urlencoded` | +| Authentication | Cookie `USER_SESSION=` (the v2/v3 `icSessionId` is accepted) | +| CSRF | Double submit: cookie `XSRF_TOKEN` must equal form field `csrfToken` | +| Response | HTML page; `var hasErrors = "true"` and `#InfErrorDiv` carry errors; HTTP 200 always | + +Form layout (`
`, about 1,000 fields, all submitted on save): + +- `name`, `id`, `version`, `actionMethod=update`, `csrfToken`, plus UI state + fields (`isAnySensitiveField`, `customConfig*`, `uiUpgradeStatus`, + `crossIFrameCallState`, `iframeWorkspaceId`, `launchPagePath`). +- System rows, six fields each: `sysConfigType`, `sysConfigSubType`, + `sysConfigName`, `sysConfigValue`, `sysConfigDefault`, `sysConfigOverriden`. +- Engine rows, four fields each: `engineConfigKey`, `engineConfigValue`, + `engineConfigDefault`, `engineConfigOverriden`, plus checkbox + `engineConfigSensitive` (sent only when checked). The key is + ``, for example + `process-engine15124581clusterprimary-node`. Each row is wrapped in + ``, and the + `#engineConfigSel` select maps service keys to display names. +- Editing a property sets its value and `...Overriden=true`. Resetting sets the + value to the default and `...Overriden=false` (from `pnAgent.js`). +- Browsers send newlines in values as CRLF. + +--- + +## Acceptance Criteria + +- [x] `agent configs get` lists all engine and system properties of an agent +- [x] `agent configs set` changes and resets engine properties and verifies them +- [x] Multi-line values via `@file` round-trip unchanged +- [x] Errors from the form (invalid value) are returned as command errors +- [x] Unknown, ambiguous and duplicate properties are rejected before saving +- [x] Client methods have tests; `go build`, `go test`, `go vet`, `golangci-lint` pass +- [x] Two-layer rule respected; no new dependencies + +--- + +## Implementation Notes (filled in by Claude during implementation) + +- No HTML parser dependency was added. A small tokenizer handles input, + textarea and select fields the way a browser serializes a form: checked + checkboxes only, skips disabled fields, uses the selected option, drops the + first newline in a textarea, and sends CRLF newlines. Against a live page and + the user's HAR capture, the encoded body matched the browser's field for + field (1,041 fields). +- The page only renders a `csrfToken` value when an `XSRF_TOKEN` cookie is + sent. The CLI generates a random token for both. The `JSESSIONID` cookie from + the GET is sent back on the POST. +- If the page has no agent form (session not accepted), the client logs in + again once and retries the GET. +- Live test on tst, agent CAI-A03 (group CAI, no group-level properties): + - `OpsInsightsDataCollector:OpsInsights:scriptLogRetentionCount` set to `'101'` + and reset. Both confirmed through the form and the REST `agent details`. + - `process-engine:logging:additional-logging` (multi-line) changed through + `@file` and restored. An earlier invalid value was rejected by the server + with "For configuration additional-logging invalid value specified ...", + and nothing was saved. + - System rows (`agent:AGENTCORE_LOG4J/INFO` and `/DEBUG` + `log4j.appender.aclog.MaxBackupIndex`): the server returned no error but did + not store the change, also after waiting 80 seconds. `set` therefore + refuses `agent:` properties. Supporting them needs a HAR capture of a system + property save in the UI. + - After a save, the agent redeploys the affected service. OI Data Collector + went NEED_RUNNING, then DEPLOYING, then RUNNING. Process Server + (`REPLACE_VERSION`) started a new version next to the running one. +- Setting a property to its current value is a no-op, so defaults do not turn + into overrides. +- `--debug` now masks the `Cookie` and `Set-Cookie` headers, which carry the + session. diff --git a/docs/documentation/agent.md b/docs/documentation/agent.md index 02d2899..51b7ceb 100644 --- a/docs/documentation/agent.md +++ b/docs/documentation/agent.md @@ -20,6 +20,8 @@ iics agent [flags] | `stop` | Stop an agent service | | `installer-info` | Get Secure Agent installer download information | | `installer-download` | Download the Secure Agent installer (optional checksum verify) | +| `configs get` | Show an agent's service properties (experimental) | +| `configs set` | Change or reset an agent's service properties (experimental) | --- @@ -193,6 +195,151 @@ iics agent details --hostname devinfacld01 --full --- +## agent configs + +Show and change the service properties of **one agent**, for example the +Process Server cluster settings (`primary-node`, `load-balance-url`), its +database URLs and connection properties, or JVM heap sizes. The Secure Agent +group equivalent is [environment configs](environment.md#environment-configs). + +> **Experimental.** The public REST API only configures properties for a whole +> Secure Agent group. Agent-level properties are read and saved through the +> Administrator UI form `/runtime/app/agent.do`, which is undocumented and may +> change without notice. + +How it works: + +- The CLI session is sent as the `USER_SESSION` cookie. The form's CSRF check + compares the `XSRF_TOKEN` cookie with the `csrfToken` field, so the CLI sends + a random token in both. +- `set` reads the whole form, changes only the requested properties and submits + every field back, as the UI's Save button does. It then reads the form again + and fails if a change was not stored. +- The agent applies the new properties by redeploying the affected service. + Process Server (`REPLACE_VERSION`) starts a new version next to the running + one and switches over when it is ready. Check progress with + `iics agent details --services`. + +### Property names + +Properties are named `service:type:name`: + +| Part | Values | +| --------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `service` | Service name without its version (`process-engine`, `OpsInsightsDataCollector`), the display name (`Process Server`), or `agent` | +| `type` | Property type, for example `server`, `cluster`, `jvm`, `db`, `logging`, or `AGENTCORE_CFG` for `agent` properties | +| `name` | Property name, for example `primary-node` | + +Matching is case-insensitive. Use `type/subType` (for example +`AGENTCORE_LOG4J/DEBUG`) when a name exists for both the `INFO` and `DEBUG` +subtypes. `agent configs get` shows the names to use. + +`agent` stands for the agent's own Agent Core, Agent Manager and Tomcat +properties. They are shown by `get` but cannot be changed by `set`: the server +accepts the form but ignores changes to them. + +### agent configs get + +Show an agent's properties with their defaults and whether they are overridden. + +| Flag | Type | Description | +| -------------- | ------ | ------------------------------------------------------------------------------------ | +| `--id` | string | Agent ID | +| `--fid` | string | Agent federated ID | +| `--name` | string | Agent name | +| `--hostname` | string | Agent host name | +| `--service` | string | Only this service: name, display name, or `agent` | +| `--type` | string | Only this property type, optionally `type/subType` | +| `--search` | string | Regular expression on the property name (case-insensitive) | +| `--overridden` | bool | Only properties whose value is overridden (differs from the default) | + +Exactly one of `--id` / `--fid` / `--name` / `--hostname` is required. + +Table columns: `SERVICE`, `TYPE` (with `/subType` for `agent` properties), +`NAME`, `VALUE`, `DEFAULT`, `OVERRIDDEN`. `--output json` / `yaml` / `csv` +list the properties with `service`, `serviceName`, `type`, `subType`, `name`, +`value`, `defaultValue`, `overridden` and `sensitive`. + +```bash +iics agent configs get --name CAI-A03 --overridden +iics agent configs get --name CAI-A03 --service "Process Server" --type cluster +iics agent configs get --hostname myhost --service process-engine --search '^db|url' -o json +iics agent configs get --name CAI-A03 --service agent --type TOMCAT_JRE +``` + +### agent configs set + +Change or reset properties of one agent. The changes are shown as a +`PROPERTY` / `CURRENT` / `NEW` table (values that are not overridden are +marked `(default)`, sensitive values are shown as `***`) and applied after +confirmation. + +| Flag | Type | Description | +| ------------- | ------ | ------------------------------------------------------------------------------------------- | +| `--id` | string | Agent ID | +| `--fid` | string | Agent federated ID | +| `--name` | string | Agent name | +| `--hostname` | string | Agent host name | +| `--set` | string | `service:type:name=value`; a value `@file` reads the value from a file. Repeatable | +| `--reset` | string | `service:type:name` to reset to the default. Repeatable | +| `--from-file` | string | JSON list of changes (see below) | +| `--dry-run` | bool | Show the changes without applying them | +| `--yes`, `-y` | bool | Apply without asking for confirmation | +| `--force` | bool | Change the agent even if its Secure Agent group has group-level properties | + +Rules: + +- Values are stored exactly as given. Process Server values keep their YAML + quoting, for example `'4G'` or `'jdbc:postgresql://host:5432/activevos'`, as + `get` shows them. Multi-line values (connection properties, logging) are + easiest to pass with `@file`; the file content is used as is. +- Setting a property to its current value changes nothing, so a default does + not turn into an override. Use `--reset` to go back to the default. +- A property can be changed only once per command. +- If the agent's Secure Agent group has group-level properties, the group's + settings replace the agent's when the agent is added to or restarted in the + group, so `set` stops with an error. Remove the group-level properties first + (`iics environment configs`), or use `--force`. +- Errors reported by the form, such as an invalid value, are returned as the + command error and nothing is changed. + +`--from-file` format: + +```json +[ + { "property": "process-engine:cluster:primary-node", "value": "false" }, + { "property": "process-engine:jvm:max-heap", "value": "'4G'" }, + { "property": "process-engine:jvm:min-heap", "reset": true } +] +``` + +```bash +# Preview a change +iics agent configs set --name CAI-A03 --set "process-engine:cluster:primary-node=false" --dry-run + +# Make the agent the cluster primary node +iics agent configs set --name CAI-A03 --set "process-engine:cluster:primary-node=true" + +# Point Process Server at another database, connection properties from a file +iics agent configs set --name CAI-A03 \ + --set "process-engine:db:url='jdbc:postgresql://dbhost:5432/activevos'" \ + --set "process-engine:db:connection-properties=@db-props.yaml" --yes + +# Back to the default heap size +iics agent configs set --name CAI-A03 --reset process-engine:jvm:max-heap --yes + +# Several changes from a file +iics agent configs set --hostname myhost --from-file changes.json +``` + +```powershell +iics agent configs set --name CAI-A03 --set "process-engine:cluster:primary-node=false" --dry-run + +iics agent configs set --name CAI-A03 --set "process-engine:jvm:max-heap='4G'" --yes +``` + +--- + ## agent delete Delete a Secure Agent. diff --git a/docs/documentation/environment.md b/docs/documentation/environment.md index 2379526..0c7f863 100644 --- a/docs/documentation/environment.md +++ b/docs/documentation/environment.md @@ -226,6 +226,10 @@ Manage Secure Agent group service property overrides. Uses `GET` / `PUT /api/v2/runtimeEnvironment//configs`. The Secure Agent group ID is the runtime environment ID. +To change the properties of a single agent instead of the whole group, use +[agent configs](agent.md#agent-configs). Passing an agent ID to these group +endpoints fails with `REPO_54014` "Access denied to perform operation on Agent". + ### environment configs get Show the service property overrides for a group. diff --git a/internal/client/agentform.go b/internal/client/agentform.go new file mode 100644 index 0000000..05c9fc2 --- /dev/null +++ b/internal/client/agentform.go @@ -0,0 +1,628 @@ +package client + +import ( + "context" + "crypto/rand" + "encoding/base64" + "fmt" + "html" + "io" + "net/http" + "net/url" + "regexp" + "sort" + "strings" +) + +// Agent-level Secure Agent service properties have no public REST API. The +// Administrator UI edits them through a server-rendered form at +// /runtime/app/agent.do: GET ?actionMethod=displayUpdate&id= renders +// every property as form fields, and POST actionMethod=update submits the whole +// form back. This file reads and writes that form the same way the browser +// does. The endpoint is undocumented and may change without notice. +// +// Authentication: the REST session ID is accepted as the USER_SESSION cookie. +// CSRF protection is a double-submit check: the XSRF_TOKEN cookie must match +// the csrfToken form field, so a random token is sent in both. + +const agentFormPath = "/runtime/app/agent.do" + +// SystemService is the service name used for the Secure Agent's own +// (agent core, agent manager, Tomcat) properties. +const SystemService = "agent" + +// AgentConfigProperty is one editable property on an agent's configuration +// form. +type AgentConfigProperty struct { + // Service is the service name without its version suffix, for example + // process-engine, or SystemService for the agent's own properties. + Service string `json:"service"` + // ServiceName is the display name, for example "Process Server". + ServiceName string `json:"serviceName,omitempty"` + // Type is the property type, for example cluster or AGENTCORE_CFG. + Type string `json:"type"` + // SubType is INFO or DEBUG for agent log4j and other system properties. + SubType string `json:"subType,omitempty"` + Name string `json:"name"` + Value string `json:"value"` + DefaultValue string `json:"defaultValue"` + Overridden bool `json:"overridden"` + Sensitive bool `json:"sensitive,omitempty"` + + // Indexes into AgentConfigForm.fields for the value and overridden flag. + valueIdx, overIdx int + // serviceKey is the versioned service key used in the form, for example + // process-engine15124581. + serviceKey string +} + +// Key returns service:type:name, adding /subType to the type when set. +func (p AgentConfigProperty) Key() string { + t := p.Type + if p.SubType != "" { + t += "/" + p.SubType + } + return p.Service + ":" + t + ":" + p.Name +} + +// formField is one name=value pair the browser would submit, in form order. +type formField struct { + name, value string +} + +// AgentConfigForm is a parsed agent configuration form. +type AgentConfigForm struct { + AgentID string `json:"agentId"` + AgentName string `json:"agentName"` + Version string `json:"version"` + Properties []AgentConfigProperty `json:"properties"` + + fields []formField + cookies []*http.Cookie + csrf string +} + +var ( + formTagRe = regexp.MustCompile(`(?is)<(input|textarea|select)\b([^>]*)>`) + formAttrRe = regexp.MustCompile(`(?s)([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?`) + formOptRe = regexp.MustCompile(`(?is)]*)>`) + engineRowRe = regexp.MustCompile(`\s*]*id="engineConfigSel"[^>]*>(.*?)`) + optionRe = regexp.MustCompile(`(?s)`) + sensitiveRe = regexp.MustCompile(`]*>`) + hasErrorsRe = regexp.MustCompile(`var hasErrors = "(true|false)"`) + errorDivRe = regexp.MustCompile(`(?s)
]*>(.*?)
`) + tagRe = regexp.MustCompile(`<[^>]*>`) + versionTail = regexp.MustCompile(`[0-9.]+$`) +) + +// normalizeNewlines turns CRLF and CR into LF. Values are kept with LF and +// sent with CRLF, as browsers do. +func normalizeNewlines(s string) string { + return strings.ReplaceAll(strings.ReplaceAll(s, "\r\n", "\n"), "\r", "\n") +} + +func parseAttrs(s string) map[string]string { + attrs := map[string]string{} + for _, m := range formAttrRe.FindAllStringSubmatch(s, -1) { + name := strings.ToLower(m[1]) + if _, ok := attrs[name]; ok { + continue + } + attrs[name] = normalizeNewlines(html.UnescapeString(m[2] + m[3] + m[4])) + } + return attrs +} + +// parseFormFields returns the fields a browser submits for the form body, +// in document order: inputs (checkboxes only when checked), textareas and +// the selected option of each select. Disabled and nameless fields are +// skipped. +func parseFormFields(body string) []formField { + var fields []formField + pos := 0 + for { + loc := formTagRe.FindStringSubmatchIndex(body[pos:]) + if loc == nil { + return fields + } + tag := strings.ToLower(body[pos+loc[2] : pos+loc[3]]) + attrs := parseAttrs(body[pos+loc[4] : pos+loc[5]]) + end := pos + loc[1] + pos = end + name := attrs["name"] + if _, disabled := attrs["disabled"]; name == "" || disabled { + continue + } + switch tag { + case "input": + switch strings.ToLower(attrs["type"]) { + case "checkbox", "radio": + if _, ok := attrs["checked"]; !ok { + continue + } + v, ok := attrs["value"] + if !ok { + v = "on" + } + fields = append(fields, formField{name, v}) + case "submit", "button", "image", "reset", "file": + default: + fields = append(fields, formField{name, attrs["value"]}) + } + case "textarea": + closeIdx := strings.Index(strings.ToLower(body[end:]), "") + if closeIdx < 0 { + return fields + } + // Browsers drop one newline right after the opening tag. + v := strings.TrimPrefix(normalizeNewlines(html.UnescapeString(body[end:end+closeIdx])), "\n") + fields = append(fields, formField{name, v}) + pos = end + closeIdx + case "select": + closeIdx := strings.Index(strings.ToLower(body[end:]), "") + if closeIdx < 0 { + return fields + } + var first, selected string + haveFirst, haveSelected := false, false + for _, om := range formOptRe.FindAllStringSubmatch(body[end:end+closeIdx], -1) { + oa := parseAttrs(om[1]) + if !haveFirst { + first, haveFirst = oa["value"], true + } + if _, ok := oa["selected"]; ok && !haveSelected { + selected, haveSelected = oa["value"], true + } + } + switch { + case haveSelected: + fields = append(fields, formField{name, selected}) + case haveFirst: + fields = append(fields, formField{name, first}) + } + pos = end + closeIdx + } + } +} + +// agentFormSection returns the agentForm element's contents, or "" if the +// page has no agent form (for example a login page). +func agentFormSection(page string) string { + start := strings.Index(page, `") + if end < 0 { + return page[start:] + } + return page[start : start+end] +} + +// parseAgentConfigForm parses the agent configuration page. +func parseAgentConfigForm(page string) (*AgentConfigForm, error) { + section := agentFormSection(page) + if section == "" { + return nil, fmt.Errorf("agent configuration form not found in response") + } + form := &AgentConfigForm{fields: parseFormFields(section)} + + type engineRow struct{ serviceKey, typ string } + rows := map[string]engineRow{} + for _, m := range engineRowRe.FindAllStringSubmatch(section, -1) { + rows[html.UnescapeString(m[3])] = engineRow{html.UnescapeString(m[1]), html.UnescapeString(m[2])} + } + serviceNames := map[string]string{} + if m := engineSelRe.FindStringSubmatch(page); m != nil { + for _, om := range optionRe.FindAllStringSubmatch(m[1], -1) { + serviceNames[html.UnescapeString(om[1])] = strings.TrimSpace(html.UnescapeString(om[2])) + } + } + sensitive := map[string]bool{} + for _, tag := range sensitiveRe.FindAllString(section, -1) { + a := parseAttrs(strings.TrimSuffix(strings.TrimPrefix(tag, "")) + if _, ok := a["checked"]; ok { + sensitive[a["value"]] = true + } + } + + f := form.fields + for i := 0; i < len(f); i++ { + switch f[i].name { + case "id": + form.AgentID = f[i].value + case "name": + form.AgentName = f[i].value + case "version": + form.Version = f[i].value + case "csrfToken": + form.csrf = f[i].value + case "sysConfigType": + if i+5 >= len(f) || f[i+1].name != "sysConfigSubType" || f[i+2].name != "sysConfigName" || + f[i+3].name != "sysConfigValue" || f[i+4].name != "sysConfigDefault" || f[i+5].name != "sysConfigOverriden" { + return nil, fmt.Errorf("unexpected agent form layout at field %d (%s)", i, f[i].name) + } + form.Properties = append(form.Properties, AgentConfigProperty{ + Service: SystemService, + ServiceName: "Secure Agent", + Type: f[i].value, + SubType: f[i+1].value, + Name: f[i+2].value, + Value: f[i+3].value, + DefaultValue: f[i+4].value, + Overridden: f[i+5].value == "true", + valueIdx: i + 3, + overIdx: i + 5, + }) + i += 5 + case "engineConfigKey": + if i+3 >= len(f) || f[i+1].name != "engineConfigValue" || f[i+2].name != "engineConfigDefault" || f[i+3].name != "engineConfigOverriden" { + return nil, fmt.Errorf("unexpected agent form layout at field %d (%s)", i, f[i].name) + } + key := f[i].value + row, ok := rows[key] + if !ok { + return nil, fmt.Errorf("no service row found for agent form property %q", key) + } + form.Properties = append(form.Properties, AgentConfigProperty{ + Service: versionTail.ReplaceAllString(row.serviceKey, ""), + ServiceName: serviceNames[row.serviceKey], + Type: row.typ, + Name: strings.TrimPrefix(key, row.serviceKey+row.typ), + Value: f[i+1].value, + DefaultValue: f[i+2].value, + Overridden: f[i+3].value == "true", + Sensitive: sensitive[key], + valueIdx: i + 1, + overIdx: i + 3, + serviceKey: row.serviceKey, + }) + i += 3 + } + } + if form.AgentID == "" || form.Version == "" { + return nil, fmt.Errorf("agent form is missing the agent id or version") + } + return form, nil +} + +// PropertySelector identifies agent form properties: Service matches the +// service name, its versioned key or display name; Type may carry a /SubType +// suffix. Matching is case-insensitive. +type PropertySelector struct { + Service, Type, SubType, Name string +} + +// ParsePropertySelector parses service:type[/subtype]:name. +func ParsePropertySelector(s string) (PropertySelector, error) { + parts := strings.SplitN(s, ":", 3) + if len(parts) != 3 || parts[0] == "" || parts[1] == "" || parts[2] == "" { + return PropertySelector{}, fmt.Errorf("invalid property %q: expected service:type:name", s) + } + sel := PropertySelector{Service: parts[0], Type: parts[1], Name: parts[2]} + if t, sub, ok := strings.Cut(sel.Type, "/"); ok { + sel.Type, sel.SubType = t, sub + } + return sel, nil +} + +func (s PropertySelector) matches(p AgentConfigProperty) bool { + svc := strings.EqualFold(s.Service, p.Service) || strings.EqualFold(s.Service, p.serviceKey) || + (p.ServiceName != "" && strings.EqualFold(s.Service, p.ServiceName)) + return svc && strings.EqualFold(s.Type, p.Type) && strings.EqualFold(s.Name, p.Name) && + (s.SubType == "" || strings.EqualFold(s.SubType, p.SubType)) +} + +func (s PropertySelector) String() string { + t := s.Type + if s.SubType != "" { + t += "/" + s.SubType + } + return s.Service + ":" + t + ":" + s.Name +} + +// Find returns the index of the one property matching sel. +func (f *AgentConfigForm) Find(sel PropertySelector) (int, error) { + var hits []int + for i, p := range f.Properties { + if sel.matches(p) { + hits = append(hits, i) + } + } + switch len(hits) { + case 1: + return hits[0], nil + case 0: + return -1, fmt.Errorf("property %s not found on agent %s", sel, f.AgentName) + } + keys := make([]string, len(hits)) + for i, h := range hits { + keys[i] = f.Properties[h].Key() + } + sort.Strings(keys) + return -1, fmt.Errorf("property %s is ambiguous, use one of: %s", sel, strings.Join(keys, ", ")) +} + +// Set changes property i to value and marks it overridden, as the UI's edit +// action does. +func (f *AgentConfigForm) Set(i int, value string) { + p := &f.Properties[i] + p.Value, p.Overridden = value, true + f.fields[p.valueIdx].value = value + f.fields[p.overIdx].value = "true" +} + +// Reset sets property i back to its default, as the UI's reset action does. +func (f *AgentConfigForm) Reset(i int) { + p := &f.Properties[i] + p.Value, p.Overridden = p.DefaultValue, false + f.fields[p.valueIdx].value = p.DefaultValue + f.fields[p.overIdx].value = "false" +} + +// encode builds the form body for actionMethod=update, keeping field order. +func (f *AgentConfigForm) encode() string { + var b strings.Builder + for i, fld := range f.fields { + v := fld.value + switch fld.name { + case "actionMethod": + v = "update" + case "csrfToken": + v = f.csrf + } + if i > 0 { + b.WriteByte('&') + } + b.WriteString(url.QueryEscape(fld.name)) + b.WriteByte('=') + b.WriteString(url.QueryEscape(strings.ReplaceAll(normalizeNewlines(v), "\n", "\r\n"))) + } + return b.String() +} + +func newCSRFToken() (string, error) { + b := make([]byte, 16) + if _, err := rand.Read(b); err != nil { + return "", fmt.Errorf("generating CSRF token: %w", err) + } + return base64.RawURLEncoding.EncodeToString(b), nil +} + +// agentFormURL returns the agent.do URL on the host of the base API URL. +func (c *Client) agentFormURL(query url.Values) (string, error) { + base, err := url.Parse(c.BaseAPIURL()) + if err != nil { + return "", fmt.Errorf("parsing base API URL: %w", err) + } + u := url.URL{Scheme: base.Scheme, Host: base.Host, Path: agentFormPath, RawQuery: query.Encode()} + return u.String(), nil +} + +// doAgentForm sends one request to agent.do with the session and CSRF +// cookies and returns the page. +func (c *Client) doAgentForm(ctx context.Context, method, u, body, csrf string, cookies []*http.Cookie) (string, []*http.Cookie, error) { + var rd io.Reader + if body != "" { + rd = strings.NewReader(body) + } + req, err := http.NewRequestWithContext(ctx, method, u, rd) + if err != nil { + return "", nil, fmt.Errorf("creating request: %w", err) + } + req.Header.Set("Accept", "text/html") + if body != "" { + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Origin", req.URL.Scheme+"://"+req.URL.Host) + } + req.AddCookie(&http.Cookie{Name: "USER_SESSION", Value: c.SessionID()}) + req.AddCookie(&http.Cookie{Name: "XSRF_TOKEN", Value: csrf}) + for _, ck := range cookies { + req.AddCookie(&http.Cookie{Name: ck.Name, Value: ck.Value}) + } + resp, err := c.httpClient.Do(req) + if err != nil { + return "", nil, fmt.Errorf("%s %s: %w", method, agentFormPath, err) + } + defer func() { _ = resp.Body.Close() }() + data, err := io.ReadAll(resp.Body) + if err != nil { + return "", nil, fmt.Errorf("reading response: %w", err) + } + if c.debug { + debugPrintHTTP(req, nil, resp, nil) + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return "", nil, newAPIError(resp, data) + } + return string(data), resp.Cookies(), nil +} + +// GetAgentConfigForm reads the configuration form of agent agentID. +// If the session is not accepted it logs in again once. +func (c *Client) GetAgentConfigForm(ctx context.Context, agentID string) (*AgentConfigForm, error) { + if err := c.ensureSession(ctx); err != nil { + return nil, err + } + csrf, err := newCSRFToken() + if err != nil { + return nil, err + } + for attempt := 0; ; attempt++ { + u, err := c.agentFormURL(url.Values{"actionMethod": {"displayUpdate"}, "id": {agentID}}) + if err != nil { + return nil, err + } + page, cookies, err := c.doAgentForm(ctx, http.MethodGet, u, "", csrf, nil) + if err != nil { + return nil, err + } + if agentFormSection(page) == "" && attempt == 0 { + if _, loginErr := c.Login(ctx); loginErr != nil { + return nil, &SessionExpiredError{Wrapped: fmt.Errorf("%w; run 'iics login' to re-authenticate", loginErr)} + } + continue + } + form, err := parseAgentConfigForm(page) + if err != nil { + return nil, fmt.Errorf("reading agent %s configuration: %w", agentID, err) + } + form.cookies = cookies + if form.csrf == "" { + form.csrf = csrf + } + return form, nil + } +} + +// SaveAgentConfigForm submits form, with all its properties, as the UI's +// Save button does. It returns an error when the page reports one. +func (c *Client) SaveAgentConfigForm(ctx context.Context, form *AgentConfigForm) error { + u, err := c.agentFormURL(nil) + if err != nil { + return err + } + page, _, err := c.doAgentForm(ctx, http.MethodPost, u, form.encode(), form.csrf, form.cookies) + if err != nil { + return err + } + if agentFormSection(page) == "" { + return fmt.Errorf("saving agent %s configuration: unexpected response (session or CSRF token not accepted)", form.AgentID) + } + if m := hasErrorsRe.FindStringSubmatch(page); m != nil && m[1] == "true" { + msg := "the server reported an error" + if d := errorDivRe.FindStringSubmatch(page); d != nil { + if t := strings.Join(strings.Fields(html.UnescapeString(tagRe.ReplaceAllString(d[1], " "))), " "); t != "" { + msg = t + } + } + return fmt.Errorf("saving agent %s configuration: %s", form.AgentID, msg) + } + return nil +} + +// AgentConfigFilter selects properties for display. Empty fields match all. +type AgentConfigFilter struct { + Service string // service name, versioned key or display name + Type string // property type, optionally type/subType + Name string // regular expression on the property name, case-insensitive + Overridden bool // only overridden properties +} + +// Filter returns the properties matching flt. +func (f *AgentConfigForm) Filter(flt AgentConfigFilter) ([]AgentConfigProperty, error) { + var nameRe *regexp.Regexp + if flt.Name != "" { + re, err := regexp.Compile("(?i)" + flt.Name) + if err != nil { + return nil, fmt.Errorf("invalid name pattern: %w", err) + } + nameRe = re + } + typ, sub, _ := strings.Cut(flt.Type, "/") + var out []AgentConfigProperty + for _, p := range f.Properties { + if flt.Service != "" && !strings.EqualFold(flt.Service, p.Service) && !strings.EqualFold(flt.Service, p.serviceKey) && + !strings.EqualFold(flt.Service, p.ServiceName) { + continue + } + if typ != "" && !strings.EqualFold(typ, p.Type) || sub != "" && !strings.EqualFold(sub, p.SubType) { + continue + } + if nameRe != nil && !nameRe.MatchString(p.Name) || flt.Overridden && !p.Overridden { + continue + } + out = append(out, p) + } + return out, nil +} + +// AgentConfigChange is one requested property change: a new value, or a +// reset to the default when Reset is true. +type AgentConfigChange struct { + Property string `json:"property"` // service:type[/subType]:name + Value string `json:"value,omitempty"` + Reset bool `json:"reset,omitempty"` +} + +// AgentConfigDiff describes a change applied to the form. +type AgentConfigDiff struct { + Property string `json:"property"` + OldValue string `json:"oldValue"` + NewValue string `json:"newValue"` + OldOverridden bool `json:"oldOverridden"` + NewOverridden bool `json:"newOverridden"` + Sensitive bool `json:"sensitive,omitempty"` +} + +// Apply applies changes to the form and returns the properties that change. +// Setting a property to its current value leaves it untouched, so a default +// value does not become an override. +func (f *AgentConfigForm) Apply(changes []AgentConfigChange) ([]AgentConfigDiff, error) { + var diffs []AgentConfigDiff + seen := map[int]bool{} + for _, ch := range changes { + sel, err := ParsePropertySelector(ch.Property) + if err != nil { + return nil, err + } + i, err := f.Find(sel) + if err != nil { + return nil, err + } + if f.Properties[i].Service == SystemService { + return nil, fmt.Errorf("property %s: changing the agent's own (%s) properties is not supported; the server ignores them on save", f.Properties[i].Key(), SystemService) + } + if seen[i] { + return nil, fmt.Errorf("property %s is changed more than once", f.Properties[i].Key()) + } + seen[i] = true + old := f.Properties[i] + switch value := normalizeNewlines(ch.Value); { + case ch.Reset: + f.Reset(i) + case value != old.Value: + f.Set(i, value) + } + p := f.Properties[i] + if p.Value == old.Value && p.Overridden == old.Overridden { + continue + } + diffs = append(diffs, AgentConfigDiff{ + Property: p.Key(), OldValue: old.Value, NewValue: p.Value, + OldOverridden: old.Overridden, NewOverridden: p.Overridden, Sensitive: p.Sensitive, + }) + } + return diffs, nil +} + +// VerifyAgentConfigDiffs reads the agent form again and checks that each +// change was stored. Sensitive values are only checked for the overridden +// flag, as the form may not return them. +func (c *Client) VerifyAgentConfigDiffs(ctx context.Context, agentID string, diffs []AgentConfigDiff) error { + form, err := c.GetAgentConfigForm(ctx, agentID) + if err != nil { + return fmt.Errorf("verifying changes: %w", err) + } + var failed []string + for _, d := range diffs { + sel, err := ParsePropertySelector(d.Property) + if err != nil { + return err + } + i, err := form.Find(sel) + if err != nil { + failed = append(failed, d.Property+" (not found)") + continue + } + p := form.Properties[i] + if p.Overridden != d.NewOverridden || !d.Sensitive && p.Value != d.NewValue { + failed = append(failed, fmt.Sprintf("%s (now %q, overridden %t)", d.Property, p.Value, p.Overridden)) + } + } + if len(failed) > 0 { + return fmt.Errorf("the agent did not store these changes: %s", strings.Join(failed, "; ")) + } + return nil +} diff --git a/internal/client/agentform_test.go b/internal/client/agentform_test.go new file mode 100644 index 0000000..370dabf --- /dev/null +++ b/internal/client/agentform_test.go @@ -0,0 +1,306 @@ +package client + +import ( + "io" + "net/http" + "net/url" + "strings" + "testing" +) + +// agentFormPage is a trimmed agent.do page with the same markup as the +// Administrator UI: system rows, engine rows (input, textarea, sensitive +// checkbox) and the service selector. +const agentFormPage = ` + + + + + + + + + + + + + + + + + + + + clusterprimary-node + + + + + + + + dbconnection-properties + + + + + + + + OpsInsightssecretKey + + + + + + + + + + + + +` + +func agentPage(csrf, hasErrors, errMsg string) string { + r := strings.NewReplacer("%s", "\x00") + parts := strings.Split(r.Replace(agentFormPage), "\x00") + return parts[0] + csrf + parts[1] + hasErrors + parts[2] + errMsg + parts[3] +} + +func TestParseAgentConfigForm(t *testing.T) { + form, err := parseAgentConfigForm(agentPage("tok", "false", "")) + if err != nil { + t.Fatal(err) + } + if form.AgentID != "01D23X0800000000008G" || form.AgentName != "CAI-A03" || form.Version != "119" || form.csrf != "tok" { + t.Errorf("form header = %q %q %q %q", form.AgentID, form.AgentName, form.Version, form.csrf) + } + if len(form.Properties) != 5 { + t.Fatalf("got %d properties, want 5", len(form.Properties)) + } + want := []struct{ key, value, serviceName string }{ + {"agent:AGENTCORE_LOG4J/INFO:log4j.rootLogger", "INFO, aclog", "Secure Agent"}, + {"agent:AGENTCORE_LOG4J/DEBUG:log4j.rootLogger", "DEBUG, aclog", "Secure Agent"}, + {"process-engine:cluster:primary-node", "true", "Process Server"}, + {"process-engine:db:connection-properties", "- key: testOnBorrow\n value: true\n", "Process Server"}, + {"OpsInsightsDataCollector:OpsInsights:secretKey", "'********'", "OI Data Collector"}, + } + for i, w := range want { + p := form.Properties[i] + if p.Key() != w.key || p.Value != w.value || p.ServiceName != w.serviceName { + t.Errorf("property %d = %q %q %q, want %q %q %q", i, p.Key(), p.Value, p.ServiceName, w.key, w.value, w.serviceName) + } + } + if !form.Properties[4].Sensitive || !form.Properties[4].Overridden || form.Properties[2].Sensitive { + t.Error("sensitive or overridden flags not parsed") + } + for _, f := range form.fields { + if f.name == "customConfigName" { + t.Error("disabled field must not be submitted") + } + if f.name == "engineConfigSensitive" && f.value != "OpsInsightsDataCollector25OpsInsightssecretKey" { + t.Errorf("unchecked checkbox submitted: %q", f.value) + } + if f.name == "sysConfigSubTypeSel" && f.value != "INFO" { + t.Errorf("select submitted %q, want the selected option INFO", f.value) + } + } +} + +func TestParseAgentConfigFormNoForm(t *testing.T) { + if _, err := parseAgentConfigForm("login"); err == nil { + t.Error("expected an error for a page without the agent form") + } +} + +func TestAgentConfigFormApply(t *testing.T) { + form, err := parseAgentConfigForm(agentPage("tok", "false", "")) + if err != nil { + t.Fatal(err) + } + diffs, err := form.Apply([]AgentConfigChange{ + {Property: "Process Server:cluster:primary-node", Value: "false"}, + {Property: "process-engine:db:connection-properties", Value: "- key: testOnBorrow\r\n value: true\r\n"}, // unchanged + {Property: "opsinsightsdatacollector:opsinsights:secretkey", Reset: true}, + }) + if err != nil { + t.Fatal(err) + } + if len(diffs) != 2 { + t.Fatalf("got %d diffs, want 2: %+v", len(diffs), diffs) + } + if d := diffs[0]; d.Property != "process-engine:cluster:primary-node" || d.OldValue != "true" || d.NewValue != "false" || !d.NewOverridden { + t.Errorf("set diff = %+v", d) + } + if d := diffs[1]; d.NewValue != "''" || d.NewOverridden || !d.Sensitive { + t.Errorf("reset diff = %+v", d) + } + + body, err := url.ParseQuery(form.encode()) + if err != nil { + t.Fatal(err) + } + if body.Get("actionMethod") != "update" || body.Get("csrfToken") != "tok" { + t.Errorf("actionMethod=%q csrfToken=%q", body.Get("actionMethod"), body.Get("csrfToken")) + } + values := body["engineConfigValue"] + if values[0] != "false" || values[1] != "- key: testOnBorrow\r\n value: true\r\n" || values[2] != "''" { + t.Errorf("engineConfigValue = %q", values) + } + if over := body["engineConfigOverriden"]; over[0] != "true" || over[2] != "false" { + t.Errorf("engineConfigOverriden = %q", over) + } +} + +func TestAgentConfigFormApplyErrors(t *testing.T) { + form, err := parseAgentConfigForm(agentPage("tok", "false", "")) + if err != nil { + t.Fatal(err) + } + cases := map[string][]AgentConfigChange{ + "ambiguous": {{Property: "agent:AGENTCORE_LOG4J:log4j.rootLogger", Value: "x"}}, + "system": {{Property: "agent:AGENTCORE_LOG4J/INFO:log4j.rootLogger", Value: "x"}}, + "missing": {{Property: "process-engine:jvm:max-heap", Value: "x"}}, + "syntax": {{Property: "process-engine:max-heap", Value: "x"}}, + "twice": { + {Property: "process-engine:cluster:primary-node", Value: "false"}, + {Property: "process-engine:cluster:primary-node", Reset: true}, + }, + } + for name, changes := range cases { + if _, err := form.Apply(changes); err == nil { + t.Errorf("%s: expected an error", name) + } + } +} + +func TestAgentConfigFormFilter(t *testing.T) { + form, err := parseAgentConfigForm(agentPage("tok", "false", "")) + if err != nil { + t.Fatal(err) + } + got, err := form.Filter(AgentConfigFilter{Service: "process-engine", Name: "^primary"}) + if err != nil || len(got) != 1 || got[0].Name != "primary-node" { + t.Errorf("Filter by service and name = %+v, %v", got, err) + } + got, _ = form.Filter(AgentConfigFilter{Type: "AGENTCORE_LOG4J/DEBUG"}) + if len(got) != 1 || got[0].SubType != "DEBUG" { + t.Errorf("Filter by type/subType = %+v", got) + } + got, _ = form.Filter(AgentConfigFilter{Overridden: true}) + if len(got) != 1 || got[0].Name != "secretKey" { + t.Errorf("Filter overridden = %+v", got) + } + if _, err := form.Filter(AgentConfigFilter{Name: "("}); err == nil { + t.Error("expected an error for an invalid pattern") + } +} + +func TestGetAndSaveAgentConfigForm(t *testing.T) { + var posted url.Values + var postCookies map[string]string + hasErrors, errMsg := "false", "" + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != agentFormPath { + t.Errorf("path = %s", r.URL.Path) + } + cookies := map[string]string{} + for _, c := range r.Cookies() { + cookies[c.Name] = c.Value + } + if cookies["USER_SESSION"] != "test-session" || cookies["XSRF_TOKEN"] == "" { + t.Errorf("cookies = %v", cookies) + } + switch r.Method { + case http.MethodGet: + if r.URL.Query().Get("actionMethod") != "displayUpdate" || r.URL.Query().Get("id") != "01D23X0800000000008G" { + t.Errorf("query = %s", r.URL.RawQuery) + } + http.SetCookie(w, &http.Cookie{Name: "JSESSIONID", Value: "js1"}) + // The server echoes the XSRF_TOKEN cookie into the form. + _, _ = io.WriteString(w, agentPage(cookies["XSRF_TOKEN"], "false", "")) + case http.MethodPost: + if ct := r.Header.Get("Content-Type"); ct != "application/x-www-form-urlencoded" { + t.Errorf("Content-Type = %s", ct) + } + data, _ := io.ReadAll(r.Body) + posted, _ = url.ParseQuery(string(data)) + postCookies = cookies + _, _ = io.WriteString(w, agentPage(cookies["XSRF_TOKEN"], hasErrors, errMsg)) + } + }) + c := newTestClient(handler) + ctx := t.Context() + + form, err := c.GetAgentConfigForm(ctx, "01D23X0800000000008G") + if err != nil { + t.Fatal(err) + } + if _, err = form.Apply([]AgentConfigChange{{Property: "process-engine:cluster:primary-node", Value: "false"}}); err != nil { + t.Fatal(err) + } + if err = c.SaveAgentConfigForm(ctx, form); err != nil { + t.Fatal(err) + } + if posted.Get("actionMethod") != "update" || posted["engineConfigValue"][0] != "false" { + t.Errorf("posted actionMethod=%q value=%q", posted.Get("actionMethod"), posted["engineConfigValue"][0]) + } + if posted.Get("csrfToken") == "" || posted.Get("csrfToken") != postCookies["XSRF_TOKEN"] { + t.Errorf("csrfToken %q does not match XSRF_TOKEN cookie %q", posted.Get("csrfToken"), postCookies["XSRF_TOKEN"]) + } + if postCookies["JSESSIONID"] != "js1" { + t.Errorf("JSESSIONID cookie not sent back: %v", postCookies) + } + + hasErrors, errMsg = "true", "For configuration primary-node invalid value" + err = c.SaveAgentConfigForm(ctx, form) + if err == nil || !strings.Contains(err.Error(), "For configuration primary-node invalid value") { + t.Errorf("expected the page error, got %v", err) + } +} + +func TestGetAgentConfigFormRelogin(t *testing.T) { + logins := 0 + mux := http.NewServeMux() + mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { + logins++ + w.Header().Set("Content-Type", "application/json") + _, _ = io.WriteString(w, `{"products":[{"name":"Integration Cloud","baseApiUrl":"`+"http://"+r.Host+`"}],"userInfo":{"sessionId":"new-session"}}`) + }) + mux.HandleFunc(agentFormPath, func(w http.ResponseWriter, r *http.Request) { + if ck, _ := r.Cookie("USER_SESSION"); ck == nil || ck.Value != "new-session" { + _, _ = io.WriteString(w, "login page") + return + } + ck, _ := r.Cookie("XSRF_TOKEN") + _, _ = io.WriteString(w, agentPage(ck.Value, "false", "")) + }) + c := newTestClient(mux) + if _, err := c.GetAgentConfigForm(t.Context(), "01D23X0800000000008G"); err != nil { + t.Fatal(err) + } + if logins != 1 { + t.Errorf("logins = %d, want 1", logins) + } +} + +func TestParsePropertySelector(t *testing.T) { + sel, err := ParsePropertySelector("agent:AGENTCORE_LOG4J/DEBUG:log4j.logger.a:b") + if err != nil || sel.Service != "agent" || sel.Type != "AGENTCORE_LOG4J" || sel.SubType != "DEBUG" || sel.Name != "log4j.logger.a:b" { + t.Errorf("ParsePropertySelector = %+v, %v", sel, err) + } + for _, bad := range []string{"", "a:b", "a::c", ":b:c"} { + if _, err := ParsePropertySelector(bad); err == nil { + t.Errorf("ParsePropertySelector(%q): expected an error", bad) + } + } +} diff --git a/internal/client/client.go b/internal/client/client.go index 84ce7f0..0255ecc 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -250,7 +250,7 @@ func debugPrintHTTP(req *http.Request, reqData []byte, resp *http.Response, resp _, _ = fmt.Fprintf(w, "%s[DEBUG] Request Headers:\n", now) for k, vs := range req.Header { for _, v := range vs { - if strings.EqualFold(k, sessionHeaderV3) || strings.EqualFold(k, sessionHeaderV2) || strings.EqualFold(k, sessionHeaderFRS) { + if strings.EqualFold(k, sessionHeaderV3) || strings.EqualFold(k, sessionHeaderV2) || strings.EqualFold(k, sessionHeaderFRS) || strings.EqualFold(k, "Cookie") { _, _ = fmt.Fprintf(w, " %s: ***\n", k) } else { _, _ = fmt.Fprintf(w, " %s: %s\n", k, v) @@ -265,6 +265,9 @@ func debugPrintHTTP(req *http.Request, reqData []byte, resp *http.Response, resp _, _ = fmt.Fprintf(w, "%s[DEBUG] Response Headers:\n", now) for k, vs := range resp.Header { for _, v := range vs { + if strings.EqualFold(k, "Set-Cookie") { + v = "***" + } _, _ = fmt.Fprintf(w, " %s: %s\n", k, v) } } From e39aeeef798e559eda50c0b794af8e8447f4f8ca Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Mon, 19 Oct 2026 23:20:58 -0400 Subject: [PATCH 37/52] feat: YAML/JSON change files, configs get --export, restart hint (CR-0049) - agent configs set --from-file reads JSON or YAML: a nested service/type/name map (null resets) or a list of {property, value, reset}; only the listed properties change - agent configs get --export writes the filtered properties in that format (YAML, or JSON with -o json), so overrides can be copied between agents - set lists the changed services with 'iics agent restart' commands, since Process Server keeps the old values until restarted manually - docs: works for any agent service mix; corrected Process Server restart behavior --- cmd/agent_configs.go | 77 ++++++++- completions/iics.bash | 2 + .../CR-0049-agent-level-service-properties.md | 37 +++- docs/documentation/agent.md | 96 ++++++++++- internal/client/agentconfigfile.go | 159 ++++++++++++++++++ internal/client/agentconfigfile_test.go | 100 +++++++++++ internal/client/agentform.go | 3 +- 7 files changed, 454 insertions(+), 20 deletions(-) create mode 100644 internal/client/agentconfigfile.go create mode 100644 internal/client/agentconfigfile_test.go diff --git a/cmd/agent_configs.go b/cmd/agent_configs.go index 429d29a..a3f6a03 100644 --- a/cmd/agent_configs.go +++ b/cmd/agent_configs.go @@ -10,6 +10,7 @@ import ( "github.com/jbrazda/iics-cli/internal/client" "github.com/jbrazda/iics-cli/internal/output" "github.com/spf13/cobra" + "gopkg.in/yaml.v3" ) const agentConfigsExperimental = "Experimental: uses the undocumented Administrator form endpoint /runtime/app/agent.do." @@ -91,6 +92,7 @@ func newAgentConfigsGetCmd() *cobra.Command { var ( id, fid, name, hostname string flt client.AgentConfigFilter + export bool ) cmd := &cobra.Command{ Use: "get", @@ -99,7 +101,8 @@ func newAgentConfigsGetCmd() *cobra.Command { Example: ` iics agent configs get --name CAI-A03 iics agent configs get --name CAI-A03 --service process-engine --overridden iics agent configs get --hostname myhost --service "Process Server" --type db - iics agent configs get --id --service agent --type TOMCAT_JRE -o json`, + iics agent configs get --id --service agent --type TOMCAT_JRE -o json + iics agent configs get --name CAI-A03 --service process-engine --overridden --export > pe.yaml`, RunE: func(cmd *cobra.Command, args []string) error { if id == "" && fid == "" && name == "" && hostname == "" { return fmt.Errorf("one of --id, --fid, --name, or --hostname is required") @@ -121,6 +124,9 @@ func newAgentConfigsGetCmd() *cobra.Command { if err != nil { return err } + if export { + return writeAgentConfigExport(cmd, props) + } f, err := getFormatter() if err != nil { return err @@ -133,9 +139,26 @@ func newAgentConfigsGetCmd() *cobra.Command { cmd.Flags().StringVar(&flt.Type, "type", "", "property type, for example cluster, db or AGENTCORE_LOG4J/DEBUG") cmd.Flags().StringVar(&flt.Name, "search", "", "regular expression on the property name (case-insensitive)") cmd.Flags().BoolVar(&flt.Overridden, "overridden", false, "show only properties that differ from the defaults") + cmd.Flags().BoolVar(&export, "export", false, "write the properties as a change file for 'agent configs set --from-file' (YAML, or JSON with -o json)") return cmd } +// writeAgentConfigExport writes props in the --from-file change format. +func writeAgentConfigExport(cmd *cobra.Command, props []client.AgentConfigProperty) error { + w := cmd.OutOrStdout() + if outputFmt == "json" { + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + return enc.Encode(client.AgentConfigMap(props)) + } + enc := yaml.NewEncoder(w) + enc.SetIndent(2) + if err := enc.Encode(client.AgentConfigDocument(props)); err != nil { + return err + } + return enc.Close() +} + // parseAgentConfigChanges builds the change list from --set, --reset and // --from-file. A --set value starting with @ is read from that file. func parseAgentConfigChanges(sets, resets []string, fromFile string) ([]client.AgentConfigChange, error) { @@ -145,8 +168,8 @@ func parseAgentConfigChanges(sets, resets []string, fromFile string) ([]client.A if err != nil { return nil, fmt.Errorf("reading file: %w", err) } - if err := json.Unmarshal(data, &changes); err != nil { - return nil, fmt.Errorf("parsing %s: %w", fromFile, err) + if changes, err = client.ParseAgentConfigChanges(data); err != nil { + return nil, fmt.Errorf("%s: %w", fromFile, err) } } for _, s := range sets { @@ -169,6 +192,28 @@ func parseAgentConfigChanges(sets, resets []string, fromFile string) ([]client.A return changes, nil } +// printRestartHint lists the services whose properties changed. Some, such as +// Process Server, only mark themselves as needing a restart and keep running +// with the old values until restarted. +func printRestartHint(cmd *cobra.Command, agent *client.Agent, diffs []client.AgentConfigDiff) { + var services []string + seen := map[string]bool{} + for _, d := range diffs { + if d.ServiceName != "" && !seen[d.ServiceName] { + seen[d.ServiceName] = true + services = append(services, d.ServiceName) + } + } + if len(services) == 0 { + return + } + w := cmd.ErrOrStderr() + _, _ = fmt.Fprintf(w, "\nChanged services: %s. Services such as Process Server use the new values only after a restart:\n", strings.Join(services, ", ")) + for _, s := range services { + _, _ = fmt.Fprintf(w, " iics agent restart --id %s --service %q --blocking\n", agent.ID, s) + } +} + func newAgentConfigsSetCmd() *cobra.Command { var ( id, fid, name, hostname string @@ -186,10 +231,25 @@ Use type/subType (for example AGENTCORE_LOG4J/DEBUG) when a name exists for both subtypes. Values are stored as typed, including quotes: Process Server values such as '1024M' keep their single quotes. +Changes come from --set, --reset and --from-file. The file is JSON or YAML, +either a nested map (a null value resets the property), as written by +"agent configs get --export": + + process-engine: + cluster: + primary-node: "true" + jvm: + max-heap: "'4G'" + min-heap: null + +or a list: [{"property": "service:type:name", "value": "...", "reset": false}]. +Only the listed properties change. + The changes are shown first and applied after confirmation (or with --yes). The agent's whole property form is submitted, as the Administrator UI does, -and read back to verify. The agent applies changed properties itself, which -may restart the affected service. +and read back to verify. Some services, such as Process Server, keep running +with the old values and are only marked as needing a restart; restart them +with "iics agent restart". Agents in a Secure Agent group with group-level properties get those settings from the group; the command refuses to change them unless --force is given. @@ -199,7 +259,9 @@ from the group; the command refuses to change them unless --force is given. iics agent configs set --name CAI-A03 --set "process-engine:jvm:max-heap='4G'" iics agent configs set --name CAI-A03 --set "process-engine:db:connection-properties=@db-props.yaml" iics agent configs set --name CAI-A03 --reset process-engine:jvm:max-heap --yes - iics agent configs set --name CAI-A03 --from-file changes.json`, + iics agent configs set --name CAI-A03 --from-file changes.yaml + iics agent configs get --name CAI-A01 --service process-engine --overridden --export > pe.yaml + iics agent configs set --name CAI-A02 --from-file pe.yaml --dry-run`, RunE: func(cmd *cobra.Command, args []string) error { if id == "" && fid == "" && name == "" && hostname == "" { return fmt.Errorf("one of --id, --fid, --name, or --hostname is required") @@ -266,13 +328,14 @@ from the group; the command refuses to change them unless --force is given. return err } _, _ = fmt.Fprintf(w, "Updated %d propert%s on agent %s.\n", len(diffs), map[bool]string{true: "y", false: "ies"}[len(diffs) == 1], agent.Name) + printRestartHint(cmd, agent, diffs) return nil }, } addAgentSelectorFlags(cmd, &id, &fid, &name, &hostname) cmd.Flags().StringArrayVar(&sets, "set", nil, "set a property: service:type:name=value (value @file reads a file); repeatable") cmd.Flags().StringArrayVar(&resets, "reset", nil, "reset a property to its default: service:type:name; repeatable") - cmd.Flags().StringVar(&fromFile, "from-file", "", `JSON file with [{"property":"service:type:name","value":"...","reset":false}]`) + cmd.Flags().StringVar(&fromFile, "from-file", "", "JSON or YAML change file: service/type/name map (null resets) or list of {property, value, reset}") cmd.Flags().BoolVar(&dryRun, "dry-run", false, "show the changes without applying them") cmd.Flags().BoolVarP(&yes, "yes", "y", false, "apply without asking for confirmation") cmd.Flags().BoolVar(&force, "force", false, "change the agent even if its group has group-level properties") diff --git a/completions/iics.bash b/completions/iics.bash index 63a34ed..13658c3 100644 --- a/completions/iics.bash +++ b/completions/iics.bash @@ -517,6 +517,8 @@ _iics_agent_configs_get() flags_with_completion=() flags_completion=() + flags+=("--export") + local_nonpersistent_flags+=("--export") flags+=("--fid=") two_word_flags+=("--fid") local_nonpersistent_flags+=("--fid") diff --git a/docs/ChangeRequests/pending/CR-0049-agent-level-service-properties.md b/docs/ChangeRequests/pending/CR-0049-agent-level-service-properties.md index 8554431..1a8b718 100644 --- a/docs/ChangeRequests/pending/CR-0049-agent-level-service-properties.md +++ b/docs/ChangeRequests/pending/CR-0049-agent-level-service-properties.md @@ -29,14 +29,25 @@ changed from the CLI. ## Desired Change +- The commands work for any agent regardless of which services it runs, for + example Process Server and OI Data Collector on CAI agents, or Data + Integration Server, Mass Ingestion, Database Ingestion, CMI Streaming Agent, + IDMC Data Gateway and Metadata services on CDI agents. - `iics agent configs get --id|--fid|--name|--hostname` lists the agent's properties with value, default and overridden flag. Filters: `--service`, `--type` (`type/subType`), `--search` (regex on the name), `--overridden`. + `--export` writes the filtered properties as a change file (YAML, or JSON + with `-o json`). - `iics agent configs set` changes properties with `--set service:type:name=value` (value `@file` reads a file), `--reset - service:type:name` and `--from-file` (JSON list of `{property, value, reset}`). + service:type:name` and `--from-file`. The file is JSON or YAML, either a + nested `service -> type -> name: value` map (`null` resets), as written by + `get --export`, or a list of `{property, value, reset}`. Only the listed + properties change. It prints a CURRENT / NEW table, asks for confirmation (`--yes` skips, - `--dry-run` only shows), saves, and reads back to verify. + `--dry-run` only shows), saves, and reads back to verify. It then lists the + changed services with the `iics agent restart` command for each, because + services such as Process Server only mark themselves as needing a restart. - `set` refuses agents whose group has group-level properties unless `--force`, because the docs say group settings replace agent settings. - `set` refuses the agent's own properties (`agent:` Agent Core, Agent Manager, @@ -57,6 +68,8 @@ changed from the CLI. ```text internal/client/agentform.go # form read/parse/apply/save/verify internal/client/agentform_test.go # parser, apply, filter, GET/POST, re-login tests +internal/client/agentconfigfile.go # change files (JSON/YAML map or list) and export +internal/client/agentconfigfile_test.go cmd/agent_configs.go # agent configs get / set ``` @@ -143,9 +156,23 @@ Form layout (`
`, about 1,000 fields, all submitted on save) not store the change, also after waiting 80 seconds. `set` therefore refuses `agent:` properties. Supporting them needs a HAR capture of a system property save in the UI. - - After a save, the agent redeploys the affected service. OI Data Collector - went NEED_RUNNING, then DEPLOYING, then RUNNING. Process Server - (`REPLACE_VERSION`) started a new version next to the running one. + - After a save, OI Data Collector went NEED_RUNNING, then DEPLOYING, then + RUNNING by itself. Process Server does not restart by itself after a + configuration change: it keeps the old values and the Administrator UI + shows that it needs a restart, which is done manually (developer + confirmation). `set` therefore prints restart commands for the changed + services. +- Read-only checks on CDI-G01-A01 in dev and tst (12 services, 426 and 435 + properties) found every property with a unique `service:type:name` address, + including Data Integration Server types such as `TOMCAT_JRE`, `PMRDTM_CFG` + and `SYSTEM_CFG`. A dry-run of a Data Integration Server change from a JSON + file showed only that property. +- Change files are parsed as YAML nodes, so scalars keep their written text + (`true`, `512`, `1.0`) and `null` means reset. YAML removes one level of + quotes, so Process Server values need `"'4G'"`. `--export` writes values + with quotes double-quoted and multi-line values as block scalars; an + unchanged export of CAI-A03's Process Server overrides and of a CDI agent's + `TOMCAT_JRE` properties applied as "Nothing to change". - Setting a property to its current value is a no-op, so defaults do not turn into overrides. - `--debug` now masks the `Cookie` and `Set-Cookie` headers, which carry the diff --git a/docs/documentation/agent.md b/docs/documentation/agent.md index 51b7ceb..9be794a 100644 --- a/docs/documentation/agent.md +++ b/docs/documentation/agent.md @@ -202,6 +202,11 @@ Process Server cluster settings (`primary-node`, `load-balance-url`), its database URLs and connection properties, or JVM heap sizes. The Secure Agent group equivalent is [environment configs](environment.md#environment-configs). +The commands work on whatever services the agent runs: Process Server and +OI Data Collector on a CAI agent, or Data Integration Server, Mass Ingestion, +Database Ingestion, CMI Streaming Agent, IDMC Data Gateway, Metadata services +and others on a CDI agent. Each agent's own form lists its properties. + > **Experimental.** The public REST API only configures properties for a whole > Secure Agent group. Agent-level properties are read and saved through the > Administrator UI form `/runtime/app/agent.do`, which is undocumented and may @@ -215,10 +220,11 @@ How it works: - `set` reads the whole form, changes only the requested properties and submits every field back, as the UI's Save button does. It then reads the form again and fails if a change was not stored. -- The agent applies the new properties by redeploying the affected service. - Process Server (`REPLACE_VERSION`) starts a new version next to the running - one and switches over when it is ready. Check progress with - `iics agent details --services`. +- How a service picks up new values depends on the service. Process Server + does **not** restart by itself: it keeps running with the old values and the + Administrator UI shows that it needs a restart. Restart it with + [agent restart](#agent-restart). `set` lists the changed services and the + restart commands after saving. ### Property names @@ -252,9 +258,16 @@ Show an agent's properties with their defaults and whether they are overridden. | `--type` | string | Only this property type, optionally `type/subType` | | `--search` | string | Regular expression on the property name (case-insensitive) | | `--overridden` | bool | Only properties whose value is overridden (differs from the default) | +| `--export` | bool | Write the properties as a `--from-file` change file: YAML, or JSON with `-o json` | Exactly one of `--id` / `--fid` / `--name` / `--hostname` is required. +`--export` writes the filtered properties as a nested service / type / name +map, the format `agent configs set --from-file` reads. Values that contain +quotes are written as `"'4G'"` and multi-line values as YAML block scalars, so +the file reads back unchanged. `agent` properties are left out because they +cannot be set. + Table columns: `SERVICE`, `TYPE` (with `/subType` for `agent` properties), `NAME`, `VALUE`, `DEFAULT`, `OVERRIDDEN`. `--output json` / `yaml` / `csv` list the properties with `service`, `serviceName`, `type`, `subType`, `name`, @@ -265,6 +278,10 @@ iics agent configs get --name CAI-A03 --overridden iics agent configs get --name CAI-A03 --service "Process Server" --type cluster iics agent configs get --hostname myhost --service process-engine --search '^db|url' -o json iics agent configs get --name CAI-A03 --service agent --type TOMCAT_JRE +iics agent configs get --name CDI-G01-A01 --service "Data Integration Server" --type TOMCAT_JRE + +# Save the Process Server overrides of one agent as a change file +iics agent configs get --name CAI-A01 --service process-engine --overridden --export > pe.yaml ``` ### agent configs set @@ -282,7 +299,7 @@ confirmation. | `--hostname` | string | Agent host name | | `--set` | string | `service:type:name=value`; a value `@file` reads the value from a file. Repeatable | | `--reset` | string | `service:type:name` to reset to the default. Repeatable | -| `--from-file` | string | JSON list of changes (see below) | +| `--from-file` | string | JSON or YAML change file (see below); only the properties in the file change | | `--dry-run` | bool | Show the changes without applying them | | `--yes`, `-y` | bool | Apply without asking for confirmation | | `--force` | bool | Change the agent even if its Secure Agent group has group-level properties | @@ -303,7 +320,39 @@ Rules: - Errors reported by the form, such as an invalid value, are returned as the command error and nothing is changed. -`--from-file` format: +### Change files + +`--from-file` reads JSON or YAML in one of two shapes. Only the properties in +the file change; all others keep their values. `--set` and `--reset` can be +combined with a file. + +A nested service / type / name map, as written by `get --export`. A `null` +value resets the property to its default: + +```yaml +process-engine: + cluster: + primary-node: "true" + jvm: + max-heap: "'4G'" + min-heap: null + db: + connection-properties: | + - key: timeBetweenEvictionRunsMillis + value: 300000 + - key: testOnBorrow + value: true +``` + +```json +{ + "Data_Integration_Server": { + "TOMCAT_JRE": { "INFA_MEMORY": "'-Xms512m -Xmx4096m'" } + } +} +``` + +A list of changes: ```json [ @@ -313,6 +362,31 @@ Rules: ] ``` +Notes on values: + +- Values are used as written. YAML removes one level of quotes, so a + Process Server value `'4G'` must be written as `"'4G'"` in YAML, the same + as in JSON. Unquoted `true` or `512` become the strings `true` and + `512`. +- In YAML and JSON double-quoted strings a backslash must be doubled + (`C:\\Windows`). +- Service, type and name are matched like `--set` names, so the display name + (`"Process Server"`) also works as the service key. + +### After saving + +`set` prints the services whose properties changed, with the command to +restart each one, for example: + +```text +Changed services: Process Server. Services such as Process Server use the new values only after a restart: + iics agent restart --id 01D23X0800000000008G --service "Process Server" --blocking +``` + +Process Server keeps running with the old values until it is restarted. + +### Examples for set + ```bash # Preview a change iics agent configs set --name CAI-A03 --set "process-engine:cluster:primary-node=false" --dry-run @@ -329,7 +403,15 @@ iics agent configs set --name CAI-A03 \ iics agent configs set --name CAI-A03 --reset process-engine:jvm:max-heap --yes # Several changes from a file -iics agent configs set --hostname myhost --from-file changes.json +iics agent configs set --hostname myhost --from-file changes.yaml + +# Copy Process Server overrides from one agent to another, then review +iics agent configs get --name CAI-A01 --service process-engine --overridden --export > pe.yaml +# edit agent-specific values such as host-name, key-alias and primary-node +iics agent configs set --name CAI-A02 --from-file pe.yaml --dry-run + +# Restart Process Server to use the new values +iics agent restart --name CAI-A03 --service "Process Server" --blocking ``` ```powershell diff --git a/internal/client/agentconfigfile.go b/internal/client/agentconfigfile.go new file mode 100644 index 0000000..521d21b --- /dev/null +++ b/internal/client/agentconfigfile.go @@ -0,0 +1,159 @@ +package client + +import ( + "fmt" + "strings" + + "gopkg.in/yaml.v3" +) + +// ParseAgentConfigChanges parses an agent property change file, JSON or YAML, +// in one of two shapes: +// +// A nested map, as written by AgentConfigDocument. A null value resets the +// property to its default: +// +// process-engine: +// cluster: +// primary-node: "true" +// jvm: +// max-heap: "'4G'" +// min-heap: null +// +// A list of changes: +// +// [{"property": "process-engine:cluster:primary-node", "value": "true"}, +// {"property": "process-engine:jvm:min-heap", "reset": true}] +// +// Scalars are used as written, so true or 512 become "true" and "512". +func ParseAgentConfigChanges(data []byte) ([]AgentConfigChange, error) { + var doc yaml.Node + if err := yaml.Unmarshal(data, &doc); err != nil { + return nil, fmt.Errorf("parsing change file: %w", err) + } + if len(doc.Content) == 0 { + return nil, nil + } + root := doc.Content[0] + switch root.Kind { + case yaml.MappingNode: + return parseChangeMap(root) + case yaml.SequenceNode: + return parseChangeList(root) + } + return nil, fmt.Errorf("change file: expected a service map or a list of changes (line %d)", root.Line) +} + +func parseChangeMap(root *yaml.Node) ([]AgentConfigChange, error) { + var changes []AgentConfigChange + for i := 0; i+1 < len(root.Content); i += 2 { + service, types := root.Content[i].Value, root.Content[i+1] + if types.Kind != yaml.MappingNode { + return nil, fmt.Errorf("change file line %d: %s must map property types to properties", types.Line, service) + } + for j := 0; j+1 < len(types.Content); j += 2 { + typ, props := types.Content[j].Value, types.Content[j+1] + if props.Kind != yaml.MappingNode { + return nil, fmt.Errorf("change file line %d: %s:%s must map property names to values", props.Line, service, typ) + } + for k := 0; k+1 < len(props.Content); k += 2 { + name, val := props.Content[k].Value, props.Content[k+1] + ch := AgentConfigChange{Property: service + ":" + typ + ":" + name} + switch { + case val.Kind == yaml.ScalarNode && val.Tag == "!!null": + ch.Reset = true + case val.Kind == yaml.ScalarNode: + ch.Value = val.Value + default: + return nil, fmt.Errorf("change file line %d: value of %s must be a string, or null to reset", val.Line, ch.Property) + } + changes = append(changes, ch) + } + } + } + return changes, nil +} + +func parseChangeList(root *yaml.Node) ([]AgentConfigChange, error) { + var changes []AgentConfigChange + for _, item := range root.Content { + if item.Kind != yaml.MappingNode { + return nil, fmt.Errorf("change file line %d: expected {property, value} or {property, reset}", item.Line) + } + var ch AgentConfigChange + for k := 0; k+1 < len(item.Content); k += 2 { + key, val := item.Content[k].Value, item.Content[k+1] + if val.Kind != yaml.ScalarNode { + return nil, fmt.Errorf("change file line %d: %s must be a scalar", val.Line, key) + } + switch key { + case "property": + ch.Property = val.Value + case "value": + ch.Value = val.Value + case "reset": + ch.Reset = val.Value == "true" + default: + return nil, fmt.Errorf("change file line %d: unknown key %q", item.Content[k].Line, key) + } + } + if ch.Property == "" { + return nil, fmt.Errorf("change file line %d: property is required", item.Line) + } + changes = append(changes, ch) + } + return changes, nil +} + +// AgentConfigDocument returns props as a nested service/type/name map in +// form order, the format ParseAgentConfigChanges reads. The agent's own +// (SystemService) properties are left out because they cannot be set. +func AgentConfigDocument(props []AgentConfigProperty) *yaml.Node { + root := &yaml.Node{Kind: yaml.MappingNode} + child := func(parent *yaml.Node, key string) *yaml.Node { + for i := 0; i+1 < len(parent.Content); i += 2 { + if parent.Content[i].Value == key { + return parent.Content[i+1] + } + } + n := &yaml.Node{Kind: yaml.MappingNode} + parent.Content = append(parent.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: key}, n) + return n + } + for _, p := range props { + if p.Service == SystemService { + continue + } + val := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: p.Value} + switch { + case strings.Contains(p.Value, "\n"): + val.Style = yaml.LiteralStyle + case strings.Contains(p.Value, "'"): + // Process Server values carry YAML quotes: write "'4G'", not '''4G'''. + val.Style = yaml.DoubleQuotedStyle + } + types := child(root, p.Service) + names := child(types, p.Type) + names.Content = append(names.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: p.Name}, val) + } + return root +} + +// AgentConfigMap returns the same content as AgentConfigDocument as plain +// maps, for JSON output. +func AgentConfigMap(props []AgentConfigProperty) map[string]map[string]map[string]string { + out := map[string]map[string]map[string]string{} + for _, p := range props { + if p.Service == SystemService { + continue + } + if out[p.Service] == nil { + out[p.Service] = map[string]map[string]string{} + } + if out[p.Service][p.Type] == nil { + out[p.Service][p.Type] = map[string]string{} + } + out[p.Service][p.Type][p.Name] = p.Value + } + return out +} diff --git a/internal/client/agentconfigfile_test.go b/internal/client/agentconfigfile_test.go new file mode 100644 index 0000000..f75b05f --- /dev/null +++ b/internal/client/agentconfigfile_test.go @@ -0,0 +1,100 @@ +package client + +import ( + "reflect" + "testing" + + "gopkg.in/yaml.v3" +) + +func TestParseAgentConfigChangesMap(t *testing.T) { + data := ` +process-engine: + cluster: + primary-node: true + jvm: + max-heap: "'4G'" + min-heap: null + db: + connection-properties: | + - key: testOnBorrow + value: true +OpsInsightsDataCollector: + OpsInsights: + maxBufferSize: 1024 +` + got, err := ParseAgentConfigChanges([]byte(data)) + if err != nil { + t.Fatal(err) + } + want := []AgentConfigChange{ + {Property: "process-engine:cluster:primary-node", Value: "true"}, + {Property: "process-engine:jvm:max-heap", Value: "'4G'"}, + {Property: "process-engine:jvm:min-heap", Reset: true}, + {Property: "process-engine:db:connection-properties", Value: "- key: testOnBorrow\n value: true\n"}, + {Property: "OpsInsightsDataCollector:OpsInsights:maxBufferSize", Value: "1024"}, + } + if !reflect.DeepEqual(got, want) { + t.Errorf("got %+v\nwant %+v", got, want) + } +} + +func TestParseAgentConfigChangesJSON(t *testing.T) { + m, err := ParseAgentConfigChanges([]byte(`{"process-engine": {"cluster": {"primary-node": "false"}}}`)) + if err != nil || len(m) != 1 || m[0].Property != "process-engine:cluster:primary-node" || m[0].Value != "false" { + t.Errorf("JSON map = %+v, %v", m, err) + } + l, err := ParseAgentConfigChanges([]byte(`[{"property": "process-engine:jvm:max-heap", "value": "'4G'"}, + {"property": "process-engine:jvm:min-heap", "reset": true}]`)) + want := []AgentConfigChange{ + {Property: "process-engine:jvm:max-heap", Value: "'4G'"}, + {Property: "process-engine:jvm:min-heap", Reset: true}, + } + if err != nil || !reflect.DeepEqual(l, want) { + t.Errorf("JSON list = %+v, %v", l, err) + } +} + +func TestParseAgentConfigChangesErrors(t *testing.T) { + for name, data := range map[string]string{ + "scalar root": `"x"`, + "flat map": `{"process-engine": "x"}`, + "no names": `{"process-engine": {"jvm": "x"}}`, + "nested value": `{"process-engine": {"jvm": {"max-heap": {"a": 1}}}}`, + "unknown key": `[{"property": "a:b:c", "val": "x"}]`, + "no property": `[{"value": "x"}]`, + "invalid syntax": `{"a": [}`, + } { + if _, err := ParseAgentConfigChanges([]byte(data)); err == nil { + t.Errorf("%s: expected an error", name) + } + } +} + +func TestAgentConfigExportRoundTrip(t *testing.T) { + form, err := parseAgentConfigForm(agentPage("tok", "false", "")) + if err != nil { + t.Fatal(err) + } + out, err := yaml.Marshal(AgentConfigDocument(form.Properties)) + if err != nil { + t.Fatal(err) + } + changes, err := ParseAgentConfigChanges(out) + if err != nil { + t.Fatalf("parsing export: %v\n%s", err, out) + } + // System properties are not exported; the three engine properties are. + if len(changes) != 3 { + t.Fatalf("got %d changes from export:\n%s", len(changes), out) + } + // Applying an unchanged export changes nothing. + diffs, err := form.Apply(changes) + if err != nil || len(diffs) != 0 { + t.Errorf("applying unchanged export: %+v, %v\n%s", diffs, err, out) + } + m := AgentConfigMap(form.Properties) + if m["process-engine"]["cluster"]["primary-node"] != "true" || len(m) != 2 { + t.Errorf("AgentConfigMap = %v", m) + } +} diff --git a/internal/client/agentform.go b/internal/client/agentform.go index 05c9fc2..e2be52c 100644 --- a/internal/client/agentform.go +++ b/internal/client/agentform.go @@ -549,6 +549,7 @@ type AgentConfigChange struct { // AgentConfigDiff describes a change applied to the form. type AgentConfigDiff struct { Property string `json:"property"` + ServiceName string `json:"serviceName,omitempty"` OldValue string `json:"oldValue"` NewValue string `json:"newValue"` OldOverridden bool `json:"oldOverridden"` @@ -590,7 +591,7 @@ func (f *AgentConfigForm) Apply(changes []AgentConfigChange) ([]AgentConfigDiff, continue } diffs = append(diffs, AgentConfigDiff{ - Property: p.Key(), OldValue: old.Value, NewValue: p.Value, + Property: p.Key(), ServiceName: p.ServiceName, OldValue: old.Value, NewValue: p.Value, OldOverridden: old.Overridden, NewOverridden: p.Overridden, Sensitive: p.Sensitive, }) } From 8f86d59a2f1d32ace997090fd3d1f2424ca2db29 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Mon, 19 Oct 2026 23:29:34 -0400 Subject: [PATCH 38/52] feat: agent rename; hide non-editable agent system properties (CR-0049) - iics agent rename --id|--fid|--name|--hostname --new-name renames an agent through the agent form (installer registers the host name; standard names are logical, e.g. CAI-A03). Trimmed, max 100 chars, unique in the org, verified with the agent API. - agent configs get hides the agent's own Agent Core / Agent Manager / Tomcat rows (a hidden legacy form section the UI never shows and the server ignores) unless --include-hidden or --service agent is given. --- README.md | 70 ++++++++-------- cmd/agent.go | 1 + cmd/agent_configs.go | 7 +- cmd/agent_rename.go | 65 +++++++++++++++ completions/iics.bash | 66 +++++++++++++++ .../CR-0049-agent-level-service-properties.md | 21 ++++- docs/documentation/agent.md | 80 +++++++++++++++---- internal/client/agentform.go | 68 ++++++++++++++-- internal/client/agentform_test.go | 50 +++++++++++- 9 files changed, 368 insertions(+), 60 deletions(-) create mode 100644 cmd/agent_rename.go diff --git a/README.md b/README.md index 4bbdb43..95a859c 100644 --- a/README.md +++ b/README.md @@ -265,41 +265,41 @@ it easy to override credentials in CI pipelines without touching the config file ## Commands -| Command | Alias | Subcommands | Description | -| ---------------------------------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | -| [activitylog](docs/documentation/activitylog.md) | | `list`, `get` | Query activity logs for completed jobs | -| [agent](docs/documentation/agent.md) | | `list`, `get`, `details`, `delete`, `start`, `stop`, `restart`, `installer-info`, `installer-download`, `configs` | Manage Secure Agents | -| [auditlog](docs/documentation/auditlog.md) | | `list` | Query organization audit log (V2 API) | -| [completion](docs/documentation/completion.md) | | `bash`, `zsh`, `fish`, `powershell` | Generate shell completion scripts | -| [config](docs/documentation/config.md) | | `edit` | Edit global settings (new user defaults, theme, timeout, menu) | -| [connection](docs/documentation/connection.md) | `conn` | `list`, `get`, `create`, `update`, `delete`, `test`, `search`, `objects`, `metadata` | Manage connections | -| [connector](docs/documentation/connector.md) | | `list`, `metadata` | List connectors and their connection attributes | -| [export](docs/documentation/export.md) | | `run`, `start`, `status`, `download`, `create` | Export asset packages | -| [folder](docs/documentation/folder.md) | | `list`, `create`, `update`, `delete` | Manage folders | -| [import](docs/documentation/import.md) | `imp` | `run`, `upload`, `start`, `status`, `download-log` | Import asset packages | -| [login](docs/documentation/login.md) | | | Authenticate and cache session | -| [logout](docs/documentation/logout.md) | | | Invalidate session | -| [lookup](docs/documentation/lookup.md) | | | Resolve object IDs, names, and paths | -| [metering](docs/documentation/metering.md) | | `get`, `download` | Query usage and metering data | -| [objects](docs/documentation/objects.md) | | `list`, `dependencies` | List/search assets, find dependencies | -| [package](docs/documentation/package.md) | | `expand`, `create`, `dependencies` | Extract, assemble, or inspect dependencies of IICS export packages | -| [menu](docs/documentation/menu.md) | `tui` | - | Interactive main menu (bare `iics` in a terminal) | -| [permission](docs/documentation/permission.md) | `perm` | `get`, `add`, `update`, `delete`, `set`, `check`, `edit` | Manage object-level permissions | -| [privilege](docs/documentation/privilege.md) | | `list` | List available privileges | -| [profile](docs/documentation/profile.md) | | `add`, `edit`, `list`, `delete`, `set-default`, `set-password`, `show` | Manage connection profiles | -| [project](docs/documentation/project.md) | | `list`, `create`, `update`, `delete` | Manage projects | -| [publish](docs/documentation/publish.md) | | `start`, `status`, `run` | Publish CAI assets to the runtime | -| [release](docs/documentation/release.md) | | `manifest`, `validate`, `plan` | Generate multi-format release manifests and per-environment plans | -| [role](docs/documentation/role.md) | | `list`, `get`, `create`, `edit`, `add-privileges`, `remove-privileges`, `delete` | Manage roles | -| [environment](docs/documentation/environment.md) | `runtime`, `rt`, `env` | `list`, `get`, `create`, `update`, `delete`, `configs` | Manage runtime environments (Secure Agent groups) | -| [schedule](docs/documentation/schedule.md) | | `list`, `get`, `create`, `update`, `delete` | Manage schedules | -| [securitylog](docs/documentation/securitylog.md) | | `list` | Query security audit log | -| [sourcecontrol](docs/documentation/sourcecontrol.md) | `sc` | `checkout`, `checkin`, `pull`, `commit` | Source control operations | -| [state](docs/documentation/state.md) | | `fetch`, `load` | Fetch/load object state snapshots | -| [tag](docs/documentation/tag.md) | | `assign`, `remove` | Assign/remove tags on objects | -| [unpublish](docs/documentation/unpublish.md) | | `start`, `status`, `run` | Unpublish CAI assets from the runtime | -| [user](docs/documentation/user.md) | | `list`, `get`, `create`, `edit`, `update-roles`, `update-groups`, `delete` | Manage users | -| [group](docs/documentation/group.md) | `usergroup`, `ug` | `list`, `get`, `create`, `update`, `delete` | Manage user groups | +| Command | Alias | Subcommands | Description | +| ---------------------------------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | +| [activitylog](docs/documentation/activitylog.md) | | `list`, `get` | Query activity logs for completed jobs | +| [agent](docs/documentation/agent.md) | | `list`, `get`, `details`, `delete`, `start`, `stop`, `restart`, `installer-info`, `installer-download`, `configs`, `rename` | Manage Secure Agents | +| [auditlog](docs/documentation/auditlog.md) | | `list` | Query organization audit log (V2 API) | +| [completion](docs/documentation/completion.md) | | `bash`, `zsh`, `fish`, `powershell` | Generate shell completion scripts | +| [config](docs/documentation/config.md) | | `edit` | Edit global settings (new user defaults, theme, timeout, menu) | +| [connection](docs/documentation/connection.md) | `conn` | `list`, `get`, `create`, `update`, `delete`, `test`, `search`, `objects`, `metadata` | Manage connections | +| [connector](docs/documentation/connector.md) | | `list`, `metadata` | List connectors and their connection attributes | +| [export](docs/documentation/export.md) | | `run`, `start`, `status`, `download`, `create` | Export asset packages | +| [folder](docs/documentation/folder.md) | | `list`, `create`, `update`, `delete` | Manage folders | +| [import](docs/documentation/import.md) | `imp` | `run`, `upload`, `start`, `status`, `download-log` | Import asset packages | +| [login](docs/documentation/login.md) | | | Authenticate and cache session | +| [logout](docs/documentation/logout.md) | | | Invalidate session | +| [lookup](docs/documentation/lookup.md) | | | Resolve object IDs, names, and paths | +| [metering](docs/documentation/metering.md) | | `get`, `download` | Query usage and metering data | +| [objects](docs/documentation/objects.md) | | `list`, `dependencies` | List/search assets, find dependencies | +| [package](docs/documentation/package.md) | | `expand`, `create`, `dependencies` | Extract, assemble, or inspect dependencies of IICS export packages | +| [menu](docs/documentation/menu.md) | `tui` | - | Interactive main menu (bare `iics` in a terminal) | +| [permission](docs/documentation/permission.md) | `perm` | `get`, `add`, `update`, `delete`, `set`, `check`, `edit` | Manage object-level permissions | +| [privilege](docs/documentation/privilege.md) | | `list` | List available privileges | +| [profile](docs/documentation/profile.md) | | `add`, `edit`, `list`, `delete`, `set-default`, `set-password`, `show` | Manage connection profiles | +| [project](docs/documentation/project.md) | | `list`, `create`, `update`, `delete` | Manage projects | +| [publish](docs/documentation/publish.md) | | `start`, `status`, `run` | Publish CAI assets to the runtime | +| [release](docs/documentation/release.md) | | `manifest`, `validate`, `plan` | Generate multi-format release manifests and per-environment plans | +| [role](docs/documentation/role.md) | | `list`, `get`, `create`, `edit`, `add-privileges`, `remove-privileges`, `delete` | Manage roles | +| [environment](docs/documentation/environment.md) | `runtime`, `rt`, `env` | `list`, `get`, `create`, `update`, `delete`, `configs` | Manage runtime environments (Secure Agent groups) | +| [schedule](docs/documentation/schedule.md) | | `list`, `get`, `create`, `update`, `delete` | Manage schedules | +| [securitylog](docs/documentation/securitylog.md) | | `list` | Query security audit log | +| [sourcecontrol](docs/documentation/sourcecontrol.md) | `sc` | `checkout`, `checkin`, `pull`, `commit` | Source control operations | +| [state](docs/documentation/state.md) | | `fetch`, `load` | Fetch/load object state snapshots | +| [tag](docs/documentation/tag.md) | | `assign`, `remove` | Assign/remove tags on objects | +| [unpublish](docs/documentation/unpublish.md) | | `start`, `status`, `run` | Unpublish CAI assets from the runtime | +| [user](docs/documentation/user.md) | | `list`, `get`, `create`, `edit`, `update-roles`, `update-groups`, `delete` | Manage users | +| [group](docs/documentation/group.md) | `usergroup`, `ug` | `list`, `get`, `create`, `update`, `delete` | Manage user groups | > **Keeping completions up to date:** After adding or changing any command or flag, regenerate > the shell completion scripts by running `make completions`. The pre-generated scripts live in diff --git a/cmd/agent.go b/cmd/agent.go index 78807eb..42ce363 100644 --- a/cmd/agent.go +++ b/cmd/agent.go @@ -27,6 +27,7 @@ func newAgentCmd() *cobra.Command { cmd.AddCommand(newAgentInstallerInfoCmd()) cmd.AddCommand(newAgentInstallerDownloadCmd()) cmd.AddCommand(newAgentConfigsCmd()) + cmd.AddCommand(newAgentRenameCmd()) return cmd } diff --git a/cmd/agent_configs.go b/cmd/agent_configs.go index a3f6a03..12cd63e 100644 --- a/cmd/agent_configs.go +++ b/cmd/agent_configs.go @@ -20,8 +20,8 @@ func newAgentConfigsCmd() *cobra.Command { Use: "configs", Short: "Show and change agent-level Secure Agent service properties (experimental)", Long: `Show and change Secure Agent service properties of a single agent, such as -the Process Server cluster or database settings, and the agent's own -Agent Core, Agent Manager and Tomcat settings. +the Process Server cluster or database settings, for any services the agent +runs. ` + agentConfigsExperimental + ` The public REST API only configures properties for a whole Secure Agent group (see "iics environment @@ -101,7 +101,7 @@ func newAgentConfigsGetCmd() *cobra.Command { Example: ` iics agent configs get --name CAI-A03 iics agent configs get --name CAI-A03 --service process-engine --overridden iics agent configs get --hostname myhost --service "Process Server" --type db - iics agent configs get --id --service agent --type TOMCAT_JRE -o json + iics agent configs get --name CDI-G01-A01 --service "Data Integration Server" --type TOMCAT_JRE -o json iics agent configs get --name CAI-A03 --service process-engine --overridden --export > pe.yaml`, RunE: func(cmd *cobra.Command, args []string) error { if id == "" && fid == "" && name == "" && hostname == "" { @@ -139,6 +139,7 @@ func newAgentConfigsGetCmd() *cobra.Command { cmd.Flags().StringVar(&flt.Type, "type", "", "property type, for example cluster, db or AGENTCORE_LOG4J/DEBUG") cmd.Flags().StringVar(&flt.Name, "search", "", "regular expression on the property name (case-insensitive)") cmd.Flags().BoolVar(&flt.Overridden, "overridden", false, "show only properties that differ from the defaults") + cmd.Flags().BoolVar(&flt.IncludeHidden, "include-hidden", false, "also show the agent's own Agent Core, Agent Manager and Tomcat properties, which the UI hides and which cannot be changed") cmd.Flags().BoolVar(&export, "export", false, "write the properties as a change file for 'agent configs set --from-file' (YAML, or JSON with -o json)") return cmd } diff --git a/cmd/agent_rename.go b/cmd/agent_rename.go new file mode 100644 index 0000000..deb1f71 --- /dev/null +++ b/cmd/agent_rename.go @@ -0,0 +1,65 @@ +package cmd + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" +) + +func newAgentRenameCmd() *cobra.Command { + var ( + id, fid, name, hostname, newName string + yes bool + ) + cmd := &cobra.Command{ + Use: "rename", + Short: "Rename a Secure Agent (experimental)", + Long: `Rename a Secure Agent. The installer registers an agent under its host name; +use this to give it a logical name, for example CAI-A03. + +The new name must be unique in the organization and at most 100 characters. +The name is saved through the agent's Administrator form, like +"agent configs set", together with its unchanged service properties. + +` + agentConfigsExperimental, + Example: ` iics agent rename --hostname cletinfacld07 --new-name CAI-A03 + iics agent rename --name cletinfacld07.corp.example.com --new-name CAI-A03 --yes + iics agent rename --id --new-name CDI-G01-A02`, + RunE: func(cmd *cobra.Command, args []string) error { + if id == "" && fid == "" && name == "" && hostname == "" { + return fmt.Errorf("one of --id, --fid, --name, or --hostname is required") + } + if newName == "" { + return fmt.Errorf("--new-name is required") + } + c, err := getClient(cmd) + if err != nil { + return err + } + ctx := context.Background() + agent, err := resolveAgent(ctx, c, id, name, hostname, fid) + if err != nil { + return err + } + w := cmd.OutOrStdout() + if agent.Name == newName { + _, _ = fmt.Fprintf(w, "Agent %s is already named %s.\n", agent.ID, newName) + return nil + } + if !yes && !confirmAction(cmd, fmt.Sprintf("Rename agent %q (%s, host %s) to %q?", agent.Name, agent.ID, agent.AgentHost, newName)) { + return nil + } + renamed, err := c.RenameAgent(ctx, agent.ID, newName) + if err != nil { + return err + } + _, _ = fmt.Fprintf(w, "Renamed agent %s from %q to %q.\n", renamed.ID, agent.Name, renamed.Name) + return nil + }, + } + addAgentSelectorFlags(cmd, &id, &fid, &name, &hostname) + cmd.Flags().StringVar(&newName, "new-name", "", "new agent name (required)") + cmd.Flags().BoolVarP(&yes, "yes", "y", false, "rename without asking for confirmation") + return cmd +} diff --git a/completions/iics.bash b/completions/iics.bash index 13658c3..840cc3c 100644 --- a/completions/iics.bash +++ b/completions/iics.bash @@ -531,6 +531,8 @@ _iics_agent_configs_get() two_word_flags+=("--id") local_nonpersistent_flags+=("--id") local_nonpersistent_flags+=("--id=") + flags+=("--include-hidden") + local_nonpersistent_flags+=("--include-hidden") flags+=("--name=") two_word_flags+=("--name") local_nonpersistent_flags+=("--name") @@ -1014,6 +1016,69 @@ _iics_agent_list() noun_aliases=() } +_iics_agent_rename() +{ + last_command="iics_agent_rename" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--fid=") + two_word_flags+=("--fid") + local_nonpersistent_flags+=("--fid") + local_nonpersistent_flags+=("--fid=") + flags+=("--hostname=") + two_word_flags+=("--hostname") + local_nonpersistent_flags+=("--hostname") + local_nonpersistent_flags+=("--hostname=") + flags+=("--id=") + two_word_flags+=("--id") + local_nonpersistent_flags+=("--id") + local_nonpersistent_flags+=("--id=") + flags+=("--name=") + two_word_flags+=("--name") + local_nonpersistent_flags+=("--name") + local_nonpersistent_flags+=("--name=") + flags+=("--new-name=") + two_word_flags+=("--new-name") + local_nonpersistent_flags+=("--new-name") + local_nonpersistent_flags+=("--new-name=") + flags+=("--yes") + flags+=("-y") + local_nonpersistent_flags+=("--yes") + local_nonpersistent_flags+=("-y") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--http-timeout=") + two_word_flags+=("--http-timeout") + flags+=("--no-color") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + flags+=("--profile=") + two_word_flags+=("--profile") + two_word_flags+=("-p") + flags+=("--theme=") + two_word_flags+=("--theme") + flags+=("--verbose") + flags+=("-v") + flags+=("--wide") + flags+=("--width=") + two_word_flags+=("--width") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _iics_agent_restart() { last_command="iics_agent_restart" @@ -1235,6 +1300,7 @@ _iics_agent() commands+=("installer-download") commands+=("installer-info") commands+=("list") + commands+=("rename") commands+=("restart") commands+=("start") commands+=("stop") diff --git a/docs/ChangeRequests/pending/CR-0049-agent-level-service-properties.md b/docs/ChangeRequests/pending/CR-0049-agent-level-service-properties.md index 1a8b718..c2eeea2 100644 --- a/docs/ChangeRequests/pending/CR-0049-agent-level-service-properties.md +++ b/docs/ChangeRequests/pending/CR-0049-agent-level-service-properties.md @@ -5,7 +5,7 @@ ## CR Type - [ ] **New resource** -- [x] **New subcommand** - `iics agent configs get` and `iics agent configs set` +- [x] **New subcommand** - `iics agent configs get`, `iics agent configs set`, `iics agent rename` - [ ] **Enhancement** - [ ] **Output change** - [ ] **Flag / config change** @@ -51,7 +51,12 @@ changed from the CLI. - `set` refuses agents whose group has group-level properties unless `--force`, because the docs say group settings replace agent settings. - `set` refuses the agent's own properties (`agent:` Agent Core, Agent Manager, - Tomcat), because the server ignores changes to them (see notes). + Tomcat), because the server ignores changes to them (see notes). `get` hides + them unless `--include-hidden` or `--service agent` is given. +- `iics agent rename --id|--fid|--name|--hostname --new-name ` renames an + agent, for example from the host name the installer registers to a logical + name such as `CAI-A03`. The name is trimmed, at most 100 characters, and must + be unique in the org (case-insensitive). It is read back with the agent API. - Both commands are marked experimental. **Informatica docs:** @@ -71,6 +76,7 @@ internal/client/agentform_test.go # parser, apply, filter, GET/POST, re-login internal/client/agentconfigfile.go # change files (JSON/YAML map or list) and export internal/client/agentconfigfile_test.go cmd/agent_configs.go # agent configs get / set +cmd/agent_rename.go # agent rename ``` ### Files to MODIFY @@ -177,3 +183,14 @@ Form layout (``, about 1,000 fields, all submitted on save) into overrides. - `--debug` now masks the `Cookie` and `Set-Cookie` headers, which carry the session. +- The agent's own (`sysConfig*`) rows sit in a form section (`#agentSysConfig`) + that `pnAgent.js` shows only when the Service list has an `agentCoreSelOpt` + option. No checked page has one (CAI-A03 and CDI-G01-A01 in tst, CAI-A01 in + dev), and the UI screenshot shows only the agent's services. So these rows + are hidden, not editable, which matches the server ignoring them. +- The agent name is the form's `name` text field (`maxlength="100"`). Live test + on CAI-A03: renaming to an existing name (`cai-a01`) stopped before saving; + renaming to `CAI A03 Rename Test` (spaces) worked, and `agent get --name` + and `agent list` found it; renaming back via `--hostname cletinfacld07` + worked; renaming to the current name did nothing. The 14 overrides were + unchanged and no service restarted (service update times unchanged). diff --git a/docs/documentation/agent.md b/docs/documentation/agent.md index 9be794a..94b881c 100644 --- a/docs/documentation/agent.md +++ b/docs/documentation/agent.md @@ -22,6 +22,7 @@ iics agent [flags] | `installer-download` | Download the Secure Agent installer (optional checksum verify) | | `configs get` | Show an agent's service properties (experimental) | | `configs set` | Change or reset an agent's service properties (experimental) | +| `rename` | Rename an agent, for example from host name to `CAI-A03` | --- @@ -241,24 +242,29 @@ Matching is case-insensitive. Use `type/subType` (for example subtypes. `agent configs get` shows the names to use. `agent` stands for the agent's own Agent Core, Agent Manager and Tomcat -properties. They are shown by `get` but cannot be changed by `set`: the server -accepts the form but ignores changes to them. +properties (a legacy section of the form). The Administrator UI never shows +them: its Service list holds only the agent's services. They are not editable; +the server ignores changes to them, so `set` refuses them. `get` hides them +unless `--include-hidden` or `--service agent` is given. On CDI agents the +editable Tomcat and JVM settings are under the Data Integration Server +service, for example `Data_Integration_Server:TOMCAT_JRE:INFA_MEMORY`. ### agent configs get Show an agent's properties with their defaults and whether they are overridden. -| Flag | Type | Description | -| -------------- | ------ | ------------------------------------------------------------------------------------ | -| `--id` | string | Agent ID | -| `--fid` | string | Agent federated ID | -| `--name` | string | Agent name | -| `--hostname` | string | Agent host name | -| `--service` | string | Only this service: name, display name, or `agent` | -| `--type` | string | Only this property type, optionally `type/subType` | -| `--search` | string | Regular expression on the property name (case-insensitive) | -| `--overridden` | bool | Only properties whose value is overridden (differs from the default) | -| `--export` | bool | Write the properties as a `--from-file` change file: YAML, or JSON with `-o json` | +| Flag | Type | Description | +| ------------------ | ------ | --------------------------------------------------------------------------------- | +| `--id` | string | Agent ID | +| `--fid` | string | Agent federated ID | +| `--name` | string | Agent name | +| `--hostname` | string | Agent host name | +| `--service` | string | Only this service: name, display name, or `agent` | +| `--type` | string | Only this property type, optionally `type/subType` | +| `--search` | string | Regular expression on the property name (case-insensitive) | +| `--overridden` | bool | Only properties whose value is overridden (differs from the default) | +| `--export` | bool | Write the properties as a `--from-file` change file: YAML, or JSON with `-o json` | +| `--include-hidden` | bool | Also show the agent's own (`agent`) properties, which the UI hides | Exactly one of `--id` / `--fid` / `--name` / `--hostname` is required. @@ -277,7 +283,6 @@ list the properties with `service`, `serviceName`, `type`, `subType`, `name`, iics agent configs get --name CAI-A03 --overridden iics agent configs get --name CAI-A03 --service "Process Server" --type cluster iics agent configs get --hostname myhost --service process-engine --search '^db|url' -o json -iics agent configs get --name CAI-A03 --service agent --type TOMCAT_JRE iics agent configs get --name CDI-G01-A01 --service "Data Integration Server" --type TOMCAT_JRE # Save the Process Server overrides of one agent as a change file @@ -422,6 +427,53 @@ iics agent configs set --name CAI-A03 --set "process-engine:jvm:max-heap='4G'" - --- +## agent rename + +Rename a Secure Agent. The installer registers a new agent under its host +name; use `rename` to give it the logical name used across all orgs, for +example `CAI-A03`. + +> **Experimental.** The name is saved through the same Administrator form as +> [agent configs](#agent-configs), together with the agent's unchanged service +> properties. Renaming does not restart any service. + +| Flag | Type | Description | +| ------------- | ------ | ----------------------------------------- | +| `--id` | string | Agent ID | +| `--fid` | string | Agent federated ID | +| `--name` | string | Current agent name | +| `--hostname` | string | Agent host name | +| `--new-name` | string | New agent name (required) | +| `--yes`, `-y` | bool | Rename without asking for confirmation | + +Exactly one of `--id` / `--fid` / `--name` / `--hostname` is required. +`--hostname` is handy right after installation, when the name is still the +host name. + +Rules: + +- The new name is trimmed, must not be empty and can have at most 100 + characters (the form's limit). Spaces are allowed; quote the name. +- The name must be unique in the organization (compared case-insensitively); + otherwise the command stops before changing anything. +- The new name is read back with the agent API; the command fails if it was + not stored. +- Renaming to the current name does nothing. + +```bash +# Right after installation: name the agent registered as its host name +iics agent rename --hostname cletinfacld07 --new-name CAI-A03 + +iics agent rename --name "cletinfacld07.corp.example.com" --new-name CAI-A03 --yes +iics agent rename --id --new-name CDI-G01-A02 -p prd +``` + +```powershell +iics agent rename --hostname cletinfacld07 --new-name CAI-A03 +``` + +--- + ## agent delete Delete a Secure Agent. diff --git a/internal/client/agentform.go b/internal/client/agentform.go index e2be52c..87a8d92 100644 --- a/internal/client/agentform.go +++ b/internal/client/agentform.go @@ -344,6 +344,17 @@ func (f *AgentConfigForm) Find(sel PropertySelector) (int, error) { return -1, fmt.Errorf("property %s is ambiguous, use one of: %s", sel, strings.Join(keys, ", ")) } +// SetName changes the agent name submitted with the form. +func (f *AgentConfigForm) SetName(name string) { + f.AgentName = name + for i := range f.fields { + if f.fields[i].name == "name" { + f.fields[i].value = name + return + } + } +} + // Set changes property i to value and marks it overridden, as the UI's edit // action does. func (f *AgentConfigForm) Set(i int, value string) { @@ -503,11 +514,14 @@ func (c *Client) SaveAgentConfigForm(ctx context.Context, form *AgentConfigForm) } // AgentConfigFilter selects properties for display. Empty fields match all. +// The agent's own (SystemService) properties are only included with +// IncludeHidden or when Service selects them. type AgentConfigFilter struct { - Service string // service name, versioned key or display name - Type string // property type, optionally type/subType - Name string // regular expression on the property name, case-insensitive - Overridden bool // only overridden properties + IncludeHidden bool + Service string // service name, versioned key or display name + Type string // property type, optionally type/subType + Name string // regular expression on the property name, case-insensitive + Overridden bool // only overridden properties } // Filter returns the properties matching flt. @@ -523,6 +537,9 @@ func (f *AgentConfigForm) Filter(flt AgentConfigFilter) ([]AgentConfigProperty, typ, sub, _ := strings.Cut(flt.Type, "/") var out []AgentConfigProperty for _, p := range f.Properties { + if p.Service == SystemService && !flt.IncludeHidden && !strings.EqualFold(flt.Service, SystemService) { + continue + } if flt.Service != "" && !strings.EqualFold(flt.Service, p.Service) && !strings.EqualFold(flt.Service, p.serviceKey) && !strings.EqualFold(flt.Service, p.ServiceName) { continue @@ -573,7 +590,7 @@ func (f *AgentConfigForm) Apply(changes []AgentConfigChange) ([]AgentConfigDiff, return nil, err } if f.Properties[i].Service == SystemService { - return nil, fmt.Errorf("property %s: changing the agent's own (%s) properties is not supported; the server ignores them on save", f.Properties[i].Key(), SystemService) + return nil, fmt.Errorf("property %s is not editable: the Administrator UI hides the agent's own (%s) properties and the server ignores changes to them", f.Properties[i].Key(), SystemService) } if seen[i] { return nil, fmt.Errorf("property %s is changed more than once", f.Properties[i].Key()) @@ -627,3 +644,44 @@ func (c *Client) VerifyAgentConfigDiffs(ctx context.Context, agentID string, dif } return nil } + +// maxAgentNameLength is the agent name field's maxlength on the form. +const maxAgentNameLength = 100 + +// RenameAgent changes the name of agent agentID through the agent form and +// checks the new name with the agent API. The name must be unique in the +// organization. +func (c *Client) RenameAgent(ctx context.Context, agentID, newName string) (*Agent, error) { + newName = strings.TrimSpace(newName) + if newName == "" { + return nil, fmt.Errorf("the new agent name is empty") + } + if len([]rune(newName)) > maxAgentNameLength { + return nil, fmt.Errorf("the new agent name is longer than %d characters", maxAgentNameLength) + } + agents, err := c.ListAgents(ctx, AgentListOptions{}) + if err != nil { + return nil, err + } + for _, a := range agents { + if a.ID != agentID && strings.EqualFold(a.Name, newName) { + return nil, fmt.Errorf("agent %s (%s) already has the name %q", a.ID, a.AgentHost, a.Name) + } + } + form, err := c.GetAgentConfigForm(ctx, agentID) + if err != nil { + return nil, err + } + form.SetName(newName) + if err = c.SaveAgentConfigForm(ctx, form); err != nil { + return nil, err + } + agent, err := c.GetAgent(ctx, agentID) + if err != nil { + return nil, fmt.Errorf("verifying the new name: %w", err) + } + if agent.Name != newName { + return nil, fmt.Errorf("the agent did not store the new name: it is still %q", agent.Name) + } + return agent, nil +} diff --git a/internal/client/agentform_test.go b/internal/client/agentform_test.go index 370dabf..13b0ee0 100644 --- a/internal/client/agentform_test.go +++ b/internal/client/agentform_test.go @@ -192,10 +192,18 @@ func TestAgentConfigFormFilter(t *testing.T) { if err != nil || len(got) != 1 || got[0].Name != "primary-node" { t.Errorf("Filter by service and name = %+v, %v", got, err) } - got, _ = form.Filter(AgentConfigFilter{Type: "AGENTCORE_LOG4J/DEBUG"}) + got, _ = form.Filter(AgentConfigFilter{}) + if len(got) != 3 { + t.Errorf("default Filter should hide the agent's own properties, got %d", len(got)) + } + got, _ = form.Filter(AgentConfigFilter{Type: "AGENTCORE_LOG4J/DEBUG", IncludeHidden: true}) if len(got) != 1 || got[0].SubType != "DEBUG" { t.Errorf("Filter by type/subType = %+v", got) } + got, _ = form.Filter(AgentConfigFilter{Service: "agent"}) + if len(got) != 2 { + t.Errorf("Filter with --service agent = %d properties, want 2", len(got)) + } got, _ = form.Filter(AgentConfigFilter{Overridden: true}) if len(got) != 1 || got[0].Name != "secretKey" { t.Errorf("Filter overridden = %+v", got) @@ -304,3 +312,43 @@ func TestParsePropertySelector(t *testing.T) { } } } + +func TestRenameAgent(t *testing.T) { + var postedName string + name := "cletinfacld07" + mux := http.NewServeMux() + mux.HandleFunc("/api/v2/agent", func(w http.ResponseWriter, r *http.Request) { + _, _ = io.WriteString(w, `[{"id":"01D23X0800000000008G","name":"`+name+`"},{"id":"other","name":"CAI-A01","agentHost":"h1"}]`) + }) + mux.HandleFunc("/api/v2/agent/01D23X0800000000008G", func(w http.ResponseWriter, r *http.Request) { + _, _ = io.WriteString(w, `{"id":"01D23X0800000000008G","name":"`+name+`"}`) + }) + mux.HandleFunc(agentFormPath, func(w http.ResponseWriter, r *http.Request) { + ck, _ := r.Cookie("XSRF_TOKEN") + if r.Method == http.MethodPost { + data, _ := io.ReadAll(r.Body) + v, _ := url.ParseQuery(string(data)) + postedName = v.Get("name") + name = postedName + } + _, _ = io.WriteString(w, strings.Replace(agentPage(ck.Value, "false", ""), `value="CAI-A03"`, `value="`+name+`"`, 1)) + }) + c := newTestClient(mux) + ctx := t.Context() + + agent, err := c.RenameAgent(ctx, "01D23X0800000000008G", " CAI-A03 ") + if err != nil { + t.Fatal(err) + } + if postedName != "CAI-A03" || agent.Name != "CAI-A03" { + t.Errorf("posted %q, agent name %q", postedName, agent.Name) + } + if _, err := c.RenameAgent(ctx, "01D23X0800000000008G", "cai-a01"); err == nil || !strings.Contains(err.Error(), "already has the name") { + t.Errorf("expected a duplicate name error, got %v", err) + } + for _, bad := range []string{" ", strings.Repeat("x", 101)} { + if _, err := c.RenameAgent(ctx, "01D23X0800000000008G", bad); err == nil { + t.Errorf("RenameAgent(%q): expected an error", bad) + } + } +} From 2f2867e40de5980b4517bcc0d60ce96e6e1b2e79 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Mon, 19 Oct 2026 23:48:36 -0400 Subject: [PATCH 39/52] feat: agent configs get table grouped by service like agent details --full (CR-0049) --- cmd/agent_configs.go | 38 +++++++++++++++++++++++++++++++++---- docs/documentation/agent.md | 24 +++++++++++++++++++---- 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/cmd/agent_configs.go b/cmd/agent_configs.go index 12cd63e..6010e3b 100644 --- a/cmd/agent_configs.go +++ b/cmd/agent_configs.go @@ -127,11 +127,14 @@ func newAgentConfigsGetCmd() *cobra.Command { if export { return writeAgentConfigExport(cmd, props) } - f, err := getFormatter() - if err != nil { - return err + if outputFmt != "" && outputFmt != "table" { + f, err := getFormatter() + if err != nil { + return err + } + return f.Format(props, agentPropertyCols) } - return f.Format(props, agentPropertyCols) + return printAgentConfigTables(cmd, form, props) }, } addAgentSelectorFlags(cmd, &id, &fid, &name, &hostname) @@ -144,6 +147,33 @@ func newAgentConfigsGetCmd() *cobra.Command { return cmd } +// printAgentConfigTables prints props as one table per service, like +// `agent details --full`: a "Service: ()" header, +// then TYPE, NAME, VALUE, DEFAULT and OVERRIDDEN columns. +func printAgentConfigTables(cmd *cobra.Command, form *client.AgentConfigForm, props []client.AgentConfigProperty) error { + w := cmd.OutOrStdout() + _, _ = fmt.Fprintf(w, "Agent: %s (%s)\n", form.AgentName, form.AgentID) + if len(props) == 0 { + _, _ = fmt.Fprintln(w, "\nNo properties match.") + return nil + } + cfg, _ := loadConfig() + tf := output.New(output.FormatTable, w, resolveTableStyle(cfg)) + for start := 0; start < len(props); { + end := start + 1 + for end < len(props) && props[end].Service == props[start].Service { + end++ + } + p := props[start] + _, _ = fmt.Fprintf(w, "\nService: %s (%s)\n\n", p.ServiceName, p.Service) + if err := tf.Format(props[start:end], agentPropertyCols[1:]); err != nil { + return err + } + start = end + } + return nil +} + // writeAgentConfigExport writes props in the --from-file change format. func writeAgentConfigExport(cmd *cobra.Command, props []client.AgentConfigProperty) error { w := cmd.OutOrStdout() diff --git a/docs/documentation/agent.md b/docs/documentation/agent.md index 94b881c..07ec8f4 100644 --- a/docs/documentation/agent.md +++ b/docs/documentation/agent.md @@ -274,10 +274,26 @@ quotes are written as `"'4G'"` and multi-line values as YAML block scalars, so the file reads back unchanged. `agent` properties are left out because they cannot be set. -Table columns: `SERVICE`, `TYPE` (with `/subType` for `agent` properties), -`NAME`, `VALUE`, `DEFAULT`, `OVERRIDDEN`. `--output json` / `yaml` / `csv` -list the properties with `service`, `serviceName`, `type`, `subType`, `name`, -`value`, `defaultValue`, `overridden` and `sensitive`. +Table mode prints the layout of `agent details --full`: an `Agent: +()` line, then for each service a `Service: ()` +header and a table with columns `TYPE` (with `/subType` for `agent` +properties), `NAME`, `VALUE`, `DEFAULT`, `OVERRIDDEN`. Long values are wrapped. + +`--output csv` prints one flat table with an extra `SERVICE` column. +`--output json` / `yaml` list the properties with `service`, `serviceName`, +`type`, `subType`, `name`, `value`, `defaultValue`, `overridden` and +`sensitive`. + +```text +Agent: CAI-A03 (01D23X0800000000008G) + +Service: Process Server (process-engine) + + TYPE NAME VALUE DEFAULT OVERRIDDEN + cluster enabled true false yes + cluster primary-node false true yes + jvm max-heap '10G' '1536M' yes +``` ```bash iics agent configs get --name CAI-A03 --overridden From f940863afff1c715c6c45fa6f54a6c49632dbb9b Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Tue, 20 Oct 2026 00:01:13 -0400 Subject: [PATCH 40/52] feat: interactive agent property editor, agent configs edit (CR-0049) - iics agent configs edit: services -> properties -> property editor -> review and apply, in the style of role edit; agent picker without flags - one-line values in an input; YAML single quotes added back automatically; sensitive values masked - multi-value properties (LDAP, DB connection properties, logging) in an entry editor: edit/add/remove entries, edit as text or in $EDITOR, reset, undo; unchanged entries keep their exact text - review shows entry-level changes for list properties - menu: Environments and agents > Edit agent properties - NAME/PROPERTY columns no longer wrap (renderer moved them last) --- cmd/agent_configs.go | 145 +++++- completions/iics.bash | 58 +++ .../CR-0049-agent-level-service-properties.md | 26 +- docs/documentation/agent.md | 76 ++- docs/documentation/menu.md | 22 +- internal/menu/entries.go | 1 + internal/tui/agentconfigeditor.go | 466 ++++++++++++++++++ internal/tui/agentconfigeditor_test.go | 151 ++++++ internal/tui/agentconfigvalue.go | 250 ++++++++++ 9 files changed, 1151 insertions(+), 44 deletions(-) create mode 100644 internal/tui/agentconfigeditor.go create mode 100644 internal/tui/agentconfigeditor_test.go create mode 100644 internal/tui/agentconfigvalue.go diff --git a/cmd/agent_configs.go b/cmd/agent_configs.go index 6010e3b..aa40753 100644 --- a/cmd/agent_configs.go +++ b/cmd/agent_configs.go @@ -3,12 +3,14 @@ package cmd import ( "context" "encoding/json" + "errors" "fmt" "os" "strings" "github.com/jbrazda/iics-cli/internal/client" "github.com/jbrazda/iics-cli/internal/output" + "github.com/jbrazda/iics-cli/internal/tui" "github.com/spf13/cobra" "gopkg.in/yaml.v3" ) @@ -29,6 +31,7 @@ configs").`, } cmd.AddCommand(newAgentConfigsGetCmd()) cmd.AddCommand(newAgentConfigsSetCmd()) + cmd.AddCommand(newAgentConfigsEditCmd()) return cmd } @@ -43,7 +46,7 @@ var agentPropertyCols = []output.Column{ } return t }}, - {Header: "NAME", Field: "name", Width: 24, Priority: 1, Shrink: output.ShrinkWrap}, + {Header: "NAME", Field: "name", Width: 24, Priority: 1, Shrink: output.ShrinkNever}, {Header: "VALUE", Field: "value", Width: 30, Priority: 1, Func: wrapConfigField("value")}, {Header: "DEFAULT", Field: "defaultValue", Width: 30, Priority: 3, Func: wrapConfigField("defaultValue")}, {Header: "OVERRIDDEN", Field: "overridden", Width: 10, Priority: 2, Shrink: output.ShrinkNever, Func: func(v interface{}) string { @@ -58,7 +61,7 @@ var agentPropertyCols = []output.Column{ // agentConfigDiffCols is the table layout for the changes `agent configs set` // makes. var agentConfigDiffCols = []output.Column{ - {Header: "PROPERTY", Field: "property", Width: 30, Priority: 1, Shrink: output.ShrinkWrap}, + {Header: "PROPERTY", Field: "property", Width: 30, Priority: 1, Shrink: output.ShrinkNever}, {Header: "CURRENT", Field: "oldValue", Width: 30, Priority: 1, Func: diffValueFunc("oldValue", "oldOverridden")}, {Header: "NEW", Field: "newValue", Width: 30, Priority: 1, Func: diffValueFunc("newValue", "newOverridden")}, } @@ -223,6 +226,37 @@ func parseAgentConfigChanges(sets, resets []string, fromFile string) ([]client.A return changes, nil } +// checkGroupConfigs returns an error when the agent's group has group-level +// service properties, which replace the agent's own settings. +func checkGroupConfigs(ctx context.Context, c *client.Client, agent *client.Agent) error { + if agent.GroupID == "" { + return nil + } + groupCfg, err := c.GetAgentGroupConfigs(ctx, agent.GroupID) + if err != nil { + return fmt.Errorf("checking group-level properties: %w", err) + } + if len(groupCfg) > 0 { + return fmt.Errorf("agent %s is in group %s, which has group-level properties for %v; "+ + "remove them first (iics environment configs) or use --force", agent.Name, agent.GroupID, sortedKeys(groupCfg)) + } + return nil +} + +// saveAgentConfigDiffs saves the form, verifies the changes and prints the +// restart hint. +func saveAgentConfigDiffs(ctx context.Context, cmd *cobra.Command, c *client.Client, agent *client.Agent, form *client.AgentConfigForm, diffs []client.AgentConfigDiff) error { + if err := c.SaveAgentConfigForm(ctx, form); err != nil { + return err + } + if err := c.VerifyAgentConfigDiffs(ctx, agent.ID, diffs); err != nil { + return err + } + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Updated %d propert%s on agent %s.\n", len(diffs), map[bool]string{true: "y", false: "ies"}[len(diffs) == 1], agent.Name) + printRestartHint(cmd, agent, diffs) + return nil +} + // printRestartHint lists the services whose properties changed. Some, such as // Process Server, only mark themselves as needing a restart and keep running // with the old values until restarted. @@ -313,14 +347,9 @@ from the group; the command refuses to change them unless --force is given. if err != nil { return err } - if agent.GroupID != "" && !force { - groupCfg, gErr := c.GetAgentGroupConfigs(ctx, agent.GroupID) - if gErr != nil { - return fmt.Errorf("checking group-level properties: %w", gErr) - } - if len(groupCfg) > 0 { - return fmt.Errorf("agent %s is in group %s, which has group-level properties for %v; "+ - "remove them first (iics environment configs) or use --force", agent.Name, agent.GroupID, sortedKeys(groupCfg)) + if !force { + if err = checkGroupConfigs(ctx, c, agent); err != nil { + return err } } form, err := c.GetAgentConfigForm(ctx, agent.ID) @@ -352,15 +381,7 @@ from the group; the command refuses to change them unless --force is given. if !yes && !confirmAction(cmd, fmt.Sprintf("Apply %d change(s) to agent %s?", len(diffs), agent.Name)) { return nil } - if err := c.SaveAgentConfigForm(ctx, form); err != nil { - return err - } - if err := c.VerifyAgentConfigDiffs(ctx, agent.ID, diffs); err != nil { - return err - } - _, _ = fmt.Fprintf(w, "Updated %d propert%s on agent %s.\n", len(diffs), map[bool]string{true: "y", false: "ies"}[len(diffs) == 1], agent.Name) - printRestartHint(cmd, agent, diffs) - return nil + return saveAgentConfigDiffs(ctx, cmd, c, agent, form, diffs) }, } addAgentSelectorFlags(cmd, &id, &fid, &name, &hostname) @@ -372,3 +393,89 @@ from the group; the command refuses to change them unless --force is given. cmd.Flags().BoolVar(&force, "force", false, "change the agent even if its group has group-level properties") return cmd } + +func newAgentConfigsEditCmd() *cobra.Command { + var ( + id, fid, name, hostname string + force bool + ) + cmd := &cobra.Command{ + Use: "edit", + Short: "Edit an agent's service properties interactively", + Long: `Edit an agent's service properties in an interactive editor. + +Pick a service, then a property. One-line values open in an input field; +Process Server values in single quotes ('10G') are edited without the quotes, +which are added back. Multi-value properties such as LDAP properties, database +connection properties or additional logging open in an entry editor: edit, +add or remove entries, edit the whole value as text, or open it in $EDITOR +(ctrl+e). Any property can be reset to its default. + +Review shows all pending changes before they are applied. Esc goes back one +screen; on the service list it cancels. Without an agent flag the agent is +picked from a list. + +` + agentConfigsExperimental, + Example: ` iics agent configs edit + iics agent configs edit --name CAI-A03 + iics agent configs edit --hostname cletinfacld07 -p tst`, + RunE: func(cmd *cobra.Command, args []string) error { + if !isInteractiveTTY() { + return fmt.Errorf("agent configs edit requires a terminal; use agent configs set") + } + c, err := getClient(cmd) + if err != nil { + return err + } + ctx := context.Background() + w := cmd.OutOrStdout() + var agent *client.Agent + if id == "" && fid == "" && name == "" && hostname == "" { + agent, err = pickAgent(ctx, c) + } else { + agent, err = resolveAgent(ctx, c, id, name, hostname, fid) + } + if err != nil || agent == nil { + return err + } + if !force { + if err = checkGroupConfigs(ctx, c, agent); err != nil { + return err + } + } + form, err := c.GetAgentConfigForm(ctx, agent.ID) + if err != nil { + return err + } + props, err := form.Filter(client.AgentConfigFilter{}) + if err != nil { + return err + } + changes, err := tui.EditAgentConfigs(props, tui.AgentConfigEditorOptions{Title: "Agent: " + agent.Name}) + if errors.Is(err, tui.ErrCanceled) { + _, _ = fmt.Fprintln(w, "Canceled.") + return nil + } + if err != nil { + return err + } + diffs, err := form.Apply(changes) + if err != nil { + return err + } + if len(diffs) == 0 { + _, _ = fmt.Fprintln(w, "No changes.") + return nil + } + _, _ = fmt.Fprintf(w, "Changes for agent %s (%s):\n\n", agent.Name, agent.ID) + cfg, _ := loadConfig() + if err := output.New(output.FormatTable, w, resolveTableStyle(cfg)).Format(diffs, agentConfigDiffCols); err != nil { + return err + } + return saveAgentConfigDiffs(ctx, cmd, c, agent, form, diffs) + }, + } + addAgentSelectorFlags(cmd, &id, &fid, &name, &hostname) + cmd.Flags().BoolVar(&force, "force", false, "edit the agent even if its group has group-level properties") + return cmd +} diff --git a/completions/iics.bash b/completions/iics.bash index 840cc3c..f5c9577 100644 --- a/completions/iics.bash +++ b/completions/iics.bash @@ -503,6 +503,63 @@ _iics_activitylog() noun_aliases=() } +_iics_agent_configs_edit() +{ + last_command="iics_agent_configs_edit" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--fid=") + two_word_flags+=("--fid") + local_nonpersistent_flags+=("--fid") + local_nonpersistent_flags+=("--fid=") + flags+=("--force") + local_nonpersistent_flags+=("--force") + flags+=("--hostname=") + two_word_flags+=("--hostname") + local_nonpersistent_flags+=("--hostname") + local_nonpersistent_flags+=("--hostname=") + flags+=("--id=") + two_word_flags+=("--id") + local_nonpersistent_flags+=("--id") + local_nonpersistent_flags+=("--id=") + flags+=("--name=") + two_word_flags+=("--name") + local_nonpersistent_flags+=("--name") + local_nonpersistent_flags+=("--name=") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--http-timeout=") + two_word_flags+=("--http-timeout") + flags+=("--no-color") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + flags+=("--profile=") + two_word_flags+=("--profile") + two_word_flags+=("-p") + flags+=("--theme=") + two_word_flags+=("--theme") + flags+=("--verbose") + flags+=("-v") + flags+=("--wide") + flags+=("--width=") + two_word_flags+=("--width") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _iics_agent_configs_get() { last_command="iics_agent_configs_get" @@ -658,6 +715,7 @@ _iics_agent_configs() command_aliases=() commands=() + commands+=("edit") commands+=("get") commands+=("set") diff --git a/docs/ChangeRequests/pending/CR-0049-agent-level-service-properties.md b/docs/ChangeRequests/pending/CR-0049-agent-level-service-properties.md index c2eeea2..1bde169 100644 --- a/docs/ChangeRequests/pending/CR-0049-agent-level-service-properties.md +++ b/docs/ChangeRequests/pending/CR-0049-agent-level-service-properties.md @@ -5,7 +5,7 @@ ## CR Type - [ ] **New resource** -- [x] **New subcommand** - `iics agent configs get`, `iics agent configs set`, `iics agent rename` +- [x] **New subcommand** - `iics agent configs get|set|edit`, `iics agent rename` - [ ] **Enhancement** - [ ] **Output change** - [ ] **Flag / config change** @@ -53,6 +53,14 @@ changed from the CLI. - `set` refuses the agent's own properties (`agent:` Agent Core, Agent Manager, Tomcat), because the server ignores changes to them (see notes). `get` hides them unless `--include-hidden` or `--service agent` is given. +- `iics agent configs edit` opens an interactive editor in the style of + `role edit`: services, then properties, then a property editor, then review + and apply. One-line values are edited in an input (YAML single quotes are + added back automatically, sensitive values masked). Multi-value properties + (lists of maps such as LDAP, database connection properties, additional + logging) open in an entry editor with edit, add, remove, edit-as-text + (`$EDITOR` via ctrl+e), reset and undo. The review lists entry-level + changes. Menu entry "Edit agent properties". - `iics agent rename --id|--fid|--name|--hostname --new-name ` renames an agent, for example from the host name the installer registers to a logical name such as `CAI-A03`. The name is trimmed, at most 100 characters, and must @@ -77,6 +85,10 @@ internal/client/agentconfigfile.go # change files (JSON/YAML map or list) and e internal/client/agentconfigfile_test.go cmd/agent_configs.go # agent configs get / set cmd/agent_rename.go # agent rename +internal/tui/agentconfigeditor.go # interactive property editor +internal/tui/agentconfigvalue.go # quoting, list-of-maps parsing/writing, entry diff +internal/tui/agentconfigeditor_test.go +internal/menu/entries.go # "Edit agent properties" menu entry ``` ### Files to MODIFY @@ -194,3 +206,15 @@ Form layout (``, about 1,000 fields, all submitted on save) and `agent list` found it; renaming back via `--hostname cletinfacld07` worked; renaming to the current name did nothing. The 14 overrides were unchanged and no service restarted (service update times unchanged). +- The entry editor keeps the value as a YAML node tree and changes only the + edited scalars, so unchanged entries keep their text. All 8 list values on + CAI-A03 (current and default of LDAP, DB and log DB connection properties, + additional logging) round-trip byte for byte. Edited values are written + plain when YAML allows (`300000`, `true`) and quoted only when needed. +- The editor was driven in a pseudo-terminal on tst CAI-A03: editing entry 2 + and adding an entry to `db:connection-properties` showed the entry diff in + review, then Cancel saved nothing; changing + `OpsInsightsDataCollector:OpsInsights:scriptLogRetentionCount` by typing + `101` stored `'101'` (quotes added), verified, and was reset afterwards. +- Table columns `NAME` (get) and `PROPERTY` (set diff) no longer use + `ShrinkWrap`, which the table renderer moves to the last position. diff --git a/docs/documentation/agent.md b/docs/documentation/agent.md index 07ec8f4..43d53e0 100644 --- a/docs/documentation/agent.md +++ b/docs/documentation/agent.md @@ -10,19 +10,20 @@ iics agent [flags] ## Subcommands -| Subcommand | Description | -| -------------------- | -------------------------------------------------------------- | -| `list` | List Secure Agents | -| `get` | Get a single agent | -| `details` | Get agent service engine details | -| `delete` | Delete a Secure Agent | -| `start` | Start an agent service | -| `stop` | Stop an agent service | -| `installer-info` | Get Secure Agent installer download information | -| `installer-download` | Download the Secure Agent installer (optional checksum verify) | -| `configs get` | Show an agent's service properties (experimental) | -| `configs set` | Change or reset an agent's service properties (experimental) | -| `rename` | Rename an agent, for example from host name to `CAI-A03` | +| Subcommand | Description | +| -------------------- | --------------------------------------------------------------- | +| `list` | List Secure Agents | +| `get` | Get a single agent | +| `details` | Get agent service engine details | +| `delete` | Delete a Secure Agent | +| `start` | Start an agent service | +| `stop` | Stop an agent service | +| `installer-info` | Get Secure Agent installer download information | +| `installer-download` | Download the Secure Agent installer (optional checksum verify) | +| `configs get` | Show an agent's service properties (experimental) | +| `configs set` | Change or reset an agent's service properties (experimental) | +| `configs edit` | Edit an agent's service properties interactively (experimental) | +| `rename` | Rename an agent, for example from host name to `CAI-A03` | --- @@ -441,6 +442,55 @@ iics agent configs set --name CAI-A03 --set "process-engine:cluster:primary-node iics agent configs set --name CAI-A03 --set "process-engine:jvm:max-heap='4G'" --yes ``` +### agent configs edit + +Edit an agent's service properties in an interactive editor, in the style of +`iics role edit`. Without an agent flag, the agent is picked from a list. The +menu entry **Edit agent properties** runs this command. + +| Flag | Type | Description | +| ------------ | ------ | --------------------------------------------------------------------------- | +| `--id` | string | Agent ID | +| `--fid` | string | Agent federated ID | +| `--name` | string | Agent name | +| `--hostname` | string | Agent host name | +| `--force` | bool | Edit the agent even if its Secure Agent group has group-level properties | + +Screens: + +1. **Services** - each service with its number of properties, overridden + properties and pending changes, then **Review and apply** and **Cancel**. +2. **Properties** of a service - type, name and a value preview. `*` marks an + overridden property, `✎` a pending change. `/` filters. +3. **Property editor**: + - One-line values open in an input field. Values stored in YAML single + quotes (`'10G'`) are edited without the quotes, which are added back. + Sensitive values are masked. + - Multi-value properties - lists of small maps such as LDAP properties + (`ldap-properties`), database connection properties + (`connection-properties`, `logConnection-properties`) or + `additional-logging` - open as a list of entries, for example + `key=testOnBorrow value=true`. Pick an entry to edit or remove it, or + use **+ Add entry** (new entries get the same fields, such as `key` and + `value`). Unchanged entries keep their exact text. + - **Edit as text** edits the whole value as YAML; `ctrl+e` opens it in + `$EDITOR`. + - **Reset to default** and **Undo changes to this property**. +4. **Review** - all pending changes; list properties show entry changes + (`~` changed, `+` added, `-` removed). **Apply** saves. + +After applying, the command prints the change table, saves and verifies like +`agent configs set`, and lists the restart commands for the changed services. + +Keys: `↑`/`↓` move, `enter` selects, `/` filters (enter while filtering selects +the highlighted item), `esc` goes back one screen and cancels on the service +list. In entry forms `enter` moves to the next field and saves on the last one. + +```bash +iics agent configs edit +iics agent configs edit --name CAI-A03 -p tst +``` + --- ## agent rename diff --git a/docs/documentation/menu.md b/docs/documentation/menu.md index f0dcacc..d7a4c4e 100644 --- a/docs/documentation/menu.md +++ b/docs/documentation/menu.md @@ -118,17 +118,17 @@ On a production profile: ## Entries -| Group | Entries | -| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Session | Switch profile, Set as default profile ✎, Login, List profiles, Add profile ✎, Edit active profile ✎, Delete profile ✎, Edit global settings | -| Users | List users, Show user (search by user name, ID, or part of a name/email), Create user ✎, Edit user groups and roles ✎, Delete user ✎, Change password ✎ | -| User groups | List, Show, Create ✎, Edit roles ✎, Delete ✎ | -| Roles | List, Show (with privileges), Create ✎, Edit privileges ✎ | -| Privileges | List privileges | -| Projects and folders | List projects, List folders, List folders in a project, Edit project ✎, Edit folder ✎ | -| Permissions | Edit object permissions ✎ | -| Environments and agents | List runtime environments, Create runtime environment ✎, List agents, Start / Stop / Restart agent service ✎, Show agent installer info, Download agent installer (`--interactive --progress`) | -| Help | Show CLI help | +| Group | Entries | +| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Session | Switch profile, Set as default profile ✎, Login, List profiles, Add profile ✎, Edit active profile ✎, Delete profile ✎, Edit global settings | +| Users | List users, Show user (search by user name, ID, or part of a name/email), Create user ✎, Edit user groups and roles ✎, Delete user ✎, Change password ✎ | +| User groups | List, Show, Create ✎, Edit roles ✎, Delete ✎ | +| Roles | List, Show (with privileges), Create ✎, Edit privileges ✎ | +| Privileges | List privileges | +| Projects and folders | List projects, List folders, List folders in a project, Edit project ✎, Edit folder ✎ | +| Permissions | Edit object permissions ✎ | +| Environments and agents | List runtime environments, Create runtime environment ✎, List agents, Start / Stop / Restart agent service ✎, Edit agent properties ✎, Show agent installer info, Download agent installer (`--interactive --progress`) | +| Help | Show CLI help | ## Accessible mode and small terminals diff --git a/internal/menu/entries.go b/internal/menu/entries.go index 2ca3dfe..3b5c7c2 100644 --- a/internal/menu/entries.go +++ b/internal/menu/entries.go @@ -110,6 +110,7 @@ func DefaultEntries() []Entry { {Group: "Environments and agents", Label: "Start agent service", Description: "Pick an agent and start a service", Args: []string{"agent", "start", "-i"}, Writes: true}, {Group: "Environments and agents", Label: "Stop agent service", Description: "Pick an agent and stop a service", Args: []string{"agent", "stop", "-i"}, Writes: true}, {Group: "Environments and agents", Label: "Restart agent service", Description: "Pick an agent and restart a service", Args: []string{"agent", "restart", "-i"}, Writes: true}, + {Group: "Environments and agents", Label: "Edit agent properties", Description: "Pick an agent and edit its service properties, including multi-value LDAP and DB connection properties", Args: []string{"agent", "configs", "edit"}, Writes: true}, {Group: "Environments and agents", Label: "Show agent installer info", Description: "Show the installer download URL, checksum URL and install token", Args: []string{"agent", "installer-info"}}, {Group: "Environments and agents", Label: "Download agent installer", Description: "Download the Secure Agent installer; choose the folder, reuse a matching file", Args: []string{"agent", "installer-download", "--interactive", "--progress"}}, diff --git a/internal/tui/agentconfigeditor.go b/internal/tui/agentconfigeditor.go new file mode 100644 index 0000000..ff08f6d --- /dev/null +++ b/internal/tui/agentconfigeditor.go @@ -0,0 +1,466 @@ +package tui + +import ( + "errors" + "fmt" + "strconv" + "strings" + + "github.com/charmbracelet/huh" + "github.com/jbrazda/iics-cli/internal/client" +) + +// AgentConfigEditorOptions configures EditAgentConfigs. +type AgentConfigEditorOptions struct { + // Title is shown above each screen, e.g. "Agent: CAI-A03". + Title string +} + +// agentConfigEditor holds the properties and the pending edits by key. +type agentConfigEditor struct { + props []client.AgentConfigProperty + pending map[string]client.AgentConfigChange + title string +} + +const ( + choiceBack = "\x00back" + choiceAdd = "\x00add" + choiceText = "\x00text" + choiceReset = "\x00reset" + choiceUndo = "\x00undo" + choiceEdit = "\x00edit" + choiceRemove = "\x00remove" + choiceApply = "\x00apply" + previewWidth = 44 + maxListHeight = 22 +) + +// EditAgentConfigs runs the interactive agent property editor and returns +// the changes to apply, in property order. Esc goes back one screen; on the +// service screen it cancels with ErrCanceled. +func EditAgentConfigs(props []client.AgentConfigProperty, opts AgentConfigEditorOptions) ([]client.AgentConfigChange, error) { + e := &agentConfigEditor{props: props, pending: map[string]client.AgentConfigChange{}, title: opts.Title} + lastService := "" + for { + choice := lastService + options := e.serviceOptions() + sel := huh.NewSelect[string](). + Title(e.title). + Description(fmt.Sprintf("Select a service (/ to filter). %d pending change(s).", len(e.pending))). + Options(options...). + Height(min(len(options)+2, maxListHeight)). + Value(&choice) + if err := runForm(huh.NewForm(huh.NewGroup(sel))); err != nil { + return nil, err + } + switch choice { + case choiceCancel: + return nil, ErrCanceled + case choiceReview: + ok, err := e.review() + if err != nil { + return nil, err + } + if ok { + return e.changes(), nil + } + continue + } + lastService = choice + if err := e.editService(choice); err != nil { + return nil, err + } + } +} + +// back runs a form; Esc returns false instead of an error, so inner screens +// go back one level. +func back(f *huh.Form) (bool, error) { + err := runForm(f) + if errors.Is(err, ErrCanceled) { + return false, nil + } + return err == nil, err +} + +// value returns the effective value of property i with pending edits. +func (e *agentConfigEditor) value(i int) (string, bool) { + p := e.props[i] + ch, ok := e.pending[p.Key()] + switch { + case !ok: + return p.Value, p.Overridden + case ch.Reset: + return p.DefaultValue, false + } + return ch.Value, true +} + +// record stores the pending edit for property i, or drops it when the +// property ends up as it is now. +func (e *agentConfigEditor) record(i int, ch client.AgentConfigChange) { + p := e.props[i] + ch.Property = p.Key() + if ch.Reset && !p.Overridden && p.Value == p.DefaultValue || !ch.Reset && ch.Value == p.Value { + delete(e.pending, p.Key()) + return + } + e.pending[p.Key()] = ch +} + +func (e *agentConfigEditor) changes() []client.AgentConfigChange { + var out []client.AgentConfigChange + for _, p := range e.props { + if ch, ok := e.pending[p.Key()]; ok { + out = append(out, ch) + } + } + return out +} + +func (e *agentConfigEditor) serviceOptions() []huh.Option[string] { + type stats struct { + name string + total, over, changed int + } + var order []string + byService := map[string]*stats{} + width := 0 + for i, p := range e.props { + s := byService[p.Service] + if s == nil { + s = &stats{name: p.ServiceName} + if s.name == "" { + s.name = p.Service + } + byService[p.Service] = s + order = append(order, p.Service) + width = max(width, len(s.name)) + } + s.total++ + if _, over := e.value(i); over { + s.over++ + } + if _, ok := e.pending[p.Key()]; ok { + s.changed++ + } + } + opts := make([]huh.Option[string], 0, len(order)+2) + for _, svc := range order { + s := byService[svc] + label := fmt.Sprintf("%-*s %4d properties %3d overridden", width, s.name, s.total, s.over) + if s.changed > 0 { + label += fmt.Sprintf(" %d changed", s.changed) + } + opts = append(opts, huh.NewOption(label, svc)) + } + return append(opts, + huh.NewOption("> Review and apply", choiceReview), + huh.NewOption("x Cancel", choiceCancel)) +} + +// editService lists the properties of one service until the user goes back. +func (e *agentConfigEditor) editService(service string) error { + last := "" + for { + var idx []int + typeW, nameW := 0, 0 + for i, p := range e.props { + if p.Service == service { + idx = append(idx, i) + typeW, nameW = max(typeW, len(p.Type)), max(nameW, len(p.Name)) + } + } + if len(idx) == 0 { + return nil + } + nameW = min(nameW, 32) + opts := []huh.Option[string]{huh.NewOption("< Back to services", choiceBack)} + for _, i := range idx { + p := e.props[i] + v, over := e.value(i) + mark := " " + if over { + mark = "*" + } + if _, ok := e.pending[p.Key()]; ok { + mark = "✎" + } + label := fmt.Sprintf("%s %-*s %-*s %s", mark, typeW, p.Type, nameW, truncate(p.Name, nameW), previewValue(v, p.Sensitive, previewWidth)) + opts = append(opts, huh.NewOption(label, strconv.Itoa(i))) + } + choice := last + name := e.props[idx[0]].ServiceName + sel := huh.NewSelect[string](). + Title(e.title + " - " + name). + Description("Select a property to edit (/ to filter). * overridden, ✎ changed."). + Options(opts...). + Height(min(len(opts)+2, maxListHeight)). + Value(&choice) + ok, err := back(huh.NewForm(huh.NewGroup(sel))) + if err != nil || !ok || choice == choiceBack { + return err + } + last = choice + i, _ := strconv.Atoi(choice) + if err := e.editProperty(i); err != nil { + return err + } + } +} + +// editProperty edits one property: lists of maps in the entry editor, other +// values in a text field. +func (e *agentConfigEditor) editProperty(i int) error { + p := e.props[i] + for { + v, over := e.value(i) + list, isList := parseEntryList(v) + if !isList && strings.TrimSpace(v) == "" { + // An empty list property: use the default's entries as the template. + if def, ok := parseEntryList(p.DefaultValue); ok { + list, isList = def, true + for list.Len() > 0 { + list.Remove(0) + } + } + } + state := "default" + if over { + state = "overridden" + } + if _, ok := e.pending[p.Key()]; ok { + state += ", changed" + } + desc := fmt.Sprintf("Type %s · %s\nDefault: %s", p.Type, state, previewValue(p.DefaultValue, p.Sensitive, 60)) + + var opts []huh.Option[string] + if isList { + for k := 0; k < list.Len(); k++ { + opts = append(opts, huh.NewOption(fmt.Sprintf("%2d. %s", k+1, truncate(list.Label(k), 70)), strconv.Itoa(k))) + } + opts = append(opts, + huh.NewOption("+ Add entry", choiceAdd), + huh.NewOption("Edit as text", choiceText)) + } else { + desc += "\nValue: " + previewValue(v, p.Sensitive, 60) + opts = append(opts, huh.NewOption("Edit value", choiceEdit)) + } + if over { + opts = append(opts, huh.NewOption("Reset to default", choiceReset)) + } + if _, ok := e.pending[p.Key()]; ok { + opts = append(opts, huh.NewOption("Undo changes to this property", choiceUndo)) + } + opts = append(opts, huh.NewOption("< Back", choiceBack)) + + choice := opts[0].Value + sel := huh.NewSelect[string](). + Title(fmt.Sprintf("%s › %s › %s", p.ServiceName, p.Type, p.Name)). + Description(desc). + Options(opts...). + Height(min(len(opts)+2, maxListHeight)). + Value(&choice) + ok, err := back(huh.NewForm(huh.NewGroup(sel))) + if err != nil || !ok || choice == choiceBack { + return err + } + + switch choice { + case choiceReset: + e.record(i, client.AgentConfigChange{Reset: true}) + case choiceUndo: + delete(e.pending, p.Key()) + case choiceEdit: + nv, changed, err := editScalar(p, v) + if err != nil { + return err + } + if changed { + e.record(i, client.AgentConfigChange{Value: nv}) + } + case choiceText: + nv, changed, err := editText(p, v) + if err != nil { + return err + } + if changed { + e.record(i, client.AgentConfigChange{Value: nv}) + } + default: // add or an entry index + if err := e.editEntry(i, list, choice); err != nil { + return err + } + } + } +} + +// editEntry adds, changes or removes one entry of a list property. +func (e *agentConfigEditor) editEntry(i int, list *entryList, choice string) error { + p := e.props[i] + var keys, values []string + k := -1 + if choice == choiceAdd { + keys = list.Keys() + values = make([]string, len(keys)) + } else { + k, _ = strconv.Atoi(choice) + for _, f := range list.Fields(k) { + keys, values = append(keys, f[0]), append(values, f[1]) + } + action := choiceEdit + sel := huh.NewSelect[string](). + Title(fmt.Sprintf("%s › %s › %s › entry %d", p.ServiceName, p.Type, p.Name, k+1)). + Description(list.Label(k)). + Options( + huh.NewOption("Edit entry", choiceEdit), + huh.NewOption("Remove entry", choiceRemove), + huh.NewOption("< Back", choiceBack)). + Value(&action) + ok, err := back(huh.NewForm(huh.NewGroup(sel))) + if err != nil || !ok || action == choiceBack { + return err + } + if action == choiceRemove { + list.Remove(k) + return e.recordList(i, list) + } + } + + fields := make([]huh.Field, len(keys)) + for n := range keys { + fields[n] = huh.NewInput().Title(keys[n]).Value(&values[n]) + } + title := fmt.Sprintf("%s › %s › %s › new entry", p.ServiceName, p.Type, p.Name) + if k >= 0 { + title = fmt.Sprintf("%s › %s › %s › entry %d", p.ServiceName, p.Type, p.Name, k+1) + } + form := huh.NewForm(huh.NewGroup(fields...).Title(title).Description("Values are written as typed. Enter on the last field saves; Esc goes back.")) + ok, err := back(form) + if err != nil || !ok { + return err + } + if k >= 0 { + list.Set(k, values) + } else { + list.Add(keys, values) + } + return e.recordList(i, list) +} + +func (e *agentConfigEditor) recordList(i int, list *entryList) error { + v, err := list.String() + if err != nil { + return err + } + e.record(i, client.AgentConfigChange{Value: v}) + return nil +} + +// editScalar edits a one-line value. Values in YAML single quotes are edited +// without them and quoted again on save. +func editScalar(p client.AgentConfigProperty, v string) (string, bool, error) { + plain, quoted := unquoteValue(v) + if v == "" { + _, quoted = unquoteValue(p.DefaultValue) + } + desc := "Default: " + previewValue(p.DefaultValue, p.Sensitive, 60) + if quoted { + desc += "\nThe value is stored in single quotes; they are added for you." + } + if strings.Contains(v, "\n") { + return editText(p, v) + } + in := huh.NewInput(). + Title(fmt.Sprintf("%s › %s › %s", p.ServiceName, p.Type, p.Name)). + Description(desc). + Value(&plain) + if p.Sensitive { + in = in.EchoMode(huh.EchoModePassword) + } + ok, err := back(huh.NewForm(huh.NewGroup(in))) + if err != nil || !ok { + return "", false, err + } + nv := quoteValue(plain, quoted) + return nv, nv != v, nil +} + +// editText edits a value as multi-line text. +func editText(p client.AgentConfigProperty, v string) (string, bool, error) { + text := v + area := huh.NewText(). + Title(fmt.Sprintf("%s › %s › %s", p.ServiceName, p.Type, p.Name)). + Description("Edit the value as YAML text. alt+enter or ctrl+j: new line · ctrl+e: open $EDITOR · enter: save · esc: back"). + Lines(min(max(strings.Count(v, "\n")+2, 6), 18)). + EditorExtension("yaml"). + Value(&text) + ok, err := back(huh.NewForm(huh.NewGroup(area))) + if err != nil || !ok { + return "", false, err + } + if strings.Contains(v, "\n") && text != "" && !strings.HasSuffix(text, "\n") { + text += "\n" + } + return text, text != v, nil +} + +// review shows the pending changes. ok is true when the user chose to apply. +func (e *agentConfigEditor) review() (bool, error) { + var b strings.Builder + changes := e.changes() + if len(changes) == 0 { + b.WriteString("No changes.") + } + const maxLines = 24 + for n, ch := range changes { + if n >= maxLines { + fmt.Fprintf(&b, "... and %d more\n", len(changes)-n) + break + } + var p client.AgentConfigProperty + var i int + for i = range e.props { + if e.props[i].Key() == ch.Property { + p = e.props[i] + break + } + } + nv, _ := e.value(i) + suffix := "" + if ch.Reset { + suffix = " (reset to default)" + } + if lines, ok := entryDiff(p.Value, nv); ok && !p.Sensitive { + fmt.Fprintf(&b, "%s › %s › %s%s\n", p.ServiceName, p.Type, p.Name, suffix) + for _, l := range lines { + fmt.Fprintf(&b, " %s\n", truncate(l, 100)) + } + continue + } + fmt.Fprintf(&b, "%s › %s › %s: %s → %s%s\n", p.ServiceName, p.Type, p.Name, + previewValue(p.Value, p.Sensitive, 40), previewValue(nv, p.Sensitive, 40), suffix) + } + opts := []huh.Option[string]{huh.NewOption("Back to editing", choiceBack)} + if len(changes) > 0 { + opts = append([]huh.Option[string]{huh.NewOption(fmt.Sprintf("Apply %d change(s)", len(changes)), choiceApply)}, opts...) + } + opts = append(opts, huh.NewOption("Cancel", choiceCancel)) + choice := opts[0].Value + sel := huh.NewSelect[string](). + Title(e.title + " - review"). + Description(strings.TrimRight(b.String(), "\n")). + Options(opts...). + Value(&choice) + if err := runForm(huh.NewForm(huh.NewGroup(sel))); err != nil { + return false, err + } + switch choice { + case choiceApply: + return true, nil + case choiceCancel: + return false, ErrCanceled + } + return false, nil +} diff --git a/internal/tui/agentconfigeditor_test.go b/internal/tui/agentconfigeditor_test.go new file mode 100644 index 0000000..f07d0e3 --- /dev/null +++ b/internal/tui/agentconfigeditor_test.go @@ -0,0 +1,151 @@ +package tui + +import ( + "strings" + "testing" + + "github.com/jbrazda/iics-cli/internal/client" +) + +const connProps = `- key: timeBetweenEvictionRunsMillis + value: 300000 +- key: testOnBorrow + value: true +- key: url + value: 'jdbc:postgresql://db:5432/x' +` + +func TestQuoteValue(t *testing.T) { + for _, tc := range []struct { + in, plain string + quoted bool + }{ + {"'10G'", "10G", true}, + {"''", "", true}, + {"'it''s'", "it's", true}, + {"true", "true", false}, + {"'", "'", false}, + } { + plain, quoted := unquoteValue(tc.in) + if plain != tc.plain || quoted != tc.quoted { + t.Errorf("unquoteValue(%q) = %q, %v", tc.in, plain, quoted) + } + if back := quoteValue(plain, quoted); back != tc.in { + t.Errorf("quoteValue(%q, %v) = %q, want %q", plain, quoted, back, tc.in) + } + } +} + +func TestEntryListRoundTrip(t *testing.T) { + l, ok := parseEntryList(connProps) + if !ok || l.Len() != 3 { + t.Fatalf("parseEntryList: ok=%v", ok) + } + if got := l.Label(1); got != "key=testOnBorrow value=true" { + t.Errorf("Label = %q", got) + } + if s, err := l.String(); err != nil || s != connProps { + t.Errorf("unchanged String() = %q, %v", s, err) + } + + l.Set(0, []string{"timeBetweenEvictionRunsMillis", "60000"}) + l.Add(l.Keys(), []string{"maxTotal", "20"}) + l.Remove(1) + want := `- key: timeBetweenEvictionRunsMillis + value: 60000 +- key: url + value: 'jdbc:postgresql://db:5432/x' +- key: maxTotal + value: 20 +` + if s, err := l.String(); err != nil || s != want { + t.Errorf("edited String() =\n%s\nwant\n%s", s, want) + } +} + +func TestEntryListQuotesWhenNeeded(t *testing.T) { + l, _ := parseEntryList("- name: a\n level: INFO\n") + l.Set(0, []string{"org.apache: x", "WARN"}) + s, _ := l.String() + if s != "- name: 'org.apache: x'\n level: WARN\n" { + t.Errorf("String() = %q", s) + } +} + +func TestParseEntryListRejects(t *testing.T) { + for _, v := range []string{"", "'10G'", "true", "- a\n- b\n", "key: value\n", "- key: [1, 2]\n"} { + if _, ok := parseEntryList(v); ok { + t.Errorf("parseEntryList(%q) should not be a list", v) + } + } +} + +func TestPreviewValue(t *testing.T) { + if got := previewValue(connProps, false, 80); got != "[3 entries] key=timeBetweenEvictionRunsMillis value=300000" { + t.Errorf("list preview = %q", got) + } + if got := previewValue("secret", true, 60); got != "***" { + t.Errorf("sensitive preview = %q", got) + } + if got := previewValue("", false, 60); got != "(empty)" { + t.Errorf("empty preview = %q", got) + } + if got := previewValue("abcdefghij", false, 5); got != "abcd…" { + t.Errorf("truncated preview = %q", got) + } +} + +func TestAgentConfigEditorRecord(t *testing.T) { + e := &agentConfigEditor{ + props: []client.AgentConfigProperty{ + {Service: "process-engine", Type: "jvm", Name: "max-heap", Value: "'10G'", DefaultValue: "'1536M'", Overridden: true}, + {Service: "process-engine", Type: "cluster", Name: "enabled", Value: "false", DefaultValue: "false"}, + }, + pending: map[string]client.AgentConfigChange{}, + } + e.record(0, client.AgentConfigChange{Value: "'4G'"}) + e.record(1, client.AgentConfigChange{Reset: true}) // already default: no change + if len(e.pending) != 1 { + t.Fatalf("pending = %v", e.pending) + } + if v, over := e.value(0); v != "'4G'" || !over { + t.Errorf("value(0) = %q, %v", v, over) + } + e.record(0, client.AgentConfigChange{Reset: true}) + if v, over := e.value(0); v != "'1536M'" || over { + t.Errorf("value(0) after reset = %q, %v", v, over) + } + e.record(0, client.AgentConfigChange{Value: "'10G'"}) // back to the current value + if len(e.pending) != 0 { + t.Errorf("pending after undo by value = %v", e.pending) + } + e.record(1, client.AgentConfigChange{Value: "true"}) + if ch := e.changes(); len(ch) != 1 || ch[0].Property != "process-engine:cluster:enabled" || ch[0].Value != "true" { + t.Errorf("changes() = %+v", ch) + } +} + +func TestEntryDiff(t *testing.T) { + newV := `- key: timeBetweenEvictionRunsMillis + value: 300000 +- key: testOnBorrow + value: false +- key: maxTotal + value: 20 +` + lines, ok := entryDiff(connProps, newV) + want := []string{ + "~ key=testOnBorrow value: true → false", + "+ key=maxTotal value=20", + "- key=url value=jdbc:postgresql://db:5432/x", + } + if !ok || strings.Join(lines, "\n") != strings.Join(want, "\n") { + t.Errorf("entryDiff =\n%s", strings.Join(lines, "\n")) + } + if lines, ok := entryDiff("", "- name: a\n level: INFO\n"); !ok || len(lines) != 1 || lines[0] != "+ name=a level=INFO" { + t.Errorf("entryDiff from empty = %v, %v", lines, ok) + } + if _, ok := entryDiff("'10G'", "'4G'"); ok { + t.Error("scalars are not lists") + } +} diff --git a/internal/tui/agentconfigvalue.go b/internal/tui/agentconfigvalue.go new file mode 100644 index 0000000..d786028 --- /dev/null +++ b/internal/tui/agentconfigvalue.go @@ -0,0 +1,250 @@ +package tui + +import ( + "bytes" + "fmt" + "strings" + + "gopkg.in/yaml.v3" +) + +// Helpers for editing agent property values. Process Server values are YAML +// fragments: scalars are often single-quoted ('10G'), and multi-value +// properties such as LDAP or database connection properties are lists of +// small maps: +// +// - key: timeBetweenEvictionRunsMillis +// value: 300000 + +// unquoteValue removes YAML single quotes around v. quoted reports whether +// v was quoted, so the quotes can be added back with quoteValue. +func unquoteValue(v string) (s string, quoted bool) { + if len(v) >= 2 && strings.HasPrefix(v, "'") && strings.HasSuffix(v, "'") { + return strings.ReplaceAll(v[1:len(v)-1], "''", "'"), true + } + return v, false +} + +// quoteValue is the inverse of unquoteValue. +func quoteValue(s string, quoted bool) string { + if !quoted { + return s + } + return "'" + strings.ReplaceAll(s, "'", "''") + "'" +} + +// entryList is a property value that is a YAML list of maps with scalar +// values. The document node is kept so unchanged entries keep their style. +type entryList struct { + doc *yaml.Node // document node; doc.Content[0] is the sequence +} + +// parseEntryList parses v as a list of maps. ok is false for any other value. +func parseEntryList(v string) (*entryList, bool) { + if strings.TrimSpace(v) == "" { + return nil, false + } + var doc yaml.Node + if err := yaml.Unmarshal([]byte(v), &doc); err != nil || len(doc.Content) != 1 { + return nil, false + } + seq := doc.Content[0] + if seq.Kind != yaml.SequenceNode || len(seq.Content) == 0 { + return nil, false + } + for _, item := range seq.Content { + if item.Kind != yaml.MappingNode { + return nil, false + } + for _, n := range item.Content { + if n.Kind != yaml.ScalarNode { + return nil, false + } + } + } + return &entryList{doc: &doc}, true +} + +func (l *entryList) items() []*yaml.Node { return l.doc.Content[0].Content } + +// Len returns the number of entries. +func (l *entryList) Len() int { return len(l.items()) } + +// Fields returns entry i as ordered key/value pairs. +func (l *entryList) Fields(i int) [][2]string { + item := l.items()[i] + out := make([][2]string, 0, len(item.Content)/2) + for k := 0; k+1 < len(item.Content); k += 2 { + out = append(out, [2]string{item.Content[k].Value, item.Content[k+1].Value}) + } + return out +} + +// Label is a one-line summary of entry i, for example +// "key=testOnBorrow value=true". +func (l *entryList) Label(i int) string { + parts := make([]string, 0, 2) + for _, f := range l.Fields(i) { + parts = append(parts, f[0]+"="+f[1]) + } + return strings.Join(parts, " ") +} + +// Keys returns the field names of the first entry, used for new entries. +func (l *entryList) Keys() []string { + fields := l.Fields(0) + keys := make([]string, len(fields)) + for i, f := range fields { + keys[i] = f[0] + } + return keys +} + +// Set replaces the values of entry i. Values that changed are written plain +// when YAML allows it, as typed. +func (l *entryList) Set(i int, values []string) { + item := l.items()[i] + for k := 0; k+1 < len(item.Content) && k/2 < len(values); k += 2 { + n := item.Content[k+1] + if n.Value != values[k/2] { + n.Value, n.Tag, n.Style = values[k/2], "", 0 + } + } +} + +// Add appends an entry with keys and values. +func (l *entryList) Add(keys, values []string) { + item := &yaml.Node{Kind: yaml.MappingNode} + for i, k := range keys { + v := "" + if i < len(values) { + v = values[i] + } + item.Content = append(item.Content, + &yaml.Node{Kind: yaml.ScalarNode, Value: k}, + &yaml.Node{Kind: yaml.ScalarNode, Value: v}) + } + seq := l.doc.Content[0] + seq.Content = append(seq.Content, item) +} + +// Remove deletes entry i. +func (l *entryList) Remove(i int) { + seq := l.doc.Content[0] + seq.Content = append(seq.Content[:i], seq.Content[i+1:]...) +} + +// String returns the list as YAML, two-space indented, as the agent stores +// it. +func (l *entryList) String() (string, error) { + if l.Len() == 0 { + return "", nil + } + var b bytes.Buffer + enc := yaml.NewEncoder(&b) + enc.SetIndent(2) + if err := enc.Encode(l.doc); err != nil { + return "", fmt.Errorf("writing list: %w", err) + } + if err := enc.Close(); err != nil { + return "", err + } + return b.String(), nil +} + +// previewValue is a one-line preview of a property value for list labels. +func previewValue(v string, sensitive bool, width int) string { + switch { + case sensitive: + return "***" + case strings.TrimSpace(v) == "": + return "(empty)" + } + if l, ok := parseEntryList(v); ok { + return fmt.Sprintf("[%d entries] %s", l.Len(), truncate(l.Label(0), width-14)) + } + if strings.Contains(v, "\n") { + return truncate(strings.ReplaceAll(strings.TrimRight(v, "\n"), "\n", " / "), width) + } + return truncate(v, width) +} + +func truncate(s string, width int) string { + r := []rune(s) + if width < 2 || len(r) <= width { + return s + } + return string(r[:width-1]) + "…" +} + +// entryDiff lists the entry changes between two list values, matching +// entries by their first field (key or name): "~ key=a value: 1 → 2", +// "+ key=b value=3", "- key=c value=4". ok is false when either value is +// not a list. +func entryDiff(oldV, newV string) (lines []string, ok bool) { + oldL, okOld := parseEntryList(oldV) + newL, okNew := parseEntryList(newV) + if !okOld && !okNew || (!okOld && strings.TrimSpace(oldV) != "") || (!okNew && strings.TrimSpace(newV) != "") { + return nil, false + } + type entry struct { + id string + fields [][2]string + } + collect := func(l *entryList) []entry { + if l == nil { + return nil + } + out := make([]entry, l.Len()) + for i := range out { + f := l.Fields(i) + out[i] = entry{fields: f} + if len(f) > 0 { + out[i].id = f[0][0] + "=" + f[0][1] + } + } + return out + } + label := func(fields [][2]string) string { + parts := make([]string, len(fields)) + for i, f := range fields { + parts[i] = f[0] + "=" + f[1] + } + return strings.Join(parts, " ") + } + olds, news := collect(oldL), collect(newL) + used := make([]bool, len(olds)) + for _, n := range news { + match := -1 + for i, o := range olds { + if !used[i] && o.id == n.id { + match = i + break + } + } + if match < 0 { + lines = append(lines, "+ "+label(n.fields)) + continue + } + used[match] = true + var changes []string + of := olds[match].fields + for k, f := range n.fields { + switch { + case k >= len(of): + changes = append(changes, f[0]+"="+f[1]) + case of[k][1] != f[1]: + changes = append(changes, fmt.Sprintf("%s: %s → %s", f[0], of[k][1], f[1])) + } + } + if len(changes) > 0 { + lines = append(lines, "~ "+n.id+" "+strings.Join(changes, " ")) + } + } + for i, o := range olds { + if !used[i] { + lines = append(lines, "- "+label(o.fields)) + } + } + return lines, true +} From 525735e8229a7b6c06f74e4513cfecc093e08912 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Tue, 20 Oct 2026 00:28:21 -0400 Subject: [PATCH 41/52] feat: custom agent properties (Custom Configuration Details) (CR-0049) - parse custom property rows and the custom types each service offers - agent configs get lists custom properties (OVERRIDDEN shows 'custom') - agent configs set --add / --remove; --set changes a custom value; --reset removes a custom property; --add-missing adds properties from --set/--from-file that do not exist (copying settings between agents) - change files: list items take add/remove/sensitive; null removes customs - type labels from the UI (Tomcat, DTM) are accepted in property names - agent configs edit: + Add custom property, edit value, remove/keep - rows are added after the last custom row with a millisecond ID; the empty template row is replaced or restored like the Administrator UI --- cmd/agent_configs.go | 57 +++- completions/iics.bash | 10 + .../CR-0049-agent-level-service-properties.md | 30 ++ docs/documentation/agent.md | 91 +++-- internal/client/agentconfigfile.go | 12 +- internal/client/agentcustom.go | 312 ++++++++++++++++++ internal/client/agentcustom_test.go | 214 ++++++++++++ internal/client/agentform.go | 150 ++++++++- internal/tui/agentconfigeditor.go | 185 ++++++++++- internal/tui/agentconfigeditor_test.go | 28 ++ 10 files changed, 1028 insertions(+), 61 deletions(-) create mode 100644 internal/client/agentcustom.go create mode 100644 internal/client/agentcustom_test.go diff --git a/cmd/agent_configs.go b/cmd/agent_configs.go index aa40753..a78b478 100644 --- a/cmd/agent_configs.go +++ b/cmd/agent_configs.go @@ -51,6 +51,9 @@ var agentPropertyCols = []output.Column{ {Header: "DEFAULT", Field: "defaultValue", Width: 30, Priority: 3, Func: wrapConfigField("defaultValue")}, {Header: "OVERRIDDEN", Field: "overridden", Width: 10, Priority: 2, Shrink: output.ShrinkNever, Func: func(v interface{}) string { row, _ := v.(map[string]interface{}) + if b, _ := row["custom"].(bool); b { + return "custom" + } if b, _ := row["overridden"].(bool); b { return "yes" } @@ -72,10 +75,20 @@ func diffValueFunc(valueField, overField string) func(v interface{}) string { return func(v interface{}) string { row, _ := v.(map[string]interface{}) s, _ := row[valueField].(string) + change, _ := row["change"].(string) + switch { + case change == "add" && valueField == "oldValue": + return "(new custom property)" + case change == "remove" && valueField == "newValue": + return "(removed)" + } if sens, _ := row["sensitive"].(bool); sens { s = "***" } s = output.WrapCell(s, 44) + if custom, _ := row["custom"].(bool); custom { + return s + } if over, _ := row[overField].(bool); !over { s += " (default)" } @@ -195,7 +208,7 @@ func writeAgentConfigExport(cmd *cobra.Command, props []client.AgentConfigProper // parseAgentConfigChanges builds the change list from --set, --reset and // --from-file. A --set value starting with @ is read from that file. -func parseAgentConfigChanges(sets, resets []string, fromFile string) ([]client.AgentConfigChange, error) { +func parseAgentConfigChanges(sets, adds, resets, removes []string, fromFile string) ([]client.AgentConfigChange, error) { var changes []client.AgentConfigChange if fromFile != "" { data, err := os.ReadFile(fromFile) @@ -206,23 +219,37 @@ func parseAgentConfigChanges(sets, resets []string, fromFile string) ([]client.A return nil, fmt.Errorf("%s: %w", fromFile, err) } } - for _, s := range sets { + parseValue := func(flag, s string, add bool) error { prop, value, ok := strings.Cut(s, "=") if !ok { - return nil, fmt.Errorf("invalid --set %q: expected service:type:name=value", s) + return fmt.Errorf("invalid --%s %q: expected service:type:name=value", flag, s) } if file, isFile := strings.CutPrefix(value, "@"); isFile { data, err := os.ReadFile(file) if err != nil { - return nil, fmt.Errorf("reading value for %s: %w", prop, err) + return fmt.Errorf("reading value for %s: %w", prop, err) } value = string(data) } - changes = append(changes, client.AgentConfigChange{Property: prop, Value: value}) + changes = append(changes, client.AgentConfigChange{Property: prop, Value: value, Add: add}) + return nil + } + for _, s := range sets { + if err := parseValue("set", s, false); err != nil { + return nil, err + } + } + for _, s := range adds { + if err := parseValue("add", s, true); err != nil { + return nil, err + } } for _, r := range resets { changes = append(changes, client.AgentConfigChange{Property: r, Reset: true}) } + for _, r := range removes { + changes = append(changes, client.AgentConfigChange{Property: r, Remove: true}) + } return changes, nil } @@ -282,7 +309,9 @@ func printRestartHint(cmd *cobra.Command, agent *client.Agent, diffs []client.Ag func newAgentConfigsSetCmd() *cobra.Command { var ( id, fid, name, hostname string - sets, resets []string + sets, adds []string + resets, removes []string + addMissing bool fromFile string dryRun, yes, force bool ) @@ -331,12 +360,12 @@ from the group; the command refuses to change them unless --force is given. if id == "" && fid == "" && name == "" && hostname == "" { return fmt.Errorf("one of --id, --fid, --name, or --hostname is required") } - changes, err := parseAgentConfigChanges(sets, resets, fromFile) + changes, err := parseAgentConfigChanges(sets, adds, resets, removes, fromFile) if err != nil { return err } if len(changes) == 0 { - return fmt.Errorf("nothing to change: use --set, --reset or --from-file") + return fmt.Errorf("nothing to change: use --set, --add, --reset, --remove or --from-file") } c, err := getClient(cmd) if err != nil { @@ -356,7 +385,7 @@ from the group; the command refuses to change them unless --force is given. if err != nil { return err } - diffs, err := form.Apply(changes) + diffs, err := form.ApplyWith(changes, client.ApplyOptions{AddMissing: addMissing}) if err != nil { return err } @@ -386,7 +415,10 @@ from the group; the command refuses to change them unless --force is given. } addAgentSelectorFlags(cmd, &id, &fid, &name, &hostname) cmd.Flags().StringArrayVar(&sets, "set", nil, "set a property: service:type:name=value (value @file reads a file); repeatable") - cmd.Flags().StringArrayVar(&resets, "reset", nil, "reset a property to its default: service:type:name; repeatable") + cmd.Flags().StringArrayVar(&resets, "reset", nil, "reset a property to its default (removes a custom property): service:type:name; repeatable") + cmd.Flags().StringArrayVar(&adds, "add", nil, "add a custom property: service:type:name=value; repeatable") + cmd.Flags().StringArrayVar(&removes, "remove", nil, "remove a custom property: service:type:name; repeatable") + cmd.Flags().BoolVar(&addMissing, "add-missing", false, "add properties from --set or --from-file that do not exist as custom properties") cmd.Flags().StringVar(&fromFile, "from-file", "", "JSON or YAML change file: service/type/name map (null resets) or list of {property, value, reset}") cmd.Flags().BoolVar(&dryRun, "dry-run", false, "show the changes without applying them") cmd.Flags().BoolVarP(&yes, "yes", "y", false, "apply without asking for confirmation") @@ -451,7 +483,10 @@ picked from a list. if err != nil { return err } - changes, err := tui.EditAgentConfigs(props, tui.AgentConfigEditorOptions{Title: "Agent: " + agent.Name}) + changes, err := tui.EditAgentConfigs(props, tui.AgentConfigEditorOptions{ + Title: "Agent: " + agent.Name, + CustomTypes: form.CustomTypes(), + }) if errors.Is(err, tui.ErrCanceled) { _, _ = fmt.Fprintln(w, "Canceled.") return nil diff --git a/completions/iics.bash b/completions/iics.bash index f5c9577..a103585 100644 --- a/completions/iics.bash +++ b/completions/iics.bash @@ -647,6 +647,12 @@ _iics_agent_configs_set() flags_with_completion=() flags_completion=() + flags+=("--add=") + two_word_flags+=("--add") + local_nonpersistent_flags+=("--add") + local_nonpersistent_flags+=("--add=") + flags+=("--add-missing") + local_nonpersistent_flags+=("--add-missing") flags+=("--dry-run") local_nonpersistent_flags+=("--dry-run") flags+=("--fid=") @@ -671,6 +677,10 @@ _iics_agent_configs_set() two_word_flags+=("--name") local_nonpersistent_flags+=("--name") local_nonpersistent_flags+=("--name=") + flags+=("--remove=") + two_word_flags+=("--remove") + local_nonpersistent_flags+=("--remove") + local_nonpersistent_flags+=("--remove=") flags+=("--reset=") two_word_flags+=("--reset") local_nonpersistent_flags+=("--reset") diff --git a/docs/ChangeRequests/pending/CR-0049-agent-level-service-properties.md b/docs/ChangeRequests/pending/CR-0049-agent-level-service-properties.md index 1bde169..c18c6a4 100644 --- a/docs/ChangeRequests/pending/CR-0049-agent-level-service-properties.md +++ b/docs/ChangeRequests/pending/CR-0049-agent-level-service-properties.md @@ -61,6 +61,13 @@ changed from the CLI. logging) open in an entry editor with edit, add, remove, edit-as-text (`$EDITOR` via ctrl+e), reset and undo. The review lists entry-level changes. Menu entry "Edit agent properties". +- Custom properties (Custom Configuration Details), for example + `maxDTMProcesses` for the Data Integration Server Tomcat on CDI agents: + `get` lists them (marked `custom`), `set --add service:type:name=value` adds + one with a type the service offers, `--set` changes it, `--remove` (or + `--reset`) removes it, `--add-missing` adds missing properties from a change + file, and `agent configs edit` has **+ Add custom property** and remove. + Type labels from the UI (`Tomcat`, `DTM`) are accepted in property names. - `iics agent rename --id|--fid|--name|--hostname --new-name ` renames an agent, for example from the host name the installer registers to a logical name such as `CAI-A03`. The name is trimmed, at most 100 characters, and must @@ -82,6 +89,8 @@ changed from the CLI. internal/client/agentform.go # form read/parse/apply/save/verify internal/client/agentform_test.go # parser, apply, filter, GET/POST, re-login tests internal/client/agentconfigfile.go # change files (JSON/YAML map or list) and export +internal/client/agentcustom.go # custom property rows: parse, add, remove, submit +internal/client/agentcustom_test.go internal/client/agentconfigfile_test.go cmd/agent_configs.go # agent configs get / set cmd/agent_rename.go # agent rename @@ -218,3 +227,24 @@ Form layout (``, about 1,000 fields, all submitted on save) `101` stored `'101'` (quotes added), verified, and was reset afterwards. - Table columns `NAME` (get) and `PROPERTY` (set diff) no longer use `ShrinkWrap`, which the table renderer moves to the last position. +- Custom property rows (`#customConfigTable`) submit, in order: + `customConfigEngine` (service key), `customConfigType` (`-1`), + `customConfigTypeEngine` (type), `customConfigSubType` (`-1`), + `customConfigName` (maxlength 255), `customConfigValue` (maxlength 512), + `customConfigSensitive` (checkbox, only when checked), + `engineCustomConfigIdInput` (row ID) and `isSensitiveConfig`. The types per + service are the `data-engine` options of `customConfigTypeEngine`. From + `pnAgent.js`: a new row gets `Date.now()` as its ID; removing the last row + leaves an empty template row with `-1` values, which the form always + submits when there are no custom properties. The client keeps the parsed + fields as they are and applies additions and removals when it builds the + request, so field positions stay valid. +- Read-only on tst CDI-G01-A01: the 5 custom properties from the UI + (`maxDTMProcesses`, `csv-reader-disable-safety-switch`, `maxUrlLength`, + `INFA_CODEPAGENAME`, `MatchQuotesPastEndOfLine`) are listed, exported, and + a dry run of add, set and remove shows the expected changes. +- Live on tst CAI-A03 (OI Data Collector custom property, which replaces + the empty template row): `set --add`, `--set`, a refused duplicate `--add`, + and `--remove`, each verified by reading back; then add and remove through + `agent configs edit` in a pseudo-terminal. The agent has no custom + properties afterwards, as before. diff --git a/docs/documentation/agent.md b/docs/documentation/agent.md index 43d53e0..554ac58 100644 --- a/docs/documentation/agent.md +++ b/docs/documentation/agent.md @@ -235,7 +235,7 @@ Properties are named `service:type:name`: | Part | Values | | --------- | -------------------------------------------------------------------------------------------------------------------------------- | | `service` | Service name without its version (`process-engine`, `OpsInsightsDataCollector`), the display name (`Process Server`), or `agent` | -| `type` | Property type, for example `server`, `cluster`, `jvm`, `db`, `logging`, or `AGENTCORE_CFG` for `agent` properties | +| `type` | Property type, for example `server`, `cluster`, `jvm`, `db`, `logging`, `TOMCAT_CFG`, or the UI label (`Tomcat`, `DTM`) | | `name` | Property name, for example `primary-node` | Matching is case-insensitive. Use `type/subType` (for example @@ -250,6 +250,33 @@ unless `--include-hidden` or `--service agent` is given. On CDI agents the editable Tomcat and JVM settings are under the Data Integration Server service, for example `Data_Integration_Server:TOMCAT_JRE:INFA_MEMORY`. +### Custom properties + +Services also take **custom properties** (Custom Configuration Details in the +Administrator UI): properties that are not in the service's standard list, +for example `maxDTMProcesses` or `maxUrlLength` for the Data Integration +Server Tomcat, or `INFA_CODEPAGENAME` for its JRE. Each service offers its own +types, for example: + +| Service | Custom property types | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| Data Integration Server | `TOMCAT_CFG` (Tomcat), `TOMCAT_JRE` (Tomcat JRE), `TOMCAT_LOG4J`, `PMRDTM_CFG` (DTM), `PLATFORM_CFG`, `SYSTEM_CFG`, `IDQ`, ... | +| Mass Ingestion | `AGENT_RUNTIME_SETTINGS`, `PGP_SETTINGS`, `JVM_SETTINGS` | +| Process Server | `server`, `cluster`, `jvm`, `connectors`, `db`, `logging` | + +The types come from the agent's own form, so they always match the services +the agent runs. A wrong type is rejected with the list of valid types. + +- `get` lists custom properties with the standard ones, marked `custom` in the + `OVERRIDDEN` column (`"custom": true` in JSON). `--overridden` includes them. +- `set --add service:type:name=value` adds one; `set --set` changes its value; + `set --remove service:type:name` (or `--reset`) removes it. +- Names can have up to 255 characters, values up to 512, on one line. +- `agent configs edit` lists them with a `+` mark and offers + **+ Add custom property** for each service. +- `get --export` includes them. When applying such a file to another agent, + `--add-missing` adds the custom properties that agent does not have yet. + ### agent configs get Show an agent's properties with their defaults and whether they are overridden. @@ -282,8 +309,8 @@ properties), `NAME`, `VALUE`, `DEFAULT`, `OVERRIDDEN`. Long values are wrapped. `--output csv` prints one flat table with an extra `SERVICE` column. `--output json` / `yaml` list the properties with `service`, `serviceName`, -`type`, `subType`, `name`, `value`, `defaultValue`, `overridden` and -`sensitive`. +`type`, `subType`, `name`, `value`, `defaultValue`, `overridden`, +`sensitive` and `custom`. ```text Agent: CAI-A03 (01D23X0800000000008G) @@ -313,18 +340,21 @@ Change or reset properties of one agent. The changes are shown as a marked `(default)`, sensitive values are shown as `***`) and applied after confirmation. -| Flag | Type | Description | -| ------------- | ------ | ------------------------------------------------------------------------------------------- | -| `--id` | string | Agent ID | -| `--fid` | string | Agent federated ID | -| `--name` | string | Agent name | -| `--hostname` | string | Agent host name | -| `--set` | string | `service:type:name=value`; a value `@file` reads the value from a file. Repeatable | -| `--reset` | string | `service:type:name` to reset to the default. Repeatable | -| `--from-file` | string | JSON or YAML change file (see below); only the properties in the file change | -| `--dry-run` | bool | Show the changes without applying them | -| `--yes`, `-y` | bool | Apply without asking for confirmation | -| `--force` | bool | Change the agent even if its Secure Agent group has group-level properties | +| Flag | Type | Description | +| --------------- | ------ | ---------------------------------------------------------------------------------------- | +| `--id` | string | Agent ID | +| `--fid` | string | Agent federated ID | +| `--name` | string | Agent name | +| `--hostname` | string | Agent host name | +| `--set` | string | `service:type:name=value`; a value `@file` reads the value from a file. Repeatable | +| `--reset` | string | `service:type:name` to reset to the default; removes a custom property. Repeatable | +| `--add` | string | `service:type:name=value` to add a custom property (`@file` reads the value). Repeatable | +| `--remove` | string | `service:type:name` of a custom property to remove. Repeatable | +| `--add-missing` | bool | Add properties from `--set` or `--from-file` that do not exist, as custom properties | +| `--from-file` | string | JSON or YAML change file (see below); only the properties in the file change | +| `--dry-run` | bool | Show the changes without applying them | +| `--yes`, `-y` | bool | Apply without asking for confirmation | +| `--force` | bool | Change the agent even if its Secure Agent group has group-level properties | Rules: @@ -380,10 +410,17 @@ A list of changes: [ { "property": "process-engine:cluster:primary-node", "value": "false" }, { "property": "process-engine:jvm:max-heap", "value": "'4G'" }, - { "property": "process-engine:jvm:min-heap", "reset": true } + { "property": "process-engine:jvm:min-heap", "reset": true }, + { "property": "Data_Integration_Server:TOMCAT_CFG:maxDTMProcesses", "value": "5", "add": true }, + { "property": "Data_Integration_Server:PMRDTM_CFG:MatchQuotesPastEndOfLine", "remove": true } ] ``` +List items take `property` and one of `value`, `reset: true`, `add: true` +(with `value`, optionally `sensitive: true`) or `remove: true`. In the nested +map, `null` resets a standard property or removes a custom one; properties +that do not exist are rejected unless `--add-missing` is given. + Notes on values: - Values are used as written. YAML removes one level of quotes, so a @@ -432,6 +469,17 @@ iics agent configs get --name CAI-A01 --service process-engine --overridden --ex # edit agent-specific values such as host-name, key-alias and primary-node iics agent configs set --name CAI-A02 --from-file pe.yaml --dry-run +# Custom properties on a CDI agent (type value or UI label) +iics agent configs set --name CDI-G01-A01 \ + --add "Data Integration Server:Tomcat:maxDTMProcesses=5" \ + --add "Data_Integration_Server:TOMCAT_JRE:INFA_CODEPAGENAME=UTF-8" +iics agent configs set --name CDI-G01-A01 --set "Data_Integration_Server:TOMCAT_CFG:maxDTMProcesses=8" +iics agent configs set --name CDI-G01-A01 --remove "Data_Integration_Server:PMRDTM_CFG:MatchQuotesPastEndOfLine" + +# Copy a CDI agent's Data Integration Server overrides and custom properties to a new agent +iics agent configs get --name CDI-G01-A01 --service Data_Integration_Server --overridden --export > dis.yaml +iics agent configs set --name CDI-G01-A02 --from-file dis.yaml --add-missing --dry-run + # Restart Process Server to use the new values iics agent restart --name CAI-A03 --service "Process Server" --blocking ``` @@ -458,10 +506,13 @@ menu entry **Edit agent properties** runs this command. Screens: -1. **Services** - each service with its number of properties, overridden - properties and pending changes, then **Review and apply** and **Cancel**. +1. **Services** - each service with its number of properties, overridden and + custom properties and pending changes, then **Review and apply** and + **Cancel**. 2. **Properties** of a service - type, name and a value preview. `*` marks an - overridden property, `✎` a pending change. `/` filters. + overridden property, `+` a custom property, `✎` a pending change, `✗` a + custom property to remove. `/` filters. **+ Add custom property** asks for + the type (from the service's types), name and value. 3. **Property editor**: - One-line values open in an input field. Values stored in YAML single quotes (`'10G'`) are edited without the quotes, which are added back. @@ -476,6 +527,8 @@ Screens: - **Edit as text** edits the whole value as YAML; `ctrl+e` opens it in `$EDITOR`. - **Reset to default** and **Undo changes to this property**. + - Custom properties: **Edit value**, **Remove custom property** (or + **Keep this property** to undo a removal). 4. **Review** - all pending changes; list properties show entry changes (`~` changed, `+` added, `-` removed). **Apply** saves. diff --git a/internal/client/agentconfigfile.go b/internal/client/agentconfigfile.go index 521d21b..765f106 100644 --- a/internal/client/agentconfigfile.go +++ b/internal/client/agentconfigfile.go @@ -11,7 +11,7 @@ import ( // in one of two shapes: // // A nested map, as written by AgentConfigDocument. A null value resets the -// property to its default: +// property to its default, or removes a custom property: // // process-engine: // cluster: @@ -23,7 +23,9 @@ import ( // A list of changes: // // [{"property": "process-engine:cluster:primary-node", "value": "true"}, -// {"property": "process-engine:jvm:min-heap", "reset": true}] +// {"property": "process-engine:jvm:min-heap", "reset": true}, +// {"property": "Data_Integration_Server:TOMCAT_CFG:maxDTMProcesses", "value": "5", "add": true}, +// {"property": "Data_Integration_Server:PMRDTM_CFG:MatchQuotesPastEndOfLine", "remove": true}] // // Scalars are used as written, so true or 512 become "true" and "512". func ParseAgentConfigChanges(data []byte) ([]AgentConfigChange, error) { @@ -93,6 +95,12 @@ func parseChangeList(root *yaml.Node) ([]AgentConfigChange, error) { ch.Value = val.Value case "reset": ch.Reset = val.Value == "true" + case "add": + ch.Add = val.Value == "true" + case "remove": + ch.Remove = val.Value == "true" + case "sensitive": + ch.Sensitive = val.Value == "true" default: return nil, fmt.Errorf("change file line %d: unknown key %q", item.Content[k].Line, key) } diff --git a/internal/client/agentcustom.go b/internal/client/agentcustom.go new file mode 100644 index 0000000..32c4d07 --- /dev/null +++ b/internal/client/agentcustom.go @@ -0,0 +1,312 @@ +package client + +import ( + "fmt" + "html" + "regexp" + "sort" + "strconv" + "strings" + "time" +) + +// Custom properties ("Custom Configuration Details" in the Administrator UI) +// are extra properties added to a service, such as maxDTMProcesses for the +// Data Integration Server. Each one is a row of form fields: +// +// customConfigEngine service key, e.g. Data_Integration_Server77.0 +// customConfigType legacy type, always -1 for services +// customConfigTypeEngine property type, e.g. TOMCAT_CFG +// customConfigSubType -1 +// customConfigName name (maxlength 255) +// customConfigValue value (maxlength 512) +// customConfigSensitive checkbox, sent only when checked +// engineCustomConfigIdInput row ID; new rows use the time in milliseconds +// isSensitiveConfig stored sensitive flag +// +// The form always submits at least one row; with no custom properties it is +// an empty template row with -1 values. The UI's add and remove actions are +// reproduced in submitFields. + +const ( + maxCustomNameLength = 255 + maxCustomValueLength = 512 +) + +// CustomConfigType is a property type a service accepts for custom +// properties, e.g. {TOMCAT_CFG, Tomcat}. +type CustomConfigType struct { + Value string `json:"value"` + Label string `json:"label"` +} + +// customRow is one custom property row: fields[start] to fields[end]. +type customRow struct { + start, end int + template bool +} + +var ( + customTypeSelRe = regexp.MustCompile(`(?s)]*name="customConfigTypeEngine"[^>]*>(.*?)`) + customEngineSelRe = regexp.MustCompile(`(?s)]*name="customConfigEngine"[^>]*>(.*?)`) + customOptionRe = regexp.MustCompile(`(?s)]*)>([^<]*)`) +) + +// propertyNotFoundError is returned by Find when no property matches. +type propertyNotFoundError struct { + sel PropertySelector + agent string +} + +func (e *propertyNotFoundError) Error() string { + return fmt.Sprintf("property %s not found on agent %s", e.sel, e.agent) +} + +// parseCustomRow reads the custom property row starting at fields[i] and +// returns the index of its last field. +func (f *AgentConfigForm) parseCustomRow(i int) (int, error) { + row := customRow{start: i} + vals := map[string]string{} + valueIdx := -1 + sensitive := false + j := i + for ; j < len(f.fields); j++ { + fld := f.fields[j] + switch fld.name { + case "customConfigValue": + valueIdx = j + case "customConfigSensitive": + sensitive = true + } + if _, dup := vals[fld.name]; dup { + return 0, fmt.Errorf("unexpected agent form layout in custom property row at field %d", i) + } + vals[fld.name] = fld.value + if fld.name == "isSensitiveConfig" { + break + } + } + if j == len(f.fields) || valueIdx < 0 { + return 0, fmt.Errorf("unexpected agent form layout in custom property row at field %d", i) + } + row.end = j + engine := vals["customConfigEngine"] + if engine == "-1" || engine == "" { + row.template = true + f.customRows = append(f.customRows, row) + return j, nil + } + typ := vals["customConfigTypeEngine"] + if typ == "-1" || typ == "" { + typ = vals["customConfigType"] + } + sub := vals["customConfigSubType"] + if sub == "-1" { + sub = "" + } + f.customRows = append(f.customRows, row) + f.Properties = append(f.Properties, AgentConfigProperty{ + Service: versionTail.ReplaceAllString(engine, ""), + Type: typ, + SubType: sub, + Name: vals["customConfigName"], + Value: vals["customConfigValue"], + Sensitive: sensitive || vals["isSensitiveConfig"] == "true", + Custom: true, + valueIdx: valueIdx, + rowStart: i, + serviceKey: engine, + }) + return j, nil +} + +// parseCustomTypes reads the custom property types each service offers, and +// the service names, from the first custom row's selects. +func (f *AgentConfigForm) parseCustomTypes(section string) { + f.customTypes = map[string][]CustomConfigType{} + if m := customTypeSelRe.FindStringSubmatch(section); m != nil { + for _, om := range customOptionRe.FindAllStringSubmatch(m[1], -1) { + a := parseAttrs(om[1]) + engine, value := a["data-engine"], a["value"] + if engine == "" || value == "" || value == "-1" { + continue + } + label := strings.TrimSpace(html.UnescapeString(om[2])) + if label == "" { + label = value + } + f.customTypes[engine] = append(f.customTypes[engine], CustomConfigType{Value: value, Label: label}) + } + } + if m := customEngineSelRe.FindStringSubmatch(section); m != nil { + for _, om := range customOptionRe.FindAllStringSubmatch(m[1], -1) { + a := parseAttrs(om[1]) + if v := a["value"]; v != "" && v != "-1" && f.serviceNames[v] == "" { + f.serviceNames[v] = strings.TrimSpace(html.UnescapeString(om[2])) + } + } + } +} + +// typeValue returns the type value whose UI label is label for the service +// key, or "" when there is none. +func (f *AgentConfigForm) typeValue(serviceKey, label string) string { + for _, t := range f.customTypes[serviceKey] { + if strings.EqualFold(t.Label, label) { + return t.Value + } + } + return "" +} + +// CustomTypes returns the custom property types by service name (without +// version), for the services on this agent. +func (f *AgentConfigForm) CustomTypes() map[string][]CustomConfigType { + out := map[string][]CustomConfigType{} + for key, types := range f.customTypes { + out[versionTail.ReplaceAllString(key, "")] = types + } + return out +} + +// customService finds the service key for a service name, versioned key or +// display name among the services that accept custom properties. +func (f *AgentConfigForm) customService(name string) (string, error) { + var keys []string + for key := range f.customTypes { + if strings.EqualFold(name, key) || strings.EqualFold(name, versionTail.ReplaceAllString(key, "")) || + strings.EqualFold(name, f.serviceNames[key]) { + return key, nil + } + keys = append(keys, versionTail.ReplaceAllString(key, "")) + } + sort.Strings(keys) + return "", fmt.Errorf("service %q does not take custom properties on agent %s; services: %s", name, f.AgentName, strings.Join(keys, ", ")) +} + +// addCustom adds a custom property for sel. +func (f *AgentConfigForm) addCustom(sel PropertySelector, value string, sensitive bool) (AgentConfigDiff, error) { + if len(f.customRows) == 0 { + return AgentConfigDiff{}, fmt.Errorf("the agent form has no custom properties section") + } + key, err := f.customService(sel.Service) + if err != nil { + return AgentConfigDiff{}, err + } + if sel.SubType != "" { + return AgentConfigDiff{}, fmt.Errorf("custom property %s: sub-types are not supported", sel) + } + typ := "" + var labels []string + for _, t := range f.customTypes[key] { + if strings.EqualFold(sel.Type, t.Value) || strings.EqualFold(sel.Type, t.Label) { + typ = t.Value + } + labels = append(labels, t.Value) + } + if typ == "" { + return AgentConfigDiff{}, fmt.Errorf("custom property %s: type %q is not available for %s; types: %s", + sel, sel.Type, f.serviceNames[key], strings.Join(labels, ", ")) + } + name := strings.TrimSpace(sel.Name) + switch { + case name == "": + return AgentConfigDiff{}, fmt.Errorf("custom property %s: the name is empty", sel) + case len([]rune(name)) > maxCustomNameLength: + return AgentConfigDiff{}, fmt.Errorf("custom property %s: the name is longer than %d characters", sel, maxCustomNameLength) + case len([]rune(value)) > maxCustomValueLength: + return AgentConfigDiff{}, fmt.Errorf("custom property %s: the value is longer than %d characters", sel, maxCustomValueLength) + case strings.Contains(value, "\n"): + return AgentConfigDiff{}, fmt.Errorf("custom property %s: the value must be a single line", sel) + } + p := AgentConfigProperty{ + Service: versionTail.ReplaceAllString(key, ""), + ServiceName: f.serviceNames[key], + Type: typ, + Name: name, + Value: value, + Sensitive: sensitive, + Custom: true, + rowStart: -1, + serviceKey: key, + } + f.Properties = append(f.Properties, p) + return AgentConfigDiff{Property: p.Key(), ServiceName: p.ServiceName, NewValue: value, + Sensitive: sensitive, Custom: true, Change: "add"}, nil +} + +// submitFields returns the fields to submit: removed custom rows are left +// out, added custom properties follow the last custom row, and the empty +// template row is kept only while there is no custom property. +func (f *AgentConfigForm) submitFields() []formField { + removedRows := map[int]bool{} + var added []AgentConfigProperty + for _, p := range f.Properties { + switch { + case p.Custom && p.removed && p.rowStart >= 0: + removedRows[p.rowStart] = true + case p.Custom && !p.removed && p.rowStart < 0: + added = append(added, p) + } + } + if len(removedRows) == 0 && len(added) == 0 { + return f.fields + } + remaining := len(added) + template := -1 + for _, r := range f.customRows { + switch { + case r.template: + template = r.start + case !removedRows[r.start]: + remaining++ + } + } + skip := map[int]int{} // row start -> row end + for _, r := range f.customRows { + if removedRows[r.start] || r.template && remaining > 0 { + skip[r.start] = r.end + } + } + last := f.customRows[len(f.customRows)-1].end + id := time.Now().UnixMilli() + out := make([]formField, 0, len(f.fields)+9*len(added)) + for i := 0; i < len(f.fields); i++ { + if end, ok := skip[i]; ok { + i = end + } else { + out = append(out, f.fields[i]) + } + if i != last { + continue + } + for _, p := range added { + out = append(out, customRowFields(p.serviceKey, p.Type, p.Name, p.Value, p.Sensitive, id)...) + id++ + } + if remaining == 0 && template < 0 { + out = append(out, customRowFields("-1", "-1", "", "", false, id)...) + } + } + return out +} + +// customRowFields builds the fields of one custom property row as the UI +// submits a new row. +func customRowFields(serviceKey, typ, name, value string, sensitive bool, id int64) []formField { + ids := strconv.FormatInt(id, 10) + fields := []formField{ + {"customConfigEngine", serviceKey}, + {"customConfigType", "-1"}, + {"customConfigTypeEngine", typ}, + {"customConfigSubType", "-1"}, + {"customConfigName", name}, + {"customConfigValue", value}, + } + if sensitive { + fields = append(fields, formField{"customConfigSensitive", ids}) + } + return append(fields, + formField{"engineCustomConfigIdInput", ids}, + formField{"isSensitiveConfig", "false"}) +} diff --git a/internal/client/agentcustom_test.go b/internal/client/agentcustom_test.go new file mode 100644 index 0000000..10145c9 --- /dev/null +++ b/internal/client/agentcustom_test.go @@ -0,0 +1,214 @@ +package client + +import ( + "net/url" + "strings" + "testing" +) + +// customRowHTML is one custom property row as the Administrator UI renders +// it. engine and typ are the selected options; "-1" leaves them unselected. +func customRowHTML(engine, typ, name, value, id string) string { + sel := func(v, want string) string { + if v == want { + return " selected" + } + return "" + } + return ` + +
+
+ + + + + + +` +} + +func customFormPage(rows ...string) string { + return ` + + + + + + + + + +` + strings.Join(rows, "\n") + `
+ + + + +` +} + +// submitted returns the custom rows of the encoded form as +// "engine|type|name=value|id" strings. +func submitted(t *testing.T, f *AgentConfigForm) []string { + t.Helper() + body := f.encode() + var rows []string + var cur []string + for _, kv := range strings.Split(body, "&") { + k, v, _ := strings.Cut(kv, "=") + v, _ = url.QueryUnescape(v) + switch k { + case "customConfigEngine", "customConfigTypeEngine": + cur = append(cur, v) + case "customConfigName": + cur = append(cur, v) + case "customConfigValue": + cur[len(cur)-1] += "=" + v + case "isSensitiveConfig": + rows = append(rows, strings.Join(cur, "|")) + cur = nil + } + } + return rows +} + +func TestParseCustomProperties(t *testing.T) { + page := customFormPage( + customRowHTML("Data_Integration_Server77.0", "TOMCAT_CFG", "maxDTMProcesses", "5", "1170373"), + customRowHTML("Data_Integration_Server77.0", "PMRDTM_CFG", "MatchQuotesPastEndOfLine", "Yes", "1170385")) + f, err := parseAgentConfigForm(page) + if err != nil { + t.Fatal(err) + } + var custom []string + for _, p := range f.Properties { + if p.Custom { + custom = append(custom, p.Key()+"="+p.Value+" ("+p.ServiceName+")") + } + } + want := "Data_Integration_Server:TOMCAT_CFG:maxDTMProcesses=5 (Data Integration Server)," + + "Data_Integration_Server:PMRDTM_CFG:MatchQuotesPastEndOfLine=Yes (Data Integration Server)" + if strings.Join(custom, ",") != want { + t.Errorf("custom properties = %v", custom) + } + types := f.CustomTypes() + if len(types["Data_Integration_Server"]) != 2 || types["Data_Integration_Server"][0] != (CustomConfigType{"TOMCAT_CFG", "Tomcat"}) { + t.Errorf("CustomTypes = %v", types) + } + // Unchanged forms submit the fields as they are. + if got := submitted(t, f); len(got) != 2 { + t.Errorf("unchanged submit = %v", got) + } +} + +func TestApplyCustomProperties(t *testing.T) { + page := customFormPage( + customRowHTML("Data_Integration_Server77.0", "TOMCAT_CFG", "maxDTMProcesses", "5", "1170373"), + customRowHTML("Data_Integration_Server77.0", "PMRDTM_CFG", "MatchQuotesPastEndOfLine", "Yes", "1170385")) + f, err := parseAgentConfigForm(page) + if err != nil { + t.Fatal(err) + } + diffs, err := f.Apply([]AgentConfigChange{ + {Property: "Data Integration Server:Tomcat:maxDTMProcesses", Value: "6"}, // display name and type label + {Property: "Data_Integration_Server:PMRDTM_CFG:MatchQuotesPastEndOfLine", Remove: true}, + {Property: "Data_Integration_Server:DTM:NewOne", Value: "x", Add: true}, + }) + if err != nil { + t.Fatal(err) + } + if len(diffs) != 3 || diffs[0].NewValue != "6" || diffs[1].Change != "remove" || diffs[2].Change != "add" || + diffs[2].Property != "Data_Integration_Server:PMRDTM_CFG:NewOne" { + t.Errorf("diffs = %+v", diffs) + } + got := strings.Join(submitted(t, f), ",") + want := "Data_Integration_Server77.0|TOMCAT_CFG|maxDTMProcesses=6,Data_Integration_Server77.0|PMRDTM_CFG|NewOne=x" + if got != want { + t.Errorf("submitted rows = %s\nwant %s", got, want) + } + if !strings.Contains(f.encode(), "engineCustomConfigIdInput=1170373") { + t.Error("existing row lost its ID") + } +} + +func TestApplyCustomTemplateRow(t *testing.T) { + // No custom properties: the form has an empty template row. + f, err := parseAgentConfigForm(customFormPage(customRowHTML("-1", "-1", "", "", "-1"))) + if err != nil { + t.Fatal(err) + } + if _, err = f.Apply([]AgentConfigChange{{Property: "OpsInsightsDataCollector:OpsInsights:cliTest", Value: "1", Add: true}}); err != nil { + t.Fatal(err) + } + if got := strings.Join(submitted(t, f), ","); got != "OpsInsightsDataCollector25|OpsInsights|cliTest=1" { + t.Errorf("template should be replaced, got %s", got) + } + + // Removing the last custom property submits an empty template row. + g, err := parseAgentConfigForm(customFormPage(customRowHTML("Data_Integration_Server77.0", "TOMCAT_CFG", "a", "1", "11"))) + if err != nil { + t.Fatal(err) + } + if _, err = g.Apply([]AgentConfigChange{{Property: "Data_Integration_Server:TOMCAT_CFG:a", Reset: true}}); err != nil { + t.Fatal(err) + } + if got := strings.Join(submitted(t, g), ","); got != "-1|-1|=" { + t.Errorf("expected only an empty template row, got %s", got) + } +} + +func TestApplyCustomErrors(t *testing.T) { + page := customFormPage(customRowHTML("Data_Integration_Server77.0", "TOMCAT_CFG", "maxDTMProcesses", "5", "1")) + cases := map[string]AgentConfigChange{ + "exists": {Property: "Data_Integration_Server:TOMCAT_CFG:maxDTMProcesses", Value: "1", Add: true}, + "standard": {Property: "Data_Integration_Server:TOMCAT_CFG:maxThreads", Value: "1", Add: true}, + "bad type": {Property: "Data_Integration_Server:BOGUS:x", Value: "1", Add: true}, + "bad service": {Property: "process-engine:jvm:x", Value: "1", Add: true}, + "missing": {Property: "Data_Integration_Server:TOMCAT_CFG:nope", Value: "1"}, + "remove std": {Property: "Data_Integration_Server:TOMCAT_CFG:maxThreads", Remove: true}, + "multi-line": {Property: "Data_Integration_Server:TOMCAT_CFG:x", Value: "a\nb", Add: true}, + "long value": {Property: "Data_Integration_Server:TOMCAT_CFG:x", Value: strings.Repeat("v", 513), Add: true}, + "add and remove": {Property: "Data_Integration_Server:TOMCAT_CFG:x", Add: true, Remove: true}, + } + for name, ch := range cases { + f, err := parseAgentConfigForm(page) + if err != nil { + t.Fatal(err) + } + if _, err := f.Apply([]AgentConfigChange{ch}); err == nil { + t.Errorf("%s: expected an error", name) + } + } +} + +func TestApplyAddMissing(t *testing.T) { + f, err := parseAgentConfigForm(customFormPage(customRowHTML("-1", "-1", "", "", "-1"))) + if err != nil { + t.Fatal(err) + } + diffs, err := f.ApplyWith([]AgentConfigChange{ + {Property: "Data_Integration_Server:TOMCAT_CFG:maxThreads", Value: "300"}, // standard: set + {Property: "Data_Integration_Server:TOMCAT_CFG:maxDTMProcesses", Value: "5"}, + }, ApplyOptions{AddMissing: true}) + if err != nil { + t.Fatal(err) + } + if len(diffs) != 2 || diffs[0].Change != "" || diffs[1].Change != "add" { + t.Errorf("diffs = %+v", diffs) + } +} diff --git a/internal/client/agentform.go b/internal/client/agentform.go index 87a8d92..111d124 100644 --- a/internal/client/agentform.go +++ b/internal/client/agentform.go @@ -4,6 +4,7 @@ import ( "context" "crypto/rand" "encoding/base64" + "errors" "fmt" "html" "io" @@ -48,9 +49,16 @@ type AgentConfigProperty struct { DefaultValue string `json:"defaultValue"` Overridden bool `json:"overridden"` Sensitive bool `json:"sensitive,omitempty"` + // Custom marks a custom property (Custom Configuration Details), added + // to a service on top of its standard properties. It has no default. + Custom bool `json:"custom,omitempty"` // Indexes into AgentConfigForm.fields for the value and overridden flag. - valueIdx, overIdx int + // For custom properties overIdx is unused, rowStart is the index of the + // row's first field, or -1 for a property added by Apply. + valueIdx, overIdx, rowStart int + // removed marks a custom property deleted by Apply. + removed bool // serviceKey is the versioned service key used in the form, for example // process-engine15124581. serviceKey string @@ -80,6 +88,13 @@ type AgentConfigForm struct { fields []formField cookies []*http.Cookie csrf string + + // Custom configuration rows (including the empty template row the form + // keeps when there are none), custom types per service key, and service + // display names by service key. + customRows []customRow + customTypes map[string][]CustomConfigType + serviceNames map[string]string } var ( @@ -256,6 +271,12 @@ func parseAgentConfigForm(page string) (*AgentConfigForm, error) { overIdx: i + 5, }) i += 5 + case "customConfigEngine": + end, err := form.parseCustomRow(i) + if err != nil { + return nil, err + } + i = end case "engineConfigKey": if i+3 >= len(f) || f[i+1].name != "engineConfigValue" || f[i+2].name != "engineConfigDefault" || f[i+3].name != "engineConfigOverriden" { return nil, fmt.Errorf("unexpected agent form layout at field %d (%s)", i, f[i].name) @@ -284,6 +305,14 @@ func parseAgentConfigForm(page string) (*AgentConfigForm, error) { if form.AgentID == "" || form.Version == "" { return nil, fmt.Errorf("agent form is missing the agent id or version") } + form.serviceNames = serviceNames + form.parseCustomTypes(section) + for i := range form.Properties { + p := &form.Properties[i] + if p.Custom && p.ServiceName == "" { + p.ServiceName = form.serviceNames[p.serviceKey] + } + } return form, nil } @@ -326,15 +355,27 @@ func (s PropertySelector) String() string { func (f *AgentConfigForm) Find(sel PropertySelector) (int, error) { var hits []int for i, p := range f.Properties { + if p.removed { + continue + } if sel.matches(p) { hits = append(hits, i) + continue + } + // The type may be given by its UI label, e.g. Tomcat for TOMCAT_CFG. + if v := f.typeValue(p.serviceKey, sel.Type); v != "" { + alias := sel + alias.Type = v + if alias.matches(p) { + hits = append(hits, i) + } } } switch len(hits) { case 1: return hits[0], nil case 0: - return -1, fmt.Errorf("property %s not found on agent %s", sel, f.AgentName) + return -1, &propertyNotFoundError{sel: sel, agent: f.AgentName} } keys := make([]string, len(hits)) for i, h := range hits { @@ -375,7 +416,7 @@ func (f *AgentConfigForm) Reset(i int) { // encode builds the form body for actionMethod=update, keeping field order. func (f *AgentConfigForm) encode() string { var b strings.Builder - for i, fld := range f.fields { + for i, fld := range f.submitFields() { v := fld.value switch fld.name { case "actionMethod": @@ -547,7 +588,7 @@ func (f *AgentConfigForm) Filter(flt AgentConfigFilter) ([]AgentConfigProperty, if typ != "" && !strings.EqualFold(typ, p.Type) || sub != "" && !strings.EqualFold(sub, p.SubType) { continue } - if nameRe != nil && !nameRe.MatchString(p.Name) || flt.Overridden && !p.Overridden { + if p.removed || nameRe != nil && !nameRe.MatchString(p.Name) || flt.Overridden && !p.Overridden && !p.Custom { continue } out = append(out, p) @@ -555,15 +596,20 @@ func (f *AgentConfigForm) Filter(flt AgentConfigFilter) ([]AgentConfigProperty, return out, nil } -// AgentConfigChange is one requested property change: a new value, or a -// reset to the default when Reset is true. +// AgentConfigChange is one requested property change: a new value, a reset +// to the default, or adding or removing a custom property. Reset on a custom +// property removes it. type AgentConfigChange struct { - Property string `json:"property"` // service:type[/subType]:name - Value string `json:"value,omitempty"` - Reset bool `json:"reset,omitempty"` + Property string `json:"property"` // service:type[/subType]:name + Value string `json:"value,omitempty"` + Reset bool `json:"reset,omitempty"` + Add bool `json:"add,omitempty"` // create a custom property + Remove bool `json:"remove,omitempty"` // delete a custom property + Sensitive bool `json:"sensitive,omitempty"` // mark a new custom property sensitive } -// AgentConfigDiff describes a change applied to the form. +// AgentConfigDiff describes a change applied to the form. Change is "add" or +// "remove" for custom properties that are created or deleted. type AgentConfigDiff struct { Property string `json:"property"` ServiceName string `json:"serviceName,omitempty"` @@ -572,38 +618,100 @@ type AgentConfigDiff struct { OldOverridden bool `json:"oldOverridden"` NewOverridden bool `json:"newOverridden"` Sensitive bool `json:"sensitive,omitempty"` + Custom bool `json:"custom,omitempty"` + Change string `json:"change,omitempty"` +} + +// ApplyOptions changes how Apply treats properties. +type ApplyOptions struct { + // AddMissing adds a custom property for a value change whose property + // does not exist, for example when copying settings to another agent. + AddMissing bool } // Apply applies changes to the form and returns the properties that change. // Setting a property to its current value leaves it untouched, so a default // value does not become an override. func (f *AgentConfigForm) Apply(changes []AgentConfigChange) ([]AgentConfigDiff, error) { + return f.ApplyWith(changes, ApplyOptions{}) +} + +// ApplyWith is Apply with options. +func (f *AgentConfigForm) ApplyWith(changes []AgentConfigChange, opts ApplyOptions) ([]AgentConfigDiff, error) { var diffs []AgentConfigDiff - seen := map[int]bool{} + seen := map[string]bool{} for _, ch := range changes { sel, err := ParsePropertySelector(ch.Property) if err != nil { return nil, err } + if ch.Add && (ch.Remove || ch.Reset) { + return nil, fmt.Errorf("property %s: add cannot be combined with remove or reset", sel) + } i, err := f.Find(sel) + var notFound *propertyNotFoundError + missing := errors.As(err, ¬Found) + if ch.Add || missing && opts.AddMissing && !ch.Reset && !ch.Remove { + if err == nil { + if f.Properties[i].Custom { + return nil, fmt.Errorf("custom property %s already exists; use set to change its value", f.Properties[i].Key()) + } + return nil, fmt.Errorf("%s is a standard property; use set to change its value", f.Properties[i].Key()) + } + if !missing { + return nil, err + } + d, addErr := f.addCustom(sel, normalizeNewlines(ch.Value), ch.Sensitive) + if addErr != nil { + return nil, addErr + } + if seen[d.Property] { + return nil, fmt.Errorf("property %s is changed more than once", d.Property) + } + seen[d.Property] = true + diffs = append(diffs, d) + continue + } if err != nil { + if missing { + return nil, fmt.Errorf("%w; use add to create a custom property", err) + } return nil, err } - if f.Properties[i].Service == SystemService { - return nil, fmt.Errorf("property %s is not editable: the Administrator UI hides the agent's own (%s) properties and the server ignores changes to them", f.Properties[i].Key(), SystemService) + p := &f.Properties[i] + if p.Service == SystemService { + return nil, fmt.Errorf("property %s is not editable: the Administrator UI hides the agent's own (%s) properties and the server ignores changes to them", p.Key(), SystemService) + } + if seen[p.Key()] { + return nil, fmt.Errorf("property %s is changed more than once", p.Key()) + } + seen[p.Key()] = true + old := *p + if p.Custom { + switch value := normalizeNewlines(ch.Value); { + case ch.Remove || ch.Reset: + p.removed = true + diffs = append(diffs, AgentConfigDiff{Property: p.Key(), ServiceName: p.ServiceName, OldValue: old.Value, + Sensitive: p.Sensitive, Custom: true, Change: "remove"}) + case value != old.Value: + p.Value = value + if p.rowStart >= 0 { + f.fields[p.valueIdx].value = value + } + diffs = append(diffs, AgentConfigDiff{Property: p.Key(), ServiceName: p.ServiceName, OldValue: old.Value, + NewValue: value, Sensitive: p.Sensitive, Custom: true}) + } + continue } - if seen[i] { - return nil, fmt.Errorf("property %s is changed more than once", f.Properties[i].Key()) + if ch.Remove { + return nil, fmt.Errorf("%s is a standard property and cannot be removed; use reset", p.Key()) } - seen[i] = true - old := f.Properties[i] switch value := normalizeNewlines(ch.Value); { case ch.Reset: f.Reset(i) case value != old.Value: f.Set(i, value) } - p := f.Properties[i] if p.Value == old.Value && p.Overridden == old.Overridden { continue } @@ -630,6 +738,12 @@ func (c *Client) VerifyAgentConfigDiffs(ctx context.Context, agentID string, dif return err } i, err := form.Find(sel) + if d.Change == "remove" { + if err == nil { + failed = append(failed, d.Property+" (still present)") + } + continue + } if err != nil { failed = append(failed, d.Property+" (not found)") continue diff --git a/internal/tui/agentconfigeditor.go b/internal/tui/agentconfigeditor.go index ff08f6d..2681bfc 100644 --- a/internal/tui/agentconfigeditor.go +++ b/internal/tui/agentconfigeditor.go @@ -14,13 +14,18 @@ import ( type AgentConfigEditorOptions struct { // Title is shown above each screen, e.g. "Agent: CAI-A03". Title string + // CustomTypes lists the custom property types by service name; services + // with types offer "Add custom property". + CustomTypes map[string][]client.CustomConfigType } // agentConfigEditor holds the properties and the pending edits by key. +// Custom properties added in the editor are appended to props. type agentConfigEditor struct { - props []client.AgentConfigProperty - pending map[string]client.AgentConfigChange - title string + props []client.AgentConfigProperty + pending map[string]client.AgentConfigChange + title string + customTypes map[string][]client.CustomConfigType } const ( @@ -32,6 +37,7 @@ const ( choiceEdit = "\x00edit" choiceRemove = "\x00remove" choiceApply = "\x00apply" + choiceCustom = "\x00custom" previewWidth = 44 maxListHeight = 22 ) @@ -40,7 +46,7 @@ const ( // the changes to apply, in property order. Esc goes back one screen; on the // service screen it cancels with ErrCanceled. func EditAgentConfigs(props []client.AgentConfigProperty, opts AgentConfigEditorOptions) ([]client.AgentConfigChange, error) { - e := &agentConfigEditor{props: props, pending: map[string]client.AgentConfigChange{}, title: opts.Title} + e := &agentConfigEditor{props: props, pending: map[string]client.AgentConfigChange{}, title: opts.Title, customTypes: opts.CustomTypes} lastService := "" for { choice := lastService @@ -89,7 +95,7 @@ func (e *agentConfigEditor) value(i int) (string, bool) { p := e.props[i] ch, ok := e.pending[p.Key()] switch { - case !ok: + case !ok || ch.Remove: return p.Value, p.Overridden case ch.Reset: return p.DefaultValue, false @@ -97,11 +103,25 @@ func (e *agentConfigEditor) value(i int) (string, bool) { return ch.Value, true } +// removing reports whether custom property i is marked for removal. +func (e *agentConfigEditor) removing(i int) bool { + return e.pending[e.props[i].Key()].Remove +} + // record stores the pending edit for property i, or drops it when the -// property ends up as it is now. +// property ends up as it is now. A custom property added in the editor stays +// an addition with the new value. func (e *agentConfigEditor) record(i int, ch client.AgentConfigChange) { p := e.props[i] ch.Property = p.Key() + if prev := e.pending[p.Key()]; prev.Add { + e.pending[p.Key()] = client.AgentConfigChange{Property: p.Key(), Value: ch.Value, Add: true} + return + } + if ch.Remove || ch.Reset && p.Custom { + e.pending[p.Key()] = client.AgentConfigChange{Property: p.Key(), Remove: true} + return + } if ch.Reset && !p.Overridden && p.Value == p.DefaultValue || !ch.Reset && ch.Value == p.Value { delete(e.pending, p.Key()) return @@ -121,8 +141,8 @@ func (e *agentConfigEditor) changes() []client.AgentConfigChange { func (e *agentConfigEditor) serviceOptions() []huh.Option[string] { type stats struct { - name string - total, over, changed int + name string + total, over, custom, changed int } var order []string byService := map[string]*stats{} @@ -139,7 +159,10 @@ func (e *agentConfigEditor) serviceOptions() []huh.Option[string] { width = max(width, len(s.name)) } s.total++ - if _, over := e.value(i); over { + switch _, over := e.value(i); { + case p.Custom: + s.custom++ + case over: s.over++ } if _, ok := e.pending[p.Key()]; ok { @@ -150,6 +173,9 @@ func (e *agentConfigEditor) serviceOptions() []huh.Option[string] { for _, svc := range order { s := byService[svc] label := fmt.Sprintf("%-*s %4d properties %3d overridden", width, s.name, s.total, s.over) + if s.custom > 0 { + label += fmt.Sprintf(" %d custom", s.custom) + } if s.changed > 0 { label += fmt.Sprintf(" %d changed", s.changed) } @@ -177,16 +203,25 @@ func (e *agentConfigEditor) editService(service string) error { } nameW = min(nameW, 32) opts := []huh.Option[string]{huh.NewOption("< Back to services", choiceBack)} + if len(e.customTypes[service]) > 0 { + opts = append(opts, huh.NewOption("+ Add custom property", choiceCustom)) + } for _, i := range idx { p := e.props[i] v, over := e.value(i) mark := " " - if over { + switch { + case p.Custom: + mark = "+" + case over: mark = "*" } if _, ok := e.pending[p.Key()]; ok { mark = "✎" } + if e.removing(i) { + mark, v = "✗", "(removed)" + } label := fmt.Sprintf("%s %-*s %-*s %s", mark, typeW, p.Type, nameW, truncate(p.Name, nameW), previewValue(v, p.Sensitive, previewWidth)) opts = append(opts, huh.NewOption(label, strconv.Itoa(i))) } @@ -194,7 +229,7 @@ func (e *agentConfigEditor) editService(service string) error { name := e.props[idx[0]].ServiceName sel := huh.NewSelect[string](). Title(e.title + " - " + name). - Description("Select a property to edit (/ to filter). * overridden, ✎ changed."). + Description("Select a property to edit (/ to filter). * overridden, + custom, ✎ changed, ✗ removed."). Options(opts...). Height(min(len(opts)+2, maxListHeight)). Value(&choice) @@ -203,7 +238,19 @@ func (e *agentConfigEditor) editService(service string) error { return err } last = choice + if choice == choiceCustom { + if err := e.addCustom(service, e.props[idx[0]].ServiceName); err != nil { + return err + } + continue + } i, _ := strconv.Atoi(choice) + if e.props[i].Custom { + if err := e.editCustom(i); err != nil { + return err + } + continue + } if err := e.editProperty(i); err != nil { return err } @@ -428,6 +475,14 @@ func (e *agentConfigEditor) review() (bool, error) { } } nv, _ := e.value(i) + switch { + case ch.Add: + fmt.Fprintf(&b, "+ %s › %s › %s = %s (new custom property)\n", p.ServiceName, p.Type, p.Name, previewValue(nv, p.Sensitive, 40)) + continue + case ch.Remove: + fmt.Fprintf(&b, "- %s › %s › %s (remove custom property)\n", p.ServiceName, p.Type, p.Name) + continue + } suffix := "" if ch.Reset { suffix = " (reset to default)" @@ -464,3 +519,111 @@ func (e *agentConfigEditor) review() (bool, error) { } return false, nil } + +// editCustom edits a custom property: change its value or remove it. +func (e *agentConfigEditor) editCustom(i int) error { + for { + p := e.props[i] + v, _ := e.value(i) + _, changed := e.pending[p.Key()] + added := e.pending[p.Key()].Add + desc := fmt.Sprintf("Custom property, type %s\nValue: %s", p.Type, previewValue(v, p.Sensitive, 60)) + var opts []huh.Option[string] + if e.removing(i) { + desc += "\nMarked for removal." + opts = append(opts, huh.NewOption("Keep this property", choiceUndo)) + } else { + opts = append(opts, huh.NewOption("Edit value", choiceEdit), huh.NewOption("Remove custom property", choiceRemove)) + if changed && !added { + opts = append(opts, huh.NewOption("Undo changes to this property", choiceUndo)) + } + } + opts = append(opts, huh.NewOption("< Back", choiceBack)) + choice := opts[0].Value + sel := huh.NewSelect[string](). + Title(fmt.Sprintf("%s › %s › %s", p.ServiceName, p.Type, p.Name)). + Description(desc). + Options(opts...). + Value(&choice) + ok, err := back(huh.NewForm(huh.NewGroup(sel))) + if err != nil || !ok || choice == choiceBack { + return err + } + switch choice { + case choiceUndo: + delete(e.pending, p.Key()) + case choiceRemove: + if added { + // Never saved: drop it. + delete(e.pending, p.Key()) + e.props = append(e.props[:i], e.props[i+1:]...) + return nil + } + e.record(i, client.AgentConfigChange{Remove: true}) + case choiceEdit: + nv := v + in := huh.NewInput(). + Title(fmt.Sprintf("%s › %s › %s", p.ServiceName, p.Type, p.Name)). + Description("Custom property value (one line, up to 512 characters)."). + CharLimit(512). + Value(&nv) + if p.Sensitive { + in = in.EchoMode(huh.EchoModePassword) + } + ok, err := back(huh.NewForm(huh.NewGroup(in))) + if err != nil { + return err + } + if ok && nv != v { + e.record(i, client.AgentConfigChange{Value: nv}) + } + } + } +} + +// addCustom asks for the type, name and value of a new custom property of +// service and adds it as a pending change. +func (e *agentConfigEditor) addCustom(service, serviceName string) error { + types := e.customTypes[service] + typ := types[0].Value + typeOpts := make([]huh.Option[string], len(types)) + for n, t := range types { + label := t.Label + if t.Label != t.Value { + label = fmt.Sprintf("%s (%s)", t.Label, t.Value) + } + typeOpts[n] = huh.NewOption(label, t.Value) + } + var name, value string + exists := func(n string) bool { + key := (client.AgentConfigProperty{Service: service, Type: typ, Name: strings.TrimSpace(n)}).Key() + for _, p := range e.props { + if strings.EqualFold(p.Key(), key) { + return true + } + } + return false + } + form := huh.NewForm(huh.NewGroup( + huh.NewSelect[string]().Title("Type").Options(typeOpts...).Height(min(len(typeOpts)+2, 12)).Value(&typ), + huh.NewInput().Title("Name").CharLimit(255).Value(&name).Validate(func(s string) error { + switch { + case strings.TrimSpace(s) == "": + return errors.New("enter a name") + case exists(s): + return errors.New("this service already has a property with this type and name") + } + return nil + }), + huh.NewInput().Title("Value").CharLimit(512).Value(&value), + ).Title(fmt.Sprintf("%s › new custom property", serviceName)). + Description("Custom properties are added to the service in addition to its standard properties.")) + ok, err := back(form) + if err != nil || !ok { + return err + } + p := client.AgentConfigProperty{Service: service, ServiceName: serviceName, Type: typ, Name: strings.TrimSpace(name), Custom: true} + e.props = append(e.props, p) + e.pending[p.Key()] = client.AgentConfigChange{Property: p.Key(), Value: value, Add: true} + return nil +} diff --git a/internal/tui/agentconfigeditor_test.go b/internal/tui/agentconfigeditor_test.go index f07d0e3..8a6ee75 100644 --- a/internal/tui/agentconfigeditor_test.go +++ b/internal/tui/agentconfigeditor_test.go @@ -149,3 +149,31 @@ func TestEntryDiff(t *testing.T) { t.Error("scalars are not lists") } } + +func TestAgentConfigEditorCustom(t *testing.T) { + e := &agentConfigEditor{ + props: []client.AgentConfigProperty{ + {Service: "Data_Integration_Server", Type: "TOMCAT_CFG", Name: "maxDTMProcesses", Value: "5", Custom: true}, + }, + pending: map[string]client.AgentConfigChange{}, + } + e.record(0, client.AgentConfigChange{Value: "6"}) + if ch := e.pending["Data_Integration_Server:TOMCAT_CFG:maxDTMProcesses"]; ch.Value != "6" || ch.Add || ch.Remove { + t.Errorf("custom value change = %+v", ch) + } + e.record(0, client.AgentConfigChange{Reset: true}) // reset on a custom property removes it + if !e.removing(0) { + t.Error("reset of a custom property should remove it") + } + + // A property added in the editor stays an addition when its value changes. + e.props = append(e.props, client.AgentConfigProperty{Service: "Data_Integration_Server", Type: "PMRDTM_CFG", Name: "New", Custom: true}) + e.pending["Data_Integration_Server:PMRDTM_CFG:New"] = client.AgentConfigChange{Property: "Data_Integration_Server:PMRDTM_CFG:New", Value: "a", Add: true} + e.record(1, client.AgentConfigChange{Value: "b"}) + if ch := e.pending["Data_Integration_Server:PMRDTM_CFG:New"]; !ch.Add || ch.Value != "b" { + t.Errorf("added property change = %+v", ch) + } + if got := e.changes(); len(got) != 2 || !got[0].Remove || !got[1].Add { + t.Errorf("changes() = %+v", got) + } +} From 8db041d0c426e596a310a41082738b6d7a9b6434 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Tue, 20 Oct 2026 00:47:36 -0400 Subject: [PATCH 42/52] feat: reset all agent properties; agent details -i with view picker (CR-0049) - agent configs set --reset-all [--service]: reset every overridden standard property like the UI's Reset All (custom kept, sensitive skipped) - editor: Reset all to defaults (all services) and per service, with a confirmation listing the properties - agent details -i (or no agent flag in a terminal): pick the agent, then services summary, details, full details or service properties - menu: Environments and agents > Show agent details - agent configs get wraps long property names in the cell instead of falling back to the vertical layout --- cmd/agent.go | 97 +++++++++++++++- cmd/agent_configs.go | 32 ++++- completions/iics.bash | 10 ++ .../CR-0049-agent-level-service-properties.md | 21 ++++ docs/documentation/agent.md | 67 ++++++++--- docs/documentation/menu.md | 22 ++-- internal/client/agentform.go | 31 +++++ internal/client/agentform_test.go | 18 +++ internal/menu/entries.go | 1 + internal/tui/agentconfigeditor.go | 109 ++++++++++++++++-- internal/tui/agentconfigeditor_test.go | 27 +++++ 11 files changed, 390 insertions(+), 45 deletions(-) diff --git a/cmd/agent.go b/cmd/agent.go index 42ce363..d3d160e 100644 --- a/cmd/agent.go +++ b/cmd/agent.go @@ -7,8 +7,10 @@ import ( "strings" "github.com/jbrazda/iics-cli/internal/client" + "github.com/jbrazda/iics-cli/internal/config" "github.com/jbrazda/iics-cli/internal/filter" "github.com/jbrazda/iics-cli/internal/output" + "github.com/jbrazda/iics-cli/internal/tui" "github.com/spf13/cobra" ) @@ -310,16 +312,25 @@ func newAgentDetailsCmd() *cobra.Command { id, fid, name, hostname string full bool servicesOnly bool + interactive bool ) cmd := &cobra.Command{ Use: "details", Short: "Get agent service engine details", + Long: `Get agent service engine details. + +With --interactive (-i), or without an agent flag in a terminal, pick the +agent from a list, then the view: services summary (--services), details +with service status, full details with configuration (--full), or the +agent's service properties (agent configs get).`, Example: ` iics agent details --id iics agent details --hostname devinfacld01 --full iics agent details --id --services - iics agent details --name "My Agent" --output json`, + iics agent details --name "My Agent" --output json + iics agent details -i`, RunE: func(cmd *cobra.Command, args []string) error { - if id == "" && fid == "" && name == "" && hostname == "" { + noAgent := id == "" && fid == "" && name == "" && hostname == "" + if noAgent && !interactive && !config.IsTerminal() { return fmt.Errorf("one of --id, --fid, --name, or --hostname is required") } c, err := getClient(cmd) @@ -327,10 +338,31 @@ func newAgentDetailsCmd() *cobra.Command { return err } ctx := context.Background() - agentID, err := resolveAgentID(ctx, c, id, name, hostname, fid) - if err != nil { + var agentID string + if noAgent { + picked, pErr := pickAgent(ctx, c) + if pErr != nil || picked == nil { + return pErr + } + agentID = picked.ID + name = picked.Name + } else if agentID, err = resolveAgentID(ctx, c, id, name, hostname, fid); err != nil { return err } + if (interactive || noAgent) && !full && !servicesOnly { + view, vErr := pickAgentDetailsView(agentID, name) + if vErr != nil || view == agentViewNone { + return vErr + } + switch view { + case agentViewServices: + servicesOnly = true + case agentViewFull: + full = true + case agentViewProperties: + return showAgentProperties(ctx, cmd, c, agentID) + } + } details, err := c.GetAgentDetails(ctx, agentID, full) if err != nil { return err @@ -400,6 +432,7 @@ func newAgentDetailsCmd() *cobra.Command { cmd.Flags().StringVar(&hostname, "hostname", "", "agent host name") cmd.Flags().BoolVar(&full, "full", false, "include agent and service configuration properties") cmd.Flags().BoolVar(&servicesOnly, "services", false, "show the agent summary and a single services table (no per-service or config sections)") + cmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "pick the agent (when no agent flag is given) and the view from lists") cmd.MarkFlagsMutuallyExclusive("id", "fid", "name", "hostname") cmd.MarkFlagsMutuallyExclusive("full", "services") return cmd @@ -445,3 +478,59 @@ func newAgentDeleteCmd() *cobra.Command { cmd.MarkFlagsMutuallyExclusive("id", "name", "hostname") return cmd } + +// Views offered by `agent details -i`. +const ( + agentViewNone = iota + agentViewServices + agentViewDetails + agentViewFull + agentViewProperties +) + +// agentView is one entry of the `agent details -i` view list. +type agentView struct { + label string + view int +} + +// pickAgentDetailsView asks which agent details view to show. It returns +// agentViewNone when the user cancels. +func pickAgentDetailsView(agentID, agentName string) (int, error) { + views := []agentView{ + {"Services summary (--services)", agentViewServices}, + {"Details with service status", agentViewDetails}, + {"Full details with configuration (--full)", agentViewFull}, + {"Service properties with defaults (agent configs get)", agentViewProperties}, + } + label := "Show agent " + agentID + if agentName != "" { + label = "Show agent " + agentName + } + idx, err := tui.PickOne(prompter, label, views, func(v agentView) string { return v.label }) + if err != nil || idx < 0 { + return agentViewNone, err + } + return views[idx].view, nil +} + +// showAgentProperties prints the agent's service properties like +// `agent configs get`. +func showAgentProperties(ctx context.Context, cmd *cobra.Command, c *client.Client, agentID string) error { + form, err := c.GetAgentConfigForm(ctx, agentID) + if err != nil { + return err + } + props, err := form.Filter(client.AgentConfigFilter{}) + if err != nil { + return err + } + if outputFmt != "" && outputFmt != "table" { + f, err := getFormatter() + if err != nil { + return err + } + return f.Format(props, agentPropertyCols) + } + return printAgentConfigTables(cmd, form, props) +} diff --git a/cmd/agent_configs.go b/cmd/agent_configs.go index a78b478..5f5f38f 100644 --- a/cmd/agent_configs.go +++ b/cmd/agent_configs.go @@ -46,7 +46,13 @@ var agentPropertyCols = []output.Column{ } return t }}, - {Header: "NAME", Field: "name", Width: 24, Priority: 1, Shrink: output.ShrinkNever}, + // Long names (up to 76 characters on Process Server) wrap inside the + // cell; ShrinkWrap would move the column to the end. + {Header: "NAME", Field: "name", Width: 24, Priority: 1, Shrink: output.ShrinkNever, Func: func(v interface{}) string { + row, _ := v.(map[string]interface{}) + s, _ := row["name"].(string) + return output.WrapCell(s, 36) + }}, {Header: "VALUE", Field: "value", Width: 30, Priority: 1, Func: wrapConfigField("value")}, {Header: "DEFAULT", Field: "defaultValue", Width: 30, Priority: 3, Func: wrapConfigField("defaultValue")}, {Header: "OVERRIDDEN", Field: "overridden", Width: 10, Priority: 2, Shrink: output.ShrinkNever, Func: func(v interface{}) string { @@ -311,7 +317,8 @@ func newAgentConfigsSetCmd() *cobra.Command { id, fid, name, hostname string sets, adds []string resets, removes []string - addMissing bool + addMissing, resetAll bool + resetService string fromFile string dryRun, yes, force bool ) @@ -353,6 +360,7 @@ from the group; the command refuses to change them unless --force is given. iics agent configs set --name CAI-A03 --set "process-engine:jvm:max-heap='4G'" iics agent configs set --name CAI-A03 --set "process-engine:db:connection-properties=@db-props.yaml" iics agent configs set --name CAI-A03 --reset process-engine:jvm:max-heap --yes + iics agent configs set --name CAI-A03 --reset-all --service "Process Server" --dry-run iics agent configs set --name CAI-A03 --from-file changes.yaml iics agent configs get --name CAI-A01 --service process-engine --overridden --export > pe.yaml iics agent configs set --name CAI-A02 --from-file pe.yaml --dry-run`, @@ -364,8 +372,11 @@ from the group; the command refuses to change them unless --force is given. if err != nil { return err } - if len(changes) == 0 { - return fmt.Errorf("nothing to change: use --set, --add, --reset, --remove or --from-file") + if len(changes) == 0 && !resetAll { + return fmt.Errorf("nothing to change: use --set, --add, --reset, --reset-all, --remove or --from-file") + } + if resetService != "" && !resetAll { + return fmt.Errorf("--service requires --reset-all") } c, err := getClient(cmd) if err != nil { @@ -385,6 +396,17 @@ from the group; the command refuses to change them unless --force is given. if err != nil { return err } + if resetAll { + resets, skipped, rErr := form.ResetAllChanges(resetService) + if rErr != nil { + return rErr + } + if skipped > 0 { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Reset all: %d sensitive propert%s left unchanged; reset them one by one with --reset.\n", + skipped, map[bool]string{true: "y", false: "ies"}[skipped == 1]) + } + changes = append(changes, resets...) + } diffs, err := form.ApplyWith(changes, client.ApplyOptions{AddMissing: addMissing}) if err != nil { return err @@ -418,6 +440,8 @@ from the group; the command refuses to change them unless --force is given. cmd.Flags().StringArrayVar(&resets, "reset", nil, "reset a property to its default (removes a custom property): service:type:name; repeatable") cmd.Flags().StringArrayVar(&adds, "add", nil, "add a custom property: service:type:name=value; repeatable") cmd.Flags().StringArrayVar(&removes, "remove", nil, "remove a custom property: service:type:name; repeatable") + cmd.Flags().BoolVar(&resetAll, "reset-all", false, "reset every overridden standard property to its default, like the UI's Reset All (custom and sensitive properties are kept)") + cmd.Flags().StringVar(&resetService, "service", "", "with --reset-all: only this service (name, display name or versioned key)") cmd.Flags().BoolVar(&addMissing, "add-missing", false, "add properties from --set or --from-file that do not exist as custom properties") cmd.Flags().StringVar(&fromFile, "from-file", "", "JSON or YAML change file: service/type/name map (null resets) or list of {property, value, reset}") cmd.Flags().BoolVar(&dryRun, "dry-run", false, "show the changes without applying them") diff --git a/completions/iics.bash b/completions/iics.bash index a103585..1feee41 100644 --- a/completions/iics.bash +++ b/completions/iics.bash @@ -685,6 +685,12 @@ _iics_agent_configs_set() two_word_flags+=("--reset") local_nonpersistent_flags+=("--reset") local_nonpersistent_flags+=("--reset=") + flags+=("--reset-all") + local_nonpersistent_flags+=("--reset-all") + flags+=("--service=") + two_word_flags+=("--service") + local_nonpersistent_flags+=("--service") + local_nonpersistent_flags+=("--service=") flags+=("--set=") two_word_flags+=("--set") local_nonpersistent_flags+=("--set") @@ -843,6 +849,10 @@ _iics_agent_details() two_word_flags+=("--id") local_nonpersistent_flags+=("--id") local_nonpersistent_flags+=("--id=") + flags+=("--interactive") + flags+=("-i") + local_nonpersistent_flags+=("--interactive") + local_nonpersistent_flags+=("-i") flags+=("--name=") two_word_flags+=("--name") local_nonpersistent_flags+=("--name") diff --git a/docs/ChangeRequests/pending/CR-0049-agent-level-service-properties.md b/docs/ChangeRequests/pending/CR-0049-agent-level-service-properties.md index c18c6a4..2f027e4 100644 --- a/docs/ChangeRequests/pending/CR-0049-agent-level-service-properties.md +++ b/docs/ChangeRequests/pending/CR-0049-agent-level-service-properties.md @@ -68,6 +68,18 @@ changed from the CLI. `--reset`) removes it, `--add-missing` adds missing properties from a change file, and `agent configs edit` has **+ Add custom property** and remove. Type labels from the UI (`Tomcat`, `DTM`) are accepted in property names. +- Reset all, like the UI's **Reset All** button (`pnAgent.resetAllSystemConfigs` + resets every standard property; custom rows are kept; the UI disables it + while sensitive properties are set): `set --reset-all [--service ]` + and, in `agent configs edit`, **↺ Reset all to defaults** on the service + list and **↺ Reset all <service> properties to defaults** per service, + with a confirmation listing the properties. Sensitive properties are left + unchanged and reported. Per-property **Reset to default** stays in the + property editor. +- `iics agent details -i` (also without an agent flag in a terminal) picks the + agent from a list, then the view: services summary (`--services`), + details, full details (`--full`) or service properties (`agent configs + get`). Menu entry "Show agent details". - `iics agent rename --id|--fid|--name|--hostname --new-name ` renames an agent, for example from the host name the installer registers to a logical name such as `CAI-A03`. The name is trimmed, at most 100 characters, and must @@ -248,3 +260,12 @@ Form layout (`
`, about 1,000 fields, all submitted on save) and `--remove`, each verified by reading back; then add and remove through `agent configs edit` in a pseudo-terminal. The agent has no custom properties afterwards, as before. +- Reset all was checked on tst CAI-A03 as a dry run (`--reset-all --service + "Process Server"` lists the 14 overrides; CDI-G01-A01 lists + `INFA_MEMORY`) and in the editor through review, then canceled, so the + agent kept its overrides. +- `agent details -i` was driven in a pseudo-terminal on tst: agent list, + view list, services summary and properties views. +- The `NAME` column of `agent configs get` wraps names longer than 36 + characters inside the cell: Process Server has names of up to 76 + characters, which made the table fall back to the vertical layout. diff --git a/docs/documentation/agent.md b/docs/documentation/agent.md index 554ac58..6bc8470 100644 --- a/docs/documentation/agent.md +++ b/docs/documentation/agent.md @@ -108,6 +108,23 @@ Exactly one of `--id` / `--name` is required; they are mutually exclusive. All [global flags](../../README.md#global-flags) apply. +### Interactive mode + +With `--interactive` (`-i`), or without an agent flag in a terminal, the +command lists the agents (name, host, ID; `/` filters) and then asks for the +view: + +| View | Same as | +| -------------------------------------- | ---------------------------------------- | +| Services summary | `agent details --services` | +| Details with service status | `agent details` | +| Full details with configuration | `agent details --full` | +| Service properties with defaults | `agent configs get` | + +With an agent flag, `-i` only asks for the view; `--services` or `--full` +skip the view question. The main menu entry **Show agent details** runs +`agent details -i`. + ### Output In table mode the agent is printed as a vertical `PROPERTY` / `VALUE` listing. @@ -140,17 +157,18 @@ service (engine) with its status and, optionally, its configuration properties. ### Flags -| Flag | Type | Description | -| ------------ | ------ | --------------------------------------------------------------------------------------------------- | -| `--id` | string | Agent ID | -| `--fid` | string | Agent federated ID | -| `--name` | string | Agent name | -| `--hostname` | string | Agent host name | -| `--full` | bool | Include agent-level and per-service configuration properties (adds `onlyStatus=false`) | -| `--services` | bool | Show only the agent summary and one horizontal table of services (mutually exclusive with `--full`) | - -Exactly one of `--id` / `--fid` / `--name` / `--hostname` is required; they are -mutually exclusive. Only `--id` maps to the API directly; `--name` uses the +| Flag | Type | Description | +| --------------------- | ------ | --------------------------------------------------------------------------------------------------- | +| `--id` | string | Agent ID | +| `--fid` | string | Agent federated ID | +| `--name` | string | Agent name | +| `--hostname` | string | Agent host name | +| `--full` | bool | Include agent-level and per-service configuration properties (adds `onlyStatus=false`) | +| `--services` | bool | Show only the agent summary and one horizontal table of services (mutually exclusive with `--full`) | +| `--interactive`, `-i` | bool | Pick the agent (when no agent flag is given) and the view from lists | + +One of `--id` / `--fid` / `--name` / `--hostname` is required, unless the +command runs interactively; they are mutually exclusive. Only `--id` maps to the API directly; `--name` uses the by-name endpoint, and `--fid` / `--hostname` are resolved to an ID by scanning the agent list (with a runtime-environment fallback for `--fid`). @@ -185,6 +203,9 @@ iics agent details --id --services iics agent details --hostname devinfacld01 --full iics agent details --name "My Agent" --output json + +# Pick the agent and the view from lists +iics agent details -i ``` ```powershell @@ -351,6 +372,8 @@ confirmation. | `--add` | string | `service:type:name=value` to add a custom property (`@file` reads the value). Repeatable | | `--remove` | string | `service:type:name` of a custom property to remove. Repeatable | | `--add-missing` | bool | Add properties from `--set` or `--from-file` that do not exist, as custom properties | +| `--reset-all` | bool | Reset every overridden standard property to its default, like the UI's **Reset All** | +| `--service` | string | With `--reset-all`: only this service | | `--from-file` | string | JSON or YAML change file (see below); only the properties in the file change | | `--dry-run` | bool | Show the changes without applying them | | `--yes`, `-y` | bool | Apply without asking for confirmation | @@ -362,6 +385,12 @@ Rules: quoting, for example `'4G'` or `'jdbc:postgresql://host:5432/activevos'`, as `get` shows them. Multi-line values (connection properties, logging) are easiest to pass with `@file`; the file content is used as is. +- `--reset-all` works like the Administrator UI's **Reset All** button: every + overridden standard property of every service (or of `--service`) goes back + to its default. Custom properties are kept. Sensitive properties are left + unchanged (the UI disables Reset All for them) and reported; reset them one + by one with `--reset`. It can be combined with other changes, which apply + on top. Check the list with `--dry-run` first. - Setting a property to its current value changes nothing, so a default does not turn into an override. Use `--reset` to go back to the default. - A property can be changed only once per command. @@ -507,12 +536,13 @@ menu entry **Edit agent properties** runs this command. Screens: 1. **Services** - each service with its number of properties, overridden and - custom properties and pending changes, then **Review and apply** and - **Cancel**. + custom properties and pending changes, then **Review and apply**, + **↺ Reset all to defaults** and **Cancel**. 2. **Properties** of a service - type, name and a value preview. `*` marks an overridden property, `+` a custom property, `✎` a pending change, `✗` a custom property to remove. `/` filters. **+ Add custom property** asks for - the type (from the service's types), name and value. + the type (from the service's types), name and value. **↺ Reset all + <service> properties to defaults** resets this service only. 3. **Property editor**: - One-line values open in an input field. Values stored in YAML single quotes (`'10G'`) are edited without the quotes, which are added back. @@ -526,12 +556,19 @@ Screens: `value`). Unchanged entries keep their exact text. - **Edit as text** edits the whole value as YAML; `ctrl+e` opens it in `$EDITOR`. - - **Reset to default** and **Undo changes to this property**. + - **Reset to default** (shown for overridden properties) sets the value back + to the default, like the reset icon in the UI; **Undo changes to this + property** drops pending edits. - Custom properties: **Edit value**, **Remove custom property** (or **Keep this property** to undo a removal). 4. **Review** - all pending changes; list properties show entry changes (`~` changed, `+` added, `-` removed). **Apply** saves. +Reset all (for all services or one) works like the UI's **Reset All**: it +lists the overridden standard properties it will reset and asks first (the +default answer is **< Back**). Custom properties are kept, sensitive ones are +left unchanged, and nothing is saved until **Apply** in the review. + After applying, the command prints the change table, saves and verifies like `agent configs set`, and lists the restart commands for the changed services. diff --git a/docs/documentation/menu.md b/docs/documentation/menu.md index d7a4c4e..82d21f3 100644 --- a/docs/documentation/menu.md +++ b/docs/documentation/menu.md @@ -118,17 +118,17 @@ On a production profile: ## Entries -| Group | Entries | -| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Session | Switch profile, Set as default profile ✎, Login, List profiles, Add profile ✎, Edit active profile ✎, Delete profile ✎, Edit global settings | -| Users | List users, Show user (search by user name, ID, or part of a name/email), Create user ✎, Edit user groups and roles ✎, Delete user ✎, Change password ✎ | -| User groups | List, Show, Create ✎, Edit roles ✎, Delete ✎ | -| Roles | List, Show (with privileges), Create ✎, Edit privileges ✎ | -| Privileges | List privileges | -| Projects and folders | List projects, List folders, List folders in a project, Edit project ✎, Edit folder ✎ | -| Permissions | Edit object permissions ✎ | -| Environments and agents | List runtime environments, Create runtime environment ✎, List agents, Start / Stop / Restart agent service ✎, Edit agent properties ✎, Show agent installer info, Download agent installer (`--interactive --progress`) | -| Help | Show CLI help | +| Group | Entries | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Session | Switch profile, Set as default profile ✎, Login, List profiles, Add profile ✎, Edit active profile ✎, Delete profile ✎, Edit global settings | +| Users | List users, Show user (search by user name, ID, or part of a name/email), Create user ✎, Edit user groups and roles ✎, Delete user ✎, Change password ✎ | +| User groups | List, Show, Create ✎, Edit roles ✎, Delete ✎ | +| Roles | List, Show (with privileges), Create ✎, Edit privileges ✎ | +| Privileges | List privileges | +| Projects and folders | List projects, List folders, List folders in a project, Edit project ✎, Edit folder ✎ | +| Permissions | Edit object permissions ✎ | +| Environments and agents | List runtime environments, Create runtime environment ✎, List agents, Show agent details, Start / Stop / Restart agent service ✎, Edit agent properties ✎, Show agent installer info, Download agent installer (`--interactive --progress`) | +| Help | Show CLI help | ## Accessible mode and small terminals diff --git a/internal/client/agentform.go b/internal/client/agentform.go index 111d124..f6e55db 100644 --- a/internal/client/agentform.go +++ b/internal/client/agentform.go @@ -799,3 +799,34 @@ func (c *Client) RenameAgent(ctx context.Context, agentID, newName string) (*Age } return agent, nil } + +// ResetAllChanges returns reset changes for every overridden standard +// property, like the Administrator UI's Reset All button. service limits it +// to one service (name, versioned key or display name); empty means all. +// Custom properties, the agent's own (SystemService) properties and +// sensitive properties are left alone; skipped counts the sensitive ones. +func (f *AgentConfigForm) ResetAllChanges(service string) (changes []AgentConfigChange, skipped int, err error) { + found := service == "" + for _, p := range f.Properties { + if p.Custom || p.removed || p.Service == SystemService { + continue + } + if service != "" && !strings.EqualFold(service, p.Service) && !strings.EqualFold(service, p.serviceKey) && + !strings.EqualFold(service, p.ServiceName) { + continue + } + found = true + if !p.Overridden { + continue + } + if p.Sensitive { + skipped++ + continue + } + changes = append(changes, AgentConfigChange{Property: p.Key(), Reset: true}) + } + if !found { + return nil, 0, fmt.Errorf("service %q not found on agent %s", service, f.AgentName) + } + return changes, skipped, nil +} diff --git a/internal/client/agentform_test.go b/internal/client/agentform_test.go index 13b0ee0..1eeb40d 100644 --- a/internal/client/agentform_test.go +++ b/internal/client/agentform_test.go @@ -352,3 +352,21 @@ func TestRenameAgent(t *testing.T) { } } } + +func TestResetAllChanges(t *testing.T) { + form, err := parseAgentConfigForm(agentPage("tok", "false", "")) + if err != nil { + t.Fatal(err) + } + form.Set(2, "false") // process-engine:cluster:primary-node, now overridden + changes, skipped, err := form.ResetAllChanges("") + if err != nil || len(changes) != 1 || changes[0].Property != "process-engine:cluster:primary-node" || !changes[0].Reset || skipped != 1 { + t.Errorf("ResetAllChanges = %+v, %d, %v", changes, skipped, err) + } + if changes, _, _ = form.ResetAllChanges("OI Data Collector"); len(changes) != 0 { + t.Errorf("service filter: %+v", changes) + } + if _, _, err = form.ResetAllChanges("nope"); err == nil { + t.Error("expected an error for an unknown service") + } +} diff --git a/internal/menu/entries.go b/internal/menu/entries.go index 3b5c7c2..e4d3691 100644 --- a/internal/menu/entries.go +++ b/internal/menu/entries.go @@ -107,6 +107,7 @@ func DefaultEntries() []Entry { {Group: "Environments and agents", Label: "List runtime environments", Description: "Show runtime environments (Secure Agent groups)", Args: []string{"environment", "list"}}, {Group: "Environments and agents", Label: "Create runtime environment", Description: "Create a runtime environment and add agents", Args: []string{"environment", "create", "-i"}, Writes: true}, {Group: "Environments and agents", Label: "List agents", Description: "Show Secure Agents", Args: []string{"agent", "list"}}, + {Group: "Environments and agents", Label: "Show agent details", Description: "Pick an agent, then show its services summary, details, full details with configuration, or service properties", Args: []string{"agent", "details", "-i"}}, {Group: "Environments and agents", Label: "Start agent service", Description: "Pick an agent and start a service", Args: []string{"agent", "start", "-i"}, Writes: true}, {Group: "Environments and agents", Label: "Stop agent service", Description: "Pick an agent and stop a service", Args: []string{"agent", "stop", "-i"}, Writes: true}, {Group: "Environments and agents", Label: "Restart agent service", Description: "Pick an agent and restart a service", Args: []string{"agent", "restart", "-i"}, Writes: true}, diff --git a/internal/tui/agentconfigeditor.go b/internal/tui/agentconfigeditor.go index 2681bfc..9451b22 100644 --- a/internal/tui/agentconfigeditor.go +++ b/internal/tui/agentconfigeditor.go @@ -29,17 +29,18 @@ type agentConfigEditor struct { } const ( - choiceBack = "\x00back" - choiceAdd = "\x00add" - choiceText = "\x00text" - choiceReset = "\x00reset" - choiceUndo = "\x00undo" - choiceEdit = "\x00edit" - choiceRemove = "\x00remove" - choiceApply = "\x00apply" - choiceCustom = "\x00custom" - previewWidth = 44 - maxListHeight = 22 + choiceBack = "\x00back" + choiceAdd = "\x00add" + choiceText = "\x00text" + choiceReset = "\x00reset" + choiceUndo = "\x00undo" + choiceEdit = "\x00edit" + choiceRemove = "\x00remove" + choiceApply = "\x00apply" + choiceCustom = "\x00custom" + choiceResetAll = "\x00resetall" + previewWidth = 44 + maxListHeight = 22 ) // EditAgentConfigs runs the interactive agent property editor and returns @@ -63,6 +64,11 @@ func EditAgentConfigs(props []client.AgentConfigProperty, opts AgentConfigEditor switch choice { case choiceCancel: return nil, ErrCanceled + case choiceResetAll: + if err := e.resetAll(""); err != nil { + return nil, err + } + continue case choiceReview: ok, err := e.review() if err != nil { @@ -183,6 +189,7 @@ func (e *agentConfigEditor) serviceOptions() []huh.Option[string] { } return append(opts, huh.NewOption("> Review and apply", choiceReview), + huh.NewOption("↺ Reset all to defaults", choiceResetAll), huh.NewOption("x Cancel", choiceCancel)) } @@ -206,6 +213,7 @@ func (e *agentConfigEditor) editService(service string) error { if len(e.customTypes[service]) > 0 { opts = append(opts, huh.NewOption("+ Add custom property", choiceCustom)) } + opts = append(opts, huh.NewOption("↺ Reset all "+e.props[idx[0]].ServiceName+" properties to defaults", choiceResetAll)) for _, i := range idx { p := e.props[i] v, over := e.value(i) @@ -238,6 +246,12 @@ func (e *agentConfigEditor) editService(service string) error { return err } last = choice + if choice == choiceResetAll { + if err := e.resetAll(service); err != nil { + return err + } + continue + } if choice == choiceCustom { if err := e.addCustom(service, e.props[idx[0]].ServiceName); err != nil { return err @@ -627,3 +641,76 @@ func (e *agentConfigEditor) addCustom(service, serviceName string) error { e.pending[p.Key()] = client.AgentConfigChange{Property: p.Key(), Value: value, Add: true} return nil } + +// resetTargets returns the properties Reset all changes for service (all +// services when empty): standard properties that are overridden or have a +// pending edit. Sensitive ones are counted in skipped and left alone, like +// the Administrator UI, which disables Reset All for them. +func (e *agentConfigEditor) resetTargets(service string) (targets []int, skipped int) { + for i, p := range e.props { + if p.Custom || service != "" && p.Service != service { + continue + } + _, pending := e.pending[p.Key()] + if !p.Overridden && !pending { + continue + } + if p.Sensitive { + skipped++ + continue + } + targets = append(targets, i) + } + return targets, skipped +} + +// resetAll asks for confirmation and marks the targets for reset, like the +// Administrator UI's Reset All button. Custom properties are kept. +func (e *agentConfigEditor) resetAll(service string) error { + targets, skipped := e.resetTargets(service) + scope := "all services" + if service != "" { + for _, p := range e.props { + if p.Service == service { + scope = p.ServiceName + break + } + } + } + var b strings.Builder + if len(targets) == 0 { + fmt.Fprintf(&b, "No overridden properties in %s.", scope) + } else { + fmt.Fprintf(&b, "Reset %d propert%s in %s to their defaults:\n", len(targets), map[bool]string{true: "y", false: "ies"}[len(targets) == 1], scope) + for n, i := range targets { + if n == 15 { + fmt.Fprintf(&b, " ... and %d more\n", len(targets)-n) + break + } + p := e.props[i] + fmt.Fprintf(&b, " %s › %s › %s\n", p.ServiceName, p.Type, p.Name) + } + b.WriteString("Custom properties are kept. Pending edits of these properties are replaced.") + } + if skipped > 0 { + fmt.Fprintf(&b, "\n%d sensitive propert%s left unchanged; reset them one by one.", skipped, map[bool]string{true: "y", false: "ies"}[skipped == 1]) + } + opts := []huh.Option[string]{huh.NewOption("< Back", choiceBack)} + if len(targets) > 0 { + opts = append([]huh.Option[string]{huh.NewOption(fmt.Sprintf("Reset %d propert%s", len(targets), map[bool]string{true: "y", false: "ies"}[len(targets) == 1]), choiceResetAll)}, opts...) + } + choice := choiceBack + sel := huh.NewSelect[string](). + Title(e.title + " - reset all"). + Description(strings.TrimRight(b.String(), "\n")). + Options(opts...). + Value(&choice) + ok, err := back(huh.NewForm(huh.NewGroup(sel))) + if err != nil || !ok || choice != choiceResetAll { + return err + } + for _, i := range targets { + e.record(i, client.AgentConfigChange{Reset: true}) + } + return nil +} diff --git a/internal/tui/agentconfigeditor_test.go b/internal/tui/agentconfigeditor_test.go index 8a6ee75..04d1208 100644 --- a/internal/tui/agentconfigeditor_test.go +++ b/internal/tui/agentconfigeditor_test.go @@ -177,3 +177,30 @@ func TestAgentConfigEditorCustom(t *testing.T) { t.Errorf("changes() = %+v", got) } } + +func TestAgentConfigEditorResetTargets(t *testing.T) { + e := &agentConfigEditor{ + props: []client.AgentConfigProperty{ + {Service: "process-engine", Type: "jvm", Name: "max-heap", Value: "'10G'", DefaultValue: "'1536M'", Overridden: true}, + {Service: "process-engine", Type: "cluster", Name: "enabled", Value: "false", DefaultValue: "false"}, + {Service: "process-engine", Type: "server", Name: "key-store-password", Value: "x", Overridden: true, Sensitive: true}, + {Service: "process-engine", Type: "jvm", Name: "custom1", Value: "1", Custom: true}, + {Service: "OpsInsightsDataCollector", Type: "OpsInsights", Name: "maxBufferSize", Value: "512", DefaultValue: "512"}, + }, + pending: map[string]client.AgentConfigChange{}, + } + e.record(4, client.AgentConfigChange{Value: "1024"}) // pending edit on a default property + targets, skipped := e.resetTargets("") + if len(targets) != 2 || targets[0] != 0 || targets[1] != 4 || skipped != 1 { + t.Errorf("resetTargets(all) = %v, %d", targets, skipped) + } + if targets, _ = e.resetTargets("process-engine"); len(targets) != 1 || targets[0] != 0 { + t.Errorf("resetTargets(process-engine) = %v", targets) + } + for _, i := range []int{0, 4} { + e.record(i, client.AgentConfigChange{Reset: true}) + } + if ch := e.changes(); len(ch) != 1 || ch[0].Property != "process-engine:jvm:max-heap" || !ch[0].Reset { + t.Errorf("after reset all: %+v", ch) + } +} From 407eb19993941e04bb9d29644f88bca0fbc18da5 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Tue, 20 Oct 2026 12:53:13 -0400 Subject: [PATCH 43/52] docs: add asciinema demo of key CLI features to README Recorded against a test org; host names, user names, the organization name and the org ID are masked. --- README.md | 9 + docs/demo/iics-demo.cast | 905 +++++++++++++++++++++++++++++++++++++++ docs/demo/iics-demo.gif | Bin 0 -> 1384781 bytes 3 files changed, 914 insertions(+) create mode 100644 docs/demo/iics-demo.cast create mode 100644 docs/demo/iics-demo.gif diff --git a/README.md b/README.md index 95a859c..5d31332 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,15 @@ A comprehensive command-line interface for the [Informatica Intelligent Cloud Services (IICS)](https://www.informatica.com/products/cloud-integration.html) platform REST API v3. Manage assets, users, connections, export/import packages, and more - from your terminal or CI/CD pipelines. +## Demo + +![iics demo: profiles, assets, agents, agent service properties, JSON output, the property editor and the main menu](docs/demo/iics-demo.gif) + +The demo shows profiles, asset listing, Secure Agents and their services, per-agent service +properties with a dry-run change, JSON output for scripts, the interactive property editor and the +main menu. Host names, user names and the organization name are masked. To replay it in a terminal, +run `asciinema play docs/demo/iics-demo.cast`. + ## Features - **Full API v3 coverage** - objects, connections, schedules, export/import, users, roles, permissions, runtime environments, agents, tags, source control, and more diff --git a/docs/demo/iics-demo.cast b/docs/demo/iics-demo.cast new file mode 100644 index 0000000..f2e64ff --- /dev/null +++ b/docs/demo/iics-demo.cast @@ -0,0 +1,905 @@ +{"version": 2, "width": 140, "height": 34, "timestamp": 1792514405, "idle_time_limit": 3, "title": "iics - Informatica Cloud CLI", "env": {"TERM": "xterm-256color", "SHELL": "/bin/bash"}} +[0.05, "o", "\u001b[1;36m~/demo\u001b[0m \u001b[1;32m\u276f\u001b[0m "] +[0.05, "o", "\u001b[2m"] +[0.0857, "o", "#"] +[0.1197, "o", " "] +[0.1509, "o", "o"] +[0.1761, "o", "n"] +[0.2039, "o", "e"] +[0.2378, "o", " "] +[0.2818, "o", "p"] +[0.3133, "o", "r"] +[0.3395, "o", "o"] +[0.3727, "o", "f"] +[0.4023, "o", "i"] +[0.4381, "o", "l"] +[0.4702, "o", "e"] +[0.5029, "o", " "] +[0.5368, "o", "p"] +[0.5614, "o", "e"] +[0.5829, "o", "r"] +[0.6241, "o", " "] +[0.651, "o", "o"] +[0.6711, "o", "r"] +[0.7152, "o", "g"] +[0.7513, "o", ";"] +[0.7797, "o", " "] +[0.8049, "o", "p"] +[0.8332, "o", "a"] +[0.8653, "o", "s"] +[0.9076, "o", "s"] +[0.9388, "o", "w"] +[0.9688, "o", "o"] +[1.0081, "o", "r"] +[1.044, "o", "d"] +[1.0664, "o", "s"] +[1.0901, "o", " "] +[1.1308, "o", "s"] +[1.1582, "o", "t"] +[1.1788, "o", "a"] +[1.2124, "o", "y"] +[1.2497, "o", " "] +[1.2917, "o", "i"] +[1.3355, "o", "n"] +[1.3634, "o", " "] +[1.3873, "o", "t"] +[1.4102, "o", "h"] +[1.4414, "o", "e"] +[1.4838, "o", " "] +[1.5249, "o", "O"] +[1.5697, "o", "S"] +[1.5922, "o", " "] +[1.6183, "o", "k"] +[1.6498, "o", "e"] +[1.6834, "o", "y"] +[1.7214, "o", "c"] +[1.75, "o", "h"] +[1.7769, "o", "a"] +[1.8054, "o", "i"] +[1.8413, "o", "n"] +[2.3413, "o", "\u001b[0m\r\n"] +[2.3913, "o", "\u001b[1;36m~/demo\u001b[0m \u001b[1;32m\u276f\u001b[0m "] +[2.8467, "o", "i"] +[2.8851, "o", "i"] +[2.9601, "o", "c"] +[3.021, "o", "s"] +[3.0729, "o", " "] +[3.1252, "o", "p"] +[3.1683, "o", "r"] +[3.2246, "o", "o"] +[3.2822, "o", "f"] +[3.343, "o", "i"] +[3.3766, "o", "l"] +[3.4263, "o", "e"] +[3.4833, "o", " "] +[3.529, "o", "l"] +[3.5844, "o", "i"] +[3.6268, "o", "s"] +[3.6599, "o", "t"] +[4.0099, "o", "\r\n"] +[4.0172, "o", "\u001b]11;?\u001b\\\u001b[6n"] +[4.0186, "o", "\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\r\n\u2502 \u001b[1;36mNAME\u001b[0m \u2502 \u001b[1;36mUSERNAME \u001b[0m \u2502 \u001b[1;36mENDPOINT \u001b[0m \u2502 \u001b[1;36mPOD \u001b[0m \u2502 \u001b[1;36mDEFAULT \u001b[0m \u2502 \u001b[1;36mKEYCHAIN\u001b[0m \u2502\r\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\r\n\u2502 dev \u2502 alexandra.smith.dev@acme.com \u2502 https://dm-us.informaticacloud.com/saas/public/core/v3/login \u2502 USE4 \u2502 yes \u2502 yes \u2502\r\n"] +[4.0187, "o", "\u2502 prd \u2502 alexandra.smith.prod@acme.com \u2502 https://dm-us.informaticacloud.com/saas/public/core/v3/login \u2502 USE4 \u2502 \u2502 yes \u2502\r\n\u2502 qa \u2502 alexandra.smith.qa@acme.com \u2502 https://dm-us.informaticacloud.com/saas/public/core/v3/login \u2502 USE4 \u2502 \u2502 yes \u2502\r\n\u2502 stg \u2502 alexandra.smith.stg@acme.com \u2502 https://dm-us.informaticacloud.com/saas/public/core/v3/login \u2502 USE4 \u2502 \u2502 yes \u2502\r\n\u2502 tst \u2502 alexandra.smith.tst@acme.com \u2502 https://dm-us.informaticacloud.com/saas/public/core/v3/login \u2502 USE4 \u2502 \u2502 yes \u2502\r\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\r\n5 rows\r\n"] +[6.5687, "o", "\u001b[1;36m~/demo\u001b[0m \u001b[1;32m\u276f\u001b[0m "] +[6.5687, "o", "\u001b[2m"] +[6.6116, "o", "#"] +[6.648, "o", " "] +[6.6835, "o", "b"] +[6.7153, "o", "r"] +[6.7464, "o", "o"] +[6.7798, "o", "w"] +[6.8199, "o", "s"] +[6.8443, "o", "e"] +[6.8701, "o", " "] +[6.9063, "o", "a"] +[6.9386, "o", "s"] +[6.9709, "o", "s"] +[7.011, "o", "e"] +[7.0375, "o", "t"] +[7.0734, "o", "s"] +[7.5734, "o", "\u001b[0m\r\n"] +[7.6234, "o", "\u001b[1;36m~/demo\u001b[0m \u001b[1;32m\u276f\u001b[0m "] +[8.0666, "o", "i"] +[8.1194, "o", "i"] +[8.1743, "o", "c"] +[8.2472, "o", "s"] +[8.3037, "o", " "] +[8.3812, "o", "o"] +[8.4348, "o", "b"] +[8.4739, "o", "j"] +[8.5335, "o", "e"] +[8.6011, "o", "c"] +[8.6384, "o", "t"] +[8.6798, "o", "s"] +[8.7521, "o", " "] +[8.8038, "o", "l"] +[8.8703, "o", "i"] +[8.9396, "o", "s"] +[9.0155, "o", "t"] +[9.0584, "o", " "] +[9.1153, "o", "-"] +[9.1841, "o", "-"] +[9.2431, "o", "t"] +[9.2801, "o", "y"] +[9.3463, "o", "p"] +[9.4097, "o", "e"] +[9.4842, "o", " "] +[9.521, "o", "P"] +[9.5677, "o", "r"] +[9.642, "o", "o"] +[9.7096, "o", "j"] +[9.7846, "o", "e"] +[9.8151, "o", "c"] +[9.8923, "o", "t"] +[9.9581, "o", " "] +[10.0017, "o", "-"] +[10.0812, "o", "-"] +[10.1525, "o", "l"] +[10.2059, "o", "i"] +[10.239, "o", "m"] +[10.2729, "o", "i"] +[10.339, "o", "t"] +[10.3745, "o", " "] +[10.4173, "o", "8"] +[10.7673, "o", "\r\n"] +[10.7745, "o", "\u001b]11;?\u001b\\\u001b[6n"] +[10.971, "o", "\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\r\n\u2502 \u001b[1;36mID \u001b[0m \u2502 \u001b[1;36mPATH \u001b[0m \u2502 \u001b[1;36mTYPE \u001b[0m \u2502 \u001b[1;36mUPDATED BY \u001b[0m \u2502 \u001b[1;36mUPDATED \u001b[0m \u2502\r\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\r\n\u2502 aVsmaaaqXLedBiyGKt0VKm \u2502 Ad-Hoc-Utilities \u2502 Project \u2502 devops.automation@acme.com.tst \u2502 2026-07-22T16:38:42Z \u2502\r\n\u2502 4v9NEUt2qXUdUpD4Skp3Qq \u2502 Add-On Bundles \u2502 Project \u2502 (Informatica) \u2502 2024-11-26T17:05:58Z \u2502\r\n\u2502 5sxuxvUkzAzgxviLFvzw8B \u2502 Alerting \u2502 Project \u2502 alexandra.smith.tst@acme.com \u2502 2025-06-11T18:45:04Z \u2502\r\n\u2502 7hP4XfGVgL1gAROTXpKPG1 \u2502 BillingCenter \u2502 Project \u2502 devops.automation@acme.com.tst \u2502 2026-07-07T19:48:34Z \u2502\r\n\u2502 7jxYrSb2K7LlCndk3Mp7UL \u2502 ClaimCenter_Conversion \u2502 Project \u2502 alexandra.smith.tst@acme.com \u2502 2024-12-13T20:23:18Z \u2502\r\n\u2502 0AyoKN5WqWYhuhPX9TLF45 \u2502 ClaimCenter_GW \u2502 Project \u2502 alexandra.smith.tst@acme.com \u2502 2025-01-08T15:39:18Z \u2502\r\n\u2502 3mWf8DgABdxcJK2ASzk3IM \u2502 Connections \u2502 Project \u2502 alexandra.smith.tst@acme.com \u2502 2025-01-08T15:39:18Z \u2502\r\n\u2502 ax6sEFs6lDXktxZx6DBWmr \u2502 Connectors \u2502 Project \u2502 alexandra.smith.tst@acme.com \u2502 2025-02-06T13:47:55Z \u2502\r\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\r\n8 rows\r\n"] +[13.521, "o", "\u001b[1;36m~/demo\u001b[0m \u001b[1;32m\u276f\u001b[0m "] +[13.521, "o", "\u001b[2m"] +[13.5448, "o", "#"] +[13.578, "o", " "] +[13.6043, "o", "s"] +[13.6312, "o", "e"] +[13.6691, "o", "c"] +[13.7006, "o", "u"] +[13.7381, "o", "r"] +[13.772, "o", "e"] +[13.8115, "o", " "] +[13.8332, "o", "a"] +[13.8764, "o", "g"] +[13.9131, "o", "e"] +[13.9541, "o", "n"] +[13.9819, "o", "t"] +[14.0066, "o", "s"] +[14.0311, "o", " "] +[14.0614, "o", "a"] +[14.0898, "o", "n"] +[14.1131, "o", "d"] +[14.1526, "o", " "] +[14.1742, "o", "t"] +[14.2192, "o", "h"] +[14.2428, "o", "e"] +[14.2806, "o", " "] +[14.3044, "o", "s"] +[14.3293, "o", "e"] +[14.3504, "o", "r"] +[14.3884, "o", "v"] +[14.4259, "o", "i"] +[14.4627, "o", "c"] +[14.5048, "o", "e"] +[14.5361, "o", "s"] +[14.5626, "o", " "] +[14.6062, "o", "t"] +[14.6504, "o", "h"] +[14.6926, "o", "e"] +[14.7164, "o", "y"] +[14.7604, "o", " "] +[14.7982, "o", "r"] +[14.8244, "o", "u"] +[14.8559, "o", "n"] +[15.3559, "o", "\u001b[0m\r\n"] +[15.4059, "o", "\u001b[1;36m~/demo\u001b[0m \u001b[1;32m\u276f\u001b[0m "] +[15.8524, "o", "i"] +[15.9027, "o", "i"] +[15.9804, "o", "c"] +[16.0371, "o", "s"] +[16.0687, "o", " "] +[16.1276, "o", "a"] +[16.1952, "o", "g"] +[16.245, "o", "e"] +[16.299, "o", "n"] +[16.3776, "o", "t"] +[16.4373, "o", " "] +[16.4757, "o", "l"] +[16.5519, "o", "i"] +[16.5908, "o", "s"] +[16.6557, "o", "t"] +[17.0057, "o", "\r\n"] +[17.0128, "o", "\u001b]11;?\u001b\\\u001b[6n"] +[17.1415, "o", "\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\r\n\u2502 \u001b[1;36mNAME \u001b[0m \u2502 \u001b[1;36mHOST \u001b[0m \u2502 \u001b[1;36mACTIVE \u001b[0m \u2502 \u001b[1;36mREADY \u001b[0m \u2502 \u001b[1;36mPLATFORM \u001b[0m \u2502 \u001b[1;36mVERSION \u001b[0m \u2502\r\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\r\n\u2502 CAI-A01 \u2502 agent-host-02 \u2502 \u001b[1;32myes\u001b[0m \u2502 \u001b[1;32myes\u001b[0m \u2502 win64 \u2502 76.17 \u2502\r\n\u2502 CAI-A02 \u2502 agent-host-06 \u2502 \u001b[1;32myes\u001b[0m \u2502 \u001b[1;32myes\u001b[0m \u2502 win64 \u2502 76.17 \u2502\r\n\u2502 CAI-A03 \u2502 agent-host-07 \u2502 \u001b[1;32myes\u001b[0m \u2502 \u001b[1;32myes\u001b[0m \u2502 win64 \u2502 76.17 \u2502\r\n"] +[17.1416, "o", "\u2502 CDI-G01-A01 \u2502 agent-host-04 \u2502 \u001b[1;32myes\u001b[0m \u2502 \u001b[1;32myes\u001b[0m \u2502 win64 \u2502 76.17 \u2502\r\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\r\n4 rows\r\n# 2 column(s) hidden (ID, GROUP ID) - use --wide or -o json\r\n"] +[19.6916, "o", "\u001b[1;36m~/demo\u001b[0m \u001b[1;32m\u276f\u001b[0m "] +[20.1257, "o", "i"] +[20.1753, "o", "i"] +[20.2194, "o", "c"] +[20.2697, "o", "s"] +[20.3051, "o", " "] +[20.3673, "o", "a"] +[20.4339, "o", "g"] +[20.5111, "o", "e"] +[20.5587, "o", "n"] +[20.6092, "o", "t"] +[20.6634, "o", " "] +[20.6954, "o", "d"] +[20.7629, "o", "e"] +[20.7952, "o", "t"] +[20.8349, "o", "a"] +[20.8908, "o", "i"] +[20.9458, "o", "l"] +[21.0153, "o", "s"] +[21.0516, "o", " "] +[21.113, "o", "-"] +[21.1561, "o", "-"] +[21.1928, "o", "n"] +[21.2316, "o", "a"] +[21.2821, "o", "m"] +[21.357, "o", "e"] +[21.4274, "o", " "] +[21.4774, "o", "C"] +[21.5466, "o", "A"] +[21.5857, "o", "I"] +[21.6257, "o", "-"] +[21.6646, "o", "A"] +[21.728, "o", "0"] +[21.7945, "o", "3"] +[21.8394, "o", " "] +[21.9181, "o", "-"] +[21.9597, "o", "-"] +[22.023, "o", "s"] +[22.0806, "o", "e"] +[22.1256, "o", "r"] +[22.2036, "o", "v"] +[22.2599, "o", "i"] +[22.3387, "o", "c"] +[22.3911, "o", "e"] +[22.4459, "o", "s"] +[22.7959, "o", "\r\n"] +[22.8033, "o", "\u001b]11;?\u001b\\\u001b[6n"] +[22.9561, "o", "Agent:\r\n\r\n"] +[22.9562, "o", "\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\r\n"] +[22.9562, "o", "\u2502 \u001b[1;36mPROPERTY \u001b[0m \u2502 \u001b[1;36mVALUE \u001b[0m \u2502\r\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\r\n\u2502 id \u2502 0ACME00800000000008G \u2502\r\n\u2502 orgId \u2502 0ACME0 \u2502\r\n\u2502 name \u2502 CAI-A03 \u2502\r\n\u2502 federatedId \u2502 \u2502\r\n\u2502 active \u2502 true \u2502\r\n\u2502 readyToRun \u2502 false \u2502\r\n"] +[22.9563, "o", "\u2502 platform \u2502 win64 \u2502\r\n\u2502 agentHost \u2502 agent-host-07 \u2502\r\n\u2502 agentVersion \u2502 76.17 \u2502\r\n\u2502 upgradeStatus \u2502 NotUpgrading \u2502\r\n\u2502 lastUpgraded \u2502 2026-09-28T16:21:01.000Z \u2502\r\n\u2502 lastUpgradeCheck \u2502 2026-10-20T16:38:49.000Z \u2502\r\n\u2502 lastStatusChange \u2502 2026-10-20T04:31:34.000Z \u2502\r\n"] +[22.9563, "o", "\u2502 configUpdateTime \u2502 2026-10-20T04:27:10.000Z \u2502\r\n\u2502 agentGroupId \u2502 \u2502\r\n\u2502 createdBy \u2502 admin \u2502\r\n\u2502 createTime \u2502 2026-09-28T16:20:59.000Z \u2502\r\n\u2502 updatedBy \u2502 admin \u2502\r\n\u2502 updateTime \u2502 2026-10-20T16:38:49.000Z \u2502\r\n\u2502 platformAgent \u2502 true \u2502\r\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\r\n20 rows\r\n"] +[22.9563, "o", "\r\nServices (2):\r\n"] +[22.9563, "o", "\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\r\n"] +[22.9564, "o", "\u2502 \u001b[1;36mSERVICE \u001b[0m \u2502 \u001b[1;36mAPP NAME \u001b[0m \u2502 \u001b[1;36mVERSION \u001b[0m \u2502 \u001b[1;36mSTATUS \u001b[0m \u2502 \u001b[1;36mDESIRED \u001b[0m \u2502 \u001b[1;36mSUBSTATE \u001b[0m \u2502 \u001b[1;36mREPLACE \u001b[0m \u2502 \u001b[1;36mUPDATED \u001b[0m \u2502\r\n"] +[22.9564, "o", "\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\r\n\u2502 OI Data Collector \u2502 OpsInsightsDa\u2026 \u2502 25.10 \u2502 RUNNING \u2502 RUNNING \u2502 0 \u2502 REPLACE \u2502 2026-10-20T04\u2026 \u2502\r\n"] +[22.9564, "o", "\u2502 Process Server \u2502 process-engine \u2502 15124581.9 \u2502 RUNNING \u2502 RUNNING \u2502 0 \u2502 REPLACE_VERSION \u2502 2026-10-20T03\u2026 \u2502\r\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\r\n2 rows\r\n"] +[26.5064, "o", "\u001b[1;36m~/demo\u001b[0m \u001b[1;32m\u276f\u001b[0m "] +[26.5064, "o", "\u001b[2m"] +[26.5324, "o", "#"] +[26.5545, "o", " "] +[26.5882, "o", "s"] +[26.6189, "o", "e"] +[26.6456, "o", "r"] +[26.6667, "o", "v"] +[26.6906, "o", "i"] +[26.7308, "o", "c"] +[26.7736, "o", "e"] +[26.8054, "o", " "] +[26.8437, "o", "p"] +[26.8858, "o", "r"] +[26.9186, "o", "o"] +[26.9536, "o", "p"] +[26.9798, "o", "e"] +[27.0045, "o", "r"] +[27.0425, "o", "t"] +[27.0678, "o", "i"] +[27.0995, "o", "e"] +[27.1343, "o", "s"] +[27.1619, "o", " "] +[27.2069, "o", "o"] +[27.2307, "o", "f"] +[27.2601, "o", " "] +[27.2823, "o", "a"] +[27.3117, "o", " "] +[27.3444, "o", "s"] +[27.379, "o", "i"] +[27.4162, "o", "n"] +[27.444, "o", "g"] +[27.4889, "o", "l"] +[27.5284, "o", "e"] +[27.5526, "o", " "] +[27.5917, "o", "a"] +[27.6274, "o", "g"] +[27.6693, "o", "e"] +[27.7059, "o", "n"] +[27.7457, "o", "t"] +[27.7783, "o", ","] +[27.8167, "o", " "] +[27.8588, "o", "n"] +[27.8843, "o", "o"] +[27.9288, "o", "t"] +[27.9517, "o", " "] +[27.9837, "o", "o"] +[28.0268, "o", "n"] +[28.0518, "o", "l"] +[28.091, "o", "y"] +[28.1132, "o", " "] +[28.1538, "o", "g"] +[28.1773, "o", "r"] +[28.2012, "o", "o"] +[28.2326, "o", "u"] +[28.2558, "o", "p"] +[28.2884, "o", " "] +[28.3146, "o", "d"] +[28.3577, "o", "e"] +[28.391, "o", "f"] +[28.4268, "o", "a"] +[28.457, "o", "u"] +[28.4799, "o", "l"] +[28.5023, "o", "t"] +[28.5431, "o", "s"] +[29.0431, "o", "\u001b[0m\r\n"] +[29.0931, "o", "\u001b[1;36m~/demo\u001b[0m \u001b[1;32m\u276f\u001b[0m "] +[29.5265, "o", "i"] +[29.5945, "o", "i"] +[29.6467, "o", "c"] +[29.68, "o", "s"] +[29.7354, "o", " "] +[29.8024, "o", "a"] +[29.8546, "o", "g"] +[29.8905, "o", "e"] +[29.9565, "o", "n"] +[30.0106, "o", "t"] +[30.0708, "o", " "] +[30.1309, "o", "c"] +[30.1788, "o", "o"] +[30.2547, "o", "n"] +[30.3262, "o", "f"] +[30.3928, "o", "i"] +[30.4707, "o", "g"] +[30.522, "o", "s"] +[30.5686, "o", " "] +[30.6418, "o", "g"] +[30.7025, "o", "e"] +[30.7647, "o", "t"] +[30.8239, "o", " "] +[30.8952, "o", "-"] +[30.9611, "o", "-"] +[30.9996, "o", "n"] +[31.0541, "o", "a"] +[31.1321, "o", "m"] +[31.1826, "o", "e"] +[31.2358, "o", " "] +[31.3034, "o", "C"] +[31.3548, "o", "A"] +[31.3859, "o", "I"] +[31.4457, "o", "-"] +[31.5006, "o", "A"] +[31.5804, "o", "0"] +[31.6202, "o", "3"] +[31.6659, "o", " "] +[31.72, "o", "-"] +[31.792, "o", "-"] +[31.8543, "o", "s"] +[31.8931, "o", "e"] +[31.9624, "o", "r"] +[32.0034, "o", "v"] +[32.0389, "o", "i"] +[32.1077, "o", "c"] +[32.172, "o", "e"] +[32.2058, "o", " "] +[32.2516, "o", "'"] +[32.3095, "o", "P"] +[32.3426, "o", "r"] +[32.381, "o", "o"] +[32.4198, "o", "c"] +[32.4722, "o", "e"] +[32.5426, "o", "s"] +[32.6148, "o", "s"] +[32.6752, "o", " "] +[32.7548, "o", "S"] +[32.7922, "o", "e"] +[32.8398, "o", "r"] +[32.8912, "o", "v"] +[32.9584, "o", "e"] +[33.0057, "o", "r"] +[33.0555, "o", "'"] +[33.0953, "o", " "] +[33.1746, "o", "-"] +[33.2371, "o", "-"] +[33.2978, "o", "t"] +[33.3637, "o", "y"] +[33.4035, "o", "p"] +[33.4482, "o", "e"] +[33.5196, "o", " "] +[33.5805, "o", "j"] +[33.658, "o", "v"] +[33.726, "o", "m"] +[34.076, "o", "\r\n"] +[34.0825, "o", "\u001b]11;?\u001b\\\u001b[6n"] +[34.6825, "o", "Agent: CAI-A03 (0ACME00800000000008G)\r\n"] +[34.6828, "o", "\r\nService: Process Server (process-engine)\r\n\r\n\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\r\n"] +[34.6829, "o", "\u2502 \u001b[1;36mTYPE \u001b[0m \u2502 \u001b[1;36mNAME \u001b[0m \u2502 \u001b[1;36mVALUE \u001b[0m \u2502 \u001b[1;36mDEFAULT \u001b[0m \u2502 \u001b[1;36mOVERRIDDEN\u001b[0m \u2502\r\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\r\n"] +[34.683, "o", "\u2502 jvm \u2502 min-heap \u2502 '1024M' \u2502 '512M' \u2502 yes \u2502\r\n\u2502 jvm \u2502 max-heap \u2502 '10G' \u2502 '1536M' \u2502 yes \u2502\r\n\u2502 jvm \u2502 additional-properties \u2502 '-Djava.library.path=C:\\Windows\\SysWOW64 \u2502 \u2502 yes \u2502\r\n\u2502 \u2502 \u2502 -DDisableSetMandatoryForFields=true' \u2502 \u2502 \u2502\r\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\r\n3 rows\r\n"] +[37.733, "o", "\u001b[1;36m~/demo\u001b[0m \u001b[1;32m\u276f\u001b[0m "] +[38.1711, "o", "i"] +[38.2233, "o", "i"] +[38.2814, "o", "c"] +[38.3492, "o", "s"] +[38.4242, "o", " "] +[38.4948, "o", "a"] +[38.5288, "o", "g"] +[38.562, "o", "e"] +[38.6058, "o", "n"] +[38.645, "o", "t"] +[38.6865, "o", " "] +[38.7452, "o", "c"] +[38.8067, "o", "o"] +[38.8629, "o", "n"] +[38.9033, "o", "f"] +[38.9366, "o", "i"] +[39.0129, "o", "g"] +[39.0482, "o", "s"] +[39.1153, "o", " "] +[39.1872, "o", "s"] +[39.2453, "o", "e"] +[39.3016, "o", "t"] +[39.351, "o", " "] +[39.4166, "o", "-"] +[39.4955, "o", "-"] +[39.5598, "o", "n"] +[39.6321, "o", "a"] +[39.6758, "o", "m"] +[39.7521, "o", "e"] +[39.7976, "o", " "] +[39.8633, "o", "C"] +[39.9116, "o", "A"] +[39.9743, "o", "I"] +[40.0424, "o", "-"] +[40.0851, "o", "A"] +[40.1476, "o", "0"] +[40.2132, "o", "3"] +[40.2645, "o", " "] +[40.2993, "o", "-"] +[40.3505, "o", "-"] +[40.4126, "o", "s"] +[40.4747, "o", "e"] +[40.53, "o", "t"] +[40.5943, "o", " "] +[40.6297, "o", "\""] +[40.6629, "o", "p"] +[40.7225, "o", "r"] +[40.7668, "o", "o"] +[40.8147, "o", "c"] +[40.8922, "o", "e"] +[40.9367, "o", "s"] +[41.0134, "o", "s"] +[41.0484, "o", "-"] +[41.0938, "o", "e"] +[41.1686, "o", "n"] +[41.2254, "o", "g"] +[41.2956, "o", "i"] +[41.3558, "o", "n"] +[41.3861, "o", "e"] +[41.4175, "o", ":"] +[41.4658, "o", "j"] +[41.5227, "o", "v"] +[41.5735, "o", "m"] +[41.6091, "o", ":"] +[41.6801, "o", "m"] +[41.7135, "o", "a"] +[41.7559, "o", "x"] +[41.8272, "o", "-"] +[41.9036, "o", "h"] +[41.9576, "o", "e"] +[41.9964, "o", "a"] +[42.0641, "o", "p"] +[42.118, "o", "="] +[42.1832, "o", "'"] +[42.2249, "o", "1"] +[42.2801, "o", "2"] +[42.3311, "o", "G"] +[42.3906, "o", "'"] +[42.435, "o", "\""] +[42.4979, "o", " "] +[42.5347, "o", "-"] +[42.5874, "o", "-"] +[42.6585, "o", "d"] +[42.692, "o", "r"] +[42.7521, "o", "y"] +[42.8049, "o", "-"] +[42.8722, "o", "r"] +[42.9412, "o", "u"] +[42.9774, "o", "n"] +[43.3274, "o", "\r\n"] +[43.3339, "o", "\u001b]11;?\u001b\\\u001b[6n"] +[43.9339, "o", "Changes for agent CAI-A03 (0ACME00800000000008G):\r\n\r\n"] +[43.9343, "o", "\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\r\n\u2502 \u001b[1;36mPROPERTY \u001b[0m \u2502 \u001b[1;36mCURRENT \u001b[0m \u2502 \u001b[1;36mNEW \u001b[0m \u2502\r\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\r\n\u2502 process-engine:jvm:max-heap \u2502 '10G' \u2502 '12G' \u2502\r\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\r\n1 row\r\n"] +[46.9843, "o", "\u001b[1;36m~/demo\u001b[0m \u001b[1;32m\u276f\u001b[0m "] +[46.9843, "o", "\u001b[2m"] +[47.0066, "o", "#"] +[47.0384, "o", " "] +[47.0828, "o", "J"] +[47.1254, "o", "S"] +[47.156, "o", "O"] +[47.186, "o", "N"] +[47.2251, "o", " "] +[47.2575, "o", "o"] +[47.3019, "o", "u"] +[47.3323, "o", "t"] +[47.3735, "o", "p"] +[47.4055, "o", "u"] +[47.429, "o", "t"] +[47.4718, "o", " "] +[47.4977, "o", "f"] +[47.5347, "o", "o"] +[47.579, "o", "r"] +[47.61, "o", " "] +[47.6517, "o", "s"] +[47.6916, "o", "c"] +[47.7275, "o", "r"] +[47.7617, "o", "i"] +[47.7893, "o", "p"] +[47.8148, "o", "t"] +[47.8541, "o", "s"] +[47.892, "o", " "] +[47.9123, "o", "a"] +[47.9513, "o", "n"] +[47.9733, "o", "d"] +[47.9949, "o", " "] +[48.0303, "o", "p"] +[48.061, "o", "i"] +[48.0874, "o", "p"] +[48.1188, "o", "e"] +[48.1599, "o", "l"] +[48.1936, "o", "i"] +[48.2317, "o", "n"] +[48.2749, "o", "e"] +[48.3073, "o", "s"] +[48.8073, "o", "\u001b[0m\r\n"] +[48.8573, "o", "\u001b[1;36m~/demo\u001b[0m \u001b[1;32m\u276f\u001b[0m "] +[49.3043, "o", "i"] +[49.3741, "o", "i"] +[49.4405, "o", "c"] +[49.502, "o", "s"] +[49.5332, "o", " "] +[49.572, "o", "a"] +[49.6154, "o", "g"] +[49.672, "o", "e"] +[49.7093, "o", "n"] +[49.7785, "o", "t"] +[49.854, "o", " "] +[49.8922, "o", "l"] +[49.923, "o", "i"] +[49.9796, "o", "s"] +[50.0473, "o", "t"] +[50.1144, "o", " "] +[50.1831, "o", "-"] +[50.233, "o", "o"] +[50.2797, "o", " "] +[50.3406, "o", "j"] +[50.4013, "o", "s"] +[50.4596, "o", "o"] +[50.5359, "o", "n"] +[50.5789, "o", " "] +[50.6497, "o", "|"] +[50.7242, "o", " "] +[50.7892, "o", "j"] +[50.8425, "o", "q"] +[50.8765, "o", " "] +[50.9073, "o", "-"] +[50.9498, "o", "r"] +[51.0151, "o", " "] +[51.0514, "o", "'"] +[51.1094, "o", "."] +[51.1589, "o", "["] +[51.1955, "o", "]"] +[51.2524, "o", " "] +[51.3242, "o", "|"] +[51.3973, "o", " "] +[51.4681, "o", "["] +[51.5388, "o", "."] +[51.5744, "o", "n"] +[51.6283, "o", "a"] +[51.7035, "o", "m"] +[51.7642, "o", "e"] +[51.8324, "o", ","] +[51.9063, "o", " "] +[51.9742, "o", "."] +[52.0134, "o", "a"] +[52.0769, "o", "g"] +[52.1418, "o", "e"] +[52.184, "o", "n"] +[52.2188, "o", "t"] +[52.2757, "o", "H"] +[52.3521, "o", "o"] +[52.3973, "o", "s"] +[52.4279, "o", "t"] +[52.507, "o", ","] +[52.5418, "o", " "] +[52.6143, "o", "."] +[52.672, "o", "a"] +[52.743, "o", "g"] +[52.8207, "o", "e"] +[52.8553, "o", "n"] +[52.8894, "o", "t"] +[52.9442, "o", "V"] +[53.016, "o", "e"] +[53.0878, "o", "r"] +[53.1297, "o", "s"] +[53.1693, "o", "i"] +[53.2095, "o", "o"] +[53.2695, "o", "n"] +[53.3048, "o", "]"] +[53.368, "o", " "] +[53.4471, "o", "|"] +[53.5059, "o", " "] +[53.5482, "o", "@"] +[53.5892, "o", "t"] +[53.6665, "o", "s"] +[53.7143, "o", "v"] +[53.7693, "o", "'"] +[53.8343, "o", " "] +[53.8946, "o", "|"] +[53.9507, "o", " "] +[54.0105, "o", "c"] +[54.0725, "o", "o"] +[54.1031, "o", "l"] +[54.183, "o", "u"] +[54.2415, "o", "m"] +[54.304, "o", "n"] +[54.3496, "o", " "] +[54.4006, "o", "-"] +[54.4668, "o", "t"] +[54.8168, "o", "\r\n"] +[54.9524, "o", "CAI-A01 agent-host-02 76.17\r\nCAI-A02 agent-host-06 76.17\r\nCAI-A03 agent-host-07 76.17\r\nCDI-G01-A01 agent-host-04 76.17\r\n"] +[57.5024, "o", "\u001b[1;36m~/demo\u001b[0m \u001b[1;32m\u276f\u001b[0m "] +[57.5024, "o", "\u001b[2m"] +[57.5421, "o", "#"] +[57.5637, "o", " "] +[57.6012, "o", "i"] +[57.6323, "o", "n"] +[57.656, "o", "t"] +[57.6889, "o", "e"] +[57.7327, "o", "r"] +[57.7715, "o", "a"] +[57.8079, "o", "c"] +[57.832, "o", "t"] +[57.8747, "o", "i"] +[57.9143, "o", "v"] +[57.937, "o", "e"] +[57.9599, "o", " "] +[57.9851, "o", "p"] +[58.0223, "o", "r"] +[58.0425, "o", "o"] +[58.0681, "o", "p"] +[58.0921, "o", "e"] +[58.1244, "o", "r"] +[58.166, "o", "t"] +[58.1906, "o", "y"] +[58.231, "o", " "] +[58.2611, "o", "e"] +[58.2989, "o", "d"] +[58.3431, "o", "i"] +[58.3837, "o", "t"] +[58.4141, "o", "o"] +[58.4459, "o", "r"] +[58.4879, "o", " "] +[58.5093, "o", "("] +[58.5427, "o", "c"] +[58.5676, "o", "h"] +[58.6057, "o", "a"] +[58.6506, "o", "n"] +[58.6747, "o", "g"] +[58.7095, "o", "e"] +[58.7414, "o", "s"] +[58.7857, "o", " "] +[58.8098, "o", "a"] +[58.8466, "o", "r"] +[58.8821, "o", "e"] +[58.907, "o", " "] +[58.9301, "o", "r"] +[58.9582, "o", "e"] +[58.9805, "o", "v"] +[59.0118, "o", "i"] +[59.0383, "o", "e"] +[59.0639, "o", "w"] +[59.1066, "o", "e"] +[59.1339, "o", "d"] +[59.1704, "o", " "] +[59.1935, "o", "b"] +[59.2266, "o", "e"] +[59.2622, "o", "f"] +[59.2857, "o", "o"] +[59.3067, "o", "r"] +[59.3417, "o", "e"] +[59.3627, "o", " "] +[59.3899, "o", "s"] +[59.4276, "o", "a"] +[59.4667, "o", "v"] +[59.5094, "o", "i"] +[59.5412, "o", "n"] +[59.5677, "o", "g"] +[59.6075, "o", ")"] +[60.1075, "o", "\u001b[0m\r\n"] +[60.1575, "o", "\u001b[1;36m~/demo\u001b[0m \u001b[1;32m\u276f\u001b[0m "] +[60.6331, "o", "i"] +[60.6964, "o", "i"] +[60.7458, "o", "c"] +[60.8252, "o", "s"] +[60.8984, "o", " "] +[60.9312, "o", "a"] +[60.9869, "o", "g"] +[61.0661, "o", "e"] +[61.1405, "o", "n"] +[61.1924, "o", "t"] +[61.2693, "o", " "] +[61.3135, "o", "c"] +[61.3511, "o", "o"] +[61.3967, "o", "n"] +[61.4606, "o", "f"] +[61.5004, "o", "i"] +[61.5519, "o", "g"] +[61.6056, "o", "s"] +[61.6776, "o", " "] +[61.7288, "o", "e"] +[61.7718, "o", "d"] +[61.8252, "o", "i"] +[61.897, "o", "t"] +[61.9502, "o", " "] +[61.9919, "o", "-"] +[62.0462, "o", "-"] +[62.1135, "o", "n"] +[62.1798, "o", "a"] +[62.2424, "o", "m"] +[62.2891, "o", "e"] +[62.3504, "o", " "] +[62.4181, "o", "C"] +[62.4545, "o", "A"] +[62.498, "o", "I"] +[62.5296, "o", "-"] +[62.5657, "o", "A"] +[62.6051, "o", "0"] +[62.6439, "o", "3"] +[62.9939, "o", "\r\n"] +[63.0006, "o", "\u001b]11;?\u001b\\\u001b[6n"] +[63.6006, "o", "\u001b[?25l\u001b[?2004h\u001b[?1004h"] +[63.6173, "o", "\r\u001b[38;5;238m\u2503\u001b[0m \u001b[1;38;2;117;113;249mAgent: CAI-A03\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;243mSelect a service (/ to filter). 0 pending change(s).\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;2;247;128;226m> \u001b[0m\u001b[38;2;2;191;135mOI Data Collector 12 properties 0 overridden\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252mProcess Server 45 properties 14 overridden\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m> Review and apply\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m\u21ba Reset all to defaults\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252mx Cancel\u001b[0m \r\n\u001b[K\r\n\u001b[38;2;97;97;97m\u2191\u001b[0m \u001b[38;2;73;73;73mup\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97m\u2193\u001b[0m \u001b[38;2;73;73;73mdown\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97m/\u001b[0m \u001b[38;2;73;73;73mfilter\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97menter\u001b[0m \u001b[38;2;73;73;73msubmit\u001b[0m\u001b[K\r\n\u001b[2mesc, q (in lists) or ctrl+c to cancel\u001b[0m\u001b[K\r"] +[66.1197, "o", "\u001b[9A\u001b[38;5;238m\u2503\u001b[0m /\u001b[7m \u001b[0m \r\n\n\n\n\n\n\n\n\u001b[38;2;97;97;97m\u2191\u001b[0m \u001b[38;2;73;73;73mup\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97m\u2193\u001b[0m \u001b[38;2;73;73;73mdown\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97mesc\u001b[0m \u001b[38;2;73;73;73mset filter\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97menter\u001b[0m \u001b[38;2;73;73;73msubmit\u001b[0m\u001b[K\r\n\r"] +[66.654, "o", "\u001b[9A\u001b[38;5;238m\u2503\u001b[0m / \r\n\n\n\n\n\n\n\n\n\r"] +[66.8089, "o", "\u001b[9A\u001b[38;5;238m\u2503\u001b[0m /P\u001b[7m \u001b[0m \r\n\n\n\n\n\u001b[38;5;238m\u2503\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \r\n\n\n\r"] +[67.3423, "o", "\u001b[9A\u001b[38;5;238m\u2503\u001b[0m /P \r\n\n\n\n\n\n\n\n\n\r"] +[67.4992, "o", "\u001b[9A\u001b[38;5;238m\u2503\u001b[0m /Pr\u001b[7m \u001b[0m \r\n\n\n\n\u001b[38;5;238m\u2503\u001b[0m \r\n\n\n\n\n\r"] +[68.032, "o", "\u001b[9A\u001b[38;5;238m\u2503\u001b[0m /Pr \r\n\n\n\n\n\n\n\n\n\r"] +[68.189, "o", "\u001b[9A\u001b[38;5;238m\u2503\u001b[0m /Pro\u001b[7m \u001b[0m \r\n\n\n\n\n\n\n\n\n\r"] +[68.7218, "o", "\u001b[9A\u001b[38;5;238m\u2503\u001b[0m /Pro \r\n\n\n\n\n\n\n\n\n\r"] +[68.8772, "o", "\u001b[9A\u001b[38;5;238m\u2503\u001b[0m /Proc\u001b[7m \u001b[0m \r\n\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;2;247;128;226m> \u001b[0m\u001b[38;2;2;191;135mProcess Server 45 properties 14 overridden\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \r\n\n\n\n\n\n\r"] +[69.4109, "o", "\u001b[9A\u001b[38;5;238m\u2503\u001b[0m /Proc \r\n\n\n\n\n\n\n\n\n\r"] +[69.944, "o", "\u001b[9A\u001b[38;5;238m\u2503\u001b[0m /Proc\u001b[7m \u001b[0m \r\n\n\n\n\n\n\n\n\n\r"] +[70.7459, "o", "\u001b[9A \u001b[K\u001b[J\r"] +[70.7461, "o", "\u001b[2K\r\u001b[?2004l\u001b[?25h\u001b[?1002l"] +[70.7461, "o", "\u001b[?1003l\u001b[?1006l\u001b[?1004l"] +[70.7531, "o", "\u001b[?25l\u001b[?2004h\u001b[?1004h"] +[70.7702, "o", "\r\u001b[38;5;238m\u2503\u001b[0m \u001b[1;38;2;117;113;249mAgent: CAI-A03 - Process Server\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;243mSelect a property to edit (/ to filter). * overridden, + custom, \u270e changed, \u2717 \u001b[0m\u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;2;247;128;226m> \u001b[0m\u001b[38;2;2;191;135m< Back to services\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m+ Add custom property\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m\u21ba Reset all Process Server properties to defaults\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m* server host-name 'agent-host-07.corp.acme.com\u001b[0m\u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m server shutdown-port 7005\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m* server key-alias 'agent-host-07.corp.acme.com\u001b[0m\u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m server key-store '../conf/ae.keystore'\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m server key-store-password 'password'\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m server trust-store '../conf/ae.cacerts'\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m server trust-store-password 'changeit'\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m server ldap-enabled-realm false\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m* server ldap-properties [9 entries] key=connectionUR\u001b[0m\u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m server ssl-enabled-protocols 'TLSv1.2'\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m server ephemeral-DH-key-size 2048\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m server use-secure-ciphers-only true\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m server fips-enabled false\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m* cluster enabled true\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m cluster name 'ProcessEngine'\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m* cluster primary-node false\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m* cluster load-balance-url 'https://apitst.acme.com'\u001b[0m \u001b[K\r\n\u001b[K\r\n\u001b[38;2;97;97;97m\u2191\u001b[0m \u001b[38;2;73;73;73mup\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97m\u2193\u001b[0m \u001b[38;2;73;73;73mdown\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97m/\u001b[0m \u001b[38;2;73;73;73mfilter\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97menter\u001b[0m \u001b[38;2;73;73;73msubmit\u001b[0m\u001b[K\r\n\u001b[2mesc, q (in lists) or ctrl+c to cancel\u001b[0m\u001b[K\r"] +[70.7869, "o", "\u001b[24A\r\u001b[38;5;238m\u2503\u001b[0m \u001b[1;38;2;117;113;249mAgent: CAI-A03 - Process Server\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;243mSelect a property to edit (/ to filter). * overridden, + custom, \u270e changed, \u2717 removed.\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;2;247;128;226m> \u001b[0m\u001b[38;2;2;191;135m< Back to services\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m+ Add custom property\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m\u21ba Reset all Process Server properties to defaults\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m* server host-name 'agent-host-07.corp.acme.com'\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m server shutdown-port 7005\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m* server key-alias 'agent-host-07.corp.acme.com'\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m server key-store '../conf/ae.keystore'\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m server key-store-password 'password'\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m server trust-store '../conf/ae.cacerts'\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m server trust-store-password 'changeit'\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m server ldap-enabled-realm false\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m* server ldap-properties [9 entries] key=connectionURL value=ldap\u2026\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m server ssl-enabled-protocols 'TLSv1.2'\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m server ephemeral-DH-key-size 2048\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m server use-secure-ciphers-only true\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m server fips-enabled false\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m* cluster enabled true\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m cluster name 'ProcessEngine'\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m* cluster primary-node false\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m* cluster load-balance-url 'https://apitst.acme.com'\u001b[0m \r\n\u001b[K\r\n\u001b[38;2;97;97;97m\u2191\u001b[0m \u001b[38;2;73;73;73mup\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97m\u2193\u001b[0m \u001b[38;2;73;73;73mdown\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97m/\u001b[0m \u001b[38;2;73;73;73mfilter\u001b[0m\u001b[38;2;60;60;60m "] +[70.787, "o", "\u2022 \u001b[0m\u001b[38;2;97;97;97menter\u001b[0m \u001b[38;2;73;73;73msubmit\u001b[0m\u001b[K\r\n\u001b[2mesc, q (in lists) or ctrl+c to cancel\u001b[0m\u001b[K\r"] +[72.61, "o", "\u001b[24A\u001b[38;5;238m\u2503\u001b[0m /\u001b[7m \u001b[0m \r\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\u001b[38;2;97;97;97m\u2191\u001b[0m \u001b[38;2;73;73;73mup\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97m\u2193\u001b[0m \u001b[38;2;73;73;73mdown\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97mesc\u001b[0m \u001b[38;2;73;73;73mset filter\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97menter\u001b[0m \u001b[38;2;73;73;73msubmit\u001b[0m\u001b[K\r\n\r"] +[73.1263, "o", "\u001b[24A\u001b[38;5;238m\u2503\u001b[0m / \r\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\r"] +[73.2962, "o", "\u001b[24A\u001b[38;5;238m\u2503\u001b[0m /m\u001b[7m \u001b[0m \r\n\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;2;247;128;226m> \u001b[0m\u001b[38;2;2;191;135m+ Add custom property\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m* server host-name 'agent-host-07.corp.acme.com'\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m* server key-alias 'agent-host-07.corp.acme.com'\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m server ldap-enabled-realm false\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m server ephemeral-DH-key-size 2048\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m cluster name 'ProcessEngine'\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m* cluster primary-node false\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m* cluster load-balance-url 'https://apitst.acme.com'\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m* jvm min-heap '1024M'\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m* jvm max-heap '10G'\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m* jvm additional-properties '-Djava.library.path=C:\\Windows\\SysWOW64 -D\u2026\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m connectors http-maxThreads 600\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m connectors http-connectionTimeout 120000\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m connectors https-maxThreads 600\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m connectors https-connectionTimeout 120000\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m connectors secure-channel-maxThreads 600\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m connectors secure-channel-connectionTimeout 120000\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db maxActive 300\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db maxIdle 30\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db maxWait 30000\u001b[0m \r\n\n\n\r"] +[73.8134, "o", "\u001b[24A\u001b[38;5;238m\u2503\u001b[0m /m \r\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\r"] +[73.9828, "o", "\u001b[24A\u001b[38;5;238m\u2503\u001b[0m /ma\u001b[7m \u001b[0m \r\n\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;2;247;128;226m> \u001b[0m\u001b[38;2;2;191;135m* cluster primary-node false\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m* jvm max-heap '10G'\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m connectors http-maxThreads 600\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m connectors https-maxThreads 600\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m connectors secure-channel-maxThreads 600\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db maxActive 300\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db maxIdle 30\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db maxWait 30000\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db logMaxActive 50\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db logMaxIdle 5\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db logMaxWait 30000\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \r\n\n\n\r"] +[74.4997, "o", "\u001b[24A\u001b[38;5;238m\u2503\u001b[0m /ma \r\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\r"] +[74.6693, "o", "\u001b[24A\u001b[38;5;238m\u2503\u001b[0m /max\u001b[7m \u001b[0m \r\n\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;2;247;128;226m> \u001b[0m\u001b[38;2;2;191;135m* jvm max-heap '10G'\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m connectors http-maxThreads 600\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m connectors https-maxThreads 600\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m connectors secure-channel-maxThreads 600\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db maxActive 300\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db maxIdle 30\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db maxWait 30000\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db logMaxActive 50\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db logMaxIdle 5\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db logMaxWait 30000\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \r\n\n\n\n\n\n\n\n\n\n\n\n\r"] +[75.1856, "o", "\u001b[24A\u001b[38;5;238m\u2503\u001b[0m /max \r\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\r"] +[75.3384, "o", "\u001b[24A\u001b[38;5;238m\u2503\u001b[0m /max-\u001b[7m \u001b[0m \r\n\n\n\u001b[38;5;238m\u2503\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \r\n\n\n\n\n\n\n\n\n\n\n\n\n\r"] +[75.8725, "o", "\u001b[24A\u001b[38;5;238m\u2503\u001b[0m /max- \r\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\r"] +[76.0285, "o", "\u001b[24A\u001b[38;5;238m\u2503\u001b[0m /max-h\u001b[7m \u001b[0m \r\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\r"] +[76.5614, "o", "\u001b[24A\u001b[38;5;238m\u2503\u001b[0m /max-h \r\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\r"] +[77.0947, "o", "\u001b[24A\u001b[38;5;238m\u2503\u001b[0m /max-h\u001b[7m \u001b[0m \r\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\r"] +[77.8969, "o", "\u001b[24A\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;243m/max-h\u001b[0m \r\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\u001b[38;2;97;97;97m\u2191\u001b[0m \u001b[38;2;73;73;73mup\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97m\u2193\u001b[0m \u001b[38;2;73;73;73mdown\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97m/\u001b[0m \u001b[38;2;73;73;73mfilter\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97mesc\u001b[0m \u001b[38;2;73;73;73mclear filter\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97menter\u001b[0m \u001b[38;2;73;73;73msubmit\u001b[0m\u001b[K\r\n\r"] +[77.8975, "o", "\u001b[24A \u001b[K\u001b[J\r\u001b[2K\r\u001b[?2004l\u001b[?25h\u001b[?1002l\u001b[?1003l\u001b[?1006l\u001b[?1004l"] +[77.8984, "o", "\u001b[?25l\u001b[?2004h\u001b[?1004h"] +[77.9154, "o", "\r\u001b[38;5;238m\u2503\u001b[0m \u001b[1;38;2;117;113;249mProcess Server \u203a jvm \u203a max-heap\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;243mType jvm \u00b7 overridden\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;243mDefault: '1536M'\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;243mValue: '10G'\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;2;247;128;226m> \u001b[0m\u001b[38;2;2;191;135mEdit value\u001b[0m \r\n\u001b[K\r\n\u001b[38;2;97;97;97m\u2191\u001b[0m \u001b[38;2;73;73;73mup\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97m\u2193\u001b[0m \u001b[38;2;73;73;73mdown\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97m/\u001b[0m \u001b[38;2;73;73;73mfilter\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97menter\u001b[0m \u001b[38;2;73;73;73msubmit\u001b[0m\u001b[K\r\n\u001b[2mesc, q (in lists) or ctrl+c to cancel\u001b[0m\u001b[K\r"] +[79.7176, "o", "\u001b[7A \u001b[K\u001b[J\r\u001b[2K\r\u001b[?2004l\u001b[?25h\u001b[?1002l\u001b[?1003l\u001b[?1006l\u001b[?1004l"] +[79.7183, "o", "\u001b[?25l\u001b[?2004h\u001b[?1004h"] +[79.7354, "o", "\r\u001b[38;5;238m\u2503\u001b[0m \u001b[1;38;2;117;113;249mProcess Server \u203a jvm \u203a max-heap\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;243mDefault: '1536M'\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;243mThe value is stored in single quotes; they are added for you.\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;2;247;128;226m> \u001b[0m10G\u001b[7;38;2;2;191;135m \u001b[0m \r\n\u001b[K\r\n\u001b[38;2;97;97;97menter\u001b[0m \u001b[38;2;73;73;73msubmit\u001b[0m\u001b[K\r\n\u001b[2mesc, q (in lists) or ctrl+c to cancel\u001b[0m\u001b[K\r"] +[80.2517, "o", "\u001b[6A\r\u001b[38;5;238m\u2503\u001b[0m \u001b[1;38;2;117;113;249mProcess Server \u203a jvm \u203a max-heap\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;243mDefault: '1536M'\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;243mThe value is stored in single quotes; they are added for you.\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;2;247;128;226m> \u001b[0m10G \r\n\u001b[K\r\n\u001b[38;2;97;97;97menter\u001b[0m \u001b[38;2;73;73;73msubmit\u001b[0m\u001b[K\r\n\u001b[2mesc, q (in lists) or ctrl+c to cancel\u001b[0m\u001b[K\r"] +[80.7855, "o", "\u001b[6A\n\n\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;2;247;128;226m> \u001b[0m10G\u001b[7;38;2;2;191;135m \u001b[0m \r\n\n\n\r"] +[81.7892, "o", "\u001b[6A\n\n\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;2;247;128;226m> \u001b[0m\u001b[7;38;2;2;191;135m \u001b[0m \r\n\n\n\r"] +[82.3235, "o", "\u001b[6A\n\n\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;2;247;128;226m> \u001b[0m \r\n\n\n\r"] +[82.5882, "o", "\u001b[6A\n\n\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;2;247;128;226m> \u001b[0m1\u001b[7;38;2;2;191;135m \u001b[0m \r\n\n\n\r"] +[83.1045, "o", "\u001b[6A\n\n\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;2;247;128;226m> \u001b[0m1 \r\n\n\n\r"] +[83.3711, "o", "\u001b[6A\n\n\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;2;247;128;226m> \u001b[0m12\u001b[7;38;2;2;191;135m \u001b[0m \r\n\n\n\r"] +[83.8867, "o", "\u001b[6A\n\n\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;2;247;128;226m> \u001b[0m12 \r\n\n\n\r"] +[84.153, "o", "\u001b[6A\n\n\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;2;247;128;226m> \u001b[0m12G\u001b[7;38;2;2;191;135m \u001b[0m \r\n\n\n\r"] +[84.6697, "o", "\u001b[6A\n\n\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;2;247;128;226m> \u001b[0m12G \r\n\n\n\r"] +[85.2028, "o", "\u001b[6A\n\n\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;2;247;128;226m> \u001b[0m12G\u001b[7;38;2;2;191;135m \u001b[0m \r\n\n\n\r"] +[86.0043, "o", "\u001b[6A \u001b[K\u001b[J\r\u001b[2K\r\u001b[?2004l\u001b[?25h\u001b[?1002l\u001b[?1003l\u001b[?1006l\u001b[?1004l"] +[86.0056, "o", "\u001b[?25l\u001b[?2004h\u001b[?1004h"] +[86.0226, "o", "\r\u001b[38;5;238m\u2503\u001b[0m \u001b[1;38;2;117;113;249mProcess Server \u203a jvm \u203a max-heap\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;243mType jvm \u00b7 overridden, changed\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;243mDefault: '1536M'\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;243mValue: '12G'\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;2;247;128;226m> \u001b[0m\u001b[38;2;2;191;135mEdit value\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252mReset to default\u001b[0m \r\n\u001b[K\r\n\u001b[38;2;97;97;97m\u2191\u001b[0m \u001b[38;2;73;73;73mup\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97m\u2193\u001b[0m \u001b[38;2;73;73;73mdown\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97m/\u001b[0m \u001b[38;2;73;73;73mfilter\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97menter\u001b[0m \u001b[38;2;73;73;73msubmit\u001b[0m\u001b[K\r\n\u001b[2mesc, q (in lists) or ctrl+c to cancel\u001b[0m\u001b[K\r"] +[87.823, "o", "\u001b[8A \u001b[K\u001b[J\r\u001b[2K\r\u001b[?2004l\u001b[?25h\u001b[?1002l\u001b[?1003l\u001b[?1006l\u001b[?1004l"] +[87.8304, "o", "\u001b[?25l\u001b[?2004h\u001b[?1004h"] +[87.8471, "o", "\r\u001b[38;5;238m\u2503\u001b[0m \u001b[1;38;2;117;113;249mAgent: CAI-A03 - Process Server\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;243mSelect a property to edit (/ to filter). * overridden, + custom, \u270e changed, \u2717 \u001b[0m\u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;2;247;128;226m> \u001b[0m\u001b[38;2;2;191;135m\u270e jvm max-heap '12G'\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m* jvm additional-properties '-Djava.library.path=C:\\Wind\u001b[0m\u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m connectors http-port 7080\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m connectors http-maxThreads 600\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m connectors http-connectionTimeout 120000\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m connectors https-port 7443\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m connectors https-maxThreads 600\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m connectors https-connectionTimeout 120000\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m connectors secure-channel-maxThreads 600\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m connectors secure-channel-connectionTimeout 120000\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db type 'postgre'\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db driver 'org.postgresql.Driver'\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m* db url 'jdbc:postgresql://agent-hos\u001b[0m\u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db maxActive 300\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db maxIdle 30\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db maxWait 30000\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m* db connection-properties [3 entries] key=timeBetweenE\u001b[0m\u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m* db logUrl 'jdbc:postgresql://agent-hos\u001b[0m\u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db logMaxActive 50\u001b[0m \u001b[K\r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db logMaxIdle 5\u001b[0m \u001b[K\r\n\u001b[K\r\n\u001b[38;2;97;97;97m\u2191\u001b[0m \u001b[38;2;73;73;73mup\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97m\u2193\u001b[0m \u001b[38;2;73;73;73mdown\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97m/\u001b[0m \u001b[38;2;73;73;73mfilter\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97menter\u001b[0m \u001b[38;2;73;73;73msubmit\u001b[0m\u001b[K\r\n\u001b[2mesc, q (in lists) or ctrl+c to cancel\u001b[0m\u001b[K\r"] +[87.8639, "o", "\u001b[24A\r\u001b[38;5;238m\u2503\u001b[0m \u001b[1;38;2;117;113;249mAgent: CAI-A03 - Process Server\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;243mSelect a property to edit (/ to filter). * overridden, + custom, \u270e changed, \u2717 removed.\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;2;247;128;226m> \u001b[0m\u001b[38;2;2;191;135m\u270e jvm max-heap '12G'\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m* jvm additional-properties '-Djava.library.path=C:\\Windows\\SysWOW64 -D\u2026\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m connectors http-port 7080\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m connectors http-maxThreads 600\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m connectors http-connectionTimeout 120000\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m connectors https-port 7443\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m connectors https-maxThreads 600\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m connectors https-connectionTimeout 120000\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m connectors secure-channel-maxThreads 600\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m connectors secure-channel-connectionTimeout 120000\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db type 'postgre'\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db driver 'org.postgresql.Driver'\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m* db url 'jdbc:postgresql://agent-host-02:5432/activ\u2026\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db maxActive 300\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db maxIdle 30\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db maxWait 30000\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m* db connection-properties [3 entries] key=timeBetweenEvictionRunsMi\u2026\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m* db logUrl 'jdbc:postgresql://agent-host-02:5432/activ\u2026\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db logMaxActive 50\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m db logMaxIdle 5\u001b[0m \r\n\u001b[K\r\n\u001b[38;2;97;97;97m\u2191\u001b[0m \u001b[38;2;73;73;73mup\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97m\u2193\u001b[0m \u001b[38;2;73;73;73mdown\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97m/\u001b[0m \u001b[38;2;73;73;73mfilter\u001b[0m\u001b[38;2;60;6"] +[87.8639, "o", "0;60m \u2022 \u001b[0m\u001b[38;2;97;97;97menter\u001b[0m \u001b[38;2;73;73;73msubmit\u001b[0m\u001b[K\r\n\u001b[2mesc, q (in lists) or ctrl+c to cancel\u001b[0m\u001b[K\r"] +[89.6645, "o", "\u001b[24A \u001b[K\u001b[J\r\u001b[2K\r\u001b[?2004l\u001b[?25h\u001b[?1002l\u001b[?1003l\u001b[?1006l\u001b[?1004l"] +[89.6659, "o", "\u001b[?25l\u001b[?2004h\u001b[?1004h"] +[89.6834, "o", "\r\u001b[38;5;238m\u2503\u001b[0m \u001b[1;38;2;117;113;249mAgent: CAI-A03\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;243mSelect a service (/ to filter). 1 pending change(s).\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;2;247;128;226m> \u001b[0m\u001b[38;2;2;191;135mProcess Server 45 properties 14 overridden 1 changed\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m> Review and apply\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252m\u21ba Reset all to defaults\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252mx Cancel\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \r\n\u001b[K\r\n\u001b[38;2;97;97;97m\u2191\u001b[0m \u001b[38;2;73;73;73mup\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97m\u2193\u001b[0m \u001b[38;2;73;73;73mdown\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97m/\u001b[0m \u001b[38;2;73;73;73mfilter\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97menter\u001b[0m \u001b[38;2;73;73;73msubmit\u001b[0m\u001b[K\r\n\u001b[2mesc, q (in lists) or ctrl+c to cancel\u001b[0m\u001b[K\r"] +[91.1954, "o", "\u001b[9A\u001b[38;5;238m\u2503\u001b[0m /\u001b[7m \u001b[0m \r\n\n\n\n\n\n\n\n\u001b[38;2;97;97;97m\u2191\u001b[0m \u001b[38;2;73;73;73mup\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97m\u2193\u001b[0m \u001b[38;2;73;73;73mdown\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97mesc\u001b[0m \u001b[38;2;73;73;73mset filter\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97menter\u001b[0m \u001b[38;2;73;73;73msubmit\u001b[0m\u001b[K\r\n\r"] +[91.7287, "o", "\u001b[9A\u001b[38;5;238m\u2503\u001b[0m / \r\n\n\n\n\n\n\n\n\n\r"] +[91.8816, "o", "\u001b[9A\u001b[38;5;238m\u2503\u001b[0m /R\u001b[7m \u001b[0m \r\n\n\n\n\n\u001b[38;5;238m\u2503\u001b[0m \r\n\n\n\n\r"] +[92.4156, "o", "\u001b[9A\u001b[38;5;238m\u2503\u001b[0m /R \r\n\n\n\n\n\n\n\n\n\r"] +[92.5713, "o", "\u001b[9A\u001b[38;5;238m\u2503\u001b[0m /Re\u001b[7m \u001b[0m \r\n\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;2;247;128;226m> \u001b[0m\u001b[38;2;2;191;135m\u21ba Reset all to defaults\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \r\n\n\n\n\n\r"] +[93.1046, "o", "\u001b[9A\u001b[38;5;238m\u2503\u001b[0m /Re \r\n\n\n\n\n\n\n\n\n\r"] +[93.2608, "o", "\u001b[9A\u001b[38;5;238m\u2503\u001b[0m /Rev\u001b[7m \u001b[0m \r\n\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;2;247;128;226m> \u001b[0m\u001b[38;2;2;191;135m> Review and apply\u001b[0m \r\n\n\n\n\n\n\n\r"] +[93.7938, "o", "\u001b[9A\u001b[38;5;238m\u2503\u001b[0m /Rev \r\n\n\n\n\n\n\n\n\n\r"] +[94.3958, "o", "\u001b[9A \u001b[K\u001b[J\r\u001b[2K\r\u001b[?2004l\u001b[?25h\u001b[?1002l\u001b[?1003l\u001b[?1006l\u001b[?1004l"] +[94.397, "o", "\u001b[?25l\u001b[?2004h\u001b[?1004h"] +[94.4148, "o", "\r\u001b[38;5;238m\u2503\u001b[0m \u001b[1;38;2;117;113;249mAgent: CAI-A03 - review\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;243mProcess Server \u203a jvm \u203a max-heap: '10G' \u2192 '12G'\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;2;247;128;226m> \u001b[0m\u001b[38;2;2;191;135mApply 1 change(s)\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252mBack to editing\u001b[0m \r\n\u001b[38;5;238m\u2503\u001b[0m \u001b[38;5;252mCancel\u001b[0m \r\n\u001b[K\r\n\u001b[38;2;97;97;97m\u2191\u001b[0m \u001b[38;2;73;73;73mup\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97m\u2193\u001b[0m \u001b[38;2;73;73;73mdown\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97m/\u001b[0m \u001b[38;2;73;73;73mfilter\u001b[0m\u001b[38;2;60;60;60m \u2022 \u001b[0m\u001b[38;2;97;97;97menter\u001b[0m \u001b[38;2;73;73;73msubmit\u001b[0m\u001b[K\r\n\u001b[2mesc, q (in lists) or ctrl+c to cancel\u001b[0m\u001b[K\r"] +[97.9152, "o", "\u001b[7A \u001b[K\u001b[J\r\u001b[2K\r\u001b[?2004l\u001b[?25h\u001b[?1002l\u001b[?1003l\u001b[?1006l\u001b[?1004l"] +[97.9152, "o", "Canceled.\r\n"] +[100.4652, "o", "\u001b[1;36m~/demo\u001b[0m \u001b[1;32m\u276f\u001b[0m "] +[100.4652, "o", "\u001b[2m"] +[100.4966, "o", "#"] +[100.5334, "o", " "] +[100.5712, "o", "o"] +[100.6141, "o", "r"] +[100.6449, "o", " "] +[100.6874, "o", "r"] +[100.7252, "o", "u"] +[100.7475, "o", "n"] +[100.771, "o", " "] +[100.8052, "o", "i"] +[100.8403, "o", "i"] +[100.879, "o", "c"] +[100.8994, "o", "s"] +[100.9312, "o", " "] +[100.9715, "o", "w"] +[100.995, "o", "i"] +[101.0311, "o", "t"] +[101.0703, "o", "h"] +[101.0951, "o", " "] +[101.1233, "o", "n"] +[101.1477, "o", "o"] +[101.1848, "o", " "] +[101.2184, "o", "a"] +[101.2526, "o", "r"] +[101.2858, "o", "g"] +[101.3225, "o", "u"] +[101.3508, "o", "m"] +[101.3908, "o", "e"] +[101.4182, "o", "n"] +[101.4628, "o", "t"] +[101.5029, "o", "s"] +[101.533, "o", " "] +[101.5646, "o", "f"] +[101.6085, "o", "o"] +[101.6443, "o", "r"] +[101.6732, "o", " "] +[101.6984, "o", "t"] +[101.7306, "o", "h"] +[101.7584, "o", "e"] +[101.7848, "o", " "] +[101.8148, "o", "m"] +[101.8486, "o", "a"] +[101.8689, "o", "i"] +[101.9078, "o", "n"] +[101.9448, "o", " "] +[101.9724, "o", "m"] +[102.0129, "o", "e"] +[102.0498, "o", "n"] +[102.0719, "o", "u"] +[102.5719, "o", "\u001b[0m\r\n"] +[102.6219, "o", "\u001b[1;36m~/demo\u001b[0m \u001b[1;32m\u276f\u001b[0m "] +[103.0924, "o", "i"] +[103.1619, "o", "i"] +[103.1991, "o", "c"] +[103.2489, "o", "s"] +[103.5989, "o", "\r\n"] +[103.6056, "o", "\u001b]11;?\u001b\\\u001b[6n"] +[103.6114, "o", "\u001b[?25l\u001b[?1049h\u001b[2J\u001b[H\u001b[?25l\u001b[?2004h"] +[103.6289, "o", "\u001b[H\r\u001b[1;36miics\u001b[0m \u001b[38;5;244mmain menu\u001b[0m\u001b[K\r\n\u001b[38;5;250mprofile tst \u00b7 org Acme Insurance Corp - TST \u00b7 alexandra.smith.tst@acme.com \u00b7 session ok (23 min left)\u001b[0m\u001b[K\r\n\u001b[K\r\n\u001b[1;34mSession\u001b[0m\u001b[K\r\n\u001b[1;7m> Switch profile\u001b[0m\u001b[K\r\n Set as default profile \u270e\u001b[K\r\n Login\u001b[K\r\n List profiles\u001b[K\r\n Add profile \u270e\u001b[K\r\n Edit active profile \u270e\u001b[K\r\n Delete profile \u270e\u001b[K\r\n Edit global settings\u001b[K\r\n\u001b[1;34mUsers\u001b[0m\u001b[K\r\n List users\u001b[K\r\n Show user\u001b[K\r\n Create user \u270e\u001b[K\r\n Edit user groups and roles \u270e\u001b[K\r\n Delete user \u270e\u001b[K\r\n Change password \u270e\u001b[K\r\n\u001b[1;34mUser groups\u001b[0m\u001b[K\r\n List user groups\u001b[K\r\n Show user group\u001b[K\r\n Create user group \u270e\u001b[K\r\n Edit user group roles \u270e\u001b[K\r\n Delete user group \u270e\u001b[K\r\n\u001b[1;34mRoles\u001b[0m\u001b[K\r\n List roles\u001b[K\r\n Show role\u001b[K\r\n Create role \u270e\u001b[K\r\n Edit role privileges \u270e\u001b[K\r\n\u001b[38;5;244m 1-27 of 50\u001b[0m\u001b[K\r\n\u001b[38;5;252mUse another org profile for this menu session (key p)\u001b[0m\u001b[K\r\n\u001b[K\r\n\u001b[38;5;244menter run \u00b7 / filter \u00b7 p profile \u00b7 ? help \u00b7 q quit\u001b[0m\u001b[K\u001b[34;H"] +[106.1335, "o", "\u001b[H\n\n\n\n Switch profile\u001b[K\r\n\u001b[1;7m> Set as default profile \u270e\u001b[0m\u001b[K\r\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\u001b[38;5;252mMake the active profile the default in ~/.iics/config.yaml\u001b[0m\u001b[K\r\n\n\u001b[34;H"] +[106.6477, "o", "\u001b[H\n\n\n\n\n Set as default profile \u270e\u001b[K\r\n\u001b[1;7m> Login\u001b[0m\u001b[K\r\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\u001b[38;5;252mSign in and refresh the session for the active profile\u001b[0m\u001b[K\r\n\u001b[3;38;5;244m$ iics login --profile tst\u001b[0m\u001b[K\r\n\u001b[34;H"] +[107.1584, "o", "\u001b[H\n\n\n\n\n\n Login\u001b[K\r\n\u001b[1;7m> List profiles\u001b[0m\u001b[K\r\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\u001b[38;5;252mShow configured profiles\u001b[0m\u001b[K\r\n\u001b[3;38;5;244m$ iics profile list\u001b[0m\u001b[K\r\n\u001b[34;H"] +[108.1726, "o", "\u001b[H\u001b[1;36miics\u001b[0m \u001b[38;5;244mmain menu\u001b[0m /\u001b[7mf\u001b[0m\u001b[38;5;240milter\u001b[0m\u001b[K\r\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\u001b[34;H"] +[108.3289, "o", "\u001b[H\u001b[1;36miics\u001b[0m \u001b[38;5;244mmain menu\u001b[0m /a\u001b[7m \u001b[0m\u001b[K\r\n\n\n\n\n\n\n\u001b[1;7m> Add profile \u270e\u001b[0m\u001b[K\r\n Edit active profile \u270e\u001b[K\r\n Delete profile \u270e\u001b[K\r\n Edit global settings\u001b[K\r\n\u001b[1;34mUsers\u001b[0m\u001b[K\r\n List users\u001b[K\r\n Show user\u001b[K\r\n Create user \u270e\u001b[K\r\n Edit user groups and roles \u270e\u001b[K\r\n Delete user \u270e\u001b[K\r\n Change password \u270e\u001b[K\r\n\u001b[1;34mUser groups\u001b[0m\u001b[K\r\n List user groups\u001b[K\r\n Show user group\u001b[K\r\n Create user group \u270e\u001b[K\r\n Edit user group roles \u270e\u001b[K\r\n Delete user group \u270e\u001b[K\r\n\u001b[1;34mRoles\u001b[0m\u001b[K\r\n List roles\u001b[K\r\n Show role\u001b[K\r\n Create role \u270e\u001b[K\r\n Edit role privileges \u270e\u001b[K\r\n\u001b[1;34mPrivileges\u001b[0m\u001b[K\r\n\u001b[38;5;244m 1-27 of 49\u001b[0m\u001b[K\r\n\u001b[38;5;252mSet up a new org profile\u001b[0m\u001b[K\r\n\u001b[3;38;5;244m$ iics profile add ''\u001b[0m\u001b[K\r\n\u001b[34;H"] +[108.4887, "o", "\u001b[H\u001b[1;36miics\u001b[0m \u001b[38;5;244mmain menu\u001b[0m /ag\u001b[7m \u001b[0m\u001b[K\r\n\n\n\n Edit active profile \u270e\u001b[K\r\n\u001b[1;34mUsers\u001b[0m\u001b[K\r\n Create user \u270e\u001b[K\r\n\u001b[1;34mEnvironments and agents\u001b[0m\u001b[K\r\n\u001b[1;7m> List runtime environments\u001b[0m\u001b[K\r\n Create runtime environment \u270e\u001b[K\r\n List agents\u001b[K\r\n Show agent details\u001b[K\r\n Start agent service \u270e\u001b[K\r\n Stop agent service \u270e\u001b[K\r\n Restart agent service \u270e\u001b[K\r\n Edit agent properties \u270e\u001b[K\r\n Show agent installer info\u001b[K\r\n Download agent installer\u001b[K\r\n\u001b[1;34mHelp\u001b[0m\u001b[K\r\n Show CLI help\u001b[K\r\n\u001b[K\r\n\u001b[38;5;252mShow runtime environments (Secure Agent groups)\u001b[0m\u001b[K\r\n\u001b[3;38;5;244m$ iics environment list --profile tst\u001b[0m\u001b[K\r\n\u001b[38;5;244menter run \u00b7 / filter \u00b7 p profile \u00b7 ? help \u00b7 q quit\u001b[0m\u001b[K\u001b[J\u001b[24;H"] +[108.644, "o", "\u001b[H\u001b[1;36miics\u001b[0m \u001b[38;5;244mmain menu\u001b[0m /age\u001b[7m \u001b[0m\u001b[K\r\n\n\n\u001b[1;34mUsers\u001b[0m\u001b[K\r\n Create user \u270e\u001b[K\r\n\u001b[1;34mEnvironments and agents\u001b[0m\u001b[K\r\n List runtime environments\u001b[K\r\n Create runtime environment \u270e\u001b[K\r\n\u001b[1;7m> List agents\u001b[0m\u001b[K\r\n Show agent details\u001b[K\r\n Start agent service \u270e\u001b[K\r\n Stop agent service \u270e\u001b[K\r\n Restart agent service \u270e\u001b[K\r\n Edit agent properties \u270e\u001b[K\r\n Show agent installer info\u001b[K\r\n Download agent installer\u001b[K\r\n\u001b[K\r\n\u001b[38;5;252mShow Secure Agents\u001b[0m\u001b[K\r\n\u001b[3;38;5;244m$ iics agent list --profile tst\u001b[0m\u001b[K\r\n\u001b[38;5;244menter run \u00b7 / filter \u00b7 p profile \u00b7 ? help \u00b7 q quit\u001b[0m\u001b[K\u001b[J\u001b[20;H"] +[108.8042, "o", "\u001b[H\u001b[1;36miics\u001b[0m \u001b[38;5;244mmain menu\u001b[0m /agen\u001b[7m \u001b[0m\u001b[K\r\n\n\n\u001b[1;34mEnvironments and agents\u001b[0m\u001b[K\r\n List runtime environments\u001b[K\r\n Create runtime environment \u270e\u001b[K\r\n List agents\u001b[K\r\n Show agent details\u001b[K\r\n\u001b[1;7m> Start agent service \u270e\u001b[0m\u001b[K\r\n Stop agent service \u270e\u001b[K\r\n Restart agent service \u270e\u001b[K\r\n Edit agent properties \u270e\u001b[K\r\n Show agent installer info\u001b[K\r\n Download agent installer\u001b[K\r\n\u001b[K\r\n\u001b[38;5;252mPick an agent and start a service\u001b[0m\u001b[K\r\n\u001b[3;38;5;244m$ iics agent start -i --profile tst\u001b[0m\u001b[K\r\n\u001b[38;5;244menter run \u00b7 / filter \u00b7 p profile \u00b7 ? help \u00b7 q quit\u001b[0m\u001b[K\u001b[J\u001b[18;H"] +[108.9594, "o", "\u001b[H\u001b[1;36miics\u001b[0m \u001b[38;5;244mmain menu\u001b[0m /agent\u001b[7m \u001b[0m\u001b[K\r\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\u001b[18;H"] +[111.4612, "o", "\u001b[H\u001b[1;36miics\u001b[0m \u001b[38;5;244mmain menu\u001b[0m\u001b[K\r\n\n\n\u001b[1;34mSession\u001b[0m\u001b[K\r\n Switch profile\u001b[K\r\n Set as default profile \u270e\u001b[K\r\n Login\u001b[K\r\n List profiles\u001b[K\r\n\u001b[1;7m> Add profile \u270e\u001b[0m\u001b[K\r\n Edit active profile \u270e\u001b[K\r\n Delete profile \u270e\u001b[K\r\n Edit global settings\u001b[K\r\n\u001b[1;34mUsers\u001b[0m\u001b[K\r\n List users\u001b[K\r\n Show user\u001b[K\r\n Create user \u270e\u001b[K\r\n Edit user groups and roles \u270e\u001b[K\r\n Delete user \u270e\u001b[K\r\n Change password \u270e\u001b[K\r\n\u001b[1;34mUser groups\u001b[0m\u001b[K\r\n List user groups\u001b[K\r\n Show user group\u001b[K\r\n Create user group \u270e\u001b[K\r\n Edit user group roles \u270e\u001b[K\r\n Delete user group \u270e\u001b[K\r\n\u001b[1;34mRoles\u001b[0m\u001b[K\r\n List roles\u001b[K\r\n Show role\u001b[K\r\n Create role \u270e\u001b[K\r\n Edit role privileges \u270e\u001b[K\r\n\u001b[38;5;244m 1-27 of 50\u001b[0m\u001b[K\r\n\u001b[38;5;252mSet up a new org profile\u001b[0m\u001b[K\r\n\u001b[3;38;5;244m$ iics profile add ''\u001b[0m\u001b[K\r\n\u001b[38;5;244menter run \u00b7 / filter \u00b7 p profile \u00b7 ? help \u00b7 q quit\u001b[0m\u001b[K\u001b[34;H"] +[112.9616, "o", "\u001b[2K\r\u001b[?2004l\u001b[?25h\u001b[?1002l\u001b[?1003l\u001b[?1006l\u001b[?1049l\u001b[?25h"] +[114.5116, "o", "\u001b[1;36m~/demo\u001b[0m \u001b[1;32m\u276f\u001b[0m "] diff --git a/docs/demo/iics-demo.gif b/docs/demo/iics-demo.gif new file mode 100644 index 0000000000000000000000000000000000000000..6ddd3b85ab80f79e0db85bdac4750fb008a1d20f GIT binary patch literal 1384781 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

i1p9J3}-_h*jTcAMb!-upbn&u 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�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ŽViHyrI4yTU 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

&QatC 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

w;-<%4h# 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

E4LCQfrgXjU&e6VZtK_ijnj4? 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

+Tc;yXn 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⋘N6+*IJYrmT5 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%0D(ZK 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 z

rVwJ2nUcNL=6oUqQ6e?>_)8s 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 BZ2pMH#pPx+krPt?TU7K^X`wz+8 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 z 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 V!c@G>=x^rx)f;L=sIUQv{{dE`q Date: Tue, 20 Oct 2026 13:02:54 -0400 Subject: [PATCH 44/52] chore: add README demo recorder with local-only masking rules scripts/demo/record.py records docs/demo/iics-demo.cast from scenes.json and masks sensitive output with same-length replacements. The rules live in scripts/demo/masks.local.json, which is ignored by git; masks.example.json documents the format. make demo builds iics, records the cast and renders the GIF with agg. --- .gitignore | 5 +- Makefile | 13 +- README.md | 3 +- scripts/demo/README.md | 71 ++++++++++ scripts/demo/masks.example.json | 16 +++ scripts/demo/record.py | 222 ++++++++++++++++++++++++++++++++ scripts/demo/scenes.json | 219 +++++++++++++++++++++++++++++++ 7 files changed, 546 insertions(+), 3 deletions(-) create mode 100644 scripts/demo/README.md create mode 100644 scripts/demo/masks.example.json create mode 100644 scripts/demo/record.py create mode 100644 scripts/demo/scenes.json diff --git a/.gitignore b/.gitignore index ecc8e87..2ae3407 100644 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,7 @@ __debug_bin* # Claude Code runtime artifacts .claude/ -target/ \ No newline at end of file +target/ +# Demo recording masking rules (they name the values they hide) +scripts/demo/masks.local.json +__pycache__/ diff --git a/Makefile b/Makefile index a97cbaa..ba6d691 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ BINARY_NAME := iics VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") LDFLAGS := -ldflags "-s -w -X main.version=$(VERSION)" -.PHONY: build clean test vet lint fmt install completions +.PHONY: build clean test vet lint fmt install completions demo build: go build $(LDFLAGS) -o $(BINARY_NAME) ./cmd/iics @@ -32,4 +32,15 @@ completions: go run ./cmd/iics completion fish > completions/iics.fish go run ./cmd/iics completion powershell > completions/iics.ps1 +# Re-record the README demo with the profile and agent named in +# scripts/demo/scenes.json. Needs scripts/demo/masks.local.json and agg +# (https://github.com/asciinema/agg). See scripts/demo/README.md. +DEMO_FONT := JetBrainsMono Nerd Font Mono,FiraCode Nerd Font Mono,DejaVu Sans Mono + +demo: build + PATH="$(CURDIR):$$PATH" python3 -I scripts/demo/record.py + agg --font-family "$(DEMO_FONT)" --font-size 14 --theme dracula \ + --idle-time-limit 3 --last-frame-duration 4 \ + docs/demo/iics-demo.cast docs/demo/iics-demo.gif + all: fmt vet test clean build completions diff --git a/README.md b/README.md index 5d31332..19bb3ab 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,8 @@ A comprehensive command-line interface for the [Informatica Intelligent Cloud Se The demo shows profiles, asset listing, Secure Agents and their services, per-agent service properties with a dry-run change, JSON output for scripts, the interactive property editor and the main menu. Host names, user names and the organization name are masked. To replay it in a terminal, -run `asciinema play docs/demo/iics-demo.cast`. +run `asciinema play docs/demo/iics-demo.cast`; to record it again, see +[scripts/demo](scripts/demo/README.md). ## Features diff --git a/scripts/demo/README.md b/scripts/demo/README.md new file mode 100644 index 0000000..620f906 --- /dev/null +++ b/scripts/demo/README.md @@ -0,0 +1,71 @@ +# README demo recording + +`record.py` records the demo shown in the main [README](../../README.md) as an +[asciinema](https://asciinema.org) cast, `docs/demo/iics-demo.cast`, and +`make demo` renders it to `docs/demo/iics-demo.gif`. + +The demo runs real commands against a live org, so the output contains host +names, emails and the organization name. These are masked before the cast is +written, using rules kept in a local file that is never committed. + +## Requirements + +- Python 3 (standard library only) on Linux or macOS. +- [agg](https://github.com/asciinema/agg) to render the GIF, for example + `cargo install --git https://github.com/asciinema/agg`. +- `jq` and `column`, used by one scene. +- A profile with access to the org and agent named in `scenes.json`. Use a + test org: the editor scene cancels its change, but every command runs for + real. + +## Masking rules + +Copy `masks.example.json` to `masks.local.json` (ignored by git) and fill in +your values. Every replacement keeps the length of the text it replaces, so +tables stay aligned; shorter results are padded with spaces and longer ones cut. + +| Key | Meaning | +| --- | --- | +| `emails.keep` | Email local parts left as they are, such as service accounts. | +| `emails.names` | Fixed fake names for local parts, ideally the same length. | +| `emails.suffixes` | Endings kept after the fake name, such as `.tst`. | +| `emails.other` | Name for any other local part; `{n}` is a counter. | +| `emails.domains` | Regular expressions and replacements for email domains. | +| `rules` | Regular expressions and replacements for the whole output, applied in order after emails. Group references such as `\\1` work. | +| `forbid` | Text that must not remain (case-insensitive). If any is found, the cast is not written. | + +Masking runs on the whole output stream, so text split across output chunks +or cut short by a narrow column is still caught. Write rules that also match +truncated values, as the host name rule in the example does. + +## Recording + +```bash +make demo +``` + +This builds `iics`, records with `scenes.json` and renders the GIF. To record +only the cast, or into another file: + +```bash +PATH="$PWD:$PATH" python3 scripts/demo/record.py scripts/demo/scenes.json /tmp/demo.cast +asciinema play /tmp/demo.cast +``` + +`--masks FILE` reads the rules from another file, and `--no-mask` records +without masking, for local checks only. + +## Scenes + +`scenes.json` sets the terminal size, environment (such as `IICS_PROFILE`) and +the scenes, in order: + +- `{"comment": "text"}` types a shell comment. +- `{"cmd": "iics ...", "pause": 2.5}` types and runs a command, then waits. + `show` types different text than what runs. +- `keys` sends keystrokes to an interactive command: + `{"k": "\r", "before": 0.6}` waits `before` seconds, then sends `k`. + Use `\u001b` for Esc and `\u001b[B` for the down arrow. + +End interactive scenes with a cancel, never an apply, so recording the demo +does not change the org. diff --git a/scripts/demo/masks.example.json b/scripts/demo/masks.example.json new file mode 100644 index 0000000..bd58b06 --- /dev/null +++ b/scripts/demo/masks.example.json @@ -0,0 +1,16 @@ +{ + "emails": { + "keep": ["admin"], + "names": {"first.last": "alexa.smit"}, + "suffixes": [".dev", ".prod", ".qa", ".stg", ".tst"], + "other": "user{n}", + "domains": {"(?i)\\bexample-corp\\b": "acme-corpxx"} + }, + "rules": [ + {"pattern": "(?i)myhost[a-z]*?(\\d*)(?![a-z0-9])", "replace": "agent-\\1"}, + {"pattern": "(?i)\\bexample-corp\\b", "replace": "acme"}, + {"pattern": "ORGID1", "replace": "0ACME0"}, + {"pattern": "(?i)Example Corporation", "replace": "Acme Insurance"} + ], + "forbid": ["example-corp", "myhost", "first.last", "orgid1", "example corporation"] +} diff --git a/scripts/demo/record.py b/scripts/demo/record.py new file mode 100644 index 0000000..95bf211 --- /dev/null +++ b/scripts/demo/record.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +"""Record the README demo as an asciinema v2 cast. + +usage: record.py [--masks FILE] [--no-mask] [scenes.json] [out.cast] + +Each scene types a command at a fake prompt, runs it in a pseudo-terminal, +optionally sends keystrokes (for TUIs) and records the output. Long API +waits are capped so the recording stays short. + +Sensitive text (host names, emails, organization names) is masked after +recording with same-length replacements, so tables stay aligned. The +masking rules are read from masks.local.json next to this script, which is +not committed: the rules name the very strings they hide. See +masks.example.json for the format and README.md for the full workflow. +""" +import argparse +import codecs +import fcntl +import json +import os +import pty +import random +import re +import select +import struct +import sys +import termios +import time + +HERE = os.path.dirname(os.path.abspath(__file__)) +MAX_GAP = 0.6 # cap for idle gaps while a command runs + + +class Recorder: + def __init__(self, cols, rows): + self.cols, self.rows = cols, rows + self.events = [] # [time, data] + self.clock = 0.0 + + def emit(self, data, dt=0.0): + self.clock += dt + self.events.append([round(self.clock, 4), data]) + + def prompt(self): + self.emit("\x1b[1;36m~/demo\x1b[0m \x1b[1;32m❯\x1b[0m ", 0.05) + + def type_text(self, text, lo=0.03, hi=0.08): + for ch in text: + self.emit(ch, random.uniform(lo, hi)) + + def comment(self, text): + self.prompt() + self.emit("\x1b[2m") + self.type_text("# " + text, 0.02, 0.045) + self.emit("\x1b[0m\r\n", 0.5) + + def command(self, shown, cmd, keys, env): + self.prompt() + self.clock += 0.4 + self.type_text(shown) + self.emit("\r\n", 0.35) + self.run(cmd, keys, env) + + def run(self, cmd, keys, env): + pid, fd = pty.fork() + if pid == 0: + os.environ.update(env) + os.execvp("bash", ["bash", "-c", cmd]) + fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", self.rows, self.cols, 0, 0)) + dec = codecs.getincrementaldecoder("utf-8")("replace") + last = [time.time()] + + def pump(seconds): + end = time.time() + seconds + while time.time() < end: + ready, _, _ = select.select([fd], [], [], 0.03) + if not ready: + continue + try: + data = os.read(fd, 65536) + except OSError: + return False + if not data: + return False + now = time.time() + self.clock += min(now - last[0], MAX_GAP) + last[0] = now + # Answer terminal queries as a dark terminal would. + if b"\x1b]11;?" in data: + os.write(fd, b"\x1b]11;rgb:1e1e/1e1e/2e2e\x1b\\") + if b"\x1b]10;?" in data: + os.write(fd, b"\x1b]10;rgb:cdcd/d6d6/f4f4\x1b\\") + if b"\x1b[6n" in data: + os.write(fd, b"\x1b[%d;1R" % self.rows) + if b"\x1b[c" in data: + os.write(fd, b"\x1b[?62;c") + text = dec.decode(data) + if text: + self.events.append([round(self.clock, 4), text]) + return True + + alive = pump(0.3) + for k in keys: + if not alive: + break + before = k.get("before", 0.6) + alive = pump(before) + last[0] = time.time() + self.clock += before + os.write(fd, k["k"].encode()) + alive = pump(k.get("after", 0.4)) + while alive: + alive = pump(1.0) + os.waitpid(pid, 0) + os.close(fd) + + +def fit(s, n): + """Pads or cuts s to exactly n characters.""" + return s[:n].ljust(n) + + +class Masker: + """Same-length masking from a rules file (see masks.example.json).""" + + def __init__(self, rules): + em = rules.get("emails", {}) + self.keep = {k.lower() for k in em.get("keep", [])} + self.names = {k.lower(): v for k, v in em.get("names", {}).items()} + self.suffixes = em.get("suffixes", []) + self.other = em.get("other", "user{n}") + self.domains = [(re.compile(p), r) for p, r in em.get("domains", {}).items()] + self.rules = [(re.compile(r["pattern"]), r["replace"]) for r in rules.get("rules", [])] + self.forbid = [f.lower() for f in rules.get("forbid", [])] + self.seen = {} + + def email(self, m): + local, domain = m.group(1), m.group(2) + if local.lower() not in self.keep: + suffix = next((s for s in self.suffixes if local.lower().endswith(s)), "") + base = local[: len(local) - len(suffix)] + if base.lower() not in self.seen: + fake = self.names.get(base.lower()) or self.other.format(n=len(self.seen) + 1) + self.seen[base.lower()] = fake + local = self.seen[base.lower()] + suffix + for pat, rep in self.domains: + domain = pat.sub(rep, domain) + out = local + "@" + domain + return fit(out, len(m.group(0))) if len(out) <= len(m.group(0)) else "x" * len(m.group(0)) + + def __call__(self, text): + text = re.sub(r"([A-Za-z0-9._%+-]+)@([A-Za-z0-9.-]+\.[A-Za-z]{2,})", self.email, text) + for pat, rep in self.rules: + text = pat.sub(lambda m, rep=rep: fit(m.expand(rep), len(m.group(0))), text) + return text + + def check(self, text): + low = text.lower() + return [f for f in self.forbid if f in low] + + +def main(): + ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) + ap.add_argument("scenes", nargs="?", default=os.path.join(HERE, "scenes.json")) + ap.add_argument("out", nargs="?", default=os.path.join(HERE, "..", "..", "docs", "demo", "iics-demo.cast")) + ap.add_argument("--masks", default=os.path.join(HERE, "masks.local.json")) + ap.add_argument("--no-mask", action="store_true", help="record without masking (for local use only)") + args = ap.parse_args() + + masker = None + if not args.no_mask: + if not os.path.exists(args.masks): + sys.exit(f"masking rules not found: {args.masks}\n" + "copy masks.example.json to masks.local.json and fill in your values, or pass --no-mask") + with open(args.masks) as f: + masker = Masker(json.load(f)) + + with open(args.scenes) as f: + spec = json.load(f) + cols, rows = spec.get("width", 140), spec.get("height", 34) + env = {"TERM": "xterm-256color", "COLORTERM": "truecolor", "COLUMNS": str(cols), "LINES": str(rows)} + env.update(spec.get("env", {})) + + rec = Recorder(cols, rows) + for sc in spec["scenes"]: + if "comment" in sc: + rec.comment(sc["comment"]) + continue + rec.command(sc.get("show", sc["cmd"]), sc["cmd"], sc.get("keys", []), env) + rec.clock += sc.get("pause", 2.5) + rec.prompt() + rec.emit("", 2.0) + + if masker: + # Mask the joined stream, so text split across output chunks is + # caught, then cut it back at the original chunk boundaries. + joined = "".join(e[1] for e in rec.events) + masked = masker(joined) + assert len(masked) == len(joined), "masking must keep the length" + left = masker.check(masked) + if left: + sys.exit(f"not written: unmasked text remains: {', '.join(left)}") + pos = 0 + for e in rec.events: + n = len(e[1]) + e[1] = masked[pos : pos + n] + pos += n + + os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True) + with open(args.out, "w") as f: + f.write(json.dumps({"version": 2, "width": cols, "height": rows, + "timestamp": int(time.time()), "idle_time_limit": 3, + "title": spec.get("title", "iics demo"), + "env": {"TERM": "xterm-256color", "SHELL": "/bin/bash"}}) + "\n") + for t, d in rec.events: + if d: + f.write(json.dumps([t, "o", d]) + "\n") + print(f"wrote {args.out}: {len(rec.events)} events, {rec.clock:.1f}s") + + +if __name__ == "__main__": + main() diff --git a/scripts/demo/scenes.json b/scripts/demo/scenes.json new file mode 100644 index 0000000..a4e59f3 --- /dev/null +++ b/scripts/demo/scenes.json @@ -0,0 +1,219 @@ +{ + "title": "iics - Informatica Cloud CLI", + "width": 140, + "height": 34, + "env": { + "IICS_PROFILE": "tst" + }, + "scenes": [ + { + "comment": "one profile per org; passwords stay in the OS keychain" + }, + { + "cmd": "iics profile list" + }, + { + "comment": "browse assets" + }, + { + "cmd": "iics objects list --type Project --limit 8" + }, + { + "comment": "secure agents and the services they run" + }, + { + "cmd": "iics agent list" + }, + { + "cmd": "iics agent details --name CAI-A03 --services", + "pause": 3.5 + }, + { + "comment": "service properties of a single agent, not only group defaults" + }, + { + "cmd": "iics agent configs get --name CAI-A03 --service 'Process Server' --type jvm", + "pause": 3 + }, + { + "cmd": "iics agent configs set --name CAI-A03 --set \"process-engine:jvm:max-heap='12G'\" --dry-run", + "pause": 3 + }, + { + "comment": "JSON output for scripts and pipelines" + }, + { + "cmd": "iics agent list -o json | jq -r '.[] | [.name, .agentHost, .agentVersion] | @tsv' | column -t" + }, + { + "comment": "interactive property editor (changes are reviewed before saving)" + }, + { + "cmd": "iics agent configs edit --name CAI-A03", + "keys": [ + { + "k": "/", + "before": 2.5 + }, + { + "k": "P", + "before": 0.15 + }, + { + "k": "r", + "before": 0.15 + }, + { + "k": "o", + "before": 0.15 + }, + { + "k": "c", + "before": 0.15 + }, + { + "k": "\r", + "before": 0.8 + }, + { + "k": "/", + "before": 1.8 + }, + { + "k": "m", + "before": 0.15 + }, + { + "k": "a", + "before": 0.15 + }, + { + "k": "x", + "before": 0.15 + }, + { + "k": "-", + "before": 0.15 + }, + { + "k": "h", + "before": 0.15 + }, + { + "k": "\r", + "before": 0.8 + }, + { + "k": "\r", + "before": 1.8 + }, + { + "k": "\u0015", + "before": 1.0 + }, + { + "k": "1", + "before": 0.25 + }, + { + "k": "2", + "before": 0.25 + }, + { + "k": "G", + "before": 0.25 + }, + { + "k": "\r", + "before": 0.8 + }, + { + "k": "\u001b", + "before": 1.8 + }, + { + "k": "\u001b", + "before": 1.8 + }, + { + "k": "/", + "before": 1.5 + }, + { + "k": "R", + "before": 0.15 + }, + { + "k": "e", + "before": 0.15 + }, + { + "k": "v", + "before": 0.15 + }, + { + "k": "\r", + "before": 0.6 + }, + { + "k": "\u001b", + "before": 3.5 + } + ], + "pause": 2.5 + }, + { + "comment": "or run iics with no arguments for the main menu" + }, + { + "cmd": "iics", + "keys": [ + { + "k": "\u001b[B", + "before": 2.5 + }, + { + "k": "\u001b[B", + "before": 0.5 + }, + { + "k": "\u001b[B", + "before": 0.5 + }, + { + "k": "/", + "before": 1.0 + }, + { + "k": "a", + "before": 0.15 + }, + { + "k": "g", + "before": 0.15 + }, + { + "k": "e", + "before": 0.15 + }, + { + "k": "n", + "before": 0.15 + }, + { + "k": "t", + "before": 0.15 + }, + { + "k": "\u001b", + "before": 2.5 + }, + { + "k": "q", + "before": 1.5 + } + ], + "pause": 1.5 + } + ] +} \ No newline at end of file From 4859fe0623824ab75187ad0953c7bbd9bf1eebec Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Tue, 20 Oct 2026 13:17:53 -0400 Subject: [PATCH 45/52] chore: update CHANGELOG for v0.6.0 release --- CHANGELOG.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1b0b4d..4e852dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,57 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.6.0] - 2026-10-20 + +First production-ready release. The core functionality (profiles and +sessions, asset export/import, release plans, publishing, and user, role, +permission and agent administration) is stable and is used for production +release management. From this release on, breaking changes to commands, +flags and output fields are called out in this changelog. + ### Added -- Main menu: **Delete profile** entry in the Session group picks a profile - from the list and runs `profile delete` for it +- Interactive main menu: running `iics` without arguments in a terminal + opens a menu of tasks grouped by area that runs the matching command, + with a profile switcher and a **Delete profile** entry (CR-0044) +- `agent configs get`, `set` and `edit` read and change the service + properties of a single Secure Agent, not only group defaults, through + the Administrator agent form. Supported: + - nested YAML/JSON change files and `--export`; + - custom properties (Custom Configuration Details), with `--add`, + `--remove` and `--add-missing`; + - `--reset` and `--reset-all` to return to defaults, like the web UI's + Reset All; + - `--dry-run` with a change table; + - an interactive editor with list-entry editing for multi-value + properties such as LDAP or database connection properties; + - a restart hint for services, such as Process Server, that need a + manual restart (CR-0049) +- `agent rename` sets a logical agent name after installation (CR-0049) +- `agent details -i` picks an agent, then a view: services summary, + details, full details or service properties (CR-0049) +- `role edit` interactive privilege editor, and `role create --from-role` + to copy an existing role (CR-0040) +- Interactive object permission (ACL) editor (CR-0042) +- `user update-roles`, `user update-groups` and the interactive `user edit` + (CR-0043) +- Configurable user name and email patterns for new users, with + per-profile overrides; the create wizard rejects existing user names + (CR-0045) +- Paged profile wizard and `config edit` for global settings (CR-0046) +- Connection management: `connection get --name`, `test`, `search`, + `objects` and `metadata`, partial updates with `update --partial`, and a + new `connector` command with `list` and `metadata` (CR-0047) +- `project update` and `folder update` use PATCH and gain an interactive + mode; `project list` and `folder list` show created and updated details + (CR-0047) +- Interactive agent installer download and installer info +- Shared TUI prompts and wizards (filterable pickers, checklists, paged + forms with a review step) for user, group and environment wizards + (CR-0041) +- `Esc` and `q` cancel prompts and wizards, and confirmations take a single + key (CR-0048) +- README demo recording and `make demo` to record it again ### Changed @@ -19,12 +66,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 source with `go install github.com/jbrazda/iics-cli/cmd/iics@latest`; the previous `go install github.com/jbrazda/iics-cli@latest` path no longer works. Release archives are unaffected. +- `user update` is removed: the API cannot update user properties. Use + `user edit`, `user update-roles` or `user update-groups` (CR-0043) +- `connection update` sends `POST` with the V2 `@type` discriminator, as + the API requires, instead of `PUT` (CR-0047) ### Fixed - Main menu: global flags given when starting the menu (such as `--config`, `--theme` or `--http-timeout`) are now passed on to the commands the menu runs; previously they were dropped, so commands read the default config +- Main menu clears the screen before each entry runs +- Role commands aligned with the V3 Roles API +- Permission commands aligned with the object permissions API +- User edit keeps the last direct role and skips roles inherited from + groups +- Project and folder names with spaces are encoded in update requests + (CR-0047) ## [0.5.6] - 2026-09-22 From fa77997809790067cffeac246a24750123c5f8b0 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Tue, 20 Oct 2026 13:24:40 -0400 Subject: [PATCH 46/52] chore: move CR-0049 to completed --- .../CR-0049-agent-level-service-properties.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/ChangeRequests/{pending => completed}/CR-0049-agent-level-service-properties.md (100%) diff --git a/docs/ChangeRequests/pending/CR-0049-agent-level-service-properties.md b/docs/ChangeRequests/completed/CR-0049-agent-level-service-properties.md similarity index 100% rename from docs/ChangeRequests/pending/CR-0049-agent-level-service-properties.md rename to docs/ChangeRequests/completed/CR-0049-agent-level-service-properties.md From eee0f742f7b19510166ca49836a57ef3faa15cb5 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Fri, 23 Oct 2026 09:50:51 -0400 Subject: [PATCH 47/52] fix: agent configs save via configFormJson; ignore whitespace-only overrides The Administrator UI now bundles the agent form's property rows into the configFormJson field (POP-88806); posting them as flat parameters exceeds Tomcat's maxParameterCount and the server answers HTTP 500 CMN_001. Pack the rows the same way when the form has that field. Values that differ from the default only in line endings, trailing whitespace or surrounding blank lines are no longer reported as overridden, and whitespace-only sets, sets to the default and resets of non-overridden properties are no longer shown as changes. --- CHANGELOG.md | 10 ++ docs/documentation/agent.md | 10 ++ ...ent-configs-set-http-500-configFormJson.md | 93 ++++++++++++++ internal/client/agentform.go | 102 ++++++++++++++- internal/client/agentform_test.go | 121 ++++++++++++++++++ internal/tui/agentconfigeditor.go | 2 +- 6 files changed, 331 insertions(+), 7 deletions(-) create mode 100644 docs/issues/pending/Bug-agent-configs-set-http-500-configFormJson.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e852dd..65ae7af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- `agent configs set`, `agent configs edit` and `agent rename` failed with + HTTP 500 after an Administrator UI change. The agent form's property rows + are now sent in the `configFormJson` field, as the UI sends them. +- `agent configs` no longer reports values that differ from the default only + in line endings or whitespace as overridden. Setting such a value, or + resetting a property that is not overridden, is no longer shown as a + change. + ## [0.6.0] - 2026-10-20 First production-ready release. The core functionality (profiles and diff --git a/docs/documentation/agent.md b/docs/documentation/agent.md index 6bc8470..913567b 100644 --- a/docs/documentation/agent.md +++ b/docs/documentation/agent.md @@ -328,6 +328,11 @@ Table mode prints the layout of `agent details --full`: an `Agent: header and a table with columns `TYPE` (with `/subType` for `agent` properties), `NAME`, `VALUE`, `DEFAULT`, `OVERRIDDEN`. Long values are wrapped. +A value that differs from its default only in line endings, trailing +whitespace on a line, or leading and trailing blank lines is shown as not +overridden, even when the server flags it as overridden. Indentation counts, +as it is significant in YAML values. + `--output csv` prints one flat table with an extra `SERVICE` column. `--output json` / `yaml` list the properties with `service`, `serviceName`, `type`, `subType`, `name`, `value`, `defaultValue`, `overridden`, @@ -393,6 +398,11 @@ Rules: on top. Check the list with `--dry-run` first. - Setting a property to its current value changes nothing, so a default does not turn into an override. Use `--reset` to go back to the default. + Values are compared ignoring line endings, trailing whitespace on a line, + and leading and trailing blank lines, so a YAML file with an extra trailing + newline is not a change. +- Setting a property to its default value resets it, and resetting a + property that is not overridden changes nothing. - A property can be changed only once per command. - If the agent's Secure Agent group has group-level properties, the group's settings replace the agent's when the agent is added to or restarted in the diff --git a/docs/issues/pending/Bug-agent-configs-set-http-500-configFormJson.md b/docs/issues/pending/Bug-agent-configs-set-http-500-configFormJson.md new file mode 100644 index 0000000..c6994f1 --- /dev/null +++ b/docs/issues/pending/Bug-agent-configs-set-http-500-configFormJson.md @@ -0,0 +1,93 @@ +# Bug: agent configs set fails with HTTP 500 - form rows must be packed into configFormJson + +## Summary + +`iics agent configs set` (and `agent configs edit`, `agent rename`) fail with +HTTP 500 `CMN_001` when saving the agent form. Informatica changed the +Administrator UI form at `/runtime/app/agent.do` (internal ticket POP-88806): +the property rows are no longer submitted as flat form parameters but bundled +into one hidden JSON field, `configFormJson`, because the flat form exceeded +Tomcat's `maxParameterCount`. The CLI still sent about 1,100 flat parameters, +so the server rejected the POST. + +## Error + +```text +Changes for agent CAI-A03 (01D23X0800000000008G): +... +Error: IICS API error (HTTP 500): +HTTP 500 Internal Server Error +... + { + "error": { + "code": "CMN_001", + "message": "Internal system error. Contact Informatica Global Customer Support.", + "requestId": "1Z71Pf2BP2gdRKnkdT7SIs" + } + } +``` + +The form GET still works; only the POST fails, with an upstream service time +of a few milliseconds. + +## Root Cause + +The UI's `doUpdate` now calls `pnAgent.packConfigFormParams(form)` from +`/runtime/js/pnAgent.js` before submitting. It collects every element named +in `configFormParamNames` (`sysConfig*`, `customConfig*`, `engineConfig*`, +`engineCustomConfigIdInput`, `isSensitiveConfig`), stores +`{"name": ["value", ...]}` in `configFormJson`, and disables the original +elements so they are not posted. Unchecked checkboxes are left out, and +names without elements are omitted. The server expands the JSON with +`ConfigFormRequestWrapper`. + +`AgentConfigForm.encode()` in `internal/client/agentform.go` posted all rows as +flat parameters and left `configFormJson` empty. + +## Fix + +`encode()` now passes the submitted fields through `packConfigFields`. When the +form contains a `configFormJson` field, the row fields are moved into it as +JSON in the UI's key order, with LF line endings as the browser's +`element.value` has. Forms without the field are still sent flat. + +Verified live on tst agent CAI-A03: setting +`process-engine:connectors:https-maxThreads=601` saves and reads back. All +other property values are unchanged. Four multi-line Process Server properties +(`server:ldap-properties`, `db:connection-properties`, +`db:logConnection-properties`, `logging:additional-logging`) whose values +equal their defaults were flagged overridden before the save and are now +reported as not overridden, the same as a save from the UI. + +## Follow-up: whitespace-only overrides + +The server flags a value as overridden when it differs from the default only +in line endings or whitespace. On tst CAI-A01 and CAI-A02 the four multi-line +Process Server properties above were listed by `--overridden` and exported by +`--export` for that reason alone. + +`client.SameValue` compares values ignoring CRLF/LF differences, trailing +whitespace on a line, and leading and trailing blank lines. Indentation +counts, as it is significant in YAML. It is used: + +- when the form is parsed, to clear the overridden flag of such values, so + `get --overridden`, `--export`, `--reset-all` and the editor agree +- in `Apply`, so a whitespace-only `--set` is no change, a value equal to the + default resets the property, and resetting a property that is not + overridden is no change +- when verifying a save, and in the TUI editor's pending-change check + +## Steps to Reproduce + +```bash +iics --profile tst agent configs set --name CAI-A03 \ + --set "process-engine:connectors:https-maxThreads=601" +``` + +## Expected Behaviour + +The property is saved and the restart hint is printed. + +## Actual Behaviour + +HTTP 500 `CMN_001` from `POST /runtime/app/agent.do`. diff --git a/internal/client/agentform.go b/internal/client/agentform.go index f6e55db..f33309f 100644 --- a/internal/client/agentform.go +++ b/internal/client/agentform.go @@ -4,6 +4,7 @@ import ( "context" "crypto/rand" "encoding/base64" + "encoding/json" "errors" "fmt" "html" @@ -117,6 +118,20 @@ func normalizeNewlines(s string) string { return strings.ReplaceAll(strings.ReplaceAll(s, "\r\n", "\n"), "\r", "\n") } +// SameValue reports whether a and b differ only in line endings, trailing +// whitespace on a line, or leading and trailing blank lines. Indentation +// counts, as it is significant in YAML values. +func SameValue(a, b string) bool { + norm := func(s string) string { + lines := strings.Split(normalizeNewlines(s), "\n") + for i, l := range lines { + lines[i] = strings.TrimRight(l, " \t") + } + return strings.Trim(strings.Join(lines, "\n"), "\n") + } + return a == b || norm(a) == norm(b) +} + func parseAttrs(s string) map[string]string { attrs := map[string]string{} for _, m := range formAttrRe.FindAllStringSubmatch(s, -1) { @@ -312,6 +327,12 @@ func parseAgentConfigForm(page string) (*AgentConfigForm, error) { if p.Custom && p.ServiceName == "" { p.ServiceName = form.serviceNames[p.serviceKey] } + // The server flags a value as overridden when it differs from the + // default only in line endings or whitespace, for example after a + // save with CRLF line breaks. Such values are the default. + if !p.Custom && p.Overridden && !p.Sensitive && SameValue(p.Value, p.DefaultValue) { + p.Overridden = false + } } return form, nil } @@ -413,10 +434,75 @@ func (f *AgentConfigForm) Reset(i int) { f.fields[p.overIdx].value = "false" } +// configFormJSONField is the hidden field that carries the property rows. +// The UI (pnAgent.packConfigFormParams) bundles the fields named in +// configFormParamNames into it as {"name": ["value", ...]} and does not send +// them as form parameters, because the flat form exceeds Tomcat's +// maxParameterCount and the server answers HTTP 500. +const configFormJSONField = "configFormJson" + +var configFormParamNames = []string{"sysConfigType", "sysConfigSubType", "sysConfigName", "sysConfigValue", + "sysConfigOverriden", "sysConfigDefault", "sysConfigSubTypeSel", "customConfigEngine", "customConfigType", + "customConfigSubType", "customConfigName", "customConfigValue", "customConfigTypeEngine", + "customConfigSensitive", "engineCustomConfigIdInput", "engineConfigKey", "engineConfigValue", + "engineConfigSensitive", "engineConfigDefault", "engineConfigOverriden", "isSensitiveConfig"} + +// packConfigFields moves the property row fields into the configFormJson +// field, as the UI does. Forms without that field are returned unchanged. +func packConfigFields(fields []formField) []formField { + jsonIdx := -1 + for i, fld := range fields { + if fld.name == configFormJSONField { + jsonIdx = i + break + } + } + if jsonIdx < 0 { + return fields + } + packed := map[string]bool{} + for _, n := range configFormParamNames { + packed[n] = true + } + values := map[string][]string{} + out := make([]formField, 0, len(fields)) + for _, fld := range fields { + if packed[fld.name] { + values[fld.name] = append(values[fld.name], normalizeNewlines(fld.value)) + continue + } + out = append(out, fld) + } + // Keys in the UI's order; names without fields are left out. + var b strings.Builder + b.WriteByte('{') + for _, n := range configFormParamNames { + v, ok := values[n] + if !ok { + continue + } + if b.Len() > 1 { + b.WriteByte(',') + } + k, _ := json.Marshal(n) + a, _ := json.Marshal(v) + b.Write(k) + b.WriteByte(':') + b.Write(a) + } + b.WriteByte('}') + for i := range out { + if out[i].name == configFormJSONField { + out[i].value = b.String() + } + } + return out +} + // encode builds the form body for actionMethod=update, keeping field order. func (f *AgentConfigForm) encode() string { var b strings.Builder - for i, fld := range f.submitFields() { + for i, fld := range packConfigFields(f.submitFields()) { v := fld.value switch fld.name { case "actionMethod": @@ -693,7 +779,7 @@ func (f *AgentConfigForm) ApplyWith(changes []AgentConfigChange, opts ApplyOptio p.removed = true diffs = append(diffs, AgentConfigDiff{Property: p.Key(), ServiceName: p.ServiceName, OldValue: old.Value, Sensitive: p.Sensitive, Custom: true, Change: "remove"}) - case value != old.Value: + case !SameValue(value, old.Value): p.Value = value if p.rowStart >= 0 { f.fields[p.valueIdx].value = value @@ -707,9 +793,13 @@ func (f *AgentConfigForm) ApplyWith(changes []AgentConfigChange, opts ApplyOptio return nil, fmt.Errorf("%s is a standard property and cannot be removed; use reset", p.Key()) } switch value := normalizeNewlines(ch.Value); { - case ch.Reset: - f.Reset(i) - case value != old.Value: + case ch.Reset || !p.Sensitive && SameValue(value, p.DefaultValue): + // A value equal to the default is reported as not overridden + // when read back, so it is stored as the default. + if old.Overridden { + f.Reset(i) + } + case !SameValue(value, old.Value): f.Set(i, value) } if p.Value == old.Value && p.Overridden == old.Overridden { @@ -749,7 +839,7 @@ func (c *Client) VerifyAgentConfigDiffs(ctx context.Context, agentID string, dif continue } p := form.Properties[i] - if p.Overridden != d.NewOverridden || !d.Sensitive && p.Value != d.NewValue { + if p.Overridden != d.NewOverridden || !d.Sensitive && !SameValue(p.Value, d.NewValue) { failed = append(failed, fmt.Sprintf("%s (now %q, overridden %t)", d.Property, p.Value, p.Overridden)) } } diff --git a/internal/client/agentform_test.go b/internal/client/agentform_test.go index 1eeb40d..a012b15 100644 --- a/internal/client/agentform_test.go +++ b/internal/client/agentform_test.go @@ -1,6 +1,7 @@ package client import ( + "encoding/json" "io" "net/http" "net/url" @@ -161,6 +162,126 @@ func TestAgentConfigFormApply(t *testing.T) { } } +func TestAgentConfigFormEncodePacked(t *testing.T) { + page := strings.Replace(agentPage("tok", "false", ""), `", "- key: testOnBorrow \r\n value: true\r\n\r\n", 1) + return strings.Replace(page, ``, + ``, 1) +} + +func TestWhitespaceOnlyOverride(t *testing.T) { + form, err := parseAgentConfigForm(whitespaceOverridePage()) + if err != nil { + t.Fatal(err) + } + i, err := form.Find(PropertySelector{Service: "process-engine", Type: "db", Name: "connection-properties"}) + if err != nil { + t.Fatal(err) + } + if form.Properties[i].Overridden { + t.Error("whitespace-only difference reported as overridden") + } + props, err := form.Filter(AgentConfigFilter{Service: "process-engine", Overridden: true}) + if err != nil { + t.Fatal(err) + } + if len(props) != 0 { + t.Errorf("--overridden lists %d properties, want 0", len(props)) + } + + diffs, err := form.Apply([]AgentConfigChange{ + {Property: "process-engine:db:connection-properties", Value: "- key: testOnBorrow\n value: true"}, + {Property: "process-engine:cluster:primary-node", Reset: true}, + }) + if err != nil { + t.Fatal(err) + } + if len(diffs) != 0 { + t.Errorf("got diffs for whitespace-only changes and a no-op reset: %+v", diffs) + } +} + +func TestSetToDefaultResets(t *testing.T) { + form, err := parseAgentConfigForm(agentPage("tok", "false", "")) + if err != nil { + t.Fatal(err) + } + if _, err = form.Apply([]AgentConfigChange{{Property: "process-engine:cluster:primary-node", Value: "false"}}); err != nil { + t.Fatal(err) + } + // primary-node is now overridden with false; setting the default back + // resets it. + diffs, err := form.Apply([]AgentConfigChange{{Property: "process-engine:cluster:primary-node", Value: "true\n"}}) + if err != nil { + t.Fatal(err) + } + if len(diffs) != 1 || diffs[0].NewValue != "true" || diffs[0].NewOverridden { + t.Errorf("diffs = %+v, want a reset to the default", diffs) + } +} + func TestAgentConfigFormApplyErrors(t *testing.T) { form, err := parseAgentConfigForm(agentPage("tok", "false", "")) if err != nil { diff --git a/internal/tui/agentconfigeditor.go b/internal/tui/agentconfigeditor.go index 9451b22..b10760e 100644 --- a/internal/tui/agentconfigeditor.go +++ b/internal/tui/agentconfigeditor.go @@ -128,7 +128,7 @@ func (e *agentConfigEditor) record(i int, ch client.AgentConfigChange) { e.pending[p.Key()] = client.AgentConfigChange{Property: p.Key(), Remove: true} return } - if ch.Reset && !p.Overridden && p.Value == p.DefaultValue || !ch.Reset && ch.Value == p.Value { + if ch.Reset && !p.Overridden || !ch.Reset && client.SameValue(ch.Value, p.Value) { delete(e.pending, p.Key()) return } From 9ee5bab305d3335e6e3cc4c57f4779ad2d4c288a Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Fri, 23 Oct 2026 10:29:49 -0400 Subject: [PATCH 48/52] fix: agent start/restart wait up to 20 minutes; label service subStates Process Server can take well over five minutes to become operational after a start, passing through RUNNING subState -1 and 2 before 0, so restart --blocking timed out with the old 300 second default. start and restart now default --max-wait-time to 1200 seconds (stop keeps 300), poll lines label subStates (-1 Initializing, 2 Starting, 0 Operational), and a timeout reports the last state seen. --- CHANGELOG.md | 5 ++++ cmd/agent_service.go | 56 +++++++++++++++++++++++++++++++------ docs/documentation/agent.md | 29 +++++++++++++++---- 3 files changed, 76 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65ae7af..d45ad86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 in line endings or whitespace as overridden. Setting such a value, or resetting a property that is not overridden, is no longer shown as a change. +- `agent restart --blocking` and `agent start --blocking` timed out while + Process Server was still starting. `--max-wait-time` now defaults to 1200 + seconds (20 minutes) for `start` and `restart` (`stop` keeps 300), poll + lines label subStates (`-1 - Initializing`, `2 - Starting`, + `0 - Operational`), and a timeout reports the last state seen. ## [0.6.0] - 2026-10-20 diff --git a/cmd/agent_service.go b/cmd/agent_service.go index 938df81..7ab75a1 100644 --- a/cmd/agent_service.go +++ b/cmd/agent_service.go @@ -24,7 +24,14 @@ type agentServiceFlags struct { maxWaitTime int } -func (f *agentServiceFlags) bind(cmd *cobra.Command, past string) { +// Default --max-wait-time values. Process Server can take well over five +// minutes to become operational after a start. +const ( + defaultStopWaitSec = 300 + defaultStartWaitSec = 1200 +) + +func (f *agentServiceFlags) bind(cmd *cobra.Command, past string, maxWaitSec int) { cmd.Flags().StringVar(&f.id, "id", "", "agent ID") cmd.Flags().StringVar(&f.name, "name", "", "agent name") cmd.Flags().StringVar(&f.hostname, "hostname", "", "agent host name") @@ -32,7 +39,7 @@ func (f *agentServiceFlags) bind(cmd *cobra.Command, past string) { cmd.Flags().BoolVarP(&f.interactive, "interactive", "i", false, "select the agent and service interactively") cmd.Flags().BoolVar(&f.blocking, "blocking", false, fmt.Sprintf("poll the service status until it has %s", past)) cmd.Flags().IntVar(&f.pollInterval, "poll-interval", 10, "seconds between status polls (with --blocking)") - cmd.Flags().IntVar(&f.maxWaitTime, "max-wait-time", 300, "maximum seconds to wait (with --blocking)") + cmd.Flags().IntVar(&f.maxWaitTime, "max-wait-time", maxWaitSec, "maximum seconds to wait (with --blocking)") cmd.MarkFlagsMutuallyExclusive("id", "name", "hostname") } @@ -149,7 +156,11 @@ func newAgentServiceCmd(use, short string, action client.AgentServiceAction, pas return nil }, } - f.bind(cmd, past) + maxWait := defaultStopWaitSec + if action == client.AgentServiceStart { + maxWait = defaultStartWaitSec + } + f.bind(cmd, past, maxWait) return cmd } @@ -163,7 +174,9 @@ func newAgentRestartCmd() *cobra.Command { The service is stopped and the command always waits for it to leave the agent's service listing, then it is started again. With --blocking (or after the interactive prompt) it also waits for the service to come back up (RUNNING with -subState 0). --max-wait-time bounds the whole operation.`, +subState 0). --max-wait-time bounds the whole operation; the default of 20 +minutes allows for services such as Process Server that take well over five +minutes to start.`, Example: ` iics agent restart --id --service "Data Integration Server" iics agent restart -i`, RunE: func(cmd *cobra.Command, args []string) error { @@ -223,7 +236,7 @@ subState 0). --max-wait-time bounds the whole operation.`, return nil }, } - f.bind(cmd, "restarted") + f.bind(cmd, "restarted", defaultStartWaitSec) return cmd } @@ -304,6 +317,20 @@ func serviceStates(d *client.AgentDetails, svc string) []client.AgentEngineStatu return out } +// subStateLabel describes an engine subState in the poll log. "2" is shown as +// a warning in the Administrator UI, but the service is still starting. +func subStateLabel(sub string) string { + switch sub { + case "0": + return "Operational" + case "-1": + return "Initializing" + case "2": + return "Starting" + } + return "" +} + func servicePending(status string) bool { switch strings.ToUpper(status) { case "NEED_RUNNING", "NEED_STOP", "DEPLOYING", "STARTING", "STOPPING": @@ -328,8 +355,8 @@ func serviceReached(verb string, states []client.AgentEngineStatus) (bool, error return false, nil } if strings.EqualFold(s.Status, "RUNNING") { - // subState "0" means the engine is fully operational; "-1" - // (and other values) mean it is still initializing. + // subState "0" means the engine is fully operational; "-1", + // "2" and other values mean it is still initializing. if s.SubState == "0" { operational = true } else { @@ -372,8 +399,15 @@ func waitForAgentService(ctx context.Context, c *client.Client, agentID, svc, ve if len(states) == 0 { _, _ = fmt.Fprintf(w, "%s%s: (not reported)\n", ts(), svc) } + var last []string for _, s := range states { - _, _ = fmt.Fprintf(w, "%s%s: %s (desired %s, subState %s)\n", ts(), svc, s.Status, s.DesiredStatus, s.SubState) + sub := s.SubState + if l := subStateLabel(sub); l != "" { + sub += " - " + l + } + line := fmt.Sprintf("%s (desired %s, subState %s)", s.Status, s.DesiredStatus, sub) + last = append(last, line) + _, _ = fmt.Fprintf(w, "%s%s: %s\n", ts(), svc, line) } done, rerr := serviceReached(verb, states) @@ -385,7 +419,11 @@ func waitForAgentService(ctx context.Context, c *client.Client, agentID, svc, ve return nil } if time.Now().After(deadline) { - return fmt.Errorf("timed out waiting for service %q to %s", svc, verb) + if len(last) == 0 { + last = []string{"not reported"} + } + return fmt.Errorf("timed out waiting for service %q to %s; last state: %s (raise --max-wait-time to wait longer)", + svc, verb, strings.Join(last, "; ")) } } } diff --git a/docs/documentation/agent.md b/docs/documentation/agent.md index 913567b..3a5ecde 100644 --- a/docs/documentation/agent.md +++ b/docs/documentation/agent.md @@ -690,7 +690,7 @@ Start or stop a service on a Secure Agent. Uses | `--interactive`, `-i` | bool | false | Select the agent and service interactively | | `--blocking` | bool | false | Poll the service status until it has started/stopped, printing each poll | | `--poll-interval` | int | 10 | Seconds between status polls (with `--blocking`) | -| `--max-wait-time` | int | 300 | Maximum seconds to wait (with `--blocking`) | +| `--max-wait-time` | int | 1200 | Maximum seconds to wait (with `--blocking`); `stop` defaults to 300 | Exactly one of `--id` / `--name` / `--hostname` is required, unless `--interactive` is used. `--interactive` lists the Secure Agents to choose from, @@ -704,8 +704,26 @@ asks for confirmation, and (unless `--blocking` was passed explicitly) asks With `--blocking` the command returns only once the service is fully started (`RUNNING` with `subState 0`) or stopped (gone from the listing), or -`--max-wait-time` elapses (non-zero exit). A service in `ERROR` also fails the -wait. +`--max-wait-time` elapses (non-zero exit, with the last state seen). A service +in `ERROR` also fails the wait. The default wait is 20 minutes for `start` and +5 minutes for `stop`, because services such as Process Server can take well +over five minutes to become operational. + +Each poll prints the status, desired status and subState. Known subStates are +labeled: + +| subState | Label | Meaning | +| -------- | -------------- | ---------------------------------------------------------------------------------------------------------- | +| `-1` | `Initializing` | The service has been deployed and is initializing | +| `2` | `Starting` | The service is still starting; the Administrator UI shows a warning state, but the wait continues | +| `0` | `Operational` | The service is fully started | + +```text +[2026-10-23 09:55:20.451] Process Server: RUNNING (desired RUNNING, subState -1 - Initializing) +[2026-10-23 09:56:24.102] Process Server: RUNNING (desired RUNNING, subState 2 - Starting) +[2026-10-23 10:01:44.530] Process Server: RUNNING (desired RUNNING, subState 0 - Operational) +[2026-10-23 10:01:44.530] Service "Process Server" started. +``` All [global flags](../../README.md#global-flags) apply. @@ -741,8 +759,9 @@ Same as `agent start` / `agent stop` The wait for the **stop** to complete is always performed. The wait for the **start** to complete happens with `--blocking`, or after answering yes to the interactive `Wait for service "" to start` prompt. `--max-wait-time` -bounds the whole operation (stop wait + start wait combined). "Fully started" -means the engine reports `RUNNING` with `subState 0`. +bounds the whole operation (stop wait + start wait combined) and defaults to +1200 seconds (20 minutes). "Fully started" means the engine reports `RUNNING` +with `subState 0`; see [subState labels](#agent-start--agent-stop). ### Examples From 9d8760013e1601bb472eca53120902dca740983e Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Fri, 23 Oct 2026 10:44:02 -0400 Subject: [PATCH 49/52] fix: agent details --full reports overridden properties like configs get The API's customized flag only marks custom properties (Custom Configuration Details); overridden standard properties always come back with customized false. Compute an overridden field from value and default with SameValue, and replace the CUSTOMIZED column with OVERRIDDEN (custom, yes or empty), matching agent configs get. --- CHANGELOG.md | 5 +++++ cmd/agent.go | 7 +++++-- docs/documentation/agent.md | 11 +++++++++-- internal/client/agents.go | 19 ++++++++++++++++++- internal/client/agents_test.go | 27 +++++++++++++++++++++++++++ 5 files changed, 64 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d45ad86..11e4e87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 seconds (20 minutes) for `start` and `restart` (`stop` keeps 300), poll lines label subStates (`-1 - Initializing`, `2 - Starting`, `0 - Operational`), and a timeout reports the last state seen. +- `agent details --full` showed every property as not customized, because the + API's `customized` flag only marks custom properties. The `CUSTOMIZED` + column is replaced by `OVERRIDDEN` (`custom`, `yes` or empty), computed from + value and default as in `agent configs get`, and JSON/YAML output adds an + `overridden` field. ## [0.6.0] - 2026-10-20 diff --git a/cmd/agent.go b/cmd/agent.go index d3d160e..e14692e 100644 --- a/cmd/agent.go +++ b/cmd/agent.go @@ -264,12 +264,15 @@ var agentEngineConfigCols = []output.Column{ {Header: "NAME", Field: "name", Width: 30}, {Header: "VALUE", Field: "value", Width: 40, Func: wrapConfigField("value")}, {Header: "DEFAULT", Field: "defaultValue", Width: 40, Func: wrapConfigField("defaultValue")}, - {Header: "CUSTOMIZED", Field: "customized", Width: 10, Func: func(v interface{}) string { + {Header: "OVERRIDDEN", Field: "overridden", Width: 10, Func: func(v interface{}) string { row, _ := v.(map[string]interface{}) if b, _ := row["customized"].(bool); b { + return "custom" + } + if b, _ := row["overridden"].(bool); b { return "yes" } - return "no" + return "" }}, } diff --git a/docs/documentation/agent.md b/docs/documentation/agent.md index 3a5ecde..6915761 100644 --- a/docs/documentation/agent.md +++ b/docs/documentation/agent.md @@ -185,13 +185,20 @@ Table mode prints, in order: a vertical status listing, and (with `--full`) a configuration table. Configuration tables have columns `TYPE`, `NAME`, `VALUE`, `DEFAULT`, -`CUSTOMIZED`. Long `VALUE` / `DEFAULT` cells are wrapped onto multiple lines. +`OVERRIDDEN`. Long `VALUE` / `DEFAULT` cells are wrapped onto multiple lines. +`OVERRIDDEN` follows [`agent configs get`](#agent-configs-get): `custom` for a +custom property (Custom Configuration Details), `yes` for a property whose +value differs from its default (ignoring line endings and whitespace), empty +otherwise. The API's own `customized` flag marks custom properties only, so it +is not used for overrides. With `--services`, only the `Agent:` summary and a single `Services (N):` table are printed, one row per service with columns `SERVICE`, `APP NAME`, `VERSION`, `STATUS`, `DESIRED`, `SUBSTATE`, `REPLACE`, `UPDATED`. -`--output json` / `yaml` render the full nested details document. +`--output json` / `yaml` render the full nested details document. Each +configuration property also has a computed `overridden` field, next to the +API's `customized` field. ### Examples diff --git a/internal/client/agents.go b/internal/client/agents.go index d451cc4..e5a9f27 100644 --- a/internal/client/agents.go +++ b/internal/client/agents.go @@ -50,7 +50,20 @@ type AgentEngineConfig struct { Value string `json:"value,omitempty"` Platform string `json:"platform,omitempty"` DefaultValue string `json:"defaultValue,omitempty"` - Customized bool `json:"customized,omitempty"` + // Customized is set by the API for custom properties (Custom + // Configuration Details), not for overridden standard properties. + Customized bool `json:"customized,omitempty"` + // Overridden is computed: the value differs from the default, ignoring + // line endings and whitespace (see SameValue). The API has no such flag. + Overridden bool `json:"overridden"` +} + +// markOverridden sets Overridden on standard properties whose value differs +// from the default, as agent configs get reports them. +func markOverridden(cfgs []AgentEngineConfig) { + for i := range cfgs { + cfgs[i].Overridden = !cfgs[i].Customized && !SameValue(cfgs[i].Value, cfgs[i].DefaultValue) + } } // AgentEngineStatus represents the status of a service (engine) running on the agent. @@ -144,6 +157,10 @@ func (c *Client) GetAgentDetails(ctx context.Context, id string, full bool) (*Ag if err := c.doJSONWithQuery(ctx, http.MethodGet, fmt.Sprintf("%s/agent/details/%s", BaseAPIPathV2, id), query, nil, &resp); err != nil { return nil, err } + markOverridden(resp.AgentConfigs) + for i := range resp.AgentEngines { + markOverridden(resp.AgentEngines[i].AgentEngineConfigs) + } return &resp, nil } diff --git a/internal/client/agents_test.go b/internal/client/agents_test.go index 6cc04ad..1d40acb 100644 --- a/internal/client/agents_test.go +++ b/internal/client/agents_test.go @@ -100,6 +100,33 @@ func TestGetAgentDetailsNested(t *testing.T) { if len(d.AgentConfigs) != 1 || !d.AgentConfigs[0].Customized { t.Errorf("agent configs not parsed: %+v", d.AgentConfigs) } + if !e.AgentEngineConfigs[0].Overridden { + t.Error("host-name differs from its default but is not overridden") + } + if d.AgentConfigs[0].Overridden { + t.Error("custom property reported as overridden") + } +} + +func TestGetAgentDetailsOverridden(t *testing.T) { + const body = `{"id":"a1","agentEngines":[{"agentEngineStatus":{"appDisplayName":"Process Server"}, + "agentEngineConfigs":[ + {"type":"connectors","name":"https-maxThreads","value":"601","defaultValue":"600","customized":false}, + {"type":"connectors","name":"http-maxThreads","value":"600","defaultValue":"600","customized":false}, + {"type":"db","name":"connection-properties","value":"- key: a \r\n value: b\r\n","defaultValue":"- key: a\n value: b","customized":false}]}]}` + c := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(body)) + })) + d, err := c.GetAgentDetails(context.Background(), "a1", true) + if err != nil { + t.Fatal(err) + } + want := []bool{true, false, false} + for i, cfg := range d.AgentEngines[0].AgentEngineConfigs { + if cfg.Overridden != want[i] { + t.Errorf("%s overridden = %t, want %t", cfg.Name, cfg.Overridden, want[i]) + } + } } func TestSetAgentServiceState(t *testing.T) { From 02fdb1536e2a846811f7241a352a578949bd0513 Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Fri, 23 Oct 2026 10:49:38 -0400 Subject: [PATCH 50/52] chore: move agent configs HTTP 500 bug to completed --- .../Bug-agent-configs-set-http-500-configFormJson.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/issues/{pending => completed}/Bug-agent-configs-set-http-500-configFormJson.md (100%) diff --git a/docs/issues/pending/Bug-agent-configs-set-http-500-configFormJson.md b/docs/issues/completed/Bug-agent-configs-set-http-500-configFormJson.md similarity index 100% rename from docs/issues/pending/Bug-agent-configs-set-http-500-configFormJson.md rename to docs/issues/completed/Bug-agent-configs-set-http-500-configFormJson.md From 3cd23f476f4cb06c28688bb2d0025b38686a74dc Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Fri, 23 Oct 2026 15:00:12 -0400 Subject: [PATCH 51/52] chore: version the commit and release skills The skills lived only under the gitignored .claude/ and were copied between machines by hand. Track .claude/skills/ (settings.local.json stays ignored). commit: dev-only branch guard, the full CLAUDE.md check list (gofmt, vet, golangci-lint, tests), docs/completions/CHANGELOG and CR/bug move checks, staging by path instead of git add -A, no Co-Authored-By trailer. release: plain go instead of the MacPorts path, asks before moving pending CRs and bugs, and lands dev on main through a release PR - fast-forward by default (keeps every commit, no back-merge), squash on request - instead of an ff push that only worked through the ruleset bypass. --- .claude/skills/commit/SKILL.md | 56 ++++++++++++++++ .claude/skills/release/SKILL.md | 114 ++++++++++++++++++++++++++++++++ .gitignore | 5 +- CLAUDE.md | 6 +- 4 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 .claude/skills/commit/SKILL.md create mode 100644 .claude/skills/release/SKILL.md diff --git a/.claude/skills/commit/SKILL.md b/.claude/skills/commit/SKILL.md new file mode 100644 index 0000000..0714d04 --- /dev/null +++ b/.claude/skills/commit/SKILL.md @@ -0,0 +1,56 @@ +--- +name: commit +description: Commit work in the iics-cli repo with the project's checks - branch guard (dev only), gofmt, go vet, golangci-lint, tests, docs and completions, CR and bug file moves, then a conventional commit without a Co-Authored-By trailer. Use when asked to commit, save, or check in changes in this repo, including "commit and push". +--- + +# Commit + +Follow these steps in order. Stop and report on any failure. + +## 1. Branch guard + +Run `git branch --show-current`. Continue only on `dev`; otherwise stop and ask. + +## 2. Checks + +```bash +gofmt -s -w . +go vet ./... +golangci-lint run ./... +go test ./... +``` + +Fix what they report, then run them again. If `go.mod` changed, check that its +Go version matches `go-version` in `.github/workflows/*.yml`. + +## 3. Completeness + +Check the diff (`git status --short`, `git diff --stat`) against these rules: + +- A command or flag was added or changed: `docs/documentation/.md` + and the `README.md` commands table are updated, and `make completions` was + run. Commit the `completions/` changes with the code. +- A user-visible fix or feature: `CHANGELOG.md` has an entry under + `## [Unreleased]`. +- A CR was implemented: its file moves from `docs/ChangeRequests/new/` to + `pending/` in the same commit. A bug was fixed: same for `docs/issues/`. + Move files to `completed/` only when the user confirms. +- Markdown files were changed: they lint clean (`markdown-lint` skill). + +Report anything missing and fix it, or ask if it needs a decision. + +## 4. Stage and commit + +- Stage the files of this change by path (`git add `). Do not use + `git add -A`; leave unrelated or unknown files unstaged and mention them. +- Write a conventional commit message (`feat:`, `fix:`, `docs:`, `chore:`, + `refactor:`, `test:`; scope optional, CR number in the subject when there is + one, for example `feat(agent): ... (CR-0049)`). The body says why. +- No `Co-Authored-By` trailer (project rule; it overrides the default + attribution). +- Commit with a heredoc (`git commit -F - <<'EOF'`), then show + `git log --oneline -1` and `git status --short`. + +## 5. Push + +Push only when the user asked for it: `git push origin dev`. diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md new file mode 100644 index 0000000..59c4fa2 --- /dev/null +++ b/.claude/skills/release/SKILL.md @@ -0,0 +1,114 @@ +--- +name: release +description: Release a new iics-cli version from dev - checks, CHANGELOG, a release PR to main, fast-forward (keeps every commit) or squash merge, tag, GoReleaser, and CI monitoring. Use when asked to release, cut, tag or publish a version, or to merge dev into main for a release. +--- + +# Release + +The release goes from `dev` to `main` through a pull request, which records the +release and runs CI on it. How it lands on `main`: + +- **Fast-forward (default):** push `dev` to `main`. Every commit is kept, + `main` and `dev` stay identical, and GitHub marks the PR as merged because + its commits are now on `main`. Needs your bypass of the `main` ruleset while + it requires pull requests. +- **Squash (when the user asks for one commit):** `gh pr merge --squash`, then + merge `main` back into `dev`. + +Stop and report on any failure. Ask before every step that pushes. + +## 1. Branch guard and sync + +```bash +git branch --show-current # must be dev +git fetch origin +git status --short # must be clean +git merge-base --is-ancestor origin/main dev # fast-forward possible +``` + +If `dev` is behind `origin/dev`, stop. If `main` is not an ancestor of `dev` +(a squash release or a commit made on `main`), a fast-forward is impossible: +report it and offer squash, or `git merge origin/main` into `dev` first. + +## 2. Checks + +```bash +gofmt -s -l . # must print nothing +go vet ./... +golangci-lint run ./... +go test -race ./... +``` + +Check that the Go version in `go.mod` matches `go-version` in +`.github/workflows/*.yml`, and that `make completions` leaves no changes. + +## 3. Pending CRs and bugs + +List the files in `docs/ChangeRequests/pending/` and `docs/issues/pending/` +with the commit that implemented each (`git log --oneline -- `). Ask the +user which ones are confirmed. Move only those with `git mv` to `completed/` +and commit: `chore: move to completed`. + +## 4. Version + +Show the latest tag (`git tag --sort=-version:refname | head -1`) and the +commits since it (`git log --oneline ..dev`). Suggest a version: minor +for features or breaking changes, patch for fixes only. Ask the user to +confirm; it must match `v\d+\.\d+\.\d+`. + +## 5. CHANGELOG + +In `CHANGELOG.md`, rename `## [Unreleased]` to `## [X.Y.Z] - YYYY-MM-DD` +(today) and add an empty `## [Unreleased]` above it. Edit in place; do not +rewrite the file. Check that breaking changes to commands, flags or output +fields are called out. Lint the file (`markdown-lint` skill), then commit +`chore: update CHANGELOG for vX.Y.Z release` (no `Co-Authored-By` trailer) +and push `dev`. + +## 6. Release PR + +```bash +gh pr create --base main --head dev --title "Release vX.Y.Z" --body-file +``` + +The body: one line on what the release is, the merge method, and the +CHANGELOG section for the version. End it with the PR attribution line. Wait +for the PR checks (`gh pr checks --watch`); stop if any fail. + +## 7. Land on main + +Fast-forward: + +```bash +git push origin dev:main +gh pr view --json state # MERGED +``` + +Squash, only when asked: + +```bash +gh pr merge --squash --subject "Release vX.Y.Z (#)" +git fetch origin +git merge --no-edit origin/main # on dev +git push origin dev +``` + +## 8. Tag + +Tag the commit now on `main` and push the tag; it starts the GoReleaser +workflow. + +```bash +git tag -a vX.Y.Z -m "Release vX.Y.Z" origin/main +git push origin vX.Y.Z +``` + +## 9. Monitor + +```bash +gh run list --workflow Release --limit 3 +gh run watch --exit-status +gh release view vX.Y.Z +``` + +Report the result of the Release job and the release URL with its assets. diff --git a/.gitignore b/.gitignore index 2ae3407..d9c0202 100644 --- a/.gitignore +++ b/.gitignore @@ -44,8 +44,9 @@ dist/ # Debug binaries __debug_bin* -# Claude Code runtime artifacts -.claude/ +# Claude Code runtime artifacts; the project skills are versioned +.claude/* +!.claude/skills/ target/ # Demo recording masking rules (they name the values they hide) diff --git a/CLAUDE.md b/CLAUDE.md index 65525d7..49bb1e4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -85,6 +85,8 @@ golangci-lint run ./... ## Commit Style +- Commit with the `commit` skill and release with the `release` skill + (`.claude/skills/`); they carry the full checklists below. - Do **not** include `Co-Authored-By: Claude ...` trailers in commit messages. - **Before every commit**, run the following and fix all reported issues: @@ -146,7 +148,9 @@ Commit the updated files in `completions/` together with the code change. ## Markdown Rules -- Generate Markdown that follows the [Markdown Lint Rules](https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md) +- Generate Markdown that follows the [Markdown Lint Rules](https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md). + `.markdownlint-cli2.yaml` holds the project rules; a global hook formats and + lints every Markdown file Claude writes (prettier, then markdownlint-cli2). - Do **not** use em dashes (`-`) in generated Markdown; use a regular hyphen (`-`) instead See [docs/CLAUDE.md](docs/CLAUDE.md) for complete code templates for all four files. From 47a1fdfb17779efa352897613089d222e3e4914f Mon Sep 17 00:00:00 2001 From: Jaroslav Brazda Date: Fri, 23 Oct 2026 20:00:43 -0400 Subject: [PATCH 52/52] chore: update CHANGELOG for v0.6.1 release --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11e4e87..eee912d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.6.1] - 2026-10-23 + ### Fixed - `agent configs set`, `agent configs edit` and `agent rename` failed with