Skip to content

Commit

Permalink
feat: interactive CLI (#2713)
Browse files Browse the repository at this point in the history
Typing just "ftl" will enter the interactive CLI

No completion or anything else yet, but it works fine.

<img width="585" alt="image"
src="https://github.com/user-attachments/assets/7143ff27-8575-4f77-9381-c74a23f91e66">
  • Loading branch information
alecthomas authored Sep 17, 2024
1 parent d3c2947 commit a0e0272
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 30 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/.ftl-project/
.hermit/
.vscode/*
!/.vscode/settings.json
Expand Down Expand Up @@ -39,4 +40,4 @@ junit*.xml
/readme-tests
/docs/public
.ftl.lock
docker-build/
docker-build/
64 changes: 64 additions & 0 deletions frontend/cli/cmd_interactive.go
Original file line number Diff line number Diff line change
@@ -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...))
}
64 changes: 35 additions & 29 deletions frontend/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <ref> and the secret will be stored in the password field." placeholder:"VAULT"`
Expand Down Expand Up @@ -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)
Expand All @@ -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))
Expand All @@ -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
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit a0e0272

Please sign in to comment.