feat(ccw): migrate backend to TypeScript

- Convert 40 JS files to TypeScript (CLI, tools, core, MCP server)
- Add Zod for runtime parameter validation
- Add type definitions in src/types/
- Keep src/templates/ as JavaScript (dashboard frontend)
- Update bin entries to use dist/
- Add tsconfig.json with strict mode
- Add backward-compatible exports for tests
- All 39 tests passing

Breaking changes: None (backward compatible)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
catlog22
2025-12-13 10:43:15 +08:00
parent d4e59770d0
commit 25ac862f46
93 changed files with 5531 additions and 9302 deletions

177
ccw/src/tools/write-file.ts Normal file
View File

@@ -0,0 +1,177 @@
/**
* Write File Tool - Create or overwrite files
*
* Features:
* - Create new files or overwrite existing
* - Auto-create parent directories
* - Support for text content with proper encoding
* - Optional backup before overwrite
*/
import { z } from 'zod';
import type { ToolSchema, ToolResult } from '../types/tool.js';
import { writeFileSync, readFileSync, existsSync, mkdirSync, renameSync } from 'fs';
import { resolve, isAbsolute, dirname, basename } from 'path';
// Define Zod schema for validation
const ParamsSchema = z.object({
path: z.string().min(1, 'Path is required'),
content: z.string(),
createDirectories: z.boolean().default(true),
backup: z.boolean().default(false),
encoding: z.enum(['utf8', 'utf-8', 'ascii', 'latin1', 'binary', 'hex', 'base64']).default('utf8'),
});
type Params = z.infer<typeof ParamsSchema>;
interface WriteResult {
success: boolean;
path: string;
created: boolean;
overwritten: boolean;
backupPath: string | null;
bytes: number;
message: string;
}
/**
* Ensure parent directory exists
* @param filePath - Path to file
*/
function ensureDir(filePath: string): void {
const dir = dirname(filePath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
}
/**
* Create backup of existing file
* @param filePath - Path to file
* @returns Backup path or null if no backup created
*/
function createBackup(filePath: string): string | null {
if (!existsSync(filePath)) {
return null;
}
const dir = dirname(filePath);
const name = basename(filePath);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupPath = resolve(dir, `.${name}.${timestamp}.bak`);
try {
const content = readFileSync(filePath);
writeFileSync(backupPath, content);
return backupPath;
} catch (error) {
throw new Error(`Failed to create backup: ${(error as Error).message}`);
}
}
// Tool schema for MCP
export const schema: ToolSchema = {
name: 'write_file',
description: `Write content to file. Auto-creates parent directories.
Usage: write_file(path="file.js", content="code here")
Options: backup=true (backup before overwrite), encoding="utf8"`,
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'Path to the file to create or overwrite',
},
content: {
type: 'string',
description: 'Content to write to the file',
},
createDirectories: {
type: 'boolean',
description: 'Create parent directories if they do not exist (default: true)',
default: true,
},
backup: {
type: 'boolean',
description: 'Create backup of existing file before overwriting (default: false)',
default: false,
},
encoding: {
type: 'string',
description: 'File encoding (default: utf8)',
default: 'utf8',
enum: ['utf8', 'utf-8', 'ascii', 'latin1', 'binary', 'hex', 'base64'],
},
},
required: ['path', 'content'],
},
};
// Handler function
export async function handler(params: Record<string, unknown>): Promise<ToolResult<WriteResult>> {
const parsed = ParamsSchema.safeParse(params);
if (!parsed.success) {
return { success: false, error: `Invalid params: ${parsed.error.message}` };
}
const {
path: filePath,
content,
createDirectories,
backup,
encoding,
} = parsed.data;
// Resolve path
const resolvedPath = isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath);
const fileExists = existsSync(resolvedPath);
// Create parent directories if needed
if (createDirectories) {
ensureDir(resolvedPath);
} else if (!existsSync(dirname(resolvedPath))) {
return {
success: false,
error: `Parent directory does not exist: ${dirname(resolvedPath)}`,
};
}
// Create backup if requested and file exists
let backupPath: string | null = null;
if (backup && fileExists) {
try {
backupPath = createBackup(resolvedPath);
} catch (error) {
return {
success: false,
error: (error as Error).message,
};
}
}
// Write file
try {
writeFileSync(resolvedPath, content, { encoding });
return {
success: true,
result: {
success: true,
path: resolvedPath,
created: !fileExists,
overwritten: fileExists,
backupPath,
bytes: Buffer.byteLength(content, encoding),
message: fileExists
? `Successfully overwrote ${filePath}${backupPath ? ` (backup: ${backupPath})` : ''}`
: `Successfully created ${filePath}`,
},
};
} catch (error) {
return {
success: false,
error: `Failed to write file: ${(error as Error).message}`,
};
}
}