type: skill name: Refactoring description: Safe code refactoring with step-by-step approach skillSlug: refactoring phases: [E] generated: 2026-03-18 status: filled scaffoldVersion: "2.0.0"
Refactoring
Safe, systematic refactoring procedures for the @dotcontext/cli codebase.
When to Use
- Extracting logic into a new service or utility
- Consolidating duplicate code across services or generators
- Restructuring MCP gateway handlers
- Improving type safety or removing
anyusage - Simplifying complex functions (high cyclomatic complexity)
- Migrating between patterns (e.g., callback to async/await)
Pre-Refactoring Checklist
Before starting any refactoring:
- All tests pass:
npm test - Build succeeds:
npm run build - Create a branch or ensure changes can be reverted
- Identify all callers of the code being refactored (use grep or IDE references)
Common Refactoring Scenarios
Extracting a New Service
The project follows a directory-per-service pattern. To extract logic into a new service:
- Create
src/services/<name>/directory - Create the service class:
// src/services/cache/cacheService.ts
export interface CacheServiceOptions {
repoPath: string;
cacheDir?: string;
}
export class CacheService {
private readonly repoPath: string;
private readonly cacheDir: string;
constructor(options: CacheServiceOptions) {
this.repoPath = options.repoPath;
this.cacheDir = options.cacheDir || '.context/.cache';
}
async get(key: string): Promise<string | null> { /* ... */ }
async set(key: string, value: string): Promise<void> { /* ... */ }
}
- Create
src/services/<name>/index.tsbarrel:
export { CacheService, CacheServiceOptions } from './cacheService';
- Update callers to import from the new service
- Write tests in
src/services/<name>/cacheService.test.ts
Splitting a Large Gateway Handler
Gateway handlers in src/services/mcp/gateway/ can grow large. To split:
- Identify action groups that share logic
- Extract helper functions within the same file first
- If helpers grow, extract into a separate utility or service
- Keep the main handler function as a thin dispatcher:
export async function handleContext(params: ContextParams, options: ContextOptions): Promise<MCPToolResponse> {
switch (params.action) {
case 'check': return handleCheck(options);
case 'init': return handleInit(params, options);
// Each case delegates to a focused function
}
}
Improving Type Safety
Common type improvements:
- Replace
anywithunknown: Then add type guards:
// Before
function process(data: any) { return data.value; }
// After
function process(data: unknown): string {
if (typeof data === 'object' && data !== null && 'value' in data) {
return String((data as { value: unknown }).value);
}
throw new Error('Invalid data shape');
}
- Add discriminated unions for action-based params:
type ContextParams =
| { action: 'check'; repoPath?: string }
| { action: 'init'; repoPath?: string; type?: string }
| { action: 'fill'; repoPath?: string; target?: string };
- Extract interface from implementation: When a class has grown, extract its public interface for dependency injection (pattern used by
AIContextMCPServerwithcontextBuilder).
Consolidating Duplicate Logic
Common duplication spots in this project:
- Path resolution: Multiple services resolve
repoPath+outputDir. Extract tosrc/services/shared/. - Frontmatter parsing: Used in generators, scaffold tools, and MCP flows. Centralized in
src/utils/frontMatter.ts. - File listing with glob: Used in semantic analysis, generators, and export. Check
src/services/shared/for existing utilities. - Error response creation: MCP handlers should all use
createErrorResponse()fromresponse.ts.
Updating Barrel Exports
When moving or renaming files, update the export chain:
src/services/mcp/gateway/<file>.ts -- implementation
-> src/services/mcp/gateway/index.ts -- gateway barrel
-> src/services/mcp/gatewayTools.ts -- compatibility barrel
-> src/services/mcp/mcpServer.ts -- consumer
Step-by-Step Refactoring Process
- Identify: Find the code smell (large function, duplication, poor typing)
- Plan: Determine the target structure and list affected files
- Test: Ensure existing tests pass and cover the code being changed
- Extract: Move code to the new location, keeping the old code as a thin wrapper
- Redirect: Update all callers to use the new location
- Remove: Delete the old wrapper once all callers are redirected
- Verify: Run
npm testandnpm run build - Commit: Use
refactor(<scope>):prefix
Refactoring Safety Rails
Tests as Safety Net
- Never refactor code that lacks tests. Write tests first.
- Run
npx jest --testPathPattern="<area>"after each step.
Preserve Public APIs
- MCP tool names and schemas are public API. Do not rename tools or remove actions without a deprecation strategy.
- CLI command names and flags are public API.
- Barrel export paths are consumed by other modules. Keep backward-compatible re-exports.
TypeScript Compiler as Guard
npm run buildwill catch broken imports, type mismatches, and missing exports.- Enable
noUnusedLocalstemporarily to find dead code after extraction.
Small Commits
- Commit after each logical step (extract, redirect, remove).
- This makes it easy to bisect if something breaks.
Anti-Patterns to Watch For
- Big bang refactors: Changing everything at once. Prefer incremental changes.
- Refactoring while adding features: Do one or the other per commit.
- Changing test behavior during refactoring: Refactoring should not change test expectations.
- Forgetting barrel exports: New file locations need updated export chains.
- Breaking MCP backward compatibility: Clients depend on stable tool names and schemas.