Skip to content

Commit 1c3c070

Browse files
author
catlog22
committed
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
1 parent 91e4792 commit 1c3c070

File tree

2 files changed

+125
-25
lines changed

2 files changed

+125
-25
lines changed

ccw/src/tools/generate-module-docs.js

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
* Generate documentation for modules and projects with multiple strategies
44
*/
55

6-
import { readdirSync, statSync, existsSync, readFileSync, mkdirSync } from 'fs';
6+
import { readdirSync, statSync, existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync } from 'fs';
77
import { join, resolve, basename, extname, relative } from 'path';
88
import { execSync } from 'child_process';
9+
import { tmpdir } from 'os';
910

1011
// Directories to exclude
1112
const EXCLUDE_DIRS = [
@@ -26,7 +27,7 @@ const DEFAULT_MODELS = {
2627
codex: 'gpt5-codex'
2728
};
2829

29-
// Template paths
30+
// Template paths (relative to user home directory)
3031
const TEMPLATE_BASE = '.claude/workflows/cli-templates/prompts/documentation';
3132

3233
/**
@@ -94,21 +95,40 @@ function loadTemplate(templateName) {
9495
}
9596

9697
/**
97-
* Build CLI command
98+
* Create temporary prompt file and return path
9899
*/
99-
function buildCliCommand(tool, prompt, model) {
100-
const escapedPrompt = prompt.replace(/"/g, '\\"');
100+
function createPromptFile(prompt) {
101+
const timestamp = Date.now();
102+
const randomSuffix = Math.random().toString(36).substring(2, 8);
103+
const promptFile = join(tmpdir(), `docs-prompt-${timestamp}-${randomSuffix}.txt`);
104+
writeFileSync(promptFile, prompt, 'utf8');
105+
return promptFile;
106+
}
101107

108+
/**
109+
* Build CLI command using stdin piping (avoids shell escaping issues)
110+
*/
111+
function buildCliCommand(tool, promptFile, model) {
112+
const normalizedPath = promptFile.replace(/\\/g, '/');
113+
const isWindows = process.platform === 'win32';
114+
115+
// Build the cat/read command based on platform
116+
const catCmd = isWindows ? `Get-Content -Raw "${normalizedPath}" | ` : `cat "${normalizedPath}" | `;
117+
102118
switch (tool) {
103119
case 'qwen':
104120
return model === 'coder-model'
105-
? `qwen -p "${escapedPrompt}" --yolo`
106-
: `qwen -p "${escapedPrompt}" -m "${model}" --yolo`;
121+
? `${catCmd}qwen --yolo`
122+
: `${catCmd}qwen -m "${model}" --yolo`;
107123
case 'codex':
108-
return `codex --full-auto exec "${escapedPrompt}" -m "${model}" --skip-git-repo-check -s danger-full-access`;
124+
// codex uses different syntax - prompt as exec argument
125+
if (isWindows) {
126+
return `codex --full-auto exec (Get-Content -Raw "${normalizedPath}") -m "${model}" --skip-git-repo-check -s danger-full-access`;
127+
}
128+
return `codex --full-auto exec "$(cat "${normalizedPath}")" -m "${model}" --skip-git-repo-check -s danger-full-access`;
109129
case 'gemini':
110130
default:
111-
return `gemini -p "${escapedPrompt}" -m "${model}" --yolo`;
131+
return `${catCmd}gemini -m "${model}" --yolo`;
112132
}
113133
}
114134

@@ -279,8 +299,17 @@ Output directory: .workflow/docs/${projectName}/api/`;
279299
break;
280300
}
281301

282-
// Build and execute command
283-
const command = buildCliCommand(tool, prompt, actualModel);
302+
// Create temporary prompt file (avoids shell escaping issues)
303+
const promptFile = createPromptFile(prompt);
304+
305+
// Build command using file-based prompt
306+
const command = buildCliCommand(tool, promptFile, actualModel);
307+
308+
// Log execution info
309+
console.log(`📚 Generating docs: ${sourcePath}`);
310+
console.log(` Strategy: ${strategy} | Tool: ${tool} | Model: ${actualModel}`);
311+
console.log(` Output: ${outputPath}`);
312+
console.log(` Prompt file: ${promptFile}`);
284313

285314
try {
286315
const startTime = Date.now();
@@ -289,11 +318,21 @@ Output directory: .workflow/docs/${projectName}/api/`;
289318
cwd: targetPath,
290319
encoding: 'utf8',
291320
stdio: 'inherit',
292-
timeout: 600000 // 10 minutes
321+
timeout: 600000, // 10 minutes
322+
shell: process.platform === 'win32' ? 'powershell.exe' : '/bin/bash'
293323
});
294324

295325
const duration = Math.round((Date.now() - startTime) / 1000);
296326

327+
// Cleanup prompt file
328+
try {
329+
unlinkSync(promptFile);
330+
} catch (e) {
331+
// Ignore cleanup errors
332+
}
333+
334+
console.log(` ✅ Completed in ${duration}s`);
335+
297336
return {
298337
success: true,
299338
strategy,
@@ -307,6 +346,15 @@ Output directory: .workflow/docs/${projectName}/api/`;
307346
message: `Documentation generated successfully in ${duration}s`
308347
};
309348
} catch (error) {
349+
// Cleanup prompt file on error
350+
try {
351+
unlinkSync(promptFile);
352+
} catch (e) {
353+
// Ignore cleanup errors
354+
}
355+
356+
console.log(` ❌ Generation failed: ${error.message}`);
357+
310358
return {
311359
success: false,
312360
strategy,

ccw/src/tools/update-module-claude.js

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
* Generate/update CLAUDE.md module documentation using CLI tools
44
*/
55

6-
import { readdirSync, statSync, existsSync, readFileSync } from 'fs';
6+
import { readdirSync, statSync, existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
77
import { join, resolve, basename, extname } from 'path';
88
import { execSync } from 'child_process';
9+
import { tmpdir } from 'os';
910

1011
// Directories to exclude
1112
const EXCLUDE_DIRS = [
@@ -147,21 +148,43 @@ function loadTemplate() {
147148
}
148149

149150
/**
150-
* Build CLI command
151+
* Create temporary prompt file and return cleanup function
151152
*/
152-
function buildCliCommand(tool, prompt, model) {
153-
const escapedPrompt = prompt.replace(/"/g, '\\"');
153+
function createPromptFile(prompt) {
154+
const timestamp = Date.now();
155+
const randomSuffix = Math.random().toString(36).substring(2, 8);
156+
const promptFile = join(tmpdir(), `claude-prompt-${timestamp}-${randomSuffix}.txt`);
157+
writeFileSync(promptFile, prompt, 'utf8');
158+
return promptFile;
159+
}
154160

161+
/**
162+
* Build CLI command using stdin piping for prompt (avoids shell escaping issues)
163+
*/
164+
function buildCliCommand(tool, promptFile, model) {
165+
// Use stdin piping: cat file | tool or Get-Content | tool
166+
// This avoids shell escaping issues with multiline prompts
167+
const normalizedPath = promptFile.replace(/\\/g, '/');
168+
const isWindows = process.platform === 'win32';
169+
170+
// Build the cat/read command based on platform
171+
const catCmd = isWindows ? `Get-Content -Raw "${normalizedPath}" | ` : `cat "${normalizedPath}" | `;
172+
155173
switch (tool) {
156174
case 'qwen':
157175
return model === 'coder-model'
158-
? `qwen -p "${escapedPrompt}" --yolo`
159-
: `qwen -p "${escapedPrompt}" -m "${model}" --yolo`;
176+
? `${catCmd}qwen --yolo`
177+
: `${catCmd}qwen -m "${model}" --yolo`;
160178
case 'codex':
161-
return `codex --full-auto exec "${escapedPrompt}" -m "${model}" --skip-git-repo-check -s danger-full-access`;
179+
// codex uses different syntax - prompt as exec argument
180+
if (isWindows) {
181+
return `codex --full-auto exec (Get-Content -Raw "${normalizedPath}") -m "${model}" --skip-git-repo-check -s danger-full-access`;
182+
}
183+
return `codex --full-auto exec "$(cat "${normalizedPath}")" -m "${model}" --skip-git-repo-check -s danger-full-access`;
162184
case 'gemini':
163185
default:
164-
return `gemini -p "${escapedPrompt}" -m "${model}" --yolo`;
186+
// gemini reads from stdin when no positional prompt is given
187+
return `${catCmd}gemini -m "${model}" --yolo`;
165188
}
166189
}
167190

@@ -232,7 +255,8 @@ Instructions:
232255
- Work bottom-up: deepest directories first
233256
- Parent directories reference children
234257
- Each CLAUDE.md file must be in its respective directory
235-
- Follow the template guidelines above for consistent structure`;
258+
- Follow the template guidelines above for consistent structure
259+
- Use the structure analysis to understand directory hierarchy`;
236260
} else {
237261
prompt = `Directory Structure Analysis:
238262
${structureInfo}
@@ -247,11 +271,20 @@ ${templateContent}
247271
Instructions:
248272
- Create exactly one CLAUDE.md file in the current directory
249273
- Reference child CLAUDE.md files, do not duplicate their content
250-
- Follow the template guidelines above for consistent structure`;
274+
- Follow the template guidelines above for consistent structure
275+
- Use the structure analysis to understand the current directory context`;
251276
}
252277

253-
// Build and execute command
254-
const command = buildCliCommand(tool, prompt, actualModel);
278+
// Create temporary prompt file (avoids shell escaping issues with multiline prompts)
279+
const promptFile = createPromptFile(prompt);
280+
281+
// Build command using file-based prompt
282+
const command = buildCliCommand(tool, promptFile, actualModel);
283+
284+
// Log execution info
285+
console.log(`⚡ Updating: ${modulePath}`);
286+
console.log(` Strategy: ${strategy} | Tool: ${tool} | Model: ${actualModel} | Files: ${fileCount}`);
287+
console.log(` Prompt file: ${promptFile}`);
255288

256289
try {
257290
const startTime = Date.now();
@@ -260,11 +293,21 @@ Instructions:
260293
cwd: targetPath,
261294
encoding: 'utf8',
262295
stdio: 'inherit',
263-
timeout: 300000 // 5 minutes
296+
timeout: 300000, // 5 minutes
297+
shell: process.platform === 'win32' ? 'powershell.exe' : '/bin/bash'
264298
});
265299

266300
const duration = Math.round((Date.now() - startTime) / 1000);
267301

302+
// Cleanup prompt file
303+
try {
304+
unlinkSync(promptFile);
305+
} catch (e) {
306+
// Ignore cleanup errors
307+
}
308+
309+
console.log(` ✅ Completed in ${duration}s`);
310+
268311
return {
269312
success: true,
270313
strategy,
@@ -276,6 +319,15 @@ Instructions:
276319
message: `CLAUDE.md updated successfully in ${duration}s`
277320
};
278321
} catch (error) {
322+
// Cleanup prompt file on error
323+
try {
324+
unlinkSync(promptFile);
325+
} catch (e) {
326+
// Ignore cleanup errors
327+
}
328+
329+
console.log(` ❌ Update failed: ${error.message}`);
330+
279331
return {
280332
success: false,
281333
strategy,

0 commit comments

Comments
 (0)