svharness 0.14.11 → 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.
- package/README.md +4 -2
- package/bin/launch-codechat-cli.js +1 -1
- package/dist/commands/shell-integration.js +87 -15
- package/dist/core/next-steps.js +1 -1
- package/dist/index.js +2 -2
- package/dist/lib/agent-launcher.js +104 -13
- package/dist/lib/win-registry.js +1 -1
- package/docs/agent-launcher-design.md +67 -15
- package/package.json +1 -1
- package/templates/default-claude.env +60 -0
package/README.md
CHANGED
|
@@ -580,6 +580,8 @@ convert-failures.json
|
|
|
580
580
|
|
|
581
581
|
详细设计见 [`docs/agent-launcher-design.md`](docs/agent-launcher-design.md)。
|
|
582
582
|
|
|
583
|
+
**env 策略(单向,只写用户目录)**:可**读取**项目 `<workdir>/.claude/.env` 并同步到 `~/.claude/.env`;**绝不**在项目内创建或修改 `.claude/.env`。无 env 时内置模板**仅**生成 `~/.claude/.env`。
|
|
584
|
+
|
|
583
585
|
#### 快速示例
|
|
584
586
|
|
|
585
587
|
```bash
|
|
@@ -603,7 +605,7 @@ svharness start-agent --work-dir . --no-sync-env --no-skip-permissions
|
|
|
603
605
|
| `[workdir]` | 工作目录(前一参数为 agent 时使用) | — |
|
|
604
606
|
| `--work-dir <path>` | Agent 工作目录(harness 项目根) | 当前目录 |
|
|
605
607
|
| `--agent <agent>` | 目标 Agent(**当前仅 codechat 可实际启动**) | `codechat` |
|
|
606
|
-
| `--no-sync-env` |
|
|
608
|
+
| `--no-sync-env` | 不读取项目 `.claude/.env` 同步到用户级(仍可用已有 `~/.claude/.env`;若无则仅写入用户级内置模板) | false |
|
|
607
609
|
| `--no-skip-permissions` | 不传 `--dangerously-skip-permissions` | 默认传 |
|
|
608
610
|
|
|
609
611
|
#### 平台说明
|
|
@@ -625,7 +627,7 @@ launch_codechat_cli ~/projects/my-app # Linux
|
|
|
625
627
|
|
|
626
628
|
### `shell` —— Windows 右键菜单(v0.16+,仅 win32)
|
|
627
629
|
|
|
628
|
-
在资源管理器**文件夹空白处**与**文件夹图标**右键增加「在此启动 CodeChat Agent
|
|
630
|
+
在资源管理器**文件夹空白处**与**文件夹图标**右键增加「在此启动 CodeChat Agent」,点击后打开 **PowerShell** 窗口并启动 Agent。
|
|
629
631
|
|
|
630
632
|
```bash
|
|
631
633
|
svharness shell install # 注册(幂等)
|
|
@@ -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,10 +13,12 @@ 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');
|
|
18
|
-
const STUB_PATH = path_1.default.join(STUB_DIR, 'launch_codechat_cli.
|
|
20
|
+
const STUB_PATH = path_1.default.join(STUB_DIR, 'launch_codechat_cli.ps1');
|
|
21
|
+
const LEGACY_STUB_PATH = path_1.default.join(STUB_DIR, 'launch_codechat_cli.cmd');
|
|
19
22
|
function logInfo(msg, silent) {
|
|
20
23
|
if (!silent)
|
|
21
24
|
logger_1.logger.info(msg);
|
|
@@ -49,19 +52,74 @@ function resolveLaunchScriptPath() {
|
|
|
49
52
|
}
|
|
50
53
|
throw new Error(`未找到 launch-codechat-cli.js(预期:${fromDist})`);
|
|
51
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
|
+
}
|
|
52
90
|
function buildStubContent(launchScriptPath) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
91
|
+
const escapedNodeScript = launchScriptPath.replace(/'/g, "''");
|
|
92
|
+
// ASCII-only + UTF-8 BOM. Legacy conhost fallback sets UTF-8 before CodeChat TUI.
|
|
93
|
+
return `param(
|
|
94
|
+
[string]$WorkDir = (Get-Location).Path
|
|
95
|
+
)
|
|
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
|
+
|
|
104
|
+
Set-Location -LiteralPath $WorkDir
|
|
105
|
+
& node '${escapedNodeScript}' $WorkDir
|
|
106
|
+
exit $LASTEXITCODE
|
|
60
107
|
`;
|
|
61
108
|
}
|
|
62
|
-
function
|
|
63
|
-
const
|
|
64
|
-
return
|
|
109
|
+
function writeStubFile(launchScriptPath) {
|
|
110
|
+
const content = buildStubContent(launchScriptPath);
|
|
111
|
+
return fs_extra_1.default.writeFile(STUB_PATH, `\uFEFF${content}`, 'utf8');
|
|
112
|
+
}
|
|
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}`;
|
|
65
123
|
}
|
|
66
124
|
function registerMenuKey(menuKey, command) {
|
|
67
125
|
(0, win_registry_1.regSetDefault)(menuKey, win_registry_1.SHELL_MENU_LABEL);
|
|
@@ -74,14 +132,22 @@ async function runShellInstall(opts = {}) {
|
|
|
74
132
|
assertWin32();
|
|
75
133
|
const launchScriptPath = resolveLaunchScriptPath();
|
|
76
134
|
await fs_extra_1.default.ensureDir(STUB_DIR);
|
|
77
|
-
await
|
|
78
|
-
|
|
79
|
-
|
|
135
|
+
await writeStubFile(launchScriptPath);
|
|
136
|
+
if (await fs_extra_1.default.pathExists(LEGACY_STUB_PATH)) {
|
|
137
|
+
await fs_extra_1.default.remove(LEGACY_STUB_PATH);
|
|
138
|
+
}
|
|
139
|
+
const wtPath = resolveWindowsTerminalPath();
|
|
140
|
+
const bgCommand = buildMenuCommand(STUB_PATH, '"%V"', wtPath);
|
|
141
|
+
const dirCommand = buildMenuCommand(STUB_PATH, '"%1"', wtPath);
|
|
80
142
|
registerMenuKey(win_registry_1.SHELL_MENU_KEY_BG, bgCommand);
|
|
81
143
|
registerMenuKey(win_registry_1.SHELL_MENU_KEY_DIR, dirCommand);
|
|
82
|
-
|
|
144
|
+
const via = wtPath ? 'Windows Terminal' : 'PowerShell(UTF-8)';
|
|
145
|
+
logSuccess(`Windows 右键菜单已注册:「在此启动 CodeChat Agent」(${via})`, opts.silent);
|
|
83
146
|
logInfo(` stub: ${STUB_PATH}`, opts.silent);
|
|
84
147
|
logInfo(` 脚本: ${launchScriptPath}`, opts.silent);
|
|
148
|
+
if (wtPath) {
|
|
149
|
+
logInfo(` wt: ${wtPath}`, opts.silent);
|
|
150
|
+
}
|
|
85
151
|
}
|
|
86
152
|
/**
|
|
87
153
|
* Remove HKCU context menus and stub file.
|
|
@@ -93,6 +159,9 @@ async function runShellUninstall(opts = {}) {
|
|
|
93
159
|
if (await fs_extra_1.default.pathExists(STUB_PATH)) {
|
|
94
160
|
await fs_extra_1.default.remove(STUB_PATH);
|
|
95
161
|
}
|
|
162
|
+
if (await fs_extra_1.default.pathExists(LEGACY_STUB_PATH)) {
|
|
163
|
+
await fs_extra_1.default.remove(LEGACY_STUB_PATH);
|
|
164
|
+
}
|
|
96
165
|
logSuccess('Windows 右键菜单已卸载', opts.silent);
|
|
97
166
|
}
|
|
98
167
|
/**
|
|
@@ -110,12 +179,14 @@ function getShellStatus() {
|
|
|
110
179
|
const backgroundMenu = (0, win_registry_1.regKeyExists)(win_registry_1.SHELL_MENU_KEY_BG);
|
|
111
180
|
const directoryMenu = (0, win_registry_1.regKeyExists)(win_registry_1.SHELL_MENU_KEY_DIR);
|
|
112
181
|
const stubExists = fs_extra_1.default.existsSync(STUB_PATH);
|
|
182
|
+
const wtPath = resolveWindowsTerminalPath();
|
|
113
183
|
return {
|
|
114
184
|
installed: backgroundMenu && directoryMenu && stubExists,
|
|
115
185
|
stubPath: STUB_PATH,
|
|
116
186
|
backgroundMenu,
|
|
117
187
|
directoryMenu,
|
|
118
188
|
launchScriptPath,
|
|
189
|
+
viaWindowsTerminal: wtPath !== null,
|
|
119
190
|
};
|
|
120
191
|
}
|
|
121
192
|
function printShellStatus() {
|
|
@@ -126,6 +197,7 @@ function printShellStatus() {
|
|
|
126
197
|
logger_1.logger.plain(` 空白处菜单: ${status.backgroundMenu ? '已注册' : '未注册'}`);
|
|
127
198
|
logger_1.logger.plain(` 文件夹菜单: ${status.directoryMenu ? '已注册' : '未注册'}`);
|
|
128
199
|
logger_1.logger.plain(` 启动脚本: ${status.launchScriptPath}`);
|
|
200
|
+
logger_1.logger.plain(` 经 WT 启动: ${status.viaWindowsTerminal ? '是(install 时检测到 wt.exe)' : '否'}`);
|
|
129
201
|
}
|
|
130
202
|
async function runShellInstallSafe(opts = {}) {
|
|
131
203
|
try {
|
package/dist/core/next-steps.js
CHANGED
|
@@ -65,7 +65,7 @@ function printNextSteps(input) {
|
|
|
65
65
|
lines.push(' - ' + picocolors_1.default.cyan('svharness start-agent --work-dir .'));
|
|
66
66
|
lines.push(' - 或: ' + picocolors_1.default.cyan('launch_codechat_cli'));
|
|
67
67
|
if (process.platform === 'win32') {
|
|
68
|
-
lines.push(' - 或: 资源管理器右键 →「在此启动 CodeChat Agent
|
|
68
|
+
lines.push(' - 或: 资源管理器右键 →「在此启动 CodeChat Agent」(PowerShell)');
|
|
69
69
|
}
|
|
70
70
|
lines.push('');
|
|
71
71
|
}
|
package/dist/index.js
CHANGED
|
@@ -422,7 +422,7 @@ function main() {
|
|
|
422
422
|
.description('启动 Agent CLI 填充 harness(build 后 S10–S90)或 apply 后的业务开发。默认 agent=codechat,默认 workdir=当前目录。')
|
|
423
423
|
.option('--work-dir <path>', 'Agent 工作目录(harness 项目根)')
|
|
424
424
|
.option('--agent <agent>', '目标 Agent(当前仅 codechat 可启动)')
|
|
425
|
-
.option('--no-sync-env', '
|
|
425
|
+
.option('--no-sync-env', '不读取项目 .claude/.env 同步到 ~/.claude/.env(永不写入项目目录)')
|
|
426
426
|
.option('--no-skip-permissions', '不向 CodeChat CLI 传递 --dangerously-skip-permissions')
|
|
427
427
|
.action(async (agentOrWorkdir, workdir, opts) => {
|
|
428
428
|
try {
|
|
@@ -447,7 +447,7 @@ function main() {
|
|
|
447
447
|
.description('Windows 资源管理器右键菜单管理(仅 win32)');
|
|
448
448
|
shellCmd
|
|
449
449
|
.command('install')
|
|
450
|
-
.description('注册 HKCU 右键菜单「在此启动 CodeChat
|
|
450
|
+
.description('注册 HKCU 右键菜单「在此启动 CodeChat CLI」')
|
|
451
451
|
.action(async () => {
|
|
452
452
|
try {
|
|
453
453
|
await (0, shell_integration_1.runShellInstall)();
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.resolveWorkdir = resolveWorkdir;
|
|
7
|
+
exports.resolveDefaultEnvTemplatePath = resolveDefaultEnvTemplatePath;
|
|
7
8
|
exports.runStartAgent = runStartAgent;
|
|
8
9
|
exports.parseStartAgentPositionals = parseStartAgentPositionals;
|
|
9
10
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
@@ -13,9 +14,46 @@ const child_process_1 = require("child_process");
|
|
|
13
14
|
const logger_1 = require("../utils/logger");
|
|
14
15
|
const validate_args_1 = require("../utils/validate-args");
|
|
15
16
|
const SUPPORTED_START_AGENTS = ['codechat'];
|
|
17
|
+
const DEFAULT_ENV_TEMPLATE = 'default-claude.env';
|
|
16
18
|
function homePath(...segments) {
|
|
17
19
|
return path_1.default.join(os_1.default.homedir(), ...segments);
|
|
18
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
|
+
}
|
|
19
57
|
function getCodechatRunnerConfig() {
|
|
20
58
|
const platform = process.platform;
|
|
21
59
|
if (platform === 'win32') {
|
|
@@ -62,14 +100,66 @@ function resolveWorkdir(opts) {
|
|
|
62
100
|
}
|
|
63
101
|
return resolved;
|
|
64
102
|
}
|
|
65
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Bundled templates/default-claude.env (shipped with npm package).
|
|
105
|
+
*/
|
|
106
|
+
function resolveDefaultEnvTemplatePath() {
|
|
107
|
+
const candidates = [
|
|
108
|
+
path_1.default.resolve(__dirname, '..', '..', 'templates', DEFAULT_ENV_TEMPLATE),
|
|
109
|
+
path_1.default.resolve(__dirname, '..', '..', '..', 'templates', DEFAULT_ENV_TEMPLATE),
|
|
110
|
+
];
|
|
111
|
+
for (const candidate of candidates) {
|
|
112
|
+
if (fs_extra_1.default.existsSync(candidate)) {
|
|
113
|
+
return candidate;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
throw new Error(`未找到内置 env 模板(预期之一:\n ${candidates.join('\n ')})`);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Env writes are allowed ONLY on ~/.claude/.env — never on project workdir.
|
|
120
|
+
*/
|
|
121
|
+
function assertUserEnvWriteTarget(userEnvPath) {
|
|
122
|
+
const expected = normalizeComparable(homePath('.claude', '.env'));
|
|
123
|
+
const actual = normalizeComparable(path_1.default.resolve(userEnvPath));
|
|
124
|
+
if (actual !== expected) {
|
|
125
|
+
throw new Error(`内部错误:env 只能写入用户目录 ${homePath('.claude', '.env')},禁止写入项目路径。`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function copyEnvToUserOnly(fromPath, userEnvPath) {
|
|
129
|
+
assertUserEnvWriteTarget(userEnvPath);
|
|
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);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Sync project env to user profile, or seed bundled default when none exists.
|
|
138
|
+
*
|
|
139
|
+
* **One-way only (read project, write user):**
|
|
140
|
+
* - May read `<workdir>/.claude/.env` as source
|
|
141
|
+
* - May write ONLY `~/.claude/.env`
|
|
142
|
+
* - NEVER creates or modifies `<workdir>/.claude/.env`
|
|
143
|
+
*
|
|
144
|
+
* Priority:
|
|
145
|
+
* 1. Project env exists + syncEnv → copy project → user (user overwritten, project untouched)
|
|
146
|
+
* 2. User env exists → keep
|
|
147
|
+
* 3. Bundled default → copy to user only
|
|
148
|
+
*/
|
|
149
|
+
async function ensureUserEnv(workdir, config, syncEnv) {
|
|
66
150
|
const projectEnv = path_1.default.join(workdir, config.envSyncFromRel);
|
|
67
|
-
|
|
151
|
+
const userEnv = config.envFile;
|
|
152
|
+
if (syncEnv && (await fs_extra_1.default.pathExists(projectEnv))) {
|
|
153
|
+
await copyEnvToUserOnly(projectEnv, userEnv);
|
|
154
|
+
logger_1.logger.info(`已读取项目 env 并同步到用户目录(未修改项目文件)→ ${userEnv}`);
|
|
68
155
|
return;
|
|
69
156
|
}
|
|
70
|
-
await fs_extra_1.default.
|
|
71
|
-
|
|
72
|
-
|
|
157
|
+
if (await fs_extra_1.default.pathExists(userEnv)) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const defaultTemplate = resolveDefaultEnvTemplatePath();
|
|
161
|
+
await copyEnvToUserOnly(defaultTemplate, userEnv);
|
|
162
|
+
logger_1.logger.info(`已写入内置默认 env 到用户目录(未写入项目)→ ${userEnv}`);
|
|
73
163
|
}
|
|
74
164
|
function warnIfNotHarnessProject(workdir) {
|
|
75
165
|
const hints = [];
|
|
@@ -93,9 +183,10 @@ function warnIfNotHarnessProject(workdir) {
|
|
|
93
183
|
}
|
|
94
184
|
}
|
|
95
185
|
function buildRunnerArgs(config, workdir, skipPermissions) {
|
|
186
|
+
const envFile = resolveUserEnvPath(config);
|
|
96
187
|
const args = [
|
|
97
|
-
|
|
98
|
-
|
|
188
|
+
formatRunnerArg('--env-file', envFile),
|
|
189
|
+
formatRunnerArg('--cwd', workdir),
|
|
99
190
|
];
|
|
100
191
|
if (skipPermissions) {
|
|
101
192
|
args.push('--dangerously-skip-permissions');
|
|
@@ -118,16 +209,16 @@ async function runStartAgent(opts) {
|
|
|
118
209
|
const config = getCodechatRunnerConfig();
|
|
119
210
|
const syncEnv = opts.syncEnv !== false;
|
|
120
211
|
const skipPermissions = opts.skipPermissions !== false;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
212
|
+
await ensureUserEnv(workdir, config, syncEnv);
|
|
213
|
+
const userEnvFile = resolveUserEnvPath(config);
|
|
124
214
|
if (!(await fs_extra_1.default.pathExists(config.runner))) {
|
|
125
215
|
throw new Error(`未找到 CodeChat CLI:${config.runner}\n请先安装 CodeChat CLI 到用户目录。`);
|
|
126
216
|
}
|
|
127
|
-
if (!(await fs_extra_1.default.pathExists(
|
|
128
|
-
throw new Error(`未找到 env 文件:${
|
|
129
|
-
|
|
217
|
+
if (!(await fs_extra_1.default.pathExists(userEnvFile))) {
|
|
218
|
+
throw new Error(`未找到 env 文件:${userEnvFile}\n` +
|
|
219
|
+
`请提供 ${path_1.default.join(workdir, config.envSyncFromRel)},或确保内置模板 templates/default-claude.env 可访问。`);
|
|
130
220
|
}
|
|
221
|
+
await verifyUserEnvWritten(userEnvFile);
|
|
131
222
|
warnIfNotHarnessProject(workdir);
|
|
132
223
|
const args = buildRunnerArgs(config, workdir, skipPermissions);
|
|
133
224
|
logger_1.logger.info(`启动 CodeChat Agent(workdir: ${workdir})`);
|
package/dist/lib/win-registry.js
CHANGED
|
@@ -49,4 +49,4 @@ function regKeyExists(key) {
|
|
|
49
49
|
}
|
|
50
50
|
exports.SHELL_MENU_KEY_BG = 'HKCU\\Software\\Classes\\Directory\\Background\\shell\\SvharnessLaunchCodeChatAgent';
|
|
51
51
|
exports.SHELL_MENU_KEY_DIR = 'HKCU\\Software\\Classes\\Directory\\shell\\SvharnessLaunchCodeChatAgent';
|
|
52
|
-
exports.SHELL_MENU_LABEL = '在此启动 CodeChat
|
|
52
|
+
exports.SHELL_MENU_LABEL = '在此启动 CodeChat CLI';
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
│ 入口层 │
|
|
37
37
|
│ launch_codechat_cli [workdir] │
|
|
38
38
|
│ svharness start-agent [agent] [workdir] [--work-dir ...] │
|
|
39
|
-
│ Explorer 右键 → stub.
|
|
39
|
+
│ Explorer 右键 → stub.ps1 → launch-codechat-cli.js │
|
|
40
40
|
└───────────────────────────┬─────────────────────────────────┘
|
|
41
41
|
│
|
|
42
42
|
┌───────────────────────────▼─────────────────────────────────┐
|
|
@@ -125,14 +125,28 @@ launch_codechat_cli ./my-app # 指定 workdir
|
|
|
125
125
|
|
|
126
126
|
## 5. env 同步策略
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
|
131
|
-
|
|
132
|
-
|
|
|
133
|
-
|
|
|
128
|
+
**铁律:单向同步,只写用户目录,永不碰项目。**
|
|
129
|
+
|
|
130
|
+
| 操作 | 允许 | 禁止 |
|
|
131
|
+
|------|------|------|
|
|
132
|
+
| 读取 `<workdir>/.claude/.env` | ✅ | — |
|
|
133
|
+
| 写入 `~/.claude/.env` | ✅ | — |
|
|
134
|
+
| 写入 `<workdir>/.claude/.env` | — | ❌ |
|
|
135
|
+
| 覆盖 / 修改项目级 `.claude/.env` | — | ❌ |
|
|
136
|
+
|
|
137
|
+
优先级(高 → 低):
|
|
138
|
+
|
|
139
|
+
| 优先级 | 条件 | 行为 |
|
|
140
|
+
|--------|------|------|
|
|
141
|
+
| 1 | 项目 `.claude/.env` 存在且未 `--no-sync-env` | **只读**项目文件,内容复制到 `~/.claude/.env`(覆盖用户级;**项目文件不变**) |
|
|
142
|
+
| 2 | `~/.claude/.env` 已存在 | 保持不变 |
|
|
143
|
+
| 3 | 以上均不满足 | 从 `templates/default-claude.env` **仅**生成 `~/.claude/.env` |
|
|
144
|
+
|
|
145
|
+
内置模板为德赛 CodeChat 通用配置(含默认 `ANTHROPIC_AUTH_TOKEN`)。项目 `.claude/.env` 仍可覆盖同步到用户目录。
|
|
134
146
|
|
|
135
|
-
|
|
147
|
+
启动时 CodeChat 使用 `--env-file=~/.claude/.env`(Windows 下带引号的绝对路径,避免 cmd 解析偶发失败)。
|
|
148
|
+
|
|
149
|
+
写入 `~/.claude/.env` 后会做 **存在 + 非空** 校验(最多 5 次、间隔 40ms),缓解 Windows 上杀毒/索引导致的短暂不可见。
|
|
136
150
|
|
|
137
151
|
---
|
|
138
152
|
|
|
@@ -156,25 +170,63 @@ macOS (`darwin`) 路径约定与 Linux 相同;若 runner 不存在则报错。
|
|
|
156
170
|
资源管理器触发的命令 **不保证** 含 `%AppData%\npm` PATH。因此 registry 指向固定路径:
|
|
157
171
|
|
|
158
172
|
```
|
|
159
|
-
%LOCALAPPDATA%\svharness\bin\launch_codechat_cli.
|
|
173
|
+
%LOCALAPPDATA%\svharness\bin\launch_codechat_cli.ps1
|
|
160
174
|
```
|
|
161
175
|
|
|
162
176
|
stub 接收 Explorer 传入的目录参数,`cd` 后调用 **绝对路径** 的 `bin/launch-codechat-cli.js`。
|
|
163
177
|
|
|
164
|
-
### 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 注册表结构
|
|
165
195
|
|
|
166
196
|
| 位置 | 键 |
|
|
167
197
|
|------|-----|
|
|
168
198
|
| 文件夹空白处 | `HKCU\Software\Classes\Directory\Background\shell\SvharnessLaunchCodeChatAgent` |
|
|
169
199
|
| 文件夹图标 | `HKCU\Software\Classes\Directory\shell\SvharnessLaunchCodeChatAgent` |
|
|
170
200
|
|
|
171
|
-
菜单标题:`在此启动 CodeChat
|
|
201
|
+
菜单标题:`在此启动 CodeChat CLI`
|
|
202
|
+
|
|
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 时):
|
|
210
|
+
|
|
211
|
+
```text
|
|
212
|
+
powershell.exe -NoExit -NoProfile -ExecutionPolicy Bypass -File "<stub.ps1>" "%V"
|
|
213
|
+
```
|
|
214
|
+
|
|
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 时):
|
|
172
222
|
|
|
173
|
-
|
|
223
|
+
```text
|
|
224
|
+
powershell.exe -NoExit -NoProfile -ExecutionPolicy Bypass -File "<stub.ps1>" "%1"
|
|
225
|
+
```
|
|
174
226
|
|
|
175
|
-
|
|
227
|
+
使用 `-NoExit` 保持 PowerShell 窗口开启以便 CodeChat 交互;`-ExecutionPolicy Bypass` 避免本地脚本执行策略拦截 stub。
|
|
176
228
|
|
|
177
|
-
|
|
229
|
+
stub 脚本须为 **UTF-8 BOM + ASCII 正文**(Windows PowerShell 5.1 对无 BOM 的 UTF-8 文件中的非 ASCII 注释会解析失败)。
|
|
178
230
|
|
|
179
231
|
---
|
|
180
232
|
|
|
@@ -239,7 +291,7 @@ svharness start-agent --work-dir . --no-skip-permissions
|
|
|
239
291
|
**Q: 右键菜单无反应**
|
|
240
292
|
|
|
241
293
|
1. `svharness shell status` 检查注册状态
|
|
242
|
-
2. 确认 stub 存在:`%LOCALAPPDATA%\svharness\bin\launch_codechat_cli.
|
|
294
|
+
2. 确认 stub 存在:`%LOCALAPPDATA%\svharness\bin\launch_codechat_cli.ps1`
|
|
243
295
|
3. 重新安装:`svharness shell install`
|
|
244
296
|
|
|
245
297
|
**Q: 未找到 run.bat / run.sh**
|
package/package.json
CHANGED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# CodeChat 接口(svharness 内置默认模板 → 仅 ~/.claude/.env)
|
|
3
|
+
# 仅写入用户目录;不会创建或修改项目 <workdir>/.claude/.env
|
|
4
|
+
# 若项目已有 .claude/.env,start-agent 只读并同步到 ~/.claude/.env
|
|
5
|
+
# ============================================================
|
|
6
|
+
ANTHROPIC_AUTH_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVpZHEzNjQwIiwiZXhwIjoxNzgyMDM2MTkxfQ.GXsNuJ2Wi92VHepau51dTWGswHft6xovvOufVjtuBYk
|
|
7
|
+
ANTHROPIC_BASE_URL=https://aiplus.desaysv.com/chatbot/anthropic
|
|
8
|
+
# ANTHROPIC_BASE_URL=http://127.0.0.1:6008/chatbot/anthropic # 德赛本地调试
|
|
9
|
+
ANTHROPIC_MODEL=deepseek
|
|
10
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL=deepseek
|
|
11
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL=deepseek
|
|
12
|
+
API_TIMEOUT_MS=3000000
|
|
13
|
+
|
|
14
|
+
# ============================================================
|
|
15
|
+
# OpenAI(通过 LiteLLM 代理)
|
|
16
|
+
# 先启动: litellm --config litellm_config.yaml --port 4000
|
|
17
|
+
# ============================================================
|
|
18
|
+
# ANTHROPIC_AUTH_TOKEN=sk-anything
|
|
19
|
+
# ANTHROPIC_BASE_URL=http://localhost:4000
|
|
20
|
+
# ANTHROPIC_MODEL=gpt-4o
|
|
21
|
+
# ANTHROPIC_DEFAULT_SONNET_MODEL=gpt-4o
|
|
22
|
+
# ANTHROPIC_DEFAULT_HAIKU_MODEL=gpt-4o
|
|
23
|
+
# ANTHROPIC_DEFAULT_OPUS_MODEL=gpt-4o
|
|
24
|
+
# API_TIMEOUT_MS=3000000
|
|
25
|
+
|
|
26
|
+
# ============================================================
|
|
27
|
+
# DeepSeek(通过 LiteLLM 代理)
|
|
28
|
+
# ============================================================
|
|
29
|
+
# ANTHROPIC_AUTH_TOKEN=sk-anything
|
|
30
|
+
# ANTHROPIC_BASE_URL=http://localhost:4000
|
|
31
|
+
# ANTHROPIC_MODEL=deepseek-chat
|
|
32
|
+
# ANTHROPIC_DEFAULT_SONNET_MODEL=deepseek-chat
|
|
33
|
+
# ANTHROPIC_DEFAULT_HAIKU_MODEL=deepseek-chat
|
|
34
|
+
# ANTHROPIC_DEFAULT_OPUS_MODEL=deepseek-chat
|
|
35
|
+
# API_TIMEOUT_MS=3000000
|
|
36
|
+
|
|
37
|
+
# ============================================================
|
|
38
|
+
# OpenRouter(直连 Anthropic 兼容接口)
|
|
39
|
+
# ============================================================
|
|
40
|
+
# ANTHROPIC_AUTH_TOKEN=sk-or-v1-xxx
|
|
41
|
+
# ANTHROPIC_BASE_URL=https://openrouter.ai/api/v1
|
|
42
|
+
# ANTHROPIC_MODEL=openai/gpt-4o
|
|
43
|
+
# ANTHROPIC_DEFAULT_SONNET_MODEL=openai/gpt-4o
|
|
44
|
+
# ANTHROPIC_DEFAULT_HAIKU_MODEL=openai/gpt-4o-mini
|
|
45
|
+
# ANTHROPIC_DEFAULT_OPUS_MODEL=openai/gpt-4o
|
|
46
|
+
|
|
47
|
+
# ============================================================
|
|
48
|
+
# 德赛调试专用
|
|
49
|
+
# ============================================================
|
|
50
|
+
CLAUDE_CODE_TRACE_ENABLED=0
|
|
51
|
+
CLAUDE_CODE_TRACE_PIPELINE=0
|
|
52
|
+
CLAUDE_CODE_TRACE_PIPELINE_FILE=
|
|
53
|
+
CLAUDE_CODE_TRACE_WIRE_PARAMS=0
|
|
54
|
+
|
|
55
|
+
# ============================================================
|
|
56
|
+
# 通用设置(建议始终开启)
|
|
57
|
+
# ============================================================
|
|
58
|
+
DISABLE_TELEMETRY=1
|
|
59
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
|
|
60
|
+
ENABLE_TOOL_SEARCH=0
|