svharness 0.14.17 → 0.14.18

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 CHANGED
@@ -733,11 +733,13 @@ svharness start-agent --work-dir ~/projects/my-app
733
733
 
734
734
  #### 平台说明
735
735
 
736
- | 平台 | spawn 模式 | 右键菜单 |
737
- |------|------------|----------|
736
+ | 平台 | spawn 模式 | 右键菜单 / PowerShell 别名 |
737
+ |------|------------|----------------------------|
738
738
  | Windows | `shell: true` | 支持(`svharness shell`) |
739
739
  | Linux / macOS | `shell: false` | 不支持 |
740
740
 
741
+ Windows 下首次 `start-agent` 会**尽力**写入 PowerShell `codechat` 函数(与 `shell install` 相同逻辑);若未全局安装 svharness,可手动执行 `svharness shell install`。
742
+
741
743
  ### 无需 svharness:独立 PowerShell 启动器
742
744
 
743
745
  未安装 npm / svharness 时,可拷贝 [`templates/codechat/Start-CodeChat.ps1`](templates/codechat/Start-CodeChat.ps1) 到项目 `.claude\`,实现相同的 env 同步与 `run.bat` 解析(含 Android Studio / IDEA 插件路径):
@@ -745,7 +747,12 @@ svharness start-agent --work-dir ~/projects/my-app
745
747
  ```powershell
746
748
  Copy-Item .\templates\codechat\Start-CodeChat.ps1 .\.claude\
747
749
  .\.claude\Start-CodeChat.ps1 # 启动
748
- .\.claude\Start-CodeChat.ps1 -Action InstallMenu # 可选:独立右键菜单
750
+ .\.claude\Start-CodeChat.ps1 -Action Install # 可选:独立右键 + 别名
751
+ .\.claude\Start-CodeChat.ps1 -Action InstallMenu # 仅右键
752
+ .\.claude\Start-CodeChat.ps1 -Action InstallAlias # 仅别名
753
+ .\.claude\Start-CodeChat.ps1 -Action UninstallMenu # 反注册独立右键
754
+ .\.claude\Start-CodeChat.ps1 -Action UninstallAlias # 反注册独立别名
755
+ .\.claude\Start-CodeChat.ps1 -Action Status
749
756
  ```
750
757
 
751
758
  右键菜单使用注册表键 `CodeChatStandaloneAgent`,**不与** `svharness shell install` 冲突;可与 svharness 菜单并存。
@@ -762,24 +769,74 @@ launch_codechat_cli D:\projects\my-app # Windows
762
769
  launch_codechat_cli ~/projects/my-app # Linux
763
770
  ```
764
771
 
765
- ### `shell` —— Windows 右键菜单(v0.16+,仅 win32)
772
+ ### `shell` —— Windows 右键菜单与 PowerShell 别名(v0.16+,仅 win32)
773
+
774
+ 在资源管理器**文件夹空白处**与**文件夹图标**右键增加「在此启动 CodeChat Agent」,并在 PowerShell `$PROFILE` 写入 `codechat` 函数(等价于 `launch_codechat_cli [workdir]`)。**注册与反注册**对右键菜单、本地 stub、PowerShell 别名**一并**处理。
766
775
 
767
- 在资源管理器**文件夹空白处**与**文件夹图标**右键增加「在此启动 CodeChat Agent」,点击后打开 **PowerShell** 窗口并启动 Agent。
776
+ #### 注册
768
777
 
769
778
  ```bash
770
- svharness shell install # 注册(幂等)
771
- svharness shell uninstall # 移除
772
- svharness shell status # 查看 stub / 注册表状态
773
- svharness shell uninstall
774
- npm uninstall -g svharness
779
+ svharness shell install # 注册右键 + 安装 codechat 别名(幂等)
780
+ svharness shell status # 查看是否已完整安装
775
781
  ```
776
782
 
777
- `npm install -g svharness` 在 Windows 上 **默认自动** `shell install`。跳过:
783
+ 注册时会写入:
784
+
785
+ | 项 | 路径 / 键 |
786
+ |----|-----------|
787
+ | 空白处右键 | `HKCU\Software\Classes\Directory\Background\shell\SvharnessLaunchCodeChatAgent` |
788
+ | 文件夹右键 | `HKCU\Software\Classes\Directory\shell\SvharnessLaunchCodeChatAgent` |
789
+ | 启动 stub | `%LOCALAPPDATA%\svharness\bin\launch_codechat_cli.ps1` |
790
+ | 菜单图标 | `%LOCALAPPDATA%\svharness\bin\codechat-agent.ico` |
791
+ | PowerShell 别名 | 当前用户 `$PROFILE` 内 `# >>> codechat >>>` … `# <<< codechat <<<` 块 |
792
+
793
+ `npm install -g svharness` 在 Windows 上 **默认自动** 执行上述注册。跳过:
778
794
 
779
795
  ```bash
780
796
  SVHARNESS_SKIP_SHELL=1 npm install -g svharness
781
797
  ```
782
798
 
799
+ **PowerShell 别名用法**(注册后需**新开** PowerShell 窗口才生效):
800
+
801
+ ```powershell
802
+ codechat # 当前目录启动
803
+ codechat D:\projects\my-app # 指定项目根
804
+ ```
805
+
806
+ #### 反注册
807
+
808
+ ```bash
809
+ svharness shell uninstall # 移除右键、stub、图标与 profile 中的 codechat 块
810
+ svharness shell status # 确认「右键菜单完整 / codechat 别名」均为否
811
+ ```
812
+
813
+ 反注册时会删除:
814
+
815
+ | 项 | 说明 |
816
+ |----|------|
817
+ | 上述两个 HKCU 注册表键 | 资源管理器右键菜单项消失(无需重启 Explorer) |
818
+ | `%LOCALAPPDATA%\svharness\bin\` | 删除 stub `.ps1` 与 `.ico` |
819
+ | `$PROFILE` 中的 codechat 块 | 移除 `# >>> codechat >>>` … `# <<< codechat <<<` 整段;**不**删除 profile 文件本身 |
820
+
821
+ 全局卸载 npm 包时,`preuninstall` 会**尽力**自动反注册(与 `shell uninstall` 相同范围):
822
+
823
+ ```bash
824
+ npm uninstall -g svharness
825
+ ```
826
+
827
+ 若自动反注册失败,可手动执行 `svharness shell uninstall`(包仍在时),或自行检查:
828
+
829
+ - 注册表:`reg query HKCU\Software\Classes\Directory\Background\shell\SvharnessLaunchCodeChatAgent` 应报错「找不到」
830
+ - 目录:`%LOCALAPPDATA%\svharness\bin\` 应不存在或为空
831
+ - profile:打开 `$PROFILE`,确认无 `# >>> codechat >>>` 标记块
832
+
833
+ **注意**:`shell uninstall` **仅**移除 svharness 注册的菜单(键名 `SvharnessLaunchCodeChatAgent`)。若曾用独立脚本 [`Start-CodeChat.ps1`](templates/codechat/Start-CodeChat.ps1) 注册过菜单(键名 `CodeChatStandaloneAgent`),需单独反注册:
834
+
835
+ ```powershell
836
+ .\.claude\Start-CodeChat.ps1 -Action UninstallMenu # 独立右键
837
+ .\.claude\Start-CodeChat.ps1 -Action UninstallAlias # 独立别名(若曾 Install / InstallAlias)
838
+ ```
839
+
783
840
  ---
784
841
 
785
842
  ## 架构模板
@@ -3,9 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveLaunchScriptPath = void 0;
6
7
  exports.resolveShellMenuIconSourcePath = resolveShellMenuIconSourcePath;
7
8
  exports.installShellMenuIcon = installShellMenuIcon;
8
- exports.resolveLaunchScriptPath = resolveLaunchScriptPath;
9
9
  exports.resolveWindowsTerminalPath = resolveWindowsTerminalPath;
10
10
  exports.runShellInstall = runShellInstall;
11
11
  exports.runShellUninstall = runShellUninstall;
@@ -18,6 +18,9 @@ const os_1 = __importDefault(require("os"));
18
18
  const child_process_1 = require("child_process");
19
19
  const logger_1 = require("../utils/logger");
20
20
  const win_registry_1 = require("../lib/win-registry");
21
+ const ps_codechat_alias_1 = require("../lib/ps-codechat-alias");
22
+ const launch_script_path_1 = require("../lib/launch-script-path");
23
+ Object.defineProperty(exports, "resolveLaunchScriptPath", { enumerable: true, get: function () { return launch_script_path_1.resolveLaunchScriptPath; } });
21
24
  const STUB_DIR = path_1.default.join(process.env.LOCALAPPDATA ?? path_1.default.join(os_1.default.homedir(), 'AppData', 'Local'), 'svharness', 'bin');
22
25
  const STUB_PATH = path_1.default.join(STUB_DIR, 'launch_codechat_cli.ps1');
23
26
  const LEGACY_STUB_PATH = path_1.default.join(STUB_DIR, 'launch_codechat_cli.cmd');
@@ -64,22 +67,6 @@ async function installShellMenuIcon() {
64
67
  await fs_extra_1.default.copy(source, ICON_STUB_PATH, { overwrite: true });
65
68
  return `${ICON_STUB_PATH},0`;
66
69
  }
67
- /**
68
- * Resolve absolute path to bin/launch-codechat-cli.js in the installed package.
69
- */
70
- function resolveLaunchScriptPath() {
71
- // dist/commands/shell-integration.js → ../../bin/launch-codechat-cli.js
72
- const fromDist = path_1.default.resolve(__dirname, '..', '..', 'bin', 'launch-codechat-cli.js');
73
- if (fs_extra_1.default.existsSync(fromDist)) {
74
- return fromDist;
75
- }
76
- // fallback: package root from scripts/postinstall.js context
77
- const fromCwd = path_1.default.resolve(__dirname, '..', '..', '..', 'bin', 'launch-codechat-cli.js');
78
- if (fs_extra_1.default.existsSync(fromCwd)) {
79
- return fromCwd;
80
- }
81
- throw new Error(`未找到 launch-codechat-cli.js(预期:${fromDist})`);
82
- }
83
70
  function canAccessExecutable(filePath) {
84
71
  try {
85
72
  fs_extra_1.default.accessSync(filePath, fs_extra_1.default.constants.F_OK);
@@ -162,7 +149,7 @@ async function runShellInstall(opts = {}) {
162
149
  // Avoid duplicate menu entries: overwrite standalone menu if present.
163
150
  (0, win_registry_1.regDeleteKey)(STANDALONE_MENU_KEY_BG);
164
151
  (0, win_registry_1.regDeleteKey)(STANDALONE_MENU_KEY_DIR);
165
- const launchScriptPath = resolveLaunchScriptPath();
152
+ const launchScriptPath = (0, launch_script_path_1.resolveLaunchScriptPath)();
166
153
  await fs_extra_1.default.ensureDir(STUB_DIR);
167
154
  await writeStubFile(launchScriptPath);
168
155
  if (await fs_extra_1.default.pathExists(LEGACY_STUB_PATH)) {
@@ -182,6 +169,7 @@ async function runShellInstall(opts = {}) {
182
169
  if (wtPath) {
183
170
  logInfo(` wt: ${wtPath}`, opts.silent);
184
171
  }
172
+ await (0, ps_codechat_alias_1.installCodechatAlias)({ silent: opts.silent });
185
173
  }
186
174
  /**
187
175
  * Remove HKCU context menus and stub file.
@@ -199,7 +187,8 @@ async function runShellUninstall(opts = {}) {
199
187
  if (await fs_extra_1.default.pathExists(LEGACY_STUB_PATH)) {
200
188
  await fs_extra_1.default.remove(LEGACY_STUB_PATH);
201
189
  }
202
- logSuccess('Windows 右键菜单已卸载', opts.silent);
190
+ await (0, ps_codechat_alias_1.uninstallCodechatAlias)({ silent: opts.silent });
191
+ logSuccess('Windows 右键菜单与 PowerShell 别名已卸载', opts.silent);
203
192
  }
204
193
  /**
205
194
  * Report shell integration installation state.
@@ -208,7 +197,7 @@ function getShellStatus() {
208
197
  assertWin32();
209
198
  let launchScriptPath = '';
210
199
  try {
211
- launchScriptPath = resolveLaunchScriptPath();
200
+ launchScriptPath = (0, launch_script_path_1.resolveLaunchScriptPath)();
212
201
  }
213
202
  catch {
214
203
  launchScriptPath = '(未找到)';
@@ -217,6 +206,7 @@ function getShellStatus() {
217
206
  const directoryMenu = (0, win_registry_1.regKeyExists)(win_registry_1.SHELL_MENU_KEY_DIR);
218
207
  const stubExists = fs_extra_1.default.existsSync(STUB_PATH);
219
208
  const wtPath = resolveWindowsTerminalPath();
209
+ const aliasStatus = (0, ps_codechat_alias_1.getCodechatAliasStatus)();
220
210
  return {
221
211
  installed: backgroundMenu && directoryMenu && stubExists,
222
212
  stubPath: STUB_PATH,
@@ -224,24 +214,28 @@ function getShellStatus() {
224
214
  directoryMenu,
225
215
  launchScriptPath,
226
216
  viaWindowsTerminal: wtPath !== null,
217
+ aliasInstalled: aliasStatus.installed,
218
+ profilePath: aliasStatus.profilePath,
227
219
  };
228
220
  }
229
221
  function printShellStatus() {
230
222
  const status = getShellStatus();
231
- logger_1.logger.section('Windows 右键菜单状态');
232
- logger_1.logger.plain(` 已完整安装: ${status.installed ? '是' : '否'}`);
233
- logger_1.logger.plain(` stub 路径: ${status.stubPath}`);
234
- logger_1.logger.plain(` 空白处菜单: ${status.backgroundMenu ? '已注册' : '未注册'}`);
235
- logger_1.logger.plain(` 文件夹菜单: ${status.directoryMenu ? '已注册' : '未注册'}`);
236
- logger_1.logger.plain(` 启动脚本: ${status.launchScriptPath}`);
237
- logger_1.logger.plain(` 经 WT 启动: ${status.viaWindowsTerminal ? '是(install 时检测到 wt.exe)' : '否'}`);
223
+ logger_1.logger.section('Windows shell 集成状态');
224
+ logger_1.logger.plain(` 右键菜单完整: ${status.installed ? '是' : '否'}`);
225
+ logger_1.logger.plain(` stub 路径: ${status.stubPath}`);
226
+ logger_1.logger.plain(` 空白处菜单: ${status.backgroundMenu ? '已注册' : '未注册'}`);
227
+ logger_1.logger.plain(` 文件夹菜单: ${status.directoryMenu ? '已注册' : '未注册'}`);
228
+ logger_1.logger.plain(` 启动脚本: ${status.launchScriptPath}`);
229
+ logger_1.logger.plain(` 经 WT 启动: ${status.viaWindowsTerminal ? '是(install 时检测到 wt.exe)' : '否'}`);
230
+ logger_1.logger.plain(` codechat 别名: ${status.aliasInstalled ? '已安装' : '未安装'}`);
231
+ logger_1.logger.plain(` profile 路径: ${status.profilePath}`);
238
232
  }
239
233
  async function runShellInstallSafe(opts = {}) {
240
234
  try {
241
235
  await runShellInstall(opts);
242
236
  }
243
237
  catch (err) {
244
- logWarn(`自动注册 Windows 右键菜单失败:${err.message}\n` +
238
+ logWarn(`自动注册 Windows shell 集成失败:${err.message}\n` +
245
239
  ` 可稍后手动执行:svharness shell install`);
246
240
  }
247
241
  }
package/dist/index.js CHANGED
@@ -492,10 +492,10 @@ function main() {
492
492
  });
493
493
  const shellCmd = program
494
494
  .command('shell')
495
- .description('Windows 资源管理器右键菜单管理(仅 win32)');
495
+ .description('Windows 右键菜单与 PowerShell codechat 别名(仅 win32)');
496
496
  shellCmd
497
497
  .command('install')
498
- .description('注册 HKCU 右键菜单「在此启动 CodeChat CLI」')
498
+ .description('注册 HKCU 右键菜单,并在 $PROFILE 安装 codechat 函数(幂等)')
499
499
  .action(async () => {
500
500
  try {
501
501
  await (0, shell_integration_1.runShellInstall)();
@@ -507,7 +507,7 @@ function main() {
507
507
  });
508
508
  shellCmd
509
509
  .command('uninstall')
510
- .description('移除右键菜单与本地 stub')
510
+ .description('移除右键菜单、本地 stub 与 PowerShell codechat 别名')
511
511
  .action(async () => {
512
512
  try {
513
513
  await (0, shell_integration_1.runShellUninstall)();
@@ -519,7 +519,7 @@ function main() {
519
519
  });
520
520
  shellCmd
521
521
  .command('status')
522
- .description('查看右键菜单安装状态')
522
+ .description('查看右键菜单与 PowerShell codechat 别名状态')
523
523
  .action(() => {
524
524
  try {
525
525
  (0, shell_integration_1.printShellStatus)();
@@ -14,6 +14,7 @@ const child_process_1 = require("child_process");
14
14
  const logger_1 = require("../utils/logger");
15
15
  const validate_args_1 = require("../utils/validate-args");
16
16
  const codechat_runner_resolver_1 = require("./codechat-runner-resolver");
17
+ const ps_codechat_alias_1 = require("./ps-codechat-alias");
17
18
  const SUPPORTED_START_AGENTS = ['codechat'];
18
19
  const DEFAULT_ENV_TEMPLATE = 'default-claude.env';
19
20
  function homePath(...segments) {
@@ -160,27 +161,6 @@ async function ensureUserEnv(workdir, config, syncEnv) {
160
161
  await copyEnvToUserOnly(defaultTemplate, userEnv);
161
162
  logger_1.logger.info(`已写入内置默认 env 到用户目录(未写入项目)→ ${userEnv}`);
162
163
  }
163
- function warnIfNotHarnessProject(workdir) {
164
- const hints = [];
165
- if (fs_extra_1.default.existsSync(path_1.default.join(workdir, 'CLAUDE.md')))
166
- hints.push('CLAUDE.md');
167
- if (fs_extra_1.default.existsSync(path_1.default.join(workdir, 'AGENTS.md')))
168
- hints.push('AGENTS.md');
169
- if (fs_extra_1.default.existsSync(path_1.default.join(workdir, '.he-harness')))
170
- hints.push('.he-harness/');
171
- try {
172
- const entries = fs_extra_1.default.readdirSync(workdir);
173
- if (entries.some((e) => e.endsWith('-harness') && fs_extra_1.default.statSync(path_1.default.join(workdir, e)).isDirectory())) {
174
- hints.push('*-harness/');
175
- }
176
- }
177
- catch {
178
- // ignore
179
- }
180
- if (hints.length === 0) {
181
- logger_1.logger.warn(`当前目录可能不是 harness 项目根(未找到 CLAUDE.md / AGENTS.md / *-harness/ / .he-harness/):\n ${workdir}`);
182
- }
183
- }
184
164
  function buildRunnerArgs(config, workdir, skipPermissions) {
185
165
  const envFile = resolveUserEnvPath(config);
186
166
  const args = [
@@ -205,6 +185,9 @@ async function runStartAgent(opts) {
205
185
  workDir: opts.workDir,
206
186
  positionalWorkdir: opts.positionalWorkdir,
207
187
  });
188
+ if (process.platform === 'win32') {
189
+ await (0, ps_codechat_alias_1.ensureCodechatAliasSafe)({ silent: true });
190
+ }
208
191
  const config = getCodechatEnvConfig();
209
192
  const syncEnv = opts.syncEnv !== false;
210
193
  const skipPermissions = opts.skipPermissions !== false;
@@ -216,7 +199,6 @@ async function runStartAgent(opts) {
216
199
  `请提供 ${path_1.default.join(workdir, config.envSyncFromRel)},或确保内置模板 templates/default-claude.env 可访问。`);
217
200
  }
218
201
  await verifyUserEnvWritten(userEnvFile);
219
- warnIfNotHarnessProject(workdir);
220
202
  const args = buildRunnerArgs(config, workdir, skipPermissions);
221
203
  logger_1.logger.info(`CodeChat CLI: ${runner} (${formatRunnerSourceLog(source, label)})`);
222
204
  logger_1.logger.info(`启动 CodeChat Agent(workdir: ${workdir})`);
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveLaunchScriptPath = resolveLaunchScriptPath;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const path_1 = __importDefault(require("path"));
9
+ /**
10
+ * Resolve absolute path to bin/launch-codechat-cli.js in the installed package.
11
+ */
12
+ function resolveLaunchScriptPath() {
13
+ // dist/lib/launch-script-path.js → ../../bin/launch-codechat-cli.js
14
+ const fromDist = path_1.default.resolve(__dirname, '..', '..', 'bin', 'launch-codechat-cli.js');
15
+ if (fs_extra_1.default.existsSync(fromDist)) {
16
+ return fromDist;
17
+ }
18
+ // fallback: package root from scripts/postinstall.js context
19
+ const fromCwd = path_1.default.resolve(__dirname, '..', '..', '..', 'bin', 'launch-codechat-cli.js');
20
+ if (fs_extra_1.default.existsSync(fromCwd)) {
21
+ return fromCwd;
22
+ }
23
+ throw new Error(`未找到 launch-codechat-cli.js(预期:${fromDist})`);
24
+ }
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getPowerShellProfilePath = getPowerShellProfilePath;
7
+ exports.installCodechatAlias = installCodechatAlias;
8
+ exports.uninstallCodechatAlias = uninstallCodechatAlias;
9
+ exports.getCodechatAliasStatus = getCodechatAliasStatus;
10
+ exports.printCodechatAliasStatus = printCodechatAliasStatus;
11
+ exports.ensureCodechatAliasSafe = ensureCodechatAliasSafe;
12
+ const fs_extra_1 = __importDefault(require("fs-extra"));
13
+ const path_1 = __importDefault(require("path"));
14
+ const child_process_1 = require("child_process");
15
+ const logger_1 = require("../utils/logger");
16
+ const launch_script_path_1 = require("./launch-script-path");
17
+ const START_MARKER = /^#\s*>>>\s*codechat\s*>>>/;
18
+ const END_MARKER = /^#\s*<<<\s*codechat\s*<<</;
19
+ function assertWin32() {
20
+ if (process.platform !== 'win32') {
21
+ throw new Error('PowerShell codechat 别名仅支持 Windows');
22
+ }
23
+ }
24
+ /**
25
+ * Resolve the current user's PowerShell $PROFILE path.
26
+ */
27
+ function getPowerShellProfilePath() {
28
+ assertWin32();
29
+ const out = (0, child_process_1.execFileSync)('powershell.exe', ['-NoProfile', '-Command', 'Write-Output $PROFILE'], { encoding: 'utf8', windowsHide: true }).trim();
30
+ if (!out) {
31
+ throw new Error('无法解析 PowerShell $PROFILE 路径');
32
+ }
33
+ return out;
34
+ }
35
+ function normalizePathForCompare(value) {
36
+ return path_1.default.resolve(value).replace(/\\/g, '/').toLowerCase();
37
+ }
38
+ function stripCodechatBlock(content) {
39
+ const lines = content.split(/\r?\n/);
40
+ const clean = [];
41
+ let hadBlock = false;
42
+ let inBlock = false;
43
+ for (const line of lines) {
44
+ if (START_MARKER.test(line)) {
45
+ hadBlock = true;
46
+ inBlock = true;
47
+ continue;
48
+ }
49
+ if (inBlock) {
50
+ if (END_MARKER.test(line)) {
51
+ inBlock = false;
52
+ }
53
+ continue;
54
+ }
55
+ clean.push(line);
56
+ }
57
+ const joined = clean.join('\n').replace(/\n+$/, '');
58
+ return { clean: joined ? `${joined}\n` : '', hadBlock };
59
+ }
60
+ function buildAliasSnippet(launchScriptPath) {
61
+ const escapedPath = launchScriptPath.replace(/'/g, "''");
62
+ return `
63
+
64
+ # >>> codechat >>>
65
+ function codechat {
66
+ param([string]$Path)
67
+ if (-not $Path) { $Path = (Get-Location).Path }
68
+ & node '${escapedPath}' $Path
69
+ }
70
+ # <<< codechat <<<
71
+
72
+ `;
73
+ }
74
+ function profileHasSvharnessAlias(content, launchScriptPath) {
75
+ if (!START_MARKER.test(content)) {
76
+ return false;
77
+ }
78
+ const needle = normalizePathForCompare(launchScriptPath);
79
+ return normalizePathForCompare(content).includes(needle);
80
+ }
81
+ /**
82
+ * Install or refresh the PowerShell `codechat` function in $PROFILE (idempotent).
83
+ * Ported from templates/codechat/Start-CodeChat.ps1 Install-CodeChatAlias.
84
+ */
85
+ async function installCodechatAlias(opts = {}) {
86
+ assertWin32();
87
+ const profilePath = getPowerShellProfilePath();
88
+ const launchScriptPath = (0, launch_script_path_1.resolveLaunchScriptPath)();
89
+ const existing = (await fs_extra_1.default.pathExists(profilePath))
90
+ ? await fs_extra_1.default.readFile(profilePath, 'utf8')
91
+ : '';
92
+ if (profileHasSvharnessAlias(existing, launchScriptPath)) {
93
+ if (!opts.silent) {
94
+ logger_1.logger.info(`PowerShell 别名 codechat 已存在:${profilePath}`);
95
+ }
96
+ return;
97
+ }
98
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(profilePath));
99
+ if (!(await fs_extra_1.default.pathExists(profilePath))) {
100
+ await fs_extra_1.default.writeFile(profilePath, '', 'utf8');
101
+ }
102
+ const { clean, hadBlock } = stripCodechatBlock(existing);
103
+ const snippet = buildAliasSnippet(launchScriptPath);
104
+ await fs_extra_1.default.writeFile(profilePath, `${clean}${snippet}`, 'utf8');
105
+ if (!opts.silent) {
106
+ if (hadBlock) {
107
+ logger_1.logger.success(`已更新 PowerShell 别名 codechat → ${launchScriptPath}`);
108
+ }
109
+ else {
110
+ logger_1.logger.success(`已安装 PowerShell 别名 codechat → ${profilePath}`);
111
+ }
112
+ logger_1.logger.info(' 新开 PowerShell 窗口后可直接运行:codechat [项目路径]');
113
+ }
114
+ }
115
+ /**
116
+ * Remove the codechat block from $PROFILE.
117
+ */
118
+ async function uninstallCodechatAlias(opts = {}) {
119
+ assertWin32();
120
+ const profilePath = getPowerShellProfilePath();
121
+ if (!(await fs_extra_1.default.pathExists(profilePath))) {
122
+ if (!opts.silent) {
123
+ logger_1.logger.info("PowerShell 别名 codechat 未安装(profile 不存在)");
124
+ }
125
+ return;
126
+ }
127
+ const existing = await fs_extra_1.default.readFile(profilePath, 'utf8');
128
+ const { clean, hadBlock } = stripCodechatBlock(existing);
129
+ if (!hadBlock) {
130
+ if (!opts.silent) {
131
+ logger_1.logger.info("PowerShell 别名 codechat 未安装");
132
+ }
133
+ return;
134
+ }
135
+ await fs_extra_1.default.writeFile(profilePath, clean, 'utf8');
136
+ if (!opts.silent) {
137
+ logger_1.logger.success(`已从 ${profilePath} 移除 PowerShell 别名 codechat`);
138
+ }
139
+ }
140
+ function getCodechatAliasStatus() {
141
+ assertWin32();
142
+ const profilePath = getPowerShellProfilePath();
143
+ let launchScriptPath = '';
144
+ try {
145
+ launchScriptPath = (0, launch_script_path_1.resolveLaunchScriptPath)();
146
+ }
147
+ catch {
148
+ launchScriptPath = '(未找到)';
149
+ }
150
+ let installed = false;
151
+ if (fs_extra_1.default.existsSync(profilePath) && launchScriptPath !== '(未找到)') {
152
+ try {
153
+ const content = fs_extra_1.default.readFileSync(profilePath, 'utf8');
154
+ installed = profileHasSvharnessAlias(content, launchScriptPath);
155
+ }
156
+ catch {
157
+ installed = false;
158
+ }
159
+ }
160
+ return { profilePath, installed, launchScriptPath };
161
+ }
162
+ function printCodechatAliasStatus() {
163
+ const status = getCodechatAliasStatus();
164
+ logger_1.logger.section('PowerShell codechat 别名状态');
165
+ logger_1.logger.plain(` 已安装: ${status.installed ? '是' : '否'}`);
166
+ logger_1.logger.plain(` profile: ${status.profilePath}`);
167
+ logger_1.logger.plain(` 启动脚本: ${status.launchScriptPath}`);
168
+ }
169
+ /**
170
+ * Best-effort alias install; never throws (warn only).
171
+ */
172
+ async function ensureCodechatAliasSafe(opts = {}) {
173
+ if (process.platform !== 'win32') {
174
+ return;
175
+ }
176
+ try {
177
+ await installCodechatAlias({ silent: opts.silent ?? true });
178
+ }
179
+ catch (err) {
180
+ logger_1.logger.warn(`自动安装 PowerShell 别名 codechat 失败:${err.message}\n` +
181
+ ` 可稍后手动执行:svharness shell install`);
182
+ }
183
+ }
@@ -23,9 +23,10 @@ cd D:\projects\my-app
23
23
  # 3. 可选:注册 Explorer 右键(独立菜单项)
24
24
  .\.claude\Start-CodeChat.ps1 -Action InstallMenu
25
25
 
26
- # 4. 状态 / 卸载右键
26
+ # 4. 状态 / 反注册
27
27
  .\.claude\Start-CodeChat.ps1 -Action Status
28
28
  .\.claude\Start-CodeChat.ps1 -Action UninstallMenu
29
+ .\.claude\Start-CodeChat.ps1 -Action UninstallAlias
29
30
  ```
30
31
 
31
32
  ## 子命令 `-Action`
@@ -33,8 +34,11 @@ cd D:\projects\my-app
33
34
  | Action | 说明 |
34
35
  |--------|------|
35
36
  | `Start`(默认) | 同步 env → 解析 `run.bat` → 启动 CodeChat |
37
+ | `Install` | 注册 Explorer 右键 **并** 安装 PowerShell `codechat` 别名 |
36
38
  | `InstallMenu` | 复制脚本到 `%LOCALAPPDATA%\codechat-standalone\` 并注册 HKCU 右键 |
37
39
  | `UninstallMenu` | 删除独立注册表键与 `%LOCALAPPDATA%\codechat-standalone\` |
40
+ | `InstallAlias` | 在 `$PROFILE` 安装 `codechat` 函数 |
41
+ | `UninstallAlias` | 从 `$PROFILE` 移除 `codechat` 函数块 |
38
42
  | `Status` | 打印 runner / env / 菜单注册状态 |
39
43
 
40
44
  ## Start 参数
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svharness",
3
- "version": "0.14.17",
3
+ "version": "0.14.18",
4
4
  "description": "CLI scaffolder for SDD-Driven Agent-Agnostic Coding Framework (harness)",
5
5
  "bin": {
6
6
  "svharness": "./bin/cli.js",
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * postinstall: global install on Windows registers Explorer context menu.
4
+ * postinstall: global install on Windows registers Explorer context menu + PowerShell codechat alias.
5
5
  * Skip: SVHARNESS_SKIP_SHELL=1 npm install -g svharness
6
6
  */
7
7
  if (process.platform !== 'win32') {
@@ -20,7 +20,7 @@ if (process.env.SVHARNESS_SKIP_SHELL === '1') {
20
20
  await runShellInstallSafe({ silent: false });
21
21
  } catch (err) {
22
22
  console.warn(
23
- '[svharness postinstall] 注册 Windows 右键菜单失败:',
23
+ '[svharness postinstall] 注册 Windows shell 集成失败:',
24
24
  err && err.message ? err.message : err,
25
25
  );
26
26
  console.warn('[svharness postinstall] 可稍后手动执行:svharness shell install');
@@ -55,3 +55,48 @@ regDeleteKey('HKCU\\Software\\Classes\\Directory\\shell\\svharness_codechat');
55
55
 
56
56
  // Stub files + icon under %LOCALAPPDATA%\svharness
57
57
  removeDirRecursive(STUB_DIR);
58
+
59
+ function stripCodechatAliasFromProfile(profilePath) {
60
+ if (!profilePath || !fs.existsSync(profilePath)) {
61
+ return;
62
+ }
63
+ try {
64
+ const content = fs.readFileSync(profilePath, 'utf8');
65
+ const lines = content.split(/\r?\n/);
66
+ const clean = [];
67
+ let inBlock = false;
68
+ let hadBlock = false;
69
+ for (const line of lines) {
70
+ if (/^#\s*>>>\s*codechat\s*>>>/.test(line)) {
71
+ hadBlock = true;
72
+ inBlock = true;
73
+ continue;
74
+ }
75
+ if (inBlock) {
76
+ if (/^#\s*<<<\s*codechat\s*<<</.test(line)) {
77
+ inBlock = false;
78
+ }
79
+ continue;
80
+ }
81
+ clean.push(line);
82
+ }
83
+ if (!hadBlock) {
84
+ return;
85
+ }
86
+ const joined = clean.join('\n').replace(/\n+$/, '');
87
+ fs.writeFileSync(profilePath, joined ? `${joined}\n` : '', 'utf8');
88
+ } catch {
89
+ // best effort
90
+ }
91
+ }
92
+
93
+ try {
94
+ const profilePath = execFileSync(
95
+ 'powershell.exe',
96
+ ['-NoProfile', '-Command', 'Write-Output $PROFILE'],
97
+ { encoding: 'utf8', windowsHide: true },
98
+ ).trim();
99
+ stripCodechatAliasFromProfile(profilePath);
100
+ } catch {
101
+ // best effort
102
+ }