This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Codify is a configuration-as-code CLI tool that brings Infrastructure-as-Code principles to local development environments. It allows developers to declaratively define their development setup (packages, tools, system settings) in configuration files and apply them in a reproducible way. Think "Terraform for your local machine."
npm run build # Build TypeScript to dist/
npm run lint # Type-check with tscnpm test # Run all tests with Vitest
npm test -- path/to/test # Run specific test file
npm run posttest # Runs lint after tests./bin/dev.js <command> # Run CLI in development mode
./bin/dev.js apply # Example: run apply commandThe test command spins up a Tart VM to test Codify configs in isolation:
./bin/dev.js test --vm-os darwin # Test on macOS VM
./bin/dev.js test --vm-os linux # Test on Linux VM-
Command-Orchestrator Pattern: Commands (
src/commands/) are thin oclif wrappers. Orchestrators (src/orchestrators/) contain all business logic and workflow coordination. This separation enables reusability. -
Multi-Process Plugin System: The most unique architectural decision is running plugins as separate Node.js child processes communicating via IPC:
- Why: Isolation (crashes don't crash CLI), security (parent controls sudo), flexibility
- Plugin Process (
src/plugins/plugin-process.ts): Spawns plugins usingfork() - IPC Protocol (
src/plugins/plugin-message.ts): Type-safe message passing - Security: Plugins run isolated; parent process controls all sudo operations
- When plugins need sudo, they send
COMMAND_REQUESTevents back to parent
-
Event-Driven Architecture: Central event bus (
src/events/context.ts) using EventEmitter:- Tracks process/subprocess lifecycle (PLAN, APPLY, INITIALIZE_PLUGINS, etc.)
- Enables plugin-to-CLI communication (sudo prompts, login credentials, etc.)
- Powers progress tracking for UI
-
Reporter Pattern: Abstract
Reporterinterface with multiple implementations selected via--outputflag:DefaultReporter: Rich Ink-based TUI with React componentsPlainReporter: Simple text outputJsonReporter: Machine-readable JSONDebugReporter: Verbose loggingStubReporter: No-op for testing
-
Resource Lifecycle State Machine:
Parse Config → Validate → Resolve Dependencies → Plan → Apply- ResourceConfig: Desired state from config file
- Plan: Computed difference between desired and current state
- ResourcePlan: Per-resource operations (CREATE, UPDATE, DELETE, NOOP)
- Project: Container with dependency graph
-
Dependency Resolution:
- Explicit:
dependsOnfield in config - Implicit: Extracted from parameter references (e.g.,
${other-resource.param}) - Plugin-level: Plugins declare type dependencies (e.g., xcode-tools on macOS)
- Topological sort ensures correct evaluation order (
src/utils/dependency-graph-resolver.ts)
- Explicit:
-
/src/orchestrators/: Business logic layer - each file implements one CLI command's workflowplan.ts: Parse → Validate → Resolve deps → Generate planapply.ts: Execute plan after user confirmationimport.ts: Import existing resources into configtest.ts: VM-based testing with live config sync via file watcher
-
/src/plugins/: Plugin infrastructureplugin-manager.ts: Registry routing operations to pluginsplugin-process.ts: Child process lifecycle and IPCplugin.ts: High-level plugin API
-
/src/entities/: Domain models with rich behaviorProject: Container with dependency resolutionResourceConfig: Mutable config with dependency trackingPlan: Immutable plan with sorting/filtering
-
/src/parser/: Multi-format config parsing (JSON, JSONC, JSON5, YAML)- All parsers maintain source maps for error messages
- Cloud parser fetches from Dashboard API via UUID
-
/src/ui/: User interface layer/reporters/: Output strategy implementations/components/: React components for Ink TUI/store/: Jotai state management for UI
-
/src/connect/: Dashboard integration- WebSocket server for persistent connection
- OAuth flow handling
- JWT credential management
-
/src/generators/: Config file writers- Computes diffs for updating existing configs
- Writes to local files or cloud (via Dashboard API)
Apply Command Flow:
ApplyOrchestrator.run()
→ PlanOrchestrator.run()
→ PluginInitOrchestrator.run()
→ Parse configs → Project
→ PluginManager.initialize() → ResourceDefinitions
→ Project.resolveDependencies()
→ PluginManager.plan() → Plan
→ Reporter.promptConfirmation()
→ PluginManager.apply()
→ For each resource (topologically sorted):
→ Plugin.apply() [IPC to child process]
Plugin Communication Flow:
Parent Process Plugin Process
|-- initialize() -------->|
|<-- resourceDefinitions -|
|-- plan(resource) ------>|
| [Plugin needs sudo]
|<-- COMMAND_REQUEST -----|
|-- prompt user |
|-- COMMAND_GRANTED ----->|
|<-- PlanResponse --------|
- Single file Projects: Projects only currently support one file
- Cloud-First: UUIDs are valid "file paths" - enables seamless local/cloud switching
- XCode Tools Injection: On macOS,
xcode-toolsautomatically prepended (most resources depend on it) - Test VM Strategy: Uses Tart VMs with bind mounts (not copying) + file watcher for live config editing
- OS Filtering: Resources specify
os: ["Darwin", "Linux"]for conditional inclusion - Secure Mode:
--secureflag forces sudo prompt for every command (no password caching)
- Plugin Resolution: Local plugins use file paths (
.ts/.js), network plugins use semver versions - Source Maps: Preserved through entire parse → validate → plan flow for accurate error messages
- Event Timing: Events fire synchronously; use
ctx.once()carefully to avoid race conditions - Process Cleanup: Plugins must be killed on exit via
registerKillListeners - Reporter Lifecycle: Call
reporter.hide()before synchronous output to prevent UI corruption
- Ink Component Tests: Must polyfill
console.Consolefor test environment:import { Console } from 'node:console'; if (!console.Console) { console.Console = Console; }
- Plugin Tests: Use
StubReporterto avoid UI initialization - VM Tests:
testcommand uses Tart VMs with bind mounts for integration testing
- Framework: oclif CLI framework with manifest generation
- Module System: ES modules with NodeNext resolution
- Packaging:
oclif pack tarballsfor multi-platform binaries - Updates: Self-updating via S3 (
@oclif/plugin-update) - Code Signing: macOS notarization via
scripts/notarize.sh
- Import Paths: Use
.jsextensions in imports even though files are.ts(ES module resolution) - Schema Validation: Config changes require updating schemas in
@codifycli/schemaspackage - Plugin IPC: Plugins cannot directly read stdin (security isolation)
- Sudo Caching: Password cached in memory during session unless
--secureflag used - File Watcher: Use
persistent: falseoption to prevent hanging processes - Linting: ESLint enforces single quotes, specific import ordering, and strict type safety
- Reporter display methods are async: All
Reporterinterface display methods (displayPlan,displayImportResult,displayFileModifications,displayMessage,displayPluginError) returnPromise<void>. Alwaysawaitthem at call sites —DefaultReporter.updateRenderState()has a 50ms sleep, so unawaited calls causeprocess.exit(1)to fire before the UI renders. - Mock reporter async assertions: Assertions inside
MockReporterconfig callbacks (e.g.displayFileModifications) will silently pass if the call isn't awaited. Making display methods async surfaced latent bugs where expected file paths were wrong.
Plugin errors flow as structured PluginErrorData over IPC and are caught as PluginError instances on the CLI side:
IPC envelope (@codifycli/schemas):
interface PluginErrorData {
errorType: string; // 'apply_validation' | 'sudo_error' | 'unknown'
message: string;
data?: unknown;
}CLI carrier (src/common/errors.ts): PluginError extends CodifyError holds pluginName, resourceType, and errorData: PluginErrorData.
Reporter as view model: Reporters (not components) decide how to render each errorType. DefaultReporter.displayPluginError() branches on errorType to set the appropriate RenderStatus (APPLY_VALIDATION_ERROR with a ResourcePlan for plan diffs, PLUGIN_ERROR with a message string for generic errors). The DefaultComponent is purely display.
Shared formatter: src/ui/plugin-error-formatter.ts exports formatApplyValidationError(error: PluginError): string used by both PlainReporter and DefaultComponent.
Backward compat: plugin.ts#toErrorData() validates IPC data against ErrorResponseDataSchema (AJV); falls back to { errorType: 'unknown', message: data } for old plugins sending bare strings.