Generating a CLI Skeleton¶
The journey of a thousand miles begins with a single step, and for your new tool, that step is gtb generate project. π οΈ
Scaffolding a project from scratch can be tedious. generate project fast-tracks this process by setting up a robust, industry-standard project structure that's ready for high-scale development.
What's included in the box?¶
When you run generate project, we set up a complete, working CLI project:
- Project Core
- A
mainpackage incmd/<name>/and arootcommand inpkg/cmd/root. - Modern Tooling
- A
go.modpinned to the Go version that generated it (--go-version), withtooldirectives for the framework's changelog and docs tooling sogo toolruns them without a global install. - CI/CD Readiness
- A pipeline for the forge the project is hosted on: GitHub Actions workflows
for
--forge-backend github, a.gitlab-ci.ymlforgitlab. Gitea, Codeberg and Bitbucket have no CI skeleton, and the run says so. - Standard Layout
- A
pkg/directory for your logic and a DiΓ‘taxis-structureddocs/directory for your users. - The Manifest
- A
.gtb/manifest.yamlfile that acts as the brain of your project, tracking your settings and command hierarchy, beside a.gtb/ignorefor files you take over.
Project Structure Summary¶
What gtb generate project --name mytool --repo acme/mytool --features init,update,doctor,docs writes:
mytool/
βββ .github/workflows/ # CI/CD for the GitHub backend (a .gitlab-ci.yml for GitLab)
βββ .gtb/
β βββ manifest.yaml # The Brain: settings and command hierarchy
β βββ ignore # Files regenerate must leave alone
βββ cmd/mytool/
β βββ main.go # Entry Point: builds the root and hands it to Execute
β βββ chat.go # Chat provider links; present only with the ai feature
β βββ forge.go # Forge adapter links; present only with a forge feature
βββ internal/version/version.go # Build-time version, stamped by goreleaser
βββ pkg/cmd/root/
β βββ cmd.go # The Root: Props construction and command registration
β βββ generate.go # Generator marker; grows as commands are added
β βββ assets/init/config.yaml # Seed config for the init feature: yours to edit
βββ docs/ # Documentation: a DiΓ‘taxis-structured site
β βββ tutorials/ # Learning-oriented (neutral; off-site/blog by default)
β βββ how-to/ # Task-oriented guides
β βββ reference/cli/ # Generated CLI command reference
β βββ explanation/components/ # Generated package/architecture docs
βββ go.mod # Dependencies, plus tool directives for changelog and docs
βββ justfile # Build, test, lint, docs and release recipes
βββ README.md # Onboarding: install, build, develop, and links into GTB docs
The keychain feature adds cmd/mytool/keychain.go, a blank import of the
framework's keychain link; --signing adds internal/trustkeys/ and
pkg/cmd/root/signing.go.
The Generated Root Command¶
pkg/cmd/root/cmd.go describes the tool, builds its Props through
props.New (the one construction path) and returns the root command. It is
a DO NOT EDIT file: the manifest owns it, and gtb enable, gtb disable,
gtb set and gtb generate command rewrite it.
Annotated Example: pkg/cmd/root/cmd.go¶
As generated, with one command added by gtb generate command --name greet:
// Code generated by gtb. DO NOT EDIT.
package root
import (
"embed"
afero "github.com/spf13/afero"
greet "github.com/acme/mytool/pkg/cmd/greet"
gtbRoot "gitlab.com/phpboyscout/go-tool-base/pkg/cmd/root"
logger "gitlab.com/phpboyscout/go-tool-base/pkg/logger"
props "gitlab.com/phpboyscout/go-tool-base/pkg/props"
setup "gitlab.com/phpboyscout/go-tool-base/pkg/setup"
forge "gitlab.com/phpboyscout/go-tool-base/pkg/setup/forge"
version "gitlab.com/phpboyscout/go-tool-base/pkg/version"
"os"
)
//go:embed assets/*
var assets embed.FS
// NewCmdRoot builds the tool's Props through props.New, the one construction
// path, and the command tree on it. The error is a wiring defect in this
// file (an unnamed tool, a feature enabled that no import declares); main
// exits 2 on it.
func NewCmdRoot(v version.Info) (*setup.Command, *props.Props, error) {
l := logger.NewCharm(os.Stderr, logger.WithTimestamp(true), logger.WithLevel(logger.InfoLevel))
tool := props.Tool{
Description: "Example tool",
Features: props.SetFeatures(props.Disable(props.McpCmd), props.Disable(props.ChangelogCmd), props.Enable(forge.GithubFeature)),
Name: "mytool",
ReleaseSource: props.ReleaseSource{
Host: "github.com",
Owner: "acme",
Repo: "mytool",
Type: "github",
},
Summary: "mytool utility",
}
p, err := props.New(tool, l, afero.NewOsFs(), props.WithAssets(props.NewAssets(props.AssetMap{"root": &assets})), props.WithVersion(v))
if err != nil {
return nil, nil, err
}
rootCmd := gtbRoot.NewCmdRoot(p,
greet.NewCmdGreet(p))
return rootCmd, p, nil
}
Three things to notice. Features lists only what differs from the framework
defaults, so mcp and changelog appear as disabled and the GitHub forge as
enabled; the six default built-ins are on without being named. props.New
returns an error rather than panicking, and the root command is a
*setup.Command, the framework's wrapper that carries the feature gate and
middleware chain. Commands are registered as arguments to gtbRoot.NewCmdRoot,
one per generated command. A help channel chosen with --help-type appears as
Help: props.SlackHelp{...} on the Tool literal.
The Tool Entry Point¶
cmd/mytool/main.go builds the root and hands it to gtbRoot.Execute, which
owns the exit code:
// Code generated by gtb. DO NOT EDIT.
package main
import (
"fmt"
version "github.com/acme/mytool/internal/version"
root "github.com/acme/mytool/pkg/cmd/root"
gtbRoot "gitlab.com/phpboyscout/go-tool-base/pkg/cmd/root"
errorhandling "gitlab.com/phpboyscout/go/errorhandling"
"os"
)
// main delegates to gtbRoot.Execute, which runs the command tree with a
// signal-aware context: SIGINT/SIGTERM cancel cmd.Context() for graceful
// shutdown, a second signal force-exits immediately, and a signal-terminated
// run exits 128+signum (130 SIGINT, 143 SIGTERM).
// A construction error (an unnamed tool, an undeclared feature enabled) is a
// defect in this project's wiring and exits 2, the usage code, before any
// command runs.
func main() {
rootCmd, p, err := root.NewCmdRoot(version.Get())
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(errorhandling.ExitCodeUsage)
}
gtbRoot.Execute(rootCmd, p)
}
gtbRoot.Execute silences Cobra's own error printing and routes any error
returned from RunE through ErrorHandler at fatal level. The only
os.Exit in main.go is the one for a construction error.
The Generated README¶
The scaffold ships a rich, generic README.md that orients a newcomer: the
install command (go install <module>/cmd/<name>@latest), the just build/run
recipes, a Develop section (project layout, the .gtb/manifest.yaml regeneration
model, config and the environment-variable prefix), an "Enabled built-ins" list,
Releasing, and links into the GTB documentation site. Every command and path it
mentions exists in the generated tree.
Only one block is a placeholder: the "What is this?" section. Replace it with
your product's description: everything else is accurate framework prose you can
keep or trim. The README is yours: along with docs/index.md, justfile and
the init seed config it is scaffolded once and then preserved on every
gtb regenerate project, even under --overwrite allow, so your product blurb
never needs defending. An organisation that wants a house-style README can
supply one via a template overlay, which replaces this
embedded default.
How to run it¶
Navigate to the directory where you want your project to live and run:
gtb generate project \
--name "my-awesome-tool" \
--repo "my-github-org/my-awesome-tool-repo" \
--forge-backend github \
--help-type slack \
--slack-channel "#help" \
--slack-team "My Team"
Interactive wizard¶
You don't have to remember all the flags. Run gtb generate project without
--name (or without --repo for a hosted project) in a terminal and the
wizard asks one page at a time. Pages appear only when they apply; each is
one huh group, so shift+tab goes back a page and ctrl+c cancels.
- Start
- Name, description, destination path, the features to enable, whether the project is hosted on a forge (yes by default), and the help channel. Forges are not features here: the next page chooses one.
- Forge (hosted on a forge: yes)
- Forge Backend (GitHub, GitLab, Gitea, Codeberg or Bitbucket), the host
(leave empty for the backend's own host, shown as the placeholder; set it
for a self-managed instance), the repository as
org/repo(orgroup/subgroup/repoon GitLab), whether it is private, and any other forges to capture credentials for (theirinit <forge>wizard and adapter, never the release source). Only GitHub and GitLab have a CI skeleton; the others get no CI files and the run says so. - Module (hosted on a forge: no)
- The Go module path, since there is no host and repository to derive one from. A tool that is never imported can be a single word, and empty uses the project name, so the page never stops you going back to choose a forge after all.
- Environment variable prefix
- A choice: the prefix derived from the project name (
my-appgivesMY_APP, soMY_APP_LOG_LEVELoverrideslog.level), which is the default; None, so environment variables never override config; or Other, which opens a page to type one. The flag takes the value itself and empty means none. - Self-update (the
updatefeature selected) - The release channel, named for the forge chosen on the forge page and its host (GitLab releases (code.example.com)): the tool reads that repository's releases; the self-update policy (notify only, prompt, enforce); and the check interval. A self-updating tool cannot be generated without a channel, and the forge is the only channel in this release, so a project that is not hosted on a forge is refused here: go back and deselect Self-Update. A static release channel with no forge is being designed under #90.
- Chat providers (always)
- Which
go/chatprovider modules the binary links, recorded aschat.providers: a shortcut for wiring, for the tool's own code or for theaifeature, and independent of it. Per module:codex-locallinkschat-openai, which registersopenaiandopenai-compatibletoo. Nothing is ticked unlessaiwas selected on the flags, in which case the default set is; theaifeature needs at least one. - AI defaults (the
aifeature selected) - The default provider and optionally a model, for the AI-based features the
aiflag switches (docs ask,init ai). One linked provider is its own default; between several you choose, the wizard never does. The default select narrows to what is ticked. See chat defaults. - AI endpoint (the default is
openai-compatibleorazure-openai) - The API endpoint (HTTPS), and for Azure the dated API version.
- AI cloud addressing (the default is
gemini-vertexorbedrock) - Optional project and region; the modules fall back to the platform's environment.
- Telemetry (the
telemetryfeature selected) - Where usage events and OpenTelemetry data go; both optional.
- MCP (the
mcpfeature selected) - The publication mode, compact (three discovery tools, the default) or direct
(one native tool per command). On
gtb wizardthe page also lists the project's commands, ticked when they are on the MCP surface. - Help channel (Slack or Teams chosen)
- Slack channel and team, or Teams channel and team.
- Release signing (the
updatefeature selected) - Whether to verify self-update downloads, and if so the WKD email, key
source, key id, and whether to require a verified checksum on every update.
Requiring a signature is not asked on a first run (it breaks every update
until a signed release exists);
--signing-require-signatureandgtb enable signing --require-signatureset it. Answering No after entering details discards them.
The same wizard runs again on an existing project as
gtb wizard, pre-filled from the manifest;
there the name is shown rather than asked, there is no destination page, the
MCP page also asks which commands stay on the surface, and the signing page
also asks whether to require a signature.
Available Flags¶
| Flag | Short | Description | Default |
|---|---|---|---|
--name |
-n |
Name of your CLI tool | β |
--repo |
-r |
Repository in org/repo format |
β |
--forge-backend |
Forge the project is hosted on (github, gitlab, gitea, codeberg, bitbucket) |
github |
|
--host |
Git host (overrides backend default, for self-hosted instances) | β | |
--private |
Mark the repository as private (requires a token for updates) | false |
|
--description |
-d |
Short description of the tool | A tool built with gtb |
--path |
-p |
Destination path for the generated project | . |
--features |
-f |
Features to enable: built-ins (update, init, docs, doctor, changelog, ai, config, telemetry, man) and the links keychain and mcp. A forge is chosen with --forge-backend, not here. Replaces the default set rather than extending it: see the generate reference |
update, init, mcp, docs, doctor, changelog, keychain |
--go-version |
Go version for go.mod |
running toolchain version | |
--help-type |
Help channel type (slack, teams, or none) |
none |
|
--overwrite |
How to handle file conflicts (allow, deny, or ask) |
ask |
|
--slack-channel |
Slack channel (e.g. #my-team-help) |
β | |
--slack-team |
Slack team name (e.g. My Team) |
β | |
--teams-channel |
Microsoft Teams channel | β | |
--teams-team |
Microsoft Teams team name | β | |
--env-prefix |
Environment variable prefix for config overrides (e.g. MY_APP) |
β | |
--update-policy |
Self-update posture for the generated tool (disabled, prompt, or enabled); empty = framework default (disabled) |
β | |
--update-check-interval |
Baseline interval between self-update checks as a Go duration (e.g. 24h, 168h); empty = framework default (24h) |
β | |
--ci-component-source |
Override the phpboyscout/cicd component include base in the scaffolded GitLab pipeline |
gitlab.com/phpboyscout/cicd |
|
--no-git |
Skip the post-generation git init and initial commit (init + commit is on by default) | false |
|
--push |
After the initial commit, add the derived remote as origin and push the default branch (push failures are non-fatal) | false |
|
--git-branch |
Default branch the initial commit lands on | main |
|
--signing |
Enable consumer-side release-signing verification (scaffolds internal/trustkeys and wires props.Signing) |
false |
|
--signing-email |
Release WKD email for signing (external_key_email); implies --signing |
β | |
--signing-key-source |
Signing trust-anchor source (embedded, external, or both) |
both |
|
--signing-require-external-crosscheck |
Fail signing closed when the external (WKD) resolver is unreachable | false |
|
--signing-key-id |
Signing key id/ARN/alias (or PEM path for local) the release pipeline signs with; wires the GoReleaser signs block |
β | |
--signing-backend |
gtb sign backend for the release pipeline |
aws-kms (when --signing-key-id is set) |
|
--signing-kms-region |
AWS region for the aws-kms backend |
eu-west-2 |
|
--signing-public-key |
Path to the embedded public key the signature identifies | internal/trustkeys/keys/signing-key-v1.asc |
|
--template |
Custom template overlay source <src>@<ref> (local path or forge repo); repeatable, layered in order |
β | |
--dry-run |
Preview changes without writing files | false |
Custom Template Overlays¶
--template <src>@<ref> layers a custom template overlay over the embedded skeleton. Your own SECURITY.md, CODEOWNERS, a bespoke CI pipeline, etc. The flag is repeatable (sources layer in order, last writer wins). Manage sources on an existing project with the gtb template command group.
Dry-Run Mode¶
Use --dry-run to preview what generate project would produce without writing anything to disk:
This materialises all generated files into a temporary directory, runs go mod tidy and golangci-lint run --fix, then shows a summary of files that would be created or modified along with unified diffs.
Tip
The --host flag is only needed for a self-managed instance. For github.com, gitlab.com, gitea.com, codeberg.org or bitbucket.org, the host follows --forge-backend.
Help Channel Configuration¶
The skeleton generator supports two built-in help channel types, which populate the Tool.Help field in the generated root command:
Slack. Users are directed to a Slack channel in error messages:
Microsoft Teams. Users are directed to a Teams channel:
Both use the errorhandling.HelpConfig interface, so you can also provide a custom implementation.
Next Steps¶
Once your skeleton is generated, your project is ready to grow! Head over to the Command Generation guide to see how to add functionality.