mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-27 09:13:07 +08:00
feat: 增加对 Windows CLI 工具的支持,允许使用 cmd 作为首选 shell,并改进错误处理
This commit is contained in:
@@ -4,4 +4,19 @@
|
||||
* Entry point for running CCW tools as an MCP server
|
||||
*/
|
||||
|
||||
import '../dist/mcp-server/index.js';
|
||||
// IMPORTANT:
|
||||
// MCP stdio servers must not write arbitrary text to stdout.
|
||||
// stdout is reserved for JSON-RPC protocol messages.
|
||||
// Redirect common console output to stderr to avoid breaking handshake.
|
||||
const toStderr = (...args) => console.error(...args);
|
||||
console.log = toStderr;
|
||||
console.info = toStderr;
|
||||
console.debug = toStderr;
|
||||
console.dir = toStderr;
|
||||
|
||||
try {
|
||||
await import('../dist/mcp-server/index.js');
|
||||
} catch (err) {
|
||||
console.error('[ccw-mcp] Failed to start MCP server:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/RadioGroup';
|
||||
|
||||
export type CliTool = 'claude' | 'gemini' | 'qwen' | 'codex' | 'opencode';
|
||||
export type LaunchMode = 'default' | 'yolo';
|
||||
export type ShellKind = 'bash' | 'pwsh';
|
||||
export type ShellKind = 'bash' | 'pwsh' | 'cmd';
|
||||
|
||||
export interface CliSessionConfig {
|
||||
tool: CliTool;
|
||||
@@ -69,7 +69,10 @@ export function CliConfigModal({
|
||||
const [tool, setTool] = React.useState<CliTool>('gemini');
|
||||
const [model, setModel] = React.useState<string | undefined>(MODEL_OPTIONS.gemini[0]);
|
||||
const [launchMode, setLaunchMode] = React.useState<LaunchMode>('yolo');
|
||||
const [preferredShell, setPreferredShell] = React.useState<ShellKind>('bash');
|
||||
// Default to 'cmd' on Windows for better compatibility with npm CLI tools (.cmd files)
|
||||
const [preferredShell, setPreferredShell] = React.useState<ShellKind>(
|
||||
typeof navigator !== 'undefined' && navigator.platform.toLowerCase().includes('win') ? 'cmd' : 'bash'
|
||||
);
|
||||
const [workingDir, setWorkingDir] = React.useState<string>(defaultWorkingDir ?? '');
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
||||
@@ -216,8 +219,9 @@ export function CliConfigModal({
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="bash">bash</SelectItem>
|
||||
<SelectItem value="pwsh">pwsh</SelectItem>
|
||||
<SelectItem value="cmd">cmd (推荐 Windows)</SelectItem>
|
||||
<SelectItem value="bash">bash (Git Bash/WSL)</SelectItem>
|
||||
<SelectItem value="pwsh">pwsh (PowerShell)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
import { useIssues, useIssueQueue } from '@/hooks/useIssues';
|
||||
import { useTerminalGridStore, selectTerminalGridFocusedPaneId } from '@/stores/terminalGridStore';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
import { toast } from '@/stores/notificationStore';
|
||||
import { CliConfigModal, type CliSessionConfig } from './CliConfigModal';
|
||||
|
||||
// ========== Types ==========
|
||||
@@ -79,6 +80,7 @@ const LAYOUT_PRESETS = [
|
||||
];
|
||||
|
||||
type LaunchMode = 'default' | 'yolo';
|
||||
type ShellKind = 'bash' | 'pwsh' | 'cmd';
|
||||
|
||||
const CLI_TOOLS = ['claude', 'gemini', 'qwen', 'codex', 'opencode'] as const;
|
||||
type CliTool = (typeof CLI_TOOLS)[number];
|
||||
@@ -124,6 +126,9 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [selectedTool, setSelectedTool] = useState<CliTool>('gemini');
|
||||
const [launchMode, setLaunchMode] = useState<LaunchMode>('yolo');
|
||||
const [selectedShell, setSelectedShell] = useState<ShellKind>(
|
||||
typeof navigator !== 'undefined' && navigator.platform.toLowerCase().includes('win') ? 'cmd' : 'bash'
|
||||
);
|
||||
const [isConfigOpen, setIsConfigOpen] = useState(false);
|
||||
|
||||
// Helper to get or create a focused pane
|
||||
@@ -140,18 +145,29 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen
|
||||
setIsCreating(true);
|
||||
try {
|
||||
const targetPaneId = getOrCreateFocusedPane();
|
||||
if (!targetPaneId) return;
|
||||
if (!targetPaneId) {
|
||||
toast.error('无法创建会话', '未能获取或创建窗格');
|
||||
return;
|
||||
}
|
||||
|
||||
await createSessionAndAssign(targetPaneId, {
|
||||
workingDir: projectPath,
|
||||
preferredShell: 'bash',
|
||||
preferredShell: selectedShell,
|
||||
tool: selectedTool,
|
||||
launchMode,
|
||||
}, projectPath);
|
||||
} catch (error: unknown) {
|
||||
// Handle both Error instances and ApiError-like objects
|
||||
const message = error instanceof Error
|
||||
? error.message
|
||||
: (error as { message?: string })?.message
|
||||
? (error as { message: string }).message
|
||||
: String(error);
|
||||
toast.error(`CLI 会话创建失败 (${selectedTool})`, message);
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
}, [projectPath, createSessionAndAssign, selectedTool, launchMode, getOrCreateFocusedPane]);
|
||||
}, [projectPath, createSessionAndAssign, selectedTool, selectedShell, launchMode, getOrCreateFocusedPane]);
|
||||
|
||||
const handleConfigure = useCallback(() => {
|
||||
setIsConfigOpen(true);
|
||||
@@ -164,7 +180,7 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen
|
||||
const targetPaneId = getOrCreateFocusedPane();
|
||||
if (!targetPaneId) throw new Error('Failed to create pane');
|
||||
|
||||
const created = await createSessionAndAssign(
|
||||
await createSessionAndAssign(
|
||||
targetPaneId,
|
||||
{
|
||||
workingDir: config.workingDir || projectPath,
|
||||
@@ -175,8 +191,15 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen
|
||||
},
|
||||
projectPath
|
||||
);
|
||||
|
||||
if (!created?.session?.sessionKey) throw new Error('createSessionAndAssign failed');
|
||||
} catch (error: unknown) {
|
||||
// Handle both Error instances and ApiError-like objects
|
||||
const message = error instanceof Error
|
||||
? error.message
|
||||
: (error as { message?: string })?.message
|
||||
? (error as { message: string }).message
|
||||
: String(error);
|
||||
toast.error(`CLI 会话创建失败 (${config.tool})`, message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
@@ -249,6 +272,31 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger className="gap-2">
|
||||
<span>{formatMessage({ id: 'terminalDashboard.toolbar.shell' })}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{selectedShell === 'cmd' ? 'cmd' : selectedShell === 'pwsh' ? 'pwsh' : 'bash'}
|
||||
</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuRadioGroup
|
||||
value={selectedShell}
|
||||
onValueChange={(v) => setSelectedShell(v as ShellKind)}
|
||||
>
|
||||
<DropdownMenuRadioItem value="cmd">
|
||||
cmd {formatMessage({ id: 'terminalDashboard.toolbar.shellCmdDesc' })}
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="bash">
|
||||
bash (Git Bash/WSL)
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="pwsh">
|
||||
pwsh (PowerShell)
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={handleQuickCreate}
|
||||
|
||||
@@ -152,7 +152,9 @@ async function fetchApi<T>(
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
try {
|
||||
const body = await response.json();
|
||||
// Check both 'message' and 'error' fields for error message
|
||||
if (body.message) error.message = body.message;
|
||||
else if (body.error) error.message = body.error;
|
||||
if (body.code) error.code = body.code;
|
||||
} catch (parseError) {
|
||||
// Silently ignore JSON parse errors for non-JSON responses
|
||||
@@ -6344,7 +6346,8 @@ export interface CreateCliSessionInput {
|
||||
workingDir?: string;
|
||||
cols?: number;
|
||||
rows?: number;
|
||||
preferredShell?: 'bash' | 'pwsh';
|
||||
/** Shell to use for spawning CLI tools on Windows. */
|
||||
preferredShell?: 'bash' | 'pwsh' | 'cmd';
|
||||
tool?: string;
|
||||
model?: string;
|
||||
resumeKey?: string;
|
||||
|
||||
@@ -83,6 +83,8 @@
|
||||
"mode": "Mode",
|
||||
"modeDefault": "Default",
|
||||
"modeYolo": "Yolo",
|
||||
"shell": "Shell",
|
||||
"shellCmdDesc": "(Recommended for Windows)",
|
||||
"quickCreate": "Quick Create",
|
||||
"configure": "Configure...",
|
||||
"fullscreen": "Fullscreen",
|
||||
|
||||
@@ -21,7 +21,14 @@
|
||||
"toolbar": {
|
||||
"refresh": "刷新",
|
||||
"clearAll": "清空所有",
|
||||
"settings": "设置"
|
||||
"settings": "设置",
|
||||
"back": "返回",
|
||||
"addExecution": "添加",
|
||||
"running": "运行中",
|
||||
"executions": "执行",
|
||||
"executionsList": "最近执行",
|
||||
"fullscreen": "全屏",
|
||||
"exitFullscreen": "退出全屏"
|
||||
},
|
||||
"emptyState": {
|
||||
"title": "暂无 CLI 执行",
|
||||
|
||||
@@ -83,6 +83,8 @@
|
||||
"mode": "模式",
|
||||
"modeDefault": "默认",
|
||||
"modeYolo": "Yolo",
|
||||
"shell": "Shell",
|
||||
"shellCmdDesc": "(推荐 Windows)",
|
||||
"quickCreate": "快速创建",
|
||||
"configure": "配置...",
|
||||
"fullscreen": "全屏",
|
||||
|
||||
@@ -341,9 +341,16 @@ export const useTerminalGridStore = create<TerminalGridStore>()(
|
||||
);
|
||||
|
||||
return { paneId: newPaneId, session };
|
||||
} catch (error) {
|
||||
console.error('Failed to create CLI session:', error);
|
||||
return null;
|
||||
} catch (error: unknown) {
|
||||
// Handle both Error instances and ApiError objects
|
||||
const errorMsg = error instanceof Error
|
||||
? error.message
|
||||
: (error as { message?: string })?.message
|
||||
? (error as { message: string }).message
|
||||
: String(error);
|
||||
console.error('Failed to create CLI session:', errorMsg, { config, projectPath, rawError: error });
|
||||
// Re-throw with meaningful message so UI can display it
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from 'path';
|
||||
|
||||
export type CliSessionShellKind = 'wsl-bash' | 'git-bash' | 'pwsh';
|
||||
export type CliSessionShellKind = 'wsl-bash' | 'git-bash' | 'pwsh' | 'cmd';
|
||||
|
||||
export type CliSessionResumeStrategy = 'nativeResume' | 'promptConcat';
|
||||
|
||||
|
||||
@@ -34,7 +34,8 @@ export interface CreateCliSessionOptions {
|
||||
workingDir: string;
|
||||
cols?: number;
|
||||
rows?: number;
|
||||
preferredShell?: 'bash' | 'pwsh';
|
||||
/** Shell to use for spawning CLI tools on Windows. */
|
||||
preferredShell?: 'bash' | 'pwsh' | 'cmd';
|
||||
tool?: string;
|
||||
model?: string;
|
||||
resumeKey?: string;
|
||||
@@ -224,10 +225,59 @@ export class CliSessionManager {
|
||||
// Native CLI interactive session: spawn the CLI process directly
|
||||
const launchMode = options.launchMode ?? 'default';
|
||||
const config = getLaunchConfig(options.tool, launchMode);
|
||||
shellKind = 'git-bash'; // PTY shell kind label (not actually a shell)
|
||||
file = config.command;
|
||||
args = config.args;
|
||||
cliTool = options.tool;
|
||||
|
||||
// Build the full command string with arguments
|
||||
const fullCommand = config.args.length > 0
|
||||
? `${config.command} ${config.args.join(' ')}`
|
||||
: config.command;
|
||||
|
||||
// On Windows, CLI tools installed via npm are typically .cmd files.
|
||||
// node-pty cannot spawn .cmd files directly, so we need a shell wrapper.
|
||||
// On Unix systems, direct spawn usually works.
|
||||
if (os.platform() === 'win32') {
|
||||
// Use user's preferred shell (default to cmd for reliability)
|
||||
const shell = options.preferredShell ?? 'cmd';
|
||||
|
||||
if (shell === 'cmd') {
|
||||
shellKind = 'cmd';
|
||||
file = 'cmd.exe';
|
||||
args = ['/c', fullCommand];
|
||||
} else if (shell === 'pwsh') {
|
||||
shellKind = 'pwsh';
|
||||
// Check for PowerShell Core (pwsh) or fall back to Windows PowerShell
|
||||
const pwshPath = spawnSync('where', ['pwsh'], { encoding: 'utf8', windowsHide: true });
|
||||
if (pwshPath.status === 0) {
|
||||
file = 'pwsh';
|
||||
} else {
|
||||
file = 'powershell';
|
||||
}
|
||||
args = ['-NoLogo', '-Command', fullCommand];
|
||||
} else {
|
||||
// bash - try git-bash or WSL
|
||||
const gitBash = findGitBashExe();
|
||||
if (gitBash) {
|
||||
shellKind = 'git-bash';
|
||||
file = gitBash;
|
||||
args = ['-l', '-i', '-c', fullCommand];
|
||||
} else if (isWslAvailable()) {
|
||||
shellKind = 'wsl-bash';
|
||||
file = 'wsl.exe';
|
||||
args = ['-e', 'bash', '-l', '-i', '-c', fullCommand];
|
||||
} else {
|
||||
// Fall back to cmd if no bash available
|
||||
shellKind = 'cmd';
|
||||
file = 'cmd.exe';
|
||||
args = ['/c', fullCommand];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Unix: direct spawn works for most CLI tools
|
||||
shellKind = 'git-bash';
|
||||
file = config.command;
|
||||
args = config.args;
|
||||
}
|
||||
|
||||
} else {
|
||||
// Legacy shell session: spawn bash/pwsh
|
||||
const preferredShell = options.preferredShell ?? 'bash';
|
||||
@@ -237,13 +287,21 @@ export class CliSessionManager {
|
||||
args = picked.args;
|
||||
}
|
||||
|
||||
const pty = nodePty.spawn(file, args, {
|
||||
name: 'xterm-256color',
|
||||
cols: options.cols ?? 120,
|
||||
rows: options.rows ?? 30,
|
||||
cwd: workingDir,
|
||||
env: process.env as Record<string, string>
|
||||
});
|
||||
let pty: nodePty.IPty;
|
||||
try {
|
||||
pty = nodePty.spawn(file, args, {
|
||||
name: 'xterm-256color',
|
||||
cols: options.cols ?? 120,
|
||||
rows: options.rows ?? 30,
|
||||
cwd: workingDir,
|
||||
env: process.env as Record<string, string>
|
||||
});
|
||||
} catch (spawnError: unknown) {
|
||||
const errorMsg = spawnError instanceof Error ? spawnError.message : String(spawnError);
|
||||
const toolInfo = options.tool ? `tool '${options.tool}' (` : '';
|
||||
const shellInfo = options.tool ? `)` : `shell '${file}'`;
|
||||
throw new Error(`Failed to spawn ${toolInfo}${shellInfo}: ${errorMsg}. Ensure the CLI tool is installed and available in PATH.`);
|
||||
}
|
||||
|
||||
const session: CliSessionInternal = {
|
||||
sessionKey,
|
||||
|
||||
Reference in New Issue
Block a user