# Change log
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
1.0.0 Linux support and multi-resource declarations [#100-linux-support-and-multi-resource-declarations]
* Added comprehensive Linux support across all resources
* New package manager resources: `apt`, `yum`, `dnf`, and `snap`
* Fixed compatibility issues for asdf, homebrew, aws-cli, vscode, git, python, and other resources
* Improved shell compatibility across different Linux distributions
* Multi-resource declaration support to reduce config verbosity
* `aliases` resource now supports declaring multiple aliases in a single config
* `path` resource supports multiple path entries in one declaration
* `git-repository` resource supports cloning multiple repositories in a single config
* New resources
* `npm-login` resource for npm authentication
* `tart` resource for VM/container management on macOS
* Technical improvements
* Migrated to Zod schema validation for better type safety
* Updated to @codifycli scoped packages
* Improved test infrastructure with persistent VMs for faster test execution
0.15.0 Added docker [#0150-added-docker]
* Added `docker` resource to install docker
0.14.1 Added npm and macports support [#0141-added-npm-and-macports-support]
* Added `npm` resource for managing npm global packages
* Added `macports` resource to install macports and install ports
0.14.0 Additional python support [#0140-additional-python-support]
* Added additional support for various python tools
* `venv-project` resource supports creating a virtual environment using `python -m venv`
* `pip` resources allows the programmatic installations of PyPI dependencies via `pip`
* `pip-sync` resource installs `pip-tools` and supports managing Python project dependencies using `pip-sync`
* Fixed bugs in existing resources
* `xcode-tools` did not automatically install the latest `xcode-tools`
* `homebrew` casks and formulae sometimes broke with fully qualified names
* `homebrew` uninstalled formulae and casks after installing them
* `alias` did not parse properly if the aliased value had an equal sign
* The `cwd` was not printed for certain commands.
0.13.0 New tools and verbosity changes [#0130-new-tools-and-verbosity-changes]
* Added new resources to support `virtualenv`, `virtualenv-project`, `pnpm`, and `wait-github-ssh-key`.
* `virtualenv` and `virtualenv-project` adds support for Python virtual environments. See [here for more info](/docs/core-resources/python/virtualenv).
* `pnpm` adds support for installing and configuring `pnpm`. See [here for more info](/docs/core-resources/javascript/pnpm).
* `wait-github-ssh-key` is a new resource that makes creating new Github SSH keys easier. This resource will display a holding banner until
a user has added their SSH key to Github. See [here for more info](/docs/core-resources/git/wait-github-ssh-key).
* Added verbosity level support. At the default verbosity, commands will only print the command that was run but not the output. This cleans up the
terminal output and prevents sensitive data from being printed.
* Various changes to support the new `codify init` command.
**BREAKING CHANGES:**
* The `git-clone` resource was re-named to `git-repository`.
0.12.0 Import improvements [#0120-import-improvements]
* Improved results returned by importing. Parameters are transformed back to a more human-readable format and default values are removed.
* Improved path, homebrew, jenv import results
0.11.0 Parallelization and massive performance gains on plans [#0110-parallelization-and-massive-performance-gains-on-plans]
* Reworked internal architecture and switched to using a pty terminal.
* Switched to a lightweight model of running commands and piping results back to avoid
interactive shell start-up costs and process overheads.
* Parallelized all plan commands
* Bug fixes:
* Asdf plan was not returning the right results
* Brew casks parsing incorrect
0.10.0 Scripting capabilities [#0100-scripting-capabilities]
* Added the `action` and `file` resources
* `action` can perform any scripting `action` if the parameter `condition` is met
* `file` can create and maintain files
0.9.0 Big stability improvements [#090-big-stability-improvements]
* Added double the amount of test coverage for all resources and fixed a lot of bugs
along the way
* Fixed ANSI escape character problems for certain terminals
* List of fixes:
* **git-lfs** Added missing `git lfs install`
* **path** Fixed paths not parsing and saving correctly
* **alias** Fixed alias not parsing and saving correctly
* **git-clone** Fixed parent directory not working
* **aws-profile** Fixed region and output parameters
* **homebrew** Improved casks parameter to ignore any existing programs.
This fixes an app already exists error thrown by brew. This behavior can be disabled using `skipAlreadyInstalledCasks: false`
* **ssh-config** Improvements to internal structure
* **terraform** Fixed error caused by ANSI escape characters
0.8.0 Added ssh resources [#080-added-ssh-resources]
* Adds resources for ssh key generation, ssh config setup and the ssh-add command.
* Resource `ssh-key`:
* Generates a new ssh key if it doesn't exist
* This resource support customizing the encryption type, comments, the file name,
and customizing the number of encryption bits
* Resource `ssh-config`:
* Sets up and maintains a user's `~/.ssh/config` file. Almost all config options are supported.
* Resource `ssh-add`:
* Manages a user's ssh agent via the `ssh-add` command.
* It has support for the macOS specific `--apple-use-keychain`
* This resource is buggy in CLI versions below 0.3.1 when using the standard reporter.
Please use CLI version 0.3.1 or greater.
0.7.0 Importing [#070-importing]
* Added importing. The import command allows users to automatically generate
Codify configs based on the tools and programs already installed on their system.
Just type `codify import [type2] [type3]....` Upgrade to codify cli >0.3.0 to
use the import command.
* Added filtering for resource array parameters in stateless mode.
* Path resource fixes
**BREAKING CHANGES:**
* Removed the `remote` parameter from `git-clone`. Use the existing repository
parameter instead. This was done to simplify the logic since remote was only
an alias of repository. This parameter may be added back in the future if
alias support is added.
0.6.0 Asdf is now here [#060-asdf-is-now-here]
* Added asdf resources including `asdf`, `asdf-plugin`, `asdf-local`, `asdf-global` and
`asdf-install`. See [the asdf resources page](/docs/core-resources/asdf/asdf) for more
info.
* Fixes for nvm resource.
* Fixes for vscode resource.
* Fixes for the pyenv resource.
* Debugging improvements to improve developer productivity.
0.5.0 Major refactoring and improvements [#050-major-refactoring-and-improvements]
* Internal improvements and maintainability improvements.
* Major code refactors enabling faster development in the future.
* Performance improvements.
* Adds the framework for stateful mode in the future.
0.4.0 Android studios [#040-android-studios]
* Adds a Android Studios resource.
* The Android Studios resource supports fetching available versions directly from Google and allows
the installation of previous Android Studio versions.
* It also supports beta and preview versions of Android Studios.
0.3.0 Git git git [#030-git-git-git]
* Adds a git resource.
* The resource configures the global git email and username
0.2.0 Java and jenv [#020-java-and-jenv]
* Adds a jenv resource.
* The jenv resource allows the installation and configuration of jenv
* Additionally the jenv resource adds the functionality to directly install LTS versions of the JDK. This
is additional functionality on top of what jenv by default provides. Requires homebrew to be installed.
0.1.0 Alias and git clone [#010-alias-and-git-clone]
* Adds an alias and git clone resource.
0.0.0 Initial release [#000-initial-release]
* Initial release of the core plugin;
* Adds support for aws-cli, aws-profile, git-lfs, homebrew, nvm, path, pgcli, pyenv, terraform, vscode and xcode-tools;
* Adds sudo handling.
1.0.0 Dashboard integration and cloud file support [#100-dashboard-integration-and-cloud-file-support]
* Added dashboard integration with `codify login`, `codify logout`, and `codify edit` commands
* Authenticate with the Codify dashboard directly from the CLI
* Edit configurations in the browser-based editor
* Connect local CLI to your cloud account
* Added cloud file support to load configurations from remote sources
* Use `codify apply` and `codify plan` with cloud-hosted files
* Share configurations across teams without local file dependencies
* New `codify refresh` command to refresh project state
* New `codify test` command to test configurations before applying
* Full Linux support with OS-specific parameter handling
* Cross-platform shell compatibility improvements
* Fixed sudo command execution across different shells
* Security improvements
* Support for sensitive parameters (these parameters are skipped in init and improts)
* Bug fixes
* Improved environment variable handling with sudo
* Enhanced progress display and error handling
0.8.0 JSONC and JSON5 [#080-jsonc-and-json5]
* Adds support for the jsonc and json5 file types which allow commenting.
* Jsonc is now the recommended file format for Codify.
* Json5 is preferred if your editor supports it since it has looser syntax, multi-line strings, trailing commas, and
allows keys to remain un-quoted. This makes it less verbose and easier to write by hand.
0.7.1 Add verbosity support [#071-add-verbosity-support]
* In version 0.7.0, the output was made less verbose.
* Version 0.7.1 gives the option to increase verbosity using the --debug flag.
0.7.0 New init command! [#070-new-init-command]
* A new command `init` was added to make it easier than ever to get started with Codify.
* Unlike the `import` command no input is needed from the user. The `init` command will automatically try to import
as many resources as possible.
* Destroy command improvements! Now the destroy command will ask for additional information to be able to destroy
resources previous that were not supported.
* Verbosity changes. Now the output will only report the command that was run but not the output. This makes the
output much more readable. Use the `--verbose` option to see the full output as well.
* Improved help descriptions for all commands.
* Cleaned up unused arguments and parameters for all commands.
0.6.0 Import overhaul! [#060-import-overhaul]
* The import system was overhauled to improve the first time experience using Codify and to make it easier to keep Codify files updated without active management.
* Use the import command with no arguments to update an existing codify file with new changes on the system. No additional prompting is required.
* Import results can now be smartly written to existing files following existing formatting.
* Importing now support '\*' and '?' matching to make it easier to import multiple resource types at the same time.
* Updated import interface with easier to use UI.
* Improved all resources to better support importing and return more accurate results.
0.5.1 Bug fix for import output [#051-bug-fix-for-import-output]
* Fixed the import command to generate the correct output without the surrounding JSON.
0.5.0 Massive plan performance uplifts (7x to 9x) [#050-massive-plan-performance-uplifts-7x-to-9x]
* Reworked internal architecture and switched to using a pty terminal.
* Switched to a lightweight model of running commands and piping results back to avoid
interactive shell start-up costs and process overheads.
* Parallelized all plan commands
* Improved plugin versioning + storage on the system.
* Bug fixes:
* Fixed the `--path` flag for all commands
0.4.0 Updates, updates, updates [#040-updates-updates-updates]
* Added `codify update` command to perform automated updates
* Bug fixes for multiple named configs and destroy
* Internal refactoring
* Moved plugin cache to `~/.codify`
0.3.1 TTY fixes [#031-tty-fixes]
* Bug fix for tty messages not showing up (for example SSH passphrase prompts). Now
all tty messages will be shown to the user and not hidden.
* Updated spinner style.
0.3.0 Importing is now live [#030-importing-is-now-live]
* Added importing. The import command allows users to automatically generate Codify
configs based on the tools and programs already installed on their system.
Just type `codify import [type2] [type3]....`
Type1, type2, type3 represent the type ids of the resources to be imported
such as homebrew, nvm, jenv, etc...
* Added UI to allow imports to specify the required parameters to configure
the import. For example when importing an alias, the name of the
alias needs to be passed in.
* Import will return a config of the current state of the resource on
the system. If the resource is not found then an empty array will be returned.
* Import is supported in all output modes (default, plain, debug)
**BREAKING CHANGES:**
* Re-named uninstall command to destroy to match the name of the operation.
The name destroy makes more sense in the context of creating and destroying
resources which encompass more than tools and programs that can be installed
or uninstalled.
0.2.0 Performance improvements [#020-performance-improvements]
* Build improvements, reduced binary size and performance improvements.
* Removed unnecessary files from the final installer.
0.1.0 Error source mapping [#010-error-source-mapping]
* Adds improved error handling.
* Adds line numbers and code fragments for validation error messages to make it easier
to pinpoint where an error is
* Adds support for YAML file parsing
0.0.0 Initial release! [#000-initial-release-1]
* Initial release of Codify.
* Support `codify apply` and `codify plan` commands
* Adds plugin parsing and codify.jsonc parsing.
* Adds plugin initialization, dependency resolution, and apply order sorting
* Adds UI for displaying plans
# Cloud DevOps
Configuration [#configuration]
```json title="codify.jsonc"
[
{
"type": "homebrew",
"formulae": ["docker"]
},
{ "type": "terraform" },
{ "type": "aws-cli" },
{
"type": "aws-profile",
"profile": "default",
"csvCredentials": "TODO add path to csv credentials file"
}
]
```
What Gets Installed [#what-gets-installed]
This configuration sets up a complete cloud DevOps environment with industry-standard tools:
Docker [#docker]
[Docker](https://www.docker.com) is a containerization platform for running applications separately from infrastructure. It's widely used for creating reproducible development environments and deploying applications.
Terraform [#terraform]
[Terraform](https://www.terraform.io) is an infrastructure-as-code tool for managing cloud environments. It enables reproducible and consistent cloud infrastructure management, allowing you to manage entire AWS environments through code.
AWS CLI [#aws-cli]
The AWS Command Line Interface is the official tool for managing AWS services from the terminal. It provides direct access to AWS services and automation capabilities.
AWS Profile Setup [#aws-profile-setup]
The `aws-profile` resource is a Codify utility that helps configure AWS CLI with your credentials, making it easier to get started with AWS development.
Additional Setup Required [#additional-setup-required]
Before running this configuration:
1. Replace `"TODO add path to csv credentials file"` with the actual path to your AWS credentials CSV file
2. Ensure you have valid AWS credentials (download from AWS IAM console)
Usage [#usage]
After installation, you can:
* Verify Docker: `docker --version`
* Verify Terraform: `terraform --version`
* Verify AWS CLI: `aws --version`
* Test AWS credentials: `aws sts get-caller-identity`
Learn More [#learn-more]
* [Docker Documentation](https://docs.docker.com)
* [Terraform Documentation](https://www.terraform.io/docs)
* [AWS CLI Documentation](https://docs.aws.amazon.com/cli/)
* [Codify AWS CLI Resource](/docs/resources/aws-cli)
* [Codify Terraform Resource](/docs/resources/terraform)
# Complete Company Setup
Configuration [#configuration]
```json title="codify.jsonc"
[
{
"type": "homebrew",
"formulae": ["jq", "openjdk@17", "jenv", "docker"],
"casks": ["openvpn-connect", "sublime-text", "dbeaver-community", "intellij-idea-ce", "1password", "google-chrome"]
},
{
"type": "nvm",
"nodeVersions": ["20"],
"global": "20"
},
{ "type": "vscode" },
{ "type": "aws-cli" },
{ "type": "terraform" },
{ "type": "pgcli" },
{
"type": "pyenv",
"global": "3.12",
"pythonVersions": ["3.12"]
},
{ "type": "git-lfs" },
{ "type": "git-clone", "parentDirectory": "~/projects", "remote": "git@github.com:org/repo1.git" },
{ "type": "git-clone", "parentDirectory": "~/projects", "remote": "git@github.com:org/repo2.git", "dependsOn": ["git-lfs"] },
{ "type": "git-clone", "parentDirectory": "~/projects", "remote": "git@github.com:org/repo3.git" },
{ "type": "git-clone", "parentDirectory": "~/projects", "remote": "git@github.com:org/repo4.git" },
{ "type": "git-clone", "parentDirectory": "~/projects", "remote": "git@github.com:org/repo5.git" },
{ "type": "git-clone", "parentDirectory": "~/projects", "remote": "git@github.com:org/repo6.git" },
{ "type": "path", "path": "$HOME/projects/tools/bin", "dependsOn": ["git-clone.0"] }
]
```
What Gets Installed [#what-gets-installed]
This is a comprehensive setup configuration designed for backend and DevOps developers. It installs a complete development environment including tools, applications, and project repositories.
Development Tools [#development-tools]
* **jq** - Command-line JSON processor
* **Docker** - Container platform
* **Git LFS** - Git Large File Storage
* **pgcli** - PostgreSQL CLI with auto-completion
Programming Languages [#programming-languages]
* **Node.js 20** (via nvm)
* **Python 3.12** (via pyenv)
* **Java 17** (via Homebrew + jenv)
IDEs and Editors [#ides-and-editors]
* **VS Code** - Microsoft's code editor
* **Sublime Text** - Fast text editor
* **IntelliJ IDEA CE** - JetBrains Java IDE
* **DBeaver Community** - Database management tool
Cloud and DevOps [#cloud-and-devops]
* **AWS CLI** - AWS command-line interface
* **Terraform** - Infrastructure as code tool
Applications [#applications]
* **OpenVPN Connect** - VPN client
* **1Password** - Password manager
* **Google Chrome** - Web browser
Project Repositories [#project-repositories]
The configuration clones multiple project repositories into `~/projects` and adds custom tooling to your PATH.
Prerequisites [#prerequisites]
Before running this configuration:
1. **Set up SSH keys for GitHub** - Follow the [GitHub SSH setup recipe](/docs/recipes/github-ssh)
2. **Update repository URLs** - Replace `git@github.com:org/repo1.git` with your actual repository URLs
3. **Verify repository access** - Ensure your SSH key has access to all repositories
Customization [#customization]
This configuration can be customized for your organization:
* **Add/remove tools**: Modify the `formulae` and `casks` arrays in the Homebrew resource
* **Change versions**: Update version numbers for Node.js, Python, or Java
* **Update repositories**: Replace the git-clone resources with your team's repositories
* **Adjust paths**: Modify the `parentDirectory` for repositories or the custom PATH location
Usage Notes [#usage-notes]
* Some repositories depend on git-lfs being installed first (note the `dependsOn` field)
* The PATH modification depends on the first repository being cloned (referenced as `git-clone.0`)
* After installation, open a new terminal or source your shell configuration to use the new tools
Real-World Example [#real-world-example]
This is a redacted version of a configuration used for setting up developer laptops at a startup, demonstrating how Codify can standardize onboarding and environment setup across an entire engineering team.
Learn More [#learn-more]
* [Codify Homebrew Resource](/docs/resources/homebrew)
* [Codify Git Resources](/docs/resources/git)
* [Codify PATH Resource](/docs/resources/path)
# GitHub SSH Setup
Configuration [#configuration]
```json title="codify.jsonc"
[
{
"type": "ssh-key",
"passphrase": "ReplacePassphraseHere"
},
{
"type": "ssh-config",
"hosts": [{
"Host": "github.com",
"AddKeysToAgent": true,
"UseKeychain": true,
"IdentityFile": "~/.ssh/id_ed25519",
"IgnoreUnknown": "UseKeychain"
}]
},
{
"type": "ssh-add",
"path": "~/.ssh/id_ed25519",
"appleUseKeychain": true
}
]
```
What Gets Configured [#what-gets-configured]
This configuration completes the SSH setup for [GitHub](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent), allowing you to push and pull changes without repeatedly entering your credentials.
The configuration will:
* Generate a new SSH key (if it doesn't already exist)
* Automatically load the key to the ssh-agent
* Save the passphrase to the Apple Keychain (on macOS)
* Configure SSH to use the key for GitHub connections
Additional Setup Required [#additional-setup-required]
To complete the GitHub SSH setup:
1. Run `codify apply` to execute the configuration
2. Copy the contents of your public key:
```bash
cat ~/.ssh/id_ed25519.pub
```
3. Add the public key to GitHub:
* Go to [GitHub SSH Keys Settings](https://github.com/settings/keys)
* Click "New SSH key"
* Paste the public key content
* Save the key
After completing these steps, git operations (clone, pull, push) over SSH should work seamlessly.
Learn More [#learn-more]
* [GitHub SSH Documentation](https://docs.github.com/en/authentication/connecting-to-github-with-ssh)
* [Codify SSH Resources](/docs/resources/ssh)
# Overview
Welcome to the Codify template collection! These are pre-configured `codify.jsonc` setups designed to help you quickly set up common development environments and workflows.
What Are Templates? [#what-are-templates]
Templates are complete, ready-to-use Codify configurations that demonstrate best practices for setting up specific development environments or tools. Each template includes:
* A complete `codify.jsonc` configuration
* Detailed explanation of what gets installed
* Any additional setup steps required
* Links to relevant documentation
How to Use Templates [#how-to-use-templates]
1. Copy the configuration from any template into your `codify.jsonc` file
2. Modify any values as needed (such as version numbers or paths)
3. Run `codify apply` to execute the configuration
4. Follow any additional setup instructions provided in the template
Available Templates [#available-templates]
Browse our collection of templates for common development scenarios:
* **[Node.js](/docs/recipes/nodejs)** - Set up Node.js development with nvm
* **[GitHub SSH](/docs/recipes/github-ssh)** - Configure SSH authentication for GitHub
* **[Python](/docs/recipes/python)** - Set up Python development with pyenv
* **[Java](/docs/recipes/java)** - Configure Java development with jenv
* **[Cloud DevOps](/docs/recipes/cloud-devops)** - Install AWS, Terraform, and Docker
* **[Company Setup](/docs/recipes/company-setup)** - Complete environment setup for backend and DevOps teams
Contributing Templates [#contributing-templates]
Have a useful Codify configuration you'd like to share? Visit our [support page](https://codifycli.com/support/) to submit your recipe ideas.
# Java Development
Configuration [#configuration]
```json title="codify.jsonc"
[
{ "type": "homebrew" },
{
"type": "jenv",
"global": "21",
"add": [
"21"
]
}
]
```
What Gets Installed [#what-gets-installed]
This configuration installs [jenv](https://www.jenv.be) (Java Environment Manager), a popular tool for managing multiple Java versions on your system. Unlike nvm or pyenv, jenv cannot install Java versions by itself—it only manages versions that are already installed.
Codify's core plugin extends jenv's functionality by automatically installing Java versions via Homebrew when needed.
This configuration will:
* Install Homebrew (if not already present)
* Install jenv
* Install Java 21 via Homebrew
* Configure jenv to manage Java 21
* Set Java 21 as the system default
Usage [#usage]
After installation, you can:
* Check the Java version: `java -version`
* Switch Java versions: `jenv global ` or `jenv local `
* Install additional versions: Add them to the `add` array
* Change the default version: Update the `global` property
Learn More [#learn-more]
* [jenv Documentation](https://www.jenv.be)
* [Codify jenv Resource](/docs/resources/java/jenv)
* [Codify Homebrew Resource](/docs/resources/homebrew)
# Node.js Development
Configuration [#configuration]
```json title="codify.jsonc"
[
{
"type": "nvm",
"nodeVersions": [
"20"
],
"global": "20"
}
]
```
What Gets Installed [#what-gets-installed]
This configuration installs [nvm](https://github.com/nvm-sh/nvm) (Node Version Manager), a popular tool for downloading, installing, and managing Node.js versions. Using nvm, multiple versions of Node.js can be installed simultaneously and switched between using the `nvm use` command.
This configuration will:
* Install nvm if not already present
* Install Node.js version 20
* Set Node.js 20 as the default system version
Usage [#usage]
After installation, you can:
* Check the Node.js version: `node --version`
* Switch Node.js versions: `nvm use `
* Install additional versions: Add them to the `nodeVersions` array
* Change the default version: Update the `global` property
Learn More [#learn-more]
* [nvm Documentation](https://github.com/nvm-sh/nvm)
* [Codify nvm Resource](/docs/resources/javascript/nvm)
# Python Development
Configuration [#configuration]
```json title="codify.jsonc"
[
{
"type": "pyenv",
"global": "3.12",
"pythonVersions": [
"3.12"
]
}
]
```
What Gets Installed [#what-gets-installed]
This configuration installs [pyenv](https://github.com/pyenv/pyenv) (Python Version Manager), a popular tool for downloading, managing, and installing Python versions. Similar to nvm for Node.js, pyenv allows multiple Python versions to be installed simultaneously and switched between easily.
This configuration will:
* Install pyenv if not already present
* Install Python 3.12
* Set Python 3.12 as the default system version
Usage [#usage]
After installation, you can:
* Check the Python version: `python --version`
* Switch Python versions: `pyenv global ` or `pyenv local `
* Install additional versions: Add them to the `pythonVersions` array
* Change the default version: Update the `global` property
Learn More [#learn-more]
* [pyenv Documentation](https://github.com/pyenv/pyenv)
* [Codify pyenv Resource](/docs/resources/python/pyenv)
# Quick Start
import { Callout } from 'fumadocs-ui/components/callout';
There are three ways to get started: use the **Web App** for the fastest setup with no download, use the **Desktop App** for regular use with the full feature set, or use the **CLI** if you prefer working in the terminal.
***
Start in the Web App [#start-in-the-web-app]
No download required. Go to [dashboard.codifycli.com](https://dashboard.codifycli.com) and sign in.
1. Start a new chat with the Codify Agent.
2. Describe what you want to set up. For example:
> *"Set up my machine for Node.js development with VS Code"*
3. The agent generates a configuration based on your description.
4. Review the configuration and click **Apply**. Codify shows you the plan before making any changes.
***
Use the Desktop App [#use-the-desktop-app]
[Download and install the Desktop App](/docs/installation), then:
1. Open the app and start a new chat with the Codify Agent.
2. Describe what you want to set up. For example:
> *"Set up my machine for Node.js development with VS Code"*
3. The agent generates a configuration based on your description.
4. Review the configuration and click **Apply**. Codify shows you the plan before making any changes.
[Learn more about the Codify Agent →](/docs/user-guide/agent)
***
Your First Configuration [#your-first-configuration]
Prefer the terminal? [Install Codify](/docs/installation), create a `codify.jsonc` file, and run `codify apply`.
```json title="codify.jsonc"
[
{
"type": "nvm",
"nodeVersions": ["20"],
"global": "20"
},
{ "type": "vscode" }
]
```
Codify generates a plan showing exactly what it will install. Review it and select "Yes" to apply.
```bash title="codify"
➜ ~ codify apply
...
╭───────────────────────────────────────╮
│Codify Plan │
╰───────────────────────────────────────╯
The following actions will be performed:
+ vscode will be created
{
"directory": "/Applications"
}
+ nvm will be created
{
"nodeVersions": [
"20"
],
"global": "20"
}
Do you want to apply the above changes?
❯ Yes
No
```
See the [codify.jsonc documentation](/docs/user-guide/configuration-language) for the full config reference, or explore the [nvm](/docs/resources/javascript/nvm) and [vscode](/docs/resources/vscode) resource options.
***
# Overview
import { Cards, Card } from 'fumadocs-ui/components/card';
Welcome to Codify, a declarative configuration-as-code ecosystem for managing your development environment. Define your installations and settings once and apply them to any Mac, Linux, or Windows machine.
Codify consists of a [configuration language](/docs/user-guide/configuration-language), a [web](https://dashboard.codifycli.com) and [desktop](/docs/installation) app, a [growing library](/docs/resources) of 50+ supported tools and applications, an [execution engine](/docs/user-guide/cli), and an [AI agent](/docs/user-guide/agent).
Key Features [#key-features]
* **Declarative**: Define your desired environment state in the codify config language (fully JSON), and Codify determines what changes are needed to achieve it.
* **Plan and Apply Workflow**: Run `codify plan` to preview changes before execution, then `codify apply` to apply them.
* **Flexible and Stateless**: Manage only what you want. Codify works alongside manually installed tools without requiring you to import everything into configuration.
* **Manage your existing system**: Import existing system configurations with `codify import`, or apply configurations to new machines. Share your complete setup with teammates in a single file.
Desktop App & CLI [#desktop-app--cli]
**The [Desktop App](/docs/installation#desktop-app) is the recommended way to use Codify.** It includes a visual config editor with auto-complete and validation, one-click apply, and the built-in Codify Agent. Available for macOS, Linux, and Windows.
The **CLI** is best if you prefer the terminal or need to automate Codify in scripts and CI workflows. Use commands like `codify apply`, `codify plan`, and `codify init` to manage your environment directly.
The **[Codify Agent](/docs/user-guide/agent)** lets you describe your environment in plain English. Unlike general AI tools that respond with raw shell commands (which can misconfigure your system and are hard to undo) the Codify Agent uses a library of human-validated resources, so changes are pre-tested and consistent.
Supported Packages [#supported-packages]
Codify supports a wide range of tools and applications. See [the default plugin](/docs/resources) for
a list of all supported resources. Anyone can kick off an [AI process](https://github.com/codifycli/default-plugin/issues/new?template=resource-request.yml) to create a new resource for Codify. We then code review and test the resource manually before merging it in (10 business days turnaround time).
**Included Resources**:
* [Homebrew](/docs/resources/homebrew)
* [Xcode Tools](/docs/resources/xcode-tools)
* [AWS CLI](/docs/resources/aws-cli)
* [Node.js (nvm)](/docs/resources/javascript/nvm)
* [Python (pyenv)](/docs/resources/python/pyenv)
* [Path](/docs/resources/path) and [Alias](/docs/core-resources/alias)
* Git ([git-lfs](/docs/resources/git/git-lfs), [git repository](/docs/core-resources/git/git-repository))
* And many more!
# Installation
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
import { Callout } from 'fumadocs-ui/components/callout';
import { Cards, Card } from 'fumadocs-ui/components/card';
import { OsAwareTabs } from '@/components/OsAwareTabs';
Desktop App [#desktop-app]
Open the downloaded `.dmg` file and drag Codify to your Applications folder.
AppImage is the recommended installation method as it works on most Linux distributions without requiring root privileges.
1. Download the AppImage for your architecture above.
2. Make it executable:
```sh title="terminal"
chmod +x Codify_*.AppImage
```
3. Run it:
```sh title="terminal"
./Codify_*.AppImage
```
Some distributions require `libfuse2` to run AppImages. If you see a FUSE-related error, install it first:
* **Debian / Ubuntu**: `sudo apt install libfuse2`
* **Fedora**: `sudo dnf install fuse-libs`
* **Arch**: `sudo pacman -S fuse2`
1. Download the `.deb` package for your architecture above.
2. Install it:
```sh title="terminal"
sudo apt install ./Codify_*.deb
```
3. Launch Codify from your application menu or run:
```sh title="terminal"
codify-desktop
```
If the installation fails or is broken, run the following steps and then retry:
```sh title="terminal"
sudo apt update
sudo apt install -f
```
`sudo apt update` refreshes the package list. `sudo apt install -f` fixes any broken dependencies.
1. Download the `.rpm` package for your architecture above.
2. Install it:
**Fedora:**
```sh title="terminal"
sudo dnf install Codify_*.rpm
```
**RHEL / CentOS:**
```sh title="terminal"
sudo rpm -i Codify_*.rpm
```
3. Launch Codify from your application menu or run:
```sh title="terminal"
codify-desktop
```
The Windows installer is not yet code-signed. Windows SmartScreen will show a warning when you run the installer. Click **"More info"** then **"Run anyway"** to proceed.
Run the downloaded installer and follow the on-screen instructions.
Codify runs inside **WSL (Windows Subsystem for Linux)**. The app will guide you through selecting a WSL distro and installing the Codify CLI into it on first launch.
***
CLI [#cli]
```sh title="terminal"
/bin/bash -c "$(curl -fsSL https://releases.codifycli.com/install.sh)"
```
This command-line installation method works for both arm64 and x64 architectures and supports most POSIX-compliant shells on macOS, Linux, and Windows (WSL).
The installation script will request sudo privileges to complete the installation.
If you have Node.js installed, you can install Codify globally from [npm](https://www.npmjs.com/package/codify):
```sh title="terminal"
npm install -g codify
```
This puts the `codify` command on your PATH, the same as the install script.
To run Codify without installing it, use `npx`:
```sh title="terminal"
npx codify [command]
```
For example, to apply a config:
```sh title="terminal"
npx codify apply
```
This is handy for trying Codify out or for running it in CI.
**macOS**:
**Linux**:
For a portable installation, download the tarball for Codify and extract it. You will need to manually add
the executable (`$TARBALL_LOCATION/bin/codify.sh`) to your PATH to run Codify from anywhere on your system.
**macOS**:
Download the pre-packaged installer, run it, and follow the on-screen instructions to install Codify.
Verify Installation [#verify-installation]
If the installation was successful, you should see output similar to this:
```sh title="terminal"
> codify --version
codify/0.7.0 darwin-arm64 node-v20.15.0
```
Uninstalling Codify [#uninstalling-codify]
To fully uninstall Codify. Run the following command:
```sh title="terminal"
sudo /bin/bash -c "$(curl -fsSL https://releases.codifycli.com/uninstall.sh)"
```
***
IDE support [#ide-support]
Enable auto-completion and syntax validation in your favourite editor / IDE for `codify.jsonc` files.
Auto-completion and validation for `codify.jsonc` files work automatically without additional setup.
This applies to all JetBrains-based IDEs including IntelliJ IDEA, WebStorm, PhpStorm, PyCharm, CLion, Android Studio, and others.
JSON auto-completion in JetBrains IDEs has several unresolved bugs
([see here](https://intellij-support.jetbrains.com/hc/en-us/community/posts/360007665720-JSON-Schema-autocompletion-if-then-else-array-of-objects-properties)
and [here](https://youtrack.jetbrains.com/issue/IJPL-63615/JSON-properties-from-subschemas-applied-conditionally-are-not-suggested-in-completion))
which may cause additional properties to be suggested even when they're not valid for the configuration.
The validation system should catch these and highlight them as errors.
To enable auto-completion in VS Code:
* For JSON support, install the [JSON Schema Store Catalog](https://marketplace.visualstudio.com/items?itemName=remcohaszing.schemastore) extension
* For YAML support, install the [YAML](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml) extension
To enable auto-completion in Sublime Text:
* For JSON support, install [LSP-json](https://packagecontrol.io/packages/LSP-json)
* For YAML support, install [LSP-yaml](https://packagecontrol.io/packages/LSP-yaml)
**Instructions**:
1. Open the command palette and run `Package Control: Install Package`, then select `LSP`
2. Install `LSP-yaml` or `LSP-json` from Package Control
3. Restart Sublime Text
To enable auto-completion in Emacs:
Install [eglot](https://github.com/joaotavora/eglot).
To enable auto-completion in Neovim:
Install [SchemaStore.nvim](https://github.com/b0o/SchemaStore.nvim)
For other editors and custom use cases, the JSON schema for Codify and
the core plugin can be found here: [https://raw.githubusercontent.com/codifycli/codify-schemas/main/src/schemastore/codify-schema.json](https://raw.githubusercontent.com/codifycli/codify-schemas/main/src/schemastore/codify-schema.json)
Load this schema in any JSON Schema-compatible editor to enable auto-completion and validation.
This file is regularly updated with new resources and changes.
# apply
```sh title="terminal"
codify apply
[--json]
[--debug]
[-o plain|default|debug|json]
[-s]
[-p ]
```
Apply a `codify.jsonc` file. Codify apply will first generate a plan of the changes needed to meet
the desired config. The user will then be asked to confirm the plan before apply makes changes on the system.
By default, apply operates in stateless mode which means that resources can only be created and
modified but not destroyed. Existing packages do not need to be imported into Codify in stateless
mode and no state files are generated. Use `codify destroy` to destroy resources in stateless mode.
Options [#options]
**Path:** *--path -p*
Specify the path for the `codify.jsonc` file. This parameter accepts both relative and absolute paths.
**Output mode:** *--output -o*
Specify the CLI output mode. Options: \[default, plain, json]. The output mode controls what gets
printed to the console.
**Debug output mode:** *--debug*
A flag to enable debug mode. In debug mode, the debug output will be turned on and the DEBUG=codify flag
will be passed to all plugins. Additional and verbose logging will be printed. Useful for debugging
any issues.
**Secure mode:** *--secure -s*
Activates secure mode which sandboxes all plugins and prevents them from using sudo unless explictly
permitted by the user. This is done by launching the plugins as a separate processes without TTY access.
Secure mode is automatically turned on if an unverified plugin is used.
Examples [#examples]
```sh title="terminal"
codify apply
codify apply -p ~/Downloads --output plain -s
codify apply --debug
```
# connect
```sh title="terminal"
codify connect
```
Establish a secure connection between your local Codify CLI and your [Codify cloud](https://dashboard.codifycli.com) account.
Once connected, you can trigger actions such as `apply`, `plan`, and `import` directly from the Codify web dashboard
Overview [#overview]
`codify connect` generates a one-time connection code that links your local environment to Codify Cloud.
This allows the Codify dashboard to communicate with your CLI instance and execute commands remotely
such as `plan`, `apply`, `refresh` and `import`.
Usage [#usage]
* When you run the command, Codify generates a connection code.
* The CLI attempts to open your browser and automatically save the code to Codify Cloud.
* If this step fails for any reason, you can manually save the code by clicking the **Connect** button in the dashboard.
* If you are not logged in, a login page will be opened first.
* Once complete, both the CLI and the dashboard will confirm that the connection was successful.
For more information about Codify Cloud
# destroy
```sh title="terminal"
codify destroy [type...]
[--json]
[--debug]
[-o plain|default|debug|json]
[-s]
```
Use `codify destroy` to destroy (uninstall) resources on the system. `codify destroy` performs
a plan in stateful mode with the desired state being null. This generates a plan that destroys all of
the specified resource types (ids) if they exist on the system. A plan of the changes and a
confirmation is shown before any changes are applied onto the system.
This command was previously named `codify uninstall` in `0.2.0`.
Arguments [#arguments]
**\**: The type id of the resources to destroy. Multiple ids separated by spaces
can be specified.
Options [#options]
**Output mode:** *--output -o*
Specify the CLI output mode. Options: \[default, plain, json]. The output mode controls what gets
printed to the console.
**Debug output mode:** *--debug*
A flag to enable debug mode. In debug mode, the debug output will be turned on and the DEBUG=codify flag
will be passed to all plugins. Additional and verbose logging will be printed. Useful for debugging
any issues.
**Secure mode:** *--secure -s*
Activates secure mode which sandboxes all plugins and prevents them from using sudo unless explictly
permitted by the user. This is done by launching the plugins as a separate processes without TTY access.
Secure mode is automatically turned on if an unverified plugin is used.
Examples [#examples]
```sh title="terminal"
codify destroy homebrew nvm vscode
codify destroy asdf pgcli --output plain -s
codify destroy jenv --debug
```
# edit
```sh title="terminal"
codify edit
```
Launches the [Codify editor](https://dashboard.codifycli.com). By default, this will open your default
document in the Codify editor.
This command automatically establishes a secure connection with your Codify editor
(similar to [`codify connect`](/docs/commands/connect)) so that `apply`, `plan`, `refresh` and `import`
commands can be directly used from the editor.
Overview [#overview]
`codify edit` performs two actions:
1. **Establishes a connection with Codify Cloud.**
The command creates a connection with your Codify editor allowing you to
`apply`, `plan`, and `import` directly from the editor.
2. **Opens your default document in the Codify editor.**
# import
```sh title="terminal"
codify import [type...]
[--json]
[--debug]
[-o plain|default|debug|json]
[-s]
[-p ]
```
Use `codify import` to generate new resource configs for programs and settings currently installed on your system that
aren't yet defined in your `codify.jsonc` file.
The import command scans your system and creates Codify configuration blocks for the tools and applications you
specify, making it easy to bring existing installations under Codify management without having to write configs by hand.
Use Cases [#use-cases]
* **Getting started**: Quickly generate configs for your existing setup instead of writing them manually. Import specific resources like `codify import homebrew nvm` or scan for everything with `codify import`.
* **Adding new resources**: You've been managing some tools outside of Codify and want to add them to your configuration file.
* **Discovering what's supported**: Run import to see which of your installed tools Codify can manage.
How it Works [#how-it-works]
With Type Arguments [#with-type-arguments]
When you specify resource types, Codify will:
1. Scan your system for those specific resources
2. Prompt you for any required identifying information (for multi-instance resources)
3. Generate configuration blocks for the found resources
4. Show you a diff if updating an existing file
5. Write the configs to your `codify.jsonc` file
Without Type Arguments (Auto-Discovery) [#without-type-arguments-auto-discovery]
When you run `codify import` with no arguments, it functions like `codify init`:
1. Automatically scans your system for all supported resources
2. Generates configs for everything it finds without requiring user input
3. If a `codify.jsonc` file already exists, adds only the resources that aren't already defined
4. If no file exists, creates a new one with all discovered resources
This makes `codify import` perfect for both getting started and incrementally adding new resources to existing configurations.
Multiple Instances [#multiple-instances]
For resources that can have multiple instances on the system (like `git-clone`, `path`, or `alias`), Codify will prompt you for identifying information to import the specific instance you want.
For example, with `git-clone`, you'll be prompted for the path of the directory to import. The prompt includes shortcut keys to remove an import or add additional ones.
Diff Preview [#diff-preview]
If you have an existing `codify.jsonc` file, Codify will show you a diff of the new resources that will be added. The import preserves your existing file's formatting (spacing, indentation, structure) when inserting new configs.
You can choose to:
* Update your existing file with the new resources
* Write to a new file instead
* Cancel the operation
Import vs Refresh [#import-vs-refresh]
Understanding the difference between these two commands:
* **`codify import`**: Generates new resource configs for tools not yet in your `codify.jsonc` file
* **`codify refresh`**: Updates resources already in your `codify.jsonc` file with changes from your system
**Example scenario:**
* Your `codify.jsonc` contains a homebrew resource
* You want to add your `nvm` setup which isn't in your config yet
* Run `codify import nvm` → Adds a new nvm resource block to your file
* Later, you manually change your global Node version
* Run `codify refresh` → Updates the existing nvm resource with the new global version
Arguments [#arguments]
[#type]
The type id of the resources to import. Multiple ids separated by spaces can be specified. Wildcards `*` and `?` can be used to match multiple types at once.
**Special behavior:**
* **With types specified**: Imports only those specific resource types
* **No types specified**: Auto-discovers and imports all supported resources (like `codify init`)
**Examples:**
```bash
codify import homebrew nvm vscode # Import specific resources
codify import asdf* # Import all resources starting with 'asdf'
codify import * # Import all supported resources
codify import # Auto-discover and import everything
```
Options [#options]
Path: —path -p [#path-path--p]
Specify the path for the `codify.jsonc` file. This parameter accepts both relative and absolute paths.
**Example:**
```bash
codify import nvm -p ~/projects/my-config/codify.jsonc
```
Output mode: —output -o [#output-mode-output--o]
Specify the CLI output mode. Options: `[default, plain, json]`. The output mode controls what gets printed to the console.
**Example:**
```bash
codify import homebrew -o json
```
Debug output mode: —debug [#debug-output-mode-debug]
A flag to enable debug mode. In debug mode, the debug output will be turned on and the `DEBUG=codify` flag will be passed to all plugins. Additional and verbose logging will be printed. Useful for debugging any issues.
**Example:**
```bash
codify import vscode --debug
```
Examples [#examples]
Auto-discover everything (quick start) [#auto-discover-everything-quick-start]
Let Codify find and import all supported resources automatically:
```bash
codify import
```
This will scan your system and import everything it can find, perfect for getting started quickly or adding multiple new resources at once.
Import specific resources [#import-specific-resources]
Import only the tools you specify:
```bash
codify import homebrew nvm vscode
```
Import with wildcards [#import-with-wildcards]
Import all resources matching a pattern:
```bash
codify import asdf* # Import asdf, asdf-java, asdf-python, etc.
codify import git* # Import git, git-lfs, git-clone, etc.
```
Import all supported resources [#import-all-supported-resources]
Explicitly import everything Codify supports:
```bash
codify import *
```
Import to custom location [#import-to-custom-location]
Import resources and save to a specific file:
```bash
codify import nvm pyenv -p ~/team-config/codify.jsonc
```
Import with JSON output [#import-with-json-output]
Get machine-readable output for automation:
```bash
codify import homebrew -o json
```
Common Workflows [#common-workflows]
Workflow 1: Getting Started with Codify [#workflow-1-getting-started-with-codify]
```bash
# First time using Codify? Let it discover your setup automatically
codify import
# Review the generated config
cat ~/codify.jsonc
# Apply it to verify everything works
codify apply
```
Workflow 2: Incremental Adoption [#workflow-2-incremental-adoption]
```bash
# You already have a codify.jsonc with homebrew
# Now you want to add your Node.js setup
codify import nvm
# Later, add Python
codify import pyenv
# Your config grows incrementally as you adopt Codify
```
Workflow 3: Selective Import [#workflow-3-selective-import]
```bash
# Import only version managers
codify import nvm pyenv jenv rbenv
# Import all git-related resources
codify import git*
```
Workflow 4: Project Setup [#workflow-4-project-setup]
```bash
# Starting a new project, import project-specific tools
cd ~/projects/my-app
codify import nvm vscode git-clone -p ./codify.jsonc
# Commit to version control
git add codify.jsonc
git commit -m "Add Codify configuration"
```
Workflow 5: Team Onboarding [#workflow-5-team-onboarding]
```bash
# New team member clones repo with codify.jsonc
# They have some tools installed but not all
# Import missing tools automatically
codify import
# Then apply to install anything missing
codify apply
```
Tips [#tips]
* Use `codify import` with no arguments for the fastest way to get started or add multiple new resources at once
* Import only adds new resources—it won't modify resources already in your config (use `codify refresh` for that)
* Wildcards are powerful for importing related resources: `codify import asdf*` imports all asdf-related resources
* The import command never changes what's installed on your system—it only generates/updates your config file
* After importing, always run `codify apply` to verify the imported config matches your system state
* Combine `codify import` and `codify refresh` in your workflow: import new resources, refresh existing ones
Related Commands [#related-commands]
* **`codify init`** - Interactive wizard for first-time setup (prompts you to select which resources to import)
* **`codify refresh`** - Update existing resources in your config with system changes
* **`codify apply`** - Apply your configuration to install or update resources on your system
* **`codify plan`** - See what changes would be made without applying them
# init
```sh title="terminal"
codify init
[--debug]
[-o plain|default|json]
```
The `init` command is a helper to quickly get started using Codify.
Use this command to quickly generate a starter `codify.jsonc` based on
what is currently installed on the system. By default, the new file
will be written to `~/codify.jsonc`.
[//]: # ""
Options [#options]
**Output mode:** *--output -o*
Specify the CLI output mode. Options: \[default, plain, json]. The output mode controls what gets
printed to the console.
**Debug output mode:** *--debug*
A flag to enable debug mode. In debug mode, the debug output will be turned on and the DEBUG=codify flag
will be passed to all plugins. Additional and verbose logging will be printed. Useful for debugging
any issues.
User Flow: [#user-flow]
When using the init command, the user is first greeted with a banner introducing Codify. Note that the
init command will do some scanning of the files on the system to determine which resources to import. No telemetry
is collected through Codify and everything stays local to the system.
```
█▀▀ █▀█ █▀▄ █ █▀▀ █▄█
█▄▄ █▄█ █▄▀ █ █▀ █
Codify is a configuration-as-code tool that helps you setup and manage your system.
Use this init flow to get started quickly with Codify.
Codify will scan your system for any supported programs or settings and automatically generate configs for you.
❯ Continue
```
After accepting, the init command will scan the system for possible installations, settings and cli tools to import.
This process is automatically and no user input is required. Only resources currently supported by Codify will have
configs generated.
The user will then be prompted to specify which resources to omit from importing. Press the enter key to continue.
```
Select the resources to import:
❯ ◉ android-studio
◉ aws-cli
◉ aws-profile
◉ git
◉ git-lfs
◉ git-repository
◉ homebrew
◉ jenv
◉ pgcli
Use to select and to submit.
Use to select all items and to de-select all items.
```
The final step is choosing a location to store the newly generated `codify.jsonc` file.
```
Where to save the new Codify configs? (leave blank for ~/codify.jsonc)
~/codify.jsonc
```
And then viola, a starter config file has been generated and ready to use.
# login
```sh title="terminal"
codify login
```
Authenticate your local Codify CLI with your Codify account.
This allows the CLI to access your cloud-hosted configuration files and
perform actions such as `apply`, `plan`, and `import` directly against projects stored the editor.
Overview [#overview]
`codify login` opens a browser window where you can sign in to your Codify account.
Once authenticated, an access token will be saved on your system that lets it interact with your cloud workspace.
After completing the login flow, all standard Codify operations can also operate on cloud documents.
Notes [#notes]
* Use the [`codify logout`](/docs/commands/logout) command to logout
* Use [`codify connect`](/docs/commands/connect) to establish a secure connection to your Codify editor. This allows the
editor to run `apply`, `plan`, `import`, `refresh` commands directly from the browser.
# logout
```sh title="terminal"
codify logout
```
Sign out of your Codify account.
# plan
```sh title="terminal"
codify plan
[--json]
[--debug]
[-o plain|default|debug|json]
[-s]
[-p ]
```
Use `codify plan` to generate a plan from the config but not apply them. `codify plan` is mainly used to
check that the correct plan is generated from a config.
By default, plan operates in stateless mode, which means resources will only show
up in the plan as `create` and `modify` but not `destroy`.
Existing packages do not need to be imported into Codify in stateless mode and no
state files are generated. This makes Codify easy to integrate into existing setups.
Options [#options]
**Path:** *--path -p*
Specify the path for the `codify.jsonc` file. This parameter accepts both relative and absolute paths.
**Output mode:** *--output -o*
Specify the CLI output mode. Options: \[default, plain, json]. The output mode controls what gets
printed to the console.
**Debug output mode:** *--debug*
A flag to enable debug mode. In debug mode, the debug output will be turned on and the DEBUG=codify flag
will be passed to all plugins. Additional and verbose logging will be printed. Useful for debugging
any issues.
**Secure mode:** *--secure -s*
Activates secure mode which sandboxes all plugins and prevents them from using sudo unless explictly
permitted by the user. This is done by launching the plugins as a separate processes without TTY access.
Secure mode is automatically turned on if an unverified plugin is used.
Examples [#examples]
```sh title="terminal"
codify plan
codify plan -p ~/Downloads --output plain -s
codify plan --debug
```
# refresh
```sh title="terminal"
codify refresh [type...]
[--json]
[--debug]
[-o plain|default|debug|json]
[-s]
[-p ]
```
Use `codify refresh` to reconcile your existing `codify.jsonc` file with changes made directly on your system outside of Codify.
The refresh command scans your system for changes to resources already defined in your `codify.jsonc` file and updates the
file to reflect the current state. This is useful when you've manually installed packages, changed configurations,
or made other modifications without using Codify.
Use Cases [#use-cases]
* **Syncing manual changes**: You installed additional homebrew formulae or changed your global Node version
manually and want to update your `codify.jsonc` to reflect these changes.
* **Keeping configs up to date**: Team members made changes to their setup outside of Codify and need to
update their configuration files to stay in sync.
* **Drift detection**: You want to ensure your `codify.jsonc` accurately represents what's currently installed on your system.
How it Works [#how-it-works]
When you run `codify refresh` in a directory containing a `codify.jsonc` file:
1. Codify reads your existing configuration file
2. For each resource defined in the file, it scans your system to detect the current state
3. It calculates a diff showing what has changed
4. You're prompted to review and apply the changes to your `codify.jsonc` file
The command will preserve your file's existing formatting, spacing, and structure while inserting the updates.
Diff Preview [#diff-preview]
Before making any changes, Codify shows you a diff of what will be updated in your configuration file.
You can review these changes and choose whether to proceed.
Refresh vs Import [#refresh-vs-import]
Understanding the difference between these two commands:
* **`codify refresh`**: Updates resources that already exist in your `codify.jsonc` file with new changes from your system
* **`codify import`**: Generates new resource configs for tools and settings not yet defined in your `codify.jsonc` file
**Example scenario:**
* Your `codify.jsonc` contains a homebrew resource with `["jq", "git"]`
* You manually install `docker` via homebrew
* Run `codify refresh` → Updates the existing homebrew resource to include `"docker"`
* You want to add your `nvm` setup which isn't in your config yet
* Run `codify import nvm` → Adds a new nvm resource block to your file
Arguments [#arguments]
[#type]
The type id of the resources to refresh. Multiple ids separated by spaces can be specified. Wildcards `*` and `?` can be used to match multiple types at once.
**Special behavior:**
* **With types specified**: Refresh only those specific resource types
* **No types specified**: Refreshes the entire file
**Examples:**
```bash
codify refresh homebrew nvm vscode # Refresh specific resources
codify refresh asdf* # Refresh all resources starting with 'asdf'
codify refresh # Refresh all resources in the file
```
Options [#options]
**Path:** *--path -p*
Specify the path for the `codify.jsonc` file. This parameter accepts both relative and absolute paths.
**Output mode:** *--output -o*
Specify the CLI output mode. Options: \[default, plain, json]. The output mode controls what gets
printed to the console.
**Debug output mode:** *--debug*
A flag to enable debug mode. In debug mode, the debug output will be turned on and the DEBUG=codify flag
will be passed to all plugins. Additional and verbose logging will be printed. Useful for debugging
any issues.
Examples [#examples]
Basic refresh [#basic-refresh]
Update your existing config with system changes:
```bash
codify refresh
```
Refresh with custom path [#refresh-with-custom-path]
Refresh a config file in a different location:
```bash
codify refresh -p ~/team-configs/codify.jsonc
```
Refresh with JSON output [#refresh-with-json-output]
Get machine-readable output for automation:
```bash
codify refresh -o json
```
Debug mode refresh [#debug-mode-refresh]
Troubleshoot issues with verbose logging:
```bash
codify refresh --debug
```
Common Workflows [#common-workflows]
Workflow 1: After Manual Installation [#workflow-1-after-manual-installation]
```bash
# You manually installed packages outside of Codify
brew install wget
npm install -g typescript
# Update your codify.jsonc to reflect these changes
codify refresh
# Review the diff and apply changes
```
Workflow 2: Configuration Audit [#workflow-2-configuration-audit]
```bash
# Periodically check if your config matches reality
codify refresh
# If changes are detected, review and decide whether to:
# 1. Apply them to your codify.jsonc (keep the manual changes)
# 2. Run codify apply to revert to your declared config (undo manual changes)
```
Tips [#tips]
* Run `codify refresh` regularly to keep your configuration file in sync with your system
* Combine with `codify import ` to add new resources and refresh existing ones in your workflow
* The refresh command only modifies your `codify.jsonc` file—it doesn't change anything on your system
# test
```sh title="terminal"
codify test
[--debug]
[-o plain|default|debug|json]
[-S ]
[--operatingSystem macOS|linux]
```
The `test` command allows you to test your Codify configuration in an isolated virtual machine environment before applying it to your local system. This is particularly useful for validating complex configurations, testing cross-platform compatibility, or experimenting with new resources without affecting your actual machine.
Codify uses [Tart](https://github.com/cirruslabs/tart) VMs to create an isolated testing environment. The command automatically:
1. Validates your configuration file
2. Spins up a clean VM (macOS or Linux)
3. Mounts your Codify configuration with live-reload support
4. Opens a terminal in the VM to run `codify apply`
Changes to your local `codify.jsonc` file are automatically synced to the VM in real-time, allowing you to iterate quickly on your configuration.
Prerequisites [#prerequisites]
The `test` command requires:
* **macOS host machine** (currently the only supported host platform)
* **At least 25GB of free disk space** for the VM image installation
If [Tart](https://github.com/cirruslabs/tart) is not installed or the required base VM image (`codify-test-vm-macos` or `codify-test-vm-linux`) is missing, the `test` command will automatically install them for you.
Tart and the VM image requires approximately 25GB of disk space. Ensure you have sufficient storage available before running the test command.
Options [#options]
**Operating System:** *--operatingSystem -o*
Specify the operating system to use for the test VM. Options: `macOS` or `linux`. Defaults to your current host operating system.
**Debug output mode:** *--debug*
Enable debug mode for verbose logging. The `DEBUG=codify` flag will be passed to all plugins for detailed troubleshooting output.
How It Works [#how-it-works]
1. **Validation Phase**: Your configuration is validated locally before the VM starts to catch any syntax or schema errors early.
2. **VM Creation**: A temporary VM clone is created from the base image. The VM is automatically deleted when you exit.
3. **Bind Mounting**: Your Codify installation and configuration directory are mounted into the VM as shared volumes, enabling live changes without copying files.
4. **Live Sync**: A file watcher monitors your local configuration file. Any changes are automatically reflected in the VM, allowing you to test modifications in real-time.
5. **Interactive Testing**: A terminal opens in the VM where you can run `codify apply` and observe the results in the isolated environment.
Use Cases [#use-cases]
**Test before you apply**: Validate that your configuration works as expected without risking your local machine setup.
**Cross-platform testing**: Test Linux configurations from a macOS host, or vice versa, to ensure your config works across different operating systems.
**Experiment safely**: Try out new resources, plugins, or configurations in a disposable environment that can be easily reset.
**Team validation**: Test shared team configurations before distributing them to ensure they work on clean systems.
Examples [#examples]
```sh title="terminal"
# Test on your current OS
codify test
# Test Linux configuration from macOS host
codify test --operatingSystem linux
# Test with debug output
codify test --debug
```
Workflow Example [#workflow-example]
```sh title="terminal"
# Start testing your configuration
codify test
# The VM starts and opens a terminal
# In the VM terminal, run:
codify apply
# Edit your local codify.jsonc file
# Changes sync automatically to the VM
# Run codify apply again in the VM to test the changes
# Exit the VM terminal when done
# The VM is automatically deleted
```
Notes [#notes]
* The VM runs in the foreground. You must manually exit the VM GUI/terminal to stop the test and delete the VM.
* The base VM images (`codify-test-vm-macos` and `codify-test-vm-linux`) must be set up beforehand using Tart.
* Configuration files are mounted read-only to prevent accidental modifications from within the VM.
* Each test creates a unique VM clone to ensure test isolation.
# update
```sh title="terminal"
codify update [CHANNEL] [--force | [-a | -v | -i]] [--verbose]
```
Use `codify update` to update the Codify CLI to the latest version or switch between release channels.
The update command checks for new versions of Codify and downloads them automatically.
Update Channels [#update-channels]
Codify supports multiple release channels, similar to how Google Chrome manages different release tracks. This allows you to choose between stable, tested releases or early access to new features.
Available channels:
* **stable** (default) - Production-ready releases, thoroughly tested
* **beta** - Pre-release versions with new features, may contain bugs
By default, you'll be on the `stable` channel.
Arguments [#arguments]
[CHANNEL] [#channel]
Optional. The release channel to update to or stay on.
**Examples:**
```bash
codify update stable # Switch to/update on stable channel
codify update beta # Switch to/update on beta channel
```
If no channel is specified, Codify updates to the latest version on your current channel.
Flags [#flags]
Available versions: --available -a [#available-versions---available--a]
Display a list of all available versions without installing anything. Useful for seeing what versions are published.
**Example:**
```bash
codify update --available
```
**Output:**
```
Available versions:
0.8.0 (stable)
0.7.5 (stable)
0.7.4 (stable)
0.8.1-beta (beta)
0.8.0-beta (beta)
```
Verbose: --verbose [#verbose---verbose]
Show more detailed information about available versions when combined with `--available`.
**Example:**
```bash
codify update --available --verbose
```
Interactive: --interactive -i [#interactive---interactive--i]
Interactively select which version to install from a list. This provides a menu-driven interface for choosing specific versions.
**Note:** This flag is ignored if a channel is specified.
**Example:**
```bash
codify update --interactive
```
**Output:**
```
? Select version to install:
❯ 0.8.0 (latest)
0.7.5
0.7.4
0.7.3
```
Version: --version -v [#version---version--v]
Install a specific version by providing the version number directly.
**Example:**
```bash
codify update --version 0.7.5
```
Force: --force [#force---force]
Force a re-download of the requested version, even if it's already installed. Useful for repairing a corrupted installation or ensuring you have a clean version.
**Example:**
```bash
codify update --force
```
Examples [#examples]
Update to latest on current channel [#update-to-latest-on-current-channel]
Stay on your current channel and get the latest version:
```bash
codify update
```
Switch to stable channel [#switch-to-stable-channel]
Move to the stable channel and get its latest version:
```bash
codify update stable
```
Switch to beta channel [#switch-to-beta-channel]
Start receiving beta releases:
```bash
codify update beta
```
Install a specific version [#install-a-specific-version]
Downgrade or upgrade to an exact version:
```bash
codify update --version 0.7.5
```
Interactively choose a version [#interactively-choose-a-version]
Browse and select from available versions:
```bash
codify update --interactive
```
See what's available [#see-whats-available]
Check available versions without installing:
```bash
codify update --available
```
See detailed version info [#see-detailed-version-info]
Get comprehensive information about releases:
```bash
codify update --available --verbose
```
Force reinstall current version [#force-reinstall-current-version]
Repair your installation:
```bash
codify update --force
```
Version Verification [#version-verification]
After updating, verify your new version:
```bash
codify --version
```
This should display the newly installed version number.
Update Behavior [#update-behavior]
* **Non-destructive**: Updating Codify doesn't affect your configurations or installed resources
* **Automatic backups**: Your previous version is preserved in case you need to roll back
* **Clean updates**: Each version is installed fresh to avoid conflicts
Troubleshooting [#troubleshooting]
Update fails to download [#update-fails-to-download]
```bash
# Try forcing a fresh download
codify update --force
```
Want to roll back to previous version [#want-to-roll-back-to-previous-version]
```bash
# Install the specific version you had before
codify update --version 0.7.4
```
Tips [#tips]
* Run `codify update` regularly to get the latest features and bug fixes
* Use `--available` before updating to see what's new
* Beta channels are great for testing new features but may be less stable
* The `--force` flag is your friend if an update seems corrupted
* You can always roll back to any previous version using `--version`
* Updates preserve all your configurations and settings—nothing is lost
# validate
```sh title="terminal"
codify validate
[--debug]
[-o plain|default|debug|json]
[-p ]
```
The `validate` command checks your Codify configuration file for errors without making any changes to your system. This is useful for quickly verifying that your configuration is syntactically correct, uses valid resource types, and passes all schema validation rules before running `codify plan` or `codify apply`.
Validation performs several checks:
1. **Syntax validation**: Ensures your JSON/JSONC/JSON5/YAML file is properly formatted
2. **Schema validation**: Verifies all resources match their expected schemas
3. **Type validation**: Confirms all resource types are recognized and supported
4. **OS compatibility**: Checks that resources are compatible with your operating system
5. **Plugin validation**: Runs plugin-specific validation rules for each resource
Unlike `plan` or `apply`, validate does not query your system's current state or calculate what changes would be made. It only validates the configuration file structure and content.
Options [#options]
**Path:** *--path -p*
Specify the path for the configuration file. This parameter accepts both relative and absolute paths. Supports `codify.jsonc`, `codify.json`, `codify.json5`, and `codify.yaml` files.
**Output mode:** *--output -o*
Specify the CLI output mode. Options: \[default, plain, json]. The output mode controls what gets printed to the console.
**Debug output mode:** *--debug*
Enable debug mode for verbose logging. The `DEBUG=codify` flag will be passed to all plugins for detailed troubleshooting output. Useful for debugging validation issues.
Examples [#examples]
```sh title="terminal"
# Validate the codify.jsonc in current directory
codify validate
# Validate a specific configuration file
codify validate -p ~/my-configs/codify.jsonc
# Validate with plain output (useful for scripts)
codify validate --output plain
# Validate with debug output for troubleshooting
codify validate --debug
```
Common Validation Errors [#common-validation-errors]
**Unknown resource type**: The `type` field contains a resource type that isn't recognized by any installed plugin.
**Missing required fields**: A resource is missing required configuration parameters.
**Invalid field values**: A parameter value doesn't match the expected type or format (e.g., string instead of number).
**OS incompatibility**: A resource is not supported on your current operating system.
**Schema violations**: The resource configuration doesn't conform to the JSON schema definition.
# Architecture
An architecture overview to help with developing new plugins.
High level [#high-level]
At a high level the codify loads plugins from the registry which contains resources. Resources do 2 main
functions, they get the current status of the system and they apply changes.
Refresh [#refresh]
Plugins are required to implement the `async refresh(parameters: Partial): Promise | null>`
method of the plugin library. Within this method, plugins are expected to pull the current config of the
system based on the desired parameters of the user that are passed in. The refresh method returns a config
of the current status of the system, that is to say if the returned config were to be applied no changes
would result. Codify will then calculate a change set that is needed to turn the current config generated
by `refresh()` to the desired config supplied by the user.
Plan [#plan]
A collection of change sets across multiple resources is called a plan. A plan consists of change sets which are
in turn composed by one resource operation and a list of parameter operations. A resource operation can be one
of: `create, destroy, re-create, modify or no-op`. This controls tells the resource to create, destroy, re-create or
modify the resource. The resource level operation is calculated using a couple of factors:
1. Does the current config exist? If null is returned by the refresh then the `create` operation is returned.
2. Based on resource parameters. If the resource exists then Codify will use the parameter operations to
determine the resource operation. By default, any parameter changes will cause the resource to be re-created.
However, resources can explicitly enable parameter level modifications in their configs.
3. If the desired and current config matches perfectly, then `no-op` or no operation is returned
Parameter operations can be one of `add, modify, remove, or no-op`. A parameter change is `add` if it exists in the desired
config but not the current. `modify` is returned when the parameter exists in both but the value has changed.
`remove` is returned when a parameter exists in current but not desired. `no-op` is returned if the parameter
stays the same between the desired and current configs.
Apply [#apply]
Applies make the changes in the plan on the user's operating system. The operations undertaken by an apply
is entirely described by a plan. Plugins are required to implement:
* `async applyCreate(plan: CreatePlan): Promise`
* `async applyDestroy(plan:DestroyPlan): Promise`
* Optionally: `applyModify(pc: ParameterChange, plan: ModifyPlan): Promise`
In `applyCreate` a resource is expected to install packages on the operating system. In `applyDestroy`
the resource is expected to uninstall packages. Optionally resources to override `applyModify` to apply
parameter level changes like for example in the `aws-profile` resource, modify was overridden to allow the
modification of the `awsAccessKeyId` and `awsAccessKeySecret` without re-creating a profile.
Example: aws-cli resource [#example-aws-cli-resource]
Let's go over an example to make the overall architecture more clear.
**Refresh**
In the refresh method of the `aws-cli` resource. The resource will check if aws cli is already installed on the
system. If it's not then return `null`, if it is then it returns an empty config `{}` because the `aws-cli` resource
has no parameters.
**Plan**
Codify will use this information to generate a change set and a plan. Based on if the current config is null or not
Codify will determine if the resource operation is `apply` or `no-op`.
**Apply**
During the Apply, Codify will choose to either call `applyCreate` or not call anything at all depends on the plan.
If `applyCreate` is called, the `aws-cli` resource will install `aws-cli` on the system using the method
recommended by AWS. For arm based macs, this means installing `aws-cli` using homebrew.
# Registry
Under construction. Give us some time!
# Common issues
How to include a custom plugin [#how-to-include-a-custom-plugin]
Codify supports adding custom plugins using the `project` config block.
```json title="codify.jsonc"
[
{
"type": "project",
"plugins": {
"my-plugin": "0.0.0"
}
}
]
```
# Error messages
A list of common error messages to help debug your `codify.jsonc` file
Codify plan [#codify-plan]
Syntax errors [#syntax-errors]
```ansi title="terminal"
codify plan
...
Syntax error in codify.jsonc file
Unexpected token ","(,) in JSON at position 138
7 | },
8 | {
9 | "type": "path",,
| ^
10 | "path": "$HOME/path/to/folder"
11 | }
12 |]
```
A syntax error happens when the configuration inside the `codify.jsonc` file is incorrect and does
not follow the grammar of JSON. The parser will try to point out the error in most cases to make
it easy to fix the problem. In this case there was a double comma on line 9.
Validation errors [#validation-errors]
```ansi title="terminal"
codify plan
...
Validation error: invalid resource config for "homebrew" resource.
"/dependsOn" must be array
File: /Users/kevinwang/Projects/codify/codify.jsonc
17 | "cirruslabs/cli/cirrus",
18 | "cirruslabs/cli/tart"
19 | ],
> 20 | "dependsOn": "string"
21 | },
22 | {
23 | "type": "vscode"
```
A validation error happens when the configuration is valid JSON or YAML but the contents does not
validate to be a proper Codify config. The parser will try to generate a helpful error with a code
snippet to make it easier to fix. In the above example, dependsOn must be an array and not a string.
Some validation errors include unknown type ids (types not found on any plugins), invalid parameters,
and invalid parameter values.
Invalid project config [#invalid-project-config]
```ansi title="terminal"
codify plan
...
Syntax error in codify.jsonc file
Unexpected token ","(,) in JSON at position 138
7 | },
8 | {
9 | "type": "path",,
| ^
10 | "path": "$HOME/path/to/folder"
11 | }
12 |]
```
Codify apply [#codify-apply]
All plan errors are applicable to apply as well.
Additional changes are needed [#additional-changes-are-needed]
```ansi title="terminal"
codify apply
...
Plugin: 'default'. Resource: 'homebrew'. Additional changes are
needed to match the desired plan.
Validation returned: "modify" instead of "noop". These changes are remaining.
{
~ "casks":
+ jetbrains-toolbox,
],
}
```
This error is shown when a plugin could not apply the desired configuration. Scroll
up on the terminal to view the detailed logs for where the error has occurred. This issue
is most likely caused by a plugin error or a mis-configuration in `codify.jsonc`. An example
of this occurring is when the `homebrew` resource tries to install a cask for a program
that already exists. Depending on the error message, it may be fine to ignore this error. In this
example, the homebrew cask `jetbrains-toolbox` can be removed from `codify.jsonc` and be applied
again. Otherwise, report this to the plugin developer.
Apply error [#apply-error]
```ansi title="terminal"
codify apply
...
Apply error for plugin: "default", resource: "path"
Path could not be applied
```
An error occurred during the apply process, this could either be caused by a bug in a particular plugin
or `codify.jsonc` was mis-configured. The cause of the error is returned in the response and scrolling up
to view the generated output logs may give more context.
Plugin did not respond in 10 minutes [#plugin-did-not-respond-in-10-minutes]
```ansi title="terminal"
codify apply
...
Plugin did not respond in 10:00 minutes: apply
```
Codify will time out a plugin if it hasn't responded with a message, stdout or stderr within 10 minutes.
Mostly like the plugin is stuck or frozen. Try scrolling up to see if there were any errors in the detailed
logs. Report this issue to the plugin developer. If the long runtime was intentional, a future feature will
allow the timeout duration to be extended.
3 incorrect password attempts [#3-incorrect-password-attempts]
```ansi title="terminal"
codify apply
...
sudo: 3 incorrect password attempts
```
When installing dependencies that require root access to install (for example moving files to
`/usr/localbin`), a sudo prompt will be shown to the user to enter their password. Users get a
maximum of 3 attempts to enter the password. It will display this error if the user is not able
to enter the password in 3 attempts.
# Codify Agent
import { Callout } from 'fumadocs-ui/components/callout';
import { Steps, Step } from 'fumadocs-ui/components/steps';
The Codify Agent is an AI assistant that turns plain English descriptions into working developer environment setups. You describe what you need and the agent figures out the rest. No config files, no docs to read.
How it works [#how-it-works]
The agent uses Codify's resource library: a catalog of validated operations for tools like Homebrew, nvm, pyenv, Docker, VS Code, and more. When you describe what you want, the agent finds the right resources, builds a configuration, and validates it before showing it to you.
Unlike a general AI assistant, the Codify Agent does not write shell scripts. It generates configurations that Codify knows how to apply, modify, and reverse cleanly. Every change is reviewable before it runs.
Setting up your environment [#setting-up-your-environment]
**Open the Desktop App** and start a new conversation with the Codify Agent.
**Describe what you need.** Be as specific or as general as you like:
> *"Set up my machine for Python data science work"*
> *"I need Node.js 20, VS Code, and Docker for a web project"*
> *"Set up a Go backend development environment with Postgres"*
**Answer any clarifying questions.** The agent may ask follow-up questions to get things right (for example, which version of a tool you prefer or whether you need a database running locally).
**Review the configuration.** The agent generates a `codify.jsonc` config and shows it to you before doing anything.
**Click Apply.** Codify shows you the full plan (exactly what will be installed or changed) and asks for your confirmation. Once you approve, it runs.
You can also access the agent from the terminal by running `codify edit`, which opens the desktop app.
Editing an existing configuration [#editing-an-existing-configuration]
Every `codify.jsonc` file you open in the Desktop App has an agent sidebar. Use it to make changes without editing JSON directly.
For example:
* *"Add Python 3.11"*
* *"Remove Docker"*
* *"Switch to Node 22"*
The agent proposes a diff showing exactly what changed. Accept or reject it before anything is written to the file.
Why use the agent instead of a general AI [#why-use-the-agent-instead-of-a-general-ai]
Most AI assistants generate shell scripts or command lists. This approach has drawbacks: the commands may not be idempotent, there is no easy way to reverse them, and you have to trust they do exactly what you expect.
The Codify Agent is different:
* **Built on validated resources** — Every resource in Codify's library handles install, modify, and remove correctly. The agent tries it's best to use Codify resources. It will fall back to action resources (scripts) if something is not available.
* **Review before anything runs** — The generated config is shown to you first. Codify always presents a plan before making changes. Nothing runs without your approval.
* **Reversible** — Changes with Codify are generally reversible.
* **Handles dependencies** — The agent knows that Homebrew must be installed before Homebrew formulae, for example. It handles ordering automatically.
Supported tools [#supported-tools]
The agent can configure any tool in Codify's resource library. This includes Homebrew, apt, nvm, pyenv, jenv, VS Code, Docker, AWS CLI, Git, SSH keys, and many more. See the [full resource list](/docs/resources) for details.
Don't see a tool you need? [Request it on GitHub](https://github.com/codifycli/default-plugin/issues/new?template=resource-request.yml). This kicks off an AI process to generate the resource. The Codify team then reviews and tests it before merging - the turnaround is 10 business days.
# Codify CLI
The Codify CLI (`codify`) is the core engine behind everything Codify does. It reads your config file, compares it to your current system, and applies only what needs to change. The [Desktop App](/docs/installation#desktop-app) and [Codify Agent](/docs/user-guide/agent) both use the CLI under the hood.
Use the CLI if you prefer the terminal, need to script environment setup, or want to integrate Codify into CI workflows.
Core Workflow [#core-workflow]
Every Codify workflow follows the same pattern:
1. **Write** a `codify.jsonc` config file declaring your desired environment.
2. **Preview** changes with `codify plan` — no changes are made.
3. **Apply** changes with `codify apply` — Codify asks for approval first.
```sh title="terminal"
codify plan
codify apply
```
`codify plan` reads your config, checks your current system state, and prints a list of changes. `codify apply` runs the same check, then prompts you to confirm before touching anything.
Codify runs in **stateless mode** by default. It compares your config to your current system on every run — no state file required. Resources not in your config are left untouched.
Commands [#commands]
| Command | Description |
| ------------------------------------- | -------------------------------------------------------------- |
| [`apply`](/docs/commands/apply) | Generate a plan and apply it after approval |
| [`plan`](/docs/commands/plan) | Preview changes without applying them |
| [`validate`](/docs/commands/validate) | Check a config file for errors |
| [`init`](/docs/commands/init) | Generate a config from currently installed tools |
| [`import`](/docs/commands/import) | Import specific installed resources into your config |
| [`refresh`](/docs/commands/refresh) | Re-evaluate the current state of your resources |
| [`destroy`](/docs/commands/destroy) | Uninstall resources defined in your config |
| [`test`](/docs/commands/test) | Apply a config in an isolated environment, then destroy it |
| [`edit`](/docs/commands/edit) | Open the Desktop App to edit your config with the Codify Agent |
| [`login`](/docs/commands/login) | Authenticate with Codify |
| [`logout`](/docs/commands/logout) | Sign out of Codify |
| [`update`](/docs/commands/update) | Update the Codify CLI to the latest version |
Config File [#config-file]
The CLI reads a `codify.jsonc` file — a top-level JSON array of resource objects. Each object requires a `type` field that identifies the resource.
```jsonc title="codify.jsonc"
[
{
"type": "homebrew",
"formulae": ["jq", "ripgrep"]
},
{
"type": "nvm",
"version": "20"
}
]
```
Supported file formats:
| Format | Recommended |
| -------- | -------------------------------- |
| `.jsonc` | Yes — supports inline comments |
| `.json` | Supported |
| `.json5` | Supported |
| `.yaml` | Supported (import not available) |
Most IDEs with SchemaStore support provide autocomplete for `codify.jsonc` automatically. See [codify.jsonc](/docs/user-guide/configuration-language) for the full config reference.
# Configuration Language
The Codify configuration language is a JSON-based format for describing your development environment. It consists of an array of resource objects, where each object represents a tool, application, or system setting for Codify to manage.
Example [#example]
```json title="codify.jsonc"
[
{
"type": "homebrew",
"formulae": ["jq", "openjdk@17", "jenv", "docker"],
"casks": ["openvpn-connect", "1password", "google-chrome"],
"os": ["macOS"]
},
{ "type": "git-lfs" },
{
"type": "git-clone",
"parentDirectory": "~/projects",
"remote": "git@github.com:kevinwang5658/codify-plugin-lib.git"
},
{
"type": "path",
"path": "$HOME/projects/dev-tools/bin",
"dependsOn": ["git-clone"]
}
]
```
**How it works:**
1. Each object in the array is a resource config. The `type` field identifies which resource to manage. All configs require a `type`.
2. Resources accept additional parameters that control how they are installed or configured. Parameters can be required or optional — check the individual resource docs for details.
3. Reserved keywords like `dependsOn` and `os` are available on every resource. See [Keywords](#keywords) for the full list.
Resource Configs [#resource-configs]
A resource config is a JSON object with a `type` field added to the top-level array. See the [resource reference](/docs/resources) for all available types.
```json title="codify.jsonc"
[
{
"type": "homebrew",
"name": "main",
"formulae": ["jq", "openjdk@17", "jenv", "docker"],
"casks": ["openvpn-connect", "1password", "google-chrome"],
"os": ["macOS"],
"dependsOn": ["rubyenv"]
}
]
```
**`name`** uniquely identifies a resource when multiple resources share the same type (for example, two `git-clone` entries). Codify forms the fully qualified name as `type.name`. Without a name, Codify assigns a zero-indexed integer automatically if multiple of the same resource exists: `git-clone.0`, `git-clone.1`, etc.
**`dependsOn`** controls the order resources are applied. Pass an array of fully qualified ids (`type.name`) or just a type to match all resources of that type.
**`os`** restricts a resource to specific operating systems. Resources with an `os` filter are excluded from plans and applies on non-matching systems.
Project Configs [#project-configs]
A project config uses the reserved type `"project"`. It declares metadata and additional plugins for the config file.
```json title="codify.jsonc"
[
{
"type": "project",
"description": "A cloud infrastructure environment setup config file",
"plugins": {
"default": "^1.0.0"
}
},
{ "type": "ssh" }
]
```
**`description`** documents the purpose of the config. It is not used by the CLI.
**`plugins`** controls which plugins are loaded and at what version. The default plugin loads automatically at the latest stable version. Use this field to pin it to a specific version, opt into beta, or load a custom plugin from a local file path.
```jsonc
"plugins": {
"default": "^1.0.0", // pin to a version range
"default": "beta", // opt into latest unreleased changes
"my-custom-plugin": "./my-plugin.ts" // load a custom plugin
}
```
Keywords [#keywords]
Resource Configs [#resource-configs-1]
* **type** (required): The resource type identifier. Must start with a letter, followed by alphanumeric characters, underscores, or hyphens. Examples: `homebrew`, `nvm`, `pyenv`, `terraform`.
* **name** (optional): Uniquely identifies a resource when multiple resources share the same type. Alphanumeric, underscores, and hyphens allowed. Auto-assigned as `0`, `1`, `2` when omitted. Examples: `"name": "main"`, `"name": "infrastructure-repo"`.
* **dependsOn** (optional): Controls apply order. Takes an array of fully qualified ids (`type.name`) or a bare type. Examples: `"dependsOn": ["git-clone.0", "git-clone.1"]`, `"dependsOn": ["path"]`.
* **os** (optional): Restricts the resource to specific operating systems. Allowed values: `"linux"`, `"macOS"`, `"windows"`. Examples: `"os": ["macOS"]`, `"os": ["linux", "macOS"]`.
* **distro** (optional): Further restricts a Linux resource to specific distributions. Allowed values: `"debian-based"`, `"rpm-based"`, `"arch"`, `"ubuntu"`, `"debian"`, `"fedora"`, `"rhel"`, `"centos"`, `"alpine"`, `"amzn"`, `"opensuse"`, `"sles"`, `"manjaro"`, `"linuxmint"`, `"pop"`, `"elementary"`, `"kali"`, `"gentoo"`, `"slackware"`. Example: `"distro": ["ubuntu", "debian"]`.
Project Configs [#project-configs-1]
* **type** (required): Must be `"project"`.
* **description** (optional): A human-readable description of the config file. Not used by the CLI. Example: `"description": "This is a dependency setup file"`.
* **version** (optional): A semver string specifying the minimum Codify CLI version required. Examples: `"version": ">=0.5.0"`, `"version": "^1.3"`.
* **plugins** (optional): Maps plugin names to version strings. Required before using a plugin's resource types. Accepts local paths for development. Example: `"plugins": { "default": "^1.0.0", "custom": "../plugins/dev.ts" }`.
# Editor
The Codify editor is a tool for creating and editing configurations. It is available as a [web app](https://dashboard.codifycli.com) and as a [Desktop App](/docs/installation#desktop-app). A free tier is available with paid plans for additional features.
Overview [#overview]
The editor provides two ways to work with configurations:
* **Block Editor** - Visual, card-based interface with drag-and-drop
* **Code Editor** - JSON editor with syntax highlighting and validation
Accessing the Editor [#accessing-the-editor]
**Web**: Visit [dashboard.codifycli.com](https://dashboard.codifycli.com) and sign in.
**Desktop**: Download and install the [Desktop App](/docs/installation#desktop-app), then sign in with your Codify account.
Alternatively, run:
```bash
codify edit
```
This opens the editor and connects your local CLI automatically.
Connecting Your CLI [#connecting-your-cli]
Connecting your CLI bridges the editor with your local machine. This allows you to:
* Execute commands (apply, plan, import, refresh, test) from the browser
* View real-time terminal output in the editor
* Run interactive terminal sessions
**Desktop App:** The CLI connects automatically. No setup required.
**Web app:** Run the following command, then enter the connection code shown in the editor's top bar:
```bash
codify connect
```
The connection is secure and allows commands to execute on your local machine while you edit in the browser. Without a CLI connection, you can still edit configurations but cannot execute commands.
Collaboration [#collaboration]
The editor supports real-time collaboration, making it easy for teams to share and edit configurations together.
**Sharing:**
Click your avatar, then "Share" to control access. Documents can be Private (only you), Internal (anyone in your organization), or Public (anyone with the link).
**Permission Levels:**
Owners have full control. Admins can edit and share with others. Editors can edit but not share. Viewers have read-only access.
Files [#files]
File uploads are available on paid plans. Upload files in the Files sidebar and associate them with your configurations. Each file is tied to a specific config and can be used with the [remote-file resource](/docs/resources/remote-file).
Common use cases include wallpapers, configuration files, scripts, fonts, and other assets needed for your setup.
Applying from the CLI [#applying-from-the-cli]
The CLI works with both local files and cloud documents. After logging in with `codify login`, you can apply any cloud document directly from the CLI using its document ID:
```bash
# Local files
codify apply # Uses local codify.jsonc
# Cloud documents
codify apply # Applies cloud document (requires login)
codify apply # Uses default document if no local file exists
codify edit # Opens editor to default document + connects CLI
```
**Default Document:**
Your default document is the cloud configuration that gets applied automatically when you run `codify apply` with no arguments (while logged in and no local `codify.jsonc` exists). Set this to your standard setup file so you can quickly apply your configuration on any machine. Change your default document in the editor main page (document with a star).
Next Steps [#next-steps]
* [Commands](/docs/commands/apply) - Learn CLI commands
* [Templates](/docs/templates) - Pre-built configurations
* [Resources](/docs/resources) - Available resource types
# MCP Server
import { Callout } from 'fumadocs-ui/components/callout';
Codify runs a remote MCP server that gives any MCP-compatible AI coding tool direct access to Codify's resource library. Connect it to Claude Code, Codex, Cursor, or another MCP client, and the tool can look up real resource schemas and generate valid `codify.jsonc` files.
Connecting [#connecting]
The server is available over Streamable HTTP at:
```
https://api.codifycli.com/v1/mcp
```
No API key or authentication is required.
**Claude Code:**
```sh title="terminal"
claude mcp add --transport http codify https://api.codifycli.com/v1/mcp
```
**Codex CLI:** Codex has no CLI shortcut for HTTP servers yet, so add the server directly to `~/.codex/config.toml`:
```toml title="~/.codex/config.toml"
[mcp_servers.codify]
url = "https://api.codifycli.com/v1/mcp"
```
**Other tools:** Check your tool's MCP documentation for how to add a remote HTTP server. Tools that only support stdio servers need a local bridge such as [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) pointed at the URL above.
Tools [#tools]
| Tool | Description |
| ---------------------- | -------------------------------------------------------------------------------------------------- |
| `search_resources` | Semantically search resource types by capability (e.g. "install Node.js"). |
| `get_resource_schema` | Get the full schema, parameters, and example configs for one resource type. |
| `list_resources` | List all available resource types in the default plugin. |
| `search_templates` | Semantically search pre-built config templates by use case. |
| `get_template` | Fetch a full template config by name or ID. |
| `generate_config_hint` | Given a description of what you want, return the resource schemas and examples needed to write it. |
The server also sends config authoring rules (valid file naming, when to use `dependsOn` and `os` gating, how to write idempotent `action` conditions, and so on) as MCP server instructions. Clients that support this pick it up automatically, so the AI tool is grounded in Codify's actual conventions rather than inventing its own.
Example [#example]
You ask your AI coding assistant:
> *"Set up a Node.js and Docker dev environment"*
The assistant calls `search_resources` for "Node.js" and "Docker", finds `nvm` and `docker`, then calls `get_resource_schema` for each to confirm the correct parameters. It writes the result to `codify.jsonc`:
```jsonc title="codify.jsonc"
[
{
"type": "nvm",
"version": "22"
},
{
"type": "docker"
}
]
```
Connecting the MCP server only helps the AI tool write the config. Always run `codify plan` to review the changes, then `codify apply` to confirm before anything is installed.
# What is Codify?
import { Callout } from 'fumadocs-ui/components/callout';
import { Files, Folder, File } from 'fumadocs-ui/components/files';
Codify is a configuration-as-code tool for managing developer environments. You define your setup in a simple config file — which tools to install, which versions to use, how they should be configured — and Codify handles the rest. It compares the config to what's already installed on your machine, shows you what needs to change, and applies it.
Codify is also AI-powered: the built-in [Codify Agent](/docs/user-guide/agent) can generate that config file for you from a plain English description, so you don't have to write JSON if you don't want to.
How does it work [#how-does-it-work]
Codify has a few main parts that work together:
1. **CLI**: Reads `codify.jsonc` configuration files, generates a plan of what needs to change, and applies those changes to your system. This is the core of Codify — everything else builds on it.
2. **Desktop App**: The recommended way to use Codify. It includes a visual config editor with auto-complete, the Codify Agent for AI-assisted config generation, and one-click apply. Available for macOS, Linux, and Windows.
3. **codify.jsonc**: The configuration file that describes your desired environment — a list of resources (tools, apps, settings) and how they should be configured.
4. **Codify Agent**: An AI assistant built into the Desktop App. Describe what you need in plain English, and the agent generates a valid `codify.jsonc` for you. [Learn more →](/docs/user-guide/agent)
Multiple file types are supported: `.jsonc`, `.json`, `.json5`, and `.yaml`. The recommended format is `jsonc` for its comment support and wide editor compatibility.
Workflow [#workflow]
The typical Codify workflow:
* **Write or generate a config**: Create a `codify.jsonc` file listing the tools you want. You can write it by hand, start from a [template](/docs/templates), generate it with `codify init`, or use the Codify Agent to describe what you need in plain English.
* **Plan**: Run `codify plan` (or `codify apply`) to see what Codify will do. This compares your config against what's currently installed and generates a list of changes.
* **Apply**: Run `codify apply`, review the plan, and confirm. Codify installs, updates, or configures each resource as needed.
You always see a plan before anything changes. Codify never makes changes without your approval.
Usage Scenarios [#usage-scenarios]
Setting up a new computer [#setting-up-a-new-computer]
Codify is great for setting up a new machine from scratch. Instead of spending hours reading wikis, installing tools one by one, and debugging strange errors — apply a `codify.jsonc` file and you're done. Teams can maintain a shared config file, or generate a new one from scratch with `codify init` or the Codify Agent.
Project Integration [#project-integration]
You can commit a `codify.jsonc` file directly into a project repository. Anyone who clones the project can run `codify apply` to get all the required tools installed automatically — no setup docs needed.
# Resource & Plugins
Codify ships with a default plugin that covers 50+ tools out of the box. For most users, no plugin configuration is required.
Resources [#resources]
A **resource** is the interface between Codify and your system. Each resource knows how to install, check the current state of, modify, and remove a specific tool or system setting. Resources are what Codify executes when you run `codify apply`.
Every resource has a `type` that you reference in your config:
* `homebrew` — Homebrew packages and casks
* `nvm` — Node.js version management
* `vscode` — VS Code installation
* `git` — Git global configuration
* `path` — Shell PATH entries
* `alias` — Shell aliases
See the [full resource list](/docs/resources) for all available resources.
Plugins [#plugins]
A **plugin** is a collection of resources. Codify ships with one built-in plugin: the **default plugin**. It loads automatically and requires no configuration for basic use.
Advanced users can sideload local resources or build custom plugins. See [Custom Plugins](#custom-plugins) below.
The Default Plugin [#the-default-plugin]
You can pin the default plugin to a specific version using a project config block:
```jsonc
[
{
"type": "project",
"plugins": {
"default": "^1.0.0"
}
},
{
"type": "homebrew",
"formulae": ["git", "node"]
}
]
```
**Plugin version options:**
* `"latest"` — always use the most recent stable version
* `"beta"` — use the latest beta version
* Semantic version (e.g., `"^1.0.0"`, `"1.2.3"`) — specific version constraint
If you omit the project config block, Codify loads the latest stable version of the default plugin automatically.
Requesting New Resources [#requesting-new-resources]
If a tool you need is not in the default plugin, you can request it. Open a [resource request](https://github.com/codifycli/default-plugin/issues/new?template=resource-request.yml) on GitHub. An AI workflow generates the resource and opens a pull request. The Codify team then reviews and tests it before merging. Turnaround is 10 business days.
Custom Plugins [#custom-plugins]
For tools that need custom behavior, you can build your own plugin and reference it locally in your project config:
```jsonc
[
{
"type": "project",
"plugins": {
"default": "latest",
"./local/plugin.ts": "1.0.0"
}
}
]
```
See the [Plugin Development Guide](/docs/developer-guide/plugin-development) for how to build, test, and use custom plugins.
# Security
Codify places a high priority on security measures to safeguard user environments and
ensure the integrity of operations. Here are key aspects demonstrating Codify's approach to security:
Secure Mode for Enhanced Protection [#secure-mode-for-enhanced-protection]
Codify offers a Secure Mode that enforces strict sandboxing for all plugins.
This sandboxing mechanism restricts plugin activities, preventing unauthorized access
and usage of system-level commands like sudo, unless explicitly permitted by the user.
By default, Codify ensures that plugins operate within a controlled environment,
minimizing the risk of potential security breaches.
Injection Attack Prevention [#injection-attack-prevention]
To mitigate risks associated with injection attacks, Codify employs a robust plugin
library that systematically escapes all parameters. This proactive measure prevents
malicious input from third-party templates or rogue scripts, safeguarding against
potential vulnerabilities that could compromise system integrity.
Rigorous Third-Party Plugin Review Process [#rigorous-third-party-plugin-review-process]
All third-party plugins undergo a thorough review by the Codify team prior to integration
into the platform. During this evaluation, plugins are scrutinized for adherence to
security best practices and potential vulnerabilities. Plugins that pass the review
process are marked as verified, ensuring they meet Codify's stringent security standards.
Vulnerability Notifications and Secure Execution [#vulnerability-notifications-and-secure-execution]
For transparency and user awareness, Codify provides clear notifications regarding
the security status of plugins. Verified plugins are labeled accordingly, indicating
their reliability. In contrast, plugins that haven't undergone verification are flagged,
prompting Codify to automatically activate Secure Mode as a precautionary measure.
This approach ensures that even unverified plugins operate within a secure environment,
prioritizing user safety at all times.
Continuous Improvement and Community Engagement [#continuous-improvement-and-community-engagement]
Codify remains committed to ongoing improvement of its security measures through
continuous monitoring, feedback from users, and proactive updates to address
emerging threats. By fostering a collaborative environment and prioritizing
security-conscious practices, Codify aims to provide a reliable and secure tool
for managing dependencies and configurations across diverse environments.
# Terminology
* **CLI**: The `codify` command-line tool. It reads configuration files, generates plans, and applies changes to your system. All other Codify interfaces (Desktop App, agent) use the CLI under the hood.
* **Desktop App**: The recommended graphical interface for Codify. It includes a config editor with auto-complete, the Codify Agent, and one-click apply — all in one place.
* **Plugin**: A collection of resources. Codify ships with a default plugin that handles the most popular macOS and Linux packages. Additional plugins can be added to support more tools.
* **Resource**: A specific tool, application, or system setting that Codify can manage. Each resource has a `type` (e.g. `"homebrew"`, `"nvm"`) and optional parameters that control how it's configured.
* **Config**: A single resource declaration inside a `codify.jsonc` file. Every config must have a `type` field. The file as a whole is a top-level array of these config objects.
* **Plan**: A preview of what Codify will do. Before making any changes, Codify generates a plan showing every resource it will create, modify, or remove. You always approve the plan before anything runs.
* **Stateless Mode**: Codify's default mode. Codify doesn't keep track of what it's previously installed — it just looks at your current system state and compares it to your config. In stateless mode, Codify can add and modify resources but won't automatically remove ones that aren't in your config.
* **Agent**: The Codify AI assistant, available in the Desktop App. Describe your environment in plain English, and the agent generates a `codify.jsonc` config using the resource library. It asks clarifying questions when needed, then lets you review and apply the result. [Learn more →](/docs/user-guide/agent)
* **Resource Library**: The catalog of validated tools and applications Codify knows how to manage — things like Homebrew, nvm, pyenv, VS Code, Docker, and more. The agent uses this library when generating configurations.
* **Resource Operations**: The actions a resource can take: `create`, `destroy`, `modify`, `recreate`, and `no-op`.
* **Parameter Operations**: Changes to individual settings within a resource: `add`, `remove`, `modify`.
# Use Cases
Codify adapts to how you work. Whether you jump between client projects, manage a growing engineering team, or just want to build without worrying about setup, there is a workflow that fits.
Freelancers [#freelancers]
Every project has a different stack. Codify lets you save a config per project and spin up a working environment in minutes. No more remembering what versions you used last time or manually reinstalling tools after switching machines.
* Switch between project environments without starting from scratch
* Restore your full setup on a new machine with a single command
* Version-control your environment alongside your code
Software Teams [#software-teams]
Share one `codify.jsonc` with your team and every engineer starts from the same baseline. New hires run `codify apply` and have a working environment before their first standup.
* Eliminate "works on my machine" problems across the team
* Onboard new engineers in minutes, not days
* Keep everyone on the same tool versions as the project evolves
* Store the config in Git alongside your codebase
Vibe Coders [#vibe-coders]
Skip the setup entirely. Describe what you want to build to the [Codify Agent](/docs/user-guide/agent) and it generates the config and applies it for you. No JSON, no manual installs, no prior setup experience needed.
* Tell the agent your stack in plain English
* Get a ready-to-go environment without writing a single config line
* Iterate fast by describing changes instead of editing files
# API reference
API Reference [#api-reference]
Core Classes [#core-classes]
* **`Plugin`** - Top-level plugin container
* **`Resource`** - Abstract base class for resources
* **`StatefulParameter`** - Stateful parameter implementation
* **`Plan`** - Change set representation
Utility Functions [#utility-functions]
```typescript
// PTY access
getPty(): IPty
// Path utilities
tildify(absolutePath: string): string
untildify(pathWithTilde: string): string
// File utilities
fileExists(path: string): Promise
directoryExists(path: string): Promise
// Array utilities
areArraysEqual(a: T[], b: T[], isEqual?: (a: T, b: T) => boolean): boolean
```
# Best practices
Best Practices for refresh() [#best-practices-for-refresh]
1. **Return `null` for non-existent resources** - This is how Codify knows to create the resource.
2. **Query only requested parameters** - Check the `parameters` argument to see what the user configured. Don't query everything.
3. **Handle errors gracefully** - If a command fails, return `null` rather than throwing (unless it's an unexpected error).
4. **Be efficient** - This method is called during every plan operation. Avoid expensive operations if possible.
5. **Parse output carefully** - System commands can return different formats. Use robust parsing logic.
create() [#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 apply` when the resource operation is `CREATE`
* 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 wants
* `plan.isStateful` - Whether this is a stateful operation
* `plan.id` - Unique identifier for this specific resource instance
```typescript
async create(plan: CreatePlan): Promise {
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 [#real-world-example-homebrew-installation]
```typescript
async create(plan: CreatePlan): Promise {
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 [#handling-stateful-parameters]
If your resource has stateful parameters (like Homebrew's formulae), the framework automatically calls their `add()` methods after `create()` completes:
```typescript
// In create(), just install the base resource
async create(plan: CreatePlan): Promise {
await this.installHomebrew();
// Don't install formulae here - the FormulaeParameter.add() handles that
}
```
destroy() [#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 apply` when the resource operation is `DESTROY`
* 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?
```typescript
async destroy(plan: DestroyPlan): Promise {
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 [#real-world-example-git-repository-resource]
The git-repository resource refuses to delete directories because it could destroy user work:
```typescript
async destroy(plan: DestroyPlan): Promise {
// 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 [#handling-stateful-parameters-1]
If `removeStatefulParametersBeforeDestroy` is true in your resource settings, stateful parameters' `remove()` methods are called before `destroy()`:
```typescript
getSettings(): ResourceSettings {
return {
id: 'homebrew',
removeStatefulParametersBeforeDestroy: true, // Uninstall formulae first
// ...
};
}
async destroy(plan: DestroyPlan): Promise {
// 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() [#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 apply` when the resource operation is `MODIFY`
* Once per parameter that changed
* Only for parameters where `canModify: true` in parameter settings
**What you receive:**
* `parameterChange.name` - Which parameter changed
* `parameterChange.operation` - ADD, REMOVE, or MODIFY
* `parameterChange.newValue` - The new value
* `parameterChange.previousValue` - The old value
* `plan` - Full plan context with desired and current configs
```typescript
async modify(
parameterChange: ParameterChange,
plan: ModifyPlan
): Promise {
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 [#real-world-example-alias-resource]
The alias resource modifies the alias value by finding and replacing the line in the shell RC file:
```typescript
async modify(
pc: ParameterChange,
plan: ModifyPlan
): Promise {
// 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() [#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.
Schema Validation [#schema-validation]
Use JSON Schema or Zod for validation:
JSON Schema [#json-schema]
```typescript
// my-resource-schema.json
{
"type": "object",
"properties": {
"version": { "type": "string" },
"path": { "type": "string" }
},
"required": ["version"]
}
// my-resource.ts
import Schema from './my-resource-schema.json';
interface MyConfig extends StringIndexedObject {
version: string;
path?: string;
}
getSettings() {
return { schema: Schema };
}
```
Zod (Preferred) [#zod-preferred]
Zod provides type safety with a single source of truth:
```typescript
import { z } from 'zod';
const schema = z.object({
version: z.string(),
path: z.string().optional(),
});
type MyConfig = z.infer;
getSettings() {
return { schema };
}
```
PTY Abstraction [#pty-abstraction]
The **PTY (Pseudo-Terminal)** abstraction is how plugins execute shell commands. Instead of using Node's `child_process` directly, you use `getPty()` to get a PTY instance that handles stdout/stderr streaming, error handling, and privilege escalation.
Why PTY Instead of child_process? [#why-pty-instead-of-child_process]
1. **Interactive Commands**: PTY supports commands that require user input (like installers)
2. **Output Streaming**: Real-time stdout/stderr is streamed to the user's terminal
3. **Privilege Escalation**: Automatic handling of `sudo` password prompts
4. **Context Awareness**: Different PTY implementations for planning vs applying
Getting the PTY Instance [#getting-the-pty-instance]
```typescript
import { getPty } from '@codifycli/plugin-core';
const pty = getPty();
```
The `getPty()` function uses async local storage to provide the correct PTY instance for the current context:
* **BackgroundPty** during `refresh()` - Allows parallel execution for faster planning
* **SequentialPty** during `create()`, `modify()`, `destroy()` - Executes commands sequentially with proper error handling
spawn() - Execute with Error Handling [#spawn---execute-with-error-handling]
Use `spawn()` when you expect the command to succeed. If it fails (non-zero exit code), an error is thrown and execution stops.
```typescript
const pty = getPty();
// Throws on non-zero exit code
const result = await pty.spawn('brew install jq');
console.log(result.data); // stdout output as string
```
**When to use `spawn()`:**
* During `create()`, `modify()`, `destroy()` when failure should stop execution
* When the command must succeed for the operation to be valid
* When you want automatic error propagation
spawnSafe() - Execute Without Throwing [#spawnsafe---execute-without-throwing]
Use `spawnSafe()` when you need to check if something exists or when failure is an expected outcome.
```typescript
const pty = getPty();
// Never throws - returns status object
const result = await pty.spawnSafe('which jq');
if (result.status === SpawnStatus.SUCCESS) {
console.log('jq is installed at:', result.data);
} else {
console.log('jq not found');
}
```
**When to use `spawnSafe()`:**
* During `refresh()` to check if resources exist
* When checking prerequisites or system state
* When multiple outcomes are valid
Spawn Options [#spawn-options]
Both `spawn()` and `spawnSafe()` accept an options object:
```typescript
await pty.spawn('npm install', {
// Working directory for the command
cwd: '/path/to/project',
// Environment variables (merged with current env)
env: { NODE_ENV: 'production' },
// Allow interactive input (prompts, confirmations)
interactive: true,
// Require root/sudo privileges
requiresRoot: true,
// Provide input to stdin
stdin: true,
});
```
Option Details [#option-details]
**`cwd`** - Change working directory:
```typescript
// Clone a repo into a specific directory
await pty.spawn('git clone https://github.com/user/repo.git', {
cwd: '/Users/john/projects'
});
```
**`env`** - Set environment variables:
```typescript
// Run installer non-interactively
await pty.spawn('./install.sh', {
env: { NONINTERACTIVE: 1 }
});
```
**`interactive`** - Allow user input:
```typescript
// Run installer that may prompt for choices
await pty.spawn('/bin/bash -c "$(curl -fsSL https://install.sh)"', {
interactive: true,
stdin: true
});
```
**`requiresRoot`** - Execute with sudo:
```typescript
// Install to system directory
await pty.spawn('cp binary /usr/local/bin/', {
requiresRoot: true
});
```
When `requiresRoot: true`, Codify:
1. Sends a message to the parent CLI process
2. CLI prompts user for their sudo password
3. Password is sent back to plugin securely
4. Command executes with sudo
This ensures plugins never have direct access to sudo—they must request it through the parent process.
Real-World Examples [#real-world-examples]
Checking if a tool is installed: [#checking-if-a-tool-is-installed]
```typescript
async refresh(parameters: Partial): Promise | null> {
const pty = getPty();
// Check if tool exists
const result = await pty.spawnSafe('which my-tool');
if (result.status === SpawnStatus.ERROR) {
return null; // Not installed
}
// Get version
const versionResult = await pty.spawnSafe('my-tool --version');
const version = versionResult.data.match(/v(\d+\.\d+\.\d+)/)?.[1];
return { version };
}
```
Installing with platform-specific commands: [#installing-with-platform-specific-commands]
```typescript
async create(plan: CreatePlan): Promise {
const pty = getPty();
if (Utils.isMacOS()) {
await pty.spawn('brew install my-tool');
} else if (Utils.isLinux()) {
await pty.spawn('apt-get install -y my-tool', {
requiresRoot: true
});
}
}
```
Running commands in a specific directory: [#running-commands-in-a-specific-directory]
```typescript
async create(plan: CreatePlan): Promise {
const pty = getPty();
const projectPath = plan.desiredConfig.directory;
// Create directory
await pty.spawn(`mkdir -p ${projectPath}`);
// Initialize project in that directory
await pty.spawn('npm init -y', { cwd: projectPath });
await pty.spawn('npm install express', { cwd: projectPath });
}
```
PTY Implementation Details [#pty-implementation-details]
Codify uses two PTY implementations:
**BackgroundPty** (during planning):
* Allows parallel command execution for faster refresh operations
* Automatically killed after planning completes
* Used in `refresh()`, `validate()`
**SequentialPty** (during apply):
* Executes commands one at a time, in order
* Ensures proper error handling and output streaming
* Used in `create()`, `modify()`, `destroy()`
You don't need to worry about which PTY you're using—`getPty()` provides the correct one automatically based on context.
Resource Patterns [#resource-patterns]
Codify supports several resource patterns to handle different use cases. Understanding these patterns will help you design resources that match user expectations.
Simple Singleton [#simple-singleton]
The **simple singleton** pattern is the most basic resource type. Each config entry creates exactly one resource instance on the system.
**Use this pattern when:**
* Each resource is unique and independent
* Users manage resources one at a time
* No system-wide discovery is needed
**Example use case**: A single git global configuration per system.
One resource instance per config entry:
```typescript
class AliasResource extends Resource {
getSettings(): ResourceSettings {
return {
id: 'alias',
operatingSystems: ['darwin', 'linux'],
schema: {
type: 'object',
properties: {
alias: { type: 'string' },
value: { type: 'string' }
},
required: ['alias', 'value']
},
allowMultiple: {
identifyingParameters: ['alias'] // Each unique alias = different resource
}
};
}
// ... implement refresh/create/destroy
}
```
**Usage:**
```json
[
{ "type": "alias", "alias": "ll", "value": "ls -la" },
{ "type": "alias", "alias": "gs", "value": "git status" }
]
```
Each config entry creates a separate resource. The `alias` field uniquely identifies each resource—two aliases with the same name would conflict.
Multiple Instances with Custom Matcher [#multiple-instances-with-custom-matcher]
The **custom matcher** pattern allows resources to exist multiple times on the system with custom logic for matching desired configs to existing resources. This is more flexible than `identifyingParameters` when matching logic is complex.
**Use this pattern when:**
* Multiple instances of a resource can coexist
* Matching logic is more complex than simple field equality
* You need platform-specific matching (e.g., case-insensitive paths on macOS)
* You want to auto-discover existing resources on the system
**Example use case**: Git repositories can exist in multiple directories, and on macOS paths are case-insensitive.
Use custom logic to match desired configs with system state:
```typescript
getSettings(): ResourceSettings {
return {
id: 'my-resource',
allowMultiple: {
// Custom matching logic
matcher: (desired, current) => {
return desired.directory === current.directory;
},
// Auto-discovery for `codify import`
async findAllParameters() {
// Discover all instances on system
const instances = await discoverInstances();
return instances.map(i => ({ directory: i.path }));
}
}
};
}
```
Real-World Example: Git Repository with Platform-Aware Matching [#real-world-example-git-repository-with-platform-aware-matching]
```typescript
getSettings(): ResourceSettings {
return {
id: 'git-repository',
allowMultiple: {
matcher: (desired, current) => {
// Get absolute paths for comparison
const desiredPath = path.resolve(desired.directory);
const currentPath = path.resolve(current.directory);
// macOS is case-insensitive, Linux is case-sensitive
if (process.platform === 'darwin') {
return desiredPath.toLowerCase() === currentPath.toLowerCase();
}
return desiredPath === currentPath;
},
async findAllParameters() {
const pty = getPty();
// Find all git repos in home directory
const { data } = await pty.spawnSafe(
'find ~ -name .git -type d -not -path "*/Library/*"'
);
const directories = data
.split('\n')
.filter(Boolean)
.map(p => path.dirname(p))
.map(directory => ({ directory }));
return directories;
}
}
};
}
```
The `findAllParameters()` method enables `codify import` to discover existing resources automatically. When users run `codify import`, Codify calls this method and generates config entries for all found instances.
Multi-Declaration Resources [#multi-declaration-resources]
The **multi-declaration** pattern allows users to manage multiple related items in a single config entry. This is cleaner than requiring separate config entries for each item.
**Use this pattern when:**
* Users typically manage groups of similar items together
* Items are closely related and share common settings
* Individual items don't need separate configurations
* You want to offer both declarative and stateful modes
**Example use case**: Managing multiple shell aliases as a group.
Manage multiple items in a single resource:
```typescript
const schema = z.object({
aliases: z.array(z.object({
alias: z.string(),
value: z.string()
}))
});
type AliasesConfig = z.infer;
class AliasesResource extends Resource {
getSettings(): ResourceSettings {
return {
id: 'aliases',
schema,
parameterSettings: {
aliases: {
type: 'array',
canModify: true,
isElementEqual: (a, b) => a.alias === b.alias,
filterInStatelessMode: (desired, current) =>
current.filter(c => desired.some(d => d.alias === c.alias))
}
}
};
}
// ... implement refresh/create/modify/destroy
}
```
**Usage:**
```json
{
"type": "aliases",
"aliases": [
{ "alias": "ll", "value": "ls -la" },
{ "alias": "gs", "value": "git status" }
]
}
```
Key Implementation Details [#key-implementation-details]
The critical part of multi-declaration resources is the `filterInStatelessMode` function:
```typescript
parameterSettings: {
aliases: {
type: 'array',
isElementEqual: (a, b) => a.alias === b.alias && a.value === b.value,
// In stateless mode, only track aliases the user declared
filterInStatelessMode: (desired, current) =>
current.filter(c => desired.some(d => d.alias === c.alias))
}
}
```
This ensures that in **stateless mode**, Codify only manages aliases explicitly declared in the config. If the user has 50 aliases but only declares 2 in Codify, only those 2 are managed.
For **stateful mode**, you don't need filtering—Codify tracks all changes and the full state.
Modify Implementation [#modify-implementation]
Multi-declaration resources typically need a `modify()` implementation to handle array changes efficiently:
```typescript
async modify(
pc: ParameterChange,
plan: ModifyPlan
): Promise {
const { isStateful } = plan;
if (isStateful) {
// In stateful mode, remove deleted items and add new ones
const aliasesToRemove = pc.previousValue?.filter(
a => !pc.newValue?.some(c => c.alias === a.alias)
);
const aliasesToAdd = pc.newValue?.filter(
a => !pc.previousValue?.some(c => c.alias === a.alias)
);
await this.removeAliases(aliasesToRemove);
await this.addAliases(aliasesToAdd);
} else {
// In stateless mode, only update changed values
const aliasesToRemove = pc.previousValue?.filter(
a => pc.newValue?.some(c => c.alias === a.alias && c.value !== a.value)
);
const aliasesToAdd = pc.newValue?.filter(
a => !pc.previousValue?.some(c => c.alias === a.alias) ||
pc.previousValue?.some(c => c.alias === a.alias && c.value !== a.value)
);
await this.removeAliases(aliasesToRemove);
await this.addAliases(aliasesToAdd);
}
}
```
Stateful Parameters [#stateful-parameters]
**Stateful parameters** are parameters that have their own lifecycle—they can be independently created, modified, and destroyed—but are still tied to the parent resource's lifecycle.
**Use this pattern when:**
* A parameter represents installable sub-components (packages, versions, plugins)
* Sub-components can be added/removed independently
* The parent resource manages the environment for sub-components
* You want granular control over parameter changes
**Example use cases**:
* Homebrew formulae (packages installed within Homebrew)
* NVM Node versions (versions managed within NVM)
* Python pip packages (packages installed in a virtualenv)
Parameters with their own lifecycle, tied to the parent resource (e.g., Homebrew formulae, NVM Node versions):
```typescript
import { StatefulParameter } from '@codifycli/plugin-core';
class FormulaeParameter extends StatefulParameter {
async refresh(desired: string[] | null): Promise {
const pty = getPty();
const result = await pty.spawnSafe('brew list --formula');
if (result.status === 'error') return null;
return result.data.split('\n').filter(Boolean);
}
async add(formulae: string[], plan: Plan): Promise {
const pty = getPty();
await pty.spawn(`brew install --formula ${formulae.join(' ')}`);
}
async remove(formulae: string[], plan: Plan): Promise {
const pty = getPty();
await pty.spawn(`brew uninstall --formula ${formulae.join(' ')}`);
}
async modify(newValue: string[], previousValue: string[]): Promise {
// Handle updates
}
}
```
Register in resource settings:
```typescript
getSettings(): ResourceSettings {
return {
id: 'homebrew',
parameterSettings: {
formulae: {
type: 'stateful',
definition: new FormulaeParameter(),
order: 2 // Execute after taps (order: 1)
}
}
};
}
```
How Stateful Parameters Work [#how-stateful-parameters-work]
When a user configures Homebrew with formulae:
```json
{
"type": "homebrew",
"formulae": ["git", "node", "python"]
}
```
The framework:
1. **Refresh Phase**: Calls `FormulaeParameter.refresh()` to get currently installed formulae
2. **Planning Phase**: Compares desired `["git", "node", "python"]` with current state
3. **Apply Phase**:
* If formulae is new → calls `FormulaeParameter.add(["git", "node", "python"])`
* If formulae changed → calls `add()` for new items, `remove()` for deleted items
* If formula values changed → calls `modify()` for changed items
Lifecycle Integration [#lifecycle-integration]
Stateful parameters integrate with the parent resource lifecycle:
**During CREATE:**
```
1. Resource.create() is called
2. FormulaeParameter.add() is called (if formulae specified)
```
**During MODIFY:**
```
1. FormulaeParameter.add() for new formulae
2. FormulaeParameter.remove() for removed formulae
3. FormulaeParameter.modify() for changed formulae
4. Resource.modify() for other parameters
```
**During DESTROY (if removeStatefulParametersBeforeDestroy: true):**
```
1. FormulaeParameter.remove() for all formulae
2. Resource.destroy() is called
```
Real-World Example: NVM Node Versions [#real-world-example-nvm-node-versions]
```typescript
class NodeVersionsParameter extends StatefulParameter {
async refresh(desired: string[] | null): Promise {
const pty = getPty();
// List installed Node versions
const result = await pty.spawnSafe('nvm list');
if (result.status === 'error') return null;
// Parse output like "v18.0.0", "v20.0.0"
const versions = result.data
.split('\n')
.filter(line => line.includes('v'))
.map(line => line.match(/v(\d+\.\d+\.\d+)/)?.[1])
.filter(Boolean);
return versions;
}
async add(versions: string[], plan: Plan): Promise {
const pty = getPty();
for (const version of versions) {
await pty.spawn(`nvm install ${version}`);
}
}
async remove(versions: string[], plan: Plan): Promise {
const pty = getPty();
for (const version of versions) {
await pty.spawn(`nvm uninstall ${version}`);
}
}
async modify(newValue: string[], previousValue: string[]): Promise {
// For version numbers, modification doesn't make sense
// Versions are either added or removed
}
}
```
Order of Execution [#order-of-execution]
Use the `order` property to control the sequence of stateful parameter operations:
```typescript
parameterSettings: {
taps: {
type: 'stateful',
definition: new TapsParameter(),
order: 1 // Install taps first
},
formulae: {
type: 'stateful',
definition: new FormulaeParameter(),
order: 2 // Then install formulae (which may come from taps)
},
casks: {
type: 'stateful',
definition: new CasksParameter(),
order: 3 // Finally install casks
}
}
```
Lower order numbers execute first. This is important when parameters have dependencies on each other.
Parameter Settings [#parameter-settings]
Configure parameter behavior:
```typescript
parameterSettings: {
// Modifiable parameter
version: {
canModify: true
},
// Directory path with transformation
path: {
type: 'directory',
inputTransformation: {
to: (input) => untildify(input), // Expand ~
from: (current) => tildify(current) // Convert to ~
}
},
// Sensitive parameter (hidden in output)
apiKey: {
isSensitive: true
},
// Array parameter with custom equality
tags: {
type: 'array',
isElementEqual: (a, b) => a.name === b.name,
filterInStatelessMode: (desired, current) =>
current.filter(c => desired.some(d => d.name === c.name))
},
// Boolean setting (not tracked in state)
skipCache: {
type: 'boolean',
default: true,
setting: true
}
}
```
Cross-Platform Support [#cross-platform-support]
Platform Detection [#platform-detection]
```typescript
import { Utils } from '@codifycli/plugin-core';
if (Utils.isMacOS()) {
// macOS-specific logic
} else if (Utils.isLinux()) {
// Linux-specific logic
}
```
OS Declaration [#os-declaration]
```typescript
getSettings(): ResourceSettings {
return {
operatingSystems: ['darwin', 'linux'],
linuxDistros: ['ubuntu', 'debian', 'fedora']
};
}
```
Path Handling [#path-handling]
```typescript
import { tildify, untildify } from '@codifycli/plugin-core';
const absolutePath = untildify('~/projects'); // /Users/john/projects
const tildePath = tildify('/Users/john/projects'); // ~/projects
```
Testing [#testing]
Testing is crucial for plugins because they interact with the real system. The `@codifycli/plugin-test` package provides utilities for writing comprehensive tests that verify the entire resource lifecycle.
Test Strategy [#test-strategy]
Codify plugins should have two types of tests:
**Unit Tests** (in `src/**/*.test.ts`):
* Fast, isolated tests
* Test parsing logic, utility functions, data transformations
* No system calls or side effects
* Run with every code change
**Integration Tests** (in `test/**/*.test.ts`):
* Full lifecycle tests against the real system
* Test `create → modify → destroy` flow
* Verify actual system changes
* Slower, require specific prerequisites
Integration Testing with PluginTester [#integration-testing-with-plugintester]
The `PluginTester.fullTest()` method runs a complete resource lifecycle test:
```typescript
import { PluginTester, testSpawn } from '@codifycli/plugin-test';
import { describe, it } from 'vitest';
import path from 'node:path';
describe('Alias resource', () => {
const pluginPath = path.resolve('./src/index.ts');
it('manages shell alias lifecycle', { timeout: 300000 }, async () => {
await PluginTester.fullTest(
pluginPath,
[
{
type: 'alias',
alias: 'my-alias',
value: 'ls -l'
}
],
{
// Validate the CREATE operation
validateApply: async () => {
const { data } = await testSpawn('alias');
expect(data).toContain('my-alias');
expect(data).toContain('ls -l');
// Test that alias actually works
const result = await testSpawn('my-alias');
expect(result.status).toBe('success');
},
// Test the MODIFY operation
testModify: {
modifiedConfigs: [{
type: 'alias',
alias: 'my-alias',
value: 'pwd' // Changed value
}],
validateModify: async () => {
const { data } = await testSpawn('alias');
expect(data).toContain('my-alias');
expect(data).toContain('pwd');
expect(data).not.toContain('ls -l');
}
},
// Validate the DESTROY operation
validateDestroy: async () => {
const { data } = await testSpawn('alias');
expect(data).not.toContain('my-alias');
}
}
);
});
});
```
What fullTest() Does [#what-fulltest-does]
The `fullTest()` method executes this sequence:
1. **Initial Refresh**: Saves current system state
2. **Validate**: Validates configs against schemas
3. **Plan (Create)**: Generates CREATE plan
4. **Apply (Create)**: Executes CREATE operation
5. **Validate Apply**: Runs your `validateApply()` callback
6. **Plan (Modify)**: Generates MODIFY plan with modified configs
7. **Apply (Modify)**: Executes MODIFY operation
8. **Validate Modify**: Runs your `validateModify()` callback
9. **Plan (Destroy)**: Generates DESTROY plan
10. **Apply (Destroy)**: Executes DESTROY operation
11. **Validate Destroy**: Runs your `validateDestroy()` callback
This ensures your resource handles the complete lifecycle correctly.
Testing Configuration Variations [#testing-configuration-variations]
Test different configuration scenarios:
```typescript
it('handles multiple aliases', async () => {
await PluginTester.fullTest(
pluginPath,
[
{
type: 'aliases',
aliases: [
{ alias: 'gs', value: 'git status' },
{ alias: 'gp', value: 'git pull' },
{ alias: 'gc', value: 'git commit' }
]
}
],
{
validateApply: async () => {
const { data } = await testSpawn('alias');
expect(data).toContain('gs=');
expect(data).toContain('gp=');
expect(data).toContain('gc=');
},
testModify: {
modifiedConfigs: [{
type: 'aliases',
aliases: [
{ alias: 'gs', value: 'git status' },
// Removed gp
{ alias: 'gc', value: 'git commit -v' }, // Modified
{ alias: 'gd', value: 'git diff' } // Added
]
}],
validateModify: async () => {
const { data } = await testSpawn('alias');
expect(data).toContain('gs=');
expect(data).not.toContain('gp=');
expect(data).toContain('gc=\'git commit -v\'');
expect(data).toContain('gd=');
}
},
validateDestroy: async () => {
const { data } = await testSpawn('alias');
expect(data).not.toContain('gs=');
expect(data).not.toContain('gc=');
expect(data).not.toContain('gd=');
}
}
);
});
```
Testing Validation Errors [#testing-validation-errors]
Test that your resource properly rejects invalid configurations:
```typescript
it('rejects invalid alias names', async () => {
await expect(async () => {
await PluginTester.fullTest(
pluginPath,
[{
type: 'alias',
alias: 'invalid-name-with-$-symbols',
value: 'ls'
}]
);
}).rejects.toThrow();
});
it('requires both alias and value fields', async () => {
await expect(async () => {
await PluginTester.fullTest(
pluginPath,
[{
type: 'alias',
alias: 'myalias'
// Missing value field
}]
);
}).rejects.toThrow();
});
```
Platform-Specific Tests [#platform-specific-tests]
Test different behaviors on different platforms:
```typescript
import { Utils } from '@codifycli/plugin-core';
it('handles platform-specific installation', async () => {
const expectedCommand = Utils.isMacOS()
? 'brew install my-tool'
: 'apt-get install my-tool';
await PluginTester.fullTest(
pluginPath,
[{ type: 'my-tool' }],
{
validateApply: async () => {
// Verify tool was installed correctly
const result = await testSpawn('which my-tool');
expect(result.status).toBe('success');
}
}
);
});
```
Test Setup and Teardown [#test-setup-and-teardown]
Use Vitest's lifecycle hooks to set up and clean up test environments:
```typescript
import { beforeAll, afterAll, beforeEach, describe, it } from 'vitest';
describe('Homebrew tests', () => {
beforeAll(async () => {
// Ensure Homebrew is installed before running tests
const result = await testSpawn('which brew');
if (result.status === 'error') {
throw new Error('Homebrew must be installed to run these tests');
}
});
afterAll(async () => {
// Clean up any test artifacts
await testSpawn('brew cleanup');
});
beforeEach(() => {
// Reset state before each test
});
it('installs formulae', async () => {
// Test implementation
});
});
```
Testing with Real Files [#testing-with-real-files]
When testing resources that modify files, use temporary directories:
```typescript
import os from 'node:os';
import path from 'node:path';
import fs from 'node:fs/promises';
it('creates config file', async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'codify-test-'));
try {
await PluginTester.fullTest(
pluginPath,
[{
type: 'my-config',
configPath: path.join(tempDir, 'config.json'),
setting: 'value'
}],
{
validateApply: async () => {
const configExists = await fs.access(
path.join(tempDir, 'config.json')
).then(() => true).catch(() => false);
expect(configExists).toBe(true);
const config = JSON.parse(
await fs.readFile(path.join(tempDir, 'config.json'), 'utf8')
);
expect(config.setting).toBe('value');
},
validateDestroy: async () => {
const configExists = await fs.access(
path.join(tempDir, 'config.json')
).then(() => true).catch(() => false);
expect(configExists).toBe(false);
}
}
);
} finally {
// Clean up temp directory
await fs.rm(tempDir, { recursive: true, force: true });
}
});
```
Unit Testing Utilities [#unit-testing-utilities]
For utility functions and parsing logic, write simple unit tests:
```typescript
import { describe, it, expect } from 'vitest';
import { parseBrewList } from './homebrew-utils.js';
describe('parseBrewList', () => {
it('parses brew list output', () => {
const output = `git
node
python@3.11`;
const result = parseBrewList(output);
expect(result).toEqual(['git', 'node', 'python@3.11']);
});
it('handles empty output', () => {
expect(parseBrewList('')).toEqual([]);
});
it('filters out warnings', () => {
const output = `Warning: Some warning message
git
node`;
const result = parseBrewList(output);
expect(result).toEqual(['git', 'node']);
});
});
```
Best Practices for Testing [#best-practices-for-testing]
1. **Use long timeouts for integration tests**: System operations can be slow
```typescript
it('installs packages', { timeout: 300000 }, async () => {
// Test implementation
});
```
2. **Clean up after tests**: Always restore system state
```typescript
afterAll(async () => {
await cleanup();
});
```
3. **Test edge cases**: Empty configs, missing fields, invalid values
4. **Test error conditions**: What happens when commands fail?
5. **Use descriptive test names**: Make failures easy to understand
```typescript
it('installs Homebrew in custom directory and adds to PATH', async () => {
// ...
});
```
6. **Isolate tests**: Each test should be independent and not rely on others
7. **Mock external dependencies in unit tests**: Use mocking for fast, deterministic unit tests
Project Structure [#project-structure]
```
my-plugin/
├── src/
│ ├── index.ts # Plugin entry point
│ ├── resources/
│ │ ├── my-resource/
│ │ │ ├── my-resource.ts
│ │ │ └── my-resource-schema.json
│ │ └── ...
│ └── utils/
├── test/
│ └── my-resource.test.ts
├── package.json
├── tsconfig.json
└── vitest.config.ts
```
package.json [#packagejson]
```json
{
"name": "my-codify-plugin",
"version": "1.0.0",
"type": "module",
"main": "dist/index.js",
"dependencies": {
"@codifycli/plugin-core": "^1.0.0",
"@codifycli/schemas": "^1.0.0"
},
"devDependencies": {
"@codifycli/plugin-test": "^1.0.0",
"typescript": "^5.0.0",
"vitest": "^2.0.0"
}
}
```
tsconfig.json [#tsconfigjson]
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test"]
}
```
Building and Deployment [#building-and-deployment]
Build Script [#build-script]
```bash
npm run build
```
Use Rollup to bundle your plugin:
```javascript
// rollup.config.js
export default {
input: 'src/index.ts',
output: {
file: 'dist/index.js',
format: 'esm'
},
external: [
'@codifycli/plugin-core',
'@codifycli/schemas',
/^node:.*/
]
};
```
Best Practices [#best-practices]
Error Handling [#error-handling]
Proper error handling makes your plugin reliable and helps users understand what went wrong.
**Provide context in error messages:**
```typescript
async create(plan: CreatePlan): Promise {
const pty = getPty();
try {
await pty.spawn('install-command');
} catch (error) {
// Bad: Generic error
throw error;
// Good: Contextual error with actionable information
throw new Error(
`Failed to install ${plan.desiredConfig.name}. ` +
`Ensure you have internet connectivity and sufficient disk space. ` +
`Original error: ${error.message}`
);
}
}
```
**Validate prerequisites in refresh():**
```typescript
async refresh(parameters: Partial): Promise | null> {
const pty = getPty();
// Check if parent resource exists
const brewInstalled = await pty.spawnSafe('which brew');
if (brewInstalled.status === 'error') {
throw new Error(
'Homebrew is required but not installed. ' +
'Add a homebrew resource to your config first.'
);
}
// Continue with refresh...
}
```
**Handle partial failures gracefully:**
```typescript
async create(plan: CreatePlan): Promise {
const pty = getPty();
const packages = plan.desiredConfig.packages;
const failed: string[] = [];
for (const pkg of packages) {
try {
await pty.spawn(`install ${pkg}`);
} catch (error) {
failed.push(pkg);
console.error(`Failed to install ${pkg}: ${error.message}`);
}
}
if (failed.length > 0) {
throw new Error(
`Failed to install packages: ${failed.join(', ')}. ` +
`Successfully installed: ${packages.filter(p => !failed.includes(p)).join(', ')}`
);
}
}
```
Resource Dependencies [#resource-dependencies]
Dependencies ensure resources are applied in the correct order. Codify automatically sorts resources based on their dependency graph.
**Declare dependencies explicitly:**
```typescript
getSettings(): ResourceSettings {
return {
id: 'git-repository',
// These resources must exist before cloning repos
dependencies: ['ssh-key', 'git'],
};
}
```
**Use dependencies for shared resources:**
```typescript
getSettings(): ResourceSettings {
return {
id: 'pip-package',
// Ensure Python and pip are installed first
dependencies: ['python', 'pyenv'],
};
}
```
**Dependencies are transitive:**
If resource A depends on B, and B depends on C, Codify ensures C → B → A execution order.
**Circular dependencies are detected:**
Codify will throw an error if you create circular dependencies, preventing infinite loops.
Refresh Return Semantics [#refresh-return-semantics]
The value you return from `refresh()` determines how Codify plans operations. Follow these conventions:
**Return `null` when resource doesn't exist:**
```typescript
async refresh(parameters: Partial): Promise | null> {
const pty = getPty();
const result = await pty.spawnSafe('check-installation');
if (result.status === 'error') {
return null; // Will trigger CREATE operation
}
return {
version: parseVersion(result.data)
};
}
```
**Return `{}` for resources with no trackable parameters:**
```typescript
async refresh(parameters: Partial): Promise | null> {
const pty = getPty();
// Check if Docker is running
const result = await pty.spawnSafe('docker info');
if (result.status === 'error') {
return null; // Docker not installed
}
// Docker is installed but has no parameters to track
return {};
}
```
**Return partial config for existing resources:**
```typescript
async refresh(parameters: Partial): Promise | null> {
const pty = getPty();
const nameResult = await pty.spawnSafe('git config --global user.name');
const emailResult = await pty.spawnSafe('git config --global user.email');
// Git is installed but not configured
if (nameResult.status === 'error' && emailResult.status === 'error') {
return {}; // Will trigger CREATE with all parameters
}
// Return only configured parameters
return {
userName: nameResult.status === 'success' ? nameResult.data.trim() : undefined,
userEmail: emailResult.status === 'success' ? emailResult.data.trim() : undefined,
};
}
```
**Only query requested parameters:**
```typescript
async refresh(parameters: Partial): Promise | null> {
const result: Partial = {};
// Only check directory if user specified it
if (parameters.directory) {
result.directory = await this.getCurrentDirectory();
}
// Don't query other parameters that weren't requested
return result;
}
```
Declarative Mode Filtering [#declarative-mode-filtering]
For array parameters in stateless mode, use `filterInStatelessMode` to implement declarative behavior:
```typescript
parameterSettings: {
paths: {
type: 'array',
canModify: true,
// Only track paths that user explicitly declared
filterInStatelessMode: (desired, current) =>
current.filter(c => desired.some(d => d === c))
}
}
```
This pattern ensures Codify only manages items the user explicitly declared, ignoring other system state.
**For complex objects:**
```typescript
parameterSettings: {
aliases: {
type: 'array',
itemType: 'object',
// Match by alias name, not value
isElementEqual: (a, b) => a.alias === b.alias,
filterInStatelessMode: (desired, current) =>
current.filter(c => desired.some(d => d.alias === c.alias))
}
}
```
Idempotency [#idempotency]
Ensure your resources can be applied multiple times safely:
```typescript
async create(plan: CreatePlan): Promise {
const pty = getPty();
// Bad: Fails if directory already exists
await pty.spawn(`mkdir ${plan.desiredConfig.directory}`);
// Good: Idempotent - safe to run multiple times
await pty.spawn(`mkdir -p ${plan.desiredConfig.directory}`);
}
```
**Check before modifying:**
```typescript
async create(plan: CreatePlan): Promise {
const shellRcPath = Utils.getPrimaryShellRc();
// Create file if it doesn't exist
if (!(await fileExists(shellRcPath))) {
await fs.writeFile(shellRcPath, '', 'utf8');
}
// Check if alias already exists
const contents = await fs.readFile(shellRcPath, 'utf8');
const aliasString = `alias ${plan.desiredConfig.alias}='${plan.desiredConfig.value}'`;
if (!contents.includes(aliasString)) {
await FileUtils.addToStartupFile(aliasString);
}
}
```
Performance Optimization [#performance-optimization]
**Use spawnSafe() efficiently:**
```typescript
// Bad: Sequential checks
const hasGit = await pty.spawnSafe('which git');
const hasNode = await pty.spawnSafe('which node');
const hasPython = await pty.spawnSafe('which python');
// Good: Single command
const result = await pty.spawnSafe('which git node python');
```
**Cache expensive operations:**
```typescript
private cachedBrewInfo: string | null = null;
async getBrewInfo(): Promise {
if (this.cachedBrewInfo) {
return this.cachedBrewInfo;
}
const pty = getPty();
const result = await pty.spawn('brew config');
this.cachedBrewInfo = result.data;
return result.data;
}
```
Security Considerations [#security-considerations]
**Sanitize user input:**
```typescript
async create(plan: CreatePlan): Promise {
const pty = getPty();
const { directory, name } = plan.desiredConfig;
// Bad: Command injection vulnerability
await pty.spawn(`mkdir ${directory}/${name}`);
// Good: Validate and sanitize
if (name.includes('/') || name.includes('..')) {
throw new Error('Invalid name: must not contain / or ..');
}
await pty.spawn(`mkdir -p "${path.join(directory, name)}"`);
}
```
**Use requiresRoot judiciously:**
```typescript
// Only require root when absolutely necessary
async create(plan: CreatePlan): Promise {
const pty = getPty();
// Install to user directory - no sudo needed
if (plan.desiredConfig.directory.startsWith(os.homedir())) {
await pty.spawn(`./install.sh --prefix ${plan.desiredConfig.directory}`);
} else {
// System directory - need sudo
await pty.spawn(
`./install.sh --prefix ${plan.desiredConfig.directory}`,
{ requiresRoot: true }
);
}
}
```
**Mark sensitive parameters:**
```typescript
getSettings(): ResourceSettings {
return {
id: 'aws-profile',
isSensitive: true, // Prevents auto-import
parameterSettings: {
awsAccessKeyId: {
isSensitive: true // Hides value in plan output
},
awsSecretAccessKey: {
isSensitive: true
}
}
};
}
```
# Building and deployment
# Core concepts
Plugin [#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.
```typescript
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 [#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` abstract class and must implement these lifecycle methods:
Required 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 [#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 [#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 [#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) [#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
```json
{
"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 [#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
```json
{
"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 [#configuring-modes]
Mode is typically controlled by the resource implementation. For array parameters, you can use `filterInStatelessMode` to configure stateless behavior:
```typescript
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 [#next-steps]
* [Resource Lifecycle](/docs/developer-guide/plugin-development/resource-lifecycle) - Detailed guide to implementing lifecycle methods
* [Resource Patterns](/docs/developer-guide/plugin-development/resource-patterns) - Learn about different resource architectures
# Cross-platform support
Cross-Platform Support [#cross-platform-support]
Platform Detection [#platform-detection]
```typescript
import { Utils } from '@codifycli/plugin-core';
if (Utils.isMacOS()) {
// macOS-specific logic
} else if (Utils.isLinux()) {
// Linux-specific logic
}
```
OS Declaration [#os-declaration]
```typescript
getSettings(): ResourceSettings {
return {
operatingSystems: ['darwin', 'linux'],
linuxDistros: ['ubuntu', 'debian', 'fedora']
};
}
```
Path Handling [#path-handling]
```typescript
import { tildify, untildify } from '@codifycli/plugin-core';
const absolutePath = untildify('~/projects'); // /Users/john/projects
const tildePath = tildify('/Users/john/projects'); // ~/projects
```
# Examples
Examples [#examples]
See real-world examples in the [default plugin repository](https://github.com/kevinwang5658/codify-homebrew-plugin):
* **Simple**: [Alias Resource](https://github.com/kevinwang5658/codify-homebrew-plugin/blob/main/src/resources/shell/alias/alias-resource.ts)
* **Multi-declaration**: [Aliases Resource](https://github.com/kevinwang5658/codify-homebrew-plugin/blob/main/src/resources/shell/aliases/aliases-resource.ts)
* **Stateful**: [Homebrew Resource](https://github.com/kevinwang5658/codify-homebrew-plugin/blob/main/src/resources/homebrew/homebrew.ts)
* **Complex**: [Git Repository Resource](https://github.com/kevinwang5658/codify-homebrew-plugin/blob/main/src/resources/git/repository/git-repository.ts)
# Plugin development
Codify plugins extend the CLI's capabilities by implementing resources that can be created, modified, and destroyed on a system. This guide covers everything you need to build your own plugin.
Overview [#overview]
A Codify plugin is a Node.js module that extends Codify's functionality by implementing **resources**—manageable entities on your system such as applications, CLI tools, or configuration settings. Think of plugins as the bridge between Codify's declarative configuration syntax and the actual system commands needed to install and configure software.
Architecture [#architecture]
Codify uses a **multi-process plugin architecture** for security and isolation:
* **CLI Process**: The main Codify CLI runs in the parent process, orchestrating the workflow, managing user interaction, and coordinating plugins.
* **Plugin Process**: Each plugin runs in an isolated child process, communicating with the CLI via Inter-Process Communication (IPC). This ensures plugins can't directly access privileged operations without user approval.
* **Type-Safe IPC**: All messages between the CLI and plugins are validated against JSON schemas, ensuring data integrity and preventing malformed requests.
This architecture means your plugin:
* Defines one or more **resources** (applications, CLI tools, settings)
* Runs in an isolated child process and communicates with the CLI via IPC
* Implements a standardized lifecycle: validate → plan → apply
* Uses the `@codifycli/plugin-core` framework for resource management
How Plugins Work [#how-plugins-work]
When a user runs `codify apply`, the following workflow occurs:
1. **Parse Configuration**: The CLI reads the user's `codify.jsonc` file
2. **Initialize Plugins**: For each resource type in the config, the CLI spawns the appropriate plugin process
3. **Validate**: Plugins validate the user's configuration against their schemas
4. **Refresh**: Plugins query the current system state for each resource
5. **Plan**: Plugins compare desired state vs current state and generate a change set
6. **Apply**: After user approval, plugins execute the planned changes sequentially
Your plugin participates in each of these phases by implementing the `Resource` interface.
Prerequisites [#prerequisites]
* Node.js ≥ 22.0.0
* TypeScript knowledge
* Understanding of [Codify basics](/docs/user-guide/overview)
What's Next [#whats-next]
This guide is organized into the following sections:
* **[Quick Start](/docs/developer-guide/plugin-development/quick-start)** - Build your first plugin in minutes
* **[Core Concepts](/docs/developer-guide/plugin-development/core-concepts)** - Understand plugins, resources, plans, and modes
* **[Resource Lifecycle](/docs/developer-guide/plugin-development/resource-lifecycle)** - Implement the resource lifecycle methods
* **[Schema Validation](/docs/developer-guide/plugin-development/schema-validation)** - Validate user configurations
* **[PTY Abstraction](/docs/developer-guide/plugin-development/pty-abstraction)** - Execute shell commands
* **[Resource Patterns](/docs/developer-guide/plugin-development/resource-patterns)** - Common plugin patterns and architectures
* **[Parameter Settings](/docs/developer-guide/plugin-development/parameter-settings)** - Configure parameter behavior
* **[Cross-Platform Support](/docs/developer-guide/plugin-development/cross-platform)** - Build plugins for macOS and Linux
* **[Testing](/docs/developer-guide/plugin-development/testing)** - Test your plugins thoroughly
* **[Project Structure](/docs/developer-guide/plugin-development/project-structure)** - Organize your plugin project
* **[Building and Deployment](/docs/developer-guide/plugin-development/building-deployment)** - Package and distribute your plugin
* **[Best Practices](/docs/developer-guide/plugin-development/best-practices)** - Write reliable, maintainable plugins
* **[API Reference](/docs/developer-guide/plugin-development/api-reference)** - Quick reference for classes and utilities
* **[Examples](/docs/developer-guide/plugin-development/examples)** - Real-world plugin examples
Quick Links [#quick-links]
* [plugin-core README](https://github.com/codifycli/plugin-core) - Detailed API documentation
* [Default plugin source](https://github.com/kevinwang5658/codify-homebrew-plugin) - 50+ resource examples
* [Plugin Registry](/docs/developer-guide/registry) - Discover existing plugins
# Parameter settings
Parameter Settings [#parameter-settings]
Configure parameter behavior:
```typescript
parameterSettings: {
// Modifiable parameter
version: {
canModify: true
},
// Directory path with transformation
path: {
type: 'directory',
inputTransformation: {
to: (input) => untildify(input), // Expand ~
from: (current) => tildify(current) // Convert to ~
}
},
// Sensitive parameter (hidden in output)
apiKey: {
isSensitive: true
},
// Array parameter with custom equality
tags: {
type: 'array',
isElementEqual: (a, b) => a.name === b.name,
filterInStatelessMode: (desired, current) =>
current.filter(c => desired.some(d => d.name === c.name))
},
// Boolean setting (not tracked in state)
skipCache: {
type: 'boolean',
default: true,
setting: true
}
}
```
# Project structure
Project Structure [#project-structure]
```
my-plugin/
├── src/
│ ├── index.ts # Plugin entry point
│ ├── resources/
│ │ ├── my-resource/
│ │ │ ├── my-resource.ts
│ │ │ └── my-resource-schema.json
│ │ └── ...
│ └── utils/
├── test/
│ └── my-resource.test.ts
├── package.json
├── tsconfig.json
└── vitest.config.ts
```
package.json [#packagejson]
```json
{
"name": "my-codify-plugin",
"version": "1.0.0",
"type": "module",
"main": "dist/index.js",
"dependencies": {
"@codifycli/plugin-core": "^1.0.0",
"@codifycli/schemas": "^1.0.0"
},
"devDependencies": {
"@codifycli/plugin-test": "^1.0.0",
"typescript": "^5.0.0",
"vitest": "^2.0.0"
}
}
```
tsconfig.json [#tsconfigjson]
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test"]
}
```
# PTY abstraction
PTY Abstraction [#pty-abstraction]
The **PTY (Pseudo-Terminal)** abstraction is how plugins execute shell commands. Instead of using Node's `child_process` directly, you use `getPty()` to get a PTY instance that handles stdout/stderr streaming, error handling, and privilege escalation.
Why PTY Instead of child_process? [#why-pty-instead-of-child_process]
1. **Interactive Commands**: PTY supports commands that require user input (like installers)
2. **Output Streaming**: Real-time stdout/stderr is streamed to the user's terminal
3. **Privilege Escalation**: Automatic handling of `sudo` password prompts
4. **Context Awareness**: Different PTY implementations for planning vs applying
Getting the PTY Instance [#getting-the-pty-instance]
```typescript
import { getPty } from '@codifycli/plugin-core';
const pty = getPty();
```
The `getPty()` function uses async local storage to provide the correct PTY instance for the current context:
* **BackgroundPty** during `refresh()` - Allows parallel execution for faster planning
* **SequentialPty** during `create()`, `modify()`, `destroy()` - Executes commands sequentially with proper error handling
spawn() - Execute with Error Handling [#spawn---execute-with-error-handling]
Use `spawn()` when you expect the command to succeed. If it fails (non-zero exit code), an error is thrown and execution stops.
```typescript
const pty = getPty();
// Throws on non-zero exit code
const result = await pty.spawn('brew install jq');
console.log(result.data); // stdout output as string
```
**When to use `spawn()`:**
* During `create()`, `modify()`, `destroy()` when failure should stop execution
* When the command must succeed for the operation to be valid
* When you want automatic error propagation
spawnSafe() - Execute Without Throwing [#spawnsafe---execute-without-throwing]
Use `spawnSafe()` when you need to check if something exists or when failure is an expected outcome.
```typescript
const pty = getPty();
// Never throws - returns status object
const result = await pty.spawnSafe('which jq');
if (result.status === SpawnStatus.SUCCESS) {
console.log('jq is installed at:', result.data);
} else {
console.log('jq not found');
}
```
**When to use `spawnSafe()`:**
* During `refresh()` to check if resources exist
* When checking prerequisites or system state
* When multiple outcomes are valid
Spawn Options [#spawn-options]
Both `spawn()` and `spawnSafe()` accept an options object:
```typescript
await pty.spawn('npm install', {
// Working directory for the command
cwd: '/path/to/project',
// Environment variables (merged with current env)
env: { NODE_ENV: 'production' },
// Allow interactive input (prompts, confirmations)
interactive: true,
// Require root/sudo privileges
requiresRoot: true,
// Provide input to stdin
stdin: true,
});
```
Option Details [#option-details]
**`cwd`** - Change working directory:
```typescript
// Clone a repo into a specific directory
await pty.spawn('git clone https://github.com/user/repo.git', {
cwd: '/Users/john/projects'
});
```
**`env`** - Set environment variables:
```typescript
// Run installer non-interactively
await pty.spawn('./install.sh', {
env: { NONINTERACTIVE: 1 }
});
```
**`interactive`** - Allow user input:
```typescript
// Run installer that may prompt for choices
await pty.spawn('/bin/bash -c "$(curl -fsSL https://install.sh)"', {
interactive: true,
stdin: true
});
```
**`requiresRoot`** - Execute with sudo:
```typescript
// Install to system directory
await pty.spawn('cp binary /usr/local/bin/', {
requiresRoot: true
});
```
When `requiresRoot: true`, Codify:
1. Sends a message to the parent CLI process
2. CLI prompts user for their sudo password
3. Password is sent back to plugin securely
4. Command executes with sudo
This ensures plugins never have direct access to sudo—they must request it through the parent process.
Real-World Examples [#real-world-examples]
Checking if a tool is installed: [#checking-if-a-tool-is-installed]
```typescript
async refresh(parameters: Partial): Promise | null> {
const pty = getPty();
// Check if tool exists
const result = await pty.spawnSafe('which my-tool');
if (result.status === SpawnStatus.ERROR) {
return null; // Not installed
}
// Get version
const versionResult = await pty.spawnSafe('my-tool --version');
const version = versionResult.data.match(/v(\d+\.\d+\.\d+)/)?.[1];
return { version };
}
```
Installing with platform-specific commands: [#installing-with-platform-specific-commands]
```typescript
async create(plan: CreatePlan): Promise {
const pty = getPty();
if (Utils.isMacOS()) {
await pty.spawn('brew install my-tool');
} else if (Utils.isLinux()) {
await pty.spawn('apt-get install -y my-tool', {
requiresRoot: true
});
}
}
```
Running commands in a specific directory: [#running-commands-in-a-specific-directory]
```typescript
async create(plan: CreatePlan): Promise {
const pty = getPty();
const projectPath = plan.desiredConfig.directory;
// Create directory
await pty.spawn(`mkdir -p ${projectPath}`);
// Initialize project in that directory
await pty.spawn('npm init -y', { cwd: projectPath });
await pty.spawn('npm install express', { cwd: projectPath });
}
```
PTY Implementation Details [#pty-implementation-details]
Codify uses two PTY implementations:
**BackgroundPty** (during planning):
* Allows parallel command execution for faster refresh operations
* Automatically killed after planning completes
* Used in `refresh()`, `validate()`
**SequentialPty** (during apply):
* Executes commands one at a time, in order
* Ensures proper error handling and output streaming
* Used in `create()`, `modify()`, `destroy()`
You don't need to worry about which PTY you're using—`getPty()` provides the correct one automatically based on context.
# Quick start
Installation [#installation]
First, install the plugin SDK in your Node.js project:
```bash
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 [#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 [#the-problem]
Users often need to configure git with their name and email. Traditionally, this requires running:
```bash
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`:
```json
{
"type": "git-config",
"userName": "John Doe",
"userEmail": "john@example.com"
}
```
Here's the complete plugin implementation:
```typescript
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 {
getSettings(): ResourceSettings {
return {
id: 'git-config',
operatingSystems: ['darwin', 'linux'],
schema: {
type: 'object',
properties: {
userName: { type: 'string' },
userEmail: { type: 'string' }
}
}
};
}
async refresh(parameters: Partial) {
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 [#understanding-the-example]
Let's break down each part of this plugin:
1. Configuration Interface [#1-configuration-interface]
```typescript
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 [#2-resource-settings]
```typescript
getSettings(): ResourceSettings {
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) [#3-refresh-query-current-state]
```typescript
async refresh(parameters: Partial) {
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) [#4-create-installconfigure]
```typescript
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) [#5-destroy-removeuninstall]
```typescript
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 [#6-plugin-registration]
```typescript
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 [#using-your-plugin]
Users can now use your resource in their `codify.jsonc`:
```json
{
"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 [#next-steps]
Now that you've built your first plugin, learn more about:
* [Core Concepts](/docs/developer-guide/plugin-development/core-concepts) - Deep dive into plugins, resources, and plans
* [Resource Lifecycle](/docs/developer-guide/plugin-development/resource-lifecycle) - Detailed guide to implementing lifecycle methods
* [Testing](/docs/developer-guide/plugin-development/testing) - How to test your plugin thoroughly
# Resource lifecycle
Resource Lifecycle Methods [#resource-lifecycle-methods]
getSettings() [#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.
```typescript
getSettings(): ResourceSettings {
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 [#common-configuration-patterns]
**Simple resource with no special behavior:**
```typescript
getSettings(): ResourceSettings {
return {
id: 'simple-tool',
operatingSystems: ['darwin', 'linux'],
schema: MySchema,
};
}
```
**Resource with dependencies:**
```typescript
getSettings(): ResourceSettings {
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):**
```typescript
getSettings(): ResourceSettings {
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() [#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 plan` to generate the change set
* During `codify apply` before execution
* During `codify import` to discover existing resources
**What to return:**
* **`null`** - Resource doesn't exist on the system
* **`{}`** - Resource exists but has no trackable parameters
* **`Partial`** - 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.
```typescript
async refresh(parameters: Partial): Promise | 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 [#real-world-example-homebrew-resource]
```typescript
async refresh(parameters: Partial): Promise | 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 = {}
// 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 [#refresh-for-resources-with-allowmultiple]
When your resource allows multiple instances (like multiple git repositories), `refresh()` can return an array:
```typescript
async refresh(parameters: Partial): Promise | 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() [#best-practices-for-refresh]
1. **Return `null` for non-existent resources** - This is how Codify knows to create the resource.
2. **Query only requested parameters** - Check the `parameters` argument to see what the user configured. Don't query everything.
3. **Handle errors gracefully** - If a command fails, return `null` rather than throwing (unless it's an unexpected error).
4. **Be efficient** - This method is called during every plan operation. Avoid expensive operations if possible.
5. **Parse output carefully** - System commands can return different formats. Use robust parsing logic.
create() [#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 apply` when the resource operation is `CREATE`
* 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 wants
* `plan.isStateful` - Whether this is a stateful operation
* `plan.id` - Unique identifier for this specific resource instance
```typescript
async create(plan: CreatePlan): Promise {
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 [#real-world-example-homebrew-installation]
```typescript
async create(plan: CreatePlan): Promise {
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 [#handling-stateful-parameters]
If your resource has stateful parameters (like Homebrew's formulae), the framework automatically calls their `add()` methods after `create()` completes:
```typescript
// In create(), just install the base resource
async create(plan: CreatePlan): Promise {
await this.installHomebrew();
// Don't install formulae here - the FormulaeParameter.add() handles that
}
```
destroy() [#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 apply` when the resource operation is `DESTROY`
* 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?
```typescript
async destroy(plan: DestroyPlan): Promise {
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 [#real-world-example-git-repository-resource]
The git-repository resource refuses to delete directories because it could destroy user work:
```typescript
async destroy(plan: DestroyPlan): Promise {
// 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 [#handling-stateful-parameters-1]
If `removeStatefulParametersBeforeDestroy` is true in your resource settings, stateful parameters' `remove()` methods are called before `destroy()`:
```typescript
getSettings(): ResourceSettings {
return {
id: 'homebrew',
removeStatefulParametersBeforeDestroy: true, // Uninstall formulae first
// ...
};
}
async destroy(plan: DestroyPlan): Promise {
// 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() [#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 apply` when the resource operation is `MODIFY`
* Once per parameter that changed
* Only for parameters where `canModify: true` in parameter settings
**What you receive:**
* `parameterChange.name` - Which parameter changed
* `parameterChange.operation` - ADD, REMOVE, or MODIFY
* `parameterChange.newValue` - The new value
* `parameterChange.previousValue` - The old value
* `plan` - Full plan context with desired and current configs
```typescript
async modify(
parameterChange: ParameterChange,
plan: ModifyPlan
): Promise {
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 [#real-world-example-alias-resource]
The alias resource modifies the alias value by finding and replacing the line in the shell RC file:
```typescript
async modify(
pc: ParameterChange,
plan: ModifyPlan
): Promise {
// 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() [#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.
# Resource patterns
Resource Patterns [#resource-patterns]
Codify supports several resource patterns to handle different use cases. Understanding these patterns will help you design resources that match user expectations.
Simple Singleton [#simple-singleton]
The **simple singleton** pattern is the most basic resource type. Each config entry creates exactly one resource instance on the system.
**Use this pattern when:**
* Each resource is unique and independent
* Users manage resources one at a time
* No system-wide discovery is needed
**Example use case**: A single git global configuration per system.
One resource instance per config entry:
```typescript
class AliasResource extends Resource {
getSettings(): ResourceSettings {
return {
id: 'alias',
operatingSystems: ['darwin', 'linux'],
schema: {
type: 'object',
properties: {
alias: { type: 'string' },
value: { type: 'string' }
},
required: ['alias', 'value']
},
allowMultiple: {
identifyingParameters: ['alias'] // Each unique alias = different resource
}
};
}
// ... implement refresh/create/destroy
}
```
**Usage:**
```json
[
{ "type": "alias", "alias": "ll", "value": "ls -la" },
{ "type": "alias", "alias": "gs", "value": "git status" }
]
```
Each config entry creates a separate resource. The `alias` field uniquely identifies each resource—two aliases with the same name would conflict.
Multiple Instances with Custom Matcher [#multiple-instances-with-custom-matcher]
The **custom matcher** pattern allows resources to exist multiple times on the system with custom logic for matching desired configs to existing resources. This is more flexible than `identifyingParameters` when matching logic is complex.
**Use this pattern when:**
* Multiple instances of a resource can coexist
* Matching logic is more complex than simple field equality
* You need platform-specific matching (e.g., case-insensitive paths on macOS)
* You want to auto-discover existing resources on the system
**Example use case**: Git repositories can exist in multiple directories, and on macOS paths are case-insensitive.
Use custom logic to match desired configs with system state:
```typescript
getSettings(): ResourceSettings {
return {
id: 'my-resource',
allowMultiple: {
// Custom matching logic
matcher: (desired, current) => {
return desired.directory === current.directory;
},
// Auto-discovery for `codify import`
async findAllParameters() {
// Discover all instances on system
const instances = await discoverInstances();
return instances.map(i => ({ directory: i.path }));
}
}
};
}
```
Real-World Example: Git Repository with Platform-Aware Matching [#real-world-example-git-repository-with-platform-aware-matching]
```typescript
getSettings(): ResourceSettings {
return {
id: 'git-repository',
allowMultiple: {
matcher: (desired, current) => {
// Get absolute paths for comparison
const desiredPath = path.resolve(desired.directory);
const currentPath = path.resolve(current.directory);
// macOS is case-insensitive, Linux is case-sensitive
if (process.platform === 'darwin') {
return desiredPath.toLowerCase() === currentPath.toLowerCase();
}
return desiredPath === currentPath;
},
async findAllParameters() {
const pty = getPty();
// Find all git repos in home directory
const { data } = await pty.spawnSafe(
'find ~ -name .git -type d -not -path "*/Library/*"'
);
const directories = data
.split('\n')
.filter(Boolean)
.map(p => path.dirname(p))
.map(directory => ({ directory }));
return directories;
}
}
};
}
```
The `findAllParameters()` method enables `codify import` to discover existing resources automatically. When users run `codify import`, Codify calls this method and generates config entries for all found instances.
Multi-Declaration Resources [#multi-declaration-resources]
The **multi-declaration** pattern allows users to manage multiple related items in a single config entry. This is cleaner than requiring separate config entries for each item.
**Use this pattern when:**
* Users typically manage groups of similar items together
* Items are closely related and share common settings
* Individual items don't need separate configurations
* You want to offer both declarative and stateful modes
**Example use case**: Managing multiple shell aliases as a group.
Manage multiple items in a single resource:
```typescript
const schema = z.object({
aliases: z.array(z.object({
alias: z.string(),
value: z.string()
}))
});
type AliasesConfig = z.infer;
class AliasesResource extends Resource {
getSettings(): ResourceSettings {
return {
id: 'aliases',
schema,
parameterSettings: {
aliases: {
type: 'array',
canModify: true,
isElementEqual: (a, b) => a.alias === b.alias,
filterInStatelessMode: (desired, current) =>
current.filter(c => desired.some(d => d.alias === c.alias))
}
}
};
}
// ... implement refresh/create/modify/destroy
}
```
**Usage:**
```json
{
"type": "aliases",
"aliases": [
{ "alias": "ll", "value": "ls -la" },
{ "alias": "gs", "value": "git status" }
]
}
```
Key Implementation Details [#key-implementation-details]
The critical part of multi-declaration resources is the `filterInStatelessMode` function:
```typescript
parameterSettings: {
aliases: {
type: 'array',
isElementEqual: (a, b) => a.alias === b.alias && a.value === b.value,
// In stateless mode, only track aliases the user declared
filterInStatelessMode: (desired, current) =>
current.filter(c => desired.some(d => d.alias === c.alias))
}
}
```
This ensures that in **stateless mode**, Codify only manages aliases explicitly declared in the config. If the user has 50 aliases but only declares 2 in Codify, only those 2 are managed.
For **stateful mode**, you don't need filtering—Codify tracks all changes and the full state.
Modify Implementation [#modify-implementation]
Multi-declaration resources typically need a `modify()` implementation to handle array changes efficiently:
```typescript
async modify(
pc: ParameterChange,
plan: ModifyPlan
): Promise {
const { isStateful } = plan;
if (isStateful) {
// In stateful mode, remove deleted items and add new ones
const aliasesToRemove = pc.previousValue?.filter(
a => !pc.newValue?.some(c => c.alias === a.alias)
);
const aliasesToAdd = pc.newValue?.filter(
a => !pc.previousValue?.some(c => c.alias === a.alias)
);
await this.removeAliases(aliasesToRemove);
await this.addAliases(aliasesToAdd);
} else {
// In stateless mode, only update changed values
const aliasesToRemove = pc.previousValue?.filter(
a => pc.newValue?.some(c => c.alias === a.alias && c.value !== a.value)
);
const aliasesToAdd = pc.newValue?.filter(
a => !pc.previousValue?.some(c => c.alias === a.alias) ||
pc.previousValue?.some(c => c.alias === a.alias && c.value !== a.value)
);
await this.removeAliases(aliasesToRemove);
await this.addAliases(aliasesToAdd);
}
}
```
Stateful Parameters [#stateful-parameters]
**Stateful parameters** are parameters that have their own lifecycle—they can be independently created, modified, and destroyed—but are still tied to the parent resource's lifecycle.
**Use this pattern when:**
* A parameter represents installable sub-components (packages, versions, plugins)
* Sub-components can be added/removed independently
* The parent resource manages the environment for sub-components
* You want granular control over parameter changes
**Example use cases**:
* Homebrew formulae (packages installed within Homebrew)
* NVM Node versions (versions managed within NVM)
* Python pip packages (packages installed in a virtualenv)
Parameters with their own lifecycle, tied to the parent resource (e.g., Homebrew formulae, NVM Node versions):
```typescript
import { StatefulParameter } from '@codifycli/plugin-core';
class FormulaeParameter extends StatefulParameter {
async refresh(desired: string[] | null): Promise {
const pty = getPty();
const result = await pty.spawnSafe('brew list --formula');
if (result.status === 'error') return null;
return result.data.split('\n').filter(Boolean);
}
async add(formulae: string[], plan: Plan): Promise {
const pty = getPty();
await pty.spawn(`brew install --formula ${formulae.join(' ')}`);
}
async remove(formulae: string[], plan: Plan): Promise {
const pty = getPty();
await pty.spawn(`brew uninstall --formula ${formulae.join(' ')}`);
}
async modify(newValue: string[], previousValue: string[]): Promise {
// Handle updates
}
}
```
Register in resource settings:
```typescript
getSettings(): ResourceSettings {
return {
id: 'homebrew',
parameterSettings: {
formulae: {
type: 'stateful',
definition: new FormulaeParameter(),
order: 2 // Execute after taps (order: 1)
}
}
};
}
```
How Stateful Parameters Work [#how-stateful-parameters-work]
When a user configures Homebrew with formulae:
```json
{
"type": "homebrew",
"formulae": ["git", "node", "python"]
}
```
The framework:
1. **Refresh Phase**: Calls `FormulaeParameter.refresh()` to get currently installed formulae
2. **Planning Phase**: Compares desired `["git", "node", "python"]` with current state
3. **Apply Phase**:
* If formulae is new → calls `FormulaeParameter.add(["git", "node", "python"])`
* If formulae changed → calls `add()` for new items, `remove()` for deleted items
* If formula values changed → calls `modify()` for changed items
Lifecycle Integration [#lifecycle-integration]
Stateful parameters integrate with the parent resource lifecycle:
**During CREATE:**
```
1. Resource.create() is called
2. FormulaeParameter.add() is called (if formulae specified)
```
**During MODIFY:**
```
1. FormulaeParameter.add() for new formulae
2. FormulaeParameter.remove() for removed formulae
3. FormulaeParameter.modify() for changed formulae
4. Resource.modify() for other parameters
```
**During DESTROY (if removeStatefulParametersBeforeDestroy: true):**
```
1. FormulaeParameter.remove() for all formulae
2. Resource.destroy() is called
```
Real-World Example: NVM Node Versions [#real-world-example-nvm-node-versions]
```typescript
class NodeVersionsParameter extends StatefulParameter {
async refresh(desired: string[] | null): Promise {
const pty = getPty();
// List installed Node versions
const result = await pty.spawnSafe('nvm list');
if (result.status === 'error') return null;
// Parse output like "v18.0.0", "v20.0.0"
const versions = result.data
.split('\n')
.filter(line => line.includes('v'))
.map(line => line.match(/v(\d+\.\d+\.\d+)/)?.[1])
.filter(Boolean);
return versions;
}
async add(versions: string[], plan: Plan): Promise {
const pty = getPty();
for (const version of versions) {
await pty.spawn(`nvm install ${version}`);
}
}
async remove(versions: string[], plan: Plan): Promise {
const pty = getPty();
for (const version of versions) {
await pty.spawn(`nvm uninstall ${version}`);
}
}
async modify(newValue: string[], previousValue: string[]): Promise {
// For version numbers, modification doesn't make sense
// Versions are either added or removed
}
}
```
Order of Execution [#order-of-execution]
Use the `order` property to control the sequence of stateful parameter operations:
```typescript
parameterSettings: {
taps: {
type: 'stateful',
definition: new TapsParameter(),
order: 1 // Install taps first
},
formulae: {
type: 'stateful',
definition: new FormulaeParameter(),
order: 2 // Then install formulae (which may come from taps)
},
casks: {
type: 'stateful',
definition: new CasksParameter(),
order: 3 // Finally install casks
}
}
```
Lower order numbers execute first. This is important when parameters have dependencies on each other.
# Schema validation
Schema Validation [#schema-validation]
Use JSON Schema or Zod for validation:
JSON Schema [#json-schema]
```typescript
// my-resource-schema.json
{
"type": "object",
"properties": {
"version": { "type": "string" },
"path": { "type": "string" }
},
"required": ["version"]
}
// my-resource.ts
import Schema from './my-resource-schema.json';
interface MyConfig extends StringIndexedObject {
version: string;
path?: string;
}
getSettings() {
return { schema: Schema };
}
```
Zod (Preferred) [#zod-preferred]
Zod provides type safety with a single source of truth:
```typescript
import { z } from 'zod';
const schema = z.object({
version: z.string(),
path: z.string().optional(),
});
type MyConfig = z.infer;
getSettings() {
return { schema };
}
```
# Testing
Testing [#testing]
Testing is crucial for plugins because they interact with the real system. The `@codifycli/plugin-test` package provides utilities for writing comprehensive tests that verify the entire resource lifecycle.
Test Strategy [#test-strategy]
Codify plugins should have two types of tests:
**Unit Tests** (in `src/**/*.test.ts`):
* Fast, isolated tests
* Test parsing logic, utility functions, data transformations
* No system calls or side effects
* Run with every code change
**Integration Tests** (in `test/**/*.test.ts`):
* Full lifecycle tests against the real system
* Test `create → modify → destroy` flow
* Verify actual system changes
* Slower, require specific prerequisites
Integration Testing with PluginTester [#integration-testing-with-plugintester]
The `PluginTester.fullTest()` method runs a complete resource lifecycle test:
```typescript
import { PluginTester, testSpawn } from '@codifycli/plugin-test';
import { describe, it } from 'vitest';
import path from 'node:path';
describe('Alias resource', () => {
const pluginPath = path.resolve('./src/index.ts');
it('manages shell alias lifecycle', { timeout: 300000 }, async () => {
await PluginTester.fullTest(
pluginPath,
[
{
type: 'alias',
alias: 'my-alias',
value: 'ls -l'
}
],
{
// Validate the CREATE operation
validateApply: async () => {
const { data } = await testSpawn('alias');
expect(data).toContain('my-alias');
expect(data).toContain('ls -l');
// Test that alias actually works
const result = await testSpawn('my-alias');
expect(result.status).toBe('success');
},
// Test the MODIFY operation
testModify: {
modifiedConfigs: [{
type: 'alias',
alias: 'my-alias',
value: 'pwd' // Changed value
}],
validateModify: async () => {
const { data } = await testSpawn('alias');
expect(data).toContain('my-alias');
expect(data).toContain('pwd');
expect(data).not.toContain('ls -l');
}
},
// Validate the DESTROY operation
validateDestroy: async () => {
const { data } = await testSpawn('alias');
expect(data).not.toContain('my-alias');
}
}
);
});
});
```
What fullTest() Does [#what-fulltest-does]
The `fullTest()` method executes this sequence:
1. **Initial Refresh**: Saves current system state
2. **Validate**: Validates configs against schemas
3. **Plan (Create)**: Generates CREATE plan
4. **Apply (Create)**: Executes CREATE operation
5. **Validate Apply**: Runs your `validateApply()` callback
6. **Plan (Modify)**: Generates MODIFY plan with modified configs
7. **Apply (Modify)**: Executes MODIFY operation
8. **Validate Modify**: Runs your `validateModify()` callback
9. **Plan (Destroy)**: Generates DESTROY plan
10. **Apply (Destroy)**: Executes DESTROY operation
11. **Validate Destroy**: Runs your `validateDestroy()` callback
This ensures your resource handles the complete lifecycle correctly.
Testing Configuration Variations [#testing-configuration-variations]
Test different configuration scenarios:
```typescript
it('handles multiple aliases', async () => {
await PluginTester.fullTest(
pluginPath,
[
{
type: 'aliases',
aliases: [
{ alias: 'gs', value: 'git status' },
{ alias: 'gp', value: 'git pull' },
{ alias: 'gc', value: 'git commit' }
]
}
],
{
validateApply: async () => {
const { data } = await testSpawn('alias');
expect(data).toContain('gs=');
expect(data).toContain('gp=');
expect(data).toContain('gc=');
},
testModify: {
modifiedConfigs: [{
type: 'aliases',
aliases: [
{ alias: 'gs', value: 'git status' },
// Removed gp
{ alias: 'gc', value: 'git commit -v' }, // Modified
{ alias: 'gd', value: 'git diff' } // Added
]
}],
validateModify: async () => {
const { data } = await testSpawn('alias');
expect(data).toContain('gs=');
expect(data).not.toContain('gp=');
expect(data).toContain('gc=\'git commit -v\'');
expect(data).toContain('gd=');
}
},
validateDestroy: async () => {
const { data } = await testSpawn('alias');
expect(data).not.toContain('gs=');
expect(data).not.toContain('gc=');
expect(data).not.toContain('gd=');
}
}
);
});
```
Testing Validation Errors [#testing-validation-errors]
Test that your resource properly rejects invalid configurations:
```typescript
it('rejects invalid alias names', async () => {
await expect(async () => {
await PluginTester.fullTest(
pluginPath,
[{
type: 'alias',
alias: 'invalid-name-with-$-symbols',
value: 'ls'
}]
);
}).rejects.toThrow();
});
it('requires both alias and value fields', async () => {
await expect(async () => {
await PluginTester.fullTest(
pluginPath,
[{
type: 'alias',
alias: 'myalias'
// Missing value field
}]
);
}).rejects.toThrow();
});
```
Platform-Specific Tests [#platform-specific-tests]
Test different behaviors on different platforms:
```typescript
import { Utils } from '@codifycli/plugin-core';
it('handles platform-specific installation', async () => {
const expectedCommand = Utils.isMacOS()
? 'brew install my-tool'
: 'apt-get install my-tool';
await PluginTester.fullTest(
pluginPath,
[{ type: 'my-tool' }],
{
validateApply: async () => {
// Verify tool was installed correctly
const result = await testSpawn('which my-tool');
expect(result.status).toBe('success');
}
}
);
});
```
Test Setup and Teardown [#test-setup-and-teardown]
Use Vitest's lifecycle hooks to set up and clean up test environments:
```typescript
import { beforeAll, afterAll, beforeEach, describe, it } from 'vitest';
describe('Homebrew tests', () => {
beforeAll(async () => {
// Ensure Homebrew is installed before running tests
const result = await testSpawn('which brew');
if (result.status === 'error') {
throw new Error('Homebrew must be installed to run these tests');
}
});
afterAll(async () => {
// Clean up any test artifacts
await testSpawn('brew cleanup');
});
beforeEach(() => {
// Reset state before each test
});
it('installs formulae', async () => {
// Test implementation
});
});
```
Testing with Real Files [#testing-with-real-files]
When testing resources that modify files, use temporary directories:
```typescript
import os from 'node:os';
import path from 'node:path';
import fs from 'node:fs/promises';
it('creates config file', async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'codify-test-'));
try {
await PluginTester.fullTest(
pluginPath,
[{
type: 'my-config',
configPath: path.join(tempDir, 'config.json'),
setting: 'value'
}],
{
validateApply: async () => {
const configExists = await fs.access(
path.join(tempDir, 'config.json')
).then(() => true).catch(() => false);
expect(configExists).toBe(true);
const config = JSON.parse(
await fs.readFile(path.join(tempDir, 'config.json'), 'utf8')
);
expect(config.setting).toBe('value');
},
validateDestroy: async () => {
const configExists = await fs.access(
path.join(tempDir, 'config.json')
).then(() => true).catch(() => false);
expect(configExists).toBe(false);
}
}
);
} finally {
// Clean up temp directory
await fs.rm(tempDir, { recursive: true, force: true });
}
});
```
Unit Testing Utilities [#unit-testing-utilities]
For utility functions and parsing logic, write simple unit tests:
```typescript
import { describe, it, expect } from 'vitest';
import { parseBrewList } from './homebrew-utils.js';
describe('parseBrewList', () => {
it('parses brew list output', () => {
const output = `git
node
python@3.11`;
const result = parseBrewList(output);
expect(result).toEqual(['git', 'node', 'python@3.11']);
});
it('handles empty output', () => {
expect(parseBrewList('')).toEqual([]);
});
it('filters out warnings', () => {
const output = `Warning: Some warning message
git
node`;
const result = parseBrewList(output);
expect(result).toEqual(['git', 'node']);
});
});
```
Best Practices for Testing [#best-practices-for-testing]
1. **Use long timeouts for integration tests**: System operations can be slow
```typescript
it('installs packages', { timeout: 300000 }, async () => {
// Test implementation
});
```
2. **Clean up after tests**: Always restore system state
```typescript
afterAll(async () => {
await cleanup();
});
```
3. **Test edge cases**: Empty configs, missing fields, invalid values
4. **Test error conditions**: What happens when commands fail?
5. **Use descriptive test names**: Make failures easy to understand
```typescript
it('installs Homebrew in custom directory and adds to PATH', async () => {
// ...
});
```
6. **Isolate tests**: Each test should be independent and not rely on others
7. **Mock external dependencies in unit tests**: Use mocking for fast, deterministic unit tests
# Overview
The **default plugin** is the core plugin for Codify, providing 50+ declarative resources for managing development tools and system configuration across macOS and Linux. It's automatically included with Codify and covers the most common development environment needs.
Overview [#overview]
The default plugin brings Infrastructure-as-Code principles to your local development machine. Instead of manually installing packages, configuring tools, and setting up your environment, you describe your desired system state in a JSON configuration file and let Codify handle the implementation.
What's Included? [#whats-included]
The default plugin provides resources organized into several categories:
Package Managers [#package-managers]
Manage system packages across different platforms:
* **[homebrew](/docs/resources/package-managers/homebrew)** - Install and manage Homebrew formulae and casks (macOS)
* **apt** - Debian/Ubuntu package management
* **yum** - RedHat/CentOS package management
* **dnf** - Fedora package management
* **snap** - Universal Linux packages
* **[macports](/docs/resources/package-managers/macports)** - MacPorts package manager
Version Managers [#version-managers]
Install and manage multiple versions of programming languages:
* **nvm** - Node.js version management
* **pyenv** - Python version management
* **[rbenv](/docs/resources/ruby/rbenv)** - Ruby version management
* **[jenv](/docs/resources/jenv)** - Java version management
* **[asdf](/docs/resources/asdf/asdf)** - Universal version manager for multiple languages
* **[goenv](/docs/resources/go/goenv)** - Go version management
Programming Languages & Tools [#programming-languages--tools]
Manage language-specific packages and environments:
* **npm** - Node.js packages (global installs)
* **npm-login** - NPM authentication
* **pnpm** - Fast, disk-efficient package manager
* **pip** - Python package installation
* **pip-sync** - Python dependency synchronization
* **[uv](/docs/resources/python/uv)** - Fast Python package and project manager
* **virtualenv** - Python virtual environments
* **venv-project** - Python venv projects
Version Control [#version-control]
Configure Git and manage repositories:
* **[git](/docs/resources/git)** - Git configuration (name, email, signing)
* **git-lfs** - Git Large File Storage
* **git-repository** - Clone and manage git repositories
* **wait-github-ssh-key** - Wait for GitHub SSH key availability
Cloud & DevOps [#cloud--devops]
Set up cloud and infrastructure tools:
* **[aws-cli](/docs/resources/aws-cli)** - AWS Command Line Interface
* **[aws-profile](/docs/resources/aws-profile)** - AWS credential profiles
* **[docker](/docs/resources/docker)** - Docker container platform
* **[terraform](/docs/resources/terraform)** - Infrastructure as Code
Shell Configuration [#shell-configuration]
Customize your shell environment:
* **[alias](/docs/resources/shell/alias)** - Individual shell aliases
* **[aliases](/docs/resources/shell/aliases)** - Manage multiple aliases at once
* **[path](/docs/resources/shell/path)** - PATH environment variable management
* **action** - Custom shell scripts and actions
SSH & Security [#ssh--security]
Manage SSH keys and configuration:
* **[ssh](/docs/resources/ssh)-key** - Generate and manage SSH keys
* **[ssh](/docs/resources/ssh)-config** - SSH client configuration
* **[ssh](/docs/resources/ssh)-add** - Add SSH keys to agent
AI & ML Tools [#ai--ml-tools]
Run AI models locally:
* **[ollama](/docs/resources/ollama)** - Run large language models locally
Development Tools [#development-tools]
Configure popular development environments:
* **[vscode](/docs/resources/vscode)** - Visual Studio Code extensions and settings
* **[android-cli](/docs/resources/android-cli)** - Android CLI, SDK packages, and emulators
* **[android-studio](/docs/resources/android-studio)** - Android Studio IDE
* **[xcode-tools](/docs/resources/xcode-tools)** - Xcode Command Line Tools
* **[pgcli](/docs/resources/pgcli)** - Postgres CLI with auto-completion
* **[webstorm](/docs/resources/webstorm)** - JetBrains WebStorm JavaScript IDE
* **[intellij-idea](/docs/resources/intellij-idea)** - JetBrains IntelliJ IDEA Java/Kotlin IDE
* **[pycharm](/docs/resources/pycharm)** - JetBrains PyCharm Python IDE
* **[clion](/docs/resources/clion)** - JetBrains CLion C/C++ IDE
* **[rustrover](/docs/resources/rustrover)** - JetBrains RustRover Rust IDE
* **[phpstorm](/docs/resources/phpstorm)** - JetBrains PhpStorm PHP IDE
* **[goland](/docs/resources/goland)** - JetBrains GoLand Go IDE
* **[rider](/docs/resources/rider)** - JetBrains Rider .NET/C# IDE
* **[rubymine](/docs/resources/rubymine)** - JetBrains RubyMine Ruby/Rails IDE
File Synchronisation [#file-synchronisation]
Sync files across devices:
* **[syncthing](/docs/resources/syncthing/syncthing)** - Continuous file synchronisation daemon
* **[syncthing-device](/docs/resources/syncthing/syncthing-device)** - Add a remote peer device
* **[syncthing-folder](/docs/resources/syncthing/syncthing-folder)** - Share a local folder with peers
Virtualization [#virtualization]
Manage virtual machines:
* **tart** - macOS and Linux VM management
* **tart-vm** - Individual Tart VMs
File Management [#file-management]
Handle local and remote files:
* **file** - Local file management
* **remote-file** - Download and manage remote files
Contributing [#contributing]
The default plugin is open source and welcomes contributions. Visit the [GitHub repository](https://github.com/kevinwang5658/codify-homebrew-plugin) to:
* Report bugs or request features
* Contribute new resources
* Improve documentation
* Share your configurations
***
**Need help?** Check the documentation for individual resources in the sidebar or visit the [Codify community](https://github.com/codifycli/codify/discussions) for support.
# aws-cli
The aws-cli resource reference. This resource will install the latest version of AWS
CLI v3.
Parameters: [#parameters]
No parameters are currently available for the awscli install
Example usage: [#example-usage]
```json title="codify.jsonc"
[
{ "type": "aws-cli" }
]
```
Dependencies: [#dependencies]
The arm version of aws-cli has a dependency on `homebrew`. A working version of homebrew must be installed in order for this
resource to work. See [the homebrew resource](/docs/core-resources/homebrew) to see how to install it using
Codify. The dependency between the two resources will be automatically resolved and homebrew will be installed
first if both are specified in a config.
The x86 version of this resource will be installed directly using the AWS provided installer.
# aws-profile
import { Step, Steps } from 'fumadocs-ui/components/steps';
The AWS CLI resource reference. This resource will install the latest version of AWS
CLI v3.
Parameters: [#parameters]
* **awsAccessKeyId**: *(string)*
The access key id from AWS to use for the profile. The aws-profile
resource requires either `awsAccessKeyId` and `awsSecretAccessKey` or `csvCredentials` to
be specified.
* **awsSecretAccessKey**: *(string)* The secret access key to be added to the profile.
* **csvCredentials**: *(string)* Instead of passing in credentials in plaintext, a `csv`
credentials file path can be used instead. Both relative and absolute paths are supported.
* **profile**: *(string, required)* The profile to configure
Dependencies [#dependencies]
The aws cli must be installed before aws-profile can be used.
Example usage: [#example-usage]
```json title="codify.jsonc"
[
{ "type": "aws-cli" },
{
"type": "aws-profile",
"profile": "sandbox",
"csvCredentials": "~/Documents/rootkey.csv"
},
{
"type": "aws-profile",
"profile": "production",
"awsAccessKeyId": "AKIA...",
"awsSecretAccessKey": "adADSjsada..."
}
]
```
Setting up AWS CLI locally [#setting-up-aws-cli-locally]
Basic instructions for getting AWS CLI up and running locally using Codify.
Login to your AWS environment and navigate to
`IAM > Security credentials`
Under the access keys section click
`Create access key`
. Note that it is not
recommended to create access keys for the root account.
On the
`Retrieve access key`
page, click
`Download .csv file`
.
Apply the following
`codify.jsonc`
file to install AWS CLI with that account
as the default profile.
```json title="codify.json"
[
{ "type": "aws-cli" },
{
"type": "aws-profile",
"profile": "default",
"csvCredentials": "path/to/csv/file.csv"
}
]
```
# codex-app
The codex-app resource installs the [Codex desktop app](https://developers.openai.com/codex/app) — OpenAI's "Codex command center" for managing coding agent threads, projects, and worktrees. It is a thin wrapper around the Homebrew cask install; the app shares its login, configuration, and MCP servers with the [`codex`](/docs/resources/codex/codex) CLI and IDE extension via `~/.codex/config.toml`.
This resource is **macOS only**. The Codex desktop app is also available on Windows via the Microsoft Store, but is not yet available on Linux.
Parameters [#parameters]
This resource has no configurable parameters — it manages installation only.
Example usage [#example-usage]
```json title="codify.jsonc"
[
{
"type": "codex-app",
"os": ["macOS"]
}
]
```
Install the CLI and the desktop app together [#install-the-cli-and-the-desktop-app-together]
```json title="codify.jsonc"
[
{
"type": "codex",
"os": ["macOS"]
},
{
"type": "codex-app",
"os": ["macOS"]
}
]
```
Notes [#notes]
* Installed via `brew install --cask codex-app` to `/Applications/Codex.app`.
* The desktop app, CLI, and IDE extension all read and write `~/.codex/config.toml` and share the same login session — use the [`codex`](/docs/resources/codex/codex) resource to manage settings and MCP servers declaratively.
# codex-project
The codex-project resource manages **per-project** Codex configuration. It writes a project-scoped `AGENTS.md`, settings, and MCP servers under a specific directory — leaving global configuration untouched. Use it alongside the [`codex`](/docs/resources/codex/codex) resource, which handles installation.
Parameters [#parameters]
* **directory**: *(string, required)* Path to the project directory. Configuration files are written relative to this path:
* `/AGENTS.md`
* `/.codex/config.toml`
* **agentsMd**: *(string, optional)* Content for `/AGENTS.md`. Accepts inline text, an `https://` URL, or a `codify://documentId:fileId` cloud URL. Codex walks from the project root down to the current working directory and concatenates any `AGENTS.md` files it finds, using them as project-specific instructions.
* **config**: *(object, optional)* Key-value pairs to merge into `/.codex/config.toml`. On apply, the declared keys are written; on destroy, only the declared keys are removed. Supports the same keys as the global config, e.g. `model`, `approval_policy`, `sandbox_mode`, `sandbox_workspace_write`.
* Note: Codex ignores `model_provider`, `openai_base_url`, `notify`, `otel`, and `profiles` in project-level config files for security reasons.
* **mcpServers**: *(array, optional)* MCP servers to register for this project under `[mcp_servers]` in `/.codex/config.toml`. Each entry requires a `name` and `type`, plus transport-specific fields:
* **stdio**: `{ name, type: "stdio", command, args?, env?, envVars?, cwd?, startupTimeoutSec?, toolTimeoutSec? }` — local process server
* **http**: `{ name, type: "http", url, bearerTokenEnvVar?, httpHeaders? }` — remote streamable-HTTP server
Example usage [#example-usage]
Per-project AGENTS.md and sandbox policy [#per-project-agentsmd-and-sandbox-policy]
```json title="codify.jsonc"
[
{
"type": "codex-project",
"directory": "~/projects/my-api",
"agentsMd": "# Project Instructions\n\nThis is a Node.js API. Always use async/await.\nRun `npm test` before committing.",
"config": {
"sandbox_mode": "workspace-write",
"approval_policy": "on-request"
}
}
]
```
Per-project AGENTS.md with an MCP server [#per-project-agentsmd-with-an-mcp-server]
```json title="codify.jsonc"
[
{
"type": "codex-project",
"directory": "~/projects/my-api",
"agentsMd": "# Project Instructions\n\nAlways check types with `npm run typecheck` before submitting.",
"mcpServers": [
{
"name": "project-db",
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"]
}
]
}
]
```
Per-project AGENTS.md from a remote URL [#per-project-agentsmd-from-a-remote-url]
```json title="codify.jsonc"
[
{
"type": "codex-project",
"directory": "~/projects/my-api",
"agentsMd": "codify://my-document-id:my-file-id"
}
]
```
Or from a public HTTPS URL:
```json title="codify.jsonc"
[
{
"type": "codex-project",
"directory": "~/projects/my-api",
"agentsMd": "https://raw.githubusercontent.com/my-org/dotfiles/main/AGENTS.md"
}
]
```
Global install + per-project config together [#global-install--per-project-config-together]
```json title="codify.jsonc"
[
{
"type": "codex",
"config": {
"model": "gpt-5.1-codex"
}
},
{
"type": "codex-project",
"directory": "~/projects/my-api",
"agentsMd": "# My API\n\nNode.js + TypeScript. Run `npm test` before any commit."
}
]
```
Notes [#notes]
* The `codex` resource must be applied before `codex-project` (it declares a dependency automatically). If Codex is not installed, this resource will report as not present.
* Multiple `codex-project` entries can coexist — each unique `directory` is a separate resource instance.
* Destroying a `codex-project` resource removes only the per-project files (`AGENTS.md` and the `.codex/config.toml` directory). The Codex binary and global configuration are left untouched.
* The `config` parameter merges only the declared top-level keys. Existing project config not in your Codify config is left untouched.
* The `agentsMd` parameter manages the entire `AGENTS.md` file. On destroy, the file is removed.
* MCP servers are stored under `[mcp_servers.]` in `/.codex/config.toml`. Removing an MCP server from your config removes its table; other servers are untouched.
# codex
The codex resource installs the [Codex CLI](https://developers.openai.com/codex) — OpenAI's terminal-based coding agent — and manages its global configuration. It handles installation via the official installer script and gives you declarative control over `~/.codex/config.toml` settings and global MCP servers.
Parameters [#parameters]
* **config**: *(object, optional)* Key-value pairs to merge into `~/.codex/config.toml`. On apply, the declared keys are written; on destroy, only the declared keys are removed. Common settings include:
* `model` — the model Codex uses (e.g. `"gpt-5.1-codex"`)
* `model_provider` — the model provider (default: `"openai"`)
* `approval_policy` — `"untrusted"` | `"on-request"` | `"never"` (default: `"on-request"`)
* `sandbox_mode` — `"read-only"` | `"workspace-write"` | `"danger-full-access"` (default: `"workspace-write"`)
* `sandbox_workspace_write` — `{ network_access, writable_roots, ... }`
* `model_reasoning_effort` — `"minimal"` | `"low"` | `"medium"` | `"high"` | `"xhigh"`
* `model_reasoning_summary` — `"auto"` | `"concise"` | `"detailed"` | `"none"`
* `web_search` — `"disabled"` | `"cached"` | `"live"` (default: `"cached"`)
* `file_opener` — `"vscode"` | `"cursor"` | `"windsurf"` | `"none"`
* `history` — `{ persistence, max_bytes }`
* `shell_environment_policy` — `{ inherit, set, include_only, exclude }`
* **mcpServers**: *(array, optional)* MCP servers to register globally under `[mcp_servers]` in `~/.codex/config.toml`. Each entry requires a `name` and `type`, plus transport-specific fields:
* **stdio**: `{ name, type: "stdio", command, args?, env?, envVars?, cwd?, startupTimeoutSec?, toolTimeoutSec? }` — local process server
* **http**: `{ name, type: "http", url, bearerTokenEnvVar?, httpHeaders? }` — remote streamable-HTTP server
Example usage [#example-usage]
Install Codex with custom settings [#install-codex-with-custom-settings]
```json title="codify.jsonc"
[
{
"type": "codex",
"config": {
"model": "gpt-5.1-codex",
"approval_policy": "on-request",
"sandbox_mode": "workspace-write"
}
}
]
```
Codex with an MCP server [#codex-with-an-mcp-server]
```json title="codify.jsonc"
[
{
"type": "codex",
"mcpServers": [
{
"name": "filesystem",
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
}
]
}
]
```
Codex with a remote MCP server [#codex-with-a-remote-mcp-server]
```json title="codify.jsonc"
[
{
"type": "codex",
"mcpServers": [
{
"name": "figma",
"type": "http",
"url": "https://mcp.figma.com/mcp",
"bearerTokenEnvVar": "FIGMA_OAUTH_TOKEN"
}
]
}
]
```
Notes [#notes]
* Codex is installed via the official installer (`curl -fsSL https://chatgpt.com/codex/install.sh | sh`) on both macOS and Linux. The binary is placed at `~/.local/bin/codex`.
* The installer adds `~/.local/bin` to your PATH. This entry remains after destroy — remove it manually if you no longer want it.
* The `config` parameter merges only the declared top-level keys of `~/.codex/config.toml`. Existing keys not in your Codify config (including `mcp_servers`) are left untouched.
* MCP servers are stored under `[mcp_servers.]` in `~/.codex/config.toml`. Removing an MCP server from your config removes its table; other servers are untouched.
* For per-project configuration (AGENTS.md, project-scoped settings and MCP servers), see [`codex-project`](/docs/resources/codex/codex-project). For the Codex desktop app, see [`codex-app`](/docs/resources/codex/codex-app).
* Authentication (`codex login`) is interactive and not managed by this resource.
# docker
The docker resource reference. This resource installs docker. This resource
installs docker using the recommended approach from
[the docker docs](https://docs.docker.com/desktop/setup/install/mac-install/#install-from-the-command-line).
This installs Docker Desktop, Docker Engine, Docker CLI and Docker compose.
By default, permissions are configured for the current user, and the license is automatically accepted.
**Note**: the installation may take several minutes to complete, and the Docker installer does not
display logs during the process.
Parameters: [#parameters]
* **acceptLicense**: *(boolean)* Automatically accepts the license agreement. Corresponds to the `--accept-license` flag. Defaults to true
* **useCurrentUser**: *(boolean)* Use the current user to install docker. Corresponds to the `--username=` flag. Defaults to true
Example usage: [#example-usage]
```json title="codify.jsonc"
[
{
"type": "docker"
}
]
```
```json title="codify.jsonc"
[
{
"type": "docker",
"acceptLicense": false,
"useCurrentUser": false
}
]
```
# env-file
The env-file resource manages a single `.env` file at a specified directory. It supports writing key-value pairs directly or syncing the file from Codify cloud. The resource is marked sensitive and will not be automatically imported.
Parameters [#parameters]
* **dir** *(string, required)*: The directory where the env file is located.
* **name** *(string, optional)*: The filename. Defaults to `.env`. Use this to manage files like `.env.local`, `.dev.vars`, or `.env.production`.
* **contents** *(array\[object], optional)*: Key-value pairs to write into the file. Each entry has `key` (string) and `value` (string). Mutually exclusive with `remoteFile`.
* **remoteFile** *(string, optional)*: A Codify cloud file reference (`codify://:`). The file is downloaded and written on each apply. Mutually exclusive with `contents`.
Example usage [#example-usage]
Declare env file contents [#declare-env-file-contents]
```json title="codify.jsonc"
[
{
"type": "env-file",
"dir": "~/projects/my-app",
"contents": [
{ "key": "DATABASE_URL", "value": "postgres://localhost:5432/mydb" },
{ "key": "API_KEY", "value": "" },
{ "key": "DEBUG", "value": "false" }
]
}
]
```
Use a named env file [#use-a-named-env-file]
```json title="codify.jsonc"
[
{
"type": "env-file",
"dir": "~/projects/my-worker",
"name": ".dev.vars",
"contents": [
{ "key": "API_TOKEN", "value": "" }
]
}
]
```
Sync from Codify cloud [#sync-from-codify-cloud]
```json title="codify.jsonc"
[
{
"type": "env-file",
"dir": "~/projects/my-app",
"name": ".env.local",
"remoteFile": "codify://:"
}
]
```
Notes [#notes]
* The resource is marked as **sensitive**: values in `contents` are not shown in plan output.
* Import is disabled (`preventImport: true`) — existing `.env` files on the system are never automatically imported.
* Both `contents` and `remoteFile` cannot be specified at the same time.
* Multiple `env-file` resources can coexist in the same directory as long as they use different `name` values.
# env-files
The env-files resource manages multiple `.env` files inside a single directory. It is useful for projects that rely on several env files at once (e.g. `.env`, `.env.local`, `.dev.vars`). The resource is marked sensitive and will not be automatically imported.
Parameters [#parameters]
* **dir** *(string, required)*: The directory containing the env files.
* **envFiles** *(array\[object], required)*: The env files to manage. Each entry has:
* **name** *(string, required)*: The filename (e.g. `.env`, `.env.local`, `.dev.vars`).
* **contents** *(array\[object], optional)*: Key-value pairs written into the file. Each entry has `key` and `value` strings. Mutually exclusive with `remoteFile`.
* **remoteFile** *(string, optional)*: A Codify cloud file reference (`codify://:`). Mutually exclusive with `contents`.
Example usage [#example-usage]
Manage multiple Cloudflare Worker env files [#manage-multiple-cloudflare-worker-env-files]
```json title="codify.jsonc"
[
{
"type": "env-files",
"dir": "~/projects/my-worker",
"envFiles": [
{
"name": ".dev.vars",
"contents": [
{ "key": "API_TOKEN", "value": "" },
{ "key": "ENVIRONMENT", "value": "development" }
]
},
{
"name": ".env.production",
"contents": [
{ "key": "API_TOKEN", "value": "" },
{ "key": "ENVIRONMENT", "value": "production" }
]
}
]
}
]
```
Sync multiple env files from Codify cloud [#sync-multiple-env-files-from-codify-cloud]
```json title="codify.jsonc"
[
{
"type": "env-files",
"dir": "~/projects/my-app",
"envFiles": [
{
"name": ".env",
"remoteFile": "codify://:"
},
{
"name": ".env.local",
"remoteFile": "codify://:"
}
]
}
]
```
Notes [#notes]
* The resource is marked as **sensitive**: values in `contents` are not shown in plan output.
* Import is disabled (`preventImport: true`) — existing env files on the system are never automatically imported.
* Only one `env-files` resource per directory is allowed. Use multiple `env-file` resources if you prefer managing files individually.
* Within each `envFiles` entry, `contents` and `remoteFile` cannot both be specified.
* When an entry is removed from `envFiles` during modify, the corresponding file is deleted from disk.
# github-cli
The GitHub CLI resources install and configure the [GitHub CLI (`gh`)](https://cli.github.com/manual/) tool. Four resources are provided to manage distinct concerns: installation and global configuration, authentication, command aliases, and GitHub account SSH keys.
***
github-cli [#github-cli]
Installs `gh` and manages global configuration settings such as the default git protocol, editor, pager, and browser.
Parameters [#parameters]
* **gitProtocol**: *(string: `https` | `ssh`)* Default protocol for git operations. Defaults to `https`.
* **editor**: *(string)* Default text editor for gh commands (e.g. `vim`, `nano`, `code --wait`).
* **prompt**: *(string: `enabled` | `disabled`)* Whether interactive prompts are shown. Defaults to `enabled`.
* **pager**: *(string)* Pager program used to display long output (e.g. `less`).
* **browser**: *(string)* Default browser to open URLs (e.g. `firefox`).
Example usage [#example-usage]
```json title="codify.jsonc"
[
{
"type": "github-cli",
"gitProtocol": "ssh",
"editor": "vim"
}
]
```
***
github-cli-auth [#github-cli-auth]
Authenticates the GitHub CLI using a Personal Access Token (PAT). Supports multiple accounts and GitHub Enterprise Server hostnames.
> **Security note:** The `token` field is marked sensitive and is never logged or displayed by Codify. Store PATs in a secrets manager and reference them via environment variables where possible.
Parameters [#parameters-1]
* **token** *(required)*: *(string)* GitHub personal access token (classic or fine-grained).
* **hostname**: *(string)* GitHub hostname. Defaults to `github.com`. Set to your GHE hostname (e.g. `github.mycompany.com`) for enterprise instances.
Example usage [#example-usage-1]
```json title="codify.jsonc"
[
{
"type": "github-cli",
"gitProtocol": "https"
},
{
"type": "github-cli-auth",
"token": ""
}
]
```
***
github-cli-alias [#github-cli-alias]
Creates a short-hand alias for a `gh` command. Each alias is an independent resource, identified by its name.
Parameters [#parameters-2]
* **alias** *(required)*: *(string)* The alias name used to invoke the command (e.g. `prc`).
* **expansion** *(required)*: *(string)* The gh command or shell command this alias expands to (e.g. `pr create`).
* **shell**: *(boolean)* When `true`, the expansion is executed as a shell command via `sh`, enabling pipes, redirects, and other shell features. Defaults to `false`.
Example usage [#example-usage-2]
```json title="codify.jsonc"
[
{
"type": "github-cli-alias",
"alias": "prc",
"expansion": "pr create"
},
{
"type": "github-cli-alias",
"alias": "prs",
"expansion": "pr status"
}
]
```
***
github-cli-ssh-key [#github-cli-ssh-key]
Uploads a local SSH public key to your GitHub account. This is distinct from the `ssh-key` resource, which manages local key files — this resource registers an existing key with GitHub via the `gh ssh-key add` command.
Requires authentication (`github-cli-auth`) to be configured.
Parameters [#parameters-3]
* **title** *(required)*: *(string)* Display name for the key on GitHub (e.g. `My Laptop`).
* **keyFile** *(required)*: *(string)* Path to the local SSH public key file (e.g. `~/.ssh/id_ed25519.pub`).
* **keyType**: *(string: `authentication` | `signing`)* Key usage type. Use `authentication` (default) for git over SSH, or `signing` for commit signing.
Example usage [#example-usage-3]
```json title="codify.jsonc"
[
{
"type": "github-cli"
},
{
"type": "github-cli-auth",
"token": ""
},
{
"type": "github-cli-ssh-key",
"title": "My Laptop",
"keyFile": "~/.ssh/id_ed25519.pub",
"keyType": "authentication"
}
]
```
# ios-simulator
The ios-simulator resource manages iOS (and iPadOS/watchOS/tvOS/visionOS) simulator instances on macOS
using `xcrun simctl`. A single resource declaration manages a list of simulators, making it easy to
define a full testing matrix across device types and OS versions in one place. Simulators are created with the specified device type and runtime. Removing the resource deletes all
declared simulators from the system. Xcode Command Line Tools must be installed — add an
`xcode-tools` resource as a dependency if you are not sure they are present.
Parameters: [#parameters]
* **simulators** *(object\[], optional)* — List of simulators to create and manage. Each entry has:
* **name** *(string, required)* — Human-readable name for the simulator instance (e.g. `"iPhone 15 Dev"`). Must be unique across your declared simulators.
* **deviceType** *(string, required)* — CoreSimulator device type identifier. Use the format `com.apple.CoreSimulator.SimDeviceType.`. Run `xcrun simctl list devicetypes` to see identifiers available on your machine.
* **runtime** *(string, required)* — CoreSimulator runtime identifier. Use the format `com.apple.CoreSimulator.SimRuntime.-`. Run `xcrun simctl list runtimes` to see installed runtimes.
Example usage: [#example-usage]
```json title="codify.jsonc"
[
{
"type": "ios-simulators",
"simulators": [
{
"name": "iPhone 15 Dev",
"deviceType": "com.apple.CoreSimulator.SimDeviceType.iPhone-15",
"runtime": "com.apple.CoreSimulator.SimRuntime.iOS-18-0",
}
],
"os": ["macOS"]
}
]
```
```json title="codify.jsonc"
[
{
"type": "xcode-tools",
"os": ["macOS"]
},
{
"type": "ios-simulators",
"simulators": [
{
"name": "iPhone 15 Pro",
"deviceType": "com.apple.CoreSimulator.SimDeviceType.iPhone-15-Pro",
"runtime": "com.apple.CoreSimulator.SimRuntime.iOS-18-0",
},
{
"name": "iPad Pro 11-inch",
"deviceType": "com.apple.CoreSimulator.SimDeviceType.iPad-Pro-11-inch-M4",
"runtime": "com.apple.CoreSimulator.SimRuntime.iOS-18-0",
}
],
"os": ["macOS"]
}
]
```
# jenv
import { Step, Steps } from 'fumadocs-ui/components/steps';
The jenv resource reference. This resource installs jenv, a Java version manager that allows the system to
switch between multiple versions of Java and use one as the global default. For more information on jenv,
[see here](https://www.jenv.be). Normally, jenv will not automatically install java on your behalf unlike
[nvm](/docs/core-resources/javascript/nvm) or [pyenv](/docs/core-resources/python/pyenv). However, this resources has
additional custom functionality and will attempt to install LTS versions of Java using the `openjdk@` formulas
if Homebrew is installed.
Parameters: [#parameters]
* **global**: *(string)* The global version of Java to use.
* **add**: *(array\[string])* Java install location to add. This accepts a relative or absolute path to a java
home directory. Some common locations for Java installations are:
* Homebrew (aarch64): `/opt/homebrew/Cellar/`**`[openjdk@21]`**`/21.0.3/libexec/openjdk.jdk/Contents/Home`
* Homebrew (Intel): `/usr/local/opt/`**`[openjdk@21]`**`/libexec/openjdk.jdk/Contents/Home`
* Regular Java installs: `/Library/Java/JavaVirtualMachines/`**`[zulu-18.jdk]`**`/Contents/Home`
* Note that the **bolded** sections represent a specific installation of Java. Replace it your version to
make it work.
Additionally a integer version number can be provided for LTS versions. This resource will attempt to install
the Java version directly using homebrew if it's available using the [openjdk formulas](https://formulae.brew.sh/formula/openjdk).
The versions that are available for automated install are `[8, 11, 17, 21, 22]`.
Dependencies: [#dependencies]
There is a dependency on `homebrew` only if automated Java installations are needed. Otherwise, `jenv` can
operate independently of homebrew.
Example usage: [#example-usage]
```json title="codify.json"
[
{
"type": "jenv",
"global": "21",
"add": [
"21",
"8",
"/Library/Java/JavaVirtualMachines/zulu-18.jdk/Contents/Home"
]
}
]
```
Setting up Java [#setting-up-java]
Create a
`codify.json`
file anywhere.
Open
`codify.json`
with your file editor and paste in the follow configs.
```json title="codify.json"
[
{ "type": "homebrew" },
{
"type": "jenv",
"global": "21",
"add": [
"21"
]
}
]
```
Run
`codify apply`
in the directory of the file. Open a new terminal and run the command
`java --version`
to
confirm that Java has been installed
```sh title="terminal"
codify apply
```
# macos-settings
The macos-settings resource manages common macOS system preferences using the built-in `defaults` command. It covers mouse, keyboard, trackpad, and Dock settings — everything you need to reproduce your preferred system configuration on a new Mac.
Parameters [#parameters]
All sections and their sub-keys are optional. You only need to declare the settings you want to manage.
mouse [#mouse]
| Parameter | Type | Default | Description |
| ------------------ | ------------ | ------- | ---------------------------------------------------------------------------------------------------------------------- |
| `naturalScrolling` | boolean | `true` | Scroll content in the natural direction (content follows finger). When `false`, uses the traditional scroll direction. |
| `acceleration` | boolean | `true` | Enable mouse acceleration. When `false`, the cursor moves at a fixed speed regardless of how fast the mouse is moved. |
| `speed` | number (0–3) | `1.5` | Mouse tracking speed. Higher values make the cursor move farther per physical movement. |
keyboard [#keyboard]
| Parameter | Type | Default | Description |
| ---------------------- | ------- | ------- | --------------------------------------------------------------------------------------------------------------------------- |
| `keyRepeat` | integer | `6` | Rate of key repeat while a key is held. Lower = faster (1 is fastest; 120 effectively disables repeat). |
| `initialKeyRepeat` | integer | `68` | Delay before key repeat begins (in ticks). Lower = shorter delay (10 minimum). |
| `pressAndHold` | boolean | `true` | When `true`, holding a key shows the accent character picker. When `false`, the key repeats instead. |
| `fnKeysAsStandardKeys` | boolean | `false` | When `true`, the F1–F12 keys act as standard function keys; press Fn to trigger special actions (brightness, volume, etc.). |
| `keyboardNavigation` | boolean | `false` | When `true`, enables Tab-based focus navigation in system dialogs (equivalent to "Keyboard navigation" in System Settings). |
trackpad [#trackpad]
| Parameter | Type | Default | Description |
| --------- | ------------ | ------- | --------------------------------------------------------------------------------------- |
| `speed` | number (0–3) | `1.5` | Trackpad tracking speed. Higher values make the cursor move farther per swipe distance. |
dock [#dock]
| Parameter | Type | Default | Description |
| ---------------- | ----------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------- |
| `position` | `"left"` \| `"bottom"` \| `"right"` | `"bottom"` | Position of the Dock on screen. |
| `iconSize` | integer (16–128) | `48` | Dock icon size in pixels. |
| `autohide` | boolean | `false` | Automatically hide and show the Dock when the cursor moves near the screen edge. |
| `hoverDelay` | number | `0.2` | Seconds to wait before the Dock appears when hovering near the screen edge. Set to `0` for instant reveal. |
| `animationSpeed` | number | `0.5` | Duration in seconds of the Dock slide-in/out animation. Set to `0` to disable the animation entirely. |
| `showRecents` | boolean | `true` | Show recently opened apps in a dedicated section of the Dock. |
| `minimizeEffect` | `"genie"` \| `"scale"` \| `"suck"` | `"genie"` | Window minimize animation style. |
macOS defaults mapping [#macos-defaults-mapping]
The table below shows the underlying `defaults` key used for each friendly parameter name.
| Section | Parameter | Domain | Key |
| -------- | ---------------------- | ---------------- | ----------------------------------- |
| mouse | `naturalScrolling` | `NSGlobalDomain` | `com.apple.swipescrolldirection` |
| mouse | `acceleration` | `NSGlobalDomain` | `com.apple.mouse.linear` (inverted) |
| mouse | `speed` | `NSGlobalDomain` | `com.apple.mouse.scaling` |
| keyboard | `keyRepeat` | `NSGlobalDomain` | `KeyRepeat` |
| keyboard | `initialKeyRepeat` | `NSGlobalDomain` | `InitialKeyRepeat` |
| keyboard | `pressAndHold` | `NSGlobalDomain` | `ApplePressAndHoldEnabled` |
| keyboard | `fnKeysAsStandardKeys` | `NSGlobalDomain` | `com.apple.keyboard.fnState` |
| keyboard | `keyboardNavigation` | `NSGlobalDomain` | `AppleKeyboardUIMode` (0/2) |
| trackpad | `speed` | `NSGlobalDomain` | `com.apple.trackpad.scaling` |
| dock | `position` | `com.apple.dock` | `orientation` |
| dock | `iconSize` | `com.apple.dock` | `tilesize` |
| dock | `autohide` | `com.apple.dock` | `autohide` |
| dock | `hoverDelay` | `com.apple.dock` | `autohide-delay` |
| dock | `animationSpeed` | `com.apple.dock` | `autohide-time-modifier` |
| dock | `showRecents` | `com.apple.dock` | `show-recents` |
| dock | `minimizeEffect` | `com.apple.dock` | `mineffect` |
Example usage [#example-usage]
Common macOS preferences [#common-macos-preferences]
```json title="codify.jsonc"
[
{
"type": "macos-settings",
"os": ["macOS"],
"mouse": {
"naturalScrolling": true
},
"keyboard": {
"keyRepeat": 2,
"initialKeyRepeat": 15,
"pressAndHold": false
},
"dock": {
"position": "left",
"iconSize": 36,
"autohide": true,
"showRecents": false
}
}
]
```
Non-Apple keyboard setup [#non-apple-keyboard-setup]
```json title="codify.jsonc"
[
{
"type": "macos-settings",
"os": ["macOS"],
"mouse": {
"naturalScrolling": false,
"acceleration": false
},
"keyboard": {
"fnKeysAsStandardKeys": true
}
}
]
```
Trackpad speed only [#trackpad-speed-only]
```json title="codify.jsonc"
[
{
"type": "macos-settings",
"os": ["macOS"],
"trackpad": {
"speed": 2.5
}
}
]
```
Notes [#notes]
* This resource is **macOS only** and has no effect on Linux.
* No software installation is required — `defaults` is a built-in macOS command.
* Dock settings take effect immediately (the Dock is automatically restarted). Other settings typically take effect the next time you open an application or after logging out.
* When the resource is removed from your configuration, all managed settings are reset to their macOS system defaults using `defaults delete`.
* Changes to `fnKeysAsStandardKeys` may require a full system restart to take effect.
* The `keyRepeat` and `initialKeyRepeat` values use macOS internal tick units, not milliseconds. Smaller values produce faster key repeat.
# pgcli
import { Step, Steps } from 'fumadocs-ui/components/steps';
The Pgcli resource reference. This resource installs [pgcli](https://www.pgcli.com) a command line REPL to
manipulate postgres databases.
Parameters: [#parameters]
No parameters are currently available for the pgcli install
Example usage: [#example-usage]
```json title="codify.jsonc"
[
{
"type": "pgcli"
}
]
```
Dependencies [#dependencies]
This resource has a dependency on `homebrew`. A working version of homebrew must be installed in order for this
resource to work. See [the homebrew resource](/docs/core-resources/homebrew) to see how to install it using
Codify. The dependency between the two resources will be automatically resolved and homebrew will be installed
first if both are specified in a config.
The pgcli tool also has a dependency on `postgres`. The pgcli resources uses homebrew underneath and will
automatically install postgres if it's not installed.
Setting up a local Postgres DB [#setting-up-a-local-postgres-db]
Create a
`codify.jsonc`
file anywhere.
Open
`codify.json`
with your file editor and paste in the follow configs.
```json title="codify.json"
[
{
"type": "homebrew",
"formulae": [
"postgresql@14"
]
},
{ "type": "pgcli" }
]
```
Run
`codify apply`
in the directory of the file. And it should install
`postgres`
and
`pgcli`
to your system
```sh title="terminal"
codify apply
```
Open a new terminal window and start the database server with this command
```sh title="terminal"
brew services start postgresql
```
Create a new database. Replace
`mydb`
with your desired name
```sh title="terminal"
createdb mydb
```
Connect to the database using
`pgcli`
and you're done!
```sh title="terminal"
pgcli mydb
```
Once you're done using postgres, stop the service.
```sh title="terminal"
brew services stop postgresql
```
# rust
The rust resource installs [Rust](https://www.rust-lang.org/) via [rustup](https://rustup.rs/), the official Rust toolchain installer. It also manages global CLI tools installed through `cargo install`. Supported on macOS and Linux.
Parameters: [#parameters]
* **cargoPackages**: *(array\[string])* Global CLI tools to install via `cargo install`. Use the `name@version` syntax to pin a specific version (e.g. `"ripgrep@14.1.0"`). Omitting the version installs the latest release. Codify adds packages that are missing and removes packages that are no longer listed (in stateful mode).
Example usage: [#example-usage]
Install Rust with common CLI tools [#install-rust-with-common-cli-tools]
```json title="codify.jsonc"
[
{
"type": "rust",
"cargoPackages": ["ripgrep", "bat", "fd-find"]
}
]
```
Install Rust with pinned package versions [#install-rust-with-pinned-package-versions]
```json title="codify.jsonc"
[
{
"type": "rust",
"cargoPackages": ["ripgrep@14.1.0", "bat@0.24.0"]
}
]
```
Install Rust without any additional packages [#install-rust-without-any-additional-packages]
```json title="codify.jsonc"
[
{
"type": "rust"
}
]
```
Notes: [#notes]
* On macOS, Xcode Command Line Tools must be installed before applying the rust resource. The [xcode-tools](/docs/resources/xcode-tools) resource can install them, and is added as a dependency automatically.
* Rust is installed via the official `rustup` script (`https://sh.rustup.rs`). This adds `~/.cargo/bin` to your `PATH` in your shell RC file. Open a new terminal after applying to pick up the updated `PATH`.
* To uninstall Rust and the toolchain, remove the rust resource from your config and run `codify apply`. This runs `rustup self uninstall`.
* Package versions in `cargoPackages` must match a published version on [crates.io](https://crates.io). To find available versions, run `cargo search ` or visit the crate's page on crates.io.
* Omitting `@version` for a package always tracks the latest release. Adding `@version` pins the exact version and will reinstall if the pin changes.
# terraform
The terraform resource reference. This resource adds the infrastructure as code tool [terraform](https://www.terraform.io).
Terraform is the tool that inspired the design of Codify. It allows infrastructure developers to use
config files to set up cloud environments like GCP, AWS and Azure. See more on the Terraform
[help pages](https://developer.hashicorp.com/terraform?product_intent=terraform).
Parameters: [#parameters]
* **directory**: *(string)* A custom directory to install the terraform executable into. The default is
`/usr/local/bin`.
* **version**: *(string)* A specific version of terraform to install. By default, the latest version will be
installed.
Example usage: [#example-usage]
```json title="codify.jsonc"
[
{
"type": "terraform"
}
]
```
Getting started: [#getting-started]
Getting started guides: [https://developer.hashicorp.com/terraform?product\_intent=terraform](https://developer.hashicorp.com/terraform?product_intent=terraform)
# xcode-tools
The xcode-tools resource reference. This resource installs `xcode-tools` to your system. Xcode tools is a
standalone CLI only tools by Apple that provides essential tools for developing on Mac. Xcode tools is a
dependency of many common and popular CLI tools and will automatically included in the plan if it's not
already installed. Xcode tools installs essential programs like `git, gcc, python3, swift`.
Parameters: [#parameters]
No parameters are currently available for the xcode-tools install.
Example usage: [#example-usage]
```json title="codify.jsonc"
[
{
"type": "xcode-tools"
}
]
```
# xcodes
The xcodes resource installs the [xcodes CLI](https://github.com/XcodesOrg/xcodes) tool and manages multiple Xcode versions on macOS. xcodes is the recommended way for iOS/macOS teams to ensure all developers are running the same Xcode version.
Installing Xcode versions requires an Apple Developer account. xcodes will prompt for Apple ID credentials interactively on first use and caches them in the macOS Keychain. For non-interactive environments, supply `appleId` and `appSpecificPassword` directly in the resource config.
Parameters [#parameters]
* **xcodeVersions**: *(string\[])* List of Xcode versions to install (e.g. `["15.4", "14.3.1"]`). Version strings match what `xcodes list` shows — stable versions use a dotted number (`15.4`), beta/RC versions include the label (`15 Beta 3`).
* **selected**: *(string)* The active Xcode version to use, equivalent to running `xcodes select`. Must be one of the installed `xcodeVersions`.
* **appleId**: *(string, optional)* Apple ID email used to authenticate with Apple's servers when downloading Xcode. If omitted, xcodes will prompt interactively.
* **appleIdPassword**: *(string, optional)* Apple ID password. Required alongside `appleId` for non-interactive installs.
Example usage [#example-usage]
```json title="codify.jsonc"
[
{
"type": "xcodes",
"xcodeVersions": ["15.4"],
"selected": "15.4",
"os": ["macOS"]
}
]
```
Multiple versions side by side:
```json title="codify.jsonc"
[
{
"type": "xcodes",
"xcodeVersions": ["14.3.1", "15.4"],
"selected": "15.4",
"os": ["macOS"]
}
]
```
Authentication [#authentication]
xcodes requires Apple ID credentials to download Xcode from Apple's servers. On first install Codify will prompt for your credentials interactively (including two-factor authentication). The credentials are stored in the macOS Keychain for future use.
For CI or fully non-interactive environments, add `appleId` and `appSpecificPassword` to the resource config:
```json title="codify.jsonc"
[
{
"type": "xcodes",
"xcodeVersions": ["15.4"],
"selected": "15.4",
"appleId": "your@apple.id",
"appleIdPassword": "",
"os": ["macOS"]
}
]
```
Note that 2FA will still trigger interactively even with credentials set — xcodes does not support fully headless 2FA bypass.
# claude-code-project
The claude-code-project resource manages **per-project** Claude Code configuration. It writes project-scoped instructions, settings, and MCP servers under a specific directory — leaving global configuration untouched. Use it alongside the [`claude-code`](/docs/resources/claude-code/claude-code) resource, which handles installation.
Parameters [#parameters]
* **directory**: *(string, required)* Path to the project directory. All configuration files are written relative to this path:
* `/.claude/CLAUDE.md`
* `/.claude/settings.json`
* `/.claude.json`
* **claudeMd**: *(string, optional)* Content for `/.claude/CLAUDE.md`. Accepts inline text, an `https://` URL, or a `codify://documentId:fileId` cloud URL. Claude Code reads this at the start of every session within the project, making it ideal for project-specific conventions, preferred libraries, and review checklists.
* **settings**: *(object, optional)* Key-value pairs to merge into `/.claude/settings.json`. On apply, the declared keys are written; on destroy, only the declared keys are removed. Supports the same keys as the global settings:
* `model` — override the default Claude model for this project
* `effortLevel` — `"low"` | `"medium"` | `"high"` | `"xhigh"`
* `editorMode` — `"normal"` | `"vim"`
* `permissions` — `{ allow: [...], deny: [...] }`
* `env` — environment variables injected into every session
* `hooks` — lifecycle hooks (PreToolUse, PostToolUse, SessionStart, etc.)
* **mcpServers**: *(array, optional)* MCP servers to register for this project in `/.claude.json`. Each entry requires a `name` and `type`, plus transport-specific fields:
* **stdio**: `{ name, type: "stdio", command, args?, env? }` — local process server
* **http**: `{ name, type: "http", url, headers? }` — remote HTTP (streamable-http) server
* **sse**: `{ name, type: "sse", url, headers? }` — remote SSE server (deprecated; prefer http)
Example usage [#example-usage]
Per-project instructions and permissions [#per-project-instructions-and-permissions]
```json title="codify.jsonc"
[
{
"type": "claude-code-project",
"directory": "~/projects/my-api",
"claudeMd": "# Project Instructions\n\nThis is a Node.js API. Always use async/await.\nRun `npm test` before committing.",
"settings": {
"permissions": {
"allow": ["Bash(npm run *)", "Bash(git *)"],
"deny": ["Bash(rm -rf *)"]
}
}
}
]
```
Per-project instructions with an MCP server [#per-project-instructions-with-an-mcp-server]
```json title="codify.jsonc"
[
{
"type": "claude-code-project",
"directory": "~/projects/my-api",
"claudeMd": "# Project Instructions\n\nAlways check types with `npm run typecheck` before submitting.",
"mcpServers": [
{
"name": "project-db",
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"]
}
]
}
]
```
Per-project CLAUDE.md from a remote URL [#per-project-claudemd-from-a-remote-url]
```json title="codify.jsonc"
[
{
"type": "claude-code-project",
"directory": "~/projects/my-api",
"claudeMd": "codify://my-document-id:my-file-id"
}
]
```
Or from a public HTTPS URL:
```json title="codify.jsonc"
[
{
"type": "claude-code-project",
"directory": "~/projects/my-api",
"claudeMd": "https://raw.githubusercontent.com/my-org/dotfiles/main/CLAUDE.md"
}
]
```
Global install + per-project config together [#global-install--per-project-config-together]
```json title="codify.jsonc"
[
{
"type": "claude-code",
"settings": {
"model": "claude-opus-4-7"
}
},
{
"type": "claude-code-project",
"directory": "~/projects/my-api",
"claudeMd": "# My API\n\nNode.js + TypeScript. Run `npm test` before any commit."
}
]
```
Notes [#notes]
* The `claude-code` resource must be applied before `claude-code-project` (it declares a dependency automatically). If Claude Code is not installed, this resource will report as not present.
* Multiple `claude-code-project` entries can coexist — each unique `directory` is a separate resource instance.
* Destroying a `claude-code-project` resource removes only the per-project files (`CLAUDE.md`, the declared `settings` keys, and the declared `mcpServers`). The Claude Code binary and global configuration are left untouched.
* The `settings` parameter merges only the declared keys. Existing project settings not in your Codify config are left untouched.
* The `claudeMd` parameter manages the entire file. On destroy, the file is removed.
* MCP servers are stored in `/.claude.json` under the `mcpServers` key. Removing an MCP server from your config removes it from the file; other servers are untouched.
# claude-code
The claude-code resource installs [Claude Code](https://code.claude.com) — Anthropic's agentic coding assistant — and manages its configuration. It handles installation via the official installer script and gives you declarative control over settings, MCP servers, and global instructions.
Parameters [#parameters]
* **globalClaudeMd**: *(string, optional)* Content to write to `~/.claude/CLAUDE.md`. Claude Code reads this file at the start of every session, making it ideal for global coding standards, preferred libraries, and review checklists that apply to all projects.
* **settings**: *(object, optional)* Key-value pairs to merge into `~/.claude/settings.json`. On apply, the declared keys are written; on destroy, only the declared keys are removed. Common settings include:
* `model` — override the default Claude model
* `effortLevel` — `"low"` | `"medium"` | `"high"` | `"xhigh"`
* `editorMode` — `"normal"` | `"vim"`
* `permissions` — `{ allow: [...], deny: [...] }`
* `env` — environment variables injected into every session
* `hooks` — lifecycle hooks (PreToolUse, PostToolUse, SessionStart, etc.)
* `autoMemoryEnabled` — enable/disable auto memory (default: `true`)
* **mcpServers**: *(array, optional)* MCP servers to register globally in `~/.claude.json`. Each entry requires a `name` and `type`, plus transport-specific fields:
* **stdio**: `{ name, type: "stdio", command, args?, env? }` — local process server
* **http**: `{ name, type: "http", url, headers? }` — remote HTTP (streamable-http) server
* **sse**: `{ name, type: "sse", url, headers? }` — remote SSE server (deprecated; prefer http)
Example usage [#example-usage]
Install Claude Code with custom settings [#install-claude-code-with-custom-settings]
```json title="codify.jsonc"
[
{
"type": "claude-code",
"settings": {
"model": "claude-opus-4-7",
"effortLevel": "high",
"editorMode": "vim",
"permissions": {
"allow": ["Bash(npm run *)", "Bash(git *)"],
"deny": ["Bash(rm -rf *)"]
}
}
}
]
```
Claude Code with global instructions and an MCP server [#claude-code-with-global-instructions-and-an-mcp-server]
```json title="codify.jsonc"
[
{
"type": "claude-code",
"globalClaudeMd": "# Global Instructions\n\nAlways follow security best practices.\nPrefer TypeScript over JavaScript.",
"mcpServers": [
{
"name": "filesystem",
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
}
]
}
]
```
Claude Code with hooks [#claude-code-with-hooks]
```json title="codify.jsonc"
[
{
"type": "claude-code",
"settings": {
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npx",
"args": ["eslint", "--fix", "${tool_input.file_path}"]
}
]
}
]
}
}
}
]
```
Notes [#notes]
* Claude Code is installed via the official installer (`curl -fsSL https://claude.ai/install.sh | bash`) on both macOS and Linux. The binary is placed at `~/.local/bin/claude`.
* The installer adds `~/.local/bin` to your PATH via your shell RC file (`.bashrc` or `.zshrc`). This entry remains after destroy — remove it manually if you no longer want it.
* The `settings` parameter merges only the declared keys. Existing settings not in your Codify config are left untouched.
* The `globalClaudeMd` parameter manages the entire file. On destroy, the file is removed.
* MCP servers are stored in `~/.claude.json` under the `mcpServers` key. Each server's `name` becomes its key in that object. Removing an MCP server from your config removes it from the file; other servers are untouched.
* To see all available settings, run `claude config list` or visit the [settings reference](https://code.claude.com/docs/en/settings).
# ollama
The ollama resource installs [Ollama](https://ollama.com/), a runtime for running large language models locally. On macOS it is installed via Homebrew and started as a background service; on Linux the official install script is used, which registers a systemd service automatically.
Parameters: [#parameters]
* **models**: *(array\[string])* AI models to pull and keep installed. Model names match those listed in the [Ollama library](https://ollama.com/library) (e.g. `"llama3.2"`, `"mistral"`, `"qwen2.5-coder:7b"`). Codify adds models that are missing and removes models that are no longer listed.
Example usage: [#example-usage]
Install Ollama with a single model [#install-ollama-with-a-single-model]
```json title="codify.jsonc"
[
{
"type": "ollama",
"models": ["llama3.2"]
}
]
```
Install Ollama with multiple models [#install-ollama-with-multiple-models]
```json title="codify.jsonc"
[
{
"type": "ollama",
"models": ["llama3.2", "mistral", "qwen2.5-coder"]
}
]
```
Install Ollama without pulling any models [#install-ollama-without-pulling-any-models]
```json title="codify.jsonc"
[
{
"type": "ollama"
}
]
```
Notes: [#notes]
* On macOS, Homebrew must be installed before applying the ollama resource. The [homebrew](/docs/resources/package-managers/homebrew) resource can install it.
* On Linux, the official install script (`https://ollama.com/install.sh`) requires `curl` and `sudo` privileges. The script creates an `ollama` system user and registers a systemd service.
* Models can be large (several gigabytes each). Make sure you have sufficient disk space before adding them to your configuration.
* To see available model names and tags, visit [ollama.com/library](https://ollama.com/library) or run `ollama list` after installation.
* Removing the ollama resource stops and uninstalls the Ollama binary but does **not** automatically remove downloaded model data (stored in `~/.ollama` on macOS or `/usr/share/ollama` on Linux for system-level data).
# openclaw
The openclaw resource installs [OpenClaw](https://docs.openclaw.ai/) — a self-hosted gateway that connects chat channels (Discord, Slack, Telegram, WhatsApp, Signal, iMessage, Matrix, Teams, and more) to AI agents — and manages its configuration. It handles installation via the official installer script and gives you declarative control over the gateway, agents, models, channels, tools, and every other section of the OpenClaw config.
Parameters [#parameters]
* **settings**: *(object, optional)* Top-level keys to merge into `~/.openclaw/openclaw.json`. On apply, the declared keys are written; on destroy, only the declared keys are removed. Common sections include:
* `gateway` — `mode` (`"local"` | `"remote"`), `port` (default `18789`), `bind` (`"loopback"` | `"lan"` | `"tailnet"` | `"auto"` | `"custom"`), `auth`, `tls`, `controlUi`
* `agents` — `defaults.{workspace,model,thinking,heartbeat,memory,media,skills}`, `list[]` for per-agent overrides
* `models` — `pricing.enabled`, `mode` (`"merge"` | `"replace"`), `providers` (custom/local model providers such as Ollama or LM Studio)
* `channels` — per-provider sections under `channels.` (e.g. `discord`, `slack`, `telegram`, `whatsapp`, `signal`, `imessage`, `matrix`, `teams`). A channel starts automatically once its config section exists (unless `enabled: false`). Common fields: `dmPolicy`, `groupPolicy`, `allowFrom`, `mediaMaxMb`, `historyLimit`, plus provider-specific credentials (`token`, `botToken`, `appToken`, etc.)
* `tools` — `policy.{allow,deny}` lists controlling which tools (`exec`, `read`, `write`, `browser`, `web_search`, `cron`, etc.) agents can call
* `skills` — `allowBundled`, `load.extraDirs`, `install.nodeManager`
* `plugins` — `enabled`, `allow`/`deny`, `entries.*`
* `mcp` — `servers`, `sessionIdleTtlMs`
* `browser`, `logging`, `cron`, `hooks`, `ui`, `env`, `secrets`, `auth`, `discovery`, `acp` — see the [configuration reference](https://docs.openclaw.ai/gateway/configuration-reference) for the full list of fields
Example usage [#example-usage]
Install OpenClaw with gateway and agent defaults [#install-openclaw-with-gateway-and-agent-defaults]
```json title="codify.jsonc"
[
{
"type": "openclaw",
"settings": {
"gateway": { "port": 18789, "bind": "loopback" },
"agents": { "defaults": { "model": "anthropic/claude-sonnet-4-6" } }
}
}
]
```
OpenClaw with a Telegram channel and restricted tools [#openclaw-with-a-telegram-channel-and-restricted-tools]
```json title="codify.jsonc"
[
{
"type": "openclaw",
"settings": {
"channels": {
"telegram": {
"botToken": "",
"dmPolicy": "allowlist",
"allowFrom": ["123456789"]
}
},
"tools": {
"policy": { "allow": ["exec", "read", "write", "web_search"] }
}
}
}
]
```
Notes [#notes]
* OpenClaw is installed via the official installer (`curl -fsSL https://openclaw.ai/install.sh | bash`) on both macOS and Linux.
* The configuration file lives at `~/.openclaw/openclaw.json` and uses JSON5 (Codify reads/writes it as plain JSON, so any comments in a hand-edited file will not be preserved).
* The `settings` parameter merges only the declared top-level keys. Existing sections not in your Codify config are left untouched.
* After applying settings changes, Codify runs `openclaw gateway restart` so the running gateway picks up the new configuration.
* On destroy, the declared `settings` keys are removed and the OpenClaw binary, config, and state directory (`~/.openclaw`) are removed.
* Model provider authentication (API keys) and full guided onboarding are not managed by this resource — configure credentials under `settings.models` / `settings.auth`, or run `openclaw onboard` manually for an interactive setup.
* See the [OpenClaw configuration reference](https://docs.openclaw.ai/gateway/configuration-reference) for the complete list of configuration sections and fields.
# android-cli
The `android-cli` resource installs and configures [Android CLI](https://developer.android.com/tools/agents/android-cli), Google's command-line tool for managing the Android development environment. It manages the CLI itself, SDK packages, and Android Virtual Devices (AVDs) in a single resource.
On macOS, Android CLI is installed via the official curl script (ARM64 and x86\_64 supported). On Linux, only AMD64/x86\_64 is supported.
Parameters [#parameters]
* **sdkPath**: *(string)* Path to the Android SDK directory. Written to `~/.androidrc` as `--sdk=`. Defaults to the android CLI's built-in default location if omitted.
* **sdkPackages**: *(string\[])* Android SDK packages to install declaratively. Package paths use forward-slash notation (e.g. `platforms/android-35`, `build-tools/35.0.0`, `cmdline-tools/latest`, `platform-tools`, `system-images/android-35/google_apis_playstore/x86_64`). Run `android sdk list --all` to see all available identifiers.
* **emulators**: *(string\[])* Android emulator profiles to create as AVDs. Each string is a hardware profile name (e.g. `medium_phone`, `pixel_9`). Emulators are always created after `sdkPackages` are installed. Run `android emulator create --list-profiles` to see available profiles.
Example usage [#example-usage]
Install the CLI with essential SDK packages:
```json title="codify.jsonc"
[
{
"type": "android-cli",
"sdkPackages": [
"cmdline-tools/latest",
"platform-tools",
"platforms/android-35",
"build-tools/35.0.0"
]
}
]
```
Full Android development environment with an emulator:
```json title="codify.jsonc"
[
{
"type": "android-cli",
"sdkPackages": [
"cmdline-tools/latest",
"platform-tools",
"platforms/android-35",
"build-tools/35.0.0",
"system-images/android-35/google_apis_playstore/x86_64"
],
"emulators": ["pixel_9"]
}
]
```
Common emulator profiles [#common-emulator-profiles]
| Profile | Description |
| ---------------------------- | ------------------------------ |
| `medium_phone` | Generic medium phone (default) |
| `small_phone` | Generic small phone |
| `foldable` | Foldable form factor |
| `medium_tablet` | Generic medium tablet |
| `pixel_9` | Google Pixel 9 |
| `pixel_9_pro` | Google Pixel 9 Pro |
| `pixel_9_pro_fold` | Google Pixel 9 Pro Fold |
| `pixel_8` | Google Pixel 8 |
| `wear_os_large_round` | Wear OS round watch |
| `tv_1080p` | Android TV 1080p |
| `automotive_1024p_landscape` | Android Automotive |
Notes [#notes]
* Linux ARM64 is **not** supported. Only AMD64/x86\_64 is supported on Linux.
* AVDs are removed using `android emulator remove`.
* Run `android info` to display the default SDK path in use.
# android-studio
The Android Studio resource installs Android Studios. It supports the current and all previous versions.
It also allows preview and beta versions to be installed.
Parameters: [#parameters]
* **version**: *(string)* The version to install. This will default to the latest stable version if it isn't
specified. For a list of all available versions please see: [https://developer.android.com/studio/archive](https://developer.android.com/studio/archive).
* **directory**: *(string)* A custom directory to install Android Studios to. This defaults to `/Applications`
if left unspecified
Example usage: [#example-usage]
Stable version:
```json title="codify.jsonc"
[
{
"type": "Android Studio"
}
]
```
Stable and previous version:
```json title="codify.jsonc"
[
{
"type": "Android Studio"
},
{
"type": "Android Studio",
"version": "2024.2.1.8"
}
]
```
Custom directory:
```json title="codify.jsonc"
[
{
"type": "Android Studio",
"version": "2024.2.1.7",
"directory": "~/programs"
}
]
```
# asdf-install
Asdf is an all-in-one tool manager designed to replace individual language specific version managers
such as pyenv, nvm, rbenv, etc.
The `asdf-install` resource is responsible for installing
`.tool-versions` files and/or installing specific versions of tools. The `directory` parameter
installs all the tools found within the `${directory}/.tool-versions` file and loads it for
that directory and any subdirectories. When using the `directory` parameter, Codify
will also automatically install all the required plugins found in that file unlike
the `asdf` CLI.
The `plugin` and `versions` parameters install specific versions of a tool and must be
used together but cannot be used with the `directory` parameter. Tools installed using `plugin`
and `version` parameters must be loaded separately using the `asdf-local` or `asdf-global`
resources.
See [the main page](/docs/core-resources/asdf/asdf) for an overview of the various asdf resources.
Parameters: [#parameters]
* **directory**: *(string)* The directory that contains `.tool-versions` file to install. This
parameter cannot be used together with the plugin and versions parameter.
* **plugin**: *(string)* The name of the asdf plugin. The plugin must already exist on the system.
Install plugins using [`asdf`](/docs/core-resources/asdf/asdf) or
[`asdf-plugin`](/docs/core-resources/asdf/asdf-plugin). This parameter must be used together
with the versions parameter.
* **versions**: *(array\[string])* An list of tool versions to install. The versions
must match one of the versions listed in `asdf list all `, asdf does not perform
any fuzzy matching. Asdf provides the `latest` version to install the latest version of a tool.
This parameter must be used together with the `plugin` parameter
Example usage: [#example-usage]
Install a .tool-versions file [#install-a-tool-versions-file]
```json title="codify.jsonc"
[
{
"type": "asdf"
},
{
"type": "asdf-install",
"directory": "~/projects/my-project"
}
]
```
Install the global .tool-versions file [#install-the-global-tool-versions-file]
```json title="codify.jsonc"
[
{
"type": "asdf"
},
{
"type": "asdf-install",
"directory": "~"
}
]
```
Install the latest version of ruby [#install-the-latest-version-of-ruby]
```json title="codify.jsonc"
[
{
"type": "asdf",
"plugins": ["ruby"]
},
{
"type": "asdf-install",
"plugin": "ruby",
"versions": ["latest"]
},
{
"type": "asdf-global",
"plugin": "ruby",
"version": "ruby"
}
]
```
Dependencies: [#dependencies]
asdf-install has a hard dependency on asdf being installed on the system already or
`asdf` being in the config.
asdf-install using the `plugin` and `versions` parameters requires that the plugin be
already installed on the system or `asdf.plugins` or `asdf-plugins` exists in the config.
# asdf-plugin
Asdf is an all-in-one tool manager designed to replace individual language specific version managers
such as pyenv, nvm, rbenv, etc.
The `asdf-plugin` resource installs and
manages plugins. A full list of plugins can be found at [https://github.com/asdf-community](https://github.com/asdf-community).
Optionally `asdf-plugin` can also install and manage versions of the tool using the
`versions` parameter. This will install the tool and not just the plugin that manages
the tool. Tools are still not usable after they are installed until they are loaded
with `asdf-local` or `asdf-global`.
See [the main page](/docs/core-resources/asdf/asdf) for an overview of the various asdf
resources.
Parameters: [#parameters]
* **plugin**: *(string, required)* The name of the asdf plugin. The name must match one of the plugins
found in [https://github.com/asdf-community](https://github.com/asdf-community).
* **versions**: *(array\[string])* An optional list of tool versions to install. The versions
must match one of the versions listed in `asdf list all `, asdf does not perform
any fuzzy matching. Asdf provides the `latest` version to install the latest version of a tool.
* **gitUrl**: *(string)* Optionally, specify a git url to clone the asdf plugin from.
Example usage: [#example-usage]
Install asdf and the golang plugin: [#install-asdf-and-the-golang-plugin]
```json title="codify.jsonc"
[
{
"type": "asdf"
},
{
"type": "asdf-plugin",
"plugin": "golang"
}
]
```
Install asdf, the golang plugin, and the latest version of golang: [#install-asdf-the-golang-plugin-and-the-latest-version-of-golang]
```json title="codify.jsonc"
[
{
"type": "asdf"
},
{
"type": "asdf-plugin",
"plugin": "golang",
"versions": ["latest"]
}
]
```
Fully specify the url of the golang plugin [#fully-specify-the-url-of-the-golang-plugin]
```json title="codify.jsonc"
[
{
"type": "asdf"
},
{
"type": "asdf-plugin",
"plugin": "golang",
"gitUrl": "https://github.com/asdf-community/asdf-golang.git"
}
]
```
Dependencies: [#dependencies]
asdf-plugin has a hard dependency on asdf being installed on the system already or
`asdf` being in the config.
# asdf
Asdf is an all-in-one tool version manager designed to replace individual language-specific version managers such as pyenv, nvm, rbenv, and others. It operates on a plugin-based system to support different tools. A full list of supported plugins can be found at [https://github.com/asdf-community](https://github.com/asdf-community).
Installing a tool with asdf is a multistep process:
1. Install asdf
2. Install the plugin for the desired language/tool: `asdf plugin add `
3. Install the desired version of the tool: `asdf install `
4. Set the version globally or locally before it can be used: `asdf global ` or `asdf local `
Asdf also supports `.tool-versions` files. Running `asdf install` in a directory containing a `.tool-versions` file installs all the tools listed in it. The installed tools are then available in that directory and its subdirectories.
Platform-specific installation [#platform-specific-installation]
* **macOS**: Installed via Homebrew (`brew install asdf`). Homebrew must be present.
* **Linux**: The latest release binary is downloaded directly from GitHub and placed in `~/.local/bin`. Codify also adds the asdf shims directory to `PATH` in your shell RC file.
Codify Resources [#codify-resources]
* **asdf** — Installs asdf itself and optionally installs plugins.
* **[asdf-plugin](/docs/resources/asdf/asdf-plugin)** — Installs an asdf plugin and optionally installs specific versions of the tool.
* **[asdf-install](/docs/resources/asdf/asdf-install)** — Installs a `.tool-versions` file in a directory, or installs specific versions of a tool. Automatically installs missing plugins.
Parameters: [#parameters]
* **plugins**: *(array\[string])* An optional list of asdf plugins to install. The plugin names must match those found at [https://github.com/asdf-community](https://github.com/asdf-community). This list is managed statefully — plugins are added when they appear in the list and removed when they are taken off.
Example usage: [#example-usage]
Install asdf [#install-asdf]
```json title="codify.jsonc"
[
{
"type": "asdf"
}
]
```
Install asdf with plugins [#install-asdf-with-plugins]
```json title="codify.jsonc"
[
{
"type": "asdf",
"plugins": ["nodejs", "python"]
}
]
```
Install a .tool-versions file [#install-a-tool-versions-file]
The `asdf-install` resource automatically installs missing plugins when using the `directory` parameter. The config below installs asdf and then installs all tool versions specified in `~/path/to/dir/.tool-versions`.
```json title="codify.jsonc"
[
{
"type": "asdf"
},
{
"type": "asdf-install",
"directory": "~/path/to/dir"
}
]
```
Install and version-pin a tool [#install-and-version-pin-a-tool]
Asdf provides the `latest` keyword as a shortcut. Otherwise a fully qualified version (down to the patch level) must be used. The config below installs asdf, the Node.js plugin, and two Node.js versions.
```json title="codify.jsonc"
[
{
"type": "asdf",
"plugins": ["nodejs"]
},
{
"type": "asdf-install",
"plugin": "nodejs",
"versions": ["latest", "20.18.0"]
}
]
```
# clion
The clion resource installs [JetBrains CLion](https://www.jetbrains.com/clion/), a C/C++ IDE. On macOS it is installed via Homebrew Cask (`brew install --cask clion`); on Linux via Snap (`snap install clion --classic`).
Parameters [#parameters]
* **settingsZip** *(string, optional)* — Absolute path to a CLion settings ZIP file (exported via *File | Manage IDE Settings | Export Settings*) to import on first install. The archive is extracted directly into the CLion config directory, so all exported settings (keymaps, code styles, inspections, etc.) are applied before CLion is first launched.
* **importSettings** *(boolean, optional, default: `true`)* — Controls whether the `settingsZip` is imported during `create`. Set to `false` to skip the import even when `settingsZip` is specified. This is a setting parameter and is not tracked as state, so it only has effect when the resource is first applied.
* **plugins** *(string\[], optional)* — JetBrains Marketplace plugin IDs to install (e.g. `"com.github.copilot"`, `"Docker"`). Plugin IDs can be found on the plugin's page in the Marketplace under *Additional Information*. Plugins are managed statefully: Codify adds missing plugins and removes plugins no longer in the list.
* **jvmMaxHeapSize** *(string, optional)* — Maximum JVM heap allocated to CLion, e.g. `"2048m"` for 2 GB or `"4096m"` for 4 GB. Written to `clion.vmoptions` in the IDE config directory as `-Xmx`.
* **jvmMinHeapSize** *(string, optional)* — Initial JVM heap allocated to CLion, e.g. `"512m"`. Written to `clion.vmoptions` as `-Xms`. Typically set to half the max heap size.
Example usage [#example-usage]
Install CLion with plugins [#install-clion-with-plugins]
```json title="codify.jsonc"
[
{
"type": "clion",
"plugins": [
"com.github.copilot",
"Docker"
]
}
]
```
Install CLion, import previous settings, and increase heap [#install-clion-import-previous-settings-and-increase-heap]
```json title="codify.jsonc"
[
{
"type": "clion",
"settingsZip": "/path/to/clion-settings.zip",
"importSettings": true,
"jvmMaxHeapSize": "4096m",
"jvmMinHeapSize": "1024m",
"plugins": [
"com.github.copilot",
"Docker"
]
}
]
```
Notes [#notes]
* On macOS a CLI launcher symlink is created at `/usr/local/bin/clion` during install so that `clion` is available in terminal sessions. It is removed on destroy.
* Plugin IDs must be exact JetBrains Marketplace IDs. You can find them on the plugin's Marketplace page under *Additional Information → Plugin ID*.
* The `settingsZip` import only runs during `create` (first apply), not on subsequent applies. If you need to re-import, destroy and re-apply the resource.
* JVM options are written to `clion.vmoptions` in `~/Library/Application Support/JetBrains/CLion/` on macOS and `~/.config/JetBrains/CLion/` on Linux. If CLion has never been launched, Codify creates this directory and file automatically.
* On Linux, Snap must be available. Codify will attempt to install `snapd` via the system package manager if it is not found.
# cursor
The cursor resource installs [Cursor](https://cursor.com) — an AI-first code editor built on VS Code — and manages its extensions, editor settings, and MCP (Model Context Protocol) server configuration.
On **macOS**, Cursor is installed via Homebrew cask (`brew install --cask cursor`).
On **Linux**, Cursor is downloaded as an AppImage to `~/.local/bin/cursor`.
Parameters [#parameters]
* **directory**: *(string)* Installation directory. Defaults to `/Applications` on macOS and `~/.local/bin` on Linux.
* **extensions**: *(string\[])* Cursor extensions to install by ID (e.g. `"ms-python.python"`). Cursor is compatible with most VS Code extensions available on the [Open VSX Registry](https://open-vsx.org).
* **settings**: *(object)* Editor settings to merge into Cursor's `settings.json`. Uses the same key/value format as VS Code settings.
* macOS path: `~/Library/Application Support/Cursor/User/settings.json`
* Linux path: `~/.config/Cursor/User/settings.json`
* **mcpServers**: *(object)* MCP servers to configure in `~/.cursor/mcp.json`. Each key is the server name and each value is a server configuration object with:
* `command` *(string, optional)*: The executable to run (e.g. `"npx"`)
* `args` *(string\[], optional)*: Arguments to pass to the command
* `env` *(object, optional)*: Environment variables for the server process
* `url` *(string, optional)*: URL for SSE-based remote MCP servers
Example usage [#example-usage]
```json title="codify.jsonc"
[
{
"type": "cursor",
"extensions": ["ms-python.python", "eamodio.gitlens"],
"settings": {
"editor.fontSize": 14,
"editor.formatOnSave": true
}
}
]
```
```json title="codify.jsonc"
[
{
"type": "cursor",
"extensions": ["ms-python.python", "eamodio.gitlens"],
"settings": {
"editor.fontSize": 14,
"editor.tabSize": 2,
"editor.formatOnSave": true
},
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/projects"]
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": ""
}
}
}
}
]
```
# goland
The goland resource installs [JetBrains GoLand](https://www.jetbrains.com/go/), a Go IDE. On macOS it is installed via Homebrew Cask (`brew install --cask goland`); on Linux via Snap (`snap install goland --classic`).
Parameters [#parameters]
* **settingsZip** *(string, optional)* — Absolute path to a GoLand settings ZIP file (exported via *File | Manage IDE Settings | Export Settings*) to import on first install. The archive is extracted directly into the GoLand config directory, so all exported settings (keymaps, code styles, inspections, etc.) are applied before GoLand is first launched.
* **importSettings** *(boolean, optional, default: `true`)* — Controls whether the `settingsZip` is imported during `create`. Set to `false` to skip the import even when `settingsZip` is specified. This is a setting parameter and is not tracked as state, so it only has effect when the resource is first applied.
* **plugins** *(string\[], optional)* — JetBrains Marketplace plugin IDs to install (e.g. `"com.github.copilot"`, `"Docker"`). Plugin IDs can be found on the plugin's page in the Marketplace under *Additional Information*. Plugins are managed statefully: Codify adds missing plugins and removes plugins no longer in the list.
* **jvmMaxHeapSize** *(string, optional)* — Maximum JVM heap allocated to GoLand, e.g. `"2048m"` for 2 GB or `"4096m"` for 4 GB. Written to `goland.vmoptions` in the IDE config directory as `-Xmx`.
* **jvmMinHeapSize** *(string, optional)* — Initial JVM heap allocated to GoLand, e.g. `"512m"`. Written to `goland.vmoptions` as `-Xms`. Typically set to half the max heap size.
Example usage [#example-usage]
Install GoLand with plugins [#install-goland-with-plugins]
```json title="codify.jsonc"
[
{
"type": "goland",
"plugins": [
"com.github.copilot",
"Docker"
]
}
]
```
Install GoLand, import previous settings, and increase heap [#install-goland-import-previous-settings-and-increase-heap]
```json title="codify.jsonc"
[
{
"type": "goland",
"settingsZip": "/path/to/goland-settings.zip",
"importSettings": true,
"jvmMaxHeapSize": "4096m",
"jvmMinHeapSize": "1024m",
"plugins": [
"com.github.copilot",
"Docker"
]
}
]
```
Notes [#notes]
* On macOS a CLI launcher symlink is created at `/usr/local/bin/goland` during install so that `goland` is available in terminal sessions. It is removed on destroy.
* Plugin IDs must be exact JetBrains Marketplace IDs. You can find them on the plugin's Marketplace page under *Additional Information → Plugin ID*.
* The `settingsZip` import only runs during `create` (first apply), not on subsequent applies. If you need to re-import, destroy and re-apply the resource.
* JVM options are written to `goland.vmoptions` in `~/Library/Application Support/JetBrains/GoLand/` on macOS and `~/.config/JetBrains/GoLand/` on Linux. If GoLand has never been launched, Codify creates this directory and file automatically.
* On Linux, Snap must be available. Codify will attempt to install `snapd` via the system package manager if it is not found.
# intellij-idea
The intellij-idea resource installs [JetBrains IntelliJ IDEA](https://www.jetbrains.com/idea/), a general-purpose JVM/Java IDE. As of 2025.3, IntelliJ IDEA ships as a unified distribution that includes both the free Community tier and Ultimate features, which unlock within the same install via a subscription. On macOS it is installed via Homebrew Cask (`brew install --cask intellij-idea`); on Linux via Snap (`snap install intellij-idea-community --classic`).
Parameters [#parameters]
* **settingsZip** *(string, optional)* — Absolute path to an IntelliJ IDEA settings ZIP file (exported via *File | Manage IDE Settings | Export Settings*) to import on first install. The archive is extracted directly into the IntelliJ IDEA config directory, so all exported settings (keymaps, code styles, inspections, etc.) are applied before IntelliJ IDEA is first launched.
* **importSettings** *(boolean, optional, default: `true`)* — Controls whether the `settingsZip` is imported during `create`. Set to `false` to skip the import even when `settingsZip` is specified. This is a setting parameter and is not tracked as state, so it only has effect when the resource is first applied.
* **plugins** *(string\[], optional)* — JetBrains Marketplace plugin IDs to install (e.g. `"com.github.copilot"`, `"Docker"`). Plugin IDs can be found on the plugin's page in the Marketplace under *Additional Information*. Plugins are managed statefully: Codify adds missing plugins and removes plugins no longer in the list.
* **jvmMaxHeapSize** *(string, optional)* — Maximum JVM heap allocated to IntelliJ IDEA, e.g. `"2048m"` for 2 GB or `"4096m"` for 4 GB. Written to `idea.vmoptions` in the IDE config directory as `-Xmx`.
* **jvmMinHeapSize** *(string, optional)* — Initial JVM heap allocated to IntelliJ IDEA, e.g. `"512m"`. Written to `idea.vmoptions` as `-Xms`. Typically set to half the max heap size.
Example usage [#example-usage]
Install IntelliJ IDEA with plugins [#install-intellij-idea-with-plugins]
```json title="codify.jsonc"
[
{
"type": "intellij-idea",
"plugins": [
"com.github.copilot",
"Docker"
]
}
]
```
Install IntelliJ IDEA, import previous settings, and increase heap [#install-intellij-idea-import-previous-settings-and-increase-heap]
```json title="codify.jsonc"
[
{
"type": "intellij-idea",
"settingsZip": "/path/to/intellij-idea-settings.zip",
"importSettings": true,
"jvmMaxHeapSize": "4096m",
"jvmMinHeapSize": "1024m",
"plugins": [
"com.github.copilot",
"Docker"
]
}
]
```
Notes [#notes]
* On macOS a CLI launcher symlink is created at `/usr/local/bin/idea` during install so that `idea` is available in terminal sessions. It is removed on destroy.
* Plugin IDs must be exact JetBrains Marketplace IDs. You can find them on the plugin's Marketplace page under *Additional Information → Plugin ID*.
* The `settingsZip` import only runs during `create` (first apply), not on subsequent applies. If you need to re-import, destroy and re-apply the resource.
* JVM options are written to `idea.vmoptions` in `~/Library/Application Support/JetBrains/IntelliJIdea/` on macOS and `~/.config/JetBrains/IntelliJIdea/` on Linux. If IntelliJ IDEA has never been launched, Codify creates this directory and file automatically.
* On Linux, Snap must be available. Codify will attempt to install `snapd` via the system package manager if it is not found.
* IntelliJ IDEA is the unified distribution covering both the free Community tier and the Ultimate edition; Ultimate features are unlocked within the same install via an active subscription.
# phpstorm
The phpstorm resource installs [JetBrains PhpStorm](https://www.jetbrains.com/phpstorm/), a PHP IDE. On macOS it is installed via Homebrew Cask (`brew install --cask phpstorm`); on Linux via Snap (`snap install phpstorm --classic`).
Parameters [#parameters]
* **settingsZip** *(string, optional)* — Absolute path to a PhpStorm settings ZIP file (exported via *File | Manage IDE Settings | Export Settings*) to import on first install. The archive is extracted directly into the PhpStorm config directory, so all exported settings (keymaps, code styles, inspections, etc.) are applied before PhpStorm is first launched.
* **importSettings** *(boolean, optional, default: `true`)* — Controls whether the `settingsZip` is imported during `create`. Set to `false` to skip the import even when `settingsZip` is specified. This is a setting parameter and is not tracked as state, so it only has effect when the resource is first applied.
* **plugins** *(string\[], optional)* — JetBrains Marketplace plugin IDs to install (e.g. `"com.github.copilot"`, `"Docker"`). Plugin IDs can be found on the plugin's page in the Marketplace under *Additional Information*. Plugins are managed statefully: Codify adds missing plugins and removes plugins no longer in the list.
* **jvmMaxHeapSize** *(string, optional)* — Maximum JVM heap allocated to PhpStorm, e.g. `"2048m"` for 2 GB or `"4096m"` for 4 GB. Written to `phpstorm.vmoptions` in the IDE config directory as `-Xmx`.
* **jvmMinHeapSize** *(string, optional)* — Initial JVM heap allocated to PhpStorm, e.g. `"512m"`. Written to `phpstorm.vmoptions` as `-Xms`. Typically set to half the max heap size.
Example usage [#example-usage]
Install PhpStorm with plugins [#install-phpstorm-with-plugins]
```json title="codify.jsonc"
[
{
"type": "phpstorm",
"plugins": [
"com.github.copilot",
"Docker"
]
}
]
```
Install PhpStorm, import previous settings, and increase heap [#install-phpstorm-import-previous-settings-and-increase-heap]
```json title="codify.jsonc"
[
{
"type": "phpstorm",
"settingsZip": "/path/to/phpstorm-settings.zip",
"importSettings": true,
"jvmMaxHeapSize": "4096m",
"jvmMinHeapSize": "1024m",
"plugins": [
"com.github.copilot",
"Docker"
]
}
]
```
Notes [#notes]
* On macOS a CLI launcher symlink is created at `/usr/local/bin/phpstorm` during install so that `phpstorm` is available in terminal sessions. It is removed on destroy.
* Plugin IDs must be exact JetBrains Marketplace IDs. You can find them on the plugin's Marketplace page under *Additional Information → Plugin ID*.
* The `settingsZip` import only runs during `create` (first apply), not on subsequent applies. If you need to re-import, destroy and re-apply the resource.
* JVM options are written to `phpstorm.vmoptions` in `~/Library/Application Support/JetBrains/PhpStorm/` on macOS and `~/.config/JetBrains/PhpStorm/` on Linux. If PhpStorm has never been launched, Codify creates this directory and file automatically.
* On Linux, Snap must be available. Codify will attempt to install `snapd` via the system package manager if it is not found.
# pycharm
The pycharm resource installs [JetBrains PyCharm](https://www.jetbrains.com/pycharm/), a Python IDE. On macOS it is installed via Homebrew Cask (`brew install --cask pycharm`); on Linux via Snap (`snap install pycharm-community --classic`).
Parameters [#parameters]
* **settingsZip** *(string, optional)* — Absolute path to a PyCharm settings ZIP file (exported via *File | Manage IDE Settings | Export Settings*) to import on first install. The archive is extracted directly into the PyCharm config directory, so all exported settings (keymaps, code styles, inspections, etc.) are applied before PyCharm is first launched.
* **importSettings** *(boolean, optional, default: `true`)* — Controls whether the `settingsZip` is imported during `create`. Set to `false` to skip the import even when `settingsZip` is specified. This is a setting parameter and is not tracked as state, so it only has effect when the resource is first applied.
* **plugins** *(string\[], optional)* — JetBrains Marketplace plugin IDs to install (e.g. `"intellij.jupyter"`, `"Docker"`). Plugin IDs can be found on the plugin's page in the Marketplace under *Additional Information*. Plugins are managed statefully: Codify adds missing plugins and removes plugins no longer in the list.
* **jvmMaxHeapSize** *(string, optional)* — Maximum JVM heap allocated to PyCharm, e.g. `"2048m"` for 2 GB or `"4096m"` for 4 GB. Written to `pycharm.vmoptions` in the IDE config directory as `-Xmx