cmdutil

package
v0.4.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 29, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

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

View Source
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

View Source
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.

View Source
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.

View Source
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

func CanonicalCmdExampleBlock(lines ...string) string

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

type CanonicalCmdFlags struct {
	DryRun bool
	Yes    bool
	Force  bool
}

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

type CanonicalFileEntry struct {
	Scope      string
	BaseName   string
	SourcePath string
}

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

type RemoveDeps struct {
	DryRun bool
	Yes    bool
	Force  bool
}

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

func NewRouter(checker *config.Checker) *Router

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

func (r *Router) CheckWrite(p config.Principal, target RoutedTarget) (config.Verdict, error)

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

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

type SubCmdStrings struct {
	Use     string
	Short   string
	Long    string
	Example string
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL