Core Principles: YAGNI | KISS | DRY
Code Goals: Readability > cleverness | Type safety | Explicit | Maintainability | Testability
tsconfig.json: strict: true, noUnusedLocals: true, noUnusedParameters: true, noImplicitReturns: true
- Explicit return types for public functions
- Type inference for simple variables
- Use
unknowninstead ofany - Types for unions; interfaces for extensible shapes
- Zod schemas for runtime validation
Use optional chaining (?.), nullish coalescing (??), explicit === null || === undefined checks. Avoid implicit falsy checks.
- Submodules: 50-100 lines (soft target)
- Facades: 50-150 lines (public API re-exports)
- Hard limit: 200 lines per file
- File naming: kebab-case, self-documenting names
src/
├── cli/ # CLI infrastructure
├── commands/ # Commands (init, new, skills, etc) + phase handlers
├── domains/ # Business logic by domain (facade pattern)
├── services/ # Cross-domain services
├── shared/ # Pure utilities (no domain logic)
├── types/ # Domain-specific types & Zod schemas
└── index.ts # Entry point
- Target: <100 LOC per submodule
- Maximum: 200 LOC (hard limit)
- Facades: 50-150 LOC (orchestration only)
- Split if exceeding: Extract to smaller focused modules
Each domain exposes facade that re-exports public API, provides backward-compatible interface, hides implementation.
Complex commands: orchestrator (~100 LOC) + phase handlers (~50-100 LOC each). Each phase handles one responsibility, independently testable.
API subcommands: action router → typed handler. Each handler validates input (Zod schema), calls API client, formats output. Standard structure:
export async function handle(options: ApiHandlerOptions): Promise<void> {
const client = createApiClient(apiKey);
const result = await client.call(endpoint, params);
outputJson(result, options.json);
}Handlers support --json flag for machine-readable output.
- Use kebab-case:
file-scanner.ts,hash-calculator.ts - Self-documenting: Name describes purpose without reading content
- LLM-friendly: Grep/Glob tools understand filename purpose
- Test structure: Mirrors source (
src/domains/config/settings-merger.ts→tests/domains/config/settings-merger.test.ts)
// 1. Node.js built-in imports
import { resolve } from "node:path";
// 2. Internal imports (@/ aliases, sorted)
import { AuthManager } from "@/domains/github/github-auth.js";
import { logger } from "@/shared/logger.js";
// 3. External dependencies (sorted)
import { Octokit } from "@octokit/rest";
// 4. Constants, types, implementationUse @/ for all internal imports (defined in tsconfig.json):
@/*→src/*@/domains/*→src/domains/*@/shared/*→src/shared/*@/types→src/types
Always include .js extension for ESM compatibility.
- camelCase:
targetDirectory,downloadFile() - Descriptive:
customClaudeFilesnotcf,tmpDirnott
- PascalCase:
AuthManager,DownloadProgress,ArchiveType
- UPPER_SNAKE_CASE:
MAX_EXTRACTION_SIZE,SERVICE_NAME - Readonly arrays:
as constfor immutability
- Prefix with is/has/should:
isNonInteractive,hasAccess,shouldExclude
- Target: <50 LOC per function
- Maximum: <100 LOC per function
- Single Responsibility Principle: One clear purpose
- Early returns: Reduce nesting
- Use options object for >3 parameters
- Destructure at function start:
const { url, name, destDir } = options;
- Explicit error types with messages
- Validate inputs early (fail fast)
- Try-catch for async/fallible operations
- Use
finallyfor cleanup - Never swallow errors silently
Critical: Inside withProcessLock(), throw errors instead of process.exit(1). Exit handler will perform graceful cleanup.
await withProcessLock("lock-name", async () => {
if (error) throw new Error("User message"); // ✅ Throw, not process.exit()
});- Static constants
- Instance properties
- Constructor
- Public methods
- Private methods
- private: Internal implementation details
- public: Public API
- static: Utilities with no instance state
export class ClaudeKitError extends Error {
constructor(message: string, public code?: string, public statusCode?: number) {
super(message);
this.name = "ClaudeKitError";
}
}
export class AuthenticationError extends ClaudeKitError {
constructor(message: string) {
super(message, "AUTH_ERROR", 401);
}
}- Try-catch with specific error handling (check error.status, error.code)
- Cleanup in
finallyblocks - Always provide context in error messages
- Always
awaitpromises - Use
Promise.all()for parallel operations - Never fire-and-forget promises
- Top-level async for commands
- Return promises explicitly when needed
- Avoid nested callbacks
export const NewCommandOptionsSchema = z.object({
dir: z.string().default("."),
kit: KitType.optional(),
force: z.boolean().default(false),
});
export type NewCommandOptions = z.infer<typeof NewCommandOptionsSchema>;- Define schemas for all external inputs
- Validate at boundaries (commands, API)
- Use
refine()for custom validation rules
- Never log tokens directly:
logger.debug("Token method:", method);✅ - Sanitize in logger: Replace patterns like
ghp_[...]/gwithghp_*** - Keychain integration for secure storage
- Format validation:
ghp_*,github_pat_*
- Resolve to canonical paths
- Reject relative paths with ".."
- Verify target within base:
!relativePath.startsWith("..")
Always skip: .env, .env.local, *.key, *.pem, node_modules/, .git/, dist/, build/, .gitignore, CLAUDE.md, .mcp.json
- Unit tests for all core libraries
- Command integration tests
- Authentication/download/extraction tests
- Skills migration tests
- Doctor command tests
- Mirrors source structure
- Uses Bun test runner
- Temporary directories for filesystem isolation
- No fake data or mocks (real implementations)
Use PathResolver (from shared/path-resolver.ts) for all path operations:
getConfigDir(),getCacheDir()- Platform-aware config/cachebuildSkillsPath(),buildComponentPath()- Local vs global modes- Respects XDG spec (Linux/macOS) and %LOCALAPPDATA% (Windows)
- Validates paths before operations to prevent traversal attacks
Never hardcode platform-specific paths.
Require user confirmation before installing dependencies. Skip auto-install in non-interactive environments (CI/CD). Never elevate privileges automatically. Provide manual fallback instructions.
Must be listed in package.json (not just transitive):
express— HTTP serverws— WebSocket supportchokidar— File watching (HMR)get-port— Port detection/fallbackopen— Browser auto-open
- Biome formatting: Long JSX lines auto-wrapped to fit terminal
- Props destructuring: Prefer destructuring in function signature
- Type safety: All props typed via
React.FC<Props>orinterface Props - File naming: kebab-case (e.g.,
config-editor.tsx,migration-plan-view.tsx) - Component size: <300 LOC per component, split if larger
- Dashboard entry:
src/ui/(Express server + Vite dev server on single port) - Pages: GlobalConfig, ProjectConfig, Migrate, Skills, Onboarding, ProjectDashboard
- Backend API routes: 16 routes covering action, migration, project, skill, config management
- WebSocket: Live update support for long-running operations
Before every commit, run:
bun run typecheck && bun run lint:fix && bun test && bun run build && bun run ui:buildAll must pass. No exceptions.
- #346 Process-lock safety: Throw errors instead of
process.exit(1)insidewithProcessLock() - #344 Installation detection: Fallback support for installs without metadata.json
- Skills rename: Command renamed from
skilltoskills - Deletion handling: Glob pattern support via picomatch, cross-platform path.sep