mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-11 17:21:03 +08:00
修复codex调用无输出问题
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -14,7 +14,7 @@ yarn-error.log*
|
|||||||
|
|
||||||
# Package files
|
# Package files
|
||||||
*.tgz
|
*.tgz
|
||||||
|
docs-astro
|
||||||
# OS files
|
# OS files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|||||||
@@ -642,7 +642,8 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
|||||||
console.log(chalk.yellow(' Debug mode enabled\n'));
|
console.log(chalk.yellow(' Debug mode enabled\n'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority: 1. --file, 2. stdin (piped), 3. --prompt/-p option, 4. positional argument
|
// Priority: 1. --file, 2. --prompt/-p option, 3. positional argument, 4. stdin (piped)
|
||||||
|
// IMPORTANT: In host CLIs, stdin may be non-TTY and kept open. Reading fd 0 first can block.
|
||||||
// Note: On Windows, quoted arguments like -p "say hello" may be split into
|
// Note: On Windows, quoted arguments like -p "say hello" may be split into
|
||||||
// -p "say" and positional "hello". We merge them back together.
|
// -p "say" and positional "hello". We merge them back together.
|
||||||
let finalPrompt: string | undefined;
|
let finalPrompt: string | undefined;
|
||||||
@@ -661,29 +662,26 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
|||||||
console.error(chalk.red('Error: File is empty'));
|
console.error(chalk.red('Error: File is empty'));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} else if (!process.stdin.isTTY) {
|
} else {
|
||||||
// Read from stdin (piped input) - enables: echo "prompt" | ccw cli --tool gemini
|
|
||||||
// This bypasses Windows shell multi-line argument limitations
|
|
||||||
const { readFileSync } = await import('fs');
|
|
||||||
try {
|
|
||||||
finalPrompt = readFileSync(0, 'utf8').trim(); // fd 0 = stdin
|
|
||||||
if (debug) {
|
|
||||||
console.log(chalk.gray(` Read ${finalPrompt.length} chars from stdin`));
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// stdin not available or empty, fall through to other methods
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no stdin input, try --prompt/-p option or positional argument
|
|
||||||
if (!finalPrompt) {
|
|
||||||
if (optionPrompt) {
|
if (optionPrompt) {
|
||||||
// Use --prompt/-p option (preferred for multi-line)
|
// Use --prompt/-p option (preferred for multi-line)
|
||||||
// Merge with positional argument if Windows split the quoted string
|
// Merge with positional argument if Windows split the quoted string
|
||||||
finalPrompt = positionalPrompt ? `${optionPrompt} ${positionalPrompt}` : optionPrompt;
|
finalPrompt = positionalPrompt ? `${optionPrompt} ${positionalPrompt}` : optionPrompt;
|
||||||
} else {
|
} else if (positionalPrompt) {
|
||||||
// Fall back to positional argument
|
// Fall back to positional argument
|
||||||
finalPrompt = positionalPrompt;
|
finalPrompt = positionalPrompt;
|
||||||
|
} else if (!process.stdin.isTTY) {
|
||||||
|
// Read from stdin only when no explicit prompt input is provided
|
||||||
|
// (enables: echo "prompt" | ccw cli --tool gemini)
|
||||||
|
const { readFileSync } = await import('fs');
|
||||||
|
try {
|
||||||
|
finalPrompt = readFileSync(0, 'utf8').trim(); // fd 0 = stdin
|
||||||
|
if (debug) {
|
||||||
|
console.log(chalk.gray(` Read ${finalPrompt.length} chars from stdin`));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// stdin not available or empty, keep finalPrompt undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -904,14 +902,29 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
|||||||
const nativeMode = noNative ? ' (prompt-concat)' : '';
|
const nativeMode = noNative ? ' (prompt-concat)' : '';
|
||||||
const idInfo = id ? ` [${id}]` : '';
|
const idInfo = id ? ` [${id}]` : '';
|
||||||
|
|
||||||
|
const autoStream = !stream
|
||||||
|
&& !raw
|
||||||
|
&& !finalOnly
|
||||||
|
&& !process.stdout.isTTY
|
||||||
|
&& Boolean(process.env.CLAUDECODE);
|
||||||
|
const effectiveStream = Boolean(stream || autoStream);
|
||||||
|
const shouldPassthroughCodexJsonl = effectiveStream
|
||||||
|
&& tool === 'codex'
|
||||||
|
&& !raw
|
||||||
|
&& !finalOnly
|
||||||
|
&& !process.stdout.isTTY
|
||||||
|
&& Boolean(process.env.CLAUDECODE);
|
||||||
|
|
||||||
// Programmatic output mode:
|
// Programmatic output mode:
|
||||||
// - `--raw`: stdout/stderr passthrough semantics (minimal noise)
|
// - `--raw`: stdout/stderr passthrough semantics (minimal noise)
|
||||||
// - `--final`: agent-message only semantics (minimal noise)
|
// - `--final`: agent-message only semantics (minimal noise)
|
||||||
// - non-TTY stdout (e.g. called from another process): default to final-only unless `--stream` is used
|
// - non-TTY stdout (e.g. called from another process): default to final-only unless streaming is enabled
|
||||||
const programmaticOutput = Boolean(raw || finalOnly) || (!process.stdout.isTTY && !stream);
|
const programmaticOutput = shouldPassthroughCodexJsonl
|
||||||
|
|| Boolean(raw || finalOnly)
|
||||||
|
|| (!process.stdout.isTTY && !effectiveStream);
|
||||||
const showUi = !programmaticOutput;
|
const showUi = !programmaticOutput;
|
||||||
const useRawOutput = Boolean(raw);
|
const useRawOutput = Boolean(raw);
|
||||||
const useFinalOnlyOutput = Boolean(finalOnly) || (!useRawOutput && !process.stdout.isTTY && !stream);
|
const useFinalOnlyOutput = Boolean(finalOnly) || (!useRawOutput && !process.stdout.isTTY && !effectiveStream);
|
||||||
|
|
||||||
// Show merge details
|
// Show merge details
|
||||||
if (isMerge && showUi) {
|
if (isMerge && showUi) {
|
||||||
@@ -931,7 +944,7 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
|||||||
console.log();
|
console.log();
|
||||||
}
|
}
|
||||||
|
|
||||||
const spinner = (showUi && !stream) ? createSpinner(` ${spinnerBaseText}`).start() : null;
|
const spinner = (showUi && !effectiveStream) ? createSpinner(` ${spinnerBaseText}`).start() : null;
|
||||||
const elapsedInterval = spinner
|
const elapsedInterval = spinner
|
||||||
? setInterval(() => {
|
? setInterval(() => {
|
||||||
const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
|
const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
|
||||||
@@ -996,6 +1009,22 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
|||||||
// Buffer to accumulate output when both --stream and --to-file are specified
|
// Buffer to accumulate output when both --stream and --to-file are specified
|
||||||
let streamBuffer = '';
|
let streamBuffer = '';
|
||||||
|
|
||||||
|
const shouldSkipCodexPassthroughLine = (rawLine: string): boolean => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(rawLine) as {
|
||||||
|
type?: string;
|
||||||
|
item?: { type?: string };
|
||||||
|
item_type?: string;
|
||||||
|
};
|
||||||
|
if (!parsed || typeof parsed !== 'object') return false;
|
||||||
|
const eventType = parsed.type || '';
|
||||||
|
const itemType = parsed.item?.type || parsed.item_type || '';
|
||||||
|
return eventType.startsWith('item.') && itemType === 'command_execution';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Streaming output handler - broadcasts to dashboard AND writes to stdout
|
// Streaming output handler - broadcasts to dashboard AND writes to stdout
|
||||||
const onOutput = (unit: CliOutputUnit) => {
|
const onOutput = (unit: CliOutputUnit) => {
|
||||||
// Always broadcast to dashboard for real-time viewing
|
// Always broadcast to dashboard for real-time viewing
|
||||||
@@ -1010,8 +1039,17 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
|||||||
unit // New structured format
|
unit // New structured format
|
||||||
});
|
});
|
||||||
|
|
||||||
// Write to terminal only when --stream flag is passed
|
// Write to terminal when streaming is enabled (explicit --stream or auto-stream)
|
||||||
if (stream) {
|
if (effectiveStream) {
|
||||||
|
if (shouldPassthroughCodexJsonl && unit.rawLine) {
|
||||||
|
if (shouldSkipCodexPassthroughLine(unit.rawLine)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const line = `${unit.rawLine}\n`;
|
||||||
|
process.stdout.write(line);
|
||||||
|
if (toFile) streamBuffer += line;
|
||||||
|
return;
|
||||||
|
}
|
||||||
switch (unit.type) {
|
switch (unit.type) {
|
||||||
case 'stdout':
|
case 'stdout':
|
||||||
case 'code':
|
case 'code':
|
||||||
@@ -1060,7 +1098,7 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
|||||||
resume,
|
resume,
|
||||||
id: executionId, // unified execution ID (matches broadcast events)
|
id: executionId, // unified execution ID (matches broadcast events)
|
||||||
noNative,
|
noNative,
|
||||||
stream: !!stream, // stream=true → streaming enabled (no cache), stream=false → cache output (default)
|
stream: effectiveStream, // stream=true → streaming enabled (no cache), stream=false → cache output (default)
|
||||||
outputFormat, // Enable JSONL parsing for tools that support it
|
outputFormat, // Enable JSONL parsing for tools that support it
|
||||||
// Codex review options
|
// Codex review options
|
||||||
uncommitted,
|
uncommitted,
|
||||||
@@ -1086,7 +1124,7 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
|||||||
|
|
||||||
// If not streaming (default), print output now
|
// If not streaming (default), print output now
|
||||||
// Prefer parsedOutput (from stream parser) over raw stdout for better formatting
|
// Prefer parsedOutput (from stream parser) over raw stdout for better formatting
|
||||||
if (!stream) {
|
if (!effectiveStream) {
|
||||||
const output = useRawOutput
|
const output = useRawOutput
|
||||||
? result.stdout
|
? result.stdout
|
||||||
: (useFinalOnlyOutput ? (result.finalOutput || result.parsedOutput || result.stdout) : (result.parsedOutput || result.stdout));
|
: (useFinalOnlyOutput ? (result.finalOutput || result.parsedOutput || result.stdout) : (result.parsedOutput || result.stdout));
|
||||||
@@ -1123,7 +1161,7 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// Save streaming output to file if needed
|
// Save streaming output to file if needed
|
||||||
if (stream && toFile && streamBuffer) {
|
if (effectiveStream && toFile && streamBuffer) {
|
||||||
try {
|
try {
|
||||||
const { writeFileSync, mkdirSync } = await import('fs');
|
const { writeFileSync, mkdirSync } = await import('fs');
|
||||||
const { dirname, resolve } = await import('path');
|
const { dirname, resolve } = await import('path');
|
||||||
@@ -1156,7 +1194,7 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
|||||||
console.log(chalk.gray(` Total: ${result.conversation.turn_count} turns, ${(result.conversation.total_duration_ms / 1000).toFixed(1)}s`));
|
console.log(chalk.gray(` Total: ${result.conversation.turn_count} turns, ${(result.conversation.total_duration_ms / 1000).toFixed(1)}s`));
|
||||||
}
|
}
|
||||||
console.log(chalk.dim(` Continue: ccw cli -p "..." --resume ${result.execution.id}`));
|
console.log(chalk.dim(` Continue: ccw cli -p "..." --resume ${result.execution.id}`));
|
||||||
if (!stream) {
|
if (!effectiveStream) {
|
||||||
console.log(chalk.dim(` Output (optional): ccw cli output ${result.execution.id}`));
|
console.log(chalk.dim(` Output (optional): ccw cli output ${result.execution.id}`));
|
||||||
}
|
}
|
||||||
if (toFile) {
|
if (toFile) {
|
||||||
|
|||||||
@@ -250,6 +250,42 @@ export class DeepWikiService {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get DeepWiki storage statistics
|
||||||
|
*/
|
||||||
|
public getStats(): { files: number; symbols: number; docs: number } {
|
||||||
|
const db = this.getConnection();
|
||||||
|
if (!db) {
|
||||||
|
return { files: 0, symbols: 0, docs: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filesRow = db.prepare(`
|
||||||
|
SELECT COUNT(DISTINCT source_file) as count
|
||||||
|
FROM deepwiki_symbols
|
||||||
|
`).get() as { count: number } | undefined;
|
||||||
|
|
||||||
|
const symbolsRow = db.prepare(`
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM deepwiki_symbols
|
||||||
|
`).get() as { count: number } | undefined;
|
||||||
|
|
||||||
|
const docsRow = db.prepare(`
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM deepwiki_docs
|
||||||
|
`).get() as { count: number } | undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
files: filesRow?.count ?? 0,
|
||||||
|
symbols: symbolsRow?.count ?? 0,
|
||||||
|
docs: docsRow?.count ?? 0,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DeepWiki] Error getting stats:', error);
|
||||||
|
return { files: 0, symbols: 0, docs: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Singleton instance
|
// Singleton instance
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export interface CliOutputUnit<T = any> {
|
|||||||
type: CliOutputUnitType;
|
type: CliOutputUnitType;
|
||||||
content: T;
|
content: T;
|
||||||
timestamp: string; // ISO 8601 format
|
timestamp: string; // ISO 8601 format
|
||||||
|
rawLine?: string; // Original JSONL line (for pass-through streaming when needed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Parser Interface ==========
|
// ========== Parser Interface ==========
|
||||||
@@ -234,6 +235,7 @@ export class JsonLinesParser implements IOutputParser {
|
|||||||
// Map JSON structure to IR type
|
// Map JSON structure to IR type
|
||||||
const unit = this.mapJsonToIR(parsed, streamType);
|
const unit = this.mapJsonToIR(parsed, streamType);
|
||||||
if (unit) {
|
if (unit) {
|
||||||
|
unit.rawLine = trimmed;
|
||||||
units.push(unit);
|
units.push(unit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,6 +252,7 @@ export class JsonLinesParser implements IOutputParser {
|
|||||||
const parsed = JSON.parse(this.buffer.trim());
|
const parsed = JSON.parse(this.buffer.trim());
|
||||||
const unit = this.mapJsonToIR(parsed, 'stdout');
|
const unit = this.mapJsonToIR(parsed, 'stdout');
|
||||||
if (unit) {
|
if (unit) {
|
||||||
|
unit.rawLine = this.buffer.trim();
|
||||||
units.push(unit);
|
units.push(unit);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { after, afterEach, before, describe, it, mock } from 'node:test';
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import http from 'node:http';
|
import http from 'node:http';
|
||||||
import { existsSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
import { existsSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||||
|
import * as fs from 'node:fs';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import inquirer from 'inquirer';
|
import inquirer from 'inquirer';
|
||||||
@@ -187,6 +188,179 @@ describe('cli command module', async () => {
|
|||||||
assert.deepEqual(exitCodes, [0]);
|
assert.deepEqual(exitCodes, [0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('prefers --prompt over stdin when stdin is non-TTY (avoid blocking)', async () => {
|
||||||
|
stubHttpRequest();
|
||||||
|
mock.method(console, 'log', () => {});
|
||||||
|
mock.method(console, 'error', () => {});
|
||||||
|
|
||||||
|
const prevStdinIsTty = process.stdin.isTTY;
|
||||||
|
Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true });
|
||||||
|
|
||||||
|
const realReadFileSync = fs.readFileSync;
|
||||||
|
let stdinReadCount = 0;
|
||||||
|
mock.method(fs, 'readFileSync', ((pathLike: fs.PathOrFileDescriptor, ...args: unknown[]) => {
|
||||||
|
if (pathLike === 0) {
|
||||||
|
stdinReadCount += 1;
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return (realReadFileSync as unknown as (...all: unknown[]) => unknown)(pathLike, ...args);
|
||||||
|
}) as unknown as typeof fs.readFileSync);
|
||||||
|
|
||||||
|
const calls: any[] = [];
|
||||||
|
mock.method(cliExecutorModule.cliExecutorTool, 'execute', async (params: any) => {
|
||||||
|
calls.push(params);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
stdout: 'ok',
|
||||||
|
stderr: '',
|
||||||
|
execution: { id: 'EXEC-NONTTY', duration_ms: 1, status: 'success' },
|
||||||
|
conversation: { turn_count: 1, total_duration_ms: 1 },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const exitCodes: Array<number | undefined> = [];
|
||||||
|
mock.method(process as any, 'exit', (code?: number) => {
|
||||||
|
exitCodes.push(code);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await cliModule.cliCommand('exec', [], { prompt: 'Hello', tool: 'codex' });
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||||
|
} finally {
|
||||||
|
Object.defineProperty(process.stdin, 'isTTY', { value: prevStdinIsTty, configurable: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.equal(stdinReadCount, 0);
|
||||||
|
assert.equal(calls.length, 1);
|
||||||
|
assert.equal(calls[0].prompt, 'Hello');
|
||||||
|
assert.deepEqual(exitCodes, [0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('auto-enables stream in Claude Code task environment when stdout is non-TTY', async () => {
|
||||||
|
stubHttpRequest();
|
||||||
|
mock.method(console, 'log', () => {});
|
||||||
|
mock.method(console, 'error', () => {});
|
||||||
|
|
||||||
|
const prevStdoutIsTty = process.stdout.isTTY;
|
||||||
|
Object.defineProperty(process.stdout, 'isTTY', { value: false, configurable: true });
|
||||||
|
|
||||||
|
const prevClaudeCode = process.env.CLAUDECODE;
|
||||||
|
process.env.CLAUDECODE = '1';
|
||||||
|
|
||||||
|
const calls: any[] = [];
|
||||||
|
mock.method(cliExecutorModule.cliExecutorTool, 'execute', async (params: any) => {
|
||||||
|
calls.push(params);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
stdout: 'ok',
|
||||||
|
stderr: '',
|
||||||
|
execution: { id: 'EXEC-AUTO-STREAM', duration_ms: 1, status: 'success' },
|
||||||
|
conversation: { turn_count: 1, total_duration_ms: 1 },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const exitCodes: Array<number | undefined> = [];
|
||||||
|
mock.method(process as any, 'exit', (code?: number) => {
|
||||||
|
exitCodes.push(code);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await cliModule.cliCommand('exec', [], { prompt: 'Hello', tool: 'codex' });
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||||
|
} finally {
|
||||||
|
if (prevClaudeCode === undefined) {
|
||||||
|
delete process.env.CLAUDECODE;
|
||||||
|
} else {
|
||||||
|
process.env.CLAUDECODE = prevClaudeCode;
|
||||||
|
}
|
||||||
|
Object.defineProperty(process.stdout, 'isTTY', { value: prevStdoutIsTty, configurable: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.equal(calls.length, 1);
|
||||||
|
assert.equal(calls[0].stream, true);
|
||||||
|
assert.deepEqual(exitCodes, [0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes through codex JSONL events in Claude Code task streaming mode', async () => {
|
||||||
|
stubHttpRequest();
|
||||||
|
|
||||||
|
const logs: string[] = [];
|
||||||
|
mock.method(console, 'log', (...args: any[]) => {
|
||||||
|
logs.push(args.map(String).join(' '));
|
||||||
|
});
|
||||||
|
mock.method(console, 'error', () => {});
|
||||||
|
|
||||||
|
const writes: string[] = [];
|
||||||
|
mock.method(process.stdout as any, 'write', (chunk: any) => {
|
||||||
|
writes.push(String(chunk));
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const prevStdoutIsTty = process.stdout.isTTY;
|
||||||
|
Object.defineProperty(process.stdout, 'isTTY', { value: false, configurable: true });
|
||||||
|
|
||||||
|
const prevClaudeCode = process.env.CLAUDECODE;
|
||||||
|
process.env.CLAUDECODE = '1';
|
||||||
|
|
||||||
|
mock.method(cliExecutorModule.cliExecutorTool, 'execute', async (_params: any, onOutput?: (unit: any) => void) => {
|
||||||
|
onOutput?.({
|
||||||
|
type: 'metadata',
|
||||||
|
content: { tool: 'codex', threadId: 'THREAD-1' },
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
rawLine: '{"type":"thread.started","thread_id":"THREAD-1"}',
|
||||||
|
});
|
||||||
|
onOutput?.({
|
||||||
|
type: 'progress',
|
||||||
|
content: { message: 'Turn started', tool: 'codex' },
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
rawLine: '{"type":"turn.started"}',
|
||||||
|
});
|
||||||
|
onOutput?.({
|
||||||
|
type: 'progress',
|
||||||
|
content: { message: 'Executing: Get-ChildItem', tool: 'codex' },
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
rawLine: '{"type":"item.started","item":{"id":"item_cmd_1","type":"command_execution","status":"in_progress"}}',
|
||||||
|
});
|
||||||
|
onOutput?.({
|
||||||
|
type: 'code',
|
||||||
|
content: { command: 'Get-ChildItem', output: '...', status: 'completed' },
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
rawLine: '{"type":"item.completed","item":{"id":"item_cmd_1","type":"command_execution","status":"completed","exit_code":0}}',
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
stdout: '',
|
||||||
|
stderr: '',
|
||||||
|
execution: { id: 'EXEC-PASSTHROUGH', duration_ms: 1, status: 'success' },
|
||||||
|
conversation: { turn_count: 1, total_duration_ms: 1 },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const exitCodes: Array<number | undefined> = [];
|
||||||
|
mock.method(process as any, 'exit', (code?: number) => {
|
||||||
|
exitCodes.push(code);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await cliModule.cliCommand('exec', [], { prompt: 'Hello', tool: 'codex' });
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||||
|
} finally {
|
||||||
|
if (prevClaudeCode === undefined) {
|
||||||
|
delete process.env.CLAUDECODE;
|
||||||
|
} else {
|
||||||
|
process.env.CLAUDECODE = prevClaudeCode;
|
||||||
|
}
|
||||||
|
Object.defineProperty(process.stdout, 'isTTY', { value: prevStdoutIsTty, configurable: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const joined = writes.join('');
|
||||||
|
assert.ok(joined.includes('{"type":"thread.started","thread_id":"THREAD-1"}\n'));
|
||||||
|
assert.ok(joined.includes('{"type":"turn.started"}\n'));
|
||||||
|
assert.equal(joined.includes('"type":"command_execution"'), false);
|
||||||
|
assert.equal(logs.some((l) => l.includes('Executing codex')), false);
|
||||||
|
assert.deepEqual(exitCodes, [0]);
|
||||||
|
});
|
||||||
|
|
||||||
it('prints full output hint immediately after stderr truncation (no troubleshooting duplicate)', async () => {
|
it('prints full output hint immediately after stderr truncation (no troubleshooting duplicate)', async () => {
|
||||||
stubHttpRequest();
|
stubHttpRequest();
|
||||||
|
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
/**
|
|
||||||
* 版本同步脚本
|
|
||||||
* 用法:
|
|
||||||
* node scripts/sync-version.mjs # 检查版本状态
|
|
||||||
* node scripts/sync-version.mjs --sync # 同步到最新 npm 版本
|
|
||||||
* node scripts/sync-version.mjs --tag # 创建对应 git tag
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { execSync } from 'child_process';
|
|
||||||
import { readFileSync, writeFileSync } from 'fs';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import { dirname, join } from 'path';
|
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
||||||
const rootDir = join(__dirname, '..');
|
|
||||||
const packagePath = join(rootDir, 'package.json');
|
|
||||||
|
|
||||||
function run(cmd, silent = false) {
|
|
||||||
try {
|
|
||||||
return execSync(cmd, { cwd: rootDir, encoding: 'utf-8', stdio: silent ? 'pipe' : 'inherit' });
|
|
||||||
} catch (e) {
|
|
||||||
if (!silent) console.error(`Command failed: ${cmd}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function runSilent(cmd) {
|
|
||||||
try {
|
|
||||||
return execSync(cmd, { cwd: rootDir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLocalVersion() {
|
|
||||||
const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'));
|
|
||||||
return pkg.version;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNpmVersion() {
|
|
||||||
const result = runSilent('npm view claude-code-workflow version');
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLatestTag() {
|
|
||||||
const result = runSilent('git describe --tags --abbrev=0 2>/dev/null');
|
|
||||||
return result ? result.replace(/^v/, '') : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLocalVersion(version) {
|
|
||||||
const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'));
|
|
||||||
pkg.version = version;
|
|
||||||
writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n');
|
|
||||||
console.log(`✅ Updated package.json to ${version}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createTag(version) {
|
|
||||||
const tagName = `v${version}`;
|
|
||||||
run(`git tag -a ${tagName} -m "Release ${tagName}"`);
|
|
||||||
console.log(`✅ Created tag ${tagName}`);
|
|
||||||
console.log('💡 Run `git push origin ${tagName}` to push the tag');
|
|
||||||
}
|
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
|
||||||
const shouldSync = args.includes('--sync');
|
|
||||||
const shouldTag = args.includes('--tag');
|
|
||||||
|
|
||||||
console.log('📦 Version Status\n');
|
|
||||||
|
|
||||||
const localVersion = getLocalVersion();
|
|
||||||
const npmVersion = getNpmVersion();
|
|
||||||
const tagVersion = getLatestTag();
|
|
||||||
|
|
||||||
console.log(` Local (package.json): ${localVersion}`);
|
|
||||||
console.log(` NPM (latest): ${npmVersion || 'not published'}`);
|
|
||||||
console.log(` GitHub Tag (latest): ${tagVersion || 'no tags'}`);
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
const allVersions = [localVersion, npmVersion, tagVersion].filter(Boolean);
|
|
||||||
const allMatch = allVersions.every(v => v === allVersions[0]);
|
|
||||||
|
|
||||||
if (allMatch) {
|
|
||||||
console.log('✅ All versions are in sync!\n');
|
|
||||||
} else {
|
|
||||||
console.log('⚠️ Versions are out of sync!\n');
|
|
||||||
|
|
||||||
if (shouldSync && npmVersion) {
|
|
||||||
console.log(`Syncing to npm version: ${npmVersion}`);
|
|
||||||
setLocalVersion(npmVersion);
|
|
||||||
} else if (!shouldSync) {
|
|
||||||
console.log('💡 Run with --sync to sync local version to npm');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldTag && localVersion) {
|
|
||||||
const currentTag = `v${localVersion}`;
|
|
||||||
const existingTags = runSilent('git tag -l ' + currentTag);
|
|
||||||
|
|
||||||
if (existingTags) {
|
|
||||||
console.log(`⚠️ Tag ${currentTag} already exists`);
|
|
||||||
} else {
|
|
||||||
createTag(localVersion);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!shouldSync && !shouldTag && !allMatch) {
|
|
||||||
console.log('Suggested actions:');
|
|
||||||
if (npmVersion && localVersion !== npmVersion) {
|
|
||||||
console.log(` node scripts/sync-version.mjs --sync # Sync to npm ${npmVersion}`);
|
|
||||||
}
|
|
||||||
if (tagVersion !== localVersion) {
|
|
||||||
console.log(` node scripts/sync-version.mjs --tag # Create tag v${localVersion}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user