diff --git a/.gitignore b/.gitignore index 13a6c3e5ac..3770281bf8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/.ftl-project/ .hermit/ .vscode/* !/.vscode/settings.json @@ -39,4 +40,4 @@ junit*.xml /readme-tests /docs/public .ftl.lock -docker-build/ \ No newline at end of file +docker-build/ diff --git a/frontend/cli/cmd_interactive.go b/frontend/cli/cmd_interactive.go new file mode 100644 index 0000000000..dac5845282 --- /dev/null +++ b/frontend/cli/cmd_interactive.go @@ -0,0 +1,64 @@ +package main + +import ( + "context" + "errors" + "fmt" + "io" + "strings" + + "github.com/alecthomas/kong" + "github.com/chzyer/readline" + "github.com/kballard/go-shellquote" + + "github.com/TBD54566975/ftl/internal/projectconfig" +) + +type interactiveCmd struct { +} + +func (i *interactiveCmd) Run(ctx context.Context, k *kong.Kong, projectConfig projectconfig.Config) error { + l, err := readline.NewEx(&readline.Config{ + Prompt: "\033[32m>\033[0m ", + InterruptPrompt: "^C", + EOFPrompt: "exit", + }) + if err != nil { + return fmt.Errorf("init readline: %w", err) + } + l.CaptureExitSignal() + for { + line, err := l.Readline() + if errors.Is(err, readline.ErrInterrupt) { + if len(line) == 0 { + break + } + continue + } else if errors.Is(err, io.EOF) { + break + } + line = strings.TrimSpace(line) + args, err := shellquote.Split(line) + if err != nil { + errorf("%s", err) + continue + } + kctx, err := k.Parse(args) + if err != nil { + errorf("%s", err) + continue + } + subctx := bindContext(ctx, kctx, projectConfig) + + err = kctx.Run(subctx) + if err != nil { + errorf("%s", err) + continue + } + } + return nil +} + +func errorf(format string, args ...any) { + fmt.Printf("\033[31m%s\033[0m\n", fmt.Sprintf(format, args...)) +} diff --git a/frontend/cli/main.go b/frontend/cli/main.go index aef0439b7e..8cee5b23c0 100644 --- a/frontend/cli/main.go +++ b/frontend/cli/main.go @@ -30,26 +30,27 @@ type CLI struct { Authenticators map[string]string `help:"Authenticators to use for FTL endpoints." mapsep:"," env:"FTL_AUTHENTICATORS" placeholder:"HOST=EXE,…"` Insecure bool `help:"Skip TLS certificate verification. Caution: susceptible to machine-in-the-middle attacks."` - Ping pingCmd `cmd:"" help:"Ping the FTL cluster."` - Status statusCmd `cmd:"" help:"Show FTL status."` - Init initCmd `cmd:"" help:"Initialize a new FTL project."` - New newCmd `cmd:"" help:"Create a new FTL module."` - Dev devCmd `cmd:"" help:"Develop FTL modules. Will start the FTL cluster, build and deploy all modules found in the specified directories, and watch for changes."` - PS psCmd `cmd:"" help:"List deployments."` - Serve serveCmd `cmd:"" help:"Start the FTL server."` - Call callCmd `cmd:"" help:"Call an FTL function."` - Update updateCmd `cmd:"" help:"Update a deployment."` - Kill killCmd `cmd:"" help:"Kill a deployment."` - Schema schemaCmd `cmd:"" help:"FTL schema commands."` - Build buildCmd `cmd:"" help:"Build all modules found in the specified directories."` - Box boxCmd `cmd:"" help:"Build a self-contained Docker container for running a set of module."` - BoxRun boxRunCmd `cmd:"" hidden:"" help:"Run FTL inside an ftl-in-a-box container"` - Deploy deployCmd `cmd:"" help:"Build and deploy all modules found in the specified directories."` - Migrate migrateCmd `cmd:"" help:"Run a database migration, if required, based on the migration table."` - Download downloadCmd `cmd:"" help:"Download a deployment."` - Secret secretCmd `cmd:"" help:"Manage secrets."` - Config configCmd `cmd:"" help:"Manage configuration."` - Pubsub pubsubCmd `cmd:"" help:"Manage pub/sub."` + Interactive interactiveCmd `cmd:"" help:"Interactive mode." default:""` + Ping pingCmd `cmd:"" help:"Ping the FTL cluster."` + Status statusCmd `cmd:"" help:"Show FTL status."` + Init initCmd `cmd:"" help:"Initialize a new FTL project."` + New newCmd `cmd:"" help:"Create a new FTL module."` + Dev devCmd `cmd:"" help:"Develop FTL modules. Will start the FTL cluster, build and deploy all modules found in the specified directories, and watch for changes."` + PS psCmd `cmd:"" help:"List deployments."` + Serve serveCmd `cmd:"" help:"Start the FTL server."` + Call callCmd `cmd:"" help:"Call an FTL function."` + Update updateCmd `cmd:"" help:"Update a deployment."` + Kill killCmd `cmd:"" help:"Kill a deployment."` + Schema schemaCmd `cmd:"" help:"FTL schema commands."` + Build buildCmd `cmd:"" help:"Build all modules found in the specified directories."` + Box boxCmd `cmd:"" help:"Build a self-contained Docker container for running a set of module."` + BoxRun boxRunCmd `cmd:"" hidden:"" help:"Run FTL inside an ftl-in-a-box container"` + Deploy deployCmd `cmd:"" help:"Build and deploy all modules found in the specified directories."` + Migrate migrateCmd `cmd:"" help:"Run a database migration, if required, based on the migration table."` + Download downloadCmd `cmd:"" help:"Download a deployment."` + Secret secretCmd `cmd:"" help:"Manage secrets."` + Config configCmd `cmd:"" help:"Manage configuration."` + Pubsub pubsubCmd `cmd:"" help:"Manage pub/sub."` // Specify the 1Password vault to access secrets from. Vault string `name:"opvault" help:"1Password vault to be used for secrets. The name of the 1Password item will be the and the secret will be stored in the password field." placeholder:"VAULT"` @@ -103,12 +104,6 @@ func main() { os.Setenv("FTL_CONFIG", configPath) - config, err := projectconfig.Load(ctx, configPath) - if err != nil && !errors.Is(err, os.ErrNotExist) { - kctx.Fatalf(err.Error()) - } - kctx.Bind(config) - // Handle signals. sigch := make(chan os.Signal, 1) signal.Notify(sigch, syscall.SIGINT, syscall.SIGTERM) @@ -120,6 +115,19 @@ func main() { os.Exit(0) }() + config, err := projectconfig.Load(ctx, configPath) + if err != nil && !errors.Is(err, os.ErrNotExist) { + kctx.FatalIfErrorf(err) + } + ctx = bindContext(ctx, kctx, config) + + err = kctx.Run(ctx) + kctx.FatalIfErrorf(err) +} + +func bindContext(ctx context.Context, kctx *kong.Context, projectConfig projectconfig.Config) context.Context { + kctx.Bind(projectConfig) + controllerServiceClient := rpc.Dial(ftlv1connect.NewControllerServiceClient, cli.Endpoint.String(), log.Error) ctx = rpc.ContextWithClient(ctx, controllerServiceClient) kctx.BindTo(controllerServiceClient, (*ftlv1connect.ControllerServiceClient)(nil)) @@ -130,7 +138,5 @@ func main() { kctx.Bind(cli.Endpoint) kctx.BindTo(ctx, (*context.Context)(nil)) - - err = kctx.Run(ctx) - kctx.FatalIfErrorf(err) + return ctx } diff --git a/go.mod b/go.mod index 75e497774f..b45b44f09d 100644 --- a/go.mod +++ b/go.mod @@ -155,6 +155,7 @@ require ( github.com/alessio/shellescape v1.4.2 // indirect github.com/benbjohnson/clock v1.3.5 github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/chzyer/readline v1.5.1 github.com/danieljoos/wincred v1.2.0 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dop251/goja v0.0.0-20240816181238-8130cadc5774 // indirect diff --git a/go.sum b/go.sum index a89877528f..710c50fcac 100644 --- a/go.sum +++ b/go.sum @@ -89,6 +89,12 @@ github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -391,6 +397,7 @@ golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=