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