Files
Claude-Code-Workflow/ccw/src/tools/update-module-claude.js
catlog22 1c3c070db4 fix(tools): 修复CLI工具多行prompt的shell转义问题
修复文件:
- update-module-claude.js
- generate-module-docs.js

主要修复:
- 使用临时文件+stdin管道传递prompt,避免shell转义问题
- 添加Windows PowerShell兼容性支持
- 添加执行日志输出便于调试
- 添加临时文件清理逻辑

技术细节:
- gemini/qwen: 使用 cat file | tool 方式
- codex: 使用 \ 命令替换
- Windows: 使用 Get-Content -Raw | tool
2025-12-08 21:56:41 +08:00

381 lines
11 KiB
JavaScript

/**
* Update Module CLAUDE.md Tool
* Generate/update CLAUDE.md module documentation using CLI tools
*/
import { readdirSync, statSync, existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
import { join, resolve, basename, extname } from 'path';
import { execSync } from 'child_process';
import { tmpdir } from 'os';
// 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.';
}
/**
* Create temporary prompt file and return cleanup function
*/
function createPromptFile(prompt) {
const timestamp = Date.now();
const randomSuffix = Math.random().toString(36).substring(2, 8);
const promptFile = join(tmpdir(), `claude-prompt-${timestamp}-${randomSuffix}.txt`);
writeFileSync(promptFile, prompt, 'utf8');
return promptFile;
}
/**
* Build CLI command using stdin piping for prompt (avoids shell escaping issues)
*/
function buildCliCommand(tool, promptFile, model) {
// Use stdin piping: cat file | tool or Get-Content | tool
// This avoids shell escaping issues with multiline prompts
const normalizedPath = promptFile.replace(/\\/g, '/');
const isWindows = process.platform === 'win32';
// Build the cat/read command based on platform
const catCmd = isWindows ? `Get-Content -Raw "${normalizedPath}" | ` : `cat "${normalizedPath}" | `;
switch (tool) {
case 'qwen':
return model === 'coder-model'
? `${catCmd}qwen --yolo`
: `${catCmd}qwen -m "${model}" --yolo`;
case 'codex':
// codex uses different syntax - prompt as exec argument
if (isWindows) {
return `codex --full-auto exec (Get-Content -Raw "${normalizedPath}") -m "${model}" --skip-git-repo-check -s danger-full-access`;
}
return `codex --full-auto exec "$(cat "${normalizedPath}")" -m "${model}" --skip-git-repo-check -s danger-full-access`;
case 'gemini':
default:
// gemini reads from stdin when no positional prompt is given
return `${catCmd}gemini -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
- Use the structure analysis to understand directory hierarchy`;
} 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
- Use the structure analysis to understand the current directory context`;
}
// Create temporary prompt file (avoids shell escaping issues with multiline prompts)
const promptFile = createPromptFile(prompt);
// Build command using file-based prompt
const command = buildCliCommand(tool, promptFile, actualModel);
// Log execution info
console.log(`⚡ Updating: ${modulePath}`);
console.log(` Strategy: ${strategy} | Tool: ${tool} | Model: ${actualModel} | Files: ${fileCount}`);
console.log(` Prompt file: ${promptFile}`);
try {
const startTime = Date.now();
execSync(command, {
cwd: targetPath,
encoding: 'utf8',
stdio: 'inherit',
timeout: 300000, // 5 minutes
shell: process.platform === 'win32' ? 'powershell.exe' : '/bin/bash'
});
const duration = Math.round((Date.now() - startTime) / 1000);
// Cleanup prompt file
try {
unlinkSync(promptFile);
} catch (e) {
// Ignore cleanup errors
}
console.log(` ✅ Completed in ${duration}s`);
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) {
// Cleanup prompt file on error
try {
unlinkSync(promptFile);
} catch (e) {
// Ignore cleanup errors
}
console.log(` ❌ Update failed: ${error.message}`);
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
};