# CCS Code Standards

Last Updated: 2026-04-07

Code standards, modularization patterns, and conventions for the CCS codebase.

---

## Core Principles

### YAGNI (You Aren't Gonna Need It)
- No features "just in case"
- Only implement what is currently needed
- Delete unused code rather than commenting it out

### KISS (Keep It Simple, Stupid)
- Prefer simple solutions over clever ones
- Reduce complexity at every opportunity
- Use established patterns over custom implementations

### DRY (Don't Repeat Yourself)
- One source of truth for configuration
- Extract common logic into shared utilities
- Use barrel exports to centralize imports

---

## File Organization

### Directory Structure Rules

1. **Domain-based organization**: Group files by business domain, not by file type
2. **Barrel exports required**: Every directory must have an `index.ts` aggregating exports
3. **Flat within depth**: Keep nesting to 3 levels maximum
4. **Co-location**: Keep related files together (component + hooks + utils)

### File Naming Conventions

| Convention | Example | When to Use |
|------------|---------|-------------|
| kebab-case | `cliproxy-executor.ts` | All TypeScript/TSX files |
| kebab-case | `profile-detector.ts` | Multi-word file names |
| *-adapter.ts | `claude-adapter.ts`, `droid-adapter.ts` | TargetAdapter implementations |
| *-detector.ts | `droid-detector.ts` | Binary detection logic |
| *-manager.ts | `droid-config-manager.ts` | Config/state management |
| PascalCase | `BinaryManager` | Class exports only |
| camelCase | `detectProfile` | Function exports |

**File names should be descriptive**: LLMs should understand the file's purpose from its name alone without reading content.

### Correct Examples

```
src/cliproxy/binary-manager.ts      # Binary management logic
src/commands/doctor-command.ts      # Doctor CLI command handler
ui/src/components/cliproxy/provider-editor/index.tsx
```

### Incorrect Examples

```
src/utils/helper.ts                 # Too vague
src/cliproxy/manager.ts             # Which manager?
ui/src/components/Editor.tsx        # Not kebab-case
```

---

## File Size Limit: 200 Lines

**Target**: All code files should be under 200 lines.

**Exceptions** (with justification):
- Data files (model-pricing.ts, model-catalog.ts)
- Entry points with routing logic (ccs.ts)
- Complex transformation logic that cannot be meaningfully split

### Why 200 Lines?

1. **Context efficiency**: LLMs process smaller files faster
2. **Single responsibility**: Forces focused, testable modules
3. **Navigation**: Easier to scan and understand
4. **Maintainability**: Reduces merge conflicts

### When Files Exceed 200 Lines

If a file grows beyond 200 lines:

1. **Identify extraction candidates**:
   - Helper functions that could be utilities
   - Constants and type definitions
   - Subcomponents within React components
   - Related logic that forms a cohesive unit

2. **Create subdirectory structure**:
   ```
   # Before
   provider-editor.tsx (921 lines)

   # After
   provider-editor/
   ├── index.tsx           # Main component (200 lines)
   ├── model-mapping-form.tsx
   ├── endpoint-config.tsx
   ├── auth-section.tsx
   ├── hooks.ts
   ├── types.ts
   └── utils.ts
   ```

3. **Preserve public API**: Main export remains the same through barrel export

---

## Barrel Export Pattern

### What is a Barrel Export?

An `index.ts` file that aggregates and re-exports module contents:

```typescript
// src/cliproxy/index.ts

// Types (with explicit type keyword)
export type { PlatformInfo, BinaryInfo } from './types';

// Functions
export { detectPlatform } from './platform-detector';
export { BinaryManager } from './binary-manager';

// From subdirectories
export * from './auth';
export * from './services';
```

### Rules for Barrel Exports

1. **Every domain directory must have `index.ts`**
2. **Export types with `export type`** for tree-shaking
3. **Re-export subdirectories** for deep access
4. **Keep barrel exports flat** - no logic, only exports

### Import Patterns

```typescript
// CORRECT: Import from domain barrel
import { execClaudeWithCLIProxy, CLIProxyProvider } from '../cliproxy';
import { Config, Settings } from '../types';

// INCORRECT: Import from specific file (bypasses barrel)
import { execClaudeWithCLIProxy } from '../cliproxy/cliproxy-executor';
```

### Exception: Deep Imports

Allowed when:
- Importing private utilities not exposed in barrel
- Circular dependency avoidance
- Performance-critical tree-shaking

---

## Target Adapter Pattern

The target adapter pattern enables pluggable support for multiple CLI implementations (Claude Code, Factory Droid, Codex CLI, etc.) while preserving a unified profile system.

### Pattern Overview

**Each CLI target implements a `TargetAdapter` interface:**

```typescript
interface TargetAdapter {
  readonly type: TargetType;                                    // 'claude' | 'droid' | 'codex'
  readonly displayName: string;                                 // Human-readable name

  detectBinary(): TargetBinaryInfo | null;                      // Find CLI on system
  prepareCredentials(creds: TargetCredentials): Promise<void>;  // Deliver credentials
  buildArgs(profile: string, userArgs: string[]): string[];    // Build CLI args
  buildEnv(creds: TargetCredentials, type: string): Env;       // Build env vars
  exec(args: string[], env: Env): void;                        // Spawn CLI process
  supportsProfileType(type: string): boolean;                  // Validate profile
}
```

### Key Differences Per Target

| Aspect | Claude | Droid | Codex |
|--------|--------|-------|-------|
| **Credential delivery** | Environment variables | Config file (~/.factory/settings.json) | Transient `-c` overrides + `CCS_CODEX_API_KEY` |
| **Spawn args** | `claude <args>` | `droid -m custom:ccs-<profile> <args>` | `codex <args>` or `codex -c ... <args>` |
| **Config write** | None (uses env) | `upsertCcsModel()` writes to settings | None at runtime; dashboard edits user-owned `~/.codex/config.toml` only |
| **Binary detection** | `detectClaudeCli()` | `detectDroidCli()` with version check | `detectCodexCli()` plus `--config` capability probe |

### Target Resolution Priority

Resolves which adapter to use via `resolveTargetType()`:

```
1. --target <name> flag (highest priority)
   ↓
2. explicit runtime entrypoint (`CCS_INTERNAL_ENTRY_TARGET`):
   - ccs-droid / ccsd → droid
   - ccs-codex / ccsx → codex
   - ccsxp → codex (provider shortcut)
   ↓
3. argv[0] detection (runtime alias pattern / custom alias map):
   - ccs-droid → droid
   - ccsd → droid
   - ccs-codex → codex
   - ccsx → codex
   - ccs → default
   ↓
4. Profile config: profileConfig.target field
   ↓
5. Fallback: 'claude' (lowest priority)
```

### Registration Pattern

At startup, adapters self-register into the runtime registry:

```typescript
// In ccs.ts or initialization
registerTarget(new ClaudeAdapter());
registerTarget(new DroidAdapter());
registerTarget(new CodexAdapter());

// Later, when executing
const targetType = resolveTargetType(args, profileConfig);
const adapter = getTarget(targetType);

await adapter.prepareCredentials(credentials);
const spawnArgs = adapter.buildArgs(profile, userArgs);
adapter.exec(spawnArgs, adapter.buildEnv(credentials, profileType));
```

### Adding a New Target

To add support for a new CLI (e.g., `newcli`):

1. Create `src/targets/newcli-adapter.ts` implementing `TargetAdapter`
2. Implement each required method (detection, credential delivery, spawning)
3. Create `src/targets/newcli-detector.ts` for binary detection logic
4. Export from `src/targets/index.ts`
5. Register in `ccs.ts`: `registerTarget(new NewCliAdapter())`
6. Update `TargetType` union to include `'newcli'`

---

## Monster File Splitting Methodology

When splitting large files (500+ lines), follow this process:

### Step 1: Analyze Structure

Identify logical boundaries:
- Render sections in React components
- Handler groups in route files
- Related utility functions
- Constants and types

### Step 2: Extract Types First

```typescript
// types.ts
export interface ProviderEditorProps {
  providerId: string;
  onSave: (config: ProviderConfig) => void;
}

export interface ModelMappingValues {
  model: string;
  endpoint: string;
}
```

### Step 3: Extract Utilities

```typescript
// utils.ts
export function validateEndpoint(url: string): boolean { ... }
export function formatModelName(name: string): string { ... }
```

### Step 4: Extract Hooks

```typescript
// hooks.ts
export function useProviderConfig(providerId: string) { ... }
export function useModelValidation() { ... }
```

### Step 5: Extract Subcomponents

```typescript
// model-mapping-form.tsx
export function ModelMappingForm({ values, onChange }: Props) { ... }
```

### Step 6: Compose in Index

```typescript
// index.tsx
import { ModelMappingForm } from './model-mapping-form';
import { useProviderConfig } from './hooks';
import type { ProviderEditorProps } from './types';

export function ProviderEditor({ providerId, onSave }: ProviderEditorProps) {
  const config = useProviderConfig(providerId);
  return (
    <div>
      <ModelMappingForm values={config.mapping} onChange={...} />
    </div>
  );
}

// Re-export types for consumers
export type { ProviderEditorProps, ModelMappingValues } from './types';
```

---

## TypeScript Standards

### Strict Mode Required

All projects use TypeScript strict mode:

```json
{
  "compilerOptions": {
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  }
}
```

### Type Annotations

```typescript
// CORRECT: Explicit return types for public functions
export function detectProfile(args: string[]): DetectedProfile { ... }

// CORRECT: Inferred types for internal functions
const formatName = (name: string) => name.trim().toLowerCase();

// INCORRECT: any type
function processData(data: any) { ... }  // Use unknown or proper type
```

### Type Exports

```typescript
// CORRECT: Use type keyword for type-only exports
export type { Config, Settings } from './config';

// CORRECT: Group type exports in barrel
export type {
  PlatformInfo,
  BinaryInfo,
  DownloadProgress,
} from './types';
```

---

## ESLint Rules (Enforced)

| Rule | Level | Notes |
|------|-------|-------|
| `@typescript-eslint/no-unused-vars` | error | Ignore `_` prefix |
| `@typescript-eslint/no-explicit-any` | error | Use proper types |
| `@typescript-eslint/no-non-null-assertion` | error | No `!` assertions |
| `prefer-const` | error | Immutable by default |
| `no-var` | error | Use const/let |
| `eqeqeq` | error | Strict equality |
| `react-hooks/*` | recommended | (UI only) |

---

## Terminal Output Standards

### CCS Logging Standards

- Use the shared logger from `src/services/logging/` for CCS-owned runtime diagnostics, request tracing, and structured events.
- Keep `utils/ui` and deliberate `console.log`/`console.error` output for user-facing CLI UX only.
- Redact secrets before persistence; never write raw tokens, cookies, API keys, or password hashes into CCS-owned logs.
- Persist CCS-owned logs only under `getCcsDir()/logs`; do not invent per-feature log roots.
- When adding dashboard polling or diagnostics routes, prevent them from recursively logging the log viewer itself.

### ASCII Only

```typescript
// CORRECT
console.log('[OK] Operation successful');
console.log('[!] Warning message');
console.log('[X] Error occurred');
console.log('[i] Information');

// INCORRECT - NO EMOJIS
console.log('Operation successful');  // NO
console.log('Warning message');       // NO
```

### Color Handling

```typescript
import { colors } from '../utils/ui';

// Colors are TTY-aware and respect NO_COLOR
console.log(colors.green('[OK]') + ' Operation successful');
```

### Box Borders

Use ASCII box drawing for error displays:

```
+=====================================+
|  [X] ERROR: Configuration failed    |
|                                     |
|  Details: Unable to parse config    |
+=====================================+
```

### Cross-Platform Adapter Spawning

When implementing target adapters, handle platform differences for binary spawning:

```typescript
// Window shell detection (.cmd, .bat, .ps1 require shell)
const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(binaryPath);

if (needsShell) {
  // Escape arguments and use shell: true
  const cmdString = [binaryPath, ...args].map(escapeShellArg).join(' ');
  spawn(cmdString, { shell: true, stdio: 'inherit' });
} else {
  // Direct spawn (Unix-like, unshelled Windows executables)
  spawn(binaryPath, args, { stdio: 'inherit' });
}
```

This pattern is used in both `ClaudeAdapter` and `DroidAdapter` to ensure cross-platform consistency.

For all Claude child-process launches (delegation, adapters, proxies, helper spawners), sanitize env before spawn:

```typescript
const cleanEnv = stripClaudeCodeEnv(mergedEnv); // case-insensitive remove of CLAUDECODE
spawn(binaryPath, args, { env: cleanEnv, stdio: 'inherit' });
```

This prevents Claude Code nested-session guard failures when CCS runs inside parent Claude sessions.

---

## React Component Standards (UI)

### Component Structure

```typescript
// component-name.tsx

// 1. Imports (grouped: react, external, internal, relative)
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { useProfiles } from '@/hooks';
import { formatName } from './utils';
import type { ComponentProps } from './types';

// 2. Types (if not in separate file)
interface Props {
  id: string;
  onSave: () => void;
}

// 3. Component
export function ComponentName({ id, onSave }: Props) {
  // Hooks first
  const profiles = useProfiles();
  const [state, setState] = useState(null);

  // Handlers
  const handleClick = () => { ... };

  // Render
  return ( ... );
}
```

### Naming Conventions

| Item | Convention | Example |
|------|------------|---------|
| Component files | kebab-case.tsx | `provider-editor.tsx` |
| Component exports | PascalCase | `ProviderEditor` |
| Hook files | use-*.ts | `use-profiles.ts` |
| Hook exports | useCamelCase | `useProfiles` |
| Utility files | kebab-case.ts | `path-utils.ts` |
| Utility exports | camelCase | `formatPath` |

---

## Input State Persistence Patterns

When building forms and editors that allow users to make changes, follow these patterns to prevent data loss.

### Pattern 1: Key-Based Remounting

**Use when**: Component has complex local state that should reset on prop changes.

```typescript
// Parent component
<ProfileEditor
  key={profileId}  // Forces remount when profile changes
  profileId={profileId}
  onSave={handleSave}
/>
```

**Why**: Without `key`, React reuses the component instance. Local `useState` values persist even when props change, causing stale data bugs.

### Pattern 2: Unsaved Changes Confirmation

**Use when**: User might navigate away while editing.

```typescript
// Parent tracks dirty state
const [editorHasChanges, setEditorHasChanges] = useState(false);
const [pendingSwitch, setPendingSwitch] = useState<string | null>(null);

// Child notifies parent of dirty state
useEffect(() => {
  onHasChangesUpdate?.(computedHasChanges);
}, [computedHasChanges, onHasChangesUpdate]);

// Intercept navigation
const handleSelect = (id: string) => {
  if (editorHasChanges && currentId !== id) {
    setPendingSwitch(id);  // Show confirmation dialog
  } else {
    setCurrentId(id);
  }
};
```

**Flow**:
1. Child computes `hasChanges` from local state vs saved data
2. Child notifies parent via callback
3. Parent intercepts navigation when dirty
4. Show confirmation dialog: "Discard & Switch" or "Cancel"
5. On confirm: reset dirty state, then switch

### Pattern 3: Auto-Save with Visual Feedback

**Use when**: Simple inputs that should save immediately.

```typescript
const [saved, setSaved] = useState(false);

const handleBlur = async () => {
  if (value !== savedValue) {
    await saveToBackend(value);
    setSaved(true);
    setTimeout(() => setSaved(false), 2000);
  }
};

return (
  <div className="flex items-center gap-2">
    <Input value={value} onChange={...} onBlur={handleBlur} />
    {saved && (
      <span className="text-green-600 text-xs flex items-center gap-1">
        <Check className="w-3.5 h-3.5" /> Saved
      </span>
    )}
  </div>
);
```

**When to use which**:
| Scenario | Pattern |
|----------|---------|
| Complex multi-field editor | Pattern 2 (confirmation dialog) |
| Simple single input | Pattern 3 (auto-save + feedback) |
| List item selection | Pattern 1 (key-based remount) + Pattern 2 |

---

## Quality Gates

### Pre-Commit Sequence

```bash
# Main project
bun run format
bun run lint:fix
bun run validate
bun run validate:ci-parity

# UI project (if changed)
cd ui
bun run format
bun run lint:fix
bun run validate
```

### Validate Runs

| Project | Command | Checks |
|---------|---------|--------|
| Main | `bun run validate` | typecheck + lint + format:check + test:fast |
| UI | `bun run validate` | typecheck + lint + format:check |

---

## Conventional Commits

All commits must follow conventional commit format:

```
<type>(<scope>): <description>
```

### Types

| Type | When to Use | Version Bump |
|------|-------------|--------------|
| `feat` | New feature | MINOR |
| `fix` | Bug fix | PATCH |
| `perf` | Performance | PATCH |
| `docs` | Documentation | None |
| `style` | Formatting | None |
| `refactor` | Code restructure | None |
| `test` | Tests | None |
| `chore` | Maintenance | None |

### Examples

```bash
# Correct
git commit -m "feat(cliproxy): add OAuth token refresh"
git commit -m "fix(doctor): handle missing config gracefully"
git commit -m "refactor(ui): split provider-editor into modules"

# Incorrect - REJECTED
git commit -m "added new feature"
git commit -m "Fixed bug"
```

---

## Anti-Patterns to Avoid

### 1. God Files

```typescript
// BAD: One file doing everything
// src/utils.ts (2000 lines with mixed concerns)

// GOOD: Split by domain
// src/utils/ui/colors.ts
// src/utils/ui/boxes.ts
// src/utils/shell-executor.ts
// src/utils/config-manager.ts
```

### 2. Barrel Import Bypass

```typescript
// BAD: Direct import bypassing barrel
import { detectPlatform } from '../cliproxy/platform-detector';

// GOOD: Import from domain barrel
import { detectPlatform } from '../cliproxy';
```

### 3. Inline Everything

```typescript
// BAD: Huge inline functions in components
function Component() {
  const handleComplexOperation = () => {
    // 100 lines of logic...
  };
}

// GOOD: Extract to hooks or utilities
function Component() {
  const { handleComplexOperation } = useComplexOperation();
}
```

### 4. Type Duplication

```typescript
// BAD: Same types defined in multiple files
// file1.ts
interface Config { ... }
// file2.ts
interface Config { ... }

// GOOD: Single source of truth
// types/config.ts
export interface Config { ... }
```

### 5. Config Priority Pattern

When resolving configuration from multiple sources, follow this priority order:

```typescript
// proxy-config-resolver.ts pattern
// Priority: CLI flags > Environment variables > config.yaml > defaults

const resolved = {
  ...DEFAULT_CONFIG,                    // 4. Defaults (lowest)
  ...yamlConfig,                        // 3. config.yaml
  ...envConfig,                         // 2. Environment variables
  ...cliFlags,                          // 1. CLI flags (highest)
};
```

This pattern is used in:
- `src/cliproxy/proxy-config-resolver.ts` - Remote proxy config
- `src/config/unified-config-loader.ts` - Main config loading

---

## Related Documentation

- [Codebase Summary](./codebase-summary.md) - Full directory structure
- [System Architecture](./system-architecture/index.md) - Architecture diagrams
- [CLAUDE.md](../CLAUDE.md) - AI-facing development guidance
