Codify
Plugin Development

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 CLI
  • validate - Validate user configurations
  • plan - Generate change sets
  • apply - Execute changes
  • import - 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. Return null if 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 call destroy() then create() 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 created
  • DESTROY - Resource exists, will be removed
  • MODIFY - Resource exists, some parameters will change
  • RECREATE - Resource will be destroyed and recreated
  • NOOP - No changes needed

Parameter-Level Changes (for MODIFY operations): Each parameter can have its own operation:

  • ADD - Parameter doesn't exist, will be added
  • REMOVE - Parameter exists, will be removed
  • MODIFY - Parameter value will change
  • NOOP - 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"]

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 apply multiple 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

On this page