Resource lifecycle
Implement the resource lifecycle methods
Resource Lifecycle Methods
getSettings()
The getSettings() method is the first thing Codify calls when initializing your plugin. It returns a ResourceSettings object that defines everything Codify needs to know about your resource.
This method is called once during plugin initialization and the settings are cached. It should be pure and deterministic—always return the same settings for the same resource.
getSettings(): ResourceSettings<MyConfig> {
return {
// Required: Unique identifier for this resource type
// This becomes the "type" field users use in their configs
id: 'my-resource',
// Required: Which operating systems this resource supports
// Options: 'darwin' (macOS), 'linux', 'win32' (Windows)
operatingSystems: ['darwin', 'linux'],
// Required: JSON Schema or Zod schema for validation
// Validates user input before any operations are performed
schema: { /* JSON Schema or Zod */ },
// Optional: Specific Linux distributions supported
// Only checked when operatingSystems includes 'linux'
linuxDistros: ['ubuntu', 'debian', 'fedora'],
// Optional: Other resources this resource depends on
// Codify ensures dependencies are applied first
dependencies: ['homebrew', 'git'],
// Optional: Allow multiple instances of this resource
// See "Resource Patterns" section for details
allowMultiple: true,
// Optional: Whether this resource can be destroyed
// Set to false for critical system resources
canDestroy: true,
// Optional: Mark resource as sensitive
// Prevents auto-import and hides values in output
isSensitive: false,
// Optional: Per-parameter configuration
// Controls how individual parameters behave
parameterSettings: {
myParam: {
canModify: true, // Can be changed without recreating
type: 'directory', // Parameter type hint
isSensitive: false, // Hide this parameter in plans
default: 'value', // Default value if not provided
// ... more settings (see Parameter Settings section)
}
}
};
}Common Configuration Patterns
Simple resource with no special behavior:
getSettings(): ResourceSettings<MyConfig> {
return {
id: 'simple-tool',
operatingSystems: ['darwin', 'linux'],
schema: MySchema,
};
}Resource with dependencies:
getSettings(): ResourceSettings<GitRepositoryConfig> {
return {
id: 'git-repository',
operatingSystems: ['darwin', 'linux'],
schema: GitRepoSchema,
// Ensure SSH and git are set up before cloning repositories
dependencies: ['ssh-key', 'git'],
};
}Sensitive resource (e.g., API keys):
getSettings(): ResourceSettings<AwsProfileConfig> {
return {
id: 'aws-profile',
operatingSystems: ['darwin', 'linux'],
schema: AwsSchema,
isSensitive: true, // Prevents auto-discovery and import
parameterSettings: {
awsSecretAccessKey: {
isSensitive: true, // Hides value in plan output
}
}
};
}refresh()
The refresh() method is called during the planning phase to query the current state of the resource on the system. This is one of the most important methods in your resource—it tells Codify what currently exists so it can calculate what changes are needed.
When it's called:
- During
codify planto generate the change set - During
codify applybefore execution - During
codify importto discover existing resources
What to return:
null- Resource doesn't exist on the system{}- Resource exists but has no trackable parametersPartial<MyConfig>- Resource exists with these parameter values
Important: Only query parameters that are passed in the parameters argument. Don't query all possible parameters—Codify tells you what it cares about based on the user's config.
async refresh(parameters: Partial<MyConfig>): Promise<Partial<MyConfig> | null> {
const pty = getPty();
// Use spawnSafe() to avoid throwing on errors
const result = await pty.spawnSafe('check-if-installed');
if (result.status === 'error') {
return null; // Resource doesn't exist
}
// Parse the output and return current state
return {
version: parseVersion(result.data),
path: parsePath(result.data)
};
}Real-World Example: Homebrew Resource
async refresh(parameters: Partial<HomebrewConfig>): Promise<Partial<HomebrewConfig> | null> {
const pty = getPty();
// Check if Homebrew is installed
const homebrewInfo = await pty.spawnSafe('brew config');
if (homebrewInfo.status === SpawnStatus.ERROR) {
return null; // Homebrew not installed
}
const result: Partial<HomebrewConfig> = {}
// Only query directory if user specified it in their config
if (parameters.directory) {
result.directory = this.getCurrentLocation(homebrewInfo.data);
}
// Stateful parameters (formulae, casks) are handled automatically
// by StatefulParameter classes - no need to query them here
return result;
}Refresh for Resources with allowMultiple
When your resource allows multiple instances (like multiple git repositories), refresh() can return an array:
async refresh(parameters: Partial<GitRepositoryConfig>): Promise<Partial<GitRepositoryConfig> | null> {
const pty = getPty();
if (parameters.parentDirectory) {
// Find all git repos in parent directory
const { data } = await pty.spawnSafe(
`find "${parameters.parentDirectory}" -maxdepth 2 -type d -name .git`
);
const gitDirs = data?.split(/\n/)?.filter(Boolean) ?? [];
if (gitDirs.length === 0) {
return null;
}
// Query each repository's remote URL
const repositories: string[] = [];
for (const gitDir of gitDirs) {
const repoPath = path.dirname(gitDir);
const { data: url } = await pty.spawnSafe(
'git config --get remote.origin.url',
{ cwd: repoPath }
);
if (url) repositories.push(url.trim());
}
return {
parentDirectory: parameters.parentDirectory,
repositories,
};
}
// Single repository case
if (parameters.directory) {
const exists = await fileExists(parameters.directory);
if (!exists) return null;
const { data: url } = await pty.spawn(
'git config --get remote.origin.url',
{ cwd: parameters.directory }
);
return {
directory: parameters.directory,
repository: url.trim(),
};
}
throw new Error('Either directory or parentDirectory must be supplied');
}Best Practices for refresh()
-
Return
nullfor non-existent resources - This is how Codify knows to create the resource. -
Query only requested parameters - Check the
parametersargument to see what the user configured. Don't query everything. -
Handle errors gracefully - If a command fails, return
nullrather than throwing (unless it's an unexpected error). -
Be efficient - This method is called during every plan operation. Avoid expensive operations if possible.
-
Parse output carefully - System commands can return different formats. Use robust parsing logic.
create()
The create() method is called during the apply phase when Codify needs to install or configure a resource that doesn't currently exist on the system. This is where you execute the actual system commands to make the desired state a reality.
When it's called:
- During
codify applywhen the resource operation isCREATE - After the user has approved the plan
- In a sequential PTY context (commands run one at a time, in order)
What you receive:
plan.desiredConfig- The complete configuration the user wantsplan.isStateful- Whether this is a stateful operationplan.id- Unique identifier for this specific resource instance
async create(plan: CreatePlan<MyConfig>): Promise<void> {
const pty = getPty();
const config = plan.desiredConfig;
// Install the tool with the specified version
await pty.spawn(`install-tool --version ${config.version}`);
// Configure it if needed
if (config.enableFeature) {
await pty.spawn(`tool-config --enable ${config.enableFeature}`);
}
}Real-World Example: Homebrew Installation
async create(plan: CreatePlan<HomebrewConfig>): Promise<void> {
const pty = getPty();
// Install Homebrew in custom directory if specified
if (plan.desiredConfig.directory) {
return this.installBrewInCustomDir(plan.desiredConfig.directory);
}
// Standard installation using official script
await pty.spawn(
'/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"',
{
stdin: true, // Allow interactive input
env: { NONINTERACTIVE: 1 } // But run non-interactively
}
);
// Add Homebrew to shell PATH
const brewPath = Utils.isLinux()
? '/home/linuxbrew/.linuxbrew/bin/brew'
: '/opt/homebrew/bin/brew';
await FileUtils.addToShellRc(`eval "$(${brewPath} shellenv)"`);
}Handling Stateful Parameters
If your resource has stateful parameters (like Homebrew's formulae), the framework automatically calls their add() methods after create() completes:
// In create(), just install the base resource
async create(plan: CreatePlan<HomebrewConfig>): Promise<void> {
await this.installHomebrew();
// Don't install formulae here - the FormulaeParameter.add() handles that
}destroy()
The destroy() method removes a resource from the system. This is only called in stateful mode when a user removes the resource from their configuration.
When it's called:
- During
codify applywhen the resource operation isDESTROY - Only in stateful mode (never in stateless mode)
- After user approval of the plan
Important: Be very careful in destroy() implementations. You're deleting user data and system state. Consider:
- Can this operation be safely reversed?
- Should you prompt for additional confirmation?
- Should you refuse to destroy if the resource has uncommitted changes?
async destroy(plan: DestroyPlan<MyConfig>): Promise<void> {
const pty = getPty();
const config = plan.currentConfig;
// Uninstall the tool
await pty.spawn(`uninstall-tool ${config.path}`);
// Clean up configuration files
await fs.rm(config.configPath, { recursive: true });
}Real-World Example: Git Repository Resource
The git-repository resource refuses to delete directories because it could destroy user work:
async destroy(plan: DestroyPlan<GitRepositoryConfig>): Promise<void> {
// Never automatically delete a git repository - too dangerous!
throw new Error(
`The git-repository resource doesn't automatically delete repositories. ` +
`Please delete ${plan.currentConfig.directory} manually and re-apply.`
);
}This is a good pattern for resources that manage user data—force manual intervention rather than risking data loss.
Handling Stateful Parameters
If removeStatefulParametersBeforeDestroy is true in your resource settings, stateful parameters' remove() methods are called before destroy():
getSettings(): ResourceSettings<HomebrewConfig> {
return {
id: 'homebrew',
removeStatefulParametersBeforeDestroy: true, // Uninstall formulae first
// ...
};
}
async destroy(plan: DestroyPlan<HomebrewConfig>): Promise<void> {
// All formulae have been uninstalled already
// Now remove Homebrew itself
await pty.spawn('brew cleanup');
await pty.spawn('rm -rf /opt/homebrew', { requiresRoot: true });
}modify()
The modify() method updates specific parameters of an existing resource without recreating it. This is optional—if you don't implement it, Codify will use a RECREATE operation (destroy then create) instead.
Implementing modify() is important for resources where recreation would be:
- Slow (downloading large files, compiling code)
- Destructive (losing data or state)
- Disruptive (restarting services)
When it's called:
- During
codify applywhen the resource operation isMODIFY - Once per parameter that changed
- Only for parameters where
canModify: truein parameter settings
What you receive:
parameterChange.name- Which parameter changedparameterChange.operation- ADD, REMOVE, or MODIFYparameterChange.newValue- The new valueparameterChange.previousValue- The old valueplan- Full plan context with desired and current configs
async modify(
parameterChange: ParameterChange<MyConfig>,
plan: ModifyPlan<MyConfig>
): Promise<void> {
const pty = getPty();
// Only handle version changes - other parameters require recreation
if (parameterChange.name === 'version') {
await pty.spawn(`update-tool --to ${plan.desiredConfig.version}`);
}
// For parameters we can't modify, do nothing
// Codify will use RECREATE instead
}Real-World Example: Alias Resource
The alias resource modifies the alias value by finding and replacing the line in the shell RC file:
async modify(
pc: ParameterChange<AliasConfig>,
plan: ModifyPlan<AliasConfig>
): Promise<void> {
// Only the value can be modified; changing alias name requires recreate
if (pc.name !== 'value') {
return;
}
const { alias, value } = plan.currentConfig;
// Find which file contains this alias
const aliasInfo = await this.findAlias(alias, value);
if (!aliasInfo) {
throw new Error(
`Unable to find alias: ${alias}. ` +
`Please delete it manually and re-run Codify.`
);
}
// Read file, find the line, replace it, write back
const lines = aliasInfo.contents.split('\n');
const aliasString = `alias ${alias}='${value}'`;
const aliasLineNum = lines.findIndex(l => l.trim() === aliasString);
if (aliasLineNum === -1) {
throw new Error(`Cannot find line in ${aliasInfo.path}`);
}
const newAlias = `alias ${plan.desiredConfig.alias}='${plan.desiredConfig.value}'`;
lines.splice(aliasLineNum, 1, newAlias);
await fs.writeFile(aliasInfo.path, lines.join('\n'), 'utf8');
}When NOT to Implement modify()
Don't implement modify() if:
- Recreation is fast and safe
- Parameters are deeply coupled (changing one requires changing others)
- Modification is complex and error-prone
Let Codify use RECREATE instead—it's simpler and more reliable.