CLI Command Migration Guide (Glazed + Bootstrap + Profiles)

Step-by-step guide to migrate a Cobra command to the current Glazed + bootstrap path with profile resolution and shared inference-debug output.

Sections

Terminology & Glossary
📖 Documentation
Navigation
54 sectionsv0.1
📄 CLI Command Migration Guide (Glazed + Bootstrap + Profiles) — glaze help geppetto-cli-bootstrap-profile-migration
geppetto-cli-bootstrap-profile-migration

CLI Command Migration Guide (Glazed + Bootstrap + Profiles)

Step-by-step guide to migrate a Cobra command to the current Glazed + bootstrap path with profile resolution and shared inference-debug output.

Tutorialgeppettocliglazedbootstrapprofilesconfigmigrationtutorialgeppettoconfig-fileprofileprofile-registriesprint-parsed-fields+1

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:

  • parses flags and positional arguments through Glazed,
  • resolves config and profiles through a single bootstrap contract,
  • creates engines from the final merged InferenceSettings,
  • and can print one combined inference-debug document for troubleshooting.

What You Are Building

A migrated command has five layers:

  1. A Glazed CommandDescription for the command’s own flags and arguments.
  2. Shared Geppetto sections for inference settings and profile selection.
  3. An app-owned middleware chain that merges Cobra, arguments, environment, config files, and defaults.
  4. A single ResolveCLIEngineSettings(...) call that produces the final merged InferenceSettings.
  5. An optional shared inference-debug path owned by geppetto/pkg/cli/bootstrap.

The most useful reference files are:

Ownership Boundary

This is the most important concept to keep straight.

Geppetto owns the generic bootstrap behavior:

  • AppBootstrapConfig
  • profile/config resolution
  • final merged engine settings
  • inference-debug output

Applications own only their app-specific bootstrap wiring:

  • app name and env prefix
  • config-file mapper
  • command-specific runtime behavior

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.

Why This Migration Matters

Raw Cobra commands drift over time in predictable ways:

  • config loading differs from one command to another,
  • profile selection and registry handling diverge,
  • engine creation bypasses merged settings,
  • and debugging becomes inconsistent.

The shared bootstrap path gives every command the same answers to the same questions:

  • Which config file was loaded?
  • Which profile registries were used?
  • Which profile actually resolved?
  • What are the final InferenceSettings?
  • Why does a specific field have its current value?

Step 1: Replace Local Cobra Flags with a Glazed Command Description

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:

  • the command-specific flags become declarative and Glazed-owned,
  • and the command becomes compatible with cli.BuildCobraCommand(...).

Step 2: Reuse the Shared Geppetto Sections

For a command that needs inference/profile resolution, start with:

  • geppetto/pkg/sections.CreateGeppettoSections()

That gives you:

  • ai-chat
  • provider sections like openai-chat
  • ai-inference
  • profile-settings

If you also want inference debug output, add:

  • geppetto/pkg/cli/bootstrap.NewInferenceDebugSection()

That gives you:

  • --print-inference-settings

Because CreateGeppettoSections() already includes profile-settings, you do not need to define your own --profile or --profile-registries flags.

Step 3: Build Cobra Through Glazed

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-yaml
  • normal Glazed command parsing

If the command still bypasses cli.BuildCobraCommand(...), it will keep behaving like a special-case Cobra command.

Step 4: Define an App-Owned Bootstrap Contract

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().

Hidden Base vs Final Settings

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:

  • BaseInferenceSettings
  • FinalInferenceSettings

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

  • Put cross-profile settings in shared Geppetto sections such as ai-client.
  • Build engines from FinalInferenceSettings, not from raw profile payloads.
  • Do not assume the profile itself is the whole runtime configuration.

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.

Step 5: Use an App-Aware Config Middleware

The middleware chain should load:

  • Cobra values,
  • positional arguments,
  • environment variables,
  • config files,
  • and defaults.

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.

Step 6: Resolve Final Engine Settings Once

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.ProfileRuntime
  • resolved.BaseInferenceSettings
  • resolved.FinalInferenceSettings
  • resolved.ResolvedEngineProfile

For a normal engine-backed command, create engines from resolved.FinalInferenceSettings.

The rule to keep is simple:

  • build engines from the final merged settings, not from raw profile payload alone.

Step 7: Use the Shared Inference Debug Helper

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:

  • settings
  • sources

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

  1. The helper is Geppetto-owned, not app-owned.
  2. The helper reconstructs hidden-base parsed values for provenance internally, so callers usually do not need to do that themselves.

If your command injects an extra command-specific baseline before profile overlay, pass it as InferenceDebugOutputOptions.CommandBase. Otherwise leave it nil.

Step 8: If the Command Exposes Runtime Profile APIs, Pass the Defaults Through

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:

  • build engines from the final merged settings, not from raw profile payload alone.

Pinocchio Companion

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(...)
  • Pinocchio JS runtime defaults

Validation Checklist

Run these after the migration:

  1. go run ./cmd/<app> <command> --help --long-help
  2. go run ./cmd/<app> <command> --print-parsed-fields
  3. go run ./cmd/<app> <command> --print-inference-settings
  4. Confirm the --print-inference-settings output includes both settings: and sources:
  5. Confirm sensitive values are masked as ***
  6. Run the repo-local tests for the command and bootstrap package

Troubleshooting

ProblemCauseSolution
unknown flag: --print-parsed-fieldsThe command still bypasses cli.BuildCobraCommand(...)Convert it to a Glazed command and build Cobra through cli.BuildCobraCommand(...)
App config parsing fails on non-section keysThe config-file mapper is missing or too naiveDefine an app-specific ConfigFileMapper in AppBootstrapConfig
must be configured when profile-settings.profile is setA profile was selected without any registry sources, and no implicit ${XDG_CONFIG_HOME:-~/.config}/<app>/profiles.yaml fallback was availableConfigure 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 URLThe command built from raw profile data instead of merged settingsCreate engines from resolved.FinalInferenceSettings
--print-inference-settings shows weak provenanceThe command did not resolve settings through the shared bootstrap pathUse ResolveCLIEngineSettings(...) and HandleInferenceDebugOutput(...) from geppetto/pkg/cli/bootstrap
Downstream app loads the wrong config namespaceThe app copied another app’s assumptions instead of defining its own bootstrap configCreate an app-owned AppBootstrapConfig with the right app name, env prefix, and config mapper

See Also