Core concepts
Understand plugins, resources, plans, and operational modes
Plugin
The Plugin class is the top-level container that manages multiple resources and handles all IPC communication with the Codify CLI. In most cases, you'll create a single plugin instance that bundles all your related resources together.
const plugin = Plugin.create('plugin-name', [
new Resource1(),
new Resource2(),
new Resource3(),
]);
runPlugin(plugin);When runPlugin() is called, it starts an IPC message loop that listens for commands from the parent Codify CLI process. The plugin handles requests like:
initialize- Send resource definitions to CLIvalidate- Validate user configurationsplan- Generate change setsapply- Execute changesimport- Discover existing resources on the system
You rarely need to interact with the Plugin class directly—it's automatically managed by the framework.
Resource
A Resource represents a single type of manageable system entity. Examples include:
- Applications: Docker, VS Code, Chrome
- CLI Tools: Git, Homebrew, Node.js
- Settings: Git config, SSH keys, shell aliases
- Package Managers: Homebrew, apt, npm packages
Every resource extends the Resource<T> abstract class and must implement these lifecycle methods:
Required Methods
-
getSettings()- Define resource configuration including its unique ID, JSON schema for validation, supported operating systems, parameter settings, and dependencies on other resources. -
refresh(parameters, context)- Query the current state of the resource on the system. This is called during the planning phase to determine what changes are needed. Returnnullif the resource doesn't exist, or an object with the current parameter values if it does. -
create(plan)- Install or create the resource based on the desired configuration in the plan. This is called during the apply phase when the resource doesn't currently exist. -
destroy(plan)- Remove or uninstall the resource from the system. Only called in stateful mode when a user removes the resource from their configuration.
Optional Methods
-
modify(parameterChange, plan)- Update specific parameters of an existing resource. If not implemented, Codify will calldestroy()thencreate()to make changes (a "recreate" operation). -
initialize()- Perform one-time setup when the plugin starts. Useful for checking prerequisites or loading configuration. -
validate(parameters)- Add custom validation logic beyond JSON schema validation. Throw an error if validation fails.
Plan
A Plan is similar to Terraform's execution plans—it represents the exact changes that will be made to transform the current system state into the desired state defined in the user's configuration.
Plans are calculated during the planning phase by comparing:
- Desired State: What the user declared in their
codify.jsonc - Current State: What
refresh()returned about the actual system
Each plan contains:
Resource-Level Operation:
CREATE- Resource doesn't exist, will be createdDESTROY- Resource exists, will be removedMODIFY- Resource exists, some parameters will changeRECREATE- Resource will be destroyed and recreatedNOOP- No changes needed
Parameter-Level Changes (for MODIFY operations): Each parameter can have its own operation:
ADD- Parameter doesn't exist, will be addedREMOVE- Parameter exists, will be removedMODIFY- Parameter value will changeNOOP- Parameter unchanged
For example, if a user has Homebrew installed with ["git", "node"] and changes their config to ["git", "python"], the plan would be:
- Resource Operation:
MODIFY - Parameter Changes:
formulae:MODIFY- Add:
["python"] - Remove:
["node"] - Keep:
["git"]
- Add:
Stateless vs Stateful Modes
One of Codify's most important concepts is the distinction between stateless and stateful mode. This determines how Codify tracks and manages resources over time.
Stateless Mode (Default)
In stateless mode, Codify treats configurations as declarations rather than state tracking. This means:
-
Only declared items are managed: If a user declares two shell aliases in their config, Codify only manages those two aliases. Any other aliases on the system are ignored.
-
No automatic cleanup: When a user removes something from their config, Codify doesn't delete it from the system. The config is purely declarative—"ensure these items exist."
-
Idempotent: Running
codify applymultiple times with the same config produces the same result. -
Safer for shared resources: If multiple tools or users modify the same resource (like shell PATH), Codify won't accidentally remove items it didn't create.
Use stateless mode when:
- Resources are shared with other tools
- You want declarative "ensure exists" behavior
- Cleanup is dangerous or not desired
Example: Shell aliases in stateless mode
{
"type": "aliases",
"aliases": [
{ "alias": "ll", "value": "ls -la" }
]
}Codify ensures the ll alias exists. If the user has 20 other aliases, Codify ignores them. If the user removes this config, the alias remains on the system.
Stateful Mode
In stateful mode, Codify tracks complete resource state over time by maintaining a state file (similar to Terraform). This means:
-
Full state tracking: Codify tracks every change made to the resource.
-
Automatic cleanup: When a user removes something from their config, Codify destroys it on the next apply.
-
Change detection: Codify can detect drift—changes made outside of Codify.
-
Granular control: Track additions, removals, and modifications separately.
Use stateful mode when:
- You have exclusive control over the resource
- Cleanup is important (e.g., installed packages)
- You want Terraform-like state management
Example: Homebrew formulae in stateful mode
{
"type": "homebrew",
"formulae": ["git", "node"]
}Codify tracks these formulae. If the user removes "node" from the config, Codify will uninstall it on the next apply. If someone manually installs jq outside of Codify, it shows up as drift in the plan.
Configuring Modes
Mode is typically controlled by the resource implementation. For array parameters, you can use filterInStatelessMode to configure stateless behavior:
parameterSettings: {
aliases: {
type: 'array',
filterInStatelessMode: (desired, current) =>
current.filter(c => desired.some(d => d.alias === c.alias))
}
}This filters current state to only include aliases that match the user's declarations, implementing stateless behavior.
Next Steps
- Resource Lifecycle - Detailed guide to implementing lifecycle methods
- Resource Patterns - Learn about different resource architectures