svharness 0.14.12 → 0.14.13
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.
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.resolveLaunchScriptPath = resolveLaunchScriptPath;
|
|
7
|
+
exports.resolveWindowsTerminalPath = resolveWindowsTerminalPath;
|
|
7
8
|
exports.runShellInstall = runShellInstall;
|
|
8
9
|
exports.runShellUninstall = runShellUninstall;
|
|
9
10
|
exports.getShellStatus = getShellStatus;
|
|
@@ -12,6 +13,7 @@ exports.runShellInstallSafe = runShellInstallSafe;
|
|
|
12
13
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
13
14
|
const path_1 = __importDefault(require("path"));
|
|
14
15
|
const os_1 = __importDefault(require("os"));
|
|
16
|
+
const child_process_1 = require("child_process");
|
|
15
17
|
const logger_1 = require("../utils/logger");
|
|
16
18
|
const win_registry_1 = require("../lib/win-registry");
|
|
17
19
|
const STUB_DIR = path_1.default.join(process.env.LOCALAPPDATA ?? path_1.default.join(os_1.default.homedir(), 'AppData', 'Local'), 'svharness', 'bin');
|
|
@@ -50,13 +52,55 @@ function resolveLaunchScriptPath() {
|
|
|
50
52
|
}
|
|
51
53
|
throw new Error(`未找到 launch-codechat-cli.js(预期:${fromDist})`);
|
|
52
54
|
}
|
|
55
|
+
function canAccessExecutable(filePath) {
|
|
56
|
+
try {
|
|
57
|
+
fs_extra_1.default.accessSync(filePath, fs_extra_1.default.constants.F_OK);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Resolve wt.exe at shell install time (Explorer context menus use this path directly).
|
|
66
|
+
*/
|
|
67
|
+
function resolveWindowsTerminalPath() {
|
|
68
|
+
try {
|
|
69
|
+
const out = (0, child_process_1.execFileSync)('where', ['wt'], { encoding: 'utf8', windowsHide: true }).trim();
|
|
70
|
+
const first = out.split(/\r?\n/)[0]?.trim();
|
|
71
|
+
if (first && canAccessExecutable(first)) {
|
|
72
|
+
return first;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// wt not on PATH during install
|
|
77
|
+
}
|
|
78
|
+
const localAppData = process.env.LOCALAPPDATA ?? path_1.default.join(os_1.default.homedir(), 'AppData', 'Local');
|
|
79
|
+
const candidates = [
|
|
80
|
+
path_1.default.join(localAppData, 'Microsoft', 'WindowsApps', 'wt.exe'),
|
|
81
|
+
path_1.default.join(process.env.ProgramFiles ?? 'C:\\Program Files', 'Windows Terminal', 'wt.exe'),
|
|
82
|
+
];
|
|
83
|
+
for (const candidate of candidates) {
|
|
84
|
+
if (candidate && canAccessExecutable(candidate)) {
|
|
85
|
+
return candidate;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
53
90
|
function buildStubContent(launchScriptPath) {
|
|
54
91
|
const escapedNodeScript = launchScriptPath.replace(/'/g, "''");
|
|
55
|
-
// ASCII-only + UTF-8 BOM
|
|
92
|
+
// ASCII-only + UTF-8 BOM. Legacy conhost fallback sets UTF-8 before CodeChat TUI.
|
|
56
93
|
return `param(
|
|
57
94
|
[string]$WorkDir = (Get-Location).Path
|
|
58
95
|
)
|
|
59
96
|
|
|
97
|
+
$ErrorActionPreference = 'Stop'
|
|
98
|
+
|
|
99
|
+
try { chcp 65001 | Out-Null } catch {}
|
|
100
|
+
[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false)
|
|
101
|
+
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
|
102
|
+
$OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
|
103
|
+
|
|
60
104
|
Set-Location -LiteralPath $WorkDir
|
|
61
105
|
& node '${escapedNodeScript}' $WorkDir
|
|
62
106
|
exit $LASTEXITCODE
|
|
@@ -66,8 +110,16 @@ function writeStubFile(launchScriptPath) {
|
|
|
66
110
|
const content = buildStubContent(launchScriptPath);
|
|
67
111
|
return fs_extra_1.default.writeFile(STUB_PATH, `\uFEFF${content}`, 'utf8');
|
|
68
112
|
}
|
|
69
|
-
function
|
|
70
|
-
return
|
|
113
|
+
function buildPsInvokeArgs(ps1Path, dirPlaceholder) {
|
|
114
|
+
return `-NoExit -NoProfile -ExecutionPolicy Bypass -File "${ps1Path}" ${dirPlaceholder}`;
|
|
115
|
+
}
|
|
116
|
+
function buildMenuCommand(ps1Path, dirPlaceholder, wtPath) {
|
|
117
|
+
const psArgs = buildPsInvokeArgs(ps1Path, dirPlaceholder);
|
|
118
|
+
if (wtPath) {
|
|
119
|
+
// Launch WT directly from registry — avoids an extra conhost window from stub bootstrap.
|
|
120
|
+
return `"${wtPath}" -d ${dirPlaceholder} --title CodeChat -- powershell.exe ${psArgs}`;
|
|
121
|
+
}
|
|
122
|
+
return `powershell.exe ${psArgs}`;
|
|
71
123
|
}
|
|
72
124
|
function registerMenuKey(menuKey, command) {
|
|
73
125
|
(0, win_registry_1.regSetDefault)(menuKey, win_registry_1.SHELL_MENU_LABEL);
|
|
@@ -84,13 +136,18 @@ async function runShellInstall(opts = {}) {
|
|
|
84
136
|
if (await fs_extra_1.default.pathExists(LEGACY_STUB_PATH)) {
|
|
85
137
|
await fs_extra_1.default.remove(LEGACY_STUB_PATH);
|
|
86
138
|
}
|
|
87
|
-
const
|
|
88
|
-
const
|
|
139
|
+
const wtPath = resolveWindowsTerminalPath();
|
|
140
|
+
const bgCommand = buildMenuCommand(STUB_PATH, '"%V"', wtPath);
|
|
141
|
+
const dirCommand = buildMenuCommand(STUB_PATH, '"%1"', wtPath);
|
|
89
142
|
registerMenuKey(win_registry_1.SHELL_MENU_KEY_BG, bgCommand);
|
|
90
143
|
registerMenuKey(win_registry_1.SHELL_MENU_KEY_DIR, dirCommand);
|
|
91
|
-
|
|
144
|
+
const via = wtPath ? 'Windows Terminal' : 'PowerShell(UTF-8)';
|
|
145
|
+
logSuccess(`Windows 右键菜单已注册:「在此启动 CodeChat Agent」(${via})`, opts.silent);
|
|
92
146
|
logInfo(` stub: ${STUB_PATH}`, opts.silent);
|
|
93
147
|
logInfo(` 脚本: ${launchScriptPath}`, opts.silent);
|
|
148
|
+
if (wtPath) {
|
|
149
|
+
logInfo(` wt: ${wtPath}`, opts.silent);
|
|
150
|
+
}
|
|
94
151
|
}
|
|
95
152
|
/**
|
|
96
153
|
* Remove HKCU context menus and stub file.
|
|
@@ -122,12 +179,14 @@ function getShellStatus() {
|
|
|
122
179
|
const backgroundMenu = (0, win_registry_1.regKeyExists)(win_registry_1.SHELL_MENU_KEY_BG);
|
|
123
180
|
const directoryMenu = (0, win_registry_1.regKeyExists)(win_registry_1.SHELL_MENU_KEY_DIR);
|
|
124
181
|
const stubExists = fs_extra_1.default.existsSync(STUB_PATH);
|
|
182
|
+
const wtPath = resolveWindowsTerminalPath();
|
|
125
183
|
return {
|
|
126
184
|
installed: backgroundMenu && directoryMenu && stubExists,
|
|
127
185
|
stubPath: STUB_PATH,
|
|
128
186
|
backgroundMenu,
|
|
129
187
|
directoryMenu,
|
|
130
188
|
launchScriptPath,
|
|
189
|
+
viaWindowsTerminal: wtPath !== null,
|
|
131
190
|
};
|
|
132
191
|
}
|
|
133
192
|
function printShellStatus() {
|
|
@@ -138,6 +197,7 @@ function printShellStatus() {
|
|
|
138
197
|
logger_1.logger.plain(` 空白处菜单: ${status.backgroundMenu ? '已注册' : '未注册'}`);
|
|
139
198
|
logger_1.logger.plain(` 文件夹菜单: ${status.directoryMenu ? '已注册' : '未注册'}`);
|
|
140
199
|
logger_1.logger.plain(` 启动脚本: ${status.launchScriptPath}`);
|
|
200
|
+
logger_1.logger.plain(` 经 WT 启动: ${status.viaWindowsTerminal ? '是(install 时检测到 wt.exe)' : '否'}`);
|
|
141
201
|
}
|
|
142
202
|
async function runShellInstallSafe(opts = {}) {
|
|
143
203
|
try {
|
|
@@ -18,6 +18,42 @@ const DEFAULT_ENV_TEMPLATE = 'default-claude.env';
|
|
|
18
18
|
function homePath(...segments) {
|
|
19
19
|
return path_1.default.join(os_1.default.homedir(), ...segments);
|
|
20
20
|
}
|
|
21
|
+
/** Absolute normalized path for runner CLI args. */
|
|
22
|
+
function resolveUserEnvPath(config) {
|
|
23
|
+
return path_1.default.resolve(config.envFile);
|
|
24
|
+
}
|
|
25
|
+
function formatRunnerArg(name, value) {
|
|
26
|
+
const resolved = path_1.default.resolve(value);
|
|
27
|
+
if (process.platform === 'win32') {
|
|
28
|
+
// Quote paths for cmd.exe (matches manual: --env-file="C:\Users\...\.claude\.env")
|
|
29
|
+
return `${name}="${resolved.replace(/"/g, '""')}"`;
|
|
30
|
+
}
|
|
31
|
+
return `${name}=${resolved}`;
|
|
32
|
+
}
|
|
33
|
+
const ENV_FILE_VERIFY_RETRIES = 5;
|
|
34
|
+
const ENV_FILE_VERIFY_DELAY_MS = 40;
|
|
35
|
+
async function delay(ms) {
|
|
36
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
37
|
+
}
|
|
38
|
+
/** Wait until user env exists and is non-empty (Windows AV / flush visibility). */
|
|
39
|
+
async function verifyUserEnvWritten(userEnvPath) {
|
|
40
|
+
for (let attempt = 1; attempt <= ENV_FILE_VERIFY_RETRIES; attempt++) {
|
|
41
|
+
try {
|
|
42
|
+
const stat = await fs_extra_1.default.stat(userEnvPath);
|
|
43
|
+
if (stat.isFile() && stat.size > 0) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// retry
|
|
49
|
+
}
|
|
50
|
+
if (attempt < ENV_FILE_VERIFY_RETRIES) {
|
|
51
|
+
await delay(ENV_FILE_VERIFY_DELAY_MS);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`env 写入后校验失败(文件不存在或为空):${userEnvPath}\n` +
|
|
55
|
+
`请重试 svharness start-agent,或手动创建该文件。`);
|
|
56
|
+
}
|
|
21
57
|
function getCodechatRunnerConfig() {
|
|
22
58
|
const platform = process.platform;
|
|
23
59
|
if (platform === 'win32') {
|
|
@@ -91,8 +127,11 @@ function assertUserEnvWriteTarget(userEnvPath) {
|
|
|
91
127
|
}
|
|
92
128
|
async function copyEnvToUserOnly(fromPath, userEnvPath) {
|
|
93
129
|
assertUserEnvWriteTarget(userEnvPath);
|
|
94
|
-
|
|
95
|
-
await fs_extra_1.default.
|
|
130
|
+
const dest = path_1.default.resolve(userEnvPath);
|
|
131
|
+
await fs_extra_1.default.ensureDir(path_1.default.dirname(dest));
|
|
132
|
+
const content = await fs_extra_1.default.readFile(fromPath, 'utf8');
|
|
133
|
+
await fs_extra_1.default.writeFile(dest, content, { encoding: 'utf8' });
|
|
134
|
+
await verifyUserEnvWritten(dest);
|
|
96
135
|
}
|
|
97
136
|
/**
|
|
98
137
|
* Sync project env to user profile, or seed bundled default when none exists.
|
|
@@ -144,9 +183,10 @@ function warnIfNotHarnessProject(workdir) {
|
|
|
144
183
|
}
|
|
145
184
|
}
|
|
146
185
|
function buildRunnerArgs(config, workdir, skipPermissions) {
|
|
186
|
+
const envFile = resolveUserEnvPath(config);
|
|
147
187
|
const args = [
|
|
148
|
-
|
|
149
|
-
|
|
188
|
+
formatRunnerArg('--env-file', envFile),
|
|
189
|
+
formatRunnerArg('--cwd', workdir),
|
|
150
190
|
];
|
|
151
191
|
if (skipPermissions) {
|
|
152
192
|
args.push('--dangerously-skip-permissions');
|
|
@@ -170,13 +210,15 @@ async function runStartAgent(opts) {
|
|
|
170
210
|
const syncEnv = opts.syncEnv !== false;
|
|
171
211
|
const skipPermissions = opts.skipPermissions !== false;
|
|
172
212
|
await ensureUserEnv(workdir, config, syncEnv);
|
|
213
|
+
const userEnvFile = resolveUserEnvPath(config);
|
|
173
214
|
if (!(await fs_extra_1.default.pathExists(config.runner))) {
|
|
174
215
|
throw new Error(`未找到 CodeChat CLI:${config.runner}\n请先安装 CodeChat CLI 到用户目录。`);
|
|
175
216
|
}
|
|
176
|
-
if (!(await fs_extra_1.default.pathExists(
|
|
177
|
-
throw new Error(`未找到 env 文件:${
|
|
217
|
+
if (!(await fs_extra_1.default.pathExists(userEnvFile))) {
|
|
218
|
+
throw new Error(`未找到 env 文件:${userEnvFile}\n` +
|
|
178
219
|
`请提供 ${path_1.default.join(workdir, config.envSyncFromRel)},或确保内置模板 templates/default-claude.env 可访问。`);
|
|
179
220
|
}
|
|
221
|
+
await verifyUserEnvWritten(userEnvFile);
|
|
180
222
|
warnIfNotHarnessProject(workdir);
|
|
181
223
|
const args = buildRunnerArgs(config, workdir, skipPermissions);
|
|
182
224
|
logger_1.logger.info(`启动 CodeChat Agent(workdir: ${workdir})`);
|
|
@@ -142,9 +142,11 @@ launch_codechat_cli ./my-app # 指定 workdir
|
|
|
142
142
|
| 2 | `~/.claude/.env` 已存在 | 保持不变 |
|
|
143
143
|
| 3 | 以上均不满足 | 从 `templates/default-claude.env` **仅**生成 `~/.claude/.env` |
|
|
144
144
|
|
|
145
|
-
内置模板为德赛 CodeChat
|
|
145
|
+
内置模板为德赛 CodeChat 通用配置(含默认 `ANTHROPIC_AUTH_TOKEN`)。项目 `.claude/.env` 仍可覆盖同步到用户目录。
|
|
146
146
|
|
|
147
|
-
启动时 CodeChat 使用 `--env-file=~/.claude/.env
|
|
147
|
+
启动时 CodeChat 使用 `--env-file=~/.claude/.env`(Windows 下带引号的绝对路径,避免 cmd 解析偶发失败)。
|
|
148
|
+
|
|
149
|
+
写入 `~/.claude/.env` 后会做 **存在 + 非空** 校验(最多 5 次、间隔 40ms),缓解 Windows 上杀毒/索引导致的短暂不可见。
|
|
148
150
|
|
|
149
151
|
---
|
|
150
152
|
|
|
@@ -173,7 +175,23 @@ macOS (`darwin`) 路径约定与 Linux 相同;若 runner 不存在则报错。
|
|
|
173
175
|
|
|
174
176
|
stub 接收 Explorer 传入的目录参数,`cd` 后调用 **绝对路径** 的 `bin/launch-codechat-cli.js`。
|
|
175
177
|
|
|
176
|
-
### 7.
|
|
178
|
+
### 7.3 终端与编码(右键 vs 手动 PowerShell)
|
|
179
|
+
|
|
180
|
+
| 启动方式 | 典型终端 | 现象 |
|
|
181
|
+
|----------|----------|------|
|
|
182
|
+
| 手动打开 PowerShell 再 `start-agent` | **Windows Terminal** / 已配置 UTF-8 的会话 | CodeChat TUI 框线正常 |
|
|
183
|
+
| 资源管理器右键(旧实现) | **conhost 经典蓝底** + `-NoProfile` | Unicode 框线乱码、`>>` 变 `\|\|` |
|
|
184
|
+
|
|
185
|
+
原因:CodeChat CLI 使用 Unicode 制表符(`╔═╗`)。经典控制台默认代码页(如 GBK)无法正确渲染。
|
|
186
|
+
|
|
187
|
+
**修复(注册表直接启动 WT)**:
|
|
188
|
+
|
|
189
|
+
1. `shell install` 检测到 **Windows Terminal**(`wt.exe`)时,注册表 command **直接调用 wt**,不再先开 conhost 再二次跳转(避免两个窗口)
|
|
190
|
+
2. 未安装 WT 时 fallback:`powershell.exe` + stub 内 `chcp 65001` UTF-8
|
|
191
|
+
|
|
192
|
+
重新注册:`svharness shell install`
|
|
193
|
+
|
|
194
|
+
### 7.4 注册表结构
|
|
177
195
|
|
|
178
196
|
| 位置 | 键 |
|
|
179
197
|
|------|-----|
|
|
@@ -182,13 +200,25 @@ stub 接收 Explorer 传入的目录参数,`cd` 后调用 **绝对路径** 的
|
|
|
182
200
|
|
|
183
201
|
菜单标题:`在此启动 CodeChat CLI`
|
|
184
202
|
|
|
185
|
-
Command(Background
|
|
203
|
+
Command(Background,已安装 WT 时):
|
|
204
|
+
|
|
205
|
+
```text
|
|
206
|
+
"C:\Users\<user>\AppData\Local\Microsoft\WindowsApps\wt.exe" -d "%V" --title CodeChat -- powershell.exe -NoExit -NoProfile -ExecutionPolicy Bypass -File "<stub.ps1>" "%V"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Command(Background,无 WT 时):
|
|
186
210
|
|
|
187
211
|
```text
|
|
188
212
|
powershell.exe -NoExit -NoProfile -ExecutionPolicy Bypass -File "<stub.ps1>" "%V"
|
|
189
213
|
```
|
|
190
214
|
|
|
191
|
-
Command(Directory
|
|
215
|
+
Command(Directory,已安装 WT 时):
|
|
216
|
+
|
|
217
|
+
```text
|
|
218
|
+
"C:\Users\<user>\AppData\Local\Microsoft\WindowsApps\wt.exe" -d "%1" --title CodeChat -- powershell.exe -NoExit -NoProfile -ExecutionPolicy Bypass -File "<stub.ps1>" "%1"
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Command(Directory,无 WT 时):
|
|
192
222
|
|
|
193
223
|
```text
|
|
194
224
|
powershell.exe -NoExit -NoProfile -ExecutionPolicy Bypass -File "<stub.ps1>" "%1"
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# 仅写入用户目录;不会创建或修改项目 <workdir>/.claude/.env
|
|
4
4
|
# 若项目已有 .claude/.env,start-agent 只读并同步到 ~/.claude/.env
|
|
5
5
|
# ============================================================
|
|
6
|
-
ANTHROPIC_AUTH_TOKEN=
|
|
6
|
+
ANTHROPIC_AUTH_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVpZHEzNjQwIiwiZXhwIjoxNzgyMDM2MTkxfQ.GXsNuJ2Wi92VHepau51dTWGswHft6xovvOufVjtuBYk
|
|
7
7
|
ANTHROPIC_BASE_URL=https://aiplus.desaysv.com/chatbot/anthropic
|
|
8
8
|
# ANTHROPIC_BASE_URL=http://127.0.0.1:6008/chatbot/anthropic # 德赛本地调试
|
|
9
9
|
ANTHROPIC_MODEL=deepseek
|