From 55aea0c49032107e731a5a7db65ffa1671094b5c Mon Sep 17 00:00:00 2001 From: yuaanlin Date: Fri, 20 Feb 2026 15:14:31 +0800 Subject: [PATCH] feat: add `service exec` command and resolve IDs from service instead of context Add `zeabur service exec` to run commands inside service containers via the `executeCommand` GraphQL mutation. Remove context-based ID resolution from all commands. When a service ID is provided, environment and project IDs are now derived from the service itself rather than relying on the (often wrong) project context. This eliminates mismatches where context points to a different project. Co-Authored-By: Claude Opus 4.6 --- internal/cmd/deploy/deploy.go | 1 - internal/cmd/deployment/get/get.go | 77 +++++------- internal/cmd/deployment/list/list.go | 48 +++----- internal/cmd/deployment/log/log.go | 48 +++----- internal/cmd/domain/create/create.go | 44 ++++--- internal/cmd/domain/delete/delete.go | 28 +++-- internal/cmd/domain/list/list.go | 32 ++--- internal/cmd/service/delete/delete.go | 36 ++---- internal/cmd/service/deploy/deploy.go | 22 ++-- internal/cmd/service/exec/exec.go | 110 ++++++++++++++++++ internal/cmd/service/expose/expose.go | 54 ++++----- internal/cmd/service/get/get.go | 31 ++--- .../cmd/service/instruction/instruction.go | 57 +++++---- internal/cmd/service/list/list.go | 37 ++---- internal/cmd/service/metric/metric.go | 59 ++++------ internal/cmd/service/network/network.go | 47 +++----- internal/cmd/service/redeploy/redeploy.go | 36 ++---- internal/cmd/service/restart/restart.go | 36 ++---- internal/cmd/service/service.go | 2 + internal/cmd/service/suspend/suspend.go | 36 ++---- internal/cmd/service/update/tag/tag.go | 44 ++----- internal/cmd/template/deploy/deploy.go | 11 +- internal/cmd/upload/upload.go | 1 - internal/cmd/variable/create/create.go | 32 ++--- internal/cmd/variable/delete/delete.go | 32 ++--- internal/cmd/variable/env/env.go | 47 +++++--- internal/cmd/variable/list/list.go | 34 +++--- internal/cmd/variable/update/update.go | 32 ++--- internal/util/env.go | 15 ++- pkg/api/interface.go | 1 + pkg/api/service.go | 17 +++ pkg/model/command.go | 7 ++ 32 files changed, 552 insertions(+), 562 deletions(-) create mode 100644 internal/cmd/service/exec/exec.go create mode 100644 pkg/model/command.go diff --git a/internal/cmd/deploy/deploy.go b/internal/cmd/deploy/deploy.go index b85d3a1..533b57c 100644 --- a/internal/cmd/deploy/deploy.go +++ b/internal/cmd/deploy/deploy.go @@ -33,7 +33,6 @@ func NewCmdDeploy(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "deploy", Short: "Deploy local project to Zeabur with one command", - PreRunE: util.NeedProjectContextWhenNonInteractive(f), RunE: func(cmd *cobra.Command, args []string) error { return runDeploy(f, opts) }, diff --git a/internal/cmd/deployment/get/get.go b/internal/cmd/deployment/get/get.go index 68f8609..c12c285 100644 --- a/internal/cmd/deployment/get/get.go +++ b/internal/cmd/deployment/get/get.go @@ -24,20 +24,17 @@ func NewCmdGet(f *cmdutil.Factory) *cobra.Command { opts := &Options{} cmd := &cobra.Command{ - Use: "get", - Short: "Get deployment, if deployment-id is not specified, use serviceID/serviceName and environmentID to get the deployment", - PreRunE: util.NeedProjectContextWhenNonInteractive(f), + Use: "get", + Short: "Get deployment, if deployment-id is not specified, use serviceID/serviceName and environmentID to get the deployment", RunE: func(cmd *cobra.Command, args []string) error { return runGet(f, opts) }, } - zctx := f.Config.GetContext() - cmd.Flags().StringVar(&opts.deploymentID, "deployment-id", "", "Deployment ID") - cmd.Flags().StringVar(&opts.serviceID, "service-id", zctx.GetService().GetID(), "Service ID") - cmd.Flags().StringVar(&opts.serviceName, "service-name", zctx.GetService().GetName(), "Service Name") - cmd.Flags().StringVar(&opts.environmentID, "env-id", zctx.GetEnvironment().GetID(), "Environment ID") + cmd.Flags().StringVar(&opts.serviceID, "service-id", "", "Service ID") + cmd.Flags().StringVar(&opts.serviceName, "service-name", "", "Service Name") + cmd.Flags().StringVar(&opts.environmentID, "env-id", "", "Environment ID") return cmd } @@ -69,39 +66,39 @@ func runGetInteractive(f *cmdutil.Factory, opts *Options) error { } func runGetNonInteractive(f *cmdutil.Factory, opts *Options) (err error) { - if opts.deploymentID == "" && opts.environmentID == "" { - projectID := f.Config.GetContext().GetProject().GetID() - envID, resolveErr := util.ResolveEnvironmentID(f.ApiClient, projectID) - if resolveErr != nil { - return resolveErr + // If deployment ID is provided, just use it directly + if opts.deploymentID != "" { + deployment, err := getDeploymentByID(f, opts.deploymentID) + if err != nil { + return err } - opts.environmentID = envID + f.Printer.Table(deployment.Header(), deployment.Rows()) + return nil } - if err = paramCheck(opts); err != nil { - return err + // Resolve service ID from name + if opts.serviceID == "" && opts.serviceName != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.serviceName) + if err != nil { + return fmt.Errorf("failed to get service: %w", err) + } + opts.serviceID = service.ID } - var deployment *model.Deployment + if opts.serviceID == "" { + return errors.New("--deployment-id or --service-id/--service-name is required") + } - // If deployment id is provided, get deployment by deployment id - if opts.deploymentID != "" { - deployment, err = getDeploymentByID(f, opts.deploymentID) - } else { - // or, get deployment by service id and environment id - - // If service id is not provided, get service id by service name - if opts.serviceID == "" { - var service *model.Service - if service, err = util.GetServiceByName(f.Config, f.ApiClient, opts.serviceName); err != nil { - return fmt.Errorf("failed to get service: %w", err) - } else { - opts.serviceID = service.ID - } + // Resolve environment from service's project + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.serviceID) + if err != nil { + return err } - deployment, err = getDeploymentByServiceAndEnvironment(f, opts.serviceID, opts.environmentID) + opts.environmentID = envID } + deployment, err := getDeploymentByServiceAndEnvironment(f, opts.serviceID, opts.environmentID) if err != nil { return err } @@ -132,19 +129,3 @@ func getDeploymentByServiceAndEnvironment(f *cmdutil.Factory, serviceID, environ return deployment, nil } - -func paramCheck(opts *Options) error { - if opts.deploymentID != "" { - return nil - } - - if opts.serviceID == "" && opts.serviceName == "" { - return errors.New("when deployment-id is not specified, service-id or service-name is required") - } - - if opts.environmentID == "" { - return errors.New("when deployment-id is not specified, env-id is required") - } - - return nil -} diff --git a/internal/cmd/deployment/list/list.go b/internal/cmd/deployment/list/list.go index 3c34c69..81b1d0a 100644 --- a/internal/cmd/deployment/list/list.go +++ b/internal/cmd/deployment/list/list.go @@ -12,7 +12,6 @@ import ( ) type Options struct { - // todo: support service name serviceID string serviceName string environmentID string @@ -25,17 +24,14 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { Use: "list", Short: "List deployments", Aliases: []string{"ls"}, - PreRunE: util.NeedProjectContextWhenNonInteractive(f), RunE: func(cmd *cobra.Command, args []string) error { return runList(f, opts) }, } - zctx := f.Config.GetContext() - - cmd.Flags().StringVar(&opts.serviceID, "service-id", zctx.GetService().GetID(), "Service ID") - cmd.Flags().StringVar(&opts.serviceName, "service-name", zctx.GetService().GetName(), "Service Name") - cmd.Flags().StringVar(&opts.environmentID, "env-id", zctx.GetEnvironment().GetID(), "Environment ID") + cmd.Flags().StringVar(&opts.serviceID, "service-id", "", "Service ID") + cmd.Flags().StringVar(&opts.serviceName, "service-name", "", "Service Name") + cmd.Flags().StringVar(&opts.environmentID, "env-id", "", "Environment ID") return cmd } @@ -65,26 +61,26 @@ func runListInteractive(f *cmdutil.Factory, opts *Options) error { } func runListNonInteractive(f *cmdutil.Factory, opts *Options) error { - if opts.environmentID == "" { - projectID := f.Config.GetContext().GetProject().GetID() - envID, err := util.ResolveEnvironmentID(f.ApiClient, projectID) + // Resolve service ID from name + if opts.serviceID == "" && opts.serviceName != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.serviceName) if err != nil { - return err + return fmt.Errorf("failed to get service: %w", err) } - opts.environmentID = envID + opts.serviceID = service.ID } - if err := paramCheck(opts); err != nil { - return err + if opts.serviceID == "" { + return errors.New("--service-id or --service-name is required") } - // If service id is not provided, get service id by service name - if opts.serviceID == "" { - if service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.serviceName); err != nil { - return fmt.Errorf("failed to get service: %w", err) - } else { - opts.serviceID = service.ID + // Resolve environment from service's project + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.serviceID) + if err != nil { + return err } + opts.environmentID = envID } deployments, err := f.ApiClient.ListAllDeployments(context.Background(), opts.serviceID, opts.environmentID) @@ -101,15 +97,3 @@ func runListNonInteractive(f *cmdutil.Factory, opts *Options) error { return nil } - -func paramCheck(opts *Options) error { - if opts.serviceID == "" && opts.serviceName == "" { - return errors.New("service-id or service-name is required") - } - - if opts.environmentID == "" { - return errors.New("environment is required") - } - - return nil -} diff --git a/internal/cmd/deployment/log/log.go b/internal/cmd/deployment/log/log.go index d241071..456cffd 100644 --- a/internal/cmd/deployment/log/log.go +++ b/internal/cmd/deployment/log/log.go @@ -33,21 +33,18 @@ func NewCmdLog(f *cmdutil.Factory) *cobra.Command { opts := &Options{} cmd := &cobra.Command{ - Use: "log", - Short: "Get deployment logs, if deployment-id is not specified, use serviceID/serviceName and environmentID to get the deployment", - PreRunE: util.NeedProjectContextWhenNonInteractive(f), + Use: "log", + Short: "Get deployment logs, if deployment-id is not specified, use serviceID/serviceName and environmentID to get the deployment", RunE: func(cmd *cobra.Command, args []string) error { return runLog(f, opts) }, } - zctx := f.Config.GetContext() - - cmd.Flags().StringVar(&opts.projectID, "project-id", zctx.GetProject().GetID(), "Project ID") + cmd.Flags().StringVar(&opts.projectID, "project-id", "", "Project ID") cmd.Flags().StringVar(&opts.deploymentID, "deployment-id", "", "Deployment ID") - cmd.Flags().StringVar(&opts.serviceID, "service-id", zctx.GetService().GetID(), "Service ID") - cmd.Flags().StringVar(&opts.serviceName, "service-name", zctx.GetService().GetName(), "Service Name") - cmd.Flags().StringVar(&opts.environmentID, "env-id", zctx.GetEnvironment().GetID(), "Environment ID") + cmd.Flags().StringVar(&opts.serviceID, "service-id", "", "Service ID") + cmd.Flags().StringVar(&opts.serviceName, "service-name", "", "Service Name") + cmd.Flags().StringVar(&opts.environmentID, "env-id", "", "Environment ID") cmd.Flags().StringVarP(&opts.logType, "type", "t", logTypeRuntime, "Log type, runtime or build") cmd.Flags().BoolVarP(&opts.watch, "watch", "w", false, "Watch logs") @@ -63,13 +60,8 @@ func runLog(f *cmdutil.Factory, opts *Options) error { } func runLogInteractive(f *cmdutil.Factory, opts *Options) error { - zctx := f.Config.GetContext() - - if opts.projectID == "" { - opts.projectID = zctx.GetProject().GetID() - } - if opts.deploymentID == "" { + zctx := f.Config.GetContext() _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ ProjectCtx: zctx, ServiceID: &opts.serviceID, @@ -95,8 +87,7 @@ func runLogNonInteractive(f *cmdutil.Factory, opts *Options) (err error) { opts.serviceID = service.ID } - // When serviceID is available, always resolve projectID and environmentID from the service - // instead of relying on context (which may point to a different project). + // When serviceID is available, resolve projectID and environmentID from the service if opts.serviceID != "" { service, err := f.ApiClient.GetService(context.Background(), opts.serviceID, "", "", "") if err != nil { @@ -105,24 +96,13 @@ func runLogNonInteractive(f *cmdutil.Factory, opts *Options) (err error) { if service.Project != nil { opts.projectID = service.Project.ID } - envID, resolveErr := util.ResolveEnvironmentID(f.ApiClient, opts.projectID) - if resolveErr != nil { - return resolveErr - } - opts.environmentID = envID - } - - // Fallback: resolve environmentID from context project if still empty - if opts.deploymentID == "" && opts.environmentID == "" { - projectID := opts.projectID - if projectID == "" { - projectID = f.Config.GetContext().GetProject().GetID() - } - envID, resolveErr := util.ResolveEnvironmentID(f.ApiClient, projectID) - if resolveErr != nil { - return resolveErr + if opts.environmentID == "" { + envID, resolveErr := util.ResolveEnvironmentID(f.ApiClient, opts.projectID) + if resolveErr != nil { + return resolveErr + } + opts.environmentID = envID } - opts.environmentID = envID } if err = paramCheck(opts); err != nil { diff --git a/internal/cmd/domain/create/create.go b/internal/cmd/domain/create/create.go index f858493..f6fdfad 100644 --- a/internal/cmd/domain/create/create.go +++ b/internal/cmd/domain/create/create.go @@ -23,17 +23,11 @@ type Options struct { func NewCmdCreateDomain(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "create", Short: "create a domain", Long: `Create a domain for a service`, - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runCreateDomain(f, opts) }, @@ -93,7 +87,17 @@ func runCreateDomainInteractive(f *cmdutil.Factory, opts *Options) error { opts.domainName = domainInput } - project, err := f.ApiClient.GetProject(context.Background(), zctx.GetProject().GetID(), "", "") + // Get project from the service to check domain availability + service, err := f.ApiClient.GetService(context.Background(), opts.id, "", "", "") + if err != nil { + return fmt.Errorf("get service failed: %w", err) + } + projectID := "" + if service.Project != nil { + projectID = service.Project.ID + } + + project, err := f.ApiClient.GetProject(context.Background(), projectID, "", "") if err != nil { return err } @@ -146,16 +150,24 @@ func runCreateDomainInteractive(f *cmdutil.Factory, opts *Options) error { } func runCreateDomainNonInteractive(f *cmdutil.Factory, opts *Options) error { - zctx := f.Config.GetContext() + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if err != nil { + return err + } + opts.id = service.ID + } - if _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ - ProjectCtx: zctx, - ServiceID: &opts.id, - ServiceName: &opts.name, - EnvironmentID: &opts.environmentID, - CreateNew: false, - }); err != nil { - return err + if opts.id == "" { + return fmt.Errorf("--id or --name is required") + } + + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) + if err != nil { + return err + } + opts.environmentID = envID } s := spinner.New(cmdutil.SpinnerCharSet, cmdutil.SpinnerInterval, diff --git a/internal/cmd/domain/delete/delete.go b/internal/cmd/domain/delete/delete.go index b234b0a..c6f4e92 100644 --- a/internal/cmd/domain/delete/delete.go +++ b/internal/cmd/domain/delete/delete.go @@ -22,18 +22,12 @@ type Options struct { func NewCmdDeleteDomain(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "delete", Short: "Delete domain", Long: `Delete domain of a service`, Aliases: []string{"del"}, - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runDeleteDomain(f, opts) }, @@ -112,16 +106,20 @@ func runDeleteDomainInteractive(f *cmdutil.Factory, opts *Options) error { } func runDeleteDomainNonInteractive(f *cmdutil.Factory, opts *Options) error { - zctx := f.Config.GetContext() + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if err != nil { + return err + } + opts.id = service.ID + } - if _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ - ProjectCtx: zctx, - ServiceID: &opts.id, - ServiceName: &opts.name, - EnvironmentID: &opts.environmentID, - CreateNew: false, - }); err != nil { - return err + if opts.environmentID == "" && opts.id != "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) + if err != nil { + return err + } + opts.environmentID = envID } s := spinner.New(cmdutil.SpinnerCharSet, cmdutil.SpinnerInterval, diff --git a/internal/cmd/domain/list/list.go b/internal/cmd/domain/list/list.go index d087280..673406f 100644 --- a/internal/cmd/domain/list/list.go +++ b/internal/cmd/domain/list/list.go @@ -20,7 +20,6 @@ type Options struct { func NewCmdListDomains(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "list", @@ -28,11 +27,6 @@ func NewCmdListDomains(f *cmdutil.Factory) *cobra.Command { Long: `List domains of a service`, Args: cobra.NoArgs, Aliases: []string{"ls"}, - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runListDomains(f, opts) }, @@ -69,16 +63,24 @@ func runListDomainsInteractive(f *cmdutil.Factory, opts *Options) error { } func runListDomainsNonInteractive(f *cmdutil.Factory, opts *Options) error { - zctx := f.Config.GetContext() + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if err != nil { + return err + } + opts.id = service.ID + } - if _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ - ProjectCtx: zctx, - ServiceID: &opts.id, - ServiceName: &opts.name, - EnvironmentID: &opts.environmentID, - CreateNew: false, - }); err != nil { - return err + if opts.id == "" { + return fmt.Errorf("--id or --name is required") + } + + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) + if err != nil { + return err + } + opts.environmentID = envID } s := spinner.New(cmdutil.SpinnerCharSet, cmdutil.SpinnerInterval, diff --git a/internal/cmd/service/delete/delete.go b/internal/cmd/service/delete/delete.go index 5b919da..bb9aaf1 100644 --- a/internal/cmd/service/delete/delete.go +++ b/internal/cmd/service/delete/delete.go @@ -21,16 +21,10 @@ type Options struct { func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "delete", Short: "Delete a service", - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runDelete(f, opts) }, @@ -67,26 +61,24 @@ func runDeleteInteractive(f *cmdutil.Factory, opts *Options) error { } func runDeleteNonInteractive(f *cmdutil.Factory, opts *Options) error { - if opts.environmentID == "" { - projectID := f.Config.GetContext().GetProject().GetID() - envID, err := util.ResolveEnvironmentID(f.ApiClient, projectID) + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) if err != nil { return err } - opts.environmentID = envID + opts.id = service.ID } - if err := checkParams(opts); err != nil { - return err + if opts.id == "" { + return fmt.Errorf("--id or --name is required") } - // if name is set, get service id by name - if opts.id == "" && opts.name != "" { - service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) if err != nil { return err } - opts.id = service.ID + opts.environmentID = envID } // to show friendly message @@ -118,15 +110,3 @@ func runDeleteNonInteractive(f *cmdutil.Factory, opts *Options) error { return nil } - -func checkParams(opts *Options) error { - if opts.id == "" && opts.name == "" { - return fmt.Errorf("--id or --name is required") - } - - if opts.environmentID == "" { - return fmt.Errorf("--env-id is required") - } - - return nil -} diff --git a/internal/cmd/service/deploy/deploy.go b/internal/cmd/service/deploy/deploy.go index 7df7944..00e1fb4 100644 --- a/internal/cmd/service/deploy/deploy.go +++ b/internal/cmd/service/deploy/deploy.go @@ -8,7 +8,6 @@ import ( "github.com/briandowns/spinner" "github.com/spf13/cobra" "github.com/zeabur/cli/internal/cmdutil" - "github.com/zeabur/cli/internal/util" "github.com/zeabur/cli/pkg/constant" ) @@ -25,20 +24,15 @@ type Options struct { func NewCmdDeploy(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() - cmd := &cobra.Command{ Use: "deploy", Short: "Deploy a service", - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetProject(), &opts.projectID, new(string)), - ), RunE: func(cmd *cobra.Command, args []string) error { return runDeploy(f, opts) }, } + cmd.Flags().StringVar(&opts.projectID, "project-id", "", "Project ID") cmd.Flags().StringVar(&opts.name, "name", "", "Service Name") cmd.Flags().StringVar(&opts.template, "template", "", "Service template") cmd.Flags().StringVar(&opts.marketplaceCode, "marketplace-code", "", "Marketplace item code") @@ -58,6 +52,10 @@ func runDeploy(f *cmdutil.Factory, opts *Options) error { } func runDeployNonInteractive(f *cmdutil.Factory, opts *Options) error { + if opts.projectID == "" { + return fmt.Errorf("--project-id is required") + } + err := paramCheck(opts) if err != nil { return err @@ -77,7 +75,7 @@ func runDeployNonInteractive(f *cmdutil.Factory, opts *Options) error { f.Log.Infof("Service %s created", service.Name) return nil case "GIT": - _, err = f.ApiClient.CreateService(context.Background(), f.Config.GetContext().GetProject().GetID(), opts.name, opts.repoID, opts.branchName) + _, err = f.ApiClient.CreateService(context.Background(), opts.projectID, opts.name, opts.repoID, opts.branchName) if err != nil { return fmt.Errorf("create service failed: %w", err) } @@ -91,8 +89,10 @@ func runDeployNonInteractive(f *cmdutil.Factory, opts *Options) error { func runDeployInteractive(f *cmdutil.Factory, opts *Options) error { // fill project id if not set by asking user - if _, err := f.ParamFiller.Project(&opts.projectID); err != nil { - return err + if opts.projectID == "" { + if _, err := f.ParamFiller.Project(&opts.projectID); err != nil { + return err + } } if opts.template == "" { @@ -210,7 +210,7 @@ func runDeployInteractive(f *cmdutil.Factory, opts *Options) error { ) s.Start() - _, err = f.ApiClient.CreateService(context.Background(), f.Config.GetContext().GetProject().GetID(), opts.name, opts.repoID, opts.branchName) + _, err = f.ApiClient.CreateService(context.Background(), opts.projectID, opts.name, opts.repoID, opts.branchName) if err != nil { return err } diff --git a/internal/cmd/service/exec/exec.go b/internal/cmd/service/exec/exec.go new file mode 100644 index 0000000..9bd8a62 --- /dev/null +++ b/internal/cmd/service/exec/exec.go @@ -0,0 +1,110 @@ +package exec + +import ( + "context" + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/zeabur/cli/internal/cmdutil" + "github.com/zeabur/cli/internal/util" + "github.com/zeabur/cli/pkg/fill" +) + +type Options struct { + id string + name string + + environmentID string + + command []string +} + +func NewCmdExec(f *cmdutil.Factory) *cobra.Command { + opts := &Options{} + + cmd := &cobra.Command{ + Use: "exec -- [args...]", + Short: "Execute a command in a service container", + Long: "Execute a command in a running service's container. The command and its arguments should be specified after \"--\".", + Example: ` # List files in the service container + zeabur service exec -- ls -la + + # Run a shell command + zeabur service exec -- sh -c "echo hello" + + # Specify service by name + zeabur service exec --name my-svc --env-id xxx -- cat /etc/hostname`, + RunE: func(cmd *cobra.Command, args []string) error { + opts.command = cmd.Flags().Args() + if len(opts.command) == 0 { + return fmt.Errorf("command is required, use -- to separate it from flags") + } + return runExec(f, opts) + }, + } + + util.AddServiceParam(cmd, &opts.id, &opts.name) + util.AddEnvOfServiceParam(cmd, &opts.environmentID) + + return cmd +} + +func runExec(f *cmdutil.Factory, opts *Options) error { + if f.Interactive { + return runExecInteractive(f, opts) + } + return runExecNonInteractive(f, opts) +} + +func runExecInteractive(f *cmdutil.Factory, opts *Options) error { + if opts.id == "" && opts.name == "" { + zctx := f.Config.GetContext() + if _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ + ProjectCtx: zctx, + ServiceID: &opts.id, + ServiceName: &opts.name, + EnvironmentID: &opts.environmentID, + CreateNew: false, + }); err != nil { + return err + } + } + + return runExecNonInteractive(f, opts) +} + +func runExecNonInteractive(f *cmdutil.Factory, opts *Options) error { + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if err != nil { + return err + } + opts.id = service.ID + } + + if opts.id == "" { + return fmt.Errorf("--id or --name is required") + } + + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) + if err != nil { + return err + } + opts.environmentID = envID + } + + result, err := f.ApiClient.ExecuteCommand(context.Background(), opts.id, opts.environmentID, opts.command) + if err != nil { + return fmt.Errorf("execute command failed: %w", err) + } + + fmt.Print(result.Output) + + if result.ExitCode != 0 { + os.Exit(result.ExitCode) + } + + return nil +} diff --git a/internal/cmd/service/expose/expose.go b/internal/cmd/service/expose/expose.go index 6582355..ac361e5 100644 --- a/internal/cmd/service/expose/expose.go +++ b/internal/cmd/service/expose/expose.go @@ -20,22 +20,16 @@ type Options struct { func NewCmdExpose(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "expose", Short: "Expose a service temporarily", Long: `Expose a service temporarily, default 3600 seconds. example: - zeabur service expose # cli will try to get service from context or prompt to select one + zeabur service expose # cli will prompt to select a service zeabur service expose --id xxxxx --env-id xxxx # use id and env-id to expose service - zeabur service expose --name xxxxx --env-id xxxx # if project context is set, use name, env-id to expose service + zeabur service expose --name xxxxx --env-id xxxx # use name, env-id to expose service `, - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runExpose(f, opts) }, @@ -56,23 +50,41 @@ func runExpose(f *cmdutil.Factory, opts *Options) error { } func runExposeNonInteractive(f *cmdutil.Factory, opts *Options) error { + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if err != nil { + return err + } + opts.id = service.ID + } + + if opts.id == "" { + return fmt.Errorf("please specify --id or --name") + } + + // Resolve environment and project from the service + service, err := f.ApiClient.GetService(context.Background(), opts.id, "", "", "") + if err != nil { + return fmt.Errorf("get service failed: %w", err) + } + if opts.environmentID == "" { - projectID := f.Config.GetContext().GetProject().GetID() - envID, err := util.ResolveEnvironmentID(f.ApiClient, projectID) + if service.Project == nil || service.Project.ID == "" { + return fmt.Errorf("service has no associated project") + } + envID, err := util.ResolveEnvironmentID(f.ApiClient, service.Project.ID) if err != nil { return err } opts.environmentID = envID } - err := paramCheck(opts) - if err != nil { - return err + projectID := "" + if service.Project != nil { + projectID = service.Project.ID } ctx := context.Background() - projectID := f.Config.GetContext().GetProject().GetID() - tempTCPPort, err := f.ApiClient.ExposeService(ctx, opts.id, opts.environmentID, projectID, opts.name) if err != nil { return fmt.Errorf("failed to expose service: %w", err) @@ -97,15 +109,3 @@ func runExposeInteractive(f *cmdutil.Factory, opts *Options) error { return runExposeNonInteractive(f, opts) } - -func paramCheck(opts *Options) error { - if opts.id == "" && opts.name == "" { - return fmt.Errorf("please specify --id or --name") - } - - if opts.environmentID == "" { - return fmt.Errorf("please specify --env-id") - } - - return nil -} diff --git a/internal/cmd/service/get/get.go b/internal/cmd/service/get/get.go index 87978f0..0b70576 100644 --- a/internal/cmd/service/get/get.go +++ b/internal/cmd/service/get/get.go @@ -22,16 +22,10 @@ type Options struct { func NewCmdGet(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "get", Short: "Get a service, if environment is specified, get the service details in the environment", - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runGet(f, opts) }, @@ -64,8 +58,17 @@ func runGetInteractive(f *cmdutil.Factory, opts *Options) error { } func runGetNonInteractive(f *cmdutil.Factory, opts *Options) error { - projectName := f.Config.GetContext().GetProject().GetName() - username := f.Config.GetUsername() + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if err != nil { + return err + } + opts.id = service.ID + } + + if opts.id == "" { + return fmt.Errorf("--id or --name is required") + } var ( t model.Tabler @@ -73,9 +76,9 @@ func runGetNonInteractive(f *cmdutil.Factory, opts *Options) error { ) if opts.environmentID == "" { - t, err = getServiceBrief(f.ApiClient, opts.id, username, projectName, opts.name) + t, err = getServiceBrief(f.ApiClient, opts.id) } else { - t, err = getServiceDetails(f.ApiClient, opts.id, username, projectName, opts.name, opts.environmentID) + t, err = getServiceDetails(f.ApiClient, opts.id, opts.environmentID) } if err != nil { @@ -87,9 +90,9 @@ func runGetNonInteractive(f *cmdutil.Factory, opts *Options) error { return nil } -func getServiceBrief(client api.ServiceAPI, id, username, projectName, name string) (t model.Tabler, err error) { +func getServiceBrief(client api.ServiceAPI, id string) (t model.Tabler, err error) { ctx := context.Background() - service, err := client.GetService(ctx, id, username, projectName, name) + service, err := client.GetService(ctx, id, "", "", "") if err != nil { return nil, fmt.Errorf("get service failed: %w", err) } @@ -97,9 +100,9 @@ func getServiceBrief(client api.ServiceAPI, id, username, projectName, name stri return service, nil } -func getServiceDetails(client api.ServiceAPI, id, username, projectID, name, environmentID string) (t model.Tabler, err error) { +func getServiceDetails(client api.ServiceAPI, id, environmentID string) (t model.Tabler, err error) { ctx := context.Background() - serviceDetail, err := client.GetServiceDetailByEnvironment(ctx, id, username, projectID, name, environmentID) + serviceDetail, err := client.GetServiceDetailByEnvironment(ctx, id, "", "", "", environmentID) if err != nil { return nil, fmt.Errorf("get service failed: %w", err) } diff --git a/internal/cmd/service/instruction/instruction.go b/internal/cmd/service/instruction/instruction.go index ce75525..f49a151 100644 --- a/internal/cmd/service/instruction/instruction.go +++ b/internal/cmd/service/instruction/instruction.go @@ -20,17 +20,10 @@ type Options struct { func NewCmdInstruction(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() - cmd := &cobra.Command{ Use: "instruction", Short: "Instruction for prebuiltservice", Long: `Instruction for prebuilt service`, - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runInstruction(f, opts) }, @@ -43,22 +36,20 @@ func NewCmdInstruction(f *cmdutil.Factory) *cobra.Command { } func runInstruction(f *cmdutil.Factory, opts *Options) error { - zctx := f.Config.GetContext() - if _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ - ProjectCtx: zctx, - ServiceID: &opts.id, - ServiceName: &opts.name, - EnvironmentID: &opts.environmentID, - CreateNew: false, - FilterFunc: func(service *model.Service) bool { - return service.Template == "PREBUILT" - }, - }); err != nil { - return err - } - - if err := paramCheck(opts); err != nil { - return err + if f.Interactive && opts.id == "" && opts.name == "" { + zctx := f.Config.GetContext() + if _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ + ProjectCtx: zctx, + ServiceID: &opts.id, + ServiceName: &opts.name, + EnvironmentID: &opts.environmentID, + CreateNew: false, + FilterFunc: func(service *model.Service) bool { + return service.Template == "PREBUILT" + }, + }); err != nil { + return err + } } if opts.id == "" && opts.name != "" { @@ -69,6 +60,18 @@ func runInstruction(f *cmdutil.Factory, opts *Options) error { opts.id = service.ID } + if opts.id == "" { + return fmt.Errorf("service id or name is required") + } + + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) + if err != nil { + return err + } + opts.environmentID = envID + } + instructions, err := f.ApiClient.ServiceInstructions(context.Background(), opts.id, opts.environmentID) if err != nil { return err @@ -80,11 +83,3 @@ func runInstruction(f *cmdutil.Factory, opts *Options) error { return nil } - -func paramCheck(opts *Options) error { - if opts.id == "" && opts.name == "" { - return fmt.Errorf("service id or name is required") - } - - return nil -} diff --git a/internal/cmd/service/list/list.go b/internal/cmd/service/list/list.go index 38f52de..78cbe9d 100644 --- a/internal/cmd/service/list/list.go +++ b/internal/cmd/service/list/list.go @@ -15,10 +15,7 @@ type Options struct { } func NewCmdList(f *cmdutil.Factory) *cobra.Command { - opts := &Options{ - projectID: f.Config.GetContext().GetProject().GetID(), - } - ctx := f.Config.GetContext() + opts := &Options{} cmd := &cobra.Command{ Use: "list", @@ -26,16 +23,12 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { Long: `List services, if env-id is provided, list services in the environment in detail`, Args: cobra.NoArgs, Aliases: []string{"ls"}, - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f, &opts.projectID), - util.DefaultIDByContext(ctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runList(f, opts) }, } - cmd.Flags().StringVar(&opts.projectID, "project-id", opts.projectID, "Project ID") + cmd.Flags().StringVar(&opts.projectID, "project-id", "", "Project ID") util.AddEnvOfServiceParam(cmd, &opts.environmentID) return cmd @@ -50,30 +43,24 @@ func runList(f *cmdutil.Factory, opts *Options) error { } func runListInteractive(f *cmdutil.Factory, opts *Options) error { - // if project id is not set by flag, fetch from context if opts.projectID == "" { - opts.projectID = f.Config.GetContext().GetProject().GetID() - } - - // if project id is still not set, prompt to select one - if _, err := f.ParamFiller.Project(&opts.projectID); err != nil { - return err + if _, err := f.ParamFiller.Project(&opts.projectID); err != nil { + return err + } } return runListNonInteractive(f, opts) } func runListNonInteractive(f *cmdutil.Factory, opts *Options) error { - err := paramCheck(opts) - if err != nil { - return err + if opts.projectID == "" { + return fmt.Errorf("--project-id is required") } if opts.environmentID == "" { return listServicesBrief(f, opts.projectID) - } else { - return listServicesDetailByEnvironment(f, opts.projectID, opts.environmentID) } + return listServicesDetailByEnvironment(f, opts.projectID, opts.environmentID) } func listServicesBrief(f *cmdutil.Factory, projectID string) error { @@ -107,11 +94,3 @@ func listServicesDetailByEnvironment(f *cmdutil.Factory, projectID, environmentI return nil } - -func paramCheck(opts *Options) error { - if opts.projectID == "" { - return fmt.Errorf("project-id is required") - } - - return nil -} diff --git a/internal/cmd/service/metric/metric.go b/internal/cmd/service/metric/metric.go index e1fd2d6..bc2e659 100644 --- a/internal/cmd/service/metric/metric.go +++ b/internal/cmd/service/metric/metric.go @@ -24,8 +24,6 @@ type Options struct { func NewCmdMetric(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() - cmd := &cobra.Command{ Use: "metric [CPU|MEMORY|NETWORK]", Short: "Show metric of a service", @@ -36,11 +34,6 @@ func NewCmdMetric(f *cmdutil.Factory) *cobra.Command { string(model.MetricTypeMemory), string(model.MetricTypeNetwork), }, - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { opts.metricType = args[0] return runMetric(f, opts) @@ -79,36 +72,48 @@ func runMetricInteractive(f *cmdutil.Factory, opts *Options) error { } func runMetricNonInteractive(f *cmdutil.Factory, opts *Options) error { - if opts.environmentID == "" { - projectID := f.Config.GetContext().GetProject().GetID() - envID, err := util.ResolveEnvironmentID(f.ApiClient, projectID) + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) if err != nil { return err } - opts.environmentID = envID + opts.id = service.ID } - if err := paramCheck(opts); err != nil { - return err + if opts.id == "" { + return fmt.Errorf("--id or --name is required") } - // if name is set, get service id by name - if opts.id == "" && opts.name != "" { - service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + // Resolve environment and project from the service + service, err := f.ApiClient.GetService(context.Background(), opts.id, "", "", "") + if err != nil { + return fmt.Errorf("get service failed: %w", err) + } + + projectID := "" + if service.Project != nil { + projectID = service.Project.ID + } + + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentID(f.ApiClient, projectID) if err != nil { return err } - opts.id = service.ID + opts.environmentID = envID } - upperCaseMetricType := strings.ToUpper(opts.metricType) + if opts.metricType == "" { + return fmt.Errorf("metric type is required") + } + upperCaseMetricType := strings.ToUpper(opts.metricType) mt := model.MetricType(opts.metricType) startTime := time.Now().Add(-time.Duration(opts.hour) * time.Hour) endTime := time.Now() - metrics, err := f.ApiClient.ServiceMetric(context.Background(), opts.id, f.Config.GetContext().GetProject().GetID(), opts.environmentID, upperCaseMetricType, startTime, endTime) + metrics, err := f.ApiClient.ServiceMetric(context.Background(), opts.id, projectID, opts.environmentID, upperCaseMetricType, startTime, endTime) if err != nil { return fmt.Errorf("get service metric failed: %w", err) } @@ -147,19 +152,3 @@ func runMetricNonInteractive(f *cmdutil.Factory, opts *Options) error { return nil } - -func paramCheck(opts *Options) error { - if opts.id == "" && opts.name == "" { - return fmt.Errorf("--id or --name is required") - } - - if opts.environmentID == "" { - return fmt.Errorf("--env-id is required") - } - - if opts.metricType == "" { - return fmt.Errorf("metric type is required") - } - - return nil -} diff --git a/internal/cmd/service/network/network.go b/internal/cmd/service/network/network.go index bbda132..adf0b37 100644 --- a/internal/cmd/service/network/network.go +++ b/internal/cmd/service/network/network.go @@ -20,20 +20,13 @@ type Options struct { func NewCmdPrivateNetwork(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() - cmd := &cobra.Command{ Use: "network", Short: "Network information for service", Long: `Network information for service`, Aliases: []string{"net"}, - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { - return runInstruction(f, opts) + return runNetwork(f, opts) }, } @@ -43,20 +36,18 @@ func NewCmdPrivateNetwork(f *cmdutil.Factory) *cobra.Command { return cmd } -func runInstruction(f *cmdutil.Factory, opts *Options) error { - zctx := f.Config.GetContext() - if _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ - ProjectCtx: zctx, - ServiceID: &opts.id, - ServiceName: &opts.name, - EnvironmentID: &opts.environmentID, - CreateNew: false, - }); err != nil { - return err - } - - if err := paramCheck(opts); err != nil { - return err +func runNetwork(f *cmdutil.Factory, opts *Options) error { + if f.Interactive && opts.id == "" && opts.name == "" { + zctx := f.Config.GetContext() + if _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ + ProjectCtx: zctx, + ServiceID: &opts.id, + ServiceName: &opts.name, + EnvironmentID: &opts.environmentID, + CreateNew: false, + }); err != nil { + return err + } } if opts.id == "" && opts.name != "" { @@ -67,6 +58,10 @@ func runInstruction(f *cmdutil.Factory, opts *Options) error { opts.id = service.ID } + if opts.id == "" { + return fmt.Errorf("service id or name is required") + } + s := spinner.New(cmdutil.SpinnerCharSet, cmdutil.SpinnerInterval, spinner.WithColor(cmdutil.SpinnerColor), spinner.WithSuffix(fmt.Sprintf(" Fetching network information of service %s ...", opts.name)), @@ -85,11 +80,3 @@ func runInstruction(f *cmdutil.Factory, opts *Options) error { return nil } - -func paramCheck(opts *Options) error { - if opts.id == "" && opts.name == "" { - return fmt.Errorf("service id or name is required") - } - - return nil -} diff --git a/internal/cmd/service/redeploy/redeploy.go b/internal/cmd/service/redeploy/redeploy.go index 7471af1..f39ca8d 100644 --- a/internal/cmd/service/redeploy/redeploy.go +++ b/internal/cmd/service/redeploy/redeploy.go @@ -21,16 +21,10 @@ type Options struct { func NewCmdRedeploy(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "redeploy", Short: "redeploy a service", - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runRedeploy(f, opts) }, @@ -68,26 +62,24 @@ func runRedeployInteractive(f *cmdutil.Factory, opts *Options) error { } func runRedeployNonInteractive(f *cmdutil.Factory, opts *Options) error { - if opts.environmentID == "" { - projectID := f.Config.GetContext().GetProject().GetID() - envID, err := util.ResolveEnvironmentID(f.ApiClient, projectID) + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) if err != nil { return err } - opts.environmentID = envID + opts.id = service.ID } - if err := checkParams(opts); err != nil { - return err + if opts.id == "" { + return fmt.Errorf("--id or --name is required") } - // if name is set, get service id by name - if opts.id == "" && opts.name != "" { - service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) if err != nil { return err } - opts.id = service.ID + opts.environmentID = envID } // to show friendly message @@ -116,15 +108,3 @@ func runRedeployNonInteractive(f *cmdutil.Factory, opts *Options) error { return nil } - -func checkParams(opts *Options) error { - if opts.id == "" && opts.name == "" { - return fmt.Errorf("--id or --name is required") - } - - if opts.environmentID == "" { - return fmt.Errorf("--env-id is required") - } - - return nil -} diff --git a/internal/cmd/service/restart/restart.go b/internal/cmd/service/restart/restart.go index 318d404..d407ec1 100644 --- a/internal/cmd/service/restart/restart.go +++ b/internal/cmd/service/restart/restart.go @@ -21,16 +21,10 @@ type Options struct { func NewCmdRestart(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "restart", Short: "restart a service", - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runRestart(f, opts) }, @@ -68,26 +62,24 @@ func runRestartInteractive(f *cmdutil.Factory, opts *Options) error { } func runRestartNonInteractive(f *cmdutil.Factory, opts *Options) error { - if opts.environmentID == "" { - projectID := f.Config.GetContext().GetProject().GetID() - envID, err := util.ResolveEnvironmentID(f.ApiClient, projectID) + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) if err != nil { return err } - opts.environmentID = envID + opts.id = service.ID } - if err := checkParams(opts); err != nil { - return err + if opts.id == "" { + return fmt.Errorf("--id or --name is required") } - // if name is set, get service id by name - if opts.id == "" && opts.name != "" { - service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) if err != nil { return err } - opts.id = service.ID + opts.environmentID = envID } // to show friendly message @@ -116,15 +108,3 @@ func runRestartNonInteractive(f *cmdutil.Factory, opts *Options) error { return nil } - -func checkParams(opts *Options) error { - if opts.id == "" && opts.name == "" { - return fmt.Errorf("--id or --name is required") - } - - if opts.environmentID == "" { - return fmt.Errorf("--env-id is required") - } - - return nil -} diff --git a/internal/cmd/service/service.go b/internal/cmd/service/service.go index 026f5ed..ae22b01 100644 --- a/internal/cmd/service/service.go +++ b/internal/cmd/service/service.go @@ -6,6 +6,7 @@ import ( serviceDeleteCmd "github.com/zeabur/cli/internal/cmd/service/delete" serviceDeployCmd "github.com/zeabur/cli/internal/cmd/service/deploy" + serviceExecCmd "github.com/zeabur/cli/internal/cmd/service/exec" serviceExposeCmd "github.com/zeabur/cli/internal/cmd/service/expose" serviceGetCmd "github.com/zeabur/cli/internal/cmd/service/get" serviceInstructionCmd "github.com/zeabur/cli/internal/cmd/service/instruction" @@ -36,6 +37,7 @@ func NewCmdService(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(serviceSuspendCmd.NewCmdSuspend(f)) cmd.AddCommand(serviceDeleteCmd.NewCmdDelete(f)) cmd.AddCommand(serviceDeployCmd.NewCmdDeploy(f)) + cmd.AddCommand(serviceExecCmd.NewCmdExec(f)) cmd.AddCommand(serviceInstructionCmd.NewCmdInstruction(f)) cmd.AddCommand(serviceNetworkCmd.NewCmdPrivateNetwork(f)) cmd.AddCommand(serviceUpdateCmd.NewCmdUpdate(f)) diff --git a/internal/cmd/service/suspend/suspend.go b/internal/cmd/service/suspend/suspend.go index c64e3de..a96a729 100644 --- a/internal/cmd/service/suspend/suspend.go +++ b/internal/cmd/service/suspend/suspend.go @@ -21,16 +21,10 @@ type Options struct { func NewCmdSuspend(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "suspend", Short: "suspend a service", - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runSuspend(f, opts) }, @@ -68,26 +62,24 @@ func runSuspendInteractive(f *cmdutil.Factory, opts *Options) error { } func runSuspendNonInteractive(f *cmdutil.Factory, opts *Options) error { - if opts.environmentID == "" { - projectID := f.Config.GetContext().GetProject().GetID() - envID, err := util.ResolveEnvironmentID(f.ApiClient, projectID) + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) if err != nil { return err } - opts.environmentID = envID + opts.id = service.ID } - if err := checkParams(opts); err != nil { - return err + if opts.id == "" { + return fmt.Errorf("--id or --name is required") } - // if name is set, get service id by name - if opts.id == "" && opts.name != "" { - service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) if err != nil { return err } - opts.id = service.ID + opts.environmentID = envID } // to show friendly message @@ -116,15 +108,3 @@ func runSuspendNonInteractive(f *cmdutil.Factory, opts *Options) error { return nil } - -func checkParams(opts *Options) error { - if opts.id == "" && opts.name == "" { - return fmt.Errorf("--id or --name is required") - } - - if opts.environmentID == "" { - return fmt.Errorf("--env-id is required") - } - - return nil -} diff --git a/internal/cmd/service/update/tag/tag.go b/internal/cmd/service/update/tag/tag.go index 97f3727..043f201 100644 --- a/internal/cmd/service/update/tag/tag.go +++ b/internal/cmd/service/update/tag/tag.go @@ -24,16 +24,10 @@ type Options struct { func NewCmdTag(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "tag", Short: "Update image tag of a prebuilt service", - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runUpdate(f, opts) }, @@ -83,26 +77,28 @@ func runInteractive(f *cmdutil.Factory, opts *Options) error { } func runNonInteractive(f *cmdutil.Factory, opts *Options) error { - if opts.environmentID == "" { - projectID := f.Config.GetContext().GetProject().GetID() - envID, err := util.ResolveEnvironmentID(f.ApiClient, projectID) + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) if err != nil { return err } - opts.environmentID = envID + opts.id = service.ID } - if err := checkParams(opts); err != nil { - return err + if opts.id == "" { + return fmt.Errorf("--id or --name is required") } - // if name is set, get service id by name - if opts.id == "" && opts.name != "" { - service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) if err != nil { return err } - opts.id = service.ID + opts.environmentID = envID + } + + if opts.tag == "" { + return fmt.Errorf("--tag is required") } idOrName := opts.name @@ -128,19 +124,3 @@ func runNonInteractive(f *cmdutil.Factory, opts *Options) error { return nil } - -func checkParams(opts *Options) error { - if opts.id == "" && opts.name == "" { - return fmt.Errorf("--id or --name is required") - } - - if opts.environmentID == "" { - return fmt.Errorf("--env-id is required") - } - - if opts.tag == "" { - return fmt.Errorf("--tag is required") - } - - return nil -} diff --git a/internal/cmd/template/deploy/deploy.go b/internal/cmd/template/deploy/deploy.go index f9b8720..d928be9 100644 --- a/internal/cmd/template/deploy/deploy.go +++ b/internal/cmd/template/deploy/deploy.go @@ -23,6 +23,7 @@ import ( type Options struct { file string projectID string + region string skipValidation bool vars map[string]string } @@ -40,6 +41,7 @@ func NewCmdDeploy(f *cmdutil.Factory) *cobra.Command { cmd.Flags().StringVarP(&opts.file, "file", "f", "", "Template file") cmd.Flags().StringVar(&opts.projectID, "project-id", "", "Project ID to deploy on") + cmd.Flags().StringVarP(&opts.region, "region", "r", "", "Region to create a new project in (e.g. tpe0, sfo0)") cmd.Flags().BoolVar(&opts.skipValidation, "skip-validation", false, "Skip template validation") cmd.Flags().StringToStringVar(&opts.vars, "var", nil, "Template variables (e.g. --var KEY=value)") @@ -88,7 +90,14 @@ func runDeploy(f *cmdutil.Factory, opts *Options) error { } } - if _, err := f.ParamFiller.ProjectCreatePreferred(&opts.projectID); err != nil { + if opts.region != "" && opts.projectID == "" { + project, err := f.ApiClient.CreateProject(context.Background(), opts.region, nil) + if err != nil { + return fmt.Errorf("create project in region %s: %w", opts.region, err) + } + opts.projectID = project.ID + f.Log.Infof("Created project %q in region %s.", project.ID, opts.region) + } else if _, err := f.ParamFiller.ProjectCreatePreferred(&opts.projectID); err != nil { return err } diff --git a/internal/cmd/upload/upload.go b/internal/cmd/upload/upload.go index 2378d75..4614ebe 100644 --- a/internal/cmd/upload/upload.go +++ b/internal/cmd/upload/upload.go @@ -27,7 +27,6 @@ func NewCmdUpload(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "upload", Short: "Upload local project to Zeabur", - PreRunE: util.NeedProjectContextWhenNonInteractive(f), RunE: func(cmd *cobra.Command, args []string) error { return runUpload(f, opts) }, diff --git a/internal/cmd/variable/create/create.go b/internal/cmd/variable/create/create.go index 3956d9c..a72439c 100644 --- a/internal/cmd/variable/create/create.go +++ b/internal/cmd/variable/create/create.go @@ -23,17 +23,11 @@ type Options struct { func NewCmdCreateVariable(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "create", Short: "create variable(s)", Long: `Create variable(s) for a service`, - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runCreateVariable(f, opts) }, @@ -92,16 +86,24 @@ func runCreateVariableInteractive(f *cmdutil.Factory, opts *Options) error { } func runCreateVariableNonInteractive(f *cmdutil.Factory, opts *Options) error { - zctx := f.Config.GetContext() + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if err != nil { + return err + } + opts.id = service.ID + } - if _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ - ProjectCtx: zctx, - ServiceID: &opts.id, - ServiceName: &opts.name, - EnvironmentID: &opts.environmentID, - CreateNew: false, - }); err != nil { - return err + if opts.id == "" { + return fmt.Errorf("--id or --name is required") + } + + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) + if err != nil { + return err + } + opts.environmentID = envID } s := spinner.New(cmdutil.SpinnerCharSet, cmdutil.SpinnerInterval, diff --git a/internal/cmd/variable/delete/delete.go b/internal/cmd/variable/delete/delete.go index 7cc04a1..708c2af 100644 --- a/internal/cmd/variable/delete/delete.go +++ b/internal/cmd/variable/delete/delete.go @@ -23,18 +23,12 @@ type Options struct { func NewCmdDeleteVariable(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "delete", Short: "delete variable(s)", Long: `delete variable(s) for a service`, Aliases: []string{"del"}, - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runDeleteVariable(f, opts) }, @@ -111,16 +105,24 @@ func runDeleteVariableInteractive(f *cmdutil.Factory, opts *Options) error { } func runDeleteVariableNonInteractive(f *cmdutil.Factory, opts *Options) error { - zctx := f.Config.GetContext() + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if err != nil { + return err + } + opts.id = service.ID + } - if _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ - ProjectCtx: zctx, - ServiceID: &opts.id, - ServiceName: &opts.name, - EnvironmentID: &opts.environmentID, - CreateNew: false, - }); err != nil { - return err + if opts.id == "" { + return fmt.Errorf("--id or --name is required") + } + + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) + if err != nil { + return err + } + opts.environmentID = envID } for _, v := range opts.deleteKeys { diff --git a/internal/cmd/variable/env/env.go b/internal/cmd/variable/env/env.go index 97b0480..5084d67 100644 --- a/internal/cmd/variable/env/env.go +++ b/internal/cmd/variable/env/env.go @@ -22,17 +22,11 @@ type Options struct { func NewCmdEnvVariable(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "env", Short: "update variables from .env", Long: "overwrite variables from a .env file", - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runUpdateVariableByEnv(f, opts) }, @@ -54,20 +48,41 @@ func runUpdateVariableByEnv(f *cmdutil.Factory, opts *Options) error { return fmt.Errorf("file cannot open: %s (%w)", opts.envFilename, err) } + if f.Interactive && opts.id == "" && opts.name == "" { + zctx := f.Config.GetContext() + if _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ + ProjectCtx: zctx, + ServiceID: &opts.id, + ServiceName: &opts.name, + EnvironmentID: &opts.environmentID, + CreateNew: false, + }); err != nil { + return err + } + } + return runUpdateVariableNonInteractive(f, opts) } func runUpdateVariableNonInteractive(f *cmdutil.Factory, opts *Options) error { - zctx := f.Config.GetContext() - - if _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ - ProjectCtx: zctx, - ServiceID: &opts.id, - ServiceName: &opts.name, - EnvironmentID: &opts.environmentID, - CreateNew: false, - }); err != nil { - return err + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if err != nil { + return err + } + opts.id = service.ID + } + + if opts.id == "" { + return fmt.Errorf("--id or --name is required") + } + + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) + if err != nil { + return err + } + opts.environmentID = envID } s := spinner.New(cmdutil.SpinnerCharSet, cmdutil.SpinnerInterval, diff --git a/internal/cmd/variable/list/list.go b/internal/cmd/variable/list/list.go index d5e4944..6f90c83 100644 --- a/internal/cmd/variable/list/list.go +++ b/internal/cmd/variable/list/list.go @@ -19,7 +19,6 @@ type Options struct { func NewCmdListVariables(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "list", @@ -27,11 +26,6 @@ func NewCmdListVariables(f *cmdutil.Factory) *cobra.Command { Long: `List environment variables of a service`, Args: cobra.NoArgs, Aliases: []string{"ls"}, - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runListVariables(f, opts) }, @@ -68,21 +62,29 @@ func runListVariablesInteractive(f *cmdutil.Factory, opts *Options) error { } func runListVariablesNonInteractive(f *cmdutil.Factory, opts *Options) error { - zctx := f.Config.GetContext() + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if err != nil { + return err + } + opts.id = service.ID + } - if _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ - ProjectCtx: zctx, - ServiceID: &opts.id, - ServiceName: &opts.name, - EnvironmentID: &opts.environmentID, - CreateNew: false, - }); err != nil { - return err + if opts.id == "" { + return fmt.Errorf("--id or --name is required") + } + + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) + if err != nil { + return err + } + opts.environmentID = envID } s := spinner.New(cmdutil.SpinnerCharSet, cmdutil.SpinnerInterval, spinner.WithColor(cmdutil.SpinnerColor), - spinner.WithSuffix(fmt.Sprintf(" Fetching environment variablesof service %s ...", opts.name)), + spinner.WithSuffix(fmt.Sprintf(" Fetching environment variables of service %s ...", opts.name)), ) s.Start() variableList, readonlyVariableList, err := f.ApiClient.ListVariables(context.Background(), opts.id, opts.environmentID) diff --git a/internal/cmd/variable/update/update.go b/internal/cmd/variable/update/update.go index 94c6589..c5fe442 100644 --- a/internal/cmd/variable/update/update.go +++ b/internal/cmd/variable/update/update.go @@ -22,17 +22,11 @@ type Options struct { func NewCmdUpdateVariable(f *cmdutil.Factory) *cobra.Command { opts := &Options{} - zctx := f.Config.GetContext() cmd := &cobra.Command{ Use: "update", Short: "update variable(s)", Long: `update variable(s) for a service`, - PreRunE: util.RunEChain( - util.NeedProjectContextWhenNonInteractive(f), - util.DefaultIDNameByContext(zctx.GetService(), &opts.id, &opts.name), - util.DefaultIDByContext(zctx.GetEnvironment(), &opts.environmentID), - ), RunE: func(cmd *cobra.Command, args []string) error { return runUpdateVariable(f, opts) }, @@ -113,16 +107,24 @@ func runUpdateVariableInteractive(f *cmdutil.Factory, opts *Options) error { } func runUpdateVariableNonInteractive(f *cmdutil.Factory, opts *Options) error { - zctx := f.Config.GetContext() + if opts.id == "" && opts.name != "" { + service, err := util.GetServiceByName(f.Config, f.ApiClient, opts.name) + if err != nil { + return err + } + opts.id = service.ID + } - if _, err := f.ParamFiller.ServiceByNameWithEnvironment(fill.ServiceByNameWithEnvironmentOptions{ - ProjectCtx: zctx, - ServiceID: &opts.id, - ServiceName: &opts.name, - EnvironmentID: &opts.environmentID, - CreateNew: false, - }); err != nil { - return err + if opts.id == "" { + return fmt.Errorf("--id or --name is required") + } + + if opts.environmentID == "" { + envID, err := util.ResolveEnvironmentIDByServiceID(f.ApiClient, opts.id) + if err != nil { + return err + } + opts.environmentID = envID } s := spinner.New(cmdutil.SpinnerCharSet, cmdutil.SpinnerInterval, diff --git a/internal/util/env.go b/internal/util/env.go index 6c4e52b..964c02e 100644 --- a/internal/util/env.go +++ b/internal/util/env.go @@ -22,7 +22,7 @@ func AddEnvOfServiceParam(cmd *cobra.Command, id *string) { // Every project has exactly one environment since environments are deprecated. func ResolveEnvironmentID(client api.Client, projectID string) (string, error) { if projectID == "" { - return "", fmt.Errorf("project ID is required to resolve environment ID; please set project context with `zeabur context set project`") + return "", fmt.Errorf("project ID is required to resolve environment ID") } environments, err := client.ListEnvironments(context.Background(), projectID) @@ -36,3 +36,16 @@ func ResolveEnvironmentID(client api.Client, projectID string) (string, error) { return environments[0].ID, nil } + +// ResolveEnvironmentIDByServiceID fetches the service, finds its project, +// and resolves the environment ID from that project. +func ResolveEnvironmentIDByServiceID(client api.Client, serviceID string) (string, error) { + service, err := client.GetService(context.Background(), serviceID, "", "", "") + if err != nil { + return "", fmt.Errorf("get service failed: %w", err) + } + if service.Project == nil || service.Project.ID == "" { + return "", fmt.Errorf("service has no associated project") + } + return ResolveEnvironmentID(client, service.Project.ID) +} diff --git a/pkg/api/interface.go b/pkg/api/interface.go index 7529a2c..eacff24 100644 --- a/pkg/api/interface.go +++ b/pkg/api/interface.go @@ -69,6 +69,7 @@ type ( GetDNSName(ctx context.Context, serviceID string) (string, error) UpdateImageTag(ctx context.Context, serviceID string, environmentID string, tag string) error DeleteService(ctx context.Context, id string, environmentID string) error + ExecuteCommand(ctx context.Context, serviceID string, environmentID string, command []string) (*model.CommandResult, error) } VariableAPI interface { diff --git a/pkg/api/service.go b/pkg/api/service.go index 8e23dc8..244538b 100644 --- a/pkg/api/service.go +++ b/pkg/api/service.go @@ -530,3 +530,20 @@ func (c *client) DeleteService(ctx context.Context, id string, environmentID str return err } + +func (c *client) ExecuteCommand(ctx context.Context, serviceID string, environmentID string, command []string) (*model.CommandResult, error) { + var mutation struct { + ExecuteCommand model.CommandResult `graphql:"executeCommand(serviceID: $serviceID, environmentID: $environmentID, command: $command)"` + } + + err := c.Mutate(ctx, &mutation, V{ + "serviceID": ObjectID(serviceID), + "environmentID": ObjectID(environmentID), + "command": command, + }) + if err != nil { + return nil, err + } + + return &mutation.ExecuteCommand, nil +} diff --git a/pkg/model/command.go b/pkg/model/command.go new file mode 100644 index 0000000..5f55f58 --- /dev/null +++ b/pkg/model/command.go @@ -0,0 +1,7 @@ +package model + +// CommandResult represents the result of a command execution. +type CommandResult struct { + ExitCode int `graphql:"exitCode"` + Output string `graphql:"output"` +}