CLI Usage#
This document describes how to invoke scafctl from the command line. The CLI follows a kubectl-style structure where verbs, kinds, and names are explicit and positional.
The general pattern is:
scafctl <verb> <kind> <name[@version(or constraint)]> [flags]<verb>describes what you want to do<kind>identifies the type of object<name>identifies the object@versionis optional and resolved via the catalog ( or constraint)
Implementation Status#
| Command | Status | Notes |
|---|---|---|
run solution | ✅ Implemented | Requires workflow (errors if no workflow defined; use run resolver for resolver-only) |
run resolver | ✅ Implemented | Resolver-only execution for debugging and inspection |
render solution | ✅ Implemented | Includes action-graph and snapshot modes |
get solution/provider/resolver | ✅ Implemented | |
explain solution/provider | ✅ Implemented | |
config * | ✅ Implemented | view, get, set, unset, add-catalog, remove-catalog, use-catalog, init, schema, validate |
snapshot show/diff | ✅ Implemented | |
solution diff | ✅ Implemented | Structural comparison of two solution files |
secrets * | ✅ Implemented | list, get, set, delete, exists, export, import, rotate |
auth * | ✅ Implemented | login, logout, status, token |
resolver graph | ❌ Removed | Use run resolver --graph instead |
build solution | ✅ Implemented | Catalog feature |
catalog list/inspect/delete/prune | ✅ Implemented | Catalog management |
catalog save/load | ✅ Implemented | Offline distribution |
eval cel | ✅ Implemented | Evaluate CEL expressions from CLI |
eval template | ✅ Implemented | Evaluate Go templates from CLI |
eval validate | ✅ Implemented | Validate solution files from CLI |
new solution | ✅ Implemented | Scaffold a new solution from template |
lint rules | ✅ Implemented | List all available lint rules |
lint explain | ✅ Implemented | Explain a specific lint rule |
examples list | ✅ Implemented | List available example configurations |
examples get | ✅ Implemented | Get/download an example file |
push solution/plugin | 📋 Planned | Remote catalog feature |
pull solution/plugin | 📋 Planned | Remote catalog feature |
tag solution/plugin | 📋 Planned | Catalog feature |
--catalog flag | 📋 Planned | Catalog feature |
Version constraints (@^1.2) | 📋 Planned | Requires catalog |
Core Concepts#
Kinds#
solutionproviderresolvercatalog(planned)
Names and Versions#
Names identify an object within a kind.
Versions are optional and may be:
- an exact version (
1.0.0) - a constraint (
^1.2,>=1.0 <2.0) (planned - requires catalog) - omitted (default resolution rules apply)
Shell escaping: Complex version constraints with special characters should be quoted:
scafctl run solution "example@>=1.0 <2.0" # planned
scafctl run solution "example@^1.2" # plannedFile Paths vs. Catalog References#
When a command accepts both a positional name argument and a -f/--file flag, the CLI distinguishes catalog references from local file paths:
| Input | Interpretation | Example |
|---|---|---|
| Bare name | Catalog reference | my-app, deploy |
| Versioned name | Catalog reference | my-app@1.0.0, deploy@^1.2 |
| Registry reference | Catalog reference | ghcr.io/org/sol:v1, localhost:5000/sol |
| URL | Remote reference | https://example.com/sol.yaml |
Starts with / or . | Rejected — use -f | /tmp/sol.yaml, ./sol.yaml |
Ends with .yaml, .yml, .json | Rejected — use -f | solution.yaml |
| Path with separators, non-hostname first segment | Rejected — use -f | configs/solution, relative/path/sol |
| Windows path | Rejected — use -f | C:\dir\sol, dir\sol |
To pass a local file path, always use the -f/--file flag:
# Correct: file via -f flag
scafctl run solution -f ./solution.yaml
# Correct: catalog reference as positional arg
scafctl run solution my-app@1.0.0
# Error: local file path as positional arg
scafctl run solution solution.yaml # rejected with helpful errorThis separation keeps the CLI unambiguous — positional arguments are always catalog/registry lookups while -f is always a file path.
Running a Solution#
Execute a solution’s resolvers and perform its workflow actions. The solution must define a workflow section with actions — if no workflow is defined, the command errors and suggests using scafctl run resolver instead.
scafctl run solution exampleRun a specific version:
scafctl run solution example@1.0.0Run with a version constraint:
scafctl run solution example@^1.2Getting a Solution#
Show metadata of the latest example solution:
scafctl get solution exampleShow metadata of version 1.0.0 of the example solution:
scafctl get solution example@1.0.0Listing Resources#
Following kubectl conventions, use singular or plural forms:
# List all solutions in the catalog
scafctl get solutions
# List all providers
scafctl get providers
# Get a specific solution
scafctl get solution exampleBoth singular and plural forms without a name will list all resources of that kind.#
Rendering a Solution#
Render executes resolvers and renders actions but does not perform side effects.
From Catalog (by name)#
scafctl render solution exampleFrom File#
Use -f or --file to specify a file path:
scafctl render solution -f mysolution.yamlFrom stdin:
cat solution1.yaml | scafctl render solution -f -Note: The -f flag is used consistently across commands (run, render, publish) to indicate a file source rather than a catalog lookup.
Render a specific version:
scafctl render solution example@1.0.0Typical uses:
- dry runs
- snapshot testing
- delegating execution to another system
- reviewing generated artifacts
Passing Resolver Parameters#
Resolver parameters are passed using -r or --resolver.
scafctl run solution example -r env=prodMultiple parameters:
scafctl run solution example \
-r env=prod \
-r region=us-east1Parameters participate in normal resolver resolution via the parameter provider.
Key-Value Format#
Resolver parameters (and other similar flags) use a key=value format where:
- Key: Simple string identifier (no spaces or newlines allowed)
- Value: Supports ALL characters including newlines, special characters, quotes, etc.
Basic Usage#
The flag can be repeated for each key-value pair:
scafctl run solution example \
-r someKey=sk_live_abc123 \
-r config='{"nested": "json"}' \
-r script="line1
line2
line3"CSV Support#
New: You can also pass multiple comma-separated key=value pairs in a single flag:
# Multiple pairs in one flag
scafctl run solution example \
-r "env=prod,region=us-east1,region=us-west1"To include commas in values, use quotes:
# Quoted values preserve commas as literal characters
scafctl run solution example \
-r "msg=\"Hello, world\"" \
-r "data='item1,item2,item3'"Escaped quotes are supported within quoted values:
scafctl run solution example \
-r "json=\"{\\\"key\\\":\\\"value\\\"}\""Multiple Values for Same Key#
Values for the same key are automatically combined, whether using CSV or repeated flags:
# Using repeated flags
scafctl run solution example \
-r region=us-east1 \
-r region=us-west1 \
-r region=eu-west1
# Using CSV in one flag
scafctl run solution example \
-r "region=us-east1,region=us-west1,region=eu-west1"
# Combining both approaches
scafctl run solution example \
-r "region=us-east1,region=us-west1" \
-r region=eu-west1
# All three produce: region = [us-east1, us-west1, eu-west1]Usage Patterns#
Separate flags (traditional):
scafctl run solution example \
-r key1=value1 \
-r key2=value2CSV in single flag (convenient for multiple pairs):
scafctl run solution example \
-r "key1=value1,key2=value2"Mixed approach:
scafctl run solution example \
-r "env=prod,region=us-east1" \
-r region=us-west1 \
-r apiKey=secretTechnical Note: The CLI uses StringArrayVarP with custom CSV parsing (via pkg/flags.ParseKeyValueCSV) to avoid Cobra’s built-in CSV issues while still supporting comma-separated values with proper quote handling
URI Scheme Support#
To simplify passing complex data like JSON or YAML without escaping, use URI scheme prefixes:
Supported schemes: json://, yaml://, base64://, http://, https://, file://
# JSON without quote escaping
scafctl run solution example \
-r "config=json://{\"key\":\"value\",\"count\":42}"
# JSON with commas in CSV context
scafctl run solution example \
-r "env=prod,data=json://[1,2,3],region=us-east1"
# YAML configuration
scafctl run solution example \
-r "config=yaml://items: [a, b, c]"
# Base64 encoded data
scafctl run solution example \
-r "token=base64://SGVsbG8sIFdvcmxkIQ=="Important: The scheme prefix is preserved and should be processed by your solution logic.
Validation: Values with URI schemes are automatically validated:
json://- Validated as well-formed JSONyaml://- Validated as well-formed YAMLbase64://- Validated as proper base64 encodingfile://- Verified that file exists and is not a directoryhttp://,https://- Validated as properly formatted URLs
Validation errors are reported immediately with helpful messages.
Parameter File References (@file)#
Load all parameters from a YAML or JSON file using the @ prefix:
# Load from a YAML file
scafctl run resolver -f solution.yaml -r @params.yaml
# Load from a JSON file
scafctl run resolver -f solution.yaml -r @params.json
# Mix file and inline parameters
scafctl run resolver -f solution.yaml -r @defaults.yaml -r env=prodStdin Parameters (@-)#
Read all parameters from stdin as YAML or JSON using @-, following the same convention as curl:
# Pipe JSON from echo
echo '{"env": "prod", "region": "us-east1"}' | scafctl run resolver -f solution.yaml -r @-
# Pipe from a file via cat
cat params.yaml | scafctl run solution example -r @-
# Use as positional argument
echo '{"env": "prod"}' | scafctl run resolver -f solution.yaml @-Per-Key Stdin and File References (key=@- / key=@file)#
Assign raw content from stdin or a file directly to a single key. Unlike standalone @- (which parses YAML/JSON into multiple keys), key=@- reads stdin as a raw string value:
# Pipe raw text into a single parameter
echo hello | scafctl run provider message message=@-
echo hello | scafctl run resolver -f solution.yaml -r message=@-
# Read a file's raw content into a parameter
scafctl run provider http url=https://example.com body=@request.json
scafctl run resolver -f solution.yaml -r config=@defaults.txt
# Mix with other parameter forms
scafctl run resolver -f solution.yaml -r name=Alice body=@template.txtA single trailing newline is trimmed automatically (matching shell
echobehavior).
Restrictions:
@-can only appear once (stdin is consumed on first read) — this applies across both standalone@-andkey=@-@-cannot be combined with-f -(both read from stdin)
Rendering With Parameters#
scafctl render solution example \
-r env=staging \
-r dryRun=trueRender Options#
The render command supports additional modes for debugging and testing:
Execution Snapshots#
Capture resolver execution state for testing and comparison:
# Save snapshot after rendering
scafctl render solution -f solution.yaml --snapshot output.json
# Redact sensitive values
scafctl render solution -f solution.yaml --snapshot output.json --redactSnapshots can be analyzed with dedicated commands:
# Display a saved snapshot
scafctl snapshot show output.json
# Compare two snapshots
scafctl snapshot diff before.json after.jsonWorking With the Catalog#
Status: ✅ Implemented - Local catalog with build, list, inspect, delete, prune, save, and load. Remote push/pull planned for Phase 2.
Run a solution directly from the catalog:
scafctl run solution example@1.7.0Building Artifacts#
Status: ✅ Implemented
Build a solution for the local catalog (analogous to docker build):
# Build a solution from file
scafctl build solution -f ./solution.yaml --version 1.0.0
# Build using version from metadata
scafctl build solution -f ./solution.yaml
# Overwrite existing version
scafctl build solution -f ./solution.yaml --version 1.0.0 --forceThe build process validates, resolves dependencies, bundles local files, vendors catalog dependencies, and packages artifacts into the local catalog. See catalog-build-bundling.md for the full bundling design.
Additional build flags:
# Dry-run: show what would be bundled without building
scafctl build solution -f ./solution.yaml --dry-run
# Skip file bundling (legacy single-layer artifact)
scafctl build solution -f ./solution.yaml --no-bundle
# Skip vendoring catalog dependencies
scafctl build solution -f ./solution.yaml --no-vendor
# Set max bundle size
scafctl build solution -f ./solution.yaml --bundle-max-size 100MB
# Re-resolve and update the lock file
scafctl build solution -f ./solution.yaml --update-lockPublishing Artifacts#
Status: 📋 Planned
Push artifacts to a remote catalog (analogous to docker push):
# Push a solution
scafctl push solution my-solution@1.7.0
# Push a plugin
scafctl push plugin aws-provider@1.5.0
# Push to a specific catalog
scafctl push solution my-solution@1.7.0 --catalog=productionPulling Artifacts#
Status: 📋 Planned
Pull artifacts from a remote catalog to local (analogous to docker pull):
# Pull a solution
scafctl pull solution example@1.7.0
# Pull a plugin
scafctl pull plugin aws-provider@1.5.0Inspecting Artifacts#
Status: ✅ Implemented
View artifact metadata, dependencies, and structure:
# Inspect a solution (latest version)
scafctl catalog inspect example
# Inspect specific version
scafctl catalog inspect example@1.7.0
# JSON output
scafctl catalog inspect example -o jsonTagging Artifacts#
Status: 📋 Planned
Create version aliases:
# Tag a solution
scafctl tag solution my-solution@1.2.3 my-solution:latest
# Tag a plugin
scafctl tag plugin aws-provider@1.5.0 aws-provider:stableOffline Distribution#
Status: ✅ Implemented
Export and import artifacts for air-gapped environments (analogous to docker save/load):
# Save a solution (exports latest version by default)
scafctl catalog save my-solution -o solution.tar
# Save specific version
scafctl catalog save my-solution@1.2.3 -o solution.tar
# Load from archive
scafctl catalog load --input solution.tar
# Force overwrite if artifact already exists
scafctl catalog load --input solution.tar --forceThe archive uses OCI Image Layout format, making it compatible with OCI registry tools.
Deleting Artifacts#
Status: ✅ Implemented
Remove an artifact from the local catalog:
# Delete specific version (version required)
scafctl catalog delete example@1.7.0
# Prune orphaned blobs after deletion
scafctl catalog pruneCatalog Resolution#
Status: 📋 Planned
By default, scafctl uses the local filesystem as the default catalog. Use --catalog to target a specific configured catalog:
scafctl run solution example --catalog=internal
scafctl get solutions --catalog=productionExplaining Resources#
Get detailed metadata and documentation for solutions and providers:
Explain Solution#
# From file
scafctl explain solution -f solution.yaml
# From catalog
scafctl explain solution example
scafctl explain solution example@1.0.0Outputs:
- Name, version, description
- List of resolvers with their providers
- List of actions with types
- Required parameters
- Dependency summary
Explain Provider#
scafctl explain provider github
scafctl explain provider staticOutputs:
- Provider description
- Configuration schema with types and validation
- Example configurations
- Supported features
Global Flags#
These flags are available on all commands. Run scafctl options to see them:
| Flag | Short | Description | Status |
|---|---|---|---|
--cwd | -C | Change the working directory before executing the command (similar to git -C) | ✅ Implemented |
--quiet | -q | Suppress non-essential output | ✅ Implemented |
--no-color | Disable colored output | ✅ Implemented | |
--config | Path to config file (default: ~/.scafctl/config.yaml) | ✅ Implemented | |
--log-level | Set log level (none, error, warn, info, debug, trace, or numeric V-level) | ✅ Implemented | |
--debug | -d | Enable debug logging (shorthand for –log-level debug) | ✅ Implemented |
--log-format | Log format: console (default) or json | ✅ Implemented | |
--log-file | Write logs to a file path | ✅ Implemented | |
--catalog | Target a specific configured catalog | 📋 Planned |
Note: The -o/--output flag is available per-command (not global) on commands that support structured output.
Output format support:
get,render,explain,config view: Full support for-oflagrun: Supports-oflag for result outputauth status,secrets list: Support-oflag
Configuration#
scafctl uses a configuration file at ~/.scafctl/config.yaml managed via Viper
. Configuration can also be set via environment variables with the SCAFCTL_ prefix.
Config File Structure#
catalogs:
- name: default
type: filesystem
path: ./
- name: internal
type: oci
url: oci://registry.example.com/scafctl
settings:
defaultCatalog: default
action:
# Default output directory for action file operations
# CLI --output-dir flag overrides this setting
outputDir: "/path/to/output"Config Commands#
View the current configuration:
scafctl config viewGet a specific setting:
scafctl config get settings.defaultCatalogSet a configuration value:
scafctl config set settings.defaultCatalog=internalUnset a configuration value:
scafctl config unset settings.defaultCatalogCatalog Management#
Convenience commands for catalog configuration:
# Add a catalog
scafctl config add-catalog internal --type=oci --url=oci://registry.example.com/scafctl
# Remove a catalog
scafctl config remove-catalog internal
# Set the default catalog
scafctl config use-catalog internalEnvironment Variables#
All configuration can be overridden via environment variables:
export SCAFCTL_SETTINGS_DEFAULTCATALOG=internal
export SCAFCTL_CONFIG=/path/to/custom/config.yamlManaging Secrets#
Securely manage encrypted secrets for authentication and configuration:
# List all secrets
scafctl secrets list
# List all secrets including internal (auth tokens, etc.)
scafctl secrets list --all
# Get a secret value
scafctl secrets get my-api-key
# Get an internal secret (e.g. auth token metadata)
scafctl secrets get scafctl.auth.entra.metadata --all
# Set a secret (prompts for value)
scafctl secrets set my-api-key
# Set with value directly
scafctl secrets set my-api-key --value="secret-value"
# Delete a secret
scafctl secrets delete my-api-key
# Check if secret exists
scafctl secrets exists my-api-key
# Export secrets (encrypted)
scafctl secrets export -o secrets.enc
# Import secrets
scafctl secrets import -i secrets.enc
# Rotate encryption key
scafctl secrets rotateSecrets are encrypted with AES-256-GCM and stored in platform-specific locations:
- macOS:
~/.local/share/scafctl/secrets/ - Linux:
~/.local/share/scafctl/secrets/ - Windows:
%APPDATA%\scafctl\secrets\
Authentication#
Manage authentication for accessing protected resources:
# Login with an auth handler
scafctl auth login entra
# Check authentication status
scafctl auth status
scafctl auth status entra
# Get a token (for debugging)
scafctl auth token entra --scope "https://graph.microsoft.com/.default"
# Logout
scafctl auth logout entraSupported auth handlers:
entra- Microsoft Entra ID (formerly Azure AD)
Resolver Commands#
Note: The standalone
scafctl resolver graphcommand has been removed. Usescafctl run resolver --graphinstead.
Running Resolvers#
The run resolver command executes resolvers from a solution without running actions. This is designed for debugging and inspecting resolver execution.
# Run all resolvers
scafctl run resolver -f solution.yaml
# Run specific resolvers (with their transitive dependencies)
scafctl run resolver db config -f solution.yaml
# JSON output (includes __execution metadata by default)
scafctl run resolver -f solution.yaml -o json
# JSON output without __execution metadata
scafctl run resolver -f solution.yaml -o json --hide-execution
# Skip transform and validation phases
scafctl run resolver --skip-transform -f solution.yaml
# Dependency graph (ASCII, DOT, Mermaid, or JSON)
scafctl run resolver --graph -f solution.yaml
scafctl run resolver --graph --graph-format=dot -f solution.yaml
# Snapshot execution state
scafctl run resolver --snapshot --snapshot-file=out.json -f solution.yaml
scafctl run resolver --snapshot --snapshot-file=out.json --redact -f solution.yaml
# Interactive TUI for exploring results
scafctl run resolver -f solution.yaml -iAliases: res, resolvers
Help and Discovery#
List available verbs:
scafctl helpList supported kinds for a verb:
scafctl run --helpGet help for a specific kind:
scafctl run solution --helpBecause kinds are registered dynamically, help output always reflects what is available at runtime.
Summary#
The scafctl CLI follows a structured, extensible pattern:
- Verbs describe intent
- Kinds identify object types
- Names and versions identify concrete artifacts
This design enables dynamic extension, clear UX, and long-term scalability without breaking existing commands.
Evaluating Expressions#
The eval command group lets you test CEL expressions, Go templates, and validate solution files without running a full solution.
Evaluate CEL#
# Simple expression
scafctl eval cel "1 + 2"
# With JSON data
scafctl eval cel '_.name == "test"' --data '{"name": "test"}'
# From a data file
scafctl eval cel '_.items.size() > 0' --data-file data.json
# Output as JSON
scafctl eval cel '_.items.filter(i, i.active)' --data-file data.json -o jsonEvaluate Go Template#
# Simple template
scafctl eval template '{{.name}}' --data '{"name": "hello"}'
# Template from file
scafctl eval template --template-file template.txt --data-file data.json
# With output file
scafctl eval template --template-file template.txt --data-file data.json --output result.txtValidate Solution#
# Validate a solution YAML file
scafctl eval validate -f solution.yaml
# Output as JSON
scafctl eval validate -f solution.yaml -o jsonCreating New Solutions#
Scaffold a new solution from a built-in template:
# Interactive — prompts for name, description, providers
scafctl new solution
# With flags
scafctl new solution --name my-solution --description "My new solution" --output my-solution.yaml
# With specific providers
scafctl new solution --name my-solution --providers static,exec,celExploring Lint Rules#
List Rules#
List all available lint rules with severity, category, and descriptions:
# List all rules
scafctl lint rules
# Output as JSON
scafctl lint rules -o jsonExplain a Rule#
Get a detailed explanation of a specific lint rule:
# Show rule details, examples, and fix guidance
scafctl lint explain <rule-id>
# Output as JSON
scafctl lint explain <rule-id> -o jsonBrowsing Examples#
Discover and download built-in example configurations:
List Examples#
# List all examples
scafctl examples list
# Filter by category
scafctl examples list --category solutions
scafctl examples list --category resolvers
scafctl examples list --category actions
# Output as JSON
scafctl examples list -o json
# Output as YAML
scafctl examples list -o yamlGet an Example#
# Print example to stdout
scafctl examples get resolvers/hello-world.yaml
# Save to file
scafctl examples get resolvers/hello-world.yaml -o output.yamlAliases: ls for list
Command Grammar: Verb-Noun vs Noun-Verb#
Current State of scafctl#
scafctl uses two distinct command grammar patterns:
Verb-Noun (kubectl-style) — the verb is the top-level command, the noun follows:
| Command | Verb | Noun |
|---|---|---|
scafctl run solution | run | solution |
scafctl run resolver | run | resolver |
scafctl get solution | get | solution |
scafctl render solution | render | solution |
scafctl explain solution | explain | solution |
scafctl build solution | build | solution |
scafctl new solution | new | solution |
scafctl push solution | push | solution |
scafctl pull solution | pull | solution |
scafctl tag solution | tag | solution |
Noun-Verb — the noun is the top-level command, the verb follows:
| Command | Noun | Verb |
|---|---|---|
scafctl secrets get | secrets | get |
scafctl secrets set | secrets | set |
scafctl secrets list | secrets | list |
scafctl secrets delete | secrets | delete |
scafctl auth login | auth | login |
scafctl auth logout | auth | logout |
scafctl auth status | auth | status |
scafctl config view | config | view |
scafctl config set | config | set |
scafctl config get | config | get |
scafctl catalog list | catalog | list |
scafctl catalog inspect | catalog | inspect |
scafctl snapshot show | snapshot | show |
scafctl snapshot diff | snapshot | diff |
scafctl solution diff | solution | diff |
scafctl lint rules | lint | rules |
scafctl lint explain | lint | explain |
scafctl eval cel | eval | cel |
scafctl eval template | eval | template |
scafctl examples list | examples | list |
scafctl examples get | examples | get |
scafctl cache clean | cache | clean |
scafctl plugins list | plugins | list |
scafctl bundle create | bundle | create |
scafctl vendor sync | vendor | sync |
Standalone (no sub-noun or sub-verb):
| Command | Notes |
|---|---|
scafctl version | informational |
scafctl mcp | launches MCP server |
scafctl test | runs solution tests |
What Major CLIs Do#
Most successful CLIs converge on the same hybrid pattern scafctl already uses:
| CLI | Core Domain Objects | Service/Infrastructure | Example |
|---|---|---|---|
| kubectl | verb-noun: get pods, delete svc, apply -f | noun-verb: config use-context, auth can-i | Domain is verb-noun; plumbing is noun-verb |
| docker | verb-noun: run, build, pull, push | noun-verb: network create, volume ls, system prune | Top-level verbs act on images/containers; subsystems are noun-verb |
| git | verb-first: clone, commit, push, pull | noun-verb: remote add, branch delete, stash pop | Core workflow is verbs; ancillary resource management is noun-verb |
| gh (GitHub CLI) | verb-noun: pr create, issue list | noun-verb: auth login, config set, secret set | Domain objects verb-noun; infrastructure noun-verb |
| az (Azure CLI) | noun-verb: az vm create, az storage blob upload | noun-verb throughout | Purely noun-verb (resource-group style) |
| gcloud | noun-verb: gcloud compute instances create | noun-verb throughout | Purely noun-verb (resource hierarchy) |
| terraform | verb-first: plan, apply, destroy | — | No sub-resources, single verb layer |
| helm | verb-noun: install, upgrade, rollback | noun-verb: repo add, plugin install | Charts are verb-noun; supporting systems noun-verb |
Observation: The most widely-used developer CLIs (kubectl, docker, git, gh, helm) all use a hybrid model. Only cloud-provider CLIs (az, gcloud) that model deep resource hierarchies go fully noun-verb. Purely verb-noun CLIs (terraform) tend to have a flat, single-resource domain.
Best Practice: The Delineation Rule#
The hybrid pattern is not arbitrary — it follows a clear principle:
Use verb-noun for domain operations on core business objects. Use noun-verb for infrastructure, plumbing, and service management.
The deciding question: “Is this a core workflow action the user came here to do, or is it managing supporting infrastructure?”
| Category | Pattern | Rationale | scafctl examples |
|---|---|---|---|
| Core domain operations | <verb> <kind> | The user thinks in terms of what they want to do: run, get, render, build. The kind is just a target. | run solution, get provider, render solution, build solution, new solution |
| Infrastructure / services | <noun> <action> | The user thinks in terms of which subsystem they need to manage. The subsystem is the anchor; actions within it are secondary. | config set, secrets get, auth login, catalog list, cache clean |
| Standalone utilities | <verb> or <noun> | Single-purpose commands that don’t need a sub-resource. | version, mcp, test |
Why this works:
- Discoverability — Users can type
scafctland immediately see the core verbs (run,get,render) alongside the subsystems (config,secrets,auth). The top-level command list reads like a table-of-contents. - Composability — Verb-noun allows the same verb to apply to multiple kinds (
run solution,run resolver). Noun-verb allows subsystems to have independent, self-documenting action sets. - Scalability — New kinds slot into existing verbs. New subsystem features slot into their noun group. Neither pollutes the other.
- Precedent — kubectl, docker, git, gh, and helm all draw the same line, so users already have muscle memory for the split.
Verdict on scafctl#
The current hybrid structure is correct. No changes needed.
scafctl already follows the established delineation:
- Verb-noun for core domain operations —
run,get,render,explain,build,new,push,pull,tagall act on domain kinds (solution, provider, resolver). - Noun-verb for infrastructure/services —
config,secrets,auth,catalog,snapshot,lint,eval,examples,cache,plugins,bundle,vendorare all subsystem groups with their own actions. - Standalone for single-purpose utilities —
version,mcp,test.
This matches how kubectl, docker, git, gh, and helm structure their CLIs. Attempting to “unify” to pure verb-noun or pure noun-verb would:
- Break user expectations from other tools.
- Create awkward commands —
scafctl manage secrets get(verb-noun forced) orscafctl solution run(noun-verb forced for domain ops) reads worse in both cases. - Lose discoverability — a flat verb-only top level hides subsystem structure; a flat noun-only top level hides the core workflow.
The rule of thumb going forward: if adding a new top-level command, ask “Is the user performing a core domain action on a kind, or managing a subsystem?” — verb-noun for the former, noun-verb for the latter.