helloagents 3.0.39 → 3.1.1
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +14 -10
- package/README_CN.md +14 -10
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/scripts/cli-branch.mjs +2 -5
- package/scripts/cli-codex-config.mjs +186 -0
- package/scripts/cli-codex.mjs +10 -4
- package/scripts/cli-doctor-codex.mjs +17 -8
- package/scripts/cli-doctor.mjs +48 -15
- package/scripts/cli-host-detect.mjs +38 -5
- package/scripts/cli-lifecycle-hosts.mjs +48 -20
- package/scripts/cli-messages.mjs +9 -8
- package/scripts/cli-process.mjs +16 -0
- package/scripts/cli-runtime-root.mjs +55 -12
- package/scripts/cli-toml.mjs +4 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "helloagents",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"description": "HelloAGENTS — The orchestration kernel that makes any AI CLI smarter. Adds intelligent routing, unified QA gates, safety guards, and notifications.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "HelloWind",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "helloagents",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"description": "HelloAGENTS — Quality-driven orchestration kernel for AI CLIs with intelligent routing, unified QA gates, safety guards, and notifications.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "HelloWind",
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
**A workflow layer for AI coding CLIs: skills, project knowledge, delivery checks, safer config writes, and resumable execution.**
|
|
10
10
|
|
|
11
|
-
[](./package.json)
|
|
12
12
|
[](https://www.npmjs.com/package/helloagents)
|
|
13
13
|
[](./package.json)
|
|
14
14
|
[](./skills)
|
|
@@ -225,8 +225,10 @@ The CLI manages host files explicitly:
|
|
|
225
225
|
- `update` refreshes the selected target or all targets
|
|
226
226
|
- `cleanup` removes managed injections and links
|
|
227
227
|
- `uninstall` performs scoped cleanup before package removal
|
|
228
|
-
- `doctor` reports drift in carriers, links, hooks, config entries, plugin roots, cache copies, and
|
|
228
|
+
- `doctor` reports drift in carriers, links, hooks, config entries, plugin roots, cache copies, versions, and real Claude/Gemini global install artifacts; for Codex, it also surfaces native `codex doctor` output when available
|
|
229
|
+
- Codex managed `notify = ["helloagents-js", "codex-notify"]` stays portable, and `doctor`, `cleanup`, and `uninstall` also recognize wrapped `--previous-notify` chains used by Codex App / Computer Use
|
|
229
230
|
- per-host mode tracking is written only after host setup succeeds, and failed native global cleanup keeps the host tracked as `global` instead of silently layering standby on top
|
|
231
|
+
- Windows `.cmd` / `.bat` lifecycle calls now run through an explicit command wrapper, so host installs, branch switching, and doctor flows do not emit Node `DEP0190` shell deprecation warnings
|
|
230
232
|
|
|
231
233
|
## Quick Start
|
|
232
234
|
|
|
@@ -313,7 +315,7 @@ If you omit `--standby` or `--global`, HelloAGENTS first reuses the tracked/dete
|
|
|
313
315
|
|
|
314
316
|
Use these when you do not want to depend on the `helloagents` binary being available during package updates. In `HELLOAGENTS=target[:mode]`, target can be `all`, `claude`, `gemini`, or `codex`; mode can be `standby` or `global`. For install, an omitted mode is treated as `standby`. For update, cleanup, uninstall, and branch switching, an omitted mode is forwarded unchanged so HelloAGENTS can reuse the tracked or detected mode for that CLI first. If you do not provide `HELLOAGENTS`, the one-shot install scripts now behave like plain package install: they install or update the package only and do not auto-deploy any host CLI. For a custom tarball or package spec, set `HELLOAGENTS_PACKAGE` instead of `HELLOAGENTS_BRANCH`. For a guaranteed refresh of an already installed package, prefer `npm explore -g helloagents -- npm run sync-hosts -- ...` after the package command.
|
|
315
317
|
|
|
316
|
-
Host configs use the stable `helloagents-js` entrypoint and runtime root `~/.helloagents/helloagents`, so Node global package paths can change without breaking managed hooks or Codex `notify`. Codex hooks use standalone `~/.codex/hooks.json` instead of adding large hook blocks to `config.toml`, and Codex global plugin roots plus plugin cache now link back to that same stable runtime root.
|
|
318
|
+
Host configs use the stable `helloagents-js` entrypoint and runtime root `~/.helloagents/helloagents`, so Node global package paths can change without breaking managed hooks or Codex `notify`. Codex hooks use standalone `~/.codex/hooks.json` instead of adding large hook blocks to `config.toml`, and Codex global plugin roots plus plugin cache now link back to that same stable runtime root. Claude Code global installs now use a dedicated local marketplace projection under `~/.helloagents/host-projections/claude-marketplace`, and Gemini global extension packaging uses `~/.helloagents/host-projections/gemini`, so host-specific packaging stays isolated from the shared runtime root.
|
|
317
319
|
|
|
318
320
|
#### npm commands
|
|
319
321
|
|
|
@@ -445,16 +447,16 @@ npm uninstall -g helloagents
|
|
|
445
447
|
|
|
446
448
|
| CLI | Install method | Files involved |
|
|
447
449
|
|-----|----------------|----------------|
|
|
448
|
-
| Claude Code | native plugin install |
|
|
449
|
-
| Gemini CLI | native extension install |
|
|
450
|
+
| Claude Code | native plugin install | `~/.helloagents/host-projections/claude-marketplace`, Claude Code plugin metadata/cache managed by the host |
|
|
451
|
+
| Gemini CLI | native extension install | `~/.helloagents/host-projections/gemini`, `~/.gemini/extensions/helloagents` |
|
|
450
452
|
| Codex CLI | native local-plugin chain | `~/.agents/plugins/marketplace.json`, `~/plugins/helloagents/ -> ~/.helloagents/helloagents`, `~/.codex/plugins/cache/local-plugins/helloagents/local/ -> ~/.helloagents/helloagents`, `~/.codex/config.toml`, `~/.codex/hooks.json`, `~/.codex/helloagents -> ~/.helloagents/helloagents` |
|
|
451
453
|
|
|
452
|
-
In global mode, HelloAGENTS now attempts the host-native install commands automatically.
|
|
454
|
+
In global mode, HelloAGENTS now attempts the host-native install commands automatically. Claude Code uses the local marketplace projection, Gemini uses the local extension projection, and Codex keeps linking back to the same stable runtime root, so install, update, branch switching, mode switching, cleanup, and uninstall all refresh against one consistent runtime copy. If a host command is unavailable, run the same commands manually:
|
|
453
455
|
|
|
454
456
|
```text
|
|
455
|
-
/plugin marketplace add
|
|
457
|
+
/plugin marketplace add "~/.helloagents/host-projections/claude-marketplace"
|
|
456
458
|
/plugin install helloagents@helloagents
|
|
457
|
-
|
|
459
|
+
gemini extensions link "~/.helloagents/host-projections/gemini"
|
|
458
460
|
```
|
|
459
461
|
|
|
460
462
|
For Claude Code, the CLI also tries the equivalent `claude plugin marketplace add ...` and `claude plugin install ...` commands. The marketplace is named `helloagents`, and the plugin is also named `helloagents`, so the install target is `helloagents@helloagents`. Restart the host CLI after a global install.
|
|
@@ -652,13 +654,14 @@ Codex is rules-file driven by default.
|
|
|
652
654
|
|
|
653
655
|
- standby writes `~/.codex/AGENTS.md`
|
|
654
656
|
- standby writes a portable managed `model_instructions_file = "~/.codex/AGENTS.md"`
|
|
655
|
-
- standby writes a managed `notify = ["helloagents-js", "codex-notify"]` command for closeout notification
|
|
657
|
+
- standby writes a managed and portable `notify = ["helloagents-js", "codex-notify"]` command for closeout notification, so reinstalling, updating, or moving to another machine does not require rewriting an absolute path
|
|
656
658
|
- standby writes silent Codex hooks to `~/.codex/hooks.json`
|
|
657
659
|
- Codex `SessionStart` stays silent and reads the current `~/.helloagents/helloagents.json` at runtime instead of baking a config snapshot into `config.toml`, so first-turn and post-compaction settings stay current
|
|
658
660
|
- install and update also sync HelloAGENTS-managed Codex hook trust state in `~/.codex/config.toml`, so Codex 0.129.0+ does not re-prompt for the managed hooks
|
|
659
661
|
- that hook trust state is machine-local generated metadata derived from the current absolute `~/.codex/hooks.json` path; unlike `model_instructions_file = "~/.codex/AGENTS.md"`, it is not portable config and should be regenerated on each machine
|
|
660
662
|
- standby creates `~/.codex/helloagents -> ~/.helloagents/helloagents`
|
|
661
663
|
- global mode installs the native local-plugin chain, but keeps `~/.helloagents/helloagents` as the single managed runtime source by linking plugin roots, plugin cache, and `~/.codex/helloagents` back to it
|
|
664
|
+
- `doctor`, `cleanup`, and `uninstall` also recognize wrapped notify chains such as `--previous-notify ["helloagents-js", "codex-notify"]`, so Codex App / Computer Use wrappers do not cause false drift reports or break notify restoration
|
|
662
665
|
- for Codex app/plugin discovery, `global` is the native path; `standby` remains the lighter default for explicit project work
|
|
663
666
|
- cleanup removes only the HelloAGENTS-managed hook trust entries, while keeping user-owned hook state untouched
|
|
664
667
|
- Codex hooks only synchronize runtime state and enforce Stop gates; they do not inject HelloAGENTS rules or route text through hook output
|
|
@@ -678,10 +681,11 @@ npm test
|
|
|
678
681
|
The current suite covers:
|
|
679
682
|
|
|
680
683
|
- install, update, cleanup, uninstall, branch switching, and mode switching
|
|
684
|
+
- Windows `.cmd` / `.bat` lifecycle dispatch without Node `DEP0190` warnings
|
|
681
685
|
- one-shot shell and PowerShell lifecycle dispatch, plus wrapper env cleanup and mode-routing rules for install, update, cleanup, uninstall, and branch switching
|
|
682
686
|
- Claude, Gemini, and Codex host integration behavior, including global-to-standby cleanup and failed native cleanup tracking
|
|
683
687
|
- Codex managed `model_instructions_file`, `notify`, `hooks.json`, hook trust state, local plugin, marketplace, and cache behavior
|
|
684
|
-
- Codex cleanup and canonical managed notify restoration rules
|
|
688
|
+
- Codex cleanup and canonical managed notify restoration rules, including wrapped `--previous-notify` chains
|
|
685
689
|
- Codex `/goal` feature toggles, long-running route context, and goal-aware command contracts
|
|
686
690
|
- `helloagents doctor`
|
|
687
691
|
- project storage and `repo-shared` behavior
|
package/README_CN.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
**面向 AI 编码 CLI 的工作流层:技能、知识库、交付检查、更安全的配置写入,以及可恢复的执行流程。**
|
|
10
10
|
|
|
11
|
-
[](./package.json)
|
|
12
12
|
[](https://www.npmjs.com/package/helloagents)
|
|
13
13
|
[](./package.json)
|
|
14
14
|
[](./skills)
|
|
@@ -225,8 +225,10 @@ CLI 显式管理宿主文件:
|
|
|
225
225
|
- `update` 刷新指定目标或全部目标
|
|
226
226
|
- `cleanup` 删除受管注入和链接
|
|
227
227
|
- `uninstall` 在移除包前执行对应清理
|
|
228
|
-
- `doctor` 检查规则文件、链接、hooks
|
|
228
|
+
- `doctor` 检查规则文件、链接、hooks、配置项、插件根目录、缓存副本、版本漂移,以及 Claude / Gemini 是否真的装上了全局插件或扩展;对 Codex 还会在可用时附带原生 `codex doctor` 结果
|
|
229
|
+
- Codex 受管 `notify = ["helloagents-js", "codex-notify"]` 会继续保持可移植;`doctor`、`cleanup` 和 `uninstall` 也能识别 Codex App / Computer Use 使用的 `--previous-notify` 包装链
|
|
229
230
|
- 单 CLI 模式记录只会在宿主安装成功后写入;如果原生全局清理失败,也会继续保留 `global` 记录,而不是悄悄叠加 standby
|
|
231
|
+
- Windows 下的 `.cmd` / `.bat` 生命周期调用现在统一走显式命令包装,不再出现 Node `DEP0190` shell 弃用警告
|
|
230
232
|
|
|
231
233
|
## 快速开始
|
|
232
234
|
|
|
@@ -313,7 +315,7 @@ helloagents codex goals enable
|
|
|
313
315
|
|
|
314
316
|
当你不想依赖更新过程中的 `helloagents` 可执行文件时,用 npm 或一键脚本。`HELLOAGENTS=目标[:模式]` 中,目标支持 `all`、`claude`、`gemini`、`codex`;模式支持 `standby`、`global`。用于安装时,省略模式按 `standby` 处理;用于更新、清理、卸载和切换分支时,省略模式会原样下传,让 HelloAGENTS 先复用该 CLI 已记录或检测到的模式。如果未提供 `HELLOAGENTS`,一键安装脚本现在会保持“只装包/只升级包”的默认语义,不会自动部署任何宿主 CLI。若要安装自定义 tarball 或包规格,用 `HELLOAGENTS_PACKAGE`,不要写 `HELLOAGENTS_BRANCH`。对于已经装好的包,如需确保宿主一定刷新,优先在包命令后显式执行一次 `npm explore -g helloagents -- npm run sync-hosts -- ...`。
|
|
315
317
|
|
|
316
|
-
宿主配置使用稳定的 `helloagents-js` 入口和运行根目录 `~/.helloagents/helloagents`,Node 全局包路径变化不会破坏受管 hooks 或 Codex `notify`。Codex hooks 使用独立 `~/.codex/hooks.json`,不把大段配置写入 `config.toml`;Codex 全局插件根目录和插件缓存也会回链到这个稳定运行根目录。
|
|
318
|
+
宿主配置使用稳定的 `helloagents-js` 入口和运行根目录 `~/.helloagents/helloagents`,Node 全局包路径变化不会破坏受管 hooks 或 Codex `notify`。Codex hooks 使用独立 `~/.codex/hooks.json`,不把大段配置写入 `config.toml`;Codex 全局插件根目录和插件缓存也会回链到这个稳定运行根目录。Claude Code 的 global 安装现在使用独立本地 marketplace 投影 `~/.helloagents/host-projections/claude-marketplace`,Gemini 的 global 扩展使用 `~/.helloagents/host-projections/gemini`,宿主专用打包链路不再污染共享运行根。
|
|
317
319
|
|
|
318
320
|
#### npm 命令
|
|
319
321
|
|
|
@@ -445,16 +447,16 @@ npm uninstall -g helloagents
|
|
|
445
447
|
|
|
446
448
|
| CLI | 安装方式 | 涉及文件 |
|
|
447
449
|
|-----|----------|----------|
|
|
448
|
-
| Claude Code | 原生插件安装 |
|
|
449
|
-
| Gemini CLI | 原生扩展安装 |
|
|
450
|
+
| Claude Code | 原生插件安装 | `~/.helloagents/host-projections/claude-marketplace`,以及由 Claude Code 宿主管理的插件元数据 / 缓存 |
|
|
451
|
+
| Gemini CLI | 原生扩展安装 | `~/.helloagents/host-projections/gemini`、`~/.gemini/extensions/helloagents` |
|
|
450
452
|
| Codex CLI | 原生本地插件流程 | `~/.agents/plugins/marketplace.json`、`~/plugins/helloagents/ -> ~/.helloagents/helloagents`、`~/.codex/plugins/cache/local-plugins/helloagents/local/ -> ~/.helloagents/helloagents`、`~/.codex/config.toml`、`~/.codex/hooks.json`、`~/.codex/helloagents -> ~/.helloagents/helloagents` |
|
|
451
453
|
|
|
452
|
-
全局模式下,HelloAGENTS
|
|
454
|
+
全局模式下,HelloAGENTS 会自动尝试宿主原生命令。Claude Code 走本地 marketplace 投影,Gemini 走本地 extension 投影,Codex 继续回链同一个稳定运行根,因此安装、更新、切分支、切模式、清理和卸载都会围绕同一份运行时副本刷新。若宿主命令不可用,再手动执行:
|
|
453
455
|
|
|
454
456
|
```text
|
|
455
|
-
/plugin marketplace add
|
|
457
|
+
/plugin marketplace add "~/.helloagents/host-projections/claude-marketplace"
|
|
456
458
|
/plugin install helloagents@helloagents
|
|
457
|
-
|
|
459
|
+
gemini extensions link "~/.helloagents/host-projections/gemini"
|
|
458
460
|
```
|
|
459
461
|
|
|
460
462
|
Claude Code 会自动尝试等价的 `claude plugin marketplace add ...` 和 `claude plugin install ...` 命令。marketplace 名称和插件名称都是 `helloagents`,所以安装目标是 `helloagents@helloagents`。全局安装后需要重启宿主 CLI。
|
|
@@ -656,13 +658,14 @@ Codex 默认走规则文件驱动。
|
|
|
656
658
|
|
|
657
659
|
- 标准模式写入 `~/.codex/AGENTS.md`
|
|
658
660
|
- 标准模式写入可移植的受管 `model_instructions_file = "~/.codex/AGENTS.md"`
|
|
659
|
-
-
|
|
661
|
+
- 标准模式写入受管且可移植的 `notify = ["helloagents-js", "codex-notify"]` 命令用于收尾通知,因此重装、更新或换电脑时都不需要改写绝对路径
|
|
660
662
|
- 标准模式把静默 Codex hooks 写入 `~/.codex/hooks.json`
|
|
661
663
|
- Codex 的 `SessionStart` 保持静默,并在运行时读取当前 `~/.helloagents/helloagents.json`,不会把配置快照固化进 `config.toml`,因此首次对话和上下文压缩后的设置都能保持最新
|
|
662
664
|
- 安装和更新还会把 HelloAGENTS 受管的 Codex hook trust 状态同步到 `~/.codex/config.toml`,因此 Codex 0.129.0+ 不会再对这些受管 hooks 反复提示确认
|
|
663
665
|
- 这些 hook trust 状态是基于当前机器 `~/.codex/hooks.json` 真实绝对路径生成的本机状态;它不同于 `model_instructions_file = "~/.codex/AGENTS.md"` 这类可移植配置,应在每台机器上重新生成
|
|
664
666
|
- 标准模式创建 `~/.codex/helloagents -> ~/.helloagents/helloagents`
|
|
665
667
|
- 全局模式安装原生本地插件流程,但仍把 `~/.helloagents/helloagents` 作为唯一受管运行时源;插件根目录、插件缓存和 `~/.codex/helloagents` 都会回链到它
|
|
668
|
+
- `doctor`、`cleanup` 和 `uninstall` 也能识别 `--previous-notify ["helloagents-js", "codex-notify"]` 这类包装后的 notify 链,因此 Codex App / Computer Use 不会再触发误报或破坏 notify 恢复
|
|
666
669
|
- 如果你主要看重 Codex app / 插件发现链路,优先使用 `global`;如果你主要看重更轻量、更显式的项目工作流,保留 `standby`
|
|
667
670
|
- 清理时只删除 HelloAGENTS 自己写入的 hook trust 条目,不影响用户已有的 hook 状态
|
|
668
671
|
- Codex hooks 只做静默运行态同步和 Stop 门禁,不通过 hook 注入 HelloAGENTS 规则或路由说明
|
|
@@ -682,10 +685,11 @@ npm test
|
|
|
682
685
|
当前测试覆盖:
|
|
683
686
|
|
|
684
687
|
- 安装、更新、清理、卸载、分支切换和模式切换
|
|
688
|
+
- Windows `.cmd` / `.bat` 生命周期分发链路,且不再出现 Node `DEP0190` 警告
|
|
685
689
|
- shell 与 PowerShell 一键脚本分发链路,以及包装脚本在安装、更新、清理、卸载和分支切换中的环境清理与模式传递规则
|
|
686
690
|
- Claude、Gemini、Codex 的宿主集成行为,包括全局切回标准模式的清理和原生清理失败时的模式保留
|
|
687
691
|
- Codex 受管 `model_instructions_file`、`notify`、`hooks.json`、hook trust 状态、本地插件、marketplace 和缓存行为
|
|
688
|
-
- Codex
|
|
692
|
+
- Codex 清理链路,以及包括 wrapped `--previous-notify` 在内的受管 notify 恢复规则
|
|
689
693
|
- Codex `/goal` 功能开关、长程路由上下文和 goal 感知命令契约
|
|
690
694
|
- `helloagents doctor`
|
|
691
695
|
- 项目存储和 `repo-shared`
|
package/gemini-extension.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "helloagents",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "HelloAGENTS — The orchestration kernel that makes any AI CLI smarter. Adds intelligent routing, unified QA gates, safety guards, and notifications.",
|
|
6
6
|
"author": "HelloWind",
|
package/scripts/cli-branch.mjs
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process'
|
|
2
|
-
|
|
3
1
|
import { normalizeHost } from './cli-lifecycle.mjs'
|
|
2
|
+
import { spawnCommandSync } from './cli-process.mjs'
|
|
4
3
|
|
|
5
4
|
const DEFAULT_REPO_ARCHIVE_BASE = 'https://github.com/hellowind777/helloagents/archive/refs/heads'
|
|
6
5
|
|
|
7
6
|
function runCommand(command, args) {
|
|
8
|
-
const
|
|
9
|
-
const result = spawnSync(command, args, {
|
|
7
|
+
const result = spawnCommandSync(command, args, {
|
|
10
8
|
encoding: 'utf-8',
|
|
11
9
|
errors: 'replace',
|
|
12
|
-
shell: needsShell,
|
|
13
10
|
stdio: 'inherit',
|
|
14
11
|
windowsHide: true,
|
|
15
12
|
})
|
|
@@ -24,6 +24,155 @@ function normalizePath(value = '') {
|
|
|
24
24
|
return String(value || '').replace(/\\/g, '/')
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function isManagedCodexNotifyParts(parts) {
|
|
28
|
+
return Array.isArray(parts)
|
|
29
|
+
&& parts.length === 2
|
|
30
|
+
&& parts[0] === CODEX_MANAGED_NOTIFY_COMMAND
|
|
31
|
+
&& parts[1] === 'codex-notify'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function extractTomlArrayLiteral(text = '') {
|
|
35
|
+
const source = String(text || '')
|
|
36
|
+
const equalsIndex = source.indexOf('=')
|
|
37
|
+
let quoted = false
|
|
38
|
+
let escaped = false
|
|
39
|
+
let commented = false
|
|
40
|
+
let depth = 0
|
|
41
|
+
let start = -1
|
|
42
|
+
|
|
43
|
+
for (let index = equalsIndex >= 0 ? equalsIndex + 1 : 0; index < source.length; index += 1) {
|
|
44
|
+
const char = source[index]
|
|
45
|
+
|
|
46
|
+
if (commented) {
|
|
47
|
+
if (char === '\n') commented = false
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
if (escaped) {
|
|
51
|
+
escaped = false
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
if (char === '\\' && quoted) {
|
|
55
|
+
escaped = true
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
if (char === '"') {
|
|
59
|
+
quoted = !quoted
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
if (quoted) continue
|
|
63
|
+
if (char === '#') {
|
|
64
|
+
commented = true
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
if (char === '[') {
|
|
68
|
+
if (depth === 0) start = index
|
|
69
|
+
depth += 1
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
if (char === ']' && depth > 0) {
|
|
73
|
+
depth -= 1
|
|
74
|
+
if (depth === 0 && start >= 0) return source.slice(start, index + 1)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return ''
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseTomlStringArrayLiteral(literal = '') {
|
|
82
|
+
const source = String(literal || '').trim()
|
|
83
|
+
if (!source.startsWith('[') || !source.endsWith(']')) return null
|
|
84
|
+
|
|
85
|
+
const items = []
|
|
86
|
+
let quoted = false
|
|
87
|
+
let escaped = false
|
|
88
|
+
let tokenStart = -1
|
|
89
|
+
|
|
90
|
+
for (let index = 1; index < source.length; index += 1) {
|
|
91
|
+
const char = source[index]
|
|
92
|
+
|
|
93
|
+
if (quoted) {
|
|
94
|
+
if (escaped) {
|
|
95
|
+
escaped = false
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
if (char === '\\') {
|
|
99
|
+
escaped = true
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
if (char === '"') {
|
|
103
|
+
try {
|
|
104
|
+
items.push(JSON.parse(source.slice(tokenStart, index + 1)))
|
|
105
|
+
} catch {
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
quoted = false
|
|
109
|
+
tokenStart = -1
|
|
110
|
+
}
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (char === '#') {
|
|
115
|
+
while (index < source.length && source[index] !== '\n') index += 1
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
if (/\s|,/.test(char)) continue
|
|
119
|
+
if (char === ']') return items
|
|
120
|
+
if (char !== '"') return null
|
|
121
|
+
|
|
122
|
+
quoted = true
|
|
123
|
+
tokenStart = index
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function analyzeNotifyCommandParts(parts = []) {
|
|
130
|
+
if (isManagedCodexNotifyParts(parts)) {
|
|
131
|
+
return {
|
|
132
|
+
managed: true,
|
|
133
|
+
shape: 'direct',
|
|
134
|
+
containsCodexNotify: true,
|
|
135
|
+
entrypoint: [...parts],
|
|
136
|
+
wrapper: '',
|
|
137
|
+
rawCommand: [...parts],
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let containsCodexNotify = parts.includes('codex-notify')
|
|
142
|
+
for (let index = 0; index < parts.length - 1; index += 1) {
|
|
143
|
+
if (parts[index] !== '--previous-notify') continue
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const nested = JSON.parse(parts[index + 1])
|
|
147
|
+
if (!Array.isArray(nested) || !nested.every((entry) => typeof entry === 'string')) continue
|
|
148
|
+
|
|
149
|
+
const nestedAnalysis = analyzeNotifyCommandParts(nested)
|
|
150
|
+
containsCodexNotify = containsCodexNotify || nestedAnalysis.containsCodexNotify
|
|
151
|
+
if (!nestedAnalysis.managed) continue
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
managed: true,
|
|
155
|
+
shape: 'chained',
|
|
156
|
+
containsCodexNotify: true,
|
|
157
|
+
entrypoint: [...nestedAnalysis.entrypoint],
|
|
158
|
+
wrapper: parts[0] || '',
|
|
159
|
+
rawCommand: [...parts],
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
managed: false,
|
|
168
|
+
shape: parts.length ? 'external' : 'invalid',
|
|
169
|
+
containsCodexNotify,
|
|
170
|
+
entrypoint: [],
|
|
171
|
+
wrapper: '',
|
|
172
|
+
rawCommand: [...parts],
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
27
176
|
function splitTomlLines(text = '') {
|
|
28
177
|
return String(text || '').replace(/\r\n/g, '\n').split('\n')
|
|
29
178
|
}
|
|
@@ -158,6 +307,43 @@ export function isManagedCodexNotify(line = '') {
|
|
|
158
307
|
&& value.includes(CODEX_MANAGED_NOTIFY_VALUE)
|
|
159
308
|
}
|
|
160
309
|
|
|
310
|
+
export function analyzeCodexNotifyBlock(block = '') {
|
|
311
|
+
const source = String(block || '').trim()
|
|
312
|
+
if (!source) {
|
|
313
|
+
return {
|
|
314
|
+
exists: false,
|
|
315
|
+
managed: false,
|
|
316
|
+
containsCodexNotify: false,
|
|
317
|
+
shape: 'missing',
|
|
318
|
+
entrypoint: [],
|
|
319
|
+
wrapper: '',
|
|
320
|
+
rawCommand: [],
|
|
321
|
+
rawBlock: '',
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const literal = extractTomlArrayLiteral(source)
|
|
326
|
+
const parts = literal ? parseTomlStringArrayLiteral(literal) : null
|
|
327
|
+
if (!parts) {
|
|
328
|
+
return {
|
|
329
|
+
exists: true,
|
|
330
|
+
managed: false,
|
|
331
|
+
containsCodexNotify: source.includes('codex-notify'),
|
|
332
|
+
shape: 'invalid',
|
|
333
|
+
entrypoint: [],
|
|
334
|
+
wrapper: '',
|
|
335
|
+
rawCommand: [],
|
|
336
|
+
rawBlock: source,
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
exists: true,
|
|
342
|
+
...analyzeNotifyCommandParts(parts),
|
|
343
|
+
rawBlock: source,
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
161
347
|
export function isManagedCodexTuiNotifications(line = '') {
|
|
162
348
|
return String(line || '').includes(CODEX_MANAGED_TUI_NOTIFICATIONS_VALUE)
|
|
163
349
|
&& String(line || '').includes(CODEX_MANAGED_TOML_COMMENT)
|
package/scripts/cli-codex.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from './cli-utils.mjs';
|
|
9
9
|
import { ensureTimestampedBackup, readCodexBackup, removeCodexBackup } from './cli-codex-backup.mjs';
|
|
10
10
|
import {
|
|
11
|
+
analyzeCodexNotifyBlock,
|
|
11
12
|
CODEX_MANAGED_TOML_COMMENT,
|
|
12
13
|
CODEX_MANAGED_MODEL_INSTRUCTIONS_PATH,
|
|
13
14
|
CODEX_PLUGIN_CONFIG_HEADER,
|
|
@@ -30,8 +31,10 @@ import {
|
|
|
30
31
|
syncManagedCodexHookTrust,
|
|
31
32
|
} from './cli-codex-hooks-state.mjs';
|
|
32
33
|
import {
|
|
34
|
+
hasTopLevelTomlBlock,
|
|
33
35
|
readTopLevelTomlLine,
|
|
34
36
|
readTopLevelTomlBlock,
|
|
37
|
+
removeTopLevelTomlBlock,
|
|
35
38
|
removeTopLevelTomlLines,
|
|
36
39
|
} from './cli-toml.mjs';
|
|
37
40
|
import { buildRuntimeCarrier, readCarrierSettings } from './cli-runtime-carrier.mjs';
|
|
@@ -153,9 +156,10 @@ function cleanupCodexManagedConfig(configPath, { removePluginConfig = false } =
|
|
|
153
156
|
const currentModelInstructions = readTopLevelTomlLine(toml, 'model_instructions_file');
|
|
154
157
|
const currentNotify = readTopLevelTomlBlock(toml, 'notify');
|
|
155
158
|
const currentCodexGoalsFeature = readCodexGoalsFeatureLine(toml);
|
|
159
|
+
const currentNotifyAnalysis = analyzeCodexNotifyBlock(currentNotify);
|
|
156
160
|
|
|
157
161
|
const shouldRestoreModelInstructions = isManagedCodexModelInstruction(currentModelInstructions);
|
|
158
|
-
const shouldRestoreNotify = isManagedCodexNotify(currentNotify);
|
|
162
|
+
const shouldRestoreNotify = currentNotifyAnalysis.managed || isManagedCodexNotify(currentNotify);
|
|
159
163
|
const shouldRestoreCodexGoalsFeature = isManagedCodexGoalsFeature(currentCodexGoalsFeature);
|
|
160
164
|
|
|
161
165
|
if (removePluginConfig) {
|
|
@@ -170,8 +174,10 @@ function cleanupCodexManagedConfig(configPath, { removePluginConfig = false } =
|
|
|
170
174
|
line.startsWith('model_instructions_file =') && isManagedCodexModelInstruction(line)).text;
|
|
171
175
|
}
|
|
172
176
|
if (shouldRestoreNotify) {
|
|
173
|
-
toml =
|
|
174
|
-
|
|
177
|
+
toml = hasTopLevelTomlBlock(toml, 'notify')
|
|
178
|
+
? removeTopLevelTomlBlock(toml, 'notify')
|
|
179
|
+
: removeTopLevelTomlLines(toml, (line) =>
|
|
180
|
+
line.startsWith('notify =')).text;
|
|
175
181
|
}
|
|
176
182
|
|
|
177
183
|
const backupModelInstructions = readTopLevelTomlLine(backupToml, 'model_instructions_file');
|
|
@@ -182,7 +188,7 @@ function cleanupCodexManagedConfig(configPath, { removePluginConfig = false } =
|
|
|
182
188
|
modelInstructionsLine: shouldRestoreModelInstructions && !isManagedCodexBackupInstruction(backupModelInstructions)
|
|
183
189
|
? backupModelInstructions
|
|
184
190
|
: '',
|
|
185
|
-
notifyLine: shouldRestoreNotify && !
|
|
191
|
+
notifyLine: shouldRestoreNotify && !analyzeCodexNotifyBlock(backupNotify).managed
|
|
186
192
|
? backupNotify
|
|
187
193
|
: '',
|
|
188
194
|
});
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process'
|
|
2
1
|
import { existsSync, realpathSync } from 'node:fs'
|
|
3
2
|
import { platform } from 'node:os'
|
|
4
3
|
import { join } from 'node:path'
|
|
5
4
|
|
|
6
5
|
import { CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_CONFIG_HEADER, CODEX_PLUGIN_NAME } from './cli-codex.mjs'
|
|
7
6
|
import {
|
|
7
|
+
analyzeCodexNotifyBlock,
|
|
8
8
|
CODEX_MANAGED_MODEL_INSTRUCTIONS_PATH,
|
|
9
|
-
CODEX_MANAGED_NOTIFY_VALUE,
|
|
10
9
|
readCodexGoalsFeatureLine,
|
|
11
10
|
readCodexHooksFeatureLine,
|
|
12
11
|
} from './cli-codex-config.mjs'
|
|
@@ -16,7 +15,8 @@ import {
|
|
|
16
15
|
} from './cli-codex-hooks-state.mjs'
|
|
17
16
|
import { getStableRuntimeRoot } from './cli-runtime-root.mjs'
|
|
18
17
|
import { buildRuntimeCarrier } from './cli-runtime-carrier.mjs'
|
|
19
|
-
import { readTopLevelTomlLine } from './cli-toml.mjs'
|
|
18
|
+
import { readTopLevelTomlBlock, readTopLevelTomlLine } from './cli-toml.mjs'
|
|
19
|
+
import { spawnCommandSync } from './cli-process.mjs'
|
|
20
20
|
import { loadHooksWithCliEntry, safeJson, safeRead } from './cli-utils.mjs'
|
|
21
21
|
|
|
22
22
|
function safeRealTarget(linkPath) {
|
|
@@ -149,7 +149,7 @@ function summarizeNativeCodexDoctorOutput(payload = {}) {
|
|
|
149
149
|
function inspectNativeCodexDoctor(runtime) {
|
|
150
150
|
const command = platform() === 'win32' ? 'codex.cmd' : 'codex'
|
|
151
151
|
try {
|
|
152
|
-
const result =
|
|
152
|
+
const result = spawnCommandSync(command, ['doctor', '--json'], {
|
|
153
153
|
cwd: process.cwd(),
|
|
154
154
|
env: {
|
|
155
155
|
...process.env,
|
|
@@ -159,7 +159,6 @@ function inspectNativeCodexDoctor(runtime) {
|
|
|
159
159
|
},
|
|
160
160
|
encoding: 'utf-8',
|
|
161
161
|
timeout: 20_000,
|
|
162
|
-
shell: platform() === 'win32',
|
|
163
162
|
windowsHide: true,
|
|
164
163
|
})
|
|
165
164
|
|
|
@@ -259,6 +258,8 @@ function appendCodexGlobalIssues(runtime, issues, checks, pluginVersion, cacheVe
|
|
|
259
258
|
if (!checks.pluginCache) issues.push(buildDoctorIssue(runtime, 'global-plugin-cache-missing', 'global 插件缓存目录缺失', 'Global plugin cache directory is missing'))
|
|
260
259
|
if (checks.pluginRoot && !checks.pluginRootLink) issues.push(buildDoctorIssue(runtime, 'global-plugin-root-link-drift', 'global 插件根目录未链接到稳定运行根目录', 'Global plugin root does not link to the stable runtime root'))
|
|
261
260
|
if (checks.pluginCache && !checks.pluginCacheLink) issues.push(buildDoctorIssue(runtime, 'global-plugin-cache-link-drift', 'global 插件缓存未链接到稳定运行根目录', 'Global plugin cache does not link to the stable runtime root'))
|
|
261
|
+
if (checks.pluginGenericHooks) issues.push(buildDoctorIssue(runtime, 'global-plugin-generic-hooks-present', 'global 插件根目录中意外存在通用 `hooks/hooks.json`,可能污染 Codex 本地插件 hook 加载', 'Global plugin root unexpectedly contains a generic `hooks/hooks.json`, which can pollute Codex local-plugin hook loading'))
|
|
262
|
+
if (checks.pluginCacheGenericHooks) issues.push(buildDoctorIssue(runtime, 'global-plugin-cache-generic-hooks-present', 'global 插件缓存中意外存在通用 `hooks/hooks.json`,可能污染 Codex 本地插件 hook 加载', 'Global plugin cache unexpectedly contains a generic `hooks/hooks.json`, which can pollute Codex local-plugin hook loading'))
|
|
262
263
|
if (checks.pluginRoot && !checks.pluginCarrierMatch) issues.push(buildDoctorIssue(runtime, 'global-plugin-carrier-drift', 'global 插件根目录中的 AGENTS.md 与当前全局模式规则不一致', 'Global plugin AGENTS.md differs from the current global rules'))
|
|
263
264
|
if (checks.pluginCache && !checks.pluginCacheCarrierMatch) issues.push(buildDoctorIssue(runtime, 'global-plugin-cache-carrier-drift', 'global 插件缓存中的 AGENTS.md 与当前全局模式规则不一致', 'Global plugin cache AGENTS.md differs from the current global rules'))
|
|
264
265
|
if (!checks.marketplaceEntry) issues.push(buildDoctorIssue(runtime, 'global-marketplace-missing', 'global marketplace 条目缺失', 'Global marketplace entry is missing'))
|
|
@@ -285,12 +286,16 @@ function buildCodexChecks(runtime, settings, trackedMode, detectedMode) {
|
|
|
285
286
|
const homeLinkTarget = safeRealTarget(join(codexDir, 'helloagents'))
|
|
286
287
|
const pluginRootTarget = safeRealTarget(pluginRoot)
|
|
287
288
|
const pluginCacheTarget = safeRealTarget(pluginCacheRoot)
|
|
289
|
+
const pluginGenericHooks = !!safeRead(join(pluginRoot, 'hooks', 'hooks.json'))
|
|
290
|
+
const pluginCacheGenericHooks = !!safeRead(join(pluginCacheRoot, 'hooks', 'hooks.json'))
|
|
288
291
|
const expectedHomeCarrier = (detectedMode === 'global' || (detectedMode === 'none' && trackedMode === 'global'))
|
|
289
292
|
? 'bootstrap.md'
|
|
290
293
|
: 'bootstrap-lite.md'
|
|
291
294
|
const codexHooks = safeJson(join(codexDir, 'hooks.json')) || {}
|
|
292
295
|
const marketplace = safeJson(join(runtime.home, '.agents', 'plugins', 'marketplace.json')) || {}
|
|
293
296
|
const modelInstructionsLine = readTopLevelTomlLine(codexConfig, 'model_instructions_file')
|
|
297
|
+
const notifyBlock = readTopLevelTomlBlock(codexConfig, 'notify')
|
|
298
|
+
const notifyAnalysis = analyzeCodexNotifyBlock(notifyBlock)
|
|
294
299
|
const expectedHooks = readExpectedHooks(runtime, 'hooks-codex.json', '${PLUGIN_ROOT}')
|
|
295
300
|
const expectedHookTrust = buildManagedCodexHookTrustEntries(join(codexDir, 'hooks.json'), codexHooks)
|
|
296
301
|
const managedHookTrust = new Map(
|
|
@@ -314,8 +319,9 @@ function buildCodexChecks(runtime, settings, trackedMode, detectedMode) {
|
|
|
314
319
|
globalHomeLink: homeLinkTarget === runtimeRoot,
|
|
315
320
|
modelInstructionsFile: !!modelInstructionsLine,
|
|
316
321
|
modelInstructionsPathMatch: !!modelInstructionsLine && normalizePath(modelInstructionsLine).includes(`"${CODEX_MANAGED_MODEL_INSTRUCTIONS_PATH}"`),
|
|
317
|
-
codexNotify:
|
|
318
|
-
notifyPathMatch:
|
|
322
|
+
codexNotify: notifyAnalysis.containsCodexNotify,
|
|
323
|
+
notifyPathMatch: notifyAnalysis.managed,
|
|
324
|
+
notifyShape: notifyAnalysis.shape,
|
|
319
325
|
codexHooksFeature: !/^\s*hooks\s*=\s*false\b/.test(hooksFeatureLine),
|
|
320
326
|
codexGoalsFeature: /^\s*goals\s*=\s*true\b/.test(goalsFeatureLine),
|
|
321
327
|
standaloneHooks: JSON.stringify(codexHooks.hooks || {}).includes('helloagents'),
|
|
@@ -326,11 +332,13 @@ function buildCodexChecks(runtime, settings, trackedMode, detectedMode) {
|
|
|
326
332
|
pluginCache: existsSync(pluginCacheRoot),
|
|
327
333
|
pluginRootLink: pluginRootTarget === runtimeRoot,
|
|
328
334
|
pluginCacheLink: pluginCacheTarget === runtimeRoot,
|
|
335
|
+
pluginGenericHooks,
|
|
336
|
+
pluginCacheGenericHooks,
|
|
329
337
|
pluginCarrierMatch: normalizeText(safeRead(join(pluginRoot, 'AGENTS.md')) || '') === readExpectedCarrierContent(runtime, 'bootstrap.md', settings, { profile: 'full' }),
|
|
330
338
|
pluginCacheCarrierMatch: normalizeText(safeRead(join(pluginCacheRoot, 'AGENTS.md')) || '') === readExpectedCarrierContent(runtime, 'bootstrap.md', settings, { profile: 'full' }),
|
|
331
339
|
marketplaceEntry: Array.isArray(marketplace.plugins) && marketplace.plugins.some((plugin) => plugin?.name === CODEX_PLUGIN_NAME),
|
|
332
340
|
pluginEnabled: codexConfig.includes(CODEX_PLUGIN_CONFIG_HEADER) && codexConfig.includes('enabled = true'),
|
|
333
|
-
globalNotifyPathMatch:
|
|
341
|
+
globalNotifyPathMatch: notifyAnalysis.managed,
|
|
334
342
|
pluginVersionMatch: false,
|
|
335
343
|
pluginCacheVersionMatch: false,
|
|
336
344
|
},
|
|
@@ -364,6 +372,7 @@ export function inspectCodexDoctor(runtime, settings) {
|
|
|
364
372
|
if (!checks.pluginVersionMatch && !pluginVersion && detectedMode === 'global') notes.push(runtime.msg('未读到 global 插件根目录版本信息', 'Global plugin root version was not readable'))
|
|
365
373
|
if (!checks.pluginCacheVersionMatch && !cacheVersion && detectedMode === 'global') notes.push(runtime.msg('未读到 global 插件缓存版本信息', 'Global plugin cache version was not readable'))
|
|
366
374
|
if (detectedMode !== 'none' && !checks.codexGoalsFeature) notes.push(runtime.msg('Codex /goal 未启用;如需长程执行,可运行 `helloagents codex goals enable`。', 'Codex /goal is not enabled; run `helloagents codex goals enable` if you need long-running goals.'))
|
|
375
|
+
if (checks.notifyShape === 'chained') notes.push(runtime.msg('HelloAGENTS notify 当前通过 Codex Computer Use / wrapper 链式转发,仍视为有效。', 'HelloAGENTS notify is currently chained through Codex Computer Use / a wrapper and is still treated as valid.'))
|
|
367
376
|
if (!nativeDoctor.available) notes.push(runtime.msg('未检测到原生 `codex doctor`;当前仅检查 HelloAGENTS 受管覆盖层。', 'Native `codex doctor` was not available; only the HelloAGENTS managed overlay was checked.'))
|
|
368
377
|
|
|
369
378
|
const status = summarizeDoctorStatus(issues, { trackedMode, detectedMode })
|
package/scripts/cli-doctor.mjs
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import { realpathSync } from 'node:fs'
|
|
1
|
+
import { existsSync, realpathSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
|
|
4
4
|
import { DEFAULTS } from './cli-config.mjs'
|
|
5
5
|
import { inspectCodexDoctor as inspectCodexDoctorImpl } from './cli-doctor-codex.mjs'
|
|
6
6
|
import { printDoctorText } from './cli-doctor-render.mjs'
|
|
7
7
|
import { buildRuntimeCarrier } from './cli-runtime-carrier.mjs'
|
|
8
|
+
import { getClaudeMarketplaceRoot, getGeminiExtensionRoot } from './cli-runtime-root.mjs'
|
|
8
9
|
import { loadHooksWithCliEntry, safeJson, safeRead } from './cli-utils.mjs'
|
|
9
10
|
|
|
11
|
+
const CLAUDE_PLUGIN = 'helloagents@helloagents'
|
|
12
|
+
const GEMINI_EXTENSION = 'helloagents'
|
|
13
|
+
|
|
10
14
|
const runtime = {
|
|
11
15
|
home: '',
|
|
12
16
|
pkgRoot: '',
|
|
@@ -93,6 +97,16 @@ function normalizeDoctorMode(mode = '') {
|
|
|
93
97
|
return mode || 'none'
|
|
94
98
|
}
|
|
95
99
|
|
|
100
|
+
function hasEnabledPlugin(enabledPlugins, pluginName) {
|
|
101
|
+
if (Array.isArray(enabledPlugins)) {
|
|
102
|
+
return enabledPlugins.includes(pluginName)
|
|
103
|
+
}
|
|
104
|
+
if (enabledPlugins && typeof enabledPlugins === 'object') {
|
|
105
|
+
return Boolean(enabledPlugins[pluginName])
|
|
106
|
+
}
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
|
|
96
110
|
function summarizeDoctorStatus(issues, { host, trackedMode, detectedMode } = {}) {
|
|
97
111
|
if (issues.length > 0) return 'drift'
|
|
98
112
|
if (detectedMode !== 'none') return 'ok'
|
|
@@ -106,8 +120,8 @@ function suggestDoctorFix(host, status, trackedMode) {
|
|
|
106
120
|
return `helloagents update ${host}${trackedMode && trackedMode !== 'none' ? ` --${trackedMode}` : ''}`
|
|
107
121
|
}
|
|
108
122
|
if (status === 'manual-plugin') {
|
|
109
|
-
if (host === 'claude') return
|
|
110
|
-
if (host === 'gemini') return
|
|
123
|
+
if (host === 'claude') return `/plugin marketplace add "${getClaudeMarketplaceRoot(runtime.home)}"; /plugin install helloagents@helloagents`
|
|
124
|
+
if (host === 'gemini') return `gemini extensions link "${getGeminiExtensionRoot(runtime.home)}"`
|
|
111
125
|
}
|
|
112
126
|
if (status === 'not-installed') {
|
|
113
127
|
return `helloagents install ${host} --standby`
|
|
@@ -125,12 +139,18 @@ function inspectClaudeDoctor(settings) {
|
|
|
125
139
|
const detectedMode = normalizeDoctorMode(runtime.detectHostMode(host))
|
|
126
140
|
const claudeDir = join(runtime.home, '.claude')
|
|
127
141
|
const claudeSettings = safeJson(join(claudeDir, 'settings.json')) || {}
|
|
142
|
+
const claudePlugins = safeJson(join(claudeDir, 'plugins', 'installed_plugins.json')) || {}
|
|
128
143
|
const expectedHooks = readExpectedHooks('hooks-claude.json', '${CLAUDE_PLUGIN_ROOT}')
|
|
144
|
+
const marketplaceRoot = getClaudeMarketplaceRoot(runtime.home)
|
|
145
|
+
const globalPluginInstalled = Boolean(claudePlugins.plugins?.[CLAUDE_PLUGIN]?.length)
|
|
146
|
+
|| hasEnabledPlugin(claudeSettings.enabledPlugins, CLAUDE_PLUGIN)
|
|
129
147
|
const checks = {
|
|
130
148
|
carrierMarker: (safeRead(join(claudeDir, 'CLAUDE.md')) || '').includes('HELLOAGENTS_START'),
|
|
131
149
|
carrierContentMatch: extractManagedCarrierContent(join(claudeDir, 'CLAUDE.md'))
|
|
132
150
|
=== readExpectedCarrierContent('bootstrap-lite.md', settings),
|
|
133
151
|
homeLink: safeRealTarget(join(claudeDir, 'helloagents')) === runtime.pkgRoot,
|
|
152
|
+
globalMarketplaceRoot: existsSync(marketplaceRoot),
|
|
153
|
+
globalPluginInstalled,
|
|
134
154
|
settingsHooks: JSON.stringify(claudeSettings.hooks || {}).includes('helloagents'),
|
|
135
155
|
settingsHooksMatch: managedHooksMatch(claudeSettings.hooks || {}, expectedHooks),
|
|
136
156
|
settingsPermission: Array.isArray(claudeSettings.permissions?.allow)
|
|
@@ -150,14 +170,17 @@ function inspectClaudeDoctor(settings) {
|
|
|
150
170
|
if (checks.settingsHooks && !checks.settingsHooksMatch) issues.push(buildDoctorIssue('standby-hooks-drift', 'standby settings hooks 与当前 hooks 配置不一致', 'Standby settings hooks differ from the current hook configuration'))
|
|
151
171
|
if (!checks.settingsPermission) issues.push(buildDoctorIssue('standby-permission-missing', 'standby Claude 权限注入缺失', 'Standby Claude permission injection is missing'))
|
|
152
172
|
}
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
'Claude Code global mode is managed by the host plugin system; doctor only checks for standby residue and does not inspect plugin state directly.',
|
|
157
|
-
))
|
|
173
|
+
if (detectedMode === 'global') {
|
|
174
|
+
if (!checks.globalMarketplaceRoot) issues.push(buildDoctorIssue('global-marketplace-root-missing', 'global marketplace 投影缺失', 'Global marketplace projection is missing'))
|
|
175
|
+
if (!checks.globalPluginInstalled) issues.push(buildDoctorIssue('global-plugin-missing', 'global Claude 插件未安装', 'Global Claude plugin is not installed'))
|
|
158
176
|
if (checks.carrierMarker || checks.homeLink || checks.settingsHooks || checks.settingsPermission) {
|
|
159
|
-
issues.push(buildDoctorIssue('global-standby-residue', 'global 模式下仍残留 standby 注入/链接', 'Standby injections or links still remain while the host is
|
|
177
|
+
issues.push(buildDoctorIssue('global-standby-residue', 'global 模式下仍残留 standby 注入/链接', 'Standby injections or links still remain while the host is detected as global'))
|
|
160
178
|
}
|
|
179
|
+
} else if (trackedMode === 'global') {
|
|
180
|
+
notes.push(runtime.msg(
|
|
181
|
+
'Claude Code 的 global 模式由宿主插件系统管理;doctor 会检查本地 marketplace 投影、已安装插件记录与 standby 残留。',
|
|
182
|
+
'Claude Code global mode is managed by the host plugin system; doctor checks the local marketplace projection, installed-plugin records, and standby residue.',
|
|
183
|
+
))
|
|
161
184
|
}
|
|
162
185
|
if (trackedMode === 'none' && detectedMode !== 'none') {
|
|
163
186
|
issues.push(buildDoctorIssue('untracked-managed-state', '检测到受管状态,但配置中未记录该 CLI 模式', 'Managed state detected but this CLI mode is not tracked in config'))
|
|
@@ -177,11 +200,17 @@ function inspectGeminiDoctor(settings) {
|
|
|
177
200
|
const geminiDir = join(runtime.home, '.gemini')
|
|
178
201
|
const geminiSettings = safeJson(join(geminiDir, 'settings.json')) || {}
|
|
179
202
|
const expectedHooks = readExpectedHooks('hooks-gemini.json', '${extensionPath}')
|
|
203
|
+
const extensionRoot = getGeminiExtensionRoot(runtime.home)
|
|
204
|
+
const extensionInstallRoot = join(geminiDir, 'extensions', GEMINI_EXTENSION)
|
|
205
|
+
const expectedExtensionTarget = safeRealTarget(extensionRoot) || normalizePath(extensionRoot)
|
|
180
206
|
const checks = {
|
|
181
207
|
carrierMarker: (safeRead(join(geminiDir, 'GEMINI.md')) || '').includes('HELLOAGENTS_START'),
|
|
182
208
|
carrierContentMatch: extractManagedCarrierContent(join(geminiDir, 'GEMINI.md'))
|
|
183
209
|
=== readExpectedCarrierContent('bootstrap-lite.md', settings),
|
|
184
210
|
homeLink: safeRealTarget(join(geminiDir, 'helloagents')) === runtime.pkgRoot,
|
|
211
|
+
globalExtensionRoot: existsSync(extensionRoot),
|
|
212
|
+
globalExtensionLink: safeRealTarget(extensionInstallRoot) === expectedExtensionTarget,
|
|
213
|
+
globalExtensionInstall: existsSync(extensionInstallRoot),
|
|
185
214
|
settingsHooks: JSON.stringify(geminiSettings.hooks || {}).includes('helloagents'),
|
|
186
215
|
settingsHooksMatch: managedHooksMatch(geminiSettings.hooks || {}, expectedHooks),
|
|
187
216
|
}
|
|
@@ -198,14 +227,18 @@ function inspectGeminiDoctor(settings) {
|
|
|
198
227
|
if (!checks.settingsHooks) issues.push(buildDoctorIssue('standby-hooks-missing', 'standby settings hooks 缺失', 'Standby settings hooks are missing'))
|
|
199
228
|
if (checks.settingsHooks && !checks.settingsHooksMatch) issues.push(buildDoctorIssue('standby-hooks-drift', 'standby settings hooks 与当前 hooks 配置不一致', 'Standby settings hooks differ from the current hook configuration'))
|
|
200
229
|
}
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
))
|
|
230
|
+
if (detectedMode === 'global') {
|
|
231
|
+
if (!checks.globalExtensionRoot) issues.push(buildDoctorIssue('global-extension-root-missing', 'global extension 投影缺失', 'Global extension projection is missing'))
|
|
232
|
+
if (!checks.globalExtensionInstall) issues.push(buildDoctorIssue('global-extension-missing', 'global Gemini 扩展未安装', 'Global Gemini extension is not installed'))
|
|
233
|
+
if (!checks.globalExtensionLink) issues.push(buildDoctorIssue('global-extension-link-missing', 'global Gemini 扩展链接未指向投影目录', 'Global Gemini extension link does not point to the projection root'))
|
|
206
234
|
if (checks.carrierMarker || checks.homeLink || checks.settingsHooks) {
|
|
207
|
-
issues.push(buildDoctorIssue('global-standby-residue', 'global 模式下仍残留 standby 注入/链接', 'Standby injections or links still remain while the host is
|
|
235
|
+
issues.push(buildDoctorIssue('global-standby-residue', 'global 模式下仍残留 standby 注入/链接', 'Standby injections or links still remain while the host is detected as global'))
|
|
208
236
|
}
|
|
237
|
+
} else if (trackedMode === 'global') {
|
|
238
|
+
notes.push(runtime.msg(
|
|
239
|
+
'Gemini CLI 的 global 模式由宿主扩展系统管理;doctor 会检查本地扩展投影、已安装链接与 standby 残留。',
|
|
240
|
+
'Gemini CLI global mode is managed by the host extension system; doctor checks the local extension projection, installed link, and standby residue.',
|
|
241
|
+
))
|
|
209
242
|
}
|
|
210
243
|
if (trackedMode === 'none' && detectedMode !== 'none') {
|
|
211
244
|
issues.push(buildDoctorIssue('untracked-managed-state', '检测到受管状态,但配置中未记录该 CLI 模式', 'Managed state detected but this CLI mode is not tracked in config'))
|
|
@@ -6,9 +6,12 @@ import {
|
|
|
6
6
|
CODEX_PLUGIN_KEY,
|
|
7
7
|
CODEX_PLUGIN_NAME,
|
|
8
8
|
} from './cli-codex.mjs'
|
|
9
|
-
import { getStableRuntimeRoot } from './cli-runtime-root.mjs'
|
|
9
|
+
import { getGeminiExtensionRoot, getStableRuntimeRoot } from './cli-runtime-root.mjs'
|
|
10
10
|
import { safeJson, safeRead } from './cli-utils.mjs'
|
|
11
11
|
|
|
12
|
+
const CLAUDE_PLUGIN = 'helloagents@helloagents'
|
|
13
|
+
const GEMINI_EXTENSION = 'helloagents'
|
|
14
|
+
|
|
12
15
|
const HOST_ALIASES = new Map([
|
|
13
16
|
['all', 'all'],
|
|
14
17
|
['*', 'all'],
|
|
@@ -24,8 +27,14 @@ function hasHelloagentsMarker(filePath) {
|
|
|
24
27
|
return (safeRead(filePath) || '').includes('HELLOAGENTS_START')
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
function hasHelloagentsSettings(filePath) {
|
|
28
|
-
|
|
30
|
+
function hasHelloagentsSettings(filePath, host = '') {
|
|
31
|
+
const settings = safeJson(filePath) || {}
|
|
32
|
+
const hooksText = JSON.stringify(settings.hooks || {})
|
|
33
|
+
if (hooksText.includes('helloagents')) return true
|
|
34
|
+
if (host === 'claude') {
|
|
35
|
+
return JSON.stringify(settings.permissions?.allow || []).includes('~/.helloagents/helloagents')
|
|
36
|
+
}
|
|
37
|
+
return false
|
|
29
38
|
}
|
|
30
39
|
|
|
31
40
|
function normalizePath(value = '') {
|
|
@@ -40,12 +49,27 @@ function safeRealTarget(linkPath) {
|
|
|
40
49
|
}
|
|
41
50
|
}
|
|
42
51
|
|
|
52
|
+
function hasEnabledPlugin(enabledPlugins, pluginName) {
|
|
53
|
+
if (Array.isArray(enabledPlugins)) {
|
|
54
|
+
return enabledPlugins.includes(pluginName)
|
|
55
|
+
}
|
|
56
|
+
if (enabledPlugins && typeof enabledPlugins === 'object') {
|
|
57
|
+
return Boolean(enabledPlugins[pluginName])
|
|
58
|
+
}
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
|
|
43
62
|
function detectClaudeMode(home) {
|
|
44
63
|
const claudeDir = join(home, '.claude')
|
|
64
|
+
const settings = safeJson(join(claudeDir, 'settings.json')) || {}
|
|
65
|
+
const installedPlugins = safeJson(join(claudeDir, 'plugins', 'installed_plugins.json')) || {}
|
|
66
|
+
if (hasEnabledPlugin(settings.enabledPlugins, CLAUDE_PLUGIN) || installedPlugins.plugins?.[CLAUDE_PLUGIN]?.length) {
|
|
67
|
+
return 'global'
|
|
68
|
+
}
|
|
45
69
|
if (
|
|
46
70
|
existsSync(join(claudeDir, 'helloagents'))
|
|
47
71
|
|| hasHelloagentsMarker(join(claudeDir, 'CLAUDE.md'))
|
|
48
|
-
|| hasHelloagentsSettings(join(claudeDir, 'settings.json'))
|
|
72
|
+
|| hasHelloagentsSettings(join(claudeDir, 'settings.json'), 'claude')
|
|
49
73
|
) {
|
|
50
74
|
return 'standby'
|
|
51
75
|
}
|
|
@@ -54,10 +78,19 @@ function detectClaudeMode(home) {
|
|
|
54
78
|
|
|
55
79
|
function detectGeminiMode(home) {
|
|
56
80
|
const geminiDir = join(home, '.gemini')
|
|
81
|
+
const extensionRoot = safeRealTarget(getGeminiExtensionRoot(home)) || normalizePath(getGeminiExtensionRoot(home))
|
|
82
|
+
const installedExtensionRoot = join(geminiDir, 'extensions', GEMINI_EXTENSION)
|
|
83
|
+
const installedExtension = safeJson(join(installedExtensionRoot, 'gemini-extension.json')) || {}
|
|
84
|
+
if (
|
|
85
|
+
existsSync(installedExtensionRoot)
|
|
86
|
+
&& (safeRealTarget(installedExtensionRoot) === extensionRoot || installedExtension.name === GEMINI_EXTENSION)
|
|
87
|
+
) {
|
|
88
|
+
return 'global'
|
|
89
|
+
}
|
|
57
90
|
if (
|
|
58
91
|
existsSync(join(geminiDir, 'helloagents'))
|
|
59
92
|
|| hasHelloagentsMarker(join(geminiDir, 'GEMINI.md'))
|
|
60
|
-
|| hasHelloagentsSettings(join(geminiDir, 'settings.json'))
|
|
93
|
+
|| hasHelloagentsSettings(join(geminiDir, 'settings.json'), 'gemini')
|
|
61
94
|
) {
|
|
62
95
|
return 'standby'
|
|
63
96
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process'
|
|
2
1
|
import { platform } from 'node:os'
|
|
3
2
|
|
|
4
3
|
import {
|
|
@@ -14,11 +13,22 @@ import {
|
|
|
14
13
|
uninstallCodexGlobal,
|
|
15
14
|
uninstallCodexStandby,
|
|
16
15
|
} from './cli-codex.mjs'
|
|
17
|
-
import {
|
|
16
|
+
import { spawnCommandSync } from './cli-process.mjs'
|
|
17
|
+
import {
|
|
18
|
+
detectHostMode as detectRuntimeHostMode,
|
|
19
|
+
getHostLabel,
|
|
20
|
+
} from './cli-host-detect.mjs'
|
|
21
|
+
import {
|
|
22
|
+
getClaudeMarketplaceRoot,
|
|
23
|
+
getGeminiExtensionRoot,
|
|
24
|
+
removeClaudeMarketplaceRoot,
|
|
25
|
+
removeGeminiExtensionRoot,
|
|
26
|
+
syncClaudeMarketplaceRoot,
|
|
27
|
+
syncGeminiExtensionRoot,
|
|
28
|
+
} from './cli-runtime-root.mjs'
|
|
18
29
|
|
|
19
30
|
const CLAUDE_COMMAND = process.env.HELLOAGENTS_CLAUDE_CMD || 'claude'
|
|
20
31
|
const GEMINI_COMMAND = process.env.HELLOAGENTS_GEMINI_CMD || 'gemini'
|
|
21
|
-
const CLAUDE_MARKETPLACE = 'https://github.com/hellowind777/helloagents.git'
|
|
22
32
|
const CLAUDE_PLUGIN = 'helloagents@helloagents'
|
|
23
33
|
|
|
24
34
|
function normalizeCommand(command = '') {
|
|
@@ -44,11 +54,9 @@ function runHostCommand(command, args) {
|
|
|
44
54
|
let lastResult = null
|
|
45
55
|
|
|
46
56
|
for (const candidate of attempts) {
|
|
47
|
-
const
|
|
48
|
-
const result = spawnSync(candidate, args, {
|
|
57
|
+
const result = spawnCommandSync(candidate, args, {
|
|
49
58
|
encoding: 'utf-8',
|
|
50
59
|
errors: 'replace',
|
|
51
|
-
shell: needsShell,
|
|
52
60
|
windowsHide: true,
|
|
53
61
|
})
|
|
54
62
|
lastResult = result
|
|
@@ -81,8 +89,8 @@ function preserveTrackedModeOnFailure(result = {}, trackedMode = '') {
|
|
|
81
89
|
return result
|
|
82
90
|
}
|
|
83
91
|
|
|
84
|
-
function installClaudeGlobalPlugin() {
|
|
85
|
-
const add = runHostCommand(CLAUDE_COMMAND, ['plugin', 'marketplace', 'add',
|
|
92
|
+
function installClaudeGlobalPlugin(marketplaceRoot) {
|
|
93
|
+
const add = runHostCommand(CLAUDE_COMMAND, ['plugin', 'marketplace', 'add', marketplaceRoot])
|
|
86
94
|
if (!add.ok && add.missing) return { ok: false, output: '未找到 claude 命令' }
|
|
87
95
|
const install = runHostCommand(CLAUDE_COMMAND, ['plugin', 'install', CLAUDE_PLUGIN, '--scope', 'user'])
|
|
88
96
|
return { ok: install.ok, output: install.output || add.output }
|
|
@@ -158,12 +166,14 @@ function installHostStandby(runtime, host, { previousMode = '' } = {}) {
|
|
|
158
166
|
const cleanupResult = prepareClaudeStandby(previousMode)
|
|
159
167
|
if (cleanupResult.ok === false) return cleanupResult
|
|
160
168
|
installClaudeStandby(runtime.home, runtime.pkgRoot)
|
|
169
|
+
if (detectRuntimeHostMode('claude', runtime) !== 'global') removeClaudeMarketplaceRoot(runtime.home)
|
|
161
170
|
return cleanupResult
|
|
162
171
|
}
|
|
163
172
|
if (host === 'gemini') {
|
|
164
173
|
const cleanupResult = prepareGeminiStandby(previousMode)
|
|
165
174
|
if (cleanupResult.ok === false) return cleanupResult
|
|
166
175
|
installGeminiStandby(runtime.home, runtime.pkgRoot)
|
|
176
|
+
if (detectRuntimeHostMode('gemini', runtime) !== 'global') removeGeminiExtensionRoot(runtime.home)
|
|
167
177
|
return cleanupResult
|
|
168
178
|
}
|
|
169
179
|
if (!installCodexStandby(runtime.home, runtime.pkgRoot)) return { skipped: true }
|
|
@@ -174,31 +184,45 @@ function installHostStandby(runtime, host, { previousMode = '' } = {}) {
|
|
|
174
184
|
function installHostGlobal(runtime, host) {
|
|
175
185
|
if (host === 'claude') {
|
|
176
186
|
uninstallClaudeStandby(runtime.home)
|
|
177
|
-
|
|
178
|
-
|
|
187
|
+
const marketplaceRoot = getClaudeMarketplaceRoot(runtime.home)
|
|
188
|
+
syncClaudeMarketplaceRoot(runtime.pkgRoot, marketplaceRoot)
|
|
189
|
+
const result = buildNativeResult(
|
|
190
|
+
installClaudeGlobalPlugin(marketplaceRoot),
|
|
179
191
|
'已自动安装 Claude Code 插件;重启 Claude Code 后生效',
|
|
180
192
|
'Claude Code plugin installed automatically; restart Claude Code to apply',
|
|
181
|
-
|
|
182
|
-
|
|
193
|
+
`Claude Code 插件自动安装失败,请在 Claude Code 中执行: /plugin marketplace add "${marketplaceRoot}";/plugin install helloagents@helloagents`,
|
|
194
|
+
`Claude Code plugin auto-install failed. Run inside Claude Code: /plugin marketplace add "${marketplaceRoot}"; /plugin install helloagents@helloagents`,
|
|
183
195
|
)
|
|
196
|
+
return result
|
|
184
197
|
}
|
|
185
198
|
if (host === 'gemini') {
|
|
186
199
|
uninstallGeminiStandby(runtime.home)
|
|
187
|
-
|
|
188
|
-
|
|
200
|
+
const extensionRoot = getGeminiExtensionRoot(runtime.home)
|
|
201
|
+
syncGeminiExtensionRoot(runtime.pkgRoot, extensionRoot)
|
|
202
|
+
const result = buildNativeResult(
|
|
203
|
+
installGeminiGlobalExtension(extensionRoot),
|
|
189
204
|
'已自动安装 Gemini CLI 扩展;重启 Gemini CLI 后生效',
|
|
190
205
|
'Gemini CLI extension installed automatically; restart Gemini CLI to apply',
|
|
191
|
-
`Gemini CLI 扩展自动安装失败,请手动执行: gemini extensions link ${
|
|
192
|
-
`Gemini CLI extension auto-install failed. Run manually: gemini extensions link ${
|
|
206
|
+
`Gemini CLI 扩展自动安装失败,请手动执行: gemini extensions link "${extensionRoot}"`,
|
|
207
|
+
`Gemini CLI extension auto-install failed. Run manually: gemini extensions link "${extensionRoot}"`,
|
|
193
208
|
)
|
|
209
|
+
return result
|
|
194
210
|
}
|
|
195
211
|
uninstallCodexStandby(runtime.home)
|
|
196
212
|
return installCodexGlobal(runtime.home, runtime.pkgRoot) ? {} : { skipped: true }
|
|
197
213
|
}
|
|
198
214
|
|
|
199
215
|
function cleanupHostStandby(runtime, host) {
|
|
200
|
-
if (host === 'claude')
|
|
201
|
-
|
|
216
|
+
if (host === 'claude') {
|
|
217
|
+
const skipped = !uninstallClaudeStandby(runtime.home)
|
|
218
|
+
if (detectRuntimeHostMode('claude', runtime) !== 'global') removeClaudeMarketplaceRoot(runtime.home)
|
|
219
|
+
return { skipped }
|
|
220
|
+
}
|
|
221
|
+
if (host === 'gemini') {
|
|
222
|
+
const skipped = !uninstallGeminiStandby(runtime.home)
|
|
223
|
+
if (detectRuntimeHostMode('gemini', runtime) !== 'global') removeGeminiExtensionRoot(runtime.home)
|
|
224
|
+
return { skipped }
|
|
225
|
+
}
|
|
202
226
|
const standbyCleaned = uninstallCodexStandby(runtime.home)
|
|
203
227
|
const globalResidueCleaned = uninstallCodexGlobal(runtime.home)
|
|
204
228
|
return { skipped: !(standbyCleaned || globalResidueCleaned) }
|
|
@@ -207,7 +231,7 @@ function cleanupHostStandby(runtime, host) {
|
|
|
207
231
|
function cleanupHostGlobal(runtime, host) {
|
|
208
232
|
if (host === 'claude') {
|
|
209
233
|
uninstallClaudeStandby(runtime.home)
|
|
210
|
-
|
|
234
|
+
const result = preserveTrackedModeOnFailure(
|
|
211
235
|
buildNativeResult(
|
|
212
236
|
removeClaudeGlobalPlugin(),
|
|
213
237
|
'已自动移除 Claude Code 插件',
|
|
@@ -217,10 +241,12 @@ function cleanupHostGlobal(runtime, host) {
|
|
|
217
241
|
),
|
|
218
242
|
'global',
|
|
219
243
|
)
|
|
244
|
+
if (result.ok) removeClaudeMarketplaceRoot(runtime.home)
|
|
245
|
+
return result
|
|
220
246
|
}
|
|
221
247
|
if (host === 'gemini') {
|
|
222
248
|
uninstallGeminiStandby(runtime.home)
|
|
223
|
-
|
|
249
|
+
const result = preserveTrackedModeOnFailure(
|
|
224
250
|
buildNativeResult(
|
|
225
251
|
removeGeminiGlobalExtension(),
|
|
226
252
|
'已自动移除 Gemini CLI 扩展',
|
|
@@ -230,6 +256,8 @@ function cleanupHostGlobal(runtime, host) {
|
|
|
230
256
|
),
|
|
231
257
|
'global',
|
|
232
258
|
)
|
|
259
|
+
if (result.ok) removeGeminiExtensionRoot(runtime.home)
|
|
260
|
+
return result
|
|
233
261
|
}
|
|
234
262
|
return { skipped: !uninstallCodexGlobal(runtime.home) }
|
|
235
263
|
}
|
package/scripts/cli-messages.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
+
import { getClaudeMarketplaceRoot, getGeminiExtensionRoot } from './cli-runtime-root.mjs'
|
|
3
4
|
|
|
4
5
|
export function createMessageHelpers(isCN) {
|
|
5
6
|
const msg = (cn, en) => (isCN ? cn : en)
|
|
@@ -19,11 +20,11 @@ function codexGlobalStatus({ home, msg }) {
|
|
|
19
20
|
: msg('安装 Codex CLI 后重新运行 npm install -g helloagents', 'Install Codex CLI then re-run npm install -g helloagents')
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
function pluginCommands() {
|
|
23
|
+
function pluginCommands(home) {
|
|
23
24
|
return [
|
|
24
|
-
|
|
25
|
+
` Claude Code: /plugin marketplace add "${getClaudeMarketplaceRoot(home)}"`,
|
|
25
26
|
' /plugin install helloagents@helloagents',
|
|
26
|
-
|
|
27
|
+
` Gemini CLI: gemini extensions link "${getGeminiExtensionRoot(home)}"`,
|
|
27
28
|
].join('\n')
|
|
28
29
|
}
|
|
29
30
|
|
|
@@ -42,24 +43,24 @@ function restartHint(msg) {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
function renderInstallMessage(context, mode, state) {
|
|
45
|
-
const { msg } = context
|
|
46
|
+
const { home, msg } = context
|
|
46
47
|
const install = state === 'install'
|
|
47
48
|
const refresh = state === 'refresh'
|
|
48
49
|
|
|
49
50
|
if (mode === 'global') {
|
|
50
51
|
if (install) {
|
|
51
52
|
return msg(
|
|
52
|
-
`\n ✅ HelloAGENTS 已安装(global 模式)!\n\n Claude Code / Gemini CLI: 已自动尝试宿主原生插件/扩展安装\n Codex: ${codexGlobalStatus(context)}(~/.agents/plugins/marketplace.json + ~/plugins/helloagents)\n\n ${restartHint(msg)}\n\n 若宿主命令不可用,请手动执行:\n${pluginCommands()}\n\n 切换模式:\n helloagents --standby 标准模式(默认,非插件安装)`,
|
|
53
|
-
`\n ✅ HelloAGENTS installed (global mode)!\n\n Claude Code / Gemini CLI: native plugin/extension install attempted automatically\n Codex: ${codexGlobalStatus(context)} (~/.agents/plugins/marketplace.json + ~/plugins/helloagents)\n\n ${restartHint(msg)}\n\n If a host command is unavailable, run manually:\n${pluginCommands()}\n\n Switch modes:\n helloagents --standby Standby mode (default, non-plugin install)`,
|
|
53
|
+
`\n ✅ HelloAGENTS 已安装(global 模式)!\n\n Claude Code / Gemini CLI: 已自动尝试宿主原生插件/扩展安装\n Codex: ${codexGlobalStatus(context)}(~/.agents/plugins/marketplace.json + ~/plugins/helloagents)\n\n ${restartHint(msg)}\n\n 若宿主命令不可用,请手动执行:\n${pluginCommands(home)}\n\n 切换模式:\n helloagents --standby 标准模式(默认,非插件安装)`,
|
|
54
|
+
`\n ✅ HelloAGENTS installed (global mode)!\n\n Claude Code / Gemini CLI: native plugin/extension install attempted automatically\n Codex: ${codexGlobalStatus(context)} (~/.agents/plugins/marketplace.json + ~/plugins/helloagents)\n\n ${restartHint(msg)}\n\n If a host command is unavailable, run manually:\n${pluginCommands(home)}\n\n Switch modes:\n helloagents --standby Standby mode (default, non-plugin install)`,
|
|
54
55
|
)
|
|
55
56
|
}
|
|
56
57
|
return msg(
|
|
57
58
|
refresh
|
|
58
59
|
? ` global 模式已刷新。\n Claude Code / Gemini 已自动尝试刷新宿主插件/扩展;Codex 原生本地插件已重装并同步最新文件。\n ${restartHint(msg)}`
|
|
59
|
-
: ` 所有项目将自动启用完整 HelloAGENTS 规则。\n Claude Code / Gemini 已自动尝试安装宿主插件/扩展;Codex 已自动安装原生本地插件。\n ${restartHint(msg)}\n\n若宿主命令不可用,请手动执行:\n${pluginCommands()}`,
|
|
60
|
+
: ` 所有项目将自动启用完整 HelloAGENTS 规则。\n Claude Code / Gemini 已自动尝试安装宿主插件/扩展;Codex 已自动安装原生本地插件。\n ${restartHint(msg)}\n\n若宿主命令不可用,请手动执行:\n${pluginCommands(home)}`,
|
|
60
61
|
refresh
|
|
61
62
|
? ` Global mode refreshed.\n Claude Code / Gemini native plugin/extension refresh was attempted automatically; Codex native local-plugin files were reinstalled and synced.\n ${restartHint(msg)}`
|
|
62
|
-
: ` All projects will use full HelloAGENTS rules.\n Claude Code / Gemini native plugin/extension install was attempted automatically; Codex now uses the native local-plugin path automatically.\n ${restartHint(msg)}\n\nIf a host command is unavailable, run manually:\n${pluginCommands()}`,
|
|
63
|
+
: ` All projects will use full HelloAGENTS rules.\n Claude Code / Gemini native plugin/extension install was attempted automatically; Codex now uses the native local-plugin path automatically.\n ${restartHint(msg)}\n\nIf a host command is unavailable, run manually:\n${pluginCommands(home)}`,
|
|
63
64
|
)
|
|
64
65
|
}
|
|
65
66
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Run a command on all platforms, including Windows .cmd/.bat files, without
|
|
5
|
+
* relying on child_process shell=true argument concatenation.
|
|
6
|
+
*/
|
|
7
|
+
export function spawnCommandSync(command, args = [], options = {}) {
|
|
8
|
+
const normalizedArgs = Array.isArray(args) ? args.map((arg) => String(arg)) : []
|
|
9
|
+
const isWindowsShellScript = process.platform === 'win32' && /\.(cmd|bat)$/i.test(String(command || ''))
|
|
10
|
+
if (!isWindowsShellScript) {
|
|
11
|
+
return spawnSync(command, normalizedArgs, options)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const comspec = process.env.ComSpec || 'cmd.exe'
|
|
15
|
+
return spawnSync(comspec, ['/d', '/s', '/c', String(command || ''), ...normalizedArgs], options)
|
|
16
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { copyFileSync, existsSync, mkdtempSync, realpathSync, renameSync } from 'node:fs'
|
|
2
2
|
import { dirname, join, resolve } from 'node:path'
|
|
3
3
|
|
|
4
|
-
import { copyEntries, ensureDir, removeIfExists } from './cli-utils.mjs'
|
|
4
|
+
import { copyEntries, createLink, ensureDir, removeIfExists } from './cli-utils.mjs'
|
|
5
5
|
|
|
6
6
|
export const RUNTIME_ROOT_ENTRIES = [
|
|
7
7
|
'.claude-plugin',
|
|
@@ -28,6 +28,16 @@ export function getStableRuntimeRoot(home) {
|
|
|
28
28
|
return join(home, '.helloagents', 'helloagents')
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/** Return the Claude local marketplace projection root derived from the shared runtime copy. */
|
|
32
|
+
export function getClaudeMarketplaceRoot(home) {
|
|
33
|
+
return join(home, '.helloagents', 'host-projections', 'claude-marketplace')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Return the Gemini extension projection root derived from the shared runtime copy. */
|
|
37
|
+
export function getGeminiExtensionRoot(home) {
|
|
38
|
+
return join(home, '.helloagents', 'host-projections', 'gemini')
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
function normalizePath(path) {
|
|
32
42
|
const resolved = resolve(path)
|
|
33
43
|
try {
|
|
@@ -63,17 +73,9 @@ function retryTransientFs(operation) {
|
|
|
63
73
|
throw lastError
|
|
64
74
|
}
|
|
65
75
|
|
|
66
|
-
function materializeGeminiHooks
|
|
67
|
-
const source = join(root, 'hooks', 'hooks-gemini.json')
|
|
68
|
-
const target = join(root, 'hooks', 'hooks.json')
|
|
69
|
-
if (!existsSync(source)) return
|
|
70
|
-
copyFileSync(source, target)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Sync package runtime files into the stable root without copying repo-only files. */
|
|
74
|
-
export function syncRuntimeRoot(sourceRoot, runtimeRoot) {
|
|
76
|
+
function syncRuntimeTree(sourceRoot, targetRoot, { materializeGeminiHooks = false } = {}) {
|
|
75
77
|
const source = resolve(sourceRoot)
|
|
76
|
-
const target = resolve(
|
|
78
|
+
const target = resolve(targetRoot)
|
|
77
79
|
if (samePath(source, target)) {
|
|
78
80
|
return { synced: false, root: target }
|
|
79
81
|
}
|
|
@@ -84,7 +86,13 @@ export function syncRuntimeRoot(sourceRoot, runtimeRoot) {
|
|
|
84
86
|
|
|
85
87
|
try {
|
|
86
88
|
copyEntries(source, staging, RUNTIME_ROOT_ENTRIES)
|
|
87
|
-
materializeGeminiHooks
|
|
89
|
+
if (materializeGeminiHooks) {
|
|
90
|
+
const sourceHooks = join(staging, 'hooks', 'hooks-gemini.json')
|
|
91
|
+
const targetHooks = join(staging, 'hooks', 'hooks.json')
|
|
92
|
+
if (existsSync(sourceHooks)) {
|
|
93
|
+
copyFileSync(sourceHooks, targetHooks)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
88
96
|
retryTransientFs(() => {
|
|
89
97
|
removeIfExists(target)
|
|
90
98
|
renameSync(staging, target)
|
|
@@ -96,7 +104,42 @@ export function syncRuntimeRoot(sourceRoot, runtimeRoot) {
|
|
|
96
104
|
}
|
|
97
105
|
}
|
|
98
106
|
|
|
107
|
+
/** Sync package runtime files into the stable root without copying repo-only files. */
|
|
108
|
+
export function syncRuntimeRoot(sourceRoot, runtimeRoot) {
|
|
109
|
+
return syncRuntimeTree(sourceRoot, runtimeRoot)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Sync a Claude local marketplace root that resolves to the stable runtime copy. */
|
|
113
|
+
export function syncClaudeMarketplaceRoot(sourceRoot, marketplaceRoot) {
|
|
114
|
+
const source = resolve(sourceRoot)
|
|
115
|
+
const target = resolve(marketplaceRoot)
|
|
116
|
+
if (samePath(source, target)) {
|
|
117
|
+
return { synced: false, root: target }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
removeIfExists(target)
|
|
121
|
+
if (createLink(source, target)) {
|
|
122
|
+
return { synced: true, root: target }
|
|
123
|
+
}
|
|
124
|
+
return syncRuntimeTree(source, target)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Sync a host-specific extension root derived from the stable runtime copy. */
|
|
128
|
+
export function syncGeminiExtensionRoot(sourceRoot, extensionRoot) {
|
|
129
|
+
return syncRuntimeTree(sourceRoot, extensionRoot, { materializeGeminiHooks: true })
|
|
130
|
+
}
|
|
131
|
+
|
|
99
132
|
/** Remove the stable runtime copy while leaving user settings under ~/.helloagents intact. */
|
|
100
133
|
export function removeRuntimeRoot(runtimeRoot) {
|
|
101
134
|
removeIfExists(runtimeRoot)
|
|
102
135
|
}
|
|
136
|
+
|
|
137
|
+
/** Remove the Claude marketplace projection root. */
|
|
138
|
+
export function removeClaudeMarketplaceRoot(home) {
|
|
139
|
+
removeIfExists(getClaudeMarketplaceRoot(home))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Remove the Gemini extension projection root. */
|
|
143
|
+
export function removeGeminiExtensionRoot(home) {
|
|
144
|
+
removeIfExists(getGeminiExtensionRoot(home))
|
|
145
|
+
}
|
package/scripts/cli-toml.mjs
CHANGED
|
@@ -117,6 +117,10 @@ export function removeTopLevelTomlBlock(text, key) {
|
|
|
117
117
|
return normalizeToml(`${normalized.slice(0, existing.start)}${normalized.slice(existing.end)}`);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
export function hasTopLevelTomlBlock(text, key) {
|
|
121
|
+
return Boolean(findTopLevelTomlBlock(text, key));
|
|
122
|
+
}
|
|
123
|
+
|
|
120
124
|
export function prependTopLevelTomlBlocks(text, blocks) {
|
|
121
125
|
const normalizedBlocks = blocks
|
|
122
126
|
.map((block) => String(block || '').trim())
|