-
Notifications
You must be signed in to change notification settings - Fork 9
feat(cli): add projects support with --project flag and name resolution #147
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
9126ebb
dac2b2a
73aca64
b110ced
e55a6c3
4024bbb
8d3fa6a
3093664
ac2bea2
a20844d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,188 @@ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "fmt" | ||
|
|
||
| "github.com/kernel/kernel-go-sdk" | ||
| "github.com/kernel/kernel-go-sdk/packages/param" | ||
|
|
||
| "github.com/pterm/pterm" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| var projectsCmd = &cobra.Command{ | ||
| Use: "projects", | ||
| Short: "Manage projects", | ||
| Run: func(cmd *cobra.Command, args []string) { | ||
| _ = cmd.Help() | ||
| }, | ||
| } | ||
|
|
||
| var projectsListCmd = &cobra.Command{ | ||
| Use: "list", | ||
| Short: "List projects", | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| client := getKernelClient(cmd) | ||
| ctx := cmd.Context() | ||
|
|
||
| projects, err := client.Projects.List(ctx, kernel.ProjectListParams{}) | ||
| if err != nil { | ||
| pterm.Error.Println("Failed to list projects:", err) | ||
| return nil | ||
| } | ||
|
|
||
| if len(projects.Items) == 0 { | ||
| pterm.Info.Println("No projects found") | ||
| return nil | ||
| } | ||
|
|
||
| table := pterm.TableData{{"ID", "Name", "Status", "Created At"}} | ||
| for _, p := range projects.Items { | ||
| table = append(table, []string{p.ID, p.Name, string(p.Status), p.CreatedAt.String()}) | ||
| } | ||
| _ = pterm.DefaultTable.WithHasHeader(true).WithData(table).Render() | ||
| return nil | ||
| }, | ||
| } | ||
|
|
||
| var projectsCreateCmd = &cobra.Command{ | ||
| Use: "create <name>", | ||
| Short: "Create a project", | ||
| Args: cobra.ExactArgs(1), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| client := getKernelClient(cmd) | ||
| ctx := cmd.Context() | ||
|
|
||
| project, err := client.Projects.New(ctx, kernel.ProjectNewParams{ | ||
| CreateProjectRequest: kernel.CreateProjectRequestParam{ | ||
| Name: args[0], | ||
| }, | ||
| }) | ||
| if err != nil { | ||
| pterm.Error.Println("Failed to create project:", err) | ||
| return nil | ||
| } | ||
|
|
||
| pterm.Success.Printf("Created project: %s (ID: %s)\n", project.Name, project.ID) | ||
| return nil | ||
| }, | ||
| } | ||
|
|
||
| var projectsGetCmd = &cobra.Command{ | ||
| Use: "get <id>", | ||
| Short: "Get a project by ID", | ||
| Args: cobra.ExactArgs(1), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| client := getKernelClient(cmd) | ||
| ctx := cmd.Context() | ||
|
|
||
| project, err := client.Projects.Get(ctx, args[0]) | ||
| if err != nil { | ||
| pterm.Error.Println("Failed to get project:", err) | ||
| return nil | ||
| } | ||
|
|
||
| table := pterm.TableData{ | ||
| {"Field", "Value"}, | ||
| {"ID", project.ID}, | ||
| {"Name", project.Name}, | ||
| {"Status", string(project.Status)}, | ||
| {"Created At", project.CreatedAt.String()}, | ||
| {"Updated At", project.UpdatedAt.String()}, | ||
| } | ||
| _ = pterm.DefaultTable.WithHasHeader(true).WithData(table).Render() | ||
| return nil | ||
| }, | ||
| } | ||
|
|
||
| var projectsDeleteCmd = &cobra.Command{ | ||
| Use: "delete <id>", | ||
| Short: "Delete a project", | ||
| Args: cobra.ExactArgs(1), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| client := getKernelClient(cmd) | ||
| ctx := cmd.Context() | ||
|
|
||
| err := client.Projects.Delete(ctx, args[0]) | ||
| if err != nil { | ||
| pterm.Error.Println("Failed to delete project:", err) | ||
| return nil | ||
| } | ||
|
|
||
| pterm.Success.Printf("Deleted project: %s\n", args[0]) | ||
| return nil | ||
| }, | ||
| } | ||
|
|
||
| var projectsLimitsGetCmd = &cobra.Command{ | ||
| Use: "get-limits <project-id>", | ||
| Short: "Get project limit overrides", | ||
| Args: cobra.ExactArgs(1), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| client := getKernelClient(cmd) | ||
| ctx := cmd.Context() | ||
|
|
||
| limits, err := client.Projects.Limits.Get(ctx, args[0]) | ||
| if err != nil { | ||
| pterm.Error.Println("Failed to get project limits:", err) | ||
| return nil | ||
| } | ||
|
|
||
| out, _ := json.MarshalIndent(limits, "", " ") | ||
|
masnwilliams marked this conversation as resolved.
Outdated
|
||
| fmt.Println(string(out)) | ||
| return nil | ||
| }, | ||
| } | ||
|
|
||
| var projectsLimitsSetCmd = &cobra.Command{ | ||
| Use: "set-limits <project-id>", | ||
| Short: "Set project limit overrides", | ||
| Args: cobra.ExactArgs(1), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| client := getKernelClient(cmd) | ||
| ctx := cmd.Context() | ||
|
|
||
| inner := kernel.UpdateProjectLimitsRequestParam{} | ||
| if v, _ := cmd.Flags().GetInt64("max-concurrent-sessions"); v >= 0 && cmd.Flags().Changed("max-concurrent-sessions") { | ||
| inner.MaxConcurrentSessions = param.NewOpt(v) | ||
| } | ||
| if v, _ := cmd.Flags().GetInt64("max-persistent-sessions"); v >= 0 && cmd.Flags().Changed("max-persistent-sessions") { | ||
| inner.MaxPersistentSessions = param.NewOpt(v) | ||
| } | ||
| if v, _ := cmd.Flags().GetInt64("max-concurrent-invocations"); v >= 0 && cmd.Flags().Changed("max-concurrent-invocations") { | ||
| inner.MaxConcurrentInvocations = param.NewOpt(v) | ||
| } | ||
| if v, _ := cmd.Flags().GetInt64("max-pooled-sessions"); v >= 0 && cmd.Flags().Changed("max-pooled-sessions") { | ||
| inner.MaxPooledSessions = param.NewOpt(v) | ||
|
cursor[bot] marked this conversation as resolved.
Outdated
|
||
| } | ||
| params := kernel.ProjectLimitUpdateParams{ | ||
| UpdateProjectLimitsRequest: inner, | ||
| } | ||
|
|
||
| limits, err := client.Projects.Limits.Update(ctx, args[0], params) | ||
| if err != nil { | ||
| pterm.Error.Println("Failed to set project limits:", err) | ||
| return nil | ||
| } | ||
|
|
||
| out, _ := json.MarshalIndent(limits, "", " ") | ||
| pterm.Success.Println("Project limits updated:") | ||
| fmt.Println(string(out)) | ||
| return nil | ||
| }, | ||
| } | ||
|
|
||
| func init() { | ||
| projectsLimitsSetCmd.Flags().Int64("max-concurrent-sessions", 0, "Maximum concurrent browser sessions (0 to remove cap)") | ||
| projectsLimitsSetCmd.Flags().Int64("max-persistent-sessions", 0, "Maximum persistent browser sessions (0 to remove cap)") | ||
| projectsLimitsSetCmd.Flags().Int64("max-concurrent-invocations", 0, "Maximum concurrent app invocations (0 to remove cap)") | ||
| projectsLimitsSetCmd.Flags().Int64("max-pooled-sessions", 0, "Maximum pooled sessions capacity (0 to remove cap)") | ||
|
|
||
| projectsCmd.AddCommand(projectsListCmd) | ||
| projectsCmd.AddCommand(projectsCreateCmd) | ||
| projectsCmd.AddCommand(projectsGetCmd) | ||
| projectsCmd.AddCommand(projectsDeleteCmd) | ||
| projectsCmd.AddCommand(projectsLimitsGetCmd) | ||
| projectsCmd.AddCommand(projectsLimitsSetCmd) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -104,6 +104,7 @@ func init() { | |
| rootCmd.PersistentFlags().BoolP("version", "v", false, "Print the CLI version") | ||
| rootCmd.PersistentFlags().BoolP("no-color", "", false, "Disable color output") | ||
| rootCmd.PersistentFlags().String("log-level", "warn", "Set the log level (trace, debug, info, warn, error, fatal, print)") | ||
| rootCmd.PersistentFlags().String("project", "", "Project ID or name to scope all requests to (or set KERNEL_PROJECT_ID env var)") | ||
| rootCmd.SilenceUsage = true | ||
| rootCmd.SilenceErrors = true | ||
| cobra.OnInitialize(initConfig) | ||
|
|
@@ -122,12 +123,41 @@ func init() { | |
| return nil | ||
| } | ||
|
|
||
| // Get authenticated client with OAuth tokens or API key fallback | ||
| client, err := auth.GetAuthenticatedClient(option.WithHeader("X-Kernel-Cli-Version", metadata.Version)) | ||
| clientOpts := []option.RequestOption{ | ||
| option.WithHeader("X-Kernel-Cli-Version", metadata.Version), | ||
| } | ||
|
|
||
| projectVal, _ := cmd.Flags().GetString("project") | ||
| if projectVal == "" { | ||
| projectVal = os.Getenv("KERNEL_PROJECT_ID") | ||
|
masnwilliams marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| // If the value looks like a name (not a cuid2 ID), we need to | ||
| // resolve it after authenticating. Build the client first without | ||
| // the project header, resolve, then re-create with the header. | ||
| needsResolve := projectVal != "" && !looksLikeCUID(projectVal) | ||
|
|
||
| if projectVal != "" && !needsResolve { | ||
| clientOpts = append(clientOpts, option.WithHeader("X-Kernel-Project-Id", projectVal)) | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Global --project flag skips name-to-ID resolutionHigh Severity The Additional Locations (1)Reviewed by Cursor Bugbot for commit a20844d. Configure here. |
||
|
|
||
| client, err := auth.GetAuthenticatedClient(clientOpts...) | ||
| if err != nil { | ||
| return fmt.Errorf("authentication required: %w", err) | ||
| } | ||
|
|
||
| if needsResolve { | ||
| resolved, resolveErr := resolveProjectByName(cmd.Context(), *client, projectVal) | ||
| if resolveErr != nil { | ||
| return resolveErr | ||
| } | ||
| clientOpts = append(clientOpts, option.WithHeader("X-Kernel-Project-Id", resolved)) | ||
| client, err = auth.GetAuthenticatedClient(clientOpts...) | ||
| if err != nil { | ||
| return fmt.Errorf("authentication required: %w", err) | ||
| } | ||
| } | ||
|
|
||
| ctx := context.WithValue(cmd.Context(), util.KernelClientKey, *client) | ||
| cmd.SetContext(ctx) | ||
| return nil | ||
|
|
@@ -144,6 +174,7 @@ func init() { | |
| rootCmd.AddCommand(extensionsCmd) | ||
| rootCmd.AddCommand(credentialsCmd) | ||
| rootCmd.AddCommand(credentialProvidersCmd) | ||
| rootCmd.AddCommand(projectsCmd) | ||
| rootCmd.AddCommand(createCmd) | ||
| rootCmd.AddCommand(mcp.MCPCmd) | ||
| rootCmd.AddCommand(upgradeCmd) | ||
|
|
@@ -223,6 +254,38 @@ func isUsageError(err error) bool { | |
| return false | ||
| } | ||
|
|
||
| // looksLikeCUID returns true if s matches the cuid2 format used for resource IDs. | ||
| // Delegates to the shared cuidRegex defined in browsers.go. | ||
| func looksLikeCUID(s string) bool { | ||
| return cuidRegex.MatchString(s) | ||
| } | ||
|
cursor[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| // resolveProjectByName lists the caller's projects and returns the ID of the | ||
| // one whose name matches (case-insensitive). Returns an error if no match or | ||
| // multiple matches are found. | ||
| func resolveProjectByName(ctx context.Context, client kernel.Client, name string) (string, error) { | ||
|
masnwilliams marked this conversation as resolved.
Outdated
|
||
| projects, err := client.Projects.List(ctx, kernel.ProjectListParams{}) | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to resolve project name %q: %w", name, err) | ||
| } | ||
| var matched []struct{ id, name string } | ||
| lower := strings.ToLower(name) | ||
| for _, p := range projects.Items { | ||
| if strings.ToLower(p.Name) == lower { | ||
| matched = append(matched, struct{ id, name string }{p.ID, p.Name}) | ||
| } | ||
| } | ||
| switch len(matched) { | ||
| case 0: | ||
| return "", fmt.Errorf("no project found with name %q", name) | ||
| case 1: | ||
| pterm.Debug.Printf("Resolved project %q → %s\n", matched[0].name, matched[0].id) | ||
| return matched[0].id, nil | ||
| default: | ||
| return "", fmt.Errorf("multiple projects match name %q; use a project ID instead", name) | ||
| } | ||
| } | ||
|
cursor[bot] marked this conversation as resolved.
|
||
|
|
||
| // onCancel runs a function when the provided context is cancelled | ||
| func onCancel(ctx context.Context, fn func()) { | ||
| go func() { | ||
|
|
||


Uh oh!
There was an error while loading. Please reload this page.