lifecycle

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: 20 Imported by: 0

Documentation

Overview

Package lifecycle hosts the project-lifecycle command cluster extracted from the root commands package as part of the root-command-decomposition plan (SHAPE.md §1, §4a).

Lifecycle commands mutate or inspect a managed project's on-disk state: add, doctor, import, import_plugins, init, install, refresh, remove, status, and the platform-tagged linkcount helpers. They share the project-mutation helpers in internal/links, internal/platform, internal/projectsync, and internal/scaffold/{home,hooks}, which is why they cluster into a single subpackage rather than per-command subpackages (contrast with commands/agents/, commands/skills/, etc., which are single-resource browsers).

This file is part of the t02 skeleton: only doc.go and deps.go land here. Individual command moves arrive in tasks t03 through t09, and the root re-export shims are deleted in t13 once commands/root.go switches to importing the lifecycle.New*Cmd constructors directly.

Index

Constants

View Source
const (
	InitCmdUse   = "init"
	InitCmdShort = "Initialize ~/.agents/ directory structure"
	InitCmdLong  = `` /* 263-byte string literal not displayed */

	InitCmdNoArgsHint = "`da init` bootstraps the shared store and does not take a project path."
)

Command-text constants for the init cobra command. The shim in commands/init.go constructs the *cobra.Command literal using these so the RunE closure (and thus the runtime symbol globalflagcov resolves) lives in package commands, not lifecycle. This keeps the t05 move transparent to the existing globalflagcov tool — folded into a single constructor when t13 deletes the shim and globalflagcov gains lifecycle in its package list.

View Source
const (
	RelClaudeSettingsJSON    = ".claude/settings.json"
	RelCursorSettingsJSON    = ".cursor/settings.json"
	RelCursorMCPJSON         = ".cursor/mcp.json"
	RelCursorHooksJSON       = ".cursor/hooks.json"
	RelCursorIgnore          = ".cursorignore"
	RelCursorIndexingIgnore  = ".cursorindexingignore"
	RelClaudeSettingsLocal   = ".claude/settings.local.json"
	RelMCPJSON               = ".mcp.json"
	RelVSCodeMCPJSON         = ".vscode/mcp.json"
	RelOpenCodeJSON          = "opencode.json"
	RelAgentsMD              = "AGENTS.md"
	RelCodexInstructionsMD   = ".codex/instructions.md"
	RelCodexRulesMD          = ".codex/rules.md"
	RelCodexConfigTOML       = ".codex/config.toml"
	RelCodexHooksJSON        = ".codex/hooks.json"
	RelCopilotInstructionsMD = ".github/copilot-instructions.md"
	RelCursorCommandsDir     = ".cursor/commands/"
	RelClaudeCommandsDir     = ".claude/commands/"
	RelOpenCodeCommandsDir   = ".opencode/commands/"
	RelClaudeOutputStylesDir = ".claude/output-styles/"
	RelOpenCodeModesDir      = ".opencode/modes/"
	RelOpenCodeThemesDir     = ".opencode/themes/"
	RelGitHubPromptsDir      = ".github/prompts/"
	RelCursorRulesDir        = ".cursor/rules/"
	RelAgentsSkillsDir       = ".agents/skills/"
	RelClaudeSkillsDir       = ".claude/skills/"
	RelGitHubAgentsDir       = ".github/agents/"
	RelCodexAgentsDir        = ".codex/agents/"
	RelOpenCodeAgentsDir     = ".opencode/agent/"
	RelGitHubHooksDir        = ".github/hooks/"
	RelAgentMarkdownSuffix   = ".agent.md"
	RelJSONSuffix            = ".json"
	AgentsHooksPrefix        = "hooks/"
)

Repo-relative path constants used by MapResourceRelToDest and referenced cross-package by commands/import.go (via aliasing const declarations) and commands/add.go. Lifted from commands/import.go in plan root-command-decomposition t02b so the resource-relative ↔ canonical-dest mapping can live alongside its consumers in the lifecycle subpackage.

Variables

View Source
var (
	Version  = "dev"
	Commit   = ""
	Describe = ""
)

Version/Commit/Describe mirror the build-info vars defined in commands/refresh.go. Populated by the same two paths as Flags above: the transitional install.go shim writes them via syncLifecycleGlobals, and the NewInstallCmd / NewDoctorCmd / NewInitCmd RunE wrappers (t13a) write them from Deps via applyDepsToGlobals.

View Source
var (
	InitForceFn  = func() bool { return initForce }
	InitDryRunFn = func() bool { return initDryRun }
	InitYesFn    = func() bool { return initYes }
)

Package-level seam vars for init (preserved per t01 SHAPE.md "PRESERVE current package-var seams during the moves"). Each is a getter so the shim in commands/init.go can repoint them at commands.Flags lazily — NewInitCmd runs at root-construction time (before flags parse), and the getters are evaluated at RunE time. Tests in this package set the underlying bool fields directly via the convenience setters below. Kept file-scoped (not promoted to deps.go) because t05's bundle write_scope intentionally limits this task to the init files only — Force/DryRun/Yes are init-specific seams today and consolidate into a shared lifecycle.Flags struct alongside the other lifecycle command moves (t03/t04/t06+).

View Source
var CanonicalImportOutputs = func(c ImportCandidate) ([]ImportOutput, bool, error) {
	return nil, false, nil
}

CanonicalImportOutputs is wired at init time by commands/import.go. Returns ([]ImportOutput, handled, err); handled=false means the candidate is not a canonical resource (not a hook bundle / settings file the canonical importer recognizes) and the caller should fall back to legacy per-file restore. Stub default returns "not handled" so the lifecycle package builds standalone and runs without the commands package's init() (e.g. in commands/lifecycle/... tests that do not exercise canonical-import).

View Source
var ErrorWithHintsFn = defaultErrorWithHints

ErrorWithHintsFn is a package-var seam onto commands.ErrorWithHints. Lifecycle cannot import commands (cycle); the t03 shim in commands/install.go assigns this to commands.ErrorWithHints at init time. A default value (plain fmt.Errorf-style formatting) keeps tests that exercise lifecycle entry points directly green even when the shim is absent. The NewXxxCmd constructors' RunE wrappers also write this from Deps.ErrorWithHints when non-nil (t13a) so a t13b call site needs no extra sync step.

View Source
var HasMultipleHardLinks = platform.HasMultipleHardLinks

HasMultipleHardLinks is the platform-tagged hard-link counter seam. Backed by platform.HasMultipleHardLinks (the canonical implementation lives in internal/platform/claude_linkcount_{unix,windows}.go after the P3 fold-back absorbed the relocation deferred by P1 — see .agents/active/fold-back/p1-hasmultiplehardlinks-move-deferred-to-p3.md). Declared as a package-level func variable so backup_test.go can override the link-count behavior without invoking real syscalls.

Exported because backup.go (this file) and status.go's claude-rules counter both read it across the same package, and seams_test.go in the root commands package still overrides it for fault injection. Once the last cross-file consumer disappears the seam can lowercase back to package-private.

View Source
var InitCmdExample = strings.Join([]string{
	"  da init",
	"  da init --dry-run",
	"  da init --force",
	"  da init --from git@github.com:you/agents-config.git",
}, "\n")

InitCmdExample is the rendered Example string for the init command.

View Source
var InitEnsureGlobalKGMCPConfigsFn = EnsureGlobalKGMCPConfigs

InitEnsureGlobalKGMCPConfigsFn is the in-package seam for the KG MCP config scaffolder. Defaults to lifecycle.EnsureGlobalKGMCPConfigs (lifted into this package by t02b), eliminating the prior cross-package indirection through the commands shim. Kept as a var (not a direct call) so tests can fault-inject failure scenarios without monkey-patching the underlying helper.

View Source
var InitUsageErrorFn = func(msg string, hints ...string) error {
	if len(hints) == 0 {
		return fmt.Errorf("%s", msg)
	}
	return fmt.Errorf("%s\n  %s", msg, strings.Join(hints, "\n  "))
}

InitUsageErrorFn is the UsageError seam (defaults to a plain error so the lifecycle subpackage does not import commands, avoiding the commands→lifecycle→commands cycle). The shim in commands/init.go overrides this with commands.UsageError to preserve the formatted-hint UX users see today.

View Source
var RestoreCanonicalResourceFileFn = func(project, resourcesDir, agentsHome, path string, deps AddDeps) (int, bool, error) {
	return 0, false, nil
}

RestoreCanonicalResourceFileFn is the canonical-import branch wired at init time by commands/import.go (canonicalImportOutputs and its hook bundle helpers stay in import.go until t06 moves the whole import command). Returns (count, handled, err); handled=false means the file is not a canonical resource and the caller should fall back to RestoreLegacyResourceFile.

Functions

func BackupExistingConfigsList

func BackupExistingConfigsList(files []string, projectPath, agentsHome, project, timestamp string, deps AddDeps) (int, error)

BackupExistingConfigsList backs up the given files into ~/.agents/resources/<project>/... and removes the originals from the project tree. No *.dot-agents-backup files are left in the project. Returns count of files processed and a non-nil error if any required backup copy failed. On backup failure the original is NOT removed (the user's only copy is preserved) and the error aborts runAdd before any destructive removal. Lifted from commands/add.go in root-command-decomposition t02b.

func CloneGitSource

func CloneGitSource(gitBin, url, ref, cacheDir string, deps InstallDeps) (string, error)

CloneGitSource is exported so the t03 shim in commands/install.go can forward to it. seams_test.go's TestCloneGitSource_MkdirAllError reaches it through the lower-case wrapper in the shim.

func CountClaudeRules

func CountClaudeRules(path string) (int, int)

CountClaudeRules is the exported entry point used by commands/seams_test.go (still in root before t11) and the legacy status_exports_test.go suite. Now delegates to the claude platform's CountLinks-style helper exposed for the test-only seam (claude.CountLinks covers the same rules-dir branch plus the managed-file extras). Returns the (ok, warn) tally for the .claude/rules directory only — matching the historical countClaudeRules signature so test fixtures keep the same assertion shape.

func DetectAndEnableNewPlatforms

func DetectAndEnableNewPlatforms(cfg *config.Config) []string

DetectAndEnableNewPlatforms re-probes every known platform and reconciles cfg with what is actually installed on this machine:

  • A platform installed but currently disabled in cfg is flipped to enabled and its live version recorded. This is the refresh-driven fix for a platform installed AFTER `da init`: init writes enabled:false for tools that were not on PATH at init time, and a plain refresh would otherwise iterate only already-enabled platforms forever (the "Nothing to refresh" dead-end).
  • A platform already enabled and still installed has its recorded version refreshed from the live probe (stale version strings are updated).

The probe is the same one init uses: platform.Platform.IsInstalled() (CLI on PATH) and Version() for the recorded version string. Detection is conservative — it never DISABLES anything. A platform that is enabled but no longer installed is left enabled (callers simply skip projecting/refreshing what is absent); refresh does not auto-disable.

The caller is responsible for persisting cfg (cfg.Save()) after this returns. Returns the display names of the platforms that were newly enabled, in All() order, so the caller can announce each one.

func EnsureGlobalKGMCPConfigs

func EnsureGlobalKGMCPConfigs(agentsHome string) error

EnsureGlobalKGMCPConfigs writes the KG MCP config files into the global mcp scope when a KG_HOME/self/config.yaml exists. No-op when the KG self config is absent. Lifted from commands/add.go in root-command-decomposition t02b.

func FindProjectByPath

func FindProjectByPath(projectPath string, deps InstallDeps) string

FindProjectByPath looks up the registered project name for a given path. Exported because the t03 shim in commands/install.go forwards to it and seams_test.go reaches it through the shim.

func InitNoArgs

func InitNoArgs(hints ...string) cobra.PositionalArgs

InitNoArgs is the exported Args validator for the init command, delegating to the file-local initNoArgs implementation. Exported so the shim can wire it into the cobra command literal.

func IsBackupArtifact

func IsBackupArtifact(name string) bool

IsBackupArtifact reports whether name is a dot-agents backup artifact. Lifted from commands/add.go in root-command-decomposition t02b.

func IsCanonicalResourceBackupRel

func IsCanonicalResourceBackupRel(relPath string) bool

IsCanonicalResourceBackupRel reports whether relPath sits under one of the canonical resource roots (rules/, settings/, mcp/, skills/, agents/, hooks/) and should therefore be ignored when the restore loop walks the resources directory (those subtrees are produced by canonical-import, not legacy backups). Exported (not in the named 15) because add_test.go covers it directly.

func IsManagedCursorRuleRel

func IsManagedCursorRuleRel(project, rel string) bool

IsManagedCursorRuleRel reports whether rel points at a managed Cursor rule under .cursor/rules/ with the project's or global namespace prefix. Internal helper used by IsManagedProjectOutput; not part of the t02b 15 but lifted alongside its sole caller to keep the backup cluster self-contained.

func IsManagedProjectOutput

func IsManagedProjectOutput(project, projectPath, filePath, agentsHome string) bool

IsManagedProjectOutput reports whether filePath is a managed dot-agents projection at projectPath under agentsHome for project. Lifted from commands/add.go in root-command-decomposition t02b.

func IsManagedSymlink(path, agentsHome string) bool

IsManagedSymlink reports whether path is a resolvable managed link (POSIX symlink / Windows junction) whose resolved target lies under agentsHome. A Windows hard-linked managed *file* has no reparse point to test against a prefix and is reported false here, matching the prior symlink-only behavior on POSIX. Lifted from commands/import.go in root-command-decomposition t02b.

func KGConfigPath

func KGConfigPath() string

KGConfigPath returns the path to KG_HOME/self/config.yaml without importing the kg subpackage. Used by EnsureGlobalKGMCPConfigs and re-aliased into commands/add.go for the legacy kgConfigPath test site until commands/init.go and commands/seams_test.go move.

func LinkResourceFromSources

func LinkResourceFromSources(resourceType, name, project string, sources []string, deps InstallDeps) error

LinkResourceFromSources symlinks a resource from the first matching source into ~/.agents/{resourceType}/{project}/{name}/. Exported because the t03 shim in commands/install.go forwards to it and seams_test.go reaches it through the shim.

func MapResourceRelToDest

func MapResourceRelToDest(project, relPath string) string

MapResourceRelToDest maps a project-relative input path (e.g. ".cursor/rules/global--foo.mdc") to the canonical ~/.agents-relative destination (e.g. "rules/global/foo.mdc"). Returns "" when no mapping applies. Lifted from commands/refresh.go in root-command-decomposition t02b; behavior unchanged from the original.

Implementation note: the logic is split into focused helpers (mapExactRel, mapBucketDirRel, mapCursorRulesRel, mapSkillsRel, mapAgentsRel, mapHooksRel, mapPassThroughRel) so each branch has a single concern and the top-level function stays inside Sonar's cognitive-complexity limit (S3776).

func MirrorBackup

func MirrorBackup(project, projectPath, srcFile, timestamp string)

MirrorBackup copies srcFile (original path, before deletion) into the ~/.agents/resources/<project>/ tree using the file's original relative path. No *.dot-agents-backup suffix is added anywhere.

This is the errorless wrapper retained for import.go callers, whose own failure handling keys off the subsequent CopyFile into the destination (a mirror-backup failure there does not destroy the user's only copy because import.go never removes the source after MirrorBackup). Callers that delete the original after backing it up (BackupExistingConfigsList) MUST use MirrorBackupChecked so a failed backup aborts before the destructive removal.

func MirrorBackupChecked

func MirrorBackupChecked(project, projectPath, srcFile, timestamp string, deps AddDeps) error

MirrorBackupChecked performs the same copy as MirrorBackup but propagates the CopyFile errors. BackupExistingConfigsList relies on this: it removes the user's only copy of an unmanaged config after backing it up, so a silent backup failure (unwritable ~/.agents/resources, disk full, unreadable source through a symlink) would destroy that config while reporting a successful backup. Exported (not in the named 15) because add_test.go exercises it directly through its var alias in commands/add.go.

func NewDoctorCmd

func NewDoctorCmd(deps Deps) *cobra.Command

NewDoctorCmd builds the `da doctor` cobra command. Mirrors the NewStatusCmd / NewInstallCmd Deps-injection pattern: lifecycle does not import the parent commands/ package (cycle), so Args/Example helpers and the UsageError formatter come in via Deps.

The RunE wrapper calls applyDepsToGlobals(deps) before delegating so the moved doctor body (which reads lifecycle.Flags / .ErrorWithHintsFn package vars directly) sees live state from the caller. After t13a this absorbs what the parent commands/doctor.go shim's syncLifecycleGlobals wrap used to do — a t13b call site of the form `lifecycle.NewDoctorCmd(buildLifecycleDeps())` works end-to-end without a separate sync step. The existing parent shim's wrap remains compatible because both syncs write the same values (idempotent).

func NewInitCmd

func NewInitCmd(deps Deps) *cobra.Command

NewInitCmd builds the `da init` cobra command. Mirrors the NewInstallCmd / NewDoctorCmd Deps-injection pattern so the t13b worker can call `lifecycle.NewInitCmd(buildLifecycleDeps())` without any further wiring. Added in t13a — before this, the parent commands/init.go shim owned the cobra literal and used SetInitFlags + InitUsageErrorFn package-var seam writes to repoint the per-init getters at commands.Flags / commands.UsageError.

Construction-time wiring:

  1. SetInitFlags is called with closures over deps so InitForceFn / InitDryRunFn / InitYesFn read live state at RunE time (cobra parses flags AFTER constructor return; a value snapshot taken at construction would be stale). FlagsFn takes precedence over the Deps.Flags value, mirroring applyDepsToGlobals.

  2. InitUsageErrorFn is repointed at deps.UsageError when non-nil so init's positional-arg rejection renders through commands.UsageError's formatted-hint UX. When deps.UsageError is nil (lifecycle-only unit tests that don't construct a hint formatter) the in-package default InitUsageErrorFn is preserved.

The RunE wrapper additionally calls applyDepsToGlobals(deps) so downstream helpers that read lifecycle.Flags / .ErrorWithHintsFn / .Version / .Commit / .Describe (none for init today, but symmetric with NewInstallCmd / NewDoctorCmd) observe the same Deps-derived state.

Compatible with the existing parent commands/init.go shim, which sets the same seams via SetInitFlags + direct InitUsageErrorFn assignment before constructing its own cobra literal — t13b's shim deletion will switch root.go to call NewInitCmd(buildLifecycleDeps()) directly and drop the parent shim's manual wiring.

func NewInstallCmd

func NewInstallCmd(deps Deps) *cobra.Command

NewInstallCmd builds the `da install` cobra command. The Deps argument carries UX helpers and the global-flags snapshot from the commands package; the RunE wrapper calls applyDepsToGlobals(deps) before each invocation so the helper functions in this file (which read lifecycle.Flags / .Version / .Commit / .Describe / .ErrorWithHintsFn package vars directly per the t01 SHAPE.md "PRESERVE current package-var seams during the moves" decision) stay in sync with the parent process's live state.

After t13a this absorbs what the parent commands/install.go shim's syncLifecycleGlobals helper used to do — a t13b call site of the form `lifecycle.NewInstallCmd(buildLifecycleDeps())` works end-to-end without a separate sync step. The existing parent shim's wrap remains compatible: the shim's syncLifecycleGlobals runs before the inner RunE, then this wrapper re-applies the same values from deps. Both writes are idempotent.

func NewRefreshCmd

func NewRefreshCmd(deps Deps) *cobra.Command

NewRefreshCmd builds the `da refresh` cobra command. The cobra metadata (Use/Short/Long/Example/Args) and the `--import` / `--inexact` flags live here in lifecycle; the RunE delegates to deps.RunRefresh which still holds the legacy runRefresh body in commands/ until t04 (add) and t06 (import) merge. See SHAPE.md §4a (refresh row) and the fold-back at .agents/active/fold-back/t07-refresh-body-deferred.md for the rationale.

func NewStatusCmd

func NewStatusCmd(deps Deps, jsonOutput func() bool) *cobra.Command

NewStatusCmd builds the `da status` cobra command. The jsonOutput closure reports whether to emit the JSON variant — the parent shim in commands/status.go passes `func() bool { return commands.Flags.JSON }` so the package-var seam stays at the root while lifecycle stays import-cycle free. Tests can pass their own closure to exercise either path.

── t13a constructor-shape decision ────────────────────────────────────────

NewStatusCmd retains the second jsonOutput func() bool argument (option (c) in the t13a fold-back observation at .agents/active/fold-back/ t13a-respawn-lifecycle-shims-not-passthroughs.yaml) rather than folding it into Deps. Rationale: the parent commands/status.go shim's RunE-override reads Flags.JSON through the jsonFlag closure inside the commands package so the globalflagcov static analyzer (which loads ./commands but not ./commands/lifecycle) sees the Flags.JSON load it requires for handler coverage. Threading jsonOutput through Deps would move that read into lifecycle and silently drop the coverage. T13b's worker can pass `lifecycle.NewStatusCmd(buildStatusDeps(), func() bool { return Flags.JSON })` without further wrapping, or widen globalflagcov's load set to include ./commands/lifecycle (preferred long-term — see t13 PR description). The jsonOutput argument also lets tests exercise both JSON and text paths without mutating any package var.

NewStatusCmd intentionally does NOT call applyDepsToGlobals because the moved status helpers do not read the lifecycle.Flags / .Version / .Commit / .Describe / .ErrorWithHintsFn package vars — status only consumes Deps directly (UsageError via statusNoArgsHint) plus the jsonOutput closure. Install/doctor/init all sync because their moved RunE bodies read the package vars; status does not.

func PrintAudit

func PrintAudit(name, path, agentsHome, agentFilter string, cfg *config.Config)

PrintAudit is the exported entry point used by commands/doctor.go (still in root before t09) to render the per-platform audit block. After t09 lands doctor in this package, the only caller is intra-package and PrintAudit can be lowercased back to printAudit (SHAPE.md OD-2 reversal pattern).

func PrintSymlinkDirAudit

func PrintSymlinkDirAudit(dir, emptyLabel, nameFormat string) (int, int)

PrintSymlinkDirAudit is the exported entry point used by commands/seams_test.go (still in root before t11). Reversed when t11 splits the test file per cluster.

func RegisterInstallProject

func RegisterInstallProject(projectName, projectPath string, deps InstallDeps) error

RegisterInstallProject upserts the project into ~/.agents/config.json, honoring --dry-run. Exported because the t03 shim in commands/install.go forwards to it and seams_test.go reaches it through the shim.

func RestoreFromResourcesCountedWithDeps

func RestoreFromResourcesCountedWithDeps(project, projectPath string, deps AddDeps) (int, error)

RestoreFromResourcesCountedWithDeps restores files from ~/.agents/resources/<project>/ and returns the number of files restored plus a non-nil error if any directory walk, mkdir, write, or copy failed. Callers that stamp success (e.g. refresh metadata) MUST observe this error: a partially-applied restore that is reported as success makes retries and doctor/refresh recovery ambiguous. Lifted from commands/add.go in root-command-decomposition t02b.

func RestoreLegacyResourceFile

func RestoreLegacyResourceFile(project, relPath, agentsHome, path string, deps AddDeps) (int, error)

RestoreLegacyResourceFile restores one non-canonical resource file by mapping its repo-relative path to its canonical ~/.agents location (via MapResourceRelToDest) and copying via deps.CopyFile. Returns 0 when the rel path has no known mapping. Lifted from commands/add.go in root-command-decomposition t02b.

func RunDoctor

func RunDoctor(cmd *cobra.Command, args []string, deps DoctorConfigLoader) error

RunDoctor is the exported entry point used by commands/doctor.go's t09→t11 root shim so seams_test.go can drive the doctor pipeline with a fault- injected DoctorConfigLoader. After t11 the shim is deleted and the lowercase runDoctor satisfies the only remaining (intra-package) callers.

func RunInit

func RunInit(cmd *cobra.Command, args []string) error

RunInit is the exported entry the shim's RunE closure calls. It fronts the unexported runInit (which keeps the initDirMaker seam file-scoped to lifecycle).

func RunInitForTest

func RunInitForTest(cmd *cobra.Command, args []string, deps InitDirMakerForTest) error

RunInitForTest is the export-for-test bridge for runInit, used by the parent package's seams_test.go fault-injection cases until t11 relocates them. Production callers go through NewInitCmd.

func RunInstall

func RunInstall(strict bool, deps InstallDeps) error

func RunInstallGenerate

func RunInstallGenerate(deps InstallDeps) error

func RunStatus

func RunStatus(audit bool, agentFilter string, deps StatusConfigLoader, jsonOut bool) error

RunStatus is the exported entry point used by the root commands/status.go shim during the t08→t11 window. After t11 splits seams_test.go into commands/lifecycle/seams_test.go, the only remaining caller is the shim itself; at that point RunStatus can be lowercased back to runStatus per SHAPE.md OD-2's reversal pattern.

func RunStatusDefault

func RunStatusDefault(audit bool, agentFilter string, jsonOut bool) error

RunStatusDefault is the convenience entry point the root status shim uses from inside the cobra RunE closure it owns. Keeping the RunE closure in the commands package (instead of inheriting lifecycle.NewStatusCmd's closure) lets the globalflagcov static analyzer trace the Flags.JSON read without expanding its package load set to ./commands/lifecycle.

func ScaffoldWorkflowAssetsForTest

func ScaffoldWorkflowAssetsForTest(agentsHome string, deps InitDirMakerForTest) error

ScaffoldWorkflowAssetsForTest mirrors RunInitForTest for the scaffoldWorkflowAssets MkdirAll fault-injection test in seams_test.go.

func SetInitFlags

func SetInitFlags(force, dryRun, yes func() bool)

SetInitFlags is the shim hook for repointing the init force/dry-run/ yes getters at the parent commands.Flags. Callers can pass nil for a getter to leave the in-package default in place (used by tests that only need to flip a subset of the seams).

func ShouldUseCachedGitSource

func ShouldUseCachedGitSource(cacheDir, url string) bool

ShouldUseCachedGitSource is exported so the t03 shim in commands/install.go can forward to it. seams_test.go's verbose-info branch test reaches it through the lower-case wrapper in the shim.

func WriteKGMCPConfigFile

func WriteKGMCPConfigFile(path string, server map[string]any, deps AddDeps) error

WriteKGMCPConfigFile merges the dot-agents-kg server entry into the JSON file at path, preserving any existing top-level keys and any other server entries. Lifted from commands/add.go in root-command-decomposition t02b.

func WriteKGMCPConfigs

func WriteKGMCPConfigs(scopeDir string, deps AddDeps) error

WriteKGMCPConfigs writes the dot-agents-kg MCP server entry into the three canonical platform config files (claude.json, cursor.json, mcp.json) under scopeDir. Lifted from commands/add.go in root-command-decomposition t02b.

Types

type AddDeps

type AddDeps interface {
	MkdirAll(path string, perm os.FileMode) error
	WriteFile(name string, data []byte, perm os.FileMode) error
	Remove(name string) error
	Executable() (string, error)
	CopyFile(src, dst string) error
	LoadConfig() (*config.Config, error)
}

AddDeps is the multi-method collaborator the lifecycle backup / restore / KG-MCP-config helpers need (interface-DI per docs/TEST_SEAMS.md). Lifted from commands/add.go in plan root-command-decomposition t02b so the helpers can live outside the root commands package without dragging in the rest of add.go.

The six operations are the add pipeline's fault-injectable touch points: filesystem materialization of resource trees and MCP config parents (MkdirAll), the MCP config payload itself (WriteFile), the destructive removal of an unmanaged config after a successful backup (Remove), the dot-agents binary path used to build the KG MCP server command (Executable), the resource copy used to back up and restore unmanaged configs (CopyFile), and config.json load for project registration lookups (LoadConfig).

type Deps

type Deps struct {
	Flags                 GlobalFlags
	ErrorWithHints        func(message string, hints ...string) error
	UsageError            func(message string, hints ...string) error
	MaximumNArgsWithHints func(n int, hints ...string) cobra.PositionalArgs
	RangeArgsWithHints    func(min, max int, hints ...string) cobra.PositionalArgs
	ExactArgsWithHints    func(n int, hints ...string) cobra.PositionalArgs
	NoArgsWithHints       func(hints ...string) cobra.PositionalArgs

	// ExampleBlock formats a multi-line cobra Example block. Mirrors the
	// commands.ExampleBlock helper. Lifecycle subcommands use it for the
	// cobra Example field at construction time.
	ExampleBlock func(lines ...string) string

	// RunRefresh is the back-edge into commands.runRefresh used by
	// NewRefreshCmd's RunE. importAlso/inexact carry the parsed --import and
	// --inexact flags through to the legacy body. The actual run body,
	// package-var seams (stdRefreshConfigLoader, stdImportDeps, stdAddDeps),
	// and the helper fan-out (mapResourceRelToDest, restoreFromResources)
	// remain in commands/ until t04 (add) and t06 (import) merge and the
	// cross-cluster constants / interfaces (addDeps, importDeps,
	// importScope*, rel*Dir) can be re-homed into lifecycle. See
	// .agents/active/fold-back/t07-refresh-body-deferred.md.
	RunRefresh func(projectFilter string, importAlso, inexact bool) error

	// FlagsFn is an optional closure over the caller's live flag state.
	// When non-nil it is invoked by NewInstallCmd / NewDoctorCmd /
	// NewInitCmd's RunE wrapper to populate the lifecycle.Flags package
	// var at RunE time (cobra has parsed flags by then; a Deps.Flags
	// value snapshot taken at constructor time would be stale). When
	// nil, Deps.Flags is copied as-is.
	//
	// T13b's worker passes a closure of the form
	//   func() GlobalFlags { return GlobalFlags{DryRun: commands.Flags.DryRun, ...} }
	// so the constructor reads live state on each invocation. Test code
	// that exercises the constructor with a deterministic snapshot leaves
	// FlagsFn nil and sets Deps.Flags directly.
	FlagsFn func() GlobalFlags

	// Version/Commit/Describe carry the build-info values the install
	// pipeline's finalizeInstall helper writes into .agentsrc.json. When
	// non-empty they are copied to the lifecycle.Version / .Commit /
	// .Describe package vars by NewInstallCmd / NewDoctorCmd / NewInitCmd's
	// RunE wrapper (the moved helpers read those vars directly). The
	// zero-value "" defaults to leaving the package var untouched, so the
	// build-info defaults set on the package vars themselves ("dev"/"" for
	// Version/Commit/Describe) survive when a caller omits them.
	Version  string
	Commit   string
	Describe string
}

Deps carries UX helpers and runtime state from the commands package into lifecycle without an import cycle. Mirrors agents.Deps and skills.Deps so the three extracted subpackages share the same shape. Only fields actually consumed by lifecycle subcommands are present.

Per SHAPE.md OD-7, there is intentionally no NewDeps() factory: the composition root in commands/root.go (and the transitional shims in commands/<verb>.go during t03–t12) construct the struct inline, the same way commands.NewAgentsCmd and commands.NewSkillsCmd do today.

── Constructor contract after t13a ────────────────────────────────────────

All four lifecycle command constructors accept Deps as the first (and only) argument:

NewInstallCmd(deps Deps) *cobra.Command
NewDoctorCmd(deps Deps)  *cobra.Command
NewInitCmd(deps Deps)    *cobra.Command
NewStatusCmd(deps Deps, jsonOutput func() bool) *cobra.Command

NewStatusCmd retains the second jsonOutput argument (option (c) in the t13a fold-back observation) rather than absorbing it into Deps.JSONFlag. Rationale: the parent commands/status.go shim already passes the jsonFlag closure as a second positional arg so the globalflagcov static analyzer (which loads ./commands but not ./commands/lifecycle) can see the Flags.JSON read for handler coverage. Folding the closure into Deps would move the read site into lifecycle and break that coverage. T13b will either widen globalflagcov's load set (preferred) or keep the second-arg shape — the t13a contract leaves both doors open. T13b's worker can pass `lifecycle.NewStatusCmd(buildStatusDeps(), func() bool { return Flags.JSON })` without further wrapping.

── Constructor-side global sync (t13a) ───────────────────────────────────

NewInstallCmd / NewDoctorCmd / NewInitCmd's RunE wrappers internally call applyDepsToGlobals(deps) before delegating to the moved RunE body. That absorbs the parent shim's syncLifecycleGlobals dance: t13b's worker just constructs Deps with live commands.Flags / Version / Commit / Describe / ErrorWithHints in FlagsFn / Version / Commit / Describe / ErrorWithHints and calls the constructor. The constructor handles the rest. FlagsFn is a closure (not a value) so it captures live state at each RunE invocation — critical because cobra parses flags AFTER constructor calls. NewStatusCmd does NOT call applyDepsToGlobals because its moved helpers do not read the commands-package globals (status uses Flags only for jsonOutput, which the caller-supplied closure carries directly).

type DoctorConfigLoader

type DoctorConfigLoader interface {
	LoadConfig() (*config.Config, error)
}

DoctorConfigLoader is the narrow collaborator doctor.go's fault-injectable LoadConfig operation needs (interface-DI per docs/TEST_SEAMS.md). Single-method, file-prefixed -er form; file-scoped — do not share with other commands files.

Exported during the t09→t11 window so commands/seams_test.go (still in root until t11 splits it per cluster) can construct test doubles via the root shim's RunDoctor entry point. After t11 the root shim drops the type alias and this can lowercase back to doctorConfigLoader.

type GlobalFlags

type GlobalFlags struct {
	DryRun  bool
	Force   bool
	Verbose bool
	Yes     bool
}

GlobalFlags mirrors the subset of commands.Flags consumed by lifecycle subcommands. Kept as a parallel type to commands.GlobalFlags so the lifecycle subpackage has no import on the parent commands/ package, matching the agents.GlobalFlags / skills.GlobalFlags pattern.

Extended in t03 with DryRun/Force/Verbose because the install command reads all four flags from a package-level Flags var (preserving the existing commands.Flags package-var seam per t01 SHAPE.md decision — "PRESERVE current package-var seams during the moves").

var Flags GlobalFlags

Flags is the package-level mirror of commands.Flags consumed by lifecycle subcommands' moved helper bodies (which still read the package var directly per the t01 SHAPE.md "PRESERVE current package-var seams during the moves" decision). Three populating paths converge here:

  1. The transitional commands/install.go shim's syncLifecycleGlobals helper (active until t13b deletes the shim) writes commands.Flags into this package var before invoking any lifecycle entry point.
  2. The NewInstallCmd / NewDoctorCmd / NewInitCmd constructors' RunE wrappers (added by t13a) call applyDepsToGlobals(deps) before delegating to the moved RunE body, so a t13b call site of the form `lifecycle.NewInstallCmd(buildLifecycleDeps())` works end-to-end without a separate sync step.
  3. Tests that exercise lifecycle entry points directly write the var via `Flags = lifecycle.GlobalFlags{...}` and restore it on cleanup.

type ImportCandidate

type ImportCandidate struct {
	Project    string
	SourceRoot string
	SourcePath string
	DestRel    string
}

ImportCandidate is the exported, lifecycle-facing shape of an import candidate: the project name plus the source root + source file path from which a canonical import is produced. Lifted from commands/import.go in plan root-command-decomposition t02b.

The shape is intentionally identical to the unexported importCandidate struct in commands/import.go; the function-var seam below (CanonicalImportOutputs) converts between the two when bridging into the canonicalImportOutputs machinery that still lives in import.go until t06 moves the whole import command. Once t06 lands, the commands/import.go duplicate can be deleted and the lifecycle shape becomes the only one.

type ImportOutput

type ImportOutput struct {
	DestRel string
	Content []byte
	Origin  string
}

ImportOutput is the exported, lifecycle-facing shape of a canonical import emission: the destination-relative path and content bytes, plus the optional origin platform id used by RFC §6 alternate naming. Lifted from commands/import.go in t02b alongside ImportCandidate.

type InitDirMakerForTest

type InitDirMakerForTest = initDirMaker

InitDirMakerForTest re-exports the file-private initDirMaker interface so the commands package's seams_test.go can pass its own fault-injection double to RunInitForTest / ScaffoldWorkflowAssetsForTest without lifecycle exposing its full internal type. Folded out in t11 when seams_test.go itself relocates here.

type InstallDeps

type InstallDeps interface {
	Getwd() (string, error)
	MkdirAll(path string, perm os.FileMode) error
	Symlink(oldname, newname string) error
	LoadConfig() (*config.Config, error)
}

InstallDeps is the multi-method collaborator RunInstall, RunInstallGenerate, and their helpers need (interface-DI per docs/TEST_SEAMS.md). File-scoped — do not share with other commands files. The four operations are the install pipeline's fault-injectable touch points: working-directory resolution (Getwd), filesystem materialization of resource link parents and git cache roots (MkdirAll), the resource symlink itself (Symlink), and config.json load (LoadConfig) for project registration and lookup.

type StatusConfigLoader

type StatusConfigLoader interface {
	LoadConfig() (*config.Config, error)
}

StatusConfigLoader is the narrow collaborator status.go's fault-injectable LoadConfig operation needs (interface-DI per docs/TEST_SEAMS.md). Single-method, file-prefixed -er form; file-scoped — do not share with other commands files.

Exported during the t08→t11 window so commands/seams_test.go (still in root until t11 splits it per cluster) can construct test doubles via the root shim's RunStatus entry point.

type StdAddDeps

type StdAddDeps struct{}

StdAddDeps is the production AddDeps backed by direct os / projectsync / config calls. Production callers construct StdAddDeps{} inline; tests substitute a fake AddDeps implementation.

func (StdAddDeps) CopyFile

func (StdAddDeps) CopyFile(src, dst string) error

func (StdAddDeps) Executable

func (StdAddDeps) Executable() (string, error)

func (StdAddDeps) LoadConfig

func (StdAddDeps) LoadConfig() (*config.Config, error)

func (StdAddDeps) MkdirAll

func (StdAddDeps) MkdirAll(path string, perm os.FileMode) error

func (StdAddDeps) Remove

func (StdAddDeps) Remove(name string) error

func (StdAddDeps) WriteFile

func (StdAddDeps) WriteFile(name string, data []byte, perm os.FileMode) error

type StdDoctorConfigLoader

type StdDoctorConfigLoader struct{}

StdDoctorConfigLoader is the production DoctorConfigLoader backed by internal/config.Load. Exported alongside DoctorConfigLoader for the t09→t11 window so the root shim's NewDoctorCmd RunE can construct one while keeping lifecycle import-cycle free.

func (StdDoctorConfigLoader) LoadConfig

func (StdDoctorConfigLoader) LoadConfig() (*config.Config, error)

LoadConfig delegates to internal/config.Load.

type StdInstallDeps

type StdInstallDeps struct{}

StdInstallDeps is the production InstallDeps backed by the os package and config.Load.

func (StdInstallDeps) Getwd

func (StdInstallDeps) Getwd() (string, error)

func (StdInstallDeps) LoadConfig

func (StdInstallDeps) LoadConfig() (*config.Config, error)

func (StdInstallDeps) MkdirAll

func (StdInstallDeps) MkdirAll(path string, perm os.FileMode) error
func (StdInstallDeps) Symlink(oldname, newname string) error

Jump to

Keyboard shortcuts

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