Generator Package¶
The internal/generator package is the core engine responsible for all code generation, project scaffolding, and AST manipulation in gtb. This document provides a deep technical dive into the architecture for contributors.
Project Creation Architecture (skeleton.go)¶
When a user runs gtb generate skeleton, the following flow executes to scaffold a new project.
sequenceDiagram
participant CLI as CLI Entrypoint
participant Gen as Generator (skeleton.go)
participant FS as FileSystem
participant Manifest as Manifest Writer
participant Tmpl as Templates
CLI->>Gen: GenerateSkeleton(config)
Gen->>Gen: Validate Config (Org/Repo)
rect rgb(240, 248, 255)
Note right of Gen: 1. Generate Go Files
Gen->>Tmpl: Render SkeletonMain (cmd/main.go)
Gen->>Tmpl: Render SkeletonRoot (pkg/cmd/root/cmd.go)
Gen->>FS: Write Go Source Files
end
rect rgb(255, 240, 245)
Note right of Gen: 2. Generate Static Assets
Gen->>Tmpl: Render go.mod
Gen->>Tmpl: Render config.yaml
Gen->>FS: Copy Embedded Assets (walkDir)
end
rect rgb(240, 255, 240)
Note right of Gen: 3. Create Manifest
Gen->>Manifest: Create Manifest Struct
Manifest->>FS: Write .gtb/manifest.yaml
end
rect rgb(255, 250, 205)
Note right of Gen: 4. Post-Generation Hooks
Gen->>CLI: exec "go mod tidy"
Gen->>CLI: exec "golangci-lint run --fix"
end
Key Implementation Details¶
- Jennifer & Templates: We use a hybrid approach.
github.com/dave/jenniferis used for generating complex Go files where imports need to be managed dynamically (thoughskeleton.gocurrently uses our own tempaltes).text/templateis used for static boilerplate and config files.
- Asset Embed: The
assets/skeleton,assets/skeleton-github, andassets/skeleton-gitlabdirectories are all embedded into the binary using//go:embed. The common assets inassets/skeletonare always applied; VCS-specific assets (skeleton-githuborskeleton-gitlab) are selected based on the--git-backendflag. This allows the CLI to operate as a single static binary without needing external resource files.
Generated Files Reference¶
The following files are copied verbatim (or rendered as templates) from the embedded assets during generate skeleton:
Core Configuration¶
.gitignore: Standard Go ignore patterns..golangci.yaml: Strict linting configuration..mockery.yml: Mock generation config.justfile: Development task runner definitions (replaces the legacyTaskfile.yml).go.mod: Go module definition (templated).
CI/CD & Automation (.github/)¶
CODEOWNERS: Default ownership rules.renovate.json5: Dependency update configuration.workflows/lint.yaml: CI linting checks.workflows/test.yaml: CI unit tests with race detection.workflows/goreleaser.yaml: Release automation.workflows/semantic-release.yaml: Automated versioning.workflows/docs.yaml: Documentation publishing (GitHub) or.gitlab/ci/pages.yml(GitLab).
Documentation (docs/)¶
zensical.toml: Documentation site configuration (Zensical/MkDocs-Material).docs/index.md: Placeholder landing page.
Command Generation Architecture¶
The command generation process is significantly more complex as it involves modifying existing code (AST manipulation) ensuring we don't break user logic. The post-generation steps are encapsulated in CommandPipeline (pipeline.go).
flowchart TD
Start([Generate Command]) --> Verify{Verify Project}
Verify -- Fail --> Error
Verify -- Pass --> Prep[Prepare & Verify Config]
Prep --> Protected{Is Protected?}
Protected -- Yes --> Stop([Abort])
Protected -- No --> ResolveFlags[Resolve Flags]
ResolveFlags --> AI{AI Requested?}
AI -- Yes --> RunAI[Run Autonomous Agent]
AI -- No --> Render[Render Templates]
RunAI --> VerifyAI{AI Success?}
VerifyAI -- No --> Fallback[Use Placeholder Logic]
VerifyAI -- Yes --> Render
Render --> FileSys[Write files to pkg/cmd/...]
FileSys --> Pipeline[CommandPipeline.Run]
subgraph PipelineSteps["CommandPipeline (pipeline.go)"]
direction TB
P1[1. Copy Assets] --> P2[2. Register in Parent]
P2 --> P3[3. Re-register Children]
P3 --> P4[4. Persist Manifest]
P4 --> P5[5. Generate Documentation]
end
Pipeline -.-> PipelineSteps
Pipeline --> End([Success])
subgraph ASTInjection["AST Injection (ast.go)"]
FindParent[Find Parent NewCmd* Func]
ParseAST[Parse File to AST]
InjectCall[Inject AddCommand Call]
InjectImport[Add Import if Missing]
WriteAST[Write File Back]
FindParent --> ParseAST --> InjectCall --> InjectImport --> WriteAST
end
P2 -.-> ASTInjection
P3 -.-> ASTInjection
Detailed Responsibilities¶
- Project Scaffolding: Creating new directory structures for tools (
skeleton.go). - Command Generation: creating boilerplate (
cmd.go) and implementation (main.go) files for new commands (commands.go). - Post-generation Pipeline: Sequencing the five ordered post-generation steps (assets, parent registration, child re-registration, manifest persistence, documentation) via
CommandPipeline(pipeline.go). - AST Manipulation: Safely modifying existing Go source files to register commands, add flags, and inject imports (
ast.go). - Manifest Management: Reading, writing, and synchronizing the
.gtb/manifest.yamlfile;ManifestCommandUpdateprovides a structured API for manifest mutations (manifest_update.go,manifest_io.go,manifest_hash.go). - Project Regeneration: Rebuilding all boilerplate from the manifest, including child command re-registration and full propagation of help-channel configuration (
regenerate.go). - AI Integration: Orchestrating the conversion of natural language or scripts into Go code (
ai.go).
Key Components¶
1. The Generator Struct¶
The Generator struct is the main entry point for all generation operations. It holds the configuration context and dependencies.
type Generator struct {
config *Config // Command-specific configuration (Name, Parent, Flags)
props *props.Props // Global tool properties (Logger, FS)
}
Common entry points:
Generate(ctx): Orchestrates the generation of a new command.Remove(ctx): Handles command removal and cleanup.RegenerateProject(ctx): Rebuilds the entire CLI boilerplate from the manifest.
2. CommandPipeline (pipeline.go)¶
CommandPipeline owns the five ordered steps that run after every cmd.go is written. It is constructed via newCommandPipeline(g, PipelineOptions{}) and its behaviour can be tuned with PipelineOptions:
Steps:
| # | Step | What it does |
|---|---|---|
| 1 | Copy Assets | Copies any embedded static assets for the command. |
| 2 | Register in Parent | Calls AddCommandToParent to inject cmd.AddCommand(...) into the parent's cmd.go. |
| 3 | Re-register Children | Reads the manifest to find existing child commands and re-injects their AddCommand calls. This preserves child registrations when a parent command is overwritten. |
| 4 | Persist Manifest | Calls updateManifest with a ManifestCommandUpdate to write hashes and metadata. |
| 5 | Generate Docs | Invokes the AI documentation helper (or skips if a doc file already exists). |
Non-fatal step failures are returned as StepWarning values inside PipelineResult rather than aborting the pipeline.
3. CommandContext (context.go)¶
CommandContext is a value type that captures the fully-resolved name, parent path, and import path for a command. buildCommandContext is the sole factory:
reRegisterChildCommands (step 3 above) uses buildCommandContext to construct a child generator with the correct package and import path before calling AddCommandToParent.
4. AST Manipulation (ast.go)¶
One of the most complex parts of the generator is safely editing existing Go code. We use the standard library go/ast (and dave/dst for better comment preservation) to parse, modify, and print Go code.
The Injection Challenge:
When adding a subcommand (e.g., server start), we must:
- Locate
pkg/cmd/server/cmd.go. - Find the
NewCmdServerfunction. - Find the variable declaration for the
cobra.Command. - Inject
cmd.AddCommand(start.NewCmdStart(props))before the return statement. - Add the import
.../pkg/cmd/server/startto the file imports.
Key Functions:
AddCommandToParent: Orchestrates the injection flow.AddFlagToCommand: Injects a flag definition (e.g.,cmd.Flags().StringVar...) into a specific command'sNewCmd*function.AddImport: Adds necessary imports only if they are missing, handling alias resolution.
Design Principle: We strictly separate Boilerplate (generated, overwritable) from Implementation (user-owned).
cmd.go: Fully owned by the generator. Can be blown away and recreated.main.go: Owned by the user. The generator only creates it if missing (or forced), and never modifies logic inside it (except via AI augmentation).
5. Manifest Management¶
The manifest.yaml serves as the "Source of Truth" for the project structure. It maps the hierarchical relationships of commands that might be scattered across the filesystem.
Structure:
commands:
- name: server
description: Start the server
commands:
- name: start
description: Start the service
flags:
- name: port
type: int
The generator ensures that filesystem changes (creating a folder) are always reflected in the manifest, and vice-versa (removing from manifest removes the folder).
Manifest mutations use the ManifestCommandUpdate struct (manifest_update.go) rather than positional parameters, making call sites self-documenting:
type ManifestCommandUpdate struct {
Name, Description, LongDescription string
Aliases []string
Args string
Hashes map[string]string
Flags []ManifestFlag
WithAssets, WithInitializer bool
PersistentPreRun, PreRun bool
Protected *bool
Hidden bool
}
Manifest file I/O lives in manifest_io.go; hash calculation in manifest_hash.go.
6. Regeneration (regenerate.go)¶
RegenerateProject reads the manifest and rebuilds all boilerplate. The root command is handled by regenerateRootCommand, which delegates field mapping to buildSkeletonRootData:
func buildSkeletonRootData(m Manifest, subcommands []templates.SkeletonSubcommand) templates.SkeletonRootData
This function is the single source of truth for mapping manifest fields โ including the full ManifestHelp struct (help type, Slack channel/team, Teams channel/team) โ to SkeletonRootData. Keeping this mapping in one place prevents settings from being silently dropped when the root command is regenerated.
Each non-root command is handled by regenerateCommandRecursive, which calls through performGeneration โ postGenerate โ CommandPipeline.Run with SkipRegistration: true (children re-register themselves in step 3 of the pipeline).
7. Templating (templates/)¶
We use Go's text/template engine to render code. Templates are stored as string constants (or embedded files) to ensure the binary is self-contained.
command.go.tmpl: The registration boilerplate.main.go.tmpl: The implementation stub.main_test.go.tmpl: Unit test scaffolding.
Development Workflows¶
Adding a New Flag Type¶
- Update
internal/generator/manifest.goto support the new type in theManifestFlagstruct. - Update
internal/generator/manifest_update.goif the new type affects theManifestCommandUpdatestruct orupdateCommandRecursivelogic. - Update
internal/generator/templates/command.goto map the type to the corresponding Cobra method (e.g.,Flags().DurationVar). - Update
internal/generator/ast.goif the flag needs to be injectable into existing ASTs (complex types might need special handling).
Debugging AST Issues¶
If the generator fails to modify a file correctly:
- Enable debug logging:
go run main.go --debug ... - Inspect the
ast.gologic. The most common issues are:- Target function not found (naming mismatch).
- Import aliases interfering with type resolution.
- Syntax errors in the source file preventing parsing.
Testing¶
The generator relies heavily on integration tests that simulate a real filesystem using afero.MemMapFs.
The pipeline_test.go file provides two shared helpers:
setupTestProject(t, path)โ scaffolds a minimal in-memory project viaGenerateSkeletonwith a mockedrunCommandand aconfig.NewFilesContainerso AI config resolution does not panic.generateCmd(t, p, path, name, parent)โ pre-creates a doc stub at the correct nested path (e.g.docs/commands/start/stop/index.md) before callingGenerate. This preventshandleDocumentationGenerationfrom making live AI API calls that would hang tests.
func TestGenerateCommand(t *testing.T) {
t.Setenv("GTB_NON_INTERACTIVE", "true")
path := "/work"
p := setupTestProject(t, path)
generateCmd(t, p, path, "mycmd", "root")
exists, _ := afero.Exists(p.FS, filepath.Join(path, "pkg/cmd/mycmd/cmd.go"))
assert.True(t, exists)
}
Key Test Files¶
| File | Purpose |
|---|---|
pipeline_test.go |
CommandPipeline behaviour, child re-registration, SkipRegistration, manifest hash consistency |
regenerate_test.go |
End-to-end RegenerateProject including help config preservation |
recursive_test.go |
ManifestCommandUpdate round-trips via updateCommandRecursive |
ast_test.go |
AST injection correctness |