Step-by-step guide to migrate a Cobra command to the current Glazed + bootstrap path with profile resolution and shared inference-debug output.
This tutorial shows how to migrate a CLI command from raw Cobra flags to the current Glazed + bootstrap path. The core APIs live in geppetto/pkg/cli/bootstrap, and the concrete reference implementations are the current Pinocchio JS command plus a downstream app-owned backend.
The target is not just “support --profile.” The target is a command that:
InferenceSettings,A migrated command has five layers:
CommandDescription for the command’s own flags and arguments.ResolveCLIEngineSettings(...) call that produces the final merged InferenceSettings.geppetto/pkg/cli/bootstrap.The most useful reference files are:
pinocchio/cmd/pinocchio/cmds/js.goThis is the most important concept to keep straight.
Geppetto owns the generic bootstrap behavior:
AppBootstrapConfigApplications own only their app-specific bootstrap wiring:
That means the reusable migration pattern belongs in Geppetto. Application packages should pass an app-owned AppBootstrapConfig into the Geppetto helper rather than recreating local copies of bootstrap logic.
Raw Cobra commands drift over time in predictable ways:
The shared bootstrap path gives every command the same answers to the same questions:
InferenceSettings?Stop declaring the normal command flags with cmd.Flags().StringVar(...). Move them into a CommandDescription.
type MyCommand struct {
*cmds.CommandDescription
}
func newMyCommand() (*MyCommand, error) {
baseSections, err := geppettosections.CreateGeppettoSections()
if err != nil {
return nil, err
}
inferenceDebugSection, err := geppettobootstrap.NewInferenceDebugSection()
if err != nil {
return nil, err
}
commandOptions := []cmds.CommandDescriptionOption{
cmds.WithShort("..."),
cmds.WithFlags(
fields.New("my-flag", fields.TypeString),
),
cmds.WithSections(inferenceDebugSection),
}
commandOptions = append(commandOptions, cmds.WithSections(baseSections...))
return &MyCommand{
CommandDescription: cmds.NewCommandDescription("my-command", commandOptions...),
}, nil
}
That does two things:
cli.BuildCobraCommand(...).For a command that needs inference/profile resolution, start with:
geppetto/pkg/sections.CreateGeppettoSections()That gives you:
ai-chatopenai-chatai-inferenceprofile-settingsIf you also want inference debug output, add:
geppetto/pkg/cli/bootstrap.NewInferenceDebugSection()That gives you:
--print-inference-settingsBecause CreateGeppettoSections() already includes profile-settings, you do not need to define your own --profile or --profile-registries flags.
This is the point where --print-parsed-fields starts working.
func NewMyCommand() *cobra.Command {
command, err := newMyCommand()
if err != nil {
panic(err)
}
cobraCommand, err := cli.BuildCobraCommand(command, cli.WithParserConfig(cli.CobraParserConfig{
MiddlewaresFunc: myMiddlewares,
}))
if err != nil {
panic(err)
}
return cobraCommand
}
This enables:
--print-parsed-fields--print-schema--print-yamlIf the command still bypasses cli.BuildCobraCommand(...), it will keep behaving like a special-case Cobra command.
Every application needs to define how Geppetto should load its config and sections. That contract is AppBootstrapConfig.
func appBootstrapConfig() geppettobootstrap.AppBootstrapConfig {
cfg := geppettobootstrap.AppBootstrapConfig{
AppName: "my-app",
EnvPrefix: "MY_APP",
ConfigFileMapper: myConfigMapper,
NewProfileSection: func() (schema.Section, error) {
return geppettosections.NewProfileSettingsSection()
},
BuildBaseSections: func() ([]schema.Section, error) {
return geppettosections.CreateGeppettoSections()
},
}
cfg.ConfigPlanBuilder = func(parsed *values.Values) (*glazedconfig.Plan, error) {
return glazedconfig.NewPlan(
glazedconfig.WithLayerOrder(glazedconfig.LayerSystem, glazedconfig.LayerUser, glazedconfig.LayerExplicit),
glazedconfig.WithDedupePaths(),
).Add(
glazedconfig.SystemAppConfig(cfg.AppName),
glazedconfig.XDGAppConfig(cfg.AppName),
glazedconfig.HomeAppConfig(cfg.AppName),
), nil
}
return cfg
}
Three details matter:
ConfigFileMapper must translate your app’s config file into section maps.BuildBaseSections must return the hidden base sections that participate in inference resolution and provenance.ConfigPlanBuilder owns app-config discovery; the shared bootstrap also uses AppName to discover an implicit ${XDG_CONFIG_HOME:-~/.config}/<app>/profiles.yaml registry source when profile-registries is otherwise empty.If you are working in Pinocchio, the application-specific bootstrap contract already exists as profilebootstrap.BootstrapConfig().
This section explains the central lifecycle concept behind the bootstrap APIs before the tutorial continues with middleware wiring.
The bootstrap path intentionally keeps two different settings objects in play:
BaseInferenceSettingsFinalInferenceSettingsThe mental model is:
shared sections + app config/env/defaults
-> base inference settings
base inference settings + resolved engine profile overlay
-> final inference settings
That is the reason AppBootstrapConfig.BuildBaseSections matters so much. It defines which shared Geppetto sections are eligible to participate in the hidden base.
Sequence sketch:
BuildBaseSections()
-> fresh schema
-> parse env + config + defaults
-> BaseInferenceSettings
-> resolve profile registry selection
-> merge profile overlay
-> FinalInferenceSettings
Practical implications:
ai-client.FinalInferenceSettings, not from raw profile payloads.Some host applications then add an additional runtime pattern on top of this: they preserve a profile-free base reconstructed from already parsed values so they can switch profiles later without losing CLI-supplied overrides. That runtime-switch pattern is application-owned, not Geppetto-owned. Pinocchio documents that companion pattern in pinocchio/pkg/doc/topics/pinocchio-profile-resolution-and-runtime-switching.md.
The middleware chain should load:
The exact config-file mapper is application-specific. A generic shape looks like this:
func myMiddlewares(parsed *values.Values, cmd *cobra.Command, args []string) ([]cmd_sources.Middleware, error) {
return []cmd_sources.Middleware{
cmd_sources.FromCobra(cmd, fields.WithSource("cobra")),
cmd_sources.FromArgs(args, fields.WithSource("arguments")),
cmd_sources.FromEnv("MY_APP", fields.WithSource("env")),
cmd_sources.FromConfigPlanBuilder(
appBootstrapConfig().ConfigPlanBuilder,
cmd_sources.WithConfigFileMapper(appBootstrapConfig().ConfigFileMapper),
cmd_sources.WithParseOptions(fields.WithSource("config")),
),
cmd_sources.FromDefaults(fields.WithSource(fields.SourceDefaults)),
}, nil
}
If you are working in Pinocchio, the important application-specific detail is still:
profilebootstrap.MapPinocchioConfigFile(...)because Pinocchio config files contain non-section keys like repositories.
Do not reimplement profile merging inside the command.
After parsing, call:
resolved, err := geppettobootstrap.ResolveCLIEngineSettings(ctx, appBootstrapConfig(), parsed)
if err != nil {
return err
}
if resolved.Close != nil {
defer resolved.Close()
}
The result gives you:
resolved.ProfileRuntimeresolved.BaseInferenceSettingsresolved.FinalInferenceSettingsresolved.ResolvedEngineProfileFor a normal engine-backed command, create engines from resolved.FinalInferenceSettings.
The rule to keep is simple:
The current debug surface is intentionally simple:
--print-parsed-fields from Glazed--print-inference-settings from geppetto/pkg/cli/bootstrap--print-inference-settings prints a combined document with:
settingssourcesSensitive values are masked as ***.
The generic pattern is:
debugSettings := &geppettobootstrap.InferenceDebugSettings{}
if err := parsed.DecodeSectionInto(geppettobootstrap.InferenceDebugSectionSlug, debugSettings); err != nil {
return err
}
if debugSettings.PrintInferenceSettings {
_, err := geppettobootstrap.HandleInferenceDebugOutput(
w,
appBootstrapConfig(),
parsed,
*debugSettings,
&geppettobootstrap.ResolvedInferenceTrace{
FinalInferenceSettings: resolved.FinalInferenceSettings,
ResolvedEngineProfile: resolved.ResolvedEngineProfile,
},
geppettobootstrap.InferenceDebugOutputOptions{},
)
return err
}
Two details matter here:
If your command injects an extra command-specific baseline before profile overlay, pass it as InferenceDebugOutputOptions.CommandBase. Otherwise leave it nil.
This is the subtle runtime bug that showed up in Pinocchio JS, but the rule is generic.
If your runtime can create engines later from selected profiles, do not feed it raw profile payload alone. Pass the CLI’s final merged settings into the runtime as defaults and merge from there.
Again, the rule is:
If you are starting from the Pinocchio repository, also read the companion guide in pinocchio/pkg/doc/tutorials/07-migrating-cli-verbs-to-glazed-profile-bootstrap.md. That document focuses only on the Pinocchio-specific deltas:
profilebootstrap.BootstrapConfig()profilebootstrap.MapPinocchioConfigFile(...)profilebootstrap.ResolveCLIEngineSettings(...)Run these after the migration:
go run ./cmd/<app> <command> --help --long-helpgo run ./cmd/<app> <command> --print-parsed-fieldsgo run ./cmd/<app> <command> --print-inference-settings--print-inference-settings output includes both settings: and sources:***| Problem | Cause | Solution |
|---|---|---|
unknown flag: --print-parsed-fields | The command still bypasses cli.BuildCobraCommand(...) | Convert it to a Glazed command and build Cobra through cli.BuildCobraCommand(...) |
| App config parsing fails on non-section keys | The config-file mapper is missing or too naive | Define an app-specific ConfigFileMapper in AppBootstrapConfig |
must be configured when profile-settings.profile is set | A profile was selected without any registry sources, and no implicit ${XDG_CONFIG_HOME:-~/.config}/<app>/profiles.yaml fallback was available | Configure profile-registries via flags/config, or ensure the app-owned default profiles.yaml exists in the XDG app config directory |
| Final engine misses API key or base URL | The command built from raw profile data instead of merged settings | Create engines from resolved.FinalInferenceSettings |
--print-inference-settings shows weak provenance | The command did not resolve settings through the shared bootstrap path | Use ResolveCLIEngineSettings(...) and HandleInferenceDebugOutput(...) from geppetto/pkg/cli/bootstrap |
| Downstream app loads the wrong config namespace | The app copied another app’s assumptions instead of defining its own bootstrap config | Create an app-owned AppBootstrapConfig with the right app name, env prefix, and config mapper |
pinocchio/cmd/pinocchio/cmds/js.go