Skip to content
This repository was archived by the owner on Feb 25, 2026. It is now read-only.

Commit 51a223d

Browse files
committed
feat: user command and request timeout flag
Added a new `user` command to the CLI, allowing the management of WorkOS users directly from the command line. This includes creating, listing, and deleting users. Additionally, introduced a `--request-timeout` flag to the root command, enabling users to specify a custom timeout for API requests made by the CLI. The default timeout is set to 10 seconds. Update pflag to the latest version.
1 parent 980df99 commit 51a223d

5 files changed

Lines changed: 306 additions & 5 deletions

File tree

README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,31 @@ workos env remove
4747
Once initialized, the CLI is ready to use:
4848

4949
```shell
50-
workos [cmd] [args]
50+
workos
51+
The WorkOS CLI is a tool to interact with WorkOS APIs via the command line.
52+
53+
Usage:
54+
workos [command]
55+
56+
Available Commands:
57+
completion Generate the autocompletion script for the specified shell
58+
env Manage configured environments
59+
fga Manage FGA resources (resource types, warrants, and resources).
60+
help Help about any command
61+
init Initialize the CLI
62+
organization Manage organizations (create, update, delete, etc).
63+
user Manage users (get, list, update, delete, etc).
64+
65+
Flags:
66+
-h, --help help for workos
67+
--timeout duration Timeout for commands (default 10s)
68+
-v, --version version for workos
69+
70+
Use "workos [command] --help" for more information about a command.
5171
```
5272

5373
### Environment Variables
74+
5475
WorkOS CLI support environment variables for initialization and environment management.
5576

5677
| Environment Variable | Description | Supported Values |
@@ -70,6 +91,7 @@ export WORKOS_ACTIVE_ENVIRONMENT=local
7091
```
7192

7293
`.workos.json`
94+
7395
```json
7496
{
7597
"environments": {

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ require (
4848
github.com/sourcegraph/conc v0.3.0 // indirect
4949
github.com/spf13/afero v1.11.0 // indirect
5050
github.com/spf13/cast v1.6.0 // indirect
51-
github.com/spf13/pflag v1.0.5 // indirect
51+
github.com/spf13/pflag v1.0.10 // indirect
5252
github.com/stretchr/testify v1.9.0 // indirect
5353
github.com/subosito/gotenv v1.6.0 // indirect
5454
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect

go.sum

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,9 @@ github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
9696
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
9797
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
9898
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
99-
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
10099
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
100+
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
101+
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
101102
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
102103
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
103104
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

internal/cmd/root.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,45 @@ package cmd
33
import (
44
"context"
55
"log"
6+
"time"
67

78
"github.com/spf13/cobra"
89
"github.com/workos/workos-cli/internal/config"
910
"github.com/workos/workos-go/v4/pkg/fga"
1011
"github.com/workos/workos-go/v4/pkg/organizations"
12+
"github.com/workos/workos-go/v4/pkg/usermanagement"
1113
)
1214

13-
var cmdConfig *config.Config
15+
const (
16+
// defaultTimeout is the default timeout for commands.
17+
defaultTimeout = 10 * time.Second
18+
)
19+
20+
var (
21+
// requestTimeout is the timeout for commands, configurable via a flag.
22+
requestTimeout time.Duration
23+
24+
// cmdConfig holds the CLI configuration loaded from disk, including configured environments and the active environment.
25+
cmdConfig *config.Config
26+
)
1427

1528
// rootCmd represents the base command when called without any subcommands
1629
var rootCmd = &cobra.Command{
1730
Use: "workos",
1831
Short: "WorkOS Command Line Interface (CLI)",
1932
Long: "The WorkOS CLI is a tool to interact with WorkOS APIs via the command line.",
33+
PersistentPreRun: func(cmd *cobra.Command, args []string) {
34+
if requestTimeout > 0 {
35+
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
36+
cobra.OnFinalize(cancel)
37+
cmd.SetContext(ctx)
38+
}
39+
},
2040
}
2141

2242
func init() {
2343
cobra.OnInitialize(initConfig)
44+
rootCmd.PersistentFlags().DurationVar(&requestTimeout, "timeout", defaultTimeout, "Timeout for commands")
2445
}
2546

2647
func SetVersion(version string) {
@@ -30,7 +51,7 @@ func SetVersion(version string) {
3051
// Execute adds all child commands to the root command and sets flags appropriately.
3152
// This is called by main.main(). It only needs to happen once to the rootCmd.
3253
func Execute() {
33-
cobra.CheckErr(rootCmd.ExecuteContext(context.Background()))
54+
cobra.CheckErr(rootCmd.Execute())
3455
}
3556

3657
func GetConfigOrExit() *config.Config {
@@ -50,8 +71,10 @@ func initConfig() {
5071
cmdConfig = config.LoadConfig()
5172
organizations.SetAPIKey(cmdConfig.Environments[cmdConfig.ActiveEnvironment].ApiKey)
5273
fga.SetAPIKey(cmdConfig.Environments[cmdConfig.ActiveEnvironment].ApiKey)
74+
usermanagement.SetAPIKey(cmdConfig.Environments[cmdConfig.ActiveEnvironment].ApiKey)
5375
if cmdConfig.Environments[cmdConfig.ActiveEnvironment].Endpoint != "" {
5476
organizations.DefaultClient.Endpoint = cmdConfig.Environments[cmdConfig.ActiveEnvironment].Endpoint
5577
fga.DefaultClient.Endpoint = cmdConfig.Environments[cmdConfig.ActiveEnvironment].Endpoint
78+
usermanagement.DefaultClient.Endpoint = cmdConfig.Environments[cmdConfig.ActiveEnvironment].Endpoint
5679
}
5780
}

internal/cmd/user.go

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/cobra"
7+
"github.com/workos/workos-cli/internal/list"
8+
"github.com/workos/workos-cli/internal/printer"
9+
"github.com/workos/workos-go/v4/pkg/usermanagement"
10+
)
11+
12+
const (
13+
flagEmail = "email"
14+
flagFirstName = "first-name"
15+
flagLastName = "last-name"
16+
flagEmailVerified = "email-verified"
17+
flagPassword = "password"
18+
flagExternalID = "external-id"
19+
flagOrganization = "organization"
20+
)
21+
22+
func init() {
23+
rootCmd.AddCommand(newUserCmd())
24+
}
25+
26+
// newUserCmd returns the user command with all subcommands.
27+
func newUserCmd() *cobra.Command {
28+
cmd := &cobra.Command{
29+
Use: "user",
30+
Short: "Manage users (get, list, update, delete, etc).",
31+
Long: "Get, list, update, and delete users.",
32+
}
33+
34+
cmd.AddCommand(newGetUserCmd())
35+
cmd.AddCommand(newListUsersCmd())
36+
cmd.AddCommand(newUpdateUserCmd())
37+
cmd.AddCommand(newDeleteUserCmd())
38+
39+
return cmd
40+
}
41+
42+
// newGetUserCmd returns a command to get a user by ID.
43+
func newGetUserCmd() *cobra.Command {
44+
return &cobra.Command{
45+
Use: "get <user_id>",
46+
Short: "Get a user by ID",
47+
Long: "Get a user by their unique identifier.",
48+
Example: "workos user get user_01E3JC5F5Z1YJNPGVYWV9SX6GH",
49+
Args: cobra.ExactArgs(1),
50+
RunE: func(cmd *cobra.Command, args []string) error {
51+
userID := args[0]
52+
53+
user, err := usermanagement.GetUser(
54+
cmd.Context(),
55+
usermanagement.GetUserOpts{
56+
User: userID,
57+
},
58+
)
59+
if err != nil {
60+
return fmt.Errorf("get user: %w", err)
61+
}
62+
63+
printer.PrintJson(user)
64+
return nil
65+
},
66+
}
67+
}
68+
69+
// newListUsersCmd returns a command to list users with optional filters.
70+
func newListUsersCmd() *cobra.Command {
71+
cmd := &cobra.Command{
72+
Use: "list",
73+
Short: "List users with optional filters",
74+
Long: "List users, optionally filtering by email, organization, limit, before/after cursor, and order (asc/desc).",
75+
Example: `workos user list --email user@example.com --limit 10
76+
workos user list --organization org_01EHZNVPK3SFK441A1RGBFSHRT --order desc`,
77+
RunE: func(cmd *cobra.Command, args []string) error {
78+
flags := cmd.Flags()
79+
email, err := flags.GetString(flagEmail)
80+
if err != nil {
81+
return fmt.Errorf("email flag: %w", err)
82+
}
83+
84+
organizationID, err := flags.GetString(flagOrganization)
85+
if err != nil {
86+
return fmt.Errorf("organization flag: %w", err)
87+
}
88+
89+
after, err := flags.GetString(list.FlagAfter)
90+
if err != nil {
91+
return fmt.Errorf("after flag: %w", err)
92+
}
93+
94+
before, err := flags.GetString(list.FlagBefore)
95+
if err != nil {
96+
return fmt.Errorf("before flag: %w", err)
97+
}
98+
99+
limit, err := flags.GetInt(list.FlagLimit)
100+
if err != nil {
101+
return fmt.Errorf("limit flag: %w", err)
102+
}
103+
104+
order, err := flags.GetString(list.FlagOrder)
105+
if err != nil {
106+
return fmt.Errorf("order flag: %w", err)
107+
}
108+
109+
users, err := usermanagement.ListUsers(
110+
cmd.Context(),
111+
usermanagement.ListUsersOpts{
112+
Email: email,
113+
OrganizationID: organizationID,
114+
Limit: limit,
115+
Before: before,
116+
After: after,
117+
Order: usermanagement.Order(order),
118+
},
119+
)
120+
if err != nil {
121+
return fmt.Errorf("list users: %w", err)
122+
}
123+
124+
tbl := printer.NewTable(140).Headers(
125+
printer.TableHeader("ID"),
126+
printer.TableHeader("Email"),
127+
printer.TableHeader("First Name"),
128+
printer.TableHeader("Last Name"),
129+
printer.TableHeader("Verified"),
130+
)
131+
for _, user := range users.Data {
132+
verified := "No"
133+
if user.EmailVerified {
134+
verified = "Yes"
135+
}
136+
tbl.Row(
137+
user.ID,
138+
user.Email,
139+
user.FirstName,
140+
user.LastName,
141+
verified,
142+
)
143+
}
144+
145+
printer.PrintMsg(tbl.Render())
146+
printer.PrintMsg(fmt.Sprintf("Before: %s", users.ListMetadata.Before))
147+
printer.PrintMsg(fmt.Sprintf("After: %s", users.ListMetadata.After))
148+
return nil
149+
},
150+
}
151+
152+
flags := cmd.Flags()
153+
flags.String(flagEmail, "", "Filter by email")
154+
flags.String(flagOrganization, "", "Filter by organization ID")
155+
flags.String(list.FlagAfter, "", "Cursor for results after a specific item")
156+
flags.String(list.FlagBefore, "", "Cursor for results before a specific item")
157+
flags.Int(list.FlagLimit, 0, "Limit the number of results")
158+
flags.String(list.FlagOrder, "", "Order of results (asc or desc)")
159+
160+
return cmd
161+
}
162+
163+
// newUpdateUserCmd returns a command to update a user's attributes.
164+
func newUpdateUserCmd() *cobra.Command {
165+
cmd := &cobra.Command{
166+
Use: "update <user_id>",
167+
Short: "Update a user",
168+
Long: "Update a user's attributes such as first name, last name, email verification status, password, or external ID.",
169+
Example: `workos user update user_01E3JC5F5Z1YJNPGVYWV9SX6GH --first-name John --last-name Doe
170+
workos user update user_01E3JC5F5Z1YJNPGVYWV9SX6GH --email-verified`,
171+
Args: cobra.ExactArgs(1),
172+
RunE: func(cmd *cobra.Command, args []string) error {
173+
userID := args[0]
174+
flags := cmd.Flags()
175+
176+
firstName, err := flags.GetString(flagFirstName)
177+
if err != nil {
178+
return fmt.Errorf("first-name flag: %w", err)
179+
}
180+
181+
lastName, err := flags.GetString(flagLastName)
182+
if err != nil {
183+
return fmt.Errorf("last-name flag: %w", err)
184+
}
185+
186+
emailVerified, err := flags.GetBool(flagEmailVerified)
187+
if err != nil {
188+
return fmt.Errorf("email-verified flag: %w", err)
189+
}
190+
191+
password, err := flags.GetString(flagPassword)
192+
if err != nil {
193+
return fmt.Errorf("password flag: %w", err)
194+
}
195+
196+
externalID, err := flags.GetString(flagExternalID)
197+
if err != nil {
198+
return fmt.Errorf("external-id flag: %w", err)
199+
}
200+
201+
opts := usermanagement.UpdateUserOpts{
202+
User: userID,
203+
FirstName: firstName,
204+
LastName: lastName,
205+
EmailVerified: emailVerified,
206+
Password: password,
207+
ExternalID: externalID,
208+
}
209+
210+
user, err := usermanagement.UpdateUser(cmd.Context(), opts)
211+
if err != nil {
212+
return fmt.Errorf("update user: %w", err)
213+
}
214+
215+
printer.PrintMsg("Updated user")
216+
printer.PrintJson(user)
217+
return nil
218+
},
219+
}
220+
221+
flags := cmd.Flags()
222+
flags.String(flagFirstName, "", "First name")
223+
flags.String(flagLastName, "", "Last name")
224+
flags.Bool(flagEmailVerified, false, "Email verification status")
225+
flags.String(flagPassword, "", "New password")
226+
flags.String(flagExternalID, "", "External ID")
227+
228+
return cmd
229+
}
230+
231+
// newDeleteUserCmd returns a command to delete a user by ID.
232+
func newDeleteUserCmd() *cobra.Command {
233+
return &cobra.Command{
234+
Use: "delete <user_id>",
235+
Short: "Delete a user",
236+
Long: "Delete a user by their unique identifier.",
237+
Example: "workos user delete user_01E3JC5F5Z1YJNPGVYWV9SX6GH",
238+
Args: cobra.ExactArgs(1),
239+
RunE: func(cmd *cobra.Command, args []string) error {
240+
userID := args[0]
241+
err := usermanagement.DeleteUser(
242+
cmd.Context(),
243+
usermanagement.DeleteUserOpts{
244+
User: userID,
245+
},
246+
)
247+
if err != nil {
248+
return fmt.Errorf("delete user: %w", err)
249+
}
250+
251+
printer.PrintMsg(fmt.Sprintf("Deleted user %s", userID))
252+
return nil
253+
},
254+
}
255+
}

0 commit comments

Comments
 (0)