mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
feat(ccw): 添加 ccw tool exec 工具系统
新增工具:
- edit_file: AI辅助文件编辑 (update/line模式)
- get_modules_by_depth: 项目结构分析
- update_module_claude: CLAUDE.md文档生成
- generate_module_docs: 模块文档生成
- detect_changed_modules: Git变更检测
- classify_folders: 文件夹分类
- discover_design_files: 设计文件发现
- convert_tokens_to_css: 设计token转CSS
- ui_generate_preview: UI预览生成
- ui_instantiate_prototypes: 原型实例化
使用方式:
ccw tool list # 列出所有工具
ccw tool exec <name> '{}' # 执行工具
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import { installCommand } from './commands/install.js';
|
|||||||
import { uninstallCommand } from './commands/uninstall.js';
|
import { uninstallCommand } from './commands/uninstall.js';
|
||||||
import { upgradeCommand } from './commands/upgrade.js';
|
import { upgradeCommand } from './commands/upgrade.js';
|
||||||
import { listCommand } from './commands/list.js';
|
import { listCommand } from './commands/list.js';
|
||||||
|
import { toolCommand } from './commands/tool.js';
|
||||||
import { readFileSync, existsSync } from 'fs';
|
import { readFileSync, existsSync } from 'fs';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { dirname, join } from 'path';
|
import { dirname, join } from 'path';
|
||||||
@@ -105,5 +106,11 @@ export function run(argv) {
|
|||||||
.description('List all installed Claude Code Workflow instances')
|
.description('List all installed Claude Code Workflow instances')
|
||||||
.action(listCommand);
|
.action(listCommand);
|
||||||
|
|
||||||
|
// Tool command
|
||||||
|
program
|
||||||
|
.command('tool [subcommand] [args] [json]')
|
||||||
|
.description('Execute CCW tools')
|
||||||
|
.action((subcommand, args, json) => toolCommand(subcommand, args, { json }));
|
||||||
|
|
||||||
program.parse(argv);
|
program.parse(argv);
|
||||||
}
|
}
|
||||||
|
|||||||
181
ccw/src/commands/tool.js
Normal file
181
ccw/src/commands/tool.js
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
/**
|
||||||
|
* Tool Command - Execute and manage CCW tools
|
||||||
|
*/
|
||||||
|
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import { listTools, executeTool, getTool, getAllToolSchemas } from '../tools/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all available tools
|
||||||
|
*/
|
||||||
|
async function listAction() {
|
||||||
|
const tools = listTools();
|
||||||
|
|
||||||
|
if (tools.length === 0) {
|
||||||
|
console.log(chalk.yellow('No tools registered'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(chalk.bold.cyan('\nAvailable Tools:\n'));
|
||||||
|
|
||||||
|
for (const tool of tools) {
|
||||||
|
console.log(chalk.bold.white(` ${tool.name}`));
|
||||||
|
console.log(chalk.gray(` ${tool.description}`));
|
||||||
|
|
||||||
|
if (tool.parameters?.properties) {
|
||||||
|
const props = tool.parameters.properties;
|
||||||
|
const required = tool.parameters.required || [];
|
||||||
|
|
||||||
|
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}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show tool schema in MCP-compatible JSON format
|
||||||
|
*/
|
||||||
|
async function schemaAction(options) {
|
||||||
|
const { name } = options;
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
const tool = getTool(name);
|
||||||
|
if (!tool) {
|
||||||
|
console.error(chalk.red(`Tool not found: ${name}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
name: tool.name,
|
||||||
|
description: tool.description,
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: tool.parameters?.properties || {},
|
||||||
|
required: tool.parameters?.required || []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
console.log(JSON.stringify(schema, null, 2));
|
||||||
|
} else {
|
||||||
|
const schemas = getAllToolSchemas();
|
||||||
|
console.log(JSON.stringify({ tools: schemas }, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read from stdin if available
|
||||||
|
*/
|
||||||
|
async function readStdin() {
|
||||||
|
// Check if stdin is a TTY (interactive terminal)
|
||||||
|
if (process.stdin.isTTY) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let data = '';
|
||||||
|
|
||||||
|
process.stdin.setEncoding('utf8');
|
||||||
|
|
||||||
|
process.stdin.on('readable', () => {
|
||||||
|
let chunk;
|
||||||
|
while ((chunk = process.stdin.read()) !== null) {
|
||||||
|
data += chunk;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
process.stdin.on('end', () => {
|
||||||
|
resolve(data.trim() || null);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.stdin.on('error', (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a tool with given parameters
|
||||||
|
*/
|
||||||
|
async function execAction(toolName, jsonInput, options) {
|
||||||
|
if (!toolName) {
|
||||||
|
console.error(chalk.red('Tool name is required'));
|
||||||
|
console.error(chalk.gray('Usage: ccw tool exec <tool-name> \'{"param": "value"}\''));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tool = getTool(toolName);
|
||||||
|
if (!tool) {
|
||||||
|
console.error(chalk.red(`Tool not found: ${toolName}`));
|
||||||
|
console.error(chalk.gray('Use "ccw tool list" to see available tools'));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse JSON input (default format)
|
||||||
|
let params = {};
|
||||||
|
|
||||||
|
if (jsonInput) {
|
||||||
|
try {
|
||||||
|
params = JSON.parse(jsonInput);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(chalk.red(`Invalid JSON: ${error.message}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for stdin input (for piped commands)
|
||||||
|
const stdinData = await readStdin();
|
||||||
|
if (stdinData) {
|
||||||
|
// If tool has an 'input' parameter, use it
|
||||||
|
// Otherwise, try to parse stdin as JSON and merge with params
|
||||||
|
if (tool.parameters?.properties?.input) {
|
||||||
|
params.input = stdinData;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const stdinJson = JSON.parse(stdinData);
|
||||||
|
params = { ...stdinJson, ...params };
|
||||||
|
} catch {
|
||||||
|
// If not JSON, store as 'input' anyway
|
||||||
|
params.input = stdinData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute tool
|
||||||
|
const result = await executeTool(toolName, params);
|
||||||
|
|
||||||
|
// Always output JSON
|
||||||
|
console.log(JSON.stringify(result, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool command entry point
|
||||||
|
*/
|
||||||
|
export async function toolCommand(subcommand, args, options) {
|
||||||
|
// Handle subcommands
|
||||||
|
switch (subcommand) {
|
||||||
|
case 'list':
|
||||||
|
await listAction();
|
||||||
|
break;
|
||||||
|
case 'schema':
|
||||||
|
await schemaAction({ name: args });
|
||||||
|
break;
|
||||||
|
case 'exec':
|
||||||
|
await execAction(args, options.json, options);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log(chalk.bold.cyan('\nCCW Tool System\n'));
|
||||||
|
console.log('Subcommands:');
|
||||||
|
console.log(chalk.gray(' list List all available tools'));
|
||||||
|
console.log(chalk.gray(' schema [name] Show tool schema (JSON)'));
|
||||||
|
console.log(chalk.gray(' exec <name> Execute a tool'));
|
||||||
|
console.log();
|
||||||
|
console.log('Examples:');
|
||||||
|
console.log(chalk.gray(' ccw tool list'));
|
||||||
|
console.log(chalk.gray(' ccw tool schema edit_file'));
|
||||||
|
console.log(chalk.gray(' ccw tool exec edit_file \'{"path":"file.txt","oldText":"old","newText":"new"}\''));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,20 +54,36 @@ function scanLiteDir(dir, type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load plan.json from session directory
|
* Load plan.json or fix-plan.json from session directory
|
||||||
* @param {string} sessionPath - Session directory path
|
* @param {string} sessionPath - Session directory path
|
||||||
* @returns {Object|null} - Plan data or null
|
* @returns {Object|null} - Plan data or null
|
||||||
*/
|
*/
|
||||||
function loadPlanJson(sessionPath) {
|
function loadPlanJson(sessionPath) {
|
||||||
|
// Try fix-plan.json first (for lite-fix), then plan.json (for lite-plan)
|
||||||
|
const fixPlanPath = join(sessionPath, 'fix-plan.json');
|
||||||
const planPath = join(sessionPath, 'plan.json');
|
const planPath = join(sessionPath, 'plan.json');
|
||||||
if (!existsSync(planPath)) return null;
|
|
||||||
|
|
||||||
try {
|
// Try fix-plan.json first
|
||||||
const content = readFileSync(planPath, 'utf8');
|
if (existsSync(fixPlanPath)) {
|
||||||
return JSON.parse(content);
|
try {
|
||||||
} catch {
|
const content = readFileSync(fixPlanPath, 'utf8');
|
||||||
return null;
|
return JSON.parse(content);
|
||||||
|
} catch {
|
||||||
|
// Continue to try plan.json
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback to plan.json
|
||||||
|
if (existsSync(planPath)) {
|
||||||
|
try {
|
||||||
|
const content = readFileSync(planPath, 'utf8');
|
||||||
|
return JSON.parse(content);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -91,6 +107,7 @@ function loadTaskJsons(sessionPath) {
|
|||||||
f.startsWith('IMPL-') ||
|
f.startsWith('IMPL-') ||
|
||||||
f.startsWith('TASK-') ||
|
f.startsWith('TASK-') ||
|
||||||
f.startsWith('task-') ||
|
f.startsWith('task-') ||
|
||||||
|
f.startsWith('diagnosis-') ||
|
||||||
/^T\d+\.json$/i.test(f)
|
/^T\d+\.json$/i.test(f)
|
||||||
))
|
))
|
||||||
.map(f => {
|
.map(f => {
|
||||||
@@ -109,12 +126,18 @@ function loadTaskJsons(sessionPath) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method 2: Check plan.json for embedded tasks array
|
// Method 2: Check plan.json or fix-plan.json for embedded tasks array
|
||||||
if (tasks.length === 0) {
|
if (tasks.length === 0) {
|
||||||
|
// Try fix-plan.json first (for lite-fix), then plan.json (for lite-plan)
|
||||||
|
const fixPlanPath = join(sessionPath, 'fix-plan.json');
|
||||||
const planPath = join(sessionPath, 'plan.json');
|
const planPath = join(sessionPath, 'plan.json');
|
||||||
if (existsSync(planPath)) {
|
|
||||||
|
const planFile = existsSync(fixPlanPath) ? fixPlanPath :
|
||||||
|
existsSync(planPath) ? planPath : null;
|
||||||
|
|
||||||
|
if (planFile) {
|
||||||
try {
|
try {
|
||||||
const plan = JSON.parse(readFileSync(planPath, 'utf8'));
|
const plan = JSON.parse(readFileSync(planFile, 'utf8'));
|
||||||
if (Array.isArray(plan.tasks)) {
|
if (Array.isArray(plan.tasks)) {
|
||||||
tasks = plan.tasks.map(t => normalizeTask(t));
|
tasks = plan.tasks.map(t => normalizeTask(t));
|
||||||
}
|
}
|
||||||
@@ -124,13 +147,14 @@ function loadTaskJsons(sessionPath) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method 3: Check for task-*.json files in session root
|
// Method 3: Check for task-*.json and diagnosis-*.json files in session root
|
||||||
if (tasks.length === 0) {
|
if (tasks.length === 0) {
|
||||||
try {
|
try {
|
||||||
const rootTasks = readdirSync(sessionPath)
|
const rootTasks = readdirSync(sessionPath)
|
||||||
.filter(f => f.endsWith('.json') && (
|
.filter(f => f.endsWith('.json') && (
|
||||||
f.startsWith('task-') ||
|
f.startsWith('task-') ||
|
||||||
f.startsWith('TASK-') ||
|
f.startsWith('TASK-') ||
|
||||||
|
f.startsWith('diagnosis-') ||
|
||||||
/^T\d+\.json$/i.test(f)
|
/^T\d+\.json$/i.test(f)
|
||||||
))
|
))
|
||||||
.map(f => {
|
.map(f => {
|
||||||
|
|||||||
@@ -14,6 +14,19 @@ const CLAUDE_SETTINGS_DIR = join(homedir(), '.claude');
|
|||||||
const CLAUDE_GLOBAL_SETTINGS = join(CLAUDE_SETTINGS_DIR, 'settings.json');
|
const CLAUDE_GLOBAL_SETTINGS = join(CLAUDE_SETTINGS_DIR, 'settings.json');
|
||||||
const CLAUDE_GLOBAL_SETTINGS_LOCAL = join(CLAUDE_SETTINGS_DIR, 'settings.local.json');
|
const CLAUDE_GLOBAL_SETTINGS_LOCAL = join(CLAUDE_SETTINGS_DIR, 'settings.local.json');
|
||||||
|
|
||||||
|
// Enterprise managed MCP paths (platform-specific)
|
||||||
|
function getEnterpriseMcpPath() {
|
||||||
|
const platform = process.platform;
|
||||||
|
if (platform === 'darwin') {
|
||||||
|
return '/Library/Application Support/ClaudeCode/managed-mcp.json';
|
||||||
|
} else if (platform === 'win32') {
|
||||||
|
return 'C:\\Program Files\\ClaudeCode\\managed-mcp.json';
|
||||||
|
} else {
|
||||||
|
// Linux and WSL
|
||||||
|
return '/etc/claude-code/managed-mcp.json';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WebSocket clients for real-time notifications
|
// WebSocket clients for real-time notifications
|
||||||
const wsClients = new Set();
|
const wsClients = new Set();
|
||||||
|
|
||||||
@@ -1042,67 +1055,99 @@ function safeReadJson(filePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get MCP servers from a settings file
|
* Get MCP servers from a JSON file (expects mcpServers key at top level)
|
||||||
* @param {string} filePath
|
* @param {string} filePath
|
||||||
* @returns {Object} mcpServers object or empty object
|
* @returns {Object} mcpServers object or empty object
|
||||||
*/
|
*/
|
||||||
function getMcpServersFromSettings(filePath) {
|
function getMcpServersFromFile(filePath) {
|
||||||
const config = safeReadJson(filePath);
|
const config = safeReadJson(filePath);
|
||||||
if (!config) return {};
|
if (!config) return {};
|
||||||
return config.mcpServers || {};
|
return config.mcpServers || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get MCP configuration from multiple sources:
|
* Get MCP configuration from multiple sources (per official Claude Code docs):
|
||||||
* 1. ~/.claude.json (project-level MCP servers)
|
*
|
||||||
* 2. ~/.claude/settings.json and settings.local.json (global MCP servers)
|
* Priority (highest to lowest):
|
||||||
* 3. Each workspace's .claude/settings.json and settings.local.json
|
* 1. Enterprise managed-mcp.json (cannot be overridden)
|
||||||
|
* 2. Local scope (project-specific private in ~/.claude.json)
|
||||||
|
* 3. Project scope (.mcp.json in project root)
|
||||||
|
* 4. User scope (mcpServers in ~/.claude.json)
|
||||||
|
*
|
||||||
|
* Note: ~/.claude/settings.json is for MCP PERMISSIONS, NOT definitions!
|
||||||
|
*
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
function getMcpConfig() {
|
function getMcpConfig() {
|
||||||
try {
|
try {
|
||||||
const result = { projects: {}, globalServers: {} };
|
const result = {
|
||||||
|
projects: {},
|
||||||
|
userServers: {}, // User-level servers from ~/.claude.json mcpServers
|
||||||
|
enterpriseServers: {}, // Enterprise managed servers (highest priority)
|
||||||
|
configSources: [] // Track where configs came from for debugging
|
||||||
|
};
|
||||||
|
|
||||||
// 1. Read from ~/.claude.json (primary source for project MCP)
|
// 1. Read Enterprise managed MCP servers (highest priority)
|
||||||
if (existsSync(CLAUDE_CONFIG_PATH)) {
|
const enterprisePath = getEnterpriseMcpPath();
|
||||||
const content = readFileSync(CLAUDE_CONFIG_PATH, 'utf8');
|
if (existsSync(enterprisePath)) {
|
||||||
const config = JSON.parse(content);
|
const enterpriseConfig = safeReadJson(enterprisePath);
|
||||||
result.projects = config.projects || {};
|
if (enterpriseConfig?.mcpServers) {
|
||||||
}
|
result.enterpriseServers = enterpriseConfig.mcpServers;
|
||||||
|
result.configSources.push({ type: 'enterprise', path: enterprisePath, count: Object.keys(enterpriseConfig.mcpServers).length });
|
||||||
// 2. Read global MCP servers from ~/.claude/settings.json and settings.local.json
|
|
||||||
const globalSettings = getMcpServersFromSettings(CLAUDE_GLOBAL_SETTINGS);
|
|
||||||
const globalSettingsLocal = getMcpServersFromSettings(CLAUDE_GLOBAL_SETTINGS_LOCAL);
|
|
||||||
result.globalServers = { ...globalSettings, ...globalSettingsLocal };
|
|
||||||
|
|
||||||
// 3. For each project, also check .claude/settings.json and settings.local.json
|
|
||||||
for (const projectPath of Object.keys(result.projects)) {
|
|
||||||
const projectClaudeDir = join(projectPath, '.claude');
|
|
||||||
const projectSettings = join(projectClaudeDir, 'settings.json');
|
|
||||||
const projectSettingsLocal = join(projectClaudeDir, 'settings.local.json');
|
|
||||||
|
|
||||||
// Merge MCP servers from workspace settings into project config
|
|
||||||
const workspaceServers = {
|
|
||||||
...getMcpServersFromSettings(projectSettings),
|
|
||||||
...getMcpServersFromSettings(projectSettingsLocal)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (Object.keys(workspaceServers).length > 0) {
|
|
||||||
// Merge workspace servers with existing project servers (workspace takes precedence)
|
|
||||||
result.projects[projectPath] = {
|
|
||||||
...result.projects[projectPath],
|
|
||||||
mcpServers: {
|
|
||||||
...(result.projects[projectPath]?.mcpServers || {}),
|
|
||||||
...workspaceServers
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. Read from ~/.claude.json
|
||||||
|
if (existsSync(CLAUDE_CONFIG_PATH)) {
|
||||||
|
const claudeConfig = safeReadJson(CLAUDE_CONFIG_PATH);
|
||||||
|
if (claudeConfig) {
|
||||||
|
// 2a. User-level mcpServers (top-level mcpServers key)
|
||||||
|
if (claudeConfig.mcpServers) {
|
||||||
|
result.userServers = claudeConfig.mcpServers;
|
||||||
|
result.configSources.push({ type: 'user', path: CLAUDE_CONFIG_PATH, count: Object.keys(claudeConfig.mcpServers).length });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2b. Project-specific configurations (projects[path].mcpServers)
|
||||||
|
if (claudeConfig.projects) {
|
||||||
|
result.projects = claudeConfig.projects;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. For each known project, check for .mcp.json (project-level config)
|
||||||
|
const projectPaths = Object.keys(result.projects);
|
||||||
|
for (const projectPath of projectPaths) {
|
||||||
|
const mcpJsonPath = join(projectPath, '.mcp.json');
|
||||||
|
if (existsSync(mcpJsonPath)) {
|
||||||
|
const mcpJsonConfig = safeReadJson(mcpJsonPath);
|
||||||
|
if (mcpJsonConfig?.mcpServers) {
|
||||||
|
// Merge .mcp.json servers into project config
|
||||||
|
// Project's .mcp.json has lower priority than ~/.claude.json projects[path].mcpServers
|
||||||
|
const existingServers = result.projects[projectPath]?.mcpServers || {};
|
||||||
|
result.projects[projectPath] = {
|
||||||
|
...result.projects[projectPath],
|
||||||
|
mcpServers: {
|
||||||
|
...mcpJsonConfig.mcpServers, // .mcp.json (lower priority)
|
||||||
|
...existingServers // ~/.claude.json projects[path] (higher priority)
|
||||||
|
},
|
||||||
|
mcpJsonPath: mcpJsonPath // Track source for debugging
|
||||||
|
};
|
||||||
|
result.configSources.push({ type: 'project-mcp-json', path: mcpJsonPath, count: Object.keys(mcpJsonConfig.mcpServers).length });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build globalServers by merging user and enterprise servers
|
||||||
|
// Enterprise servers override user servers
|
||||||
|
result.globalServers = {
|
||||||
|
...result.userServers,
|
||||||
|
...result.enterpriseServers
|
||||||
|
};
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error reading MCP config:', error);
|
console.error('Error reading MCP config:', error);
|
||||||
return { projects: {}, globalServers: {}, error: error.message };
|
return { projects: {}, globalServers: {}, userServers: {}, enterpriseServers: {}, configSources: [], error: error.message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
// MCP Manager Component
|
// MCP Manager Component
|
||||||
// Manages MCP server configuration from .claude.json
|
// Manages MCP server configuration from multiple sources:
|
||||||
|
// - Enterprise: managed-mcp.json (highest priority)
|
||||||
|
// - User: ~/.claude.json mcpServers
|
||||||
|
// - Project: .mcp.json in project root
|
||||||
|
// - Local: ~/.claude.json projects[path].mcpServers
|
||||||
|
|
||||||
// ========== MCP State ==========
|
// ========== MCP State ==========
|
||||||
let mcpConfig = null;
|
let mcpConfig = null;
|
||||||
let mcpAllProjects = {};
|
let mcpAllProjects = {};
|
||||||
let mcpGlobalServers = {};
|
let mcpGlobalServers = {};
|
||||||
|
let mcpUserServers = {};
|
||||||
|
let mcpEnterpriseServers = {};
|
||||||
let mcpCurrentProjectServers = {};
|
let mcpCurrentProjectServers = {};
|
||||||
|
let mcpConfigSources = [];
|
||||||
let mcpCreateMode = 'form'; // 'form' or 'json'
|
let mcpCreateMode = 'form'; // 'form' or 'json'
|
||||||
|
|
||||||
// ========== Initialization ==========
|
// ========== Initialization ==========
|
||||||
@@ -33,6 +40,9 @@ async function loadMcpConfig() {
|
|||||||
mcpConfig = data;
|
mcpConfig = data;
|
||||||
mcpAllProjects = data.projects || {};
|
mcpAllProjects = data.projects || {};
|
||||||
mcpGlobalServers = data.globalServers || {};
|
mcpGlobalServers = data.globalServers || {};
|
||||||
|
mcpUserServers = data.userServers || {};
|
||||||
|
mcpEnterpriseServers = data.enterpriseServers || {};
|
||||||
|
mcpConfigSources = data.configSources || [];
|
||||||
|
|
||||||
// Get current project servers
|
// Get current project servers
|
||||||
const currentPath = projectPath.replace(/\//g, '\\');
|
const currentPath = projectPath.replace(/\//g, '\\');
|
||||||
|
|||||||
@@ -157,17 +157,19 @@ async function refreshWorkspace() {
|
|||||||
// Reload data from server
|
// Reload data from server
|
||||||
const data = await loadDashboardData(projectPath);
|
const data = await loadDashboardData(projectPath);
|
||||||
if (data) {
|
if (data) {
|
||||||
// Update stores
|
// Clear and repopulate stores
|
||||||
sessionDataStore = {};
|
Object.keys(sessionDataStore).forEach(k => delete sessionDataStore[k]);
|
||||||
liteTaskDataStore = {};
|
Object.keys(liteTaskDataStore).forEach(k => delete liteTaskDataStore[k]);
|
||||||
|
|
||||||
// Populate stores
|
// Populate stores
|
||||||
[...(data.activeSessions || []), ...(data.archivedSessions || [])].forEach(s => {
|
[...(data.activeSessions || []), ...(data.archivedSessions || [])].forEach(s => {
|
||||||
sessionDataStore[s.session_id] = s;
|
const sessionKey = `session-${s.session_id}`.replace(/[^a-zA-Z0-9-]/g, '-');
|
||||||
|
sessionDataStore[sessionKey] = s;
|
||||||
});
|
});
|
||||||
|
|
||||||
[...(data.liteTasks?.litePlan || []), ...(data.liteTasks?.liteFix || [])].forEach(s => {
|
[...(data.liteTasks?.litePlan || []), ...(data.liteTasks?.liteFix || [])].forEach(s => {
|
||||||
liteTaskDataStore[s.session_id] = s;
|
const sessionKey = `lite-${s.session_id}`.replace(/[^a-zA-Z0-9-]/g, '-');
|
||||||
|
liteTaskDataStore[sessionKey] = s;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update global data
|
// Update global data
|
||||||
|
|||||||
@@ -27,9 +27,11 @@ async function renderMcpManager() {
|
|||||||
// Separate current project servers and available servers
|
// Separate current project servers and available servers
|
||||||
const currentProjectServerNames = Object.keys(projectServers);
|
const currentProjectServerNames = Object.keys(projectServers);
|
||||||
|
|
||||||
// Separate global servers and project servers that are not in current project
|
// Separate enterprise, user, and other project servers
|
||||||
const globalServerEntries = Object.entries(mcpGlobalServers)
|
const enterpriseServerEntries = Object.entries(mcpEnterpriseServers || {})
|
||||||
.filter(([name]) => !currentProjectServerNames.includes(name));
|
.filter(([name]) => !currentProjectServerNames.includes(name));
|
||||||
|
const userServerEntries = Object.entries(mcpUserServers || {})
|
||||||
|
.filter(([name]) => !currentProjectServerNames.includes(name) && !(mcpEnterpriseServers || {})[name]);
|
||||||
const otherProjectServers = Object.entries(allAvailableServers)
|
const otherProjectServers = Object.entries(allAvailableServers)
|
||||||
.filter(([name, info]) => !currentProjectServerNames.includes(name) && !info.isGlobal);
|
.filter(([name, info]) => !currentProjectServerNames.includes(name) && !info.isGlobal);
|
||||||
|
|
||||||
@@ -65,20 +67,40 @@ async function renderMcpManager() {
|
|||||||
`}
|
`}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Global MCP Servers -->
|
<!-- Enterprise MCP Servers (Managed) -->
|
||||||
${globalServerEntries.length > 0 ? `
|
${enterpriseServerEntries.length > 0 ? `
|
||||||
<div class="mcp-section mb-6">
|
<div class="mcp-section mb-6">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-lg">🌐</span>
|
<span class="text-lg">🏢</span>
|
||||||
<h3 class="text-lg font-semibold text-foreground">Global MCP Servers</h3>
|
<h3 class="text-lg font-semibold text-foreground">Enterprise MCP Servers</h3>
|
||||||
|
<span class="text-xs px-2 py-0.5 bg-warning/20 text-warning rounded-full">Managed</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-sm text-muted-foreground">${globalServerEntries.length} servers from ~/.claude/settings</span>
|
<span class="text-sm text-muted-foreground">${enterpriseServerEntries.length} servers (read-only)</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mcp-server-grid grid gap-3">
|
<div class="mcp-server-grid grid gap-3">
|
||||||
${globalServerEntries.map(([serverName, serverConfig]) => {
|
${enterpriseServerEntries.map(([serverName, serverConfig]) => {
|
||||||
return renderGlobalServerCard(serverName, serverConfig);
|
return renderEnterpriseServerCard(serverName, serverConfig);
|
||||||
|
}).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<!-- User MCP Servers -->
|
||||||
|
${userServerEntries.length > 0 ? `
|
||||||
|
<div class="mcp-section mb-6">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-lg">👤</span>
|
||||||
|
<h3 class="text-lg font-semibold text-foreground">User MCP Servers</h3>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm text-muted-foreground">${userServerEntries.length} servers from ~/.claude.json</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mcp-server-grid grid gap-3">
|
||||||
|
${userServerEntries.map(([serverName, serverConfig]) => {
|
||||||
|
return renderGlobalServerCard(serverName, serverConfig, 'user');
|
||||||
}).join('')}
|
}).join('')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -263,18 +285,19 @@ function renderAvailableServerCard(serverName, serverInfo) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderGlobalServerCard(serverName, serverConfig) {
|
function renderGlobalServerCard(serverName, serverConfig, source = 'user') {
|
||||||
const command = serverConfig.command || 'N/A';
|
const command = serverConfig.command || serverConfig.url || 'N/A';
|
||||||
const args = serverConfig.args || [];
|
const args = serverConfig.args || [];
|
||||||
const hasEnv = serverConfig.env && Object.keys(serverConfig.env).length > 0;
|
const hasEnv = serverConfig.env && Object.keys(serverConfig.env).length > 0;
|
||||||
|
const serverType = serverConfig.type || 'stdio';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="mcp-server-card mcp-server-global bg-card border border-primary/30 rounded-lg p-4 hover:shadow-md transition-all">
|
<div class="mcp-server-card mcp-server-global bg-card border border-primary/30 rounded-lg p-4 hover:shadow-md transition-all">
|
||||||
<div class="flex items-start justify-between mb-3">
|
<div class="flex items-start justify-between mb-3">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-xl">🌐</span>
|
<span class="text-xl">👤</span>
|
||||||
<h4 class="font-semibold text-foreground">${escapeHtml(serverName)}</h4>
|
<h4 class="font-semibold text-foreground">${escapeHtml(serverName)}</h4>
|
||||||
<span class="text-xs px-2 py-0.5 bg-primary/10 text-primary rounded-full">Global</span>
|
<span class="text-xs px-2 py-0.5 bg-primary/10 text-primary rounded-full">User</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="px-3 py-1 text-xs bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity"
|
<button class="px-3 py-1 text-xs bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity"
|
||||||
data-server-name="${escapeHtml(serverName)}"
|
data-server-name="${escapeHtml(serverName)}"
|
||||||
@@ -286,7 +309,7 @@ function renderGlobalServerCard(serverName, serverConfig) {
|
|||||||
|
|
||||||
<div class="mcp-server-details text-sm space-y-1">
|
<div class="mcp-server-details text-sm space-y-1">
|
||||||
<div class="flex items-center gap-2 text-muted-foreground">
|
<div class="flex items-center gap-2 text-muted-foreground">
|
||||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">cmd</span>
|
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">${serverType === 'stdio' ? 'cmd' : 'url'}</span>
|
||||||
<span class="truncate" title="${escapeHtml(command)}">${escapeHtml(command)}</span>
|
<span class="truncate" title="${escapeHtml(command)}">${escapeHtml(command)}</span>
|
||||||
</div>
|
</div>
|
||||||
${args.length > 0 ? `
|
${args.length > 0 ? `
|
||||||
@@ -302,7 +325,52 @@ function renderGlobalServerCard(serverName, serverConfig) {
|
|||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
<div class="flex items-center gap-2 text-muted-foreground mt-1">
|
<div class="flex items-center gap-2 text-muted-foreground mt-1">
|
||||||
<span class="text-xs italic">Available to all projects from ~/.claude/settings</span>
|
<span class="text-xs italic">Available to all projects from ~/.claude.json</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEnterpriseServerCard(serverName, serverConfig) {
|
||||||
|
const command = serverConfig.command || serverConfig.url || 'N/A';
|
||||||
|
const args = serverConfig.args || [];
|
||||||
|
const hasEnv = serverConfig.env && Object.keys(serverConfig.env).length > 0;
|
||||||
|
const serverType = serverConfig.type || 'stdio';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="mcp-server-card mcp-server-enterprise bg-card border border-warning/30 rounded-lg p-4 hover:shadow-md transition-all">
|
||||||
|
<div class="flex items-start justify-between mb-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-xl">🏢</span>
|
||||||
|
<h4 class="font-semibold text-foreground">${escapeHtml(serverName)}</h4>
|
||||||
|
<span class="text-xs px-2 py-0.5 bg-warning/20 text-warning rounded-full">Enterprise</span>
|
||||||
|
<span class="text-xs text-muted-foreground">🔒</span>
|
||||||
|
</div>
|
||||||
|
<span class="px-3 py-1 text-xs bg-muted text-muted-foreground rounded cursor-not-allowed">
|
||||||
|
Read-only
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mcp-server-details text-sm space-y-1">
|
||||||
|
<div class="flex items-center gap-2 text-muted-foreground">
|
||||||
|
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">${serverType === 'stdio' ? 'cmd' : 'url'}</span>
|
||||||
|
<span class="truncate" title="${escapeHtml(command)}">${escapeHtml(command)}</span>
|
||||||
|
</div>
|
||||||
|
${args.length > 0 ? `
|
||||||
|
<div class="flex items-start gap-2 text-muted-foreground">
|
||||||
|
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0">args</span>
|
||||||
|
<span class="text-xs font-mono truncate" title="${escapeHtml(args.join(' '))}">${escapeHtml(args.slice(0, 3).join(' '))}${args.length > 3 ? '...' : ''}</span>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
${hasEnv ? `
|
||||||
|
<div class="flex items-center gap-2 text-muted-foreground">
|
||||||
|
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">env</span>
|
||||||
|
<span class="text-xs">${Object.keys(serverConfig.env).length} variables</span>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
<div class="flex items-center gap-2 text-muted-foreground mt-1">
|
||||||
|
<span class="text-xs italic">Managed by organization (highest priority)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
204
ccw/src/tools/classify-folders.js
Normal file
204
ccw/src/tools/classify-folders.js
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
/**
|
||||||
|
* Classify Folders Tool
|
||||||
|
* Categorize folders by type for documentation generation
|
||||||
|
* Types: code (API.md + README.md), navigation (README.md only), skip (empty)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readdirSync, statSync, existsSync } from 'fs';
|
||||||
|
import { join, resolve, extname } from 'path';
|
||||||
|
|
||||||
|
// Code file extensions
|
||||||
|
const CODE_EXTENSIONS = [
|
||||||
|
'.ts', '.tsx', '.js', '.jsx',
|
||||||
|
'.py', '.go', '.java', '.rs',
|
||||||
|
'.c', '.cpp', '.cs', '.rb',
|
||||||
|
'.php', '.swift', '.kt'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count code files in a directory (non-recursive)
|
||||||
|
*/
|
||||||
|
function countCodeFiles(dirPath) {
|
||||||
|
try {
|
||||||
|
const entries = readdirSync(dirPath, { withFileTypes: true });
|
||||||
|
return entries.filter(e => {
|
||||||
|
if (!e.isFile()) return false;
|
||||||
|
const ext = extname(e.name).toLowerCase();
|
||||||
|
return CODE_EXTENSIONS.includes(ext);
|
||||||
|
}).length;
|
||||||
|
} catch (e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count subdirectories in a directory
|
||||||
|
*/
|
||||||
|
function countSubdirs(dirPath) {
|
||||||
|
try {
|
||||||
|
const entries = readdirSync(dirPath, { withFileTypes: true });
|
||||||
|
return entries.filter(e => e.isDirectory() && !e.name.startsWith('.')).length;
|
||||||
|
} catch (e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine folder type
|
||||||
|
*/
|
||||||
|
function classifyFolder(dirPath) {
|
||||||
|
const codeFiles = countCodeFiles(dirPath);
|
||||||
|
const subdirs = countSubdirs(dirPath);
|
||||||
|
|
||||||
|
if (codeFiles > 0) {
|
||||||
|
return { type: 'code', codeFiles, subdirs }; // Generates API.md + README.md
|
||||||
|
} else if (subdirs > 0) {
|
||||||
|
return { type: 'navigation', codeFiles, subdirs }; // README.md only
|
||||||
|
} else {
|
||||||
|
return { type: 'skip', codeFiles, subdirs }; // Empty or no relevant content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse input from get_modules_by_depth format
|
||||||
|
* Format: depth:N|path:./path|files:N|types:[ext,ext]|has_claude:yes/no
|
||||||
|
*/
|
||||||
|
function parseModuleInput(line) {
|
||||||
|
const parts = {};
|
||||||
|
line.split('|').forEach(part => {
|
||||||
|
const [key, value] = part.split(':');
|
||||||
|
if (key && value !== undefined) {
|
||||||
|
parts[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main execute function
|
||||||
|
*/
|
||||||
|
async function execute(params) {
|
||||||
|
const { input, path: targetPath } = params;
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
// Mode 1: Process piped input from get_modules_by_depth
|
||||||
|
if (input) {
|
||||||
|
let lines;
|
||||||
|
|
||||||
|
// Check if input is JSON (from ccw tool exec output)
|
||||||
|
if (typeof input === 'string' && input.trim().startsWith('{')) {
|
||||||
|
try {
|
||||||
|
const jsonInput = JSON.parse(input);
|
||||||
|
// Handle output from get_modules_by_depth tool (wrapped in result)
|
||||||
|
const output = jsonInput.result?.output || jsonInput.output;
|
||||||
|
if (output) {
|
||||||
|
lines = output.split('\n');
|
||||||
|
} else {
|
||||||
|
lines = [input];
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not JSON, treat as line-delimited text
|
||||||
|
lines = input.split('\n');
|
||||||
|
}
|
||||||
|
} else if (Array.isArray(input)) {
|
||||||
|
lines = input;
|
||||||
|
} else {
|
||||||
|
lines = input.split('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!line.trim()) continue;
|
||||||
|
|
||||||
|
const parsed = parseModuleInput(line);
|
||||||
|
const folderPath = parsed.path;
|
||||||
|
|
||||||
|
if (!folderPath) continue;
|
||||||
|
|
||||||
|
const basePath = targetPath ? resolve(process.cwd(), targetPath) : process.cwd();
|
||||||
|
const fullPath = resolve(basePath, folderPath);
|
||||||
|
|
||||||
|
if (!existsSync(fullPath) || !statSync(fullPath).isDirectory()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const classification = classifyFolder(fullPath);
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
path: folderPath,
|
||||||
|
type: classification.type,
|
||||||
|
code_files: classification.codeFiles,
|
||||||
|
subdirs: classification.subdirs
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Mode 2: Classify a single directory
|
||||||
|
else if (targetPath) {
|
||||||
|
const fullPath = resolve(process.cwd(), targetPath);
|
||||||
|
|
||||||
|
if (!existsSync(fullPath)) {
|
||||||
|
throw new Error(`Directory not found: ${fullPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!statSync(fullPath).isDirectory()) {
|
||||||
|
throw new Error(`Not a directory: ${fullPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const classification = classifyFolder(fullPath);
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
path: targetPath,
|
||||||
|
type: classification.type,
|
||||||
|
code_files: classification.codeFiles,
|
||||||
|
subdirs: classification.subdirs
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error('Either "input" or "path" parameter is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format output
|
||||||
|
const output = results.map(r =>
|
||||||
|
`${r.path}|${r.type}|code:${r.code_files}|dirs:${r.subdirs}`
|
||||||
|
).join('\n');
|
||||||
|
|
||||||
|
return {
|
||||||
|
total: results.length,
|
||||||
|
by_type: {
|
||||||
|
code: results.filter(r => r.type === 'code').length,
|
||||||
|
navigation: results.filter(r => r.type === 'navigation').length,
|
||||||
|
skip: results.filter(r => r.type === 'skip').length
|
||||||
|
},
|
||||||
|
results,
|
||||||
|
output
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool Definition
|
||||||
|
*/
|
||||||
|
export const classifyFoldersTool = {
|
||||||
|
name: 'classify_folders',
|
||||||
|
description: `Classify folders by type for documentation generation.
|
||||||
|
Types:
|
||||||
|
- code: Contains code files (generates API.md + README.md)
|
||||||
|
- navigation: Contains subdirectories only (generates README.md only)
|
||||||
|
- skip: Empty or no relevant content
|
||||||
|
|
||||||
|
Input: Either piped output from get_modules_by_depth or a single directory path.`,
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
input: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Piped input from get_modules_by_depth (one module per line)'
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Single directory path to classify'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: []
|
||||||
|
},
|
||||||
|
execute
|
||||||
|
};
|
||||||
250
ccw/src/tools/convert-tokens-to-css.js
Normal file
250
ccw/src/tools/convert-tokens-to-css.js
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
/**
|
||||||
|
* Convert Tokens to CSS Tool
|
||||||
|
* Transform design-tokens.json to CSS custom properties
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate Google Fonts import URL
|
||||||
|
*/
|
||||||
|
function generateFontImport(fonts) {
|
||||||
|
if (!fonts || typeof fonts !== 'object') return '';
|
||||||
|
|
||||||
|
const fontParams = [];
|
||||||
|
const processedFonts = new Set();
|
||||||
|
|
||||||
|
// Extract font families from typography.font_family
|
||||||
|
Object.values(fonts).forEach(fontValue => {
|
||||||
|
if (typeof fontValue !== 'string') return;
|
||||||
|
|
||||||
|
// Get the primary font (before comma)
|
||||||
|
const primaryFont = fontValue.split(',')[0].trim().replace(/['"]/g, '');
|
||||||
|
|
||||||
|
// Skip system fonts
|
||||||
|
const systemFonts = ['system-ui', 'sans-serif', 'serif', 'monospace', 'cursive', 'fantasy'];
|
||||||
|
if (systemFonts.includes(primaryFont.toLowerCase())) return;
|
||||||
|
if (processedFonts.has(primaryFont)) return;
|
||||||
|
|
||||||
|
processedFonts.add(primaryFont);
|
||||||
|
|
||||||
|
// URL encode font name
|
||||||
|
const encodedFont = primaryFont.replace(/ /g, '+');
|
||||||
|
|
||||||
|
// Special handling for common fonts
|
||||||
|
const specialFonts = {
|
||||||
|
'Comic Neue': 'Comic+Neue:wght@300;400;700',
|
||||||
|
'Patrick Hand': 'Patrick+Hand:wght@400;700',
|
||||||
|
'Caveat': 'Caveat:wght@400;700',
|
||||||
|
'Dancing Script': 'Dancing+Script:wght@400;700'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (specialFonts[primaryFont]) {
|
||||||
|
fontParams.push(`family=${specialFonts[primaryFont]}`);
|
||||||
|
} else {
|
||||||
|
fontParams.push(`family=${encodedFont}:wght@400;500;600;700`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fontParams.length === 0) return '';
|
||||||
|
|
||||||
|
return `@import url('https://fonts.googleapis.com/css2?${fontParams.join('&')}&display=swap');`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate CSS variables for a category
|
||||||
|
*/
|
||||||
|
function generateCssVars(prefix, obj, indent = ' ') {
|
||||||
|
if (!obj || typeof obj !== 'object') return [];
|
||||||
|
|
||||||
|
const lines = [];
|
||||||
|
Object.entries(obj).forEach(([key, value]) => {
|
||||||
|
const varName = `--${prefix}-${key.replace(/_/g, '-')}`;
|
||||||
|
lines.push(`${indent}${varName}: ${value};`);
|
||||||
|
});
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main execute function
|
||||||
|
*/
|
||||||
|
async function execute(params) {
|
||||||
|
const { input } = params;
|
||||||
|
|
||||||
|
if (!input) {
|
||||||
|
throw new Error('Parameter "input" (design tokens JSON) is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse input
|
||||||
|
let tokens;
|
||||||
|
try {
|
||||||
|
tokens = typeof input === 'string' ? JSON.parse(input) : input;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`Invalid JSON input: ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = [];
|
||||||
|
|
||||||
|
// Header
|
||||||
|
const styleName = tokens.meta?.name || 'Design Tokens';
|
||||||
|
lines.push('/* ========================================');
|
||||||
|
lines.push(` Design Tokens: ${styleName}`);
|
||||||
|
lines.push(' Auto-generated from design-tokens.json');
|
||||||
|
lines.push(' ======================================== */');
|
||||||
|
lines.push('');
|
||||||
|
|
||||||
|
// Google Fonts import
|
||||||
|
if (tokens.typography?.font_family) {
|
||||||
|
const fontImport = generateFontImport(tokens.typography.font_family);
|
||||||
|
if (fontImport) {
|
||||||
|
lines.push('/* Import Web Fonts */');
|
||||||
|
lines.push(fontImport);
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSS Custom Properties
|
||||||
|
lines.push(':root {');
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
if (tokens.colors) {
|
||||||
|
if (tokens.colors.brand) {
|
||||||
|
lines.push(' /* Colors - Brand */');
|
||||||
|
lines.push(...generateCssVars('color-brand', tokens.colors.brand));
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
if (tokens.colors.surface) {
|
||||||
|
lines.push(' /* Colors - Surface */');
|
||||||
|
lines.push(...generateCssVars('color-surface', tokens.colors.surface));
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
if (tokens.colors.semantic) {
|
||||||
|
lines.push(' /* Colors - Semantic */');
|
||||||
|
lines.push(...generateCssVars('color-semantic', tokens.colors.semantic));
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
if (tokens.colors.text) {
|
||||||
|
lines.push(' /* Colors - Text */');
|
||||||
|
lines.push(...generateCssVars('color-text', tokens.colors.text));
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
if (tokens.colors.border) {
|
||||||
|
lines.push(' /* Colors - Border */');
|
||||||
|
lines.push(...generateCssVars('color-border', tokens.colors.border));
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Typography
|
||||||
|
if (tokens.typography) {
|
||||||
|
if (tokens.typography.font_family) {
|
||||||
|
lines.push(' /* Typography - Font Family */');
|
||||||
|
lines.push(...generateCssVars('font-family', tokens.typography.font_family));
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
if (tokens.typography.font_size) {
|
||||||
|
lines.push(' /* Typography - Font Size */');
|
||||||
|
lines.push(...generateCssVars('font-size', tokens.typography.font_size));
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
if (tokens.typography.font_weight) {
|
||||||
|
lines.push(' /* Typography - Font Weight */');
|
||||||
|
lines.push(...generateCssVars('font-weight', tokens.typography.font_weight));
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
if (tokens.typography.line_height) {
|
||||||
|
lines.push(' /* Typography - Line Height */');
|
||||||
|
lines.push(...generateCssVars('line-height', tokens.typography.line_height));
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
if (tokens.typography.letter_spacing) {
|
||||||
|
lines.push(' /* Typography - Letter Spacing */');
|
||||||
|
lines.push(...generateCssVars('letter-spacing', tokens.typography.letter_spacing));
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spacing
|
||||||
|
if (tokens.spacing) {
|
||||||
|
lines.push(' /* Spacing */');
|
||||||
|
lines.push(...generateCssVars('spacing', tokens.spacing));
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Border Radius
|
||||||
|
if (tokens.border_radius) {
|
||||||
|
lines.push(' /* Border Radius */');
|
||||||
|
lines.push(...generateCssVars('border-radius', tokens.border_radius));
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shadows
|
||||||
|
if (tokens.shadows) {
|
||||||
|
lines.push(' /* Shadows */');
|
||||||
|
lines.push(...generateCssVars('shadow', tokens.shadows));
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Breakpoints
|
||||||
|
if (tokens.breakpoints) {
|
||||||
|
lines.push(' /* Breakpoints */');
|
||||||
|
lines.push(...generateCssVars('breakpoint', tokens.breakpoints));
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push('}');
|
||||||
|
lines.push('');
|
||||||
|
|
||||||
|
// Global Font Application
|
||||||
|
lines.push('/* ========================================');
|
||||||
|
lines.push(' Global Font Application');
|
||||||
|
lines.push(' ======================================== */');
|
||||||
|
lines.push('');
|
||||||
|
lines.push('body {');
|
||||||
|
lines.push(' font-family: var(--font-family-body);');
|
||||||
|
lines.push(' font-size: var(--font-size-base);');
|
||||||
|
lines.push(' line-height: var(--line-height-normal);');
|
||||||
|
lines.push(' color: var(--color-text-primary);');
|
||||||
|
lines.push(' background-color: var(--color-surface-background);');
|
||||||
|
lines.push('}');
|
||||||
|
lines.push('');
|
||||||
|
lines.push('h1, h2, h3, h4, h5, h6, legend {');
|
||||||
|
lines.push(' font-family: var(--font-family-heading);');
|
||||||
|
lines.push('}');
|
||||||
|
lines.push('');
|
||||||
|
lines.push('/* Reset default margins for better control */');
|
||||||
|
lines.push('* {');
|
||||||
|
lines.push(' margin: 0;');
|
||||||
|
lines.push(' padding: 0;');
|
||||||
|
lines.push(' box-sizing: border-box;');
|
||||||
|
lines.push('}');
|
||||||
|
|
||||||
|
const css = lines.join('\n');
|
||||||
|
|
||||||
|
return {
|
||||||
|
style_name: styleName,
|
||||||
|
lines_count: lines.length,
|
||||||
|
css
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool Definition
|
||||||
|
*/
|
||||||
|
export const convertTokensToCssTool = {
|
||||||
|
name: 'convert_tokens_to_css',
|
||||||
|
description: `Transform design-tokens.json to CSS custom properties.
|
||||||
|
Generates:
|
||||||
|
- Google Fonts @import URL
|
||||||
|
- CSS custom properties for colors, typography, spacing, etc.
|
||||||
|
- Global font application rules`,
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
input: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Design tokens JSON string or object'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['input']
|
||||||
|
},
|
||||||
|
execute
|
||||||
|
};
|
||||||
288
ccw/src/tools/detect-changed-modules.js
Normal file
288
ccw/src/tools/detect-changed-modules.js
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
/**
|
||||||
|
* Detect Changed Modules Tool
|
||||||
|
* Find modules affected by git changes or recent modifications
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readdirSync, statSync, existsSync, readFileSync } from 'fs';
|
||||||
|
import { join, resolve, dirname, extname, relative } from 'path';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
|
||||||
|
// Source file extensions to track
|
||||||
|
const SOURCE_EXTENSIONS = [
|
||||||
|
'.md', '.js', '.ts', '.jsx', '.tsx',
|
||||||
|
'.py', '.go', '.rs', '.java', '.cpp', '.c', '.h',
|
||||||
|
'.sh', '.ps1', '.json', '.yaml', '.yml'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Directories to exclude
|
||||||
|
const EXCLUDE_DIRS = [
|
||||||
|
'.git', '__pycache__', 'node_modules', '.venv', 'venv', 'env',
|
||||||
|
'dist', 'build', '.cache', '.pytest_cache', '.mypy_cache',
|
||||||
|
'coverage', '.nyc_output', 'logs', 'tmp', 'temp'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if git is available and we're in a repo
|
||||||
|
*/
|
||||||
|
function isGitRepo(basePath) {
|
||||||
|
try {
|
||||||
|
execSync('git rev-parse --git-dir', { cwd: basePath, stdio: 'pipe' });
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get changed files from git
|
||||||
|
*/
|
||||||
|
function getGitChangedFiles(basePath) {
|
||||||
|
try {
|
||||||
|
// Get staged + unstaged changes
|
||||||
|
let output = execSync('git diff --name-only HEAD 2>/dev/null', {
|
||||||
|
cwd: basePath,
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe']
|
||||||
|
}).trim();
|
||||||
|
|
||||||
|
const cachedOutput = execSync('git diff --name-only --cached 2>/dev/null', {
|
||||||
|
cwd: basePath,
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe']
|
||||||
|
}).trim();
|
||||||
|
|
||||||
|
if (cachedOutput) {
|
||||||
|
output = output ? `${output}\n${cachedOutput}` : cachedOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no working changes, check last commit
|
||||||
|
if (!output) {
|
||||||
|
output = execSync('git diff --name-only HEAD~1 HEAD 2>/dev/null', {
|
||||||
|
cwd: basePath,
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe']
|
||||||
|
}).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return output ? output.split('\n').filter(f => f.trim()) : [];
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find recently modified files (fallback when no git changes)
|
||||||
|
*/
|
||||||
|
function findRecentlyModified(basePath, hoursAgo = 24) {
|
||||||
|
const results = [];
|
||||||
|
const cutoffTime = Date.now() - (hoursAgo * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
function scan(dirPath) {
|
||||||
|
try {
|
||||||
|
const entries = readdirSync(dirPath, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
if (EXCLUDE_DIRS.includes(entry.name)) continue;
|
||||||
|
scan(join(dirPath, entry.name));
|
||||||
|
} else if (entry.isFile()) {
|
||||||
|
const ext = extname(entry.name).toLowerCase();
|
||||||
|
if (!SOURCE_EXTENSIONS.includes(ext)) continue;
|
||||||
|
|
||||||
|
const fullPath = join(dirPath, entry.name);
|
||||||
|
try {
|
||||||
|
const stat = statSync(fullPath);
|
||||||
|
if (stat.mtimeMs > cutoffTime) {
|
||||||
|
results.push(relative(basePath, fullPath));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Skip files we can't stat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore permission errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scan(basePath);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract unique parent directories from file list
|
||||||
|
*/
|
||||||
|
function extractDirectories(files, basePath) {
|
||||||
|
const dirs = new Set();
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const dir = dirname(file);
|
||||||
|
if (dir === '.' || dir === '') {
|
||||||
|
dirs.add('.');
|
||||||
|
} else {
|
||||||
|
dirs.add('./' + dir.replace(/\\/g, '/'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(dirs).sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count files in directory
|
||||||
|
*/
|
||||||
|
function countFiles(dirPath) {
|
||||||
|
try {
|
||||||
|
const entries = readdirSync(dirPath, { withFileTypes: true });
|
||||||
|
return entries.filter(e => e.isFile()).length;
|
||||||
|
} catch (e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file types in directory
|
||||||
|
*/
|
||||||
|
function getFileTypes(dirPath) {
|
||||||
|
const types = new Set();
|
||||||
|
try {
|
||||||
|
const entries = readdirSync(dirPath, { withFileTypes: true });
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isFile()) {
|
||||||
|
const ext = extname(entry.name).slice(1);
|
||||||
|
if (ext) types.add(ext);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
return Array.from(types);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main execute function
|
||||||
|
*/
|
||||||
|
async function execute(params) {
|
||||||
|
const { format = 'paths', path: targetPath = '.' } = params;
|
||||||
|
|
||||||
|
const basePath = resolve(process.cwd(), targetPath);
|
||||||
|
|
||||||
|
if (!existsSync(basePath)) {
|
||||||
|
throw new Error(`Directory not found: ${basePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get changed files
|
||||||
|
let changedFiles = [];
|
||||||
|
let changeSource = 'none';
|
||||||
|
|
||||||
|
if (isGitRepo(basePath)) {
|
||||||
|
changedFiles = getGitChangedFiles(basePath);
|
||||||
|
changeSource = changedFiles.length > 0 ? 'git' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to recently modified files
|
||||||
|
if (changedFiles.length === 0) {
|
||||||
|
changedFiles = findRecentlyModified(basePath);
|
||||||
|
changeSource = changedFiles.length > 0 ? 'mtime' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract affected directories
|
||||||
|
const affectedDirs = extractDirectories(changedFiles, basePath);
|
||||||
|
|
||||||
|
// Format output
|
||||||
|
let output;
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
for (const dir of affectedDirs) {
|
||||||
|
const fullPath = dir === '.' ? basePath : resolve(basePath, dir);
|
||||||
|
if (!existsSync(fullPath) || !statSync(fullPath).isDirectory()) continue;
|
||||||
|
|
||||||
|
const fileCount = countFiles(fullPath);
|
||||||
|
const types = getFileTypes(fullPath);
|
||||||
|
const depth = dir === '.' ? 0 : (dir.match(/\//g) || []).length;
|
||||||
|
const hasClaude = existsSync(join(fullPath, 'CLAUDE.md'));
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
depth,
|
||||||
|
path: dir,
|
||||||
|
files: fileCount,
|
||||||
|
types,
|
||||||
|
has_claude: hasClaude
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (format) {
|
||||||
|
case 'list':
|
||||||
|
output = results.map(r =>
|
||||||
|
`depth:${r.depth}|path:${r.path}|files:${r.files}|types:[${r.types.join(',')}]|has_claude:${r.has_claude ? 'yes' : 'no'}|status:changed`
|
||||||
|
).join('\n');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'grouped':
|
||||||
|
const maxDepth = results.length > 0 ? Math.max(...results.map(r => r.depth)) : 0;
|
||||||
|
const lines = ['Affected modules by changes:'];
|
||||||
|
|
||||||
|
for (let d = 0; d <= maxDepth; d++) {
|
||||||
|
const atDepth = results.filter(r => r.depth === d);
|
||||||
|
if (atDepth.length > 0) {
|
||||||
|
lines.push(` Depth ${d}:`);
|
||||||
|
atDepth.forEach(r => {
|
||||||
|
const claudeIndicator = r.has_claude ? ' [OK]' : '';
|
||||||
|
lines.push(` - ${r.path}${claudeIndicator} (changed)`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
lines.push(' No recent changes detected');
|
||||||
|
}
|
||||||
|
|
||||||
|
output = lines.join('\n');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'paths':
|
||||||
|
default:
|
||||||
|
output = affectedDirs.join('\n');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
format,
|
||||||
|
change_source: changeSource,
|
||||||
|
changed_files_count: changedFiles.length,
|
||||||
|
affected_modules_count: results.length,
|
||||||
|
results,
|
||||||
|
output
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool Definition
|
||||||
|
*/
|
||||||
|
export const detectChangedModulesTool = {
|
||||||
|
name: 'detect_changed_modules',
|
||||||
|
description: `Detect modules affected by git changes or recent file modifications.
|
||||||
|
Features:
|
||||||
|
- Git-aware: detects staged, unstaged, or last commit changes
|
||||||
|
- Fallback: finds files modified in last 24 hours
|
||||||
|
- Respects .gitignore patterns
|
||||||
|
|
||||||
|
Output formats: list, grouped, paths (default)`,
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
format: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['list', 'grouped', 'paths'],
|
||||||
|
description: 'Output format (default: paths)',
|
||||||
|
default: 'paths'
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Target directory path (default: current directory)',
|
||||||
|
default: '.'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: []
|
||||||
|
},
|
||||||
|
execute
|
||||||
|
};
|
||||||
134
ccw/src/tools/discover-design-files.js
Normal file
134
ccw/src/tools/discover-design-files.js
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
/**
|
||||||
|
* Discover Design Files Tool
|
||||||
|
* Find CSS/JS/HTML design-related files and output JSON
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readdirSync, statSync, existsSync, writeFileSync } from 'fs';
|
||||||
|
import { join, resolve, relative, extname } from 'path';
|
||||||
|
|
||||||
|
// Directories to exclude
|
||||||
|
const EXCLUDE_DIRS = [
|
||||||
|
'node_modules', 'dist', '.git', 'build', 'coverage',
|
||||||
|
'.cache', '.next', '.nuxt', '__pycache__', '.venv'
|
||||||
|
];
|
||||||
|
|
||||||
|
// File type patterns
|
||||||
|
const FILE_PATTERNS = {
|
||||||
|
css: ['.css', '.scss', '.sass', '.less', '.styl'],
|
||||||
|
js: ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs', '.vue', '.svelte'],
|
||||||
|
html: ['.html', '.htm']
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find files matching extensions recursively
|
||||||
|
*/
|
||||||
|
function findFiles(basePath, extensions) {
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
function scan(dirPath) {
|
||||||
|
try {
|
||||||
|
const entries = readdirSync(dirPath, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
if (EXCLUDE_DIRS.includes(entry.name)) continue;
|
||||||
|
scan(join(dirPath, entry.name));
|
||||||
|
} else if (entry.isFile()) {
|
||||||
|
const ext = extname(entry.name).toLowerCase();
|
||||||
|
if (extensions.includes(ext)) {
|
||||||
|
results.push(relative(basePath, join(dirPath, entry.name)).replace(/\\/g, '/'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore permission errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scan(basePath);
|
||||||
|
return results.sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main execute function
|
||||||
|
*/
|
||||||
|
async function execute(params) {
|
||||||
|
const { sourceDir = '.', outputPath } = params;
|
||||||
|
|
||||||
|
const basePath = resolve(process.cwd(), sourceDir);
|
||||||
|
|
||||||
|
if (!existsSync(basePath)) {
|
||||||
|
throw new Error(`Directory not found: ${basePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!statSync(basePath).isDirectory()) {
|
||||||
|
throw new Error(`Not a directory: ${basePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find files by type
|
||||||
|
const cssFiles = findFiles(basePath, FILE_PATTERNS.css);
|
||||||
|
const jsFiles = findFiles(basePath, FILE_PATTERNS.js);
|
||||||
|
const htmlFiles = findFiles(basePath, FILE_PATTERNS.html);
|
||||||
|
|
||||||
|
// Build result
|
||||||
|
const result = {
|
||||||
|
discovery_time: new Date().toISOString(),
|
||||||
|
source_directory: basePath,
|
||||||
|
file_types: {
|
||||||
|
css: {
|
||||||
|
count: cssFiles.length,
|
||||||
|
files: cssFiles
|
||||||
|
},
|
||||||
|
js: {
|
||||||
|
count: jsFiles.length,
|
||||||
|
files: jsFiles
|
||||||
|
},
|
||||||
|
html: {
|
||||||
|
count: htmlFiles.length,
|
||||||
|
files: htmlFiles
|
||||||
|
}
|
||||||
|
},
|
||||||
|
total_files: cssFiles.length + jsFiles.length + htmlFiles.length
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write to file if outputPath specified
|
||||||
|
if (outputPath) {
|
||||||
|
const outPath = resolve(process.cwd(), outputPath);
|
||||||
|
writeFileSync(outPath, JSON.stringify(result, null, 2), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
css_count: cssFiles.length,
|
||||||
|
js_count: jsFiles.length,
|
||||||
|
html_count: htmlFiles.length,
|
||||||
|
total_files: result.total_files,
|
||||||
|
output_path: outputPath || null,
|
||||||
|
result
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool Definition
|
||||||
|
*/
|
||||||
|
export const discoverDesignFilesTool = {
|
||||||
|
name: 'discover_design_files',
|
||||||
|
description: `Discover CSS/JS/HTML design-related files in a directory.
|
||||||
|
Scans recursively and excludes common build/cache directories.
|
||||||
|
Returns JSON with file discovery results.`,
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
sourceDir: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Source directory to scan (default: current directory)',
|
||||||
|
default: '.'
|
||||||
|
},
|
||||||
|
outputPath: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Optional path to write JSON output file'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: []
|
||||||
|
},
|
||||||
|
execute
|
||||||
|
};
|
||||||
238
ccw/src/tools/edit-file.js
Normal file
238
ccw/src/tools/edit-file.js
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
/**
|
||||||
|
* Edit File Tool - AI-focused file editing
|
||||||
|
* Two complementary modes:
|
||||||
|
* - update: Content-driven text replacement (AI primary use)
|
||||||
|
* - line: Position-driven line operations (precise control)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
||||||
|
import { resolve, isAbsolute } from 'path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve file path and read content
|
||||||
|
* @param {string} filePath - Path to file
|
||||||
|
* @returns {{resolvedPath: string, content: string}}
|
||||||
|
*/
|
||||||
|
function readFile(filePath) {
|
||||||
|
const resolvedPath = isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath);
|
||||||
|
|
||||||
|
if (!existsSync(resolvedPath)) {
|
||||||
|
throw new Error(`File not found: ${resolvedPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = readFileSync(resolvedPath, 'utf8');
|
||||||
|
return { resolvedPath, content };
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to read file: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write content to file
|
||||||
|
* @param {string} filePath - Path to file
|
||||||
|
* @param {string} content - Content to write
|
||||||
|
*/
|
||||||
|
function writeFile(filePath, content) {
|
||||||
|
try {
|
||||||
|
writeFileSync(filePath, content, 'utf8');
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to write file: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mode: update - Simple text replacement
|
||||||
|
* Auto-adapts line endings (CRLF/LF)
|
||||||
|
*/
|
||||||
|
function executeUpdateMode(content, params) {
|
||||||
|
const { oldText, newText } = params;
|
||||||
|
|
||||||
|
if (!oldText) throw new Error('Parameter "oldText" is required for update mode');
|
||||||
|
if (newText === undefined) throw new Error('Parameter "newText" is required for update mode');
|
||||||
|
|
||||||
|
// Detect original line ending
|
||||||
|
const hasCRLF = content.includes('\r\n');
|
||||||
|
|
||||||
|
// Normalize to LF for matching
|
||||||
|
const normalize = (str) => str.replace(/\r\n/g, '\n');
|
||||||
|
const normalizedContent = normalize(content);
|
||||||
|
const normalizedOld = normalize(oldText);
|
||||||
|
const normalizedNew = normalize(newText);
|
||||||
|
|
||||||
|
let newContent = normalizedContent;
|
||||||
|
let status = 'not found';
|
||||||
|
|
||||||
|
if (newContent.includes(normalizedOld)) {
|
||||||
|
newContent = newContent.replace(normalizedOld, normalizedNew);
|
||||||
|
status = 'replaced';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore original line ending
|
||||||
|
if (hasCRLF) {
|
||||||
|
newContent = newContent.replace(/\n/g, '\r\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: newContent,
|
||||||
|
modified: content !== newContent,
|
||||||
|
status,
|
||||||
|
message: status === 'replaced' ? 'Text replaced successfully' : 'oldText not found in file'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mode: line - Line-based operations
|
||||||
|
* Operations: insert_before, insert_after, replace, delete
|
||||||
|
*/
|
||||||
|
function executeLineMode(content, params) {
|
||||||
|
const { operation, line, text, end_line } = params;
|
||||||
|
|
||||||
|
if (!operation) throw new Error('Parameter "operation" is required for line mode');
|
||||||
|
if (line === undefined) throw new Error('Parameter "line" is required for line mode');
|
||||||
|
|
||||||
|
const lines = content.split('\n');
|
||||||
|
const lineIndex = line - 1; // Convert to 0-based
|
||||||
|
|
||||||
|
if (lineIndex < 0 || lineIndex >= lines.length) {
|
||||||
|
throw new Error(`Line ${line} out of range (1-${lines.length})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let newLines = [...lines];
|
||||||
|
let message = '';
|
||||||
|
|
||||||
|
switch (operation) {
|
||||||
|
case 'insert_before':
|
||||||
|
if (text === undefined) throw new Error('Parameter "text" is required for insert_before');
|
||||||
|
newLines.splice(lineIndex, 0, text);
|
||||||
|
message = `Inserted before line ${line}`;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'insert_after':
|
||||||
|
if (text === undefined) throw new Error('Parameter "text" is required for insert_after');
|
||||||
|
newLines.splice(lineIndex + 1, 0, text);
|
||||||
|
message = `Inserted after line ${line}`;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'replace':
|
||||||
|
if (text === undefined) throw new Error('Parameter "text" is required for replace');
|
||||||
|
const endIdx = end_line ? end_line - 1 : lineIndex;
|
||||||
|
if (endIdx < lineIndex || endIdx >= lines.length) {
|
||||||
|
throw new Error(`end_line ${end_line} is invalid`);
|
||||||
|
}
|
||||||
|
const deleteCount = endIdx - lineIndex + 1;
|
||||||
|
newLines.splice(lineIndex, deleteCount, text);
|
||||||
|
message = end_line ? `Replaced lines ${line}-${end_line}` : `Replaced line ${line}`;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'delete':
|
||||||
|
const endDelete = end_line ? end_line - 1 : lineIndex;
|
||||||
|
if (endDelete < lineIndex || endDelete >= lines.length) {
|
||||||
|
throw new Error(`end_line ${end_line} is invalid`);
|
||||||
|
}
|
||||||
|
const count = endDelete - lineIndex + 1;
|
||||||
|
newLines.splice(lineIndex, count);
|
||||||
|
message = end_line ? `Deleted lines ${line}-${end_line}` : `Deleted line ${line}`;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown operation: ${operation}. Valid: insert_before, insert_after, replace, delete`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newContent = newLines.join('\n');
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: newContent,
|
||||||
|
modified: content !== newContent,
|
||||||
|
operation,
|
||||||
|
line,
|
||||||
|
end_line,
|
||||||
|
message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main execute function - routes to appropriate mode
|
||||||
|
*/
|
||||||
|
async function execute(params) {
|
||||||
|
const { path: filePath, mode = 'update' } = params;
|
||||||
|
|
||||||
|
if (!filePath) throw new Error('Parameter "path" is required');
|
||||||
|
|
||||||
|
const { resolvedPath, content } = readFile(filePath);
|
||||||
|
|
||||||
|
let result;
|
||||||
|
switch (mode) {
|
||||||
|
case 'update':
|
||||||
|
result = executeUpdateMode(content, params);
|
||||||
|
break;
|
||||||
|
case 'line':
|
||||||
|
result = executeLineMode(content, params);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown mode: ${mode}. Valid modes: update, line`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write if modified
|
||||||
|
if (result.modified) {
|
||||||
|
writeFile(resolvedPath, result.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove content from result (don't return file content)
|
||||||
|
const { content: _, ...output } = result;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit File Tool Definition
|
||||||
|
*/
|
||||||
|
export const editFileTool = {
|
||||||
|
name: 'edit_file',
|
||||||
|
description: `Update file with two modes:
|
||||||
|
- update: Replace oldText with newText (default)
|
||||||
|
- line: Position-driven line operations`,
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
path: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Path to the file to modify'
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['update', 'line'],
|
||||||
|
description: 'Edit mode (default: update)',
|
||||||
|
default: 'update'
|
||||||
|
},
|
||||||
|
// Update mode params
|
||||||
|
oldText: {
|
||||||
|
type: 'string',
|
||||||
|
description: '[update mode] Text to find and replace'
|
||||||
|
},
|
||||||
|
newText: {
|
||||||
|
type: 'string',
|
||||||
|
description: '[update mode] Replacement text'
|
||||||
|
},
|
||||||
|
// Line mode params
|
||||||
|
operation: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['insert_before', 'insert_after', 'replace', 'delete'],
|
||||||
|
description: '[line mode] Line operation type'
|
||||||
|
},
|
||||||
|
line: {
|
||||||
|
type: 'number',
|
||||||
|
description: '[line mode] Line number (1-based)'
|
||||||
|
},
|
||||||
|
end_line: {
|
||||||
|
type: 'number',
|
||||||
|
description: '[line mode] End line for range operations'
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
type: 'string',
|
||||||
|
description: '[line mode] Text for insert/replace operations'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['path']
|
||||||
|
},
|
||||||
|
execute
|
||||||
|
};
|
||||||
368
ccw/src/tools/generate-module-docs.js
Normal file
368
ccw/src/tools/generate-module-docs.js
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
/**
|
||||||
|
* Generate Module Docs Tool
|
||||||
|
* Generate documentation for modules and projects with multiple strategies
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readdirSync, statSync, existsSync, readFileSync, mkdirSync } from 'fs';
|
||||||
|
import { join, resolve, basename, extname, relative } from 'path';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
|
||||||
|
// Directories to exclude
|
||||||
|
const EXCLUDE_DIRS = [
|
||||||
|
'.git', '__pycache__', 'node_modules', '.venv', 'venv', 'env',
|
||||||
|
'dist', 'build', '.cache', '.pytest_cache', '.mypy_cache',
|
||||||
|
'coverage', '.nyc_output', 'logs', 'tmp', 'temp', '.workflow'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Code file extensions
|
||||||
|
const CODE_EXTENSIONS = [
|
||||||
|
'.ts', '.tsx', '.js', '.jsx', '.py', '.sh', '.go', '.rs'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Default models for each tool
|
||||||
|
const DEFAULT_MODELS = {
|
||||||
|
gemini: 'gemini-2.5-flash',
|
||||||
|
qwen: 'coder-model',
|
||||||
|
codex: 'gpt5-codex'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Template paths
|
||||||
|
const TEMPLATE_BASE = '.claude/workflows/cli-templates/prompts/documentation';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect folder type (code vs navigation)
|
||||||
|
*/
|
||||||
|
function detectFolderType(dirPath) {
|
||||||
|
try {
|
||||||
|
const entries = readdirSync(dirPath, { withFileTypes: true });
|
||||||
|
const codeFiles = entries.filter(e => {
|
||||||
|
if (!e.isFile()) return false;
|
||||||
|
const ext = extname(e.name).toLowerCase();
|
||||||
|
return CODE_EXTENSIONS.includes(ext);
|
||||||
|
});
|
||||||
|
return codeFiles.length > 0 ? 'code' : 'navigation';
|
||||||
|
} catch (e) {
|
||||||
|
return 'navigation';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count files in directory
|
||||||
|
*/
|
||||||
|
function countFiles(dirPath) {
|
||||||
|
try {
|
||||||
|
const entries = readdirSync(dirPath, { withFileTypes: true });
|
||||||
|
return entries.filter(e => e.isFile() && !e.name.startsWith('.')).length;
|
||||||
|
} catch (e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate output path
|
||||||
|
*/
|
||||||
|
function calculateOutputPath(sourcePath, projectName, projectRoot) {
|
||||||
|
const absSource = resolve(sourcePath);
|
||||||
|
const normRoot = resolve(projectRoot);
|
||||||
|
let relPath = relative(normRoot, absSource);
|
||||||
|
relPath = relPath.replace(/\\/g, '/');
|
||||||
|
|
||||||
|
return join('.workflow', 'docs', projectName, relPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load template content
|
||||||
|
*/
|
||||||
|
function loadTemplate(templateName) {
|
||||||
|
const homePath = process.env.HOME || process.env.USERPROFILE;
|
||||||
|
const templatePath = join(homePath, TEMPLATE_BASE, `${templateName}.txt`);
|
||||||
|
|
||||||
|
if (existsSync(templatePath)) {
|
||||||
|
return readFileSync(templatePath, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback templates
|
||||||
|
const fallbacks = {
|
||||||
|
'api': 'Generate API documentation with function signatures, parameters, return values, and usage examples.',
|
||||||
|
'module-readme': 'Generate README documentation with purpose, usage, configuration, and examples.',
|
||||||
|
'folder-navigation': 'Generate navigation README with overview of subdirectories and their purposes.',
|
||||||
|
'project-readme': 'Generate project README with overview, installation, usage, and configuration.',
|
||||||
|
'project-architecture': 'Generate ARCHITECTURE.md with system design, components, and data flow.'
|
||||||
|
};
|
||||||
|
|
||||||
|
return fallbacks[templateName] || 'Generate comprehensive documentation.';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build CLI command
|
||||||
|
*/
|
||||||
|
function buildCliCommand(tool, prompt, model) {
|
||||||
|
const escapedPrompt = prompt.replace(/"/g, '\\"');
|
||||||
|
|
||||||
|
switch (tool) {
|
||||||
|
case 'qwen':
|
||||||
|
return model === 'coder-model'
|
||||||
|
? `qwen -p "${escapedPrompt}" --yolo`
|
||||||
|
: `qwen -p "${escapedPrompt}" -m "${model}" --yolo`;
|
||||||
|
case 'codex':
|
||||||
|
return `codex --full-auto exec "${escapedPrompt}" -m "${model}" --skip-git-repo-check -s danger-full-access`;
|
||||||
|
case 'gemini':
|
||||||
|
default:
|
||||||
|
return `gemini -p "${escapedPrompt}" -m "${model}" --yolo`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scan directory structure
|
||||||
|
*/
|
||||||
|
function scanDirectoryStructure(targetPath, strategy) {
|
||||||
|
const lines = [];
|
||||||
|
const dirName = basename(targetPath);
|
||||||
|
|
||||||
|
let totalFiles = 0;
|
||||||
|
let totalDirs = 0;
|
||||||
|
|
||||||
|
function countRecursive(dir) {
|
||||||
|
try {
|
||||||
|
const entries = readdirSync(dir, { withFileTypes: true });
|
||||||
|
entries.forEach(e => {
|
||||||
|
if (e.name.startsWith('.') || EXCLUDE_DIRS.includes(e.name)) return;
|
||||||
|
if (e.isFile()) totalFiles++;
|
||||||
|
else if (e.isDirectory()) {
|
||||||
|
totalDirs++;
|
||||||
|
countRecursive(join(dir, e.name));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
countRecursive(targetPath);
|
||||||
|
const folderType = detectFolderType(targetPath);
|
||||||
|
|
||||||
|
lines.push(`Directory: ${dirName}`);
|
||||||
|
lines.push(`Total files: ${totalFiles}`);
|
||||||
|
lines.push(`Total directories: ${totalDirs}`);
|
||||||
|
lines.push(`Folder type: ${folderType}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
info: lines.join('\n'),
|
||||||
|
folderType
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main execute function
|
||||||
|
*/
|
||||||
|
async function execute(params) {
|
||||||
|
const { strategy, sourcePath, projectName, tool = 'gemini', model } = params;
|
||||||
|
|
||||||
|
// Validate parameters
|
||||||
|
const validStrategies = ['full', 'single', 'project-readme', 'project-architecture', 'http-api'];
|
||||||
|
|
||||||
|
if (!strategy) {
|
||||||
|
throw new Error(`Parameter "strategy" is required. Valid: ${validStrategies.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validStrategies.includes(strategy)) {
|
||||||
|
throw new Error(`Invalid strategy '${strategy}'. Valid: ${validStrategies.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sourcePath) {
|
||||||
|
throw new Error('Parameter "sourcePath" is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!projectName) {
|
||||||
|
throw new Error('Parameter "projectName" is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetPath = resolve(process.cwd(), sourcePath);
|
||||||
|
|
||||||
|
if (!existsSync(targetPath)) {
|
||||||
|
throw new Error(`Directory not found: ${targetPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!statSync(targetPath).isDirectory()) {
|
||||||
|
throw new Error(`Not a directory: ${targetPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set model
|
||||||
|
const actualModel = model || DEFAULT_MODELS[tool] || DEFAULT_MODELS.gemini;
|
||||||
|
|
||||||
|
// Scan directory
|
||||||
|
const { info: structureInfo, folderType } = scanDirectoryStructure(targetPath, strategy);
|
||||||
|
|
||||||
|
// Calculate output path
|
||||||
|
const outputPath = calculateOutputPath(targetPath, projectName, process.cwd());
|
||||||
|
|
||||||
|
// Ensure output directory exists
|
||||||
|
mkdirSync(outputPath, { recursive: true });
|
||||||
|
|
||||||
|
// Build prompt based on strategy
|
||||||
|
let prompt;
|
||||||
|
let templateContent;
|
||||||
|
|
||||||
|
switch (strategy) {
|
||||||
|
case 'full':
|
||||||
|
case 'single':
|
||||||
|
if (folderType === 'code') {
|
||||||
|
templateContent = loadTemplate('api');
|
||||||
|
prompt = `Directory Structure Analysis:
|
||||||
|
${structureInfo}
|
||||||
|
|
||||||
|
Read: ${strategy === 'full' ? '@**/*' : '@*.ts @*.tsx @*.js @*.jsx @*.py @*.sh @*.md @*.json'}
|
||||||
|
|
||||||
|
Generate documentation files:
|
||||||
|
- API.md: Code API documentation
|
||||||
|
- README.md: Module overview and usage
|
||||||
|
|
||||||
|
Output directory: ${outputPath}
|
||||||
|
|
||||||
|
Template Guidelines:
|
||||||
|
${templateContent}`;
|
||||||
|
} else {
|
||||||
|
templateContent = loadTemplate('folder-navigation');
|
||||||
|
prompt = `Directory Structure Analysis:
|
||||||
|
${structureInfo}
|
||||||
|
|
||||||
|
Read: @*/API.md @*/README.md
|
||||||
|
|
||||||
|
Generate documentation file:
|
||||||
|
- README.md: Navigation overview of subdirectories
|
||||||
|
|
||||||
|
Output directory: ${outputPath}
|
||||||
|
|
||||||
|
Template Guidelines:
|
||||||
|
${templateContent}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'project-readme':
|
||||||
|
templateContent = loadTemplate('project-readme');
|
||||||
|
prompt = `Read all module documentation:
|
||||||
|
@.workflow/docs/${projectName}/**/API.md
|
||||||
|
@.workflow/docs/${projectName}/**/README.md
|
||||||
|
|
||||||
|
Generate project-level documentation:
|
||||||
|
- README.md in .workflow/docs/${projectName}/
|
||||||
|
|
||||||
|
Template Guidelines:
|
||||||
|
${templateContent}`;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'project-architecture':
|
||||||
|
templateContent = loadTemplate('project-architecture');
|
||||||
|
prompt = `Read project documentation:
|
||||||
|
@.workflow/docs/${projectName}/README.md
|
||||||
|
@.workflow/docs/${projectName}/**/API.md
|
||||||
|
|
||||||
|
Generate:
|
||||||
|
- ARCHITECTURE.md: System design documentation
|
||||||
|
- EXAMPLES.md: Usage examples
|
||||||
|
|
||||||
|
Output directory: .workflow/docs/${projectName}/
|
||||||
|
|
||||||
|
Template Guidelines:
|
||||||
|
${templateContent}`;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'http-api':
|
||||||
|
prompt = `Read API route files:
|
||||||
|
@**/routes/**/*.ts @**/routes/**/*.js
|
||||||
|
@**/api/**/*.ts @**/api/**/*.js
|
||||||
|
|
||||||
|
Generate HTTP API documentation:
|
||||||
|
- api/README.md: REST API endpoints documentation
|
||||||
|
|
||||||
|
Output directory: .workflow/docs/${projectName}/api/`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build and execute command
|
||||||
|
const command = buildCliCommand(tool, prompt, actualModel);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
execSync(command, {
|
||||||
|
cwd: targetPath,
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: 'inherit',
|
||||||
|
timeout: 600000 // 10 minutes
|
||||||
|
});
|
||||||
|
|
||||||
|
const duration = Math.round((Date.now() - startTime) / 1000);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
strategy,
|
||||||
|
source_path: sourcePath,
|
||||||
|
project_name: projectName,
|
||||||
|
output_path: outputPath,
|
||||||
|
folder_type: folderType,
|
||||||
|
tool,
|
||||||
|
model: actualModel,
|
||||||
|
duration_seconds: duration,
|
||||||
|
message: `Documentation generated successfully in ${duration}s`
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
strategy,
|
||||||
|
source_path: sourcePath,
|
||||||
|
project_name: projectName,
|
||||||
|
tool,
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool Definition
|
||||||
|
*/
|
||||||
|
export const generateModuleDocsTool = {
|
||||||
|
name: 'generate_module_docs',
|
||||||
|
description: `Generate documentation for modules and projects.
|
||||||
|
|
||||||
|
Module-Level Strategies:
|
||||||
|
- full: Full documentation (API.md + README.md for all directories)
|
||||||
|
- single: Single-layer documentation (current directory only)
|
||||||
|
|
||||||
|
Project-Level Strategies:
|
||||||
|
- project-readme: Project overview from module docs
|
||||||
|
- project-architecture: System design documentation
|
||||||
|
- http-api: HTTP API documentation
|
||||||
|
|
||||||
|
Output: .workflow/docs/{projectName}/...`,
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
strategy: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['full', 'single', 'project-readme', 'project-architecture', 'http-api'],
|
||||||
|
description: 'Documentation strategy'
|
||||||
|
},
|
||||||
|
sourcePath: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Source module directory path'
|
||||||
|
},
|
||||||
|
projectName: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Project name for output path'
|
||||||
|
},
|
||||||
|
tool: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['gemini', 'qwen', 'codex'],
|
||||||
|
description: 'CLI tool to use (default: gemini)',
|
||||||
|
default: 'gemini'
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Model name (optional, uses tool defaults)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['strategy', 'sourcePath', 'projectName']
|
||||||
|
},
|
||||||
|
execute
|
||||||
|
};
|
||||||
308
ccw/src/tools/get-modules-by-depth.js
Normal file
308
ccw/src/tools/get-modules-by-depth.js
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
/**
|
||||||
|
* Get Modules by Depth Tool
|
||||||
|
* Scan project structure and organize modules by directory depth (deepest first)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readdirSync, statSync, existsSync, readFileSync } from 'fs';
|
||||||
|
import { join, resolve, relative, extname } from 'path';
|
||||||
|
|
||||||
|
// System/cache directories to always exclude
|
||||||
|
const SYSTEM_EXCLUDES = [
|
||||||
|
// Version control and IDE
|
||||||
|
'.git', '.gitignore', '.gitmodules', '.gitattributes',
|
||||||
|
'.svn', '.hg', '.bzr',
|
||||||
|
'.history', '.vscode', '.idea', '.vs', '.vscode-test',
|
||||||
|
'.sublime-text', '.atom',
|
||||||
|
// Python
|
||||||
|
'__pycache__', '.pytest_cache', '.mypy_cache', '.tox',
|
||||||
|
'.coverage', 'htmlcov', '.nox', '.venv', 'venv', 'env',
|
||||||
|
'.egg-info', '.eggs', '.wheel',
|
||||||
|
'site-packages', '.python-version',
|
||||||
|
// Node.js/JavaScript
|
||||||
|
'node_modules', '.npm', '.yarn', '.pnpm', 'yarn-error.log',
|
||||||
|
'.nyc_output', 'coverage', '.next', '.nuxt',
|
||||||
|
'.cache', '.parcel-cache', '.vite', 'dist', 'build',
|
||||||
|
'.turbo', '.vercel', '.netlify',
|
||||||
|
// Build/compile outputs
|
||||||
|
'out', 'output', '_site', 'public',
|
||||||
|
'.output', '.generated', 'generated', 'gen',
|
||||||
|
'bin', 'obj', 'Debug', 'Release',
|
||||||
|
// Testing
|
||||||
|
'test-results', 'junit.xml', 'test_results',
|
||||||
|
'cypress', 'playwright-report', '.playwright',
|
||||||
|
// Logs and temp files
|
||||||
|
'logs', 'log', 'tmp', 'temp', '.tmp', '.temp',
|
||||||
|
// Documentation build outputs
|
||||||
|
'_book', 'docs/_build', 'site', 'gh-pages',
|
||||||
|
'.docusaurus', '.vuepress', '.gitbook',
|
||||||
|
// Cloud and deployment
|
||||||
|
'.serverless', '.terraform',
|
||||||
|
'.aws', '.azure', '.gcp',
|
||||||
|
// Mobile development
|
||||||
|
'.gradle', '.expo', '.metro',
|
||||||
|
'DerivedData',
|
||||||
|
// Game development
|
||||||
|
'Library', 'Temp', 'ProjectSettings',
|
||||||
|
'MemoryCaptures', 'UserSettings'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse .gitignore file and return patterns
|
||||||
|
*/
|
||||||
|
function parseGitignore(basePath) {
|
||||||
|
const gitignorePath = join(basePath, '.gitignore');
|
||||||
|
const patterns = [];
|
||||||
|
|
||||||
|
if (existsSync(gitignorePath)) {
|
||||||
|
const content = readFileSync(gitignorePath, 'utf8');
|
||||||
|
content.split('\n').forEach(line => {
|
||||||
|
line = line.trim();
|
||||||
|
// Skip empty lines and comments
|
||||||
|
if (!line || line.startsWith('#')) return;
|
||||||
|
// Remove trailing slash
|
||||||
|
line = line.replace(/\/$/, '');
|
||||||
|
patterns.push(line);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return patterns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a path should be excluded
|
||||||
|
*/
|
||||||
|
function shouldExclude(name, gitignorePatterns) {
|
||||||
|
// Check system excludes
|
||||||
|
if (SYSTEM_EXCLUDES.includes(name)) return true;
|
||||||
|
|
||||||
|
// Check gitignore patterns (simple matching)
|
||||||
|
for (const pattern of gitignorePatterns) {
|
||||||
|
if (name === pattern) return true;
|
||||||
|
// Simple wildcard matching
|
||||||
|
if (pattern.includes('*')) {
|
||||||
|
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
||||||
|
if (regex.test(name)) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file types in a directory
|
||||||
|
*/
|
||||||
|
function getFileTypes(dirPath) {
|
||||||
|
const types = new Set();
|
||||||
|
try {
|
||||||
|
const entries = readdirSync(dirPath, { withFileTypes: true });
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isFile()) {
|
||||||
|
const ext = extname(entry.name).slice(1);
|
||||||
|
if (ext) types.add(ext);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
return Array.from(types);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count files in a directory (non-recursive)
|
||||||
|
*/
|
||||||
|
function countFiles(dirPath) {
|
||||||
|
try {
|
||||||
|
const entries = readdirSync(dirPath, { withFileTypes: true });
|
||||||
|
return entries.filter(e => e.isFile()).length;
|
||||||
|
} catch (e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively scan directories and collect info
|
||||||
|
*/
|
||||||
|
function scanDirectories(basePath, currentPath, depth, gitignorePatterns, results) {
|
||||||
|
try {
|
||||||
|
const entries = readdirSync(currentPath, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.isDirectory()) continue;
|
||||||
|
if (shouldExclude(entry.name, gitignorePatterns)) continue;
|
||||||
|
|
||||||
|
const fullPath = join(currentPath, entry.name);
|
||||||
|
const relPath = './' + relative(basePath, fullPath).replace(/\\/g, '/');
|
||||||
|
const fileCount = countFiles(fullPath);
|
||||||
|
|
||||||
|
// Only include directories with files
|
||||||
|
if (fileCount > 0) {
|
||||||
|
const types = getFileTypes(fullPath);
|
||||||
|
const hasClaude = existsSync(join(fullPath, 'CLAUDE.md'));
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
depth: depth + 1,
|
||||||
|
path: relPath,
|
||||||
|
files: fileCount,
|
||||||
|
types,
|
||||||
|
has_claude: hasClaude
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse into subdirectories
|
||||||
|
scanDirectories(basePath, fullPath, depth + 1, gitignorePatterns, results);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore permission errors, etc.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format output as list (default)
|
||||||
|
*/
|
||||||
|
function formatList(results) {
|
||||||
|
// Sort by depth descending (deepest first)
|
||||||
|
results.sort((a, b) => b.depth - a.depth);
|
||||||
|
|
||||||
|
return results.map(r =>
|
||||||
|
`depth:${r.depth}|path:${r.path}|files:${r.files}|types:[${r.types.join(',')}]|has_claude:${r.has_claude ? 'yes' : 'no'}`
|
||||||
|
).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format output as grouped
|
||||||
|
*/
|
||||||
|
function formatGrouped(results) {
|
||||||
|
// Sort by depth descending
|
||||||
|
results.sort((a, b) => b.depth - a.depth);
|
||||||
|
|
||||||
|
const maxDepth = results.length > 0 ? Math.max(...results.map(r => r.depth)) : 0;
|
||||||
|
const lines = ['Modules by depth (deepest first):'];
|
||||||
|
|
||||||
|
for (let d = maxDepth; d >= 0; d--) {
|
||||||
|
const atDepth = results.filter(r => r.depth === d);
|
||||||
|
if (atDepth.length > 0) {
|
||||||
|
lines.push(` Depth ${d}:`);
|
||||||
|
atDepth.forEach(r => {
|
||||||
|
const claudeIndicator = r.has_claude ? ' [OK]' : '';
|
||||||
|
lines.push(` - ${r.path}${claudeIndicator}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format output as JSON
|
||||||
|
*/
|
||||||
|
function formatJson(results) {
|
||||||
|
// Sort by depth descending
|
||||||
|
results.sort((a, b) => b.depth - a.depth);
|
||||||
|
|
||||||
|
const maxDepth = results.length > 0 ? Math.max(...results.map(r => r.depth)) : 0;
|
||||||
|
const modules = {};
|
||||||
|
|
||||||
|
for (let d = maxDepth; d >= 0; d--) {
|
||||||
|
const atDepth = results.filter(r => r.depth === d);
|
||||||
|
if (atDepth.length > 0) {
|
||||||
|
modules[d] = atDepth.map(r => ({
|
||||||
|
path: r.path,
|
||||||
|
has_claude: r.has_claude
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
max_depth: maxDepth,
|
||||||
|
modules
|
||||||
|
}, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main execute function
|
||||||
|
*/
|
||||||
|
async function execute(params) {
|
||||||
|
const { format = 'list', path: targetPath = '.' } = params;
|
||||||
|
|
||||||
|
const basePath = resolve(process.cwd(), targetPath);
|
||||||
|
|
||||||
|
if (!existsSync(basePath)) {
|
||||||
|
throw new Error(`Directory not found: ${basePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stat = statSync(basePath);
|
||||||
|
if (!stat.isDirectory()) {
|
||||||
|
throw new Error(`Not a directory: ${basePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse gitignore
|
||||||
|
const gitignorePatterns = parseGitignore(basePath);
|
||||||
|
|
||||||
|
// Collect results
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
// Check root directory
|
||||||
|
const rootFileCount = countFiles(basePath);
|
||||||
|
if (rootFileCount > 0) {
|
||||||
|
results.push({
|
||||||
|
depth: 0,
|
||||||
|
path: '.',
|
||||||
|
files: rootFileCount,
|
||||||
|
types: getFileTypes(basePath),
|
||||||
|
has_claude: existsSync(join(basePath, 'CLAUDE.md'))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan subdirectories
|
||||||
|
scanDirectories(basePath, basePath, 0, gitignorePatterns, results);
|
||||||
|
|
||||||
|
// Format output
|
||||||
|
let output;
|
||||||
|
switch (format) {
|
||||||
|
case 'grouped':
|
||||||
|
output = formatGrouped(results);
|
||||||
|
break;
|
||||||
|
case 'json':
|
||||||
|
output = formatJson(results);
|
||||||
|
break;
|
||||||
|
case 'list':
|
||||||
|
default:
|
||||||
|
output = formatList(results);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
format,
|
||||||
|
total_modules: results.length,
|
||||||
|
max_depth: results.length > 0 ? Math.max(...results.map(r => r.depth)) : 0,
|
||||||
|
output
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool Definition
|
||||||
|
*/
|
||||||
|
export const getModulesByDepthTool = {
|
||||||
|
name: 'get_modules_by_depth',
|
||||||
|
description: `Scan project structure and organize modules by directory depth (deepest first).
|
||||||
|
Respects .gitignore patterns and excludes common system directories.
|
||||||
|
Output formats: list (pipe-delimited), grouped (human-readable), json.`,
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
format: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['list', 'grouped', 'json'],
|
||||||
|
description: 'Output format (default: list)',
|
||||||
|
default: 'list'
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Target directory path (default: current directory)',
|
||||||
|
default: '.'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: []
|
||||||
|
},
|
||||||
|
execute
|
||||||
|
};
|
||||||
176
ccw/src/tools/index.js
Normal file
176
ccw/src/tools/index.js
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
/**
|
||||||
|
* Tool Registry - MCP-like tool system for CCW
|
||||||
|
* Provides tool discovery, validation, and execution
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { editFileTool } from './edit-file.js';
|
||||||
|
import { getModulesByDepthTool } from './get-modules-by-depth.js';
|
||||||
|
import { classifyFoldersTool } from './classify-folders.js';
|
||||||
|
import { detectChangedModulesTool } from './detect-changed-modules.js';
|
||||||
|
import { discoverDesignFilesTool } from './discover-design-files.js';
|
||||||
|
import { generateModuleDocsTool } from './generate-module-docs.js';
|
||||||
|
import { uiGeneratePreviewTool } from './ui-generate-preview.js';
|
||||||
|
import { uiInstantiatePrototypesTool } from './ui-instantiate-prototypes.js';
|
||||||
|
import { updateModuleClaudeTool } from './update-module-claude.js';
|
||||||
|
import { convertTokensToCssTool } from './convert-tokens-to-css.js';
|
||||||
|
|
||||||
|
// Tool registry - add new tools here
|
||||||
|
const tools = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a tool in the registry
|
||||||
|
* @param {Object} tool - Tool definition
|
||||||
|
*/
|
||||||
|
function registerTool(tool) {
|
||||||
|
if (!tool.name || !tool.execute) {
|
||||||
|
throw new Error('Tool must have name and execute function');
|
||||||
|
}
|
||||||
|
tools.set(tool.name, tool);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all registered tools
|
||||||
|
* @returns {Array<Object>} - Array of tool definitions (without execute function)
|
||||||
|
*/
|
||||||
|
export function listTools() {
|
||||||
|
return Array.from(tools.values()).map(tool => ({
|
||||||
|
name: tool.name,
|
||||||
|
description: tool.description,
|
||||||
|
parameters: tool.parameters
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific tool by name
|
||||||
|
* @param {string} name - Tool name
|
||||||
|
* @returns {Object|null} - Tool definition or null
|
||||||
|
*/
|
||||||
|
export function getTool(name) {
|
||||||
|
return tools.get(name) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate parameters against tool schema
|
||||||
|
* @param {Object} tool - Tool definition
|
||||||
|
* @param {Object} params - Parameters to validate
|
||||||
|
* @returns {{valid: boolean, errors: string[]}}
|
||||||
|
*/
|
||||||
|
function validateParams(tool, params) {
|
||||||
|
const errors = [];
|
||||||
|
const schema = tool.parameters;
|
||||||
|
|
||||||
|
if (!schema || !schema.properties) {
|
||||||
|
return { valid: true, errors: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check required parameters
|
||||||
|
const required = schema.required || [];
|
||||||
|
for (const req of required) {
|
||||||
|
if (params[req] === undefined || params[req] === null) {
|
||||||
|
errors.push(`Missing required parameter: ${req}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type validation
|
||||||
|
for (const [key, value] of Object.entries(params)) {
|
||||||
|
const propSchema = schema.properties[key];
|
||||||
|
if (!propSchema) {
|
||||||
|
continue; // Allow extra params
|
||||||
|
}
|
||||||
|
|
||||||
|
if (propSchema.type === 'string' && typeof value !== 'string') {
|
||||||
|
errors.push(`Parameter '${key}' must be a string`);
|
||||||
|
}
|
||||||
|
if (propSchema.type === 'boolean' && typeof value !== 'boolean') {
|
||||||
|
errors.push(`Parameter '${key}' must be a boolean`);
|
||||||
|
}
|
||||||
|
if (propSchema.type === 'number' && typeof value !== 'number') {
|
||||||
|
errors.push(`Parameter '${key}' must be a number`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: errors.length === 0, errors };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a tool with given parameters
|
||||||
|
* @param {string} name - Tool name
|
||||||
|
* @param {Object} params - Tool parameters
|
||||||
|
* @returns {Promise<{success: boolean, result?: any, error?: string}>}
|
||||||
|
*/
|
||||||
|
export async function executeTool(name, params = {}) {
|
||||||
|
const tool = tools.get(name);
|
||||||
|
|
||||||
|
if (!tool) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Tool not found: ${name}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate parameters
|
||||||
|
const validation = validateParams(tool, params);
|
||||||
|
if (!validation.valid) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Parameter validation failed: ${validation.errors.join(', ')}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute tool
|
||||||
|
try {
|
||||||
|
const result = await tool.execute(params);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
result
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message || 'Tool execution failed'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get tool schema in MCP-compatible format
|
||||||
|
* @param {string} name - Tool name
|
||||||
|
* @returns {Object|null} - Tool schema or null
|
||||||
|
*/
|
||||||
|
export function getToolSchema(name) {
|
||||||
|
const tool = tools.get(name);
|
||||||
|
if (!tool) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: tool.name,
|
||||||
|
description: tool.description,
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: tool.parameters?.properties || {},
|
||||||
|
required: tool.parameters?.required || []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all tool schemas in MCP-compatible format
|
||||||
|
* @returns {Array<Object>} - Array of tool schemas
|
||||||
|
*/
|
||||||
|
export function getAllToolSchemas() {
|
||||||
|
return Array.from(tools.keys()).map(name => getToolSchema(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register built-in tools
|
||||||
|
registerTool(editFileTool);
|
||||||
|
registerTool(getModulesByDepthTool);
|
||||||
|
registerTool(classifyFoldersTool);
|
||||||
|
registerTool(detectChangedModulesTool);
|
||||||
|
registerTool(discoverDesignFilesTool);
|
||||||
|
registerTool(generateModuleDocsTool);
|
||||||
|
registerTool(uiGeneratePreviewTool);
|
||||||
|
registerTool(uiInstantiatePrototypesTool);
|
||||||
|
registerTool(updateModuleClaudeTool);
|
||||||
|
registerTool(convertTokensToCssTool);
|
||||||
|
|
||||||
|
// Export for external tool registration
|
||||||
|
export { registerTool };
|
||||||
327
ccw/src/tools/ui-generate-preview.js
Normal file
327
ccw/src/tools/ui-generate-preview.js
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
/**
|
||||||
|
* UI Generate Preview Tool
|
||||||
|
* Generate compare.html and index.html for UI prototypes
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readdirSync, existsSync, readFileSync, writeFileSync } from 'fs';
|
||||||
|
import { resolve, basename } from 'path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-detect matrix dimensions from file patterns
|
||||||
|
* Pattern: {target}-style-{s}-layout-{l}.html
|
||||||
|
*/
|
||||||
|
function detectMatrixDimensions(prototypesDir) {
|
||||||
|
const files = readdirSync(prototypesDir).filter(f => f.match(/.*-style-\d+-layout-\d+\.html$/));
|
||||||
|
|
||||||
|
const styles = new Set();
|
||||||
|
const layouts = new Set();
|
||||||
|
const targets = new Set();
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
const styleMatch = file.match(/-style-(\d+)-/);
|
||||||
|
const layoutMatch = file.match(/-layout-(\d+)\.html/);
|
||||||
|
const targetMatch = file.match(/^(.+)-style-/);
|
||||||
|
|
||||||
|
if (styleMatch) styles.add(parseInt(styleMatch[1]));
|
||||||
|
if (layoutMatch) layouts.add(parseInt(layoutMatch[1]));
|
||||||
|
if (targetMatch) targets.add(targetMatch[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
styles: Math.max(...Array.from(styles)),
|
||||||
|
layouts: Math.max(...Array.from(layouts)),
|
||||||
|
targets: Array.from(targets).sort()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load template from file
|
||||||
|
*/
|
||||||
|
function loadTemplate(templatePath) {
|
||||||
|
const defaultPath = resolve(
|
||||||
|
process.env.HOME || process.env.USERPROFILE,
|
||||||
|
'.claude/workflows/_template-compare-matrix.html'
|
||||||
|
);
|
||||||
|
|
||||||
|
const path = templatePath || defaultPath;
|
||||||
|
|
||||||
|
if (!existsSync(path)) {
|
||||||
|
// Return minimal fallback template
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head><title>UI Prototypes Comparison</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>UI Prototypes Matrix</h1>
|
||||||
|
<p>Styles: {{style_variants}} | Layouts: {{layout_variants}}</p>
|
||||||
|
<p>Pages: {{pages_json}}</p>
|
||||||
|
<p>Generated: {{timestamp}}</p>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return readFileSync(path, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate compare.html from template
|
||||||
|
*/
|
||||||
|
function generateCompareHtml(template, metadata) {
|
||||||
|
const { runId, sessionId, timestamp, styles, layouts, targets } = metadata;
|
||||||
|
|
||||||
|
const pagesJson = JSON.stringify(targets);
|
||||||
|
|
||||||
|
return template
|
||||||
|
.replace(/\{\{run_id\}\}/g, runId)
|
||||||
|
.replace(/\{\{session_id\}\}/g, sessionId)
|
||||||
|
.replace(/\{\{timestamp\}\}/g, timestamp)
|
||||||
|
.replace(/\{\{style_variants\}\}/g, styles.toString())
|
||||||
|
.replace(/\{\{layout_variants\}\}/g, layouts.toString())
|
||||||
|
.replace(/\{\{pages_json\}\}/g, pagesJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate index.html
|
||||||
|
*/
|
||||||
|
function generateIndexHtml(metadata) {
|
||||||
|
const { styles, layouts, targets } = metadata;
|
||||||
|
const total = styles * layouts * targets.length;
|
||||||
|
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>UI Prototypes Index</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 40px 20px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
h1 { margin-bottom: 10px; color: #333; }
|
||||||
|
.subtitle { color: #666; margin-bottom: 30px; }
|
||||||
|
.cta {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.cta h2 { margin-bottom: 10px; }
|
||||||
|
.cta a {
|
||||||
|
display: inline-block;
|
||||||
|
background: white;
|
||||||
|
color: #667eea;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.stat {
|
||||||
|
background: white;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.stat-label { font-size: 0.85em; color: #666; margin-bottom: 5px; }
|
||||||
|
.stat-value { font-size: 1.5em; font-weight: bold; color: #333; }
|
||||||
|
.files {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.files h2 { margin-bottom: 15px; color: #333; }
|
||||||
|
.file-list { list-style: none; }
|
||||||
|
.file-list li {
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
.file-list li:last-child { border-bottom: none; }
|
||||||
|
.file-list a {
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.file-list a:hover { text-decoration: underline; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>UI Prototypes</h1>
|
||||||
|
<p class="subtitle">Interactive design exploration matrix</p>
|
||||||
|
|
||||||
|
<div class="cta">
|
||||||
|
<h2>📊 Interactive Comparison</h2>
|
||||||
|
<p>View all prototypes side-by-side with synchronized scrolling</p>
|
||||||
|
<a href="compare.html">Open Comparison Matrix →</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-label">Style Variants</div>
|
||||||
|
<div class="stat-value">${styles}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-label">Layout Variants</div>
|
||||||
|
<div class="stat-value">${layouts}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-label">Pages/Components</div>
|
||||||
|
<div class="stat-value">${targets.length}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-label">Total Prototypes</div>
|
||||||
|
<div class="stat-value">${total}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="files">
|
||||||
|
<h2>Individual Prototypes</h2>
|
||||||
|
<ul class="file-list">
|
||||||
|
${targets.map(target => {
|
||||||
|
const items = [];
|
||||||
|
for (let s = 1; s <= styles; s++) {
|
||||||
|
for (let l = 1; l <= layouts; l++) {
|
||||||
|
const filename = `${target}-style-${s}-layout-${l}.html`;
|
||||||
|
items.push(` <li><a href="${filename}">${filename}</a></li>`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items.join('\n');
|
||||||
|
}).join('\n')}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate PREVIEW.md
|
||||||
|
*/
|
||||||
|
function generatePreviewMd(metadata) {
|
||||||
|
const { styles, layouts, targets } = metadata;
|
||||||
|
|
||||||
|
return `# UI Prototypes Preview
|
||||||
|
|
||||||
|
## Matrix Dimensions
|
||||||
|
|
||||||
|
- **Style Variants**: ${styles}
|
||||||
|
- **Layout Variants**: ${layouts}
|
||||||
|
- **Pages/Components**: ${targets.join(', ')}
|
||||||
|
- **Total Prototypes**: ${styles * layouts * targets.length}
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. **Interactive Comparison**: Open \`compare.html\` for side-by-side view with synchronized scrolling
|
||||||
|
2. **Browse Index**: Open \`index.html\` for a navigable list of all prototypes
|
||||||
|
3. **Individual Files**: Access specific prototypes directly (e.g., \`${targets[0]}-style-1-layout-1.html\`)
|
||||||
|
|
||||||
|
## File Naming Convention
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
{page}-style-{s}-layout-{l}.html
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
- **page**: Component/page name (${targets.join(', ')})
|
||||||
|
- **s**: Style variant number (1-${styles})
|
||||||
|
- **l**: Layout variant number (1-${layouts})
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
- Use compare.html for quick visual comparison across all variants
|
||||||
|
- Synchronized scrolling helps identify consistency issues
|
||||||
|
- Check responsive behavior across different layout variants
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main execute function
|
||||||
|
*/
|
||||||
|
async function execute(params) {
|
||||||
|
const { prototypesDir = '.', template: templatePath } = params;
|
||||||
|
|
||||||
|
const targetPath = resolve(process.cwd(), prototypesDir);
|
||||||
|
|
||||||
|
if (!existsSync(targetPath)) {
|
||||||
|
throw new Error(`Directory not found: ${targetPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-detect matrix dimensions
|
||||||
|
const { styles, layouts, targets } = detectMatrixDimensions(targetPath);
|
||||||
|
|
||||||
|
if (styles === 0 || layouts === 0 || targets.length === 0) {
|
||||||
|
throw new Error('No prototype files found matching pattern {target}-style-{s}-layout-{l}.html');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate metadata
|
||||||
|
const metadata = {
|
||||||
|
runId: `run-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5)}`,
|
||||||
|
sessionId: 'standalone',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
styles,
|
||||||
|
layouts,
|
||||||
|
targets
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load template
|
||||||
|
const template = loadTemplate(templatePath);
|
||||||
|
|
||||||
|
// Generate files
|
||||||
|
const compareHtml = generateCompareHtml(template, metadata);
|
||||||
|
const indexHtml = generateIndexHtml(metadata);
|
||||||
|
const previewMd = generatePreviewMd(metadata);
|
||||||
|
|
||||||
|
// Write files
|
||||||
|
writeFileSync(resolve(targetPath, 'compare.html'), compareHtml, 'utf8');
|
||||||
|
writeFileSync(resolve(targetPath, 'index.html'), indexHtml, 'utf8');
|
||||||
|
writeFileSync(resolve(targetPath, 'PREVIEW.md'), previewMd, 'utf8');
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
prototypes_dir: prototypesDir,
|
||||||
|
styles,
|
||||||
|
layouts,
|
||||||
|
targets,
|
||||||
|
total_prototypes: styles * layouts * targets.length,
|
||||||
|
files_generated: ['compare.html', 'index.html', 'PREVIEW.md']
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool Definition
|
||||||
|
*/
|
||||||
|
export const uiGeneratePreviewTool = {
|
||||||
|
name: 'ui_generate_preview',
|
||||||
|
description: `Generate interactive preview files for UI prototypes.
|
||||||
|
Generates:
|
||||||
|
- compare.html: Interactive matrix view with synchronized scrolling
|
||||||
|
- index.html: Navigation and statistics
|
||||||
|
- PREVIEW.md: Usage guide
|
||||||
|
|
||||||
|
Auto-detects matrix dimensions from file pattern: {target}-style-{s}-layout-{l}.html`,
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
prototypesDir: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Prototypes directory path (default: current directory)',
|
||||||
|
default: '.'
|
||||||
|
},
|
||||||
|
template: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Optional path to compare.html template'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: []
|
||||||
|
},
|
||||||
|
execute
|
||||||
|
};
|
||||||
301
ccw/src/tools/ui-instantiate-prototypes.js
Normal file
301
ccw/src/tools/ui-instantiate-prototypes.js
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
/**
|
||||||
|
* UI Instantiate Prototypes Tool
|
||||||
|
* Create final UI prototypes from templates (Style × Layout × Page matrix)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readdirSync, existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from 'fs';
|
||||||
|
import { resolve, join, basename } from 'path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-detect pages from templates directory
|
||||||
|
*/
|
||||||
|
function autoDetectPages(templatesDir) {
|
||||||
|
if (!existsSync(templatesDir)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = readdirSync(templatesDir).filter(f => f.match(/.*-layout-\d+\.html$/));
|
||||||
|
const pages = new Set();
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
const match = file.match(/^(.+)-layout-\d+\.html$/);
|
||||||
|
if (match) pages.add(match[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(pages).sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-detect style variants count
|
||||||
|
*/
|
||||||
|
function autoDetectStyleVariants(basePath) {
|
||||||
|
const styleDir = resolve(basePath, '..', 'style-extraction');
|
||||||
|
|
||||||
|
if (!existsSync(styleDir)) {
|
||||||
|
return 3; // Default
|
||||||
|
}
|
||||||
|
|
||||||
|
const dirs = readdirSync(styleDir, { withFileTypes: true })
|
||||||
|
.filter(d => d.isDirectory() && d.name.startsWith('style-'));
|
||||||
|
|
||||||
|
return dirs.length > 0 ? dirs.length : 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-detect layout variants count
|
||||||
|
*/
|
||||||
|
function autoDetectLayoutVariants(templatesDir) {
|
||||||
|
if (!existsSync(templatesDir)) {
|
||||||
|
return 3; // Default
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = readdirSync(templatesDir);
|
||||||
|
const firstPage = files.find(f => f.endsWith('-layout-1.html'));
|
||||||
|
|
||||||
|
if (!firstPage) return 3;
|
||||||
|
|
||||||
|
const pageName = firstPage.replace(/-layout-1\.html$/, '');
|
||||||
|
const layoutFiles = files.filter(f => f.match(new RegExp(`^${pageName}-layout-\\d+\\.html$`)));
|
||||||
|
|
||||||
|
return layoutFiles.length > 0 ? layoutFiles.length : 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load CSS tokens file
|
||||||
|
*/
|
||||||
|
function loadTokensCss(styleDir, styleNum) {
|
||||||
|
const tokenPath = join(styleDir, `style-${styleNum}`, 'tokens.css');
|
||||||
|
|
||||||
|
if (existsSync(tokenPath)) {
|
||||||
|
return readFileSync(tokenPath, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
return '/* No tokens.css found */';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace CSS placeholder in template
|
||||||
|
*/
|
||||||
|
function replaceCssPlaceholder(html, tokensCss) {
|
||||||
|
// Replace {{tokens.css}} placeholder
|
||||||
|
return html.replace(/\{\{tokens\.css\}\}/g, tokensCss);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate prototype from template
|
||||||
|
*/
|
||||||
|
function generatePrototype(templatePath, styleDir, styleNum, outputPath) {
|
||||||
|
const templateHtml = readFileSync(templatePath, 'utf8');
|
||||||
|
const tokensCss = loadTokensCss(styleDir, styleNum);
|
||||||
|
const finalHtml = replaceCssPlaceholder(templateHtml, tokensCss);
|
||||||
|
|
||||||
|
writeFileSync(outputPath, finalHtml, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate implementation notes
|
||||||
|
*/
|
||||||
|
function generateImplementationNotes(page, styleNum, layoutNum) {
|
||||||
|
return `# Implementation Notes: ${page}-style-${styleNum}-layout-${layoutNum}
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Prototype combining:
|
||||||
|
- **Page/Component**: ${page}
|
||||||
|
- **Style Variant**: ${styleNum}
|
||||||
|
- **Layout Variant**: ${layoutNum}
|
||||||
|
|
||||||
|
## Implementation Checklist
|
||||||
|
|
||||||
|
### 1. Style Integration
|
||||||
|
- [ ] Verify all CSS custom properties are applied correctly
|
||||||
|
- [ ] Check color palette consistency
|
||||||
|
- [ ] Validate typography settings
|
||||||
|
- [ ] Test spacing and border radius values
|
||||||
|
|
||||||
|
### 2. Layout Verification
|
||||||
|
- [ ] Confirm component structure matches layout variant
|
||||||
|
- [ ] Test responsive behavior
|
||||||
|
- [ ] Verify flex/grid layouts
|
||||||
|
- [ ] Check alignment and spacing
|
||||||
|
|
||||||
|
### 3. Accessibility
|
||||||
|
- [ ] Color contrast ratios (WCAG AA minimum)
|
||||||
|
- [ ] Keyboard navigation
|
||||||
|
- [ ] Screen reader compatibility
|
||||||
|
- [ ] Focus indicators
|
||||||
|
|
||||||
|
### 4. Browser Testing
|
||||||
|
- [ ] Chrome/Edge
|
||||||
|
- [ ] Firefox
|
||||||
|
- [ ] Safari
|
||||||
|
- [ ] Mobile browsers
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
1. Review prototype in browser
|
||||||
|
2. Compare with design specifications
|
||||||
|
3. Implement in production codebase
|
||||||
|
4. Add interactive functionality
|
||||||
|
5. Write tests
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main execute function
|
||||||
|
*/
|
||||||
|
async function execute(params) {
|
||||||
|
const {
|
||||||
|
prototypesDir,
|
||||||
|
pages: pagesParam,
|
||||||
|
styleVariants: styleVariantsParam,
|
||||||
|
layoutVariants: layoutVariantsParam,
|
||||||
|
runId: runIdParam,
|
||||||
|
sessionId = 'standalone',
|
||||||
|
generatePreview = true
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
if (!prototypesDir) {
|
||||||
|
throw new Error('Parameter "prototypesDir" is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const basePath = resolve(process.cwd(), prototypesDir);
|
||||||
|
|
||||||
|
if (!existsSync(basePath)) {
|
||||||
|
throw new Error(`Directory not found: ${basePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const templatesDir = join(basePath, '_templates');
|
||||||
|
const styleDir = resolve(basePath, '..', 'style-extraction');
|
||||||
|
|
||||||
|
// Auto-detect or use provided parameters
|
||||||
|
let pages, styleVariants, layoutVariants;
|
||||||
|
|
||||||
|
if (pagesParam && styleVariantsParam && layoutVariantsParam) {
|
||||||
|
// Manual mode
|
||||||
|
pages = Array.isArray(pagesParam) ? pagesParam : pagesParam.split(',').map(p => p.trim());
|
||||||
|
styleVariants = parseInt(styleVariantsParam);
|
||||||
|
layoutVariants = parseInt(layoutVariantsParam);
|
||||||
|
} else {
|
||||||
|
// Auto-detect mode
|
||||||
|
pages = autoDetectPages(templatesDir);
|
||||||
|
styleVariants = autoDetectStyleVariants(basePath);
|
||||||
|
layoutVariants = autoDetectLayoutVariants(templatesDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pages.length === 0) {
|
||||||
|
throw new Error('No pages detected. Ensure _templates directory contains layout files.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate run ID
|
||||||
|
const runId = runIdParam || `run-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5)}`;
|
||||||
|
|
||||||
|
// Phase 1: Copy templates and replace CSS placeholders
|
||||||
|
const generatedFiles = [];
|
||||||
|
|
||||||
|
for (const page of pages) {
|
||||||
|
for (let s = 1; s <= styleVariants; s++) {
|
||||||
|
for (let l = 1; l <= layoutVariants; l++) {
|
||||||
|
const templateFile = `${page}-layout-${l}.html`;
|
||||||
|
const templatePath = join(templatesDir, templateFile);
|
||||||
|
|
||||||
|
if (!existsSync(templatePath)) {
|
||||||
|
console.warn(`Template not found: ${templateFile}, skipping...`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputFile = `${page}-style-${s}-layout-${l}.html`;
|
||||||
|
const outputPath = join(basePath, outputFile);
|
||||||
|
|
||||||
|
// Generate prototype
|
||||||
|
generatePrototype(templatePath, styleDir, s, outputPath);
|
||||||
|
|
||||||
|
// Generate implementation notes
|
||||||
|
const notesFile = `${page}-style-${s}-layout-${l}-notes.md`;
|
||||||
|
const notesPath = join(basePath, notesFile);
|
||||||
|
const notes = generateImplementationNotes(page, s, l);
|
||||||
|
writeFileSync(notesPath, notes, 'utf8');
|
||||||
|
|
||||||
|
generatedFiles.push(outputFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: Generate preview files (optional)
|
||||||
|
const previewFiles = [];
|
||||||
|
if (generatePreview) {
|
||||||
|
// Import and execute ui_generate_preview tool
|
||||||
|
const { uiGeneratePreviewTool } = await import('./ui-generate-preview.js');
|
||||||
|
const previewResult = await uiGeneratePreviewTool.execute({ prototypesDir: basePath });
|
||||||
|
|
||||||
|
if (previewResult.success) {
|
||||||
|
previewFiles.push(...previewResult.files_generated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
run_id: runId,
|
||||||
|
session_id: sessionId,
|
||||||
|
prototypes_dir: basePath,
|
||||||
|
pages,
|
||||||
|
style_variants: styleVariants,
|
||||||
|
layout_variants: layoutVariants,
|
||||||
|
total_prototypes: generatedFiles.length,
|
||||||
|
files_generated: generatedFiles,
|
||||||
|
preview_files: previewFiles,
|
||||||
|
message: `Generated ${generatedFiles.length} prototypes (${styleVariants} styles × ${layoutVariants} layouts × ${pages.length} pages)`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool Definition
|
||||||
|
*/
|
||||||
|
export const uiInstantiatePrototypesTool = {
|
||||||
|
name: 'ui_instantiate_prototypes',
|
||||||
|
description: `Create final UI prototypes from templates (Style × Layout × Page matrix).
|
||||||
|
|
||||||
|
Two Modes:
|
||||||
|
1. Auto-detect (recommended): Only specify prototypesDir
|
||||||
|
2. Manual: Specify prototypesDir, pages, styleVariants, layoutVariants
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Copies templates and replaces CSS placeholders with tokens.css
|
||||||
|
- Generates implementation notes for each prototype
|
||||||
|
- Optionally generates preview files (compare.html, index.html, PREVIEW.md)`,
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
prototypesDir: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Prototypes directory path'
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Comma-separated list of pages (auto-detected if not provided)'
|
||||||
|
},
|
||||||
|
styleVariants: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Number of style variants (auto-detected if not provided)'
|
||||||
|
},
|
||||||
|
layoutVariants: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Number of layout variants (auto-detected if not provided)'
|
||||||
|
},
|
||||||
|
runId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Run ID (auto-generated if not provided)'
|
||||||
|
},
|
||||||
|
sessionId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Session ID (default: standalone)',
|
||||||
|
default: 'standalone'
|
||||||
|
},
|
||||||
|
generatePreview: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Generate preview files (default: true)',
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['prototypesDir']
|
||||||
|
},
|
||||||
|
execute
|
||||||
|
};
|
||||||
328
ccw/src/tools/update-module-claude.js
Normal file
328
ccw/src/tools/update-module-claude.js
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
/**
|
||||||
|
* Update Module CLAUDE.md Tool
|
||||||
|
* Generate/update CLAUDE.md module documentation using CLI tools
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readdirSync, statSync, existsSync, readFileSync } from 'fs';
|
||||||
|
import { join, resolve, basename, extname } from 'path';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
|
||||||
|
// Directories to exclude
|
||||||
|
const EXCLUDE_DIRS = [
|
||||||
|
'.git', '__pycache__', 'node_modules', '.venv', 'venv', 'env',
|
||||||
|
'dist', 'build', '.cache', '.pytest_cache', '.mypy_cache',
|
||||||
|
'coverage', '.nyc_output', 'logs', 'tmp', 'temp'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Default models for each tool
|
||||||
|
const DEFAULT_MODELS = {
|
||||||
|
gemini: 'gemini-2.5-flash',
|
||||||
|
qwen: 'coder-model',
|
||||||
|
codex: 'gpt5-codex'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count files in directory
|
||||||
|
*/
|
||||||
|
function countFiles(dirPath) {
|
||||||
|
try {
|
||||||
|
const entries = readdirSync(dirPath, { withFileTypes: true });
|
||||||
|
return entries.filter(e => e.isFile() && !e.name.startsWith('.')).length;
|
||||||
|
} catch (e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scan directory structure
|
||||||
|
*/
|
||||||
|
function scanDirectoryStructure(targetPath, strategy) {
|
||||||
|
const lines = [];
|
||||||
|
const dirName = basename(targetPath);
|
||||||
|
|
||||||
|
let totalFiles = 0;
|
||||||
|
let totalDirs = 0;
|
||||||
|
|
||||||
|
function countRecursive(dir) {
|
||||||
|
try {
|
||||||
|
const entries = readdirSync(dir, { withFileTypes: true });
|
||||||
|
entries.forEach(e => {
|
||||||
|
if (e.name.startsWith('.') || EXCLUDE_DIRS.includes(e.name)) return;
|
||||||
|
if (e.isFile()) totalFiles++;
|
||||||
|
else if (e.isDirectory()) {
|
||||||
|
totalDirs++;
|
||||||
|
countRecursive(join(dir, e.name));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
countRecursive(targetPath);
|
||||||
|
|
||||||
|
lines.push(`Directory: ${dirName}`);
|
||||||
|
lines.push(`Total files: ${totalFiles}`);
|
||||||
|
lines.push(`Total directories: ${totalDirs}`);
|
||||||
|
lines.push('');
|
||||||
|
|
||||||
|
if (strategy === 'multi-layer') {
|
||||||
|
lines.push('Subdirectories with files:');
|
||||||
|
// List subdirectories with file counts
|
||||||
|
function listSubdirs(dir, prefix = '') {
|
||||||
|
try {
|
||||||
|
const entries = readdirSync(dir, { withFileTypes: true });
|
||||||
|
entries.forEach(e => {
|
||||||
|
if (!e.isDirectory() || e.name.startsWith('.') || EXCLUDE_DIRS.includes(e.name)) return;
|
||||||
|
const subPath = join(dir, e.name);
|
||||||
|
const fileCount = countFiles(subPath);
|
||||||
|
if (fileCount > 0) {
|
||||||
|
const relPath = subPath.replace(targetPath, '').replace(/^[/\\]/, '');
|
||||||
|
lines.push(` - ${relPath}/ (${fileCount} files)`);
|
||||||
|
}
|
||||||
|
listSubdirs(subPath, prefix + ' ');
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listSubdirs(targetPath);
|
||||||
|
} else {
|
||||||
|
lines.push('Direct subdirectories:');
|
||||||
|
try {
|
||||||
|
const entries = readdirSync(targetPath, { withFileTypes: true });
|
||||||
|
entries.forEach(e => {
|
||||||
|
if (!e.isDirectory() || e.name.startsWith('.') || EXCLUDE_DIRS.includes(e.name)) return;
|
||||||
|
const subPath = join(targetPath, e.name);
|
||||||
|
const fileCount = countFiles(subPath);
|
||||||
|
const hasClaude = existsSync(join(subPath, 'CLAUDE.md')) ? ' [has CLAUDE.md]' : '';
|
||||||
|
lines.push(` - ${e.name}/ (${fileCount} files)${hasClaude}`);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count file types in current directory
|
||||||
|
lines.push('');
|
||||||
|
lines.push('Current directory files:');
|
||||||
|
try {
|
||||||
|
const entries = readdirSync(targetPath, { withFileTypes: true });
|
||||||
|
const codeExts = ['.ts', '.tsx', '.js', '.jsx', '.py', '.sh'];
|
||||||
|
const configExts = ['.json', '.yaml', '.yml', '.toml'];
|
||||||
|
|
||||||
|
let codeCount = 0, configCount = 0, docCount = 0;
|
||||||
|
entries.forEach(e => {
|
||||||
|
if (!e.isFile()) return;
|
||||||
|
const ext = extname(e.name).toLowerCase();
|
||||||
|
if (codeExts.includes(ext)) codeCount++;
|
||||||
|
else if (configExts.includes(ext)) configCount++;
|
||||||
|
else if (ext === '.md') docCount++;
|
||||||
|
});
|
||||||
|
|
||||||
|
lines.push(` - Code files: ${codeCount}`);
|
||||||
|
lines.push(` - Config files: ${configCount}`);
|
||||||
|
lines.push(` - Documentation: ${docCount}`);
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load template content
|
||||||
|
*/
|
||||||
|
function loadTemplate() {
|
||||||
|
const templatePath = join(
|
||||||
|
process.env.HOME || process.env.USERPROFILE,
|
||||||
|
'.claude/workflows/cli-templates/prompts/memory/02-document-module-structure.txt'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existsSync(templatePath)) {
|
||||||
|
return readFileSync(templatePath, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Create comprehensive CLAUDE.md documentation following standard structure with Purpose, Structure, Components, Dependencies, Integration, and Implementation sections.';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build CLI command
|
||||||
|
*/
|
||||||
|
function buildCliCommand(tool, prompt, model) {
|
||||||
|
const escapedPrompt = prompt.replace(/"/g, '\\"');
|
||||||
|
|
||||||
|
switch (tool) {
|
||||||
|
case 'qwen':
|
||||||
|
return model === 'coder-model'
|
||||||
|
? `qwen -p "${escapedPrompt}" --yolo`
|
||||||
|
: `qwen -p "${escapedPrompt}" -m "${model}" --yolo`;
|
||||||
|
case 'codex':
|
||||||
|
return `codex --full-auto exec "${escapedPrompt}" -m "${model}" --skip-git-repo-check -s danger-full-access`;
|
||||||
|
case 'gemini':
|
||||||
|
default:
|
||||||
|
return `gemini -p "${escapedPrompt}" -m "${model}" --yolo`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main execute function
|
||||||
|
*/
|
||||||
|
async function execute(params) {
|
||||||
|
const { strategy, path: modulePath, tool = 'gemini', model } = params;
|
||||||
|
|
||||||
|
// Validate parameters
|
||||||
|
if (!strategy) {
|
||||||
|
throw new Error('Parameter "strategy" is required. Valid: single-layer, multi-layer');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!['single-layer', 'multi-layer'].includes(strategy)) {
|
||||||
|
throw new Error(`Invalid strategy '${strategy}'. Valid: single-layer, multi-layer`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!modulePath) {
|
||||||
|
throw new Error('Parameter "path" is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetPath = resolve(process.cwd(), modulePath);
|
||||||
|
|
||||||
|
if (!existsSync(targetPath)) {
|
||||||
|
throw new Error(`Directory not found: ${targetPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!statSync(targetPath).isDirectory()) {
|
||||||
|
throw new Error(`Not a directory: ${targetPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if directory has files
|
||||||
|
const fileCount = countFiles(targetPath);
|
||||||
|
if (fileCount === 0) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Skipping '${modulePath}' - no files found`,
|
||||||
|
skipped: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set model
|
||||||
|
const actualModel = model || DEFAULT_MODELS[tool] || DEFAULT_MODELS.gemini;
|
||||||
|
|
||||||
|
// Load template
|
||||||
|
const templateContent = loadTemplate();
|
||||||
|
|
||||||
|
// Scan directory structure
|
||||||
|
const structureInfo = scanDirectoryStructure(targetPath, strategy);
|
||||||
|
|
||||||
|
// Build prompt based on strategy
|
||||||
|
let prompt;
|
||||||
|
if (strategy === 'multi-layer') {
|
||||||
|
prompt = `Directory Structure Analysis:
|
||||||
|
${structureInfo}
|
||||||
|
|
||||||
|
Read: @**/*
|
||||||
|
|
||||||
|
Generate CLAUDE.md files:
|
||||||
|
- Primary: ./CLAUDE.md (current directory)
|
||||||
|
- Additional: CLAUDE.md in each subdirectory containing files
|
||||||
|
|
||||||
|
Template Guidelines:
|
||||||
|
${templateContent}
|
||||||
|
|
||||||
|
Instructions:
|
||||||
|
- Work bottom-up: deepest directories first
|
||||||
|
- Parent directories reference children
|
||||||
|
- Each CLAUDE.md file must be in its respective directory
|
||||||
|
- Follow the template guidelines above for consistent structure`;
|
||||||
|
} else {
|
||||||
|
prompt = `Directory Structure Analysis:
|
||||||
|
${structureInfo}
|
||||||
|
|
||||||
|
Read: @*/CLAUDE.md @*.ts @*.tsx @*.js @*.jsx @*.py @*.sh @*.md @*.json @*.yaml @*.yml
|
||||||
|
|
||||||
|
Generate single file: ./CLAUDE.md
|
||||||
|
|
||||||
|
Template Guidelines:
|
||||||
|
${templateContent}
|
||||||
|
|
||||||
|
Instructions:
|
||||||
|
- Create exactly one CLAUDE.md file in the current directory
|
||||||
|
- Reference child CLAUDE.md files, do not duplicate their content
|
||||||
|
- Follow the template guidelines above for consistent structure`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build and execute command
|
||||||
|
const command = buildCliCommand(tool, prompt, actualModel);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
execSync(command, {
|
||||||
|
cwd: targetPath,
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: 'inherit',
|
||||||
|
timeout: 300000 // 5 minutes
|
||||||
|
});
|
||||||
|
|
||||||
|
const duration = Math.round((Date.now() - startTime) / 1000);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
strategy,
|
||||||
|
path: modulePath,
|
||||||
|
tool,
|
||||||
|
model: actualModel,
|
||||||
|
file_count: fileCount,
|
||||||
|
duration_seconds: duration,
|
||||||
|
message: `CLAUDE.md updated successfully in ${duration}s`
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
strategy,
|
||||||
|
path: modulePath,
|
||||||
|
tool,
|
||||||
|
model: actualModel,
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool Definition
|
||||||
|
*/
|
||||||
|
export const updateModuleClaudeTool = {
|
||||||
|
name: 'update_module_claude',
|
||||||
|
description: `Generate/update CLAUDE.md module documentation using CLI tools.
|
||||||
|
|
||||||
|
Strategies:
|
||||||
|
- single-layer: Read current dir code + child CLAUDE.md, generate ./CLAUDE.md
|
||||||
|
- multi-layer: Read all files, generate CLAUDE.md for each directory
|
||||||
|
|
||||||
|
Tools: gemini (default), qwen, codex`,
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
strategy: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['single-layer', 'multi-layer'],
|
||||||
|
description: 'Generation strategy'
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Module directory path'
|
||||||
|
},
|
||||||
|
tool: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['gemini', 'qwen', 'codex'],
|
||||||
|
description: 'CLI tool to use (default: gemini)',
|
||||||
|
default: 'gemini'
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Model name (optional, uses tool defaults)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['strategy', 'path']
|
||||||
|
},
|
||||||
|
execute
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user