Documentation
¶
Overview ¶
Package cmdutil holds shared helpers for the commands/* CLI tree.
canonfile.go specifically powers the settings/mcp/rules subcommand families. The three families differ only in noun, dir segment, and a small number of message strings; the list/show/remove flows underneath are identical. Per .agents/workflow/specs/production-code-helper-extraction/design.md, extracting these into RunCanonical{List,Show,Remove} drains the three-way duplication SonarCloud flags at settings.go:106 ↔ mcp.go:106 ↔ rules.go:122 (and the parallel show/remove blocks).
Index ¶
- Constants
- Variables
- func BindScopeSourceFlags(cmd *cobra.Command, f *ScopeSourceFlags)
- func CanonicalCmdExampleBlock(lines ...string) string
- func NewCanonicalListCmd(spec CanonicalFileSpec) *cobra.Command
- func NewCanonicalRemoveCmd(spec CanonicalFileSpec) *cobra.Command
- func NewCanonicalResourceCmd(spec CanonicalFileSpec) *cobra.Command
- func NewCanonicalShowCmd(spec CanonicalFileSpec) *cobra.Command
- func RunCanonicalList(scope string, spec CanonicalFileSpec) error
- func RunCanonicalRemove(deps RemoveDeps, scope, name string, spec CanonicalFileSpec) error
- func RunCanonicalShow(scope, name string, spec CanonicalFileSpec, extras ...func(srcPath string)) error
- type CanonicalCmdFlags
- type CanonicalFileEntry
- type CanonicalFileSpec
- type CanonicalResourceDef
- type RemoveDeps
- type ResourceRunners
- type RoutedTarget
- type Router
- type ScopeSourceFlags
- type SubCmdStrings
Constants ¶
const ( // FlagScope is the long name of the ownership-scope flag. FlagScope = "scope" // FlagSource is the long name of the source-id flag. FlagSource = "source" )
Variables ¶
var MCPResource = CanonicalResourceDef{ Kind: "MCP", DirSegment: "mcp", SingularRem: "MCP file", EnsureScope: platform.EnsureUnderMCPScopeTree, EmptyHint: func(scope string) string { return "No MCP config files (.json/.yaml/.yml/.toml) under ~/.agents/mcp/" + scope + "/" }, MissingDirHint: func(scope string) string { return "No ~/.agents/mcp/" + scope + "/ directory yet (no canonical MCP files for this scope)." }, Use: "mcp", Short: "Inspect and manage canonical ~/.agents/mcp config files", Long: `Commands for MCP server configs stored under ~/.agents/mcp/<scope>/. Scopes are either global (~/.agents/mcp/global/) or a managed project name (~/.agents/mcp/<project>/), matching da status. These files are what add, import, refresh, install, and remove wire into Cursor, Claude Code, Copilot, and related projections. Prefer editing canonical paths here, then run refresh or install for the project.`, Examples: []string{ " da mcp list", " da mcp list my-app", " da mcp show global mcp.json", " da mcp remove global stale.json", }, ListShort: "List canonical MCP config files for a scope", ListExamples: []string{ " da mcp list", " da mcp list billing-api", }, ListArgsHint: "Optionally pass a project scope (or `global`) to inspect that MCP tree.", ShowShort: "Show metadata for one MCP file under ~/.agents/mcp/", ShowArgsHint: "`scope` is `global` or a managed project name; `name` is the file (e.g. mcp.json) or stem (mcp).", RemoveShort: "Remove an MCP file from ~/.agents/mcp/ (canonical storage only)", RemoveLong: `Deletes the file from managed MCP storage only (not repo links). After removal, run da refresh or install for the relevant project so platform MCP links stay consistent.`, RemoveArgsHint: canonicalRemoveArgsHint, }
MCPResource owns the static `da mcp` resource definition. The matching runner closures (List/Resolve) live in commands/internal/mcp/seams.go alongside findMCPSpec, which wraps platform.ResolveCanonicalMCPFile errors via deps.ErrorWithHints / deps.UsageError.
var RulesResource = CanonicalResourceDef{ Kind: "Rule", DirSegment: "rules", SingularRem: "rule file", EnsureScope: platform.EnsureUnderRulesScopeTree, EmptyHint: func(scope string) string { return "No rule files (.mdc/.md/.txt) under ~/.agents/rules/" + scope + "/" }, MissingDirHint: func(scope string) string { return "No ~/.agents/rules/" + scope + "/ directory yet (no canonical rule files for this scope)." }, Use: "rules", Short: "Inspect and manage canonical ~/.agents/rules files", Long: `Commands for rule files stored under ~/.agents/rules/<scope>/. Scopes are either global (~/.agents/rules/global/) or a managed project name (~/.agents/rules/<project>/), matching da status. These files are what add, import, refresh, install, and remove wire into Cursor, Claude Code, Codex, and Copilot projections. Prefer editing canonical paths here, then run refresh or install for the project — do not hand-edit platform copies unless you know they are unmanaged.`, Examples: []string{ " da rules list", " da rules list my-app", " da rules show global rules.mdc", " da rules remove global old-rule.mdc", }, ListShort: "List canonical rule files for a scope", ListExamples: []string{ " da rules list", " da rules list billing-api", }, ListArgsHint: "Optionally pass a project scope (or `global`) to inspect that rules tree.", ShowShort: "Show metadata for one rule file under ~/.agents/rules/", ShowArgsHint: "`scope` is `global` or a managed project name; `name` is the file (e.g. rules.mdc) or stem (rules).", RemoveShort: "Remove a rule file from ~/.agents/rules/ (canonical storage only)", RemoveLong: `Deletes the file from managed rule storage only (not repo links). After removal, run da refresh or install for the relevant project so platform rule links stay consistent.`, RemoveArgsHint: canonicalRemoveArgsHint, }
RulesResource owns the static `da rules` resource definition. Note the Kind is "Rule" (singular) because that is what the ui.Header prints — matching the pre-refactor rules/list.go literal verbatim.
var SettingsResource = CanonicalResourceDef{ Kind: "Settings", DirSegment: "settings", SingularRem: "settings file", EnsureScope: platform.EnsureUnderSettingsScopeTree, EmptyHint: func(scope string) string { return "No settings files under ~/.agents/settings/" + scope + "/" }, Use: "settings", Short: "Inspect and manage canonical ~/.agents/settings files", Long: `Commands for platform settings files stored under ~/.agents/settings/<scope>/. Scopes are either global (~/.agents/settings/global/) or a managed project name (~/.agents/settings/<project>/), matching da status. Files include JSON/TOML/YAML configs (e.g. cursor.json, claude-code.json) and cursorignore. These are wired by add, import, refresh, install, and remove. Prefer editing canonical paths here, then run refresh or install.`, Examples: []string{ " da settings list", " da settings list my-app", " da settings show global cursor.json", " da settings remove proj cursorignore", }, ListShort: "List canonical settings files for a scope", ListExamples: []string{ " da settings list", " da settings list billing-api", }, ListArgsHint: "Optionally pass a project scope (or `global`) to inspect that settings tree.", ShowShort: "Show metadata for one settings file under ~/.agents/settings/", ShowArgsHint: "`scope` is `global` or a managed project name; `name` is the file (e.g. cursor.json) or stem.", RemoveShort: "Remove a settings file from ~/.agents/settings/ (canonical storage only)", RemoveLong: `Deletes the file from managed settings storage only (not repo links). After removal, run da refresh or install for the relevant project so platform settings links stay consistent.`, RemoveArgsHint: canonicalRemoveArgsHint, }
SettingsResource owns the static `da settings` resource definition. MissingDirHint is intentionally nil so RunCanonicalList emits the generic fallback message ("No ~/.agents/settings/<scope>/ directory yet ..."), preserving the pre-refactor settings/list.go behavior verbatim.
Functions ¶
func BindScopeSourceFlags ¶
func BindScopeSourceFlags(cmd *cobra.Command, f *ScopeSourceFlags)
BindScopeSourceFlags registers --scope and --source on cmd, writing parsed values back into f. Every mutating canonical command calls this so the flag surface stays identical across the family.
func CanonicalCmdExampleBlock ¶
CanonicalCmdExampleBlock joins example lines for canonical subcommand `Example:` fields. Shared across rules/mcp/settings command trees.
func NewCanonicalListCmd ¶
func NewCanonicalListCmd(spec CanonicalFileSpec) *cobra.Command
NewCanonicalListCmd builds the list leaf from spec. Exported so leaf packages can keep a one-liner standalone constructor (mcp.NewListCmd etc.) for cross-cutting coverage tests in the parent shim.
Scope defaulting: when args is empty the runner receives "global", matching the behavior the three leaves previously open-coded.
func NewCanonicalRemoveCmd ¶
func NewCanonicalRemoveCmd(spec CanonicalFileSpec) *cobra.Command
NewCanonicalRemoveCmd builds the remove leaf from spec. Args validator must enforce exactly two positional arguments before this RunE fires.
func NewCanonicalResourceCmd ¶
func NewCanonicalResourceCmd(spec CanonicalFileSpec) *cobra.Command
NewCanonicalResourceCmd assembles the parent `da <kind>` cobra tree from a CanonicalFileSpec — parent command plus its list/show/remove children — using the spec's CLI-surface fields (Use/Short/Long/ Example and the per-verb SubCmdStrings + Args + Run). The returned command is ready to attach to the root command.
This used to live in commands/internal/canonical; folding it back into cmdutil collapses the two-package split that had every resource family carrying a parallel ResourceCmdSpec literal alongside its existing CanonicalFileSpec. Now there is exactly one struct literal per resource (mcp/settings/rules).
func NewCanonicalShowCmd ¶
func NewCanonicalShowCmd(spec CanonicalFileSpec) *cobra.Command
NewCanonicalShowCmd builds the show leaf from spec. Args validator must enforce exactly two positional arguments before this RunE fires.
func RunCanonicalList ¶
func RunCanonicalList(scope string, spec CanonicalFileSpec) error
RunCanonicalList prints the canonical entries for a scope. Emits an info message for missing scope directories (via spec.MissingDirHint) and empty scope directories (via spec.EmptyHint).
func RunCanonicalRemove ¶
func RunCanonicalRemove(deps RemoveDeps, scope, name string, spec CanonicalFileSpec) error
RunCanonicalRemove removes one entry, with dry-run + confirm gates matching the original per-subcommand handlers.
func RunCanonicalShow ¶
func RunCanonicalShow(scope, name string, spec CanonicalFileSpec, extras ...func(srcPath string)) error
RunCanonicalShow prints metadata for one entry. Each `extra` callback receives the resolved source path and may print additional lines (e.g. rules append a frontmatter description).
Types ¶
type CanonicalCmdFlags ¶
CanonicalCmdFlags captures the global flags relevant to canonical `da <kind>` subcommands (rules, mcp, settings, …). Lifted from commands/rules.go in plan root-command-decomposition t10pre so the three resource subpackages (rules, mcp, settings) can share a single definition once they split out of package commands.
type CanonicalFileEntry ¶
CanonicalFileEntry is the projection of platform.{Settings,MCP,Rule}FileSpec that the List/Show/Remove helpers operate on. Per-subcommand specs convert their typed platform.* slices into []CanonicalFileEntry.
func EntriesFromSpecs ¶
func EntriesFromSpecs[T any](specs []T, project func(T) CanonicalFileEntry) []CanonicalFileEntry
EntriesFromSpecs converts a slice of typed platform.{MCP,Settings,Rule}FileSpec values into the CanonicalFileEntry projection the data-layer helpers operate on. The three leaves used to inline this 5-line loop inside their List closures; lifting it here removes the last bit of structural duplication. Callers pass a per-spec projector so this stays generic across the three distinct FileSpec types.
type CanonicalFileSpec ¶
type CanonicalFileSpec struct {
// Kind is the capitalized header label ("Settings" | "MCP" | "Rules").
Kind string
// DirSegment is the directory under ~/.agents/<DirSegment>/.
// Used in user-facing path text, the remove header, and the
// confirmation prompt.
DirSegment string
// SingularRem is the singular noun used in remove output strings
// ("settings file" | "MCP file" | "rule file").
SingularRem string
// EmptyHint produces the message shown when no files exist for
// the scope. Per-subcommand because the message lists the kind's
// own valid file extensions or noun.
EmptyHint func(scope string) string
// MissingDirHint produces the message shown when the
// agentsHome/<DirSegment>/<scope>/ directory itself doesn't exist.
// Defaults if nil to "No ~/.agents/<DirSegment>/<scope>/ directory yet ...".
MissingDirHint func(scope string) string
// List enumerates entries for a scope. Returning an os.IsNotExist
// error indicates the scope directory is missing — the helper
// turns that into the MissingDirHint informational message.
List func(agentsHome, scope string) ([]CanonicalFileEntry, error)
// Resolve finds one entry by basename or stem; the returned error
// is propagated as-is, so callers can wrap with errorWithHints.
Resolve func(agentsHome, scope, name string) (CanonicalFileEntry, error)
// EnsureScope verifies target is under <agentsHome>/<DirSegment>/<scope>/.
EnsureScope func(agentsHome, scope, target string) error
// Use / Short / Long / Example populate the parent cobra.Command
// (the `da <kind>` node). Example is a pre-joined string — leaves
// build multi-line examples with CanonicalCmdExampleBlock(...) or
// a plain string literal.
Use string
Short string
Long string
Example string
// List / Show / Remove subcommand strings + Args validators.
// Args is the pre-bound cobra.PositionalArgs validator from the
// leaf's Deps (mcp uses MaxArgsWithHints, settings/rules use
// MaximumNArgsWithHints, so binding stays at the leaf). Run is the
// leaf-specific runner that receives unpacked scope/name args from
// the canonical RunE wrapper.
ListSub SubCmdStrings
ListArgs cobra.PositionalArgs
ListRun func(scope string) error
ShowSub SubCmdStrings
ShowArgs cobra.PositionalArgs
ShowRun func(scope, name string) error
RemoveSub SubCmdStrings
RemoveArgs cobra.PositionalArgs
RemoveRun func(scope, name string) error
}
CanonicalFileSpec parameterizes both the data-layer RunCanonicalList/Show/Remove helpers and the cobra-tree assembly in NewCanonicalResourceCmd. Settings/MCP/Rules each populate one of these — there is exactly one struct literal per resource family.
The fields split into three groups:
- Resource identity (Kind, DirSegment, SingularRem)
- User-facing message hooks (EmptyHint, MissingDirHint)
- Data-layer callbacks (List, Resolve, EnsureScope)
- CLI-surface strings (Use, Short, Long, Example and the per-verb SubCmdStrings + cobra.PositionalArgs)
Leaves wire their per-verb runners through the package-level RunList/RunShow/RunRemove functions so this spec stays a pure data description with no behavior.
func SpecForResource ¶
func SpecForResource(def CanonicalResourceDef, runners ResourceRunners, listArgs, showArgs, removeArgs cobra.PositionalArgs) CanonicalFileSpec
SpecForResource assembles a CanonicalFileSpec from the static def + the leaf's runner closures + pre-built positional args validators. This is the SINGLE place the field-by-field assembly happens; each leaf's canonicalSpec(deps) becomes a one-liner forwarding into here.
The args validators come pre-bound from the leaf because mcp uses Deps.MaxArgsWithHints while settings/rules use Deps.MaximumNArgsWithHints — both produce cobra.PositionalArgs, but the bindings live on different Deps fields so they cannot be moved into the def or this factory.
type CanonicalResourceDef ¶
type CanonicalResourceDef struct {
// Identity — used by the data-layer RunCanonical* helpers and the
// parent commands.* user-facing strings.
Kind string // "MCP" | "Settings" | "Rule"
DirSegment string // "mcp" | "settings" | "rules"
SingularRem string // "MCP file" | "settings file" | "rule file"
// EnsureScope verifies the resolved target path is under
// <agentsHome>/<DirSegment>/<scope>/. The three platform.EnsureUnder*
// helpers have the same signature, so we can bind them directly.
EnsureScope func(agentsHome, scope, target string) error
// EmptyHint / MissingDirHint produce the informational messages the
// list path prints when the scope has no files / no directory.
// MissingDirHint is optional — RunCanonicalList falls back to a
// generic message when nil (settings uses the fallback).
EmptyHint func(scope string) string
MissingDirHint func(scope string) string
// CLI surface — parent `da <kind>` cobra.Command.
Use string // "mcp" | "settings" | "rules"
Short string // one-line summary
Long string // multi-line description for `--help`
Examples []string // top-level Example block lines (joined with "\n")
// List subcommand strings.
ListShort string
ListExamples []string
ListArgsHint string // passed to MaxArgsWithHints / MaximumNArgsWithHints
// Show subcommand strings.
ShowShort string
ShowArgsHint string
// Remove subcommand strings.
RemoveShort string
RemoveLong string
RemoveArgsHint string
}
CanonicalResourceDef carries the STATIC per-resource configuration that the mcp/settings/rules subpackages used to inline as 90-line struct literals inside their respective canonicalSpec(deps) builders. Pulling the strings, dir segment, noun forms, and EnsureScope target into a single typed table here is what lets each leaf's canonicalSpec collapse into a one-liner forwarding to SpecForResource — eliminating the three-way duplication Sonar flagged (settings/list.go 66.2%, mcp/seams.go 36.1%, rules/list.go 29.6%) without changing observable CLI behavior (help strings, examples, error messages all preserved verbatim).
Per-resource RUNNERS (List/Resolve callbacks that need the leaf package's platform.* helpers and the leaf's findXxxSpec error wrapping) still come from the leaf via ResourceRunners — they cannot live here because they close over leaf-specific Deps for hint-aware errors.
type RemoveDeps ¶
RemoveDeps carries the user-facing flags consumed by RunCanonicalRemove.
type ResourceRunners ¶
type ResourceRunners struct {
// List enumerates entries for a scope. Returning an os.IsNotExist
// error indicates the scope directory is missing — RunCanonicalList
// turns that into the def's MissingDirHint informational message.
List func(agentsHome, scope string) ([]CanonicalFileEntry, error)
// Resolve looks up one entry by basename or stem. Errors are
// propagated verbatim, so leaves should wrap with the parent
// commands.ErrorWithHints / commands.UsageError shape via their
// deps before returning.
Resolve func(agentsHome, scope, name string) (CanonicalFileEntry, error)
// ListRun / ShowRun / RemoveRun are the leaf's per-verb runners.
// The cobra RunE wrappers in canonical_cmd.go unpack args and call
// these directly.
ListRun func(scope string) error
ShowRun func(scope, name string) error
RemoveRun func(scope, name string) error
}
ResourceRunners carries the per-leaf closures that cannot live alongside the static CanonicalResourceDef strings: List and Resolve close over the leaf's platform.ListCanonicalXxxFiles helper and the leaf's findXxxSpec error wrapper (which threads the leaf's Deps for hint-aware errors).
The three verb runners (ListRun/ShowRun/RemoveRun) are also leaf-bound because each forwards into the leaf's own RunList/RunShow/RunRemove — some of those take deps directly (rules.RunList) while others bind deps via closure (mcp.RunList, settings.RunList).
type RoutedTarget ¶
type RoutedTarget struct {
// Scope is the resolved ownership scope.
Scope config.EditScope
// SourceID is the stable local source identifier the write lands in.
SourceID string
// Owner names the team/org that owns a governed or owned-project source.
// Empty for local and personal-project targets.
Owner string
}
RoutedTarget is what a command writes to once routing succeeds: the resolved ownership scope plus the source id. It is the command-facing projection of config.WriteTarget — the descriptor the caller threads into its write path.
func ResolveTarget ¶
func ResolveTarget(f ScopeSourceFlags, owner string) (RoutedTarget, error)
ResolveTarget maps the parsed flags to a RoutedTarget, applying the default scope (local) and validating the requested scope. owner names the owning team/org for a governed or owned-project source; pass "" for local/personal.
It performs NO editability check — that is CheckWrite's job — so a command can resolve once and reuse the target.
type Router ¶
type Router struct {
// contains filtered or unexported fields
}
Router maps --scope/--source flags to a RoutedTarget and gates mutating ops through the governance seam. Construct it with NewRouter, binding the config.Checker the host wired (a nil checker uses the safe default-prompt behavior for governed scopes).
func NewRouter ¶
NewRouter returns a Router that runs editability checks through checker. A nil checker is replaced with config.NewChecker(nil) so governed scopes still fail closed to a prompt rather than panicking.
func (*Router) CheckWrite ¶
CheckWrite runs the editability check for target on behalf of principal, CONSUMING the governance seam. It returns:
- the verdict and nil error when the write is ALLOWED;
- the verdict and a non-nil error when the write is DENIED or requires a PROMPT, with the verdict's operator-facing Reason surfaced in the error.
Callers that want to honor a prompt (confirm-then-write) inspect the returned verdict's Decision; callers that treat any non-allow as a hard stop can rely on the returned error alone.
func (*Router) Route ¶
func (r *Router) Route(f ScopeSourceFlags, p config.Principal, owner string) (RoutedTarget, config.Verdict, error)
Route is the one-call convenience wrapping ResolveTarget + CheckWrite for the common path: resolve the flags, then gate the write. It returns the resolved target alongside the verdict so an allowed caller writes straight to it, and a prompt-honoring caller can re-check the verdict's Decision.
type ScopeSourceFlags ¶
type ScopeSourceFlags struct {
// Scope is the requested ownership scope (local/team/org/project). Empty
// resolves to the default, ScopeLocal.
Scope string
// Source is the stable local source id (the `id` in the `sources` array,
// §3) the write targets. Required for governed scopes; for local it names
// the personal store and may be left to the command's default.
Source string
}
ScopeSourceFlags holds the raw --scope/--source flag values for a mutating command. Bind it once per command with BindScopeSourceFlags; the cobra flag package writes the parsed values back into these fields.
type SubCmdStrings ¶
SubCmdStrings carries the per-leaf strings for one canonical subcommand. Use/Short are required; Long/Example are optional. Mirrors what the old commands/internal/canonical package exposed so the leaf spec literals retain a familiar shape.