This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is a Codify plugin that provides 50+ declarative system configuration resources (Homebrew, Git, shell aliases, Python environments, etc.) built on the @codifycli/plugin-core framework. Users write JSON configurations describing their desired system state, and the framework generates and executes plans to achieve that state.
# Build the plugin (compiles TypeScript, bundles with Rollup, generates schemas.json)
npm run build
# Run all tests (unit + integration)
npm test
# Run unit tests only (fast - tests in src/**/*.test.ts)
npm run test:unit
# Run integration tests only (slow - full lifecycle tests in test/**/*.test.ts)
npm run test:integration
# Run integration tests in development mode
npm run test:integration:dev
# Deploy to Cloudflare R2
npm run deploy
# Deploy beta version
npm run deploy:betaRunning a single test:
# Unit test
npx vitest src/resources/shell/path/path-resource.test.ts
# Integration test
npx vitest test/shell/path.test.tsThis plugin uses a Resource-based architecture where:
- Each resource type (git, homebrew, alias, etc.) extends the
Resource<ConfigType>base class from@codifycli/plugin-core - Resources are registered in
src/index.tsviaPlugin.create('default', [resource instances]) - Resources implement 5 core lifecycle methods:
getSettings()- Define schema, parameters, dependencies, OS supportrefresh()- Read current system statecreate()- Create new resourcemodify()- Modify existing resource (optional)destroy()- Remove resource
All resources are registered in /src/index.ts:
runPlugin(Plugin.create('default', [
new GitResource(),
new HomebrewResource(),
new AliasResource(),
// ... 50+ more resources
]))Every resource follows this pattern:
export class MyResource extends Resource<MyConfig> {
getSettings(): ResourceSettings<MyConfig> {
return {
id: 'unique-id',
operatingSystems: [OS.Darwin, OS.Linux],
schema: JSONSchema or ZodSchema,
parameterSettings: { /* ... */ },
dependencies: ['other-resource-ids'],
allowMultiple: { /* ... */ }
}
}
async refresh(params): Promise<Partial<MyConfig> | null> {
// Returns null if resource doesn't exist
// Returns object with current state if it exists
}
async create(plan): Promise<void> { /* ... */ }
async modify(pc, plan): Promise<void> { /* ... */ }
async destroy(plan): Promise<void> { /* ... */ }
}1. Simple Singleton (e.g., shell/alias/alias-resource.ts):
- One resource instance per config entry
- Uses
allowMultiple.identifyingParameters: ['alias']to support multiple aliases - Each unique identifying parameter value becomes a separate resource
2. Multi-Declaration (e.g., shell/aliases/aliases-resource.ts):
- Manages multiple items in a single resource (array of aliases)
- Uses Zod schemas for type safety
- Implements
declarationsOnlymode for stateless/stateful behavior
3. Complex with Auto-Discovery (e.g., git/repository/git-repository.ts):
- Supports multiple configuration modes (single repo vs multiple repos)
- Uses
allowMultiple.matcher()for custom matching logic - Uses
allowMultiple.findAllParameters()for system discovery - Declares
dependenciesto ensure prerequisites are met
CRITICAL DISTINCTION:
- Framework only manages explicitly declared items in the config
- System state is filtered to match declarations
- Safer default - won't accidentally capture unwanted system state
- Example: Only manage the paths/aliases the user explicitly listed
Implementation:
parameterSettings: {
paths: {
filterInStatelessMode: (desired, current) =>
current.filter((c) => desired.some((d) => d === c))
}
}- Framework manages complete state of resource
- Tracks what changed over time (additions/removals)
- Uses
StatefulParameterclasses withadd(),modify(),remove()methods - Example: Homebrew formulae - track all installed packages
Implementation:
parameterSettings: {
formulae: {
type: 'stateful',
definition: new FormulaeParameter(),
order: 2
}
}Two approaches are supported:
1. JSON Schema (traditional):
// Separate .json file
import Schema from './my-resource-schema.json'
export interface MyConfig extends StringIndexedObject {
field: string
}
getSettings() {
return { schema: Schema }
}2. Zod Schema (preferred):
// Single source of truth - schema and types in sync
export const schema = z.object({
field: z.string(),
optional: z.boolean().optional(),
})
export type MyConfig = z.infer<typeof schema>
getSettings() {
return { schema }
}Zod is preferred because types are automatically inferred from the schema, preventing drift between validation and TypeScript types.
- Fast tests for parsing, regex, data transformation
- No system calls
- Test individual functions in isolation
- Full lifecycle tests against real system
- Uses
PluginTester.fullTest()from@codifycli/plugin-test - Tests create → modify → destroy flow
- Includes validation callbacks
Integration Test Pattern:
import { PluginTester } from '@codifycli/plugin-test'
await PluginTester.fullTest(pluginPath, [
{ type: 'alias', alias: 'my-alias', value: 'ls -l' }
], {
validateApply: async () => {
// Verify resource was created
},
testModify: {
modifiedConfigs: [{ type: 'alias', alias: 'my-alias', value: 'pwd' }],
validateModify: async () => {
// Verify modification succeeded
}
},
validateDestroy: async () => {
// Verify resource was removed
}
})- Global
beforeAllsaves shell RC state and ensures prerequisites (Xcode, Homebrew on macOS) - Global
afterAllrestores shell RC to original state - Platform-specific setup using
TestUtils
The @codifycli/plugin-core framework provides:
Shell/PTY Access:
const $ = getPty()
// Safe spawn (never throws, returns status)
const { data, status } = await $.spawnSafe('command')
if (status === SpawnStatus.ERROR) { /* handle */ }
// Regular spawn (throws on error)
const { data } = await $.spawn('command', {
interactive: true,
cwd: '/path',
requiresRoot: true,
env: { VAR: 'value' }
})File Operations:
await FileUtils.addToStartupFile(lineToAdd)
await FileUtils.addToShellRc(lineToAdd)
await FileUtils.addPathToPrimaryShellRc(pathValue, prepend)
await FileUtils.removeLineFromFile(filePath, lineContent)
await FileUtils.fileExists(path)
await FileUtils.dirExists(path)OS Detection:
Utils.isMacOS()
Utils.isLinux()
Utils.isWindows()The build process (scripts/build.ts) does:
- Removes
dist/folder - Runs Rollup to compile TypeScript → ES modules
- Forks the built plugin and queries it for all resource schemas
- Merges each resource schema with base
ResourceSchema - Rebuilds with Rollup → CommonJS
- Writes
dist/schemas.jsoncontaining all resource schemas
The dist/schemas.json file is used by the CLI for validation and documentation.
Deployment (scripts/deploy.ts) uploads the built plugin to Cloudflare R2:
- Production:
plugins/{name}/{version}/index.js - Beta:
plugins/{name}/beta/index.js
Simple boolean:
allowMultiple: trueWith identifying parameters:
allowMultiple: {
identifyingParameters: ['path'] // Each unique 'path' = different resource
}With custom matcher and auto-discovery:
allowMultiple: {
matcher: (desired, current) => desired.directory === current.directory,
async findAllParameters() {
// Discover all instances on system
return [{ directory: '...' }, ...]
}
}parameterSettings: {
// Boolean setting (not tracked in state)
skipAlreadyInstalledCasks: {
type: 'boolean',
default: true,
setting: true
},
// Directory path
directory: {
type: 'directory'
},
// Modifiable array
paths: {
type: 'array',
itemType: 'directory',
canModify: true,
isElementEqual: (a, b) => a === b,
filterInStatelessMode: (desired, current) => /* ... */
},
// Stateful parameter with custom handler
formulae: {
type: 'stateful',
definition: new FormulaeParameter(),
order: 2
}
}Resources can declare dependencies on other resources:
getSettings() {
return {
dependencies: ['ssh-key', 'git'] // Apply these first
}
}The framework automatically validates dependencies exist and orders execution.
null= Resource doesn't exist on system{}= Resource exists with no state to track- Return
nullif refresh fails or resource not found
- File paths are case-insensitive
- Use
.toLowerCase()when comparing paths inallowMultiple.matcher() - Xcode Command Line Tools required for many operations
- Homebrew commonly used for package management
- File paths are case-sensitive
- Multiple package managers (apt, yum, dnf, snap)
- Shell RC files vary by distribution
- Always declare
operatingSystemsingetSettings() - Use
Utils.isMacOS(),Utils.isLinux()for platform-specific logic - Use
FileUtilsfor cross-platform file operations - Test on both macOS and Linux when possible
- Create directory:
src/resources/category/resource-name/ - Create schema file (JSON or Zod):
resource-name-schema.jsonor inline Zod - Create resource class extending
Resource<ConfigType> - Implement all required lifecycle methods
- Register in
src/index.ts - Create integration test in
test/category/resource-name.test.ts - Run
npm run testto validate
Core:
/src/index.ts- Resource registration/codify.json- Example configuration
Build:
/scripts/build.ts- Build process with schema collection/scripts/deploy.ts- Deployment to Cloudflare R2/rollup.config.js- Bundling configuration/tsconfig.json- TypeScript config (ES2024, strict mode)/vitest.config.ts- Test runner config
Testing:
/test/setup.ts- Global test setup/teardown/test/test-utils.ts- Test helpers
Example Resources (by complexity):
- Simple:
src/resources/shell/alias/alias-resource.ts - Multi-item:
src/resources/shell/aliases/aliases-resource.ts - Complex:
src/resources/git/repository/git-repository.ts - Stateful:
src/resources/homebrew/homebrew.ts