Add a Command
This chapter adds a Command that lists the blog's posts in the terminal. You will learn how to:
- Create a Command with
act make:command - Use the App's services from a Command
- Return a result that people read as text and agents read as JSON
- Run the Command with
act, and in Agent mode
Overview
A Command is an action you run from the terminal. The App's own binary hosts its Commands: run with no arguments, it serves requests, and run with a Command's name, it runs that Command. act builds the binary and runs it for you, so act posts:list runs the App's posts:list.
Before a Command runs, the App starts as it does for serving, without listening for requests: providers register their bindings, and the boot, start and ready hooks run. It shuts down once the Command returns. The Command then asks the container for the store, the same binding the routes use.
Create the Command
act make:command writes a Command into app/commands, named as you type it, and adds it to the App. The posts: part groups it with other Commands about posts:
act make:command posts:listCREATE app/commands/posts_list.go
UPDATE start/app.go
+ app.Command(commands.PostsList(app))The generator imported the new package in start/app.go:
"blog/app/commands"
"blog/app/exceptions"
"blog/app/providers"
"blog/config"It also added the Command to the App, after the provider:
app := tinka.New(cfg.HTTP)
app.Provide(&providers.PostsProvider{})
app.Command(commands.PostsList(app))
app.OnError(exceptions.Handler)The generated Command already runs. It writes a status line and returns nothing:
act posts:list INFO posts:list ranList the posts
A Command doesn't print its result: its Run function returns it as a cli.Result, which is any type with a String method. People see what String returns. Agents, and anyone who passes --json, get the type's exported fields as JSON instead, so one Command serves both. postList holds the posts, with a json tag for the JSON key, and its String method writes one line per post.
PostsList receives the App, so Run can ask its container for the store. Make returns an error instead of panicking, which Run returns as the Command's failure. Run passes its ctx to the store, as the handlers pass the request: it's cancelled when you press Ctrl+C. Replace the generated file's contents:
package commands
import (
"context"
"fmt"
"strings"
"github.com/mucolabs/tinka"
"github.com/mucolabs/tinka/cli"
"blog/app/posts"
)
// postList is the posts:list Command's result.
type postList struct {
Posts []posts.Post `json:"posts"`
}
// String lists the posts for people, one per line.
func (l postList) String() string {
var b strings.Builder
for _, p := range l.Posts {
fmt.Fprintf(&b, "%4d %s\n", p.ID, p.Title)
}
return strings.TrimSuffix(b.String(), "\n")
}
// PostsList is the posts:list Command. It runs with the App started,
// so it can use the App's services.
func PostsList(app *tinka.App) *cli.Command {
return &cli.Command{
Name: "posts:list",
Summary: "List the blog's posts",
Run: func(ctx context.Context, c *cli.Call) (cli.Result, error) {
store, err := app.Container().Make[*posts.Store]()
if err != nil {
return nil, err
}
all, err := store.All(ctx)
if err != nil {
return nil, err
}
return postList{Posts: all}, nil
},
}
}Run the Command
Run it from the App's directory:
act posts:list 1 Hello, Tinka
2 Routes and handlersThe Command lists the two posts the store starts with, even when you created others through act serve. The Command runs in a process of its own, with its own App and its own store in memory. A store backed by a database would list the same posts in both, since both would read the same rows.
Summary describes the Command in the App's list of Commands, which act --help prints:
act --helpRun the App's Commands
Usage:
act <command> [flags]
Commands:
about Describe the App: versions, Mode and what its providers configure
generate:key Generate a random APP_KEY and write it to .env
posts:list List the blog's posts
completion Print a shell completion script
Flags:
-h, --help Show help
--json Print a single JSON result instead of text
--version Print the versionRun it as an agent
When an agent, such as a coding assistant, runs a Command, it gets Agent mode: the Command never prompts, and prints a single JSON result. act detects agents on its own. Pass --json to see what they get:
act posts:list --json{"command":"posts:list","result":"ok","posts":[{"id":1,"title":"Hello, Tinka","body":"The first post on the blog."},{"id":2,"title":"Routes and handlers","body":"How a request finds its handler."}],"duration_ms":0}postList's posts field sits between the keys every result has: command, result, and duration_ms. A Command that fails answers with "result":"error" and an error object instead, so an agent never has to parse text written for people.
Recap
posts:list lives in app/commands/posts_list.go, and start/app.go adds it with app.Command. It runs with the App started, takes the store from the container, and returns a postList that people read as lines and agents read as JSON. Next, Testing checks the routes and this Command with tests.
See also: Command line, Creating Commands, Flags, Agent mode