# Creating Commands

> Write your own Commands as Go values, return results for people and agents, use the App's services, and guard Commands that change data.

This guide covers creating your own Commands. You will learn how to:

- Create a Command and add it to the App
- Return a result for people and agents
- Name, describe and group Commands
- Use the App's services
- Report errors that tell the reader how to recover
- Stop cleanly on Ctrl+C, and run other programs
- Guard Commands that change data

## Create your first Command

A Command is a `*cli.Command` (`github.com/mucolabs/tinka/cli`). It needs a `Name`, which people type, and a `Run` function. Add it to the App with `app.Command`:

```go title="main.go"
package main

import (
	"context"
	"os"

	"github.com/mucolabs/tinka"
	"github.com/mucolabs/tinka/cli"
)

// greeting is the greet Command's result.
type greeting string

func (g greeting) String() string { return string(g) }

func main() {
	app := tinka.New(tinka.HTTPConfig{})
	app.Command(&cli.Command{
		Name:    "greet",
		Summary: "Greet someone",
		Args:    []cli.Arg{{Name: "name", Usage: "Who to greet"}},
		Run: func(ctx context.Context, c *cli.Call) (cli.Result, error) {
			name := "world"
			if len(c.Args) > 0 {
				name = c.Args[0]
			}
			return greeting("Hello, " + name + "!"), nil
		},
	})
	os.Exit(app.Main(os.Args[1:]))
}
```

Run it with `act`:

```sh
act greet Bea
```

```text title="Output"
Hello, Bea!
```

`Run` receives a context and a `*cli.Call`, which holds the arguments and the [prompts](prompts.md) and [output](terminal-output.md) the Command uses.

## Return a result

`Run` returns what the Command did as a `cli.Result`, instead of printing it. A Result is any type with a `String` method: people see its `String` form on stdout. Agents, and anyone passing `--json`, get its exported fields as JSON:

```go
type published struct {
	Count int `json:"count"`
}

func (p published) String() string { return fmt.Sprintf("Published %d posts", p.Count) }
```

```text title="Output"
$ act posts:publish
Published 3 posts

$ act posts:publish --json
{"command":"posts:publish","result":"ok","count":3,"duration_ms":12}
```

Return `nil` when there's nothing to report. Keep the Result to what the Command did; what it's doing while it runs is [progress](terminal-output.md), which goes to stderr.

## Name and describe Commands

Group related Commands under a namespace with a colon, such as `posts:publish` and `posts:import`. Fill in the rest of the fields for help:

```go
var greet = &cli.Command{
	Name:    "greet",
	Summary: "Greet someone",
	Help:    "Greets the name given, or the world.\n\nact greet Bea",
	Aliases: []string{"hello"},
	Run:     runGreet,
}
```

::::options

:::option{name="Summary" type="string"}
The one-line description shown in the list of Commands.
:::

:::option{name="Help" type="string"}
The longer description shown by the Command's own help, such as examples.
:::

:::option{name="Aliases" type="[]string"}
Other names that run the Command.
:::

:::option{name="Hidden" type="bool"}
Leaves the Command out of help and lists. It still runs.
:::

:::option{name="Commands" type="[]*cli.Command"}
Commands nested under this one, run as `act db seed`. A Command with Commands and no `Run` only groups them.
:::

::::

## Use the App's services

The App starts before one of its Commands runs, and shuts down after the Command returns, so the Command can use the App's services, such as a database pool, from the [container](../concepts/dependency-injection.md). Build the Command in a function that receives the App:

```go title="publish.go"
package main

import (
	"context"
	"fmt"

	"github.com/mucolabs/tinka"
	"github.com/mucolabs/tinka/cli"
)

type published struct {
	Count int `json:"count"`
}

func (p published) String() string { return fmt.Sprintf("Published %d posts", p.Count) }

func publishCommand(app *tinka.App) *cli.Command {
	var limit int
	return &cli.Command{
		Name:    "posts:publish",
		Summary: "Publish the posts scheduled until now",
		Flags: func(f *cli.FlagSet) {
			f.IntVar(&limit, "limit", 100, "Publish at most this many posts")
		},
		Run: func(ctx context.Context, c *cli.Call) (cli.Result, error) {
			posts, err := app.Container().Make[*Posts]()
			if err != nil {
				return nil, err
			}
			n, err := posts.PublishScheduled(ctx, limit)
			if err != nil {
				return nil, err
			}
			return published{Count: n}, nil
		},
	}
}
```

Add it in `main` with `app.Command(publishCommand(app))`. Starting runs the providers' `Start` and `Ready` hooks, and the App's own start and ready hooks, without serving requests. See [Application lifecycle](../concepts/application-lifecycle.md).

## Report errors

Return an error from `Run` when the Command fails: it exits with status `1` and shows the error. Return a `*cli.Error` when you know how the reader can recover, since people and agents both read its fields:

```go
		if !strings.HasSuffix(file, ".json") {
			return nil, &cli.Error{
				Code:    "unsupported_file",
				Message: "posts:import reads JSON files only",
				Fix:     "Export the posts as JSON, then run the Command again",
			}
		}
```

```text title="Output"
Error: posts:import reads JSON files only.
  Fix: Export the posts as JSON, then run the Command again
```

::::options

:::option{name="Code" type="string"}
A stable snake_case identifier that scripts and agents match on.
:::

:::option{name="Message" type="string"}
What went wrong, in one sentence without a final period.
:::

:::option{name="Hint" type="string"}
Why it happened, or what to check.
:::

:::option{name="Fix" type="string"}
The command or change that resolves it.
:::

:::option{name="Docs" type="string"}
A link to the page that explains it.
:::

::::

When the Command was called wrongly, such as with a value out of range, return `cli.UsageError`. It exits with status `2` and points to the Command's help:

```go
			if port < 1 || port > 65535 {
				return nil, cli.UsageError("--port must be between 1 and 65535, got %d", port)
			}
```

## Stop on Ctrl+C

When the person presses Ctrl+C, or the process receives SIGTERM, the context `Run` received is cancelled. A long-running Command, such as a queue worker, watches it to finish the work in hand and return:

```go
		Run: func(ctx context.Context, c *cli.Call) (cli.Result, error) {
			for {
				select {
				case <-ctx.Done():
					return nil, nil // finish the job in hand, then stop
				case job := <-jobs:
					process(ctx, job)
				}
			}
		},
```

The Command then exits with status `130` after Ctrl+C, or `143` after SIGTERM. A second Ctrl+C ends the process at once.

## Run other programs

`c.Exec` runs a program and waits for it. Its output goes to the Command's [progress](terminal-output.md), and when the context is cancelled the program is interrupted, then killed if it hasn't exited five seconds later:

```go
			if err := c.Exec(ctx, "go", "mod", "tidy"); err != nil {
				return nil, err
			}
```

## Guard Commands that change data

Mark a Command that changes or deletes data in ways that are hard to undo as `Destructive`. When the App runs in production, it asks for confirmation first. With nobody to answer, it refuses to run until `--force` is passed. The Command gains the `--force` flag on its own:

```go
var wipe = &cli.Command{
	Name:        "db:wipe",
	Summary:     "Drop every table",
	Destructive: true,
	Run:         wipeTables,
}
```

```text title="Output"
$ APP_ENV=production act db:wipe
 WARN  The App is in production.
? Are you sure you want to run db:wipe? (y/N)
```

Production here is any `APP_ENV` other than `development` or `test`, so a mistyped Mode still asks.

To refuse a Command entirely, even with `--force`, prohibit it. Call `Prohibit` before `Main`:

```go
	if e.Mode() == env.Production {
		app.Prohibit("db:wipe")
	}
```

### Run one at a time

Mark a Command `Isolated` when two runs at once would do harm, such as an import that a scheduler starts again before the last one finished. It gains an `--isolated` flag: with it, the run is skipped while another `--isolated` run of the same Command holds the lock, on the same machine and for the same App. A skipped run succeeds, so a scheduler doesn't report a failure, and says so; `--isolated=<code>` exits with that status instead:

```sh
act posts:import feed.json --isolated
```

The lock is a file lock, so the operating system releases it when the process ends, even after a crash.

See also: [Arguments](arguments.md), [Flags](flags.md), [Prompts](prompts.md), [Terminal output](terminal-output.md), [Testing Commands](testing.md)
