tinka alpha
tinkaalpha

Add a Command

This chapter adds a Command that lists the blog's posts in the terminal. You will learn how to:

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:

Terminal
act make:command posts:list
Output
CREATE app/commands/posts_list.go
UPDATE start/app.go
         + app.Command(commands.PostsList(app))

The generator imported the new package in start/app.go:

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:

start/app.go
	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:

Terminal
act posts:list
Output
  INFO  posts:list ran

List 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:

app/commands/posts_list.go
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:

Terminal
act posts:list
Output
   1  Hello, Tinka
   2  Routes and handlers

The 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:

Terminal
act --help
Output
Run 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 version

Run 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:

Terminal
act posts:list --json
Output
{"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