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

View File

@@ -11,10 +11,26 @@ import {
getExecutionDetail
} from '../tools/cli-executor.js';
interface CliExecOptions {
tool?: string;
mode?: string;
model?: string;
cd?: string;
includeDirs?: string;
timeout?: string;
noStream?: boolean;
}
interface HistoryOptions {
limit?: string;
tool?: string;
status?: string;
}
/**
* Show CLI tool status
*/
async function statusAction() {
async function statusAction(): Promise<void> {
console.log(chalk.bold.cyan('\n CLI Tools Status\n'));
const status = await getCliToolsStatus();
@@ -37,7 +53,7 @@ async function statusAction() {
* @param {string} prompt - Prompt to execute
* @param {Object} options - CLI options
*/
async function execAction(prompt, options) {
async function execAction(prompt: string | undefined, options: CliExecOptions): Promise<void> {
if (!prompt) {
console.error(chalk.red('Error: Prompt is required'));
console.error(chalk.gray('Usage: ccw cli exec "<prompt>" --tool gemini'));
@@ -49,7 +65,7 @@ async function execAction(prompt, options) {
console.log(chalk.cyan(`\n Executing ${tool} (${mode} mode)...\n`));
// Streaming output handler
const onOutput = noStream ? null : (chunk) => {
const onOutput = noStream ? null : (chunk: any) => {
process.stdout.write(chunk.data);
};
@@ -63,7 +79,7 @@ async function execAction(prompt, options) {
include: includeDirs,
timeout: timeout ? parseInt(timeout, 10) : 300000,
stream: !noStream
}, onOutput);
});
// If not streaming, print output now
if (noStream && result.stdout) {
@@ -82,7 +98,8 @@ async function execAction(prompt, options) {
process.exit(1);
}
} catch (error) {
console.error(chalk.red(` Error: ${error.message}`));
const err = error as Error;
console.error(chalk.red(` Error: ${err.message}`));
process.exit(1);
}
}
@@ -91,8 +108,8 @@ async function execAction(prompt, options) {
* Show execution history
* @param {Object} options - CLI options
*/
async function historyAction(options) {
const { limit = 20, tool, status } = options;
async function historyAction(options: HistoryOptions): Promise<void> {
const { limit = '20', tool, status } = options;
console.log(chalk.bold.cyan('\n CLI Execution History\n'));
@@ -125,7 +142,7 @@ async function historyAction(options) {
* Show execution detail
* @param {string} executionId - Execution ID
*/
async function detailAction(executionId) {
async function detailAction(executionId: string | undefined): Promise<void> {
if (!executionId) {
console.error(chalk.red('Error: Execution ID is required'));
console.error(chalk.gray('Usage: ccw cli detail <execution-id>'));
@@ -173,8 +190,8 @@ async function detailAction(executionId) {
* @param {Date} date
* @returns {string}
*/
function getTimeAgo(date) {
const seconds = Math.floor((new Date() - date) / 1000);
function getTimeAgo(date: Date): string {
const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000);
if (seconds < 60) return 'just now';
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
@@ -189,7 +206,11 @@ function getTimeAgo(date) {
* @param {string[]} args - Arguments array
* @param {Object} options - CLI options
*/
export async function cliCommand(subcommand, args, options) {
export async function cliCommand(
subcommand: string,
args: string | string[],
options: CliExecOptions | HistoryOptions
): Promise<void> {
const argsArray = Array.isArray(args) ? args : (args ? [args] : []);
switch (subcommand) {
@@ -198,11 +219,11 @@ export async function cliCommand(subcommand, args, options) {
break;
case 'exec':
await execAction(argsArray[0], options);
await execAction(argsArray[0], options as CliExecOptions);
break;
case 'history':
await historyAction(options);
await historyAction(options as HistoryOptions);
break;
case 'detail':

View File

@@ -7,6 +7,7 @@ import chalk from 'chalk';
import { showHeader, createSpinner, info, warning, error, summaryBox, divider } from '../utils/ui.js';
import { createManifest, addFileEntry, addDirectoryEntry, saveManifest, findManifest, getAllManifests } from '../core/manifest.js';
import { validatePath } from '../utils/path-resolver.js';
import type { Spinner } from 'ora';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -17,13 +18,24 @@ const SOURCE_DIRS = ['.claude', '.codex', '.gemini', '.qwen'];
// Subdirectories that should always be installed to global (~/.claude/)
const GLOBAL_SUBDIRS = ['workflows', 'scripts', 'templates'];
interface InstallOptions {
mode?: string;
path?: string;
force?: boolean;
}
interface CopyResult {
files: number;
directories: number;
}
// Get package root directory (ccw/src/commands -> ccw)
function getPackageRoot() {
function getPackageRoot(): string {
return join(__dirname, '..', '..');
}
// Get source installation directory (parent of ccw)
function getSourceDir() {
function getSourceDir(): string {
return join(getPackageRoot(), '..');
}
@@ -31,7 +43,7 @@ function getSourceDir() {
* Install command handler
* @param {Object} options - Command options
*/
export async function installCommand(options) {
export async function installCommand(options: InstallOptions): Promise<void> {
const version = getVersion();
// Show beautiful header
@@ -67,7 +79,7 @@ export async function installCommand(options) {
// Interactive mode selection
const mode = options.mode || await selectMode();
let installPath;
let installPath: string;
if (mode === 'Global') {
installPath = homedir();
info(`Global installation to: ${installPath}`);
@@ -76,7 +88,7 @@ export async function installCommand(options) {
// Validate the installation path
const pathValidation = validatePath(inputPath, { mustExist: true });
if (!pathValidation.valid) {
if (!pathValidation.valid || !pathValidation.path) {
error(`Invalid installation path: ${pathValidation.error}`);
process.exit(1);
}
@@ -171,7 +183,8 @@ export async function installCommand(options) {
} catch (err) {
spinner.fail('Installation failed');
error(err.message);
const errMsg = err as Error;
error(errMsg.message);
process.exit(1);
}
@@ -212,7 +225,7 @@ export async function installCommand(options) {
* Interactive mode selection
* @returns {Promise<string>} - Selected mode
*/
async function selectMode() {
async function selectMode(): Promise<string> {
const { mode } = await inquirer.prompt([{
type: 'list',
name: 'mode',
@@ -236,13 +249,13 @@ async function selectMode() {
* Interactive path selection
* @returns {Promise<string>} - Selected path
*/
async function selectPath() {
async function selectPath(): Promise<string> {
const { path } = await inquirer.prompt([{
type: 'input',
name: 'path',
message: 'Enter installation path:',
default: process.cwd(),
validate: (input) => {
validate: (input: string) => {
if (!input) return 'Path is required';
if (!existsSync(input)) {
return `Path does not exist: ${input}`;
@@ -259,7 +272,7 @@ async function selectPath() {
* @param {string} installPath - Installation path
* @param {Object} manifest - Existing manifest
*/
async function createBackup(installPath, manifest) {
async function createBackup(installPath: string, manifest: any): Promise<void> {
const spinner = createSpinner('Creating backup...').start();
try {
@@ -276,7 +289,8 @@ async function createBackup(installPath, manifest) {
spinner.succeed(`Backup created: ${backupDir}`);
} catch (err) {
spinner.warn(`Backup failed: ${err.message}`);
const errMsg = err as Error;
spinner.warn(`Backup failed: ${errMsg.message}`);
}
}
@@ -288,7 +302,12 @@ async function createBackup(installPath, manifest) {
* @param {string[]} excludeDirs - Directory names to exclude (optional)
* @returns {Object} - Count of files and directories
*/
async function copyDirectory(src, dest, manifest = null, excludeDirs = []) {
async function copyDirectory(
src: string,
dest: string,
manifest: any = null,
excludeDirs: string[] = []
): Promise<CopyResult> {
let files = 0;
let directories = 0;
@@ -329,7 +348,7 @@ async function copyDirectory(src, dest, manifest = null, excludeDirs = []) {
* Get package version
* @returns {string} - Version string
*/
function getVersion() {
function getVersion(): string {
try {
// First try root package.json (parent of ccw)
const rootPkgPath = join(getSourceDir(), 'package.json');

View File

@@ -5,7 +5,7 @@ import { getAllManifests } from '../core/manifest.js';
/**
* List command handler - shows all installations
*/
export async function listCommand() {
export async function listCommand(): Promise<void> {
showBanner();
console.log(chalk.cyan.bold(' Installed Claude Code Workflow Instances\n'));

View File

@@ -2,19 +2,26 @@ import { startServer } from '../core/server.js';
import { launchBrowser } from '../utils/browser-launcher.js';
import { resolvePath, validatePath } from '../utils/path-resolver.js';
import chalk from 'chalk';
import type { Server } from 'http';
interface ServeOptions {
port?: number;
path?: string;
browser?: boolean;
}
/**
* Serve command handler - starts dashboard server with live path switching
* @param {Object} options - Command options
*/
export async function serveCommand(options) {
export async function serveCommand(options: ServeOptions): Promise<void> {
const port = options.port || 3456;
// Validate project path
let initialPath = process.cwd();
if (options.path) {
const pathValidation = validatePath(options.path, { mustExist: true });
if (!pathValidation.valid) {
if (!pathValidation.valid || !pathValidation.path) {
console.error(chalk.red(`\n Error: ${pathValidation.error}\n`));
process.exit(1);
}
@@ -40,7 +47,8 @@ export async function serveCommand(options) {
await launchBrowser(url);
console.log(chalk.green.bold('\n Dashboard opened in browser!'));
} catch (err) {
console.log(chalk.yellow(`\n Could not open browser: ${err.message}`));
const error = err as Error;
console.log(chalk.yellow(`\n Could not open browser: ${error.message}`));
console.log(chalk.gray(` Open manually: ${url}`));
}
}
@@ -57,8 +65,9 @@ export async function serveCommand(options) {
});
} catch (error) {
console.error(chalk.red(`\n Error: ${error.message}\n`));
if (error.code === 'EADDRINUSE') {
const err = error as Error & { code?: string };
console.error(chalk.red(`\n Error: ${err.message}\n`));
if (err.code === 'EADDRINUSE') {
console.error(chalk.yellow(` Port ${port} is already in use.`));
console.error(chalk.gray(` Try a different port: ccw serve --port ${port + 1}\n`));
}

View File

@@ -8,18 +8,61 @@ import http from 'http';
import { executeTool } from '../tools/index.js';
// Handle EPIPE errors gracefully (occurs when piping to head/jq that closes early)
process.stdout.on('error', (err) => {
process.stdout.on('error', (err: NodeJS.ErrnoException) => {
if (err.code === 'EPIPE') {
process.exit(0);
}
throw err;
});
interface ListOptions {
location?: string;
metadata?: boolean;
}
interface InitOptions {
type?: string;
}
interface ReadOptions {
type?: string;
taskId?: string;
filename?: string;
dimension?: string;
iteration?: string;
raw?: boolean;
}
interface WriteOptions {
type?: string;
content?: string;
taskId?: string;
filename?: string;
dimension?: string;
iteration?: string;
}
interface UpdateOptions {
type?: string;
content?: string;
taskId?: string;
}
interface ArchiveOptions {
updateStatus?: boolean;
}
interface MkdirOptions {
subdir?: string;
}
interface StatsOptions {}
/**
* Notify dashboard of granular events (fire and forget)
* @param {Object} data - Event data
*/
function notifyDashboard(data) {
function notifyDashboard(data: any): void {
const DASHBOARD_PORT = process.env.CCW_PORT || 3456;
const payload = JSON.stringify({
...data,
@@ -49,7 +92,7 @@ function notifyDashboard(data) {
* List sessions
* @param {Object} options - CLI options
*/
async function listAction(options) {
async function listAction(options: ListOptions): Promise<void> {
const params = {
operation: 'list',
location: options.location || 'both',
@@ -63,7 +106,7 @@ async function listAction(options) {
process.exit(1);
}
const { active = [], archived = [], total } = result.result;
const { active = [], archived = [], total } = (result.result as any);
console.log(chalk.bold.cyan('\nWorkflow Sessions\n'));
@@ -100,7 +143,7 @@ async function listAction(options) {
* @param {string} sessionId - Session ID
* @param {Object} options - CLI options
*/
async function initAction(sessionId, options) {
async function initAction(sessionId: string | undefined, options: InitOptions): Promise<void> {
if (!sessionId) {
console.error(chalk.red('Session ID is required'));
console.error(chalk.gray('Usage: ccw session init <session_id> [--type <type>]'));
@@ -128,7 +171,7 @@ async function initAction(sessionId, options) {
});
console.log(chalk.green(`✓ Session "${sessionId}" initialized`));
console.log(chalk.gray(` Location: ${result.result.path}`));
console.log(chalk.gray(` Location: ${(result.result as any).path}`));
}
/**
@@ -136,14 +179,14 @@ async function initAction(sessionId, options) {
* @param {string} sessionId - Session ID
* @param {Object} options - CLI options
*/
async function readAction(sessionId, options) {
async function readAction(sessionId: string | undefined, options: ReadOptions): Promise<void> {
if (!sessionId) {
console.error(chalk.red('Session ID is required'));
console.error(chalk.gray('Usage: ccw session read <session_id> --type <content_type>'));
process.exit(1);
}
const params = {
const params: any = {
operation: 'read',
session_id: sessionId,
content_type: options.type || 'session'
@@ -164,9 +207,9 @@ async function readAction(sessionId, options) {
// Output raw content for piping
if (options.raw) {
console.log(typeof result.result.content === 'string'
? result.result.content
: JSON.stringify(result.result.content, null, 2));
console.log(typeof (result.result as any).content === 'string'
? (result.result as any).content
: JSON.stringify((result.result as any).content, null, 2));
} else {
console.log(JSON.stringify(result, null, 2));
}
@@ -177,7 +220,7 @@ async function readAction(sessionId, options) {
* @param {string} sessionId - Session ID
* @param {Object} options - CLI options
*/
async function writeAction(sessionId, options) {
async function writeAction(sessionId: string | undefined, options: WriteOptions): Promise<void> {
if (!sessionId) {
console.error(chalk.red('Session ID is required'));
console.error(chalk.gray('Usage: ccw session write <session_id> --type <content_type> --content <json>'));
@@ -189,7 +232,7 @@ async function writeAction(sessionId, options) {
process.exit(1);
}
let content;
let content: any;
try {
content = JSON.parse(options.content);
} catch {
@@ -197,7 +240,7 @@ async function writeAction(sessionId, options) {
content = options.content;
}
const params = {
const params: any = {
operation: 'write',
session_id: sessionId,
content_type: options.type || 'session',
@@ -254,10 +297,10 @@ async function writeAction(sessionId, options) {
sessionId: sessionId,
entityId: entityId,
contentType: contentType,
payload: result.result.written_content || content
payload: (result.result as any).written_content || content
});
console.log(chalk.green(`✓ Content written to ${result.result.path}`));
console.log(chalk.green(`✓ Content written to ${(result.result as any).path}`));
}
/**
@@ -265,7 +308,7 @@ async function writeAction(sessionId, options) {
* @param {string} sessionId - Session ID
* @param {Object} options - CLI options
*/
async function updateAction(sessionId, options) {
async function updateAction(sessionId: string | undefined, options: UpdateOptions): Promise<void> {
if (!sessionId) {
console.error(chalk.red('Session ID is required'));
console.error(chalk.gray('Usage: ccw session update <session_id> --content <json>'));
@@ -277,16 +320,17 @@ async function updateAction(sessionId, options) {
process.exit(1);
}
let content;
let content: any;
try {
content = JSON.parse(options.content);
} catch (e) {
const error = e as Error;
console.error(chalk.red('Content must be valid JSON for update operation'));
console.error(chalk.gray(`Parse error: ${e.message}`));
console.error(chalk.gray(`Parse error: ${error.message}`));
process.exit(1);
}
const params = {
const params: any = {
operation: 'update',
session_id: sessionId,
content_type: options.type || 'session',
@@ -309,7 +353,7 @@ async function updateAction(sessionId, options) {
type: eventType,
sessionId: sessionId,
entityId: options.taskId || null,
payload: result.result.merged_data || content
payload: (result.result as any).merged_data || content
});
console.log(chalk.green(`✓ Session "${sessionId}" updated`));
@@ -320,7 +364,7 @@ async function updateAction(sessionId, options) {
* @param {string} sessionId - Session ID
* @param {Object} options - CLI options
*/
async function archiveAction(sessionId, options) {
async function archiveAction(sessionId: string | undefined, options: ArchiveOptions): Promise<void> {
if (!sessionId) {
console.error(chalk.red('Session ID is required'));
console.error(chalk.gray('Usage: ccw session archive <session_id>'));
@@ -348,7 +392,7 @@ async function archiveAction(sessionId, options) {
});
console.log(chalk.green(`✓ Session "${sessionId}" archived`));
console.log(chalk.gray(` Location: ${result.result.destination}`));
console.log(chalk.gray(` Location: ${(result.result as any).destination}`));
}
/**
@@ -356,7 +400,7 @@ async function archiveAction(sessionId, options) {
* @param {string} sessionId - Session ID
* @param {string} newStatus - New status value
*/
async function statusAction(sessionId, newStatus) {
async function statusAction(sessionId: string | undefined, newStatus: string | undefined): Promise<void> {
if (!sessionId) {
console.error(chalk.red('Session ID is required'));
console.error(chalk.gray('Usage: ccw session status <session_id> <status>'));
@@ -406,7 +450,11 @@ async function statusAction(sessionId, newStatus) {
* @param {string} taskId - Task ID
* @param {string} newStatus - New status value
*/
async function taskAction(sessionId, taskId, newStatus) {
async function taskAction(
sessionId: string | undefined,
taskId: string | undefined,
newStatus: string | undefined
): Promise<void> {
if (!sessionId) {
console.error(chalk.red('Session ID is required'));
console.error(chalk.gray('Usage: ccw session task <session_id> <task_id> <status>'));
@@ -442,11 +490,11 @@ async function taskAction(sessionId, taskId, newStatus) {
const readResult = await executeTool('session_manager', readParams);
let currentTask = {};
let currentTask: any = {};
let oldStatus = 'unknown';
if (readResult.success) {
currentTask = readResult.result.content || {};
currentTask = (readResult.result as any).content || {};
oldStatus = currentTask.status || 'unknown';
}
@@ -493,7 +541,7 @@ async function taskAction(sessionId, taskId, newStatus) {
* @param {string} sessionId - Session ID
* @param {Object} options - CLI options
*/
async function mkdirAction(sessionId, options) {
async function mkdirAction(sessionId: string | undefined, options: MkdirOptions): Promise<void> {
if (!sessionId) {
console.error(chalk.red('Session ID is required'));
console.error(chalk.gray('Usage: ccw session mkdir <session_id> --subdir <subdir>'));
@@ -522,23 +570,18 @@ async function mkdirAction(sessionId, options) {
notifyDashboard({
type: 'DIRECTORY_CREATED',
sessionId: sessionId,
payload: { directories: result.result.directories_created }
payload: { directories: (result.result as any).directories_created }
});
console.log(chalk.green(`✓ Directory created: ${result.result.directories_created.join(', ')}`));
console.log(chalk.green(`✓ Directory created: ${(result.result as any).directories_created.join(', ')}`));
}
/**
* Execute raw operation (advanced)
* @param {string} jsonParams - JSON parameters
*/
/**
* Delete file within session
* @param {string} sessionId - Session ID
* @param {string} filePath - Relative file path
*/
async function deleteAction(sessionId, filePath) {
async function deleteAction(sessionId: string | undefined, filePath: string | undefined): Promise<void> {
if (!sessionId) {
console.error(chalk.red('Session ID is required'));
console.error(chalk.gray('Usage: ccw session delete <session_id> <file_path>'));
@@ -571,14 +614,14 @@ async function deleteAction(sessionId, filePath) {
payload: { file_path: filePath }
});
console.log(chalk.green(`✓ File deleted: ${result.result.deleted}`));
console.log(chalk.green(`✓ File deleted: ${(result.result as any).deleted}`));
}
/**
* Get session statistics
* @param {string} sessionId - Session ID
*/
async function statsAction(sessionId, options = {}) {
async function statsAction(sessionId: string | undefined, options: StatsOptions = {}): Promise<void> {
if (!sessionId) {
console.error(chalk.red('Session ID is required'));
console.error(chalk.gray('Usage: ccw session stats <session_id>'));
@@ -597,7 +640,7 @@ async function statsAction(sessionId, options = {}) {
process.exit(1);
}
const { tasks, summaries, has_plan, location } = result.result;
const { tasks, summaries, has_plan, location } = (result.result as any);
console.log(chalk.bold.cyan(`\nSession Statistics: ${sessionId}`));
console.log(chalk.gray(`Location: ${location}\n`));
@@ -614,19 +657,21 @@ async function statsAction(sessionId, options = {}) {
console.log(chalk.gray(` Summaries: ${summaries}`));
console.log(chalk.gray(` Plan: ${has_plan ? 'Yes' : 'No'}`));
}
async function execAction(jsonParams) {
async function execAction(jsonParams: string | undefined): Promise<void> {
if (!jsonParams) {
console.error(chalk.red('JSON parameters required'));
console.error(chalk.gray('Usage: ccw session exec \'{"operation":"list","location":"active"}\''));
process.exit(1);
}
let params;
let params: any;
try {
params = JSON.parse(jsonParams);
} catch (e) {
const error = e as Error;
console.error(chalk.red('Invalid JSON'));
console.error(chalk.gray(`Parse error: ${e.message}`));
console.error(chalk.gray(`Parse error: ${error.message}`));
process.exit(1);
}
@@ -636,7 +681,7 @@ async function execAction(jsonParams) {
if (result.success && params.operation) {
const writeOps = ['init', 'write', 'update', 'archive', 'mkdir', 'delete'];
if (writeOps.includes(params.operation)) {
const eventMap = {
const eventMap: Record<string, string> = {
init: 'SESSION_CREATED',
write: 'CONTENT_WRITTEN',
update: 'SESSION_UPDATED',
@@ -662,7 +707,11 @@ async function execAction(jsonParams) {
* @param {string[]} args - Arguments
* @param {Object} options - CLI options
*/
export async function sessionCommand(subcommand, args, options) {
export async function sessionCommand(
subcommand: string,
args: string | string[],
options: any
): Promise<void> {
const argsArray = Array.isArray(args) ? args : (args ? [args] : []);
switch (subcommand) {

View File

@@ -4,12 +4,17 @@ import { promisify } from 'util';
const execAsync = promisify(exec);
interface StopOptions {
port?: number;
force?: boolean;
}
/**
* Find process using a specific port (Windows)
* @param {number} port - Port number
* @returns {Promise<string|null>} PID or null
*/
async function findProcessOnPort(port) {
async function findProcessOnPort(port: number): Promise<string | null> {
try {
const { stdout } = await execAsync(`netstat -ano | findstr :${port} | findstr LISTENING`);
const lines = stdout.trim().split('\n');
@@ -28,7 +33,7 @@ async function findProcessOnPort(port) {
* @param {string} pid - Process ID
* @returns {Promise<boolean>} Success status
*/
async function killProcess(pid) {
async function killProcess(pid: string): Promise<boolean> {
try {
await execAsync(`taskkill /PID ${pid} /F`);
return true;
@@ -41,7 +46,7 @@ async function killProcess(pid) {
* Stop command handler - stops the running CCW dashboard server
* @param {Object} options - Command options
*/
export async function stopCommand(options) {
export async function stopCommand(options: StopOptions): Promise<void> {
const port = options.port || 3456;
const force = options.force || false;
@@ -96,6 +101,7 @@ export async function stopCommand(options) {
}
} catch (err) {
console.error(chalk.red(`\n Error: ${err.message}\n`));
const error = err as Error;
console.error(chalk.red(`\n Error: ${error.message}\n`));
}
}

View File

@@ -5,10 +5,32 @@
import chalk from 'chalk';
import { listTools, executeTool, getTool, getAllToolSchemas } from '../tools/index.js';
interface ToolOptions {
name?: string;
}
interface ExecOptions {
path?: string;
old?: string;
new?: string;
action?: string;
query?: string;
limit?: string;
file?: string;
files?: string;
languages?: string;
mode?: string;
operation?: string;
line?: string;
text?: string;
dryRun?: boolean;
replaceAll?: boolean;
}
/**
* List all available tools
*/
async function listAction() {
async function listAction(): Promise<void> {
const tools = listTools();
if (tools.length === 0) {
@@ -29,8 +51,8 @@ async function listAction() {
console.log(chalk.gray(' Parameters:'));
for (const [name, schema] of Object.entries(props)) {
const req = required.includes(name) ? chalk.red('*') : '';
const defaultVal = schema.default !== undefined ? chalk.gray(` (default: ${schema.default})`) : '';
console.log(chalk.gray(` - ${name}${req}: ${schema.description}${defaultVal}`));
const defaultVal = (schema as any).default !== undefined ? chalk.gray(` (default: ${(schema as any).default})`) : '';
console.log(chalk.gray(` - ${name}${req}: ${(schema as any).description}${defaultVal}`));
}
}
console.log();
@@ -40,7 +62,7 @@ async function listAction() {
/**
* Show tool schema in MCP-compatible JSON format
*/
async function schemaAction(options) {
async function schemaAction(options: ToolOptions): Promise<void> {
const { name } = options;
if (name) {
@@ -72,7 +94,7 @@ async function schemaAction(options) {
* @param {string|undefined} jsonParams - JSON string of parameters
* @param {Object} options - CLI options
*/
async function execAction(toolName, jsonParams, options) {
async function execAction(toolName: string | undefined, jsonParams: string | undefined, options: ExecOptions): Promise<void> {
if (!toolName) {
console.error(chalk.red('Tool name is required'));
console.error(chalk.gray('Usage: ccw tool exec <tool_name> \'{"param": "value"}\''));
@@ -89,15 +111,16 @@ async function execAction(toolName, jsonParams, options) {
}
// Build params from CLI options or JSON
let params = {};
let params: any = {};
// Check if JSON params provided
if (jsonParams && jsonParams.trim().startsWith('{')) {
try {
params = JSON.parse(jsonParams);
} catch (e) {
const error = e as Error;
console.error(chalk.red('Invalid JSON parameters'));
console.error(chalk.gray(`Parse error: ${e.message}`));
console.error(chalk.gray(`Parse error: ${error.message}`));
process.exit(1);
}
} else if (toolName === 'edit_file') {
@@ -146,7 +169,7 @@ async function execAction(toolName, jsonParams, options) {
* @param {string[]} args - Arguments array [toolName, jsonParams, ...]
* @param {Object} options - CLI options
*/
export async function toolCommand(subcommand, args, options) {
export async function toolCommand(subcommand: string, args: string | string[], options: ExecOptions): Promise<void> {
// args is now an array due to [args...] in cli.js
const argsArray = Array.isArray(args) ? args : (args ? [args] : []);

View File

@@ -9,11 +9,18 @@ import { getAllManifests, deleteManifest } from '../core/manifest.js';
// Global subdirectories that should be protected when Global installation exists
const GLOBAL_SUBDIRS = ['workflows', 'scripts', 'templates'];
interface UninstallOptions {}
interface FileEntry {
path: string;
error: string;
}
/**
* Uninstall command handler
* @param {Object} options - Command options
*/
export async function uninstallCommand(options) {
export async function uninstallCommand(options: UninstallOptions): Promise<void> {
showBanner();
console.log(chalk.cyan.bold(' Uninstall Claude Code Workflow\n'));
@@ -42,7 +49,7 @@ export async function uninstallCommand(options) {
divider();
// Select installation to uninstall
let selectedManifest;
let selectedManifest: any;
if (manifests.length === 1) {
const { confirm } = await inquirer.prompt([{
@@ -117,7 +124,7 @@ export async function uninstallCommand(options) {
let removedFiles = 0;
let removedDirs = 0;
let failedFiles = [];
let failedFiles: FileEntry[] = [];
try {
// Remove files first (in reverse order to handle nested files)
@@ -152,7 +159,8 @@ export async function uninstallCommand(options) {
removedFiles++;
}
} catch (err) {
failedFiles.push({ path: filePath, error: err.message });
const error = err as Error;
failedFiles.push({ path: filePath, error: error.message });
}
}
@@ -160,7 +168,7 @@ export async function uninstallCommand(options) {
const directories = [...(selectedManifest.directories || [])].reverse();
// Sort by path length (deepest first)
directories.sort((a, b) => b.path.length - a.path.length);
directories.sort((a: any, b: any) => b.path.length - a.path.length);
for (const dirEntry of directories) {
const dirPath = dirEntry.path;
@@ -197,7 +205,8 @@ export async function uninstallCommand(options) {
} catch (err) {
spinner.fail('Uninstall failed');
error(err.message);
const errMsg = err as Error;
error(errMsg.message);
return;
}
@@ -207,7 +216,7 @@ export async function uninstallCommand(options) {
// Show summary
console.log('');
const summaryLines = [];
const summaryLines: string[] = [];
if (failedFiles.length > 0) {
summaryLines.push(chalk.yellow.bold('⚠ Partially Completed'));
@@ -216,15 +225,15 @@ export async function uninstallCommand(options) {
}
summaryLines.push('');
summaryLines.push(chalk.white(`Files removed: ${chalk.green(removedFiles)}`));
summaryLines.push(chalk.white(`Directories removed: ${chalk.green(removedDirs)}`));
summaryLines.push(chalk.white(`Files removed: ${chalk.green(removedFiles.toString())}`));
summaryLines.push(chalk.white(`Directories removed: ${chalk.green(removedDirs.toString())}`));
if (skippedFiles > 0) {
summaryLines.push(chalk.white(`Global files preserved: ${chalk.cyan(skippedFiles)}`));
summaryLines.push(chalk.white(`Global files preserved: ${chalk.cyan(skippedFiles.toString())}`));
}
if (failedFiles.length > 0) {
summaryLines.push(chalk.white(`Failed: ${chalk.red(failedFiles.length)}`));
summaryLines.push(chalk.white(`Failed: ${chalk.red(failedFiles.length.toString())}`));
summaryLines.push('');
summaryLines.push(chalk.gray('Some files could not be removed.'));
summaryLines.push(chalk.gray('They may be in use or require elevated permissions.'));
@@ -254,7 +263,7 @@ export async function uninstallCommand(options) {
* Recursively remove empty directories
* @param {string} dirPath - Directory path
*/
async function removeEmptyDirs(dirPath) {
async function removeEmptyDirs(dirPath: string): Promise<void> {
if (!existsSync(dirPath)) return;
const stat = statSync(dirPath);
@@ -276,4 +285,3 @@ async function removeEmptyDirs(dirPath) {
rmdirSync(dirPath);
}
}

View File

@@ -16,13 +16,27 @@ const SOURCE_DIRS = ['.claude', '.codex', '.gemini', '.qwen'];
// Subdirectories that should always be installed to global (~/.claude/)
const GLOBAL_SUBDIRS = ['workflows', 'scripts', 'templates'];
interface UpgradeOptions {
all?: boolean;
}
interface UpgradeResult {
files: number;
directories: number;
}
interface CopyResult {
files: number;
directories: number;
}
// Get package root directory (ccw/src/commands -> ccw)
function getPackageRoot() {
function getPackageRoot(): string {
return join(__dirname, '..', '..');
}
// Get source installation directory (parent of ccw)
function getSourceDir() {
function getSourceDir(): string {
return join(getPackageRoot(), '..');
}
@@ -30,7 +44,7 @@ function getSourceDir() {
* Get package version
* @returns {string} - Version string
*/
function getVersion() {
function getVersion(): string {
try {
// First try root package.json (parent of ccw)
const rootPkgPath = join(getSourceDir(), 'package.json');
@@ -51,7 +65,7 @@ function getVersion() {
* Upgrade command handler
* @param {Object} options - Command options
*/
export async function upgradeCommand(options) {
export async function upgradeCommand(options: UpgradeOptions): Promise<void> {
showBanner();
console.log(chalk.cyan.bold(' Upgrade Claude Code Workflow\n'));
@@ -69,7 +83,7 @@ export async function upgradeCommand(options) {
// Display current installations
console.log(chalk.white.bold(' Current installations:\n'));
const upgradeTargets = [];
const upgradeTargets: any[] = [];
for (let i = 0; i < manifests.length; i++) {
const m = manifests[i];
@@ -116,7 +130,7 @@ export async function upgradeCommand(options) {
}
// Select which installations to upgrade
let selectedManifests = [];
let selectedManifests: any[] = [];
if (options.all) {
selectedManifests = upgradeTargets.map(t => t.manifest);
@@ -154,12 +168,12 @@ export async function upgradeCommand(options) {
return;
}
selectedManifests = selections.map(i => upgradeTargets[i].manifest);
selectedManifests = selections.map((i: number) => upgradeTargets[i].manifest);
}
// Perform upgrades
console.log('');
const results = [];
const results: any[] = [];
const sourceDir = getSourceDir();
for (const manifest of selectedManifests) {
@@ -170,9 +184,10 @@ export async function upgradeCommand(options) {
upgradeSpinner.succeed(`Upgraded ${manifest.installation_mode}: ${result.files} files`);
results.push({ manifest, success: true, ...result });
} catch (err) {
const errMsg = err as Error;
upgradeSpinner.fail(`Failed to upgrade ${manifest.installation_mode}`);
error(err.message);
results.push({ manifest, success: false, error: err.message });
error(errMsg.message);
results.push({ manifest, success: false, error: errMsg.message });
}
}
@@ -219,7 +234,7 @@ export async function upgradeCommand(options) {
* @param {string} version - Version string
* @returns {Promise<Object>} - Upgrade result
*/
async function performUpgrade(manifest, sourceDir, version) {
async function performUpgrade(manifest: any, sourceDir: string, version: string): Promise<UpgradeResult> {
const installPath = manifest.installation_path;
const mode = manifest.installation_mode;
@@ -294,7 +309,12 @@ async function performUpgrade(manifest, sourceDir, version) {
* @param {string[]} excludeDirs - Directory names to exclude (optional)
* @returns {Object} - Count of files and directories
*/
async function copyDirectory(src, dest, manifest, excludeDirs = []) {
async function copyDirectory(
src: string,
dest: string,
manifest: any,
excludeDirs: string[] = []
): Promise<CopyResult> {
let files = 0;
let directories = 0;

View File

@@ -3,12 +3,24 @@ import { launchBrowser } from '../utils/browser-launcher.js';
import { validatePath } from '../utils/path-resolver.js';
import chalk from 'chalk';
interface ViewOptions {
port?: number;
path?: string;
browser?: boolean;
}
interface SwitchWorkspaceResult {
success: boolean;
path?: string;
error?: string;
}
/**
* Check if server is already running on the specified port
* @param {number} port - Port to check
* @returns {Promise<boolean>} True if server is running
*/
async function isServerRunning(port) {
async function isServerRunning(port: number): Promise<boolean> {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 1000);
@@ -30,14 +42,15 @@ async function isServerRunning(port) {
* @param {string} path - New workspace path
* @returns {Promise<Object>} Result with success status
*/
async function switchWorkspace(port, path) {
async function switchWorkspace(port: number, path: string): Promise<SwitchWorkspaceResult> {
try {
const response = await fetch(
`http://localhost:${port}/api/switch-path?path=${encodeURIComponent(path)}`
);
return await response.json();
return await response.json() as SwitchWorkspaceResult;
} catch (err) {
return { success: false, error: err.message };
const error = err as Error;
return { success: false, error: error.message };
}
}
@@ -47,14 +60,14 @@ async function switchWorkspace(port, path) {
* If not running, starts a new server
* @param {Object} options - Command options
*/
export async function viewCommand(options) {
export async function viewCommand(options: ViewOptions): Promise<void> {
const port = options.port || 3456;
// Resolve workspace path
let workspacePath = process.cwd();
if (options.path) {
const pathValidation = validatePath(options.path, { mustExist: true });
if (!pathValidation.valid) {
if (!pathValidation.valid || !pathValidation.path) {
console.error(chalk.red(`\n Error: ${pathValidation.error}\n`));
process.exit(1);
}
@@ -76,7 +89,7 @@ export async function viewCommand(options) {
console.log(chalk.green(` Workspace switched successfully`));
// Open browser with the new path
const url = `http://localhost:${port}/?path=${encodeURIComponent(result.path)}`;
const url = `http://localhost:${port}/?path=${encodeURIComponent(result.path!)}`;
if (options.browser !== false) {
console.log(chalk.cyan(' Opening in browser...'));
@@ -84,7 +97,8 @@ export async function viewCommand(options) {
await launchBrowser(url);
console.log(chalk.green.bold('\n Dashboard opened!\n'));
} catch (err) {
console.log(chalk.yellow(`\n Could not open browser: ${err.message}`));
const error = err as Error;
console.log(chalk.yellow(`\n Could not open browser: ${error.message}`));
console.log(chalk.gray(` Open manually: ${url}\n`));
}
} else {