clawt 3.8.10 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/README.zh-CN.md +4 -0
- package/dist/index.js +74 -9
- package/dist/postinstall.js +2 -2
- package/docs/merge.md +1 -1
- package/docs/resume.md +11 -5
- package/docs/run.md +1 -0
- package/docs/spec.md +6 -0
- package/package.json +1 -1
- package/src/constants/config.ts +5 -2
- package/src/constants/terminal.ts +1 -1
- package/src/types/config.ts +1 -1
- package/src/utils/shell.ts +3 -1
- package/src/utils/terminal.ts +121 -20
- package/tests/unit/utils/terminal-cmux.test.ts +288 -0
package/README.md
CHANGED
|
@@ -374,6 +374,10 @@ Hooks run asynchronously in the background (fire-and-forget), not blocking the m
|
|
|
374
374
|
|
|
375
375
|
> **Priority:** `--yes` > `CI` > `CLAWT_NON_INTERACTIVE` > default interactive mode
|
|
376
376
|
|
|
377
|
+
**Internally injected environment variables:**
|
|
378
|
+
|
|
379
|
+
All non-interactive Claude Code sessions launched via `claude -p` (task-executor and conflict-resolver) automatically inject the environment variable `CLAUDE_CODE_ENTRYPOINT="cli"`, which enables these sessions to be resumed via `--continue`. This does not apply to interactive Claude Code sessions (e.g., `clawt resume`).
|
|
380
|
+
|
|
377
381
|
## Logs
|
|
378
382
|
|
|
379
383
|
Logs are saved in `~/.clawt/logs/`, rotated by date, retained for 30 days.
|
package/README.zh-CN.md
CHANGED
|
@@ -374,6 +374,10 @@ hook 以 fire-and-forget 模式后台异步并行执行,不阻塞主流程。
|
|
|
374
374
|
|
|
375
375
|
> **优先级:** `--yes` > `CI` > `CLAWT_NON_INTERACTIVE` > 默认交互模式
|
|
376
376
|
|
|
377
|
+
**内部注入的环境变量:**
|
|
378
|
+
|
|
379
|
+
所有通过 `claude -p` 启动的非交互式 Claude Code 子进程(task-executor 和 conflict-resolver)会自动注入环境变量 `CLAUDE_CODE_ENTRYPOINT="cli"`,使这些会话支持通过 `--continue` 恢复。不适用于交互式启动 Claude Code 的场景(如 `clawt resume`)。
|
|
380
|
+
|
|
377
381
|
## 日志
|
|
378
382
|
|
|
379
383
|
日志保存在 `~/.clawt/logs/`,按日期滚动,保留 30 天。
|
package/dist/index.js
CHANGED
|
@@ -703,7 +703,7 @@ var EXIT_CODES = {
|
|
|
703
703
|
};
|
|
704
704
|
|
|
705
705
|
// src/constants/terminal.ts
|
|
706
|
-
var VALID_TERMINAL_APPS = ["auto", "iterm2", "terminal"];
|
|
706
|
+
var VALID_TERMINAL_APPS = ["auto", "iterm2", "terminal", "cmux"];
|
|
707
707
|
var ITERM2_APP_PATH = "/Applications/iTerm.app";
|
|
708
708
|
|
|
709
709
|
// src/constants/config.ts
|
|
@@ -732,7 +732,7 @@ var CONFIG_DEFINITIONS = {
|
|
|
732
732
|
},
|
|
733
733
|
terminalApp: {
|
|
734
734
|
defaultValue: "auto",
|
|
735
|
-
description: "\u6279\u91CF resume \u4F7F\u7528\u7684\u7EC8\u7AEF\u5E94\u7528\uFF1Aauto\uFF08\u81EA\u52A8\u68C0\u6D4B\uFF09\u3001iterm2\u3001terminal\uFF08macOS\uFF09",
|
|
735
|
+
description: "\u6279\u91CF resume \u4F7F\u7528\u7684\u7EC8\u7AEF\u5E94\u7528\uFF1Aauto\uFF08\u81EA\u52A8\u68C0\u6D4B\uFF09\u3001iterm2\u3001terminal\u3001cmux\uFF08macOS\uFF09",
|
|
736
736
|
allowedValues: VALID_TERMINAL_APPS
|
|
737
737
|
},
|
|
738
738
|
resumeInPlace: {
|
|
@@ -2015,14 +2015,20 @@ import { existsSync as existsSync6 } from "fs";
|
|
|
2015
2015
|
function isITerm2Installed() {
|
|
2016
2016
|
return existsSync6(ITERM2_APP_PATH);
|
|
2017
2017
|
}
|
|
2018
|
+
function isCmuxEnvironment() {
|
|
2019
|
+
return !!process.env.CMUX_WORKSPACE_ID;
|
|
2020
|
+
}
|
|
2018
2021
|
function detectTerminalApp() {
|
|
2019
2022
|
const configured = getConfigValue("terminalApp");
|
|
2020
|
-
if (configured === "iterm2" || configured === "terminal") {
|
|
2023
|
+
if (configured === "iterm2" || configured === "terminal" || configured === "cmux") {
|
|
2021
2024
|
return configured;
|
|
2022
2025
|
}
|
|
2023
2026
|
if (!VALID_TERMINAL_APPS.includes(configured)) {
|
|
2024
2027
|
logger.warn(`terminalApp \u914D\u7F6E\u503C "${configured}" \u65E0\u6548\uFF0C\u6709\u6548\u503C: ${VALID_TERMINAL_APPS.join(", ")}\uFF0C\u5C06\u4F7F\u7528\u81EA\u52A8\u68C0\u6D4B`);
|
|
2025
2028
|
}
|
|
2029
|
+
if (isCmuxEnvironment()) {
|
|
2030
|
+
return "cmux";
|
|
2031
|
+
}
|
|
2026
2032
|
if (isITerm2Installed()) {
|
|
2027
2033
|
return "iterm2";
|
|
2028
2034
|
}
|
|
@@ -2060,14 +2066,52 @@ tell application "iTerm"
|
|
|
2060
2066
|
end tell
|
|
2061
2067
|
`.trim();
|
|
2062
2068
|
}
|
|
2063
|
-
function
|
|
2064
|
-
if (
|
|
2065
|
-
throw new ClawtError(
|
|
2069
|
+
function openCommandInCmuxSurface(command, title) {
|
|
2070
|
+
if (!isCmuxEnvironment()) {
|
|
2071
|
+
throw new ClawtError(
|
|
2072
|
+
"\u5F53\u524D\u4E0D\u5728 cmux \u73AF\u5883\u4E2D\uFF0C\u65E0\u6CD5\u521B\u5EFA surface\n\u8BF7\u786E\u4FDD\u5728 cmux \u7EC8\u7AEF\u4E2D\u6267\u884C clawt resume \u547D\u4EE4\uFF0C\u6216\u4FEE\u6539 terminalApp \u914D\u7F6E"
|
|
2073
|
+
);
|
|
2066
2074
|
}
|
|
2067
|
-
|
|
2068
|
-
const script = terminalApp === "iterm2" ? buildITermAppleScript(command, tabTitle) : buildTerminalAppleScript(command, tabTitle);
|
|
2069
|
-
logger.debug(`\u6253\u5F00\u7EC8\u7AEF Tab [${terminalApp}]: ${tabTitle}`);
|
|
2075
|
+
logger.debug(`\u5728 cmux \u4E2D\u521B\u5EFA\u65B0 surface: ${title}`);
|
|
2070
2076
|
logger.debug(`\u6267\u884C\u547D\u4EE4: ${command}`);
|
|
2077
|
+
try {
|
|
2078
|
+
const newSurfaceResult = execFileSync3("cmux", [
|
|
2079
|
+
"new-split",
|
|
2080
|
+
"right"
|
|
2081
|
+
// 在右侧创建新 surface
|
|
2082
|
+
], {
|
|
2083
|
+
encoding: "utf-8",
|
|
2084
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2085
|
+
timeout: 5e3
|
|
2086
|
+
// 5秒超时
|
|
2087
|
+
});
|
|
2088
|
+
logger.debug(`new-split \u8F93\u51FA: ${newSurfaceResult}`);
|
|
2089
|
+
const match = newSurfaceResult.match(/(?:OK\s+)?(surface:\d+)/i);
|
|
2090
|
+
if (!match) {
|
|
2091
|
+
throw new Error(`\u65E0\u6CD5\u89E3\u6790 cmux new-split \u8F93\u51FA: ${newSurfaceResult}`);
|
|
2092
|
+
}
|
|
2093
|
+
const surfaceRef = match[1];
|
|
2094
|
+
logger.debug(`\u5DF2\u521B\u5EFA surface: ${surfaceRef}`);
|
|
2095
|
+
execFileSync3("cmux", [
|
|
2096
|
+
"send",
|
|
2097
|
+
"--surface",
|
|
2098
|
+
surfaceRef,
|
|
2099
|
+
`${command}\\n`
|
|
2100
|
+
// 追加换行符以自动执行命令
|
|
2101
|
+
], {
|
|
2102
|
+
encoding: "utf-8",
|
|
2103
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2104
|
+
timeout: 5e3
|
|
2105
|
+
});
|
|
2106
|
+
logger.debug(`\u5DF2\u5411 ${surfaceRef} \u53D1\u9001\u547D\u4EE4`);
|
|
2107
|
+
} catch (error) {
|
|
2108
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2109
|
+
throw new ClawtError(`\u5728 cmux \u4E2D\u521B\u5EFA surface \u5931\u8D25: ${message}`);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
function executeAppleScript(script, terminalApp) {
|
|
2113
|
+
logger.debug(`\u6253\u5F00\u7EC8\u7AEF Tab [${terminalApp}]`);
|
|
2114
|
+
logger.debug(`\u6267\u884C AppleScript`);
|
|
2071
2115
|
try {
|
|
2072
2116
|
execFileSync3("osascript", ["-e", script], {
|
|
2073
2117
|
encoding: "utf-8",
|
|
@@ -2079,6 +2123,27 @@ function openCommandInNewTerminalTab(command, tabTitle) {
|
|
|
2079
2123
|
throw new ClawtError(`\u6253\u5F00\u7EC8\u7AEF Tab \u5931\u8D25: ${message}${accessibilityHint}`);
|
|
2080
2124
|
}
|
|
2081
2125
|
}
|
|
2126
|
+
function openCommandInNewTerminalTab(command, tabTitle) {
|
|
2127
|
+
if (process.platform !== "darwin") {
|
|
2128
|
+
throw new ClawtError("\u6279\u91CF resume \u76EE\u524D\u4EC5\u652F\u6301 macOS \u5E73\u53F0");
|
|
2129
|
+
}
|
|
2130
|
+
const terminalApp = detectTerminalApp();
|
|
2131
|
+
switch (terminalApp) {
|
|
2132
|
+
case "cmux":
|
|
2133
|
+
openCommandInCmuxSurface(command, tabTitle);
|
|
2134
|
+
break;
|
|
2135
|
+
case "iterm2":
|
|
2136
|
+
const itermScript = buildITermAppleScript(command, tabTitle);
|
|
2137
|
+
executeAppleScript(itermScript, "iterm2");
|
|
2138
|
+
break;
|
|
2139
|
+
case "terminal":
|
|
2140
|
+
const terminalScript = buildTerminalAppleScript(command, tabTitle);
|
|
2141
|
+
executeAppleScript(terminalScript, "terminal");
|
|
2142
|
+
break;
|
|
2143
|
+
default:
|
|
2144
|
+
throw new ClawtError(`\u4E0D\u652F\u6301\u7684\u7EC8\u7AEF\u7C7B\u578B: ${terminalApp}`);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2082
2147
|
|
|
2083
2148
|
// src/utils/claude.ts
|
|
2084
2149
|
function encodeClaudeProjectPath(absolutePath) {
|
package/dist/postinstall.js
CHANGED
|
@@ -641,7 +641,7 @@ var MESSAGES = {
|
|
|
641
641
|
};
|
|
642
642
|
|
|
643
643
|
// src/constants/terminal.ts
|
|
644
|
-
var VALID_TERMINAL_APPS = ["auto", "iterm2", "terminal"];
|
|
644
|
+
var VALID_TERMINAL_APPS = ["auto", "iterm2", "terminal", "cmux"];
|
|
645
645
|
|
|
646
646
|
// src/constants/config.ts
|
|
647
647
|
var CONFIG_DEFINITIONS = {
|
|
@@ -667,7 +667,7 @@ var CONFIG_DEFINITIONS = {
|
|
|
667
667
|
},
|
|
668
668
|
terminalApp: {
|
|
669
669
|
defaultValue: "auto",
|
|
670
|
-
description: "\u6279\u91CF resume \u4F7F\u7528\u7684\u7EC8\u7AEF\u5E94\u7528\uFF1Aauto\uFF08\u81EA\u52A8\u68C0\u6D4B\uFF09\u3001iterm2\u3001terminal\uFF08macOS\uFF09",
|
|
670
|
+
description: "\u6279\u91CF resume \u4F7F\u7528\u7684\u7EC8\u7AEF\u5E94\u7528\uFF1Aauto\uFF08\u81EA\u52A8\u68C0\u6D4B\uFF09\u3001iterm2\u3001terminal\u3001cmux\uFF08macOS\uFF09",
|
|
671
671
|
allowedValues: VALID_TERMINAL_APPS
|
|
672
672
|
},
|
|
673
673
|
resumeInPlace: {
|
package/docs/merge.md
CHANGED
|
@@ -104,7 +104,7 @@ clawt merge [-m <commitMessage>]
|
|
|
104
104
|
| `auto` | — | 直接调用 AI 解决,不询问 |
|
|
105
105
|
| `manual` | — | 输出冲突提示信息,用户手动解决 |
|
|
106
106
|
|
|
107
|
-
AI 解决冲突时,调用 Claude Code CLI 在主 worktree 中分析并解决冲突文件,超时时间由配置项 `conflictResolveTimeoutMs` 控制(默认 15
|
|
107
|
+
AI 解决冲突时,调用 Claude Code CLI 在主 worktree 中分析并解决冲突文件,超时时间由配置项 `conflictResolveTimeoutMs` 控制(默认 15 分钟)。冲突解决子进程通过 `getEnvWithoutNestedSessionFlag()` 启动,会自动注入环境变量 `CLAUDE_CODE_ENTRYPOINT="cli"` 使会话支持 `--continue` 恢复。AI 解决成功后自动执行 `git add . && git merge --continue` 完成合并。
|
|
108
108
|
10. **推送(受 `autoPullPush` 配置控制)**
|
|
109
109
|
- `autoPullPush` 为 `false` → 输出提示 `已跳过自动 pull/push,请手动执行 git pull && git push`
|
|
110
110
|
- `autoPullPush` 为 `true` → 执行 `git pull` + `git push`:
|
package/docs/resume.md
CHANGED
|
@@ -88,13 +88,19 @@ clawt resume -f tasks.md -c 2
|
|
|
88
88
|
|
|
89
89
|
| 配置值 | 行为 |
|
|
90
90
|
| ---------- | ------------------------------------------------------------ |
|
|
91
|
-
| `auto` | 自动检测:优先检测 iTerm2
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
91
|
+
| `auto` | 自动检测:优先检测 cmux 环境(通过 `CMUX_PANEL_ID` 环境变量),在 cmux 环境时在当前 pane 创建新 surface;否则检测 iTerm2 是否已安装,已安装则使用 iTerm2,否则降级到 Terminal.app |
|
|
92
|
+
| `cmux` | 在当前 cmux pane 中创建新 surface 执行命令 |
|
|
93
|
+
| `iterm2` | 强制使用 iTerm2 创建新 Tab |
|
|
94
|
+
| `terminal` | 强制使用 Terminal.app 创建新 Tab |
|
|
94
95
|
|
|
95
|
-
**平台限制:** 批量恢复目前仅支持 macOS
|
|
96
|
+
**平台限制:** 批量恢复目前仅支持 macOS 平台。非 macOS 平台会抛出错误。
|
|
96
97
|
|
|
97
|
-
|
|
98
|
+
**cmux 集成说明:**
|
|
99
|
+
- cmux 终端通过 `cmux new-surface --type terminal --pane <pane-id>` 创建新 surface
|
|
100
|
+
- 创建后通过 `cmux send --surface <surface-id> <command>` 发送启动命令
|
|
101
|
+
- 环境变量 `CMUX_PANEL_ID` 用于识别当前 pane,`CMUX_SURFACE_ID` 作为回退
|
|
102
|
+
|
|
103
|
+
**权限要求:** Terminal.app 通过 System Events 模拟键盘操作(`Cmd+T`)新建 Tab,需要在「系统设置 → 隐私与安全性 → 辅助功能」中授权终端应用。iTerm2 使用原生 AppleScript 接口,无需辅助功能权限。cmux 通过 CLI 命令操作,无需特殊权限。
|
|
98
104
|
|
|
99
105
|
启动命令通过配置项 `claudeCodeCommand`(默认值 `claude`)指定,与 `clawt run` 不传 `--tasks` 时的交互式界面行为一致。
|
|
100
106
|
|
package/docs/run.md
CHANGED
|
@@ -89,6 +89,7 @@ clawt run -b <branchName>
|
|
|
89
89
|
claude -p "<tasks[i]>" --output-format stream-json --verbose --permission-mode bypassPermissions --append-system-prompt "<系统提示>"
|
|
90
90
|
```
|
|
91
91
|
其中 `--append-system-prompt` 使用统一的 `APPEND_SYSTEM_PROMPT` 常量(定义在 `src/constants/config.ts`)。
|
|
92
|
+
子进程通过 `spawnProcess()`(`src/utils/shell.ts`)启动,会自动注入环境变量 `CLAUDE_CODE_ENTRYPOINT="cli"`(通过 `getEnvWithoutNestedSessionFlag()` 函数),使会话支持通过 `--continue` 恢复。
|
|
92
93
|
使用 `stream-json` 格式可实时获取 Claude Code 的流式事件(工具调用、文本输出、最终结果),用于在进度面板中显示每个任务的实时活动描述和结果预览。流式事件解析由 `src/utils/stream-parser.ts` 负责。
|
|
93
94
|
6. 进入**事件监听通知**阶段(见 [5.3](#53-任务完成通知机制))
|
|
94
95
|
7. **中断处理(Ctrl+C / SIGINT)**
|
package/docs/spec.md
CHANGED
|
@@ -316,6 +316,12 @@ async function interactiveConfigEditor<T extends object>(
|
|
|
316
316
|
|
|
317
317
|
> **非交互模式判断优先级:** CLI `--yes` 选项 > `CI` 环境变量 > `CLAWT_NON_INTERACTIVE` 环境变量 > 默认交互模式。实现见 `src/utils/interactive.ts`。
|
|
318
318
|
|
|
319
|
+
**Clawt 内部注入的环境变量:**
|
|
320
|
+
|
|
321
|
+
| 环境变量 | 值 | 说明 |
|
|
322
|
+
| -------- | --- | ---- |
|
|
323
|
+
| `CLAUDE_CODE_ENTRYPOINT` | `cli` | 所有通过 `claude -p` 启动的非交互式 Claude Code 子进程(task-executor 和 conflict-resolver)会自动注入此环境变量,使这些会话支持通过 `--continue` 恢复。常量定义在 `src/constants/config.ts` 的 `CLAUDE_CODE_ENTRYPOINT_VALUE`,注入逻辑在 `src/utils/shell.ts` 的 `getEnvWithoutNestedSessionFlag()` 函数中实现。不适用于交互式启动 Claude Code 的场景(如 `clawt resume`)。 |
|
|
324
|
+
|
|
319
325
|
所有命令执行前,都必须先执行**主 worktree 校验**(见 [2.1](#21-主-worktree-的定义与定位规则))。
|
|
320
326
|
|
|
321
327
|
---
|
package/package.json
CHANGED
package/src/constants/config.ts
CHANGED
|
@@ -5,7 +5,10 @@ import { VALID_TERMINAL_APPS } from './terminal.js';
|
|
|
5
5
|
export const APPEND_SYSTEM_PROMPT =
|
|
6
6
|
'Currently, you are in the git worktree directory.';
|
|
7
7
|
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* 通过 clawt 启动的 Claude Code 非交互式会话(claude -p)的 entrypoint 标识
|
|
10
|
+
* 设置为 'cli' 使 claude -p 启动的会话可以通过 --continue 恢复
|
|
11
|
+
*/
|
|
9
12
|
export const CLAUDE_CODE_ENTRYPOINT_VALUE = 'cli';
|
|
10
13
|
|
|
11
14
|
/**
|
|
@@ -35,7 +38,7 @@ export const CONFIG_DEFINITIONS: ConfigDefinitions = {
|
|
|
35
38
|
},
|
|
36
39
|
terminalApp: {
|
|
37
40
|
defaultValue: 'auto',
|
|
38
|
-
description: '批量 resume 使用的终端应用:auto(自动检测)、iterm2、terminal(macOS)',
|
|
41
|
+
description: '批量 resume 使用的终端应用:auto(自动检测)、iterm2、terminal、cmux(macOS)',
|
|
39
42
|
allowedValues: VALID_TERMINAL_APPS,
|
|
40
43
|
},
|
|
41
44
|
resumeInPlace: {
|
|
@@ -13,7 +13,7 @@ export const DISABLE_BRACKETED_PASTE = '\x1b[?2004l';
|
|
|
13
13
|
export const PASTE_THRESHOLD_MS = 10;
|
|
14
14
|
|
|
15
15
|
/** 配置项 terminalApp 允许的有效值 */
|
|
16
|
-
export const VALID_TERMINAL_APPS: readonly string[] = ['auto', 'iterm2', 'terminal'];
|
|
16
|
+
export const VALID_TERMINAL_APPS: readonly string[] = ['auto', 'iterm2', 'terminal', 'cmux'];
|
|
17
17
|
|
|
18
18
|
/** iTerm2 应用路径,用于 auto 模式检测是否已安装 */
|
|
19
19
|
export const ITERM2_APP_PATH = '/Applications/iTerm.app';
|
package/src/types/config.ts
CHANGED
|
@@ -10,7 +10,7 @@ export interface ClawtConfig {
|
|
|
10
10
|
confirmDestructiveOps: boolean;
|
|
11
11
|
/** run 命令默认最大并发数,0 表示不限制 */
|
|
12
12
|
maxConcurrency: number;
|
|
13
|
-
/** 批量 resume 使用的终端应用:'auto'(自动检测)、'iterm2'、'terminal'(macOS) */
|
|
13
|
+
/** 批量 resume 使用的终端应用:'auto'(自动检测)、'iterm2'、'terminal'、'cmux'(macOS) */
|
|
14
14
|
terminalApp: string;
|
|
15
15
|
/** resume 单选时是否在当前终端就地打开,false 则通过 terminalApp 在新 Tab 中打开 */
|
|
16
16
|
resumeInPlace: boolean;
|
package/src/utils/shell.ts
CHANGED
|
@@ -6,7 +6,9 @@ import { throwIfGitIndexLockError } from './git-lock.js';
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* 获取移除了 CLAUDECODE 嵌套会话标记的环境变量副本,并注入 CLAUDE_CODE_ENTRYPOINT 标识
|
|
9
|
-
* 仅用于 claude -p
|
|
9
|
+
* 仅用于 claude -p 等非交互式子进程:
|
|
10
|
+
* - 移除 CLAUDECODE:避免被 Claude Code 误判为嵌套会话而拒绝启动
|
|
11
|
+
* - 注入 CLAUDE_CODE_ENTRYPOINT="cli":使 claude -p 会话支持 --continue 恢复
|
|
10
12
|
* 不适用于交互式启动 Claude Code(如 clawt resume),交互式场景应保留原始环境变量
|
|
11
13
|
* @returns {NodeJS.ProcessEnv} 移除 CLAUDECODE 并注入 CLAUDE_CODE_ENTRYPOINT 后的环境变量
|
|
12
14
|
*/
|
package/src/utils/terminal.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { VALID_TERMINAL_APPS, ITERM2_APP_PATH } from '../constants/index.js';
|
|
|
6
6
|
import { getConfigValue } from './config.js';
|
|
7
7
|
|
|
8
8
|
/** 终端应用类型 */
|
|
9
|
-
type TerminalApp = 'iterm2' | 'terminal';
|
|
9
|
+
type TerminalApp = 'iterm2' | 'terminal' | 'cmux';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* 检测系统是否安装了 iTerm2
|
|
@@ -17,17 +17,28 @@ function isITerm2Installed(): boolean {
|
|
|
17
17
|
return existsSync(ITERM2_APP_PATH);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* 检测当前是否在 cmux 环境中运行
|
|
22
|
+
* 通过检查环境变量 CMUX_WORKSPACE_ID 是否存在来判断
|
|
23
|
+
* @returns {boolean} 是否在 cmux 环境中
|
|
24
|
+
*/
|
|
25
|
+
export function isCmuxEnvironment(): boolean {
|
|
26
|
+
return !!process.env.CMUX_WORKSPACE_ID;
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
/**
|
|
21
30
|
* 检测当前使用的终端应用
|
|
22
|
-
* 优先读取配置项 terminalApp;值为 'auto'
|
|
23
|
-
*
|
|
24
|
-
*
|
|
31
|
+
* 优先读取配置项 terminalApp;值为 'auto' 时按以下顺序检测:
|
|
32
|
+
* 1. cmux 环境(通过 CMUX_WORKSPACE_ID 环境变量)
|
|
33
|
+
* 2. iTerm2 是否已安装
|
|
34
|
+
* 3. 降级到 Terminal.app
|
|
35
|
+
* @returns {TerminalApp} 终端类型:'iterm2'、'terminal' 或 'cmux'
|
|
25
36
|
*/
|
|
26
37
|
export function detectTerminalApp(): TerminalApp {
|
|
27
38
|
const configured = getConfigValue('terminalApp');
|
|
28
39
|
|
|
29
40
|
// 配置了明确的终端类型,直接使用
|
|
30
|
-
if (configured === 'iterm2' || configured === 'terminal') {
|
|
41
|
+
if (configured === 'iterm2' || configured === 'terminal' || configured === 'cmux') {
|
|
31
42
|
return configured;
|
|
32
43
|
}
|
|
33
44
|
|
|
@@ -36,10 +47,15 @@ export function detectTerminalApp(): TerminalApp {
|
|
|
36
47
|
logger.warn(`terminalApp 配置值 "${configured}" 无效,有效值: ${VALID_TERMINAL_APPS.join(', ')},将使用自动检测`);
|
|
37
48
|
}
|
|
38
49
|
|
|
39
|
-
// auto 模式:优先检测
|
|
50
|
+
// auto 模式:优先检测 cmux 环境
|
|
51
|
+
if (isCmuxEnvironment()) {
|
|
52
|
+
return 'cmux';
|
|
53
|
+
}
|
|
54
|
+
|
|
40
55
|
if (isITerm2Installed()) {
|
|
41
56
|
return 'iterm2';
|
|
42
57
|
}
|
|
58
|
+
|
|
43
59
|
return 'terminal';
|
|
44
60
|
}
|
|
45
61
|
|
|
@@ -99,25 +115,78 @@ end tell
|
|
|
99
115
|
}
|
|
100
116
|
|
|
101
117
|
/**
|
|
102
|
-
*
|
|
103
|
-
* 自动检测终端类型(iTerm2 / Terminal.app),通过 AppleScript 打开新 Tab
|
|
118
|
+
* 在当前 cmux workspace 中分割创建新 surface 并执行命令
|
|
104
119
|
* @param {string} command - 要执行的 shell 命令
|
|
105
|
-
* @param {string}
|
|
106
|
-
* @throws {ClawtError}
|
|
120
|
+
* @param {string} title - surface 标题(用于日志)
|
|
121
|
+
* @throws {ClawtError} 不在 cmux 环境中或 CLI 执行失败时抛出
|
|
107
122
|
*/
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
123
|
+
function openCommandInCmuxSurface(command: string, title: string): void {
|
|
124
|
+
// 环境检查:只需要检查 WORKSPACE_ID
|
|
125
|
+
if (!isCmuxEnvironment()) {
|
|
126
|
+
throw new ClawtError(
|
|
127
|
+
'当前不在 cmux 环境中,无法创建 surface\n' +
|
|
128
|
+
'请确保在 cmux 终端中执行 clawt resume 命令,或修改 terminalApp 配置'
|
|
129
|
+
);
|
|
111
130
|
}
|
|
112
131
|
|
|
113
|
-
|
|
114
|
-
const script = terminalApp === 'iterm2'
|
|
115
|
-
? buildITermAppleScript(command, tabTitle)
|
|
116
|
-
: buildTerminalAppleScript(command, tabTitle);
|
|
117
|
-
|
|
118
|
-
logger.debug(`打开终端 Tab [${terminalApp}]: ${tabTitle}`);
|
|
132
|
+
logger.debug(`在 cmux 中创建新 surface: ${title}`);
|
|
119
133
|
logger.debug(`执行命令: ${command}`);
|
|
120
134
|
|
|
135
|
+
try {
|
|
136
|
+
// 步骤 1:分割创建新 surface(利用默认值机制)
|
|
137
|
+
const newSurfaceResult = execFileSync('cmux', [
|
|
138
|
+
'new-split',
|
|
139
|
+
'right', // 在右侧创建新 surface
|
|
140
|
+
], {
|
|
141
|
+
encoding: 'utf-8',
|
|
142
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
143
|
+
timeout: 5000, // 5秒超时
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
logger.debug(`new-split 输出: ${newSurfaceResult}`);
|
|
147
|
+
|
|
148
|
+
// 步骤 2:解析输出获取新 surface ID
|
|
149
|
+
// 输出格式可能是:
|
|
150
|
+
// - "surface:24"(简短引用)
|
|
151
|
+
// - "OK surface:24 pane:14 workspace:5"(带 OK 前缀)
|
|
152
|
+
// 需要灵活匹配
|
|
153
|
+
const match = newSurfaceResult.match(/(?:OK\s+)?(surface:\d+)/i);
|
|
154
|
+
if (!match) {
|
|
155
|
+
throw new Error(`无法解析 cmux new-split 输出: ${newSurfaceResult}`);
|
|
156
|
+
}
|
|
157
|
+
const surfaceRef = match[1];
|
|
158
|
+
|
|
159
|
+
logger.debug(`已创建 surface: ${surfaceRef}`);
|
|
160
|
+
|
|
161
|
+
// 步骤 3:向新 surface 发送命令(追加 \n 自动执行)
|
|
162
|
+
execFileSync('cmux', [
|
|
163
|
+
'send',
|
|
164
|
+
'--surface', surfaceRef,
|
|
165
|
+
`${command}\\n`, // 追加换行符以自动执行命令
|
|
166
|
+
], {
|
|
167
|
+
encoding: 'utf-8',
|
|
168
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
169
|
+
timeout: 5000,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
logger.debug(`已向 ${surfaceRef} 发送命令`);
|
|
173
|
+
|
|
174
|
+
} catch (error) {
|
|
175
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
176
|
+
throw new ClawtError(`在 cmux 中创建 surface 失败: ${message}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 执行 AppleScript 脚本
|
|
182
|
+
* @param {string} script - AppleScript 内容
|
|
183
|
+
* @param {TerminalApp} terminalApp - 终端类型('iterm2' 或 'terminal')
|
|
184
|
+
* @throws {ClawtError} AppleScript 执行失败时抛出
|
|
185
|
+
*/
|
|
186
|
+
function executeAppleScript(script: string, terminalApp: 'iterm2' | 'terminal'): void {
|
|
187
|
+
logger.debug(`打开终端 Tab [${terminalApp}]`);
|
|
188
|
+
logger.debug(`执行 AppleScript`);
|
|
189
|
+
|
|
121
190
|
try {
|
|
122
191
|
execFileSync('osascript', ['-e', script], {
|
|
123
192
|
encoding: 'utf-8',
|
|
@@ -125,10 +194,42 @@ export function openCommandInNewTerminalTab(command: string, tabTitle: string):
|
|
|
125
194
|
});
|
|
126
195
|
} catch (error) {
|
|
127
196
|
const message = error instanceof Error ? error.message : String(error);
|
|
128
|
-
// Terminal.app 通过 System Events 模拟键盘操作需要辅助功能权限
|
|
129
197
|
const accessibilityHint = terminalApp === 'terminal'
|
|
130
198
|
? '\n提示:Terminal.app 需要辅助功能权限,请在「系统设置 → 隐私与安全性 → 辅助功能」中授权终端应用'
|
|
131
199
|
: '';
|
|
132
200
|
throw new ClawtError(`打开终端 Tab 失败: ${message}${accessibilityHint}`);
|
|
133
201
|
}
|
|
134
202
|
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 在新终端 Tab 或 cmux Surface 中执行命令
|
|
206
|
+
* 自动检测终端类型(cmux / iTerm2 / Terminal.app)
|
|
207
|
+
* - cmux:在当前 pane 创建新 surface
|
|
208
|
+
* - iTerm2 / Terminal.app:通过 AppleScript 打开新 Tab
|
|
209
|
+
* @param {string} command - 要执行的 shell 命令
|
|
210
|
+
* @param {string} tabTitle - Tab 或 surface 标题
|
|
211
|
+
* @throws {ClawtError} 非 macOS 平台或终端打开失败时抛出
|
|
212
|
+
*/
|
|
213
|
+
export function openCommandInNewTerminalTab(command: string, tabTitle: string): void {
|
|
214
|
+
if (process.platform !== 'darwin') {
|
|
215
|
+
throw new ClawtError('批量 resume 目前仅支持 macOS 平台');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const terminalApp = detectTerminalApp();
|
|
219
|
+
|
|
220
|
+
switch (terminalApp) {
|
|
221
|
+
case 'cmux':
|
|
222
|
+
openCommandInCmuxSurface(command, tabTitle);
|
|
223
|
+
break;
|
|
224
|
+
case 'iterm2':
|
|
225
|
+
const itermScript = buildITermAppleScript(command, tabTitle);
|
|
226
|
+
executeAppleScript(itermScript, 'iterm2');
|
|
227
|
+
break;
|
|
228
|
+
case 'terminal':
|
|
229
|
+
const terminalScript = buildTerminalAppleScript(command, tabTitle);
|
|
230
|
+
executeAppleScript(terminalScript, 'terminal');
|
|
231
|
+
break;
|
|
232
|
+
default:
|
|
233
|
+
throw new ClawtError(`不支持的终端类型: ${terminalApp}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// mock node:child_process
|
|
4
|
+
vi.mock('node:child_process', () => ({
|
|
5
|
+
execFileSync: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
// mock node:fs
|
|
9
|
+
vi.mock('node:fs', () => ({
|
|
10
|
+
existsSync: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
// mock logger
|
|
14
|
+
vi.mock('../../../src/logger/index.js', () => ({
|
|
15
|
+
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
// mock config
|
|
19
|
+
vi.mock('../../../src/utils/config.js', () => ({
|
|
20
|
+
getConfigValue: vi.fn(),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
import { execFileSync } from 'node:child_process';
|
|
24
|
+
import { existsSync } from 'node:fs';
|
|
25
|
+
import {
|
|
26
|
+
isCmuxEnvironment,
|
|
27
|
+
detectTerminalApp,
|
|
28
|
+
openCommandInNewTerminalTab,
|
|
29
|
+
} from '../../../src/utils/terminal.js';
|
|
30
|
+
import { getConfigValue } from '../../../src/utils/config.js';
|
|
31
|
+
|
|
32
|
+
const mockedExecFileSync = vi.mocked(execFileSync);
|
|
33
|
+
const mockedExistsSync = vi.mocked(existsSync);
|
|
34
|
+
const mockedGetConfigValue = vi.mocked(getConfigValue);
|
|
35
|
+
|
|
36
|
+
describe('cmux 环境检测', () => {
|
|
37
|
+
const originalEnv = process.env;
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
// 每个测试前重置环境变量
|
|
41
|
+
vi.resetModules();
|
|
42
|
+
process.env = { ...originalEnv };
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
// 测试后恢复原始环境变量
|
|
47
|
+
process.env = originalEnv;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('isCmuxEnvironment', () => {
|
|
51
|
+
it('CMUX_WORKSPACE_ID 存在时返回 true', () => {
|
|
52
|
+
process.env.CMUX_WORKSPACE_ID = '6E83B1B3-5617-43F0-82FB-75F55E9F3F28';
|
|
53
|
+
expect(isCmuxEnvironment()).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('CMUX_WORKSPACE_ID 不存在时返回 false', () => {
|
|
57
|
+
delete process.env.CMUX_WORKSPACE_ID;
|
|
58
|
+
expect(isCmuxEnvironment()).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('终端检测优先级', () => {
|
|
64
|
+
const originalEnv = process.env;
|
|
65
|
+
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
vi.clearAllMocks();
|
|
68
|
+
process.env = { ...originalEnv };
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
process.env = originalEnv;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('明确配置 cmux 时返回 cmux', () => {
|
|
76
|
+
mockedGetConfigValue.mockReturnValue('cmux');
|
|
77
|
+
expect(detectTerminalApp()).toBe('cmux');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('明确配置 iterm2 时返回 iterm2', () => {
|
|
81
|
+
mockedGetConfigValue.mockReturnValue('iterm2');
|
|
82
|
+
expect(detectTerminalApp()).toBe('iterm2');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('明确配置 terminal 时返回 terminal', () => {
|
|
86
|
+
mockedGetConfigValue.mockReturnValue('terminal');
|
|
87
|
+
expect(detectTerminalApp()).toBe('terminal');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('auto 模式下 cmux 环境优先级最高', () => {
|
|
91
|
+
mockedGetConfigValue.mockReturnValue('auto');
|
|
92
|
+
process.env.CMUX_WORKSPACE_ID = '6E83B1B3-5617-43F0-82FB-75F55E9F3F28'; // 在 cmux 环境中
|
|
93
|
+
|
|
94
|
+
expect(detectTerminalApp()).toBe('cmux');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('auto 模式下非 cmux 环境降级到 iTerm2', () => {
|
|
98
|
+
mockedGetConfigValue.mockReturnValue('auto');
|
|
99
|
+
mockedExistsSync.mockReturnValue(true); // iTerm2 已安装
|
|
100
|
+
delete process.env.CMUX_WORKSPACE_ID; // 不在 cmux 环境中
|
|
101
|
+
|
|
102
|
+
expect(detectTerminalApp()).toBe('iterm2');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('auto 模式下无 iTerm2 时降级到 terminal', () => {
|
|
106
|
+
mockedGetConfigValue.mockReturnValue('auto');
|
|
107
|
+
mockedExistsSync.mockReturnValue(false); // iTerm2 未安装
|
|
108
|
+
delete process.env.CMUX_WORKSPACE_ID; // 不在 cmux 环境中
|
|
109
|
+
|
|
110
|
+
expect(detectTerminalApp()).toBe('terminal');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('cmux surface 创建', () => {
|
|
115
|
+
const originalEnv = process.env;
|
|
116
|
+
const originalPlatform = process.platform;
|
|
117
|
+
|
|
118
|
+
beforeEach(() => {
|
|
119
|
+
vi.clearAllMocks();
|
|
120
|
+
process.env = { ...originalEnv };
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
afterEach(() => {
|
|
124
|
+
process.env = originalEnv;
|
|
125
|
+
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('成功创建 surface 并发送命令(简短格式输出)', () => {
|
|
129
|
+
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
|
130
|
+
process.env.CMUX_WORKSPACE_ID = '6E83B1B3-5617-43F0-82FB-75F55E9F3F28';
|
|
131
|
+
|
|
132
|
+
mockedGetConfigValue.mockReturnValue('cmux');
|
|
133
|
+
mockedExecFileSync
|
|
134
|
+
.mockReturnValueOnce('surface:24') // new-split 返回简短格式
|
|
135
|
+
.mockReturnValueOnce(''); // send 返回
|
|
136
|
+
|
|
137
|
+
expect(() => openCommandInNewTerminalTab('claude', 'test-title')).not.toThrow();
|
|
138
|
+
|
|
139
|
+
// 验证 new-split 调用
|
|
140
|
+
expect(mockedExecFileSync).toHaveBeenNthCalledWith(
|
|
141
|
+
1,
|
|
142
|
+
'cmux',
|
|
143
|
+
['new-split', 'right'],
|
|
144
|
+
expect.objectContaining({ timeout: 5000 })
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// 验证 send 调用(包含 \n 以自动执行)
|
|
148
|
+
expect(mockedExecFileSync).toHaveBeenNthCalledWith(
|
|
149
|
+
2,
|
|
150
|
+
'cmux',
|
|
151
|
+
['send', '--surface', 'surface:24', 'claude\\n'],
|
|
152
|
+
expect.objectContaining({ timeout: 5000 })
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('成功创建 surface 并发送命令(带 OK 前缀输出)', () => {
|
|
157
|
+
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
|
158
|
+
process.env.CMUX_WORKSPACE_ID = '6E83B1B3-5617-43F0-82FB-75F55E9F3F28';
|
|
159
|
+
|
|
160
|
+
mockedGetConfigValue.mockReturnValue('cmux');
|
|
161
|
+
mockedExecFileSync
|
|
162
|
+
.mockReturnValueOnce('OK surface:24 pane:14 workspace:5') // new-split 返回带前缀格式
|
|
163
|
+
.mockReturnValueOnce(''); // send 返回
|
|
164
|
+
|
|
165
|
+
expect(() => openCommandInNewTerminalTab('claude', 'test-title')).not.toThrow();
|
|
166
|
+
|
|
167
|
+
// 验证解析正确(包含 \n 以自动执行)
|
|
168
|
+
expect(mockedExecFileSync).toHaveBeenNthCalledWith(
|
|
169
|
+
2,
|
|
170
|
+
'cmux',
|
|
171
|
+
['send', '--surface', 'surface:24', 'claude\\n'],
|
|
172
|
+
expect.objectContaining({ timeout: 5000 })
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('不在 cmux 环境中时抛出友好错误', () => {
|
|
177
|
+
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
|
178
|
+
delete process.env.CMUX_WORKSPACE_ID;
|
|
179
|
+
|
|
180
|
+
mockedGetConfigValue.mockReturnValue('cmux');
|
|
181
|
+
|
|
182
|
+
expect(() => openCommandInNewTerminalTab('claude', 'test-title')).toThrow(
|
|
183
|
+
/当前不在 cmux 环境中/
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('new-split 输出格式无法解析时抛出错误', () => {
|
|
188
|
+
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
|
189
|
+
process.env.CMUX_WORKSPACE_ID = '6E83B1B3-5617-43F0-82FB-75F55E9F3F28';
|
|
190
|
+
|
|
191
|
+
mockedGetConfigValue.mockReturnValue('cmux');
|
|
192
|
+
mockedExecFileSync.mockReturnValueOnce('invalid output format');
|
|
193
|
+
|
|
194
|
+
expect(() => openCommandInNewTerminalTab('claude', 'test-title')).toThrow(
|
|
195
|
+
/无法解析 cmux new-split 输出/
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('cmux CLI 执行失败时捕获并抛出错误', () => {
|
|
200
|
+
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
|
201
|
+
process.env.CMUX_WORKSPACE_ID = '6E83B1B3-5617-43F0-82FB-75F55E9F3F28';
|
|
202
|
+
|
|
203
|
+
mockedGetConfigValue.mockReturnValue('cmux');
|
|
204
|
+
mockedExecFileSync.mockImplementation(() => {
|
|
205
|
+
const error = new Error('spawn cmux ENOENT');
|
|
206
|
+
throw error;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
expect(() => openCommandInNewTerminalTab('claude', 'test-title')).toThrow(
|
|
210
|
+
/在 cmux 中创建 surface 失败/
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('非 macOS 平台抛出错误', () => {
|
|
215
|
+
Object.defineProperty(process, 'platform', { value: 'linux' });
|
|
216
|
+
|
|
217
|
+
expect(() => openCommandInNewTerminalTab('claude', 'test-title')).toThrow(
|
|
218
|
+
/仅支持 macOS 平台/
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('向后兼容性', () => {
|
|
224
|
+
const originalEnv = process.env;
|
|
225
|
+
const originalPlatform = process.platform;
|
|
226
|
+
|
|
227
|
+
beforeEach(() => {
|
|
228
|
+
vi.clearAllMocks();
|
|
229
|
+
process.env = { ...originalEnv };
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
afterEach(() => {
|
|
233
|
+
process.env = originalEnv;
|
|
234
|
+
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('iTerm2 用户不受影响', () => {
|
|
238
|
+
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
|
239
|
+
delete process.env.CMUX_WORKSPACE_ID;
|
|
240
|
+
|
|
241
|
+
mockedGetConfigValue.mockReturnValue('iterm2');
|
|
242
|
+
mockedExecFileSync.mockReturnValue('');
|
|
243
|
+
|
|
244
|
+
expect(() => openCommandInNewTerminalTab('claude', 'test-title')).not.toThrow();
|
|
245
|
+
|
|
246
|
+
// 验证使用 osascript 执行 AppleScript
|
|
247
|
+
expect(mockedExecFileSync).toHaveBeenCalledWith(
|
|
248
|
+
'osascript',
|
|
249
|
+
expect.arrayContaining([expect.stringContaining('-e')]),
|
|
250
|
+
expect.any(Object)
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('Terminal.app 用户不受影响', () => {
|
|
255
|
+
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
|
256
|
+
delete process.env.CMUX_WORKSPACE_ID;
|
|
257
|
+
|
|
258
|
+
mockedGetConfigValue.mockReturnValue('terminal');
|
|
259
|
+
mockedExecFileSync.mockReturnValue('');
|
|
260
|
+
|
|
261
|
+
expect(() => openCommandInNewTerminalTab('claude', 'test-title')).not.toThrow();
|
|
262
|
+
|
|
263
|
+
// 验证使用 osascript 执行 AppleScript
|
|
264
|
+
expect(mockedExecFileSync).toHaveBeenCalledWith(
|
|
265
|
+
'osascript',
|
|
266
|
+
expect.arrayContaining([expect.stringContaining('-e')]),
|
|
267
|
+
expect.any(Object)
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('auto 模式下原有行为不变(无 cmux 环境时)', () => {
|
|
272
|
+
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
|
273
|
+
delete process.env.CMUX_WORKSPACE_ID;
|
|
274
|
+
|
|
275
|
+
mockedGetConfigValue.mockReturnValue('auto');
|
|
276
|
+
mockedExistsSync.mockReturnValue(true); // iTerm2 已安装
|
|
277
|
+
mockedExecFileSync.mockReturnValue('');
|
|
278
|
+
|
|
279
|
+
expect(() => openCommandInNewTerminalTab('claude', 'test-title')).not.toThrow();
|
|
280
|
+
|
|
281
|
+
// 验证使用 iTerm2
|
|
282
|
+
expect(mockedExecFileSync).toHaveBeenCalledWith(
|
|
283
|
+
'osascript',
|
|
284
|
+
expect.arrayContaining([expect.stringContaining('-e')]),
|
|
285
|
+
expect.any(Object)
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
});
|