Codify
Plugin Development

Quick start

Build your first Codify plugin in minutes

Installation

First, install the plugin SDK in your Node.js project:

npm install @codifycli/plugin-core @codifycli/schemas

The @codifycli/plugin-core package provides the base classes and utilities you'll use to build your plugin, while @codifycli/schemas contains TypeScript types and validation schemas for IPC communication.

Minimal Example

Let's build a simple plugin that manages git global configuration. This example demonstrates all the core concepts you'll need to understand plugin development.

The Problem

Users often need to configure git with their name and email. Traditionally, this requires running:

git config --global user.name "John Doe"
git config --global user.email "john@example.com"

With Codify, users should be able to declare this in their codify.jsonc:

{
  "type": "git-config",
  "userName": "John Doe",
  "userEmail": "john@example.com"
}

Here's the complete plugin implementation:

import { Resource, ResourceSettings, Plugin, runPlugin, getPty } from '@codifycli/plugin-core';
import { StringIndexedObject } from '@codifycli/schemas';

interface GitConfig extends StringIndexedObject {
  userName?: string;
  userEmail?: string;
}

class GitConfigResource extends Resource<GitConfig> {
  getSettings(): ResourceSettings<GitConfig> {
    return {
      id: 'git-config',
      operatingSystems: ['darwin', 'linux'],
      schema: {
        type: 'object',
        properties: {
          userName: { type: 'string' },
          userEmail: { type: 'string' }
        }
      }
    };
  }

  async refresh(parameters: Partial<GitConfig>) {
    const pty = getPty();
    const nameResult = await pty.spawnSafe('git config --global user.name');
    const emailResult = await pty.spawnSafe('git config --global user.email');

    return {
      userName: nameResult.status === 'success' ? nameResult.data.trim() : undefined,
      userEmail: emailResult.status === 'success' ? emailResult.data.trim() : undefined
    };
  }

  async create(plan) {
    const pty = getPty();
    const config = plan.desiredConfig;

    if (config.userName) {
      await pty.spawn(`git config --global user.name "${config.userName}"`);
    }
    if (config.userEmail) {
      await pty.spawn(`git config --global user.email "${config.userEmail}"`);
    }
  }

  async destroy(plan) {
    const pty = getPty();
    await pty.spawn('git config --global --unset user.name');
    await pty.spawn('git config --global --unset user.email');
  }
}

const plugin = Plugin.create('my-plugin', [new GitConfigResource()]);
runPlugin(plugin);

Understanding the Example

Let's break down each part of this plugin:

1. Configuration Interface

interface GitConfig extends StringIndexedObject {
  userName?: string;
  userEmail?: string;
}

This TypeScript interface defines the shape of your resource's configuration. It must extend StringIndexedObject to ensure compatibility with Codify's type system. All properties are optional because users might configure only one property.

2. Resource Settings

getSettings(): ResourceSettings<GitConfig> {
  return {
    id: 'git-config',                      // The "type" field users use in configs
    operatingSystems: ['darwin', 'linux'], // macOS and Linux only
    schema: { /* JSON Schema */ }          // Validates user input
  };
}

The getSettings() method tells Codify everything it needs to know about your resource: its unique identifier, which operating systems it supports, and how to validate user configurations.

3. Refresh (Query Current State)

async refresh(parameters: Partial<GitConfig>) {
  const pty = getPty();
  const nameResult = await pty.spawnSafe('git config --global user.name');
  // ...
  return {
    userName: nameResult.status === 'success' ? nameResult.data.trim() : undefined,
    userEmail: emailResult.status === 'success' ? emailResult.data.trim() : undefined
  };
}

The refresh() method queries the current system state. It uses spawnSafe() to run git commands without throwing errors if the config doesn't exist. Returning undefined for properties means they're not currently set.

4. Create (Install/Configure)

async create(plan) {
  const pty = getPty();
  const config = plan.desiredConfig;

  if (config.userName) {
    await pty.spawn(`git config --global user.name "${config.userName}"`);
  }
  // ...
}

The create() method receives a plan containing the desired configuration and executes the commands to achieve that state. Using spawn() (not spawnSafe()) means errors will be thrown and handled by the framework.

5. Destroy (Remove/Uninstall)

async destroy(plan) {
  const pty = getPty();
  await pty.spawn('git config --global --unset user.name');
  // ...
}

The destroy() method removes the resource from the system. In stateful mode, this gets called when a user removes the resource from their config.

6. Plugin Registration

const plugin = Plugin.create('my-plugin', [new GitConfigResource()]);
runPlugin(plugin);

Finally, we create a plugin instance with our resource and call runPlugin() to start the IPC message loop. The plugin process will now respond to requests from the Codify CLI.

Using Your Plugin

Users can now use your resource in their codify.jsonc:

{
  "type": "git-config",
  "userName": "John Doe",
  "userEmail": "john@example.com"
}

When they run codify apply, Codify will:

  1. Validate the config against your schema
  2. Call refresh() to check current git configuration
  3. Generate a plan showing which properties will be set
  4. Call create() to execute the git config commands

Next Steps

Now that you've built your first plugin, learn more about:

On this page