mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-04 12:13:09 +00:00
Epic P7. Locks in the epic's gains with ESLint gates so the adoption work does not regress. - ccs/no-new-throw-error (error): custom flat-config rule that flags NEW throw new Error(...) outside a generated baseline allowlist. Forces the typed-error taxonomy (src/errors/error-types.ts). Existing 338 sites are grandfathered in eslint-rules/throw-error-baseline.json; only NEW violations error. Rule normalizes the filename to repo-root-relative to match baseline keys regardless of how ESLint reports paths. - max-lines (warn, 400, skipBlankLines/skipComments): warns on files over 400 LOC (goal of P5/P6 god-file splits). Currently 51 warnings on the not-yet-split god-files (P6 territory). - scripts/generate-throw-error-baseline.js: emits the allowlist from raw source (superset of real throws; never undercounts). Run after intentionally grandfathering a site, or quarterly to prune. - tests/unit/eslint-rules/no-throw-new-error.test.ts: rule logic (flags off-allowlist, passes on-allowlist/typed/rethrow, line-sensitivity). - docs/code-standards.md: Lint Enforcement Gates section. - docs/logging-contract.md: error.code -> ExitCode table (from P4). validate + validate:ci-parity green.
750 lines
21 KiB
Markdown
750 lines
21 KiB
Markdown
# 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 (native cliproxy 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
|
|
|
|
---
|
|
|
|
## Lint Enforcement Gates
|
|
|
|
Two ESLint gates (`eslint.config.mjs`) lock in the maintainability epic's gains:
|
|
|
|
- **`ccs/no-new-throw-error`** (error): flags new `throw new Error(...)`. Use a typed error from `src/errors/error-types.ts` (`AuthError`, `ConfigError`, `ProfileError`, `ProviderError`, `NetworkError`, `ProxyError`, `MigrationError`, `ValidationError`, `RetryableError`) so `handleError` emits a differentiated exit code. Existing ~340 sites are grandfathered in `eslint-rules/throw-error-baseline.json`; only **new** violations error. Regenerate the baseline when intentionally grandfathering a new site, or quarterly to prune converted entries:
|
|
```bash
|
|
node scripts/generate-throw-error-baseline.js
|
|
```
|
|
- **`max-lines`** (warn, 400): warns on source files over 400 lines (`skipBlankLines`, `skipComments`). Split via the Monster File Splitting methodology above (barrel `index.ts` preserves the public API).
|
|
|
|
When the no-throw rule blocks a change, prefer converting to the matching typed error. Only add to the baseline when the throw is genuinely out of scope to convert (and regenerate the baseline so the entry is explicit, not silent).
|
|
|
|
## 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
|